You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1367 lines
44 KiB

  1. <template>
  2. <div class="app-container">
  3. <!-- 搜索部分 -->
  4. <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="90px">
  5. <el-form-item label="真实姓名" prop="realName">
  6. <el-input v-model="queryParams.realName" placeholder="请输入真实姓名" clearable @keyup.enter.native="handleQuery" />
  7. </el-form-item>
  8. <el-form-item label="性别" prop="gender">
  9. <el-input v-model="queryParams.gender" placeholder="请输入性别" clearable @keyup.enter.native="handleQuery" />
  10. </el-form-item>
  11. <el-form-item label="身份证号" prop="idCard">
  12. <el-input v-model="queryParams.idCard" placeholder="请输入身份证号" clearable @keyup.enter.native="handleQuery" />
  13. </el-form-item>
  14. <el-form-item label="工作经验" prop="workExperience">
  15. <el-input v-model="queryParams.workExperience" placeholder="请输入工作经验" clearable
  16. @keyup.enter.native="handleQuery" />
  17. </el-form-item>
  18. <el-form-item label="所属医院" prop="hospital">
  19. <el-input v-model="queryParams.hospital" placeholder="请输入所属医院" clearable @keyup.enter.native="handleQuery" />
  20. </el-form-item>
  21. <el-form-item>
  22. <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
  23. <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  24. </el-form-item>
  25. </el-form>
  26. <el-row :gutter="10" class="mb8">
  27. <el-col :span="1.5">
  28. <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete"
  29. v-hasPermi="['vet:info:remove']">删除</el-button>
  30. </el-col>
  31. <el-col :span="1.5">
  32. <el-button type="warning" plain icon="el-icon-check" size="mini" :disabled="single" @click="handleAudit"
  33. v-hasPermi="['sys:vetAudit:audit']">审核</el-button>
  34. </el-col>
  35. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  36. </el-row>
  37. <!-- 表格部分 -->
  38. <el-table v-loading="loading" :data="infoList" @selection-change="handleSelectionChange">
  39. <el-table-column type="selection" width="55" align="center" />
  40. <el-table-column label="用户昵称" align="center" prop="nickName" />
  41. <el-table-column label="真实姓名" align="center" prop="realName" />
  42. <el-table-column label="擅长领域" align="center" prop="specialty" :show-overflow-tooltip="true" />
  43. <el-table-column label="职称" align="center" prop="title" />
  44. <el-table-column label="专家类型" align="center" prop="expertType" />
  45. <el-table-column label="所属医院" align="center" prop="hospital" :show-overflow-tooltip="true" />
  46. <el-table-column label="个人简介" align="center" prop="introduction" :show-overflow-tooltip="true" />
  47. <el-table-column label="审核状态" align="center" prop="auditStatus" width="100" class-name="audit-status-column">
  48. <template slot-scope="scope">
  49. <dict-tag :options="dict.type.audit_status" :value="scope.row.auditStatus" />
  50. </template>
  51. </el-table-column>
  52. <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right">
  53. <template slot-scope="scope">
  54. <el-button size="mini" type="text" icon="el-icon-view" class="info-btn view-btn" @click="handleView(scope.row)"
  55. v-hasPermi="['sys:vetAudit:view']">详情</el-button>
  56. <el-button v-if="scope.row.auditStatus != 1" size="mini" type="text" icon="el-icon-check" style="color: #072eed"
  57. class="info-btn audit-btn" @click="handleAudit(scope.row)"
  58. v-hasPermi="['sys:vetAudit:auditVetPersonalInfo']">审核</el-button>
  59. </template>
  60. </el-table-column>
  61. </el-table>
  62. <div class="pagestyle">
  63. <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
  64. @pagination="getList" />
  65. </div>
  66. <!-- 审核对话框 -->
  67. <el-dialog :title="auditTitle" :visible.sync="auditVisible" width="80%" append-to-body :close-on-click-modal="false"
  68. class="audit-dialog" @close="closeAuditDialog" :destroy-on-close="true">
  69. <el-tabs v-model="activeAuditTab" type="border-card">
  70. <!-- 基本信息审核标签页 -->
  71. <el-tab-pane label="基本信息审核" name="basic">
  72. <div v-if="loadingBasicData" class="tab-loading">
  73. <i class="el-icon-loading"></i>
  74. <div>加载基本信息...</div>
  75. </div>
  76. <div v-else>
  77. <div class="info-section">
  78. <h3>兽医基本信息</h3>
  79. <el-descriptions :column="2" border>
  80. <el-descriptions-item label="真实姓名">{{ basicInfo.realName || '-' }}</el-descriptions-item>
  81. <el-descriptions-item label="性别">{{ basicInfo.gender || '-' }}</el-descriptions-item>
  82. <el-descriptions-item label="出生日期">{{ parseTime(basicInfo.birthday, '{y}-{m}-{d}') || '-'
  83. }}</el-descriptions-item>
  84. <el-descriptions-item label="身份证号">{{ basicInfo.idCard || '-' }}</el-descriptions-item>
  85. <el-descriptions-item label="职称">{{ basicInfo.title || '-' }}</el-descriptions-item>
  86. <el-descriptions-item label="联系电话">{{ basicInfo.phone || '-' }}</el-descriptions-item>
  87. <el-descriptions-item label="电子邮箱">{{ basicInfo.email || '-' }}</el-descriptions-item>
  88. <el-descriptions-item label="专家类型">
  89. <dict-tag :options="dict.type.expert_type" :value="basicInfo.expertType" />
  90. </el-descriptions-item>
  91. <el-descriptions-item label="所属医院">{{ basicInfo.hospital || '-' }}</el-descriptions-item>
  92. <el-descriptions-item label="联系地址" :span="2">{{ basicInfo.address || '-' }}</el-descriptions-item>
  93. <el-descriptions-item label="个人简介" :span="2">{{ basicInfo.introduction || '-' }}</el-descriptions-item>
  94. <el-descriptions-item label="当前审核状态">
  95. <dict-tag :options="dict.type.audit_status" :value="basicInfo.auditStatus" />
  96. </el-descriptions-item>
  97. <el-descriptions-item label="审核人">{{ basicInfo.auditor || '-' }}</el-descriptions-item>
  98. <el-descriptions-item label="审核时间">{{ parseTime(basicInfo.auditTime) || '-' }}</el-descriptions-item>
  99. <el-descriptions-item label="审核意见" :span="2">{{ basicInfo.auditDesc || '-' }}</el-descriptions-item>
  100. </el-descriptions>
  101. </div>
  102. <div class="audit-form-section">
  103. <h3>基本信息审核</h3>
  104. <el-form ref="basicAuditForm" :model="basicAuditForm" :rules="basicAuditRules" label-width="100px">
  105. <el-form-item label="审核结果" prop="auditStatus">
  106. <el-radio-group v-model="basicAuditForm.auditStatus" @change="handleBasicAuditChange">
  107. <el-radio label="1">审核通过</el-radio>
  108. <el-radio label="2">审核不通过</el-radio>
  109. </el-radio-group>
  110. <span v-if="basicInfo.auditStatus && basicInfo.auditStatus !== '0'" class="audit-status-tag">
  111. <dict-tag :options="dict.type.audit_status" :value="basicInfo.auditStatus" />
  112. </span>
  113. </el-form-item>
  114. <el-form-item label="审核意见" prop="auditDesc" v-if="basicAuditForm.auditStatus === '2'">
  115. <el-input v-model="basicAuditForm.auditDesc" type="textarea" :rows="4" placeholder="请输入审核意见"
  116. :maxlength="500" show-word-limit />
  117. </el-form-item>
  118. </el-form>
  119. </div>
  120. </div>
  121. </el-tab-pane>
  122. <!-- 资质证书审核标签页 - 所有证书统一审核 -->
  123. <el-tab-pane label="资质证书审核" name="certificate">
  124. <div v-if="loadingCertData" class="tab-loading">
  125. <i class="el-icon-loading"></i>
  126. <div>加载证书信息...</div>
  127. </div>
  128. <div v-else>
  129. <div v-if="!certificateList || certificateList.length === 0" class="no-data">
  130. <el-alert title="该兽医暂无资质证书" type="info" :closable="false" show-icon />
  131. </div>
  132. <div v-else>
  133. <div class="cert-summary">
  134. <el-alert :title="`共 ${certificateList.length} 个资质证书待审核`" type="info" :closable="false" show-icon />
  135. </div>
  136. <!-- 证书列表展示 -->
  137. <div class="cert-list-section">
  138. <h3>证书列表</h3>
  139. <div v-for="(cert, index) in certificateList" :key="cert.qualificationId + '_' + index"
  140. class="certificate-item-wrapper">
  141. <div class="certificate-item" :class="{ 'cert-audited': cert.auditStatus && cert.auditStatus !== '0' }">
  142. <div class="cert-header">
  143. <div class="cert-title">
  144. <span>{{ cert.certName || cert.qualificationType || '未命名证书' }}</span>
  145. <dict-tag v-if="cert.auditStatus && cert.auditStatus !== '0'" :options="dict.type.audit_status"
  146. :value="cert.auditStatus" class="cert-status-tag" />
  147. <el-tag v-else type="warning" size="small" class="cert-status-tag" effect="dark">
  148. <i class="el-icon-time"></i> 待审核
  149. </el-tag>
  150. </div>
  151. <div class="cert-info">
  152. <span><strong>证书编号</strong>{{ cert.certificateNo || '-' }}</span>
  153. <span><strong>发证机构</strong>{{ cert.issueOrg || '-' }}</span>
  154. <span><strong>资质类型</strong>
  155. <dict-tag :options="dict.type.qualification_type" :value="cert.qualificationType" />
  156. </span>
  157. <span><strong>经营范围</strong>
  158. <div class="jyfw">
  159. <dict-tag style="display: grid;grid-template-columns: 1fr 1fr; row-gap: 5px;"
  160. :options="dict.type.scope_names" :value="cert.scopeIds" />
  161. </div>
  162. </span>
  163. <span><strong>颁发日期</strong>{{ parseTime(cert.issueDate, '{y}-{m}-{d}') || '-' }}</span>
  164. <span><strong>到期日期</strong>{{ parseTime(cert.expireDate, '{y}-{m}-{d}') || '-' }}</span>
  165. </div>
  166. <div v-if="cert.certImage" class="cert-image-section">
  167. <div class="image-title">
  168. <strong>证书图片</strong>
  169. </div>
  170. <div class="image-preview">
  171. <el-image style="width: 200px; height: 150px;" :src="baseUrl + cert.certImage"
  172. :preview-src-list="[baseUrl + cert.certImage]" fit="contain">
  173. <div slot="error" class="image-slot">
  174. <i class="el-icon-picture-outline"></i>
  175. <div>图片加载失败</div>
  176. </div>
  177. </el-image>
  178. </div>
  179. </div>
  180. <div v-if="cert.certificateFiles" class="cert-files-section">
  181. <div class="files-title">
  182. <strong>其他附件</strong>
  183. </div>
  184. <div class="files-list">
  185. <el-button type="text" icon="el-icon-download" @click="downloadFile(cert.certificateFiles)"
  186. class="download-btn">
  187. {{ getFileName(cert.certificateFiles) }}
  188. </el-button>
  189. </div>
  190. </div>
  191. </div>
  192. </div>
  193. </div>
  194. </div>
  195. <!-- 统一审核表单 -->
  196. <div class="cert-unified-audit-section">
  197. <div class="audit-form-section">
  198. <h3>资质证书统一审核</h3>
  199. <el-form ref="certAuditForm" :model="certAuditForm" :rules="certAuditRules" label-width="100px">
  200. <el-form-item label="审核结果" prop="auditStatus">
  201. <el-radio-group v-model="certAuditForm.auditStatus" @change="handleCertAuditChange">
  202. <el-radio label="1">全部审核通过</el-radio>
  203. <el-radio label="2">审核不通过</el-radio>
  204. </el-radio-group>
  205. </el-form-item>
  206. <el-form-item label="审核意见" prop="auditOpinion" v-if="certAuditForm.auditStatus === '2'">
  207. <el-input v-model="certAuditForm.auditOpinion" type="textarea" :rows="4"
  208. placeholder="请输入审核意见(将应用于所有证书)" :maxlength="500" show-word-limit />
  209. </el-form-item>
  210. </el-form>
  211. </div>
  212. </div>
  213. </div>
  214. </div>
  215. </el-tab-pane>
  216. </el-tabs>
  217. <div slot="footer" class="dialog-footer">
  218. <el-button type="primary" @click="submitAllAudit" :loading="submittingAll" :disabled="!isAllAuditSelected || submittingAll">
  219. {{ submittingAll ? '提交中...' : '提交审核' }}
  220. </el-button>
  221. <el-button @click="auditVisible = false"> </el-button>
  222. </div>
  223. </el-dialog>
  224. <!-- 详情对话框 -->
  225. <el-dialog :title="detailTitle" :visible.sync="detailVisible" width="80%" append-to-body :destroy-on-close="true">
  226. <div v-if="loadingDetail" class="tab-loading">
  227. <i class="el-icon-loading"></i>
  228. <div>加载详情信息...</div>
  229. </div>
  230. <div v-else-if="detailData">
  231. <el-tabs v-model="detailActiveTab" type="border-card">
  232. <el-tab-pane label="基本信息" name="basicInfo">
  233. <div class="info-section">
  234. <h3>兽医详细信息</h3>
  235. <el-descriptions :column="2" border>
  236. <el-descriptions-item label="用户昵称">{{ detailData.nickName || '-' }}</el-descriptions-item>
  237. <el-descriptions-item label="真实姓名">{{ detailData.realName || '-' }}</el-descriptions-item>
  238. <el-descriptions-item label="性别">{{ detailData.gender || '-' }}</el-descriptions-item>
  239. <el-descriptions-item label="出生日期">{{ parseTime(detailData.birthday, '{y}-{m}-{d}') || '-'
  240. }}</el-descriptions-item>
  241. <el-descriptions-item label="身份证号">{{ detailData.idCard || '-' }}</el-descriptions-item>
  242. <el-descriptions-item label="职称">{{ detailData.title || '-' }}</el-descriptions-item>
  243. <el-descriptions-item label="联系电话">{{ detailData.phone || '-' }}</el-descriptions-item>
  244. <el-descriptions-item label="电子邮箱">{{ detailData.email || '-' }}</el-descriptions-item>
  245. <el-descriptions-item label="专家类型">
  246. <dict-tag :options="dict.type.expert_type" :value="detailData.expertType" />
  247. </el-descriptions-item>
  248. <el-descriptions-item label="所属医院">{{ detailData.hospital || '-' }}</el-descriptions-item>
  249. <el-descriptions-item label="联系地址">{{ detailData.address || '-' }}</el-descriptions-item>
  250. <el-descriptions-item label="擅长领域">{{ detailData.specialty || '-' }}</el-descriptions-item>
  251. <el-descriptions-item label="工作经验">{{ detailData.workExperience || '-' }}</el-descriptions-item>
  252. <el-descriptions-item label="个人简介" :span="2">{{ detailData.introduction || '-' }}</el-descriptions-item>
  253. <el-descriptions-item label="当前审核状态">
  254. <dict-tag :options="dict.type.audit_status" :value="detailData.auditStatus" />
  255. </el-descriptions-item>
  256. <el-descriptions-item label="审核人">{{ detailData.auditor || '-' }}</el-descriptions-item>
  257. <el-descriptions-item label="审核时间">{{ parseTime(detailData.auditTime) || '-' }}</el-descriptions-item>
  258. <el-descriptions-item label="审核意见" :span="2">{{ detailData.auditDesc || '-' }}</el-descriptions-item>
  259. </el-descriptions>
  260. </div>
  261. </el-tab-pane>
  262. <el-tab-pane label="资质证书" name="certificates" v-if="detailCertificates && detailCertificates.length > 0">
  263. <div class="certificate-section">
  264. <h3>资质证书列表 {{ detailCertificates.length }} 个证书</h3>
  265. <div v-for="(cert, index) in detailCertificates" :key="cert.qualificationId + '_detail_' + index"
  266. class="certificate-item-wrapper">
  267. <div class="certificate-item view-only">
  268. <div class="cert-header">
  269. <div class="cert-title">
  270. <span>{{ cert.certName || cert.qualificationType || `证书${index + 1}` }}</span>
  271. <dict-tag :options="dict.type.audit_status" :value="cert.auditStatus" class="cert-status-tag" />
  272. </div>
  273. <div class="cert-info">
  274. <span><strong>证书编号</strong>{{ cert.certificateNo || '-' }}</span>
  275. <span><strong>发证机构</strong>{{ cert.issueOrg || '-' }}</span>
  276. <span><strong>资质类型</strong>
  277. <dict-tag :options="dict.type.qualification_type" :value="cert.qualificationType" />
  278. </span>
  279. <span><strong>经营范围</strong>
  280. <div class="jyfw">
  281. <dict-tag style="display: grid;grid-template-columns: 1fr 1fr; row-gap: 5px;"
  282. :options="dict.type.scope_names" :value="cert.scopeIds" />
  283. </div>
  284. </span>
  285. <span><strong>颁发日期</strong>{{ parseTime(cert.issueDate, '{y}-{m}-{d}') || '-' }}</span>
  286. <span><strong>到期日期</strong>{{ parseTime(cert.expireDate, '{y}-{m}-{d}') || '-' }}</span>
  287. <span><strong>审核意见</strong>{{ cert.auditOpinion || '-' }}</span>
  288. <span><strong>审核时间</strong>{{ parseTime(cert.auditTime) || '-' }}</span>
  289. </div>
  290. <div v-if="cert.certImage" class="cert-image-section">
  291. <div class="image-title">
  292. <strong>证书图片</strong>
  293. </div>
  294. <div class="image-preview">
  295. <el-image style="width: 200px; height: 150px;" :src="baseUrl + cert.certImage"
  296. :preview-src-list="[baseUrl + cert.certImage]" fit="contain">
  297. <div slot="error" class="image-slot">
  298. <i class="el-icon-picture-outline"></i>
  299. <div>图片加载失败</div>
  300. </div>
  301. </el-image>
  302. </div>
  303. </div>
  304. <div v-if="cert.certificateFiles" class="cert-files-section">
  305. <div class="files-title">
  306. <strong>其他附件</strong>
  307. </div>
  308. <div class="files-list">
  309. <el-button type="text" icon="el-icon-download" @click="downloadFile(cert.certificateFiles)"
  310. class="download-btn">
  311. {{ getFileName(cert.certificateFiles) }}
  312. </el-button>
  313. </div>
  314. </div>
  315. </div>
  316. </div>
  317. </div>
  318. </div>
  319. </el-tab-pane>
  320. </el-tabs>
  321. </div>
  322. <span slot="footer" class="dialog-footer">
  323. <el-button @click="detailVisible = false"> </el-button>
  324. </span>
  325. </el-dialog>
  326. </div>
  327. </template>
  328. <script>
  329. import { listInfo, getInfo, auditBasicInfo, auditCertificate, listQualification, delvetInfo } from "@/api/system/vetAduit"
  330. import axios from 'axios'
  331. export default {
  332. name: "Info",
  333. dicts: ['expert_type', 'audit_status', 'qualification_type', 'scope_names'],
  334. data() {
  335. return {
  336. loading: true,
  337. ids: [],
  338. single: true,
  339. multiple: true,
  340. showSearch: true,
  341. total: 0,
  342. infoList: [],
  343. baseUrl: process.env.VUE_APP_BASE_API,
  344. detailTitle: "",
  345. detailVisible: false,
  346. detailData: null,
  347. loadingDetail: false,
  348. detailActiveTab: "basicInfo",
  349. detailCertificates: [],
  350. auditTitle: "",
  351. auditVisible: false,
  352. activeAuditTab: "basic",
  353. currentVetId: null,
  354. currentUserId: null,
  355. loadingBasicData: false,
  356. loadingCertData: false,
  357. submittingAll: false,
  358. basicInfo: {},
  359. basicAuditForm: {
  360. auditStatus: "",
  361. auditDesc: ""
  362. },
  363. basicAuditRules: {
  364. auditStatus: [
  365. { required: true, message: "请选择审核结果", trigger: "change" }
  366. ]
  367. },
  368. certificateList: [],
  369. // 统一证书审核表单
  370. certAuditForm: {
  371. auditStatus: "",
  372. auditOpinion: ""
  373. },
  374. certAuditRules: {
  375. auditStatus: [
  376. { required: true, message: "请选择证书审核结果", trigger: "change" }
  377. ]
  378. },
  379. queryParams: {
  380. pageNum: 1,
  381. pageSize: 10,
  382. realName: null,
  383. gender: null,
  384. idCard: null,
  385. workExperience: null,
  386. hospital: null,
  387. },
  388. title: "",
  389. open: false,
  390. form: {},
  391. rules: {
  392. realName: [
  393. { required: true, message: "真实姓名不能为空", trigger: "blur" }
  394. ],
  395. gender: [
  396. { required: true, message: "性别不能为空", trigger: "change" }
  397. ],
  398. idCard: [
  399. { required: true, message: "身份证号不能为空", trigger: "blur" }
  400. ]
  401. }
  402. }
  403. },
  404. computed: {
  405. // 判断是否所有审核项都已选择审核结果
  406. isAllAuditSelected() {
  407. // 基本信息未选择审核结果(审核状态必须是1或2)
  408. if (!this.basicAuditForm.auditStatus ||
  409. (this.basicAuditForm.auditStatus !== '1' && this.basicAuditForm.auditStatus !== '2')) {
  410. return false
  411. }
  412. // 如果有证书,需要统一证书审核已选择结果
  413. if (this.certificateList && this.certificateList.length > 0) {
  414. if (!this.certAuditForm.auditStatus ||
  415. (this.certAuditForm.auditStatus !== '1' && this.certAuditForm.auditStatus !== '2')) {
  416. return false
  417. }
  418. }
  419. return true
  420. }
  421. },
  422. created() {
  423. this.getList()
  424. },
  425. methods: {
  426. getList() {
  427. this.loading = true
  428. listInfo(this.queryParams).then(response => {
  429. this.infoList = response.rows
  430. this.total = response.total
  431. this.loading = false
  432. }).catch(() => {
  433. this.loading = false
  434. })
  435. },
  436. handleAudit(row) {
  437. // 防止在提交过程中再次打开审核对话框
  438. if (this.submittingAll) {
  439. this.$message.warning('正在提交审核,请稍后...')
  440. return
  441. }
  442. this.resetAuditData()
  443. const id = row.id || this.ids[0]
  444. const vetName = row.realName || ''
  445. this.currentVetId = id
  446. this.auditTitle = `兽医信息审核 - ${vetName}`
  447. this.auditVisible = true
  448. this.activeAuditTab = "basic"
  449. // 同时加载基本信息和证书数据
  450. this.loadAllData(id)
  451. },
  452. // 同时加载基本信息和证书数据
  453. async loadAllData(vetId) {
  454. this.loadingBasicData = true
  455. this.loadingCertData = true
  456. try {
  457. // 加载基本信息
  458. const basicResponse = await getInfo(vetId)
  459. if (basicResponse.code === 200) {
  460. this.basicInfo = basicResponse.data || {}
  461. this.currentUserId = this.basicInfo.userId
  462. // 如果基本信息已经有审核状态(1或2),则回显
  463. if (this.basicInfo.auditStatus && this.basicInfo.auditStatus !== '0') {
  464. this.basicAuditForm = {
  465. auditStatus: this.basicInfo.auditStatus,
  466. auditDesc: this.basicInfo.auditDesc || ""
  467. }
  468. } else {
  469. this.basicAuditForm = {
  470. auditStatus: "",
  471. auditDesc: ""
  472. }
  473. }
  474. } else {
  475. this.$message.error(basicResponse.msg || '加载基本信息失败')
  476. }
  477. // 加载证书数据
  478. if (this.currentUserId) {
  479. const queryParams = {
  480. userId: this.currentUserId,
  481. pageNum: 1,
  482. pageSize: 1000
  483. }
  484. const certResponse = await listQualification(queryParams)
  485. if (certResponse.code === 200) {
  486. this.certificateList = certResponse.rows || []
  487. // 检查所有证书的审核状态是否一致
  488. const allCertAudited = this.certificateList.every(cert =>
  489. cert.auditStatus && cert.auditStatus !== '0'
  490. )
  491. const allCertSameStatus = this.certificateList.length > 0 &&
  492. this.certificateList.every(cert => cert.auditStatus === this.certificateList[0].auditStatus)
  493. // 如果所有证书都已审核且状态一致,则回显
  494. if (allCertAudited && allCertSameStatus && this.certificateList.length > 0) {
  495. this.certAuditForm = {
  496. auditStatus: this.certificateList[0].auditStatus,
  497. auditOpinion: this.certificateList[0].auditOpinion || ""
  498. }
  499. } else {
  500. this.certAuditForm = {
  501. auditStatus: "",
  502. auditOpinion: ""
  503. }
  504. }
  505. } else {
  506. this.$message.error(certResponse.msg || '加载证书列表失败')
  507. }
  508. }
  509. } catch (error) {
  510. console.error('加载数据失败:', error)
  511. this.$message.error('加载数据失败')
  512. } finally {
  513. this.loadingBasicData = false
  514. this.loadingCertData = false
  515. }
  516. },
  517. handleBasicAuditChange(e) {
  518. console.log(e);
  519. // 如果选择审核通过,清空审核意见
  520. if (this.basicAuditForm.auditStatus === '1') {
  521. this.basicAuditForm.auditDesc = ''
  522. }
  523. },
  524. handleCertAuditChange(e) {
  525. console.log(e);
  526. // 如果选择审核通过,清空审核意见
  527. if (this.certAuditForm.auditStatus === '1') {
  528. this.certAuditForm.auditOpinion = ''
  529. }
  530. },
  531. // 提交所有审核
  532. async submitAllAudit() {
  533. // 防止重复提交
  534. if (this.submittingAll) {
  535. // this.$message.warning('数据正在处理中,请勿重复提交')
  536. return
  537. }
  538. // 验证基本信息审核结果
  539. if (!this.basicAuditForm.auditStatus ||
  540. (this.basicAuditForm.auditStatus !== '1' && this.basicAuditForm.auditStatus !== '2')) {
  541. this.$message.warning('请选择基本信息审核结果')
  542. this.activeAuditTab = 'basic'
  543. return
  544. }
  545. // 验证基本信息审核意见(不通过时必须填写)
  546. if (this.basicAuditForm.auditStatus === '2' && (!this.basicAuditForm.auditDesc || this.basicAuditForm.auditDesc.trim() === '')) {
  547. this.$message.warning('审核不通过时,请填写审核意见')
  548. this.activeAuditTab = 'basic'
  549. return
  550. }
  551. // 验证证书统一审核
  552. if (this.certificateList && this.certificateList.length > 0) {
  553. if (!this.certAuditForm.auditStatus ||
  554. (this.certAuditForm.auditStatus !== '1' && this.certAuditForm.auditStatus !== '2')) {
  555. this.$message.warning('请选择证书审核结果')
  556. this.activeAuditTab = 'certificate'
  557. return
  558. }
  559. // 审核不通过时必须填写审核意见
  560. if (this.certAuditForm.auditStatus === '2' && (!this.certAuditForm.auditOpinion || this.certAuditForm.auditOpinion.trim() === '')) {
  561. this.$message.warning('证书审核不通过时,请填写审核意见')
  562. this.activeAuditTab = 'certificate'
  563. return
  564. }
  565. }
  566. this.submittingAll = true
  567. try {
  568. // 提交基本信息审核
  569. const basicAuditData = {
  570. id: this.currentVetId,
  571. auditStatus: this.basicAuditForm.auditStatus,
  572. auditDesc: this.basicAuditForm.auditDesc || ''
  573. }
  574. const basicResult = await auditBasicInfo(basicAuditData)
  575. if (basicResult.code !== 200) {
  576. throw new Error(basicResult.msg || '基本信息审核提交失败')
  577. }
  578. // 提交证书审核(如果有证书)
  579. if (this.certificateList && this.certificateList.length > 0) {
  580. // 使用 Promise.all 并发提交所有证书审核
  581. const certPromises = this.certificateList.map(cert => {
  582. const certAuditData = {
  583. qualificationId: cert.qualificationId,
  584. auditStatus: this.certAuditForm.auditStatus,
  585. auditOpinion: this.certAuditForm.auditOpinion || ''
  586. }
  587. return auditCertificate(certAuditData)
  588. })
  589. const certResults = await Promise.all(certPromises)
  590. // 检查是否有失败的证书审核
  591. const failedCert = certResults.find(result => result.code !== 200)
  592. if (failedCert) {
  593. throw new Error(failedCert.msg || '部分证书审核提交失败')
  594. }
  595. }
  596. // 所有审核提交成功
  597. this.$modal.msgSuccess('审核提交成功')
  598. this.auditVisible = false
  599. this.getList()
  600. } catch (error) {
  601. console.error('提交失败:', error)
  602. this.$modal.msgError(error.message || '提交失败')
  603. } finally {
  604. this.submittingAll = false
  605. }
  606. },
  607. resetAuditData() {
  608. this.basicInfo = {}
  609. this.basicAuditForm = {
  610. auditStatus: "",
  611. auditDesc: ""
  612. }
  613. this.certificateList = []
  614. this.certAuditForm = {
  615. auditStatus: "",
  616. auditOpinion: ""
  617. }
  618. this.currentVetId = null
  619. this.currentUserId = null
  620. this.loadingBasicData = false
  621. this.loadingCertData = false
  622. },
  623. closeAuditDialog() {
  624. // 如果正在提交,不允许关闭
  625. if (this.submittingAll) {
  626. this.$message.warning('正在提交审核,请稍后...')
  627. return
  628. }
  629. this.resetAuditData()
  630. },
  631. handleView(row) {
  632. // 防止在提交过程中打开详情
  633. if (this.submittingAll) {
  634. this.$message.warning('请等待当前操作完成')
  635. return
  636. }
  637. this.detailVisible = true
  638. this.detailTitle = "兽医详情信息"
  639. this.loadingDetail = true
  640. this.detailActiveTab = "basicInfo"
  641. this.detailCertificates = []
  642. const id = row.id
  643. getInfo(id).then(response => {
  644. if (response.code === 200) {
  645. this.detailData = response.data
  646. if (response.data.userId) {
  647. const queryParams = {
  648. userId: response.data.userId,
  649. pageNum: 1,
  650. pageSize: 1000
  651. }
  652. listQualification(queryParams).then(certResponse => {
  653. if (certResponse.code === 200) {
  654. this.detailCertificates = certResponse.rows || []
  655. }
  656. }).catch(error => {
  657. console.error('加载证书详情失败:', error)
  658. }).finally(() => {
  659. this.loadingDetail = false
  660. })
  661. } else {
  662. this.loadingDetail = false
  663. }
  664. } else {
  665. this.$message.error(response.msg || '获取详情失败')
  666. this.loadingDetail = false
  667. }
  668. }).catch(error => {
  669. console.error('获取详情失败:', error)
  670. this.$message.error('获取详情失败')
  671. this.loadingDetail = false
  672. })
  673. },
  674. handleQuery() {
  675. this.queryParams.pageNum = 1
  676. this.getList()
  677. },
  678. resetQuery() {
  679. this.resetForm("queryForm")
  680. this.handleQuery()
  681. },
  682. handleSelectionChange(selection) {
  683. this.ids = selection.map(item => item.id)
  684. this.single = selection.length !== 1
  685. this.multiple = !selection.length
  686. },
  687. handleAdd() {
  688. this.reset()
  689. this.open = true
  690. this.title = "添加兽医个人信息"
  691. },
  692. handleUpdate(row) {
  693. this.reset()
  694. const id = row.id || this.ids[0]
  695. getInfo(id).then(response => {
  696. if (response.code === 200) {
  697. this.form = response.data
  698. this.open = true
  699. this.title = "修改兽医个人信息"
  700. }
  701. })
  702. },
  703. submitForm() {
  704. this.$refs["form"].validate(valid => {
  705. if (valid) {
  706. const api = this.form.id ?
  707. (data) => this.$axios.put('/system/vetInfo', data) :
  708. (data) => this.$axios.post('/system/vetInfo', data)
  709. api(this.form).then(response => {
  710. this.$modal.msgSuccess(this.form.id ? "修改成功" : "新增成功")
  711. this.open = false
  712. this.getList()
  713. }).catch(error => {
  714. this.$modal.msgError("操作失败")
  715. })
  716. }
  717. })
  718. },
  719. // 删除
  720. handleDelete(row) {
  721. const ids = row.id || this.ids
  722. this.$modal.confirm('是否确认删除兽医个人信息编号为"' + ids + '"的数据项?').then(() => {
  723. return delvetInfo(ids)
  724. }).then(() => {
  725. this.getList()
  726. this.$modal.msgSuccess("删除成功")
  727. }).catch(() => { })
  728. },
  729. getAuditStatusText(status) {
  730. if (!status || status === 'null' || status === 'undefined') {
  731. return '未审核'
  732. }
  733. const statusMap = {
  734. '0': '待审核',
  735. '1': '审核通过',
  736. '2': '审核不通过'
  737. }
  738. return statusMap[status] || '未审核'
  739. },
  740. getAuditStatusTagType(status) {
  741. if (!status || status === 'null' || status === 'undefined') {
  742. return 'info'
  743. }
  744. const typeMap = {
  745. '0': 'warning',
  746. '1': 'success',
  747. '2': 'danger'
  748. }
  749. return typeMap[status] || 'info'
  750. },
  751. cancel() {
  752. this.open = false
  753. this.reset()
  754. },
  755. closeDetail() {
  756. this.detailVisible = false
  757. this.detailData = null
  758. this.detailCertificates = []
  759. },
  760. reset() {
  761. this.form = {
  762. id: null,
  763. userId: null,
  764. realName: null,
  765. gender: null,
  766. birthday: null,
  767. idCard: null,
  768. specialty: null,
  769. workExperience: null,
  770. hospital: null,
  771. address: null,
  772. introduction: null,
  773. title: null,
  774. phone: null,
  775. email: null,
  776. expertType: null,
  777. }
  778. this.resetForm("form")
  779. },
  780. // 文件下载方法
  781. downloadFile(filePath) {
  782. if (!filePath) {
  783. this.$message.warning('文件路径为空,无法下载')
  784. return
  785. }
  786. const loading = this.$loading({
  787. lock: true,
  788. text: '正在下载文件...',
  789. spinner: 'el-icon-loading',
  790. background: 'rgba(0, 0, 0, 0.7)'
  791. })
  792. const fullUrl = this.baseUrl + filePath
  793. axios({
  794. method: 'get',
  795. url: fullUrl,
  796. responseType: 'blob',
  797. headers: {
  798. }
  799. }).then(response => {
  800. const blob = new Blob([response.data])
  801. const downloadUrl = window.URL.createObjectURL(blob)
  802. const a = document.createElement('a')
  803. a.download = this.getFileName(filePath)
  804. a.href = downloadUrl
  805. document.body.appendChild(a)
  806. a.click()
  807. window.URL.revokeObjectURL(downloadUrl)
  808. document.body.removeChild(a)
  809. this.$message.success('文件下载成功')
  810. }).catch(error => {
  811. console.error('文件下载失败:', error)
  812. this.$message.error('文件下载失败,请重试')
  813. }).finally(() => {
  814. loading.close()
  815. })
  816. },
  817. getFileName(filePath) {
  818. if (!filePath) return '未知文件'
  819. const parts = filePath.split(/[\\/]/)
  820. const fileName = parts.pop() || '未知文件'
  821. return fileName
  822. }
  823. }
  824. }
  825. </script>
  826. <style>
  827. .el-tooltip__popper {
  828. width: 300px !important;
  829. }
  830. </style>
  831. <style scoped>
  832. ::v-deep .pagestyle .el-input {
  833. width: auto !important;
  834. }
  835. ::v-deep .jyfw .el-tag:nth-child(1) {
  836. width: 70px !important;
  837. margin-left: 10px;
  838. }
  839. </style>
  840. <style scoped lang="scss">
  841. .audit-dialog {
  842. :deep(.el-dialog__body) {
  843. max-height: 70vh;
  844. overflow-y: auto;
  845. padding: 20px;
  846. }
  847. }
  848. .certificate-item-wrapper {
  849. margin-bottom: 20px;
  850. &:last-child {
  851. margin-bottom: 0;
  852. }
  853. }
  854. .tab-loading {
  855. text-align: center;
  856. padding: 60px 0;
  857. .el-icon-loading {
  858. font-size: 32px;
  859. color: #409EFF;
  860. margin-bottom: 15px;
  861. }
  862. div {
  863. color: #606266;
  864. font-size: 14px;
  865. }
  866. }
  867. .info-section {
  868. margin-bottom: 25px;
  869. h3 {
  870. margin-bottom: 15px;
  871. color: #303133;
  872. font-size: 16px;
  873. font-weight: 600;
  874. padding-bottom: 8px;
  875. border-bottom: 2px solid #409EFF;
  876. }
  877. }
  878. .audit-form-section {
  879. margin-top: 20px;
  880. h3 {
  881. margin-bottom: 15px;
  882. color: #303133;
  883. font-size: 16px;
  884. font-weight: 600;
  885. }
  886. }
  887. .audit-status-tag {
  888. margin-left: 15px;
  889. .el-tag {
  890. font-weight: 500;
  891. }
  892. }
  893. .cert-list-section {
  894. margin-top: 30px;
  895. h3 {
  896. margin-bottom: 15px;
  897. color: #303133;
  898. font-size: 16px;
  899. font-weight: 600;
  900. padding-bottom: 8px;
  901. border-bottom: 2px solid #67C23A;
  902. }
  903. }
  904. .cert-unified-audit-section {
  905. margin-top: 20px;
  906. padding: 20px;
  907. background-color: #f8f9fa;
  908. border-radius: 8px;
  909. border: 1px solid #e4e7ed;
  910. }
  911. .certificate-section {
  912. h3 {
  913. margin-bottom: 15px;
  914. color: #303133;
  915. font-size: 16px;
  916. font-weight: 600;
  917. padding-bottom: 8px;
  918. border-bottom: 2px solid #67C23A;
  919. }
  920. }
  921. .certificate-item {
  922. border: 1px solid #ebeef5;
  923. border-radius: 6px;
  924. padding: 20px;
  925. background-color: #fff;
  926. transition: all 0.3s ease;
  927. &:hover {
  928. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  929. }
  930. &.cert-audited {
  931. background-color: #f8f9fa;
  932. border-left: 4px solid #67C23A;
  933. }
  934. &.view-only {
  935. background-color: #fafafa;
  936. &:hover {
  937. box-shadow: none;
  938. }
  939. }
  940. }
  941. .cert-header {
  942. margin-bottom: 20px;
  943. padding-bottom: 15px;
  944. border-bottom: 1px dashed #ebeef5;
  945. }
  946. .cert-title {
  947. display: flex;
  948. align-items: center;
  949. gap: 10px;
  950. margin-bottom: 15px;
  951. span {
  952. font-size: 13px;
  953. font-weight: 600;
  954. color: #303133;
  955. }
  956. }
  957. .cert-info {
  958. display: grid;
  959. grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  960. gap: 12px;
  961. font-size: 14px;
  962. color: #606266;
  963. margin-bottom: 15px;
  964. span {
  965. display: flex;
  966. align-items: center;
  967. strong {
  968. min-width: 80px;
  969. color: #303133;
  970. font-weight: 500;
  971. margin-right: 8px;
  972. }
  973. .el-tag {
  974. margin-left: 5px;
  975. }
  976. }
  977. }
  978. .cert-image-section {
  979. margin-top: 15px;
  980. padding: 12px;
  981. background-color: #f8f9fa;
  982. border-radius: 6px;
  983. border: 1px solid #e4e7ed;
  984. .image-title {
  985. margin-bottom: 10px;
  986. font-size: 14px;
  987. color: #303133;
  988. }
  989. .image-preview {
  990. .el-image {
  991. border: 1px solid #dcdfe6;
  992. border-radius: 4px;
  993. overflow: hidden;
  994. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  995. .image-slot {
  996. display: flex;
  997. flex-direction: column;
  998. align-items: center;
  999. justify-content: center;
  1000. width: 100%;
  1001. height: 100%;
  1002. background: #f5f7fa;
  1003. color: #909399;
  1004. i {
  1005. font-size: 30px;
  1006. margin-bottom: 5px;
  1007. }
  1008. div {
  1009. font-size: 12px;
  1010. }
  1011. }
  1012. }
  1013. }
  1014. }
  1015. .cert-files-section {
  1016. margin-top: 15px;
  1017. padding: 12px;
  1018. background-color: #f8f9fa;
  1019. border-radius: 6px;
  1020. border: 1px solid #e4e7ed;
  1021. .files-title {
  1022. margin-bottom: 10px;
  1023. font-size: 14px;
  1024. color: #303133;
  1025. }
  1026. .files-list {
  1027. color: #606266;
  1028. font-size: 13px;
  1029. .download-btn {
  1030. color: #409EFF;
  1031. padding: 0;
  1032. height: auto;
  1033. line-height: 1;
  1034. &:hover {
  1035. color: #66b1ff;
  1036. text-decoration: underline;
  1037. }
  1038. i {
  1039. margin-right: 5px;
  1040. font-size: 12px;
  1041. }
  1042. }
  1043. }
  1044. }
  1045. .cert-summary {
  1046. margin-bottom: 20px;
  1047. .el-alert {
  1048. border-radius: 6px;
  1049. }
  1050. }
  1051. .no-data {
  1052. text-align: center;
  1053. padding: 60px 0;
  1054. .el-alert {
  1055. max-width: 400px;
  1056. margin: 0 auto;
  1057. }
  1058. }
  1059. :deep(.dialog-footer) {
  1060. .el-button {
  1061. min-width: 100px;
  1062. &:last-child {
  1063. margin-left: 10px;
  1064. }
  1065. }
  1066. }
  1067. :deep(.el-tabs__header) {
  1068. margin-bottom: 0;
  1069. }
  1070. :deep(.el-tabs__item) {
  1071. font-weight: 500;
  1072. &.is-active {
  1073. color: #409EFF;
  1074. font-weight: 600;
  1075. }
  1076. }
  1077. :deep(.el-tag) {
  1078. &.el-tag--success {
  1079. background-color: rgba(103, 194, 58, 0.1);
  1080. border-color: rgba(103, 194, 58, 0.2);
  1081. }
  1082. &.el-tag--danger {
  1083. background-color: rgba(245, 108, 108, 0.1);
  1084. border-color: rgba(245, 108, 108, 0.2);
  1085. }
  1086. &.el-tag--warning {
  1087. background-color: rgba(230, 162, 60, 0.1);
  1088. border-color: rgba(230, 162, 60, 0.2);
  1089. }
  1090. }
  1091. .info-btn {
  1092. padding: 6px 10px;
  1093. border-radius: 4px;
  1094. margin: 0 10px;
  1095. transition: all 0.3s ease;
  1096. }
  1097. .view-btn:hover {
  1098. background-color: rgb(216, 238, 248);
  1099. transform: translateY(-1px);
  1100. }
  1101. .audit-btn:hover {
  1102. background-color: rgb(215, 223, 246);
  1103. transform: translateY(-1px);
  1104. }
  1105. ::v-deep .el-dialog {
  1106. border-radius: 12px;
  1107. overflow: hidden;
  1108. box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12);
  1109. animation: dialogFadeIn 0.3s ease;
  1110. }
  1111. ::v-deep .el-dialog__header {
  1112. background: linear-gradient(135deg, #2c7a4d 0%, #42b983 100%);
  1113. padding: 18px 24px;
  1114. border-bottom: none;
  1115. position: relative;
  1116. }
  1117. ::v-deep .el-dialog__title {
  1118. font-size: 17px;
  1119. font-weight: 600;
  1120. color: white;
  1121. letter-spacing: 0.5px;
  1122. }
  1123. ::v-deep .el-dialog__headerbtn:hover .el-dialog__close {
  1124. color: #ffd04b;
  1125. transform: rotate(90deg);
  1126. }
  1127. ::v-deep .el-dialog__body {
  1128. padding: 28px 24px 20px;
  1129. background-color: #f8fafc;
  1130. max-height: 70vh;
  1131. overflow-y: auto;
  1132. }
  1133. ::v-deep .el-form-item {
  1134. margin-bottom: 20px;
  1135. transition: all 0.3s;
  1136. }
  1137. ::v-deep .el-form-item__label {
  1138. font-weight: 500;
  1139. color: #2d3748;
  1140. font-size: 14px;
  1141. transition: color 0.3s;
  1142. }
  1143. ::v-deep .el-input,
  1144. ::v-deep .el-textarea,
  1145. ::v-deep .el-select {
  1146. width: 100%;
  1147. }
  1148. ::v-deep .el-input__inner,
  1149. ::v-deep .el-textarea__inner {
  1150. border-radius: 8px;
  1151. border: 1px solid #dcdfe6;
  1152. font-size: 14px;
  1153. transition: all 0.3s;
  1154. background-color: #fcfdfe;
  1155. }
  1156. ::v-deep .el-input__inner:focus,
  1157. ::v-deep .el-textarea__inner:focus {
  1158. border-color: #42B983;
  1159. box-shadow: 0 0 0 3px rgb(230, 255, 238);
  1160. background-color: white;
  1161. }
  1162. ::v-deep .el-select .el-input__inner {
  1163. padding-right: 35px;
  1164. }
  1165. ::v-deep .el-dialog__footer {
  1166. padding: 20px 24px;
  1167. background-color: #f8fafc;
  1168. border-top: 1px solid #eef2f7;
  1169. border-radius: 0 0 12px 12px;
  1170. }
  1171. .cert-status-tag {
  1172. min-width: 70px;
  1173. display: inline-flex;
  1174. align-items: center;
  1175. justify-content: center;
  1176. border-radius: 16px;
  1177. padding: 0 10px;
  1178. font-weight: 500;
  1179. letter-spacing: 0.3px;
  1180. transition: all 0.3s ease;
  1181. i {
  1182. margin-right: 4px;
  1183. font-size: 12px;
  1184. }
  1185. &:hover {
  1186. transform: translateY(-1px);
  1187. box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
  1188. }
  1189. &.el-tag--success {
  1190. background: linear-gradient(135deg, #2c7a4d 0%, #42b983 100%);
  1191. border: none;
  1192. color: white;
  1193. }
  1194. &.el-tag--danger {
  1195. background: linear-gradient(135deg, #d9534f 0%, #f56c6c 100%);
  1196. border: none;
  1197. color: white;
  1198. }
  1199. &.el-tag--warning {
  1200. background: linear-gradient(135deg, #ff8c00 0%, #ffb347 100%);
  1201. border: none;
  1202. color: white;
  1203. }
  1204. }
  1205. </style>