两个根因:
1. 后端 Updates: 0
- ExpertNew.vue (admin+manager) onSave 编辑分支里 delete payload.expertId
- 后端 BizExpertMapper.updateByPrimaryKey 是 where expert_id = #{expertId}
- 没有 expertId → WHERE 不匹配 → 0 updates → 抛 "操作失败" 500
- 修复: 不再 delete expertId (后端 insert/update 都依赖该字段)
2. 前端操作失败弹两次
- request.js 响应拦截器在 code != 200 时自动 ElMessage.error
- onSave 的 catch 又 ElMessage.error 一次
- 修复: 加 __silentError 请求配置项, 拦截器遇此 flag 不自动 toast
- api/public.js bizAdd/bizUpdate 加第 3 参数 opts 透传给 request
- ExpertNew.vue onSave 调用时传 __silentError: true, 由 catch 块按业务语义
决定 黄警 (手机号重复) 还是 红错 (其它)
Co-Authored-By: Claude <noreply@anthropic.com>
336 lines
11 KiB
Vue
336 lines
11 KiB
Vue
<template>
|
|
<!--
|
|
manager 视角专家新建/编辑独立页
|
|
1:1 抄 admin/ExpertNew.vue, 仅 goBack 跳 /manager/experts
|
|
通过 route.params.id 区分新建/编辑
|
|
-->
|
|
<div class="page-card expert-new">
|
|
<div class="breadcrumb">
|
|
<a @click="goBack">专家审核</a> > {{ isEdit ? '编辑专家' : '新建专家' }}
|
|
</div>
|
|
|
|
<el-card v-loading="loadingDetail" shadow="never" class="form-card nested">
|
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
|
|
<el-row :gutter="12">
|
|
<el-col :span="12">
|
|
<el-form-item label="姓名" prop="name">
|
|
<el-input v-model="form.name" placeholder="请输入姓名" maxlength="50" />
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="手机号" prop="phone">
|
|
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row :gutter="12">
|
|
<el-col :span="12">
|
|
<el-form-item label="工作单位" prop="workUnit">
|
|
<el-input v-model="form.workUnit" placeholder="请输入工作单位" maxlength="200" />
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="科室" prop="department">
|
|
<doctor-dept-select
|
|
v-model="form.department"
|
|
value-field="label"
|
|
placeholder="输入关键词搜索"
|
|
filterable
|
|
style="width:100%"
|
|
/>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row :gutter="12">
|
|
<el-col :span="12">
|
|
<el-form-item label="职称" prop="title">
|
|
<doctor-title-select
|
|
v-model="form.title"
|
|
value-field="label"
|
|
placeholder="输入关键词搜索"
|
|
filterable
|
|
style="width:100%"
|
|
/>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="地区">
|
|
<area-cascader
|
|
v-model="form.region"
|
|
format="string"
|
|
join-sep="/"
|
|
placeholder="请选择省/市/区"
|
|
style="width:100%"
|
|
/>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row :gutter="12">
|
|
<el-col :span="24">
|
|
<el-form-item label="执业医师证书">
|
|
<oss-image-uploader
|
|
v-model="form.practiceCertUrl"
|
|
dir="ry8080/expert/practice/"
|
|
placeholder="+"
|
|
/>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row :gutter="12">
|
|
<el-col :span="24">
|
|
<el-form-item label="职称证明">
|
|
<oss-image-uploader
|
|
v-model="form.titleCertUrl"
|
|
dir="ry8080/expert/title/"
|
|
placeholder="+"
|
|
/>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row :gutter="12">
|
|
<el-col :span="24">
|
|
<el-form-item label="身份证件号码">
|
|
<el-input v-model="form.idCard" placeholder="请输入身份证件号码" maxlength="18" />
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row :gutter="12">
|
|
<el-col :span="24">
|
|
<el-form-item label="身份证件照片">
|
|
<id-card-uploader
|
|
v-model:front-url="form.idCardFrontUrl"
|
|
v-model:back-url="form.idCardBackUrl"
|
|
dir="ry8080/expert/idcard/"
|
|
/>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row :gutter="12">
|
|
<el-col :span="12">
|
|
<el-form-item label="银行卡号">
|
|
<el-input v-model="form.bankCard" placeholder="请输入银行卡号" maxlength="19" />
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="银行名称">
|
|
<el-input v-model="form.bankName" placeholder="如: 中国工商银行" maxlength="50" />
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row :gutter="12">
|
|
<el-col :span="12">
|
|
<el-form-item label="开户行省份">
|
|
<el-input v-model="form.bankProvince" placeholder="如: 北京市" maxlength="50" />
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-form-item label="开户行城市">
|
|
<el-input v-model="form.bankCity" placeholder="如: 北京市" maxlength="50" />
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row :gutter="12">
|
|
<el-col :span="24">
|
|
<el-form-item label="开户行地址">
|
|
<el-input v-model="form.bankAddress" placeholder="请输入开户行详细地址" maxlength="200" />
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
</el-form>
|
|
</el-card>
|
|
|
|
<div class="form-actions">
|
|
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
|
|
<el-button @click="confirmCancel">取消</el-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { reactive, ref, onMounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { bizAdd, bizUpdate, bizGet } from '@/api/public'
|
|
import { useUserStore } from '@/store/user'
|
|
import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
|
|
import DoctorDeptSelect from '@/components/DoctorDeptSelect.vue'
|
|
import AreaCascader from '@/components/AreaCascader.vue'
|
|
import IdCardUploader from '@/components/IdCardUploader.vue'
|
|
import OssImageUploader from '@/components/OssImageUploader.vue'
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const userStore = useUserStore()
|
|
|
|
const formRef = ref(null)
|
|
const saving = ref(false)
|
|
const loadingDetail = ref(false)
|
|
const isEdit = !!route.params.id
|
|
const expertId = route.params.id || null
|
|
|
|
const form = reactive({
|
|
expertId: null,
|
|
name: '',
|
|
phone: '',
|
|
workUnit: '',
|
|
department: '',
|
|
title: '',
|
|
region: '',
|
|
practiceCertUrl: '',
|
|
titleCertUrl: '',
|
|
idCard: '',
|
|
idCardFrontUrl: '',
|
|
idCardBackUrl: '',
|
|
bankCard: '',
|
|
bankName: '',
|
|
bankProvince: '',
|
|
bankCity: '',
|
|
bankAddress: '',
|
|
})
|
|
|
|
const rules = {
|
|
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
|
phone: [
|
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
|
{ pattern: /^1[0-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
|
|
],
|
|
workUnit: [{ required: true, message: '请输入工作单位', trigger: 'blur' }],
|
|
department:[{ required: true, message: '请选择科室', trigger: 'change' }],
|
|
title: [{ required: true, message: '请选择职称', trigger: 'change' }],
|
|
}
|
|
|
|
function goBack() {
|
|
router.push('/manager/experts')
|
|
}
|
|
|
|
async function loadDetail() {
|
|
if (!expertId) return
|
|
loadingDetail.value = true
|
|
try {
|
|
const res = await bizGet('expert', expertId)
|
|
if (res?.code === 200 && res.data) {
|
|
const d = res.data
|
|
Object.assign(form, {
|
|
expertId: d.expertId,
|
|
name: d.name || '',
|
|
phone: d.phone || '',
|
|
workUnit: d.workUnit || '',
|
|
department: d.department || '',
|
|
title: d.title || '',
|
|
region: d.region || '',
|
|
practiceCertUrl: d.practiceCertUrl || '',
|
|
titleCertUrl: d.titleCertUrl || '',
|
|
idCard: d.idCard || '',
|
|
idCardFrontUrl: d.idCardFrontUrl || '',
|
|
idCardBackUrl: d.idCardBackUrl || '',
|
|
bankCard: d.bankCard || '',
|
|
bankName: d.bankName || '',
|
|
bankProvince: d.bankProvince || '',
|
|
bankCity: d.bankCity || '',
|
|
bankAddress: d.bankAddress || '',
|
|
})
|
|
} else {
|
|
ElMessage.error(res?.msg || '加载失败')
|
|
goBack()
|
|
}
|
|
} catch (e) {
|
|
ElMessage.error(e?.msg || e?.message || '加载失败')
|
|
goBack()
|
|
} finally {
|
|
loadingDetail.value = false
|
|
}
|
|
}
|
|
|
|
function confirmCancel() {
|
|
const hasContent = form.name || form.phone || form.workUnit || form.department || form.title
|
|
if (!isEdit && !hasContent) { goBack(); return }
|
|
ElMessageBox.confirm('确定取消?未保存的内容将丢失', '提示', { type: 'warning' })
|
|
.then(() => goBack())
|
|
.catch(() => {})
|
|
}
|
|
|
|
function isDuplicateError(msg) {
|
|
return /已存在|重复|已注册|duplicate|uk_expert_phone/i.test(msg || '')
|
|
}
|
|
|
|
async function onSave() {
|
|
try {
|
|
await formRef.value.validate()
|
|
} catch {
|
|
ElMessage.warning('请填写必填项')
|
|
return
|
|
}
|
|
saving.value = true
|
|
try {
|
|
const payload = { ...form }
|
|
if (!isEdit) {
|
|
// 新建: manager 创建专家, 自动通过审核, 状态正常
|
|
// 注意: 不要把 expertId 也删掉, 后端 updateByPrimaryKey / insert 都依赖 expert_id
|
|
payload.auditStatus = '2'
|
|
payload.auditBy = userStore.user?.userName || userStore.user?.nickName || 'manager'
|
|
payload.auditTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
payload.status = 'Y'
|
|
}
|
|
// __silentError: 拦截器不自动 toast, 由本组件根据业务语义选择 黄警 (重复) / 红错 (其它)
|
|
const opts = { __silentError: true }
|
|
const res = isEdit
|
|
? await bizUpdate('expert', payload, opts)
|
|
: await bizAdd('expert', payload, opts)
|
|
if (res?.code === 200) {
|
|
if (!isEdit && res.data) {
|
|
const u = res.data
|
|
ElMessageBox.alert(
|
|
`专家账号已创建\n账号: ${u.userName || form.phone}\n初始密码: ${u.password || form.phone}\n请记录后告知使用人 (密码仅本次显示)`,
|
|
'创建成功',
|
|
{ type: 'success', confirmButtonText: '我已记录, 返回' }
|
|
).then(() => goBack())
|
|
.catch(() => goBack())
|
|
} else {
|
|
ElMessage.success(isEdit ? '修改成功' : '新建成功')
|
|
goBack()
|
|
}
|
|
} else {
|
|
const msg = res?.msg || '保存失败'
|
|
if (isDuplicateError(msg)) ElMessage.warning('该手机号已注册,请更换')
|
|
else ElMessage.error(msg)
|
|
}
|
|
} catch (e) {
|
|
const msg = e?.msg || e?.message || '保存失败'
|
|
if (isDuplicateError(msg)) ElMessage.warning('该手机号已注册,请更换')
|
|
else ElMessage.error(msg)
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (isEdit) loadDetail()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.expert-new { max-width: 1200px; }
|
|
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
|
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 16px; }
|
|
.breadcrumb a { color: var(--brand-primary); cursor: pointer; text-decoration: none; }
|
|
.breadcrumb a:hover { color: var(--brand-primary-deep, var(--brand-primary)); }
|
|
|
|
.form-card.nested { background: transparent; border: none; box-shadow: none; }
|
|
:deep(.form-card .el-card__body) { padding: 24px 28px; }
|
|
|
|
.form-actions { margin-top: 16px; display: flex; gap: 8px; }
|
|
|
|
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
|
|
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep, var(--brand-primary)); border-color: var(--brand-primary-deep, var(--brand-primary)); }
|
|
:deep(.el-button) { border-radius: 4px; }
|
|
:deep(.el-input__wrapper), :deep(.el-textarea__inner) { border-radius: 4px; }
|
|
</style> |