certificate.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. <script lang="ts" setup>
  2. import type { PersonnelQualificationModel } from '#/api';
  3. import { computed, ref } from 'vue';
  4. import { useVbenModal } from '@vben/common-ui';
  5. import { ChevronLeft, ChevronRight } from '@vben/icons';
  6. import { Spin, Tabs } from 'ant-design-vue';
  7. import { getPersonnelQualificationMethod } from '#/api';
  8. import { QUALIFICATION_STATUS_LABELS } from '#/api/method/personnel-qualification';
  9. const loading = ref(false);
  10. const detail = ref<PersonnelQualificationModel.Personnel>(
  11. {} as PersonnelQualificationModel.Personnel,
  12. );
  13. const activeCertId = ref('');
  14. const activeAttachmentIndex = ref(0);
  15. const activeCertificate = computed(() =>
  16. detail.value.certificates?.find((item) => item.id === activeCertId.value),
  17. );
  18. const activeAttachments = computed(
  19. () => activeCertificate.value?.attachments ?? [],
  20. );
  21. const currentAttachment = computed(
  22. () => activeAttachments.value[activeAttachmentIndex.value],
  23. );
  24. function getTabLabel(cert: PersonnelQualificationModel.Certificate) {
  25. const label = cert.name;
  26. if (cert.status === 'expiring') {
  27. return `${label}(即将过期)`;
  28. }
  29. if (cert.status === 'expired') {
  30. return `${label}(过期)`;
  31. }
  32. return label;
  33. }
  34. function isTabWarning(cert: PersonnelQualificationModel.Certificate) {
  35. return cert.status === 'expiring' || cert.status === 'expired';
  36. }
  37. function onTabChange(key: string | number) {
  38. activeCertId.value = String(key);
  39. activeAttachmentIndex.value = 0;
  40. }
  41. function showPrevAttachment() {
  42. if (!activeAttachments.value.length) return;
  43. activeAttachmentIndex.value =
  44. (activeAttachmentIndex.value - 1 + activeAttachments.value.length) %
  45. activeAttachments.value.length;
  46. }
  47. function showNextAttachment() {
  48. if (!activeAttachments.value.length) return;
  49. activeAttachmentIndex.value =
  50. (activeAttachmentIndex.value + 1) % activeAttachments.value.length;
  51. }
  52. const [Modal, modalApi] = useVbenModal({
  53. class: 'certificate-view-modal !w-[calc(100%-120px)] !max-w-[960px]',
  54. showConfirmButton: false,
  55. async onOpenChange(isOpen) {
  56. if (!isOpen) {
  57. detail.value = {} as PersonnelQualificationModel.Personnel;
  58. activeCertId.value = '';
  59. activeAttachmentIndex.value = 0;
  60. return;
  61. }
  62. const data = modalApi.getData<PersonnelQualificationModel.Personnel>();
  63. if (!data?.id) return;
  64. loading.value = true;
  65. try {
  66. detail.value = await getPersonnelQualificationMethod(data.id);
  67. activeCertId.value = detail.value.certificates?.[0]?.id ?? '';
  68. activeAttachmentIndex.value = 0;
  69. } finally {
  70. loading.value = false;
  71. }
  72. },
  73. });
  74. </script>
  75. <template>
  76. <Modal :title="`${detail.name || ''} - 资质证书`">
  77. <Spin :spinning="loading">
  78. <div v-if="detail.certificates?.length" class="certificate-view">
  79. <Tabs
  80. :active-key="activeCertId"
  81. class="certificate-tabs"
  82. @change="onTabChange"
  83. >
  84. <Tabs.TabPane v-for="cert in detail.certificates" :key="cert.id">
  85. <template #tab>
  86. <span :class="{ 'tab-warning': isTabWarning(cert) }">
  87. {{ getTabLabel(cert) }}
  88. </span>
  89. </template>
  90. </Tabs.TabPane>
  91. </Tabs>
  92. <div v-if="activeCertificate" class="certificate-meta">
  93. <div class="meta-item">
  94. <span class="meta-label">资质编号:</span>
  95. <span>{{ activeCertificate.number || '-' }}</span>
  96. </div>
  97. <div class="meta-item">
  98. <span class="meta-label">资质到期日期:</span>
  99. <span>{{
  100. activeCertificate.longTerm
  101. ? '长期'
  102. : activeCertificate.expiryDate || '-'
  103. }}</span>
  104. </div>
  105. <div class="meta-item">
  106. <span class="meta-label">证书类型:</span>
  107. <span>{{ activeCertificate.type || '-' }}</span>
  108. </div>
  109. <div class="meta-item">
  110. <span class="meta-label">资质附件:</span>
  111. <span>共{{ activeAttachments.length }}份</span>
  112. </div>
  113. <div class="meta-item">
  114. <span class="meta-label">资质状态:</span>
  115. <span
  116. :class="{
  117. 'status-warning':
  118. activeCertificate.status === 'expiring' ||
  119. activeCertificate.status === 'expired',
  120. }"
  121. >
  122. {{ QUALIFICATION_STATUS_LABELS[activeCertificate.status] || '-' }}
  123. </span>
  124. </div>
  125. </div>
  126. <div class="attachment-viewer">
  127. <button
  128. class="nav-btn"
  129. type="button"
  130. @click="showPrevAttachment"
  131. >
  132. <ChevronLeft class="size-5" />
  133. </button>
  134. <div class="attachment-content">
  135. <template v-if="currentAttachment">
  136. <iframe
  137. v-if="currentAttachment.type === 'pdf' && currentAttachment.url"
  138. referrerpolicy="no-referrer"
  139. :src="currentAttachment.url"
  140. class="attachment-pdf"
  141. title="PDF预览"
  142. />
  143. <div
  144. v-else-if="currentAttachment.type === 'pdf'"
  145. class="attachment-placeholder"
  146. >
  147. <span class="placeholder-icon">PDF</span>
  148. <span>{{ currentAttachment.name }}</span>
  149. </div>
  150. <img
  151. v-else-if="currentAttachment.url"
  152. :alt="currentAttachment.name"
  153. :src="currentAttachment.url"
  154. class="attachment-image"
  155. />
  156. <div v-else class="attachment-placeholder">
  157. <span class="placeholder-icon">X</span>
  158. <span>{{ currentAttachment.name }}</span>
  159. </div>
  160. </template>
  161. <div v-else class="attachment-placeholder">
  162. <span>暂无附件</span>
  163. </div>
  164. </div>
  165. <button
  166. class="nav-btn"
  167. type="button"
  168. @click="showNextAttachment"
  169. >
  170. <ChevronRight class="size-5" />
  171. </button>
  172. </div>
  173. </div>
  174. <div v-else class="empty-text">暂无证书信息</div>
  175. </Spin>
  176. </Modal>
  177. </template>
  178. <style scoped>
  179. .certificate-view {
  180. padding: 8px 16px 16px;
  181. }
  182. .certificate-tabs :deep(.tab-warning) {
  183. color: #ff4d4f;
  184. }
  185. .certificate-meta {
  186. display: grid;
  187. grid-template-columns: repeat(2, minmax(0, 1fr));
  188. gap: 12px 24px;
  189. margin: 16px 0 24px;
  190. }
  191. .meta-item {
  192. line-height: 1.6;
  193. word-break: break-all;
  194. }
  195. .meta-label {
  196. color: rgb(0 0 0 / 65%);
  197. }
  198. .status-warning {
  199. color: #ff4d4f;
  200. }
  201. .attachment-viewer {
  202. display: flex;
  203. gap: 16px;
  204. align-items: center;
  205. min-height: 360px;
  206. }
  207. .nav-btn {
  208. display: flex;
  209. flex-shrink: 0;
  210. align-items: center;
  211. justify-content: center;
  212. width: 40px;
  213. height: 40px;
  214. color: rgb(0 0 0 / 45%);
  215. font-size: 18px;
  216. background: #fff;
  217. border: 1px solid #d9d9d9;
  218. border-radius: 50%;
  219. cursor: pointer;
  220. }
  221. .nav-btn:hover {
  222. color: #1677ff;
  223. border-color: #1677ff;
  224. }
  225. .attachment-content {
  226. display: flex;
  227. flex: 1;
  228. align-items: center;
  229. justify-content: center;
  230. min-height: 360px;
  231. background: #fafafa;
  232. border: 1px dashed #d9d9d9;
  233. border-radius: 8px;
  234. }
  235. .attachment-image {
  236. max-width: 100%;
  237. max-height: 340px;
  238. object-fit: contain;
  239. }
  240. .attachment-pdf {
  241. width: 100%;
  242. height: 390px;
  243. border: 0;
  244. }
  245. .attachment-placeholder {
  246. display: flex;
  247. flex-direction: column;
  248. gap: 12px;
  249. align-items: center;
  250. justify-content: center;
  251. color: rgb(0 0 0 / 45%);
  252. }
  253. .placeholder-icon {
  254. display: flex;
  255. align-items: center;
  256. justify-content: center;
  257. width: 120px;
  258. height: 120px;
  259. font-size: 48px;
  260. background: #fff;
  261. border: 1px solid #d9d9d9;
  262. }
  263. .empty-text {
  264. padding: 48px 0;
  265. color: rgb(0 0 0 / 45%);
  266. text-align: center;
  267. }
  268. </style>