Files
guoju0808/ry-vue3/src/views/manager/Experts.vue
T
郭庆泰andClaude a1c54842e2 feat(manager/experts): 审核状态后加启用状态列
新增 el-table-column "启用状态", 显示 正常(绿) / 禁用(红) el-tag,
数据由 row.status === 'N' 区分, 1:1 抄 admin/Experts.vue 同列实现。

列顺序: 姓名 手机号 工作单位 科室 职称 执业证书 职称证明 审核状态 [启用状态] 审批时间 审批人 操作

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 00:51:11 +08:00

473 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page-card manager-experts">
<div class="breadcrumb">首页 / 专家审核</div>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="" clearable style="width:160px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="" clearable style="width:160px" />
</el-form-item>
<el-form-item label="工作单位">
<el-input v-model="q.workUnit" placeholder="" clearable style="width:200px" />
</el-form-item>
<el-form-item label="审核状态">
<el-select v-model="q.auditStatus" placeholder="请选择" clearable style="width:140px">
<el-option label="未提交" value="0" />
<el-option label="待审核" value="1" />
<el-option label="通过" value="2" />
<el-option label="拒绝" value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 批量按钮区 ( People.vue 风格一致) ========== -->
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新建专家</el-button>
<el-button @click="openImport">批量导入</el-button>
<el-button @click="onBatchAudit" :disabled="!selection.length">批量审核</el-button>
<el-button @click="openImportPracticeCert">批量导入执业证书/证明</el-button>
<el-button @click="openImportTitleCert">批量导入职称证明</el-button>
</div>
<!-- ========== 表格 (按原型 11 , left 顺序) ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
<el-table-column type="selection" width="44" />
<el-table-column prop="name" label="姓名" width="100" fixed />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="科室" width="100" />
<el-table-column prop="title" label="职称" width="100" />
<el-table-column label="执业证书/证明" width="180">
<template #default="{ row }">
<template v-if="row.practiceCertUrl">
<el-button link type="primary" @click="onPreviewCert(row.practiceCertUrl)">查看</el-button>
<el-button link type="primary" @click="onDownloadCert(row.practiceCertUrl, row.phone)">下载</el-button>
</template>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="职称证明" width="180">
<template #default="{ row }">
<template v-if="row.titleCertUrl">
<el-button link type="primary" @click="onPreviewCert(row.titleCertUrl)">查看</el-button>
<el-button link type="primary" @click="onDownloadCert(row.titleCertUrl, row.phone)">下载</el-button>
</template>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="审核状态" width="100" align="center">
<template #default="{ row }">
<audit-status-tag :status="row.auditStatus" />
</template>
</el-table-column>
<el-table-column label="启用状态" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.status === 'N' ? 'danger' : 'success'">
{{ row.status === 'N' ? '禁用' : '正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="auditTime" label="审批时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.auditTime) }}</template>
</el-table-column>
<el-table-column prop="auditBy" label="审批人" width="120" align="center" />
<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="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' ? '恢复' : '禁用' }}
</el-button>
<el-button link type="info" v-if="row.auditStatus === '3'" @click="onViewAuditOpinion(row)">已拒绝</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
</div>
<!-- ========== Dialog: 新建/编辑专家 (按原型 top=239 确定/取消) ========== -->
<el-dialog v-model="editOpen" :title="editForm.expertId ? '编辑专家' : '新建专家'" width="640px">
<el-form :model="editForm" label-width="120px" :rules="editRules" ref="editFormRef">
<el-form-item label="姓名" prop="name"><el-input v-model="editForm.name" /></el-form-item>
<el-form-item label="手机号" prop="phone"><el-input v-model="editForm.phone" maxlength="11" /></el-form-item>
<el-form-item label="工作单位" prop="workUnit"><el-input v-model="editForm.workUnit" /></el-form-item>
<el-form-item label="科室" prop="department"><el-input v-model="editForm.department" placeholder="如:呼吸科" /></el-form-item>
<el-form-item label="职称" prop="title">
<el-select v-model="editForm.title" placeholder="请选择" style="width:100%">
<el-option label="住院医师" value="住院医师" />
<el-option label="主治医师" value="主治医师" />
<el-option label="副主任医师" value="副主任医师" />
<el-option label="主任医师" value="主任医师" />
<el-option label="教授" value="教授" />
<el-option label="副教授" value="副教授" />
</el-select>
</el-form-item>
<el-form-item label="执业证书 URL"><el-input v-model="editForm.practiceCertUrl" /></el-form-item>
<el-form-item label="职称证明 URL"><el-input v-model="editForm.titleCertUrl" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="editOpen=false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitEdit">确定</el-button>
</template>
</el-dialog>
<!-- ========== Dialog: 审核 (原型 top=404 确认/取消) ========== -->
<el-dialog v-model="auditOpen" title="审核" width="520px">
<p style="margin-bottom:12px">专家: <b>{{ auditForm.name }}</b> ({{ auditForm.phone }})</p>
<el-form :model="auditForm" label-width="100px">
<el-form-item label="审核结果">
<el-radio-group v-model="auditForm.result">
<el-radio value="2">通过</el-radio>
<el-radio value="3">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见">
<el-input v-model="auditForm.opinion" type="textarea" :rows="4" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="auditOpen=false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitAudit">确认</el-button>
</template>
</el-dialog>
<!-- ========== Dialog: 批量导入 (原型 top=239 确定/取消, radio 通过/退回) ========== -->
<el-dialog v-model="importOpen" title="批量导入专家" width="520px">
<div style="margin-bottom:12px;font-size:13px">
<el-link type="primary" @click="downloadImportTpl">批量导入模板下载</el-link>
</div>
<p style="color:#606266;margin-bottom:8px">*批量导入模板 xls 与列表表项一致</p>
<el-form label-width="100px">
<el-form-item label="审核结果">
<el-radio-group v-model="importAuditResult">
<el-radio value="2">通过</el-radio>
<el-radio value="3">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见">
<el-input v-model="importAuditOpinion" type="textarea" :rows="3" placeholder="*新建和批量导入审核状态直接为审核通过" />
</el-form-item>
<el-form-item label="选择文件">
<el-input v-model="importFileUrl" placeholder="上传文件 URL 或选择文件" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen=false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
</template>
</el-dialog>
<!-- ========== Dialog: 批量导入执业证书/证明 (原型 u6124) ========== -->
<el-dialog v-model="importCertOpen" title="批量导入执业证书/证明" width="520px">
<p style="color:#606266;margin-bottom:8px">*退回后如专家重新提交则审核状态变为待审核</p>
<p style="color:#606266;margin-bottom:8px">提示需将证明文件名称修改为手机号进行导入</p>
<el-input v-model="importCertUrl" placeholder="上传文件 URL 或选择文件" />
<template #footer>
<el-button @click="importCertOpen=false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImportCert">确定</el-button>
</template>
</el-dialog>
<!-- ========== Dialog: 批量导入职称证明 (原型 u6126) ========== -->
<el-dialog v-model="importTitleCertOpen" title="批量导入职称证明" width="520px">
<p style="color:#606266;margin-bottom:8px">提示需将证明文件名称修改为手机号进行导入</p>
<el-input v-model="importTitleCertUrl" placeholder="上传文件 URL 或选择文件" />
<template #footer>
<el-button @click="importTitleCertOpen=false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImportTitleCert">确定</el-button>
</template>
</el-dialog>
<!-- ========== Dialog: 查看专家详情 (原型 操作列 "查看" 按钮) ========== -->
<el-dialog v-model="viewOpen" title="专家详情" width="640px">
<el-descriptions :column="2" border v-if="currentRow">
<el-descriptions-item label="姓名">{{ currentRow.name }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ currentRow.phone }}</el-descriptions-item>
<el-descriptions-item label="工作单位">{{ currentRow.workUnit }}</el-descriptions-item>
<el-descriptions-item label="科室">{{ currentRow.department }}</el-descriptions-item>
<el-descriptions-item label="职称">{{ currentRow.title }}</el-descriptions-item>
<el-descriptions-item label="审核状态"><audit-status-tag :status="currentRow.auditStatus" /></el-descriptions-item>
<el-descriptions-item label="执业证书">
<el-link v-if="currentRow.practiceCertUrl" type="primary" @click="onPreviewCert(currentRow.practiceCertUrl)">查看证书</el-link>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="职称证明">
<el-link v-if="currentRow.titleCertUrl" type="primary" @click="onPreviewCert(currentRow.titleCertUrl)">查看证书</el-link>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="审批时间">{{ fmtTime(currentRow.auditTime) }}</el-descriptions-item>
<el-descriptions-item label="审批人">{{ currentRow.auditBy }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<!-- 证书/证明 图片预览 dialog (1:1 admin/Experts.vue) -->
<el-dialog v-model="previewOpen" title="证书预览" width="640px" :show-close="true" destroy-on-close>
<div class="cert-preview">
<el-image
:src="previewUrl"
fit="contain"
:preview-src-list="[previewUrl]"
style="width:100%;max-height:70vh"
loading="lazy"
>
<template #error>
<div class="cert-preview-error">图片加载失败, <a @click="onDownloadCert(previewUrl)">下载</a>查看</div>
</template>
</el-image>
</div>
<template #footer>
<el-button @click="onDownloadCert(previewUrl)">下载</el-button>
<el-button type="primary" @click="previewOpen=false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
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([])
const loading = ref(false)
const submitting = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
async function load() {
loading.value = true
try {
const { data } = await bizList('expert', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []
page.total = data?.total || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
q.value = { name: '', phone: '', workUnit: '', auditStatus: '' }
page.pageNum = 1
load()
}
function onSel(rs) { selection.value = rs }
// auditStatusText / auditStatusClass 已删除: 改用 AuditStatusTag 组件
function fmtTime(d) {
if (!d) return ''
const dt = new Date(d)
const pad = n => String(n).padStart(2, '0')
return `${dt.getFullYear()}.${pad(dt.getMonth() + 1)}.${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`
}
// ========== 新建/编辑 dialog ==========
const editOpen = ref(false)
const editFormRef = ref(null)
const editForm = reactive({
expertId: '', name: '', phone: '', workUnit: '', department: '', title: '',
practiceCertUrl: '', titleCertUrl: ''
})
const editRules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
workUnit: [{ required: true, message: '请输入工作单位', trigger: 'blur' }],
department: [{ required: true, message: '请输入科室', trigger: 'blur' }],
title: [{ required: true, message: '请选择职称', trigger: 'change' }]
}
function openAdd() {
Object.assign(editForm, { expertId: '', name: '', phone: '', workUnit: '', department: '', title: '', practiceCertUrl: '', 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()
submitting.value = true
try {
if (editForm.expertId) {
await bizUpdate('expert', { ...editForm })
ElMessage.success('修改成功')
} else {
await bizAdd('expert', { ...editForm, auditStatus: '2', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') })
ElMessage.success('新建成功')
}
editOpen.value = false
load()
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
finally { submitting.value = false }
}
// ========== 审核 dialog ==========
const auditOpen = ref(false)
// result: '2'=通过, '3'=拒绝 (BizAuditStatusEnum: 0未提交 1待审核 2通过 3拒绝)
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '2', opinion: '' })
function onAudit(row) {
auditForm.expertId = row.expertId
auditForm.name = row.name
auditForm.phone = row.phone
auditForm.result = '2'
auditForm.opinion = ''
auditOpen.value = true
}
async function submitAudit() {
submitting.value = true
try {
await bizUpdate('expert', {
expertId: auditForm.expertId,
auditStatus: auditForm.result,
auditOpinion: auditForm.opinion,
auditBy: 'admin', // 后端应取登录用户
auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ')
})
ElMessage.success(auditForm.result === '2' ? '审核通过' : '已拒绝')
auditOpen.value = false
load()
} catch { ElMessage.error('审核失败') }
finally { submitting.value = false }
}
// ========== 批量审核 ==========
async function onBatchAudit() {
if (!selection.value.length) return ElMessage.warning('请先勾选行')
try { await ElMessage.confirm(`确定批量审核选中的 ${selection.value.length} 位专家吗?`, '批量审核', { type: 'warning' }).catch(() => { throw new Error('cancel') }) } catch { return }
submitting.value = true
let ok = 0
for (const r of selection.value) {
try { await bizUpdate('expert', { expertId: r.expertId, auditStatus: '2', auditOpinion: '批量通过', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') }); ok++ } catch {}
}
submitting.value = false
ElMessage.success(`批量审核通过 ${ok}/${selection.value.length} 条`)
selection.value = []
load()
}
// ========== 禁用/恢复 ==========
async function onDisable(row) {
try { await bizUpdate('expert', { expertId: row.expertId, status: 'N' }); ElMessage.success('已禁用'); load() } catch { ElMessage.error('禁用失败') }
}
async function onEnable(row) {
try { await bizUpdate('expert', { expertId: row.expertId, status: 'Y' }); ElMessage.success('已恢复'); load() } catch { ElMessage.error('恢复失败') }
}
// ========== 查看详情 ==========
const viewOpen = ref(false)
const currentRow = ref(null)
function onView(row) { currentRow.value = row; viewOpen.value = true }
// ========== 已拒绝: 查看拒绝意见 ==========
function onViewAuditOpinion(row) {
ElMessageBox.alert(
`拒绝时间: ${fmtTime(row.auditTime) || '-'}\n审批人: ${row.auditBy || '-'}\n拒绝意见: ${row.auditOpinion || '(无)'}`,
`专家 ${row.name} 审核已拒绝`,
{ type: 'warning', confirmButtonText: '知道了' }
).catch(() => {})
}
// ========== 证书查看/下载 ==========
// 图片预览: dialog 弹窗直接显示 (用 el-image 自带 preview-src-list 支持点击放大)
// 同步自 admin/Experts.vue (2026-08-17)
const previewOpen = ref(false)
const previewUrl = ref('')
function onPreviewCert(url) {
if (!url) return
previewUrl.value = url
previewOpen.value = true
}
function onDownloadCert(url, phone) {
if (!url) return
const a = document.createElement('a')
a.href = url
a.download = `${phone || 'cert'}_${Date.now()}`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
// ========== 批量导入 dialog ==========
const importOpen = ref(false)
const importAuditResult = ref('2')
const importAuditOpinion = ref('')
const importFileUrl = ref('')
const importing = ref(false)
function openImport() {
importAuditResult.value = '2'
importAuditOpinion.value = ''
importFileUrl.value = ''
importOpen.value = true
}
function downloadImportTpl() {
ElMessage.info('批量导入模板下载 — 待后端提供静态文件')
}
async function submitImport() {
if (!importFileUrl.value) return ElMessage.warning('请选择文件')
importing.value = true
await new Promise(r => setTimeout(r, 500))
importing.value = false
ElMessage.info(`已提交批量导入任务 (审核结果: ${importAuditResult.value === '2' ? '通过' : '拒绝'}) — 待后端实现`)
importOpen.value = false
}
// ========== 批量导入执业证书/证明 ==========
const importCertOpen = ref(false)
const importCertUrl = ref('')
function openImportPracticeCert() { importCertUrl.value = ''; importCertOpen.value = true }
async function submitImportCert() {
if (!importCertUrl.value) return ElMessage.warning('请选择文件')
importing.value = true
await new Promise(r => setTimeout(r, 500))
importing.value = false
ElMessage.info('已提交批量导入执业证书任务 — 待后端实现')
importCertOpen.value = false
}
// ========== 批量导入职称证明 ==========
const importTitleCertOpen = ref(false)
const importTitleCertUrl = ref('')
function openImportTitleCert() { importTitleCertUrl.value = ''; importTitleCertOpen.value = true }
async function submitImportTitleCert() {
if (!importTitleCertUrl.value) return ElMessage.warning('请选择文件')
importing.value = true
await new Promise(r => setTimeout(r, 500))
importing.value = false
ElMessage.info('已提交批量导入职称证明任务 — 待后端实现')
importTitleCertOpen.value = false
}
onMounted(load)
// 从 ExpertNew 跳转回来时刷新列表
watch(() => route.fullPath, (newPath) => {
if (newPath === '/manager/experts') load()
})
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-experts { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
/* 证书预览 dialog (1:1 抄 admin) */
.cert-preview { display: flex; justify-content: center; align-items: center; min-height: 200px; background: #f5f7fa; border-radius: 4px; padding: 8px; }
.cert-preview-error { color: #909399; padding: 40px 0; }
/* stage-* CSS 已删除: 改用 AuditStatusTag 组件 (el-tag 自带颜色) */
</style>