落地报价模板主账与后台:合同映射、灌种 SQL/脚本、智能体填表,并补测服运维探针与合同文档对齐。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,20 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 报价模板页:对齐原型 admin-quote-templates.html。
|
||||
* - 上传按钮只在顶栏一处(禁止双按钮)
|
||||
* - 弹窗用原生 overlay(不依赖 a-modal,避免遮罩残留导致整页点不了)
|
||||
* - 操作:预览 / 编辑 / 启用|禁用
|
||||
* - 本阶段仅 mock,暂不对接主账上传/启用接口(页面与侧栏必须保留)
|
||||
* 报价模板页:对接主账 /inquiry/quoteTemplate。
|
||||
* - 上传按钮只在顶栏一处;弹窗用原生 overlay
|
||||
* - 版式由上传 Excel 决定;列表展示「映射点数」而非假占位符
|
||||
* - 预览:弹窗内按映射回填合同样例事实(接近原型可读性);权威版式仍以下载 Excel 为准
|
||||
*/
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import {
|
||||
ALL_CATEGORIES,
|
||||
CATEGORY_BY_BIZ,
|
||||
mockTemplates,
|
||||
mapApiRow,
|
||||
type TemplateRow,
|
||||
} from "/@/views/inquiry/mock/templates";
|
||||
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||||
import {
|
||||
disableTemplate,
|
||||
downloadTemplateFile,
|
||||
editTemplateMeta,
|
||||
enableTemplate,
|
||||
getTemplateList,
|
||||
getTemplateVersion,
|
||||
uploadTemplate,
|
||||
} from "/@/views/inquiry/template/template.api";
|
||||
import {
|
||||
buildPreviewViewModel,
|
||||
emptyPreviewViewModel,
|
||||
type PreviewViewModel,
|
||||
} from "/@/views/inquiry/template/previewSample";
|
||||
|
||||
// buildTemplateFileUrl 已由 downloadTemplateFile 替代(鉴权 blob 下载)
|
||||
|
||||
/** 清掉可能残留的 Ant Modal 遮罩(会导致整页点不了) */
|
||||
function clearOrphanAntMasks() {
|
||||
@@ -28,11 +43,8 @@ function clearOrphanAntMasks() {
|
||||
document.body.classList.remove("ant-scrolling-effect");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
clearOrphanAntMasks();
|
||||
});
|
||||
|
||||
const rows = ref<TemplateRow[]>([...mockTemplates]);
|
||||
const rows = ref<TemplateRow[]>([]);
|
||||
const loading = ref(false);
|
||||
const keyword = ref("");
|
||||
const bizFilter = ref("");
|
||||
const categoryFilter = ref("");
|
||||
@@ -42,12 +54,16 @@ const formOpen = ref(false);
|
||||
const editing = ref<TemplateRow | null>(null);
|
||||
const previewOpen = ref(false);
|
||||
const previewRow = ref<TemplateRow | null>(null);
|
||||
/** 弹窗内映射回填一览(合同样例事实);下载 Excel 仍走 previewSample */
|
||||
const previewVm = ref<PreviewViewModel>(emptyPreviewViewModel());
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
const pendingFile = ref<File | null>(null);
|
||||
|
||||
const form = reactive({
|
||||
name: "",
|
||||
bizType: "海运" as TemplateRow["bizType"],
|
||||
/** 多行关键词;每行可含逗号,加减行数(对齐旧仓 keywordItems) */
|
||||
category: "",
|
||||
mode: "auto" as "auto" | "manual",
|
||||
keywordItems: [""] as string[],
|
||||
isDefault: false,
|
||||
fileName: "",
|
||||
@@ -64,30 +80,29 @@ const parseState = reactive({
|
||||
|
||||
const formTitle = computed(() => (editing.value ? "编辑报价模板" : "上传报价模板"));
|
||||
const formSub = computed(() =>
|
||||
editing.value ? "修改基本信息或更换模板文件后重新解析" : "上传 Excel / PDF,系统自动解析占位符",
|
||||
editing.value
|
||||
? "可改基本信息;更换文件将追加新版本并重新解析映射"
|
||||
: "上传 Excel,系统按黄区与标签自动建议单元格映射",
|
||||
);
|
||||
const formOkText = computed(() => (editing.value ? "保存修改" : "发布模板"));
|
||||
const formOkText = computed(() => (editing.value ? "保存" : "上传并解析"));
|
||||
const previewFootHint =
|
||||
"用户端生成时将复制模板文件并替换 {{占位符}},不改变原有 Excel / PDF 样式";
|
||||
"用户端生成时将复制本版 Excel 并按映射写入已冻结事实(黄区不改),不改变原有 Excel / PDF 样式;模板改版请重新上传";
|
||||
|
||||
const previewTotalText = computed(() => {
|
||||
const vm = previewVm.value;
|
||||
if (!vm.totalAmount) return "—";
|
||||
const n = Number(vm.totalAmount);
|
||||
const amt = Number.isFinite(n)
|
||||
? n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
: vm.totalAmount;
|
||||
return `${vm.currency || "USD"} ${amt}`;
|
||||
});
|
||||
|
||||
/** 表单不再手选分类:按业务取默认分类写入数据(列表筛选仍可用) */
|
||||
function defaultCategoryForBiz(biz: TemplateRow["bizType"]) {
|
||||
return (CATEGORY_BY_BIZ[biz] || [])[0] || "";
|
||||
}
|
||||
|
||||
const filtered = computed(() =>
|
||||
rows.value.filter((r) => {
|
||||
const kw = keyword.value.trim().toLowerCase();
|
||||
if (kw && !`${r.name}${r.bizType}${r.fileLabel}${r.category}${r.keywords.join(" ")}`.toLowerCase().includes(kw)) {
|
||||
return false;
|
||||
}
|
||||
if (bizFilter.value && r.bizType !== bizFilter.value) return false;
|
||||
if (categoryFilter.value && r.category !== categoryFilter.value) return false;
|
||||
if (statusFilter.value === "enabled" && r.status !== "enabled") return false;
|
||||
if (statusFilter.value === "disabled" && r.status !== "disabled") return false;
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
const filtered = computed(() => rows.value);
|
||||
|
||||
function bizTag(biz: string) {
|
||||
if (biz === "海运") return "tag-teal";
|
||||
@@ -95,16 +110,44 @@ function bizTag(biz: string) {
|
||||
return "tag-blue";
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await getTemplateList({
|
||||
keyword: keyword.value.trim() || undefined,
|
||||
bizType: bizFilter.value || undefined,
|
||||
category: categoryFilter.value || undefined,
|
||||
status: statusFilter.value || undefined,
|
||||
pageNo: 1,
|
||||
pageSize: 200,
|
||||
});
|
||||
const records = res?.records || res?.result?.records || [];
|
||||
rows.value = (records as any[]).map(mapApiRow);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || "加载模板列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
clearOrphanAntMasks();
|
||||
loadList();
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(form, {
|
||||
name: "",
|
||||
bizType: "海运" as TemplateRow["bizType"],
|
||||
category: defaultCategoryForBiz("海运"),
|
||||
mode: "auto" as const,
|
||||
keywordItems: [""],
|
||||
isDefault: false,
|
||||
fileName: "",
|
||||
fileMeta: "",
|
||||
format: "excel" as const,
|
||||
});
|
||||
pendingFile.value = null;
|
||||
parseState.visible = false;
|
||||
parseState.ok = true;
|
||||
parseState.title = "";
|
||||
@@ -122,22 +165,25 @@ function openEdit(row: TemplateRow) {
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
bizType: row.bizType,
|
||||
category: row.category || defaultCategoryForBiz(row.bizType),
|
||||
mode: row.mode || "auto",
|
||||
keywordItems: row.keywords.length ? [...row.keywords] : [""],
|
||||
isDefault: Boolean(row.isDefault),
|
||||
fileName: row.fileName,
|
||||
fileMeta: `${row.format === "pdf" ? "PDF" : "Excel"} · 已上传`,
|
||||
format: row.format,
|
||||
});
|
||||
pendingFile.value = null;
|
||||
parseState.visible = true;
|
||||
parseState.ok = row.parseStatus === "已解析";
|
||||
parseState.title = parseState.ok ? "解析成功" : "解析失败";
|
||||
parseState.desc = parseState.ok
|
||||
? `识别 ${row.placeholderCount} 个占位符,已映射至工单字段`
|
||||
: "请更换模板文件后重试";
|
||||
parseState.ok = row.parseStatus === "已解析" || row.parseStatus === "映射未完成";
|
||||
parseState.title = row.parseStatus || "解析状态";
|
||||
parseState.desc =
|
||||
row.parseMessage ||
|
||||
`已识别 ${row.mappingPointCount} 个映射点` +
|
||||
(row.unmappedRequired?.length ? `;未映射:${row.unmappedRequired.join(",")}` : "");
|
||||
formOpen.value = true;
|
||||
}
|
||||
|
||||
/** 去空去重,保留框内逗号;至少一条才算有效 */
|
||||
function normalizeKeywordItems(items: string[]): string[] {
|
||||
const unique: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
@@ -150,32 +196,49 @@ function normalizeKeywordItems(items: string[]): string[] {
|
||||
return unique;
|
||||
}
|
||||
|
||||
/** 加一行关键词,最多 20 行,避免误点刷屏 */
|
||||
function addKeyword() {
|
||||
if (form.keywordItems.length >= 20) return;
|
||||
form.keywordItems.push("");
|
||||
}
|
||||
|
||||
/** 至少保留一个输入框 */
|
||||
function removeKeyword(index: number) {
|
||||
if (form.keywordItems.length <= 1) return;
|
||||
form.keywordItems.splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同业务只保留一个默认模板:勾选本条时清掉同业务其它默认。
|
||||
* 演示环境本地互斥;接主账后由接口保证。
|
||||
* 打开预览:先展示列表摘要,再拉 version.mappingJson 组装样例回填一览。
|
||||
* 副作用:HTTP 读版本详情;不写库、不调 LLM。
|
||||
*/
|
||||
function applyDefaultExclusive(targetId: string, bizType: TemplateRow["bizType"], isDefault: boolean) {
|
||||
if (!isDefault) return;
|
||||
for (const row of rows.value) {
|
||||
if (row.bizType === bizType && row.id !== targetId) row.isDefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openPreview(row: TemplateRow) {
|
||||
async function openPreview(row: TemplateRow) {
|
||||
previewRow.value = row;
|
||||
previewOpen.value = true;
|
||||
previewVm.value = {
|
||||
...emptyPreviewViewModel(),
|
||||
loading: true,
|
||||
mappedCount: row.mappingPointCount || 0,
|
||||
};
|
||||
if (!row.versionId) {
|
||||
previewVm.value = {
|
||||
...emptyPreviewViewModel(),
|
||||
loading: false,
|
||||
error: "无可用版本,请先上传模板文件",
|
||||
mappedCount: row.mappingPointCount || 0,
|
||||
};
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const ver: any = await getTemplateVersion(row.versionId);
|
||||
const mappingJson = ver?.mappingJson ?? ver?.result?.mappingJson;
|
||||
previewVm.value = buildPreviewViewModel(mappingJson, row.mappingPointCount || 0);
|
||||
} catch (e: any) {
|
||||
previewVm.value = {
|
||||
...emptyPreviewViewModel(),
|
||||
loading: false,
|
||||
error: e?.message || "加载映射失败,仍可下载预览文件",
|
||||
mappedCount: row.mappingPointCount || 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function closeForm() {
|
||||
@@ -184,11 +247,26 @@ function closeForm() {
|
||||
|
||||
function closePreview() {
|
||||
previewOpen.value = false;
|
||||
previewVm.value = emptyPreviewViewModel();
|
||||
}
|
||||
|
||||
function toggleStatus(row: TemplateRow) {
|
||||
row.status = row.status === "enabled" ? "disabled" : "enabled";
|
||||
message.success(row.status === "enabled" ? "已启用" : "已禁用");
|
||||
async function toggleStatus(row: TemplateRow) {
|
||||
try {
|
||||
if (row.status === "enabled") {
|
||||
await disableTemplate(row.id);
|
||||
message.success("已停用");
|
||||
} else {
|
||||
if (!row.versionId) {
|
||||
message.warning("无可用版本,请先上传文件");
|
||||
return;
|
||||
}
|
||||
await enableTemplate(row.id, row.versionId);
|
||||
message.success("已启用");
|
||||
}
|
||||
await loadList();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || "启停失败(自动模板须完成必填映射)");
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number) {
|
||||
@@ -203,14 +281,14 @@ function applySelectedFile(file: File) {
|
||||
form.format = isPdf ? "pdf" : "excel";
|
||||
form.fileName = file.name;
|
||||
form.fileMeta = `${isPdf ? "PDF" : "Excel"} · ${formatFileSize(file.size)}`;
|
||||
pendingFile.value = file;
|
||||
if (!form.name.trim()) {
|
||||
form.name = file.name.replace(/\.(xlsx|xls|pdf)$/i, "");
|
||||
}
|
||||
const total = file.name.includes("拼柜") ? 12 : isPdf ? 9 : 14;
|
||||
parseState.visible = true;
|
||||
parseState.ok = true;
|
||||
parseState.title = "解析成功";
|
||||
parseState.desc = `识别 ${total} 个占位符,${total - 2} 个已自动映射至工单字段`;
|
||||
parseState.title = "待上传解析";
|
||||
parseState.desc = "点击保存后由主账解析黄区并自动建议映射点";
|
||||
}
|
||||
|
||||
function onFileChange(e: Event) {
|
||||
@@ -227,71 +305,76 @@ function onDrop(e: DragEvent) {
|
||||
}
|
||||
|
||||
function triggerFilePick() {
|
||||
if (!form.fileName) fileInputRef.value?.click();
|
||||
fileInputRef.value?.click();
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
if (!form.fileName) {
|
||||
message.warning("请先上传模板文件");
|
||||
return;
|
||||
}
|
||||
if (!parseState.ok || !parseState.visible) {
|
||||
message.warning("模板解析失败,请更换文件后重试");
|
||||
return;
|
||||
}
|
||||
async function submitForm() {
|
||||
const keywords = normalizeKeywordItems(form.keywordItems);
|
||||
if (!form.name.trim()) {
|
||||
message.warning("请填写模板名称");
|
||||
return;
|
||||
}
|
||||
const keywords = normalizeKeywordItems(form.keywordItems);
|
||||
if (!keywords.length) {
|
||||
message.warning("请至少填写一个关键词");
|
||||
return;
|
||||
}
|
||||
const placeholders = form.fileName.includes("拼柜") ? 12 : form.format === "pdf" ? 9 : 14;
|
||||
const category = editing.value?.category || defaultCategoryForBiz(form.bizType);
|
||||
if (editing.value) {
|
||||
Object.assign(editing.value, {
|
||||
name: form.name.trim(),
|
||||
bizType: form.bizType,
|
||||
category,
|
||||
keywords,
|
||||
isDefault: form.isDefault,
|
||||
fileLabel: form.name.trim(),
|
||||
fileName: form.fileName,
|
||||
format: form.format,
|
||||
parseStatus: "已解析" as const,
|
||||
placeholderCount: placeholders,
|
||||
});
|
||||
applyDefaultExclusive(editing.value.id, form.bizType, form.isDefault);
|
||||
message.success("模板已更新");
|
||||
} else {
|
||||
const id = `t${Date.now()}`;
|
||||
rows.value.unshift({
|
||||
id,
|
||||
name: form.name.trim(),
|
||||
bizType: form.bizType,
|
||||
category,
|
||||
keywords,
|
||||
isDefault: form.isDefault,
|
||||
fileLabel: form.name.trim(),
|
||||
fileName: form.fileName,
|
||||
format: form.format,
|
||||
parseStatus: "已解析",
|
||||
placeholderCount: placeholders,
|
||||
status: "enabled",
|
||||
useCount: 0,
|
||||
createdAt: new Date().toISOString().slice(0, 10),
|
||||
operator: "当前用户",
|
||||
});
|
||||
applyDefaultExclusive(id, form.bizType, form.isDefault);
|
||||
message.success("模板已发布");
|
||||
// 分类/模式不对运营展示:分类按业务默认,模式新建默认 auto,编辑保留原值
|
||||
const category = defaultCategoryForBiz(form.bizType);
|
||||
const mode = editing.value?.mode || "auto";
|
||||
|
||||
try {
|
||||
if (pendingFile.value) {
|
||||
const fd = new FormData();
|
||||
if (editing.value?.id) fd.append("templateId", editing.value.id);
|
||||
fd.append("name", form.name.trim());
|
||||
fd.append("bizType", form.bizType);
|
||||
fd.append("category", category);
|
||||
fd.append("keywordsJson", JSON.stringify(keywords));
|
||||
fd.append("mode", mode);
|
||||
fd.append("isDefault", String(form.isDefault));
|
||||
fd.append("file", pendingFile.value);
|
||||
const row: any = await uploadTemplate(fd);
|
||||
message.success(row?.parseMessage || "已上传并解析");
|
||||
} else if (editing.value) {
|
||||
await editTemplateMeta({
|
||||
id: editing.value.id,
|
||||
name: form.name.trim(),
|
||||
bizType: form.bizType,
|
||||
category,
|
||||
keywordsJson: JSON.stringify(keywords),
|
||||
mode,
|
||||
isDefault: form.isDefault ? 1 : 0,
|
||||
});
|
||||
message.success("模板信息已保存");
|
||||
} else {
|
||||
message.warning("请先选择模板文件");
|
||||
return;
|
||||
}
|
||||
formOpen.value = false;
|
||||
await loadList();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || "保存失败");
|
||||
}
|
||||
formOpen.value = false;
|
||||
}
|
||||
|
||||
function downloadDemo() {
|
||||
message.success("已开始下载");
|
||||
async function downloadPreviewOrOriginal() {
|
||||
const row = previewRow.value;
|
||||
if (!row?.versionId) {
|
||||
message.warning("无版本可下载");
|
||||
return;
|
||||
}
|
||||
const kind = row.format === "excel" ? "previewSample" : "download";
|
||||
try {
|
||||
message.loading({ content: "正在生成预览文件…", key: "tpl-dl", duration: 0 });
|
||||
const hint =
|
||||
kind === "previewSample"
|
||||
? (row.fileName || "preview").replace(/\.xlsx$/i, "-preview.xlsx")
|
||||
: row.fileName || "template.bin";
|
||||
await downloadTemplateFile(kind, row.versionId, hint);
|
||||
message.success({ content: "已开始下载", key: "tpl-dl" });
|
||||
} catch (e: any) {
|
||||
message.error({ content: e?.message || "下载预览文件失败", key: "tpl-dl" });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -307,19 +390,19 @@ function downloadDemo() {
|
||||
<div class="toolbar">
|
||||
<div class="search">
|
||||
<ProtoIcon class="search-icon" name="search" :size="16" />
|
||||
<input v-model="keyword" placeholder="搜索模板名称…" />
|
||||
<input v-model="keyword" placeholder="搜索模板名称…" @keyup.enter="loadList" />
|
||||
</div>
|
||||
<select v-model="bizFilter">
|
||||
<select v-model="bizFilter" @change="loadList">
|
||||
<option value="">全部业务</option>
|
||||
<option value="空运">空运</option>
|
||||
<option value="海运">海运</option>
|
||||
<option value="陆运">陆运</option>
|
||||
</select>
|
||||
<select v-model="categoryFilter">
|
||||
<select v-model="categoryFilter" @change="loadList">
|
||||
<option value="">全部分类</option>
|
||||
<option v-for="c in ALL_CATEGORIES" :key="c" :value="c">{{ c }}</option>
|
||||
</select>
|
||||
<select v-model="statusFilter">
|
||||
<select v-model="statusFilter" @change="loadList">
|
||||
<option value="">全部状态</option>
|
||||
<option value="enabled">启用</option>
|
||||
<option value="disabled">禁用</option>
|
||||
@@ -553,7 +636,9 @@ function downloadDemo() {
|
||||
<div class="qt-preview-modal-body">
|
||||
<div class="qt-preview-tags">
|
||||
<span class="tag" :class="bizTag(previewRow.bizType)">{{ previewRow.fileName }}</span>
|
||||
<span class="tag tag-green">已解析 · {{ previewRow.placeholderCount }} 个占位符</span>
|
||||
<span class="tag tag-green"
|
||||
>已解析 · {{ previewVm.mappedCount || previewRow.mappingPointCount }} 个映射点</span
|
||||
>
|
||||
</div>
|
||||
<div class="qt-paper">
|
||||
<div class="qt-paper-head">
|
||||
@@ -561,63 +646,61 @@ function downloadDemo() {
|
||||
<div class="qt-paper-title">
|
||||
{{ previewRow.name.replace(/模板$/, "") || previewRow.name }}
|
||||
</div>
|
||||
<div class="qt-paper-cap">模板版式 · 由上传 Excel 决定</div>
|
||||
<div class="qt-paper-cap">模板款式 · 由上传 Excel 决定(黄区固定)</div>
|
||||
</div>
|
||||
<div class="qt-paper-meta">
|
||||
<div>QT-202607020001</div>
|
||||
<div>2026-07-02</div>
|
||||
<div v-if="previewVm.quoteNo">{{ previewVm.quoteNo }}</div>
|
||||
<div v-if="previewVm.quoteDate">{{ previewVm.quoteDate }}</div>
|
||||
<div v-else>版本 {{ previewRow.currentVersion || "—" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="qt-paper-grid">
|
||||
<div>
|
||||
<span class="qt-paper-label">客户</span>
|
||||
<span class="qt-paper-value">ABC Trading Co.</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="qt-paper-label">箱型</span>
|
||||
<span class="qt-paper-value">40HQ × 2</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="qt-paper-label">起运港 → 目的港</span>
|
||||
<span class="qt-paper-value">CNSHA → USLAX</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="qt-paper-label">销售</span>
|
||||
<span class="qt-paper-value">张敏</span>
|
||||
</div>
|
||||
<div v-if="previewVm.loading" class="qt-paper-loading">正在加载映射回填…</div>
|
||||
<div v-else-if="previewVm.error" class="qt-paper-loading qt-paper-loading--err">
|
||||
{{ previewVm.error }}
|
||||
</div>
|
||||
<div class="qt-paper-total">
|
||||
<div class="qt-paper-total-label">报价合计</div>
|
||||
<div class="qt-paper-amt">USD 3,680.00</div>
|
||||
<div class="qt-paper-total-hint">含海运费 + 本地费 · 有效期至 2026-07-05</div>
|
||||
</div>
|
||||
<table class="qt-paper-fee">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>费用项</th>
|
||||
<th style="text-align: right">金额</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>O/F 海运费</td>
|
||||
<td style="text-align: right">2,900</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>THC + DOC + SEAL</td>
|
||||
<td style="text-align: right">780</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>合计</td>
|
||||
<td style="text-align: right">3,680</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<template v-else>
|
||||
<div class="qt-paper-grid">
|
||||
<div v-for="f in previewVm.fields" :key="f.code">
|
||||
<span class="qt-paper-label">{{ f.label }}</span>
|
||||
<span class="qt-paper-value">{{ f.value }}</span>
|
||||
</div>
|
||||
<div v-if="!previewVm.fields.length">
|
||||
<span class="qt-paper-label">映射</span>
|
||||
<span class="qt-paper-value">暂无已映射字段,请先确认映射或下载 Excel 查看</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="previewVm.totalAmount" class="qt-paper-total">
|
||||
<div class="qt-paper-total-label">报价合计(样例)</div>
|
||||
<div class="qt-paper-amt">{{ previewTotalText }}</div>
|
||||
<div class="qt-paper-total-hint">
|
||||
按映射回填合同样例事实
|
||||
<template v-if="previewVm.validUntil"> · 有效期至 {{ previewVm.validUntil }}</template>
|
||||
</div>
|
||||
</div>
|
||||
<table v-if="previewVm.fees.length" class="qt-paper-fee">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>费用项</th>
|
||||
<th style="text-align: right">金额</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(fee, idx) in previewVm.fees" :key="idx">
|
||||
<td>{{ fee.name }}</td>
|
||||
<td style="text-align: right">{{ fee.amount }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>合计</td>
|
||||
<td style="text-align: right">{{ previewVm.totalAmount }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</template>
|
||||
<p class="qt-paper-note">
|
||||
* 版式、Logo、签章与业务上传模板一致;上述为预览回填数据。
|
||||
* 版式、Logo、签章与业务上传模板一致;上述为预览回填数据,金额只来自冻结报价事实,模型不发明价格。
|
||||
</p>
|
||||
</div>
|
||||
<p class="qt-preview-foot-hint">{{ previewFootHint }}</p>
|
||||
@@ -625,8 +708,8 @@ function downloadDemo() {
|
||||
|
||||
<div class="qt-preview-modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closePreview">关闭</button>
|
||||
<button type="button" class="btn btn-primary" @click="downloadDemo">
|
||||
<ProtoIcon name="download" :size="16" />下载文件
|
||||
<button type="button" class="btn btn-primary" @click="downloadPreviewOrOriginal">
|
||||
<ProtoIcon name="download" :size="16" />下载预览文件
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -675,6 +758,16 @@ function downloadDemo() {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.qt-paper-loading {
|
||||
padding: 16px 0;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
}
|
||||
.qt-paper-loading--err {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.qt-create-row--keywords {
|
||||
/* 与名称/业务同用 center,单行时标签与输入框垂直对齐 */
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user