新增 H5 店铺订单列表/创建运单流程,优化登录首屏与分包加载;店铺订单状态标签素雅化;PC 露出创建运单/代发运单并移除仓库产品下拉。 Co-authored-by: Cursor <cursoragent@cursor.com>
544 lines
19 KiB
Vue
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>
|