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
<template>
  <div class="app-container" v-loading="loading">
    <!-- 一个 el-form 包三段面板:el-form-item 靠 provide/inject 找 el-form,
         隔在中间的 div 不影响校验,所以三段可以共用一套 rules 与一个提交入口 -->
    <el-form ref="formRef" :model="form" :rules="rules" label-width="130px" label-suffix=":" class="provider-form">
      <div class="block-title">服务商档案</div>
      <div class="info-panel">
        <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="信用代码" prop="creditCode">
          <el-input v-model="form.creditCode" maxlength="64" placeholder="请输入" />
        </el-form-item>
        <el-form-item label="企业法人" prop="legalPerson">
          <el-input v-model="form.legalPerson" maxlength="64" placeholder="请输入" />
        </el-form-item>
        <el-form-item label="注册地区" prop="region">
          <!-- value 取市名(不是城市编号):后端的 region 存的就是市名文本,与会员表的 city 同口径。
               也因此城市后来被禁用时不必做「回填原值」兜底——下拉里没有该选项,
               el-select 会把 model 里的市名字符串原样显示出来,不会变成空白或编号 -->
          <el-select v-model="form.region" placeholder="请选择" filterable style="width: 100%">
            <el-option
              v-for="item in cityOptions"
              :key="item.id"
              :label="cityLabel(item)"
              :value="item.cityName"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="注册日期" prop="registerDate">
          <el-date-picker
            v-model="form.registerDate"
            type="date"
            value-format="YYYY-MM-DD"
            placeholder="请选择"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="企业地址" prop="address">
          <el-input v-model="form.address" maxlength="255" placeholder="请输入" />
        </el-form-item>
        <el-form-item label="营业执照" prop="licenseUrl" class="license-item">
          <div class="license-row">
            <div class="license-box">
              <el-image
                v-if="form.licenseUrl"
                :src="form.licenseUrl"
                fit="contain"
                class="license-img"
                :preview-src-list="[form.licenseUrl]"
                preview-teleported
              />
              <span v-else class="license-empty">暂无营业执照</span>
            </div>
            <el-upload
              :show-file-list="false"
              :http-request="handleUpload"
              :before-upload="beforeUpload"
              accept=".png,.jpg,.jpeg"
            >
              <el-button type="primary" link :loading="uploading">上传营业执照</el-button>
            </el-upload>
            <!-- 只清表单里的 URL,不调后端的物理删除:表单允许「返回」放弃本次改动,
                 删图即删文件会让库里留下指向已删文件的旧地址(孤儿文件比死链安全) -->
            <el-button v-if="form.licenseUrl" type="danger" link icon="Delete" @click="form.licenseUrl = ''">
              删除
            </el-button>
            <span class="license-hint">支持 png / jpg / jpeg,不超过 5MB</span>
          </div>
        </el-form-item>
      </div>
 
      <div class="block-title">服务商虚拟账号权益</div>
      <div class="info-panel">
        <el-form-item label="L1 分配数量" prop="l1Quota">
          <el-input-number v-model="form.l1Quota" :min="0" controls-position="right" style="width: 140px" />
        </el-form-item>
        <el-form-item label="L2 分配数量" prop="l2Quota">
          <el-input-number v-model="form.l2Quota" :min="0" controls-position="right" style="width: 140px" />
        </el-form-item>
        <el-form-item label="L3 分配数量" prop="l3Quota">
          <el-input-number v-model="form.l3Quota" :min="0" controls-position="right" style="width: 140px" />
        </el-form-item>
        <div class="panel-hint">列表里的「已授权数」按该服务商名下的会员实时统计,此处是各等级的分配数量</div>
      </div>
 
      <div class="block-title">服务商账号设置</div>
      <div class="info-panel">
        <el-form-item label="登录账号" prop="userName">
          <el-input v-model="form.userName" maxlength="30" placeholder="请输入" />
        </el-form-item>
        <el-form-item label="登录密码" prop="password">
          <el-input
            v-model="form.password"
            type="password"
            show-password
            maxlength="20"
            :placeholder="isEdit ? '留空则不修改' : '请输入'"
          />
        </el-form-item>
        <el-form-item label="重复登录密码" prop="confirmPassword">
          <el-input
            v-model="form.confirmPassword"
            type="password"
            show-password
            maxlength="20"
            :placeholder="isEdit ? '留空则不修改' : '请输入'"
            @blur="revalidatePassword"
          />
        </el-form-item>
        <div class="panel-hint">该账号即服务商的登录账号,密码 5~20 个字符{{ isEdit ? ';留空表示本次不改密码' : '' }}</div>
      </div>
    </el-form>
 
    <div class="form-actions">
      <el-button type="success" @click="submitForm" v-hasPermi="[isEdit ? 'member:provider:edit' : 'member:provider:add']">提 交</el-button>
      <el-button @click="goBack">返回</el-button>
    </div>
  </div>
