<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>
|