完善空运锁舱与海运报价单单选回填,并增强模板预览与协同字段盖写。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -70,6 +70,8 @@ const previewState = reactive({
|
||||
isImage: true,
|
||||
mappedCount: 0,
|
||||
fileName: "",
|
||||
/** 浏览器侧已裁过白边,防止 @load 重复裁切 */
|
||||
trimmed: false,
|
||||
});
|
||||
|
||||
/** 列表「解析状态」点击弹窗:已解析 / 未解析 */
|
||||
@@ -125,16 +127,103 @@ const formSub = computed(() =>
|
||||
: "上传 Excel 后 AI 异步解析映射;失败原因会显示在列表",
|
||||
);
|
||||
const formOkText = computed(() => (editing.value ? "保存" : "上传并解析"));
|
||||
const previewFootHint = "按上传原表打印版式截图,比例 1:1 不变形;点击图片可放大。";
|
||||
const previewFootHint = "按原表打印版式等比例截图(不拉高/压扁);点击图片可放大。";
|
||||
const previewLightbox = ref(false);
|
||||
|
||||
const filtered = computed(() => rows.value);
|
||||
|
||||
function revokePreviewUrl() {
|
||||
if (previewState.objectUrl) {
|
||||
if (previewState.objectUrl && previewState.objectUrl.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(previewState.objectUrl);
|
||||
previewState.objectUrl = "";
|
||||
}
|
||||
previewState.objectUrl = "";
|
||||
previewState.trimmed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器侧再裁一层近白边:主账若仍带 A4 页底空白,预览也能贴紧内容。
|
||||
* 副作用:替换 objectUrl(旧 blob 释放);仅图片预览调用一次。
|
||||
*/
|
||||
function onSnapshotImgLoad(ev: Event) {
|
||||
if (previewState.trimmed || !previewState.isImage) return;
|
||||
const img = ev.target as HTMLImageElement | null;
|
||||
if (!img?.naturalWidth || !img.naturalHeight) return;
|
||||
try {
|
||||
const next = cropNearWhiteObjectUrl(img);
|
||||
if (!next) {
|
||||
previewState.trimmed = true;
|
||||
return;
|
||||
}
|
||||
const old = previewState.objectUrl;
|
||||
previewState.trimmed = true;
|
||||
previewState.objectUrl = next;
|
||||
if (old && old.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(old);
|
||||
}
|
||||
} catch {
|
||||
previewState.trimmed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Canvas 裁近白边 → data URL。几乎无内容可裁时返回 null。
|
||||
*/
|
||||
function cropNearWhiteObjectUrl(img: HTMLImageElement): string | null {
|
||||
const w = img.naturalWidth;
|
||||
const h = img.naturalHeight;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const data = ctx.getImageData(0, 0, w, h).data;
|
||||
const isWhite = (i: number) => data[i] >= 240 && data[i + 1] >= 240 && data[i + 2] >= 240;
|
||||
const rowMostlyWhite = (y: number) => {
|
||||
const allow = Math.max(2, Math.floor(w / 200));
|
||||
let dirty = 0;
|
||||
const row = y * w * 4;
|
||||
for (let x = 0; x < w; x++) {
|
||||
if (!isWhite(row + x * 4)) {
|
||||
dirty++;
|
||||
if (dirty > allow) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const colMostlyWhite = (x: number, y0: number, y1: number) => {
|
||||
const allow = Math.max(2, Math.floor((y1 - y0 + 1) / 200));
|
||||
let dirty = 0;
|
||||
for (let y = y0; y <= y1; y++) {
|
||||
if (!isWhite((y * w + x) * 4)) {
|
||||
dirty++;
|
||||
if (dirty > allow) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
let top = 0;
|
||||
let bottom = h - 1;
|
||||
let left = 0;
|
||||
let right = w - 1;
|
||||
while (top < h && rowMostlyWhite(top)) top++;
|
||||
while (bottom > top && rowMostlyWhite(bottom)) bottom--;
|
||||
while (left < w && colMostlyWhite(left, top, bottom)) left++;
|
||||
while (right > left && colMostlyWhite(right, top, bottom)) right--;
|
||||
if (top >= bottom || left >= right || bottom - top + 1 < h / 20) return null;
|
||||
const pad = 8;
|
||||
const x = Math.max(0, left - pad);
|
||||
const y = Math.max(0, top - pad);
|
||||
const cw = Math.min(w - 1, right + pad) - x + 1;
|
||||
const ch = Math.min(h - 1, bottom + pad) - y + 1;
|
||||
if (cw >= w && ch >= h) return null;
|
||||
const out = document.createElement("canvas");
|
||||
out.width = cw;
|
||||
out.height = ch;
|
||||
const octx = out.getContext("2d");
|
||||
if (!octx) return null;
|
||||
octx.drawImage(canvas, x, y, cw, ch, 0, 0, cw, ch);
|
||||
return out.toDataURL("image/png");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,6 +238,7 @@ async function openPreview(row: TemplateRow) {
|
||||
previewState.loading = true;
|
||||
previewState.error = "";
|
||||
previewState.isImage = true;
|
||||
previewState.trimmed = false;
|
||||
previewState.mappedCount = row.mappingPointCount || 0;
|
||||
previewState.fileName = row.fileName || "";
|
||||
if (!row.versionId) {
|
||||
@@ -919,6 +1009,7 @@ async function downloadPreviewOrOriginal() {
|
||||
:src="previewState.objectUrl"
|
||||
alt="原表截图预览"
|
||||
title="点击放大"
|
||||
@load="onSnapshotImgLoad"
|
||||
@click="openLightbox"
|
||||
/>
|
||||
<iframe
|
||||
@@ -1248,19 +1339,35 @@ async function downloadPreviewOrOriginal() {
|
||||
.qt-paper-loading--err {
|
||||
color: #b42318;
|
||||
}
|
||||
/**
|
||||
* 原表截图:等比例缩放;容器贴合图片,不人为撑出大块空白。
|
||||
* 弹窗 Teleport 到 body,关键规则在 admin-fidelity 也有 !important 兜底。
|
||||
*/
|
||||
.qt-snapshot-wrap {
|
||||
max-height: min(70vh, 720px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
max-height: min(72vh, 760px);
|
||||
overflow: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #f1f5f9;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
/* 不设 min-height,避免图矮时底下留灰白空带 */
|
||||
min-height: 0;
|
||||
}
|
||||
.qt-snapshot-img {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: none;
|
||||
display: block;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
max-height: min(70vh, 720px) !important;
|
||||
object-fit: contain !important;
|
||||
object-position: top center;
|
||||
flex: 0 0 auto;
|
||||
align-self: flex-start;
|
||||
vertical-align: top;
|
||||
background: #fff;
|
||||
cursor: zoom-in;
|
||||
|
||||
Reference in New Issue
Block a user