feat(procurement): show customer sample fulfillment and charges
This commit is contained in:
@@ -7,13 +7,10 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/components/com
|
||||
import type { TableAction } from '@/components/common/tableActions'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import {
|
||||
createProcurement,
|
||||
createTransport,
|
||||
getBusinessOrder,
|
||||
listBusinessOrders,
|
||||
type CrossBorderBusinessKind,
|
||||
type ProcurementDetail,
|
||||
type ProcurementSummary,
|
||||
type TransportDetail,
|
||||
type TransportSummary,
|
||||
} from '../api/businessOrders'
|
||||
@@ -26,23 +23,17 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const detailOrderNo = computed(() => String(route.params.orderNo || ''))
|
||||
const canCreate = computed(() => auth.hasPermission(({
|
||||
procurement: 'merchant:cross-border:procurement:create',
|
||||
trucking: 'merchant:cross-border:domestic-trucking:create',
|
||||
looseboard: 'merchant:cross-border:pallet-shipping:create',
|
||||
} as Record<CrossBorderBusinessKind, string>)[props.kind]))
|
||||
|
||||
type BusinessRow = ProcurementSummary | TransportSummary
|
||||
type BusinessDetail = ProcurementDetail | TransportDetail
|
||||
type BusinessRow = TransportSummary
|
||||
type BusinessDetail = TransportDetail
|
||||
type PageMode = 'list' | 'create' | 'detail'
|
||||
|
||||
interface ColumnDefinition { key: string; label: string; width?: number }
|
||||
|
||||
interface BusinessForm {
|
||||
purchaseCategory: string
|
||||
productName: string
|
||||
productSpecification: string
|
||||
productUrl: string
|
||||
purchaseQuantity: number
|
||||
transportMode: string
|
||||
palletRequired: string
|
||||
transportType: string
|
||||
@@ -60,28 +51,10 @@ interface BusinessForm {
|
||||
contactName: string
|
||||
contactInfo: string
|
||||
requestedDate: string
|
||||
receivingAddress: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
const configuration = computed(() => ({
|
||||
procurement: {
|
||||
title: '海外采购',
|
||||
description: '查询并管理海外采购订单',
|
||||
columns: [
|
||||
{ key: 'orderNo', label: '订单号', width: 180 },
|
||||
{ key: 'purchaseCategory', label: '采购品类', width: 130 },
|
||||
{ key: 'productName', label: '商品名称', width: 160 },
|
||||
{ key: 'productSpecification', label: '商品规格', width: 160 },
|
||||
{ key: 'purchaseQuantity', label: '采购数量', width: 100 },
|
||||
{ key: 'budgetUnitPrice', label: '预算单价', width: 110 },
|
||||
{ key: 'inboundQuantity', label: '入库量', width: 90 },
|
||||
{ key: 'outboundQuantity', label: '出库量', width: 90 },
|
||||
{ key: 'inventoryQuantity', label: '库存量', width: 90 },
|
||||
{ key: 'status', label: '状态', width: 120 },
|
||||
{ key: 'createdAt', label: '创建时间', width: 170 },
|
||||
] satisfies ColumnDefinition[],
|
||||
},
|
||||
trucking: {
|
||||
title: '美国本土卡',
|
||||
description: '查询并管理美国本土卡订单',
|
||||
@@ -117,23 +90,19 @@ const submitKey = ref('')
|
||||
const error = ref('')
|
||||
const rows = ref<BusinessRow[]>([])
|
||||
const detail = ref<BusinessDetail | null>(null)
|
||||
const fleetArrangement = computed(() => detail.value && props.kind !== 'procurement' ? (detail.value as TransportDetail).summary.fleetArrangement : null)
|
||||
const fleetArrangement = computed(() => detail.value ? (detail.value as TransportDetail).summary.fleetArrangement : null)
|
||||
const formRef = ref<FormInstance>()
|
||||
const query = reactive({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '', productName: '', purchaseCategory: '' })
|
||||
const query = reactive({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '' })
|
||||
const total = ref(0)
|
||||
|
||||
const queryModel = computed<QueryModel>({
|
||||
get: () => ({
|
||||
orderNo: query.orderNo,
|
||||
purchaseCategory: query.purchaseCategory,
|
||||
productName: query.productName,
|
||||
routeKeyword: query.routeKeyword,
|
||||
status: query.status,
|
||||
}),
|
||||
set: (value) => {
|
||||
query.orderNo = String(value.orderNo || '')
|
||||
query.purchaseCategory = String(value.purchaseCategory || '')
|
||||
query.productName = String(value.productName || '')
|
||||
query.routeKeyword = String(value.routeKeyword || '')
|
||||
query.status = String(value.status || '')
|
||||
},
|
||||
@@ -141,12 +110,7 @@ const queryModel = computed<QueryModel>({
|
||||
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'orderNo', label: '订单号', type: 'input', placeholder: '请输入订单号' },
|
||||
...(props.kind === 'procurement'
|
||||
? [
|
||||
{ key: 'purchaseCategory', label: '采购品类', type: 'input', placeholder: '请输入采购品类' } as QueryField,
|
||||
{ key: 'productName', label: '商品名称', type: 'input', placeholder: '请输入商品名称' } as QueryField,
|
||||
]
|
||||
: [{ key: 'routeKeyword', label: '运输路线', type: 'input', placeholder: '请输入起运地或目的地' } as QueryField]),
|
||||
{ key: 'routeKeyword', label: '运输路线', type: 'input', placeholder: '请输入起运地或目的地' },
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
@@ -156,11 +120,8 @@ const queryFields = computed<QueryField[]>(() => [
|
||||
{ label: '待审核', value: 'PENDING_REVIEW' },
|
||||
{ label: '已通过', value: 'APPROVED' },
|
||||
{ label: '已拒绝', value: 'REJECTED' },
|
||||
{ label: '待出库', value: 'PENDING_OUTBOUND' },
|
||||
{ label: '部分出库', value: 'PARTIALLY_OUTBOUND' },
|
||||
{ label: '全部出库', value: 'OUTBOUNDED' },
|
||||
{ label: '待接单', value: 'PENDING_ACCEPTANCE' },
|
||||
...(props.kind === 'procurement' ? [] : Object.entries(poolOrderStatusLabels).filter(([value]) => !['IN_TRANSIT', 'COMPLETED', 'CANCELLED'].includes(value)).map(([value, label]) => ({ value, label }))),
|
||||
...Object.entries(poolOrderStatusLabels).filter(([value]) => !['IN_TRANSIT', 'COMPLETED', 'CANCELLED'].includes(value)).map(([value, label]) => ({ value, label })),
|
||||
{ label: '处理中', value: 'PROCESSING' },
|
||||
{ label: '运输中', value: 'IN_TRANSIT' },
|
||||
{ label: '已完成', value: 'COMPLETED' },
|
||||
@@ -171,9 +132,6 @@ const queryFields = computed<QueryField[]>(() => [
|
||||
|
||||
const form = reactive<BusinessForm>(emptyForm())
|
||||
const rules: FormRules<BusinessForm> = {
|
||||
purchaseCategory: [{ required: true, message: '请输入采购品类', trigger: 'blur' }],
|
||||
productName: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
|
||||
purchaseQuantity: [{ required: true, type: 'number', min: 1, message: '采购数量至少为 1', trigger: 'change' }],
|
||||
origin: [{ required: true, message: '请输入起运地', trigger: 'blur' }],
|
||||
destination: [{ required: true, message: '请输入目的地', trigger: 'blur' }],
|
||||
palletCount: [{ required: true, type: 'number', min: 1, message: '托盘数至少为 1', trigger: 'change' }],
|
||||
@@ -185,21 +143,6 @@ const rules: FormRules<BusinessForm> = {
|
||||
const detailFields = computed(() => {
|
||||
if (!detail.value) return []
|
||||
const summary = detail.value.summary
|
||||
if (props.kind === 'procurement') {
|
||||
const current = detail.value as ProcurementDetail
|
||||
return [
|
||||
['采购品类', read(summary, 'purchaseCategory')],
|
||||
['商品名称', read(summary, 'productName')], ['商品规格', read(summary, 'productSpecification')],
|
||||
['采购数量', read(summary, 'purchaseQuantity')], ['预算单价', money(read(summary, 'budgetUnitPrice'), read(summary, 'currency'))],
|
||||
['入库量', read(summary, 'inboundQuantity')], ['出库量', read(summary, 'outboundQuantity')],
|
||||
['库存量', read(summary, 'inventoryQuantity')], ['预计到达时间', dateTime(current.estimatedArrivalAt)],
|
||||
['联系人', current.contactName], ['联系电话', current.contactPhone],
|
||||
['收货地址', current.receivingAddress], ['商品链接', current.productUrl],
|
||||
['国内运单', joinText(current.domesticCarrier, current.domesticTrackingNo)],
|
||||
['海外运单', joinText(current.overseasCarrier, current.overseasTrackingNo)],
|
||||
['审核备注', current.reviewRemark], ['备注', current.remark],
|
||||
]
|
||||
}
|
||||
const current = detail.value as TransportDetail
|
||||
return [
|
||||
['运输方式', transportModeLabel(read(summary, 'transportMode'))],
|
||||
@@ -228,18 +171,16 @@ const detailStatus = computed(() => detail.value ? statusLabel(detail.value.summ
|
||||
|
||||
function emptyForm(): BusinessForm {
|
||||
return {
|
||||
purchaseCategory: '', productName: '', productSpecification: '', productUrl: '', purchaseQuantity: 1,
|
||||
transportMode: 'SHARED_TRUCK', palletRequired: 'NO', transportType: 'PORT_PICKUP', origin: '', originAddress: '',
|
||||
destination: '', destinationAddress: '', containerType: DEFAULT_CONTAINER_TYPE, palletCount: 1, packageCount: 0,
|
||||
totalWeightKg: 0, totalVolumeCbm: 0, currency: 'USD', contactName: '', contactInfo: '', requestedDate: '',
|
||||
receivingAddress: '', remark: '',
|
||||
remark: '',
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() { Object.assign(form, emptyForm()); submitKey.value = ''; formRef.value?.clearValidate() }
|
||||
function read(source: object, key: string) { return Reflect.get(source, key) as unknown }
|
||||
function text(value: unknown) { return value === null || value === undefined || value === '' ? '—' : String(value) }
|
||||
function joinText(...values: unknown[]) { const result = values.filter(Boolean).join(' / '); return result || '—' }
|
||||
function money(amount: unknown, currency: unknown) { return amount === null || amount === undefined ? '—' : `${text(currency)} ${Number(amount).toFixed(2)}` }
|
||||
function dateTime(value: unknown) { return value ? new Date(String(value)).toLocaleString('zh-CN', { hour12: false }) : '—' }
|
||||
function yesNo(value: unknown) { return value === 'YES' ? '是' : value === 'NO' ? '否' : text(value) }
|
||||
@@ -247,10 +188,10 @@ function transportTypeLabel(value: unknown) { return ({ PORT_PICKUP: '港口提
|
||||
function transportModeLabel(value: unknown) { return ({ SHARED_TRUCK: '拼车', PRIORITY_CONSOLIDATION: '优先拼载' } as Record<string, string>)[String(value)] || text(value) }
|
||||
function statusLabel(value: unknown) {
|
||||
if (poolOrderStatusLabels[String(value)]) return poolOrderStatusLabels[String(value)]
|
||||
return ({ PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_OUTBOUND: '待出库', PARTIALLY_OUTBOUND: '部分出库', OUTBOUNDED: '全部出库', PENDING_ACCEPTANCE: '待接单', PROCESSING: '处理中', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' } as Record<string, string>)[String(value)] || text(value)
|
||||
return ({ PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_ACCEPTANCE: '待接单', PROCESSING: '处理中', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' } as Record<string, string>)[String(value)] || text(value)
|
||||
}
|
||||
function statusTone(value: unknown) {
|
||||
return ({ APPROVED: 'success', COMPLETED: 'success', OUTBOUNDED: 'success', REJECTED: 'danger', CANCELLED: 'info', PENDING_REVIEW: 'warning', PENDING_ACCEPTANCE: 'warning', PROCESSING: 'primary', IN_TRANSIT: 'primary' } as Record<string, 'success' | 'warning' | 'danger' | 'info' | 'primary'>)[String(value)] || 'info'
|
||||
return ({ APPROVED: 'success', COMPLETED: 'success', REJECTED: 'danger', CANCELLED: 'info', PENDING_REVIEW: 'warning', PENDING_ACCEPTANCE: 'warning', PROCESSING: 'primary', IN_TRANSIT: 'primary' } as Record<string, 'success' | 'warning' | 'danger' | 'info' | 'primary'>)[String(value)] || 'info'
|
||||
}
|
||||
function displayCell(candidate: unknown, key: string) {
|
||||
const row = candidate as BusinessRow
|
||||
@@ -260,7 +201,7 @@ function displayCell(candidate: unknown, key: string) {
|
||||
if (key === 'transportMode') return transportModeLabel(value)
|
||||
if (key === 'palletRequired') return yesNo(value)
|
||||
if (key === 'createdAt') return dateTime(value)
|
||||
if (key === 'budgetUnitPrice' || key === 'budgetAmount') return money(value, read(row, 'currency'))
|
||||
if (key === 'budgetAmount') return money(value, read(row, 'currency'))
|
||||
return text(value)
|
||||
}
|
||||
function idempotencyKey() { return globalThis.crypto?.randomUUID?.() || `client-${Date.now()}-${Math.random().toString(16).slice(2)}` }
|
||||
@@ -272,12 +213,9 @@ async function load() {
|
||||
try {
|
||||
const params = {
|
||||
page: query.page, size: query.size, orderNo: query.orderNo || undefined, status: query.status || undefined,
|
||||
routeKeyword: query.routeKeyword || undefined, productName: query.productName || undefined,
|
||||
purchaseCategory: query.purchaseCategory || undefined,
|
||||
routeKeyword: query.routeKeyword || undefined,
|
||||
}
|
||||
const result = props.kind === 'procurement'
|
||||
? await listBusinessOrders<ProcurementSummary>('procurement', params)
|
||||
: await listBusinessOrders<TransportSummary>(props.kind, params)
|
||||
const result = await listBusinessOrders<TransportSummary>(props.kind, params)
|
||||
rows.value = result.records
|
||||
total.value = result.total
|
||||
} catch (cause) {
|
||||
@@ -289,7 +227,7 @@ async function load() {
|
||||
|
||||
function resetQuery() {
|
||||
const reloadDirectly = query.page === 1
|
||||
Object.assign(query, { page: 1, orderNo: '', routeKeyword: '', status: '', productName: '', purchaseCategory: '' })
|
||||
Object.assign(query, { page: 1, orderNo: '', routeKeyword: '', status: '' })
|
||||
if (reloadDirectly) void load()
|
||||
}
|
||||
|
||||
@@ -303,16 +241,14 @@ function getRowActions(candidate: object): TableAction[] {
|
||||
}
|
||||
|
||||
function detailFieldClass(label: unknown) {
|
||||
return ['收货地址', '商品链接', '国内运单', '海外运单', '审核备注', '备注', '起运地址', '目的地址'].includes(String(label)) ? 'is-wide' : ''
|
||||
return ['备注', '起运地址', '目的地址'].includes(String(label)) ? 'is-wide' : ''
|
||||
}
|
||||
|
||||
const listRouteName = computed(() => ({
|
||||
procurement: 'cross-border-procurement',
|
||||
trucking: 'cross-border-domestic-trucking',
|
||||
looseboard: 'cross-border-pallet-shipping',
|
||||
} as Record<CrossBorderBusinessKind, string>)[props.kind])
|
||||
const detailRouteName = computed(() => ({
|
||||
procurement: 'cross-border-procurement-detail',
|
||||
trucking: 'cross-border-domestic-trucking-detail',
|
||||
looseboard: 'cross-border-pallet-shipping-detail',
|
||||
} as Record<CrossBorderBusinessKind, string>)[props.kind])
|
||||
@@ -334,9 +270,7 @@ async function loadDetail() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
detail.value = props.kind === 'procurement'
|
||||
? await getBusinessOrder<ProcurementDetail>('procurement', detailOrderNo.value)
|
||||
: await getBusinessOrder<TransportDetail>(props.kind, detailOrderNo.value)
|
||||
detail.value = await getBusinessOrder<TransportDetail>(props.kind, detailOrderNo.value)
|
||||
mode.value = 'detail'
|
||||
} catch (cause) { detail.value = null; error.value = errorMessage(cause); ElMessage.error(error.value) }
|
||||
finally { loading.value = false }
|
||||
@@ -348,31 +282,19 @@ async function submit() {
|
||||
const stableSubmitKey = submitKey.value || idempotencyKey()
|
||||
submitKey.value = stableSubmitKey
|
||||
try {
|
||||
if (props.kind === 'procurement') {
|
||||
await createProcurement({
|
||||
customerName: auth.user?.tenantName || '',
|
||||
purchaseCategory: form.purchaseCategory, productName: form.productName,
|
||||
productSpecification: form.productSpecification || undefined, productUrl: form.productUrl || undefined,
|
||||
purchaseQuantity: form.purchaseQuantity, budgetUnitPrice: form.budgetAmount,
|
||||
currency: form.currency || undefined, receivingAddress: form.receivingAddress || undefined,
|
||||
contactName: form.contactName, contactPhone: form.contactInfo, remark: form.remark || undefined,
|
||||
idempotencyKey: stableSubmitKey,
|
||||
})
|
||||
} else {
|
||||
const base = {
|
||||
transportType: form.transportType, origin: form.origin, originAddress: form.originAddress || undefined,
|
||||
destination: form.destination, destinationAddress: form.destinationAddress || undefined,
|
||||
containerType: form.containerType || undefined, packageCount: form.packageCount,
|
||||
totalWeightKg: form.totalWeightKg, totalVolumeCbm: form.totalVolumeCbm,
|
||||
budgetAmount: form.budgetAmount, currency: form.currency || undefined,
|
||||
contactName: form.contactName || undefined, contactInfo: form.contactInfo || undefined,
|
||||
requestedDate: form.requestedDate || undefined, remark: form.remark || undefined,
|
||||
idempotencyKey: stableSubmitKey,
|
||||
}
|
||||
await createTransport(props.kind, props.kind === 'trucking'
|
||||
? { ...base, palletCount: form.palletCount }
|
||||
: { ...base, transportMode: form.transportMode, palletRequired: form.palletRequired })
|
||||
const base = {
|
||||
transportType: form.transportType, origin: form.origin, originAddress: form.originAddress || undefined,
|
||||
destination: form.destination, destinationAddress: form.destinationAddress || undefined,
|
||||
containerType: form.containerType || undefined, packageCount: form.packageCount,
|
||||
totalWeightKg: form.totalWeightKg, totalVolumeCbm: form.totalVolumeCbm,
|
||||
budgetAmount: form.budgetAmount, currency: form.currency || undefined,
|
||||
contactName: form.contactName || undefined, contactInfo: form.contactInfo || undefined,
|
||||
requestedDate: form.requestedDate || undefined, remark: form.remark || undefined,
|
||||
idempotencyKey: stableSubmitKey,
|
||||
}
|
||||
await createTransport(props.kind, props.kind === 'trucking'
|
||||
? { ...base, palletCount: form.palletCount }
|
||||
: { ...base, transportMode: form.transportMode, palletRequired: form.palletRequired })
|
||||
ElMessage.success('订单提交成功')
|
||||
resetForm()
|
||||
if (props.createPage) await router.push({ name: listRouteName.value })
|
||||
@@ -442,15 +364,6 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
|
||||
|
||||
<el-card v-else shadow="never" class="form-card">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<template v-if="kind === 'procurement'">
|
||||
<el-form-item label="采购品类" prop="purchaseCategory"><el-input v-model.trim="form.purchaseCategory" maxlength="128" placeholder="请输入采购品类" /></el-form-item>
|
||||
<el-form-item label="商品名称" prop="productName"><el-input v-model.trim="form.productName" maxlength="255" placeholder="请输入商品名称" /></el-form-item>
|
||||
<el-form-item label="商品规格"><el-input v-model.trim="form.productSpecification" maxlength="500" placeholder="请输入规格、型号等信息" /></el-form-item>
|
||||
<el-form-item label="商品链接"><el-input v-model.trim="form.productUrl" maxlength="2000" placeholder="请输入商品链接" /></el-form-item>
|
||||
<el-form-item label="采购数量" prop="purchaseQuantity"><el-input-number v-model="form.purchaseQuantity" :min="1" placeholder="请输入数量" /></el-form-item>
|
||||
<el-form-item label="收货地址"><el-input v-model.trim="form.receivingAddress" maxlength="1000" placeholder="请输入收货地址" /></el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-form-item v-if="kind === 'looseboard'" label="运输方式"><el-radio-group v-model="form.transportMode"><el-radio-button value="SHARED_TRUCK">拼车</el-radio-button><el-radio-button value="PRIORITY_CONSOLIDATION">优先拼载</el-radio-button></el-radio-group></el-form-item>
|
||||
<el-form-item v-if="kind === 'looseboard'" label="是否打盘"><el-radio-group v-model="form.palletRequired"><el-radio-button value="NO">否</el-radio-button><el-radio-button value="YES">是</el-radio-button></el-radio-group></el-form-item>
|
||||
<el-form-item label="运输类型"><el-select v-model="form.transportType"><el-option label="港口提柜" value="PORT_PICKUP" /><el-option label="仓到仓" value="WAREHOUSE_TO_WAREHOUSE" /><el-option label="仓到门" value="WAREHOUSE_TO_DOOR" /></el-select></el-form-item>
|
||||
@@ -464,8 +377,7 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
|
||||
<el-form-item label="总重量(kg)"><el-input-number v-model="form.totalWeightKg" :min="0" :precision="2" placeholder="请输入总重量" /></el-form-item>
|
||||
<el-form-item label="总体积(m³)"><el-input-number v-model="form.totalVolumeCbm" :min="0" :precision="3" placeholder="请输入总体积" /></el-form-item>
|
||||
<el-form-item label="要求时间" :prop="kind === 'trucking' ? 'requestedDate' : undefined"><el-date-picker v-model="form.requestedDate" type="date" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
</template>
|
||||
<el-form-item class="budget-form-item" :label="kind === 'procurement' ? '预算单价' : '预算金额'"><el-input-number v-model="form.budgetAmount" :min="0" :precision="2" placeholder="请输入预算金额" /><el-select v-model="form.currency" class="currency"><el-option label="USD" value="USD" /><el-option label="CNY" value="CNY" /></el-select></el-form-item>
|
||||
<el-form-item class="budget-form-item" label="预算金额"><el-input-number v-model="form.budgetAmount" :min="0" :precision="2" placeholder="请输入预算金额" /><el-select v-model="form.currency" class="currency"><el-option label="USD" value="USD" /><el-option label="CNY" value="CNY" /></el-select></el-form-item>
|
||||
<el-form-item label="联系人" prop="contactName"><el-input v-model.trim="form.contactName" maxlength="128" placeholder="请输入联系人" /></el-form-item>
|
||||
<el-form-item label="联系方式" prop="contactInfo"><el-input v-model.trim="form.contactInfo" maxlength="128" placeholder="请输入电话、微信或邮箱" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="请输入其他要求" /></el-form-item>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { ProcurementDetail, ProcurementFee } from '../api/procurement'
|
||||
import { dateTime, feePurposes, feeStatus, feeTone, feeTotals, measurement, money, productHref, text } from '../procurementPresentation'
|
||||
|
||||
const props = defineProps<{ detail: ProcurementDetail }>()
|
||||
const totals = computed(() => feeTotals(props.detail.fees))
|
||||
const progress = computed(() => props.detail.progress.filter(step => step.key !== 'closed_at' || step.completedAt))
|
||||
const ended = computed(() => ['REJECTED', 'CANCELLED', 'EXCEPTION_CLOSED', 'SHIPPED_TO_CUSTOMER'].includes(props.detail.order.status))
|
||||
const hasPending = computed(() => props.detail.fees.some(f => f.walletStatus?.endsWith('_PENDING') || f.walletStatus?.endsWith('_FAILED')))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="procurement-section" aria-label="采购履约进度">
|
||||
<h3>履约进度</h3>
|
||||
<ol class="procurement-progress">
|
||||
<li v-for="step in progress" :key="step.key" :class="{ 'is-complete': step.completedAt }">
|
||||
<strong>{{ step.title }}</strong><span>{{ step.completedAt ? dateTime(step.completedAt) : ended ? '未执行' : '待处理' }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<p v-if="detail.purchaseNeedsAttention" class="procurement-note">购买资料正在核对,客服将协助处理。已发生的收货与费用结果仍保留在本单。</p>
|
||||
</section>
|
||||
<section class="procurement-section" aria-label="商品与收件信息">
|
||||
<h3>商品与收件信息</h3>
|
||||
<dl class="procurement-grid">
|
||||
<div><dt>采购品类</dt><dd>{{ detail.order.purchaseCategory }}</dd></div>
|
||||
<div><dt>商品名称</dt><dd>{{ detail.order.productName }}</dd></div>
|
||||
<div><dt>商品规格</dt><dd>{{ text(detail.order.productSpecification) }}</dd></div>
|
||||
<div><dt>采购数量</dt><dd>{{ detail.order.purchaseQuantity }} 件</dd></div>
|
||||
<div><dt>下单预算单价</dt><dd>{{ money(detail.order.budgetUnitPrice, detail.order.currency) }}</dd></div>
|
||||
<div><dt>货款预算总上限</dt><dd>{{ money(detail.budget.totalAmount, detail.budget.currency) }}</dd></div>
|
||||
<div><dt>预算核验时间</dt><dd>{{ dateTime(detail.budget.authorizedAt) }}</dd></div>
|
||||
<div><dt>预计到海外仓</dt><dd>{{ dateTime(detail.order.estimatedArrivalAt) }}</dd></div>
|
||||
<div><dt>收件人</dt><dd>{{ detail.order.contactName }}</dd></div>
|
||||
<div><dt>联系电话</dt><dd>{{ detail.order.contactPhone }}</dd></div>
|
||||
<div class="wide"><dt>下单收货地址</dt><dd>{{ detail.order.receivingAddress }}</dd></div>
|
||||
<div class="wide"><dt>实际海外仓</dt><dd>{{ text(detail.order.actualWarehouseName) }}<span v-if="detail.order.actualWarehouseCode">({{ detail.order.actualWarehouseCode }})</span></dd></div>
|
||||
<div class="wide"><dt>商品链接</dt><dd><a v-if="productHref(detail.order.productUrl)" :href="productHref(detail.order.productUrl)" target="_blank" rel="noopener noreferrer">{{ detail.order.productUrl }}</a><span v-else>{{ text(detail.order.productUrl) }}</span></dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="procurement-section" aria-label="已确认海外装箱清单">
|
||||
<h3>已确认海外装箱清单</h3>
|
||||
<el-table :data="detail.boxes" border stripe empty-text="尚无已确认的装箱清单" row-key="cartonNo">
|
||||
<el-table-column prop="cartonNo" label="箱号" min-width="170" />
|
||||
<el-table-column label="箱内商品" min-width="200"><template #default>{{ detail.order.productName }}</template></el-table-column>
|
||||
<el-table-column label="规格" min-width="180"><template #default>{{ text(detail.order.productSpecification) }}</template></el-table-column>
|
||||
<el-table-column prop="quantity" label="数量(件)" width="120" />
|
||||
<el-table-column label="外箱长 × 宽 × 高(cm)" min-width="240"><template #default="{ row }">{{ measurement(row.lengthCm, 2) }} × {{ measurement(row.widthCm, 2) }} × {{ measurement(row.heightCm, 2) }}</template></el-table-column>
|
||||
<el-table-column label="封箱毛重(kg)" width="170"><template #default="{ row }">{{ measurement(row.grossWeightKg, 3) }}</template></el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
<section class="procurement-section" aria-label="两段实际运输">
|
||||
<h3>两段实际运输</h3>
|
||||
<el-table :data="detail.shipments" border stripe empty-text="尚无实际交寄记录">
|
||||
<el-table-column label="运输段" width="150"><template #default="{ row }">{{ row.leg === 'OVERSEAS' ? '国际段 · 海外交寄' : '国内段 · 寄客户' }}</template></el-table-column>
|
||||
<el-table-column prop="shipmentNo" label="交寄编号" min-width="270" />
|
||||
<el-table-column prop="cartonNo" label="原箱号" min-width="150" />
|
||||
<el-table-column prop="quantity" label="实际件数" width="110" />
|
||||
<el-table-column prop="carrier" label="承运商" min-width="140" />
|
||||
<el-table-column prop="trackingNo" label="运单号" min-width="230" />
|
||||
<el-table-column label="实际交寄时间" width="185"><template #default="{ row }">{{ dateTime(row.shippedAt) }}</template></el-table-column>
|
||||
</el-table>
|
||||
<p class="procurement-note">这里显示已实际交给承运方的记录;国内寄客户后,本单履约完成。</p>
|
||||
</section>
|
||||
<section class="procurement-section" aria-label="采购费用与资金结果">
|
||||
<h3>费用与资金结果</h3>
|
||||
<dl class="procurement-grid money-grid">
|
||||
<div><dt>累计成功扣款</dt><dd>{{ money(totals?.paid) }}</dd></div>
|
||||
<div><dt>累计成功退款</dt><dd>{{ money(totals?.refunded) }}</dd></div>
|
||||
<div><dt>净实扣</dt><dd>{{ money(totals?.net) }}</dd></div>
|
||||
<div><dt>预估国际运费占款</dt><dd>{{ money(totals?.estimatedHold) }}</dd></div>
|
||||
</dl>
|
||||
<p class="procurement-note">预算上限仅包含样品货款。预估国际运费只冻结、不直接扣款,最终以实际费用结算;表内保留原费用、追加及退款记录。</p>
|
||||
<el-table :data="detail.fees" border stripe empty-text="尚无已核定费用" row-key="feeNo">
|
||||
<el-table-column prop="feeNo" label="费用编号" min-width="270" />
|
||||
<el-table-column label="费用用途" min-width="145"><template #default="{ row }">{{ feePurposes[row.purpose] || '待核定' }}</template></el-table-column>
|
||||
<el-table-column prop="name" label="费用名称" min-width="145" />
|
||||
<el-table-column label="原币金额" min-width="130"><template #default="{ row }">{{ money(row.originalAmount, row.originalCurrency) }}</template></el-table-column>
|
||||
<el-table-column label="汇率 / 月份" min-width="150"><template #default="{ row }">{{ measurement(row.exchangeRate, 6) }}<br /><small>{{ text(row.exchangeRateMonth) }}</small></template></el-table-column>
|
||||
<el-table-column label="人民币金额" min-width="130"><template #default="{ row }">{{ money(row.amount) }}</template></el-table-column>
|
||||
<el-table-column label="处理结果" min-width="190"><template #default="{ row }"><el-tag :type="feeTone(row as ProcurementFee)" size="small">{{ feeStatus(row as ProcurementFee) }}</el-tag><small v-if="row.purpose !== 'INTERNATIONAL_ESTIMATE' && row.walletStatus === 'SETTLED' && !row.sourceLinked" class="fee-sync">费用结果同步中</small></template></el-table-column>
|
||||
<el-table-column prop="parentFeeNo" label="关联原费用 / 预估" min-width="270"><template #default="{ row }">{{ text(row.parentFeeNo) }}</template></el-table-column>
|
||||
</el-table>
|
||||
<p v-if="hasPending" class="procurement-note">部分资金处理尚未完成,可点击页面“刷新”查看最新结果;如需协助请联系客户服务。</p>
|
||||
</section>
|
||||
<section v-if="detail.exceptionSummaries.length" class="procurement-section" aria-label="客服处理说明">
|
||||
<h3>客服处理说明</h3><ul class="procurement-summaries"><li v-for="(summary, index) in detail.exceptionSummaries" :key="index">{{ summary }}</li></ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.procurement-section { margin-top: 18px; min-width: 0; }
|
||||
h3 { margin: 0 0 10px; padding-bottom: 7px; border-bottom: 1px solid var(--border); color: var(--navy); font-size: 14px; }
|
||||
.procurement-progress { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; padding: 0; margin: 0; list-style: none; border-top: 1px solid var(--border); border-left: 1px solid var(--border); }
|
||||
.procurement-progress li { display: grid; gap: 7px; padding: 12px; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); color: var(--muted); }
|
||||
.procurement-progress strong { font-size: 13px; font-weight: 600; }.procurement-progress span { font-size: 12px; }.procurement-progress .is-complete strong { color: var(--navy); }
|
||||
.procurement-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; border-left: 1px solid var(--border); border-top: 1px solid var(--border); }
|
||||
.procurement-grid > div { display: grid; grid-template-columns: 110px minmax(0, 1fr); align-items: center; gap: 8px; padding: 9px 12px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); min-width: 0; }
|
||||
.procurement-grid dt { font-size: 12px; color: var(--muted); }.procurement-grid dd { margin: 0; font-size: 13px; line-height: 1.6; color: var(--navy); overflow-wrap: anywhere; }
|
||||
.procurement-grid .wide { grid-column: span 2; }.money-grid > div { grid-template-columns: minmax(0, 1fr); }.money-grid dd { font-size: 16px; font-weight: 600; }
|
||||
a { color: var(--client-action); } .procurement-note { margin: 10px 0; color: var(--muted); font-size: 12px; line-height: 1.7; }
|
||||
.fee-sync { display: block; padding-top: 5px; color: var(--muted); }.procurement-summaries { margin: 0; padding-left: 22px; line-height: 1.8; color: var(--navy); font-size: 13px; white-space: pre-wrap; }
|
||||
@media (max-width: 767px) {
|
||||
.procurement-progress, .procurement-grid { grid-template-columns: 1fr; }.procurement-grid .wide { grid-column: auto; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user