liujie
昨天 c983a0a398504697a777ffae48e59961b5ee2bd7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
<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>