front/src/views/order/platformStoreOrder/index.vue
zzh b178a2a35d
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m56s
feat: 支付选券、优惠入应收展示、申报产品标识编码、创建9610订单等
- 支付弹窗(PC/H5)支持选券与批量支付体验优化,运单管理复用我的订单支付弹窗;立即支付仅允许待支付订单
- 订单详情费用明细展示优惠券抵扣/会员折扣负数行与「含优惠」;价格配置与手工加费过滤抵扣费用项
- 申报明细新增产品标识编码/非标产品标识编码/标准产品标识编码(PC/H5 下单、改单、详情、开放接口文档、公式目录、Excel 模板)
- 9610 页「批量申报」改为「创建9610订单」:选运单并补充出口资质备案、电商平台
- 店铺订单批量按钮按动作独立 loading,获取面单限并发并显示进度
- 另含:国家管理、登录门户切换、监控与工作台、公告弹窗、nginx/vite 配置等改动
2026-10-19 16:34:06 +08:00

1043 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, defineAsyncComponent, nextTick, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Modal } from '@arco-design/web-vue'
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import PaginationPro from '@/components/PaginationPro/index.vue'
import SlidingTabs from '@/components/SlidingTabs/index.vue'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
import { getSalesProductList } from '@/api/channel/salesProduct'
import {
deletePlatformStoreOrder,
fetchPlatformStoreOrderLabel,
getPlatformStoreOrderList,
getPlatformStoreOrderSummary,
matchPlatformStoreOrderInventory,
readyToShipPlatformStoreOrder,
syncPlatformStoreOrderStatus,
togglePlatformStoreOrderSuspend,
} from '@/api/order/platformStore'
import { getWarehouseList } from '@/api/system/warehouse'
import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission'
import { runWithConcurrency } from '@/utils/runWithConcurrency'
import {
buildWarehouseNameMap,
formatDateTime,
formatMoney,
normalizeListRow,
pickField,
resolveWarehouseLabel,
} from '@/utils/warehouseListMapper'
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
import { buildStoreOrderStatusTagHtml } from '@/constants/storeOrderStatus'
import { WMS_PRODUCT_GROUP, type CreateLogisticsMode } from './constants'
const PullOrdersModal = defineAsyncComponent(() => import('./PullOrdersModal.vue'))
const StoreOrderDetailDrawer = defineAsyncComponent(() => import('./StoreOrderDetailDrawer.vue'))
const CreateLogisticsModal = defineAsyncComponent(() => import('./CreateLogisticsModal.vue'))
const OrderPrintModal = defineAsyncComponent(() => import('@/components/PrintTemplate/OrderPrintModal.vue'))
defineOptions({ name: 'PlatformStoreOrder' })
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const { tableContainerRef, tableHeight } = useTableContainerHeight()
const {
isAdmin,
customerId: adminCustomerId,
customerOptions,
withCustomerId,
ensureMutationCustomerId,
} = useWarehouseAdminCustomer()
/** Tab active-key;全部用 'all',请求时再转成不?fulfillStatus / platformCode */
const STATUS_TAB_ALL = 'all'
const PLATFORM_TAB_ALL = 'all'
const ALLOWED_FULFILL_STATUS = new Set(['pending', 'toShip', 'shipped', 'canceled', 'completed'])
const ALLOWED_PLATFORM = new Set(['tiktok', 'temu', 'shopee', 'lazada'])
function resolveFulfillStatusFromQuery(): string {
const raw = route.query.fulfillStatus
const value = Array.isArray(raw) ? raw[0] : raw
if (typeof value === 'string' && ALLOWED_FULFILL_STATUS.has(value)) {
return value
}
return STATUS_TAB_ALL
}
function resolvePlatformFromQuery(): string {
const raw = route.query.platformCode
const value = Array.isArray(raw) ? raw[0] : raw
if (typeof value === 'string' && ALLOWED_PLATFORM.has(value.toLowerCase())) {
return value.toLowerCase()
}
return PLATFORM_TAB_ALL
}
const fulfillStatus = ref<string>(resolveFulfillStatusFromQuery())
const platformCode = ref<string>(resolvePlatformFromQuery())
const keyword = ref('')
const summary = ref<Record<string, number>>({})
const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
const warehouseNameMap = computed(() => buildWarehouseNameMap(warehouseOptions.value))
const productOptions = ref<Array<{ label: string; value: string }>>([])
const wmsProductOptions = ref<Array<{ label: string; value: string }>>([])
const wmsDefaultProductCode = ref<string>()
const listLoading = ref(false)
const tableRows = ref<any[]>([])
const selectedRows = ref<any[]>([])
const selectedRowCount = computed(() => selectedRows.value.length)
const hasSelectedRows = computed(() => selectedRowCount.value > 0)
const currentPage = ref(1)
const pageSize = ref(50)
const totalCount = ref(0)
const pullVisible = ref(false)
type StoreOrderBatchAction = 'sync' | 'rts' | 'label' | 'suspend' | 'resume'
/** 正在执行的批量动作:只有对应按钮转圈,其余批量按钮仅禁用 */
const runningAction = ref<StoreOrderBatchAction | null>(null)
const actionLoading = computed(() => runningAction.value !== null)
const labelProgress = ref({ done: 0, total: 0 })
const LABEL_FETCH_CONCURRENCY = 3
const matchWarehouseId = ref<number>()
const matchProductCode = ref<string>()
const detailDrawerMounted = ref(false)
const detailDrawerRef = ref<{
open?: (opts: { id: number; customerId?: number }) => void
} | null>(null)
const createModalVisible = ref(false)
async function ensureStoreDetailDrawer() {
detailDrawerMounted.value = true
for (let i = 0; i < 40; i++) {
await nextTick()
if (detailDrawerRef.value?.open) return detailDrawerRef.value
await new Promise((r) => setTimeout(r, 40))
}
return detailDrawerRef.value
}
const createModalMode = ref<CreateLogisticsMode>('logistics')
const createModalIds = ref<number[]>([])
const createModalCustomerId = ref<number>()
const platformTabs = computed(() => [
{ value: PLATFORM_TAB_ALL, label: t('common.all') },
{ value: 'tiktok', label: 'TikTok' },
{ value: 'temu', label: 'Temu' },
{ value: 'shopee', label: 'Shopee' },
{ value: 'lazada', label: 'Lazada' },
])
const slidingPlatformTabs = computed(() =>
platformTabs.value.map((tab) => ({ key: tab.value, label: tab.label })),
)
const statusTabs = [
{ value: STATUS_TAB_ALL, labelKey: 'order.platformStoreOrder.status.all', countKey: null },
{ value: 'pending', labelKey: 'order.platformStoreOrder.status.pending', countKey: 'pending' as const },
{ value: 'toShip', labelKey: 'order.platformStoreOrder.status.toShip', countKey: 'toShip' as const },
{ value: 'shipped', labelKey: 'order.platformStoreOrder.status.shipped', countKey: 'shipped' as const },
]
const slidingStatusTabs = computed(() =>
statusTabs.map((tab) => ({
key: tab.value,
label: t(tab.labelKey),
badge: tab.countKey ? summary.value[tab.countKey] ?? 0 : undefined,
})),
)
const hotColumns = computed(() => {
const cols = [
{ key: 'platformOrderNo', label: t('order.platformStoreOrder.table.customsOrderNo'), width: 160 },
{ key: 'platformName', label: t('order.platformStoreOrder.table.platform'), width: 90 },
{ key: 'storeNickname', label: t('order.platformStoreOrder.table.store'), width: 120 },
{ key: 'orderCountry', label: t('order.platformStoreOrder.table.destCountry'), width: 80 },
{ key: 'recipientName', label: t('order.platformStoreOrder.table.recipient'), width: 110 },
{
key: 'fulfillStatus',
label: t('order.platformStoreOrder.table.status'),
width: 110,
renderer: (row: any) =>
buildStoreOrderStatusTagHtml(
String(row.fulfillStatus || '-'),
String(row._status || ''),
Boolean(row._suspended),
),
},
{ key: 'platformStatusLabel', label: t('order.platformStoreOrder.table.platformStatus'), width: 140 },
{ key: 'fulfillmentType', label: t('order.platformStoreOrder.table.fulfillmentType'), width: 160 },
{ key: 'shippingProvider', label: t('order.platformStoreOrder.table.shippingProvider'), width: 120 },
{ key: 'shippingLabelNo', label: t('order.platformStoreOrder.table.shippingLabel'), width: 140 },
{ 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: '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 },
]
if (isAdmin.value) {
cols.unshift(
{ key: 'customerName', label: t('order.myOrders.table.customerName'), width: 120 },
{ key: 'customerCode', label: t('order.myOrders.table.customerCode'), width: 110 },
)
}
return cols
})
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)
// fulfillStatus 本身已是 suspended 时不再拼「已搁置」后缀
if (suspended && status !== 'suspended') {
statusLabel = `${statusLabel}(${t('order.platformStoreOrder.status.suspended')})`
}
return {
id: pickField<number>(row, 'id', 'Id'),
customerId: pickField<number>(row, 'customerId', 'CustomerId'),
customerName: pickField(row, 'customerName', 'CustomerName') ?? '',
customerCode: pickField(row, 'customerCode', 'CustomerCode') ?? '',
platformOrderNo: pickField(row, 'platformOrderNo', 'PlatformOrderNo') ?? '-',
platformName: pickField(row, 'platformName', 'PlatformName') ?? platformCode ?? '-',
storeNickname: pickField(row, 'storeNickname', 'StoreNickname') ?? '-',
orderCountry: pickField(row, 'orderCountry', 'OrderCountry') ?? '-',
recipientName: pickField(row, 'recipientName', 'RecipientName') || '-',
fulfillStatus: statusLabel,
platformStatusLabel: pickField(row, 'platformStatusLabel', 'PlatformStatusLabel') || '-',
fulfillmentType: pickField(row, 'fulfillmentType', 'FulfillmentType') || '-',
shippingProvider: pickField(row, 'shippingProvider', 'ShippingProvider') || '-',
itemCount: pickField<number>(row, 'itemCount', 'ItemCount') ?? '-',
totalAmountDisplay: formatMoney(amount, currency),
linkedServiceNo: pickField(row, 'linkedServiceNo', 'LinkedServiceNo') || '-',
shippingLabelNo: pickField(row, 'shippingLabelNo', 'ShippingLabelNo') ?? '-',
shippingLabelUrl: String(pickField(row, 'shippingLabelUrl', 'ShippingLabelUrl') ?? ''),
warehouseName: resolveWarehouseLabel(whId, warehouseNameMap.value),
platformOrderTime: formatDateTime(pickField(row, 'platformOrderTime', 'PlatformOrderTime')),
lastSyncTime: formatDateTime(pickField(row, 'lastSyncTime', 'LastSyncTime')),
_raw: row,
_status: status,
_suspended: suspended,
_platformCode: platformCode.toLowerCase(),
}
}
function getSelectedIds(): number[] {
return selectedRows.value
.map((row) => Number(row.id ?? pickField(row._raw || row, 'id', 'Id')))
.filter((id) => Number.isFinite(id) && id > 0)
}
function mutationCustomerId(): number | undefined {
return ensureMutationCustomerId()
}
function mapProductOptions(items: any[]): Array<{ label: string; value: string }> {
return (items || [])
.map((p: any) => {
const code = p.productCode || p.ProductCode || ''
const name = p.productName || p.ProductName || code
return { label: `${name} (${code})`, value: code }
})
.filter((p: { value: string }) => !!p.value)
}
async function loadProducts() {
try {
const [{ items }, wmsRes] = await Promise.all([
getSalesProductList({ page: 1, pageSize: 200, isActive: 2 }),
getSalesProductList({
page: 1,
pageSize: 200,
isActive: 2,
productGroup: WMS_PRODUCT_GROUP,
}),
])
productOptions.value = mapProductOptions(items || [])
wmsProductOptions.value = mapProductOptions(wmsRes?.items || [])
if (productOptions.value.length && !matchProductCode.value) {
matchProductCode.value = productOptions.value[0].value
}
wmsDefaultProductCode.value = wmsProductOptions.value[0]?.value
} catch {
productOptions.value = []
wmsProductOptions.value = []
wmsDefaultProductCode.value = undefined
}
}
const createModalProductOptions = computed(() =>
createModalMode.value === 'fulfillWms' ? wmsProductOptions.value : productOptions.value,
)
const createModalDefaultProductCode = computed(() =>
createModalMode.value === 'fulfillWms' ? wmsDefaultProductCode.value : matchProductCode.value,
)
async function loadSummary() {
try {
const data = await getPlatformStoreOrderSummary(
withCustomerId({
platformCode: platformCode.value === PLATFORM_TAB_ALL ? undefined : platformCode.value,
}),
)
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 {
summary.value = {}
}
}
function resolveFulfillStatusFilter(): string | undefined {
const status = fulfillStatus.value
if (!status || status === STATUS_TAB_ALL) return undefined
return status
}
function resolvePlatformFilter(): string | undefined {
const code = platformCode.value
if (!code || code === PLATFORM_TAB_ALL) return undefined
return code
}
async function loadList() {
listLoading.value = true
selectedRows.value = []
try {
const { items, total } = await getPlatformStoreOrderList(
withCustomerId({
page: currentPage.value,
pageSize: pageSize.value,
fulfillStatus: resolveFulfillStatusFilter(),
platformCode: resolvePlatformFilter(),
platformOrderNo: keyword.value || undefined,
}),
)
tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
totalCount.value = Number(total || 0)
} catch (e: any) {
$baseMessage(e?.message || t('order.platformStoreOrder.message.loadFailed'), 'error')
} finally {
listLoading.value = false
}
}
function handlePlatformTabChange(key: string | number) {
platformCode.value = String(key)
currentPage.value = 1
loadSummary()
loadList()
}
function handleStatusTabChange(key: string | number) {
fulfillStatus.value = String(key)
currentPage.value = 1
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 handleCustomerChange() {
currentPage.value = 1
loadSummary()
loadList()
}
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows || []
}
async function showDetail(row: any) {
const id = Number(row._raw?.id || row.id)
if (!id) return
const drawer = await ensureStoreDetailDrawer()
drawer?.open?.({
id,
customerId: withCustomerId({}).customerId,
})
}
function buildMatchPayload(id: number) {
const customerId = mutationCustomerId()
if (isAdmin.value && customerId == null) return null
return {
id,
warehouseId: matchWarehouseId.value,
productCode: matchProductCode.value || undefined,
customerId,
}
}
async function handleMatch(row: any) {
if (!hasPermission('Order.PlatformStoreOrder.MatchInventory')) return
if (!matchWarehouseId.value) {
$baseMessage(t('order.platformStoreOrder.message.warehouseRequired'), 'warning')
return
}
const payload = buildMatchPayload(row._raw?.id || row.id)
if (!payload) return
try {
const result = await matchPlatformStoreOrderInventory(payload)
$baseMessage(result?.message || t('order.platformStoreOrder.message.matchSuccess'), 'success')
await loadSummary()
await loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.platformStoreOrder.message.matchFailed'), 'error')
}
}
function resolveCreatableSelectedIds(): number[] | null {
const selected = selectedRows.value
if (!selected.length) {
$baseMessage(t('order.platformStoreOrder.createLogistics.selectRequired'), 'warning')
return null
}
if (selected.length > 50) {
$baseMessage(t('order.platformStoreOrder.createLogistics.maxLimit'), 'warning')
return null
}
// 不限制履约状态(pending/toShip 等);仍拦截搁置、已关联物流单
const invalid = selected.filter((row) => {
if (row._suspended) return true
if (pickField(row._raw || row, 'linkedOrderId', 'LinkedOrderId')) return true
return false
})
if (invalid.length) {
$baseMessage(
t('order.platformStoreOrder.createLogistics.invalidSelection', { count: invalid.length }),
'warning',
)
return null
}
return selected.map((row) => Number(row.id)).filter((id) => id > 0)
}
function onCreateLogisticsSuccess() {
loadSummary()
loadList()
if (createModalMode.value === 'fulfill' || createModalMode.value === 'fulfillWms') {
void router.push({ name: 'FulfillOrders' })
}
}
function openCreateModal(mode: CreateLogisticsMode) {
const perm =
mode === 'fulfillWms'
? 'Order.PlatformStoreOrder.CreateFulfillWms'
: mode === 'fulfill'
? 'Order.PlatformStoreOrder.CreateFulfill'
: 'Order.PlatformStoreOrder.CreateLogistics'
if (!hasPermission(perm)) return
const customerId = mutationCustomerId()
if (isAdmin.value && customerId == null) return
const ids = resolveCreatableSelectedIds()
if (!ids) return
if (mode === 'fulfillWms' && wmsProductOptions.value.length === 0) {
$baseMessage(t('order.platformStoreOrder.createFulfillWms.productEmpty'), 'warning')
return
}
createModalMode.value = mode
createModalIds.value = ids
// 管理端显式传入客户,避免草稿查库存(orderId=0)丢客户上下文
createModalCustomerId.value = customerId ?? withCustomerId({}).customerId
createModalVisible.value = true
}
function openPullModal() {
if (!hasPermission('Order.PlatformStoreOrder.Pull')) return
if (isAdmin.value && mutationCustomerId() == null) return
pullVisible.value = true
}
function handleSyncStatus() {
if (!hasPermission('Order.PlatformStoreOrder.Sync')) return
const ids = getSelectedIds()
if (!ids.length) {
$baseMessage(t('order.platformStoreOrder.message.selectOrders'), 'warning')
return
}
const customerId = withCustomerId({}).customerId
Modal.confirm({
title: t('order.platformStoreOrder.actions.syncStatus'),
content: t('order.platformStoreOrder.message.syncConfirm', { count: ids.length }),
onOk: async () => {
runningAction.value = 'sync'
try {
const result = await syncPlatformStoreOrderStatus(ids, customerId)
$baseMessage(
result?.message
|| t('order.platformStoreOrder.message.syncResult', {
success: result?.successCount ?? 0,
fail: result?.failCount ?? 0,
}),
(result?.failCount ?? 0) > 0 ? 'warning' : 'success',
)
await loadSummary()
await loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.platformStoreOrder.message.actionFailed'), 'error')
} finally {
runningAction.value = null
}
},
})
}
function handleReadyToShip(ids?: number[]) {
if (!hasPermission('Order.PlatformStoreOrder.Rts')) return
const targetIds = ids?.length ? ids : getSelectedIds()
if (!targetIds.length) {
$baseMessage(t('order.platformStoreOrder.message.selectOrders'), 'warning')
return
}
const customerId = withCustomerId({}).customerId
Modal.confirm({
title: t('order.platformStoreOrder.actions.readyToShip'),
content: t('order.platformStoreOrder.message.rtsConfirm', { count: targetIds.length }),
onOk: async () => {
runningAction.value = 'rts'
try {
const result = await readyToShipPlatformStoreOrder(targetIds, customerId)
$baseMessage(
result?.message
|| t('order.platformStoreOrder.message.rtsResult', {
success: result?.successCount ?? 0,
fail: result?.failCount ?? 0,
}),
(result?.failCount ?? 0) > 0 ? 'warning' : 'success',
)
await loadSummary()
await loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.platformStoreOrder.message.actionFailed'), 'error')
} finally {
runningAction.value = null
}
},
})
}
function handleFetchLabel() {
if (!hasPermission('Order.PlatformStoreOrder.Label')) return
const ids = getSelectedIds()
if (!ids.length) {
$baseMessage(t('order.platformStoreOrder.message.selectOrders'), 'warning')
return
}
const customerId = withCustomerId({}).customerId
Modal.confirm({
title: t('order.platformStoreOrder.actions.fetchLabel'),
content: t('order.platformStoreOrder.message.labelConfirmBatch', { count: ids.length }),
onOk: () => {
void runFetchLabels(ids, customerId)
},
})
}
/** 逐单拉面单(限并发),确认框立即关闭,进度显示在按钮上 */
async function runFetchLabels(ids: number[], customerId?: number) {
runningAction.value = 'label'
labelProgress.value = { done: 0, total: ids.length }
let success = 0
let fail = 0
try {
await runWithConcurrency(ids, LABEL_FETCH_CONCURRENCY, async (id) => {
try {
const result = await fetchPlatformStoreOrderLabel(id, customerId)
if (result?.success === false) fail += 1
else success += 1
} catch {
fail += 1
} finally {
labelProgress.value = { ...labelProgress.value, done: labelProgress.value.done + 1 }
}
})
$baseMessage(
t('order.platformStoreOrder.message.labelResult', { success, fail }),
fail > 0 ? 'warning' : 'success',
)
await loadList()
} finally {
runningAction.value = null
}
}
function handleSuspendBatch(suspend: boolean) {
if (!hasPermission('Order.PlatformStoreOrder.ToggleSuspend')) return
const customerId = mutationCustomerId()
if (isAdmin.value && customerId == null) return
const ids = getSelectedIds()
if (!ids.length) {
$baseMessage(t('order.platformStoreOrder.message.selectOrders'), 'warning')
return
}
Modal.confirm({
title: suspend
? t('order.platformStoreOrder.actions.suspend')
: t('order.platformStoreOrder.actions.resume'),
content: suspend
? t('order.platformStoreOrder.message.suspendConfirm', { count: ids.length })
: t('order.platformStoreOrder.message.resumeConfirm', { count: ids.length }),
onOk: async () => {
runningAction.value = suspend ? 'suspend' : 'resume'
try {
for (const id of ids) {
await togglePlatformStoreOrderSuspend({
id,
isSuspended: suspend,
customerId,
})
}
$baseMessage(t('order.platformStoreOrder.message.suspendSuccess'), 'success')
await loadSummary()
await loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.platformStoreOrder.message.actionFailed'), 'error')
} finally {
runningAction.value = null
}
},
})
}
function isUsableLabelUrl(raw: unknown) {
const value = String(raw ?? '').trim()
if (!value) return false
if (value.includes('获取中')) return false
return /^https?:\/\//i.test(value)
}
const singlePrintVisible = ref(false)
const singlePrintPreparing = ref(false)
const singlePrintItems = ref<Array<{ id: number | string; label: string; url: string }>>([])
async function handleRowPrint(row: any) {
const raw = row._raw || row
const id = Number(row.id ?? pickField(raw, 'id', 'Id'))
if (!id) return
const label = String(row.platformOrderNo || pickField(raw, 'platformOrderNo', 'PlatformOrderNo') || id)
let url = String(row.shippingLabelUrl || pickField(raw, 'shippingLabelUrl', 'ShippingLabelUrl') || '')
if (isUsableLabelUrl(url)) {
singlePrintVisible.value = true
singlePrintPreparing.value = false
singlePrintItems.value = [{ id, label, url }]
return
}
if (!hasPermission('Order.PlatformStoreOrder.Label')) {
$baseMessage(t('order.platformStoreOrder.message.printNoLabel'), 'warning')
return
}
singlePrintVisible.value = true
singlePrintPreparing.value = true
singlePrintItems.value = [{ id, label, url: '' }]
try {
const customerId = withCustomerId({}).customerId
const result = await fetchPlatformStoreOrderLabel(id, customerId)
url = String(result?.shippingLabelUrl || result?.ShippingLabelUrl || '')
singlePrintPreparing.value = false
if (!isUsableLabelUrl(url)) {
$baseMessage(t('order.platformStoreOrder.message.printNoLabel'), 'warning')
singlePrintVisible.value = false
return
}
singlePrintItems.value = [{ id, label, url }]
await loadList()
} catch (e: any) {
singlePrintPreparing.value = false
singlePrintVisible.value = false
$baseMessage(e?.message || t('order.platformStoreOrder.message.labelFailed'), 'error')
}
}
function handleDelete(row: any) {
if (!hasPermission('Order.PlatformStoreOrder.Delete')) return
const customerId = mutationCustomerId()
if (isAdmin.value && customerId == null) return
Modal.confirm({
title: t('order.platformStoreOrder.actions.delete'),
content: row.platformOrderNo,
onOk: async () => {
await deletePlatformStoreOrder([row.id], customerId)
$baseMessage(t('order.platformStoreOrder.message.deleteSuccess'), 'success')
await loadSummary()
await loadList()
},
})
}
function actionRenderer(row: any) {
const parts = [
renderWarehouseActionBtn('detail', t('warehouse.common.detail'), 'view'),
]
const raw = row._raw || row
if (
hasPermission('Order.PlatformStoreOrder.MatchInventory')
&& row._status === 'pending'
&& !pickField(raw, 'linkedOrderId', 'LinkedOrderId')
) {
parts.push(renderWarehouseActionBtn('match', t('order.platformStoreOrder.actions.match'), 'match'))
}
parts.push(renderWarehouseActionBtn('print', t('order.myOrders.actions.print'), 'print'))
if (hasPermission('Order.PlatformStoreOrder.Delete')) {
parts.push(renderWarehouseActionBtn('delete', t('warehouse.common.delete')))
}
return wrapWarehouseActions(parts)
}
function handleActionClick(row: any, _i: number, action?: string) {
if (action === 'detail') showDetail(row)
if (action === 'match') handleMatch(row)
if (action === 'print') void handleRowPrint(row)
if (action === 'delete') handleDelete(row)
}
onMounted(async () => {
// 仓库下拉与列表/汇总/产品并行,避免串行等三轮接口
const warehouseTask = getWarehouseList({ page: 1, pageSize: 200 })
.then(({ items }) => {
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
})
.catch(() => {
warehouseOptions.value = []
})
await Promise.all([warehouseTask, loadProducts(), loadSummary(), loadList()])
})
watch(
() => route.query.fulfillStatus,
() => {
const next = resolveFulfillStatusFromQuery()
if (next === fulfillStatus.value) return
fulfillStatus.value = next
currentPage.value = 1
void loadList()
},
)
</script>
<template>
<div class="platform-store-order-container warehouse-list-page">
<div class="warehouse-list-page-card">
<div class="tabs-actions-section tabs-actions-section--platform">
<div class="tabs-slot">
<SlidingTabs
:model-value="platformCode"
:items="slidingPlatformTabs"
@change="handlePlatformTabChange"
/>
</div>
</div>
<div class="tabs-actions-section">
<div class="tabs-slot">
<SlidingTabs
:model-value="fulfillStatus"
:items="slidingStatusTabs"
@change="handleStatusTabChange"
/>
</div>
<div class="toolbar-left">
<div class="list-filters">
<a-select
v-if="isAdmin"
v-model="adminCustomerId"
allow-clear
:placeholder="t('warehouse.common.selectCustomer')"
:options="customerOptions"
style="width: 200px"
@change="handleCustomerChange"
/>
<a-input
v-model="keyword"
allow-clear
:placeholder="t('order.platformStoreOrder.search.customsOrderNo')"
style="width: 180px"
@press-enter="handleSearch"
/>
<a-button class="app-btn-search" @click="handleSearch">{{ t('warehouse.common.search') }}</a-button>
<a-button
v-if="hasPermission('Order.PlatformStoreOrder.Pull')"
class="app-btn-toolbar"
@click="openPullModal"
>
{{ t('order.platformStoreOrder.actions.pullOrders') }}
</a-button>
</div>
</div>
</div>
<div class="table-toolbar">
<div class="actions-right">
<a-button
v-if="hasPermission('Order.PlatformStoreOrder.Sync')"
class="app-btn-toolbar"
:loading="runningAction === 'sync'"
:disabled="!hasSelectedRows || actionLoading"
@click="handleSyncStatus"
>
{{ t('order.platformStoreOrder.actions.syncStatus') }}
</a-button>
<a-button
v-if="hasPermission('Order.PlatformStoreOrder.Rts')"
class="app-btn-toolbar"
:loading="runningAction === 'rts'"
:disabled="!hasSelectedRows || actionLoading"
@click="handleReadyToShip()"
>
{{ t('order.platformStoreOrder.actions.readyToShip') }}
</a-button>
<a-button
v-if="hasPermission('Order.PlatformStoreOrder.Label')"
class="app-btn-toolbar"
:loading="runningAction === 'label'"
:disabled="!hasSelectedRows || actionLoading"
@click="handleFetchLabel"
>
<template v-if="runningAction === 'label' && labelProgress.total > 1">
{{ t('order.platformStoreOrder.actions.fetchLabelProgress', labelProgress) }}
</template>
<template v-else>{{ t('order.platformStoreOrder.actions.fetchLabel') }}</template>
</a-button>
<a-button
v-if="hasPermission('Order.PlatformStoreOrder.ToggleSuspend')"
class="app-btn-toolbar"
:loading="runningAction === 'suspend'"
:disabled="!hasSelectedRows || actionLoading"
@click="handleSuspendBatch(true)"
>
{{ t('order.platformStoreOrder.actions.suspend') }}
</a-button>
<a-button
v-if="hasPermission('Order.PlatformStoreOrder.ToggleSuspend')"
class="app-btn-toolbar"
:loading="runningAction === 'resume'"
:disabled="!hasSelectedRows || actionLoading"
@click="handleSuspendBatch(false)"
>
{{ t('order.platformStoreOrder.actions.resume') }}
</a-button>
<a-button
v-if="hasPermission('Order.PlatformStoreOrder.CreateLogistics')"
class="app-btn-toolbar"
:disabled="!hasSelectedRows || actionLoading"
@click="openCreateModal('logistics')"
>
{{ t('order.platformStoreOrder.actions.createLogistics') }}
</a-button>
<a-button
v-if="hasPermission('Order.PlatformStoreOrder.CreateFulfill')"
class="app-btn-toolbar"
:disabled="!hasSelectedRows || actionLoading"
@click="openCreateModal('fulfill')"
>
{{ t('order.platformStoreOrder.actions.createFulfill') }}
</a-button>
<a-button
v-if="hasPermission('Order.PlatformStoreOrder.CreateFulfillWms')"
class="app-btn-toolbar"
:disabled="!hasSelectedRows || actionLoading"
@click="openCreateModal('fulfillWms')"
>
{{ t('order.platformStoreOrder.actions.createFulfillWms') }}
</a-button>
</div>
<div class="toolbar-right">
<span class="selected-count">
<i18n-t keypath="order.myOrders.selectedCount" tag="span" scope="global">
<template #count>
<span class="count-number">{{ selectedRowCount }}</span>
</template>
</i18n-t>
</span>
<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 ref="tableContainerRef" class="list-wrap">
<HandsontablePro
:columns="hotColumns"
:data="tableRows"
:height="tableHeight"
:loading="listLoading"
:stretch-columns="true"
show-action-column
:action-renderer="actionRenderer"
@action-click="handleActionClick"
@selection-change="handleSelectionChange"
/>
</div>
</div>
<StoreOrderDetailDrawer
v-if="detailDrawerMounted"
ref="detailDrawerRef"
:warehouse-name-map="warehouseNameMap"
/>
<PullOrdersModal
v-if="pullVisible"
v-model:visible="pullVisible"
:customer-id="withCustomerId({}).customerId"
:platform-code="platformCode"
@success="() => { loadSummary(); loadList() }"
/>
<CreateLogisticsModal
v-if="createModalVisible"
v-model:visible="createModalVisible"
:mode="createModalMode"
:store-order-ids="createModalIds"
:product-options="createModalProductOptions"
:warehouse-options="warehouseOptions"
:default-product-code="createModalDefaultProductCode"
:default-warehouse-id="matchWarehouseId"
:customer-id="createModalCustomerId"
@success="onCreateLogisticsSuccess"
/>
<OrderPrintModal
v-if="singlePrintVisible"
v-model:visible="singlePrintVisible"
mode="single"
:items="singlePrintItems"
:preparing="singlePrintPreparing"
/>
</div>
</template>
<style scoped lang="less">
@import '@/styles/warehouse-list-page.less';
.tabs-actions-section {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
gap: 12px;
flex-shrink: 0;
padding: 10px 16px;
border-bottom: 1px solid var(--color-border-2);
min-height: 52px;
box-sizing: border-box;
&--platform {
min-height: 48px;
border-bottom-color: var(--color-border-1);
}
.tabs-slot {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: center;
overflow: hidden;
}
:deep(.t-tabs) {
flex: 0 1 auto;
min-width: 0;
max-width: 100%;
width: max-content;
}
.toolbar-left {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: flex-end;
height: 32px;
max-width: 100%;
}
.list-filters {
display: inline-flex;
align-items: center;
gap: 8px;
height: 32px;
:deep(.arco-select-view),
:deep(.arco-input-wrapper),
:deep(.arco-btn) {
height: 32px;
}
:deep(.arco-select-view-single),
:deep(.arco-input-wrapper) {
display: inline-flex;
align-items: center;
}
:deep(.arco-input-wrapper) {
padding-top: 0;
padding-bottom: 0;
}
}
}
.table-toolbar {
.actions-right {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
flex: 1;
min-width: 0;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
.selected-count {
font-size: 13px;
color: var(--color-text-3);
white-space: nowrap;
line-height: 32px;
}
}
}
</style>