前端双部署
This commit is contained in:
@@ -514,8 +514,8 @@
|
||||
<div v-for="f in getUploadedMaterials(mat.materialTypeId)" :key="`${f.id}-${f.fileId}`"
|
||||
class="file-badge">
|
||||
<!-- 图片点击缩略图即可在手机端放大预览,非图片继续显示文件图标。 -->
|
||||
<el-image v-if="isImageFile(f)" class="material-thumbnail" :src="getMaterialSourceUrl(f)"
|
||||
:preview-src-list="[getMaterialSourceUrl(f)]" fit="cover" preview-teleported />
|
||||
<el-image v-if="isImageFile(f) && getMaterialSourceUrl(f)" class="material-thumbnail"
|
||||
:src="getMaterialSourceUrl(f)" :preview-src-list="[getMaterialSourceUrl(f)]" fit="cover" preview-teleported />
|
||||
<el-icon v-else class="material-file-icon" color="#059669">
|
||||
<Document />
|
||||
</el-icon>
|
||||
@@ -529,8 +529,9 @@
|
||||
</div>
|
||||
|
||||
<div v-if="canEdit" class="file-actions">
|
||||
<el-upload :action="getMaterialUploadUrl(mat.materialTypeId, f.id)" :accept="getUploadAccept(mat)"
|
||||
<el-upload action="#" :accept="getUploadAccept(mat)"
|
||||
:disabled="isMaterialUploading(mat.materialTypeId)" :show-file-list="false"
|
||||
:http-request="(options: any) => uploadMaterial(options, mat.materialTypeId, f.id)"
|
||||
:on-success="(response: any) => onUploadSuccess(response, mat.materialTypeId)"
|
||||
:on-error="(error: Error) => onUploadError(error, mat.materialTypeId)"
|
||||
:before-upload="(file: File) => beforeUpload(file, mat)">
|
||||
@@ -547,8 +548,9 @@
|
||||
</div>
|
||||
|
||||
<div v-if="canEdit && getRemainingFileCount(mat) > 0" class="upload-action-box">
|
||||
<el-upload :action="getMaterialUploadUrl(mat.materialTypeId)" :accept="getUploadAccept(mat)"
|
||||
<el-upload action="#" :accept="getUploadAccept(mat)"
|
||||
:disabled="isMaterialUploading(mat.materialTypeId)" :show-file-list="false"
|
||||
:http-request="(options: any) => uploadMaterial(options, mat.materialTypeId)"
|
||||
:on-success="(response: any) => onUploadSuccess(response, mat.materialTypeId)"
|
||||
:on-error="(error: Error) => onUploadError(error, mat.materialTypeId)"
|
||||
:before-upload="(file: File) => beforeUpload(file, mat)">
|
||||
@@ -642,8 +644,8 @@
|
||||
|
||||
<div class="contract-card-bottom">
|
||||
<el-button :type="isContractPrepareFailed(c.id) ? 'danger' : 'primary'" plain
|
||||
:icon="isContractPrepareFailed(c.id) ? 'Refresh' : 'Download'" :loading="isContractPreparing(c.id)"
|
||||
:disabled="!isContractPrepared(c.id) && !isContractPrepareFailed(c.id)"
|
||||
:icon="isContractPrepareFailed(c.id) ? 'Refresh' : 'Download'" :loading="isContractBusy(c.id)"
|
||||
:disabled="isContractDownloading(c.id) || (!isContractPrepared(c.id) && !isContractPrepareFailed(c.id))"
|
||||
@click="handleContractAction(c)">
|
||||
{{ getContractActionText(c.id) }}
|
||||
</el-button>
|
||||
@@ -682,9 +684,9 @@ import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch }
|
||||
import { useRoute } from "vue-router";
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
||||
import {
|
||||
OnboardingTask,
|
||||
OnboardingPublicTask,
|
||||
onboardingPublicApi
|
||||
} from "@/api/modules/biz/onboarding";
|
||||
} from "@/api/modules/open/onboarding";
|
||||
import {
|
||||
getIdCardBirthday,
|
||||
getIdCardGender,
|
||||
@@ -713,8 +715,11 @@ const uploadingMaterials = reactive<Record<string, boolean>>({});
|
||||
type ContractPrepareStatus = "idle" | "preparing" | "ready" | "error";
|
||||
const contractPrepareStatus = reactive<Record<string, ContractPrepareStatus>>({});
|
||||
const contractPrepareErrors = reactive<Record<string, string>>({});
|
||||
const contractDownloading = reactive<Record<string, boolean>>({});
|
||||
const materialPreviewUrls = reactive<Record<string, string>>({});
|
||||
let materialPreviewGeneration = 0;
|
||||
|
||||
const task = ref<OnboardingTask | null>(null);
|
||||
const task = ref<OnboardingPublicTask | null>(null);
|
||||
const contracts = ref<any[]>([]);
|
||||
const materialRequirements = ref<any[]>([]);
|
||||
const uploadedMaterials = ref<any[]>([]);
|
||||
@@ -1405,6 +1410,7 @@ async function load(silent = false) {
|
||||
try { Object.assign(customVariables, JSON.parse(data.employeeInfo.customVariablesJson)); } catch { }
|
||||
}
|
||||
}
|
||||
await refreshMaterialPreviews();
|
||||
} catch (err: any) {
|
||||
error.value = err.message || "无法加载入职任务";
|
||||
} finally {
|
||||
@@ -1492,23 +1498,60 @@ async function nextStep() {
|
||||
}
|
||||
}
|
||||
|
||||
function getMaterialUploadUrl(materialTypeId: number, taskMaterialId?: number) {
|
||||
const query = new URLSearchParams({
|
||||
token: token.value,
|
||||
materialTypeId: String(materialTypeId)
|
||||
});
|
||||
if (taskMaterialId) {
|
||||
query.set("taskMaterialId", String(taskMaterialId));
|
||||
}
|
||||
return `/api/open/onboarding/upload-material?${query.toString()}`;
|
||||
function materialPreviewKey(file: any) {
|
||||
return `${file.id}-${file.fileId || 0}`;
|
||||
}
|
||||
|
||||
function getMaterialSourceUrl(file: any) {
|
||||
return onboardingPublicApi.materialSourceUrl({
|
||||
token: token.value,
|
||||
taskMaterialId: file.id,
|
||||
fileId: file.fileId
|
||||
});
|
||||
return materialPreviewUrls[materialPreviewKey(file)] || "";
|
||||
}
|
||||
|
||||
function revokeMaterialPreviewUrls() {
|
||||
Object.values(materialPreviewUrls).forEach(url => URL.revokeObjectURL(url));
|
||||
Object.keys(materialPreviewUrls).forEach(key => delete materialPreviewUrls[key]);
|
||||
}
|
||||
|
||||
async function refreshMaterialPreviews() {
|
||||
const generation = ++materialPreviewGeneration;
|
||||
revokeMaterialPreviewUrls();
|
||||
|
||||
await Promise.all(uploadedMaterials.value
|
||||
.filter(file => isImageFile(file))
|
||||
.map(async file => {
|
||||
try {
|
||||
const result = await onboardingPublicApi.downloadMaterial({
|
||||
token: token.value,
|
||||
taskMaterialId: file.id,
|
||||
fileId: file.fileId
|
||||
}, false);
|
||||
const objectUrl = URL.createObjectURL(result.blob);
|
||||
if (generation !== materialPreviewGeneration) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
return;
|
||||
}
|
||||
materialPreviewUrls[materialPreviewKey(file)] = objectUrl;
|
||||
} catch {
|
||||
// 单张历史图片损坏时保留文件信息,不阻断整个入职任务。
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async function uploadMaterial(
|
||||
options: any,
|
||||
materialTypeId: number,
|
||||
taskMaterialId?: number
|
||||
) {
|
||||
try {
|
||||
const response = await onboardingPublicApi.uploadMaterial({
|
||||
token: token.value,
|
||||
materialTypeId,
|
||||
taskMaterialId,
|
||||
file: options.file
|
||||
});
|
||||
options.onSuccess(response);
|
||||
} catch (error: any) {
|
||||
options.onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
function isMaterialUploading(materialTypeId: number) {
|
||||
@@ -1547,9 +1590,11 @@ async function onUploadSuccess(res: any, materialTypeId: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function onUploadError(error: Error, materialTypeId: number) {
|
||||
function onUploadError(error: any, materialTypeId: number) {
|
||||
setMaterialUploading(materialTypeId, false);
|
||||
ElMessage.error(error.message || "材料上传失败,请稍后重试");
|
||||
if (!error?.reported) {
|
||||
ElMessage.error(error?.message || "材料上传失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile(id: number) {
|
||||
@@ -1594,6 +1639,14 @@ function isContractPreparing(taskContractId: number) {
|
||||
return getContractPrepareStatus(taskContractId) === "preparing";
|
||||
}
|
||||
|
||||
function isContractDownloading(taskContractId: number) {
|
||||
return Boolean(contractDownloading[String(taskContractId)]);
|
||||
}
|
||||
|
||||
function isContractBusy(taskContractId: number) {
|
||||
return isContractPreparing(taskContractId) || isContractDownloading(taskContractId);
|
||||
}
|
||||
|
||||
function isContractPrepared(taskContractId: number) {
|
||||
return getContractPrepareStatus(taskContractId) === "ready";
|
||||
}
|
||||
@@ -1603,6 +1656,9 @@ function isContractPrepareFailed(taskContractId: number) {
|
||||
}
|
||||
|
||||
function getContractActionText(taskContractId: number) {
|
||||
if (isContractDownloading(taskContractId)) {
|
||||
return "正在下载签署材料";
|
||||
}
|
||||
const status = getContractPrepareStatus(taskContractId);
|
||||
if (status === "preparing") {
|
||||
return "正在准备签署材料";
|
||||
@@ -1670,23 +1726,105 @@ async function handleContractAction(contract: any) {
|
||||
}
|
||||
|
||||
if (isContractPrepared(contract.id)) {
|
||||
previewAndDownloadContract(contract);
|
||||
await previewAndDownloadContract(contract);
|
||||
}
|
||||
}
|
||||
|
||||
function previewAndDownloadContract(c: any) {
|
||||
const url = onboardingPublicApi.contractSourceUrl({
|
||||
token: token.value,
|
||||
taskContractId: c.id,
|
||||
fileVersion: c.generatedDocxFileId
|
||||
});
|
||||
function normalizeDownloadFileName(fileName: string | undefined, fallback: string) {
|
||||
const normalized = String(fileName || fallback)
|
||||
.replace(/[\\/:*?"<>|]/g, "_")
|
||||
.trim();
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
c.hasViewed = true;
|
||||
c.isAgreed = false;
|
||||
// 下载地址必须在用户点击事件中直接打开,微信及部分手机浏览器会拦截异步跳转空白页。
|
||||
const downloadWindow = window.open(url, "_blank");
|
||||
if (!downloadWindow) {
|
||||
window.location.assign(url);
|
||||
function triggerContractDownload(blob: Blob, fileName: string) {
|
||||
if (!blob.size) {
|
||||
throw new Error("服务器返回的合同文件为空");
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = objectUrl;
|
||||
link.download = fileName;
|
||||
link.rel = "noopener noreferrer";
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
|
||||
// 安卓 WebView 可能延迟接管 Blob 下载,不能过早释放。
|
||||
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
|
||||
}
|
||||
|
||||
function buildDirectContractDownloadUrl(c: any) {
|
||||
const url = new URL("/api/open/onboarding/contract-source", window.location.origin);
|
||||
url.searchParams.set("token", token.value);
|
||||
url.searchParams.set("taskContractId", String(c.id));
|
||||
if (c.generatedDocxFileId) {
|
||||
url.searchParams.set("version", String(c.generatedDocxFileId));
|
||||
}
|
||||
url.searchParams.set("_t", String(Date.now()));
|
||||
return `${url.pathname}${url.search}`;
|
||||
}
|
||||
|
||||
function triggerDirectContractDownload(url: string) {
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
async function previewAndDownloadContract(c: any) {
|
||||
const key = String(c.id);
|
||||
if (contractDownloading[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
contractDownloading[key] = true;
|
||||
try {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
if (/Android/i.test(userAgent)) {
|
||||
// 安卓 WebView 通常不支持 blob: 下载,直接交给系统的 HTTP 下载能力处理。
|
||||
// 此处在用户点击事件后的第一个同步分支触发,避免被 WebView 当成弹窗拦截。
|
||||
triggerDirectContractDownload(buildDirectContractDownloadUrl(c));
|
||||
c.hasViewed = true;
|
||||
c.isAgreed = false;
|
||||
ElMessage.success("签署材料下载已发起,请使用 Word/WPS 打开核对");
|
||||
if (/MicroMessenger/i.test(userAgent)) {
|
||||
void ElMessageBox.alert(
|
||||
"如果微信中没有出现文件,请点击右上角“…”选择“在浏览器打开”,然后再次点击下载。",
|
||||
"安卓微信下载提示",
|
||||
{ confirmButtonText: "我知道了" }
|
||||
).catch(() => undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await onboardingPublicApi.downloadContract({
|
||||
token: token.value,
|
||||
taskContractId: c.id,
|
||||
fileVersion: c.generatedDocxFileId
|
||||
});
|
||||
const fallbackName = `${c.contractName || "入职签署材料"}.docx`;
|
||||
triggerContractDownload(
|
||||
result.blob,
|
||||
normalizeDownloadFileName(result.fileName, fallbackName)
|
||||
);
|
||||
|
||||
c.hasViewed = true;
|
||||
c.isAgreed = false;
|
||||
ElMessage.success("签署材料已开始下载,请使用 Word/WPS 打开核对");
|
||||
|
||||
} catch (err: any) {
|
||||
if (!err?.reported) {
|
||||
ElMessage.error(err?.message || "签署材料下载失败,请稍后重试");
|
||||
}
|
||||
} finally {
|
||||
contractDownloading[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1773,6 +1911,8 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
materialPreviewGeneration++;
|
||||
revokeMaterialPreviewUrls();
|
||||
if (linkExpiryTimer !== undefined) {
|
||||
window.clearInterval(linkExpiryTimer);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user