</template>
 
<script setup name="MemberProviderForm">
import { getProvider, addProvider, updateProvider, uploadLicense } from '@/api/user/provider'
import { listCityOptions } from '@/api/user/city'
 
const route = useRoute()
const { proxy } = getCurrentInstance()
 
const loading = ref(false)
const uploading = ref(false)
const formRef = ref()
const cityOptions = ref([])
 
const isEdit = computed(() => !!route.params.id)
 
const form = ref({
  id: null,
  companyName: '',
  contactName: '',
  contactPhone: '',
  creditCode: '',
  legalPerson: '',
  region: '',
  registerDate: null,
  address: '',
  licenseUrl: '',
  // 后端三个配额是 int NOT NULL DEFAULT 0,给 null 会被 el-input-number 当成空
  l1Quota: 0,
  l2Quota: 0,
  l3Quota: 0,
  userName: '',
  password: '',
  confirmPassword: ''
})
 
// 密码:新增必填、修改留空即不改;填了就必须 5~20 个字符。
// 文案与后端 AccountProviderServiceImpl.validateAccount 逐字一致,
// 免得同一件事前端提示一套、后端提示另一套。
function validatePassword(rule, value, callback) {
  if (!isEdit.value && !value) {
    callback(new Error('登录密码不能为空'))
    return
  }
  if (value && (value.length < 5 || value.length > 20)) {
    callback(new Error('登录密码长度必须在5到20个字符之间'))
    return
  }
  callback()
}
 
function validateConfirmPassword(rule, value, callback) {
  if (value && value !== form.value.password) {
    callback(new Error('两次输入的登录密码不一致'))
    return
  }
  callback()
}
 
const rules = {
  companyName: [{ required: true, message: '公司名称不能为空', trigger: 'blur' }],
  contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
  // 只判必填:与后端一致。写死 11 位手机号正则会挡住固话/带区号的号码
  contactPhone: [{ required: true, message: '手机号码不能为空', trigger: 'blur' }],
  userName: [{ required: true, message: '登录账号不能为空', trigger: 'blur' }],
  password: [{ validator: validatePassword, trigger: 'blur' }],
  confirmPassword: [{ validator: validateConfirmPassword, trigger: 'blur' }]
}
 
function cityLabel(item) {
  return item.province ? item.province + ' / ' + item.cityName : item.cityName
}
 
// 改密码后重复密码的校验结果可能已过期(el-form 只在自己失焦时重校),
// 故在重复密码上补一次主动触发
function revalidatePassword() {
  if (form.value.confirmPassword) {
    formRef.value.validateField('confirmPassword')
  }
}
 
function getCityOptions() {
  listCityOptions().then(response => {
    cityOptions.value = response.data || []
  })
}
 
function getDetail(id) {
  loading.value = true
  getProvider(id).then(response => {
    const detail = response.data
    // 后端对不存在的服务商返回 data: null(与公司档案详情一致的写法)。
    // 不拦的话会渲染出一个空表单,提交时因为 id 为空反而走成新增,故直接退回列表
    if (!detail) {
      proxy.$modal.msgError('服务商不存在或已删除')
      loading.value = false
      goBack()
      return
    }
    // 详情接口刻意不回密码,两个密码列永远是空的:
    // 修改态下「留空即不改」正是靠这一点成立的
    form.value = { ...detail, password: '', confirmPassword: '' }
    loading.value = false
  }).catch(() => {
    loading.value = false
  })
}
 
