添加字典
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
export function getDictDataByType(dictType) {
|
||||||
|
return request({
|
||||||
|
url: "/admin/dict/getDictDataByType",
|
||||||
|
method: "post",
|
||||||
|
data: {
|
||||||
|
params: {
|
||||||
|
dictType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).then((res) => res?.result || []);
|
||||||
|
}
|
||||||
@@ -193,6 +193,10 @@ import {
|
|||||||
queryPublishedManifestList,
|
queryPublishedManifestList,
|
||||||
} from "@/api/manifest.js";
|
} from "@/api/manifest.js";
|
||||||
import { uploadLaunchFile } from "@/api/launch.js";
|
import { uploadLaunchFile } from "@/api/launch.js";
|
||||||
|
import {
|
||||||
|
buildTemplateFieldLabelMap,
|
||||||
|
loadTemplateFieldList,
|
||||||
|
} from "../../shared/templateFieldDict.js";
|
||||||
import {
|
import {
|
||||||
contractSourceOptions,
|
contractSourceOptions,
|
||||||
contractTypeOptions,
|
contractTypeOptions,
|
||||||
@@ -218,32 +222,6 @@ const contractCreateModeOptions = [
|
|||||||
{ label: "手动创建", value: "manual" },
|
{ label: "手动创建", value: "manual" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ========== 可绑定业务字段列表(用于映射下拉和展示) ==========
|
|
||||||
const templateFieldList = [
|
|
||||||
{ value: "contractName", label: "合同名称" },
|
|
||||||
{ value: "contractCode", label: "合同编码" },
|
|
||||||
{ value: "contractType", label: "合同类型" },
|
|
||||||
{ value: "contractAmountDisplay", label: "合同金额" },
|
|
||||||
{ value: "effectiveStart", label: "有效期起" },
|
|
||||||
{ value: "effectiveEnd", label: "有效期止" },
|
|
||||||
{ value: "effectivePeriod", label: "有效期区间" },
|
|
||||||
{ value: "paymentMethod", label: "付款方式" },
|
|
||||||
{ value: "primaryContent", label: "合同标的说明" },
|
|
||||||
{ value: "amountExplain", label: "金额说明" },
|
|
||||||
{ value: "partyAName", label: "甲方名称" },
|
|
||||||
{ value: "partyBName", label: "乙方名称" },
|
|
||||||
{ value: "partyCName", label: "丙方名称" },
|
|
||||||
{ value: "partyDName", label: "丁方名称" },
|
|
||||||
{ value: "partyEName", label: "戊方名称" },
|
|
||||||
{ value: "partyFName", label: "己方名称" },
|
|
||||||
{ value: "counterpartySummary", label: "合同方摘要" },
|
|
||||||
{ value: "attachmentSummary", label: "附件情况" },
|
|
||||||
{ value: "templateLabel", label: "模板名称" },
|
|
||||||
{ value: "signDate", label: "签署日期" },
|
|
||||||
{ value: "businessRemark", label: "业务备注" },
|
|
||||||
{ value: "contractSubject", label: "合同标的说明" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// ========== 格式化合同金额(如 368,000.00 元) ==========
|
// ========== 格式化合同金额(如 368,000.00 元) ==========
|
||||||
function formatContractAmount(value) {
|
function formatContractAmount(value) {
|
||||||
if (value === undefined || value === null || value === "") return "";
|
if (value === undefined || value === null || value === "") return "";
|
||||||
@@ -763,63 +741,12 @@ function copyContractFormData(data = {}) {
|
|||||||
// ========== 模板变量预览数据构建 ==========
|
// ========== 模板变量预览数据构建 ==========
|
||||||
|
|
||||||
/** 将合同表单数据转换为模板变量字典(用于 docxtemplater 渲染) */
|
/** 将合同表单数据转换为模板变量字典(用于 docxtemplater 渲染) */
|
||||||
function buildTemplatePreviewData(
|
function buildTemplateRenderData(templateFieldValues = {}) {
|
||||||
formData,
|
const data = {};
|
||||||
templateLabel = "",
|
(templateFieldList.value || []).forEach((item) => {
|
||||||
creationMode = "template",
|
data[item.value] = templateFieldValues?.[item.value] ?? "";
|
||||||
) {
|
});
|
||||||
const form = copyContractFormData(formData);
|
return data;
|
||||||
const parties = getPartyList(form);
|
|
||||||
const templateValues = form.templateFieldValues || {};
|
|
||||||
|
|
||||||
function pick(fieldKey, fallback = "") {
|
|
||||||
if (
|
|
||||||
templateValues[fieldKey] !== undefined &&
|
|
||||||
templateValues[fieldKey] !== null &&
|
|
||||||
templateValues[fieldKey] !== ""
|
|
||||||
) {
|
|
||||||
return templateValues[fieldKey];
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
contractName: pick("contractName", form.contractName || ""),
|
|
||||||
contractCode: pick("contractCode", form.contractCode || ""),
|
|
||||||
contractType: pick("contractType", form.contractType || ""),
|
|
||||||
contractAmountDisplay: pick(
|
|
||||||
"contractAmountDisplay",
|
|
||||||
formatContractAmount(form.contractAmount),
|
|
||||||
),
|
|
||||||
effectiveStart: pick("effectiveStart", form.effectiveStart || ""),
|
|
||||||
effectiveEnd: pick("effectiveEnd", form.effectiveEnd || ""),
|
|
||||||
effectivePeriod: pick(
|
|
||||||
"effectivePeriod",
|
|
||||||
[form.effectiveStart, form.effectiveEnd].filter(Boolean).join(" 至 "),
|
|
||||||
),
|
|
||||||
paymentMethod: pick("paymentMethod", form.paymentMethod || ""),
|
|
||||||
primaryContent: pick("primaryContent", form.primaryContent || ""),
|
|
||||||
amountExplain: pick("amountExplain", form.amountExplain || ""),
|
|
||||||
partyAName: pick("partyAName", parties[0]?.companyName || ""),
|
|
||||||
partyBName: pick("partyBName", parties[1]?.companyName || ""),
|
|
||||||
partyCName: pick("partyCName", parties[2]?.companyName || ""),
|
|
||||||
partyDName: pick("partyDName", parties[3]?.companyName || ""),
|
|
||||||
partyEName: pick("partyEName", parties[4]?.companyName || ""),
|
|
||||||
partyFName: pick("partyFName", parties[5]?.companyName || ""),
|
|
||||||
counterpartySummary: pick("counterpartySummary", getPartySummary(form)),
|
|
||||||
attachmentSummary: pick(
|
|
||||||
"attachmentSummary",
|
|
||||||
formatFileSummary(form.otherAttachmentList),
|
|
||||||
),
|
|
||||||
templateLabel: pick(
|
|
||||||
"templateLabel",
|
|
||||||
templateLabel || (creationMode === "manual" ? "手动创建" : ""),
|
|
||||||
),
|
|
||||||
signDate: pick("signDate", form.signDate || dayjs().format("YYYY-MM-DD")),
|
|
||||||
businessRemark: pick("businessRemark", ""),
|
|
||||||
contractSubject: pick("contractSubject", ""),
|
|
||||||
templateFieldValuesJson: JSON.stringify(form.templateFieldValues || {}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 提交报文构建 ==========
|
// ========== 提交报文构建 ==========
|
||||||
@@ -925,6 +852,7 @@ const previewError = ref("");
|
|||||||
const previewBlob = ref(null);
|
const previewBlob = ref(null);
|
||||||
const manualBodyFileList = ref([]);
|
const manualBodyFileList = ref([]);
|
||||||
const publishedTemplates = ref([]);
|
const publishedTemplates = ref([]);
|
||||||
|
const templateFieldList = ref([]);
|
||||||
|
|
||||||
const templateBufferCache = new Map();
|
const templateBufferCache = new Map();
|
||||||
let previewTimer = null;
|
let previewTimer = null;
|
||||||
@@ -945,13 +873,9 @@ const isViewMode = computed(() => modalMode.value === "view");
|
|||||||
|
|
||||||
const modalTitle = computed(() => "合同正文配置");
|
const modalTitle = computed(() => "合同正文配置");
|
||||||
|
|
||||||
const templateFieldLabelMap = computed(() => {
|
const templateFieldLabelMap = computed(() =>
|
||||||
const map = {};
|
buildTemplateFieldLabelMap(templateFieldList.value),
|
||||||
templateFieldList.forEach((item) => {
|
);
|
||||||
map[item.value] = item.label;
|
|
||||||
});
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
|
|
||||||
const launchFieldList = computed(() => {
|
const launchFieldList = computed(() => {
|
||||||
if (creationMode.value === "manual") return [];
|
if (creationMode.value === "manual") return [];
|
||||||
@@ -970,67 +894,8 @@ const launchFieldList = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 模板字段中与合同主信息同名的直接映射字段 */
|
|
||||||
const CONTRACT_DIRECT_FIELDS = [
|
|
||||||
"contractName",
|
|
||||||
"contractCode",
|
|
||||||
"contractType",
|
|
||||||
"effectiveStart",
|
|
||||||
"effectiveEnd",
|
|
||||||
"paymentMethod",
|
|
||||||
"primaryContent",
|
|
||||||
"amountExplain",
|
|
||||||
"businessRemark",
|
|
||||||
"contractSubject",
|
|
||||||
];
|
|
||||||
|
|
||||||
function getLaunchFieldValue(fieldKey) {
|
function getLaunchFieldValue(fieldKey) {
|
||||||
// 1. 优先返回用户在"模板信息录入"中自定义的值
|
return contractDetailState.value.templateFieldValues?.[fieldKey] ?? "";
|
||||||
const templateVal = contractDetailState.value.templateFieldValues?.[fieldKey];
|
|
||||||
if (templateVal !== undefined && templateVal !== null && templateVal !== "") {
|
|
||||||
return templateVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 若字段与合同主信息直接同名,自动带入主信息的值
|
|
||||||
if (CONTRACT_DIRECT_FIELDS.includes(fieldKey)) {
|
|
||||||
return contractDetailState.value[fieldKey] ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 特殊计算字段
|
|
||||||
if (fieldKey === "contractAmountDisplay") {
|
|
||||||
return formatContractAmount(contractDetailState.value.contractAmount);
|
|
||||||
}
|
|
||||||
if (fieldKey === "effectivePeriod") {
|
|
||||||
return [
|
|
||||||
contractDetailState.value.effectiveStart,
|
|
||||||
contractDetailState.value.effectiveEnd,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" 至 ");
|
|
||||||
}
|
|
||||||
if (fieldKey === "templateLabel") {
|
|
||||||
return activeTemplate.value?.templateName || "";
|
|
||||||
}
|
|
||||||
if (fieldKey === "signDate") {
|
|
||||||
return contractDetailState.value.signDate || dayjs().format("YYYY-MM-DD");
|
|
||||||
}
|
|
||||||
if (fieldKey === "counterpartySummary") {
|
|
||||||
return getPartySummary(contractDetailState.value.partyState);
|
|
||||||
}
|
|
||||||
if (fieldKey === "attachmentSummary") {
|
|
||||||
return formatFileSummary(contractDetailState.value.otherAttachmentList);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 合同方名称字段
|
|
||||||
if (/^party[A-F]Name$/.test(fieldKey)) {
|
|
||||||
const fieldIndex = PARTY_NAME_KEYS.indexOf(fieldKey);
|
|
||||||
return (
|
|
||||||
getPartyList(contractDetailState.value.partyState)[fieldIndex]
|
|
||||||
?.companyName ?? ""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLaunchFieldValue(fieldKey, value) {
|
function setLaunchFieldValue(fieldKey, value) {
|
||||||
@@ -1044,11 +909,7 @@ function setLaunchFieldValue(fieldKey, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const templateVariables = computed(() =>
|
const templateVariables = computed(() =>
|
||||||
buildTemplatePreviewData(
|
buildTemplateRenderData(contractDetailState.value.templateFieldValues || {}),
|
||||||
contractDetailState.value,
|
|
||||||
activeTemplate.value?.templateName || "",
|
|
||||||
creationMode.value,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasPreviewSource = computed(() => {
|
const hasPreviewSource = computed(() => {
|
||||||
@@ -1137,12 +998,17 @@ function syncManualBodyFileList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadPublishedTemplates() {
|
function loadPublishedTemplates() {
|
||||||
// TODO: 这里后续替换为“已发布模板列表接口”
|
|
||||||
return queryPublishedManifestList().then((list) => {
|
return queryPublishedManifestList().then((list) => {
|
||||||
publishedTemplates.value = list;
|
publishedTemplates.value = list;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadTemplateFields() {
|
||||||
|
return loadTemplateFieldList().then((list) => {
|
||||||
|
templateFieldList.value = list || [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getManifestDetailLocal(templateId) {
|
function getManifestDetailLocal(templateId) {
|
||||||
return (
|
return (
|
||||||
publishedTemplates.value.find((item) => item.id === templateId) || null
|
publishedTemplates.value.find((item) => item.id === templateId) || null
|
||||||
@@ -1234,22 +1100,16 @@ async function buildFilledDocumentBlob() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
//这里是生成的测试数据
|
//这里是生成的测试数据
|
||||||
const sourceValues = buildTemplatePreviewData(
|
const sourceValues = buildTemplateRenderData(
|
||||||
contractDetailState.value,
|
contractDetailState.value.templateFieldValues || {},
|
||||||
activeTemplate.value?.templateName || "",
|
|
||||||
creationMode.value,
|
|
||||||
);
|
);
|
||||||
const mappings = activeTemplate.value.variableMappings || [];
|
const mappings = activeTemplate.value.variableMappings || [];
|
||||||
|
|
||||||
const renderData = {};
|
const renderData = {};
|
||||||
//这部分是匹配的关键
|
|
||||||
if (mappings.length) {
|
if (mappings.length) {
|
||||||
mappings.forEach((item) => {
|
mappings.forEach((item) => {
|
||||||
if (!item.variableKey) return;
|
if (!item.variableKey) return;
|
||||||
renderData[item.variableKey] =
|
renderData[item.variableKey] = sourceValues[item.bindField] ?? "";
|
||||||
sourceValues[item.bindField] ??
|
|
||||||
contractDetailState.value[item.bindField] ??
|
|
||||||
"";
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Object.assign(renderData, sourceValues);
|
Object.assign(renderData, sourceValues);
|
||||||
@@ -1460,7 +1320,7 @@ function resetForm() {
|
|||||||
async function showModal(initialValues = {}, mode = "create") {
|
async function showModal(initialValues = {}, mode = "create") {
|
||||||
modalMode.value = mode || "create";
|
modalMode.value = mode || "create";
|
||||||
const isCreate = modalMode.value === "create";
|
const isCreate = modalMode.value === "create";
|
||||||
await loadPublishedTemplates();
|
await Promise.all([loadPublishedTemplates(), loadTemplateFields()]);
|
||||||
|
|
||||||
creationMode.value = isCreate
|
creationMode.value = isCreate
|
||||||
? "template"
|
? "template"
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
<section class="template-config-panel">
|
<section class="template-config-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>合同范本维护</h3>
|
<h3>合同模板配置中心</h3>
|
||||||
<p>支持模板分类、Word 文件上传、变量自动识别与业务字段匹配。</p>
|
<p>在线维护模板配置与变量映射,推荐在本地 Word 编辑正文后回传系统。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -97,7 +97,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<a-card size="small" title="模板附件与正文预览" class="panel-card">
|
<a-card size="small" title="模板文件与配置预览" class="panel-card">
|
||||||
|
<a-alert
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
class="workflow-alert"
|
||||||
|
message="推荐维护方式"
|
||||||
|
description="下载当前模板到本地 Word 编辑正文与占位符,保存后重新上传回传;平台负责变量识别、映射、预览与发布。"
|
||||||
|
/>
|
||||||
<div class="upload-meta">
|
<div class="upload-meta">
|
||||||
<div>
|
<div>
|
||||||
<div class="meta-label">当前模板文件</div>
|
<div class="meta-label">当前模板文件</div>
|
||||||
@@ -105,7 +112,12 @@
|
|||||||
{{ formState.templateFileName || '尚未上传 .docx 模板文件' }}
|
{{ formState.templateFileName || '尚未上传 .docx 模板文件' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-space v-if="!isViewMode">
|
<a-space v-if="!isViewMode">
|
||||||
|
<a-button @click="handleDownloadCurrentTemplate" :disabled="!hasTemplateFile">
|
||||||
|
<DownloadOutlined />
|
||||||
|
下载当前模板
|
||||||
|
</a-button>
|
||||||
<a-button @click="handleReDetectVariables">
|
<a-button @click="handleReDetectVariables">
|
||||||
<ReloadOutlined />
|
<ReloadOutlined />
|
||||||
重新识别变量
|
重新识别变量
|
||||||
@@ -134,33 +146,58 @@
|
|||||||
<p class="ant-upload-hint">
|
<p class="ant-upload-hint">
|
||||||
上传后会自动识别花括号变量,如 `{contractName}`、`{partyAName}`,并生成变量匹配清单。
|
上传后会自动识别花括号变量,如 `{contractName}`、`{partyAName}`,并生成变量匹配清单。
|
||||||
</p>
|
</p>
|
||||||
|
<p class="ant-upload-hint">
|
||||||
|
如需修改正文内容,建议先下载模板到本地 Word 编辑后再重新上传回传。
|
||||||
|
</p>
|
||||||
</a-upload-dragger>
|
</a-upload-dragger>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<a-card size="small" title="变量匹配" class="panel-card">
|
<a-card size="small" title="占位符工具栏与变量映射" class="panel-card">
|
||||||
<div class="variable-header">
|
<div class="variable-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="meta-label">已识别变量</div>
|
<div class="meta-label">已识别变量</div>
|
||||||
<div class="meta-value">{{ detectedVariableKeys.length ? detectedVariableKeys.join('、') : '暂未识别到变量' }}</div>
|
<div class="meta-value">{{ detectedVariableKeys.length ? detectedVariableKeys.join('、') : '暂未识别到变量' }}</div>
|
||||||
|
<div class="catalog-help">
|
||||||
|
点击下方字段标签可直接复制占位符,点击“加入映射”可快速补充变量绑定。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a-button v-if="!isViewMode" type="primary" ghost @click="handleAddManualVariable">
|
<a-space>
|
||||||
<PlusOutlined />
|
<a-button @click="handleDownloadVariableGuide">
|
||||||
手动新增变量
|
<DownloadOutlined />
|
||||||
</a-button>
|
下载变量说明
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="!isViewMode" type="primary" ghost @click="handleAddManualVariable">
|
||||||
|
<PlusOutlined />
|
||||||
|
手动新增变量
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="catalog-wrap">
|
<div class="catalog-wrap">
|
||||||
<div class="catalog-title">可绑定业务字段</div>
|
<div class="catalog-title">占位符工具栏</div>
|
||||||
<div class="catalog-list">
|
<div class="catalog-list">
|
||||||
<a-tag
|
<div
|
||||||
v-for="item in templateFieldList"
|
v-for="item in templateFieldList"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
color="blue"
|
class="catalog-item"
|
||||||
class="catalog-tag"
|
|
||||||
@click="handleQuickAddCatalogField(item.value)"
|
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
<a-tag
|
||||||
</a-tag>
|
color="blue"
|
||||||
|
class="catalog-tag"
|
||||||
|
@click="handleCopyPlaceholder(item.value)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-tag>
|
||||||
|
<a-button
|
||||||
|
v-if="!isViewMode"
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
class="catalog-action"
|
||||||
|
@click.stop="handleQuickAddCatalogField(item.value)"
|
||||||
|
>
|
||||||
|
加入映射
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -242,7 +279,7 @@
|
|||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>模板预览</h3>
|
<h3>模板预览</h3>
|
||||||
<p>右侧基于当前变量匹配渲染 Word 模板效果,便于检查变量位置是否正确。</p>
|
<p>右侧基于当前变量映射渲染模板效果,用于检查占位符位置与展示结果,本页不直接在线修改 Word 正文。</p>
|
||||||
</div>
|
</div>
|
||||||
<a-space size="small">
|
<a-space size="small">
|
||||||
<a-tag color="geekblue">{{ formState.source || '未分类' }}</a-tag>
|
<a-tag color="geekblue">{{ formState.source || '未分类' }}</a-tag>
|
||||||
@@ -253,7 +290,7 @@
|
|||||||
<div class="preview-shell">
|
<div class="preview-shell">
|
||||||
<div v-if="!templateArrayBuffer" class="preview-state">
|
<div v-if="!templateArrayBuffer" class="preview-state">
|
||||||
<FileWordOutlined style="font-size: 42px; color: #94a3b8" />
|
<FileWordOutlined style="font-size: 42px; color: #94a3b8" />
|
||||||
<p>上传 `.docx` 模板文件后,这里会显示填充后的预览。</p>
|
<p>上传 `.docx` 模板文件后,这里会显示填充后的预览;正文修改建议在本地 Word 完成后回传。</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else ref="previewRef" class="preview-stage"></div>
|
<div v-else ref="previewRef" class="preview-stage"></div>
|
||||||
<div v-if="rendering" class="preview-state preview-mask">
|
<div v-if="rendering" class="preview-state preview-mask">
|
||||||
@@ -274,7 +311,13 @@
|
|||||||
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import Docxtemplater from 'docxtemplater'
|
import Docxtemplater from 'docxtemplater'
|
||||||
import { DeleteOutlined, FileWordOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
FileWordOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import PizZip from 'pizzip'
|
import PizZip from 'pizzip'
|
||||||
import { renderAsync } from 'docx-preview'
|
import { renderAsync } from 'docx-preview'
|
||||||
@@ -284,6 +327,11 @@ import {
|
|||||||
uploadTemplateFile,
|
uploadTemplateFile,
|
||||||
} from '@/api/manifest.js'
|
} from '@/api/manifest.js'
|
||||||
import { contractTypeTreeOptions } from '../contractTypeOptions.js'
|
import { contractTypeTreeOptions } from '../contractTypeOptions.js'
|
||||||
|
import {
|
||||||
|
buildTemplateFieldLabelMap,
|
||||||
|
buildTemplateFieldOptions,
|
||||||
|
loadTemplateFieldList,
|
||||||
|
} from '../../shared/templateFieldDict.js'
|
||||||
|
|
||||||
// ========== 模板管理字典(后续可由后端接口替换) ==========
|
// ========== 模板管理字典(后续可由后端接口替换) ==========
|
||||||
|
|
||||||
@@ -298,35 +346,6 @@ const templateSourceOptions = [
|
|||||||
{ label: '对方文本', value: '对方文本' },
|
{ label: '对方文本', value: '对方文本' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 可绑定业务字段列表
|
|
||||||
const templateFieldList = [
|
|
||||||
{ value: 'contractName', label: '合同名称' },
|
|
||||||
{ value: 'contractCode', label: '合同编码' },
|
|
||||||
{ value: 'contractType', label: '合同类型' },
|
|
||||||
{ value: 'contractAmountDisplay', label: '合同金额' },
|
|
||||||
{ value: 'effectiveStart', label: '有效期起' },
|
|
||||||
{ value: 'effectiveEnd', label: '有效期止' },
|
|
||||||
{ value: 'effectivePeriod', label: '有效期区间' },
|
|
||||||
{ value: 'paymentMethod', label: '付款方式' },
|
|
||||||
{ value: 'primaryContent', label: '合同标的说明' },
|
|
||||||
{ value: 'amountExplain', label: '业务备注' },
|
|
||||||
{ value: 'partyAName', label: '甲方名称' },
|
|
||||||
{ value: 'partyBName', label: '乙方名称' },
|
|
||||||
{ value: 'partyCName', label: '丙方名称' },
|
|
||||||
{ value: 'counterpartySummary', label: '合同方摘要' },
|
|
||||||
{ value: 'contractSubject', label: '合同标的说明' },
|
|
||||||
{ value: 'businessRemark', label: '业务备注' },
|
|
||||||
{ value: 'attachmentSummary', label: '附件情况' },
|
|
||||||
{ value: 'templateLabel', label: '模板名称' },
|
|
||||||
{ value: 'signDate', label: '签署日期' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 业务字段下拉选项(由 templateFieldList 生成)
|
|
||||||
const templateFieldOptions = templateFieldList.map((item) => ({
|
|
||||||
label: `${item.label}(${item.value})`,
|
|
||||||
value: item.value,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 批量创建变量映射
|
// 批量创建变量映射
|
||||||
function createSimpleVariableList(list = []) {
|
function createSimpleVariableList(list = []) {
|
||||||
return list.map((item, index) => ({
|
return list.map((item, index) => ({
|
||||||
@@ -349,15 +368,23 @@ const templateUploadList = ref([])
|
|||||||
const variableMappings = ref([])
|
const variableMappings = ref([])
|
||||||
const detectedVariableKeys = ref([])
|
const detectedVariableKeys = ref([])
|
||||||
const orgOptions = ref([])
|
const orgOptions = ref([])
|
||||||
|
const templateFieldList = ref([])
|
||||||
|
|
||||||
let previewTimer = null
|
let previewTimer = null
|
||||||
let renderTaskId = 0
|
let renderTaskId = 0
|
||||||
|
|
||||||
|
const templateFieldOptions = computed(() =>
|
||||||
|
buildTemplateFieldOptions(templateFieldList.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
const templateFieldLabelMap = computed(() =>
|
||||||
|
buildTemplateFieldLabelMap(templateFieldList.value),
|
||||||
|
)
|
||||||
|
|
||||||
const formRules = {
|
const formRules = {
|
||||||
templateName: [{ required: true, message: '请输入范本名称', trigger: 'blur' }],
|
templateName: [{ required: true, message: '请输入范本名称', trigger: 'blur' }],
|
||||||
source: [{ required: true, message: '请选择范本类型', trigger: 'change' }],
|
source: [{ required: true, message: '请选择范本类型', trigger: 'change' }],
|
||||||
contractType: [{ required: true, message: '请选择合同类型', trigger: 'change' }],
|
contractType: [{ required: true, message: '请选择合同类型', trigger: 'change' }],
|
||||||
selfPartyRole: [{ required: true, message: '请选择我方地位', trigger: 'change' }],
|
|
||||||
version: [{ required: true, message: '请输入版本号', trigger: 'blur' }],
|
version: [{ required: true, message: '请输入版本号', trigger: 'blur' }],
|
||||||
publishOrgIds: [{ required: true, type: 'array', min: 1, message: '请选择发布范围', trigger: 'change' }],
|
publishOrgIds: [{ required: true, type: 'array', min: 1, message: '请选择发布范围', trigger: 'change' }],
|
||||||
}
|
}
|
||||||
@@ -369,7 +396,6 @@ function createDefaultFormState() {
|
|||||||
templateCode: '',
|
templateCode: '',
|
||||||
source: '标准模板',
|
source: '标准模板',
|
||||||
contractType: 'MM',
|
contractType: 'MM',
|
||||||
selfPartyRole: '甲方',
|
|
||||||
version: 'V1.0',
|
version: 'V1.0',
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -387,10 +413,14 @@ const formState = reactive(createDefaultFormState())
|
|||||||
|
|
||||||
const isViewMode = computed(() => modalMode.value === 'view')
|
const isViewMode = computed(() => modalMode.value === 'view')
|
||||||
|
|
||||||
|
const hasTemplateFile = computed(
|
||||||
|
() => !!(formState.templateFilePath || formState.templateFileName || templateArrayBuffer.value),
|
||||||
|
)
|
||||||
|
|
||||||
const modalTitle = computed(() => {
|
const modalTitle = computed(() => {
|
||||||
if (modalMode.value === 'view') return '查看合同范本'
|
if (modalMode.value === 'view') return '查看模板配置'
|
||||||
if (modalMode.value === 'edit') return '编辑合同范本'
|
if (modalMode.value === 'edit') return '编辑模板配置'
|
||||||
return '新增合同范本'
|
return '新增模板配置'
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -431,6 +461,13 @@ function resetState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureTemplateFieldListLoaded() {
|
||||||
|
if (templateFieldList.value.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
templateFieldList.value = await loadTemplateFieldList()
|
||||||
|
}
|
||||||
|
|
||||||
function schedulePreview() {
|
function schedulePreview() {
|
||||||
clearPreviewTimer()
|
clearPreviewTimer()
|
||||||
previewTimer = window.setTimeout(() => {
|
previewTimer = window.setTimeout(() => {
|
||||||
@@ -635,6 +672,37 @@ function handleQuickAddCatalogField(fieldKey) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPlaceholderText(fieldKey) {
|
||||||
|
return `{${fieldKey}}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyTextToClipboard(text) {
|
||||||
|
if (navigator?.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = text
|
||||||
|
textarea.setAttribute('readonly', 'readonly')
|
||||||
|
textarea.style.position = 'fixed'
|
||||||
|
textarea.style.left = '-9999px'
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCopyPlaceholder(fieldKey) {
|
||||||
|
const placeholder = buildPlaceholderText(fieldKey)
|
||||||
|
try {
|
||||||
|
await copyTextToClipboard(placeholder)
|
||||||
|
message.success(`已复制占位符:${placeholder}`)
|
||||||
|
} catch (error) {
|
||||||
|
message.warning(`复制失败,请手动使用占位符:${placeholder}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function removeVariable(id) {
|
function removeVariable(id) {
|
||||||
variableMappings.value = variableMappings.value.filter((item) => item.id !== id)
|
variableMappings.value = variableMappings.value.filter((item) => item.id !== id)
|
||||||
}
|
}
|
||||||
@@ -643,7 +711,10 @@ function buildPreviewData() {
|
|||||||
const data = {}
|
const data = {}
|
||||||
variableMappings.value.forEach((item) => {
|
variableMappings.value.forEach((item) => {
|
||||||
if (!item.variableKey) return
|
if (!item.variableKey) return
|
||||||
data[item.variableKey] = item.bindField || `[${item.variableKey}]`
|
data[item.variableKey] =
|
||||||
|
templateFieldLabelMap.value[item.bindField] ||
|
||||||
|
item.bindField ||
|
||||||
|
`[${item.variableKey}]`
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
@@ -717,7 +788,6 @@ function buildRecord(status) {
|
|||||||
version: formState.version,
|
version: formState.version,
|
||||||
status,
|
status,
|
||||||
variableCount: variableMappings.value.filter((item) => item.variableKey).length,
|
variableCount: variableMappings.value.filter((item) => item.variableKey).length,
|
||||||
selfPartyRole: formState.selfPartyRole,
|
|
||||||
publishOrgIds: [...(formState.publishOrgIds || [])],
|
publishOrgIds: [...(formState.publishOrgIds || [])],
|
||||||
publishOrgNames,
|
publishOrgNames,
|
||||||
updatedBy: formState.updatedBy || '当前用户',
|
updatedBy: formState.updatedBy || '当前用户',
|
||||||
@@ -805,9 +875,81 @@ async function loadTemplateBufferFromRecord(record) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function triggerBlobDownload(blob, fileName) {
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = fileName
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVariableGuideText() {
|
||||||
|
const lines = [
|
||||||
|
'合同模板变量说明',
|
||||||
|
'',
|
||||||
|
'推荐维护方式:',
|
||||||
|
'1. 下载当前模板到本地 Word 编辑正文内容。',
|
||||||
|
'2. 在正文中插入占位符,例如 {contractName}、{partyAName}。',
|
||||||
|
'3. 保存后重新上传 .docx 文件回传系统。',
|
||||||
|
'4. 在平台内完成变量识别、字段映射、预览和发布。',
|
||||||
|
'',
|
||||||
|
'可用占位符清单:',
|
||||||
|
...templateFieldList.value.map(
|
||||||
|
(item) => `- ${item.label}: ${buildPlaceholderText(item.value)}`,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
return lines.join('\r\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDownloadVariableGuide() {
|
||||||
|
const blob = new Blob([buildVariableGuideText()], {
|
||||||
|
type: 'text/plain;charset=utf-8',
|
||||||
|
})
|
||||||
|
const fileName = `合同模板变量说明-${dayjs().format('YYYYMMDDHHmmss')}.txt`
|
||||||
|
triggerBlobDownload(blob, fileName)
|
||||||
|
message.success('变量说明已开始下载')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDownloadCurrentTemplate() {
|
||||||
|
if (!hasTemplateFile.value) {
|
||||||
|
message.warning('当前暂无可下载的模板文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let arrayBuffer = templateArrayBuffer.value
|
||||||
|
if (!arrayBuffer && formState.templateFilePath) {
|
||||||
|
const response = await downloadTemplateFile(formState.templateFilePath)
|
||||||
|
const downloaded = response?.data ?? response
|
||||||
|
arrayBuffer =
|
||||||
|
downloaded instanceof ArrayBuffer ? downloaded : downloaded?.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!arrayBuffer) {
|
||||||
|
message.warning('模板文件尚未加载完成,请稍后重试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([arrayBuffer], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
})
|
||||||
|
const fileName =
|
||||||
|
formState.templateFileName || `${formState.templateName || '合同模板'}.docx`
|
||||||
|
triggerBlobDownload(blob, fileName)
|
||||||
|
message.success('模板文件已开始下载,可在本地 Word 编辑后重新上传')
|
||||||
|
} catch (error) {
|
||||||
|
message.error('模板文件下载失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function showModal(record = {}, mode = 'create') {
|
async function showModal(record = {}, mode = 'create') {
|
||||||
resetState()
|
resetState()
|
||||||
modalMode.value = mode || 'create'
|
modalMode.value = mode || 'create'
|
||||||
|
await ensureTemplateFieldListLoaded()
|
||||||
if (!orgOptions.value.length) {
|
if (!orgOptions.value.length) {
|
||||||
const orgList = await queryManifestOrgOptions()
|
const orgList = await queryManifestOrgOptions()
|
||||||
orgOptions.value = (orgList || []).map((item) => ({
|
orgOptions.value = (orgList || []).map((item) => ({
|
||||||
@@ -910,6 +1052,10 @@ defineExpose({
|
|||||||
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-alert {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-footer {
|
.panel-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -940,6 +1086,7 @@ defineExpose({
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
@@ -972,17 +1119,37 @@ defineExpose({
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-help {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-list {
|
.catalog-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
.catalog-tag {
|
.catalog-tag {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catalog-action {
|
||||||
|
padding-inline: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.mapping-list {
|
.mapping-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { getDictDataByType } from "@/api/dict.js";
|
||||||
|
|
||||||
|
export const TEMPLATE_FIELD_DICT_TYPE = "CONTRACT_TEMPLATE_FIELDS";
|
||||||
|
|
||||||
|
let cachedTemplateFieldList = null;
|
||||||
|
|
||||||
|
function cloneTemplateFieldList(list = []) {
|
||||||
|
return list.map((item) => ({
|
||||||
|
...item,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTemplateFieldList(list = []) {
|
||||||
|
return list
|
||||||
|
.map((item) => ({
|
||||||
|
label: item?.k || item?.label || "",
|
||||||
|
value: item?.v || item?.value || "",
|
||||||
|
remark: item?.remark || "",
|
||||||
|
}))
|
||||||
|
.filter((item) => item.label && item.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadTemplateFieldList() {
|
||||||
|
if (cachedTemplateFieldList?.length) {
|
||||||
|
return cloneTemplateFieldList(cachedTemplateFieldList);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await getDictDataByType(TEMPLATE_FIELD_DICT_TYPE);
|
||||||
|
cachedTemplateFieldList = normalizeTemplateFieldList(result);
|
||||||
|
return cloneTemplateFieldList(cachedTemplateFieldList);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTemplateFieldOptions(list = []) {
|
||||||
|
return list.map((item) => ({
|
||||||
|
label: `${item.label}(${item.value})`,
|
||||||
|
value: item.value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTemplateFieldLabelMap(list = []) {
|
||||||
|
const map = {};
|
||||||
|
list.forEach((item) => {
|
||||||
|
map[item.value] = item.label;
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
@@ -31,6 +31,10 @@ export default defineConfig({
|
|||||||
target: 'http://127.0.0.1:8899',
|
target: 'http://127.0.0.1:8899',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/admin': {
|
||||||
|
target: 'http://127.0.0.1:8899',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
'/trans': {
|
'/trans': {
|
||||||
target: 'http://127.0.0.1:8899',
|
target: 'http://127.0.0.1:8899',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user