Files
client-web/src/modules/cross-border/components/BusinessOrdersView.vue
T

506 lines
30 KiB
Vue
Raw Normal View History

<script setup lang="ts">
2026-09-18 16:48:35 +08:00
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
2026-09-18 16:48:35 +08:00
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
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'
2026-09-15 10:38:53 +08:00
import { CONTAINER_SPECIFICATIONS, DEFAULT_CONTAINER_TYPE } from '../containerSpecifications'
2026-09-18 16:48:35 +08:00
const props = withDefaults(defineProps<{ kind: CrossBorderBusinessKind; createPage?: boolean; detailPage?: boolean }>(), { createPage: false, detailPage: false })
const auth = useAuthStore()
2026-09-18 16:48:35 +08:00
const router = useRouter()
const route = useRoute()
const detailOrderNo = computed(() => String(route.params.orderNo || ''))
2026-09-17 18:25:08 +08:00
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 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
origin: string
originAddress: string
destination: string
destinationAddress: string
containerType: string
palletCount: number
packageCount: number
totalWeightKg: number
totalVolumeCbm: number
budgetAmount?: number
currency: string
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 },
2026-09-14 10:02:37 +08:00
{ 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: '查询并管理美国本土卡订单',
columns: transportColumns(false),
},
looseboard: {
title: '美本土散板',
description: '查询并管理美本土散板订单',
columns: transportColumns(true),
},
})[props.kind])
function transportColumns(looseboard: boolean): ColumnDefinition[] {
return [
{ key: 'orderNo', label: '订单编号', width: 180 },
...(looseboard ? [{ key: 'transportMode', label: '运输方式', width: 120 }, { key: 'palletRequired', label: '是否打盘', width: 100 }] : []),
{ key: 'transportType', label: '运输类型', width: 130 },
{ key: 'origin', label: '起运地', width: 150 },
{ key: 'destination', label: '目的地', width: 150 },
{ key: 'containerType', label: '柜型', width: 100 },
{ key: 'totalWeightKg', label: '总重量(kg)', width: 120 },
{ key: 'budgetAmount', label: '预算总价', width: 110 },
{ key: 'requestedDate', label: '要求时间', width: 120 },
{ key: 'status', label: '状态', width: 130 },
]
}
2026-09-18 16:48:35 +08:00
const mode = ref<PageMode>(props.detailPage ? 'detail' : props.createPage ? 'create' : 'list')
const loading = ref(false)
const submitting = ref(false)
const submitKey = ref('')
const error = ref('')
const rows = ref<BusinessRow[]>([])
const detail = ref<BusinessDetail | null>(null)
const formRef = ref<FormInstance>()
const query = reactive({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '', productName: '', purchaseCategory: '' })
const total = ref(0)
2026-09-18 16:48:35 +08:00
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 || '')
},
})
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: 'status',
label: '状态',
type: 'select',
placeholder: '全部',
options: [
{ 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' },
{ label: '处理中', value: 'PROCESSING' },
{ label: '运输中', value: 'IN_TRANSIT' },
{ label: '已完成', value: 'COMPLETED' },
{ label: '已取消', value: 'CANCELLED' },
],
},
])
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' }],
contactName: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactInfo: [{ required: true, message: '请输入联系电话或联系方式', trigger: 'blur' }],
requestedDate: [{ required: true, message: '请选择要求时间', trigger: 'change' }],
}
const detailFields = computed(() => {
if (!detail.value) return []
const summary = detail.value.summary
if (props.kind === 'procurement') {
const current = detail.value as ProcurementDetail
return [
2026-09-18 16:48:35 +08:00
['采购品类', read(summary, 'purchaseCategory')],
['商品名称', read(summary, 'productName')], ['商品规格', read(summary, 'productSpecification')],
['采购数量', read(summary, 'purchaseQuantity')], ['预算单价', money(read(summary, 'budgetUnitPrice'), read(summary, 'currency'))],
2026-09-14 10:02:37 +08:00
['入库量', 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)],
2026-09-18 16:48:35 +08:00
['审核备注', current.reviewRemark], ['备注', current.remark],
]
}
const current = detail.value as TransportDetail
return [
2026-09-18 16:48:35 +08:00
['运输方式', read(summary, 'transportMode')],
['是否打盘', yesNo(read(summary, 'palletRequired'))], ['运输类型', transportTypeLabel(read(summary, 'transportType'))],
['起运地', read(summary, 'origin')], ['起运地址', current.originAddress],
['目的地', read(summary, 'destination')], ['目的地址', current.destinationAddress],
['柜型', read(summary, 'containerType')], ['托盘数', read(summary, 'palletCount')],
['件数', read(summary, 'packageCount')], ['总重量(kg)', read(summary, 'totalWeightKg')],
['总体积(m³)', read(summary, 'totalVolumeCbm')], ['预算总价', money(read(summary, 'budgetAmount'), read(summary, 'currency'))],
['联系人', read(summary, 'contactName')], ['联系方式', read(summary, 'contactInfo')],
2026-09-18 16:48:35 +08:00
['要求时间', read(summary, 'requestedDate')], ['备注', current.remark],
]
})
2026-09-18 16:48:35 +08:00
const tableColumns = computed<DataTableColumn[]>(() => [
{ key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'actions' },
...configuration.value.columns.map((column) => ({
...column,
prop: column.key,
slotName: ['orderNo', 'status'].includes(column.key) ? column.key : undefined,
formatter: (row: object) => displayCell(row, column.key),
})),
])
const detailStatus = computed(() => detail.value ? statusLabel(detail.value.summary.status) : '—')
function emptyForm(): BusinessForm {
return {
purchaseCategory: '', productName: '', productSpecification: '', productUrl: '', purchaseQuantity: 1,
transportMode: 'SHARED_TRUCK', palletRequired: 'NO', transportType: 'PORT_PICKUP', origin: '', originAddress: '',
2026-09-15 10:38:53 +08:00
destination: '', destinationAddress: '', containerType: DEFAULT_CONTAINER_TYPE, palletCount: 1, packageCount: 0,
totalWeightKg: 0, totalVolumeCbm: 0, currency: 'USD', contactName: '', contactInfo: '', requestedDate: '',
receivingAddress: '', 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) }
function transportTypeLabel(value: unknown) { return ({ PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' } as Record<string, string>)[String(value)] || text(value) }
function transportModeLabel(value: unknown) { return ({ SHARED_TRUCK: '拼车', PRIORITY_CONSOLIDATION: '优先拼载' } as Record<string, string>)[String(value)] || text(value) }
function statusLabel(value: unknown) {
2026-09-14 10:02:37 +08:00
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)
}
2026-09-18 16:48:35 +08:00
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'
}
function displayCell(candidate: unknown, key: string) {
const row = candidate as BusinessRow
const value = read(row, key)
if (key === 'status') return statusLabel(value)
if (key === 'transportType') return transportTypeLabel(value)
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'))
return text(value)
}
function idempotencyKey() { return globalThis.crypto?.randomUUID?.() || `client-${Date.now()}-${Math.random().toString(16).slice(2)}` }
function errorMessage(cause: unknown) { return cause instanceof Error ? cause.message : '请求失败,请稍后重试' }
async function load() {
loading.value = true
error.value = ''
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,
}
const result = props.kind === 'procurement'
? await listBusinessOrders<ProcurementSummary>('procurement', params)
: await listBusinessOrders<TransportSummary>(props.kind, params)
rows.value = result.records
total.value = result.total
} catch (cause) {
error.value = errorMessage(cause)
rows.value = []
total.value = 0
} finally { loading.value = false }
}
function resetQuery() {
Object.assign(query, { page: 1, orderNo: '', routeKeyword: '', status: '', productName: '', purchaseCategory: '' })
void load()
}
2026-09-18 16:48:35 +08:00
function searchOrders() {
query.page = 1
void load()
}
function getRowActions(candidate: object): TableAction[] {
return [{ key: 'detail', label: '详情', run: () => openDetail(candidate) }]
}
function detailFieldClass(label: unknown) {
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])
function returnToList() {
if (props.createPage || props.detailPage) {
void router.push({ name: listRouteName.value })
return
}
mode.value = 'list'
}
function openDetail(candidate: unknown) {
const row = candidate as BusinessRow
2026-09-18 16:48:35 +08:00
void router.push({ name: detailRouteName.value, params: { orderNo: row.orderNo } })
}
async function loadDetail() {
if (!detailOrderNo.value) return
loading.value = true
error.value = ''
try {
detail.value = props.kind === 'procurement'
2026-09-18 16:48:35 +08:00
? await getBusinessOrder<ProcurementDetail>('procurement', detailOrderNo.value)
: await getBusinessOrder<TransportDetail>(props.kind, detailOrderNo.value)
mode.value = 'detail'
2026-09-18 16:48:35 +08:00
} catch (cause) { detail.value = null; error.value = errorMessage(cause); ElMessage.error(error.value) }
finally { loading.value = false }
}
async function submit() {
if (!await formRef.value?.validate().catch(() => false)) return
submitting.value = true
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 })
}
ElMessage.success('订单提交成功')
resetForm()
2026-09-18 16:48:35 +08:00
if (props.createPage) await router.push({ name: listRouteName.value })
else { mode.value = 'list'; await load() }
} catch (cause) { ElMessage.error(errorMessage(cause)) }
finally { submitting.value = false }
}
2026-09-18 16:48:35 +08:00
watch(() => [query.page, query.size], () => { if (!props.createPage && mode.value === 'list') void load() })
onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.createPage) void load() })
</script>
<template>
2026-09-18 16:48:35 +08:00
<section v-loading="loading" class="business-orders client-ui" :aria-busy="loading">
<header v-if="mode === 'create'" class="page-head">
<div>
2026-09-18 16:48:35 +08:00
<h2>{{ configuration.title }}下单</h2>
<p>请填写订单信息,带 * 项为必填项</p>
</div>
2026-09-18 16:48:35 +08:00
<el-button @click="returnToList">返回列表</el-button>
</header>
<template v-if="mode === 'list'">
2026-09-18 16:48:35 +08:00
<QueryToolbar v-model="queryModel" layout="grid" ariaLabel="订单查询条件" :fields="queryFields" :search-on-reset="false" @search="searchOrders" @reset="resetQuery" />
<div class="business-table-card">
<DataTable v-model:page="query.page" v-model:page-size="query.size" :data="rows" :columns="tableColumns" :loading="loading" :total="total" :page-sizes="[10, 20, 50, 100]" :row-actions="getRowActions" row-key="orderNo" empty-text="暂无订单">
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="resetForm(); mode = 'create'">新增下单</el-button></template>
<template v-if="error" #error><div class="business-load-error" role="alert"><span>{{ error }}</span><el-button size="small" @click="load">重新加载</el-button></div></template>
<template #orderNo="{ row }"><button class="business-order-link" type="button" @click="openDetail(row)">{{ (row as BusinessRow).orderNo }}</button></template>
<template #status="{ row }"><el-tag :type="statusTone((row as BusinessRow).status)" effect="light" size="small">{{ statusLabel((row as BusinessRow).status) }}</el-tag></template>
</DataTable>
</div>
</template>
2026-09-18 16:48:35 +08:00
<section v-else-if="mode === 'detail'" class="business-detail-page" aria-label="订单详情">
<header class="business-detail-heading">
<div class="business-detail-heading-main"><el-button link type="primary" @click="returnToList">← 返回列表</el-button><div><h2>{{ detail?.summary.orderNo || '订单详情' }}</h2><p>{{ configuration.title }}订单</p></div></div>
<el-tag :type="statusTone(detail?.summary.status)" effect="light">{{ detailStatus }}</el-tag>
</header>
<div v-if="error" class="business-detail-error" role="alert"><strong>{{ error }}</strong><el-button size="small" @click="loadDetail">重新加载</el-button></div>
<section v-else class="business-detail-section">
<h3>订单信息</h3>
<dl class="detail-grid">
<div v-for="field in detailFields" :key="String(field[0])" :class="detailFieldClass(field[0])"><dt>{{ field[0] }}</dt><dd>{{ text(field[1]) }}</dd></div>
</dl>
</section>
</section>
<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>
<el-form-item label="起运地" prop="origin"><el-input v-model.trim="form.origin" maxlength="255" placeholder="请输入起运地" /></el-form-item>
<el-form-item label="起运地址"><el-input v-model.trim="form.originAddress" maxlength="500" placeholder="请输入详细起运地址" /></el-form-item>
<el-form-item label="目的地" prop="destination"><el-input v-model.trim="form.destination" maxlength="255" placeholder="请输入目的地" /></el-form-item>
<el-form-item label="目的地址"><el-input v-model.trim="form.destinationAddress" maxlength="500" placeholder="请输入详细目的地址" /></el-form-item>
2026-09-15 10:38:53 +08:00
<el-form-item label="柜型"><el-select v-model="form.containerType"><el-option v-for="item in CONTAINER_SPECIFICATIONS" :key="item.type" :label="item.type" :value="item.type" /></el-select></el-form-item>
<el-form-item v-if="kind === 'trucking'" label="托盘数" prop="palletCount"><el-input-number v-model="form.palletCount" :min="1" placeholder="请输入托盘数" /></el-form-item>
<el-form-item label="件数"><el-input-number v-model="form.packageCount" :min="0" placeholder="请输入件数" /></el-form-item>
<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>
2026-09-14 10:02:37 +08:00
<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 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>
2026-09-18 16:48:35 +08:00
<el-form-item><el-button @click="returnToList">取消</el-button><el-button v-if="canCreate" type="primary" :loading="submitting" @click="submit">提交订单</el-button></el-form-item>
</el-form>
</el-card>
</section>
</template>
<style scoped>
2026-09-18 16:48:35 +08:00
.business-orders { display: grid; gap: var(--client-section-gap); }
.page-head { display: flex; align-items: center; justify-content: space-between; gap: 24px; }
.page-head h2 { margin: 0; color: #111827; font-size: 26px; }
.page-head p { margin: 7px 0 0; color: #64748b; font-size: 14px; }
2026-09-18 16:48:35 +08:00
.business-table-card { overflow: hidden; border: 1px solid var(--client-border); border-radius: var(--client-radius); background: #fff; }
.business-order-link { max-width: 100%; overflow: hidden; padding: 0; border: 0; background: transparent; color: var(--client-action); cursor: pointer; font: inherit; font-weight: 650; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
.business-order-link:hover { text-decoration: underline; text-underline-offset: 3px; }
.business-load-error { display: flex; min-height: 84px; align-items: center; justify-content: center; gap: 12px; padding: 14px; color: #b42318; }
.business-detail-error { display: flex; min-height: 180px; align-items: center; justify-content: center; gap: 12px; color: #b42318; }
.business-detail-page { min-width: 0; padding: 10px 14px 20px; overflow: hidden; border: 1px solid var(--client-border); border-radius: var(--client-radius); background: #fff; }
.business-detail-heading { display: flex; min-height: 50px; align-items: center; justify-content: space-between; gap: 16px; padding: 5px 0; border-bottom: 1px solid var(--border); }
.business-detail-heading-main { display: flex; min-width: 0; align-items: center; gap: 12px; }
.business-detail-heading-main > div { min-width: 0; }
.business-detail-heading h2 { margin: 0; overflow: hidden; color: var(--navy); font-size: 18px; text-overflow: ellipsis; white-space: nowrap; }
.business-detail-heading p { margin: 3px 0 0; color: var(--muted); font-size: 11px; }
.business-detail-section { margin-top: 10px; }
.business-detail-section h3 { margin: 0; padding: 0 0 5px; border-bottom: 1px solid #eaf0f7; color: var(--navy); font-size: 13px; }
.detail-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--border); border-left: 1px solid var(--border); }
.detail-grid div { display: grid; min-width: 0; min-height: 34px; grid-template-columns: 76px minmax(0, 1fr); align-items: center; gap: 7px; padding: 3px 8px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.detail-grid div.is-wide { grid-column: span 2; }
.detail-grid dt { color: var(--muted); font-size: 10px; white-space: nowrap; }
.detail-grid dd { min-width: 0; margin: 0; overflow-wrap: anywhere; color: var(--navy); font-size: 12px; font-weight: 600; line-height: 1.4; }
.form-card :deep(.el-form) { max-width: 860px; }
.form-card :deep(.el-input), .form-card :deep(.el-textarea), .form-card :deep(.el-select) { width: 100%; }
.form-card .currency { width: 110px; margin-left: 12px; }
2026-09-14 10:02:37 +08:00
@media (max-width: 767px) {
.business-orders { gap: 14px; }
.page-head { align-items: flex-start; gap: 12px; }
.page-head h2 { font-size: 24px; }
.page-head > div { min-width: 0; }
.detail-grid { grid-template-columns: 1fr; }
2026-09-18 16:48:35 +08:00
.detail-grid div, .detail-grid div.is-wide { grid-column: auto; }
2026-09-14 10:02:37 +08:00
.detail-grid dd { overflow-wrap: break-word; word-break: normal; }
2026-09-18 16:48:35 +08:00
.business-detail-heading { align-items: flex-start; flex-direction: column; }
2026-09-14 10:02:37 +08:00
.form-card :deep(.el-form-item) { display: block; }
.form-card :deep(.el-form-item__label) {
width: auto !important;
height: auto;
margin-bottom: 7px;
line-height: 1.4;
justify-content: flex-start;
}
.form-card :deep(.el-form-item__content) { width: 100%; margin-left: 0 !important; }
.form-card :deep(.el-input-number) { width: 100%; }
.form-card .budget-form-item :deep(.el-form-item__content) { display: flex; flex-wrap: nowrap; }
.form-card .budget-form-item :deep(.el-input-number) { width: auto; min-width: 0; flex: 1 1 auto; }
.form-card .currency { width: 96px; flex: 0 0 96px; margin-left: 8px; }
}
</style>