|
|
@@ -76,18 +76,14 @@
|
|
|
<span>{{ maskIdCard(scope.row.idCard) }}</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="岗位证书" align="center" min-width="200">
|
|
|
+ <el-table-column label="岗位证书" align="center" min-width="120">
|
|
|
<template #default="scope">
|
|
|
- <el-tag
|
|
|
- v-for="(cert, idx) in getPersonnelCerts(scope.row.id)"
|
|
|
- :key="idx"
|
|
|
- :type="getCertTypeTag(cert.certType)"
|
|
|
- size="small"
|
|
|
- style="margin-right: 4px; margin-bottom: 4px"
|
|
|
- >
|
|
|
- {{ cert.certName || cert.certType }}
|
|
|
- </el-tag>
|
|
|
- <span v-if="getPersonnelCerts(scope.row.id).length === 0" class="text-placeholder">暂无证书</span>
|
|
|
+ <template v-if="getPersonnelCerts(scope.row.id).length > 0">
|
|
|
+ <el-tag type="primary" size="small" effect="plain">
|
|
|
+ {{ getPersonnelCerts(scope.row.id).length }} 个证书
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ <span v-else class="text-placeholder">暂无证书</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="工作门店" align="center" prop="storeName" :show-overflow-tooltip="true" width="160" />
|
|
|
@@ -118,7 +114,7 @@
|
|
|
<el-dialog
|
|
|
v-model="dialog.visible"
|
|
|
:title="dialog.title"
|
|
|
- width="820px"
|
|
|
+ width="960px"
|
|
|
append-to-body
|
|
|
:close-on-click-modal="false"
|
|
|
@close="closeDialog"
|
|
|
@@ -246,7 +242,7 @@
|
|
|
<el-select
|
|
|
v-model="cert.certType"
|
|
|
placeholder="请选择证书类型"
|
|
|
- style="width: 220px"
|
|
|
+ style="width: 180px"
|
|
|
>
|
|
|
<el-option
|
|
|
v-for="d in certTypeOptions"
|
|
|
@@ -255,6 +251,17 @@
|
|
|
:value="d.value"
|
|
|
/>
|
|
|
</el-select>
|
|
|
+ <el-date-picker
|
|
|
+ v-model="cert.validPeriod"
|
|
|
+ type="daterange"
|
|
|
+ range-separator="至"
|
|
|
+ start-placeholder="生效日期"
|
|
|
+ end-placeholder="到期日期"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ format="YYYY-MM-DD"
|
|
|
+ style="width: 240px"
|
|
|
+ unlink-panels
|
|
|
+ />
|
|
|
<el-upload
|
|
|
v-model:file-list="cert.fileList"
|
|
|
:http-request="customCertUpload"
|
|
|
@@ -290,20 +297,44 @@
|
|
|
</el-dialog>
|
|
|
|
|
|
<!-- 查看证书弹窗 -->
|
|
|
- <el-dialog v-model="certDialog.visible" title="岗位证书" width="640px" append-to-body>
|
|
|
- <el-descriptions v-if="currentCerts.length > 0" :column="1" border>
|
|
|
+ <el-dialog v-model="certDialog.visible" title="岗位证书" width="720px" append-to-body>
|
|
|
+ <el-descriptions v-if="currentCerts.length > 0" :column="2" border>
|
|
|
<el-descriptions-item
|
|
|
v-for="(cert, idx) in currentCerts"
|
|
|
:key="idx"
|
|
|
:label="cert.certName || cert.certType"
|
|
|
+ :span="2"
|
|
|
>
|
|
|
- <div v-if="cert.certFile" class="cert-view-box">
|
|
|
- <el-icon :size="22" class="cert-view-icon"><Document /></el-icon>
|
|
|
- <el-link :href="cert.certFile" target="_blank" type="primary" style="margin-left: 6px">
|
|
|
- 查看证书文件
|
|
|
- </el-link>
|
|
|
+ <div class="cert-view-row">
|
|
|
+ <span class="cert-view-meta">
|
|
|
+ <span class="meta-label">有效期:</span>
|
|
|
+ <span class="meta-value">
|
|
|
+ {{ formatCertPeriod(cert.issueDate, cert.expireDate) }}
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+ <span class="cert-view-meta">
|
|
|
+ <el-tag
|
|
|
+ :type="getCertStatusTag(cert.certStatus, cert.expireDate)"
|
|
|
+ size="small"
|
|
|
+ >
|
|
|
+ {{ getCertStatusLabel(cert.certStatus, cert.expireDate) }}
|
|
|
+ </el-tag>
|
|
|
+ </span>
|
|
|
+ <span class="cert-view-meta">
|
|
|
+ <template v-if="cert.certFile">
|
|
|
+ <el-icon :size="20" class="cert-view-icon"><Document /></el-icon>
|
|
|
+ <el-link
|
|
|
+ :href="cert.certFile"
|
|
|
+ target="_blank"
|
|
|
+ type="primary"
|
|
|
+ style="margin-left: 6px"
|
|
|
+ >
|
|
|
+ 查看证书文件
|
|
|
+ </el-link>
|
|
|
+ </template>
|
|
|
+ <span v-else class="text-placeholder">未上传证书文件</span>
|
|
|
+ </span>
|
|
|
</div>
|
|
|
- <span v-else class="text-placeholder">未上传证书文件</span>
|
|
|
</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
<div v-else class="empty-tip">该人员暂无岗位证书</div>
|
|
|
@@ -340,6 +371,7 @@ interface CertItem {
|
|
|
certType: string;
|
|
|
certName?: string;
|
|
|
certFile?: string;
|
|
|
+ validPeriod?: string[]; // [生效日期, 到期日期]
|
|
|
fileList: UploadUserFile[];
|
|
|
}
|
|
|
|
|
|
@@ -573,6 +605,43 @@ const getCertTypeTag = (type: string) => {
|
|
|
return map[type] || 'info';
|
|
|
};
|
|
|
|
|
|
+/** 计算证书有效状态:已过期/即将到期(≤30天)/有效 */
|
|
|
+const calcCertStatus = (expireDate?: string): { label: string; tag: string } => {
|
|
|
+ if (!expireDate) return { label: '未知', tag: 'info' };
|
|
|
+ const expire = new Date(expireDate).getTime();
|
|
|
+ if (Number.isNaN(expire)) return { label: '未知', tag: 'info' };
|
|
|
+ const now = Date.now();
|
|
|
+ if (expire < now) return { label: '已过期', tag: 'danger' };
|
|
|
+ const diffDays = Math.floor((expire - now) / (1000 * 60 * 60 * 24));
|
|
|
+ if (diffDays <= 30) return { label: '即将到期', tag: 'warning' };
|
|
|
+ return { label: '有效', tag: 'success' };
|
|
|
+};
|
|
|
+
|
|
|
+/** 查看证书弹窗用:获取证书状态(优先用后端 certStatus,否则按 expireDate 实时计算) */
|
|
|
+const getCertStatusLabel = (certStatus?: string, expireDate?: string) => {
|
|
|
+ if (certStatus && certStatus !== '有效') return certStatus;
|
|
|
+ return calcCertStatus(expireDate).label;
|
|
|
+};
|
|
|
+
|
|
|
+const getCertStatusTag = (certStatus?: string, expireDate?: string): any => {
|
|
|
+ if (certStatus && certStatus !== '有效') {
|
|
|
+ const map: Record<string, string> = {
|
|
|
+ 已过期: 'danger',
|
|
|
+ 即将到期: 'warning',
|
|
|
+ 有效: 'success',
|
|
|
+ 无效: 'danger'
|
|
|
+ };
|
|
|
+ return map[certStatus] || 'info';
|
|
|
+ }
|
|
|
+ return calcCertStatus(expireDate).tag;
|
|
|
+};
|
|
|
+
|
|
|
+/** 格式化证书有效期区间显示 */
|
|
|
+const formatCertPeriod = (issueDate?: string, expireDate?: string) => {
|
|
|
+ const fmt = (d?: string) => (d ? d.substring(0, 10) : '—');
|
|
|
+ return `${fmt(issueDate)} ~ ${fmt(expireDate)}`;
|
|
|
+};
|
|
|
+
|
|
|
// 人员类型 tag 颜色(按字典 value 映射,与设计稿一致)
|
|
|
const getJobTypeTag = (type: string) => {
|
|
|
const map: Record<string, string> = {
|
|
|
@@ -655,23 +724,34 @@ const handleUpdate = async (row: RiskPersonnelVo) => {
|
|
|
try {
|
|
|
const certRes = await listPersonnelCertByPersonnel(row.id);
|
|
|
const certs = certRes.data || [];
|
|
|
- form.value.certList = certs.map((c) => ({
|
|
|
- uid: -Math.floor(Math.random() * 100000),
|
|
|
- id: c.id,
|
|
|
- certType: c.certType,
|
|
|
- certName: c.certName,
|
|
|
- certFile: c.certFile,
|
|
|
- fileList: c.certFile
|
|
|
- ? [
|
|
|
- {
|
|
|
- uid: -Math.floor(Math.random() * 100000),
|
|
|
- name: c.certName || c.certType || '证书',
|
|
|
- status: 'success',
|
|
|
- url: c.certFile
|
|
|
- }
|
|
|
- ]
|
|
|
- : []
|
|
|
- }));
|
|
|
+ form.value.certList = certs.map((c) => {
|
|
|
+ // 后端 issueDate/expireDate → 前端 validPeriod 日期范围
|
|
|
+ let validPeriod: string[] | undefined;
|
|
|
+ if (c.issueDate || c.expireDate) {
|
|
|
+ validPeriod = [
|
|
|
+ c.issueDate ? c.issueDate.substring(0, 10) : '',
|
|
|
+ c.expireDate ? c.expireDate.substring(0, 10) : ''
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ uid: -Math.floor(Math.random() * 100000),
|
|
|
+ id: c.id,
|
|
|
+ certType: c.certType,
|
|
|
+ certName: c.certName,
|
|
|
+ certFile: c.certFile,
|
|
|
+ validPeriod,
|
|
|
+ fileList: c.certFile
|
|
|
+ ? [
|
|
|
+ {
|
|
|
+ uid: -Math.floor(Math.random() * 100000),
|
|
|
+ name: c.certName || c.certType || '证书',
|
|
|
+ status: 'success',
|
|
|
+ url: c.certFile
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ : []
|
|
|
+ };
|
|
|
+ });
|
|
|
} catch (e) {
|
|
|
form.value.certList = [];
|
|
|
}
|
|
|
@@ -771,13 +851,39 @@ const submitForm = () => {
|
|
|
if (valid) {
|
|
|
const certBos = (form.value.certList || [])
|
|
|
.filter((c: CertItem) => c.certType)
|
|
|
- .map((c: CertItem) => ({
|
|
|
- id: c.id,
|
|
|
- certType: c.certType,
|
|
|
- certName: c.certType,
|
|
|
- certFile: c.certFile || '',
|
|
|
- certStatus: '有效'
|
|
|
- }));
|
|
|
+ .map((c: CertItem) => {
|
|
|
+ // 证书有效期 validPeriod → issueDate / expireDate
|
|
|
+ const issueDate =
|
|
|
+ c.validPeriod && c.validPeriod[0]
|
|
|
+ ? `${c.validPeriod[0]} 00:00:00`
|
|
|
+ : '';
|
|
|
+ const expireDate =
|
|
|
+ c.validPeriod && c.validPeriod[1]
|
|
|
+ ? `${c.validPeriod[1]} 23:59:59`
|
|
|
+ : '';
|
|
|
+
|
|
|
+ // 根据到期日期自动判断证书状态
|
|
|
+ let certStatus = '有效';
|
|
|
+ if (expireDate) {
|
|
|
+ const expire = new Date(expireDate).getTime();
|
|
|
+ if (expire < Date.now()) {
|
|
|
+ certStatus = '已过期';
|
|
|
+ } else {
|
|
|
+ const diffDays = Math.floor((expire - Date.now()) / (1000 * 60 * 60 * 24));
|
|
|
+ if (diffDays <= 30) certStatus = '即将到期';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: c.id,
|
|
|
+ certType: c.certType,
|
|
|
+ certName: c.certType,
|
|
|
+ certFile: c.certFile || '',
|
|
|
+ issueDate,
|
|
|
+ expireDate,
|
|
|
+ certStatus
|
|
|
+ };
|
|
|
+ });
|
|
|
|
|
|
const personnel = {
|
|
|
id: form.value.id,
|
|
|
@@ -890,8 +996,16 @@ onMounted(async () => {
|
|
|
.cert-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- gap: 12px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 10px;
|
|
|
margin-bottom: 12px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ background: #fafbfc;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+.cert-item:hover {
|
|
|
+ border-color: #c0c4cc;
|
|
|
}
|
|
|
.cert-upload-trigger {
|
|
|
display: inline-flex;
|
|
|
@@ -913,4 +1027,23 @@ onMounted(async () => {
|
|
|
.cert-view-icon {
|
|
|
color: #e6a23c;
|
|
|
}
|
|
|
+.cert-view-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 18px;
|
|
|
+}
|
|
|
+.cert-view-meta {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.cert-view-row .meta-label {
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+.cert-view-row .meta-value {
|
|
|
+ color: #303133;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
</style>
|