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 {