feat(manager): 专家审核页修改按钮跳转独立编辑页

参考 /admin/experts/new 模式:
- 新建 ry-vue3/src/views/manager/ExpertNew.vue (15 字段, 1:1 抄 admin 版, 仅 goBack 跳 /manager/experts)
- router 加 /manager/experts/new 和 /manager/experts/edit/:id 两条路由, 复用 ExpertNew 组件
- manager/Experts.vue 操作列 "修改" 按钮 → router.push('/manager/experts/edit/:id')
- 加 route watcher, 从编辑页返回时自动重载列表
- 新建专家按钮保留原 dialog (按用户当前只要求改修改按钮)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
郭庆泰
2026-08-17 00:40:45 +08:00
co-authored by Claude
parent e099211cbd
commit 15fd26af30
3 changed files with 350 additions and 9 deletions
+2
View File
@@ -68,6 +68,8 @@ const routes = [
{ path: 'meetings', name: 'manager-meetings', component: () => import('@/views/manager/Meetings.vue'), meta: { title: '会议管理' } },
{ path: 'meetings/new', name: 'manager-meetings-new', component: () => import('@/views/manager/MeetingNew.vue'), meta: { title: '新建会议' } },
{ path: 'experts', name: 'manager-experts', component: () => import('@/views/manager/Experts.vue'), meta: { title: '专家审核' } },
{ path: 'experts/new', name: 'manager-experts-new', component: () => import('@/views/manager/ExpertNew.vue'), meta: { title: '新建专家' } },
{ path: 'experts/edit/:id', name: 'manager-experts-edit', component: () => import('@/views/manager/ExpertNew.vue'), meta: { title: '编辑专家' } },
{ path: 'sponsor-orgs', name: 'manager-sponsor-orgs', component: () => import('@/views/manager/SponsorOrgs.vue'), meta: { title: '支持单位管理' } },
{ path: 'executor-orgs', name: 'manager-executor-orgs', component: () => import('@/views/manager/ExecutorOrgs.vue'), meta: { title: '服务机构管理' } },
{ path: 'sponsor-people', name: 'manager-sponsor-people', component: () => import('@/views/manager/SponsorPeople.vue'), meta: { title: '支持单位下人员' } },
+335
View File
@@ -0,0 +1,335 @@
<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> &gt; {{ 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) {
delete payload.expertId
} else {
// manager 创建专家: 自动通过审核, 状态正常
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'
}
const res = isEdit
? await bizUpdate('expert', payload)
: await bizAdd('expert', payload)
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>
+13 -9
View File
@@ -74,7 +74,7 @@
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="goEdit(row)">修改</el-button>
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link :type="row.status === 'N' ? 'success' : 'danger'" @click="row.status === 'N' ? onEnable(row) : onDisable(row)">
{{ row.status === 'N' ? '恢复' : '禁用' }}
@@ -207,11 +207,15 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { bizList, bizAdd, bizUpdate } from '@/api/public'
import AuditStatusTag from '@/components/AuditStatusTag.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
const route = useRoute()
const q = ref({ name: '', phone: '', workUnit: '', auditStatus: '' })
const rows = ref([])
const selection = ref([])
@@ -261,13 +265,9 @@ function openAdd() {
Object.assign(editForm, { expertId: '', name: '', phone: '', workUnit: '', department: '', title: '', practiceCertUrl: '', titleCertUrl: '' })
editOpen.value = true
}
function onEdit(row) {
Object.assign(editForm, {
expertId: row.expertId, name: row.name, phone: row.phone, workUnit: row.workUnit,
department: row.department, title: row.title,
practiceCertUrl: row.practiceCertUrl || '', titleCertUrl: row.titleCertUrl || ''
})
editOpen.value = true
// 修改按钮: 跳转独立页 /manager/experts/edit/:id (参考 /admin/experts/new)
function goEdit(row) {
router.push(`/manager/experts/edit/${row.expertId}`)
}
async function submitEdit() {
await editFormRef.value.validate()
@@ -424,6 +424,10 @@ async function submitImportTitleCert() {
}
onMounted(load)
// 从 ExpertNew 跳转回来时刷新列表
watch(() => route.fullPath, (newPath) => {
if (newPath === '/manager/experts') load()
})
</script>
<style scoped>