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.

710 lines
20 KiB

  1. <template>
  2. <div class="app-container">
  3. <!-- 消息通知 -->
  4. <div class="info-container">
  5. <div class="card-container">
  6. <div class="info-card stat-card"
  7. @click="$router.push('/vet-info/VetNotification')"
  8. :class="{ shake: tjzs.unreadCount > 0 }"
  9. >
  10. <img class="info-icon" :src="require('@/assets/images/tongzhi.png')">
  11. <div class="info-content">
  12. <span class="info-title">通知</span>
  13. <div class="status-container">
  14. <div class="status-row">
  15. <span class="status-text">未读{{ tjzs.unreadCount }}</span>
  16. </div>
  17. <div class="status-row">
  18. <span class="status-text">已读{{ tjzs.readCount }}</span>
  19. </div>
  20. </div>
  21. </div>
  22. </div>
  23. <div class="stat-card">
  24. <div class="card-icon icon-total">
  25. <i class="el-icon-message"></i>
  26. </div>
  27. <div class="card-text">
  28. <div class="card-title">总数</div>
  29. <div class="card-value">{{ tjzs.totalCount }}</div>
  30. </div>
  31. </div>
  32. <div class="stat-card">
  33. <div class="card-icon icon-pending">
  34. <i class="el-icon-check"></i>
  35. </div>
  36. <div class="card-text">
  37. <div class="card-title">已读</div>
  38. <div class="card-value">{{ tjzs.readCount }}</div>
  39. </div>
  40. </div>
  41. <div class="stat-card">
  42. <div class="card-icon icon-handled">
  43. <i class="el-icon-bell"></i>
  44. </div>
  45. <div class="card-text">
  46. <div class="card-title">未读</div>
  47. <div class="card-value">{{ tjzs.unreadCount }}</div>
  48. </div>
  49. </div>
  50. </div>
  51. </div>
  52. <!-- 资质弹窗 -->
  53. <el-dialog :title="title" :visible.sync="flag" width="800px" append-to-body>
  54. <el-steps :active="activeStep" finish-status="success" simple style="margin-bottom: 10px;">
  55. <el-step title="选择经营范围"></el-step>
  56. <el-step title="上传资质"></el-step>
  57. <el-step title="提交审核"></el-step>
  58. </el-steps>
  59. <!-- 选择经营范围 -->
  60. <div v-if="activeStep === 0">
  61. <el-form ref="qualificationForm" :model="qualificationForm" :rules="rules" label-width="100px">
  62. <el-form-item label="真实姓名" prop="realName">
  63. <el-input v-model="qualificationForm.realName" placeholder="请输入您的真实姓名" />
  64. </el-form-item>
  65. <el-form-item label="身份证号" prop="idCard">
  66. <el-input v-model="qualificationForm.idCard" placeholder="请输入身份证号" />
  67. </el-form-item>
  68. <el-form-item label="资质类型" prop="qualificationType">
  69. <el-select
  70. v-model="qualificationForm.qualificationType"
  71. placeholder="请选择资质类型"
  72. style="width: 100%;"
  73. >
  74. <el-option
  75. v-for="option in qualificationTypeOptions"
  76. :key="option.value"
  77. :label="option.label"
  78. :value="option.value"
  79. />
  80. </el-select>
  81. </el-form-item>
  82. <el-form-item label="证书编号" prop="certificateNo">
  83. <el-input v-model="qualificationForm.certificateNo" placeholder="请输入证书编号" />
  84. </el-form-item>
  85. <el-form-item label="经营范围" prop="scopeIds">
  86. <el-select
  87. v-model="qualificationForm.scopeIds"
  88. multiple
  89. placeholder="请选择经营范围"
  90. style="width: 100%;"
  91. >
  92. <el-option
  93. v-for="scope in scopeOptions"
  94. :key="scope.value"
  95. :label="scope.label"
  96. :value="scope.value"
  97. />
  98. </el-select>
  99. <div class="scope-choice">
  100. 已选择{{ qualificationForm.scopeIds ? qualificationForm.scopeIds.length : 0 }}
  101. </div>
  102. </el-form-item>
  103. </el-form>
  104. </div>
  105. <!-- 上传资质 -->
  106. <div v-if="activeStep === 1">
  107. <div class="step-summary">
  108. <div class="summary-item">
  109. <span class="summary-label">资质类型</span>
  110. <div class="scope-tags">
  111. <el-tag
  112. v-for="qualificationType in qualificationForm.qualificationType"
  113. :key="qualificationType"
  114. type="info"
  115. size="small"
  116. class="scope-tag"
  117. >
  118. {{ getQualificationTypeLabel(qualificationForm.qualificationType) }}
  119. </el-tag>
  120. </div>
  121. </div>
  122. <div class="summary-item">
  123. <span class="summary-label">经营范围</span>
  124. <div class="scope-tags">
  125. <el-tag
  126. v-for="scopeId in qualificationForm.scopeIds"
  127. :key="scopeId"
  128. type="info"
  129. size="small"
  130. class="scope-tag"
  131. >
  132. {{ getScopeLabel(scopeId) }}
  133. </el-tag>
  134. </div>
  135. </div>
  136. </div>
  137. <div style="margin-bottom: 15px; color: #f56c6c; font-size: 14px;">
  138. <i class="el-icon-warning"></i>
  139. 需要您上传相关的资质文件
  140. </div>
  141. <el-form>
  142. <el-form-item label="资质文件" required>
  143. <el-upload
  144. ref="upload"
  145. action="#"
  146. :multiple="true"
  147. :file-list="fileList"
  148. :auto-upload="false"
  149. :on-change="handleFileChange"
  150. :on-remove="handleRemove"
  151. :show-file-list="true"
  152. accept=".jpg,.jpeg,.png,.pdf,.JPG,.JPEG,.PNG,.PDF"
  153. >
  154. <el-button type="primary">选择文件</el-button>
  155. <div slot="tip" class="el-upload__tip">
  156. 请上传清晰的资质文件照片或扫描件支持JPGPNGPDF格式
  157. </div>
  158. </el-upload>
  159. </el-form-item>
  160. <div v-if="uploadedFiles.length > 0" class="uploaded-files">
  161. <div class="file-list-title">已上传文件</div>
  162. <div v-for="(file, index) in uploadedFiles" :key="index" class="file-item">
  163. <i class="el-icon-document"></i>
  164. {{ file.name }}
  165. </div>
  166. </div>
  167. </el-form>
  168. </div>
  169. <!-- 提交审核 -->
  170. <div v-if="activeStep === 2">
  171. <div style="text-align: center; margin-bottom: 30px;">
  172. <i class="el-icon-circle-check" style="font-size: 60px; color: #67C23A;"></i>
  173. <div style="margin-top: 15px; font-size: 16px; color: #303133;">
  174. 请确认信息无误后提交审核
  175. </div>
  176. </div>
  177. <div style="background: #f8f9fa; padding: 15px; border-radius: 4px; margin-bottom: 20px;">
  178. <div style="margin-bottom: 8px;"><strong>基本信息</strong></div>
  179. <div style="margin-bottom: 5px;">真实姓名{{ qualificationForm.realName }}</div>
  180. <div style="margin-bottom: 5px;">身份证号{{ qualificationForm.idCard }}</div>
  181. <div style="margin-bottom: 5px;">资质类型{{ qualificationForm.qualificationType }}</div>
  182. <div>证书编号{{ qualificationForm.certificateNo }}</div>
  183. </div>
  184. <div style="margin-bottom: 15px; color: #606266;">
  185. <div>经营范围</div>
  186. <div class="scope-tags">
  187. <el-tag
  188. v-for="scopeId in qualificationForm.scopeIds"
  189. :key="scopeId"
  190. type="info"
  191. size="medium"
  192. class="scope-tag"
  193. >
  194. {{ getScopeLabel(scopeId) }}
  195. </el-tag>
  196. </div>
  197. </div>
  198. <div style="background: #f8f9fa; padding: 15px; border-radius: 4px; margin-bottom: 20px;">
  199. <div style="margin-bottom: 8px;"><strong>上传文件</strong></div>
  200. <div v-if="fileList.length > 0">
  201. <div v-for="(file, index) in fileList" :key="index" style="margin-bottom: 5px;">
  202. {{ file.name }}
  203. </div>
  204. </div>
  205. <div v-else style="color: #909399;">暂无文件</div>
  206. </div>
  207. </div>
  208. <!-- 操作按钮 -->
  209. <div slot="footer" class="dialog-footer">
  210. <div v-if="activeStep > 0" style="float: left;">
  211. <el-button @click="prevStep">上一步</el-button>
  212. </div>
  213. <div v-if="activeStep < 2">
  214. <el-button type="primary" @click="nextStep">
  215. {{ activeStep === 1 ? '下一步' : '下一步' }}
  216. </el-button>
  217. </div>
  218. <div v-if="activeStep === 2">
  219. <el-button @click="prevStep">上一步</el-button>
  220. <el-button type="primary" @click="submitQualification" :loading="loading">
  221. 提交审核
  222. </el-button>
  223. </div>
  224. </div>
  225. </el-dialog>
  226. </div>
  227. </template>
  228. <script>
  229. import { submitAuditQualification, qualificationAudit, getQualificationTypeOptions, getScopeOptions, uploadQualification } from "../api/vet/qualification";
  230. import { getStatsCard } from "../api/vet/notification";
  231. export default {
  232. name: "Syd",
  233. data() {
  234. return {
  235. // 消息通知
  236. tjzs: {},
  237. // 资质弹窗
  238. title: "兽医资质审核",
  239. flag: true,
  240. open: false,
  241. loading: false,
  242. activeStep: 0,
  243. qualificationForm: {
  244. qualificationId: "",
  245. realName: '',
  246. idCard: '',
  247. qualificationType: '',
  248. certificateNo: '',
  249. scopeIds: [],
  250. certificateFiles: '',
  251. },
  252. fileList: [],
  253. scopeOptions: [],
  254. uploadedFiles: [],
  255. qualificationTypeOptions: [],
  256. selectedScopesDetail: '',
  257. fileUploadResponses: [],
  258. rules: {
  259. realName: [
  260. {required: true, message: "请输入真实姓名", trigger: "blur"}
  261. ],
  262. idCard: [
  263. {required: true, message: "请输入身份证号", trigger: "blur"}
  264. ],
  265. qualificationType: [
  266. {required: true, message: "请选择资质类型", trigger: "change"}
  267. ],
  268. certificateNo : [
  269. {required: true, message: "请输入证书编号", trigger: "blur"}
  270. ],
  271. scopeIds: [
  272. {
  273. required: true,
  274. type: 'array',
  275. message: "请选择至少一个经营范围",
  276. trigger: "change"
  277. }
  278. ]
  279. }
  280. }
  281. },
  282. created() {
  283. this.initOptions();
  284. this.getcountSummary()
  285. },
  286. methods: {
  287. // 获取通知数据
  288. getcountSummary() {
  289. getStatsCard().then(res => {
  290. this.tjzs = res.data
  291. })
  292. },
  293. // 初始化选项
  294. initOptions() {
  295. const fetchData = async () => {
  296. const qtypeRes = await getQualificationTypeOptions();
  297. this.qualificationTypeOptions = qtypeRes.data;
  298. const scopeRes = await getScopeOptions();
  299. this.scopeOptions = scopeRes.data;
  300. }
  301. fetchData();
  302. },
  303. // 获取资质类型标签
  304. getQualificationTypeLabel(value) {
  305. const option = this.qualificationTypeOptions.find(item => item.value === value);
  306. return option ? option.label : value;
  307. },
  308. // 获取经营范围标签
  309. getScopeLabel(scopeId) {
  310. const scope = this.scopeOptions.find(item => item.value === scopeId);
  311. return scope ? scope.label : scopeId;
  312. },
  313. // 文件上传
  314. uploadSingleFile(file) {
  315. return new Promise((resolve, reject) => {
  316. const formData = new FormData();
  317. formData.append('file', file.raw);
  318. uploadQualification(formData)
  319. .then(response => {
  320. const filePath = response.data.filePath || response.data.path || response.data.url;
  321. this.fileUploadResponses.push({
  322. fileName: file.name,
  323. filePath: filePath,
  324. fileId: response.data.id || response.data.fileId
  325. });
  326. this.uploadedFiles.push({
  327. name: file.name,
  328. uid: file.uid,
  329. path: filePath
  330. });
  331. this.$message.success(`${file.name} 上传成功`);
  332. resolve(response);
  333. })
  334. .catch(error => {
  335. reject(error);
  336. });
  337. });
  338. },
  339. // 文件改变
  340. handleFileChange(file, fileList) {
  341. if (!file.raw ||
  342. file.raw.size / 1024 / 1024 >= 10 ||
  343. !['image/jpeg', 'image/png', 'application/pdf'].includes(file.raw.type)) {
  344. let errorMsg = '';
  345. if (!file.raw) errorMsg = '获取文件失败,请重新选择文件';
  346. else if (file.raw.size / 1024 / 1024 >= 10) errorMsg = '文件大小不能超过10MB';
  347. else errorMsg = '只能上传JPG、PNG或PDF格式的文件';
  348. this.$message.error(errorMsg);
  349. this.fileList = fileList.filter(item => item.uid !== file.uid);
  350. return;
  351. }
  352. this.fileList = fileList;
  353. if (file.status === 'ready') {
  354. file.status = 'uploading';
  355. this.uploadSingleFile(file)
  356. .then(() => file.status = 'success')
  357. .catch(() => file.status = 'fail');
  358. }
  359. },
  360. // 文件移除
  361. handleRemove(file, fileList) {
  362. this.fileList = fileList;
  363. const showIndex = this.uploadedFiles.findIndex(f => f.name === file.name);
  364. if (showIndex > -1) {
  365. this.uploadedFiles.splice(showIndex, 1);
  366. }
  367. const resIndex = this.fileUploadResponses.findIndex(f => f.fileName === file.name);
  368. if (resIndex > -1) {
  369. this.fileUploadResponses.splice(resIndex, 1);
  370. }
  371. },
  372. // 上一步
  373. prevStep() {
  374. if (this.activeStep > 0) {
  375. this.activeStep--
  376. }
  377. },
  378. // 下一步
  379. nextStep() {
  380. if (this.activeStep === 0) {
  381. this.$refs.qualificationForm.validate(valid => valid && (this.activeStep++));
  382. return;
  383. }
  384. if (this.activeStep === 1) {
  385. if (!this.fileList.length) {
  386. this.$message.warning('请上传资质文件');
  387. return;
  388. }
  389. const anyUploading = this.fileList.some(file => file.status === 'uploading');
  390. const anyFailed = this.fileList.some(file => file.status === 'fail');
  391. const allSuccess = this.fileList.every(file => file.status === 'success');
  392. if (anyUploading) {
  393. this.$message.warning('文件正在上传中,请等待上传完成');
  394. } else if (anyFailed) {
  395. this.$message.warning('存在上传失败的文件,请重新上传或移除失败的文件');
  396. } else if (!allSuccess) {
  397. this.$message.warning('请确保所有文件都上传成功');
  398. } else {
  399. this.activeStep++;
  400. }
  401. }
  402. },
  403. // 提交审核
  404. submitQualification() {
  405. this.loading = true;
  406. const requestData = {
  407. realName: this.qualificationForm.realName,
  408. idCard: this.qualificationForm.idCard,
  409. qualificationType: this.qualificationForm.qualificationType,
  410. certificateNo: this.qualificationForm.certificateNo,
  411. scopeIds: this.qualificationForm.scopeIds || [],
  412. certificateFiles: this.fileUploadResponses.map(item => item.fileUrl).join(','),
  413. certificateFileIds: this.fileUploadResponses.map(item => item.fileId).join(',')
  414. };
  415. // console.log('提交的数据:', JSON.stringify(requestData, null, 2));
  416. submitAuditQualification(requestData)
  417. .then(response => {
  418. this.$modal.msgSuccess('提交成功,请等待审核');
  419. this.flag = false;
  420. this.resetForm();
  421. })
  422. .catch(error => {
  423. console.error('提交失败:', error);
  424. console.error('错误详情:', error.response?.data);
  425. if (error.response?.data?.message) {
  426. this.$message.error('提交失败: ' + error.response.data.message);
  427. } else {
  428. this.$message.error('提交失败,请重试');
  429. }
  430. })
  431. .finally(() => {
  432. this.loading = false;
  433. });
  434. }
  435. }
  436. }
  437. </script>
  438. <style scoped lang="scss">
  439. /* 轮播背景 */
  440. .card-container {
  441. display: flex;
  442. gap: 20px;
  443. padding: 20px;
  444. }
  445. .stat-card {
  446. flex: 1;
  447. min-width: 0;
  448. padding: 30px 20px;
  449. border: 1px solid #e5e6eb;
  450. border-radius: 16px;
  451. background: #fff;
  452. display: flex;
  453. align-items: center;
  454. justify-content: center;
  455. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
  456. cursor: pointer;
  457. transition: all 0.3s ease;
  458. min-height: 180px;
  459. transform-origin: center center;
  460. &:hover {
  461. transform: translateY(-5px);
  462. box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
  463. }
  464. }
  465. .info-card.stat-card {
  466. background-color: #bbdefb;
  467. color: #0d47a1;
  468. display: flex;
  469. align-items: center;
  470. justify-content: space-around;
  471. padding: 20px;
  472. }
  473. .shake {
  474. animation: cardShake 1s infinite linear;
  475. background-color: #fffde6 !important;
  476. &:hover {
  477. animation-play-state: paused;
  478. background-color: #edeacd !important;
  479. }
  480. }
  481. .info-card .info-icon {
  482. width: 60%;
  483. cursor: pointer;
  484. }
  485. .info-card .info-title {
  486. font-size: 28px;
  487. font-weight: bold;
  488. color: #42B983;
  489. margin-bottom: 0;
  490. text-align: center;
  491. cursor: pointer;
  492. }
  493. .card-icon {
  494. width: 60px;
  495. height: 60px;
  496. border-radius: 8px;
  497. display: flex;
  498. align-items: center;
  499. justify-content: center;
  500. margin-right: 20px;
  501. }
  502. .card-icon i {
  503. font-size: 30px;
  504. color: #fff;
  505. }
  506. .card-text {
  507. display: flex;
  508. flex-direction: column;
  509. justify-content: center;
  510. align-items: center;
  511. }
  512. .card-title {
  513. font-size: 16px;
  514. color: #333;
  515. margin-bottom: 8px;
  516. }
  517. .card-value {
  518. font-size: 24px;
  519. font-weight: bold;
  520. color: #333;
  521. }
  522. .icon-total {
  523. background: linear-gradient(135deg, #ff9a44 0%, #ff6b08 100%);
  524. }
  525. .icon-pending {
  526. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  527. }
  528. .icon-handled {
  529. background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  530. }
  531. /* 资质弹窗 */
  532. /* 经营范围 */
  533. .scope-choice {
  534. color: #b6bbc6;
  535. font-size: 12px;
  536. margin-top: 5px;
  537. }
  538. .step-summary {
  539. margin-bottom: 20px;
  540. padding: 15px;
  541. background: #f8f9fa;
  542. border-radius: 4px;
  543. }
  544. .summary-item {
  545. display: flex;
  546. align-items: flex-start;
  547. margin-bottom: 10px;
  548. &:last-child {
  549. margin-bottom: 0;
  550. }
  551. }
  552. .summary-label {
  553. min-width: 80px;
  554. color: #606266;
  555. font-weight: bold;
  556. }
  557. .summary-value {
  558. color: #303133;
  559. }
  560. .scope-tags {
  561. display: flex;
  562. flex-wrap: wrap;
  563. gap: 5px;
  564. }
  565. .scope-tag {
  566. margin-right: 5px;
  567. margin-bottom: 5px;
  568. }
  569. .uploaded-files {
  570. margin-top: 15px;
  571. padding: 10px;
  572. background: #f5f7fa;
  573. border-radius: 4px;
  574. }
  575. .file-list-title {
  576. font-weight: bold;
  577. margin-bottom: 8px;
  578. color: #303133;
  579. }
  580. .file-item {
  581. padding: 5px 0;
  582. color: #606266;
  583. display: flex;
  584. align-items: center;
  585. i {
  586. margin-right: 5px;
  587. color: #409EFF;
  588. }
  589. }
  590. /* 响应式设计 */
  591. @media (max-width: 768px) {
  592. .card-container {
  593. flex-direction: column;
  594. padding: 10px;
  595. gap: 15px;
  596. }
  597. .stat-card {
  598. min-height: 150px;
  599. padding: 20px 15px;
  600. }
  601. .card-icon {
  602. width: 50px;
  603. height: 50px;
  604. margin-right: 15px;
  605. i {
  606. font-size: 24px;
  607. }
  608. }
  609. .info-card .info-icon {
  610. width: 40px;
  611. height: 40px;
  612. }
  613. .info-card .info-title {
  614. font-size: 20px;
  615. }
  616. .card-value {
  617. font-size: 20px;
  618. }
  619. }
  620. /* 抖动动画 */
  621. @keyframes cardShake {
  622. 0% { transform: translateX(0); }
  623. 10% { transform: translateX(-5px) translateY(-2px); }
  624. 20% { transform: translateX(5px) translateY(2px); }
  625. 30% { transform: translateX(-5px) translateY(-2px); }
  626. 40% { transform: translateX(5px) translateY(2px); }
  627. 50% { transform: translateX(-3px) translateY(-1px); }
  628. 60% { transform: translateX(3px) translateY(1px); }
  629. 70% { transform: translateX(-3px) translateY(-1px); }
  630. 80% { transform: translateX(3px) translateY(1px); }
  631. 90% { transform: translateX(-2px) translateY(0); }
  632. 100% { transform: translateX(0); }
  633. }
  634. .shake {
  635. animation: cardShake 1s infinite linear;
  636. &:hover {
  637. animation-play-state: paused;
  638. }
  639. }
  640. </style>