添加字典
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,
|
||||
} from "@/api/manifest.js";
|
||||
import { uploadLaunchFile } from "@/api/launch.js";
|
||||
import {
|
||||
buildTemplateFieldLabelMap,
|
||||
loadTemplateFieldList,
|
||||
} from "../../shared/templateFieldDict.js";
|
||||
import {
|
||||
contractSourceOptions,
|
||||
contractTypeOptions,
|
||||
@@ -218,32 +222,6 @@ const contractCreateModeOptions = [
|
||||
{ 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 元) ==========
|
||||
function formatContractAmount(value) {
|
||||
if (value === undefined || value === null || value === "") return "";
|
||||
@@ -763,63 +741,12 @@ function copyContractFormData(data = {}) {
|
||||
// ========== 模板变量预览数据构建 ==========
|
||||
|
||||
/** 将合同表单数据转换为模板变量字典(用于 docxtemplater 渲染) */
|
||||
function buildTemplatePreviewData(
|
||||
formData,
|
||||
templateLabel = "",
|
||||
creationMode = "template",
|
||||
) {
|
||||
const form = copyContractFormData(formData);
|
||||
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 || {}),
|
||||
};
|
||||
function buildTemplateRenderData(templateFieldValues = {}) {
|
||||
const data = {};
|
||||
(templateFieldList.value || []).forEach((item) => {
|
||||
data[item.value] = templateFieldValues?.[item.value] ?? "";
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// ========== 提交报文构建 ==========
|
||||
@@ -925,6 +852,7 @@ const previewError = ref("");
|
||||
const previewBlob = ref(null);
|
||||
const manualBodyFileList = ref([]);
|
||||
const publishedTemplates = ref([]);
|
||||
const templateFieldList = ref([]);
|
||||
|
||||
const templateBufferCache = new Map();
|
||||
let previewTimer = null;
|
||||
@@ -945,13 +873,9 @@ const isViewMode = computed(() => modalMode.value === "view");
|
||||
|
||||
const modalTitle = computed(() => "合同正文配置");
|
||||
|
||||
const templateFieldLabelMap = computed(() => {
|
||||
const map = {};
|
||||
templateFieldList.forEach((item) => {
|
||||
map[item.value] = item.label;
|
||||
});
|
||||
return map;
|
||||
});
|
||||
const templateFieldLabelMap = computed(() =>
|
||||
buildTemplateFieldLabelMap(templateFieldList.value),
|
||||
);
|
||||
|
||||
const launchFieldList = computed(() => {
|
||||
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) {
|
||||
// 1. 优先返回用户在"模板信息录入"中自定义的值
|
||||
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 "";
|
||||
return contractDetailState.value.templateFieldValues?.[fieldKey] ?? "";
|
||||
}
|
||||
|
||||
function setLaunchFieldValue(fieldKey, value) {
|
||||
@@ -1044,11 +909,7 @@ function setLaunchFieldValue(fieldKey, value) {
|
||||
}
|
||||
|
||||
const templateVariables = computed(() =>
|
||||
buildTemplatePreviewData(
|
||||
contractDetailState.value,
|
||||
activeTemplate.value?.templateName || "",
|
||||
creationMode.value,
|
||||
),
|
||||
buildTemplateRenderData(contractDetailState.value.templateFieldValues || {}),
|
||||
);
|
||||
|
||||
const hasPreviewSource = computed(() => {
|
||||
@@ -1137,12 +998,17 @@ function syncManualBodyFileList() {
|
||||
}
|
||||
|
||||
function loadPublishedTemplates() {
|
||||
// TODO: 这里后续替换为“已发布模板列表接口”
|
||||
return queryPublishedManifestList().then((list) => {
|
||||
publishedTemplates.value = list;
|
||||
});
|
||||
}
|
||||
|
||||
function loadTemplateFields() {
|
||||
return loadTemplateFieldList().then((list) => {
|
||||
templateFieldList.value = list || [];
|
||||
});
|
||||
}
|
||||
|
||||
function getManifestDetailLocal(templateId) {
|
||||
return (
|
||||
publishedTemplates.value.find((item) => item.id === templateId) || null
|
||||
@@ -1234,22 +1100,16 @@ async function buildFilledDocumentBlob() {
|
||||
},
|
||||
});
|
||||
//这里是生成的测试数据
|
||||
const sourceValues = buildTemplatePreviewData(
|
||||
contractDetailState.value,
|
||||
activeTemplate.value?.templateName || "",
|
||||
creationMode.value,
|
||||
const sourceValues = buildTemplateRenderData(
|
||||
contractDetailState.value.templateFieldValues || {},
|
||||
);
|
||||
const mappings = activeTemplate.value.variableMappings || [];
|
||||
|
||||
const renderData = {};
|
||||
//这部分是匹配的关键
|
||||
if (mappings.length) {
|
||||
mappings.forEach((item) => {
|
||||
if (!item.variableKey) return;
|
||||
renderData[item.variableKey] =
|
||||
sourceValues[item.bindField] ??
|
||||
contractDetailState.value[item.bindField] ??
|
||||
"";
|
||||
renderData[item.variableKey] = sourceValues[item.bindField] ?? "";
|
||||
});
|
||||
} else {
|
||||
Object.assign(renderData, sourceValues);
|
||||
@@ -1460,7 +1320,7 @@ function resetForm() {
|
||||
async function showModal(initialValues = {}, mode = "create") {
|
||||
modalMode.value = mode || "create";
|
||||
const isCreate = modalMode.value === "create";
|
||||
await loadPublishedTemplates();
|
||||
await Promise.all([loadPublishedTemplates(), loadTemplateFields()]);
|
||||
|
||||
creationMode.value = isCreate
|
||||
? "template"
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<section class="template-config-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h3>合同范本维护</h3>
|
||||
<p>支持模板分类、Word 文件上传、变量自动识别与业务字段匹配。</p>
|
||||
<h3>合同模板配置中心</h3>
|
||||
<p>在线维护模板配置与变量映射,推荐在本地 Word 编辑正文后回传系统。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,7 +97,14 @@
|
||||
</div>
|
||||
</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>
|
||||
<div class="meta-label">当前模板文件</div>
|
||||
@@ -105,7 +112,12 @@
|
||||
{{ formState.templateFileName || '尚未上传 .docx 模板文件' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-space v-if="!isViewMode">
|
||||
<a-button @click="handleDownloadCurrentTemplate" :disabled="!hasTemplateFile">
|
||||
<DownloadOutlined />
|
||||
下载当前模板
|
||||
</a-button>
|
||||
<a-button @click="handleReDetectVariables">
|
||||
<ReloadOutlined />
|
||||
重新识别变量
|
||||
@@ -134,33 +146,58 @@
|
||||
<p class="ant-upload-hint">
|
||||
上传后会自动识别花括号变量,如 `{contractName}`、`{partyAName}`,并生成变量匹配清单。
|
||||
</p>
|
||||
<p class="ant-upload-hint">
|
||||
如需修改正文内容,建议先下载模板到本地 Word 编辑后再重新上传回传。
|
||||
</p>
|
||||
</a-upload-dragger>
|
||||
</a-card>
|
||||
|
||||
<a-card size="small" title="变量匹配" class="panel-card">
|
||||
<a-card size="small" title="占位符工具栏与变量映射" class="panel-card">
|
||||
<div class="variable-header">
|
||||
<div>
|
||||
<div class="meta-label">已识别变量</div>
|
||||
<div class="meta-value">{{ detectedVariableKeys.length ? detectedVariableKeys.join('、') : '暂未识别到变量' }}</div>
|
||||
<div class="catalog-help">
|
||||
点击下方字段标签可直接复制占位符,点击“加入映射”可快速补充变量绑定。
|
||||
</div>
|
||||
</div>
|
||||
<a-button v-if="!isViewMode" type="primary" ghost @click="handleAddManualVariable">
|
||||
<PlusOutlined />
|
||||
手动新增变量
|
||||
</a-button>
|
||||
<a-space>
|
||||
<a-button @click="handleDownloadVariableGuide">
|
||||
<DownloadOutlined />
|
||||
下载变量说明
|
||||
</a-button>
|
||||
<a-button v-if="!isViewMode" type="primary" ghost @click="handleAddManualVariable">
|
||||
<PlusOutlined />
|
||||
手动新增变量
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div class="catalog-wrap">
|
||||
<div class="catalog-title">可绑定业务字段</div>
|
||||
<div class="catalog-title">占位符工具栏</div>
|
||||
<div class="catalog-list">
|
||||
<a-tag
|
||||
<div
|
||||
v-for="item in templateFieldList"
|
||||
:key="item.value"
|
||||
color="blue"
|
||||
class="catalog-tag"
|
||||
@click="handleQuickAddCatalogField(item.value)"
|
||||
class="catalog-item"
|
||||
>
|
||||
{{ 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>
|
||||
|
||||
@@ -242,7 +279,7 @@
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h3>模板预览</h3>
|
||||
<p>右侧基于当前变量匹配渲染 Word 模板效果,便于检查变量位置是否正确。</p>
|
||||
<p>右侧基于当前变量映射渲染模板效果,用于检查占位符位置与展示结果,本页不直接在线修改 Word 正文。</p>
|
||||
</div>
|
||||
<a-space size="small">
|
||||
<a-tag color="geekblue">{{ formState.source || '未分类' }}</a-tag>
|
||||
@@ -253,7 +290,7 @@
|
||||
<div class="preview-shell">
|
||||
<div v-if="!templateArrayBuffer" class="preview-state">
|
||||
<FileWordOutlined style="font-size: 42px; color: #94a3b8" />
|
||||
<p>上传 `.docx` 模板文件后,这里会显示填充后的预览。</p>
|
||||
<p>上传 `.docx` 模板文件后,这里会显示填充后的预览;正文修改建议在本地 Word 完成后回传。</p>
|
||||
</div>
|
||||
<div v-else ref="previewRef" class="preview-stage"></div>
|
||||
<div v-if="rendering" class="preview-state preview-mask">
|
||||
@@ -274,7 +311,13 @@
|
||||
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
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 PizZip from 'pizzip'
|
||||
import { renderAsync } from 'docx-preview'
|
||||
@@ -284,6 +327,11 @@ import {
|
||||
uploadTemplateFile,
|
||||
} from '@/api/manifest.js'
|
||||
import { contractTypeTreeOptions } from '../contractTypeOptions.js'
|
||||
import {
|
||||
buildTemplateFieldLabelMap,
|
||||
buildTemplateFieldOptions,
|
||||
loadTemplateFieldList,
|
||||
} from '../../shared/templateFieldDict.js'
|
||||
|
||||
// ========== 模板管理字典(后续可由后端接口替换) ==========
|
||||
|
||||
@@ -298,35 +346,6 @@ const templateSourceOptions = [
|
||||
{ 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 = []) {
|
||||
return list.map((item, index) => ({
|
||||
@@ -349,15 +368,23 @@ const templateUploadList = ref([])
|
||||
const variableMappings = ref([])
|
||||
const detectedVariableKeys = ref([])
|
||||
const orgOptions = ref([])
|
||||
const templateFieldList = ref([])
|
||||
|
||||
let previewTimer = null
|
||||
let renderTaskId = 0
|
||||
|
||||
const templateFieldOptions = computed(() =>
|
||||
buildTemplateFieldOptions(templateFieldList.value),
|
||||
)
|
||||
|
||||
const templateFieldLabelMap = computed(() =>
|
||||
buildTemplateFieldLabelMap(templateFieldList.value),
|
||||
)
|
||||
|
||||
const formRules = {
|
||||
templateName: [{ required: true, message: '请输入范本名称', trigger: 'blur' }],
|
||||
source: [{ required: true, message: '请选择范本类型', trigger: 'change' }],
|
||||
contractType: [{ required: true, message: '请选择合同类型', trigger: 'change' }],
|
||||
selfPartyRole: [{ required: true, message: '请选择我方地位', trigger: 'change' }],
|
||||
version: [{ required: true, message: '请输入版本号', trigger: 'blur' }],
|
||||
publishOrgIds: [{ required: true, type: 'array', min: 1, message: '请选择发布范围', trigger: 'change' }],
|
||||
}
|
||||
@@ -369,7 +396,6 @@ function createDefaultFormState() {
|
||||
templateCode: '',
|
||||
source: '标准模板',
|
||||
contractType: 'MM',
|
||||
selfPartyRole: '甲方',
|
||||
version: 'V1.0',
|
||||
status: 'draft',
|
||||
description: '',
|
||||
@@ -387,10 +413,14 @@ const formState = reactive(createDefaultFormState())
|
||||
|
||||
const isViewMode = computed(() => modalMode.value === 'view')
|
||||
|
||||
const hasTemplateFile = computed(
|
||||
() => !!(formState.templateFilePath || formState.templateFileName || templateArrayBuffer.value),
|
||||
)
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
if (modalMode.value === 'view') return '查看合同范本'
|
||||
if (modalMode.value === 'edit') return '编辑合同范本'
|
||||
return '新增合同范本'
|
||||
if (modalMode.value === 'view') return '查看模板配置'
|
||||
if (modalMode.value === 'edit') return '编辑模板配置'
|
||||
return '新增模板配置'
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -431,6 +461,13 @@ function resetState() {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTemplateFieldListLoaded() {
|
||||
if (templateFieldList.value.length) {
|
||||
return
|
||||
}
|
||||
templateFieldList.value = await loadTemplateFieldList()
|
||||
}
|
||||
|
||||
function schedulePreview() {
|
||||
clearPreviewTimer()
|
||||
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) {
|
||||
variableMappings.value = variableMappings.value.filter((item) => item.id !== id)
|
||||
}
|
||||
@@ -643,7 +711,10 @@ function buildPreviewData() {
|
||||
const data = {}
|
||||
variableMappings.value.forEach((item) => {
|
||||
if (!item.variableKey) return
|
||||
data[item.variableKey] = item.bindField || `[${item.variableKey}]`
|
||||
data[item.variableKey] =
|
||||
templateFieldLabelMap.value[item.bindField] ||
|
||||
item.bindField ||
|
||||
`[${item.variableKey}]`
|
||||
})
|
||||
return data
|
||||
}
|
||||
@@ -717,7 +788,6 @@ function buildRecord(status) {
|
||||
version: formState.version,
|
||||
status,
|
||||
variableCount: variableMappings.value.filter((item) => item.variableKey).length,
|
||||
selfPartyRole: formState.selfPartyRole,
|
||||
publishOrgIds: [...(formState.publishOrgIds || [])],
|
||||
publishOrgNames,
|
||||
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') {
|
||||
resetState()
|
||||
modalMode.value = mode || 'create'
|
||||
await ensureTemplateFieldListLoaded()
|
||||
if (!orgOptions.value.length) {
|
||||
const orgList = await queryManifestOrgOptions()
|
||||
orgOptions.value = (orgList || []).map((item) => ({
|
||||
@@ -910,6 +1052,10 @@ defineExpose({
|
||||
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.workflow-alert {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -940,6 +1086,7 @@ defineExpose({
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
@@ -972,17 +1119,37 @@ defineExpose({
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.catalog-help {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.catalog-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.catalog-action {
|
||||
padding-inline: 4px;
|
||||
}
|
||||
|
||||
.mapping-list {
|
||||
display: flex;
|
||||
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',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/admin': {
|
||||
target: 'http://127.0.0.1:8899',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/trans': {
|
||||
target: 'http://127.0.0.1:8899',
|
||||
changeOrigin: true,
|
||||
|
||||
Reference in New Issue
Block a user