diff --git a/src/api/launch.js b/src/api/launch.js index 8534ca0..249d5f2 100644 --- a/src/api/launch.js +++ b/src/api/launch.js @@ -106,6 +106,44 @@ export function finishLaunchSeal(id, data) { }); } +export function initLaunchSealPosition(id) { + return request({ + url: `/contract/launch/${id}/seal-position/init`, + method: "post", + }); +} + +export function getLaunchSealPositionPage(id) { + return request({ + url: `/contract/launch/${id}/seal-position/list`, + method: "get", + }); +} + +export function saveLaunchSealPositions(id, data = {}) { + return request({ + url: `/contract/launch/${id}/seal-position/save`, + method: "post", + data, + }); +} + +export function batchUpdateLaunchSealPositionStatus(id, data) { + return request({ + url: `/contract/launch/${id}/seal-position/batch-status`, + method: "post", + data, + }); +} + +export function checkLaunchSealPositions(id, data = {}) { + return request({ + url: `/contract/launch/${id}/seal-position/check`, + method: "post", + data, + }); +} + export function queryApproveViewUrl(data) { return request({ url: "/contract/push/settlement/approve/view-url", diff --git a/src/api/seal.js b/src/api/seal.js index f235130..bde7289 100644 --- a/src/api/seal.js +++ b/src/api/seal.js @@ -175,7 +175,7 @@ export function downloadContract(contractId) { } // ======================================== -// 回调轮询 +// 回调轮询 / 测试回调 // ======================================== export function pollCallback(requestId) { return request({ @@ -183,3 +183,11 @@ export function pollCallback(requestId) { method: 'get', }) } + +export function mockPreSignSuccessCallback(data) { + return request({ + url: '/seal/callback/presign/mock-success', + method: 'post', + data, + }) +} diff --git a/src/views/contract/launch/components/1.vue b/src/views/contract/launch/components/1.vue new file mode 100644 index 0000000..7090c1e --- /dev/null +++ b/src/views/contract/launch/components/1.vue @@ -0,0 +1,598 @@ + + + + + \ No newline at end of file diff --git a/src/views/contract/launch/components/2.vue b/src/views/contract/launch/components/2.vue new file mode 100644 index 0000000..d98cb5a --- /dev/null +++ b/src/views/contract/launch/components/2.vue @@ -0,0 +1,727 @@ + + + + + \ No newline at end of file diff --git a/src/views/contract/launch/components/SealPositionConfigModal.vue b/src/views/contract/launch/components/SealPositionConfigModal.vue new file mode 100644 index 0000000..e5bcd43 --- /dev/null +++ b/src/views/contract/launch/components/SealPositionConfigModal.vue @@ -0,0 +1,830 @@ + + + + + diff --git a/src/views/contract/launch/components/SealPositionSignModal.vue b/src/views/contract/launch/components/SealPositionSignModal.vue new file mode 100644 index 0000000..da9a0e6 --- /dev/null +++ b/src/views/contract/launch/components/SealPositionSignModal.vue @@ -0,0 +1,1438 @@ + + + + + diff --git a/src/views/contract/launch/components/TemplateRichTextEditor.vue b/src/views/contract/launch/components/TemplateRichTextEditor.vue index 2933cbc..5e8d4cb 100644 --- a/src/views/contract/launch/components/TemplateRichTextEditor.vue +++ b/src/views/contract/launch/components/TemplateRichTextEditor.vue @@ -15,7 +15,7 @@ @update:modelValue="handleChange" />
- 当前支持普通文字、列表和基础表格。为保证导出 Word 效果,建议不要使用合并单元格和复杂样式。 + 支持普通文字、列表和表格。复制带合并单元格的表格时会自动规整结构,建议粘贴后结合右侧预览确认最终 Word 效果。
@@ -44,6 +44,19 @@ const emit = defineEmits(['update:modelValue']) const mode = 'default' const editorRef = shallowRef(null) +const TABLE_STYLE_PROPS = [ + 'width', + 'min-width', + 'max-width', + 'text-align', + 'vertical-align', + 'background-color', + 'color', + 'font-weight', + 'font-style', + 'text-decoration', + 'white-space', +] const toolbarConfig = { toolbarKeys: [ @@ -83,6 +96,7 @@ const editorConfig = computed(() => ({ readOnly: props.disabled, autoFocus: false, scroll: true, + customPaste: handleCustomPaste, hoverbarKeys: { table: { menuKeys: [ @@ -114,6 +128,193 @@ function handleChange(value) { emit('update:modelValue', value || '') } +function handleCustomPaste(editor, event) { + const html = event?.clipboardData?.getData('text/html') || '' + if (!html || !/]/i.test(html)) { + return true + } + + const normalizedHtml = normalizePastedHtml(html) + if (!normalizedHtml) { + return true + } + + event.preventDefault() + editor.dangerouslyInsertHtml(normalizedHtml) + return false +} + +function normalizePastedHtml(html) { + const parser = new window.DOMParser() + const doc = parser.parseFromString(html, 'text/html') + const body = doc.body + if (!body) { + return '' + } + + body.querySelectorAll('script, style, meta, link, title').forEach((node) => node.remove()) + body.querySelectorAll('table').forEach((table) => normalizePastedTable(table, doc)) + body.querySelectorAll('*').forEach((node) => cleanupPastedNode(node)) + + return body.innerHTML.trim() +} + +function normalizePastedTable(table, doc) { + const normalizedTable = doc.createElement('table') + normalizedTable.style.width = normalizeTableWidth(table) || '100%' + normalizedTable.style.borderCollapse = 'collapse' + normalizedTable.style.tableLayout = 'fixed' + normalizedTable.style.margin = '8px 0' + + extractPastedTableRows(table).forEach((row) => { + const normalizedRow = doc.createElement('tr') + Array.from(row.children || []).forEach((cell) => { + const tag = String(cell.tagName || '').toLowerCase() === 'th' ? 'th' : 'td' + const normalizedCell = doc.createElement(tag) + copyTableSpanAttribute(cell, normalizedCell, 'rowspan') + copyTableSpanAttribute(cell, normalizedCell, 'colspan') + applyNormalizedCellStyle(cell, normalizedCell) + normalizedCell.innerHTML = sanitizeCellInnerHtml(cell.innerHTML) + normalizedRow.appendChild(normalizedCell) + }) + normalizedTable.appendChild(normalizedRow) + }) + + table.replaceWith(normalizedTable) +} + +function extractPastedTableRows(table) { + const rows = Array.from(table.querySelectorAll(':scope > tr')) + if (rows.length) { + return rows + } + + const sectionRows = ['thead', 'tbody', 'tfoot'].flatMap((tagName) => + Array.from(table.querySelectorAll(`:scope > ${tagName} > tr`)), + ) + if (sectionRows.length) { + return sectionRows + } + + return Array.from(table.querySelectorAll('tr')) +} + +function normalizeTableWidth(table) { + const inlineWidth = table.style?.width || table.getAttribute('width') || '' + const normalized = String(inlineWidth || '').trim() + if (!normalized) { + return '100%' + } + if (/^\d+(\.\d+)?$/.test(normalized)) { + return `${normalized}px` + } + return normalized +} + +function copyTableSpanAttribute(sourceCell, targetCell, attrName) { + const rawValue = sourceCell.getAttribute(attrName) + const spanValue = Number.parseInt(String(rawValue || '').trim(), 10) + if (Number.isFinite(spanValue) && spanValue > 1) { + targetCell.setAttribute(attrName, String(spanValue)) + } +} + +function applyNormalizedCellStyle(sourceCell, targetCell) { + const textAlign = sourceCell.style?.textAlign || sourceCell.getAttribute('align') || '' + const verticalAlign = sourceCell.style?.verticalAlign || sourceCell.getAttribute('valign') || '' + const backgroundColor = sourceCell.style?.backgroundColor || sourceCell.getAttribute('bgcolor') || '' + const width = sourceCell.style?.width || sourceCell.getAttribute('width') || '' + + targetCell.style.border = '1px solid #1f2937' + targetCell.style.padding = '6px 8px' + targetCell.style.verticalAlign = normalizeVerticalAlign(verticalAlign) || 'middle' + targetCell.style.wordBreak = 'break-word' + targetCell.style.whiteSpace = 'pre-wrap' + + if (textAlign) { + targetCell.style.textAlign = textAlign + } + if (backgroundColor) { + targetCell.style.backgroundColor = backgroundColor + } + if (width) { + targetCell.style.width = /^\d+(\.\d+)?$/.test(width) ? `${width}px` : width + } + + if (String(sourceCell.tagName || '').toLowerCase() === 'th') { + targetCell.style.fontWeight = '700' + targetCell.style.textAlign = targetCell.style.textAlign || 'center' + } +} + +function sanitizeCellInnerHtml(html) { + const parser = new window.DOMParser() + const doc = parser.parseFromString(`
${html || ''}
`, 'text/html') + const wrapper = doc.body.firstElementChild + if (!wrapper) { + return '' + } + + wrapper.querySelectorAll('script, style, meta, link, title').forEach((node) => node.remove()) + Array.from(wrapper.querySelectorAll('*')).forEach((node) => { + cleanupPastedNode(node) + if (String(node.tagName || '').toLowerCase() === 'table') { + normalizePastedTable(node, doc) + } + }) + return wrapper.innerHTML +} + +function cleanupPastedNode(node) { + if (!(node instanceof window.HTMLElement)) { + return + } + + node.removeAttribute('class') + node.removeAttribute('lang') + node.removeAttribute('dir') + node.removeAttribute('data-mce-style') + + const tag = String(node.tagName || '').toLowerCase() + if (tag === 'table' || tag === 'td' || tag === 'th' || tag === 'tr') { + retainInlineStyle(node, TABLE_STYLE_PROPS) + return + } + + retainInlineStyle(node, [ + 'text-align', + 'background-color', + 'color', + 'font-weight', + 'font-style', + 'text-decoration', + 'white-space', + ]) +} + +function retainInlineStyle(node, allowedProperties = []) { + const preservedEntries = allowedProperties + .map((propertyName) => { + const value = node.style?.getPropertyValue(propertyName) || '' + return value ? `${propertyName}:${value}` : '' + }) + .filter(Boolean) + + if (preservedEntries.length) { + node.setAttribute('style', preservedEntries.join(';')) + } else { + node.removeAttribute('style') + } +} + +function normalizeVerticalAlign(value) { + const normalized = String(value || '').trim().toLowerCase() + if (['top', 'middle', 'center', 'bottom'].includes(normalized)) { + return normalized === 'center' ? 'middle' : normalized + } + return '' +} + onBeforeUnmount(() => { const editor = editorRef.value if (editor) { @@ -146,10 +347,47 @@ onBeforeUnmount(() => { min-height: 240px !important; } +.editor-content :deep(.w-e-scroll) { + padding: 8px 0; +} + .editor-content :deep(.w-e-text-placeholder) { top: 14px; } +.editor-content :deep(.w-e-text-container [data-slate-editor]) { + padding: 0 14px 12px; +} + +.editor-content :deep(table) { + width: 100%; + max-width: 100%; + margin: 8px 0; + border-collapse: collapse; + table-layout: fixed; + background: #fff; +} + +.editor-content :deep(table td), +.editor-content :deep(table th) { + min-width: 84px; + border: 1px solid #1f2937 !important; + padding: 6px 8px; + vertical-align: middle; + word-break: break-word; + white-space: pre-wrap; +} + +.editor-content :deep(table th) { + font-weight: 700; + text-align: center; + background: #f8fafc; +} + +.editor-content :deep(table p) { + margin: 0; +} + .editor-tip { padding: 8px 12px 10px; font-size: 12px; diff --git a/src/views/contract/launch/components/mixedSignModal.vue b/src/views/contract/launch/components/mixedSignModal.vue index a9227f1..29800cf 100644 --- a/src/views/contract/launch/components/mixedSignModal.vue +++ b/src/views/contract/launch/components/mixedSignModal.vue @@ -177,6 +177,16 @@ 合同状态 {{ contractStatusText }} +
文件页数 {{ pdfImages.length }} 页 @@ -189,6 +199,19 @@ 已签印章 {{ signedSealCount }} 个
+
+
切换代表签署方
+ +
+
+ 当前合同已配置签章位。只能把印章拖到属于当前签署方的指定框内,系统会自动吸附到框中心。 +
@@ -201,6 +224,7 @@ width: displayWidth + 'px', height: displayHeight + 'px', }" + :data-page-num="idx + 1" @dragover.prevent @drop.prevent="onDropSeal($event, idx + 1)" @contextmenu.prevent.stop="showCtxMenu($event, idx + 1)" @@ -215,6 +239,23 @@ draggable="false" /> +
+
+ {{ box.label || '未命名签章位' }} + 必签 +
+
{{ getPartyLabel(box.partyId) }}
+
{{ getSealPositionStatusText(box.signedStatus) }}
+
印章:{{ box.sealName }}
+
签署人:{{ box.signedByName }}
+
+
1、获取签章
+
+ 当前用户未匹配到可签署方,暂时只能查看签章位,不能放章。 +
@@ -645,6 +689,13 @@
+ + 测试确认签署 + 关闭 @@ -689,6 +740,7 @@ import { completeContract, downloadContract, pollCallback, + mockPreSignSuccessCallback, getTimestamp, signTimestamp, getContractDetail, @@ -702,6 +754,9 @@ import { saveLaunchSealInfo, getCounterpartyConfirm, confirmCounterparty, + getLaunchSealPositionPage, + batchUpdateLaunchSealPositionStatus, + checkLaunchSealPositions, } from "@/api/launch.js"; const SUCCESS_CODE = "0000"; @@ -732,6 +787,12 @@ const completeSignDate = ref(""); const completeSealDate = ref(""); const completeActiveDate = ref(""); +const SEAL_POS_STATUS_EMPTY = 0; +const SEAL_POS_STATUS_SELECTED = 1; +const SEAL_POS_STATUS_SIGNING = 2; +const SEAL_POS_STATUS_SIGNED = 3; +const SEAL_POS_STATUS_FAILED = 4; + const businessStatusLabelMap = { draft: "草稿", reviewing: "审批中", @@ -790,6 +851,45 @@ const businessInfoRows = computed(() => { .map(([label, value]) => ({ label, value: String(value) })); }); +const sealPositionEnabled = computed(() => { + return !!businessRecord.value?.id && sealPositionState.enabled && sealPositionState.positions.length > 0; +}); + +const selectedPartyOptions = computed(() => { + const sourcePartyIds = [...new Set((sealPositionState.availablePartyIds || []) + .map((item) => Number(item)) + .filter((item) => !Number.isNaN(item)))]; + return sourcePartyIds + .map((partyId) => { + const party = findPartyById(partyId); + if (!party) return null; + return { + value: party.id, + label: `${party.roleName || party.roleCode || "签署方"} - ${party.companyName || party.subjectCode || party.id}`, + }; + }) + .filter(Boolean); +}); + +const sealPositionPartySelectorVisible = computed(() => selectedPartyOptions.value.length > 1); +const activeSealPositionPartyLabel = computed(() => { + if (!selectedPartyId.value) { + return selectedPartyOptions.value.length ? "请选择签署方" : "当前用户未匹配到可签署方"; + } + return getPartyLabel(selectedPartyId.value); +}); + +const mySealPositionBoxes = computed(() => { + return sealPositionState.positions.filter((item) => Number(item.partyId) === Number(selectedPartyId.value)); +}); + +const currentRequiredMissingCount = computed(() => { + if (!sealPositionEnabled.value) { + return 0; + } + return mySealPositionBoxes.value.filter((item) => isRequiredSealPositionUnfinished(item)).length; +}); + function formatBusinessAmount(value) { if (value === undefined || value === null || value === "") return ""; return `${new Intl.NumberFormat("zh-CN", { @@ -830,6 +930,132 @@ function normalizePdfPreviewImages(list = []) { .filter(Boolean); } +function wait(ms) { + return new Promise((resolve) => { + window.setTimeout(resolve, ms); + }); +} + +function roundValue(value) { + return Math.round(Number(value || 0) * 100) / 100; +} + +function findPartyById(partyId) { + return (businessRecord.value?.partyList || []).find((item) => Number(item.id) === Number(partyId)) || null; +} + +function getPartyLabel(partyId) { + const party = findPartyById(partyId); + if (!party) { + return "未识别签署方"; + } + return `${party.roleName || party.roleCode || "签署方"} - ${party.companyName || party.subjectCode || party.id}`; +} + +function normalizeSealPositionRequired(required) { + return required === "N" ? "N" : "Y"; +} + +function getSealPositionStatusText(status) { + const value = Number(status); + if (value === SEAL_POS_STATUS_SELECTED) return "待签署"; + if (value === SEAL_POS_STATUS_SIGNING) return "待签署"; + if (value === SEAL_POS_STATUS_SIGNED) return "已签署"; + if (value === SEAL_POS_STATUS_FAILED) return "待签署"; + return "待签署"; +} + +function getSealPositionBoxStyle(box) { + return { + left: `${Number(box.x || 0) * scaleRatio.value}px`, + top: `${Number(box.y || 0) * scaleRatio.value}px`, + width: `${Number(box.width || 0) * scaleRatio.value}px`, + height: `${Number(box.height || 0) * scaleRatio.value}px`, + }; +} + +function getPageSealPositionBoxes(pageNum) { + return sealPositionState.positions.filter((item) => Number(item.pageNum) === Number(pageNum)); +} + +function isMySealPositionBox(box) { + return Number(box.partyId) === Number(selectedPartyId.value); +} + +function isRequiredSealPositionUnfinished(box) { + return isMySealPositionBox(box) + && normalizeSealPositionRequired(box.required) === "Y" + && Number(box.signedStatus) !== SEAL_POS_STATUS_SIGNED; +} + +function getSealPositionBoxClass(box) { + return { + "seal-position-box": true, + "is-mine": isMySealPositionBox(box), + "is-other": !isMySealPositionBox(box), + "is-signed": Number(box.signedStatus) === SEAL_POS_STATUS_SIGNED, + "is-missing": isRequiredSealPositionUnfinished(box) && Number(box.signedStatus) !== SEAL_POS_STATUS_SELECTED && Number(box.signedStatus) !== SEAL_POS_STATUS_SIGNING && !findSealItemByBoxId(box.id), + }; +} + +function buildSealPositionPlacement(box, scale = 1) { + const boxX = Number(box.x || 0); + const boxY = Number(box.y || 0); + const boxWidth = Math.max(0, Number(box.width || 0)); + const boxHeight = Math.max(0, Number(box.height || 0)); + const renderWidth = roundValue(170 * scale); + const renderHeight = renderWidth; + const offsetX = roundValue(Math.max(0, (boxWidth - renderWidth) / 2)); + const offsetY = roundValue(Math.max(0, (boxHeight - renderHeight) / 2)); + return { + x: roundValue(boxX + offsetX), + y: roundValue(boxY + offsetY), + scale, + }; +} + +function findSealItemByBoxId(boxId) { + return sealItems.value.find((item) => Number(item._sealPositionBoxId) === Number(boxId)) || null; +} + +function getBoxById(boxId) { + return sealPositionState.positions.find((item) => Number(item.id) === Number(boxId)) || null; +} + +function buildSealItemFromBox(box) { + const scale = 1; + const placement = buildSealPositionPlacement(box, scale); + return { + id: ++sealPlacedId, + _type: "seal", + _sealPositionBoxId: box.id, + sealId: box.sealId || "", + sealName: box.sealName || box.label || "印章", + pictureUrl: "", + _pictureUrl: "", + owner: "", + pageNum: Number(box.pageNum || 1), + x: placement.x, + y: placement.y, + scale, + status: "pending", + _signer: box.signedByName || "", + }; +} + +function syncSealItemsFromSealPositions() { + if (!sealPositionEnabled.value) { + return; + } + const timestampItems = sealItems.value.filter((item) => item._type === "timestamp"); + const localPendingSeals = sealItems.value.filter((item) => { + if (item._type !== "seal") return false; + const box = getBoxById(item._sealPositionBoxId); + return !!box && Number(box.signedStatus) !== SEAL_POS_STATUS_SIGNED; + }); + sealItems.value = [...timestampItems, ...localPendingSeals]; +} + function syncCompletionDates(record = {}) { completeSignDate.value = sliceDateValue(record?.signDate) || todayString(); completeSealDate.value = sliceDateValue(record?.sealDate) || completeSignDate.value; @@ -982,7 +1208,7 @@ function buildSignedFileInfo(downloadData = {}) { `${businessRecord.value?.contractName || contractName.value || "已签署合同"}.pdf`; return { signedFileName: signedName, - signedFilePath: signedUrl || contractId.value || "", + signedFilePath: signedUrl || "", signedFileType: signedName.split(".").pop()?.toLowerCase() || "pdf", signedFileSize: downloadData.fileSize || downloadData.size || null, }; @@ -1040,6 +1266,7 @@ function resetModalState(record = null) { fetchingSeals.value = false; sealList.value = []; resetSealPlacementState(); + resetSealPositionState(); tsImageUrl.value = ""; tsData.value = null; tsFormat.value = "yyyy-MM-dd"; @@ -1079,6 +1306,7 @@ async function showModal(contractIdParam, businessRecordParam) { await loadContract(contractIdToLoad); await nextTick(); calcDisplaySize(); + await loadSealPositionState(); await loadCounterpartyConfirm(); modalLoading.value = false; return; @@ -1098,9 +1326,13 @@ async function showModal(contractIdParam, businessRecordParam) { restoreContractState(saved); await nextTick(); calcDisplaySize(); + await loadSealPositionState(); } } catch {} } + if (contextRecord?.id && !sealPositionState.positions.length) { + await loadSealPositionState(); + } await loadCounterpartyConfirm(); modalLoading.value = false; } @@ -1165,6 +1397,15 @@ const imageHeight = ref(1122); const completing = ref(false); const counterpartyConfirmMap = ref({}); const counterpartyConfirmLoading = ref(false); +const sealPositionState = reactive({ + enabled: false, + loading: false, + sealContractId: "", + positions: [], + availablePartyIds: [], + preferredPartyId: null, +}); +const selectedPartyId = ref(null); async function loadContract(cid) { try { @@ -1190,6 +1431,9 @@ async function loadContract(cid) { imageHeight.value = Number(preview?.imageHeight) || 1122; updateBusinessStatusByContractStatus(contractStatus.value); saveContractState(); + if (detail?.sealContractId) { + contractId.value = detail.sealContractId; + } } } catch { try { @@ -1314,12 +1558,136 @@ async function loadCounterpartyConfirm() { counterpartyConfirmLoading.value = false; } +function resetSealPositionState() { + sealPositionState.enabled = false; + sealPositionState.loading = false; + sealPositionState.sealContractId = ""; + sealPositionState.positions = []; + sealPositionState.availablePartyIds = []; + sealPositionState.preferredPartyId = null; + selectedPartyId.value = null; +} + +async function loadSealPositionState() { + if (!businessRecord.value?.id) { + resetSealPositionState(); + return; + } + sealPositionState.loading = true; + try { + const res = await getLaunchSealPositionPage(businessRecord.value.id); + if (!isSuccessResponse(res) || !getResponseResult(res)) { + resetSealPositionState(); + return; + } + const page = getResponseResult(res); + sealPositionState.sealContractId = page?.sealContractId || businessRecord.value?.sealContractId || contractId.value || ""; + sealPositionState.positions = Array.isArray(page?.positions) + ? page.positions.map((item) => ({ + ...item, + pageNum: Number(item.pageNum || 1), + x: roundValue(item.x), + y: roundValue(item.y), + width: roundValue(item.width), + height: roundValue(item.height), + signedStatus: Number(item.signedStatus ?? SEAL_POS_STATUS_EMPTY), + required: normalizeSealPositionRequired(item.required), + + })) + : []; + sealPositionState.availablePartyIds = Array.isArray(page?.availablePartyIds) ? page.availablePartyIds : []; + sealPositionState.preferredPartyId = page?.preferredPartyId ?? null; + sealPositionState.enabled = String(businessRecord.value?.sealPosConfigured || "N") === "Y" && sealPositionState.positions.length > 0; + if (sealPositionState.enabled) { + const availableIds = [...new Set(sealPositionState.availablePartyIds.map((item) => Number(item)).filter((item) => !Number.isNaN(item)))]; + if (!selectedPartyId.value || !availableIds.includes(Number(selectedPartyId.value))) { + selectedPartyId.value = sealPositionState.preferredPartyId || availableIds[0] || null; + } + syncSealItemsFromSealPositions(); + } else { + selectedPartyId.value = null; + } + } catch { + resetSealPositionState(); + } finally { + sealPositionState.loading = false; + } +} + +async function refreshSealPositionStateAfterCallback(requestId = "") { + if (!businessRecord.value?.id) { + return; + } + for (let attempt = 0; attempt < 4; attempt += 1) { + await loadSealPositionState(); + if (!requestId) { + return; + } + const stillSigning = sealPositionState.positions.some((item) => item.requestId === requestId && Number(item.signedStatus) === SEAL_POS_STATUS_SIGNING); + if (!stillSigning) { + return; + } + await wait(250); + } +} + +function isRequiredSealPositionMissingPlacement(box) { + return isMySealPositionBox(box) + && normalizeSealPositionRequired(box.required) === "Y" + && ![SEAL_POS_STATUS_SELECTED, SEAL_POS_STATUS_SIGNING, SEAL_POS_STATUS_SIGNED].includes(Number(box.signedStatus)) + && !findSealItemByBoxId(box.id); +} + +function findDroppableSealPositionBox(pageNum, x, y) { + return getPageSealPositionBoxes(pageNum).find((box) => { + if (!isMySealPositionBox(box)) return false; + const boxX = Number(box.x || 0); + const boxY = Number(box.y || 0); + const boxWidth = Number(box.width || 0); + const boxHeight = Number(box.height || 0); + return x >= boxX && x <= boxX + boxWidth && y >= boxY && y <= boxY + boxHeight; + }) || null; +} + +function scrollToSealPositionBox(box) { + if (!box || !pdfScrollRef.value) return; + const boxEl = pdfScrollRef.value.querySelector(`[data-seal-box-id='${box.id}']`); + const pageEl = pdfScrollRef.value.querySelector(`[data-page-num='${box.pageNum}']`); + if (boxEl?.scrollIntoView) { + boxEl.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" }); + return; + } + pageEl?.scrollIntoView?.({ behavior: "smooth", block: "center", inline: "nearest" }); +} async function handleConfirmCounterparty(item = {}) { if (!businessRecord.value?.id) return; if (!item.partyId) { message.error("缺少相对方标识,无法确认"); return; } + if (sealPositionEnabled.value) { + try { + const checkRes = await checkLaunchSealPositions(businessRecord.value.id, { + partyId: item.partyId, + }); + if (!isSuccessResponse(checkRes) || !getResponseResult(checkRes)) { + message.error(getResponseMessage(checkRes, "校验相对方签章位失败")); + return; + } + const checkResult = getResponseResult(checkRes); + if (!checkResult.passed) { + const firstMissing = Array.isArray(checkResult.missingPositions) ? checkResult.missingPositions[0] : null; + if (firstMissing) { + scrollToSealPositionBox(firstMissing); + } + message.warning(`该相对方还有 ${checkResult.missingCount || 0} 个必签位置未完成签署,请先完成签章`); + return; + } + } catch { + message.error("校验相对方签章位失败"); + return; + } + } try { const res = await confirmCounterparty(businessRecord.value.id, { partyId: String(item.partyId), @@ -1398,15 +1766,21 @@ const pagingSeal = reactive({ }); const pendingSealCount = computed(() => { - return sealItems.value.filter( - (item) => item._type === "seal" && item.status === "pending", - ).length; + return sealItems.value.filter((item) => { + if (item._type !== "seal" || item.status !== "pending") return false; + if (!sealPositionEnabled.value) return true; + const box = getBoxById(item._sealPositionBoxId); + return !!box && Number(box.partyId) === Number(selectedPartyId.value); + }).length; }); const signedSealCount = computed(() => { - return sealItems.value.filter( - (item) => item._type === "seal" && item.status === "signed", - ).length; + return sealItems.value.filter((item) => { + if (item._type !== "seal" || item.status !== "signed") return false; + if (!sealPositionEnabled.value) return true; + const box = getBoxById(item._sealPositionBoxId); + return !!box && Number(box.partyId) === Number(selectedPartyId.value); + }).length; }); const canStartPreSign = computed(() => { @@ -1420,6 +1794,7 @@ function onDragSealStart(e, seal) { sealName: seal.sealName, pictureUrl: seal.pictureUrl, owner: seal.owner || "", + scale: sealDefaultScale.value, }; e.dataTransfer.effectAllowed = "copy"; e.dataTransfer.setData("text/plain", seal.sealId || "seal"); @@ -1445,6 +1820,30 @@ function onDropSeal(e, pageNum) { Math.round(((e.clientX - rect.left) / scaleRatio.value) * 100) / 100; const y = Math.round(((e.clientY - rect.top) / scaleRatio.value) * 100) / 100; + if (dragSealData._type === "timestamp") { + sealItems.value.push({ + id: ++sealPlacedId, + _type: "timestamp", + sealId: null, + sealName: "时间戳", + pictureUrl: dragSealData.pictureUrl, + _pictureUrl: dragSealData._pictureUrl || dragSealData.pictureUrl, + owner: "", + pageNum, + x, + y, + scale: 1, + status: "pending", + }); + dragSealData = null; + return; + } + + if (sealPositionEnabled.value) { + handleDropSealOnSealPosition(e, pageNum); + return; + } + sealItems.value.push({ id: ++sealPlacedId, _type: dragSealData._type, @@ -1456,7 +1855,7 @@ function onDropSeal(e, pageNum) { pageNum, x, y, - scale: sealDefaultScale.value, + scale: dragSealData.scale || sealDefaultScale.value, status: "pending", }); @@ -1469,6 +1868,85 @@ function getPageSealItems(pageNum) { return sealItems.value.filter((item) => item.pageNum === pageNum); } +async function handleDropSealOnSealPosition(e, pageNum) { + if (!dragSealData || dragSealData._type !== "seal") return; + if (!selectedPartyId.value) { + message.warning("当前用户未匹配到可签署方,不能放章"); + dragSealData = null; + return; + } + const rect = e.currentTarget.getBoundingClientRect(); + const x = Math.round(((e.clientX - rect.left) / scaleRatio.value) * 100) / 100; + const y = Math.round(((e.clientY - rect.top) / scaleRatio.value) * 100) / 100; + const targetBox = findDroppableSealPositionBox(pageNum, x, y); + if (!targetBox) { + message.warning("请把印章拖到当前签署方的指定签章框内"); + dragSealData = null; + return; + } + if (Number(targetBox.signedStatus) === SEAL_POS_STATUS_SIGNING) { + const rolledBack = qrVisible.value && targetBox.requestId === qrRequestId.value + ? await rollbackCurrentQrSigning("已自动关闭当前二维码流程,你可以继续调整印章位置") + : false; + if (!rolledBack) { + message.warning("该签章位正在等待扫码确认,暂时不能更换印章"); + dragSealData = null; + return; + } + targetBox.requestId = ""; + targetBox.signedStatus = SEAL_POS_STATUS_SELECTED; + } + if (Number(targetBox.signedStatus) === SEAL_POS_STATUS_SIGNED) { + message.warning("该签章位已签署,不能重复放章"); + dragSealData = null; + return; + } + const existingItem = findSealItemByBoxId(targetBox.id); + const targetScale = dragSealData.scale || sealDefaultScale.value || 1; + const placement = buildSealPositionPlacement(targetBox, targetScale); + if (existingItem) { + existingItem.sealId = dragSealData.sealId; + existingItem.sealName = dragSealData.sealName; + existingItem.pictureUrl = dragSealData.pictureUrl; + existingItem._pictureUrl = dragSealData.pictureUrl; + existingItem.x = placement.x; + existingItem.y = placement.y; + existingItem.pageNum = Number(targetBox.pageNum || pageNum); + existingItem.scale = placement.scale; + existingItem.status = "pending"; + } else { + sealItems.value.push({ + id: ++sealPlacedId, + _type: "seal", + _sealPositionBoxId: targetBox.id, + sealId: dragSealData.sealId, + sealName: dragSealData.sealName, + pictureUrl: dragSealData.pictureUrl, + _pictureUrl: dragSealData.pictureUrl, + owner: dragSealData.owner || "", + pageNum: Number(targetBox.pageNum || pageNum), + x: placement.x, + y: placement.y, + scale: placement.scale, + status: "pending", + }); + } + targetBox.sealId = dragSealData.sealId; + targetBox.sealName = dragSealData.sealName; + targetBox.signedStatus = SEAL_POS_STATUS_SELECTED; + try { + await batchUpdateLaunchSealPositionStatus(businessRecord.value.id, { + partyId: selectedPartyId.value, + signedStatus: SEAL_POS_STATUS_SELECTED, + items: [{ id: targetBox.id, sealId: targetBox.sealId, sealName: targetBox.sealName }], + }); + } catch { + // 保留本地已选章状态,方便继续提交 + } + scrollToSealPositionBox(targetBox); + dragSealData = null; +} + function getPlacedStyle(seal) { const s = seal.scale || sealDefaultScale.value; const baseWidth = 170; @@ -1479,12 +1957,36 @@ function getPlacedStyle(seal) { }; } -function removePlacedSeal(id) { +async function removePlacedSeal(id) { const target = sealItems.value.find((item) => item.id === id); if (target?.status === "signed") { message.warning("已签署的内容不可删除"); return; } + if (sealPositionEnabled.value && target?._sealPositionBoxId) { + const box = getBoxById(target._sealPositionBoxId); + if (box && Number(box.signedStatus) === SEAL_POS_STATUS_SIGNING) { + const rolledBack = qrVisible.value && box.requestId === qrRequestId.value + ? await rollbackCurrentQrSigning("已自动关闭当前二维码流程,你可以继续删除或重放印章") + : false; + if (!rolledBack) { + message.warning("该签章位正在等待扫码确认,请先关闭二维码或等待回调完成"); + return; + } + box.requestId = ""; + box.signedStatus = SEAL_POS_STATUS_SELECTED; + } + if (box) { + box.sealId = ""; + box.sealName = ""; + box.signedStatus = SEAL_POS_STATUS_EMPTY; + batchUpdateLaunchSealPositionStatus(businessRecord.value.id, { + partyId: selectedPartyId.value, + signedStatus: SEAL_POS_STATUS_EMPTY, + items: [{ id: box.id }], + }).catch(() => {}); + } + } sealItems.value = sealItems.value.filter((item) => item.id !== id); } @@ -1516,6 +2018,10 @@ let originX = 0; let originY = 0; function startDragSeal(e, seal) { + if (sealPositionEnabled.value && seal._sealPositionBoxId) { + message.warning("签章位模式下,印章位置由预设框控制,不能自由拖动"); + return; + } e.preventDefault(); draggingSeal = seal; dragStartX = e.clientX; @@ -1550,6 +2056,7 @@ const qrCode = ref(""); const qrRequestId = ref(""); const qrExpiresIn = ref(300); const qrRefreshing = ref(false); +const mockingPresignSuccess = ref(false); const qrCount = ref(0); const preSigning = ref(false); let qrTimer = null; @@ -1589,7 +2096,33 @@ function stopQrCountdown() { } } -function closeQrDialog() { +async function rollbackCurrentQrSigning(reason = "") { + if (!sealPositionEnabled.value || !businessRecord.value?.id || !qrRequestId.value) { + return false; + } + const rollbackItems = mySealPositionBoxes.value + .filter((item) => Number(item.signedStatus) === SEAL_POS_STATUS_SIGNING && item.requestId === qrRequestId.value) + .map((item) => ({ id: item.id, sealId: item.sealId, sealName: item.sealName })); + if (!rollbackItems.length) { + return false; + } + try { + await batchUpdateLaunchSealPositionStatus(businessRecord.value.id, { + partyId: selectedPartyId.value, + signedStatus: SEAL_POS_STATUS_SELECTED, + items: rollbackItems, + }); + await loadSealPositionState(); + if (reason) { + message.info(reason); + } + return true; + } catch { + return false; + } +} + +async function closeQrDialog() { stopQrCountdown(); stopPolling(); qrVisible.value = false; @@ -1598,6 +2131,7 @@ function closeQrDialog() { .forEach((item) => { item.status = "pending"; }); + await rollbackCurrentQrSigning("已关闭二维码,本次预盖章已恢复为待提交状态"); } function startPolling(requestId) { @@ -1616,7 +2150,7 @@ function startPolling(requestId) { const res = await pollCallback(requestId); if (isSuccessResponse(res) && getResponseResult(res)) { stopPolling(); - handlePreSignCallback(getResponseResult(res)); + handlePreSignCallback(getResponseResult(res), requestId); } } catch {} }, 2000); @@ -1630,9 +2164,21 @@ function stopPolling() { } async function handlePreSign() { - const pendingSeals = sealItems.value.filter( - (item) => item._type === "seal" && item.status === "pending", - ); + const pendingSeals = sealItems.value.filter((item) => { + if (item._type !== "seal" || item.status !== "pending") return false; + if (!sealPositionEnabled.value) return true; + const box = getBoxById(item._sealPositionBoxId); + return !!box && Number(box.partyId) === Number(selectedPartyId.value); + }); + + if (sealPositionEnabled.value) { + const firstMissing = mySealPositionBoxes.value.find((item) => isRequiredSealPositionMissingPlacement(item)); + if (firstMissing) { + scrollToSealPositionBox(firstMissing); + message.warning("当前签署方还有必签位置未放章,请先把印章拖到所有必签框"); + return; + } + } if (!pendingSeals.length) { message.warning("请先拖拽印章到文档上"); @@ -1676,6 +2222,26 @@ async function handlePreSign() { } const data = getResponseResult(res); + if (sealPositionEnabled.value) { + await batchUpdateLaunchSealPositionStatus(businessRecord.value.id, { + partyId: selectedPartyId.value, + signedStatus: SEAL_POS_STATUS_SIGNING, + requestId: data.requestId || "", + items: pendingSeals.map((seal) => ({ + id: seal._sealPositionBoxId, + sealId: seal.sealId, + sealName: seal.sealName, + })), + }); + sealPositionState.positions.forEach((box) => { + if (pendingSeals.some((seal) => Number(seal._sealPositionBoxId) === Number(box.id))) { + box.signedStatus = SEAL_POS_STATUS_SIGNING; + box.requestId = data.requestId || ""; + } + }); + // 不展示“待扫码确认”的中间覆盖物,二维码拉起后本地预放章立即隐藏。 + sealItems.value = sealItems.value.filter((item) => !pendingSeals.some((seal) => Number(seal.id) === Number(item.id))); + } qrCode.value = data.qrCode || ""; qrRequestId.value = data.requestId || ""; qrExpiresIn.value = data.expiresIn || 300; @@ -1683,9 +2249,11 @@ async function handlePreSign() { startQrCountdown(); startPolling(data.requestId); - pendingSeals.forEach((seal) => { - seal.status = "signing"; - }); + if (!sealPositionEnabled.value) { + pendingSeals.forEach((seal) => { + seal.status = "signing"; + }); + } } catch { message.error("获取二维码失败"); } finally { @@ -1693,7 +2261,7 @@ async function handlePreSign() { } } -function handlePreSignCallback(data) { +async function handlePreSignCallback(data, requestId = "") { if (data.contractStatus !== undefined) { contractStatus.value = data.contractStatus; updateBusinessStatusByContractStatus(data.contractStatus); @@ -1710,6 +2278,12 @@ function handlePreSignCallback(data) { item.status = "signed"; item._signer = data.signer || ""; }); + // 预盖章成功后只保留签章框状态,隐藏覆盖物。 + sealItems.value = sealItems.value.filter((item) => { + if (item._type !== "seal") return true; + const box = getBoxById(item._sealPositionBoxId); + return !box || Number(box.signedStatus) !== SEAL_POS_STATUS_SIGNED; + }); if (pagingSeal.sealId) { pagingSigned.value = true; @@ -1717,13 +2291,18 @@ function handlePreSignCallback(data) { stopQrCountdown(); qrVisible.value = false; - + if (sealPositionEnabled.value) { + await refreshSealPositionStateAfterCallback(requestId || qrRequestId.value || String(data?.requestId || "")); + } if (data.contractStatus === 2000) { message.success("合约签署已完成,可直接下载文件"); } else { + const nextMissingBox = mySealPositionBoxes.value.find((item) => isRequiredSealPositionUnfinished(item)); + if (nextMissingBox) { + scrollToSealPositionBox(nextMissingBox); + } message.success(`预盖章确认成功,签署人:${data.signer || "未知"}`); } - if (businessRecord.value?.id && data.contractStatus === 2000) { writeBackLaunchSealFinished(data.contractStatus); } @@ -1737,6 +2316,42 @@ function refreshQrCode() { }); } +async function handleMockPresignSuccess() { + if (!qrRequestId.value || !contractId.value) { + message.warning("当前没有待确认的预盖章请求"); + return; + } + + stopPolling(); + mockingPresignSuccess.value = true; + try { + const res = await mockPreSignSuccessCallback({ + requestId: qrRequestId.value, + contractId: contractId.value, + signer: queryName.value || "测试签署人", + contractStatus: 20, + }); + if (!isSuccessResponse(res)) { + message.error(getResponseMessage(res, "测试确认签署失败")); + startPolling(qrRequestId.value); + return; + } + await handlePreSignCallback( + { + signer: queryName.value || "测试签署人", + contractStatus: 20, + requestId: qrRequestId.value, + }, + qrRequestId.value, + ); + } catch { + message.error("测试确认签署失败"); + startPolling(qrRequestId.value); + } finally { + mockingPresignSuccess.value = false; + } +} + const tsFormat = ref("yyyy-MM-dd"); const tsLoading = ref(false); const tsImageUrl = ref(""); @@ -1814,6 +2429,27 @@ async function handleCompleteContract() { message.warning(validateMessage); return; } + if (sealPositionEnabled.value) { + try { + const checkRes = await checkLaunchSealPositions(businessRecord.value.id, {}); + if (!isSuccessResponse(checkRes) || !getResponseResult(checkRes)) { + message.error(getResponseMessage(checkRes, "校验签章位失败")); + return; + } + const result = getResponseResult(checkRes); + if (!result.passed) { + const firstMissing = Array.isArray(result.missingPositions) ? result.missingPositions[0] : null; + if (firstMissing) { + scrollToSealPositionBox(firstMissing); + } + message.warning(`还有 ${result.missingCount || 0} 个必签位置未完成签署,请先处理后再结束合约`); + return; + } + } catch { + message.error("校验签章位失败"); + return; + } + } // 检查相对方签署确认情况 await loadCounterpartyConfirm(); @@ -2194,8 +2830,9 @@ watch(visible, (value) => { } .placed-seal .placed-seal-fallback { - width: 80px; - height: 40px; + width: 100%; + min-width: 80px; + min-height: 40px; border: 2px dashed #409eff; background: rgba(64, 158, 255, 0.1); display: flex; @@ -2203,6 +2840,10 @@ watch(visible, (value) => { justify-content: center; font-size: 12px; color: #409eff; + padding: 8px 10px; + box-sizing: border-box; + text-align: center; + line-height: 1.4; } .placed-delete { @@ -2343,6 +2984,69 @@ watch(visible, (value) => { padding: 16px; } +.seal-position-party-select { + margin-top: 14px; +} + +.seal-position-note { + margin-top: 14px; +} + +.seal-position-box { + position: absolute; + z-index: 6; + box-sizing: border-box; + border: 2px dashed #94a3b8; + background: rgba(148, 163, 184, 0.14); + border-radius: 12px; + padding: 8px; + color: #334155; + overflow: hidden; + pointer-events: none; +} + +.seal-position-box.is-mine { + border-color: #2563eb; + background: rgba(37, 99, 235, 0.12); + color: #1d4ed8; +} + +.seal-position-box.is-other { + border-color: #cbd5e1; + background: rgba(203, 213, 225, 0.24); +} + +.seal-position-box.is-signed { + border-style: solid; +} + +.seal-position-box.is-missing { + border-color: #ef4444; + background: rgba(239, 68, 68, 0.14); + color: #b91c1c; +} + +.seal-position-box-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.seal-position-box-title { + font-size: 12px; + font-weight: 700; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.seal-position-box-meta { + margin-top: 4px; + font-size: 11px; + line-height: 1.4; +} + .step-hint { color: #8b95a5; font-size: 13px; diff --git a/src/views/contract/shared/templateDocxRenderer.js b/src/views/contract/shared/templateDocxRenderer.js index 2580688..cfb6ba9 100644 --- a/src/views/contract/shared/templateDocxRenderer.js +++ b/src/views/contract/shared/templateDocxRenderer.js @@ -12,6 +12,8 @@ const TEXT_NODE = 3 const DEFAULT_WORD_TABLE_WIDTH_TWIPS = 9000 const DEFAULT_WORD_TABLE_WIDTH_PERCENT = 5000 const DEFAULT_TABLE_CELL_PADDING_TWIPS = 120 +const DEFAULT_TABLE_BORDER_SIZE = 6 +const DEFAULT_TABLE_BORDER_COLOR = '000000' const DEFAULT_PARAGRAPH_SPACING_BEFORE = 40 const DEFAULT_PARAGRAPH_SPACING_AFTER = 40 const DEFAULT_PARAGRAPH_LINE = 300 @@ -344,49 +346,22 @@ function buildParagraphXmlBySegments(segments = [], paragraphStyle = {}) { } function buildTableXml(tableElement) { - const directRows = getDirectChildElements(tableElement, 'tr') - const rows = directRows.length ? directRows : Array.from(tableElement.getElementsByTagName('tr')) + const rows = extractTableRows(tableElement) + const tableLayout = buildTableLayout(rows) const columnWidths = extractTableColumnWidths(tableElement) const tableWidth = resolveTableWordWidth(readElementWidth(tableElement)) - const normalizedColumnWidths = normalizeTableColumnWidths(rows, columnWidths, tableWidth) + const normalizedColumnWidths = normalizeTableColumnWidths( + tableLayout.rows, + columnWidths, + tableWidth, + ) - const rowXml = rows - .map((row) => { - const cells = getDirectCellElements(row) - let logicalColumnIndex = 0 - const cellXml = cells - .map((cell) => { - const isHeader = isHeaderCell(cell) - const cellStyle = parseNodeStyleInfo(cell) - const defaultCellAlign = cellStyle.align || 'center' - const blocks = [] - collectBlocksFromChildren(cell.childNodes, blocks, { - bold: isHeader, - color: cellStyle.color || '', - bgColor: '', - align: defaultCellAlign, - }) - const contentXml = - blocks.filter(Boolean).join('') || buildParagraphXml('', { align: defaultCellAlign }) - const gridSpan = parseSpanValue(cell.getAttribute?.('colspan')) - const tcWidth = - buildCellWidthFromGrid(normalizedColumnWidths, logicalColumnIndex, gridSpan) || { - value: 0, - type: 'auto', - } - logicalColumnIndex += gridSpan - const tcProps = [ - ``, - '', - cellStyle.bgColor - ? `` - : '', - gridSpan > 1 ? `` : '', - ] - .filter(Boolean) - .join('') - return `${tcProps}${contentXml}` - }) + const rowXml = tableLayout.rows + .map((rowLayout) => { + const cellXml = rowLayout.renderCells + .map((cellEntry) => + buildWordTableCellXml(cellEntry, rowLayout, tableLayout.rows, normalizedColumnWidths), + ) .join('') return `${cellXml}` }) @@ -394,7 +369,315 @@ function buildTableXml(tableElement) { const normalizedTableWidth = buildNormalizedTableWidth(tableWidth, normalizedColumnWidths) const tableGridXml = buildTableGridXml(normalizedColumnWidths) - return `${tableGridXml}${rowXml}` + return `${tableGridXml}${rowXml}` +} + +function extractTableRows(tableElement) { + const directRows = getDirectChildElements(tableElement, 'tr') + if (directRows.length) { + return directRows + } + + const sectionRows = ['thead', 'tbody', 'tfoot'].flatMap((tagName) => + getDirectChildElements(tableElement, tagName).flatMap((section) => + getDirectChildElements(section, 'tr'), + ), + ) + if (sectionRows.length) { + return sectionRows + } + + return Array.from(tableElement.getElementsByTagName('tr')) +} + +function buildTableLayout(rows = []) { + const grid = [] + let columnCount = 0 + let nextCellId = 1 + + rows.forEach((row, rowIndex) => { + const rowSlots = grid[rowIndex] || [] + grid[rowIndex] = rowSlots + const cells = getDirectCellElements(row) + let searchColumnIndex = 0 + + cells.forEach((cell) => { + const startColumn = findNextAvailableTableColumn(rowSlots, searchColumnIndex) + const gridSpan = parseSpanValue(cell.getAttribute?.('colspan')) + const rowSpan = parseSpanValue(cell.getAttribute?.('rowspan')) + const entry = { + kind: 'cell', + id: nextCellId, + cell, + rowIndex, + startColumn, + gridSpan, + rowSpan, + } + nextCellId += 1 + + for (let offset = 0; offset < gridSpan; offset += 1) { + rowSlots[startColumn + offset] = entry + } + + if (rowSpan > 1) { + for (let nextRowIndex = rowIndex + 1; nextRowIndex < rowIndex + rowSpan; nextRowIndex += 1) { + const nextRowSlots = grid[nextRowIndex] || [] + grid[nextRowIndex] = nextRowSlots + const continuationEntry = { + kind: 'continuation', + id: entry.id, + cell, + rowIndex: nextRowIndex, + startColumn, + gridSpan, + rowSpan, + } + for (let offset = 0; offset < gridSpan; offset += 1) { + nextRowSlots[startColumn + offset] = continuationEntry + } + } + } + + searchColumnIndex = startColumn + gridSpan + columnCount = Math.max(columnCount, startColumn + gridSpan) + }) + + columnCount = Math.max(columnCount, rowSlots.length) + }) + + return { + columnCount, + rows: rows.map((row, rowIndex) => { + const slots = Array.from( + { length: columnCount }, + (_, columnIndex) => grid[rowIndex]?.[columnIndex] || null, + ) + const renderCells = buildRenderableRowEntries(slots, rowIndex, columnCount) + const renderSlots = buildTableRowRenderSlots(renderCells, columnCount) + return { row, rowIndex, slots, renderCells, renderSlots } + }), + } +} + +function buildRenderableRowEntries(slots = [], rowIndex, columnCount) { + const actualEntries = [] + for (let columnIndex = 0; columnIndex < columnCount; ) { + const entry = slots[columnIndex] + if (!entry) { + columnIndex += 1 + continue + } + if (entry.startColumn === columnIndex) { + actualEntries.push(entry) + columnIndex += entry.gridSpan + continue + } + columnIndex += 1 + } + + const hasBlank = slots.some((entry) => !entry) + const canRedistribute = + hasBlank && + actualEntries.length > 0 && + actualEntries.every( + (entry) => + entry.kind === 'cell' && + Number(entry.gridSpan || 1) === 1 && + Number(entry.rowSpan || 1) === 1, + ) + + if (canRedistribute) { + return buildEvenlyDistributedRowEntries(actualEntries, rowIndex, columnCount) + } + + const renderCells = [] + for (let columnIndex = 0; columnIndex < columnCount; ) { + const entry = slots[columnIndex] + if (!entry) { + renderCells.push({ + kind: 'blank', + rowIndex, + startColumn: columnIndex, + gridSpan: 1, + rowSpan: 1, + }) + columnIndex += 1 + continue + } + if (entry.startColumn !== columnIndex) { + columnIndex += 1 + continue + } + renderCells.push(entry) + columnIndex += entry.gridSpan + } + return renderCells +} + +function buildEvenlyDistributedRowEntries(entries = [], rowIndex, columnCount) { + const totalEntries = entries.length + const baseSpan = Math.max(1, Math.floor(columnCount / totalEntries)) + let remainder = Math.max(0, columnCount - baseSpan * totalEntries) + let startColumn = 0 + + return entries.map((entry, index) => { + let gridSpan = baseSpan + if (remainder > 0) { + gridSpan += 1 + remainder -= 1 + } + if (index === totalEntries - 1) { + gridSpan = Math.max(1, columnCount - startColumn) + } + + const nextEntry = { + ...entry, + rowIndex, + startColumn, + gridSpan, + } + startColumn += gridSpan + return nextEntry + }) +} + +function buildTableRowRenderSlots(renderCells = [], columnCount) { + const slots = Array.from({ length: columnCount }, () => null) + renderCells.forEach((entry) => { + const gridSpan = Math.max(1, Number(entry?.gridSpan) || 1) + const startColumn = Math.max(0, Number(entry?.startColumn) || 0) + for (let offset = 0; offset < gridSpan; offset += 1) { + if (startColumn + offset < columnCount) { + slots[startColumn + offset] = entry + } + } + }) + return slots +} + +function findNextAvailableTableColumn(rowSlots = [], startIndex = 0) { + let columnIndex = Math.max(0, startIndex) + while (rowSlots[columnIndex]) { + columnIndex += 1 + } + return columnIndex +} + +function buildWordTableCellXml( + cellEntry, + rowLayout, + tableRows = [], + normalizedColumnWidths = [], +) { + const gridSpan = Math.max(1, Number(cellEntry?.gridSpan) || 1) + const startColumn = Math.max(0, Number(cellEntry?.startColumn) || 0) + const tcWidth = buildCellWidthFromGrid(normalizedColumnWidths, startColumn, gridSpan) || { + value: 0, + type: 'auto', + } + const borderXml = buildWordTableCellBorderXml(cellEntry, rowLayout, tableRows) + const baseCellProps = [ + ``, + gridSpan > 1 ? `` : '', + borderXml, + ] + + if (cellEntry?.kind === 'blank') { + const blankProps = [...baseCellProps, ''].filter(Boolean).join('') + return `${blankProps}${buildParagraphXml('', { align: 'left' })}` + } + + const cell = cellEntry?.cell + const isHeader = isHeaderCell(cell) + const cellStyle = parseNodeStyleInfo(cell) + const defaultCellAlign = cellStyle.align || (isHeader ? 'center' : 'left') + const verticalAlign = normalizeWordVerticalAlign(cellStyle.verticalAlign) || 'center' + const tcProps = [ + ...baseCellProps, + ``, + cellStyle.bgColor + ? `` + : '', + ] + + if (cellEntry?.kind === 'continuation') { + tcProps.push('') + return `${tcProps.filter(Boolean).join('')}${buildParagraphXml('', { align: defaultCellAlign })}` + } + + const blocks = [] + collectBlocksFromChildren(cell.childNodes, blocks, { + bold: isHeader, + color: cellStyle.color || '', + bgColor: '', + align: defaultCellAlign, + }) + const contentXml = + blocks.filter(Boolean).join('') || buildParagraphXml('', { align: defaultCellAlign }) + + if ((Number(cellEntry?.rowSpan) || 1) > 1) { + tcProps.push('') + } + + return `${tcProps.filter(Boolean).join('')}${contentXml}` +} + +function buildWordTableCellBorderXml(cellEntry, rowLayout, tableRows = []) { + const startColumn = Math.max(0, Number(cellEntry?.startColumn) || 0) + const endColumn = startColumn + Math.max(1, Number(cellEntry?.gridSpan) || 1) - 1 + const currentSlots = resolveRenderedTableSlots(rowLayout) + const previousSlots = resolveRenderedTableSlots(tableRows?.[rowLayout?.rowIndex - 1]) + const nextSlots = resolveRenderedTableSlots(tableRows?.[rowLayout?.rowIndex + 1]) + const borders = [] + + if (!isSameCellEntry(currentSlots[startColumn - 1], cellEntry)) { + borders.push(buildWordBorderSideXml('left')) + } + if (!isSameCellEntry(currentSlots[endColumn + 1], cellEntry)) { + borders.push(buildWordBorderSideXml('right')) + } + if (!isSameCellEntryRange(previousSlots, startColumn, endColumn, cellEntry)) { + borders.push(buildWordBorderSideXml('top')) + } + if (!isSameCellEntryRange(nextSlots, startColumn, endColumn, cellEntry)) { + borders.push(buildWordBorderSideXml('bottom')) + } + + return borders.length ? `${borders.join('')}` : '' +} + +function resolveRenderedTableSlots(rowLayout) { + if (Array.isArray(rowLayout?.renderSlots) && rowLayout.renderSlots.length) { + return rowLayout.renderSlots + } + return Array.isArray(rowLayout?.slots) ? rowLayout.slots : [] +} + +function isSameCellEntryRange(slots = [], startColumn, endColumn, cellEntry) { + if (!cellEntry || cellEntry.kind === 'blank') { + return false + } + for (let columnIndex = startColumn; columnIndex <= endColumn; columnIndex += 1) { + if (!isSameCellEntry(slots[columnIndex], cellEntry)) { + return false + } + } + return true +} + +function isSameCellEntry(slotEntry, cellEntry) { + if (!slotEntry || !cellEntry) { + return false + } + if (slotEntry.kind === 'blank' || cellEntry.kind === 'blank') { + return false + } + return Number(slotEntry.id) === Number(cellEntry.id) +} + +function buildWordBorderSideXml(sideName) { + return `` } function getDirectChildElements(parent, tagName) { @@ -423,8 +706,8 @@ function extractTableColumnWidths(tableElement) { return cols.map((col) => resolveCellWordWidth(readElementWidth(col))).filter(Boolean) } -function normalizeTableColumnWidths(rows, initialColumnWidths, tableWidth) { - const columnCount = getTableColumnCount(rows) +function normalizeTableColumnWidths(tableRows, initialColumnWidths, tableWidth) { + const columnCount = getTableColumnCount(tableRows) if (!columnCount) { return [] } @@ -435,21 +718,22 @@ function normalizeTableColumnWidths(rows, initialColumnWidths, tableWidth) { return Number.isFinite(existingWidth) && existingWidth > 0 ? existingWidth : 0 }) - rows.forEach((row) => { - let logicalColumnIndex = 0 - getDirectCellElements(row).forEach((cell) => { - const gridSpan = parseSpanValue(cell.getAttribute?.('colspan')) - const cellWidth = resolveCellWordWidth(readElementWidth(cell)) + tableRows.forEach((rowLayout) => { + rowLayout.renderCells.forEach((cellEntry) => { + if (cellEntry?.kind !== 'cell') { + return + } + const gridSpan = Math.max(1, Number(cellEntry.gridSpan) || 1) + const cellWidth = resolveCellWordWidth(readElementWidth(cellEntry.cell)) if (cellWidth?.type === 'dxa' && cellWidth.value > 0) { const distributedWidth = Math.max(300, Math.round(cellWidth.value / gridSpan)) for (let offset = 0; offset < gridSpan; offset += 1) { - const columnIndex = logicalColumnIndex + offset + const columnIndex = cellEntry.startColumn + offset if (columnIndex < columnCount) { widths[columnIndex] = Math.max(widths[columnIndex] || 0, distributedWidth) } } } - logicalColumnIndex += gridSpan }) }) @@ -478,14 +762,11 @@ function normalizeTableColumnWidths(rows, initialColumnWidths, tableWidth) { return scaledWidths.map((width) => Math.max(300, Math.round(width))) } -function getTableColumnCount(rows) { - return rows.reduce((maxCount, row) => { - const count = getDirectCellElements(row).reduce( - (sum, cell) => sum + parseSpanValue(cell.getAttribute?.('colspan')), - 0, - ) - return Math.max(maxCount, count) - }, 0) +function getTableColumnCount(tableRows) { + return tableRows.reduce( + (maxCount, rowLayout) => Math.max(maxCount, rowLayout?.renderSlots?.length || 0), + 0, + ) } function resolveTableTargetWidthTwips(tableWidth, columnCount) { @@ -627,16 +908,33 @@ function parseSpanValue(rawValue) { function parseNodeStyleInfo(node) { const tag = String(node?.tagName || '').toLowerCase() const styleText = String(node?.getAttribute?.('style') || '') + const fontWeight = String(readStyleProperty(styleText, 'font-weight') || '').trim().toLowerCase() + const fontStyle = String(readStyleProperty(styleText, 'font-style') || '').trim().toLowerCase() + const textDecoration = String(readStyleProperty(styleText, 'text-decoration') || '') + .trim() + .toLowerCase() const color = normalizeCssColor(readStyleProperty(styleText, 'color')) - const bgColor = normalizeCssColor(readStyleProperty(styleText, 'background-color')) - const textAlign = normalizeTextAlign(readStyleProperty(styleText, 'text-align')) + const bgColor = normalizeCssColor( + readStyleProperty(styleText, 'background-color') || node?.getAttribute?.('bgcolor') || '', + ) + const textAlign = normalizeTextAlign( + readStyleProperty(styleText, 'text-align') || node?.getAttribute?.('align') || '', + ) + const verticalAlign = normalizeVerticalAlign( + readStyleProperty(styleText, 'vertical-align') || node?.getAttribute?.('valign') || '', + ) return { - bold: tag === 'strong' || tag === 'b', - italic: tag === 'em' || tag === 'i', - underline: tag === 'u', + bold: + tag === 'strong' || + tag === 'b' || + fontWeight === 'bold' || + ['600', '700', '800', '900'].includes(fontWeight), + italic: tag === 'em' || tag === 'i' || fontStyle === 'italic' || fontStyle === 'oblique', + underline: tag === 'u' || textDecoration.includes('underline'), color, bgColor, align: textAlign, + verticalAlign, } } @@ -648,6 +946,7 @@ function mergeTextStyle(baseStyle = {}, overrideStyle = {}) { color: overrideStyle.color || baseStyle.color || '', bgColor: overrideStyle.bgColor || baseStyle.bgColor || '', align: overrideStyle.align || baseStyle.align || '', + verticalAlign: overrideStyle.verticalAlign || baseStyle.verticalAlign || '', } } @@ -704,12 +1003,26 @@ function normalizeCssColor(rawColor) { function normalizeTextAlign(rawValue) { const value = String(rawValue || '').trim().toLowerCase() + if (value === 'start') { + return 'left' + } + if (value === 'end') { + return 'right' + } if (['left', 'center', 'right', 'justify'].includes(value)) { return value } return '' } +function normalizeVerticalAlign(rawValue) { + const value = String(rawValue || '').trim().toLowerCase() + if (['top', 'middle', 'center', 'bottom'].includes(value)) { + return value + } + return '' +} + function normalizeWordAlignment(value) { if (value === 'left') return 'left' if (value === 'center') return 'center' @@ -718,6 +1031,13 @@ function normalizeWordAlignment(value) { return '' } +function normalizeWordVerticalAlign(value) { + if (value === 'top') return 'top' + if (value === 'middle' || value === 'center') return 'center' + if (value === 'bottom') return 'bottom' + return '' +} + function escapeRegExp(text) { return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } diff --git a/src/views/contract/sign/index.vue b/src/views/contract/sign/index.vue index fddc3dd..3518f67 100644 --- a/src/views/contract/sign/index.vue +++ b/src/views/contract/sign/index.vue @@ -1,29 +1,17 @@ - + - - - - - - + + + + + + @@ -205,14 +129,9 @@ - - + + + @@ -220,7 +139,9 @@ import { h, nextTick, onBeforeUnmount, onMounted, ref } from 'vue' import { RedoOutlined, SearchOutlined } from '@ant-design/icons-vue' import { message, Modal } from 'ant-design-vue' +import { getCurrentMockUser } from '@/api/mockUser' import ContractPartyPlaceholderModal from '../launch/components/ContractPartyPlaceholderModal.vue' +import SealPositionConfigModal from '../launch/components/SealPositionConfigModal.vue' import MixedSignModal from '../launch/components/mixedSignModal.vue' import UploadSignedModal from '../launch/components/UploadSignedModal.vue' import { @@ -231,6 +152,7 @@ import { downloadLaunchFile, finishLaunchSeal, getLaunchDetail, + getLaunchSealPositionPage, getLawApproveCallbackSample, queryApproveViewUrl, queryLaunchList, @@ -245,6 +167,7 @@ const signStatusOptions = [ ] const contractModalRef = ref(null) +const sealPositionConfigModalRef = ref(null) const mixedSignModalRef = ref(null) const uploadSignedModalRef = ref(null) const queryContainer = ref(null) @@ -253,6 +176,7 @@ const tableRef = ref(null) const toolbarRef = ref(null) const loading = ref(false) +const currentMockUser = ref(null) const total = ref(0) const tableHeight = ref(420) const tableData = ref([]) @@ -343,9 +267,6 @@ function getStatusColor(status) { } function getStatusLabelByRow(row) { - if (isSingleAgreementRecord(row) && row?.status === 'pending_sign') { - return '待盖章' - } return getStatusLabel(row?.status) } @@ -353,12 +274,34 @@ function isSingleAgreementRecord(row) { return String(row?.isSingleAgreement || '0') === '1' } +function isManageOperator() { + if (currentMockUser.value?.superAdmin) { + return true + } + return String(currentMockUser.value?.systemType || '').toLowerCase() === 'manage' +} + +function isSignOperator() { + if (currentMockUser.value?.superAdmin) { + return true + } + const systemType = String(currentMockUser.value?.systemType || '').toLowerCase() + return systemType === 'manage' || systemType === 'business' +} + +function canConfigSealPosition(row) { + return isManageOperator() + && !isSingleAgreementRecord(row) + && String(row.isElectron || '1') === '1' + && row.status === 'pending_sign' +} + function canOnlineSign(row) { - return ( - !isSingleAgreementRecord(row) && - String(row.isElectron || '1') === '1' && - SIGN_STAGE_STATUSES.includes(row.status) - ) + return isSignOperator() + && !isSingleAgreementRecord(row) + && String(row.isElectron || '1') === '1' + && SIGN_STAGE_STATUSES.includes(row.status) + && String(row.sealPosConfigured || 'N') === 'Y' } function canUploadSigned(row) { @@ -366,9 +309,6 @@ function canUploadSigned(row) { } function canMockFinishSign(row) { - if (isSingleAgreementRecord(row)) { - return row.status === 'pending_sign' - } return String(row.isElectron || '1') === '1' && SIGN_STAGE_STATUSES.includes(row.status) } @@ -411,15 +351,9 @@ async function fetchRecordsByStatus(status) { const data = Array.isArray(pageResult?.data) ? pageResult.data : [] totalCount = Number(pageResult?.totalSize ?? pageResult?.total ?? 0) records.push(...data) - if (!data.length) { - break - } - if (totalCount > 0 && records.length >= totalCount) { - break - } - if (data.length < SIGN_LIST_QUERY_PAGE_SIZE) { - break - } + if (!data.length) break + if (totalCount > 0 && records.length >= totalCount) break + if (data.length < SIGN_LIST_QUERY_PAGE_SIZE) break pageNo += 1 } @@ -507,7 +441,6 @@ function handleViewApprove(row) { message.warning('当前暂无法务合同ID,审批记录地址尚未生成') return } - queryApproveViewUrl({ contractId: String(row.lawContractId), ivUser: row.initiator || 'admin', @@ -528,62 +461,46 @@ function handleViewApprove(row) { function buildLawCallbackModalContent(sample) { const approvedJson = JSON.stringify({ data: sample?.approvedData || {} }, null, 2) const rejectedJson = JSON.stringify({ data: sample?.rejectedData || {} }, null, 2) - return h( - 'div', - { style: { display: 'flex', flexDirection: 'column', gap: '12px' } }, - [ - h('div', [ - h('div', { style: { marginBottom: '6px', fontWeight: 600 } }, '回调地址'), - h( - 'div', - { style: { color: '#666', wordBreak: 'break-all' } }, - `${sample?.callbackMethod || 'POST'} ${sample?.callbackUrl || '/contract/launch/callback/law/approve'}`, - ), - ]), - h('div', [ - h('div', { style: { marginBottom: '6px', fontWeight: 600 } }, '审批通过 data'), - h( - 'pre', - { - style: { - margin: 0, - maxHeight: '260px', - overflow: 'auto', - padding: '12px', - borderRadius: '8px', - background: '#f6f8fa', - fontSize: '12px', - lineHeight: '1.6', - whiteSpace: 'pre-wrap', - wordBreak: 'break-word', - }, - }, - approvedJson, - ), - ]), - h('div', [ - h('div', { style: { marginBottom: '6px', fontWeight: 600 } }, '审批驳回 data'), - h( - 'pre', - { - style: { - margin: 0, - maxHeight: '220px', - overflow: 'auto', - padding: '12px', - borderRadius: '8px', - background: '#f6f8fa', - fontSize: '12px', - lineHeight: '1.6', - whiteSpace: 'pre-wrap', - wordBreak: 'break-word', - }, - }, - rejectedJson, - ), - ]), - ], - ) + return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '12px' } }, [ + h('div', [ + h('div', { style: { marginBottom: '6px', fontWeight: 600 } }, '回调地址'), + h('div', { style: { color: '#666', wordBreak: 'break-all' } }, `${sample?.callbackMethod || 'POST'} ${sample?.callbackUrl || '/contract/launch/callback/law/approve'}`), + ]), + h('div', [ + h('div', { style: { marginBottom: '6px', fontWeight: 600 } }, '审批通过 data'), + h('pre', { + style: { + margin: 0, + maxHeight: '260px', + overflow: 'auto', + padding: '12px', + borderRadius: '8px', + background: '#f6f8fa', + fontSize: '12px', + lineHeight: '1.6', + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + }, + }, approvedJson), + ]), + h('div', [ + h('div', { style: { marginBottom: '6px', fontWeight: 600 } }, '审批驳回 data'), + h('pre', { + style: { + margin: 0, + maxHeight: '220px', + overflow: 'auto', + padding: '12px', + borderRadius: '8px', + background: '#f6f8fa', + fontSize: '12px', + lineHeight: '1.6', + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + }, + }, rejectedJson), + ]), + ]) } function handleViewLawCallback(row) { @@ -603,19 +520,15 @@ function handleViewLawCallback(row) { } function handleMockFinishSign(row) { - const singleAgreement = isSingleAgreementRecord(row) Modal.confirm({ - title: singleAgreement ? '测试操作:模拟盖章完成' : '测试操作:模拟签署完成', - content: singleAgreement - ? `模拟单方协议【${row.contractName}】内部业务章加盖完成,状态将变为「已完成」。` - : `模拟合同【${row.contractName}】签署完成,状态将变为「待采集」。`, + title: '测试操作:模拟签署完成', + content: `模拟合同【${row.contractName}】签署完成,状态将变为「待采集」。`, okText: '确认模拟', cancelText: '取消', onOk: () => { const signDate = sliceDateValue(row.signDate) || todayString() const sealDate = sliceDateValue(row.sealDate) || signDate - const activeDate = - sliceDateValue(row.activeDate) || sliceDateValue(row.effectiveStart) || signDate + const activeDate = sliceDateValue(row.activeDate) || sliceDateValue(row.effectiveStart) || signDate return finishLaunchSeal(row.id, { sealContractId: row.sealContractId || `TEST-SEAL-${row.id}`, sealContractStatus: 2000, @@ -623,22 +536,16 @@ function handleMockFinishSign(row) { signedFilePath: row.contractTextFilePath || `mock-signed/${row.id}.pdf`, signedFileType: 'pdf', signedFileSize: row.contractTextFileSize || null, - signedOfflineReason: singleAgreement - ? '测试按钮模拟内部业务章加盖完成' - : '测试按钮模拟签署完成,真实签章接入后可删除此按钮', + signedOfflineReason: '测试按钮模拟签署完成,真实签章接入后可删除此按钮', signDate: formatDateTimeValue(signDate), sealDate: formatDateTimeValue(sealDate), activeDate: formatDateTimeValue(activeDate), }).then((res) => { if (!isSuccessResponse(res)) { - message.error( - getResponseMessage(res, singleAgreement ? '测试盖章完成失败' : '测试签署完成失败'), - ) + message.error(getResponseMessage(res, '测试签署完成失败')) return } - message.success( - singleAgreement ? '测试盖章完成,单方协议已办结' : '测试签署完成,合同已流转到待采集', - ) + message.success('测试签署完成,合同已流转到待采集') getList() }) }, @@ -675,35 +582,69 @@ function handleUploadSigned(row) { uploadSignedModalRef.value?.showModal(row) } -function handleOnlineSign(row) { - getLaunchDetail(row.id).then(async (res) => { - if (!isSuccessResponse(res) || !getDataResult(res)) { - message.error(getResponseMessage(res, '获取合同详情失败')) +async function ensureDetailReady(row, actionLabel) { + const detailRes = await getLaunchDetail(row.id) + if (!isSuccessResponse(detailRes) || !getDataResult(detailRes)) { + message.error(getResponseMessage(detailRes, '获取合同详情失败')) + return null + } + let detail = getDataResult(detailRes) + if (detail.contractTextFilePath) { + return detail + } + const uploaded = await handleWatermarkUpload(row, actionLabel) + if (!uploaded) { + return null + } + const reloadRes = await getLaunchDetail(row.id) + if (!isSuccessResponse(reloadRes) || !getDataResult(reloadRes)) { + message.error(getResponseMessage(reloadRes, '获取合同详情失败')) + return null + } + detail = getDataResult(reloadRes) + return detail +} + +async function handleConfigSealPosition(row) { + const detail = await ensureDetailReady(row, '配置签章位') + if (!detail) return + sealPositionConfigModalRef.value?.showModal(detail) +} + +async function handleOnlineSign(row) { + const detail = await ensureDetailReady(row, '在线签订') + if (!detail) return + try { + const pageRes = await getLaunchSealPositionPage(row.id) + if (!isSuccessResponse(pageRes) || !getDataResult(pageRes)) { + message.error(getResponseMessage(pageRes, '获取签章位失败')) return } - const detail = getDataResult(res) - if (!detail.contractTextFilePath) { - await handleWatermarkUpload(row) + const page = getDataResult(pageRes) + if (!Array.isArray(page.positions) || !page.positions.length) { + message.warning('当前合同还未配置签章位,请先点击“配置签章位”') + return + } + if (!Array.isArray(page.availablePartyIds) || !page.availablePartyIds.length) { + message.warning('当前用户未匹配到可签署方,暂时不能在线签订') return } mixedSignModalRef.value?.showModal({ ...detail, contractId: detail.sealContractId, }) - }) + } catch { + message.error('获取签章位失败') + } } -async function handleWatermarkUpload(row) { +async function handleWatermarkUpload(row, actionLabel = '在线签订') { let file const ok = await new Promise((resolve) => { Modal.confirm({ title: '法务水印文件缺失', content: h('div', [ - h( - 'p', - { style: 'margin-bottom: 12px' }, - '法务审批回调未带回带水印的合同文件,请手动上传带水印的合同文件后继续在线签订。', - ), + h('p', { style: 'margin-bottom: 12px' }, `法务审批回调未带回带水印的合同文件,请先上传后再${actionLabel}。`), h('input', { type: 'file', accept: '.pdf,.doc,.docx', @@ -740,21 +681,25 @@ async function handleWatermarkUpload(row) { }, }) }) - if (!ok) return - getLaunchDetail(row.id).then((res) => { - if (!isSuccessResponse(res) || !getDataResult(res)) return - const detail = getDataResult(res) - mixedSignModalRef.value?.showModal({ - ...detail, - contractId: detail.sealContractId, - }) - }) + return ok +} + +function handleSealPositionSaved() { + getList() } function handleSignBusinessUpdated() { getList() } +async function loadCurrentMockUser() { + try { + currentMockUser.value = await getCurrentMockUser() + } catch { + currentMockUser.value = null + } +} + function syncTableHeight() { nextTick(() => { const viewportHeight = window.innerHeight @@ -765,6 +710,7 @@ function syncTableHeight() { } onMounted(() => { + loadCurrentMockUser() handleQuery() nextTick(() => { connectToolbar()