初始化询价智能体新仓:同步本地工程并排除密钥与 .env。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,763 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 报价模板页:对齐原型 admin-quote-templates.html。
|
||||
* - 上传按钮只在顶栏一处(禁止双按钮)
|
||||
* - 弹窗用原生 overlay(不依赖 a-modal,避免遮罩残留导致整页点不了)
|
||||
* - 操作:预览 / 编辑 / 启用|禁用
|
||||
* - 本阶段仅 mock,暂不对接主账上传/启用接口(页面与侧栏必须保留)
|
||||
*/
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import {
|
||||
ALL_CATEGORIES,
|
||||
CATEGORY_BY_BIZ,
|
||||
mockTemplates,
|
||||
type TemplateRow,
|
||||
} from "/@/views/inquiry/mock/templates";
|
||||
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||||
|
||||
/** 清掉可能残留的 Ant Modal 遮罩(会导致整页点不了) */
|
||||
function clearOrphanAntMasks() {
|
||||
document.querySelectorAll(".ant-modal-mask, .ant-modal-wrap").forEach((el) => {
|
||||
const node = el as HTMLElement;
|
||||
if (node.style.display === "none" || node.getAttribute("aria-hidden") === "true") {
|
||||
node.remove();
|
||||
}
|
||||
});
|
||||
document.body.style.removeProperty("overflow");
|
||||
document.body.classList.remove("ant-scrolling-effect");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
clearOrphanAntMasks();
|
||||
});
|
||||
|
||||
const rows = ref<TemplateRow[]>([...mockTemplates]);
|
||||
const keyword = ref("");
|
||||
const bizFilter = ref("");
|
||||
const categoryFilter = ref("");
|
||||
const statusFilter = ref("");
|
||||
|
||||
const formOpen = ref(false);
|
||||
const editing = ref<TemplateRow | null>(null);
|
||||
const previewOpen = ref(false);
|
||||
const previewRow = ref<TemplateRow | null>(null);
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const form = reactive({
|
||||
name: "",
|
||||
bizType: "海运" as TemplateRow["bizType"],
|
||||
/** 多行关键词;每行可含逗号,加减行数(对齐旧仓 keywordItems) */
|
||||
keywordItems: [""] as string[],
|
||||
isDefault: false,
|
||||
fileName: "",
|
||||
fileMeta: "",
|
||||
format: "excel" as "excel" | "pdf",
|
||||
});
|
||||
|
||||
const parseState = reactive({
|
||||
visible: false,
|
||||
ok: true,
|
||||
title: "",
|
||||
desc: "",
|
||||
});
|
||||
|
||||
const formTitle = computed(() => (editing.value ? "编辑报价模板" : "上传报价模板"));
|
||||
const formSub = computed(() =>
|
||||
editing.value ? "修改基本信息或更换模板文件后重新解析" : "上传 Excel / PDF,系统自动解析占位符",
|
||||
);
|
||||
const formOkText = computed(() => (editing.value ? "保存修改" : "发布模板"));
|
||||
const previewFootHint =
|
||||
"用户端生成时将复制模板文件并替换 {{占位符}},不改变原有 Excel / PDF 样式";
|
||||
|
||||
/** 表单不再手选分类:按业务取默认分类写入数据(列表筛选仍可用) */
|
||||
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;
|
||||
}),
|
||||
);
|
||||
|
||||
function bizTag(biz: string) {
|
||||
if (biz === "海运") return "tag-teal";
|
||||
if (biz === "陆运") return "tag-amber";
|
||||
return "tag-blue";
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(form, {
|
||||
name: "",
|
||||
bizType: "海运" as TemplateRow["bizType"],
|
||||
keywordItems: [""],
|
||||
isDefault: false,
|
||||
fileName: "",
|
||||
fileMeta: "",
|
||||
format: "excel" as const,
|
||||
});
|
||||
parseState.visible = false;
|
||||
parseState.ok = true;
|
||||
parseState.title = "";
|
||||
parseState.desc = "";
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
resetForm();
|
||||
formOpen.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row: TemplateRow) {
|
||||
editing.value = row;
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
bizType: row.bizType,
|
||||
keywordItems: row.keywords.length ? [...row.keywords] : [""],
|
||||
isDefault: Boolean(row.isDefault),
|
||||
fileName: row.fileName,
|
||||
fileMeta: `${row.format === "pdf" ? "PDF" : "Excel"} · 已上传`,
|
||||
format: row.format,
|
||||
});
|
||||
parseState.visible = true;
|
||||
parseState.ok = row.parseStatus === "已解析";
|
||||
parseState.title = parseState.ok ? "解析成功" : "解析失败";
|
||||
parseState.desc = parseState.ok
|
||||
? `识别 ${row.placeholderCount} 个占位符,已映射至工单字段`
|
||||
: "请更换模板文件后重试";
|
||||
formOpen.value = true;
|
||||
}
|
||||
|
||||
/** 去空去重,保留框内逗号;至少一条才算有效 */
|
||||
function normalizeKeywordItems(items: string[]): string[] {
|
||||
const unique: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const raw of items) {
|
||||
const kw = String(raw ?? "").trim();
|
||||
if (!kw || seen.has(kw)) continue;
|
||||
seen.add(kw);
|
||||
unique.push(kw);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同业务只保留一个默认模板:勾选本条时清掉同业务其它默认。
|
||||
* 演示环境本地互斥;接主账后由接口保证。
|
||||
*/
|
||||
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) {
|
||||
previewRow.value = row;
|
||||
previewOpen.value = true;
|
||||
}
|
||||
|
||||
function closeForm() {
|
||||
formOpen.value = false;
|
||||
}
|
||||
|
||||
function closePreview() {
|
||||
previewOpen.value = false;
|
||||
}
|
||||
|
||||
function toggleStatus(row: TemplateRow) {
|
||||
row.status = row.status === "enabled" ? "disabled" : "enabled";
|
||||
message.success(row.status === "enabled" ? "已启用" : "已禁用");
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number) {
|
||||
if (!bytes) return "—";
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function applySelectedFile(file: File) {
|
||||
const isPdf = /\.pdf$/i.test(file.name);
|
||||
form.format = isPdf ? "pdf" : "excel";
|
||||
form.fileName = file.name;
|
||||
form.fileMeta = `${isPdf ? "PDF" : "Excel"} · ${formatFileSize(file.size)}`;
|
||||
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} 个已自动映射至工单字段`;
|
||||
}
|
||||
|
||||
function onFileChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (file) applySelectedFile(file);
|
||||
input.value = "";
|
||||
}
|
||||
|
||||
function onDrop(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer?.files?.[0];
|
||||
if (file) applySelectedFile(file);
|
||||
}
|
||||
|
||||
function triggerFilePick() {
|
||||
if (!form.fileName) fileInputRef.value?.click();
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
if (!form.fileName) {
|
||||
message.warning("请先上传模板文件");
|
||||
return;
|
||||
}
|
||||
if (!parseState.ok || !parseState.visible) {
|
||||
message.warning("模板解析失败,请更换文件后重试");
|
||||
return;
|
||||
}
|
||||
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("模板已发布");
|
||||
}
|
||||
formOpen.value = false;
|
||||
}
|
||||
|
||||
function downloadDemo() {
|
||||
message.success("已开始下载");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 唯一上传入口:顶栏 -->
|
||||
<Teleport defer to="#page-topbar-actions">
|
||||
<button type="button" class="btn btn-primary" @click.stop.prevent="openCreate">
|
||||
<ProtoIcon name="upload" :size="16" />上传模板
|
||||
</button>
|
||||
</Teleport>
|
||||
|
||||
<div class="section-card qt-template-page">
|
||||
<div class="toolbar">
|
||||
<div class="search">
|
||||
<ProtoIcon class="search-icon" name="search" :size="16" />
|
||||
<input v-model="keyword" placeholder="搜索模板名称…" />
|
||||
</div>
|
||||
<select v-model="bizFilter">
|
||||
<option value="">全部业务</option>
|
||||
<option value="空运">空运</option>
|
||||
<option value="海运">海运</option>
|
||||
<option value="陆运">陆运</option>
|
||||
</select>
|
||||
<select v-model="categoryFilter">
|
||||
<option value="">全部分类</option>
|
||||
<option v-for="c in ALL_CATEGORIES" :key="c" :value="c">{{ c }}</option>
|
||||
</select>
|
||||
<select v-model="statusFilter">
|
||||
<option value="">全部状态</option>
|
||||
<option value="enabled">启用</option>
|
||||
<option value="disabled">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap qt-template-table-wrap">
|
||||
<table class="data-table qt-template-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模板名称</th>
|
||||
<th>业务</th>
|
||||
<th>分类</th>
|
||||
<th>模板文件</th>
|
||||
<th>解析状态</th>
|
||||
<th>状态</th>
|
||||
<th>总使用次数</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作人</th>
|
||||
<th class="col-actions">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in filtered" :key="row.id">
|
||||
<td class="qt-col-name">
|
||||
<span>{{ row.name }}</span>
|
||||
<span v-if="row.isDefault" class="tag tag-blue qt-default-tag">默认</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="tag" :class="bizTag(row.bizType)">{{ row.bizType }}</span>
|
||||
</td>
|
||||
<td class="qt-col-muted">{{ row.category }}</td>
|
||||
<td class="qt-col-file">{{ row.fileLabel }}</td>
|
||||
<td class="qt-col-muted">{{ row.parseStatus }}</td>
|
||||
<td class="qt-col-muted">{{ row.status === "enabled" ? "启用" : "禁用" }}</td>
|
||||
<td class="qt-col-count">{{ row.useCount }} 次</td>
|
||||
<td class="qt-col-time">{{ row.createdAt }}</td>
|
||||
<td class="qt-col-muted">{{ row.operator }}</td>
|
||||
<td class="col-actions">
|
||||
<div class="qt-row-actions">
|
||||
<button type="button" class="link-action link-muted" @click.stop="openPreview(row)">
|
||||
预览
|
||||
</button>
|
||||
<button type="button" class="link-action" @click.stop="openEdit(row)">编辑</button>
|
||||
<button
|
||||
type="button"
|
||||
class="link-action"
|
||||
:class="row.status === 'enabled' ? 'link-danger' : ''"
|
||||
@click.stop="toggleStatus(row)"
|
||||
>
|
||||
{{ row.status === "enabled" ? "禁用" : "启用" }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<span>
|
||||
共 <b style="color: #0f172a">{{ filtered.length }}</b> 个模板
|
||||
</span>
|
||||
<div class="pages">
|
||||
<button class="is-active" type="button">1</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传 / 编辑:原生遮罩,挂到 body -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="formOpen"
|
||||
class="modal-overlay"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@click.self="closeForm"
|
||||
>
|
||||
<div class="modal-dialog modal-lg" @click.stop>
|
||||
<div class="qt-modal-head">
|
||||
<div class="qt-modal-title-row">
|
||||
<div class="qt-modal-icon qt-modal-icon--blue">
|
||||
<ProtoIcon :name="editing ? 'pencil' : 'upload'" :size="16" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="qt-modal-title">{{ formTitle }}</div>
|
||||
<div class="qt-modal-sub">{{ formSub }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost" aria-label="关闭" @click="closeForm">
|
||||
<ProtoIcon name="x" :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="qt-create-modal-body">
|
||||
<div class="qt-create-form">
|
||||
<div class="qt-create-row">
|
||||
<label>模板名称</label>
|
||||
<input v-model="form.name" class="field" type="text" placeholder="上传后自动填充,可修改" />
|
||||
</div>
|
||||
<div class="qt-create-row">
|
||||
<label>业务</label>
|
||||
<select v-model="form.bizType" class="field">
|
||||
<option value="海运">海运</option>
|
||||
<option value="空运">空运</option>
|
||||
<option value="陆运">陆运</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="qt-create-row qt-create-row--keywords">
|
||||
<label>关键词</label>
|
||||
<div class="qt-keyword-list">
|
||||
<div v-for="(_, index) in form.keywordItems" :key="index" class="qt-keyword-row">
|
||||
<input
|
||||
v-model="form.keywordItems[index]"
|
||||
class="field"
|
||||
type="text"
|
||||
placeholder="请输入关键词,可包含逗号"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="qt-keyword-btn"
|
||||
title="添加"
|
||||
:disabled="form.keywordItems.length >= 20"
|
||||
@click="addKeyword"
|
||||
>
|
||||
<ProtoIcon name="plus" :size="14" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="qt-keyword-btn is-danger"
|
||||
title="删除"
|
||||
:disabled="form.keywordItems.length <= 1"
|
||||
@click="removeKeyword(index)"
|
||||
>
|
||||
<ProtoIcon name="minus" :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="parseState.visible" class="qt-parse-banner" :class="{ 'is-error': !parseState.ok }">
|
||||
<div class="qt-parse-banner-icon" :class="parseState.ok ? 'is-ok' : 'is-err'">
|
||||
<ProtoIcon :name="parseState.ok ? 'circleCheck' : 'circleX'" :size="16" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="qt-parse-banner-title">{{ parseState.title }}</div>
|
||||
<div class="qt-parse-banner-desc">{{ parseState.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="qt-upload-zone">
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
class="qt-file-input"
|
||||
accept=".xlsx,.xls,.pdf"
|
||||
@change="onFileChange"
|
||||
/>
|
||||
<div
|
||||
class="qt-upload-drop"
|
||||
:class="{ 'has-file': !!form.fileName }"
|
||||
@dragover.prevent
|
||||
@drop="onDrop"
|
||||
@click="triggerFilePick"
|
||||
>
|
||||
<template v-if="!form.fileName">
|
||||
<div class="qt-upload-empty">
|
||||
<div class="qt-upload-icon">
|
||||
<ProtoIcon name="uploadCloud" :size="20" />
|
||||
</div>
|
||||
<div class="qt-upload-title">点击或拖拽上传模板文件</div>
|
||||
<div class="qt-upload-hint">.xlsx · .xls · .pdf,不超过 20 MB</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="qt-upload-file-card" @click.stop>
|
||||
<div class="qt-file-type-icon" :class="form.format === 'pdf' ? 'is-pdf' : 'is-excel'">
|
||||
<ProtoIcon :name="form.format === 'pdf' ? 'fileText' : 'fileSpreadsheet'" :size="16" />
|
||||
</div>
|
||||
<div class="qt-file-meta">
|
||||
<div class="qt-file-name">{{ form.fileName }}</div>
|
||||
<div class="qt-file-sub">{{ form.fileMeta }}</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost qt-replace-btn" @click="fileInputRef?.click()">
|
||||
更换
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="qt-default-flag">
|
||||
<input v-model="form.isDefault" type="checkbox" />
|
||||
<span>默认模板</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="qt-create-modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closeForm">取消</button>
|
||||
<button type="button" class="btn btn-primary" @click="submitForm">
|
||||
<ProtoIcon name="check" :size="16" />{{ formOkText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 预览 -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="previewOpen && previewRow"
|
||||
class="modal-overlay"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@click.self="closePreview"
|
||||
>
|
||||
<div class="modal-dialog qt-preview-dialog" @click.stop>
|
||||
<div class="qt-modal-head">
|
||||
<div class="qt-modal-title-row">
|
||||
<div class="qt-modal-icon qt-modal-icon--green">
|
||||
<ProtoIcon name="eye" :size="16" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="qt-modal-sub">基于上传模板渲染 · 用户端预览</div>
|
||||
<div class="qt-modal-title">{{ previewRow.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost" aria-label="关闭" @click="closePreview">
|
||||
<ProtoIcon name="x" :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div class="qt-paper">
|
||||
<div class="qt-paper-head">
|
||||
<div>
|
||||
<div class="qt-paper-title">
|
||||
{{ previewRow.name.replace(/模板$/, "") || previewRow.name }}
|
||||
</div>
|
||||
<div class="qt-paper-cap">模板版式 · 由上传 Excel 决定</div>
|
||||
</div>
|
||||
<div class="qt-paper-meta">
|
||||
<div>QT-202607020001</div>
|
||||
<div>2026-07-02</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>
|
||||
<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>
|
||||
<p class="qt-paper-note">
|
||||
* 版式、Logo、签章与业务上传模板一致;上述为预览回填数据。
|
||||
</p>
|
||||
</div>
|
||||
<p class="qt-preview-foot-hint">{{ previewFootHint }}</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.qt-col-name {
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
}
|
||||
.qt-col-name .qt-default-tag {
|
||||
margin-left: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.qt-col-muted {
|
||||
font-size: 11px;
|
||||
color: #334155;
|
||||
}
|
||||
.qt-col-file {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
}
|
||||
.qt-col-count {
|
||||
font-size: 11px;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.qt-col-time {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.qt-row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.qt-create-row--keywords {
|
||||
/* 与名称/业务同用 center,单行时标签与输入框垂直对齐 */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.qt-create-row--keywords > label {
|
||||
line-height: 34px;
|
||||
/* 多行关键词时仍贴齐首行输入框中线,避免整块垂直居中飘在中间 */
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.qt-keyword-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.qt-keyword-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.qt-keyword-row .field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 34px;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.qt-keyword-btn {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #d0d5dd;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
color: #475467;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.qt-keyword-btn:hover:not(:disabled) {
|
||||
border-color: #98a2b3;
|
||||
background: #f9fafb;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.qt-keyword-btn.is-danger:hover:not(:disabled) {
|
||||
border-color: #fda29b;
|
||||
background: #fef3f2;
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.qt-keyword-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.qt-default-flag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #344054;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.qt-default-flag input {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0;
|
||||
accent-color: #155eef;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user