<template>
|
<div class="app-container" v-loading="loading">
|
<div class="block-title">城市公司档案</div>
|
|
<!-- 查看态:灰底四列只读信息(原型「查看」效果) -->
|
<div v-if="!editing" class="info-panel">
|
<div v-for="item in archiveFields" :key="item.label" class="info-item">
|
<span class="info-label">{{ item.label }}:</span>
|
<span class="info-value">{{ item.value }}</span>
|
</div>
|
</div>
|
|
<!-- 新增/修改态:灰底四列表单。状态与更新时间是只读文本,原型里也是纯文本 -->
|
<div v-else class="info-panel">
|
<!-- label-width 与查看态的 .info-label 同一个值(104px),label-suffix 补上冒号
|
(el-form 默认没有),两态切换时标签与值都不挪位。
|
不能收窄:标签在 ruoyi.scss 里被全局设成粗体,前面还挂着必填星号(*.el-form-item__label:before),
|
「* 所属城市:」在粗体下约 81px,减掉右侧 12px 内边距后标签盒至少要有 81px 才不折行 -->
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="104px" label-suffix=":" class="archive-form">
|
<el-form-item label="所属城市" prop="cityId">
|
<el-select v-model="form.cityId" placeholder="请选择" filterable style="width: 100%">
|
<el-option
|
v-for="item in citySelectOptions"
|
:key="item.id"
|
:label="cityLabel(item)"
|
:value="item.id"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="公司名称" prop="companyName">
|
<el-input v-model="form.companyName" maxlength="128" placeholder="请输入" />
|
</el-form-item>
|
<el-form-item label="联系人" prop="contactName">
|
<el-input v-model="form.contactName" maxlength="64" placeholder="请输入" />
|
</el-form-item>
|
<el-form-item label="手机号码" prop="contactPhone">
|
<el-input v-model="form.contactPhone" maxlength="20" placeholder="请输入" />
|
</el-form-item>
|
<el-form-item label="状态">{{ labelOf(ENABLE_STATUS, form.status) }}</el-form-item>
|
<el-form-item label="更新时间">{{ form.updateTime || '-' }}</el-form-item>
|
</el-form>
|
<div v-if="isBoundCityDisabled" class="panel-hint">
|
本条档案所在城市已禁用,城市保持不变可正常保存
|
</div>
|
</div>
|
|
<!-- 关联用户信息:查看态是只读面板,新增/修改态是一个带放大镜的搜索框(原型「新增」效果) -->
|
<div class="block-title" :class="{ 'block-title--row': editing }">
|
<span class="block-name">关联用户信息</span>
|
<template v-if="editing">
|
<!-- 用 el-autocomplete 而不是 el-select:原型的搜索框尾部落的是放大镜而非下拉箭头,
|
而远程筛选后从结果里选一条的交互两者一致(el-select 在 2.13.1 没有 suffix 插槽) -->
|
<el-autocomplete
|
v-model="userKeyword"
|
:fetch-suggestions="fetchUser"
|
value-key="value"
|
:trigger-on-focus="false"
|
placeholder="请输入"
|
class="user-search"
|
@input="handleUserInput"
|
@select="handleUserSelect"
|
>
|
<template #suffix>
|
<el-icon><Search /></el-icon>
|
</template>
|
</el-autocomplete>
|
<span class="user-hint">只能搜索绑定L4用户</span>
|
</template>
|
</div>
|
|
<div v-if="!editing" class="info-panel">
|
<div v-for="item in memberFields" :key="item.label" class="info-item">
|
<span class="info-label">{{ item.label }}:</span>
|
<span class="info-value">{{ item.value }}</span>
|
</div>
|
</div>
|
|
<div class="form-actions">
|
<el-button v-if="!editing" type="success" @click="editing = true" v-hasPermi="['member:company:edit']">修改</el-button>
|
<el-button v-else type="success" @click="submitForm" v-hasPermi="[isEdit ? 'member:company:edit' : 'member:company:add']">提 交</el-button>
|
<!-- 查看态给「返回」(离开页面),修改态给「取消」(退回查看态,改动作废) -->
|
<el-button v-if="editing && isEdit" @click="cancelEdit">取 消</el-button>
|
<el-button v-else @click="goBack">返回</el-button>
|
</div>
|
</div>
|
</template>
|
|
<script setup name="MemberCompanyForm">
|
import { getCompany, addCompany, updateCompany, searchL4User } from '@/api/user/company'
|
import { listCityOptions } from '@/api/user/city'
|
import {
|
ENABLE_STATUS,
|
ACCOUNT_STATUS,
|
SUBJECT_TYPES,
|
AUTH_STATUS,
|
labelOf
|
} from '@/utils/member'
|
|
const route = useRoute()
|
const { proxy } = getCurrentInstance()
|
|
const loading = ref(false)
|
const formRef = ref()
|
const cityOptions = ref([])
|
|
// 新增直接进编辑态;查看先只读,点「修改」再进编辑态(与原型一致)
|
const isEdit = computed(() => !!route.params.id)
|
const editing = ref(!isEdit.value)
|
|
const form = ref({
|
id: null,
|
cityId: null,
|
companyName: '',
|
userId: null,
|
contactName: '',
|
contactPhone: '',
|
// 新增时状态就是「正常」(后端 insert 的默认值),先放在这里,表单里才不是一片空白
|
status: isEdit.value ? null : ENABLE_STATUS[0].value,
|
updateTime: null,
|
phone: null,
|
systemLevel: null,
|
memberCreateTime: null,
|
memberStatus: null,
|
memberSubjectType: null,
|
memberAuthStatus: null,
|
memberCity: null
|
})
|
|
const rules = {
|
cityId: [{ required: true, message: '所属城市不能为空', trigger: 'change' }],
|
companyName: [{ required: true, message: '公司名称不能为空', trigger: 'blur' }],
|
userId: [{ required: true, message: '关联用户不能为空', trigger: 'change' }],
|
contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
|
contactPhone: [{ required: true, message: '手机号码不能为空', trigger: 'blur' }]
|
}
|
|
// 本条档案当前所在的城市。options 接口只回启用城市,若这个城市后来被禁用,
|
// 下拉里就没有它,页面会只剩一个城市编号(也选不回原值),故单独留一份。
|
const boundCity = ref(null)
|
|
// el-autocomplete 的显示值。它只是一个字符串,选中的会员编号另存 form.userId,
|
// 两者必须一起变——见 handleUserInput 里对「改了字但没落下选择」的处理。
|
const userKeyword = ref('')
|
const boundUser = ref(null)
|
|
function cityLabel(item) {
|
return item.province ? item.province + ' / ' + item.cityName : item.cityName
|
}
|
|
function userLabel(item) {
|
// 会员可能还没提交认证资料,姓名称为空,此时用「未认证」占位
|
const name = item.memberName || '未认证'
|
return `${name} · ${item.userId} · ${item.phone || '-'}`
|
}
|
|
const citySelectOptions = computed(() => {
|
const bound = boundCity.value
|
if (bound && !cityOptions.value.some(item => item.id === bound.id)) {
|
return [bound, ...cityOptions.value]
|
}
|
return cityOptions.value
|
})
|
|
const isBoundCityDisabled = computed(() => {
|
const bound = boundCity.value
|
return !!bound && !cityOptions.value.some(item => item.id === bound.id)
|
})
|
|
const archiveFields = computed(() => [
|
// 城市只显示市名(原型如此);省只在城市下拉里用于消歧
|
{ label: '所属城市', value: boundCity.value ? boundCity.value.cityName : '-' },
|
{ label: '公司名称', value: form.value.companyName || '-' },
|
{ label: '联系人', value: form.value.contactName || '-' },
|
{ label: '手机号码', value: form.value.contactPhone || '-' },
|
// 原型此处写作「状态状态」,按常规文案取「状态」
|
{ label: '状态', value: labelOf(ENABLE_STATUS, form.value.status) },
|
{ label: '更新时间', value: form.value.updateTime || '-' }
|
])
|
|
const memberFields = computed(() => [
|
{ label: '用户ID', value: form.value.userId ?? '-' },
|
{ label: '手机号码', value: form.value.phone || '-' },
|
{ label: '注册时间', value: form.value.memberCreateTime || '-' },
|
{ label: '账户状态', value: labelOf(ACCOUNT_STATUS, form.value.memberStatus) },
|
{ label: '主体类型', value: labelOf(SUBJECT_TYPES, form.value.memberSubjectType) },
|
{ label: '认证状态', value: labelOf(AUTH_STATUS, form.value.memberAuthStatus) },
|
{ label: '系统等级', value: form.value.systemLevel || '-' },
|
{ label: '所属城市', value: form.value.memberCity || '-' }
|
])
|
|
function getCityOptions() {
|
listCityOptions().then(response => {
|
cityOptions.value = response.data || []
|
})
|
}
|
|
function getDetail(id) {
|
loading.value = true
|
getCompany(id).then(response => {
|
const detail = response.data
|
// 后端对不存在的档案返回 data: null(与认证/注销详情一致的写法)。
|
// 不拦的话会渲染出一个空的「查看」页,提交时因为 id 为空反而走成新增,故直接退回列表。
|
if (!detail) {
|
proxy.$modal.msgError('公司档案不存在或已删除')
|
loading.value = false
|
goBack()
|
return
|
}
|
form.value = {
|
id: detail.id,
|
cityId: detail.cityId,
|
companyName: detail.companyName,
|
userId: detail.userId,
|
contactName: detail.contactName,
|
contactPhone: detail.contactPhone,
|
status: detail.status,
|
updateTime: detail.updateTime,
|
phone: detail.phone,
|
systemLevel: detail.systemLevel,
|
memberCreateTime: detail.memberCreateTime,
|
memberStatus: detail.memberStatus,
|
memberSubjectType: detail.memberSubjectType,
|
memberAuthStatus: detail.memberAuthStatus,
|
memberCity: detail.memberCity
|
}
|
boundCity.value = { id: detail.cityId, province: detail.province, cityName: detail.cityName }
|
// 会员信息除了「搜索框里显示什么」都来自详情:搜索框的文字必须与已绑定会员的标签
|
// 逐字一致,否则 handleUserInput 会把它当成「用户改了字」,一进编辑态就把 userId 清掉
|
boundUser.value = detail.userId
|
? {
|
userId: detail.userId,
|
memberName: detail.memberName,
|
phone: detail.phone,
|
systemLevel: detail.systemLevel
|
}
|
: null
|
userKeyword.value = boundUser.value ? userLabel(boundUser.value) : ''
|
loading.value = false
|
}).catch(() => {
|
loading.value = false
|
})
|
}
|
|
// el-autocomplete 的候选:空关键字直接给空数组,不放开全量 L4 会员
|
function fetchUser(query, callback) {
|
const keyword = (query || '').trim()
|
if (!keyword) {
|
callback([])
|
return
|
}
|
searchL4User(keyword).then(response => {
|
callback((response.data || []).map(item => ({ ...item, value: userLabel(item) })))
|
}).catch(() => callback([]))
|
}
|
|
// 输入框内容与已选会员的标签不一致 = 用户改了字却没从结果里选,
|
// 此时必须把 userId 清掉,否则会拿一个「看着像别人」的编号提交上去。
|
// 用事件参数而不是读 userKeyword:autocomplete 先 emit input 再 emit update:modelValue,
|
// 读 ref 拿到的是上一个字。
|
function handleUserInput(value) {
|
if (!boundUser.value || value !== userLabel(boundUser.value)) {
|
form.value.userId = null
|
}
|
}
|
|
// 只在人工选定时回填联系人/手机号码(回填值可再手工改);
|
// getDetail 里的赋值不触发 select,故不会覆盖档案里已存的联系人
|
function handleUserSelect(item) {
|
form.value.userId = item.userId
|
form.value.contactName = item.memberName || ''
|
form.value.contactPhone = item.phone || ''
|
boundUser.value = item
|
// 搜索出来的会员未必有「注册时间/账户状态」等档案级展示列(列表接口不回这些),
|
// 但此时是编辑态、面板不渲染,所以不必补
|
}
|
|
function submitForm() {
|
formRef.value.validate(valid => {
|
if (!valid) {
|
return
|
}
|
// 状态不随表单提交:它由列表页的「禁用/启用」按钮单独改
|
const payload = {
|
id: form.value.id,
|
cityId: form.value.cityId,
|
companyName: form.value.companyName,
|
userId: form.value.userId,
|
contactName: form.value.contactName,
|
contactPhone: form.value.contactPhone
|
}
|
const request = payload.id ? updateCompany(payload) : addCompany(payload)
|
request.then(() => {
|
proxy.$modal.msgSuccess(payload.id ? '修改成功' : '新增成功')
|
if (payload.id) {
|
// 改完留在本页看结果:重取详情顺带把「取消」留下的改动一并清掉
|
editing.value = false
|
getDetail(payload.id)
|
} else {
|
goBack()
|
}
|
})
|
})
|
}
|
|
function cancelEdit() {
|
// 退回查看态并重取详情:不做字段快照,省掉一份「改到一半」的状态
|
editing.value = false
|
getDetail(route.params.id)
|
}
|
|
function goBack() {
|
proxy.$tab.closeOpenPage({ path: '/member/company' })
|
}
|
|
getCityOptions()
|
if (isEdit.value) {
|
getDetail(route.params.id)
|
}
|
</script>
|
|
<style scoped>
|
.block-title {
|
margin-bottom: 12px;
|
font-size: 15px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
/* 新增态:标题与搜索框、提示同一行(原型如此)。
|
这块下面直接跟按钮(没有面板挡住),故行距要自己给足 */
|
.block-title--row {
|
display: flex;
|
align-items: center;
|
margin-bottom: 32px;
|
}
|
|
/* 128px = 表单 label-width 104px + 面板左内边距 24px,
|
即把搜索框的左边缘对到表单的输入列上(原型里两者就是同一条竖线)。
|
改上面任一常量时这里要一起改。 */
|
.block-title--row .block-name {
|
flex: 0 0 auto;
|
width: 128px;
|
}
|
|
.user-search {
|
width: 220px;
|
}
|
|
.user-hint {
|
margin-left: 12px;
|
font-size: 13px;
|
color: #f56c6c;
|
}
|
|
/* 灰底面板:查看态是四列只读信息,新增态是四列表单,
|
两态共用同一块灰底、同一套列宽、同一套标签列几何(见 .info-label),
|
点「修改」时标签与值原地不动,只是从文字变成控件 */
|
.info-panel,
|
.archive-form {
|
display: grid;
|
grid-template-columns: repeat(4, 1fr);
|
column-gap: 16px;
|
row-gap: 20px;
|
}
|
|
.info-panel {
|
padding: 24px;
|
margin-bottom: 24px;
|
background-color: #fafafa;
|
border-radius: 4px;
|
}
|
|
/* 表单是面板的整行子项,自己再排四列;不铺满就会被挤进面板的第一列里 */
|
.archive-form {
|
grid-column: 1 / -1;
|
}
|
|
/* 标签盒子照抄 el-form-item__label 的几何:宽度 = 表单的 label-width、右侧 12px 内边距、
|
文字右对齐、行高 32px(等于表单控件的行高)。两态同一套几何,点「修改」时标签与值不左右跳。
|
粗体也照抄(ruoyi.scss 把表单标签全局设成 700):不加的话切换态时标签会一边粗一边细,
|
而粗体字更宽,标签起点也会跟着错开几像素。
|
宽度是硬约束,少了标签会折行(详见模板里 el-form 上方的注释);
|
以后出现更长的标签,这里和 el-form 的 label-width 要一起调 */
|
.info-label {
|
flex: 0 0 auto;
|
box-sizing: border-box;
|
width: 104px;
|
padding-right: 12px;
|
font-weight: 700;
|
color: #606266;
|
line-height: 32px;
|
text-align: right;
|
}
|
|
.info-item {
|
display: flex;
|
}
|
|
.info-value {
|
color: #303133;
|
line-height: 32px;
|
word-break: break-all;
|
}
|
|
/* 行距交给面板的 row-gap,元素自带的 margin-bottom 会把这套栅格顶歪 */
|
.archive-form :deep(.el-form-item) {
|
margin-bottom: 0;
|
}
|
|
.panel-hint {
|
grid-column: 1 / -1;
|
padding-bottom: 4px;
|
font-size: 12px;
|
color: #909399;
|
}
|
|
.form-actions {
|
display: flex;
|
gap: 10px;
|
align-items: center;
|
}
|
</style>
|