电子签基础功能
This commit is contained in:
Vendored
+2
@@ -29,6 +29,8 @@ declare module 'vue' {
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
|
||||
@@ -4,8 +4,59 @@ import type { ReqId, ResPage } from "@/api/interface";
|
||||
const taskHttp = moduleRequest("/biz/onboarding/task/");
|
||||
const auditHttp = moduleRequest("/biz/onboarding/audit/");
|
||||
const contractDocumentHttp = moduleRequest("/biz/onboarding/contract-document/");
|
||||
const electronicSignHttp = moduleRequest("/biz/onboarding/electronic-sign/");
|
||||
const fileHttp = moduleRequest("/sys/dev/file/");
|
||||
|
||||
/**
|
||||
* 下载创建任务前的合同预览,并将统一响应中的 JSON 错误转换成可读异常。
|
||||
*/
|
||||
async function requestContractPreview(
|
||||
action: string,
|
||||
params: Record<string, unknown>,
|
||||
fallbackMessage: string
|
||||
) {
|
||||
let response: any;
|
||||
try {
|
||||
response = await contractDocumentHttp.post<any>(
|
||||
action,
|
||||
params,
|
||||
{ responseType: "blob", loading: false, cancel: false }
|
||||
);
|
||||
} catch (error: any) {
|
||||
const errorBlob = error?.response?.data;
|
||||
if (errorBlob instanceof Blob) {
|
||||
try {
|
||||
const result = JSON.parse(await errorBlob.text());
|
||||
throw new Error(result?.msg || fallbackMessage);
|
||||
} catch (parseError) {
|
||||
if (!(parseError instanceof SyntaxError)) throw parseError;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const blob = response?.data;
|
||||
const contentType = String(
|
||||
response?.headers?.["content-type"] || blob?.type || ""
|
||||
);
|
||||
if (
|
||||
blob instanceof Blob &&
|
||||
contentType.toLowerCase().includes("application/json")
|
||||
) {
|
||||
try {
|
||||
const result = JSON.parse(await blob.text());
|
||||
throw new Error(result?.msg || fallbackMessage);
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new Error(fallbackMessage);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export interface OnboardingTask {
|
||||
id: number;
|
||||
taskNo: string;
|
||||
@@ -59,6 +110,62 @@ export interface OnboardingPreparedContract {
|
||||
generatedAt?: string;
|
||||
}
|
||||
|
||||
/** 一次电子签流程中的单份合同文件。 */
|
||||
export interface OnboardingElectronicSignDocument {
|
||||
id: number;
|
||||
taskContractId: number;
|
||||
providerDocumentId?: string;
|
||||
documentName: string;
|
||||
documentSort: number;
|
||||
originalDocxFileId?: number;
|
||||
signedPdfFileId?: number;
|
||||
}
|
||||
|
||||
/** 管理端查看的入职电子签流程详情。 */
|
||||
export interface OnboardingElectronicSignDetail {
|
||||
taskId: number;
|
||||
taskNo: string;
|
||||
canInitiate: boolean;
|
||||
cannotInitiateReason?: string;
|
||||
canReinitiate: boolean;
|
||||
cannotReinitiateReason?: string;
|
||||
hasProcess: boolean;
|
||||
processId?: number;
|
||||
providerContractId?: string;
|
||||
bizId?: string;
|
||||
subject?: string;
|
||||
signStatus?: string;
|
||||
providerStatus?: string;
|
||||
employeeName?: string;
|
||||
employeeMobile?: string;
|
||||
signUrl?: string;
|
||||
signUrlExpireAt?: string;
|
||||
signUrlValid: boolean;
|
||||
startedAt?: string;
|
||||
sentAt?: string;
|
||||
completedAt?: string;
|
||||
archivedAt?: string;
|
||||
archiveFileId?: number;
|
||||
lastError?: string;
|
||||
documents: OnboardingElectronicSignDocument[];
|
||||
historyProcesses: OnboardingElectronicSignHistory[];
|
||||
}
|
||||
|
||||
/** 已被新流程替代的电子签历史记录。 */
|
||||
export interface OnboardingElectronicSignHistory {
|
||||
processId: number;
|
||||
bizId: string;
|
||||
providerContractId?: string;
|
||||
signStatus?: string;
|
||||
providerStatus?: string;
|
||||
employeeName?: string;
|
||||
employeeMobile?: string;
|
||||
startedAt?: string;
|
||||
reissuedAt?: string;
|
||||
reissueReason?: string;
|
||||
lastError?: string;
|
||||
}
|
||||
|
||||
export interface OnboardingCompanyOption {
|
||||
id: number;
|
||||
companyName: string;
|
||||
@@ -282,48 +389,22 @@ export const onboardingTaskApi = {
|
||||
return response;
|
||||
},
|
||||
|
||||
/** 创建任务前生成并下载单份临时 DOCX 合同预览。 */
|
||||
/** 创建任务前生成并下载指定的一份临时 DOCX 合同预览。 */
|
||||
async previewContractBeforeCreate(params: Record<string, unknown>) {
|
||||
let response: any;
|
||||
try {
|
||||
response = await contractDocumentHttp.post<any>(
|
||||
"preview-before-create",
|
||||
params,
|
||||
{ responseType: "blob", loading: false, cancel: false }
|
||||
);
|
||||
} catch (error: any) {
|
||||
const errorBlob = error?.response?.data;
|
||||
if (errorBlob instanceof Blob) {
|
||||
try {
|
||||
const result = JSON.parse(await errorBlob.text());
|
||||
throw new Error(result?.msg || "合同预览生成失败");
|
||||
} catch (parseError) {
|
||||
if (!(parseError instanceof SyntaxError)) throw parseError;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const blob = response?.data;
|
||||
const contentType = String(
|
||||
response?.headers?.["content-type"] || blob?.type || ""
|
||||
return requestContractPreview(
|
||||
"preview-before-create",
|
||||
params,
|
||||
"合同预览生成失败"
|
||||
);
|
||||
if (
|
||||
blob instanceof Blob &&
|
||||
contentType.toLowerCase().includes("application/json")
|
||||
) {
|
||||
try {
|
||||
const result = JSON.parse(await blob.text());
|
||||
throw new Error(result?.msg || "合同预览生成失败");
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new Error("合同预览生成失败");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
return response;
|
||||
/** 创建任务前生成全部临时 DOCX 合同预览,并以 ZIP 下载。 */
|
||||
async previewAllContractsBeforeCreate(params: Record<string, unknown>) {
|
||||
return requestContractPreview(
|
||||
"preview-all-before-create",
|
||||
params,
|
||||
"合同预览打包生成失败"
|
||||
);
|
||||
},
|
||||
|
||||
create(params: Record<string, unknown>) {
|
||||
@@ -359,6 +440,52 @@ export const onboardingTaskApi = {
|
||||
);
|
||||
},
|
||||
|
||||
/** 查询任务当前电子签流程和全部合同文件。 */
|
||||
electronicSignDetail(taskId: number) {
|
||||
return electronicSignHttp.get<OnboardingElectronicSignDetail>("detail", {
|
||||
id: taskId
|
||||
});
|
||||
},
|
||||
|
||||
/** 手动发起;失败流程会从已保存的本地断点继续。 */
|
||||
initiateElectronicSign(taskId: number) {
|
||||
return electronicSignHttp.post<OnboardingElectronicSignDetail>("initiate", {
|
||||
taskId
|
||||
});
|
||||
},
|
||||
|
||||
/** 保留旧流程记录,并以新的业务编号重新发起一次签约。 */
|
||||
reinitiateElectronicSign(taskId: number, reason: string) {
|
||||
return electronicSignHttp.post<OnboardingElectronicSignDetail>("reinitiate", {
|
||||
taskId,
|
||||
reason
|
||||
});
|
||||
},
|
||||
|
||||
/** 主动同步签署状态,完成时后端自动执行归档。 */
|
||||
syncElectronicSign(taskId: number) {
|
||||
return electronicSignHttp.post<OnboardingElectronicSignDetail>("sync", {
|
||||
taskId
|
||||
});
|
||||
},
|
||||
|
||||
/** 获取有效的员工签署链接,旧链接失效时由后端重新生成。 */
|
||||
getElectronicSignLink(taskId: number) {
|
||||
return electronicSignHttp.post<OnboardingElectronicSignDetail>("get-link", {
|
||||
taskId
|
||||
});
|
||||
},
|
||||
|
||||
/** 催办当前待签员工。 */
|
||||
noticeElectronicSign(taskId: number) {
|
||||
return electronicSignHttp.post("notice", { taskId });
|
||||
},
|
||||
|
||||
/** 作废尚未完成的电子签流程。 */
|
||||
invalidElectronicSign(taskId: number, reason: string) {
|
||||
return electronicSignHttp.post("invalid", { taskId, reason });
|
||||
},
|
||||
|
||||
archive(params: {
|
||||
taskId: number;
|
||||
auditOpinion?: string;
|
||||
|
||||
@@ -152,38 +152,116 @@
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 2. 归档合同与协议 -->
|
||||
<el-tab-pane label="归档签署材料" name="contracts">
|
||||
<el-empty v-if="!detail.contracts?.length" description="暂无已归档的签署材料" />
|
||||
<div v-else class="contracts-list">
|
||||
<div v-for="contract in detail.contracts" :key="contract.id" class="contract-box">
|
||||
<div class="contract-info-left">
|
||||
<div class="contract-title font-bold">{{ contract.contractName }}</div>
|
||||
<div class="contract-meta-text text-secondary">
|
||||
<span>状态:{{ contract.contractStatus }}</span>
|
||||
<span v-if="contract.offlineSignedAt">· 线下签署于:{{ formatTime(contract.offlineSignedAt) }}</span>
|
||||
<!-- 2. 员工入职资料与合同文件 -->
|
||||
<el-tab-pane label="员工资料信息" name="materials">
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header-row">
|
||||
<div class="card-header-title">入职上传资料</div>
|
||||
<el-tag type="info" effect="plain">{{ detail.materials?.length || 0 }} 份</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-empty v-if="!detail.materials?.length" description="暂无员工上传资料" />
|
||||
<div v-else class="material-grid">
|
||||
<div v-for="item in detail.materials" :key="item.id" class="material-file-card">
|
||||
<div class="material-preview-box">
|
||||
<el-image
|
||||
v-if="isImageMaterial(item) && materialPreviewUrls[item.fileId]"
|
||||
class="material-preview-image"
|
||||
:src="materialPreviewUrls[item.fileId]"
|
||||
:preview-src-list="[materialPreviewUrls[item.fileId]]"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
hide-on-click-modal
|
||||
/>
|
||||
<div v-else class="material-preview-placeholder">
|
||||
<el-icon :size="34"><Document /></el-icon>
|
||||
<span>{{ materialPreviewText(item) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="material-file-info">
|
||||
<div class="material-name-row">
|
||||
<span class="material-name">{{ item.materialName || "员工资料" }}</span>
|
||||
<el-tag :type="materialReviewTagType(item.reviewStatus)" size="small" effect="plain">
|
||||
{{ materialReviewText(item.reviewStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="material-file-name" :title="item.fileName">{{ materialFileName(item) }}</div>
|
||||
<div class="material-file-meta">
|
||||
<span>{{ item.fileSizeInfo || "大小未知" }}</span>
|
||||
<span>上传于 {{ formatTime(item.uploadedAt) }}</span>
|
||||
</div>
|
||||
<div v-if="item.reviewRemark" class="material-review-remark">审核备注:{{ item.reviewRemark }}</div>
|
||||
<div class="material-file-actions">
|
||||
<span v-if="isImageMaterial(item)" class="preview-hint">
|
||||
{{ materialPreviewUrls[item.fileId] ? "点击图片可放大预览" : materialPreviewText(item) }}
|
||||
</span>
|
||||
<el-button link type="primary" icon="Download" @click="download(item.fileId, materialFileName(item))"> 下载原件 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contract-action-right">
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card mt-4">
|
||||
<template #header>
|
||||
<div class="card-header-row">
|
||||
<div class="card-header-title">合同与签署归档</div>
|
||||
<el-button
|
||||
v-if="contract.generatedDocxFileId"
|
||||
type="primary"
|
||||
v-if="detail.electronicSignArchiveFileId"
|
||||
type="success"
|
||||
plain
|
||||
size="small"
|
||||
icon="Download"
|
||||
@click="
|
||||
download(
|
||||
contract.generatedDocxFileId,
|
||||
`${detail?.employee?.employeeName || '员工'}_${contract.contractName || '入职合同'}.docx`
|
||||
)
|
||||
"
|
||||
@click="download(detail.electronicSignArchiveFileId, electronicSignArchiveName())"
|
||||
>
|
||||
下载签署材料 DOCX
|
||||
下载完整电子签归档 ZIP
|
||||
</el-button>
|
||||
<el-tag v-else type="info">尚未生成电子文件</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-if="!detail.contracts?.length && !detail.electronicSignArchiveFileId" description="暂无合同或签署归档文件" />
|
||||
<div v-if="detail.contracts?.length" class="contracts-list">
|
||||
<div v-for="contract in detail.contracts" :key="contract.id" class="contract-box">
|
||||
<div class="contract-info-left">
|
||||
<div class="contract-title font-bold">{{ contract.contractName }}</div>
|
||||
<div class="contract-meta-text text-secondary">
|
||||
<span>状态:{{ contractStatusText(contract.contractStatus) }}</span>
|
||||
<span v-if="contract.offlineSignedAt">· 线下签署于:{{ formatTime(contract.offlineSignedAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contract-action-right">
|
||||
<el-button
|
||||
v-if="contract.generatedPdfFileId"
|
||||
type="success"
|
||||
plain
|
||||
size="small"
|
||||
icon="Download"
|
||||
@click="download(contract.generatedPdfFileId, contractDownloadName(contract, 'pdf'))"
|
||||
>
|
||||
下载签后 PDF
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="contract.generatedDocxFileId"
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
icon="Download"
|
||||
@click="
|
||||
download(
|
||||
contract.generatedDocxFileId,
|
||||
`${detail?.employee?.employeeName || '员工'}_${contract.contractName || '入职合同'}.docx`
|
||||
)
|
||||
"
|
||||
>
|
||||
下载生成 DOCX
|
||||
</el-button>
|
||||
<el-tag v-if="!contract.generatedPdfFileId && !contract.generatedDocxFileId" type="info">尚未生成电子文件</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 3. 入职流转与审核日志 -->
|
||||
@@ -213,8 +291,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="onboardingEmployee">
|
||||
import { computed, ref, onMounted } from "vue";
|
||||
import { UserFilled, Download } from "@element-plus/icons-vue";
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||
import { Document, Download, UserFilled } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import {
|
||||
onboardingTaskApi,
|
||||
@@ -230,6 +308,18 @@ const detailVisible = ref(false);
|
||||
const activeTab = ref("basic");
|
||||
const detail = ref<any>();
|
||||
const employeeInfo = computed<Record<string, any>>(() => detail.value?.employeeInfo || {});
|
||||
const materialPreviewUrls = reactive<Record<number, string>>({});
|
||||
const materialPreviewLoading = reactive<Record<number, boolean>>({});
|
||||
const materialPreviewFailed = reactive<Record<number, boolean>>({});
|
||||
|
||||
const imageMimeTypes: Record<string, string> = {
|
||||
bmp: "image/bmp",
|
||||
gif: "image/gif",
|
||||
jpeg: "image/jpeg",
|
||||
jpg: "image/jpeg",
|
||||
png: "image/png",
|
||||
webp: "image/webp"
|
||||
};
|
||||
|
||||
const searchCompanies = ref<OnboardingCompanyOption[]>([]);
|
||||
const searchDepartments = ref<OnboardingDepartmentOption[]>([]);
|
||||
@@ -324,6 +414,114 @@ function parseJsonList(value?: string) {
|
||||
const educationHistory = computed(() => parseJsonList(employeeInfo.value.educationHistoryJson));
|
||||
const workHistory = computed(() => parseJsonList(employeeInfo.value.workHistoryJson));
|
||||
const familyMembers = computed(() => parseJsonList(employeeInfo.value.familyMembersJson));
|
||||
function materialFileSuffix(item: any) {
|
||||
const suffix = String(item?.fileSuffix || "")
|
||||
.trim()
|
||||
.replace(/^\./, "")
|
||||
.toLowerCase();
|
||||
if (suffix) return suffix;
|
||||
|
||||
const fileName = String(item?.fileName || "");
|
||||
const dotIndex = fileName.lastIndexOf(".");
|
||||
return dotIndex >= 0 ? fileName.substring(dotIndex + 1).toLowerCase() : "";
|
||||
}
|
||||
|
||||
function isImageMaterial(item: any) {
|
||||
return Boolean(imageMimeTypes[materialFileSuffix(item)]);
|
||||
}
|
||||
|
||||
function materialFileName(item: any) {
|
||||
if (item?.fileName) return String(item.fileName);
|
||||
|
||||
const suffix = materialFileSuffix(item);
|
||||
return "资料文件" + (suffix ? "." + suffix : "");
|
||||
}
|
||||
|
||||
function materialPreviewText(item: any) {
|
||||
if (!isImageMaterial(item)) return "非图片文件";
|
||||
if (materialPreviewLoading[item.fileId]) return "图片加载中";
|
||||
if (materialPreviewFailed[item.fileId]) return "图片预览失败";
|
||||
return "图片待加载";
|
||||
}
|
||||
|
||||
function materialReviewText(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: "待复核",
|
||||
passed: "审核合格",
|
||||
need_supplement: "需补充"
|
||||
};
|
||||
return map[status || ""] || status || "未知状态";
|
||||
}
|
||||
|
||||
function materialReviewTagType(status?: string): "success" | "danger" | "warning" | "info" {
|
||||
if (status === "passed") return "success";
|
||||
if (status === "need_supplement") return "danger";
|
||||
if (status === "pending") return "warning";
|
||||
return "info";
|
||||
}
|
||||
|
||||
function contractStatusText(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending_generate: "待生成",
|
||||
pending_confirm: "待员工确认",
|
||||
confirmed: "员工已确认",
|
||||
offline_pending_sign: "待线下签署",
|
||||
electronic_signing: "电子签署中",
|
||||
electronic_signed: "电子签已完成",
|
||||
archived: "已归档"
|
||||
};
|
||||
return map[status || ""] || status || "未知状态";
|
||||
}
|
||||
|
||||
function safeDownloadName(value: string) {
|
||||
return value.replace(/[\\/:*?"<>|]/g, "_");
|
||||
}
|
||||
|
||||
function contractDownloadName(contract: any, extension: "pdf" | "docx") {
|
||||
const employeeName = detail.value?.employee?.employeeName || "员工";
|
||||
const contractName = contract?.contractName || "入职合同";
|
||||
return safeDownloadName(employeeName + "_" + contractName) + "." + extension;
|
||||
}
|
||||
|
||||
function electronicSignArchiveName() {
|
||||
const employeeName = detail.value?.employee?.employeeName || "员工";
|
||||
return safeDownloadName(employeeName + "_电子签完整归档") + ".zip";
|
||||
}
|
||||
|
||||
function clearMaterialPreviews() {
|
||||
Object.values(materialPreviewUrls).forEach(url => URL.revokeObjectURL(url));
|
||||
Object.keys(materialPreviewUrls).forEach(key => delete materialPreviewUrls[Number(key)]);
|
||||
Object.keys(materialPreviewLoading).forEach(key => delete materialPreviewLoading[Number(key)]);
|
||||
Object.keys(materialPreviewFailed).forEach(key => delete materialPreviewFailed[Number(key)]);
|
||||
}
|
||||
|
||||
async function loadMaterialPreviews(materials: any[]) {
|
||||
clearMaterialPreviews();
|
||||
const imageMaterials = (materials || []).filter(item => item?.fileId && isImageMaterial(item));
|
||||
|
||||
await Promise.all(
|
||||
imageMaterials.map(async item => {
|
||||
materialPreviewLoading[item.fileId] = true;
|
||||
materialPreviewFailed[item.fileId] = false;
|
||||
|
||||
try {
|
||||
const response: any = await onboardingTaskApi.downloadFile(item.fileId);
|
||||
const sourceBlob = response?.data;
|
||||
if (!(sourceBlob instanceof Blob)) {
|
||||
throw new Error("服务器未返回有效的图片文件");
|
||||
}
|
||||
|
||||
const mimeType = imageMimeTypes[materialFileSuffix(item)];
|
||||
const imageBlob = sourceBlob.type.startsWith("image/") ? sourceBlob : new Blob([sourceBlob], { type: mimeType });
|
||||
materialPreviewUrls[item.fileId] = URL.createObjectURL(imageBlob);
|
||||
} catch {
|
||||
materialPreviewFailed[item.fileId] = true;
|
||||
} finally {
|
||||
materialPreviewLoading[item.fileId] = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const columns: ColumnProps<OnboardingEmployee>[] = [
|
||||
{
|
||||
@@ -439,9 +637,11 @@ async function showDetail(row: OnboardingEmployee) {
|
||||
id: row.id
|
||||
});
|
||||
|
||||
detail.value = data;
|
||||
const employeeDetail = data as any;
|
||||
detail.value = employeeDetail;
|
||||
activeTab.value = "basic";
|
||||
detailVisible.value = true;
|
||||
void loadMaterialPreviews(employeeDetail?.materials || []);
|
||||
}
|
||||
|
||||
function resolveDownloadFileName(response: any, fallbackFileName?: string) {
|
||||
@@ -456,7 +656,7 @@ function resolveDownloadFileName(response: any, fallbackFileName?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackFileName || "入职签署材料.docx";
|
||||
return fallbackFileName || "员工资料文件";
|
||||
}
|
||||
|
||||
async function download(fileId: number, fallbackFileName?: string) {
|
||||
@@ -476,7 +676,7 @@ async function download(fileId: number, fallbackFileName?: string) {
|
||||
link.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.msg || error?.message || "签署材料文件下载失败");
|
||||
ElMessage.error(error?.msg || error?.message || "文件下载失败");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,6 +721,7 @@ async function loadSearchOptions() {
|
||||
}
|
||||
|
||||
onMounted(loadSearchOptions);
|
||||
onBeforeUnmount(clearMaterialPreviews);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -604,6 +805,132 @@ onMounted(loadSearchOptions);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* 员工资料信息 */
|
||||
.card-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.material-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.material-file-card {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.material-preview-box {
|
||||
flex: 0 0 112px;
|
||||
width: 112px;
|
||||
height: 90px;
|
||||
margin-right: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.material-preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.material-preview-placeholder {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.material-file-info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.material-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.material-name {
|
||||
overflow: hidden;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.material-file-name {
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.material-file-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 5px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.material-review-remark {
|
||||
margin-top: 6px;
|
||||
color: #dc2626;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.material-file-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.preview-hint {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.contract-action-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.material-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 合同列表 */
|
||||
.contracts-list {
|
||||
display: flex;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1976,18 +1976,25 @@ onBeforeUnmount(() => {
|
||||
padding: 18px;
|
||||
background: #f8fafc;
|
||||
margin-bottom: 16px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.contract-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.contract-title {
|
||||
font-size: 16px;
|
||||
color: #0f172a;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.contract-desc {
|
||||
@@ -2012,10 +2019,32 @@ onBeforeUnmount(() => {
|
||||
gap: 12px;
|
||||
border-top: 1px dashed #cbd5e1;
|
||||
padding-top: 14px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.agree-checkbox {
|
||||
font-weight: 600;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-right: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.agree-checkbox :deep(.el-checkbox__input) {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.agree-checkbox :deep(.el-checkbox__label) {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding-left: 8px;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.contract-view-hint {
|
||||
@@ -2170,5 +2199,14 @@ onBeforeUnmount(() => {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.contract-sign-card {
|
||||
padding: 14px;
|
||||
}
|
||||
.contract-card-bottom > .el-button,
|
||||
.contract-card-bottom > .agree-checkbox {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user