1.01版本 #2
@ -68,14 +68,39 @@ export async function trackOrderGetOrderOperationsByIdGet(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取订单类型字典 GET /Track/Order/GetOrderTypeDict */
|
/**
|
||||||
|
* 获取订单类型字典。
|
||||||
|
* 轨迹后端无 /Track/Order/GetOrderTypeDict(404),统一走 Dictionary/GetList。
|
||||||
|
* 返回形态兼容旧调用方:`{ data: Record<string, string> }`。
|
||||||
|
*/
|
||||||
export async function trackOrderGetOrderTypeDictGet(options?: {
|
export async function trackOrderGetOrderTypeDictGet(options?: {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}) {
|
}) {
|
||||||
return request<Record<string, any>>(`trackApi/Track/Order/GetOrderTypeDict`, {
|
const list = await request<API.EnumCacheEntry[] | Record<string, any>>(
|
||||||
method: 'GET',
|
`trackApi/Dictionary/GetList`,
|
||||||
...(options || {}),
|
{
|
||||||
});
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
type: 'TrackOrderType',
|
||||||
|
},
|
||||||
|
...(options || {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
const dict: Record<string, string> = {};
|
||||||
|
for (const item of list) {
|
||||||
|
if (item?.key == null) continue;
|
||||||
|
dict[String(item.key)] = String(item.value ?? '');
|
||||||
|
}
|
||||||
|
return { data: dict };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list && typeof list === 'object' && (list as any).data != null) {
|
||||||
|
return list as { data: Record<string, any> };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: (list as Record<string, any>) || {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取分页的轨迹订单 POST /Track/Order/GetTrackOrderList */
|
/** 获取分页的轨迹订单 POST /Track/Order/GetTrackOrderList */
|
||||||
|
|||||||
@ -219,6 +219,8 @@
|
|||||||
|
|
||||||
/** 编辑写回源数据时跳过 data watch,避免打断输入 */
|
/** 编辑写回源数据时跳过 data watch,避免打断输入 */
|
||||||
let ignoreDataWatch = false;
|
let ignoreDataWatch = false;
|
||||||
|
/** loadData / 程序化选区期间跳过 selection 回写,避免清空业务页已选 */
|
||||||
|
let selectionLock = 0;
|
||||||
/** 表头初始化/销毁期间不向业务页 emit search,避免首屏重复拉接口 */
|
/** 表头初始化/销毁期间不向业务页 emit search,避免首屏重复拉接口 */
|
||||||
let headerSearchEmitReady = false;
|
let headerSearchEmitReady = false;
|
||||||
let headerSearchReadyTimer: ReturnType<typeof setTimeout> | null = null;
|
let headerSearchReadyTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
@ -620,6 +622,54 @@
|
|||||||
return props.data[rowIndex];
|
return props.data[rowIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publishSelection(rows: any[], indices: Set<number>) {
|
||||||
|
selectedRows.value = rows;
|
||||||
|
selectedRowIndicesSet.value = indices;
|
||||||
|
emit('update:selectedRows', rows);
|
||||||
|
emit('selection-change', rows);
|
||||||
|
applySelectedRowStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 复选框列为批量操作真相源:按 checked 汇总选中行 */
|
||||||
|
function syncSelectionFromCheckboxes() {
|
||||||
|
if (!props.showCheckboxColumn) return;
|
||||||
|
const visualCount =
|
||||||
|
displayRowIndices.value.length > 0 ? displayRowIndices.value.length : props.data.length;
|
||||||
|
const indices = new Set<number>();
|
||||||
|
const rows: any[] = [];
|
||||||
|
for (let visual = 0; visual < visualCount; visual++) {
|
||||||
|
const row = rowAt(visual);
|
||||||
|
if (row && (row as any).checked) {
|
||||||
|
indices.add(visual);
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
publishSelection(rows, indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldKeepSelectionOnOutsideClick(event: MouseEvent): boolean {
|
||||||
|
const target = event?.target as HTMLElement | null;
|
||||||
|
if (!target) return false;
|
||||||
|
// 点工具栏/分页/下拉/弹层时保留选中,避免「选中后点立即支付已清空」
|
||||||
|
return !!target.closest(
|
||||||
|
[
|
||||||
|
'.table-toolbar',
|
||||||
|
'.actions-right',
|
||||||
|
'.toolbar-right',
|
||||||
|
'.toolbar-left',
|
||||||
|
'.arco-btn',
|
||||||
|
'.arco-dropdown',
|
||||||
|
'.arco-dropdown-option',
|
||||||
|
'.arco-trigger-popup',
|
||||||
|
'.arco-pagination',
|
||||||
|
'.arco-modal',
|
||||||
|
'.arco-drawer',
|
||||||
|
'.arco-select-dropdown',
|
||||||
|
'.column-config-icon-btn',
|
||||||
|
].join(','),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function buildRowDataArray(row: any, visualIndex: number): any[] {
|
function buildRowDataArray(row: any, visualIndex: number): any[] {
|
||||||
const rowData: any[] = [];
|
const rowData: any[] = [];
|
||||||
// 固定:操作 → 序号 → 复选框 → 数据
|
// 固定:操作 → 序号 → 复选框 → 数据
|
||||||
@ -1343,6 +1393,7 @@
|
|||||||
if (!changes || source === 'loadData') return;
|
if (!changes || source === 'loadData') return;
|
||||||
const { checkboxColIndex, dataColStart } = getLayoutIndices();
|
const { checkboxColIndex, dataColStart } = getLayoutIndices();
|
||||||
ignoreDataWatch = true;
|
ignoreDataWatch = true;
|
||||||
|
let checkboxChanged = false;
|
||||||
try {
|
try {
|
||||||
changes.forEach(([row, col, oldValue, newValue]: any[]) => {
|
changes.forEach(([row, col, oldValue, newValue]: any[]) => {
|
||||||
const rowData = rowAt(row);
|
const rowData = rowAt(row);
|
||||||
@ -1350,6 +1401,7 @@
|
|||||||
|
|
||||||
if (col === checkboxColIndex) {
|
if (col === checkboxColIndex) {
|
||||||
(rowData as any).checked = newValue || false;
|
(rowData as any).checked = newValue || false;
|
||||||
|
checkboxChanged = true;
|
||||||
emit('checkbox-change', rowData, newValue || false, row);
|
emit('checkbox-change', rowData, newValue || false, row);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1375,6 +1427,9 @@
|
|||||||
oldValue,
|
oldValue,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
if (checkboxChanged) {
|
||||||
|
syncSelectionFromCheckboxes();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// 可编辑模式:值已写入源对象且 HOT 自身已更新,禁止 loadData,
|
// 可编辑模式:值已写入源对象且 HOT 自身已更新,禁止 loadData,
|
||||||
// 否则会打断连续编辑 / 关掉下拉。仅在 nextTick 后恢复 data watch。
|
// 否则会打断连续编辑 / 关掉下拉。仅在 nextTick 后恢复 data watch。
|
||||||
@ -1398,6 +1453,11 @@
|
|||||||
colHeaders: headerTexts,
|
colHeaders: headerTexts,
|
||||||
rowHeaders: false,
|
rowHeaders: false,
|
||||||
selectionMode: 'multiple',
|
selectionMode: 'multiple',
|
||||||
|
// 点工具栏/分页时保留选中,避免批量按钮拿到空选区
|
||||||
|
outsideClickDeselects(event: MouseEvent) {
|
||||||
|
if (props.showCheckboxColumn) return false;
|
||||||
|
return !shouldKeepSelectionOnOutsideClick(event);
|
||||||
|
},
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: tableHeight,
|
height: tableHeight,
|
||||||
licenseKey: 'non-commercial-and-evaluation',
|
licenseKey: 'non-commercial-and-evaluation',
|
||||||
@ -1780,31 +1840,30 @@
|
|||||||
return cellProperties;
|
return cellProperties;
|
||||||
},
|
},
|
||||||
afterSelectionEnd: function(r: number, c: number, r2: number, c2: number) {
|
afterSelectionEnd: function(r: number, c: number, r2: number, c2: number) {
|
||||||
if (!hotInstance) return;
|
if (!hotInstance || selectionLock) return;
|
||||||
|
|
||||||
const selected = hotInstance.getSelected() || [];
|
// 开启复选框时,批量选中以 checked 为准,避免单元格选区覆盖
|
||||||
const selectedRowIndices = new Set<number>();
|
if (props.showCheckboxColumn) {
|
||||||
|
applySelectedRowStyles();
|
||||||
|
} else {
|
||||||
|
const selected = hotInstance.getSelected() || [];
|
||||||
|
const selectedRowIndices = new Set<number>();
|
||||||
|
|
||||||
selected.forEach(([rowStart, colStart, rowEnd, colEnd]: number[]) => {
|
selected.forEach(([rowStart, _colStart, rowEnd, _colEnd]: number[]) => {
|
||||||
const startRow = Math.min(rowStart, rowEnd);
|
const startRow = Math.min(rowStart, rowEnd);
|
||||||
const endRow = Math.max(rowStart, rowEnd);
|
const endRow = Math.max(rowStart, rowEnd);
|
||||||
for (let i = startRow; i <= endRow; i++) {
|
for (let i = startRow; i <= endRow; i++) {
|
||||||
if (i >= 0 && i < props.data.length) {
|
if (i >= 0 && i < props.data.length) {
|
||||||
selectedRowIndices.add(i);
|
selectedRowIndices.add(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
selectedRowIndicesSet.value = selectedRowIndices;
|
publishSelection(
|
||||||
selectedRows.value = Array.from(selectedRowIndices)
|
Array.from(selectedRowIndices).map((index) => rowAt(index)).filter(Boolean),
|
||||||
.map(index => rowAt(index))
|
selectedRowIndices,
|
||||||
.filter(Boolean);
|
);
|
||||||
|
}
|
||||||
emit('update:selectedRows', selectedRows.value);
|
|
||||||
emit('selection-change', selectedRows.value);
|
|
||||||
|
|
||||||
// 应用选中行样式
|
|
||||||
applySelectedRowStyles();
|
|
||||||
|
|
||||||
// 可编辑模式:单击单元格即进入编辑(表单录入体验)
|
// 可编辑模式:单击单元格即进入编辑(表单录入体验)
|
||||||
if (
|
if (
|
||||||
@ -1865,7 +1924,7 @@
|
|||||||
afterOnCellMouseDown: function(event: any, coords: any, td: any) {
|
afterOnCellMouseDown: function(event: any, coords: any, td: any) {
|
||||||
const { actionColIndex, serialColIndex, checkboxColIndex, dataColStart } = getLayoutIndices();
|
const { actionColIndex, serialColIndex, checkboxColIndex, dataColStart } = getLayoutIndices();
|
||||||
|
|
||||||
// 复选框列
|
// 复选框列(自定义 DOM,不走内置 editor)
|
||||||
if (coords.col === checkboxColIndex) {
|
if (coords.col === checkboxColIndex) {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (target.type === 'checkbox') {
|
if (target.type === 'checkbox') {
|
||||||
@ -1873,8 +1932,16 @@
|
|||||||
if (rowIndex >= 0 && rowIndex < props.data.length) {
|
if (rowIndex >= 0 && rowIndex < props.data.length) {
|
||||||
const row = rowAt(rowIndex);
|
const row = rowAt(rowIndex);
|
||||||
const checked = target.checked;
|
const checked = target.checked;
|
||||||
(row as any).checked = checked;
|
ignoreDataWatch = true;
|
||||||
emit('checkbox-change', row, checked, rowIndex);
|
try {
|
||||||
|
(row as any).checked = checked;
|
||||||
|
syncSelectionFromCheckboxes();
|
||||||
|
emit('checkbox-change', row, checked, rowIndex);
|
||||||
|
} finally {
|
||||||
|
nextTick(() => {
|
||||||
|
ignoreDataWatch = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2290,8 +2357,14 @@
|
|||||||
}
|
}
|
||||||
if (!props.data.length) {
|
if (!props.data.length) {
|
||||||
displayRowIndices.value = [];
|
displayRowIndices.value = [];
|
||||||
hotInstance.loadData([]);
|
selectionLock += 1;
|
||||||
hotInstance.render();
|
try {
|
||||||
|
hotInstance.loadData([]);
|
||||||
|
hotInstance.render();
|
||||||
|
publishSelection([], new Set());
|
||||||
|
} finally {
|
||||||
|
selectionLock -= 1;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2310,9 +2383,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = syncDisplayRows();
|
const data = syncDisplayRows();
|
||||||
|
selectionLock += 1;
|
||||||
hotInstance.loadData(data);
|
try {
|
||||||
hotInstance.render();
|
hotInstance.loadData(data);
|
||||||
|
hotInstance.render();
|
||||||
|
// loadData 会清掉 HOT 单元格选区;复选框模式按 checked 回写业务选中
|
||||||
|
if (props.showCheckboxColumn) {
|
||||||
|
syncSelectionFromCheckboxes();
|
||||||
|
} else {
|
||||||
|
applySelectedRowStyles();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
selectionLock -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新尺寸以确保 Handsontable 内置滚动视口(.wtHolder)尺寸正确
|
// 刷新尺寸以确保 Handsontable 内置滚动视口(.wtHolder)尺寸正确
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -2642,10 +2725,26 @@
|
|||||||
refresh: updateData,
|
refresh: updateData,
|
||||||
getSelectedRows: () => selectedRows.value,
|
getSelectedRows: () => selectedRows.value,
|
||||||
clearSelection: () => {
|
clearSelection: () => {
|
||||||
if (hotInstance) {
|
selectionLock += 1;
|
||||||
hotInstance.deselectCell();
|
try {
|
||||||
selectedRows.value = [];
|
if (hotInstance) {
|
||||||
selectedRowIndicesSet.value = new Set();
|
hotInstance.deselectCell();
|
||||||
|
}
|
||||||
|
if (props.showCheckboxColumn) {
|
||||||
|
ignoreDataWatch = true;
|
||||||
|
try {
|
||||||
|
props.data.forEach((row) => {
|
||||||
|
if (row) (row as any).checked = false;
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
nextTick(() => {
|
||||||
|
ignoreDataWatch = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
publishSelection([], new Set());
|
||||||
|
} finally {
|
||||||
|
selectionLock -= 1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -149,17 +149,81 @@ async function buildPrintStreamKeyCandidates(rawUrl: string): Promise<string[]>
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripUtf8Bom(bytes: Uint8Array): Uint8Array {
|
/** 去掉 BOM / 前导空白,避免面单生成器偶发换行导致 magic 校验失败 */
|
||||||
|
function stripLeadingNoise(bytes: Uint8Array): Uint8Array {
|
||||||
|
let start = 0
|
||||||
if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
||||||
return bytes.slice(3)
|
start = 3
|
||||||
}
|
}
|
||||||
return bytes
|
while (start < bytes.length) {
|
||||||
|
const b = bytes[start]
|
||||||
|
if (b === 0x09 || b === 0x0a || b === 0x0d || b === 0x20) {
|
||||||
|
start += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return start === 0 ? bytes : bytes.slice(start)
|
||||||
}
|
}
|
||||||
|
|
||||||
function bytesStartWithPdfMagic(bytes: Uint8Array): boolean {
|
function bytesStartWithPdfMagic(bytes: Uint8Array): boolean {
|
||||||
const head = bytes.slice(0, 5)
|
// 标准 PDF 头为 "%PDF-1.x";此前误用 5 字节与 4 字符 "%PDF" 比较,合法面单也会被拒
|
||||||
if (head.length < 5) return false
|
if (bytes.length < PDF_MAGIC.length) return false
|
||||||
return String.fromCharCode(...head) === PDF_MAGIC
|
const head = String.fromCharCode(...bytes.slice(0, PDF_MAGIC.length))
|
||||||
|
return head === PDF_MAGIC
|
||||||
|
}
|
||||||
|
|
||||||
|
function sniffMimeFromBytes(bytes: Uint8Array): string | null {
|
||||||
|
if (bytesStartWithPdfMagic(bytes)) return 'application/pdf'
|
||||||
|
if (bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
|
||||||
|
return 'image/png'
|
||||||
|
}
|
||||||
|
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
|
||||||
|
return 'image/jpeg'
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
bytes.length >= 6 &&
|
||||||
|
bytes[0] === 0x47 &&
|
||||||
|
bytes[1] === 0x49 &&
|
||||||
|
bytes[2] === 0x46 &&
|
||||||
|
bytes[3] === 0x38
|
||||||
|
) {
|
||||||
|
return 'image/gif'
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
bytes.length >= 12 &&
|
||||||
|
bytes[0] === 0x52 &&
|
||||||
|
bytes[1] === 0x49 &&
|
||||||
|
bytes[2] === 0x46 &&
|
||||||
|
bytes[3] === 0x46 &&
|
||||||
|
bytes[8] === 0x57 &&
|
||||||
|
bytes[9] === 0x45 &&
|
||||||
|
bytes[10] === 0x42 &&
|
||||||
|
bytes[11] === 0x50
|
||||||
|
) {
|
||||||
|
return 'image/webp'
|
||||||
|
}
|
||||||
|
const sample = new TextDecoder('utf-8', { fatal: false }).decode(bytes.slice(0, 64)).trimStart()
|
||||||
|
if (/^<!DOCTYPE\s+html/i.test(sample) || /^<html[\s>]/i.test(sample)) {
|
||||||
|
return 'text/html; charset=utf-8'
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Base64 文本面单(常见于下游回传):JVBERi… → %PDF… */
|
||||||
|
function tryDecodeBase64Pdf(bytes: Uint8Array): Uint8Array | null {
|
||||||
|
const sample = new TextDecoder('utf-8', { fatal: false }).decode(bytes.slice(0, 32)).trim()
|
||||||
|
if (!sample.startsWith('JVBERi')) return null
|
||||||
|
try {
|
||||||
|
const text = new TextDecoder('utf-8', { fatal: false }).decode(bytes).trim()
|
||||||
|
const pure = text.replace(/^data:application\/pdf;base64,/i, '').replace(/\s+/g, '')
|
||||||
|
const bin = atob(pure)
|
||||||
|
const out = new Uint8Array(bin.length)
|
||||||
|
for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i)
|
||||||
|
return bytesStartWithPdfMagic(out) ? out : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function throwIfBytesLookLikeApiError(bytes: Uint8Array) {
|
async function throwIfBytesLookLikeApiError(bytes: Uint8Array) {
|
||||||
@ -182,20 +246,47 @@ async function throwIfBytesLookLikeApiError(bytes: Uint8Array) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 校验并规范化 PDF 字节(拒绝 JSON/HTML 错误体) */
|
type NormalizedPreviewBytes = { bytes: Uint8Array; mime: string }
|
||||||
export async function normalizePdfBytes(blob: Blob, rawUrl: string): Promise<Uint8Array> {
|
|
||||||
if (!isPrintPreviewPdf(rawUrl, blob)) {
|
/** 规范化预览字节:按内容嗅探 MIME;扩展名/响应头标成 PDF 时再严格校验 */
|
||||||
return new Uint8Array(await blob.arrayBuffer())
|
async function normalizePreviewBytes(blob: Blob, rawUrl: string): Promise<NormalizedPreviewBytes> {
|
||||||
|
let bytes = stripLeadingNoise(new Uint8Array(await blob.arrayBuffer()))
|
||||||
|
const decoded = tryDecodeBase64Pdf(bytes)
|
||||||
|
if (decoded) bytes = decoded
|
||||||
|
|
||||||
|
const sniffed = sniffMimeFromBytes(bytes)
|
||||||
|
if (sniffed) {
|
||||||
|
return { bytes, mime: sniffed }
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytes = stripUtf8Bom(new Uint8Array(await blob.arrayBuffer()))
|
await throwIfBytesLookLikeApiError(bytes)
|
||||||
if (!bytesStartWithPdfMagic(bytes)) {
|
|
||||||
await throwIfBytesLookLikeApiError(bytes)
|
if (isPrintPreviewPdf(rawUrl, blob)) {
|
||||||
throw new Error('面单文件不是有效的 PDF')
|
throw new Error('面单文件不是有效的 PDF')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fallback =
|
||||||
|
(blob.type && !/octet-stream|force-download/i.test(blob.type) && blob.type) ||
|
||||||
|
guessMime(rawUrl) ||
|
||||||
|
'application/octet-stream'
|
||||||
|
return { bytes, mime: fallback }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验并规范化 PDF 字节(拒绝 JSON/HTML 错误体;图片/HTML 面单原样返回) */
|
||||||
|
export async function normalizePdfBytes(blob: Blob, rawUrl: string): Promise<Uint8Array> {
|
||||||
|
const { bytes, mime } = await normalizePreviewBytes(blob, rawUrl)
|
||||||
|
if (mime.includes('pdf')) {
|
||||||
|
if (!bytesStartWithPdfMagic(bytes)) {
|
||||||
|
throw new Error('面单文件不是有效的 PDF')
|
||||||
|
}
|
||||||
|
}
|
||||||
return bytes
|
return bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function blobFromNormalized(normalized: NormalizedPreviewBytes): Blob {
|
||||||
|
return new Blob([normalized.bytes], { type: normalized.mime })
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchBlobViaSameOriginPath(path: string): Promise<Blob> {
|
async function fetchBlobViaSameOriginPath(path: string): Promise<Blob> {
|
||||||
const res = await axiosInstance.get(path, {
|
const res = await axiosInstance.get(path, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
@ -232,14 +323,8 @@ async function fetchBlobViaCosProxy(rawUrl: string): Promise<Blob> {
|
|||||||
throw new Error(`COS 代理下载失败 HTTP ${res.status}`)
|
throw new Error(`COS 代理下载失败 HTTP ${res.status}`)
|
||||||
}
|
}
|
||||||
const mime = String(res.headers.get('content-type') || guessMime(rawUrl))
|
const mime = String(res.headers.get('content-type') || guessMime(rawUrl))
|
||||||
const bytes = stripUtf8Bom(new Uint8Array(await res.arrayBuffer()))
|
const rawBlob = new Blob([await res.arrayBuffer()], { type: mime })
|
||||||
await throwIfBytesLookLikeApiError(bytes)
|
return blobFromNormalized(await normalizePreviewBytes(rawBlob, rawUrl))
|
||||||
if (/\.pdf(\?|#|$)/i.test(rawUrl) && !bytesStartWithPdfMagic(bytes)) {
|
|
||||||
throw new Error('面单文件不是有效的 PDF')
|
|
||||||
}
|
|
||||||
return new Blob([bytes], {
|
|
||||||
type: /\.pdf(\?|#|$)/i.test(rawUrl) ? 'application/pdf' : mime,
|
|
||||||
})
|
|
||||||
} finally {
|
} finally {
|
||||||
window.clearTimeout(timer)
|
window.clearTimeout(timer)
|
||||||
}
|
}
|
||||||
@ -252,10 +337,7 @@ async function fetchViaFileStorageStream(rawUrl: string, errors: string[]): Prom
|
|||||||
const blob = await streamFileBlob(key)
|
const blob = await streamFileBlob(key)
|
||||||
const mime = guessMime(key)
|
const mime = guessMime(key)
|
||||||
const typed = blob.type ? blob : new Blob([blob], { type: mime })
|
const typed = blob.type ? blob : new Blob([blob], { type: mime })
|
||||||
const bytes = await normalizePdfBytes(typed, rawUrl)
|
return blobFromNormalized(await normalizePreviewBytes(typed, rawUrl))
|
||||||
return new Blob([bytes], {
|
|
||||||
type: isPrintPreviewPdf(rawUrl, typed) ? 'application/pdf' : mime,
|
|
||||||
})
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const msg = extractOmsRequestError(e)
|
const msg = extractOmsRequestError(e)
|
||||||
if (msg) errors.push(msg)
|
if (msg) errors.push(msg)
|
||||||
@ -274,11 +356,8 @@ async function fetchPreviewBlob(rawUrl: string): Promise<Blob> {
|
|||||||
// 开发:测试 Admin 可能尚未部署 /file-storage,先走同源 COS 代理
|
// 开发:测试 Admin 可能尚未部署 /file-storage,先走同源 COS 代理
|
||||||
if (import.meta.env.DEV && isCos) {
|
if (import.meta.env.DEV && isCos) {
|
||||||
try {
|
try {
|
||||||
const blob = await fetchBlobViaCosProxy(raw)
|
// fetchBlobViaCosProxy 内已按内容规范化 MIME
|
||||||
const bytes = await normalizePdfBytes(blob, raw)
|
return await fetchBlobViaCosProxy(raw)
|
||||||
return new Blob([bytes], {
|
|
||||||
type: isPrintPreviewPdf(raw, blob) ? 'application/pdf' : blob.type || guessMime(raw),
|
|
||||||
})
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
errors.push(extractOmsRequestError(e) || 'COS 同源代理下载失败')
|
errors.push(extractOmsRequestError(e) || 'COS 同源代理下载失败')
|
||||||
}
|
}
|
||||||
@ -291,11 +370,7 @@ async function fetchPreviewBlob(rawUrl: string): Promise<Blob> {
|
|||||||
// 同源 /__cos__ 代理(生产 nginx;开发上面已试过则跳过)
|
// 同源 /__cos__ 代理(生产 nginx;开发上面已试过则跳过)
|
||||||
if (isCos && !import.meta.env.DEV) {
|
if (isCos && !import.meta.env.DEV) {
|
||||||
try {
|
try {
|
||||||
const blob = await fetchBlobViaCosProxy(raw)
|
return await fetchBlobViaCosProxy(raw)
|
||||||
const bytes = await normalizePdfBytes(blob, raw)
|
|
||||||
return new Blob([bytes], {
|
|
||||||
type: isPrintPreviewPdf(raw, blob) ? 'application/pdf' : blob.type || guessMime(raw),
|
|
||||||
})
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
errors.push(extractOmsRequestError(e) || 'COS 同源代理下载失败')
|
errors.push(extractOmsRequestError(e) || 'COS 同源代理下载失败')
|
||||||
}
|
}
|
||||||
@ -306,10 +381,7 @@ async function fetchPreviewBlob(rawUrl: string): Promise<Blob> {
|
|||||||
if (sameOriginPath.startsWith('/')) {
|
if (sameOriginPath.startsWith('/')) {
|
||||||
try {
|
try {
|
||||||
const blob = await fetchBlobViaSameOriginPath(sameOriginPath)
|
const blob = await fetchBlobViaSameOriginPath(sameOriginPath)
|
||||||
const bytes = await normalizePdfBytes(blob, raw)
|
return blobFromNormalized(await normalizePreviewBytes(blob, raw))
|
||||||
return new Blob([bytes], {
|
|
||||||
type: isPrintPreviewPdf(raw, blob) ? 'application/pdf' : blob.type || guessMime(raw),
|
|
||||||
})
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const msg = extractOmsRequestError(e)
|
const msg = extractOmsRequestError(e)
|
||||||
if (msg) errors.push(msg)
|
if (msg) errors.push(msg)
|
||||||
@ -351,6 +423,8 @@ export async function resolvePrintPreviewSrc(rawUrl: string): Promise<string> {
|
|||||||
export function isPrintPreviewPdf(rawUrl: string, blob?: Blob | null): boolean {
|
export function isPrintPreviewPdf(rawUrl: string, blob?: Blob | null): boolean {
|
||||||
const mime = String(blob?.type || '').toLowerCase()
|
const mime = String(blob?.type || '').toLowerCase()
|
||||||
if (mime.includes('pdf')) return true
|
if (mime.includes('pdf')) return true
|
||||||
|
// 已按内容嗅探为图片/HTML 时,即使 URL 带 .pdf 也不再当 PDF
|
||||||
|
if (mime.startsWith('image/') || mime.includes('html')) return false
|
||||||
return /\.pdf(\?|#|$)/i.test(String(rawUrl || ''))
|
return /\.pdf(\?|#|$)/i.test(String(rawUrl || ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1152,6 +1152,16 @@
|
|||||||
const id = Number(key.slice(5));
|
const id = Number(key.slice(5));
|
||||||
const product = filteredProductList.value.find((p: any) => p.id === id);
|
const product = filteredProductList.value.find((p: any) => p.id === id);
|
||||||
if (!product) return;
|
if (!product) return;
|
||||||
|
// 已选中且卖价已加载:忽略重复 select(树/受控 selectedKeys 偶发连发)
|
||||||
|
if (
|
||||||
|
selectedProduct.value === id
|
||||||
|
&& activeTab.value === 'price'
|
||||||
|
&& priceTabLoadedProductId === id
|
||||||
|
&& !configFormDirty.value
|
||||||
|
&& !rulesFormDirty.value
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const ok = await confirmDiscardUnsavedChanges();
|
const ok = await confirmDiscardUnsavedChanges();
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
await handleSelectProduct(product);
|
await handleSelectProduct(product);
|
||||||
@ -1180,65 +1190,80 @@
|
|||||||
let priceTabLoadSeq = 0;
|
let priceTabLoadSeq = 0;
|
||||||
/** 当前 priceTableData 所属产品;切走再回来时直接复用,不再空表重拉 */
|
/** 当前 priceTableData 所属产品;切走再回来时直接复用,不再空表重拉 */
|
||||||
let priceTabLoadedProductId: number | null = null;
|
let priceTabLoadedProductId: number | null = null;
|
||||||
/** 已做过发布态对比的列表签名,避免每次切 tab 都解析整份卖价快照 */
|
/** 同产品进行中的列表请求复用,避免树 select 连发打两次 price/list */
|
||||||
let pricePublishCheckedSig = '';
|
let priceTabInflight: { productId: number; promise: Promise<void> } | null = null;
|
||||||
const priceListLoading = ref(false);
|
const priceListLoading = ref(false);
|
||||||
|
|
||||||
async function loadPriceTabData(productId: number) {
|
async function loadPriceTabData(productId: number, opts?: { force?: boolean }) {
|
||||||
|
if (priceTabInflight?.productId === productId) {
|
||||||
|
await priceTabInflight.promise;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 已加载过该产品(含空列表)且非强制刷新:跳过,避免重复 price/list
|
||||||
|
if (!opts?.force && priceTabLoadedProductId === productId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const seq = ++priceTabLoadSeq;
|
const seq = ++priceTabLoadSeq;
|
||||||
const showSpinner = priceTabLoadedProductId !== productId || priceTableData.value.length === 0;
|
const showSpinner = priceTabLoadedProductId !== productId || priceTableData.value.length === 0;
|
||||||
if (showSpinner) priceListLoading.value = true;
|
if (showSpinner) priceListLoading.value = true;
|
||||||
try {
|
|
||||||
// 列表不拉 details,也不等发布记录。生效版本在列表渲染后单独补。
|
const run = (async () => {
|
||||||
const prices = await getSalesProductPriceList(productId, { includeDetails: false });
|
try {
|
||||||
// 快速切换产品时丢弃过期响应
|
// 列表不拉 details,也不等发布记录。生效版本在列表渲染后单独补。
|
||||||
if (seq !== priceTabLoadSeq || selectedProduct.value !== productId) return;
|
const prices = await getSalesProductPriceList(productId, { includeDetails: false });
|
||||||
// 映射前确保字典就绪,计费因素列显示中文名而非 code
|
// 快速切换产品时丢弃过期响应
|
||||||
if (!salesPriceDictFetched.value) {
|
|
||||||
await ensureSalesPriceDictLoaded();
|
|
||||||
if (seq !== priceTabLoadSeq || selectedProduct.value !== productId) return;
|
if (seq !== priceTabLoadSeq || selectedProduct.value !== productId) return;
|
||||||
}
|
// 映射前确保字典就绪,计费因素列显示中文名而非 code
|
||||||
// factors 仍可能较多:markRaw 禁止 Vue 深度代理
|
if (!salesPriceDictFetched.value) {
|
||||||
const rows = (prices || []).map((it: any) => {
|
await ensureSalesPriceDictLoaded();
|
||||||
const factors = Array.isArray(it.factors) ? it.factors : [];
|
if (seq !== priceTabLoadSeq || selectedProduct.value !== productId) return;
|
||||||
const audit = pickResolvedAudit(it);
|
|
||||||
return {
|
|
||||||
id: it.id,
|
|
||||||
serialNumber: it.serialNumber,
|
|
||||||
status: isEnableStatusEnabled(it.status ?? it.Status),
|
|
||||||
priceName: it.priceName,
|
|
||||||
costItem: it.costItem,
|
|
||||||
costUnit: it.costUnit,
|
|
||||||
currency: it.currency,
|
|
||||||
chargingCondition: it.chargingCondition || '',
|
|
||||||
billingFactors: factors
|
|
||||||
.map((f: any) => pricingFactorColumnLabel(String(f?.factor ?? f?.name ?? '')))
|
|
||||||
.filter(Boolean)
|
|
||||||
.join('+'),
|
|
||||||
factors: markRaw(factors),
|
|
||||||
details: null as any[] | null,
|
|
||||||
modifyTime: audit.updateTime,
|
|
||||||
modifier: audit.updateBy,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
priceTableData.value = rows;
|
|
||||||
priceTabLoadedProductId = productId;
|
|
||||||
void refreshSalesProductPriceEffectiveInfo(productId);
|
|
||||||
// 发布同步态必须每次刷新:不能跟价格行 modifyTime 签名绑死,否则刚发布后签名不变会跳过刷新、徽章一直「待发布」
|
|
||||||
void refreshPricePublishSyncStatus(productId).then(() => {
|
|
||||||
if (seq === priceTabLoadSeq && selectedProduct.value === productId) {
|
|
||||||
pricePublishCheckedSig = `${productId}|${rows
|
|
||||||
.map((r) => `${r.id}:${r.modifyTime}:${r.status ? 1 : 0}`)
|
|
||||||
.join(',')}`;
|
|
||||||
}
|
}
|
||||||
});
|
// factors 仍可能较多:markRaw 禁止 Vue 深度代理
|
||||||
} catch (e) {
|
const rows = (prices || []).map((it: any) => {
|
||||||
if (seq !== priceTabLoadSeq) return;
|
const factors = Array.isArray(it.factors) ? it.factors : [];
|
||||||
console.error(e);
|
const audit = pickResolvedAudit(it);
|
||||||
effectiveVersion.value = '--';
|
return {
|
||||||
effectiveTime.value = '--';
|
id: it.id,
|
||||||
|
serialNumber: it.serialNumber,
|
||||||
|
status: isEnableStatusEnabled(it.status ?? it.Status),
|
||||||
|
priceName: it.priceName,
|
||||||
|
costItem: it.costItem,
|
||||||
|
costUnit: it.costUnit,
|
||||||
|
currency: it.currency,
|
||||||
|
chargingCondition: it.chargingCondition || '',
|
||||||
|
billingFactors: factors
|
||||||
|
.map((f: any) => pricingFactorColumnLabel(String(f?.factor ?? f?.name ?? '')))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('+'),
|
||||||
|
factors: markRaw(factors),
|
||||||
|
details: null as any[] | null,
|
||||||
|
modifyTime: audit.updateTime,
|
||||||
|
modifier: audit.updateBy,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
priceTableData.value = rows;
|
||||||
|
priceTabLoadedProductId = productId;
|
||||||
|
void refreshSalesProductPriceEffectiveInfo(productId);
|
||||||
|
// 徽章只听后端 sync-status,不在前端算差异
|
||||||
|
void refreshPricePublishSyncStatus(productId);
|
||||||
|
} catch (e) {
|
||||||
|
if (seq !== priceTabLoadSeq) return;
|
||||||
|
console.error(e);
|
||||||
|
effectiveVersion.value = '--';
|
||||||
|
effectiveTime.value = '--';
|
||||||
|
} finally {
|
||||||
|
if (seq === priceTabLoadSeq) priceListLoading.value = false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
priceTabInflight = { productId, promise: run };
|
||||||
|
try {
|
||||||
|
await run;
|
||||||
} finally {
|
} finally {
|
||||||
if (seq === priceTabLoadSeq) priceListLoading.value = false;
|
if (priceTabInflight?.productId === productId && priceTabInflight.promise === run) {
|
||||||
|
priceTabInflight = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1404,9 +1429,10 @@
|
|||||||
return detail?.content ?? detail?.Content ?? null;
|
return detail?.content ?? detail?.Content ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 仅展示后端 sync-status.pending,前端不做快照 diff */
|
||||||
async function refreshPricePublishSyncStatus(productId?: number) {
|
async function refreshPricePublishSyncStatus(productId?: number) {
|
||||||
const pid = productId ?? selectedProduct.value;
|
const pid = productId ?? selectedProduct.value;
|
||||||
if (pid == null || !priceTableData.value.length) {
|
if (pid == null) {
|
||||||
pricePublishChecking.value = false;
|
pricePublishChecking.value = false;
|
||||||
pricePublishPending.value = null;
|
pricePublishPending.value = null;
|
||||||
return;
|
return;
|
||||||
@ -1419,7 +1445,8 @@
|
|||||||
operatorType: 'price',
|
operatorType: 'price',
|
||||||
});
|
});
|
||||||
if (seq !== priceTabLoadSeq || selectedProduct.value !== pid) return;
|
if (seq !== priceTabLoadSeq || selectedProduct.value !== pid) return;
|
||||||
pricePublishPending.value = !!(status?.pending ?? status?.Pending);
|
const pending = status?.pending ?? status?.Pending;
|
||||||
|
pricePublishPending.value = typeof pending === 'boolean' ? pending : null;
|
||||||
} catch {
|
} catch {
|
||||||
if (seq === priceTabLoadSeq) pricePublishPending.value = null;
|
if (seq === priceTabLoadSeq) pricePublishPending.value = null;
|
||||||
} finally {
|
} finally {
|
||||||
@ -1561,6 +1588,20 @@
|
|||||||
|
|
||||||
const handleSelectProduct = async (product: any) => {
|
const handleSelectProduct = async (product: any) => {
|
||||||
const productChanged = selectedProduct.value !== product.id;
|
const productChanged = selectedProduct.value !== product.id;
|
||||||
|
// 同产品重复选中且当前 tab 数据已在:不再打 detail / price/list
|
||||||
|
if (
|
||||||
|
!productChanged
|
||||||
|
&& product?.id
|
||||||
|
&& (
|
||||||
|
(activeTab.value === 'price' && priceTabLoadedProductId === product.id)
|
||||||
|
|| (activeTab.value === 'rules' && rulesTabLoadedProductId === product.id)
|
||||||
|
|| (activeTab.value === 'records' && recordsTabLoadedProductId === product.id)
|
||||||
|
|| activeTab.value === 'config'
|
||||||
|
|| activeTab.value === 'customerGuide'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
selectedProduct.value = product.id;
|
selectedProduct.value = product.id;
|
||||||
configPublishPending.value = false;
|
configPublishPending.value = false;
|
||||||
rulesPublishPending.value = false;
|
rulesPublishPending.value = false;
|
||||||
@ -1569,12 +1610,12 @@
|
|||||||
if (productChanged && activeTab.value !== 'price') {
|
if (productChanged && activeTab.value !== 'price') {
|
||||||
priceTableData.value = [];
|
priceTableData.value = [];
|
||||||
priceTabLoadedProductId = null;
|
priceTabLoadedProductId = null;
|
||||||
pricePublishCheckedSig = '';
|
|
||||||
pricePublishPending.value = null;
|
pricePublishPending.value = null;
|
||||||
pricePublishChecking.value = false;
|
pricePublishChecking.value = false;
|
||||||
effectiveVersion.value = '--';
|
effectiveVersion.value = '--';
|
||||||
effectiveTime.value = '--';
|
effectiveTime.value = '--';
|
||||||
} else if (activeTab.value === 'price') {
|
} else if (activeTab.value === 'price') {
|
||||||
|
// 切到卖价 tab / 换产品:先清空徽章,等 sync-status 回来再显示
|
||||||
pricePublishPending.value = null;
|
pricePublishPending.value = null;
|
||||||
pricePublishChecking.value = false;
|
pricePublishChecking.value = false;
|
||||||
}
|
}
|
||||||
@ -2272,9 +2313,9 @@
|
|||||||
publishModalMethods.closeModal();
|
publishModalMethods.closeModal();
|
||||||
await loadRecordsTabData(pid, true);
|
await loadRecordsTabData(pid, true);
|
||||||
if (operatorType === 'price') {
|
if (operatorType === 'price') {
|
||||||
pricePublishCheckedSig = '';
|
// 不乐观改徽章;等后端 sync-status 重算
|
||||||
pricePublishPending.value = false;
|
pricePublishPending.value = null;
|
||||||
await loadPriceTabData(pid);
|
await loadPriceTabData(pid, { force: true });
|
||||||
await refreshPricePublishSyncStatus(pid);
|
await refreshPricePublishSyncStatus(pid);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -3576,7 +3617,7 @@
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// 兼容后端不返回 id:用唯一组合定位刚新增记录,切成编辑态避免下次重复 create
|
// 兼容后端不返回 id:用唯一组合定位刚新增记录,切成编辑态避免下次重复 create
|
||||||
await loadPriceTabData(pid);
|
await loadPriceTabData(pid, { force: true });
|
||||||
const latest = [...priceTableData.value]
|
const latest = [...priceTableData.value]
|
||||||
.reverse()
|
.reverse()
|
||||||
.find((row: any) => row.serialNumber === payload.serialNumber && row.costItem === payload.costItem);
|
.find((row: any) => row.serialNumber === payload.serialNumber && row.costItem === payload.costItem);
|
||||||
@ -3606,7 +3647,7 @@
|
|||||||
try {
|
try {
|
||||||
const msg = await submitPriceConfig(pid);
|
const msg = await submitPriceConfig(pid);
|
||||||
Message.success(msg);
|
Message.success(msg);
|
||||||
await loadPriceTabData(pid);
|
await loadPriceTabData(pid, { force: true });
|
||||||
priceModalVisible.value = false;
|
priceModalVisible.value = false;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Message.error(e?.message || '保存失败');
|
Message.error(e?.message || '保存失败');
|
||||||
@ -4169,7 +4210,7 @@
|
|||||||
deletePriceModalVisible.value = false;
|
deletePriceModalVisible.value = false;
|
||||||
deletingPriceRecord.value = null;
|
deletingPriceRecord.value = null;
|
||||||
deletePriceInput.value = '';
|
deletePriceInput.value = '';
|
||||||
if (productId != null) await loadPriceTabData(productId);
|
if (productId != null) await loadPriceTabData(productId, { force: true });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Message.error(e?.message || '删除失败');
|
Message.error(e?.message || '删除失败');
|
||||||
}
|
}
|
||||||
@ -4188,7 +4229,7 @@
|
|||||||
if (pid == null || record?.id == null) return
|
if (pid == null || record?.id == null) return
|
||||||
try {
|
try {
|
||||||
await toggleSalesProductPriceStatus(record.id, toEnableStatus(!!record.status))
|
await toggleSalesProductPriceStatus(record.id, toEnableStatus(!!record.status))
|
||||||
await loadPriceTabData(pid)
|
await loadPriceTabData(pid, { force: true })
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
record.status = !record.status
|
record.status = !record.status
|
||||||
Message.error(e?.message || '更新失败')
|
Message.error(e?.message || '更新失败')
|
||||||
|
|||||||
@ -4106,9 +4106,10 @@ const publishDiffStatus = computed(() => {
|
|||||||
return 'changed'
|
return 'changed'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 仅展示后端 sync-status.pending,前端不做快照 diff */
|
||||||
async function refreshPricePublishSyncStatus(serviceChannelId?: number | string) {
|
async function refreshPricePublishSyncStatus(serviceChannelId?: number | string) {
|
||||||
const cid = serviceChannelId ?? (selectedChannel.value as any)?.id
|
const cid = serviceChannelId ?? (selectedChannel.value as any)?.id
|
||||||
if (cid == null || !priceTableData.value.length) {
|
if (cid == null) {
|
||||||
pricePublishChecking.value = false
|
pricePublishChecking.value = false
|
||||||
pricePublishPending.value = null
|
pricePublishPending.value = null
|
||||||
return
|
return
|
||||||
@ -4119,7 +4120,8 @@ async function refreshPricePublishSyncStatus(serviceChannelId?: number | string)
|
|||||||
serviceChannelId: cid,
|
serviceChannelId: cid,
|
||||||
operatorType: 'price',
|
operatorType: 'price',
|
||||||
})
|
})
|
||||||
pricePublishPending.value = !!(status?.pending ?? status?.Pending)
|
const pending = status?.pending ?? status?.Pending
|
||||||
|
pricePublishPending.value = typeof pending === 'boolean' ? pending : null
|
||||||
} catch {
|
} catch {
|
||||||
pricePublishPending.value = null
|
pricePublishPending.value = null
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -83,7 +83,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p class="face-rule">
|
<p class="face-rule">
|
||||||
{{ t('h5.coupons.minOrder', { amount: formatAmount(item.minOrderAmount) }) }}
|
{{
|
||||||
|
Number(item.minOrderAmount) > 0
|
||||||
|
? t('h5.coupons.minOrder', { amount: formatAmount(item.minOrderAmount) })
|
||||||
|
: t('h5.coupons.noThreshold')
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -453,6 +453,7 @@ defineExpose({ openWithDetail, reload: load })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.h5-pklist__handle {
|
.h5-pklist__handle {
|
||||||
|
flex-shrink: 0;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
margin: 10px auto 0;
|
margin: 10px auto 0;
|
||||||
@ -461,6 +462,7 @@ defineExpose({ openWithDetail, reload: load })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.h5-pklist__head {
|
.h5-pklist__head {
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -496,10 +498,14 @@ defineExpose({ openWithDetail, reload: load })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.h5-pklist__filters {
|
.h5-pklist__filters {
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px 10px;
|
||||||
|
background: #f5f6f8;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
@ -526,10 +532,17 @@ defineExpose({ openWithDetail, reload: load })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.h5-pklist__body {
|
.h5-pklist__body {
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
min-height: 220px;
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 0 16px 16px;
|
padding: 4px 16px 16px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
||||||
|
:deep(.arco-spin-children) {
|
||||||
|
min-height: 160px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-pklist-card {
|
.h5-pklist-card {
|
||||||
|
|||||||
@ -837,6 +837,7 @@ export default {
|
|||||||
'h5.coupons.tab.used': 'Used',
|
'h5.coupons.tab.used': 'Used',
|
||||||
'h5.coupons.tab.expired': 'Expired',
|
'h5.coupons.tab.expired': 'Expired',
|
||||||
'h5.coupons.minOrder': 'Min order {amount}',
|
'h5.coupons.minOrder': 'Min order {amount}',
|
||||||
|
'h5.coupons.noThreshold': 'No minimum',
|
||||||
'h5.coupons.use': 'Use now',
|
'h5.coupons.use': 'Use now',
|
||||||
'h5.coupons.discountUnit': ' off',
|
'h5.coupons.discountUnit': ' off',
|
||||||
'h5.coupons.expiringSoon': 'Expiring soon',
|
'h5.coupons.expiringSoon': 'Expiring soon',
|
||||||
|
|||||||
@ -820,6 +820,7 @@ export default {
|
|||||||
'h5.coupons.unnamed': '优惠券',
|
'h5.coupons.unnamed': '优惠券',
|
||||||
'h5.coupons.use': '去使用',
|
'h5.coupons.use': '去使用',
|
||||||
'h5.coupons.minOrder': '满{amount}可用',
|
'h5.coupons.minOrder': '满{amount}可用',
|
||||||
|
'h5.coupons.noThreshold': '无门槛',
|
||||||
'h5.coupons.discountUnit': '折',
|
'h5.coupons.discountUnit': '折',
|
||||||
'h5.coupons.expiringSoon': '即将过期',
|
'h5.coupons.expiringSoon': '即将过期',
|
||||||
'h5.coupons.badge.newcomer': '新人',
|
'h5.coupons.badge.newcomer': '新人',
|
||||||
|
|||||||
@ -53,7 +53,6 @@
|
|||||||
<a-button
|
<a-button
|
||||||
type="text"
|
type="text"
|
||||||
size="mini"
|
size="mini"
|
||||||
:loading="previewingId === file.id"
|
|
||||||
@click="openFile(file)"
|
@click="openFile(file)"
|
||||||
>
|
>
|
||||||
{{ $t('order.myOrders.drawer.attachments.preview') }}
|
{{ $t('order.myOrders.drawer.attachments.preview') }}
|
||||||
@ -93,7 +92,6 @@ import {
|
|||||||
type OrderFileItem,
|
type OrderFileItem,
|
||||||
} from '@/api/order';
|
} from '@/api/order';
|
||||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||||
import { fetchPrintPreviewBlob } from '@/utils/printPreview';
|
|
||||||
|
|
||||||
const FILE_TYPE_DEFS = [
|
const FILE_TYPE_DEFS = [
|
||||||
{
|
{
|
||||||
@ -154,7 +152,6 @@ const { t } = useI18n();
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const uploadingType = ref<string | null>(null);
|
const uploadingType = ref<string | null>(null);
|
||||||
const deletingId = ref<number | null>(null);
|
const deletingId = ref<number | null>(null);
|
||||||
const previewingId = ref<number | null>(null);
|
|
||||||
const groups = ref<Record<string, OrderFileItem[]>>({});
|
const groups = ref<Record<string, OrderFileItem[]>>({});
|
||||||
|
|
||||||
const canUpload = computed(() => !props.readOnly && !props.customerView);
|
const canUpload = computed(() => !props.readOnly && !props.customerView);
|
||||||
@ -169,51 +166,26 @@ const visibleSections = computed(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
function resolveUrl(path?: string) {
|
/** 与打印弹窗「查看面单」一致:同步打开可访问 URL(勿先 await 再开 blob,Chrome 会当搜索词) */
|
||||||
return resolveOmsAssetUrl(path);
|
function toAbsoluteUrl(path?: string) {
|
||||||
}
|
const resolved = resolveOmsAssetUrl(path) || String(path || '').trim();
|
||||||
|
if (!resolved) return '';
|
||||||
function guessAttachMime(file: OrderFileItem, url: string): string {
|
if (/^https?:\/\//i.test(resolved) || resolved.startsWith('blob:') || resolved.startsWith('data:')) {
|
||||||
const ct = String(file.contentType || '').trim().toLowerCase();
|
return resolved;
|
||||||
if (ct && !ct.includes('octet-stream') && !ct.includes('force-download')) {
|
|
||||||
return ct;
|
|
||||||
}
|
}
|
||||||
const hint = `${file.fileName || ''} ${url}`;
|
if (resolved.startsWith('/')) return `${window.location.origin}${resolved}`;
|
||||||
if (/\.pdf(\?|#|$)/i.test(hint)) return 'application/pdf';
|
return `${window.location.origin}/${resolved}`;
|
||||||
if (/\.png(\?|#|$)/i.test(hint)) return 'image/png';
|
|
||||||
if (/\.jpe?g(\?|#|$)/i.test(hint)) return 'image/jpeg';
|
|
||||||
if (/\.webp(\?|#|$)/i.test(hint)) return 'image/webp';
|
|
||||||
if (/\.gif(\?|#|$)/i.test(hint)) return 'image/gif';
|
|
||||||
if (/\.bmp(\?|#|$)/i.test(hint)) return 'image/bmp';
|
|
||||||
return ct || 'application/octet-stream';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 拉取为带正确 MIME 的 blob 再打开,走浏览器内置预览,避免 Content-Disposition 触发下载 */
|
function openFile(file: OrderFileItem) {
|
||||||
async function openFile(file: OrderFileItem) {
|
const url = toAbsoluteUrl(file.fileUrl);
|
||||||
const raw = resolveUrl(file.fileUrl);
|
if (!url) {
|
||||||
if (!raw) return;
|
Message.warning(t('order.myOrders.drawer.attachments.previewFailed'));
|
||||||
if (previewingId.value === file.id) return;
|
return;
|
||||||
|
}
|
||||||
previewingId.value = file.id;
|
const win = window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
try {
|
if (!win) {
|
||||||
const blob = await fetchPrintPreviewBlob(raw);
|
Message.warning(t('order.myOrders.drawer.attachments.previewBlocked'));
|
||||||
const mime = guessAttachMime(file, raw);
|
|
||||||
const typed =
|
|
||||||
blob.type && !/octet-stream|force-download/i.test(blob.type)
|
|
||||||
? blob
|
|
||||||
: new Blob([blob], { type: mime });
|
|
||||||
const blobUrl = URL.createObjectURL(typed);
|
|
||||||
const win = window.open(blobUrl, '_blank');
|
|
||||||
if (!win) {
|
|
||||||
Message.warning(t('order.myOrders.drawer.attachments.previewBlocked'));
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 120_000);
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(e?.message || t('order.myOrders.drawer.attachments.previewFailed'));
|
|
||||||
} finally {
|
|
||||||
previewingId.value = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -207,23 +207,27 @@ function onClaimTypeChange(key: string) {
|
|||||||
formRef.value?.setValues?.({ ...formState, claimType: key })
|
formRef.value?.setValues?.({ ...formState, claimType: key })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modalTitle = computed(() =>
|
||||||
|
t('order.myOrders.claimed.modalTitle', { count: props.orderIds.length }),
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
async (open) => {
|
async (open) => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
resetForm()
|
resetForm()
|
||||||
modalMethods.setModalProps({
|
|
||||||
title: t('order.myOrders.claimed.modalTitle', { count: props.orderIds.length }),
|
|
||||||
width: 720,
|
|
||||||
maskClosable: false,
|
|
||||||
})
|
|
||||||
modalMethods.openModal(true)
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
formRef.value?.setValues?.({ ...formState })
|
formRef.value?.setValues?.({ ...formState })
|
||||||
formRef.value?.formRef?.clearValidate?.()
|
formRef.value?.formRef?.clearValidate?.()
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function onUpdateVisible(val: boolean) {
|
||||||
|
if (val === props.visible) return
|
||||||
|
emit('update:visible', val)
|
||||||
|
}
|
||||||
|
|
||||||
async function submitBatchJob(payload: Record<string, unknown>) {
|
async function submitBatchJob(payload: Record<string, unknown>) {
|
||||||
const job = await postOrderBatchJob({
|
const job = await postOrderBatchJob({
|
||||||
action: 'mark-claimed',
|
action: 'mark-claimed',
|
||||||
@ -314,14 +318,18 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
modalMethods.closeModal()
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalPro
|
<ModalPro
|
||||||
|
:visible="visible"
|
||||||
|
:title="modalTitle"
|
||||||
|
:width="720"
|
||||||
|
:mask-closable="false"
|
||||||
|
:confirm-loading="submitting"
|
||||||
@register="registerModal"
|
@register="registerModal"
|
||||||
:loading="submitting"
|
@update:visible="onUpdateVisible"
|
||||||
@ok="handleSubmit"
|
@ok="handleSubmit"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -166,24 +166,28 @@ function resetForm() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modalTitle = computed(() =>
|
||||||
|
t('order.myOrders.problematic.modalTitle', { count: props.orderIds.length }),
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
async (open) => {
|
async (open) => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
resetForm()
|
resetForm()
|
||||||
modalMethods.setModalProps({
|
|
||||||
title: t('order.myOrders.problematic.modalTitle', { count: props.orderIds.length }),
|
|
||||||
width: 720,
|
|
||||||
maskClosable: false,
|
|
||||||
})
|
|
||||||
modalMethods.openModal(true)
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await loadTemplateOptions()
|
await loadTemplateOptions()
|
||||||
formRef.value?.setValues?.({ ...formState })
|
formRef.value?.setValues?.({ ...formState })
|
||||||
formRef.value?.formRef?.clearValidate?.()
|
formRef.value?.formRef?.clearValidate?.()
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function onUpdateVisible(val: boolean) {
|
||||||
|
if (val === props.visible) return
|
||||||
|
emit('update:visible', val)
|
||||||
|
}
|
||||||
|
|
||||||
async function submitBatchJob(payload: Record<string, unknown>) {
|
async function submitBatchJob(payload: Record<string, unknown>) {
|
||||||
const job = await postOrderBatchJob({
|
const job = await postOrderBatchJob({
|
||||||
action: 'mark-problematic',
|
action: 'mark-problematic',
|
||||||
@ -271,14 +275,18 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
modalMethods.closeModal()
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalPro
|
<ModalPro
|
||||||
|
:visible="visible"
|
||||||
|
:title="modalTitle"
|
||||||
|
:width="720"
|
||||||
|
:mask-closable="false"
|
||||||
|
:confirm-loading="submitting"
|
||||||
@register="registerModal"
|
@register="registerModal"
|
||||||
:loading="submitting"
|
@update:visible="onUpdateVisible"
|
||||||
@ok="handleSubmit"
|
@ok="handleSubmit"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -231,6 +231,7 @@ watch(
|
|||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -207,23 +207,27 @@ function onClaimTypeChange(key: string) {
|
|||||||
formRef.value?.setValues?.({ ...formState, claimType: key })
|
formRef.value?.setValues?.({ ...formState, claimType: key })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modalTitle = computed(() =>
|
||||||
|
t('order.myOrders.claimed.modalTitle', { count: props.orderIds.length }),
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
async (open) => {
|
async (open) => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
resetForm()
|
resetForm()
|
||||||
modalMethods.setModalProps({
|
|
||||||
title: t('order.myOrders.claimed.modalTitle', { count: props.orderIds.length }),
|
|
||||||
width: 720,
|
|
||||||
maskClosable: false,
|
|
||||||
})
|
|
||||||
modalMethods.openModal(true)
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
formRef.value?.setValues?.({ ...formState })
|
formRef.value?.setValues?.({ ...formState })
|
||||||
formRef.value?.formRef?.clearValidate?.()
|
formRef.value?.formRef?.clearValidate?.()
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function onUpdateVisible(val: boolean) {
|
||||||
|
if (val === props.visible) return
|
||||||
|
emit('update:visible', val)
|
||||||
|
}
|
||||||
|
|
||||||
async function submitBatchJob(payload: Record<string, unknown>) {
|
async function submitBatchJob(payload: Record<string, unknown>) {
|
||||||
const job = await postOrderBatchJob({
|
const job = await postOrderBatchJob({
|
||||||
action: 'mark-claimed',
|
action: 'mark-claimed',
|
||||||
@ -314,14 +318,18 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
modalMethods.closeModal()
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalPro
|
<ModalPro
|
||||||
|
:visible="visible"
|
||||||
|
:title="modalTitle"
|
||||||
|
:width="720"
|
||||||
|
:mask-closable="false"
|
||||||
|
:confirm-loading="submitting"
|
||||||
@register="registerModal"
|
@register="registerModal"
|
||||||
:loading="submitting"
|
@update:visible="onUpdateVisible"
|
||||||
@ok="handleSubmit"
|
@ok="handleSubmit"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -166,24 +166,28 @@ function resetForm() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modalTitle = computed(() =>
|
||||||
|
t('order.myOrders.problematic.modalTitle', { count: props.orderIds.length }),
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
async (open) => {
|
async (open) => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
resetForm()
|
resetForm()
|
||||||
modalMethods.setModalProps({
|
|
||||||
title: t('order.myOrders.problematic.modalTitle', { count: props.orderIds.length }),
|
|
||||||
width: 720,
|
|
||||||
maskClosable: false,
|
|
||||||
})
|
|
||||||
modalMethods.openModal(true)
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await loadTemplateOptions()
|
await loadTemplateOptions()
|
||||||
formRef.value?.setValues?.({ ...formState })
|
formRef.value?.setValues?.({ ...formState })
|
||||||
formRef.value?.formRef?.clearValidate?.()
|
formRef.value?.formRef?.clearValidate?.()
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function onUpdateVisible(val: boolean) {
|
||||||
|
if (val === props.visible) return
|
||||||
|
emit('update:visible', val)
|
||||||
|
}
|
||||||
|
|
||||||
async function submitBatchJob(payload: Record<string, unknown>) {
|
async function submitBatchJob(payload: Record<string, unknown>) {
|
||||||
const job = await postOrderBatchJob({
|
const job = await postOrderBatchJob({
|
||||||
action: 'mark-problematic',
|
action: 'mark-problematic',
|
||||||
@ -271,14 +275,18 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
modalMethods.closeModal()
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalPro
|
<ModalPro
|
||||||
|
:visible="visible"
|
||||||
|
:title="modalTitle"
|
||||||
|
:width="720"
|
||||||
|
:mask-closable="false"
|
||||||
|
:confirm-loading="submitting"
|
||||||
@register="registerModal"
|
@register="registerModal"
|
||||||
:loading="submitting"
|
@update:visible="onUpdateVisible"
|
||||||
@ok="handleSubmit"
|
@ok="handleSubmit"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -229,6 +229,7 @@ watch(
|
|||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import ModalPro from '@/components/ModalPro/index.vue'
|
import ModalPro from '@/components/ModalPro/index.vue'
|
||||||
@ -86,21 +86,24 @@ function onSchemeChange(key: string) {
|
|||||||
formState.scheme = key as OrderReallocateScheme
|
formState.scheme = key as OrderReallocateScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modalTitle = computed(() =>
|
||||||
|
t('order.myOrders.reallocate.modalTitle', { count: props.orderIds.length }),
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
async (open) => {
|
(open) => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
resetForm()
|
resetForm()
|
||||||
modalMethods.setModalProps({
|
|
||||||
title: t('order.myOrders.reallocate.modalTitle', { count: props.orderIds.length }),
|
|
||||||
width: 560,
|
|
||||||
maskClosable: false,
|
|
||||||
})
|
|
||||||
modalMethods.openModal(true)
|
|
||||||
await nextTick()
|
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function onUpdateVisible(val: boolean) {
|
||||||
|
if (val === props.visible) return
|
||||||
|
emit('update:visible', val)
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => formState.scheme,
|
() => formState.scheme,
|
||||||
async (scheme) => {
|
async (scheme) => {
|
||||||
@ -209,14 +212,18 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
modalMethods.closeModal()
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalPro
|
<ModalPro
|
||||||
|
:visible="visible"
|
||||||
|
:title="modalTitle"
|
||||||
|
:width="560"
|
||||||
|
:mask-closable="false"
|
||||||
|
:confirm-loading="submitting"
|
||||||
@register="registerModal"
|
@register="registerModal"
|
||||||
:loading="submitting"
|
@update:visible="onUpdateVisible"
|
||||||
@ok="handleSubmit"
|
@ok="handleSubmit"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -364,15 +364,15 @@ body.command-home #navbar .site-nav-dock a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.command-home #navbar > ul > li.site-nav-tools {
|
body.command-home #navbar > ul > li.site-nav-tools {
|
||||||
height: 60px;
|
height: 40px;
|
||||||
padding: 5px;
|
padding: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 28px;
|
border-radius: 20px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 12px 40px rgba(15, 23, 42, 0.1),
|
0 8px 24px rgba(15, 23, 42, 0.08),
|
||||||
0 2px 8px rgba(15, 23, 42, 0.04);
|
0 1px 4px rgba(15, 23, 42, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.command-home .site-nav-tools__glass {
|
body.command-home .site-nav-tools__glass {
|
||||||
@ -436,13 +436,14 @@ body.command-home #navbar .site-nav-dock a {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0 14px;
|
padding: 0 10px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 23px;
|
border-radius: 17px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
-webkit-backdrop-filter: none;
|
-webkit-backdrop-filter: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
font-size: 12px;
|
||||||
transition:
|
transition:
|
||||||
background 0.22s ease,
|
background 0.22s ease,
|
||||||
box-shadow 0.22s ease,
|
box-shadow 0.22s ease,
|
||||||
@ -466,19 +467,27 @@ body.command-home #navbar .site-nav-dock a {
|
|||||||
body.command-home #navbar .site-nav-tools .site-nav-avatar:hover {
|
body.command-home #navbar .site-nav-tools .site-nav-avatar:hover {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 50px;
|
width: 34px;
|
||||||
height: 50px;
|
height: 34px;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.command-home #navbar .site-nav-tools .site-nav-avatar img {
|
body.command-home #navbar .site-nav-tools .site-nav-avatar img {
|
||||||
width: 36px;
|
width: 26px;
|
||||||
height: 36px;
|
height: 26px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.command-home #navbar .site-nav-tools .language-toggle-icon {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.command-home #navbar .site-nav-tools .language-toggle__next {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
body.command-home #navbar .language-toggle-link,
|
body.command-home #navbar .language-toggle-link,
|
||||||
body.command-home #navbar .language-toggle-link:focus {
|
body.command-home #navbar .language-toggle-link:focus {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|||||||
@ -33,116 +33,118 @@
|
|||||||
<span class="step-chip">{{ $t('user.recharge.deposit.form.stepChip', { step: current + 1, tip: currentTip }) }}</span>
|
<span class="step-chip">{{ $t('user.recharge.deposit.form.stepChip', { step: current + 1, tip: currentTip }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-form :model="formModel">
|
<a-form :model="formModel" class="deposit-form">
|
||||||
<div v-if="current === 0" class="form-block">
|
<div class="form-scroll">
|
||||||
<p class="block-title">{{ $t('user.recharge.deposit.form.selectAmount') }}</p>
|
<div v-if="current === 0" class="form-block">
|
||||||
<a-radio-group
|
<p class="block-title">{{ $t('user.recharge.deposit.form.selectAmount') }}</p>
|
||||||
v-model="rechargeAmount"
|
<a-radio-group
|
||||||
type="button"
|
v-model="rechargeAmount"
|
||||||
button-style="solid"
|
|
||||||
class="amount-buttons"
|
|
||||||
>
|
|
||||||
<a-radio :value="500">¥500</a-radio>
|
|
||||||
<a-radio :value="1000">¥1,000</a-radio>
|
|
||||||
<a-radio :value="1500">¥1,500</a-radio>
|
|
||||||
<a-radio :value="3000">¥3,000</a-radio>
|
|
||||||
<a-radio :value="0">{{ $t('user.recharge.deposit.form.custom') }}</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="rechargeAmount === 0"
|
|
||||||
class="custom-amount animate__animated animate__fadeIn"
|
|
||||||
>
|
|
||||||
<label>{{ $t('user.recharge.deposit.form.inputAmount') }}</label>
|
|
||||||
<a-input-number
|
|
||||||
v-model="customAmount"
|
|
||||||
:min="1"
|
|
||||||
:precision="2"
|
|
||||||
style="width: 240px"
|
|
||||||
>
|
|
||||||
<template #prefix>¥</template>
|
|
||||||
<template #suffix>RMB</template>
|
|
||||||
</a-input-number>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="current === 0" class="form-block">
|
|
||||||
<p class="block-title">{{ $t('user.recharge.deposit.form.paymentMethod') }}</p>
|
|
||||||
<div class="pay-type-grid">
|
|
||||||
<button
|
|
||||||
type="button"
|
type="button"
|
||||||
class="pay-card"
|
button-style="solid"
|
||||||
:class="{ active: payType === 'zfb' }"
|
class="amount-buttons"
|
||||||
@click="payType = 'zfb'"
|
|
||||||
>
|
>
|
||||||
<img :src="zfbIcon" :alt="$t('user.recharge.deposit.payment.alipay')" />
|
<a-radio :value="500">¥500</a-radio>
|
||||||
<div>
|
<a-radio :value="1000">¥1,000</a-radio>
|
||||||
<p class="pay-name">{{ $t('user.recharge.deposit.payment.alipay') }}</p>
|
<a-radio :value="1500">¥1,500</a-radio>
|
||||||
<p class="pay-desc">{{ $t('user.recharge.deposit.payment.alipayDesc') }}</p>
|
<a-radio :value="3000">¥3,000</a-radio>
|
||||||
</div>
|
<a-radio :value="0">{{ $t('user.recharge.deposit.form.custom') }}</a-radio>
|
||||||
</button>
|
</a-radio-group>
|
||||||
<button
|
|
||||||
type="button"
|
<div
|
||||||
class="pay-card"
|
v-if="rechargeAmount === 0"
|
||||||
:class="{ active: payType === 'wx' }"
|
class="custom-amount animate__animated animate__fadeIn"
|
||||||
@click="payType = 'wx'"
|
|
||||||
>
|
>
|
||||||
<img :src="wxIcon" :alt="$t('user.recharge.deposit.payment.wechat')" />
|
<label>{{ $t('user.recharge.deposit.form.inputAmount') }}</label>
|
||||||
<div>
|
<a-input-number
|
||||||
<p class="pay-name">{{ $t('user.recharge.deposit.payment.wechat') }}</p>
|
v-model="customAmount"
|
||||||
<p class="pay-desc">{{ $t('user.recharge.deposit.payment.wechatDesc') }}</p>
|
:min="1"
|
||||||
</div>
|
:precision="2"
|
||||||
</button>
|
style="width: 240px"
|
||||||
</div>
|
>
|
||||||
|
<template #prefix>¥</template>
|
||||||
<div class="qr-wrapper">
|
<template #suffix>RMB</template>
|
||||||
<div class="qr-image" v-if="payType === 'zfb'">
|
</a-input-number>
|
||||||
<a-image :src="zfbQrCode" :preview="true" width="220" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="qr-image" v-else>
|
</div>
|
||||||
<a-image :src="wxQrCode" :preview="true" width="220" />
|
|
||||||
|
<div v-if="current === 0" class="form-block">
|
||||||
|
<p class="block-title">{{ $t('user.recharge.deposit.form.paymentMethod') }}</p>
|
||||||
|
<div class="pay-type-grid">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pay-card"
|
||||||
|
:class="{ active: payType === 'zfb' }"
|
||||||
|
@click="payType = 'zfb'"
|
||||||
|
>
|
||||||
|
<img :src="zfbIcon" :alt="$t('user.recharge.deposit.payment.alipay')" />
|
||||||
|
<div>
|
||||||
|
<p class="pay-name">{{ $t('user.recharge.deposit.payment.alipay') }}</p>
|
||||||
|
<p class="pay-desc">{{ $t('user.recharge.deposit.payment.alipayDesc') }}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pay-card"
|
||||||
|
:class="{ active: payType === 'wx' }"
|
||||||
|
@click="payType = 'wx'"
|
||||||
|
>
|
||||||
|
<img :src="wxIcon" :alt="$t('user.recharge.deposit.payment.wechat')" />
|
||||||
|
<div>
|
||||||
|
<p class="pay-name">{{ $t('user.recharge.deposit.payment.wechat') }}</p>
|
||||||
|
<p class="pay-desc">{{ $t('user.recharge.deposit.payment.wechatDesc') }}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="qr-tip">{{ $t('user.recharge.deposit.form.qrTip') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="current === 1" class="form-block">
|
<div class="qr-wrapper">
|
||||||
<p class="block-title">{{ $t('user.recharge.deposit.form.uploadVoucher') }}</p>
|
<div class="qr-image" v-if="payType === 'zfb'">
|
||||||
<p class="block-desc">{{ $t('user.recharge.deposit.form.uploadVoucherDesc') }}</p>
|
<a-image :src="zfbQrCode" :preview="true" width="180" />
|
||||||
|
|
||||||
<a-upload
|
|
||||||
name="rechargeScreenshot"
|
|
||||||
:file-list="fileList"
|
|
||||||
list-type="picture-card"
|
|
||||||
:show-upload-list="true"
|
|
||||||
accept="image/*"
|
|
||||||
:custom-request="handleUpload"
|
|
||||||
@change="handleFileChange"
|
|
||||||
>
|
|
||||||
<template #upload-button>
|
|
||||||
<div v-if="!previewUrl" class="upload-card">
|
|
||||||
<icon-plus />
|
|
||||||
<span>{{ $t('user.recharge.deposit.form.uploadScreenshot') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="qr-image" v-else>
|
||||||
</a-upload>
|
<a-image :src="wxQrCode" :preview="true" width="180" />
|
||||||
<div v-if="previewUrl" class="preview-wrap">
|
</div>
|
||||||
<img :src="previewUrl" alt="screenshot" class="preview-image" />
|
<p class="qr-tip">{{ $t('user.recharge.deposit.form.qrTip') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="current === 1" class="form-block">
|
||||||
|
<p class="block-title">{{ $t('user.recharge.deposit.form.uploadVoucher') }}</p>
|
||||||
|
<p class="block-desc">{{ $t('user.recharge.deposit.form.uploadVoucherDesc') }}</p>
|
||||||
|
|
||||||
|
<a-upload
|
||||||
|
name="rechargeScreenshot"
|
||||||
|
:file-list="fileList"
|
||||||
|
list-type="picture-card"
|
||||||
|
:show-upload-list="true"
|
||||||
|
accept="image/*"
|
||||||
|
:custom-request="handleUpload"
|
||||||
|
@change="handleFileChange"
|
||||||
|
>
|
||||||
|
<template #upload-button>
|
||||||
|
<div v-if="!previewUrl" class="upload-card">
|
||||||
|
<icon-plus />
|
||||||
|
<span>{{ $t('user.recharge.deposit.form.uploadScreenshot') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-upload>
|
||||||
|
<div v-if="previewUrl" class="preview-wrap">
|
||||||
|
<img :src="previewUrl" alt="screenshot" class="preview-image" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="current === 2" class="form-block">
|
||||||
|
<a-result status="success" :title="$t('user.recharge.deposit.result.title')" :sub-title="$t('user.recharge.deposit.result.subtitle')">
|
||||||
|
<template #extra>
|
||||||
|
<a-space>
|
||||||
|
<a-button @click="reset">{{ $t('user.recharge.deposit.result.actions.rechargeAgain') }}</a-button>
|
||||||
|
<a-button type="primary" @click="goToRecord">{{ $t('user.recharge.deposit.result.actions.viewBill') }}</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</a-result>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="current === 2" class="form-block">
|
<div v-if="current < 2" class="action-row">
|
||||||
<a-result status="success" :title="$t('user.recharge.deposit.result.title')" :sub-title="$t('user.recharge.deposit.result.subtitle')">
|
|
||||||
<template #extra>
|
|
||||||
<a-space>
|
|
||||||
<a-button @click="reset">{{ $t('user.recharge.deposit.result.actions.rechargeAgain') }}</a-button>
|
|
||||||
<a-button type="primary" @click="goToRecord">{{ $t('user.recharge.deposit.result.actions.viewBill') }}</a-button>
|
|
||||||
</a-space>
|
|
||||||
</template>
|
|
||||||
</a-result>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="action-row">
|
|
||||||
<a-button v-if="current === 1" @click="current--">{{ $t('user.recharge.deposit.actions.prevStep') }}</a-button>
|
<a-button v-if="current === 1" @click="current--">{{ $t('user.recharge.deposit.actions.prevStep') }}</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
v-if="current === 1"
|
v-if="current === 1"
|
||||||
@ -286,29 +288,36 @@ const goToRecord = () => {
|
|||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.deposit-page {
|
.deposit-page {
|
||||||
min-height: 100vh;
|
box-sizing: border-box;
|
||||||
padding: 40px 0 60px;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 16px;
|
||||||
background: radial-gradient(circle at top, #eff4ff, #f7f8fb 60%);
|
background: radial-gradient(circle at top, #eff4ff, #f7f8fb 60%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deposit-layout {
|
.deposit-layout {
|
||||||
width: 1000px;
|
width: min(1000px, 100%);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 320px 1fr;
|
grid-template-columns: 300px minmax(0, 1fr);
|
||||||
gap: 24px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-panel {
|
.info-panel {
|
||||||
padding: 24px;
|
min-height: 0;
|
||||||
|
padding: 20px;
|
||||||
background: #0d1d3d;
|
background: #0d1d3d;
|
||||||
border-radius: 24px;
|
border-radius: 20px;
|
||||||
color: #f5f8ff;
|
color: #f5f8ff;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 18px;
|
gap: 14px;
|
||||||
|
overflow: auto;
|
||||||
box-shadow: 0 20px 40px rgba(1, 14, 38, 0.25);
|
box-shadow: 0 20px 40px rgba(1, 14, 38, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,13 +374,30 @@ const goToRecord = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-panel {
|
.form-panel {
|
||||||
padding: 28px 32px 34px;
|
min-height: 0;
|
||||||
|
padding: 20px 24px 16px;
|
||||||
background: var(--app-card-bg);
|
background: var(--app-card-bg);
|
||||||
border-radius: 32px;
|
border-radius: 24px;
|
||||||
box-shadow: 0 20px 60px rgba(31, 52, 108, 0.12);
|
box-shadow: 0 20px 60px rgba(31, 52, 108, 0.12);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 22px;
|
gap: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deposit-form {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-header {
|
.form-header {
|
||||||
@ -391,9 +417,10 @@ const goToRecord = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.amount-value {
|
.amount-value {
|
||||||
font-size: 30px;
|
font-size: 26px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1f2f4d;
|
color: #1f2f4d;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-chip {
|
.step-chip {
|
||||||
@ -504,7 +531,7 @@ const goToRecord = () => {
|
|||||||
.qr-image {
|
.qr-image {
|
||||||
border: 2px dashed #dbe3f4;
|
border: 2px dashed #dbe3f4;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 14px;
|
padding: 10px;
|
||||||
background: #fdfefe;
|
background: #fdfefe;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -513,8 +540,8 @@ const goToRecord = () => {
|
|||||||
|
|
||||||
:deep(.arco-image),
|
:deep(.arco-image),
|
||||||
:deep(.arco-image-with-footer-inner) {
|
:deep(.arco-image-with-footer-inner) {
|
||||||
width: 220px !important;
|
width: 180px !important;
|
||||||
height: 220px !important;
|
height: 180px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.arco-image-img) {
|
:deep(.arco-image-img) {
|
||||||
@ -532,20 +559,20 @@ const goToRecord = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.upload-card {
|
.upload-card {
|
||||||
width: 200px;
|
width: 160px;
|
||||||
height: 200px;
|
height: 160px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 1px dashed #d7dff0;
|
border: 1px dashed #d7dff0;
|
||||||
border-radius: 24px;
|
border-radius: 20px;
|
||||||
color: #9da9bf;
|
color: #9da9bf;
|
||||||
background: #f8faff;
|
background: #f8faff;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
.arco-icon {
|
.arco-icon {
|
||||||
font-size: 30px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,36 +581,50 @@ const goToRecord = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-image {
|
.preview-image {
|
||||||
max-width: 220px;
|
max-width: 180px;
|
||||||
max-height: 220px;
|
max-height: 180px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--color-border-2);
|
border: 1px solid var(--color-border-2);
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-row {
|
.action-row {
|
||||||
margin-top: 10px;
|
flex-shrink: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--color-border-2);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 14px;
|
gap: 12px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
background: var(--app-card-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.arco-upload-list-picture-card) {
|
:deep(.arco-upload-list-picture-card) {
|
||||||
.arco-upload-list-item {
|
.arco-upload-list-item {
|
||||||
width: 220px;
|
width: 160px;
|
||||||
height: 220px;
|
height: 160px;
|
||||||
border-radius: 24px;
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
|
.deposit-page {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.deposit-layout {
|
.deposit-layout {
|
||||||
width: 94%;
|
width: 100%;
|
||||||
|
max-height: none;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-panel {
|
.info-panel {
|
||||||
order: 2;
|
order: 2;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-panel {
|
||||||
|
min-height: min(70vh, 640px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user