front/src/views/order/platformStoreOrder/StoreOrderDetailDrawer.vue
zzh 501f0edc8a feat: H5 店铺订单与进站优化,PC 店铺订单工具栏精简
新增 H5 店铺订单列表/创建运单流程,优化登录首屏与分包加载;店铺订单状态标签素雅化;PC 露出创建运单/代发运单并移除仓库产品下拉。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 17:39:44 +08:00

544 lines
19 KiB
Vue

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import DrawerPro from '@/components/DrawerPro/index.vue'
import useDrawer from '@/components/DrawerPro/useDrawer'
import { getPlatformStoreOrderDetail } from '@/api/order/platformStore'
import { $baseMessage } from '@/utils'
import {
formatDateTime,
formatMoney,
normalizeListRow,
pickField,
resolveWarehouseLabel,
} from '@/utils/warehouseListMapper'
import { getStoreOrderStatusTagStyle } from '@/constants/storeOrderStatus'
defineOptions({ name: 'StoreOrderDetailDrawer' })
const props = defineProps<{
warehouseNameMap?: Map<number, string>
}>()
const { t } = useI18n()
const [register, { openDrawer: innerOpen, closeDrawer, setDrawerProps }] = useDrawer() as any
const loading = ref(false)
const detail = ref<Record<string, any> | null>(null)
const display = computed(() => {
const raw = detail.value
if (!raw) return null
const row = normalizeListRow(raw)
const status = String(pickField(row, 'fulfillStatus', 'FulfillStatus') ?? '')
const suspended = Boolean(pickField(row, 'isSuspended', 'IsSuspended'))
const currency = pickField<string>(row, 'currency', 'Currency')
const amount = pickField(row, 'totalAmount', 'TotalAmount')
const warehouseId = pickField<number>(row, 'warehouseId', 'WarehouseId')
const platformCode = String(pickField(row, 'platformCode', 'PlatformCode') ?? '')
const labelUrl = String(pickField(row, 'shippingLabelUrl', 'ShippingLabelUrl') ?? '')
let statusLabel = t(`order.platformStoreOrder.status.${status}`, status || '-')
if (suspended && status !== 'suspended') {
statusLabel = `${statusLabel}(${t('order.platformStoreOrder.status.suspended')})`
}
const linesRaw = (pickField(row, 'lines', 'Lines') as any[]) || []
const lines = linesRaw.map((line) => {
const r = normalizeListRow(line as Record<string, unknown>)
return {
id: pickField(r, 'id', 'Id'),
platformSku: pickField(r, 'platformSku', 'PlatformSku') || '-',
productName: pickField(r, 'productName', 'ProductName') || '-',
productImageUrl: pickField(r, 'productImageUrl', 'ProductImageUrl') || '',
variantText: pickField(r, 'variantText', 'VariantText') || '-',
unitPriceDisplay: formatMoney(pickField(r, 'unitPrice', 'UnitPrice'), pickField(r, 'currency', 'Currency') || currency),
quantity: pickField(r, 'quantity', 'Quantity') ?? 0,
mappedSkuCode: pickField(r, 'mappedSkuCode', 'MappedSkuCode') || '-',
isShipExcluded: Boolean(pickField(r, 'isShipExcluded', 'IsShipExcluded')),
}
})
return {
platformOrderNo: pickField(row, 'platformOrderNo', 'PlatformOrderNo') || '-',
platformCode,
storeNickname: pickField(row, 'storeNickname', 'StoreNickname') || '-',
orderCountry: pickField(row, 'orderCountry', 'OrderCountry') || '-',
status,
statusLabel,
suspended,
itemCount: pickField(row, 'itemCount', 'ItemCount') ?? lines.length,
amountDisplay: formatMoney(amount, currency),
platformOrderTime: formatDateTime(pickField(row, 'platformOrderTime', 'PlatformOrderTime')),
lastSyncTime: formatDateTime(pickField(row, 'lastSyncTime', 'LastSyncTime')),
platformStatusLabel: pickField(row, 'platformStatusLabel', 'PlatformStatusLabel') || '-',
customTags: pickField(row, 'customTags', 'CustomTags') || '-',
customerName: pickField(row, 'customerName', 'CustomerName') || '-',
customerCode: pickField(row, 'customerCode', 'CustomerCode') || '-',
shippingLabelNo: pickField(row, 'shippingLabelNo', 'ShippingLabelNo') || '-',
shippingLabelUrl: labelUrl,
isMultiLabel: Boolean(pickField(row, 'isMultiLabel', 'IsMultiLabel')),
warehouseName: resolveWarehouseLabel(warehouseId, props.warehouseNameMap || new Map()),
linkedServiceNo: pickField(row, 'linkedServiceNo', 'LinkedServiceNo') || '-',
linkedOrderId: pickField(row, 'linkedOrderId', 'LinkedOrderId'),
recipientName: pickField(row, 'recipientName', 'RecipientName') || '-',
recipientCompany: pickField(row, 'recipientCompany', 'RecipientCompany') || '-',
recipientPhone: pickField(row, 'recipientPhone', 'RecipientPhone') || '-',
recipientTelePhone: pickField(row, 'recipientTelePhone', 'RecipientTelePhone') || '-',
recipientEmail: pickField(row, 'recipientEmail', 'RecipientEmail') || '-',
recipientCountry: pickField(row, 'recipientCountry', 'RecipientCountry') || pickField(row, 'orderCountry', 'OrderCountry') || '-',
recipientProvince: pickField(row, 'recipientProvince', 'RecipientProvince') || '-',
recipientCity: pickField(row, 'recipientCity', 'RecipientCity') || '-',
recipientZipCode: pickField(row, 'recipientZipCode', 'RecipientZipCode') || '-',
recipientAddress1: pickField(row, 'recipientAddress1', 'RecipientAddress1') || '-',
recipientAddress2: pickField(row, 'recipientAddress2', 'RecipientAddress2') || '-',
lines,
}
})
const basicFields = computed(() => {
const d = display.value
if (!d) return []
return [
{ label: t('order.platformStoreOrder.table.platform'), value: d.platformCode || '-' },
{ label: t('order.platformStoreOrder.table.store'), value: d.storeNickname },
{ label: t('order.platformStoreOrder.table.country'), value: d.orderCountry },
{ label: t('order.platformStoreOrder.table.amount'), value: d.amountDisplay },
{ label: t('order.platformStoreOrder.table.itemCount'), value: String(d.itemCount ?? '-') },
{ label: t('order.platformStoreOrder.table.orderTime'), value: d.platformOrderTime },
{ label: t('order.platformStoreOrder.drawer.platformStatus'), value: d.platformStatusLabel },
{ label: t('order.platformStoreOrder.drawer.customTags'), value: d.customTags },
{ label: t('order.myOrders.table.customerName'), value: d.customerName },
{ label: t('order.myOrders.table.customerCode'), value: d.customerCode },
]
})
const recipientFields = computed(() => {
const d = display.value
if (!d) return []
return [
{ label: t('order.platformStoreOrder.recipient.name'), value: d.recipientName },
{ label: t('order.platformStoreOrder.recipient.company'), value: d.recipientCompany },
{ label: t('order.platformStoreOrder.recipient.phone'), value: d.recipientPhone },
{ label: t('order.platformStoreOrder.recipient.telePhone'), value: d.recipientTelePhone },
{ label: t('order.platformStoreOrder.recipient.email'), value: d.recipientEmail },
{ label: t('order.platformStoreOrder.recipient.country'), value: d.recipientCountry },
{ label: t('order.platformStoreOrder.recipient.province'), value: d.recipientProvince },
{ label: t('order.platformStoreOrder.recipient.city'), value: d.recipientCity },
{ label: t('order.platformStoreOrder.recipient.zipCode'), value: d.recipientZipCode },
{ label: t('order.platformStoreOrder.recipient.address1'), value: d.recipientAddress1 },
{ label: t('order.platformStoreOrder.recipient.address2'), value: d.recipientAddress2 },
]
})
const shippingFields = computed(() => {
const d = display.value
if (!d) return []
return [
{ label: t('order.platformStoreOrder.table.warehouse'), value: d.warehouseName },
{ label: t('order.platformStoreOrder.table.shippingLabel'), value: d.shippingLabelNo },
{ label: t('order.platformStoreOrder.drawer.multiLabel'), value: d.isMultiLabel ? t('warehouse.common.yes') : t('warehouse.common.no') },
{ label: t('order.platformStoreOrder.table.linkedServiceNo'), value: d.linkedServiceNo },
{ label: t('order.platformStoreOrder.table.lastSync'), value: d.lastSyncTime },
]
})
function statusTagStyle(status: string, suspended: boolean) {
return getStoreOrderStatusTagStyle(status, suspended)
}
async function open(payload: { id: number; customerId?: number }) {
if (!payload?.id) return
detail.value = null
setDrawerProps({
visible: true,
width: 860,
title: t('order.platformStoreOrder.drawer.detail'),
})
innerOpen(true)
loading.value = true
try {
const data = await getPlatformStoreOrderDetail(payload.id, payload.customerId)
detail.value = (data || {}) as Record<string, any>
} catch (e: any) {
$baseMessage(e?.message || t('order.platformStoreOrder.message.detailFailed'), 'error')
closeDrawer()
} finally {
loading.value = false
}
}
function handleCancel() {
closeDrawer()
detail.value = null
}
function openLabelUrl() {
const url = display.value?.shippingLabelUrl
if (url) window.open(url, '_blank')
}
defineExpose({ open, close: handleCancel })
</script>
<template>
<DrawerPro @register="register" @cancel="handleCancel">
<template #footer>
<div class="drawer-footer-banner">
<a-button @click="handleCancel">{{ t('warehouse.common.cancel') }}</a-button>
<a-button
v-if="display?.shippingLabelUrl"
type="primary"
@click="openLabelUrl"
>
{{ t('order.platformStoreOrder.drawer.openLabel') }}
</a-button>
</div>
</template>
<a-spin :loading="loading" class="store-order-detail-spin">
<div v-if="display" class="store-order-detail-drawer">
<div class="basic-summary">
<div class="basic-summary-top">
<div class="basic-summary-main">
<span class="basic-summary-label">{{ t('order.platformStoreOrder.table.orderNo') }}</span>
<span class="basic-summary-no">{{ display.platformOrderNo }}</span>
</div>
<span class="store-order-status-tag" :style="statusTagStyle(display.status, display.suspended)">
{{ display.statusLabel }}
</span>
</div>
<div class="basic-summary-chips">
<span class="basic-chip">
<span class="basic-chip-label">{{ t('order.platformStoreOrder.table.platform') }}</span>
<span class="basic-chip-value">{{ display.platformCode || '-' }}</span>
</span>
<span class="basic-chip">
<span class="basic-chip-label">{{ t('order.platformStoreOrder.table.store') }}</span>
<span class="basic-chip-value">{{ display.storeNickname }}</span>
</span>
<span class="basic-chip">
<span class="basic-chip-label">{{ t('order.platformStoreOrder.table.amount') }}</span>
<span class="basic-chip-value">{{ display.amountDisplay }}</span>
</span>
<span class="basic-chip">
<span class="basic-chip-label">{{ t('order.platformStoreOrder.table.linkedServiceNo') }}</span>
<span class="basic-chip-value">{{ display.linkedServiceNo }}</span>
</span>
<span class="basic-chip">
<span class="basic-chip-label">{{ t('order.platformStoreOrder.recipient.name') }}</span>
<span class="basic-chip-value">{{ display.recipientName }}</span>
</span>
</div>
</div>
<div class="detail-section basic">
<div class="detail-section-header">
<div class="detail-section-title-wrap">
<span class="detail-section-dot" />
<span class="detail-section-title">{{ t('order.platformStoreOrder.drawer.sectionBasic') }}</span>
</div>
</div>
<div class="detail-fields-grid">
<div v-for="field in basicFields" :key="field.label" class="detail-field-item">
<span class="detail-field-label">{{ field.label }}</span>
<div class="detail-field-value">
<span class="detail-field-text">{{ field.value }}</span>
</div>
</div>
</div>
</div>
<div class="detail-section recipient">
<div class="detail-section-header">
<div class="detail-section-title-wrap">
<span class="detail-section-dot" />
<span class="detail-section-title">{{ t('order.platformStoreOrder.drawer.sectionRecipient') }}</span>
</div>
</div>
<div class="detail-fields-grid">
<div v-for="field in recipientFields" :key="field.label" class="detail-field-item">
<span class="detail-field-label">{{ field.label }}</span>
<div class="detail-field-value">
<span class="detail-field-text">{{ field.value }}</span>
</div>
</div>
</div>
</div>
<div class="detail-section shipping">
<div class="detail-section-header">
<div class="detail-section-title-wrap">
<span class="detail-section-dot" />
<span class="detail-section-title">{{ t('order.platformStoreOrder.drawer.sectionShipping') }}</span>
</div>
</div>
<div class="detail-fields-grid">
<div v-for="field in shippingFields" :key="field.label" class="detail-field-item">
<span class="detail-field-label">{{ field.label }}</span>
<div class="detail-field-value">
<span class="detail-field-text">{{ field.value }}</span>
</div>
</div>
</div>
</div>
<div class="detail-section lines">
<div class="detail-section-header">
<div class="detail-section-title-wrap">
<span class="detail-section-dot" />
<span class="detail-section-title">{{ t('order.platformStoreOrder.drawer.sectionLines') }}</span>
<a-tag size="small" color="arcoblue">{{ display.lines.length }}</a-tag>
</div>
</div>
<div v-if="display.lines.length" class="lines-table-wrap">
<a-table :data="display.lines" :pagination="false" size="small" row-key="id">
<template #columns>
<a-table-column :title="t('order.platformStoreOrder.line.image')" :width="64">
<template #cell="{ record }">
<a-image
v-if="record.productImageUrl"
:src="record.productImageUrl"
width="40"
height="40"
fit="cover"
/>
<span v-else class="line-no-img">-</span>
</template>
</a-table-column>
<a-table-column :title="t('order.platformStoreOrder.line.sku')" data-index="platformSku" :width="120" />
<a-table-column :title="t('order.platformStoreOrder.line.name')" data-index="productName" :width="180" />
<a-table-column :title="t('order.platformStoreOrder.line.variant')" data-index="variantText" :width="120" />
<a-table-column :title="t('order.platformStoreOrder.line.price')" data-index="unitPriceDisplay" :width="100" />
<a-table-column :title="t('order.platformStoreOrder.line.qty')" data-index="quantity" :width="70" />
<a-table-column :title="t('order.platformStoreOrder.line.mappedSku')" data-index="mappedSkuCode" :width="110" />
<a-table-column :title="t('order.platformStoreOrder.line.excluded')" :width="90">
<template #cell="{ record }">
{{ record.isShipExcluded ? t('warehouse.common.yes') : t('warehouse.common.no') }}
</template>
</a-table-column>
</template>
</a-table>
</div>
<div v-else class="detail-section-empty">
<a-empty />
</div>
</div>
</div>
<div v-else-if="!loading" class="detail-section-empty">
<a-empty />
</div>
</a-spin>
</DrawerPro>
</template>
<style scoped lang="less">
.store-order-detail-spin {
width: 100%;
min-height: 240px;
}
.store-order-detail-drawer {
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 8px;
}
.basic-summary {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 16px;
border: 1px solid var(--color-border-2);
border-radius: 6px;
background: var(--app-card-bg);
}
.basic-summary-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.basic-summary-main {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.basic-summary-label {
font-size: 12px;
color: var(--color-text-3);
line-height: 20px;
}
.basic-summary-no {
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
line-height: 26px;
word-break: break-all;
}
.store-order-status-tag {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 8px;
font-size: 12px;
line-height: 20px;
letter-spacing: 0.02em;
white-space: nowrap;
}
.basic-summary-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.basic-chip {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 100%;
padding: 4px 10px;
border-radius: 4px;
background: var(--color-fill-2);
font-size: 12px;
line-height: 20px;
}
.basic-chip-label {
color: var(--color-text-3);
flex-shrink: 0;
}
.basic-chip-value {
color: var(--color-text-1);
font-weight: 500;
word-break: break-all;
}
.detail-section {
border: 1px solid var(--color-border-2);
border-radius: 6px;
overflow: hidden;
background: var(--app-card-bg);
&.basic .detail-section-dot {
background: var(--app-primary-color, #2d8c8c);
}
&.recipient .detail-section-dot {
background: #00b42a;
}
&.shipping .detail-section-dot {
background: #165dff;
}
&.lines .detail-section-dot {
background: #ff7d00;
}
}
.detail-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
background: var(--color-fill-2);
border-bottom: 1px solid var(--color-border-2);
}
.detail-section-title-wrap {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.detail-section-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.detail-section-title {
font-size: 13px;
font-weight: 600;
color: var(--color-text-1);
}
.detail-fields-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
padding: 12px;
}
.detail-field-item {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
padding: 8px 10px;
border: 1px solid var(--color-border-2);
border-radius: 4px;
background: var(--color-fill-2);
}
.detail-field-label {
font-size: 12px;
color: var(--color-text-3);
line-height: 18px;
}
.detail-field-value {
min-height: 24px;
display: flex;
align-items: center;
}
.detail-field-text {
font-size: 13px;
color: var(--color-text-1);
font-weight: 500;
word-break: break-all;
line-height: 20px;
}
.detail-section-empty {
padding: 24px 0;
}
.lines-table-wrap {
padding: 8px 12px 12px;
overflow: auto;
}
.line-no-img {
color: #c9cdd4;
}
.drawer-footer-banner {
display: flex;
justify-content: flex-end;
gap: 8px;
width: 100%;
}
@media (max-width: 900px) {
.detail-fields-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>