front/src/views/order/platformStoreOrder/index.vue
zzh 209a6e1c2a
Some checks failed
Build and Deploy Front / deploy (push) Has been cancelled
fix: 创建运单到仓方式对齐下单页(仓库地址 Popover/上门揽收字段)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-20 11:48:54 +08:00

1055 lines
36 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>>({})
type WarehouseOption = {
label: string
value: number
recipient?: string
contactPhone?: string
shippingAddress?: string
remark?: string
}
const warehouseOptions = ref<WarehouseOption[]>([])
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.WarehouseName || `#${w.id ?? w.Id}`,
value: Number(w.id ?? w.Id),
recipient: String(w.recipient ?? w.Recipient ?? '').trim() || undefined,
contactPhone: String(w.contactPhone ?? w.ContactPhone ?? '').trim() || undefined,
shippingAddress: String(w.shippingAddress ?? w.ShippingAddress ?? '').trim() || undefined,
remark: String(w.remark ?? w.Remark ?? '').trim() || undefined,
}))
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>