feat(oa): 添加CRM报价模板预览功能
- 集成umo-editor富文本编辑器实现模板渲染 - 实现报价数据模板变量替换功能 - 添加表格和数组数据动态填充支持 - 配置页面边距和打印模板样式设置 - 实现模板数据递归解析和渲染逻辑 - 添加错误处理和用户提示功能dev
parent
ebb821acad
commit
e9fb4916ba
@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="width: 100%; display: inline-block; vertical-align: top; height: calc(100vh - 50px - 34px)">
|
||||
<umo-editor v-bind="options" ref="editorRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCurrentInstance, onMounted, ref } from 'vue';
|
||||
import { UmoEditor } from '@umoteam/editor';
|
||||
import { getPrintTemplate } from '@/api/oa/base/printTemplate';
|
||||
import { templateAssign } from '@/api/oa/base/templateVariable';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const proxy = getCurrentInstance()?.proxy;
|
||||
|
||||
const editorRef = ref();
|
||||
const template = ref<any>({});
|
||||
let templateData: Record<string, any> = {};
|
||||
const defaultMargin = { left: 2.1, right: 2.1, top: 2.2, bottom: 2.2 };
|
||||
|
||||
const options = ref({
|
||||
document: {
|
||||
title: '',
|
||||
content: { type: 'doc', content: [] },
|
||||
placeholder: { en_US: 'Please enter the document content...', zh_CN: '请输入文档内容...' },
|
||||
enableSpellcheck: true,
|
||||
enableMarkdown: false,
|
||||
enableBubbleMenu: true,
|
||||
enableBlockMenu: true,
|
||||
readOnly: true,
|
||||
autofocus: false,
|
||||
characterLimit: 0,
|
||||
typographyRules: { emDash: false },
|
||||
editorProps: {},
|
||||
parseOptions: { preserveWhitespace: 'full' },
|
||||
autoSave: { enabled: false }
|
||||
},
|
||||
toolbar: { menus: ['export'] },
|
||||
disableExtensions: [
|
||||
'ordered-list',
|
||||
'quote',
|
||||
'code',
|
||||
'select-all',
|
||||
'import-word',
|
||||
'viewer',
|
||||
'print',
|
||||
'export-image',
|
||||
'export-text',
|
||||
'share',
|
||||
'embed',
|
||||
'image',
|
||||
'link'
|
||||
],
|
||||
page: { showBreakMarks: false, defaultMargin }
|
||||
});
|
||||
|
||||
// 常量赋值
|
||||
const renderTemplate = (str: string, data: Record<string, any>) => {
|
||||
return str.replace(/#\{(.*?)\}/g, (_, path) => {
|
||||
const arr = path.split('.');
|
||||
if (arr.length > 0) {
|
||||
arr[0] = `#{${arr[0]}}`;
|
||||
}
|
||||
return arr.reduce((o: any, k: string) => o?.[k], data) ?? ' ';
|
||||
});
|
||||
};
|
||||
|
||||
// 数组赋值
|
||||
const rowRenderTemplate = (str: string, index: number) => {
|
||||
let res = str;
|
||||
res = res.replace(/\^\{(.*?)\}/g, (_, path) => {
|
||||
return templateData[`^{${path}}`]?.[index] ?? ' ';
|
||||
});
|
||||
res = res.replace(/#\{(.*?)\}/g, (_, path) => {
|
||||
const arr = path.split('.');
|
||||
if (arr.length > 0) {
|
||||
arr[0] = `#{${arr[0]}}`;
|
||||
}
|
||||
return arr.reduce((o: any, k: string) => o?.[k], templateData) ?? ' ';
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
// 判断是否包含数组占位符
|
||||
const hasCaretPattern = (arr: any[]) => {
|
||||
const regex = /\^\{.*?\}/;
|
||||
let length = 0;
|
||||
let res = false;
|
||||
|
||||
function traverse(items: any[]) {
|
||||
for (const item of items) {
|
||||
if (item.text && regex.test(item.text)) {
|
||||
length = Math.max(length, templateData[item.text]?.length || 0);
|
||||
res = true;
|
||||
}
|
||||
if (item.content && Array.isArray(item.content)) {
|
||||
traverse(item.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(arr);
|
||||
return [res, length] as [boolean, number];
|
||||
};
|
||||
|
||||
// 行数据填充
|
||||
const fillRowData = (e: any, index: number) => {
|
||||
if (Array.isArray(e.content)) {
|
||||
e.content.map((item: any) => fillRowData(item, index));
|
||||
} else {
|
||||
e.text = rowRenderTemplate(e.text || '', index);
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
// 表格处理
|
||||
const tableTemplate = (table: any[]) => {
|
||||
const data = JSON.parse(JSON.stringify(table));
|
||||
const arr: any[] = [];
|
||||
data.forEach((item: any) => {
|
||||
const [bol, length] = hasCaretPattern(JSON.parse(JSON.stringify(item.content)));
|
||||
if (bol) {
|
||||
const obj = JSON.parse(JSON.stringify(item));
|
||||
Array(length)
|
||||
.fill(0)
|
||||
.forEach((_, index) => {
|
||||
arr.push(fillRowData(JSON.parse(JSON.stringify(obj)), index));
|
||||
});
|
||||
} else {
|
||||
arr.push(fillData(item));
|
||||
}
|
||||
});
|
||||
return arr.filter((item) => item);
|
||||
};
|
||||
|
||||
// 递归填充
|
||||
const fillData = (e: any): any => {
|
||||
if (e.type === 'table') {
|
||||
e.content = tableTemplate(e.content);
|
||||
} else {
|
||||
if (Array.isArray(e.content)) {
|
||||
e.content.map((item: any) => fillData(item));
|
||||
} else {
|
||||
e.text = renderTemplate(e.text || '', templateData);
|
||||
}
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const quoteIdParam = Array.isArray(route.query.quoteId) ? route.query.quoteId[0] : route.query.quoteId;
|
||||
const templateIdParam = Array.isArray(route.query.templateId) ? route.query.templateId[0] : route.query.templateId;
|
||||
const quoteId = quoteIdParam ? Number(quoteIdParam) : undefined;
|
||||
const templateId = templateIdParam ? String(templateIdParam) : undefined;
|
||||
|
||||
if (!quoteId || !templateId) {
|
||||
proxy?.$modal.msgError('缺少模板导出参数,请返回列表页重试');
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: assignData } = await templateAssign({ templateType: 4, quoteId });
|
||||
templateData = {};
|
||||
(assignData || []).forEach((item: any) => {
|
||||
templateData[item.varName] = item.varValue ?? ' ';
|
||||
});
|
||||
|
||||
const tplResp = await getPrintTemplate(templateId);
|
||||
template.value = tplResp.data;
|
||||
const editor = editorRef.value;
|
||||
|
||||
const pageConfig = JSON.parse(tplResp.data.pageConfig || '{}');
|
||||
const margin = pageConfig.pageMargin || defaultMargin;
|
||||
options.value.page.defaultMargin = {
|
||||
left: margin.left ?? defaultMargin.left,
|
||||
right: margin.right ?? defaultMargin.right,
|
||||
top: margin.top ?? defaultMargin.top,
|
||||
bottom: margin.bottom ?? defaultMargin.bottom
|
||||
};
|
||||
|
||||
const docData = JSON.parse(tplResp.data.templateData || '{}');
|
||||
const dataFilled = fillData(docData);
|
||||
editor?.setContent(dataFilled);
|
||||
} catch (error) {
|
||||
console.error('quoteView export init error', error);
|
||||
proxy?.$modal.msgError('加载报价模板失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.title {
|
||||
width: 100%;
|
||||
line-height: 40px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fieldList {
|
||||
width: 100%;
|
||||
height: calc(100vh - 50px - 34px - 40px);
|
||||
overflow-y: auto;
|
||||
|
||||
.fieldItem {
|
||||
width: 80%;
|
||||
font-size: 12px;
|
||||
padding-left: 15px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
|
||||
.btn {
|
||||
padding: 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .umo-toolbar-actions.ribbon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/deep/ .umo-status-bar-left .umo-button:nth-child(4) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/deep/ .umo-status-bar-left .umo-button:nth-child(5) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.umo-footer {
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue