From 7de33e5f13fb1a7d9bad8d60e917e8873fd4dea4 Mon Sep 17 00:00:00 2001 From: fuenmao <980740792@qq.com> Date: Mon, 14 Sep 2026 17:55:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=96=87=E6=A1=A3=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/biz/onboarding/employee/index.vue | 83 ++++++++++--- src/views/biz/onboarding/task/index.vue | 124 ++++++++++++++++++-- 2 files changed, 181 insertions(+), 26 deletions(-) diff --git a/src/views/biz/onboarding/employee/index.vue b/src/views/biz/onboarding/employee/index.vue index 970fcbb..cd5facf 100644 --- a/src/views/biz/onboarding/employee/index.vue +++ b/src/views/biz/onboarding/employee/index.vue @@ -319,7 +319,7 @@ {{ materialPreviewUrls[item.fileId] ? "点击图片可放大预览" : materialPreviewText(item) }} - 下载原件 + 下载原件 @@ -369,12 +369,7 @@ plain size="small" icon="Download" - @click=" - download( - contract.generatedDocxFileId, - `${detail?.employee?.employeeName || '员工'}_${contract.contractName || '入职合同'}.docx` - ) - " + @click="download(contract.generatedDocxFileId, contractDownloadName(contract, 'docx'))" > 下载生成 DOCX @@ -894,19 +889,69 @@ function contractStatusText(status?: string) { return map[status || ""] || status || "未知状态"; } +function safeDecodeFileName(name: string): string { + let decoded = String(name || "").trim(); + try { + while (/%[0-9a-fA-F]{2}/.test(decoded)) { + const next = decodeURIComponent(decoded.replace(/\+/g, "%20")); + if (next === decoded) break; + decoded = next; + } + } catch { + // 忽略异常,保留当前值 + } + return decoded; +} + function safeDownloadName(value: string) { - return value.replace(/[\\/:*?"<>|]/g, "_"); + return safeDecodeFileName(value) + .replace(/[\/\\:*?"<>|]+/g, "_") + .replace(/\s+/g, "") + .trim(); +} + +function getEmployeeDownloadPrefix(): string { + const taskNo = String( + detail.value?.task?.taskNo + || detail.value?.employee?.employeeNo + || detail.value?.employeeInfo?.employeeNo + || "" + ).trim(); + const employeeName = String( + detail.value?.employee?.employeeName + || detail.value?.employeeInfo?.employeeName + || "员工" + ).trim(); + + const safeNo = safeDownloadName(taskNo); + const safeName = safeDownloadName(employeeName); + + if (safeNo) { + return `${safeNo}_${safeName}`; + } + return safeName; } function contractDownloadName(contract: any, extension: "pdf" | "docx") { - const employeeName = detail.value?.employee?.employeeName || "员工"; - const contractName = contract?.contractName || "入职合同"; - return safeDownloadName(employeeName + "_" + contractName) + "." + extension; + const prefix = getEmployeeDownloadPrefix(); + const docType = String( + contract?.contractType + || contract?.contractName + || "入职合同" + ).trim(); + return `${prefix}_${safeDownloadName(docType)}.${extension}`; +} + +function materialDownloadName(item: any) { + const prefix = getEmployeeDownloadPrefix(); + const materialType = String(item?.materialName || "资料文件").trim(); + const suffix = materialFileSuffix(item) || "png"; + return `${prefix}_${safeDownloadName(materialType)}.${suffix}`; } function electronicSignArchiveName() { - const employeeName = detail.value?.employee?.employeeName || "员工"; - return safeDownloadName(employeeName + "_电子签完整归档") + ".zip"; + const prefix = getEmployeeDownloadPrefix(); + return `${prefix}_电子签完整归档.zip`; } function clearMaterialPreviews() { @@ -1201,18 +1246,18 @@ async function showDetail(row: OnboardingEmployee) { } function resolveDownloadFileName(response: any, fallbackFileName?: string) { + if (fallbackFileName) { + return safeDecodeFileName(fallbackFileName); + } + const disposition = String(response?.headers?.["content-disposition"] || ""); const encodedName = disposition.match(/filename\*=UTF-8\'\'([^;]+)/i)?.[1] || disposition.match(/filename="?([^";]+)"?/i)?.[1]; if (encodedName) { - try { - return decodeURIComponent(encodedName.replace(/\+/g, "%20")); - } catch { - return encodedName; - } + return safeDecodeFileName(encodedName); } - return fallbackFileName || "员工资料文件"; + return "员工资料文件"; } function openHrEdit(row: OnboardingEmployee) { diff --git a/src/views/biz/onboarding/task/index.vue b/src/views/biz/onboarding/task/index.vue index 4e1d3e2..0a3687b 100644 --- a/src/views/biz/onboarding/task/index.vue +++ b/src/views/biz/onboarding/task/index.vue @@ -900,7 +900,7 @@ plain size="small" icon="Download" - @click="download(item.generatedDocxFileId)" + @click="download(item.generatedDocxFileId, getContractDocxDownloadName(item))" > 下载已生成DOCX @@ -2123,20 +2123,57 @@ async function loadMaterialPreviews(materials: any[]) { })); } +function safeDecodeFileName(name: string): string { + let decoded = String(name || "").trim(); + try { + // 循环解码,应对多重 URL 编码(例如 %25e5... 或 %e5...) + while (/%[0-9a-fA-F]{2}/.test(decoded)) { + const next = decodeURIComponent(decoded.replace(/\+/g, "%20")); + if (next === decoded) break; + decoded = next; + } + } catch { + // 忽略异常,保留当前值 + } + return decoded; +} + +function getContractDocxDownloadName(item: any) { + const taskNo = String(detail.value?.task?.taskNo || "").trim(); + const employeeName = String( + detail.value?.employeeInfo?.employeeName + || detail.value?.task?.employeeName + || "待填员工" + ).trim(); + const docType = String( + item?.contractType + || item?.contractName + || "劳动合同" + ).trim(); + + const clean = (val: string) => + safeDecodeFileName(val) + .replace(/[\/\\:*?"<>|]+/g, "_") + .replace(/\s+/g, "") + .trim(); + + return `${clean(taskNo)}_${clean(employeeName)}_${clean(docType)}.docx`; +} + function resolveDownloadFileName(response: any, fallbackFileName?: string) { + if (fallbackFileName) { + return safeDecodeFileName(fallbackFileName); + } + const disposition = String(response?.headers?.["content-disposition"] || ""); const encodedName = disposition.match(/filename\*=UTF-8\'\'([^;]+)/i)?.[1] || disposition.match(/filename="?([^";]+)"?/i)?.[1]; if (encodedName) { - try { - return decodeURIComponent(encodedName.replace(/\+/g, "%20")); - } catch { - return encodedName; - } + return safeDecodeFileName(encodedName); } - return fallbackFileName || "入职材料"; + return "入职材料"; } // 列表查询并统计 @@ -3993,12 +4030,85 @@ onMounted(loadCompanies); gap: 16px; } +.contracts-panel { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 16px; + padding: 2px 0 12px; +} + +.contract-card-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; + padding: 18px 24px; + border: 1px solid var(--el-border-color-light); + border-radius: 8px; + background: #ffffff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03); + transition: all 0.2s ease; +} + +.contract-card-item:hover { + border-color: var(--el-color-primary-light-5); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.contract-left { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +} + +.contract-title { + font-size: 15px; + font-weight: 600; + color: var(--el-text-color-primary); + line-height: 1.4; +} + +.contract-sub-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; +} + +.confirmed-text { + font-size: 13px; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.contract-right { + display: flex; + align-items: center; + flex-shrink: 0; + gap: 12px; +} + @media (max-width: 768px) { .copy-count-row { align-items: flex-start; flex-direction: column; } + .contract-card-item { + flex-direction: column; + align-items: flex-start; + gap: 14px; + padding: 14px 16px; + } + + .contract-right { + width: 100%; + justify-content: flex-end; + } + .electronic-sign-document, .electronic-sign-link-card, .electronic-sign-footer {