Compare commits

..

2 Commits

Author SHA1 Message Date
zzh
52071ecf47 Merge pull request 'feat: 入库预报查看编辑合一、箱唛/SKU打印与订单改销售产品' (#10) from test into master
All checks were successful
Build and Deploy Front / deploy (push) Successful in 1m49s
Reviewed-on: #10
2026-10-22 19:13:29 +08:00
zzh
6ec41b2dbe feat: 入库预报查看编辑合一、箱唛/SKU打印与订单改销售产品
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m25s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-22 19:05:50 +08:00
12 changed files with 652 additions and 65 deletions

View File

@ -2222,6 +2222,19 @@ export async function postOrderUpdate(body: Record<string, any>): Promise<any> {
return res.data?.data ?? res.data?.Data ?? res.data
}
/** POST /order/change-product — 改销售产品并自动重排货/重算费 */
export async function postOrderChangeProduct(body: {
id: number | string
productCode: string
}): Promise<any> {
const res = await axiosInstance.post(orderUrl('/order/change-product'), {
id: Number(body.id),
productCode: String(body.productCode ?? '').trim(),
})
assertBusinessOk(res.data)
return res.data?.data ?? res.data?.Data ?? res.data
}
/** POST /order/weight/update */
export async function postOrderWeightUpdate(body: {
id: number | string

View File

@ -26,6 +26,22 @@ export const deleteWarehouseSku = (id: number) => postOk('/warehouse/sku/delete'
export const bindWarehouseSkuMapping = (body: Record<string, unknown>) =>
postOk('/warehouse/sku/bind-mapping', body)
/** POST /warehouse/sku/print-label → TemplateType=sku 单页 PDF */
export async function postWarehouseSkuPrintLabel(body: {
id: number
customerId?: number
}): Promise<{ printUrl: string; skuCode: string }> {
const data = await postOk('/warehouse/sku/print-label', {
id: body.id,
customerId: body.customerId,
})
const d = (data ?? {}) as Record<string, unknown>
return {
printUrl: String(d.printUrl ?? d.PrintUrl ?? '').trim(),
skuCode: String(d.skuCode ?? d.SkuCode ?? '').trim(),
}
}
/** 上传 SKU 图片:优先 COS 直传,落库存 key;返回 key + 预览 URL */
export async function uploadWarehouseSkuImage(
file: File,
@ -119,6 +135,23 @@ export const confirmWarehouseInboundReceive = (body: Record<string, unknown>) =>
export const getWarehouseInboundSummary = (params?: Record<string, unknown>) =>
getData('/warehouse/inbound/summary', params)
/** POST /warehouse/inbound/print-labels → 多箱合并为一个 PDF */
export async function postWarehouseInboundPrintLabels(body: {
id: number
customerId?: number
}): Promise<{ printUrl: string; noticeNo: string; pageCount: number }> {
const data = await postOk('/warehouse/inbound/print-labels', {
id: body.id,
customerId: body.customerId,
})
const d = (data ?? {}) as Record<string, unknown>
return {
printUrl: String(d.printUrl ?? d.PrintUrl ?? '').trim(),
noticeNo: String(d.noticeNo ?? d.NoticeNo ?? '').trim(),
pageCount: Number(d.pageCount ?? d.PageCount ?? 0) || 0,
}
}
// Parcel
export const getWarehouseParcelList = (body: Record<string, unknown>) => postList('/warehouse/parcel/list', body)
export const getWarehouseParcelSummary = (params?: Record<string, unknown>) =>

View File

@ -96,6 +96,7 @@ export const PERMISSION_ACTION_LABELS: Record<string, string> = {
PackVersions: '打包版本',
BinDetail: '库位明细',
History: '出入库记录',
PrintLabel: '打印箱唛',
BatchPay: '批量支付',
CreatePickup: '创建揽收',
AddMapping: '添加映射',
@ -644,6 +645,7 @@ const CORE_GROUPS: PageButtonGroup[] = [
{ code: 'Query', name: '查询' },
{ code: 'Save', name: '新增' },
{ code: 'Update', name: '编辑' },
{ code: 'PrintLabel', name: '打印标签' },
{ code: 'Delete', name: '删除' },
]),
B('Warehouse.Inventory', [
@ -663,6 +665,7 @@ const CORE_GROUPS: PageButtonGroup[] = [
{ code: 'Save', name: '新增' },
{ code: 'View', name: '详情' },
{ code: 'ConfirmReceive', name: '确认收货' },
{ code: 'PrintLabel', name: '打印箱唛' },
{ code: 'Delete', name: '删除' },
]),
B('Warehouse.BinLocation', [

View File

@ -213,6 +213,7 @@ export default {
'warehouse.sku.modal.add': 'Add SKU',
'warehouse.sku.modal.edit': 'Edit SKU',
'warehouse.sku.actions.delete': 'Delete SKU',
'warehouse.sku.actions.printLabel': 'Print Label',
'warehouse.sku.message.loadFailed': 'Failed to load SKU list',
'warehouse.sku.message.saveSuccess': 'Saved',
'warehouse.sku.message.saveFailed': 'Save failed',
@ -220,6 +221,8 @@ export default {
'warehouse.sku.message.deleteFailed': 'Delete failed',
'warehouse.sku.message.deleteConfirm': 'Delete {code}?',
'warehouse.sku.message.required': 'SKU code and name are required',
'warehouse.sku.message.printFailed': 'Failed to print SKU label',
'warehouse.sku.message.printEmpty': 'No SKU label file available',
'warehouse.inventory.table.warehouse': 'Warehouse',
'warehouse.inventory.table.binName': 'Bin Name',
'warehouse.inventory.table.skuCode': 'SKU Code',
@ -409,7 +412,7 @@ export default {
'warehouse.inbound.search.keyword': 'Notice / Mark',
'warehouse.inbound.tip': 'Forecast inbound goods. Stock increases after receive & putaway. Status: Forecasted → Received → Completed.',
'warehouse.inbound.tip.currentWarehouse': 'Notice belongs to the current warehouse; switch it from the top bar',
'warehouse.inbound.tip.markReference': 'Usually the tracking number or master mark for receiving checks',
'warehouse.inbound.tip.markReference': 'Optional; usually the tracking number or master mark for receiving checks',
'warehouse.inbound.tip.inboundType': 'Normal or return inbound; used for later reporting',
'warehouse.inbound.tip.boxSkuDetail': 'Maintain SKUs and planned qty per box; add one box or five at once',
'warehouse.inbound.tip.markBoxNo': 'Per-box mark / box number (optional)',
@ -419,7 +422,7 @@ export default {
'warehouse.inbound.form.markBoxNoPlaceholder': 'Box mark (optional)',
'warehouse.inbound.form.sku': 'SKU',
'warehouse.inbound.form.plannedQty': 'Planned Qty',
'warehouse.inbound.form.markPlaceholder': 'Usually the tracking number',
'warehouse.inbound.form.markPlaceholder': 'Usually the tracking number (optional)',
'warehouse.inbound.form.remarkPlaceholder': 'Optional',
'warehouse.inbound.form.optional': 'Optional',
'warehouse.inbound.form.boxSkuDetail': 'Boxes & SKU lines',
@ -441,6 +444,8 @@ export default {
'warehouse.inbound.actions.removeCurrentBox': 'Remove current box',
'warehouse.inbound.actions.addSku': 'Add SKU line',
'warehouse.inbound.modal.add': 'Add Inbound',
'warehouse.inbound.modal.edit': 'Edit Inbound',
'warehouse.inbound.modal.view': 'View Inbound',
'warehouse.inbound.drawer.detail': 'Inbound Detail',
'warehouse.inbound.box': 'Box',
'warehouse.inbound.line.skuCode': 'SKU',
@ -450,6 +455,9 @@ export default {
'warehouse.inbound.form.selectBin': 'Select bin',
'warehouse.inbound.actions.delete': 'Delete',
'warehouse.inbound.actions.confirmReceive': 'Confirm Receive',
'warehouse.inbound.actions.printLabel': 'Print Box Label',
'warehouse.inbound.message.printFailed': 'Failed to print box label',
'warehouse.inbound.message.printEmpty': 'No box label file available',
'warehouse.inbound.modal.confirmReceive': 'Confirm Receive & Putaway',
'warehouse.inbound.message.loadFailed': 'Failed to load inbound list',
'warehouse.inbound.message.saveSuccess': 'Saved',
@ -2533,6 +2541,8 @@ export default {
'order.myOrders.drawer.editBasicTitle': 'Edit Order Details',
'order.myOrders.drawer.editWeightTitle': 'Edit Weight & Dimensions',
'order.myOrders.drawer.message.saveSuccess': 'Saved successfully',
'order.myOrders.drawer.message.changeProductSuccess': 'Sales product updated',
'order.myOrders.drawer.placeholder.productName': 'Select sales product',
'order.myOrders.drawer.message.recalculateSuccess': 'Recalculated successfully',
'order.myOrders.drawer.costItem': 'Cost Item',
'order.myOrders.drawer.amount': 'Amount',
@ -4248,6 +4258,7 @@ export default {
'operation.printLabel.labelTemplate.inbound': 'Inbound Label',
'operation.printLabel.labelTemplate.outbound': 'Outbound Label',
'operation.printLabel.labelTemplate.sku': 'SKU Label',
'operation.printLabel.labelTemplate.box': 'Box Mark Label',
'operation.printLabel.preview.title': 'Label preview',
'operation.printLabel.preview.empty': 'Scan a number to show order info; open Preview when needed',
'operation.printLabel.preview.product': 'Product',

View File

@ -214,6 +214,7 @@ export default {
'warehouse.sku.modal.add': '新增 SKU',
'warehouse.sku.modal.edit': '编辑 SKU',
'warehouse.sku.actions.delete': '删除 SKU',
'warehouse.sku.actions.printLabel': '打印标签',
'warehouse.sku.message.loadFailed': '加载 SKU 列表失败',
'warehouse.sku.message.saveSuccess': '保存成功',
'warehouse.sku.message.saveFailed': '保存失败',
@ -221,6 +222,8 @@ export default {
'warehouse.sku.message.deleteFailed': '删除失败',
'warehouse.sku.message.deleteConfirm': '确定删除 {code}?',
'warehouse.sku.message.required': '请填写 SKU 编码和名称',
'warehouse.sku.message.printFailed': '打印 SKU 标签失败',
'warehouse.sku.message.printEmpty': '未获取到 SKU 标签文件',
'warehouse.inventory.table.warehouse': '仓库',
'warehouse.inventory.table.binName': '库位名称',
'warehouse.inventory.table.skuCode': 'SKU 编码',
@ -410,7 +413,7 @@ export default {
'warehouse.inbound.search.keyword': '单号/唛头',
'warehouse.inbound.tip': '预报到仓货物;收货上架后库存会增加。关注状态:预报 → 待入库 → 已完成。',
'warehouse.inbound.tip.currentWarehouse': '预报归属当前仓库;可在顶部切换仓库',
'warehouse.inbound.tip.markReference': '一般填物流快递单号或主唛头,便于到仓核对',
'warehouse.inbound.tip.markReference': '选填;一般填物流快递单号或主唛头,便于到仓核对',
'warehouse.inbound.tip.inboundType': '普通入库或退货入库,影响后续统计分类',
'warehouse.inbound.tip.boxSkuDetail': '按箱维护 SKU 与计划数量;可加箱或一次加 5 箱',
'warehouse.inbound.tip.markBoxNo': '单箱唛头/箱号,选填',
@ -420,7 +423,7 @@ export default {
'warehouse.inbound.form.markBoxNoPlaceholder': '箱唛/箱号(选填)',
'warehouse.inbound.form.sku': 'SKU',
'warehouse.inbound.form.plannedQty': '计划数量',
'warehouse.inbound.form.markPlaceholder': '一般填写物流快递单号',
'warehouse.inbound.form.markPlaceholder': '一般填写物流快递单号(选填)',
'warehouse.inbound.form.remarkPlaceholder': '选填',
'warehouse.inbound.form.optional': '选填',
'warehouse.inbound.form.boxSkuDetail': '箱子与 SKU 明细',
@ -442,6 +445,8 @@ export default {
'warehouse.inbound.actions.removeCurrentBox': '删除当前箱',
'warehouse.inbound.actions.addSku': '选择库内 SKU',
'warehouse.inbound.modal.add': '新增入库预报',
'warehouse.inbound.modal.edit': '编辑入库预报',
'warehouse.inbound.modal.view': '查看入库预报',
'warehouse.inbound.drawer.detail': '入库详情',
'warehouse.inbound.box': '箱',
'warehouse.inbound.line.skuCode': 'SKU',
@ -451,6 +456,9 @@ export default {
'warehouse.inbound.form.selectBin': '请选择库位',
'warehouse.inbound.actions.delete': '删除预报',
'warehouse.inbound.actions.confirmReceive': '确认收货',
'warehouse.inbound.actions.printLabel': '打印箱唛',
'warehouse.inbound.message.printFailed': '打印箱唛失败',
'warehouse.inbound.message.printEmpty': '未获取到箱唛文件',
'warehouse.inbound.modal.confirmReceive': '确认收货上架',
'warehouse.inbound.message.loadFailed': '加载入库预报失败',
'warehouse.inbound.message.saveSuccess': '保存成功',
@ -2392,6 +2400,8 @@ export default {
'order.myOrders.drawer.editBasicTitle': '编辑订单详情',
'order.myOrders.drawer.editWeightTitle': '编辑件重尺',
'order.myOrders.drawer.message.saveSuccess': '保存成功',
'order.myOrders.drawer.message.changeProductSuccess': '销售产品已更新',
'order.myOrders.drawer.placeholder.productName': '请选择销售产品',
'order.myOrders.drawer.message.recalculateSuccess': '重算成功',
'order.myOrders.drawer.costItem': '费用项',
'order.myOrders.drawer.amount': '金额',
@ -4273,6 +4283,7 @@ export default {
'operation.printLabel.labelTemplate.inbound': '入库标签',
'operation.printLabel.labelTemplate.outbound': '出库标签',
'operation.printLabel.labelTemplate.sku': 'sku标签',
'operation.printLabel.labelTemplate.box': '箱唛标签',
'operation.printLabel.preview.title': '面单预览',
'operation.printLabel.preview.empty': '扫描单号后显示订单信息,需要时可点预览查看面单',
'operation.printLabel.preview.product': '销售产品',

View File

@ -73,13 +73,27 @@ const EMPTY_LABEL_TEMPLATE = {
const initialTemplate = ref<unknown>(EMPTY_LABEL_TEMPLATE)
const BARCODE_FIELDS = new Set(['serviceNo', 'trackNo', 'clientNo', 'requestId'])
const BARCODE_FIELDS = new Set([
'serviceNo',
'trackNo',
'clientNo',
'requestId',
'noticeNo',
'markReference',
'markBoxNo',
'boxSeqNo',
'barcode',
'skuCode',
])
const LONGTEXT_FIELDS = new Set([
'recipientAddressFull',
'senderAddressFull',
'recipientStreet1',
'recipientStreet2',
'recipientStreet3',
'skuSummary',
'declareElement',
'remark',
])
const MOCK_BY_FIELD: Record<string, string> = {
@ -93,6 +107,40 @@ const MOCK_BY_FIELD: Record<string, string> = {
actualWeight: '1.25',
salesProductName: '专线-普货',
warehouseName: 'SZ-01',
noticeNo: 'IN202610220001',
markReference: 'SF1234567890',
markBoxNo: 'BOX-001',
boxSeqNo: '1',
barcode: 'BOX-001',
boxIndexText: '1/2',
skuSummary: 'SKU001 T-Shirt×2\nSKU002 Pants×1',
skuCode: 'SKU001',
skuName: 'T-Shirt',
skuNameEn: 'T-Shirt',
skuQty: '2',
unitPrice: '9.90',
exHsCode: '610910',
imHsCode: '610910',
declareElement: '棉质 T 恤',
weightG: '200',
lengthCm: '30',
widthCm: '20',
heightCm: '15',
dimensions: '30×20×15',
remark: 'fragile',
printTime: '2026-10-22 18:00:00',
operatorName: 'preview',
pageIndex: '1',
pageCount: '2',
customerCode: 'AF0A1',
customerName: 'Demo Customer',
}
function groupPanelTitle(g: LabelFieldGroup): string {
if (g.code === 'inboundBox') return '入库预报'
if (g.code === 'warehouseSku') return '库内SKU'
if (g.code === 'system') return '系统'
return `订单·${g.title}`
}
let hiprintRegistered = false
@ -183,12 +231,14 @@ function buildFieldElements(groups: LabelFieldGroup[]) {
for (const g of normalizeFieldGroups(groups)) {
const items: any[] = []
const groupKey = g.code || 'default'
for (const f of g.fields) {
const testData = MOCK_BY_FIELD[f.field] || f.title
const isLong = LONGTEXT_FIELDS.has(f.field)
const tidBase = `omsLabelFields.${groupKey}.${f.field}`
items.push({
tid: `omsLabelFields.${f.field}`,
tid: tidBase,
title: f.title,
type: isLong ? 'longText' : 'text',
options: {
@ -204,7 +254,7 @@ function buildFieldElements(groups: LabelFieldGroup[]) {
if (BARCODE_FIELDS.has(f.field)) {
items.push({
tid: `omsLabelFields.${f.field}.barcode`,
tid: `${tidBase}.barcode`,
title: `${f.title}·条码`,
type: 'text',
options: {
@ -218,7 +268,7 @@ function buildFieldElements(groups: LabelFieldGroup[]) {
},
})
items.push({
tid: `omsLabelFields.${f.field}.qrcode`,
tid: `${tidBase}.qrcode`,
title: `${f.title}·二维码`,
type: 'text',
options: {
@ -236,7 +286,7 @@ function buildFieldElements(groups: LabelFieldGroup[]) {
if (items.length) {
if (!hiprint?.PrintElementTypeGroup) continue
typeGroups.push(new hiprint.PrintElementTypeGroup(`订单·${g.title}`, items))
typeGroups.push(new hiprint.PrintElementTypeGroup(groupPanelTitle(g), items))
}
}

View File

@ -200,6 +200,7 @@ const typeOptions = computed(() => [
{ value: 'inbound', label: t('operation.printLabel.labelTemplate.inbound') },
{ value: 'outbound', label: t('operation.printLabel.labelTemplate.outbound') },
{ value: 'sku', label: t('operation.printLabel.labelTemplate.sku') },
{ value: 'box', label: t('operation.printLabel.labelTemplate.box') },
])
function typeLabel(type?: string) {

View File

@ -143,7 +143,23 @@
<div class="detail-field-value">
<template v-if="basicEditing && isOrderFieldEditable(fieldKey)">
<a-select
v-if="fieldKey === 'warehouse'"
v-if="fieldKey === 'productName'"
v-model="basicEditForm.productCode"
allow-search
size="small"
:loading="salesProductOptionsLoading"
:placeholder="$t('order.myOrders.drawer.placeholder.productName')"
>
<a-option
v-for="opt in salesProductOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-option>
</a-select>
<a-select
v-else-if="fieldKey === 'warehouse'"
v-model="basicEditForm.warehouseId"
allow-clear
allow-search
@ -719,12 +735,14 @@ import useDrawer from '@/components/DrawerPro/useDrawer';
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
import {
postOrderUpdate,
postOrderChangeProduct,
postOrderWeightUpdate,
postOrderRecalculateReceivable,
postOrderRecalculatePayable,
getOrderOperationLogList,
type OrderOperationLogItem,
} from '@/api/order';
import { getSalesProductList } from '@/api/channel/salesProduct';
import { getDictListAll, type DictDto } from '@/api/system/dict';
import { getWarehouseList } from '@/api/system/warehouse';
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
@ -893,6 +911,12 @@ const orderEditFieldDefs: FormFieldDef[] = [
/** 入库前(草稿/已预报)才允许客户编辑 */
const MY_ORDERS_EDITABLE_STATUSES = new Set(['draft', 'predeclared']);
const CHANGE_PRODUCT_STATUSES = new Set([
'draft',
'predeclared',
'pendingAllocation',
'pendingPayment',
]);
const orderEditMetaMap = Object.fromEntries(
orderEditFieldDefs.map((def) => [def.key, def])
@ -902,7 +926,16 @@ const SENDER_READONLY_KEYS = new Set(['isDefault']);
const SENDER_FORM_KEY_MAP: Record<string, string> = { country: 'countryCode' };
const RECIPIENT_FORM_KEY_MAP: Record<string, string> = { country: 'countryCode' };
const canChangeProduct = computed(() => {
const o = detail.value as any;
const paid = o?.isPaid ?? o?.IsPaid;
if (paid === true || paid === 1 || paid === '1') return false;
const code = resolveOrderStatusCode(detail.value);
return !!code && CHANGE_PRODUCT_STATUSES.has(code) && MY_ORDERS_EDITABLE_STATUSES.has(code);
});
function isOrderFieldEditable(key: string) {
if (key === 'productName') return canChangeProduct.value;
return key in orderEditMetaMap;
}
@ -936,12 +969,36 @@ const basicEditForm = reactive({
codCurrency: '',
declaredCurrency: '',
declaredSum: undefined as number | undefined,
productCode: '',
});
const initialProductCode = ref('');
const currencyOptions = ref<Array<{ value: string; label: string }>>([]);
const currencyOptionsLoading = ref(false);
const warehouseOptions = ref<Array<{ value: number; label: string }>>([]);
const warehouseOptionsLoading = ref(false);
const salesProductOptions = ref<Array<{ value: string; label: string }>>([]);
const salesProductOptionsLoading = ref(false);
async function ensureSalesProductOptions() {
if (salesProductOptions.value.length > 0) return;
salesProductOptionsLoading.value = true;
try {
const res = await getSalesProductList({ page: 1, pageSize: 500, isActive: 2 });
salesProductOptions.value = (res.items ?? [])
.map((row: any) => {
const code = String(row?.productCode ?? row?.ProductCode ?? '').trim();
const name = String(row?.productName ?? row?.ProductName ?? '').trim();
if (!code) return null;
return { value: code, label: name ? `${name} ${code}` : code };
})
.filter(Boolean) as Array<{ value: string; label: string }>;
} catch {
salesProductOptions.value = [];
} finally {
salesProductOptionsLoading.value = false;
}
}
async function ensureWarehouseOptions(preferred?: {
id?: number;
@ -1988,6 +2045,9 @@ function fillBasicEditForm() {
declaredSumRaw != null && declaredSumRaw !== '' && Number.isFinite(Number(declaredSumRaw))
? Number(declaredSumRaw)
: undefined;
const productCode = String(o.productCode ?? o.ProductCode ?? '').trim();
basicEditForm.productCode = productCode;
initialProductCode.value = productCode;
senderEditForm.senderName = String(sender.value.senderName ?? '');
senderEditForm.company = String(sender.value.company ?? '');
@ -2051,6 +2111,9 @@ function startBasicEdit() {
id: basicEditForm.warehouseId,
name: String(detail.value?.warehouse ?? detail.value?.warehouseName ?? ''),
});
if (canChangeProduct.value) {
void ensureSalesProductOptions();
}
basicEditing.value = true;
void focusFirstEditableField('.order-detail-drawer .basic-info');
}
@ -2082,6 +2145,18 @@ async function handleBasicEditSave() {
if (id == null) return;
basicSaving.value = true;
try {
const nextProductCode = String(basicEditForm.productCode ?? '').trim();
const prevProductCode = String(initialProductCode.value ?? '').trim();
if (
canChangeProduct.value
&& nextProductCode
&& nextProductCode.toLowerCase() !== prevProductCode.toLowerCase()
) {
const changed = await postOrderChangeProduct({ id, productCode: nextProductCode });
applyDetailFromApi(changed);
initialProductCode.value = nextProductCode;
}
const result = await postOrderUpdate({
id,
clientNo: basicEditForm.clientNo,

View File

@ -134,7 +134,23 @@
<div class="detail-field-value">
<template v-if="basicEditing && isOrderFieldEditable(fieldKey)">
<a-select
v-if="fieldKey === 'warehouse'"
v-if="fieldKey === 'productName'"
v-model="basicEditForm.productCode"
allow-search
size="small"
:loading="salesProductOptionsLoading"
:placeholder="$t('order.myOrders.drawer.placeholder.productName')"
>
<a-option
v-for="opt in salesProductOptions"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</a-option>
</a-select>
<a-select
v-else-if="fieldKey === 'warehouse'"
v-model="basicEditForm.warehouseId"
allow-clear
allow-search
@ -994,6 +1010,7 @@ import useDrawer from '@/components/DrawerPro/useDrawer';
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
import {
postOrderUpdate,
postOrderChangeProduct,
postOrderWeightUpdate,
postOrderRecalculateReceivable,
postOrderRecalculatePayable,
@ -1158,7 +1175,21 @@ const SENDER_READONLY_KEYS = new Set(['isDefault']);
const SENDER_FORM_KEY_MAP: Record<string, string> = { country: 'countryCode' };
const RECIPIENT_FORM_KEY_MAP: Record<string, string> = { country: 'countryCode' };
const CHANGE_PRODUCT_STATUSES = new Set([
'draft',
'predeclared',
'pendingAllocation',
'pendingPayment',
]);
const canChangeProduct = computed(() => {
if (!isOrderUnpaid.value) return false;
const code = resolveOrderStatusCode(detail.value);
return !!code && CHANGE_PRODUCT_STATUSES.has(code);
});
function isOrderFieldEditable(key: string) {
if (key === 'productName') return canChangeProduct.value;
return key in orderEditMetaMap;
}
@ -1193,12 +1224,36 @@ const basicEditForm = reactive({
codCurrency: '',
declaredCurrency: '',
declaredSum: undefined as number | undefined,
productCode: '',
});
const initialProductCode = ref('');
const currencyOptions = ref<Array<{ value: string; label: string }>>([]);
const currencyOptionsLoading = ref(false);
const warehouseOptions = ref<Array<{ value: number; label: string }>>([]);
const warehouseOptionsLoading = ref(false);
const salesProductOptions = ref<Array<{ value: string; label: string }>>([]);
const salesProductOptionsLoading = ref(false);
async function ensureSalesProductOptions() {
if (salesProductOptions.value.length > 0) return;
salesProductOptionsLoading.value = true;
try {
const res = await getSalesProductList({ page: 1, pageSize: 500, isActive: 2 });
salesProductOptions.value = (res.items ?? [])
.map((row: any) => {
const code = String(row?.productCode ?? row?.ProductCode ?? '').trim();
const name = String(row?.productName ?? row?.ProductName ?? '').trim();
if (!code) return null;
return { value: code, label: name ? `${name} ${code}` : code };
})
.filter(Boolean) as Array<{ value: string; label: string }>;
} catch {
salesProductOptions.value = [];
} finally {
salesProductOptionsLoading.value = false;
}
}
async function ensureWarehouseOptions(preferred?: {
id?: number;
@ -2481,6 +2536,9 @@ function fillBasicEditForm() {
declaredSumRaw != null && declaredSumRaw !== '' && Number.isFinite(Number(declaredSumRaw))
? Number(declaredSumRaw)
: undefined;
const productCode = String(o.productCode ?? o.ProductCode ?? '').trim();
basicEditForm.productCode = productCode;
initialProductCode.value = productCode;
senderEditForm.senderName = String(sender.value.senderName ?? '');
senderEditForm.company = String(sender.value.company ?? '');
@ -2540,6 +2598,9 @@ function startBasicEdit() {
id: basicEditForm.warehouseId,
name: String(detail.value?.warehouse ?? detail.value?.warehouseName ?? ''),
});
if (canChangeProduct.value) {
void ensureSalesProductOptions();
}
basicEditing.value = true;
void focusFirstEditableField('.order-detail-drawer .basic-info');
}
@ -2572,6 +2633,18 @@ async function handleBasicEditSave() {
if (id == null) return;
basicSaving.value = true;
try {
const nextProductCode = String(basicEditForm.productCode ?? '').trim();
const prevProductCode = String(initialProductCode.value ?? '').trim();
if (
canChangeProduct.value
&& nextProductCode
&& nextProductCode.toLowerCase() !== prevProductCode.toLowerCase()
) {
const changed = await postOrderChangeProduct({ id, productCode: nextProductCode });
applyDetailFromApi(changed);
initialProductCode.value = nextProductCode;
}
const result = await postOrderUpdate({
id,
clientNo: basicEditForm.clientNo,

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { Modal } from '@arco-design/web-vue'
import { IconDelete, IconPlus, IconQuestionCircle } from '@arco-design/web-vue/es/icon'
@ -16,11 +16,13 @@ import {
getWarehouseInboundList,
getWarehouseInboundSummary,
getWarehouseSkuList,
postWarehouseInboundPrintLabels,
saveWarehouseInbound,
} from '@/api/warehouse'
import { getWarehouseList } from '@/api/system/warehouse'
import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission'
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState'
import {
buildWarehouseNameMap,
formatDateTime,
@ -35,6 +37,10 @@ import { useCurrentWarehouseStore } from '@/store'
defineOptions({ name: 'WarehouseInboundNotice' })
const OrderPrintModal = defineAsyncComponent(
() => import('@/components/PrintTemplate/OrderPrintModal.vue'),
)
interface InboundLineForm {
skuId?: number
skuCode: string
@ -78,6 +84,10 @@ const {
resolveSelectedCustomerId,
} = useWarehouseAdminCustomer()
const currentWarehouseStore = useCurrentWarehouseStore()
const printConnectionState = useHiprintClientState()
const printVisible = ref(false)
const printItems = ref<Array<{ id: number | string; label: string; url: string }>>([])
const printPreparing = ref(false)
/** Tab active-key;全部用 'all',请求时再转成后端 receiptStatus */
const STATUS_TAB_ALL = 'all'
@ -111,11 +121,10 @@ const slidingStatusTabs = computed(() =>
})),
)
const detailVisible = ref(false)
const detailData = ref<any>(null)
const [registerFormModal, { openModal, closeModal }] = useModal() as any
const [registerReceiveModal, { openModal: openReceiveModal, closeModal: closeReceiveModal }] = useModal() as any
/** 已完成等不可改状态:同一弹窗只读查看 */
const formReadonly = ref(false)
const saving = ref(false)
const receiveSaving = ref(false)
const receiveNotice = ref<any>(null)
@ -145,11 +154,49 @@ const boxTabItems = computed(() =>
const canRemoveBox = computed(() => boxRows.value.length > 1)
const formModalTitle = computed(() => {
if (!formModel.id) return t('warehouse.inbound.modal.add')
if (formReadonly.value) return t('warehouse.inbound.modal.view')
return t('warehouse.inbound.modal.edit')
})
const formInboundTypeLabel = computed(() =>
t(`warehouse.inbound.type.${formModel.inboundType}`, formModel.inboundType || '-'),
)
const modalWarehouseName = computed(() => {
const wh = warehouseOptions.value.find((w) => w.value === formModel.warehouseId)
return wh?.label || ''
})
function displayOrDash(value: unknown) {
const text = String(value ?? '').trim()
return text || '-'
}
function formatBoxWeight(box: InboundBoxForm) {
if (box.weightG == null || Number.isNaN(Number(box.weightG))) return '-'
return `${box.weightG} g`
}
function formatBoxSize(box: InboundBoxForm) {
const l = box.lengthCm
const w = box.widthCm
const h = box.heightCm
if (l == null && w == null && h == null) return '-'
return `${l ?? '-'} × ${w ?? '-'} × ${h ?? '-'} cm`
}
function resolveSkuLabel(line: InboundLineForm) {
if (!line.skuId) return '-'
const opt = skuOptions.value.find((s) => s.value === line.skuId)
return opt?.label || line.skuCode || `#${line.skuId}`
}
function viewSkuLines(box: InboundBoxForm) {
return box.lines.filter((l) => l.skuId)
}
const receiveWarehouseName = computed(() => {
const whId = receiveNotice.value?.warehouseId
return resolveWarehouseLabel(whId, warehouseNameMap.value)
@ -261,6 +308,7 @@ function onSkuChange(line: InboundLineForm, skuId?: number) {
watch(
boxRows,
(boxes) => {
if (formReadonly.value) return
for (const box of boxes) {
const last = box.lines[box.lines.length - 1]
if (last?.skuId) {
@ -374,18 +422,6 @@ function handleSearch() {
loadList()
}
async function showDetail(row: any) {
try {
detailData.value = await getWarehouseInboundDetail(
row._raw?.id || row.id,
isAdmin.value ? resolveSelectedCustomerId() : undefined,
)
detailVisible.value = true
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.detailFailed'), 'error')
}
}
async function loadReceiveBinOptions(whId?: number) {
if (!whId) {
receiveBinOptions.value = []
@ -496,9 +532,14 @@ async function submitConfirmReceive() {
}
}
function canEditInbound(row: any) {
return row?._status === 'forecasted' || row?._status === 'received'
}
function handleAdd() {
if (!hasPermission('Warehouse.InboundNotice.Save')) return
if (isAdmin.value && ensureMutationCustomerId() == null) return
formReadonly.value = false
formModel.id = 0
formModel.warehouseId = currentWarehouseStore.resolveDefaultWarehouseId(warehouseId.value)
formModel.markReference = ''
@ -512,7 +553,82 @@ function handleAdd() {
openModal(true)
}
/** 查看/编辑融合:同一弹窗;预报/待入库可改,已完成只读 */
async function openInboundForm(row: any) {
const editable =
hasPermission('Warehouse.InboundNotice.Save') && canEditInbound(row)
if (editable && isAdmin.value && ensureMutationCustomerId() == null) return
try {
const detail = await getWarehouseInboundDetail(
row._raw?.id || row.id,
isAdmin.value ? resolveSelectedCustomerId() : undefined,
)
const normalized = normalizeListRow(detail || {})
formReadonly.value = !editable
formModel.id = Number(pickField(normalized, 'id', 'Id') || 0)
formModel.warehouseId = pickField<number>(normalized, 'warehouseId', 'WarehouseId')
formModel.markReference = String(pickField(normalized, 'markReference', 'MarkReference') ?? '')
formModel.inboundType = String(pickField(normalized, 'inboundType', 'InboundType') ?? 'normal')
formModel.remark = String(pickField(normalized, 'remark', 'Remark') ?? '')
const boxesRaw = pickField(normalized, 'boxes', 'Boxes')
const boxes = Array.isArray(boxesRaw) ? boxesRaw : []
if (!boxes.length) {
if (editable) resetBoxes()
else {
boxRows.value = []
activeBoxKey.value = ''
}
} else {
boxRows.value = boxes.map((box: any, index: number) => {
const b = normalizeListRow(box)
const linesRaw = pickField(b, 'lines', 'Lines')
const lines = Array.isArray(linesRaw) ? linesRaw : []
const mappedLines: InboundLineForm[] = lines.map((line: any) => {
const l = normalizeListRow(line)
return {
skuId: Number(pickField(l, 'skuId', 'SkuId') || 0) || undefined,
skuCode: String(pickField(l, 'skuCode', 'SkuCode') ?? ''),
plannedQty: Number(pickField(l, 'plannedQty', 'PlannedQty') ?? 1) || 1,
}
})
if (editable && (!mappedLines.length || mappedLines.every((l) => l.skuId))) {
mappedLines.push({ skuId: undefined, skuCode: '', plannedQty: 1 })
}
return {
key: `box-open-${pickField(b, 'id', 'Id') || index}-${Date.now()}`,
boxSeqNo: String(pickField(b, 'boxSeqNo', 'BoxSeqNo') ?? index + 1),
markBoxNo: String(pickField(b, 'markBoxNo', 'MarkBoxNo') ?? ''),
weightG: pickField<number>(b, 'weightG', 'WeightG') ?? undefined,
lengthCm: pickField<number>(b, 'lengthCm', 'LengthCm') ?? undefined,
widthCm: pickField<number>(b, 'widthCm', 'WidthCm') ?? undefined,
heightCm: pickField<number>(b, 'heightCm', 'HeightCm') ?? undefined,
lines: mappedLines.length
? mappedLines
: [{ skuId: undefined, skuCode: '', plannedQty: 1 }],
}
})
selectedBoxKeys.value = []
activeBoxKey.value = boxRows.value[0]?.key || ''
}
if (!formModel.warehouseId) {
$baseMessage(t('warehouse.common.noCurrentWarehouse'), 'warning')
return
}
openModal(true)
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.detailFailed'), 'error')
}
}
function closeFormModal() {
formReadonly.value = false
closeModal()
}
async function handleSave() {
if (formReadonly.value) return
if (!formModel.warehouseId) {
$baseMessage(t('warehouse.inbound.message.required'), 'warning')
return
@ -552,7 +668,7 @@ async function handleSave() {
boxes,
}))
$baseMessage(t('warehouse.inbound.message.saveSuccess'), 'success')
closeModal()
closeFormModal()
await loadSummary()
await loadList()
} catch (e: any) {
@ -577,8 +693,62 @@ function handleDelete(row: any) {
})
}
function isUsablePrintUrl(raw: unknown) {
const value = String(raw ?? '').trim()
if (!value) return false
if (value.includes('获取中')) return false
return true
}
async function handlePrintLabel(row: any) {
if (!hasPermission('Warehouse.InboundNotice.PrintLabel')) return
const id = Number(row?._raw?.id || row?.id || 0)
if (!id) return
printVisible.value = false
printItems.value = []
printPreparing.value = true
try {
const customerId = isAdmin.value ? resolveSelectedCustomerId() ?? undefined : undefined
const { printUrl, noticeNo, pageCount } = await postWarehouseInboundPrintLabels({
id,
customerId,
})
if (!isUsablePrintUrl(printUrl)) {
$baseMessage(t('warehouse.inbound.message.printEmpty'), 'warning')
return
}
const label = noticeNo || row.noticeNo || String(id)
printItems.value = [
{
id,
label: pageCount > 1 ? `${label}(${pageCount}页)` : label,
url: printUrl,
},
]
printVisible.value = true
if (!printConnectionState.enabled || !printConnectionState.connected) {
$baseMessage('已打开箱唛预览;静默打印需先在顶部连接打印开关', 'info')
}
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.printFailed'), 'error')
} finally {
printPreparing.value = false
}
}
function actionRenderer(row: any) {
const parts = [renderWarehouseActionBtn('detail', t('warehouse.common.detail'), 'view')]
const canEdit = hasPermission('Warehouse.InboundNotice.Save') && canEditInbound(row)
const parts = [
canEdit
? renderWarehouseActionBtn('open', t('warehouse.common.edit'), 'edit')
: renderWarehouseActionBtn('open', t('warehouse.common.detail'), 'view'),
]
if (hasPermission('Warehouse.InboundNotice.PrintLabel')) {
parts.push(
renderWarehouseActionBtn('printLabel', t('warehouse.inbound.actions.printLabel'), 'print'),
)
}
if (
(hasPermission('Warehouse.InboundNotice.ConfirmReceive')
|| hasPermission('Warehouse.InboundNotice.Save'))
@ -593,7 +763,8 @@ function actionRenderer(row: any) {
}
function handleActionClick(row: any, _i: number, action?: string) {
if (action === 'detail') showDetail(row)
if (action === 'open') void openInboundForm(row)
if (action === 'printLabel') void handlePrintLabel(row)
if (action === 'receive') openConfirmReceive(row)
if (action === 'delete') handleDelete(row)
}
@ -676,22 +847,13 @@ onMounted(async () => {
</div>
</div>
<a-drawer v-model:visible="detailVisible" :title="t('warehouse.inbound.drawer.detail')" :width="640">
<template v-if="detailData">
<p>{{ t('warehouse.inbound.table.noticeNo') }}: {{ detailData.noticeNo || detailData.NoticeNo }}</p>
<p>{{ t('warehouse.inbound.table.markReference') }}: {{ detailData.markReference || detailData.MarkReference || '-' }}</p>
<div v-for="box in (detailData.boxes || detailData.Boxes || [])" :key="box.id || box.Id" class="box-block">
<div class="box-title">{{ t('warehouse.inbound.box') }}: {{ box.markBoxNo || box.MarkBoxNo || box.boxSeqNo || box.BoxSeqNo }}</div>
<a-table :data="box.lines || box.Lines || []" :pagination="false" size="small">
<template #columns>
<a-table-column :title="t('warehouse.inbound.line.skuCode')" data-index="skuCode" />
<a-table-column :title="t('warehouse.inbound.line.plannedQty')" data-index="plannedQty" />
<a-table-column :title="t('warehouse.inbound.line.receivedQty')" data-index="receivedQty" />
</template>
</a-table>
</div>
</template>
</a-drawer>
<OrderPrintModal
v-if="printVisible"
v-model:visible="printVisible"
mode="single"
:items="printItems"
:preparing="printPreparing"
/>
<ModalPro
:title="t('warehouse.inbound.modal.confirmReceive')"
@ -754,11 +916,11 @@ onMounted(async () => {
</ModalPro>
<ModalPro
:title="t('warehouse.inbound.modal.add')"
:title="formModalTitle"
:width="860"
:mask-closable="false"
@register="registerFormModal"
@cancel="closeModal"
@cancel="closeFormModal"
>
<div class="wh-form-modal">
<div class="wh-form-info-bar">
@ -770,17 +932,33 @@ onMounted(async () => {
|| formModel.warehouseId
|| '-'
}}
<a-tooltip :content="t('warehouse.inbound.tip.currentWarehouse')">
<a-tooltip v-if="!formReadonly" :content="t('warehouse.inbound.tip.currentWarehouse')">
<icon-question-circle class="tip-icon" />
</a-tooltip>
</span>
</div>
<div class="form-section-title">{{ t('warehouse.modal.section.basic') }}</div>
<a-form :model="formModel" layout="vertical" class="inbound-form-basic edit-form">
<div v-if="formReadonly" class="inbound-view-basic">
<div class="inbound-view-row">
<div class="inbound-view-field inbound-view-field--grow">
<div class="inbound-view-label">{{ t('warehouse.inbound.form.markReference') }}</div>
<div class="wh-form-readonly">{{ displayOrDash(formModel.markReference) }}</div>
</div>
<div class="inbound-view-field">
<div class="inbound-view-label">{{ t('warehouse.inbound.table.inboundType') }}</div>
<div class="wh-form-readonly">{{ formInboundTypeLabel }}</div>
</div>
</div>
<div class="inbound-view-field">
<div class="inbound-view-label">{{ t('warehouse.parcel.form.remark') }}</div>
<div class="wh-form-readonly">{{ displayOrDash(formModel.remark) }}</div>
</div>
</div>
<a-form v-else :model="formModel" layout="vertical" class="inbound-form-basic edit-form">
<a-row :gutter="16">
<a-col :span="14">
<a-form-item>
<a-form-item :required="false">
<template #label>
<span class="label-with-tip">
{{ t('warehouse.inbound.form.markReference') }}
@ -837,7 +1015,7 @@ onMounted(async () => {
<icon-question-circle class="tip-icon" />
</a-tooltip>
</span>
<a-space :size="8">
<a-space v-if="!formReadonly" :size="8">
<a-button size="mini" @click="addBox">
<template #icon><IconPlus :size="12" /></template>
{{ t('warehouse.inbound.actions.addBox') }}
@ -866,7 +1044,21 @@ onMounted(async () => {
:key="box.key"
class="wh-form-box-panel inbound-box-panel"
>
<div class="inbound-box-meta">
<div v-if="formReadonly" class="inbound-box-meta inbound-box-meta--view">
<div class="inbound-box-meta__field inbound-box-meta__field--mark">
<span class="inbound-box-meta__label">{{ t('warehouse.inbound.form.markBoxNo') }}</span>
<div class="wh-form-readonly">{{ displayOrDash(box.markBoxNo) }}</div>
</div>
<div class="inbound-box-meta__field inbound-box-meta__field--weight">
<span class="inbound-box-meta__label">{{ t('warehouse.inbound.form.boxWeight') }}</span>
<div class="wh-form-readonly">{{ formatBoxWeight(box) }}</div>
</div>
<div class="inbound-box-meta__field inbound-box-meta__field--size">
<span class="inbound-box-meta__label">{{ t('warehouse.inbound.form.boxSize') }}</span>
<div class="wh-form-readonly">{{ formatBoxSize(box) }}</div>
</div>
</div>
<div v-else class="inbound-box-meta">
<div class="inbound-box-meta__field inbound-box-meta__field--mark">
<span class="inbound-box-meta__label label-with-tip">
{{ t('warehouse.inbound.form.markBoxNo') }}
@ -925,7 +1117,7 @@ onMounted(async () => {
</div>
</div>
<div class="inbound-sku-head">
<div v-if="!formReadonly" class="inbound-sku-head">
<span class="wh-form-hint label-with-tip">
{{ t('warehouse.inbound.form.autoAddSkuRow') }}
<a-tooltip :content="t('warehouse.inbound.tip.skuLines')">
@ -935,6 +1127,32 @@ onMounted(async () => {
</div>
<a-table
v-if="formReadonly"
class="inbound-sku-table"
:data="viewSkuLines(box)"
:pagination="false"
size="small"
:bordered="{ cell: true }"
:scroll="{ x: '100%', y: 240 }"
>
<template #columns>
<a-table-column :title="'#'" :width="44">
<template #cell="{ rowIndex }">{{ rowIndex + 1 }}</template>
</a-table-column>
<a-table-column :title="t('warehouse.inbound.form.sku')" :min-width="220">
<template #cell="{ record }">
<span class="wh-form-readonly">{{ resolveSkuLabel(record) }}</span>
</template>
</a-table-column>
<a-table-column :title="t('warehouse.inbound.line.plannedQty')" :width="120">
<template #cell="{ record }">
<span class="wh-form-readonly">{{ record.plannedQty }}</span>
</template>
</a-table-column>
</template>
</a-table>
<a-table
v-else
class="inbound-sku-table"
:data="box.lines"
:pagination="false"
@ -991,8 +1209,15 @@ onMounted(async () => {
<template #footer>
<a-space :size="8">
<a-button @click="closeModal">{{ t('warehouse.common.cancel') }}</a-button>
<a-button type="primary" :loading="saving" @click="handleSave">{{ t('warehouse.common.save') }}</a-button>
<a-button @click="closeFormModal">{{ t('warehouse.common.cancel') }}</a-button>
<a-button
v-if="!formReadonly"
type="primary"
:loading="saving"
@click="handleSave"
>
{{ t('warehouse.common.save') }}
</a-button>
</a-space>
</template>
</ModalPro>
@ -1003,16 +1228,40 @@ onMounted(async () => {
@import '@/styles/warehouse-list-page.less';
@import '@/styles/warehouse-form-modal.less';
.box-block {
margin-bottom: 16px;
}
.box-title {
font-weight: 500;
margin-bottom: 8px;
}
.wh-form-box-tabs {
margin-bottom: 12px;
}
.inbound-view-basic {
margin-bottom: 8px;
}
.inbound-view-row {
display: flex;
gap: 24px;
margin-bottom: 12px;
}
.inbound-view-field {
min-width: 140px;
&--grow {
flex: 1;
min-width: 0;
}
}
.inbound-view-label {
margin-bottom: 4px;
font-size: 13px;
color: var(--color-text-3);
}
.inbound-box-meta--view {
.inbound-box-meta__label {
margin-bottom: 4px;
}
}
.receive-box-block {
margin-bottom: 20px;
}

View File

@ -69,7 +69,7 @@ const historyLoading = ref(false)
const historyTab = ref<'all' | 'inbound' | 'deduct'>('all')
const historyRows = ref<any[]>([])
const historyPage = ref(1)
const historypageSize = ref(50)
const historyPageSize = ref(50)
const historyTotal = ref(0)
const [registerRelocateModal, { openModal: openRelocate, closeModal: closeRelocate }] = useModal() as any

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, defineAsyncComponent, onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { Modal } from '@arco-design/web-vue'
import { IconDelete, IconImage, IconPlus, IconQuestionCircle } from '@arco-design/web-vue/es/icon'
@ -11,6 +11,7 @@ import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import {
deleteWarehouseSku,
getWarehouseSkuList,
postWarehouseSkuPrintLabel,
saveWarehouseSku,
uploadWarehouseSkuImage,
} from '@/api/warehouse'
@ -19,6 +20,7 @@ import { resolveOmsAssetUrl } from '@/api/oms/common'
import { streamFileBlob } from '@/api/fileStorage'
import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission'
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState'
import {
formatDateTime,
formatDimensions,
@ -35,6 +37,10 @@ import {
defineOptions({ name: 'WarehouseSku' })
const OrderPrintModal = defineAsyncComponent(
() => import('@/components/PrintTemplate/OrderPrintModal.vue'),
)
const { t } = useI18n()
const { tableContainerRef, tableHeight } = useTableContainerHeight()
const {
@ -43,8 +49,13 @@ const {
customerOptions,
withCustomerId,
ensureMutationCustomerId,
resolveSelectedCustomerId,
} = useWarehouseAdminCustomer()
const currentWarehouseStore = useCurrentWarehouseStore()
const printConnectionState = useHiprintClientState()
const printVisible = ref(false)
const printItems = ref<Array<{ id: number | string; label: string; url: string }>>([])
const printPreparing = ref(false)
const listLoading = ref(false)
const tableRows = ref<any[]>([])
@ -428,8 +439,56 @@ function handleDelete(row: any) {
})
}
function isUsablePrintUrl(raw: unknown) {
const value = String(raw ?? '').trim()
if (!value) return false
if (value.includes('获取中')) return false
return true
}
async function handlePrintLabel(row: any) {
if (!hasPermission('Warehouse.Sku.PrintLabel')) return
const id = Number(row?.id || 0)
if (!id) return
printVisible.value = false
printItems.value = []
printPreparing.value = true
try {
const adminCustomerId = isAdmin.value ? resolveSelectedCustomerId() ?? undefined : undefined
const { printUrl, skuCode } = await postWarehouseSkuPrintLabel({
id,
customerId: adminCustomerId,
})
if (!isUsablePrintUrl(printUrl)) {
$baseMessage(t('warehouse.sku.message.printEmpty'), 'warning')
return
}
printItems.value = [
{
id,
label: skuCode || row.skuCode || String(id),
url: printUrl,
},
]
printVisible.value = true
if (!printConnectionState.enabled || !printConnectionState.connected) {
$baseMessage('已打开 SKU 标签预览;静默打印需先在顶部连接打印开关', 'info')
}
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.sku.message.printFailed'), 'error')
} finally {
printPreparing.value = false
}
}
function actionRenderer() {
const parts: string[] = []
if (hasPermission('Warehouse.Sku.PrintLabel')) {
parts.push(
renderWarehouseActionBtn('printLabel', t('warehouse.sku.actions.printLabel'), 'print'),
)
}
if (hasPermission('Warehouse.Sku.Save')) {
parts.push(renderWarehouseActionBtn('edit', t('warehouse.common.edit')))
}
@ -440,6 +499,7 @@ function actionRenderer() {
}
function handleActionClick(row: any, _idx: number, action?: string) {
if (action === 'printLabel') void handlePrintLabel(row)
if (action === 'edit') handleEdit(row)
if (action === 'delete') handleDelete(row)
}
@ -514,6 +574,14 @@ onMounted(async () => {
</div>
</div>
<OrderPrintModal
v-if="printVisible"
v-model:visible="printVisible"
mode="single"
:items="printItems"
:preparing="printPreparing"
/>
<ModalPro
:title="modalTitle"
:width="720"