function beforeUpload(file) {
  if (!['image/png', 'image/jpeg'].includes(file.type)) {
    proxy.$modal.msgError('营业执照仅支持 png / jpg / jpeg 格式')
    return false
  }
  if (file.size / 1024 / 1024 > 5) {
    proxy.$modal.msgError('营业执照大小不能超过 5MB')
    return false
  }
  return true
}
 
// 走 :http-request 复用仓库的 axios 封装(同 userAvatar.vue):token、/dev-api 前缀、
// 错误提示都由 utils/request.js 兜住,不必手工拼 Authorization 头。
// 字段名必须是 file(SysFileController 的参数名),响应取 data.url(绝对地址)
function handleUpload(options) {
  const formData = new FormData()
  formData.append('file', options.file)
  uploading.value = true
  return uploadLicense(formData).then(response => {
    form.value.licenseUrl = response.data.url
    proxy.$modal.msgSuccess('上传成功')
  }).catch(() => {
  }).finally(() => {
    uploading.value = false
  })
}
 
function submitForm() {
  formRef.value.validate(valid => {
    if (!valid) {
      return
    }
    // 只提交表单上的字段:service 的 update 是按 <if> 拼列的,
    // 把详情里多出来的 status/delFlag 一起回传会顺带改掉不该改的列
    const payload = {
      id: form.value.id,
      companyName: form.value.companyName,
      contactName: form.value.contactName,
      contactPhone: form.value.contactPhone,
      creditCode: form.value.creditCode,
      legalPerson: form.value.legalPerson,
      region: form.value.region,
      registerDate: form.value.registerDate,
      address: form.value.address,
      licenseUrl: form.value.licenseUrl,
      l1Quota: form.value.l1Quota,
      l2Quota: form.value.l2Quota,
      l3Quota: form.value.l3Quota,
      userName: form.value.userName,
      password: form.value.password,
      confirmPassword: form.value.confirmPassword
    }
    const request = payload.id ? updateProvider(payload) : addProvider(payload)
    request.then(() => {
      proxy.$modal.msgSuccess(payload.id ? '修改成功' : '新增成功')
      if (payload.id) {
        // 改完留在本页看结果:重取详情顺带把密码框清空,
        // 否则提交成功后两个密码框里还留着明文,再点一次提交会把密码又改一遍
        getDetail(payload.id)
      } else {
        goBack()
      }
    })
  })
}
 
function goBack() {
  proxy.$tab.closeOpenPage({ path: '/member/provider' })
}
 
getCityOptions()
if (isEdit.value) {
  getDetail(route.params.id)
}
</script>
 
<style scoped>
.block-title {
  margin-bottom: 12px;
  font-size: 15px;
  font-weight: 600;
  color: #303133;
}
 
/* 灰底面板即四列栅格,每个 el-form-item 是一格。
   行距交给 row-gap,元素自带的 margin-bottom 会把这套栅格顶歪,故下面清零 */
.info-panel {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  column-gap: 16px;
  row-gap: 20px;
  padding: 24px;
  margin-bottom: 24px;
  background-color: #fafafa;
  border-radius: 4px;
}
 
.provider-form :deep(.el-form-item) {
  margin-bottom: 0;
}
 
/* label-width 取 130px(公司档案那页是 104px)。
   本页最长的标签是「重复登录密码」(6 字),标签在 ruoyi.scss 里是粗体,
   前面还挂着必填星号:约 10 + 6×14.5 + 冒号 14 ≈ 111px,
   104px 减掉右侧 12px 内边距只剩 92px,会折行。
   以后出现更长的标签,这里要跟着调大 */
.license-item,
.panel-hint {
  grid-column: 1 / -1;
}
 
.license-row {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
}
 
.license-box {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 160px;
  height: 100px;
  background-color: #f0f2f5;
  border: 1px dashed #dcdfe6;
  border-radius: 4px;
  overflow: hidden;
}
 
.license-img {
  width: 100%;
  height: 100%;
}
 
.license-empty {
  font-size: 12px;
  color: #909399;
}
 
.license-hint {
  font-size: 12px;
  color: #909399;
}
 
.panel-hint {
  padding-bottom: 4px;
  font-size: 12px;
  color: #909399;
}
 
.form-actions {
  display: flex;
  gap: 10px;
  align-items: center;
}
</style>