优化文档下载

This commit is contained in:
fuenmao
2026-09-14 17:55:19 +08:00
parent 40010b5668
commit 7de33e5f13
2 changed files with 181 additions and 26 deletions
+64 -19
View File
@@ -319,7 +319,7 @@
<span v-if="isImageMaterial(item)" class="preview-hint"> <span v-if="isImageMaterial(item)" class="preview-hint">
{{ materialPreviewUrls[item.fileId] ? "点击图片可放大预览" : materialPreviewText(item) }} {{ materialPreviewUrls[item.fileId] ? "点击图片可放大预览" : materialPreviewText(item) }}
</span> </span>
<el-button link type="primary" icon="Download" @click="download(item.fileId, materialFileName(item))"> 下载原件 </el-button> <el-button link type="primary" icon="Download" @click="download(item.fileId, materialDownloadName(item))"> 下载原件 </el-button>
</div> </div>
</div> </div>
</div> </div>
@@ -369,12 +369,7 @@
plain plain
size="small" size="small"
icon="Download" icon="Download"
@click=" @click="download(contract.generatedDocxFileId, contractDownloadName(contract, 'docx'))"
download(
contract.generatedDocxFileId,
`${detail?.employee?.employeeName || '员工'}_${contract.contractName || '入职合同'}.docx`
)
"
> >
下载生成 DOCX 下载生成 DOCX
</el-button> </el-button>
@@ -894,19 +889,69 @@ function contractStatusText(status?: string) {
return map[status || ""] || status || "未知状态"; 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) { 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") { function contractDownloadName(contract: any, extension: "pdf" | "docx") {
const employeeName = detail.value?.employee?.employeeName || "员工"; const prefix = getEmployeeDownloadPrefix();
const contractName = contract?.contractName || "入职合同"; const docType = String(
return safeDownloadName(employeeName + "_" + contractName) + "." + extension; 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() { function electronicSignArchiveName() {
const employeeName = detail.value?.employee?.employeeName || "员工"; const prefix = getEmployeeDownloadPrefix();
return safeDownloadName(employeeName + "_电子签完整归档") + ".zip"; return `${prefix}_电子签完整归档.zip`;
} }
function clearMaterialPreviews() { function clearMaterialPreviews() {
@@ -1201,18 +1246,18 @@ async function showDetail(row: OnboardingEmployee) {
} }
function resolveDownloadFileName(response: any, fallbackFileName?: string) { function resolveDownloadFileName(response: any, fallbackFileName?: string) {
if (fallbackFileName) {
return safeDecodeFileName(fallbackFileName);
}
const disposition = String(response?.headers?.["content-disposition"] || ""); const disposition = String(response?.headers?.["content-disposition"] || "");
const encodedName = disposition.match(/filename\*=UTF-8\'\'([^;]+)/i)?.[1] || disposition.match(/filename="?([^";]+)"?/i)?.[1]; const encodedName = disposition.match(/filename\*=UTF-8\'\'([^;]+)/i)?.[1] || disposition.match(/filename="?([^";]+)"?/i)?.[1];
if (encodedName) { if (encodedName) {
try { return safeDecodeFileName(encodedName);
return decodeURIComponent(encodedName.replace(/\+/g, "%20"));
} catch {
return encodedName;
}
} }
return fallbackFileName || "员工资料文件"; return "员工资料文件";
} }
function openHrEdit(row: OnboardingEmployee) { function openHrEdit(row: OnboardingEmployee) {
+117 -7
View File
@@ -900,7 +900,7 @@
plain plain
size="small" size="small"
icon="Download" icon="Download"
@click="download(item.generatedDocxFileId)" @click="download(item.generatedDocxFileId, getContractDocxDownloadName(item))"
> >
下载已生成DOCX 下载已生成DOCX
</el-button> </el-button>
@@ -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) { function resolveDownloadFileName(response: any, fallbackFileName?: string) {
if (fallbackFileName) {
return safeDecodeFileName(fallbackFileName);
}
const disposition = String(response?.headers?.["content-disposition"] || ""); const disposition = String(response?.headers?.["content-disposition"] || "");
const encodedName = disposition.match(/filename\*=UTF-8\'\'([^;]+)/i)?.[1] const encodedName = disposition.match(/filename\*=UTF-8\'\'([^;]+)/i)?.[1]
|| disposition.match(/filename="?([^";]+)"?/i)?.[1]; || disposition.match(/filename="?([^";]+)"?/i)?.[1];
if (encodedName) { if (encodedName) {
try { return safeDecodeFileName(encodedName);
return decodeURIComponent(encodedName.replace(/\+/g, "%20"));
} catch {
return encodedName;
}
} }
return fallbackFileName || "入职材料"; return "入职材料";
} }
// 列表查询并统计 // 列表查询并统计
@@ -3993,12 +4030,85 @@ onMounted(loadCompanies);
gap: 16px; 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) { @media (max-width: 768px) {
.copy-count-row { .copy-count-row {
align-items: flex-start; align-items: flex-start;
flex-direction: column; 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-document,
.electronic-sign-link-card, .electronic-sign-link-card,
.electronic-sign-footer { .electronic-sign-footer {