优化仓储与店铺列表页,并补齐 warehouse 代理与列宽自适应。

管理端可正常查看列表;分页移至工具栏右侧,列定义对齐 WMS,Handsontable 开启 stretch-columns。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-11 16:14:44 +08:00
parent 5191d1981f
commit 30afc1728c
13 changed files with 891 additions and 268 deletions

View File

@ -119,6 +119,27 @@ server {
location ^~ /order/templateMapping { location ^~ /order/templateMapping {
try_files /index.html =200; try_files /index.html =200;
} }
location ^~ /order/platformStore {
try_files /index.html =200;
}
location ^~ /order/platformStoreOrder {
try_files /index.html =200;
}
# ============ 仓储前端路由(^~ 须比下方 /warehouse/ API 正则代理更优先)============
# 页面路径与 API 前缀相同者用精确匹配,避免 /warehouse/sku/list 等接口被误判为 SPA
location = /warehouse/parcel {
try_files /index.html =200;
}
location = /warehouse/inventory {
try_files /index.html =200;
}
location = /warehouse/sku {
try_files /index.html =200;
}
location ^~ /warehouse/inboundNotice {
try_files /index.html =200;
}
# ============ 系统管理前端路由(^~ 须比下方 /system/ API 正则代理更优先)============ # ============ 系统管理前端路由(^~ 须比下方 /system/ API 正则代理更优先)============
# 页面路径为 camelCase;API 为 kebab(如 /system/dict/list、/system/batch-email/…),勿用宽泛前缀匹配 dict # 页面路径为 camelCase;API 为 kebab(如 /system/dict/list、/system/batch-email/…),勿用宽泛前缀匹配 dict
@ -198,7 +219,7 @@ server {
} }
# ============ Admin API 同源反代(/rbac /order /xhs … → api.nanafa.com/admin)============ # ============ Admin API 同源反代(/rbac /order /xhs … → api.nanafa.com/admin)============
location ~ ^/(rbac|order|monitor|customer|recharge|channel|system)/ { location ~ ^/(rbac|order|monitor|customer|recharge|channel|system|warehouse)/ {
rewrite ^/(.*)$ /admin/$1 break; rewrite ^/(.*)$ /admin/$1 break;
proxy_pass http://api.nanafa.com; proxy_pass http://api.nanafa.com;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@ -46,6 +46,13 @@ server {
location ^~ /order/myOrders { try_files /index.html =200; } location ^~ /order/myOrders { try_files /index.html =200; }
location ^~ /order/batchJobs { try_files /index.html =200; } location ^~ /order/batchJobs { try_files /index.html =200; }
location ^~ /order/templateMapping { try_files /index.html =200; } location ^~ /order/templateMapping { try_files /index.html =200; }
location ^~ /order/platformStore { try_files /index.html =200; }
location ^~ /order/platformStoreOrder { try_files /index.html =200; }
location = /warehouse/parcel { try_files /index.html =200; }
location = /warehouse/inventory { try_files /index.html =200; }
location = /warehouse/sku { try_files /index.html =200; }
location ^~ /warehouse/inboundNotice { try_files /index.html =200; }
location ^~ /channel/salesProducts { try_files /index.html =200; } location ^~ /channel/salesProducts { try_files /index.html =200; }
location ^~ /channel/serviceChannels { try_files /index.html =200; } location ^~ /channel/serviceChannels { try_files /index.html =200; }
@ -101,7 +108,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location ~ ^/(rbac|order|monitor|customer|recharge|channel|system)/ { location ~ ^/(rbac|order|monitor|customer|recharge|channel|system|warehouse)/ {
rewrite ^/(.*)$ /admin/$1 break; rewrite ^/(.*)$ /admin/$1 break;
proxy_pass http://api.nanafa.com; proxy_pass http://api.nanafa.com;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@ -99,6 +99,10 @@ export default {
'warehouse.common.confirm': 'Confirm', 'warehouse.common.confirm': 'Confirm',
'warehouse.common.detail': 'Detail', 'warehouse.common.detail': 'Detail',
'warehouse.common.warehouse': 'Warehouse', 'warehouse.common.warehouse': 'Warehouse',
'warehouse.common.yes': 'Yes',
'warehouse.common.no': 'No',
'warehouse.sku.table.dimensions': 'Size(cm)',
'warehouse.sku.table.createTime': 'Created At',
'warehouse.sku.table.skuCode': 'SKU Code', 'warehouse.sku.table.skuCode': 'SKU Code',
'warehouse.sku.table.skuName': 'SKU Name', 'warehouse.sku.table.skuName': 'SKU Name',
'warehouse.sku.table.totalQty': 'Qty', 'warehouse.sku.table.totalQty': 'Qty',
@ -119,6 +123,8 @@ export default {
'warehouse.sku.message.deleteFailed': 'Delete failed', 'warehouse.sku.message.deleteFailed': 'Delete failed',
'warehouse.sku.message.deleteConfirm': 'Delete {code}?', 'warehouse.sku.message.deleteConfirm': 'Delete {code}?',
'warehouse.sku.message.required': 'SKU code and name are required', 'warehouse.sku.message.required': 'SKU code and name are required',
'warehouse.inventory.table.warehouse': 'Warehouse',
'warehouse.inventory.table.binName': 'Bin Name',
'warehouse.inventory.table.skuCode': 'SKU Code', 'warehouse.inventory.table.skuCode': 'SKU Code',
'warehouse.inventory.table.skuName': 'SKU Name', 'warehouse.inventory.table.skuName': 'SKU Name',
'warehouse.inventory.table.warehouseId': 'Warehouse', 'warehouse.inventory.table.warehouseId': 'Warehouse',
@ -143,6 +149,10 @@ export default {
'warehouse.parcel.status.inTransit': 'In Transit', 'warehouse.parcel.status.inTransit': 'In Transit',
'warehouse.parcel.status.arrived': 'Arrived', 'warehouse.parcel.status.arrived': 'Arrived',
'warehouse.parcel.status.stored': 'Stored', 'warehouse.parcel.status.stored': 'Stored',
'warehouse.parcel.table.warehouse': 'Warehouse',
'warehouse.parcel.table.items': 'Items',
'warehouse.parcel.table.arrivalTime': 'Arrival Time',
'warehouse.parcel.table.createTime': 'Created At',
'warehouse.parcel.table.trackingNo': 'Tracking No', 'warehouse.parcel.table.trackingNo': 'Tracking No',
'warehouse.parcel.table.carrierName': 'Carrier', 'warehouse.parcel.table.carrierName': 'Carrier',
'warehouse.parcel.table.lifecycleStatus': 'Status', 'warehouse.parcel.table.lifecycleStatus': 'Status',
@ -163,6 +173,12 @@ export default {
'warehouse.inbound.status.all': 'All', 'warehouse.inbound.status.all': 'All',
'warehouse.inbound.status.forecasted': 'Forecasted', 'warehouse.inbound.status.forecasted': 'Forecasted',
'warehouse.inbound.status.completed': 'Completed', 'warehouse.inbound.status.completed': 'Completed',
'warehouse.inbound.status.received': 'Received',
'warehouse.inbound.table.warehouse': 'Warehouse',
'warehouse.inbound.table.inboundType': 'Inbound Type',
'warehouse.inbound.table.expectedArrival': 'ETA',
'warehouse.inbound.type.normal': 'Normal',
'warehouse.inbound.type.return': 'Return',
'warehouse.inbound.table.noticeNo': 'Notice No', 'warehouse.inbound.table.noticeNo': 'Notice No',
'warehouse.inbound.table.markReference': 'Mark Ref', 'warehouse.inbound.table.markReference': 'Mark Ref',
'warehouse.inbound.table.receiptStatus': 'Status', 'warehouse.inbound.table.receiptStatus': 'Status',
@ -188,6 +204,14 @@ export default {
'warehouse.inbound.message.required': 'Select warehouse and SKU', 'warehouse.inbound.message.required': 'Select warehouse and SKU',
'warehouse.inbound.message.detailFailed': 'Failed to load detail', 'warehouse.inbound.message.detailFailed': 'Failed to load detail',
'order.platformStore.platform.manual': 'Manual', 'order.platformStore.platform.manual': 'Manual',
'order.platformStore.table.siteCountry': 'Site Country',
'order.platformStore.table.autoFulfill': 'Auto Fulfill',
'order.platformStore.table.authTime': 'Auth Time',
'order.platformStore.table.createTime': 'Created At',
'order.platformStore.form.siteCountry': 'Site Country',
'order.platformStore.auth.active': 'Active',
'order.platformStore.auth.expired': 'Expired',
'order.platformStore.auth.revoked': 'Revoked',
'order.platformStore.table.platform': 'Platform', 'order.platformStore.table.platform': 'Platform',
'order.platformStore.table.nickname': 'Nickname', 'order.platformStore.table.nickname': 'Nickname',
'order.platformStore.table.account': 'Account', 'order.platformStore.table.account': 'Account',
@ -214,6 +238,13 @@ export default {
'order.platformStoreOrder.status.pending': 'Pending', 'order.platformStoreOrder.status.pending': 'Pending',
'order.platformStoreOrder.status.toShip': 'To Ship', 'order.platformStoreOrder.status.toShip': 'To Ship',
'order.platformStoreOrder.status.shipped': 'Shipped', 'order.platformStoreOrder.status.shipped': 'Shipped',
'order.platformStoreOrder.status.suspended': 'Suspended',
'order.platformStoreOrder.table.platform': 'Platform',
'order.platformStoreOrder.table.country': 'Country',
'order.platformStoreOrder.table.itemCount': 'Items',
'order.platformStoreOrder.table.shippingLabel': 'Label No',
'order.platformStoreOrder.table.warehouse': 'Warehouse',
'order.platformStoreOrder.table.lastSync': 'Last Sync',
'order.platformStoreOrder.table.orderNo': 'Order No', 'order.platformStoreOrder.table.orderNo': 'Order No',
'order.platformStoreOrder.table.store': 'Store', 'order.platformStoreOrder.table.store': 'Store',
'order.platformStoreOrder.table.status': 'Status', 'order.platformStoreOrder.table.status': 'Status',

View File

@ -99,6 +99,10 @@ export default {
'warehouse.common.confirm': '确定', 'warehouse.common.confirm': '确定',
'warehouse.common.detail': '详情', 'warehouse.common.detail': '详情',
'warehouse.common.warehouse': '仓库', 'warehouse.common.warehouse': '仓库',
'warehouse.common.yes': '是',
'warehouse.common.no': '否',
'warehouse.sku.table.dimensions': '尺寸(cm)',
'warehouse.sku.table.createTime': '创建时间',
'warehouse.sku.table.skuCode': 'SKU 编码', 'warehouse.sku.table.skuCode': 'SKU 编码',
'warehouse.sku.table.skuName': 'SKU 名称', 'warehouse.sku.table.skuName': 'SKU 名称',
'warehouse.sku.table.totalQty': '库存', 'warehouse.sku.table.totalQty': '库存',
@ -119,6 +123,8 @@ export default {
'warehouse.sku.message.deleteFailed': '删除失败', 'warehouse.sku.message.deleteFailed': '删除失败',
'warehouse.sku.message.deleteConfirm': '确定删除 {code}?', 'warehouse.sku.message.deleteConfirm': '确定删除 {code}?',
'warehouse.sku.message.required': '请填写 SKU 编码和名称', 'warehouse.sku.message.required': '请填写 SKU 编码和名称',
'warehouse.inventory.table.warehouse': '仓库',
'warehouse.inventory.table.binName': '库位名称',
'warehouse.inventory.table.skuCode': 'SKU 编码', 'warehouse.inventory.table.skuCode': 'SKU 编码',
'warehouse.inventory.table.skuName': 'SKU 名称', 'warehouse.inventory.table.skuName': 'SKU 名称',
'warehouse.inventory.table.warehouseId': '仓库', 'warehouse.inventory.table.warehouseId': '仓库',
@ -143,6 +149,10 @@ export default {
'warehouse.parcel.status.inTransit': '在途', 'warehouse.parcel.status.inTransit': '在途',
'warehouse.parcel.status.arrived': '已到仓', 'warehouse.parcel.status.arrived': '已到仓',
'warehouse.parcel.status.stored': '已上架', 'warehouse.parcel.status.stored': '已上架',
'warehouse.parcel.table.warehouse': '仓库',
'warehouse.parcel.table.items': '商品信息',
'warehouse.parcel.table.arrivalTime': '到仓时间',
'warehouse.parcel.table.createTime': '创建时间',
'warehouse.parcel.table.trackingNo': '快递单号', 'warehouse.parcel.table.trackingNo': '快递单号',
'warehouse.parcel.table.carrierName': '快递公司', 'warehouse.parcel.table.carrierName': '快递公司',
'warehouse.parcel.table.lifecycleStatus': '状态', 'warehouse.parcel.table.lifecycleStatus': '状态',
@ -163,6 +173,12 @@ export default {
'warehouse.inbound.status.all': '全部', 'warehouse.inbound.status.all': '全部',
'warehouse.inbound.status.forecasted': '预报', 'warehouse.inbound.status.forecasted': '预报',
'warehouse.inbound.status.completed': '已完成', 'warehouse.inbound.status.completed': '已完成',
'warehouse.inbound.status.received': '待入库',
'warehouse.inbound.table.warehouse': '仓库',
'warehouse.inbound.table.inboundType': '入库类型',
'warehouse.inbound.table.expectedArrival': '预计到货',
'warehouse.inbound.type.normal': '普通入库',
'warehouse.inbound.type.return': '退货入库',
'warehouse.inbound.table.noticeNo': '预报单号', 'warehouse.inbound.table.noticeNo': '预报单号',
'warehouse.inbound.table.markReference': '唛头', 'warehouse.inbound.table.markReference': '唛头',
'warehouse.inbound.table.receiptStatus': '状态', 'warehouse.inbound.table.receiptStatus': '状态',
@ -188,6 +204,14 @@ export default {
'warehouse.inbound.message.required': '请选择仓库和 SKU', 'warehouse.inbound.message.required': '请选择仓库和 SKU',
'warehouse.inbound.message.detailFailed': '加载详情失败', 'warehouse.inbound.message.detailFailed': '加载详情失败',
'order.platformStore.platform.manual': '手工店铺', 'order.platformStore.platform.manual': '手工店铺',
'order.platformStore.table.siteCountry': '站点国家',
'order.platformStore.table.autoFulfill': '自动发货',
'order.platformStore.table.authTime': '授权时间',
'order.platformStore.table.createTime': '创建时间',
'order.platformStore.form.siteCountry': '站点国家',
'order.platformStore.auth.active': '已授权',
'order.platformStore.auth.expired': '已过期',
'order.platformStore.auth.revoked': '已撤销',
'order.platformStore.table.platform': '平台', 'order.platformStore.table.platform': '平台',
'order.platformStore.table.nickname': '店铺昵称', 'order.platformStore.table.nickname': '店铺昵称',
'order.platformStore.table.account': '账号', 'order.platformStore.table.account': '账号',
@ -214,6 +238,13 @@ export default {
'order.platformStoreOrder.status.pending': '待处理', 'order.platformStoreOrder.status.pending': '待处理',
'order.platformStoreOrder.status.toShip': '待发货', 'order.platformStoreOrder.status.toShip': '待发货',
'order.platformStoreOrder.status.shipped': '已发货', 'order.platformStoreOrder.status.shipped': '已发货',
'order.platformStoreOrder.status.suspended': '已搁置',
'order.platformStoreOrder.table.platform': '平台',
'order.platformStoreOrder.table.country': '国家',
'order.platformStoreOrder.table.itemCount': '商品数',
'order.platformStoreOrder.table.shippingLabel': '面单号',
'order.platformStoreOrder.table.warehouse': '发货仓库',
'order.platformStoreOrder.table.lastSync': '同步时间',
'order.platformStoreOrder.table.orderNo': '平台订单号', 'order.platformStoreOrder.table.orderNo': '平台订单号',
'order.platformStoreOrder.table.store': '店铺', 'order.platformStoreOrder.table.store': '店铺',
'order.platformStoreOrder.table.status': '状态', 'order.platformStoreOrder.table.status': '状态',

View File

@ -0,0 +1,73 @@
.warehouse-list-page {
width: 100%;
box-sizing: border-box;
padding: 16px 16px 24px;
display: flex;
flex-direction: column;
background: #f0f3f4;
flex: 1;
min-height: 0;
overflow: hidden;
}
.warehouse-list-page-card {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
overflow: hidden;
}
.warehouse-list-page-card .status-tabs {
padding: 16px 16px 0;
flex-shrink: 0;
}
.warehouse-list-page-card .table-toolbar {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 16px 16px 0;
margin-bottom: 16px;
flex-shrink: 0;
flex-wrap: wrap;
}
.warehouse-list-page-card .toolbar-left,
.warehouse-list-page-card .toolbar-right {
display: flex;
align-items: center;
}
.warehouse-list-page-card .toolbar-left {
flex: 1;
min-width: 0;
}
.warehouse-list-page-card .list-wrap {
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
}
.warehouse-list-page-card .list-wrap :deep(.handsontable-pro-container) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.warehouse-list-page-card .tab-count {
margin-left: 4px;
color: var(--app-primary-color);
}

View File

@ -0,0 +1,73 @@
import dayjs from 'dayjs'
type Row = Record<string, unknown>
export function pickField<T = unknown>(row: Row, ...keys: string[]): T | undefined {
for (const key of keys) {
const value = row[key]
if (value !== undefined && value !== null && value !== '') {
return value as T
}
}
return undefined
}
export function formatDateTime(value: unknown): string {
if (!value) return '-'
const formatted = dayjs(String(value))
return formatted.isValid() ? formatted.format('YYYY-MM-DD HH:mm') : String(value)
}
export function formatDimensions(row: Row): string {
const length = pickField<number | string>(row, 'lengthCm', 'LengthCm')
const width = pickField<number | string>(row, 'widthCm', 'WidthCm')
const height = pickField<number | string>(row, 'heightCm', 'HeightCm')
if (length == null && width == null && height == null) return '-'
return `${length ?? '-'}×${width ?? '-'}×${height ?? '-'}`
}
export function buildWarehouseNameMap(
options: Array<{ label: string; value: number }>,
): Map<number, string> {
return new Map(options.map((item) => [item.value, item.label]))
}
export function resolveWarehouseLabel(
warehouseId: number | undefined,
map: Map<number, string>,
): string {
if (!warehouseId) return '-'
return map.get(warehouseId) || `#${warehouseId}`
}
export function summarizeParcelItems(items: unknown): string {
const list = Array.isArray(items) ? items : []
if (!list.length) return '-'
const first = list[0] as Row
const name = pickField<string>(first, 'productName', 'ProductName') || '-'
const qty = Number(pickField(first, 'quantity', 'Quantity') ?? 0)
if (list.length === 1) return `${name} ×${qty}`
const totalQty = list.reduce(
(sum, item) => sum + Number(pickField(item as Row, 'quantity', 'Quantity') ?? 0),
0,
)
return `${name} 等${list.length}项 / ${totalQty}件`
}
export function formatMoney(amount: unknown, currency?: string | null): string {
if (amount == null || amount === '') return '-'
const code = currency?.trim()
return code ? `${amount} ${code}` : String(amount)
}
export function normalizeListRow(row: Row): Row {
const normalized: Row = { ...row }
Object.entries(row).forEach(([key, value]) => {
if (!key) return
const camel = key.charAt(0).toLowerCase() + key.slice(1)
if (normalized[camel] === undefined) normalized[camel] = value
const pascal = key.charAt(0).toUpperCase() + key.slice(1)
if (normalized[pascal] === undefined) normalized[pascal] = value
})
return normalized
}

View File

@ -11,6 +11,7 @@ import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import { deletePlatformStore, getPlatformStoreAuthUrl, getPlatformStoreList, savePlatformStore } from '@/api/order/platformStore' import { deletePlatformStore, getPlatformStoreAuthUrl, getPlatformStoreList, savePlatformStore } from '@/api/order/platformStore'
import { $baseMessage } from '@/utils' import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission' import { hasPermission } from '@/utils/rbac-permission'
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
defineOptions({ name: 'PlatformStore' }) defineOptions({ name: 'PlatformStore' })
@ -49,10 +50,33 @@ const hotColumns = computed(() => [
{ key: 'platformName', label: t('order.platformStore.table.platform'), width: 100 }, { key: 'platformName', label: t('order.platformStore.table.platform'), width: 100 },
{ key: 'storeNickname', label: t('order.platformStore.table.nickname'), width: 160 }, { key: 'storeNickname', label: t('order.platformStore.table.nickname'), width: 160 },
{ key: 'platformAccount', label: t('order.platformStore.table.account'), width: 140 }, { key: 'platformAccount', label: t('order.platformStore.table.account'), width: 140 },
{ key: 'siteCountry', label: t('order.platformStore.table.siteCountry'), width: 90 },
{ key: 'authStatus', label: t('order.platformStore.table.authStatus'), width: 100 }, { key: 'authStatus', label: t('order.platformStore.table.authStatus'), width: 100 },
{ key: 'tokenExpireTime', label: t('order.platformStore.table.tokenExpire'), width: 170 }, { key: 'isAutoFulfill', label: t('order.platformStore.table.autoFulfill'), width: 90 },
{ key: 'authTime', label: t('order.platformStore.table.authTime'), width: 160 },
{ key: 'tokenExpireTime', label: t('order.platformStore.table.tokenExpire'), width: 160 },
{ key: 'createTime', label: t('order.platformStore.table.createTime'), width: 160 },
]) ])
function mapRow(raw: Record<string, unknown>) {
const row = normalizeListRow(raw)
const authStatus = String(pickField(row, 'authStatus', 'AuthStatus') ?? '')
const autoFulfill = Boolean(pickField(row, 'isAutoFulfill', 'IsAutoFulfill'))
return {
id: pickField<number>(row, 'id', 'Id'),
platformName: pickField(row, 'platformName', 'PlatformName') ?? pickField(row, 'platformCode', 'PlatformCode') ?? '-',
storeNickname: pickField(row, 'storeNickname', 'StoreNickname') ?? '-',
platformAccount: pickField(row, 'platformAccount', 'PlatformAccount') ?? '-',
siteCountry: pickField(row, 'siteCountry', 'SiteCountry') ?? '-',
authStatus: t(`order.platformStore.auth.${authStatus}`, authStatus),
isAutoFulfill: autoFulfill ? t('warehouse.common.yes') : t('warehouse.common.no'),
authTime: formatDateTime(pickField(row, 'authTime', 'AuthTime')),
tokenExpireTime: formatDateTime(pickField(row, 'tokenExpireTime', 'TokenExpireTime')),
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')),
_raw: row,
}
}
async function loadList() { async function loadList() {
listLoading.value = true listLoading.value = true
try { try {
@ -62,7 +86,7 @@ async function loadList() {
platformCode: platformCode.value, platformCode: platformCode.value,
storeNickname: keyword.value || undefined, storeNickname: keyword.value || undefined,
}) })
tableRows.value = items || [] tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
totalCount.value = Number(total || 0) totalCount.value = Number(total || 0)
} catch (e: any) { } catch (e: any) {
$baseMessage(e?.message || t('order.platformStore.message.loadFailed'), 'error') $baseMessage(e?.message || t('order.platformStore.message.loadFailed'), 'error')
@ -71,15 +95,48 @@ async function loadList() {
} }
} }
function handlePageChange(page: number) {
currentPage.value = page
loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
loadList()
}
function handleSearch() {
currentPage.value = 1
loadList()
}
function handleAdd() { function handleAdd() {
if (!hasPermission('Order.PlatformStore.Save')) return if (!hasPermission('Order.PlatformStore.Save')) return
Object.assign(formModel, { id: 0, platformCode: 'manual', storeNickname: '', platformAccount: '', siteCountry: '', isAutoFulfill: false, remark: '' }) Object.assign(formModel, {
id: 0,
platformCode: 'manual',
storeNickname: '',
platformAccount: '',
siteCountry: '',
isAutoFulfill: false,
remark: '',
})
openModal() openModal()
} }
function handleEdit(row: any) { function handleEdit(row: any) {
if (!hasPermission('Order.PlatformStore.Save')) return if (!hasPermission('Order.PlatformStore.Save')) return
Object.assign(formModel, row) const raw = row._raw || row
Object.assign(formModel, {
id: Number(pickField(raw, 'id', 'Id') ?? 0),
platformCode: pickField(raw, 'platformCode', 'PlatformCode') ?? 'manual',
storeNickname: pickField(raw, 'storeNickname', 'StoreNickname') ?? '',
platformAccount: pickField(raw, 'platformAccount', 'PlatformAccount') ?? '',
siteCountry: pickField(raw, 'siteCountry', 'SiteCountry') ?? '',
isAutoFulfill: Boolean(pickField(raw, 'isAutoFulfill', 'IsAutoFulfill')),
remark: pickField(raw, 'remark', 'Remark') ?? '',
})
openModal() openModal()
} }
@ -103,8 +160,9 @@ async function handleSave() {
async function handleAuth(row: any) { async function handleAuth(row: any) {
if (!hasPermission('Order.PlatformStore.Auth')) return if (!hasPermission('Order.PlatformStore.Auth')) return
const raw = row._raw || row
try { try {
const result = await getPlatformStoreAuthUrl({ platformCode: row.platformCode }) const result = await getPlatformStoreAuthUrl({ platformCode: pickField(raw, 'platformCode', 'PlatformCode') })
$baseMessage(result?.message || result?.authUrl || t('order.platformStore.message.authPlaceholder'), 'info') $baseMessage(result?.message || result?.authUrl || t('order.platformStore.message.authPlaceholder'), 'info')
} catch (e: any) { } catch (e: any) {
$baseMessage(e?.message || t('order.platformStore.message.authFailed'), 'error') $baseMessage(e?.message || t('order.platformStore.message.authFailed'), 'error')
@ -148,25 +206,62 @@ onMounted(loadList)
</script> </script>
<template> <template>
<div class="platform-store-container"> <div class="platform-store-container warehouse-list-page">
<div class="page-card"> <div class="warehouse-list-page-card">
<div class="table-toolbar"> <div class="table-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<a-space wrap :size="8"> <a-space wrap :size="8">
<a-select v-model="platformCode" allow-clear :placeholder="t('order.platformStore.search.platform')" :options="platformOptions" style="width: 160px" /> <a-select
<a-input v-model="keyword" allow-clear :placeholder="t('order.platformStore.search.nickname')" style="width: 200px" /> v-model="platformCode"
<a-button type="primary" @click="() => { currentPage = 1; loadList() }">{{ t('warehouse.common.search') }}</a-button> allow-clear
<a-button v-if="hasPermission('Order.PlatformStore.Save')" type="primary" @click="handleAdd">{{ t('warehouse.common.add') }}</a-button> :placeholder="t('order.platformStore.search.platform')"
:options="platformOptions"
style="width: 160px"
/>
<a-input
v-model="keyword"
allow-clear
:placeholder="t('order.platformStore.search.nickname')"
style="width: 200px"
@press-enter="handleSearch"
/>
<a-button type="primary" @click="handleSearch">{{ t('warehouse.common.search') }}</a-button>
<a-button v-if="hasPermission('Order.PlatformStore.Save')" type="primary" @click="handleAdd">
{{ t('warehouse.common.add') }}
</a-button>
</a-space> </a-space>
</div> </div>
<div class="toolbar-right">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div> </div>
<div ref="tableContainerRef" class="list-wrap"> <div ref="tableContainerRef" class="list-wrap">
<HandsontablePro :columns="hotColumns" :data="tableRows" :height="tableHeight" :loading="listLoading" show-action-column :action-renderer="actionRenderer" @action-click="handleActionClick" /> <HandsontablePro
:columns="hotColumns"
:data="tableRows"
:height="tableHeight"
:loading="listLoading"
:stretch-columns="true"
show-action-column
:action-renderer="actionRenderer"
@action-click="handleActionClick"
/>
</div> </div>
<PaginationPro v-model:current="currentPage" v-model:page-size="pageSize" :total="totalCount" @change="loadList" @page-size-change="() => { currentPage = 1; loadList() }" />
</div> </div>
<ModalPro v-model:visible="modalVisible" :title="formModel.id ? t('order.platformStore.modal.edit') : t('order.platformStore.modal.add')" :width="640" @cancel="closeModal"> <ModalPro
v-model:visible="modalVisible"
:title="formModel.id ? t('order.platformStore.modal.edit') : t('order.platformStore.modal.add')"
:width="640"
@cancel="closeModal"
>
<FormPro :model="formModel" layout="horizontal" :row-gutter="20" :default-span="12"> <FormPro :model="formModel" layout="horizontal" :row-gutter="20" :default-span="12">
<a-form-item :label="t('order.platformStore.form.platform')" required> <a-form-item :label="t('order.platformStore.form.platform')" required>
<a-select v-model="formModel.platformCode" :options="platformOptions" :disabled="formModel.id > 0" /> <a-select v-model="formModel.platformCode" :options="platformOptions" :disabled="formModel.id > 0" />
@ -177,6 +272,9 @@ onMounted(loadList)
<a-form-item :label="t('order.platformStore.form.account')"> <a-form-item :label="t('order.platformStore.form.account')">
<a-input v-model="formModel.platformAccount" /> <a-input v-model="formModel.platformAccount" />
</a-form-item> </a-form-item>
<a-form-item :label="t('order.platformStore.form.siteCountry')">
<a-input v-model="formModel.siteCountry" />
</a-form-item>
<a-form-item :label="t('order.platformStore.form.autoFulfill')"> <a-form-item :label="t('order.platformStore.form.autoFulfill')">
<a-switch v-model="formModel.isAutoFulfill" /> <a-switch v-model="formModel.isAutoFulfill" />
</a-form-item> </a-form-item>
@ -190,25 +288,5 @@ onMounted(loadList)
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
.platform-store-container { @import '@/styles/warehouse-list-page.less';
padding: 16px;
background: #f0f3f4;
min-height: 100%;
display: flex;
flex-direction: column;
}
.page-card {
background: #fff;
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
border-radius: 4px;
}
.list-wrap {
flex: 1;
min-height: 0;
margin-top: 12px;
}
</style> </style>

View File

@ -18,6 +18,14 @@ import {
import { getWarehouseList } from '@/api/system/warehouse' import { getWarehouseList } from '@/api/system/warehouse'
import { $baseMessage } from '@/utils' import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission' import { hasPermission } from '@/utils/rbac-permission'
import {
buildWarehouseNameMap,
formatDateTime,
formatMoney,
normalizeListRow,
pickField,
resolveWarehouseLabel,
} from '@/utils/warehouseListMapper'
defineOptions({ name: 'PlatformStoreOrder' }) defineOptions({ name: 'PlatformStoreOrder' })
@ -28,6 +36,7 @@ const fulfillStatus = ref<string>()
const keyword = ref('') const keyword = ref('')
const summary = ref<Record<string, number>>({}) const summary = ref<Record<string, number>>({})
const warehouseOptions = ref<Array<{ label: string; value: number }>>([]) const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
const warehouseNameMap = computed(() => buildWarehouseNameMap(warehouseOptions.value))
const productOptions = ref<Array<{ label: string; value: string }>>([]) const productOptions = ref<Array<{ label: string; value: string }>>([])
const listLoading = ref(false) const listLoading = ref(false)
const tableRows = ref<any[]>([]) const tableRows = ref<any[]>([])
@ -36,10 +45,10 @@ const pageSize = ref(30)
const totalCount = ref(0) const totalCount = ref(0)
const statusTabs = [ const statusTabs = [
{ value: undefined, labelKey: 'order.platformStoreOrder.status.all' }, { value: undefined, labelKey: 'order.platformStoreOrder.status.all', countKey: null },
{ value: 'pending', labelKey: 'order.platformStoreOrder.status.pending' }, { value: 'pending', labelKey: 'order.platformStoreOrder.status.pending', countKey: 'pending' as const },
{ value: 'toShip', labelKey: 'order.platformStoreOrder.status.toShip' }, { value: 'toShip', labelKey: 'order.platformStoreOrder.status.toShip', countKey: 'toShip' as const },
{ value: 'shipped', labelKey: 'order.platformStoreOrder.status.shipped' }, { value: 'shipped', labelKey: 'order.platformStoreOrder.status.shipped', countKey: 'shipped' as const },
] ]
const detailVisible = ref(false) const detailVisible = ref(false)
@ -49,13 +58,50 @@ const matchProductCode = ref<string>()
const hotColumns = computed(() => [ const hotColumns = computed(() => [
{ key: 'platformOrderNo', label: t('order.platformStoreOrder.table.orderNo'), width: 160 }, { key: 'platformOrderNo', label: t('order.platformStoreOrder.table.orderNo'), width: 160 },
{ key: 'platformName', label: t('order.platformStoreOrder.table.platform'), width: 90 },
{ key: 'storeNickname', label: t('order.platformStoreOrder.table.store'), width: 120 }, { key: 'storeNickname', label: t('order.platformStoreOrder.table.store'), width: 120 },
{ key: 'orderCountry', label: t('order.platformStoreOrder.table.country'), width: 80 },
{ key: 'fulfillStatus', label: t('order.platformStoreOrder.table.status'), width: 90 }, { key: 'fulfillStatus', label: t('order.platformStoreOrder.table.status'), width: 90 },
{ key: 'itemCount', label: t('order.platformStoreOrder.table.itemCount'), width: 70 },
{ key: 'totalAmountDisplay', label: t('order.platformStoreOrder.table.amount'), width: 110 },
{ key: 'linkedServiceNo', label: t('order.platformStoreOrder.table.linkedServiceNo'), width: 150 }, { key: 'linkedServiceNo', label: t('order.platformStoreOrder.table.linkedServiceNo'), width: 150 },
{ key: 'totalAmount', label: t('order.platformStoreOrder.table.amount'), width: 90 }, { key: 'shippingLabelNo', label: t('order.platformStoreOrder.table.shippingLabel'), width: 140 },
{ key: 'platformOrderTime', label: t('order.platformStoreOrder.table.orderTime'), width: 170 }, { key: 'warehouseName', label: t('order.platformStoreOrder.table.warehouse'), width: 120 },
{ key: 'platformOrderTime', label: t('order.platformStoreOrder.table.orderTime'), width: 160 },
{ key: 'lastSyncTime', label: t('order.platformStoreOrder.table.lastSync'), width: 160 },
]) ])
function mapRow(raw: Record<string, unknown>) {
const row = normalizeListRow(raw)
const status = String(pickField(row, 'fulfillStatus', 'FulfillStatus') ?? '')
const platformCode = String(pickField(row, 'platformCode', 'PlatformCode') ?? '')
const whId = pickField<number>(row, 'warehouseId', 'WarehouseId')
const currency = pickField<string>(row, 'currency', 'Currency')
const amount = pickField(row, 'totalAmount', 'TotalAmount')
const suspended = Boolean(pickField(row, 'isSuspended', 'IsSuspended'))
let statusLabel = t(`order.platformStoreOrder.status.${status}`, status)
if (suspended) statusLabel = `${statusLabel}(${t('order.platformStoreOrder.status.suspended')})`
return {
id: pickField<number>(row, 'id', 'Id'),
platformOrderNo: pickField(row, 'platformOrderNo', 'PlatformOrderNo') ?? '-',
platformName: pickField(row, 'platformName', 'PlatformName') ?? platformCode ?? '-',
storeNickname: pickField(row, 'storeNickname', 'StoreNickname') ?? '-',
orderCountry: pickField(row, 'orderCountry', 'OrderCountry') ?? '-',
fulfillStatus: statusLabel,
itemCount: pickField<number>(row, 'itemCount', 'ItemCount') ?? '-',
totalAmountDisplay: formatMoney(amount, currency),
linkedServiceNo: pickField(row, 'linkedServiceNo', 'LinkedServiceNo') || '-',
shippingLabelNo: pickField(row, 'shippingLabelNo', 'ShippingLabelNo') ?? '-',
warehouseName: resolveWarehouseLabel(whId, warehouseNameMap.value),
platformOrderTime: formatDateTime(pickField(row, 'platformOrderTime', 'PlatformOrderTime')),
lastSyncTime: formatDateTime(pickField(row, 'lastSyncTime', 'LastSyncTime')),
_raw: row,
_status: status,
_suspended: suspended,
}
}
async function loadProducts() { async function loadProducts() {
try { try {
const { items } = await getSalesProductList({ page: 1, pageSize: 200, status: 1 }) const { items } = await getSalesProductList({ page: 1, pageSize: 200, status: 1 })
@ -77,7 +123,12 @@ async function loadProducts() {
async function loadSummary() { async function loadSummary() {
try { try {
const data = await getPlatformStoreOrderSummary() const data = await getPlatformStoreOrderSummary()
summary.value = data || {} summary.value = {
pending: data?.pending ?? data?.Pending ?? 0,
toShip: data?.toShip ?? data?.ToShip ?? 0,
shipped: data?.shipped ?? data?.Shipped ?? 0,
suspended: data?.suspended ?? data?.Suspended ?? 0,
}
} catch { } catch {
summary.value = {} summary.value = {}
} }
@ -92,12 +143,7 @@ async function loadList() {
fulfillStatus: fulfillStatus.value, fulfillStatus: fulfillStatus.value,
platformOrderNo: keyword.value || undefined, platformOrderNo: keyword.value || undefined,
}) })
tableRows.value = (items || []).map((r: any) => ({ tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
...r,
fulfillStatus: t(`order.platformStoreOrder.status.${r.fulfillStatus}`, r.fulfillStatus),
linkedServiceNo: r.linkedServiceNo || '-',
_raw: r,
}))
totalCount.value = Number(total || 0) totalCount.value = Number(total || 0)
} catch (e: any) { } catch (e: any) {
$baseMessage(e?.message || t('order.platformStoreOrder.message.loadFailed'), 'error') $baseMessage(e?.message || t('order.platformStoreOrder.message.loadFailed'), 'error')
@ -106,6 +152,22 @@ async function loadList() {
} }
} }
function handlePageChange(page: number) {
currentPage.value = page
loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
loadList()
}
function handleSearch() {
currentPage.value = 1
loadList()
}
async function showDetail(row: any) { async function showDetail(row: any) {
try { try {
detailData.value = await getPlatformStoreOrderDetail(row._raw?.id || row.id) detailData.value = await getPlatformStoreOrderDetail(row._raw?.id || row.id)
@ -147,9 +209,8 @@ async function handleBatchMatch() {
} }
const pendingIds = tableRows.value const pendingIds = tableRows.value
.map((row) => row._raw || row) .filter((row) => row._status === 'pending' && !pickField(row._raw, 'linkedOrderId', 'LinkedOrderId'))
.filter((raw) => raw.fulfillStatus === 'pending' && !raw.linkedOrderId) .map((row) => Number(row.id))
.map((raw) => raw.id)
if (!pendingIds.length) { if (!pendingIds.length) {
$baseMessage(t('order.platformStoreOrder.message.batchEmpty'), 'warning') $baseMessage(t('order.platformStoreOrder.message.batchEmpty'), 'warning')
@ -185,8 +246,12 @@ async function handleBatchMatch() {
async function handleSuspend(row: any) { async function handleSuspend(row: any) {
if (!hasPermission('Order.PlatformStoreOrder.ToggleSuspend')) return if (!hasPermission('Order.PlatformStoreOrder.ToggleSuspend')) return
const raw = row._raw || row const raw = row._raw || row
await togglePlatformStoreOrderSuspend({ id: raw.id, isSuspended: !raw.isSuspended }) await togglePlatformStoreOrderSuspend({
id: Number(pickField(raw, 'id', 'Id')),
isSuspended: !Boolean(pickField(raw, 'isSuspended', 'IsSuspended')),
})
$baseMessage(t('order.platformStoreOrder.message.suspendSuccess'), 'success') $baseMessage(t('order.platformStoreOrder.message.suspendSuccess'), 'success')
await loadSummary()
await loadList() await loadList()
} }
@ -194,10 +259,11 @@ function handleDelete(row: any) {
if (!hasPermission('Order.PlatformStoreOrder.Delete')) return if (!hasPermission('Order.PlatformStoreOrder.Delete')) return
Modal.confirm({ Modal.confirm({
title: t('order.platformStoreOrder.actions.delete'), title: t('order.platformStoreOrder.actions.delete'),
content: row._raw?.platformOrderNo || row.platformOrderNo, content: row.platformOrderNo,
onOk: async () => { onOk: async () => {
await deletePlatformStoreOrder([row._raw?.id || row.id]) await deletePlatformStoreOrder([row.id])
$baseMessage(t('order.platformStoreOrder.message.deleteSuccess'), 'success') $baseMessage(t('order.platformStoreOrder.message.deleteSuccess'), 'success')
await loadSummary()
await loadList() await loadList()
}, },
}) })
@ -206,11 +272,17 @@ function handleDelete(row: any) {
function actionRenderer(row: any) { function actionRenderer(row: any) {
const parts = [`<button type="button" class="hot-action-btn" data-action="detail">${t('warehouse.common.detail')}</button>`] const parts = [`<button type="button" class="hot-action-btn" data-action="detail">${t('warehouse.common.detail')}</button>`]
const raw = row._raw || row const raw = row._raw || row
if (hasPermission('Order.PlatformStoreOrder.MatchInventory') && raw.fulfillStatus === 'pending' && !raw.linkedOrderId) { if (
hasPermission('Order.PlatformStoreOrder.MatchInventory')
&& row._status === 'pending'
&& !pickField(raw, 'linkedOrderId', 'LinkedOrderId')
) {
parts.push(`<button type="button" class="hot-action-btn" data-action="match">${t('order.platformStoreOrder.actions.match')}</button>`) parts.push(`<button type="button" class="hot-action-btn" data-action="match">${t('order.platformStoreOrder.actions.match')}</button>`)
} }
if (hasPermission('Order.PlatformStoreOrder.ToggleSuspend')) { if (hasPermission('Order.PlatformStoreOrder.ToggleSuspend')) {
parts.push(`<button type="button" class="hot-action-btn" data-action="suspend">${raw.isSuspended ? t('order.platformStoreOrder.actions.resume') : t('order.platformStoreOrder.actions.suspend')}</button>`) parts.push(
`<button type="button" class="hot-action-btn" data-action="suspend">${row._suspended ? t('order.platformStoreOrder.actions.resume') : t('order.platformStoreOrder.actions.suspend')}</button>`,
)
} }
if (hasPermission('Order.PlatformStoreOrder.Delete')) { if (hasPermission('Order.PlatformStoreOrder.Delete')) {
parts.push(`<button type="button" class="hot-action-btn" data-action="delete">${t('warehouse.common.delete')}</button>`) parts.push(`<button type="button" class="hot-action-btn" data-action="delete">${t('warehouse.common.delete')}</button>`)
@ -227,7 +299,10 @@ function handleActionClick(row: any, _i: number, action?: string) {
onMounted(async () => { onMounted(async () => {
const { items } = await getWarehouseList({ page: 1, pageSize: 200 }).catch(() => ({ items: [] })) const { items } = await getWarehouseList({ page: 1, pageSize: 200 }).catch(() => ({ items: [] }))
warehouseOptions.value = (items || []).map((w: any) => ({ label: w.warehouseName || `#${w.id}`, value: Number(w.id) })) warehouseOptions.value = (items || []).map((w: any) => ({
label: w.warehouseName || `#${w.id}`,
value: Number(w.id),
}))
if (warehouseOptions.value.length) matchWarehouseId.value = warehouseOptions.value[0].value if (warehouseOptions.value.length) matchWarehouseId.value = warehouseOptions.value[0].value
await loadProducts() await loadProducts()
await loadSummary() await loadSummary()
@ -236,34 +311,72 @@ onMounted(async () => {
</script> </script>
<template> <template>
<div class="platform-store-order-container"> <div class="platform-store-order-container warehouse-list-page">
<div class="page-card"> <div class="warehouse-list-page-card">
<a-tabs v-model:active-key="fulfillStatus" @change="() => { currentPage = 1; loadList() }"> <a-tabs
<a-tab-pane v-for="tab in statusTabs" :key="tab.value ?? 'all'" :title="t(tab.labelKey)" /> v-model:active-key="fulfillStatus"
class="status-tabs"
@change="() => { currentPage = 1; loadList() }"
>
<a-tab-pane v-for="tab in statusTabs" :key="tab.value ?? 'all'">
<template #title>
{{ t(tab.labelKey) }}
<span v-if="tab.countKey" class="tab-count">({{ summary[tab.countKey] ?? 0 }})</span>
</template>
</a-tab-pane>
</a-tabs> </a-tabs>
<div class="table-toolbar"> <div class="table-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<a-space wrap :size="8"> <a-space wrap :size="8">
<a-select v-model="matchWarehouseId" :placeholder="t('warehouse.common.warehouse')" :options="warehouseOptions" style="width: 180px" /> <a-select
<a-select v-model="matchProductCode" allow-clear :placeholder="t('order.platformStoreOrder.form.product')" :options="productOptions" style="width: 220px" /> v-model="matchWarehouseId"
<a-input v-model="keyword" allow-clear :placeholder="t('order.platformStoreOrder.search.orderNo')" style="width: 180px" /> :placeholder="t('warehouse.common.warehouse')"
<a-button type="primary" @click="() => { currentPage = 1; loadList() }">{{ t('warehouse.common.search') }}</a-button> :options="warehouseOptions"
style="width: 180px"
/>
<a-select
v-model="matchProductCode"
allow-clear
:placeholder="t('order.platformStoreOrder.form.product')"
:options="productOptions"
style="width: 220px"
/>
<a-input
v-model="keyword"
allow-clear
:placeholder="t('order.platformStoreOrder.search.orderNo')"
style="width: 180px"
@press-enter="handleSearch"
/>
<a-button type="primary" @click="handleSearch">{{ t('warehouse.common.search') }}</a-button>
<a-button v-if="hasPermission('Order.PlatformStoreOrder.MatchInventory')" @click="handleBatchMatch"> <a-button v-if="hasPermission('Order.PlatformStoreOrder.MatchInventory')" @click="handleBatchMatch">
{{ t('order.platformStoreOrder.actions.batchMatch') }} {{ t('order.platformStoreOrder.actions.batchMatch') }}
</a-button> </a-button>
</a-space> </a-space>
</div> </div>
<div class="toolbar-right"> <div class="toolbar-right">
<a-space :size="12"> <PaginationPro
<span>{{ t('order.platformStoreOrder.summary.pending') }}: {{ summary.pending ?? 0 }}</span> :total-count="totalCount"
<span>{{ t('order.platformStoreOrder.summary.toShip') }}: {{ summary.toShip ?? 0 }}</span> v-model:current-page="currentPage"
</a-space> v-model:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div> </div>
</div> </div>
<div ref="tableContainerRef" class="list-wrap"> <div ref="tableContainerRef" class="list-wrap">
<HandsontablePro :columns="hotColumns" :data="tableRows" :height="tableHeight" :loading="listLoading" show-action-column :action-renderer="actionRenderer" @action-click="handleActionClick" /> <HandsontablePro
:columns="hotColumns"
:data="tableRows"
:height="tableHeight"
:loading="listLoading"
:stretch-columns="true"
show-action-column
:action-renderer="actionRenderer"
@action-click="handleActionClick"
/>
</div> </div>
<PaginationPro v-model:current="currentPage" v-model:page-size="pageSize" :total="totalCount" @change="loadList" @page-size-change="() => { currentPage = 1; loadList() }" />
</div> </div>
<a-drawer v-model:visible="detailVisible" :title="t('order.platformStoreOrder.drawer.detail')" :width="720"> <a-drawer v-model:visible="detailVisible" :title="t('order.platformStoreOrder.drawer.detail')" :width="720">
@ -271,7 +384,6 @@ onMounted(async () => {
<p>{{ detailData.platformOrderNo }} · {{ detailData.storeNickname }}</p> <p>{{ detailData.platformOrderNo }} · {{ detailData.storeNickname }}</p>
<p v-if="detailData.linkedServiceNo"> <p v-if="detailData.linkedServiceNo">
{{ t('order.platformStoreOrder.table.linkedServiceNo') }}: {{ detailData.linkedServiceNo }} {{ t('order.platformStoreOrder.table.linkedServiceNo') }}: {{ detailData.linkedServiceNo }}
<span v-if="detailData.linkedOrderId">(#{{ detailData.linkedOrderId }})</span>
</p> </p>
<a-table :data="detailData.lines || []" :pagination="false" size="small"> <a-table :data="detailData.lines || []" :pagination="false" size="small">
<template #columns> <template #columns>
@ -287,25 +399,5 @@ onMounted(async () => {
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
.platform-store-order-container { @import '@/styles/warehouse-list-page.less';
padding: 16px;
background: #f0f3f4;
min-height: 100%;
display: flex;
flex-direction: column;
}
.page-card {
background: #fff;
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
border-radius: 4px;
}
.list-wrap {
flex: 1;
min-height: 0;
margin-top: 12px;
}
</style> </style>

View File

@ -19,6 +19,13 @@ import { getWarehouseList } from '@/api/system/warehouse'
import { getWarehouseSkuList } from '@/api/warehouse' import { getWarehouseSkuList } from '@/api/warehouse'
import { $baseMessage } from '@/utils' import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission' import { hasPermission } from '@/utils/rbac-permission'
import {
buildWarehouseNameMap,
formatDateTime,
normalizeListRow,
pickField,
resolveWarehouseLabel,
} from '@/utils/warehouseListMapper'
defineOptions({ name: 'WarehouseInboundNotice' }) defineOptions({ name: 'WarehouseInboundNotice' })
@ -30,6 +37,7 @@ const summary = ref({ forecasted: 0, received: 0, completed: 0 })
const keyword = ref('') const keyword = ref('')
const warehouseId = ref<number>() const warehouseId = ref<number>()
const warehouseOptions = ref<Array<{ label: string; value: number }>>([]) const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
const warehouseNameMap = computed(() => buildWarehouseNameMap(warehouseOptions.value))
const skuOptions = ref<Array<{ label: string; value: number; code: string }>>([]) const skuOptions = ref<Array<{ label: string; value: number; code: string }>>([])
const listLoading = ref(false) const listLoading = ref(false)
const tableRows = ref<any[]>([]) const tableRows = ref<any[]>([])
@ -39,8 +47,9 @@ const totalCount = ref(0)
const statusTabs = [ const statusTabs = [
{ value: undefined, labelKey: 'warehouse.inbound.status.all', countKey: null }, { value: undefined, labelKey: 'warehouse.inbound.status.all', countKey: null },
{ value: 'forecasted', labelKey: 'warehouse.inbound.status.forecasted', countKey: 'forecasted' }, { value: 'forecasted', labelKey: 'warehouse.inbound.status.forecasted', countKey: 'forecasted' as const },
{ value: 'completed', labelKey: 'warehouse.inbound.status.completed', countKey: 'completed' }, { value: 'received', labelKey: 'warehouse.inbound.status.received', countKey: 'received' as const },
{ value: 'completed', labelKey: 'warehouse.inbound.status.completed', countKey: 'completed' as const },
] ]
const detailVisible = ref(false) const detailVisible = ref(false)
@ -62,12 +71,36 @@ const formModel = reactive({
const hotColumns = computed(() => [ const hotColumns = computed(() => [
{ key: 'noticeNo', label: t('warehouse.inbound.table.noticeNo'), width: 180 }, { key: 'noticeNo', label: t('warehouse.inbound.table.noticeNo'), width: 180 },
{ key: 'markReference', label: t('warehouse.inbound.table.markReference'), width: 120 }, { key: 'markReference', label: t('warehouse.inbound.table.markReference'), width: 120 },
{ key: 'warehouseName', label: t('warehouse.inbound.table.warehouse'), width: 140 },
{ key: 'inboundType', label: t('warehouse.inbound.table.inboundType'), width: 100 },
{ key: 'receiptStatus', label: t('warehouse.inbound.table.receiptStatus'), width: 100 }, { key: 'receiptStatus', label: t('warehouse.inbound.table.receiptStatus'), width: 100 },
{ key: 'boxCount', label: t('warehouse.inbound.table.boxCount'), width: 80 }, { key: 'boxCount', label: t('warehouse.inbound.table.boxCount'), width: 80 },
{ key: 'plannedQty', label: t('warehouse.inbound.table.plannedQty'), width: 90 }, { key: 'plannedQty', label: t('warehouse.inbound.table.plannedQty'), width: 90 },
{ key: 'createTime', label: t('warehouse.inbound.table.createTime'), width: 170 }, { key: 'expectedArrivalTime', label: t('warehouse.inbound.table.expectedArrival'), width: 160 },
{ key: 'createTime', label: t('warehouse.inbound.table.createTime'), width: 160 },
]) ])
function mapRow(raw: Record<string, unknown>) {
const row = normalizeListRow(raw)
const status = String(pickField(row, 'receiptStatus', 'ReceiptStatus') ?? '')
const inboundType = String(pickField(row, 'inboundType', 'InboundType') ?? 'normal')
const whId = pickField<number>(row, 'warehouseId', 'WarehouseId')
return {
id: pickField<number>(row, 'id', 'Id'),
noticeNo: pickField(row, 'noticeNo', 'NoticeNo') ?? '-',
markReference: pickField(row, 'markReference', 'MarkReference') ?? '-',
warehouseName: resolveWarehouseLabel(whId, warehouseNameMap.value),
inboundType: t(`warehouse.inbound.type.${inboundType}`, inboundType),
receiptStatus: t(`warehouse.inbound.status.${status}`, status),
boxCount: pickField<number>(row, 'boxCount', 'BoxCount') ?? 0,
plannedQty: pickField<number>(row, 'plannedQty', 'PlannedQty') ?? 0,
expectedArrivalTime: formatDateTime(pickField(row, 'expectedArrivalTime', 'ExpectedArrivalTime')),
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')),
_raw: row,
_status: status,
}
}
async function loadWarehouses() { async function loadWarehouses() {
const { items } = await getWarehouseList({ page: 1, pageSize: 200 }).catch(() => ({ items: [] })) const { items } = await getWarehouseList({ page: 1, pageSize: 200 }).catch(() => ({ items: [] }))
warehouseOptions.value = (items || []).map((w: any) => ({ warehouseOptions.value = (items || []).map((w: any) => ({
@ -79,9 +112,9 @@ async function loadWarehouses() {
async function loadSkus() { async function loadSkus() {
const { items } = await getWarehouseSkuList({ page: 1, pageSize: 500 }).catch(() => ({ items: [] })) const { items } = await getWarehouseSkuList({ page: 1, pageSize: 500 }).catch(() => ({ items: [] }))
skuOptions.value = (items || []).map((s: any) => ({ skuOptions.value = (items || []).map((s: any) => ({
label: `${s.skuCode} - ${s.skuName}`, label: `${s.skuCode || s.SkuCode} - ${s.skuName || s.SkuName}`,
value: Number(s.id), value: Number(s.id || s.Id),
code: s.skuCode, code: s.skuCode || s.SkuCode,
})) }))
} }
@ -108,11 +141,7 @@ async function loadList() {
keyword: keyword.value || undefined, keyword: keyword.value || undefined,
warehouseId: warehouseId.value, warehouseId: warehouseId.value,
}) })
tableRows.value = (items || []).map((r: any) => ({ tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
...r,
receiptStatus: t(`warehouse.inbound.status.${r.receiptStatus}`, r.receiptStatus),
_raw: r,
}))
totalCount.value = Number(total || 0) totalCount.value = Number(total || 0)
} catch (e: any) { } catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.loadFailed'), 'error') $baseMessage(e?.message || t('warehouse.inbound.message.loadFailed'), 'error')
@ -121,6 +150,22 @@ async function loadList() {
} }
} }
function handlePageChange(page: number) {
currentPage.value = page
loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
loadList()
}
function handleSearch() {
currentPage.value = 1
loadList()
}
async function showDetail(row: any) { async function showDetail(row: any) {
try { try {
detailData.value = await getWarehouseInboundDetail(row._raw?.id || row.id) detailData.value = await getWarehouseInboundDetail(row._raw?.id || row.id)
@ -179,7 +224,7 @@ function handleDelete(row: any) {
if (!hasPermission('Warehouse.InboundNotice.Delete')) return if (!hasPermission('Warehouse.InboundNotice.Delete')) return
Modal.confirm({ Modal.confirm({
title: t('warehouse.inbound.actions.delete'), title: t('warehouse.inbound.actions.delete'),
content: row._raw?.noticeNo || row.noticeNo, content: row.noticeNo,
onOk: async () => { onOk: async () => {
await deleteWarehouseInbound(row._raw?.id || row.id) await deleteWarehouseInbound(row._raw?.id || row.id)
$baseMessage(t('warehouse.inbound.message.deleteSuccess'), 'success') $baseMessage(t('warehouse.inbound.message.deleteSuccess'), 'success')
@ -191,8 +236,7 @@ function handleDelete(row: any) {
function actionRenderer(row: any) { function actionRenderer(row: any) {
const parts = [`<button type="button" class="hot-action-btn" data-action="detail">${t('warehouse.common.detail')}</button>`] const parts = [`<button type="button" class="hot-action-btn" data-action="detail">${t('warehouse.common.detail')}</button>`]
const rawStatus = row._raw?.receiptStatus if (hasPermission('Warehouse.InboundNotice.Delete') && row._status === 'forecasted') {
if (hasPermission('Warehouse.InboundNotice.Delete') && rawStatus === 'forecasted') {
parts.push(`<button type="button" class="hot-action-btn" data-action="delete">${t('warehouse.common.delete')}</button>`) parts.push(`<button type="button" class="hot-action-btn" data-action="delete">${t('warehouse.common.delete')}</button>`)
} }
return parts.join('') return parts.join('')
@ -212,35 +256,73 @@ onMounted(async () => {
</script> </script>
<template> <template>
<div class="warehouse-inbound-container"> <div class="warehouse-inbound-container warehouse-list-page">
<div class="page-card"> <div class="warehouse-list-page-card">
<a-tabs v-model:active-key="receiptStatus" @change="() => { currentPage = 1; loadList() }"> <a-tabs
v-model:active-key="receiptStatus"
class="status-tabs"
@change="() => { currentPage = 1; loadList() }"
>
<a-tab-pane v-for="tab in statusTabs" :key="tab.value ?? 'all'"> <a-tab-pane v-for="tab in statusTabs" :key="tab.value ?? 'all'">
<template #title> <template #title>
{{ t(tab.labelKey) }} {{ t(tab.labelKey) }}
<span v-if="tab.countKey" class="tab-count">({{ summary[tab.countKey as 'forecasted' | 'completed'] }})</span> <span v-if="tab.countKey" class="tab-count">({{ summary[tab.countKey] }})</span>
</template> </template>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
<div class="table-toolbar"> <div class="table-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<a-space wrap :size="8"> <a-space wrap :size="8">
<a-select v-model="warehouseId" allow-clear :placeholder="t('warehouse.common.warehouse')" :options="warehouseOptions" style="width: 200px" @change="() => { loadSummary(); currentPage = 1; loadList() }" /> <a-select
<a-input v-model="keyword" allow-clear :placeholder="t('warehouse.inbound.search.keyword')" style="width: 200px" /> v-model="warehouseId"
<a-button type="primary" @click="() => { currentPage = 1; loadList() }">{{ t('warehouse.common.search') }}</a-button> allow-clear
<a-button v-if="hasPermission('Warehouse.InboundNotice.Save')" type="primary" @click="handleAdd">{{ t('warehouse.common.add') }}</a-button> :placeholder="t('warehouse.common.warehouse')"
:options="warehouseOptions"
style="width: 200px"
@change="() => { loadSummary(); handleSearch() }"
/>
<a-input
v-model="keyword"
allow-clear
:placeholder="t('warehouse.inbound.search.keyword')"
style="width: 200px"
@press-enter="handleSearch"
/>
<a-button type="primary" @click="handleSearch">{{ t('warehouse.common.search') }}</a-button>
<a-button v-if="hasPermission('Warehouse.InboundNotice.Save')" type="primary" @click="handleAdd">
{{ t('warehouse.common.add') }}
</a-button>
</a-space> </a-space>
</div> </div>
<div class="toolbar-right">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div> </div>
<div ref="tableContainerRef" class="list-wrap"> <div ref="tableContainerRef" class="list-wrap">
<HandsontablePro :columns="hotColumns" :data="tableRows" :height="tableHeight" :loading="listLoading" show-action-column :action-renderer="actionRenderer" @action-click="handleActionClick" /> <HandsontablePro
:columns="hotColumns"
:data="tableRows"
:height="tableHeight"
:loading="listLoading"
:stretch-columns="true"
show-action-column
:action-renderer="actionRenderer"
@action-click="handleActionClick"
/>
</div> </div>
<PaginationPro v-model:current="currentPage" v-model:page-size="pageSize" :total="totalCount" @change="loadList" @page-size-change="() => { currentPage = 1; loadList() }" />
</div> </div>
<a-drawer v-model:visible="detailVisible" :title="t('warehouse.inbound.drawer.detail')" :width="640"> <a-drawer v-model:visible="detailVisible" :title="t('warehouse.inbound.drawer.detail')" :width="640">
<template v-if="detailData"> <template v-if="detailData">
<p>{{ t('warehouse.inbound.table.noticeNo') }}: {{ detailData.noticeNo }}</p> <p>{{ t('warehouse.inbound.table.noticeNo') }}: {{ detailData.noticeNo }}</p>
<p>{{ t('warehouse.inbound.table.markReference') }}: {{ detailData.markReference || '-' }}</p>
<div v-for="box in detailData.boxes || []" :key="box.id" class="box-block"> <div v-for="box in detailData.boxes || []" :key="box.id" class="box-block">
<div class="box-title">{{ t('warehouse.inbound.box') }}: {{ box.markBoxNo || box.boxSeqNo }}</div> <div class="box-title">{{ t('warehouse.inbound.box') }}: {{ box.markBoxNo || box.boxSeqNo }}</div>
<a-table :data="box.lines || []" :pagination="false" size="small"> <a-table :data="box.lines || []" :pagination="false" size="small">
@ -281,27 +363,8 @@ onMounted(async () => {
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
.warehouse-inbound-container { @import '@/styles/warehouse-list-page.less';
padding: 16px;
background: #f0f3f4;
min-height: 100%;
display: flex;
flex-direction: column;
}
.page-card {
background: #fff;
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
border-radius: 4px;
}
.list-wrap {
flex: 1;
min-height: 0;
margin-top: 12px;
}
.box-block { .box-block {
margin-bottom: 16px; margin-bottom: 16px;
} }
@ -309,8 +372,4 @@ onMounted(async () => {
font-weight: 500; font-weight: 500;
margin-bottom: 8px; margin-bottom: 8px;
} }
.tab-count {
margin-left: 4px;
color: var(--app-primary-color);
}
</style> </style>

View File

@ -15,6 +15,12 @@ import {
import { getWarehouseList } from '@/api/system/warehouse' import { getWarehouseList } from '@/api/system/warehouse'
import { $baseMessage } from '@/utils' import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission' import { hasPermission } from '@/utils/rbac-permission'
import {
buildWarehouseNameMap,
normalizeListRow,
pickField,
resolveWarehouseLabel,
} from '@/utils/warehouseListMapper'
defineOptions({ name: 'WarehouseInventory' }) defineOptions({ name: 'WarehouseInventory' })
@ -29,6 +35,7 @@ const totalCount = ref(0)
const keyword = ref('') const keyword = ref('')
const warehouseId = ref<number>() const warehouseId = ref<number>()
const warehouseOptions = ref<Array<{ label: string; value: number }>>([]) const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
const warehouseNameMap = computed(() => buildWarehouseNameMap(warehouseOptions.value))
const binDetailVisible = ref(false) const binDetailVisible = ref(false)
const binDetailRows = ref<any[]>([]) const binDetailRows = ref<any[]>([])
@ -45,11 +52,26 @@ const relocateForm = ref({
const hotColumns = computed(() => [ const hotColumns = computed(() => [
{ key: 'skuCode', label: t('warehouse.inventory.table.skuCode'), width: 140 }, { key: 'skuCode', label: t('warehouse.inventory.table.skuCode'), width: 140 },
{ key: 'skuName', label: t('warehouse.inventory.table.skuName'), width: 200 }, { key: 'skuName', label: t('warehouse.inventory.table.skuName'), width: 200 },
{ key: 'warehouseId', label: t('warehouse.inventory.table.warehouseId'), width: 100 }, { key: 'warehouseName', label: t('warehouse.inventory.table.warehouse'), width: 140 },
{ key: 'totalQty', label: t('warehouse.inventory.table.totalQty'), width: 90 }, { key: 'totalQty', label: t('warehouse.inventory.table.totalQty'), width: 90 },
{ key: 'binCount', label: t('warehouse.inventory.table.binCount'), width: 90 }, { key: 'binCount', label: t('warehouse.inventory.table.binCount'), width: 90 },
]) ])
function mapRow(raw: Record<string, unknown>) {
const row = normalizeListRow(raw)
const whId = pickField<number>(row, 'warehouseId', 'WarehouseId')
return {
skuId: pickField<number>(row, 'skuId', 'SkuId'),
skuCode: pickField(row, 'skuCode', 'SkuCode') ?? '-',
skuName: pickField(row, 'skuName', 'SkuName') ?? '-',
warehouseId: whId,
warehouseName: resolveWarehouseLabel(whId, warehouseNameMap.value),
totalQty: pickField<number>(row, 'totalQty', 'TotalQty') ?? 0,
binCount: pickField<number>(row, 'binCount', 'BinCount') ?? 0,
_raw: row,
}
}
async function loadWarehouses() { async function loadWarehouses() {
try { try {
const { items } = await getWarehouseList({ page: 1, pageSize: 200 }) const { items } = await getWarehouseList({ page: 1, pageSize: 200 })
@ -71,7 +93,7 @@ async function loadList() {
keyword: keyword.value || undefined, keyword: keyword.value || undefined,
warehouseId: warehouseId.value, warehouseId: warehouseId.value,
}) })
tableRows.value = items || [] tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
totalCount.value = Number(total || 0) totalCount.value = Number(total || 0)
} catch (e: any) { } catch (e: any) {
$baseMessage(e?.message || t('warehouse.inventory.message.loadFailed'), 'error') $baseMessage(e?.message || t('warehouse.inventory.message.loadFailed'), 'error')
@ -82,11 +104,35 @@ async function loadList() {
} }
} }
function handlePageChange(page: number) {
currentPage.value = page
loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
loadList()
}
function handleSearch() {
currentPage.value = 1
loadList()
}
async function showBinDetail(row: any) { async function showBinDetail(row: any) {
activeSku.value = row activeSku.value = row
try { try {
const data = await getWarehouseInventoryBinDetail(row.skuId, row.warehouseId) const data = await getWarehouseInventoryBinDetail(row.skuId, row.warehouseId)
binDetailRows.value = data?.items ?? data?.Items ?? (Array.isArray(data) ? data : []) const items = data?.items ?? data?.Items ?? (Array.isArray(data) ? data : [])
binDetailRows.value = (items || []).map((item: Record<string, unknown>) => {
const normalized = normalizeListRow(item)
return {
binCode: pickField(normalized, 'binCode', 'BinCode') ?? '-',
binName: pickField(normalized, 'binName', 'BinName') ?? '',
qty: pickField<number>(normalized, 'qty', 'Qty') ?? 0,
}
})
binDetailVisible.value = true binDetailVisible.value = true
} catch (e: any) { } catch (e: any) {
$baseMessage(e?.message || t('warehouse.inventory.message.binDetailFailed'), 'error') $baseMessage(e?.message || t('warehouse.inventory.message.binDetailFailed'), 'error')
@ -148,8 +194,8 @@ onMounted(async () => {
</script> </script>
<template> <template>
<div class="warehouse-inventory-container"> <div class="warehouse-inventory-container warehouse-list-page">
<div class="page-card"> <div class="warehouse-list-page-card">
<div class="table-toolbar"> <div class="table-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<a-space wrap :size="8"> <a-space wrap :size="8">
@ -160,10 +206,26 @@ onMounted(async () => {
:options="warehouseOptions" :options="warehouseOptions"
style="width: 200px" style="width: 200px"
/> />
<a-input v-model="keyword" allow-clear :placeholder="t('warehouse.inventory.search.keyword')" style="width: 200px" /> <a-input
<a-button type="primary" @click="() => { currentPage = 1; loadList() }">{{ t('warehouse.common.search') }}</a-button> v-model="keyword"
allow-clear
:placeholder="t('warehouse.inventory.search.keyword')"
style="width: 200px"
@press-enter="handleSearch"
/>
<a-button type="primary" @click="handleSearch">{{ t('warehouse.common.search') }}</a-button>
</a-space> </a-space>
</div> </div>
<div class="toolbar-right">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div> </div>
<div ref="tableContainerRef" class="list-wrap"> <div ref="tableContainerRef" class="list-wrap">
<HandsontablePro <HandsontablePro
@ -171,18 +233,19 @@ onMounted(async () => {
:data="tableRows" :data="tableRows"
:height="tableHeight" :height="tableHeight"
:loading="listLoading" :loading="listLoading"
:stretch-columns="true"
show-action-column show-action-column
:action-renderer="actionRenderer" :action-renderer="actionRenderer"
@action-click="handleActionClick" @action-click="handleActionClick"
/> />
</div> </div>
<PaginationPro v-model:current="currentPage" v-model:page-size="pageSize" :total="totalCount" @change="loadList" @page-size-change="() => { currentPage = 1; loadList() }" />
</div> </div>
<a-drawer v-model:visible="binDetailVisible" :title="t('warehouse.inventory.drawer.binDetail')" :width="520"> <a-drawer v-model:visible="binDetailVisible" :title="t('warehouse.inventory.drawer.binDetail')" :width="520">
<a-table :data="binDetailRows" :pagination="false" size="small"> <a-table :data="binDetailRows" :pagination="false" size="small">
<template #columns> <template #columns>
<a-table-column :title="t('warehouse.inventory.table.binCode')" data-index="binCode" /> <a-table-column :title="t('warehouse.inventory.table.binCode')" data-index="binCode" />
<a-table-column :title="t('warehouse.inventory.table.binName')" data-index="binName" />
<a-table-column :title="t('warehouse.inventory.table.qty')" data-index="qty" /> <a-table-column :title="t('warehouse.inventory.table.qty')" data-index="qty" />
</template> </template>
</a-table> </a-table>
@ -209,25 +272,5 @@ onMounted(async () => {
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
.warehouse-inventory-container { @import '@/styles/warehouse-list-page.less';
padding: 16px;
background: #f0f3f4;
min-height: 100%;
display: flex;
flex-direction: column;
}
.page-card {
background: #fff;
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
border-radius: 4px;
}
.list-wrap {
flex: 1;
min-height: 0;
margin-top: 12px;
}
</style> </style>

View File

@ -12,6 +12,14 @@ import { deleteWarehouseParcel, getWarehouseParcelList, saveWarehouseParcel } fr
import { getWarehouseList } from '@/api/system/warehouse' import { getWarehouseList } from '@/api/system/warehouse'
import { $baseMessage } from '@/utils' import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission' import { hasPermission } from '@/utils/rbac-permission'
import {
buildWarehouseNameMap,
formatDateTime,
normalizeListRow,
pickField,
resolveWarehouseLabel,
summarizeParcelItems,
} from '@/utils/warehouseListMapper'
defineOptions({ name: 'WarehouseParcel' }) defineOptions({ name: 'WarehouseParcel' })
@ -22,6 +30,7 @@ const lifecycleStatus = ref<string>()
const keyword = ref('') const keyword = ref('')
const warehouseId = ref<number>() const warehouseId = ref<number>()
const warehouseOptions = ref<Array<{ label: string; value: number }>>([]) const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
const warehouseNameMap = computed(() => buildWarehouseNameMap(warehouseOptions.value))
const listLoading = ref(false) const listLoading = ref(false)
const tableRows = ref<any[]>([]) const tableRows = ref<any[]>([])
const currentPage = ref(1) const currentPage = ref(1)
@ -51,10 +60,32 @@ const hotColumns = computed(() => [
{ key: 'trackingNo', label: t('warehouse.parcel.table.trackingNo'), width: 160 }, { key: 'trackingNo', label: t('warehouse.parcel.table.trackingNo'), width: 160 },
{ key: 'carrierName', label: t('warehouse.parcel.table.carrierName'), width: 120 }, { key: 'carrierName', label: t('warehouse.parcel.table.carrierName'), width: 120 },
{ key: 'lifecycleStatus', label: t('warehouse.parcel.table.lifecycleStatus'), width: 100 }, { key: 'lifecycleStatus', label: t('warehouse.parcel.table.lifecycleStatus'), width: 100 },
{ key: 'warehouseId', label: t('warehouse.parcel.table.warehouseId'), width: 90 }, { key: 'warehouseName', label: t('warehouse.parcel.table.warehouse'), width: 140 },
{ key: 'remark', label: t('warehouse.parcel.table.remark'), width: 160 }, { key: 'itemsSummary', label: t('warehouse.parcel.table.items'), width: 200 },
{ key: 'arrivalTime', label: t('warehouse.parcel.table.arrivalTime'), width: 160 },
{ key: 'createTime', label: t('warehouse.parcel.table.createTime'), width: 160 },
{ key: 'remark', label: t('warehouse.parcel.table.remark'), width: 140 },
]) ])
function mapRow(raw: Record<string, unknown>) {
const row = normalizeListRow(raw)
const status = String(pickField(row, 'lifecycleStatus', 'LifecycleStatus') ?? '')
const whId = pickField<number>(row, 'warehouseId', 'WarehouseId')
return {
id: pickField<number>(row, 'id', 'Id'),
trackingNo: pickField(row, 'trackingNo', 'TrackingNo') ?? '-',
carrierName: pickField(row, 'carrierName', 'CarrierName') ?? '-',
lifecycleStatus: t(`warehouse.parcel.status.${status}`, status),
warehouseName: resolveWarehouseLabel(whId, warehouseNameMap.value),
itemsSummary: summarizeParcelItems(pickField(row, 'items', 'Items')),
arrivalTime: formatDateTime(pickField(row, 'arrivalTime', 'ArrivalTime')),
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')),
remark: pickField(row, 'remark', 'Remark') ?? '',
_raw: row,
_status: status,
}
}
async function loadWarehouses() { async function loadWarehouses() {
try { try {
const { items } = await getWarehouseList({ page: 1, pageSize: 200 }) const { items } = await getWarehouseList({ page: 1, pageSize: 200 })
@ -77,11 +108,7 @@ async function loadList() {
keyword: keyword.value || undefined, keyword: keyword.value || undefined,
warehouseId: warehouseId.value, warehouseId: warehouseId.value,
}) })
tableRows.value = (items || []).map((r: any) => ({ tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
...r,
lifecycleStatus: t(`warehouse.parcel.status.${r.lifecycleStatus}`, r.lifecycleStatus),
_raw: r,
}))
totalCount.value = Number(total || 0) totalCount.value = Number(total || 0)
} catch (e: any) { } catch (e: any) {
$baseMessage(e?.message || t('warehouse.parcel.message.loadFailed'), 'error') $baseMessage(e?.message || t('warehouse.parcel.message.loadFailed'), 'error')
@ -92,6 +119,22 @@ async function loadList() {
} }
} }
function handlePageChange(page: number) {
currentPage.value = page
loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
loadList()
}
function handleSearch() {
currentPage.value = 1
loadList()
}
function handleAdd() { function handleAdd() {
if (!hasPermission('Warehouse.Parcel.Save')) return if (!hasPermission('Warehouse.Parcel.Save')) return
formModel.id = 0 formModel.id = 0
@ -126,18 +169,21 @@ function handleDelete(row: any) {
if (!hasPermission('Warehouse.Parcel.Delete')) return if (!hasPermission('Warehouse.Parcel.Delete')) return
Modal.confirm({ Modal.confirm({
title: t('warehouse.parcel.actions.delete'), title: t('warehouse.parcel.actions.delete'),
content: row._raw?.trackingNo || row.trackingNo, content: row.trackingNo,
onOk: async () => { onOk: async () => {
await deleteWarehouseParcel(row._raw?.id || row.id) await deleteWarehouseParcel(row.id)
$baseMessage(t('warehouse.parcel.message.deleteSuccess'), 'success') $baseMessage(t('warehouse.parcel.message.deleteSuccess'), 'success')
await loadList() await loadList()
}, },
}) })
} }
function actionRenderer() { function actionRenderer(row: any) {
const parts: string[] = [] const parts: string[] = []
if (hasPermission('Warehouse.Parcel.Delete')) { if (hasPermission('Warehouse.Parcel.Save') && row._status !== 'stored') {
parts.push(`<button type="button" class="hot-action-btn" data-action="edit">${t('warehouse.common.edit')}</button>`)
}
if (hasPermission('Warehouse.Parcel.Delete') && row._status !== 'stored') {
parts.push(`<button type="button" class="hot-action-btn" data-action="delete">${t('warehouse.common.delete')}</button>`) parts.push(`<button type="button" class="hot-action-btn" data-action="delete">${t('warehouse.common.delete')}</button>`)
} }
return parts.join('') return parts.join('')
@ -154,25 +200,61 @@ onMounted(async () => {
</script> </script>
<template> <template>
<div class="warehouse-parcel-container"> <div class="warehouse-parcel-container warehouse-list-page">
<div class="page-card"> <div class="warehouse-list-page-card">
<a-tabs v-model:active-key="lifecycleStatus" @change="() => { currentPage = 1; loadList() }"> <a-tabs
v-model:active-key="lifecycleStatus"
class="status-tabs"
@change="() => { currentPage = 1; loadList() }"
>
<a-tab-pane v-for="tab in statusTabs" :key="tab.value ?? 'all'" :title="t(tab.labelKey)" /> <a-tab-pane v-for="tab in statusTabs" :key="tab.value ?? 'all'" :title="t(tab.labelKey)" />
</a-tabs> </a-tabs>
<div class="table-toolbar"> <div class="table-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<a-space wrap :size="8"> <a-space wrap :size="8">
<a-select v-model="warehouseId" allow-clear :placeholder="t('warehouse.common.warehouse')" :options="warehouseOptions" style="width: 200px" /> <a-select
<a-input v-model="keyword" allow-clear :placeholder="t('warehouse.parcel.search.trackingNo')" style="width: 200px" /> v-model="warehouseId"
<a-button type="primary" @click="() => { currentPage = 1; loadList() }">{{ t('warehouse.common.search') }}</a-button> allow-clear
<a-button v-if="hasPermission('Warehouse.Parcel.Save')" type="primary" @click="handleAdd">{{ t('warehouse.common.add') }}</a-button> :placeholder="t('warehouse.common.warehouse')"
:options="warehouseOptions"
style="width: 200px"
/>
<a-input
v-model="keyword"
allow-clear
:placeholder="t('warehouse.parcel.search.trackingNo')"
style="width: 200px"
@press-enter="handleSearch"
/>
<a-button type="primary" @click="handleSearch">{{ t('warehouse.common.search') }}</a-button>
<a-button v-if="hasPermission('Warehouse.Parcel.Save')" type="primary" @click="handleAdd">
{{ t('warehouse.common.add') }}
</a-button>
</a-space> </a-space>
</div> </div>
<div class="toolbar-right">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div> </div>
<div ref="tableContainerRef" class="list-wrap"> <div ref="tableContainerRef" class="list-wrap">
<HandsontablePro :columns="hotColumns" :data="tableRows" :height="tableHeight" :loading="listLoading" show-action-column :action-renderer="actionRenderer" @action-click="handleActionClick" /> <HandsontablePro
:columns="hotColumns"
:data="tableRows"
:height="tableHeight"
:loading="listLoading"
:stretch-columns="true"
show-action-column
:action-renderer="actionRenderer"
@action-click="handleActionClick"
/>
</div> </div>
<PaginationPro v-model:current="currentPage" v-model:page-size="pageSize" :total="totalCount" @change="loadList" @page-size-change="() => { currentPage = 1; loadList() }" />
</div> </div>
<ModalPro v-model:visible="modalVisible" :title="t('warehouse.parcel.modal.add')" :width="640" @cancel="closeModal"> <ModalPro v-model:visible="modalVisible" :title="t('warehouse.parcel.modal.add')" :width="640" @cancel="closeModal">
@ -202,25 +284,5 @@ onMounted(async () => {
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
.warehouse-parcel-container { @import '@/styles/warehouse-list-page.less';
padding: 16px;
background: #f0f3f4;
min-height: 100%;
display: flex;
flex-direction: column;
}
.page-card {
background: #fff;
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
border-radius: 4px;
}
.list-wrap {
flex: 1;
min-height: 0;
margin-top: 12px;
}
</style> </style>

View File

@ -11,6 +11,12 @@ import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import { deleteWarehouseSku, getWarehouseSkuList, saveWarehouseSku } from '@/api/warehouse' import { deleteWarehouseSku, getWarehouseSkuList, saveWarehouseSku } from '@/api/warehouse'
import { $baseMessage } from '@/utils' import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission' import { hasPermission } from '@/utils/rbac-permission'
import {
formatDateTime,
formatDimensions,
normalizeListRow,
pickField,
} from '@/utils/warehouseListMapper'
defineOptions({ name: 'WarehouseSku' }) defineOptions({ name: 'WarehouseSku' })
@ -40,10 +46,12 @@ const formModel = reactive({
const hotColumns = computed(() => [ const hotColumns = computed(() => [
{ key: 'skuCode', label: t('warehouse.sku.table.skuCode'), width: 140 }, { key: 'skuCode', label: t('warehouse.sku.table.skuCode'), width: 140 },
{ key: 'skuName', label: t('warehouse.sku.table.skuName'), width: 200 }, { key: 'skuName', label: t('warehouse.sku.table.skuName'), width: 180 },
{ key: 'totalQty', label: t('warehouse.sku.table.totalQty'), width: 90 }, { key: 'totalQty', label: t('warehouse.sku.table.totalQty'), width: 80 },
{ key: 'weightG', label: t('warehouse.sku.table.weightG'), width: 90 }, { key: 'weightG', label: t('warehouse.sku.table.weightG'), width: 90 },
{ key: 'remark', label: t('warehouse.sku.table.remark'), width: 160 }, { key: 'dimensions', label: t('warehouse.sku.table.dimensions'), width: 120 },
{ key: 'remark', label: t('warehouse.sku.table.remark'), width: 140 },
{ key: 'createTime', label: t('warehouse.sku.table.createTime'), width: 160 },
]) ])
function resetForm() { function resetForm() {
@ -58,6 +66,21 @@ function resetForm() {
formModel.remark = '' formModel.remark = ''
} }
function mapRow(raw: Record<string, unknown>) {
const row = normalizeListRow(raw)
return {
id: pickField<number>(row, 'id', 'Id'),
skuCode: pickField(row, 'skuCode', 'SkuCode') ?? '-',
skuName: pickField(row, 'skuName', 'SkuName') ?? '-',
totalQty: pickField<number>(row, 'totalQty', 'TotalQty') ?? 0,
weightG: pickField(row, 'weightG', 'WeightG') ?? '-',
dimensions: formatDimensions(row),
remark: pickField(row, 'remark', 'Remark') ?? '',
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')),
_raw: row,
}
}
async function loadList() { async function loadList() {
listLoading.value = true listLoading.value = true
try { try {
@ -66,15 +89,7 @@ async function loadList() {
pageSize: pageSize.value, pageSize: pageSize.value,
keyword: keyword.value || undefined, keyword: keyword.value || undefined,
}) })
tableRows.value = (items || []).map((r: any) => ({ tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
id: r.id,
skuCode: r.skuCode,
skuName: r.skuName,
totalQty: r.totalQty ?? 0,
weightG: r.weightG ?? '-',
remark: r.remark ?? '',
_raw: r,
}))
totalCount.value = Number(total || 0) totalCount.value = Number(total || 0)
} catch (e: any) { } catch (e: any) {
$baseMessage(e?.message || t('warehouse.sku.message.loadFailed'), 'error') $baseMessage(e?.message || t('warehouse.sku.message.loadFailed'), 'error')
@ -85,6 +100,17 @@ async function loadList() {
} }
} }
function handlePageChange(page: number) {
currentPage.value = page
loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
loadList()
}
function handleSearch() { function handleSearch() {
currentPage.value = 1 currentPage.value = 1
loadList() loadList()
@ -99,15 +125,15 @@ function handleAdd() {
function handleEdit(row: any) { function handleEdit(row: any) {
if (!hasPermission('Warehouse.Sku.Save')) return if (!hasPermission('Warehouse.Sku.Save')) return
const raw = row._raw || row const raw = row._raw || row
formModel.id = raw.id formModel.id = Number(pickField(raw, 'id', 'Id') ?? 0)
formModel.skuCode = raw.skuCode || '' formModel.skuCode = String(pickField(raw, 'skuCode', 'SkuCode') ?? '')
formModel.skuName = raw.skuName || '' formModel.skuName = String(pickField(raw, 'skuName', 'SkuName') ?? '')
formModel.skuImageUrl = raw.skuImageUrl || '' formModel.skuImageUrl = String(pickField(raw, 'skuImageUrl', 'SkuImageUrl') ?? '')
formModel.weightG = raw.weightG formModel.weightG = pickField<number>(raw, 'weightG', 'WeightG')
formModel.lengthCm = raw.lengthCm formModel.lengthCm = pickField<number>(raw, 'lengthCm', 'LengthCm')
formModel.widthCm = raw.widthCm formModel.widthCm = pickField<number>(raw, 'widthCm', 'WidthCm')
formModel.heightCm = raw.heightCm formModel.heightCm = pickField<number>(raw, 'heightCm', 'HeightCm')
formModel.remark = raw.remark || '' formModel.remark = String(pickField(raw, 'remark', 'Remark') ?? '')
openModal() openModal()
} }
@ -146,7 +172,7 @@ function handleDelete(row: any) {
}) })
} }
function actionRenderer(row: any) { function actionRenderer() {
const parts: string[] = [] const parts: string[] = []
if (hasPermission('Warehouse.Sku.Save')) { if (hasPermission('Warehouse.Sku.Save')) {
parts.push(`<button type="button" class="hot-action-btn" data-action="edit">${t('warehouse.common.edit')}</button>`) parts.push(`<button type="button" class="hot-action-btn" data-action="edit">${t('warehouse.common.edit')}</button>`)
@ -166,8 +192,8 @@ onMounted(loadList)
</script> </script>
<template> <template>
<div class="warehouse-sku-container"> <div class="warehouse-sku-container warehouse-list-page">
<div class="page-card"> <div class="warehouse-list-page-card">
<div class="table-toolbar"> <div class="table-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<a-space wrap :size="8"> <a-space wrap :size="8">
@ -184,6 +210,16 @@ onMounted(loadList)
</a-button> </a-button>
</a-space> </a-space>
</div> </div>
<div class="toolbar-right">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div> </div>
<div ref="tableContainerRef" class="list-wrap"> <div ref="tableContainerRef" class="list-wrap">
@ -192,22 +228,20 @@ onMounted(loadList)
:data="tableRows" :data="tableRows"
:height="tableHeight" :height="tableHeight"
:loading="listLoading" :loading="listLoading"
:stretch-columns="true"
show-action-column show-action-column
:action-renderer="actionRenderer" :action-renderer="actionRenderer"
@action-click="handleActionClick" @action-click="handleActionClick"
/> />
</div> </div>
<PaginationPro
v-model:current="currentPage"
v-model:page-size="pageSize"
:total="totalCount"
@change="loadList"
@page-size-change="() => { currentPage = 1; loadList() }"
/>
</div> </div>
<ModalPro v-model:visible="modalVisible" :title="formModel.id ? t('warehouse.sku.modal.edit') : t('warehouse.sku.modal.add')" :width="720" @cancel="closeModal"> <ModalPro
v-model:visible="modalVisible"
:title="formModel.id ? t('warehouse.sku.modal.edit') : t('warehouse.sku.modal.add')"
:width="720"
@cancel="closeModal"
>
<FormPro :model="formModel" layout="horizontal" :row-gutter="20" :default-span="12"> <FormPro :model="formModel" layout="horizontal" :row-gutter="20" :default-span="12">
<a-form-item :label="t('warehouse.sku.form.skuCode')" field="skuCode" required> <a-form-item :label="t('warehouse.sku.form.skuCode')" field="skuCode" required>
<a-input v-model="formModel.skuCode" /> <a-input v-model="formModel.skuCode" />
@ -231,25 +265,5 @@ onMounted(loadList)
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
.warehouse-sku-container { @import '@/styles/warehouse-list-page.less';
padding: 16px;
background: #f0f3f4;
min-height: 100%;
display: flex;
flex-direction: column;
}
.page-card {
background: #fff;
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
border-radius: 4px;
}
.list-wrap {
flex: 1;
min-height: 0;
margin-top: 12px;
}
</style> </style>

View File

@ -44,6 +44,38 @@ export default defineConfig(({ mode }) => {
target: 'http://api.nanafa.com/admin', target: 'http://api.nanafa.com/admin',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const orderSpaPrefixes = [
'/order/guide',
'/order/priceCheck',
'/order/placeOrder',
'/order/myOrders',
'/order/batchJobs',
'/order/templateMapping',
'/order/platformStore',
'/order/platformStoreOrder',
];
if (orderSpaPrefixes.some((p) => path === p || path.startsWith(`${p}/`))) {
return '/index.html';
}
},
},
// OMS 仓储 admin 接口:/warehouse/sku/list 等(与 Swagger OMS-Warehouse 一致)
'/warehouse': {
target: 'http://api.nanafa.com/admin',
changeOrigin: true,
secure: false,
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const warehouseSpaExact = ['/warehouse/parcel', '/warehouse/inventory', '/warehouse/sku'];
if (warehouseSpaExact.some((p) => path === p || path === `${p}/`)) {
return '/index.html';
}
if (path === '/warehouse/inboundNotice' || path.startsWith('/warehouse/inboundNotice/')) {
return '/index.html';
}
},
}, },
'/monitor': { '/monitor': {
target: 'http://api.nanafa.com/admin', target: 'http://api.nanafa.com/admin',
@ -265,6 +297,13 @@ export default defineConfig(({ mode }) => {
'/order/placeOrder', '/order/placeOrder',
'/order/myOrders', '/order/myOrders',
'/order/batchJobs', '/order/batchJobs',
'/order/platformStore',
'/order/platformStoreOrder',
// 仓储前端路由(页面路径与 API 子路径相同时用精确匹配,见 /warehouse 代理 bypass)
'/warehouse/parcel',
'/warehouse/inventory',
'/warehouse/sku',
'/warehouse/inboundNotice',
// Xhs 相关前端路由(勿用 /xhs 前缀,否则 API 会被误判为 SPA) // Xhs 相关前端路由(勿用 /xhs 前缀,否则 API 会被误判为 SPA)
'/xhs/startService', '/xhs/startService',
'/xhs/analysis', '/xhs/analysis',