Some checks failed
Build and Deploy Front / deploy (push) Has been cancelled
Co-authored-by: Cursor <cursoragent@cursor.com>
1055 lines
36 KiB
Vue
1055 lines
36 KiB
Vue
<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>
|