优化客户下单页面

This commit is contained in:
2026-08-12 17:46:46 +08:00
parent c55aac436c
commit ef9859078d
4 changed files with 298 additions and 134 deletions
@@ -40,12 +40,12 @@ const emit = defineEmits<{
</template>
<style scoped>
.first-leg-editor-page { min-width: 0; border: 1px solid var(--border); background: #fff; }
.editor-page-nav { display: flex; min-height: 52px; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 20px; border-bottom: 1px solid var(--border); }
.first-leg-editor-page { min-width: 0; border: 1px solid #edf0f5; background: #fff; box-shadow: 0 5px 18px rgba(15, 31, 58, .04); }
.editor-page-nav { display: flex; min-height: 52px; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 24px; border-bottom: 1px solid #edf0f5; }
.editor-page-nav span { color: var(--muted); font-size: 12px; }
.editor-page-body { padding: 22px 24px 30px; }
.editor-page-footer { position: sticky; z-index: 3; bottom: 0; display: flex; justify-content: center; padding: 12px 24px; border-top: 1px solid var(--border); background: rgba(255, 255, 255, 0.96); box-shadow: 0 -6px 18px rgba(15, 31, 58, .06); backdrop-filter: blur(10px); }
.editor-page-footer :deep(.editor-actions) { width: min(100%, 560px); justify-content: center; }
.editor-page-body { padding: 26px 32px 34px; }
.editor-page-footer { position: sticky; z-index: 3; bottom: 0; display: flex; justify-content: center; padding: 12px 24px; border-top: 1px solid #edf0f5; background: rgba(255, 255, 255, 0.96); box-shadow: 0 -6px 18px rgba(15, 31, 58, .06); backdrop-filter: blur(10px); }
.editor-page-footer :deep(.editor-actions) { width: min(100%, 900px); justify-content: flex-start; }
.editor-page-footer :deep(.editor-actions > .el-button) { min-width: 132px; min-height: 42px; }
.editor-page-footer :deep(.editor-actions > .el-button:last-child) { min-width: 172px; }
@@ -159,9 +159,8 @@ test('requires an enabled platform receiving warehouse in the order form', () =>
assert.match(source, /availableWarehouses\.value\.length === 0/)
assert.match(source, /label="国内收货仓"[^>]*required/)
assert.match(source, /v-model="form\.receivingWarehouseId"/)
assert.match(source, /selectedWarehouse\.businessHours/)
assert.match(source, /class="selected-warehouse" aria-label="所选收货仓详情"/)
assert.doesNotMatch(source, /class="selected-warehouse"[^>]*>[\s\S]*?<strong>\{\{ selectedWarehouse\.name \}\}<\/strong>/)
assert.doesNotMatch(source, /selectedWarehouse\.businessHours/)
assert.doesNotMatch(source, /class="selected-warehouse" aria-label="所选收货仓详情"/)
assert.match(source, /first-leg-editor \.el-drawer__header\) \{[^}]*margin-bottom: 0;[^}]*border-bottom: 1px solid var\(--border\)/)
assert.match(source, /first-leg-editor \.el-drawer__body\) \{[^}]*min-height: 0;[^}]*padding: 18px 22px 32px;[^}]*overscroll-behavior: contain/)
assert.match(source, /first-leg-editor \.el-drawer__footer\) \{[^}]*border-top: 1px solid var\(--border\);[^}]*background: #fff/)
@@ -213,9 +212,9 @@ test('collects the backend-required first-leg fields', () => {
test('distills order creation into the confirmed prototype sections with server pricing', () => {
assert.match(source, /<h3>目的地信息<\/h3>/)
assert.match(source, /<h3>发货信息<\/h3>/)
assert.match(source, /<h3>客户下单<\/h3>/)
assert.match(source, /<h3>货物明细<\/h3>/)
assert.ok(source.indexOf('<h3>发货信息</h3>') < source.indexOf('<h3>目的地信息</h3>'))
assert.ok(source.indexOf('<h3>客户下单</h3>') < source.indexOf('<h3>目的地信息</h3>'))
assert.ok(source.indexOf('<h3>目的地信息</h3>') < source.indexOf('<h3>货物明细</h3>'))
assert.match(source, /class="segmented-options"/)
assert.match(source, /class="prototype-form-row prototype-form-row--tabs"/)
+287 -122
View File
@@ -2,8 +2,8 @@
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox, type FormInstance, type FormItemRule, type FormRules, type UploadRequestOptions } from 'element-plus'
import { AlertTriangle, Box, Eye, FilePlus2, ImagePlus, MapPin, PackageCheck, Pencil, Plus, Send, Trash2 } from '@lucide/vue'
import { FIRST_LEG_ORDER_STATUSES, FIRST_LEG_TRANSPORT_MODES, type FirstLegOrderStatus } from '@chongchong/shared'
import { AlertTriangle, Box, Download, Eye, FilePlus2, ImagePlus, MapPin, PackageCheck, Pencil, Plus, Send, Trash2, Upload } from '@lucide/vue'
import { FIRST_LEG_ORDER_STATUSES, type FirstLegOrderStatus } from '@chongchong/shared'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import { ApiError } from '@/api/http'
@@ -65,6 +65,19 @@ const channelError = ref('')
const priceEstimate = ref<MerchantPriceEstimate | null>(null)
const priceEstimateLoading = ref(false)
const priceEstimateError = ref('')
const selectedCargoIds = ref<string[]>([])
const cargoImportInput = ref<HTMLInputElement>()
const cargoTemplateColumns = [
['SKU NO.', 'sku'], ['中文品名', 'cargoNameCn'], ['英文品名', 'cargoNameEn'], ['报关 HS CODE', 'hsCode'], ['清关 HS CODE', 'customsHsCode'],
['包装类型', 'packageType'], ['箱规长 cm', 'lengthCm'], ['箱规宽 cm', 'widthCm'], ['箱规高 cm', 'heightCm'], ['每箱数量', 'unitsPerPackage'],
['箱数', 'packageCount'], ['总毛重', 'grossWeightKg'], ['总体积', 'volumeCbm'], ['总数量', 'pieces'], ['总净重', 'netWeightKg'],
['清关申报单价 USD', 'unitDeclaredValue'], ['清关申报金额 USD', 'declaredValue'], ['材质', 'material'], ['用途', 'usage'], ['型号', 'model'],
['品牌', 'brand'], ['产品长 cm', 'productLengthCm'], ['产品宽 cm', 'productWidthCm'], ['产品高 cm', 'productHeightCm'], ['危险品', 'dangerousGoods'],
['货件编号', 'shipmentId'], ['跟踪编号', 'referenceId'], ['UPC NO.', 'upc'], ['备注', 'remark'],
] as const
const cargoNumberKeys = new Set<string>(['lengthCm', 'widthCm', 'heightCm', 'unitsPerPackage', 'packageCount', 'grossWeightKg', 'volumeCbm', 'pieces', 'netWeightKg', 'unitDeclaredValue', 'declaredValue', 'productLengthCm', 'productWidthCm', 'productHeightCm'])
const orderColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
{ key: 'order', label: '订单', minWidth: 190, slotName: 'order' },
@@ -979,6 +992,103 @@ function removeCargoItem(index: number) {
resetEditorValidation()
}
const allCargoSelected = computed(() => form.items.length > 0 && selectedCargoIds.value.length === form.items.length)
const someCargoSelected = computed(() => selectedCargoIds.value.length > 0 && !allCargoSelected.value)
function toggleAllCargoItems(checked: boolean | string | number) {
selectedCargoIds.value = checked ? form.items.map((item) => item.id) : []
}
function toggleCargoItem(itemId: string, checked: boolean | string | number) {
if (checked) {
if (!selectedCargoIds.value.includes(itemId)) selectedCargoIds.value.push(itemId)
return
}
selectedCargoIds.value = selectedCargoIds.value.filter((id) => id !== itemId)
}
function removeSelectedCargoItems() {
if (!selectedCargoIds.value.length) {
ElMessage.warning('请先勾选要删除的货物')
return
}
if (selectedCargoIds.value.length >= form.items.length) {
ElMessage.warning('订单至少需要保留一条货物明细')
return
}
const selectedIds = new Set(selectedCargoIds.value)
const removedItems = form.items.filter((item) => selectedIds.has(item.id))
form.items.splice(0, form.items.length, ...form.items.filter((item) => !selectedIds.has(item.id)))
releaseProductImagePreviews(removedItems)
selectedCargoIds.value = []
resetEditorValidation()
}
function downloadCargoTemplate() {
const headers = cargoTemplateColumns.map(([label]) => `<th>${label}</th>`).join('')
const example = cargoTemplateColumns.map(([label]) => {
if (label === '包装类型') return '<td>CARTON</td>'
if (label === '危险品') return '<td>否</td>'
if (['每箱数量', '箱数', '总数量'].includes(label)) return '<td>1</td>'
return '<td></td>'
}).join('')
const html = `<!doctype html><html><head><meta charset="UTF-8"></head><body><table><thead><tr>${headers}</tr></thead><tbody><tr>${example}</tr></tbody></table></body></html>`
const url = URL.createObjectURL(new Blob(['\ufeff', html], { type: 'application/vnd.ms-excel;charset=utf-8' }))
const anchor = document.createElement('a')
anchor.href = url
anchor.download = '货物明细导入模板.xls'
anchor.click()
URL.revokeObjectURL(url)
}
function parseCargoTemplateRows(text: string) {
const documentNode = new DOMParser().parseFromString(text, 'text/html')
const tableRows = [...documentNode.querySelectorAll('table tr')]
if (tableRows.length < 2) return []
const headers = [...tableRows[0].querySelectorAll('th,td')].map((cell) => cell.textContent?.trim() || '')
return tableRows.slice(1).map((row) => {
const values = [...row.querySelectorAll('td')].map((cell) => cell.textContent?.trim() || '')
return { headers, values }
}).filter(({ values }) => values.some(Boolean))
}
async function importCargoTemplate(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
input.value = ''
if (!file) return
if (!file.name.toLowerCase().endsWith('.xls')) {
ElMessage.error('请上传由“下载模板”生成的 .xls 文件')
return
}
try {
const parsedRows = parseCargoTemplateRows(await file.text())
if (!parsedRows.length) throw new Error('模板中没有可导入的货物数据')
const imported = parsedRows.map(({ headers, values }) => {
const item = createEmptyCargoItem(`imported-cargo-${++cargoSequence}`)
for (const [columnLabel, itemKey] of cargoTemplateColumns) {
const value = values[headers.indexOf(columnLabel)] ?? ''
if (itemKey === 'dangerousGoods') item.dangerousGoods = ['是', 'true', '1', 'yes'].includes(value.toLowerCase())
else if (cargoNumberKeys.has(itemKey)) (item as unknown as Record<string, number>)[itemKey] = Number(value) || 0
else (item as unknown as Record<string, string>)[itemKey] = value
}
item.packageType = item.packageType || 'CARTON'
item.unitsPerPackage = item.unitsPerPackage || 1
item.packageCount = item.packageCount || 1
item.pieces = item.pieces || 1
return item
})
const shouldReplaceDefaultRow = form.items.length === 1 && !form.items[0].cargoNameCn && !form.items[0].sku
if (shouldReplaceDefaultRow) form.items.splice(0, 1, ...imported)
else form.items.push(...imported)
selectedCargoIds.value = []
resetEditorValidation()
ElMessage.success(`已导入 ${imported.length} 行货物明细`)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '货物明细导入失败')
}
}
async function validateEditorForm() {
let fieldsValid = false
try {
@@ -1157,7 +1267,7 @@ async function searchOrders(model: QueryModel) {
<FirstLegOrderEditorShell v-model="editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : '创建订单'" :before-close="requestCloseEditor" @back="closeEditor">
<el-form ref="orderFormRef" v-loading="isCreatePage && supportingDataLoading" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll-to-error>
<section class="order-form-section prototype-section">
<div class="order-section-heading"><div><h3>发货信息</h3><p>先选择货物如何送到国内收货仓,并填写本次运输安排。</p></div></div>
<div class="order-section-heading"><div><h3>客户下单</h3><p>先选择货物如何送到国内收货仓,并填写本次运输安排。</p></div></div>
<div class="prototype-form-flow shipping-form-flow">
<el-form-item label="取货方式" prop="pickupMethod" required class="prototype-form-row prototype-form-row--tabs"><el-radio-group v-model="form.pickupMethod" class="segmented-options" @change="changePickupMethod"><el-radio-button value="SELF_EXPRESS">快递自送</el-radio-button><el-radio-button value="PICKUP">我司提货</el-radio-button><el-radio-button value="SELF_TRUCK">卡车自送</el-radio-button></el-radio-group></el-form-item>
<template v-if="form.pickupMethod === 'PICKUP' && form.shipperPartySnapshot">
@@ -1167,9 +1277,8 @@ async function searchOrders(model: QueryModel) {
<el-form-item label="城市" prop="shipperPartySnapshot.city" required :rules="requiredTextRules('请输入提货城市')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.shipperPartySnapshot.city" maxlength="100" placeholder="请输入城市" /></el-form-item>
<el-form-item label="提货地址" prop="shipperPartySnapshot.address" required :rules="requiredTextRules('请输入提货地址')" class="prototype-form-row prototype-form-row--wide"><el-input v-model="form.shipperPartySnapshot.address" maxlength="300" placeholder="请输入详细提货地址" /></el-form-item>
</template>
<el-form-item label="国内收货仓" prop="receivingWarehouseId" required class="prototype-form-row prototype-form-row--wide"><el-radio-group v-model="form.receivingWarehouseId" class="warehouse-options" @change="loadSelectedWarehouse"><el-radio-button v-for="warehouse in availableWarehouses" :key="warehouse.id" :value="warehouse.id">{{ warehouse.name }}</el-radio-button></el-radio-group></el-form-item>
<el-form-item label="国内收货仓" prop="receivingWarehouseId" required class="prototype-form-row prototype-form-row--wide"><el-radio-group v-model="form.receivingWarehouseId" class="warehouse-options" @change="loadSelectedWarehouse"><el-radio v-for="warehouse in availableWarehouses" :key="warehouse.id" :value="warehouse.id">{{ warehouse.name }}</el-radio></el-radio-group></el-form-item>
</div>
<div v-if="selectedWarehouse" class="selected-warehouse" aria-label="所选收货仓详情"><MapPin :size="17" /><div><span>{{ selectedWarehouse.address ? `${selectedWarehouse.province}${selectedWarehouse.city} · ${selectedWarehouse.address}` : '仓库地址详情暂不可用' }}</span><small>{{ selectedWarehouse.businessHours ? `收货时间:${selectedWarehouse.businessHours}` : '收货时间暂不可用' }}</small><small v-if="selectedWarehouse.contactName || selectedWarehouse.contactPhone">联系人:{{ selectedWarehouse.contactName || '-' }} {{ selectedWarehouse.contactPhone }}</small></div></div>
<div class="order-form-grid compact-transport-grid">
<el-form-item label="渠道" prop="channelCode" required>
<el-select v-model="form.channelCode" :loading="channelLoading" :disabled="!form.receivingWarehouseId || channelLoading" :placeholder="form.receivingWarehouseId ? '请选择运输渠道' : '请先选择国内收货仓'" @change="selectChannel"><el-option v-for="channel in channelOptions" :key="channel.channelCode" :label="channel.channelName" :value="channel.channelCode" /></el-select>
@@ -1180,18 +1289,18 @@ async function searchOrders(model: QueryModel) {
<el-radio-group v-model="form.pricingTaxMode"><el-radio v-for="mode in availablePricingTaxModes" :key="mode" :value="mode">{{ pricingTaxModeLabel(mode) }}</el-radio></el-radio-group>
<small v-if="form.channelCode && availablePricingTaxModes.length === 0" class="field-guidance field-guidance--error">当前渠道没有可用报价方式,请重新选择渠道。</small>
</el-form-item>
<el-form-item label="买单 / 退税" prop="taxMode" required><el-radio-group v-model="form.taxMode"><el-radio value="BUY_DECLARATION">买单报关</el-radio><el-radio value="TAX_REFUND">退税报关</el-radio></el-radio-group></el-form-item>
<el-form-item label="运输方式" prop="transportMode" required><el-radio-group v-model="form.transportMode"><el-radio v-for="item in FIRST_LEG_TRANSPORT_MODES" :key="item.code" :value="item.code">{{ item.label }}</el-radio></el-radio-group></el-form-item>
<el-form-item label="买单 / 退税" prop="taxMode" required><el-radio-group v-model="form.taxMode"><el-radio value="BUY_DECLARATION">买单</el-radio><el-radio value="TAX_REFUND">退税</el-radio></el-radio-group></el-form-item>
<el-form-item label="是否整柜" prop="transportMode" required><el-radio-group v-model="form.transportMode"><el-radio value="FCL">整柜</el-radio><el-radio value="LCL">散货</el-radio></el-radio-group></el-form-item>
<el-form-item label="是否商检" prop="customsInspection" required><el-radio-group v-model="form.customsInspection"><el-radio :value="true">是</el-radio><el-radio :value="false">否</el-radio></el-radio-group></el-form-item>
<el-form-item label="期望出货日期"><el-date-picker v-model="form.expectedDepartureDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择期望出货日期" /></el-form-item>
<el-form-item label="订单备注"><el-input v-model="form.customerRemark" maxlength="1000" placeholder="选填,例如:入仓前请电话联系" /></el-form-item>
<el-form-item label="订单备注"><el-input v-model="form.customerRemark" type="textarea" :rows="3" resize="vertical" maxlength="300" show-word-limit placeholder="选填,例如:入仓前请电话联系" /></el-form-item>
</div>
</section>
<section class="order-form-section prototype-section">
<div class="order-section-heading"><div><h3>目的地信息</h3><p>再选择货物最终送达的仓库或私人地址。</p></div></div>
<section class="order-form-section prototype-section destination-section">
<div class="order-section-heading destination-section-heading"><div><h3>目的地信息</h3></div></div>
<div class="prototype-form-flow destination-form-flow">
<el-form-item label="目的仓类型" prop="destinationType" required class="prototype-form-row prototype-form-row--tabs"><el-radio-group v-model="form.destinationType" class="segmented-options" @change="changeDestinationType"><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="OVERSEAS">海外仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group></el-form-item>
<el-form-item label="目的仓类型" prop="destinationType" required class="prototype-form-row prototype-form-row--tabs destination-type-row"><el-radio-group v-model="form.destinationType" class="segmented-options destination-type-options" @change="changeDestinationType"><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="OVERSEAS">海外仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group></el-form-item>
<el-form-item v-if="form.destinationType === 'PRIVATE'" label="私人地址簿" class="prototype-form-row prototype-form-row--wide address-book-row">
<div class="address-book-control"><el-select v-model="selectedPrivateAddressId" clearable filterable placeholder="选择当前账号保存的私人地址" @change="selectPrivateAddress"><el-option v-for="address in privateAddresses" :key="address.id" :label="address.label" :value="address.id" /></el-select><el-button @click="savePrivateAddress">保存当前地址</el-button><el-button v-if="selectedPrivateAddressId" link type="danger" @click="removePrivateAddress">移除</el-button></div>
@@ -1213,77 +1322,60 @@ async function searchOrders(model: QueryModel) {
</section>
<section class="order-form-section prototype-section cargo-section">
<div class="order-section-heading"><div><h3>货物明细</h3></div><el-button @click="addCargoItem"><Plus :size="16" />添加货物</el-button></div>
<article v-for="(item, index) in form.items" :key="item.id" class="cargo-entry">
<header><strong>货物 {{ index + 1 }}</strong><el-button link type="danger" :aria-label="`删除货物 ${index + 1}`" @click="removeCargoItem(index)"><Trash2 :size="16" />删除</el-button></header>
<section class="cargo-field-section">
<h4>商品信息</h4>
<div class="cargo-form-grid cargo-form-grid--basic">
<el-form-item label="SKU NO." :prop="`items.${index}.sku`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入 SKU NO.') : undefined"><el-input v-model="item.sku" maxlength="64" placeholder="SKU NO." /></el-form-item>
<el-form-item label="中文品名" :prop="`items.${index}.cargoNameCn`" required :rules="requiredTextRules('请输入中文品名')"><el-input v-model="item.cargoNameCn" maxlength="128" placeholder="例如:蓝牙音箱" /></el-form-item>
<el-form-item label="英文品名 DESCRIPTION OF GOODS"><el-input v-model="item.cargoNameEn" maxlength="128" placeholder="Bluetooth speakers" /></el-form-item>
<el-form-item label="图片 PRODUCT PICTURE" class="product-image-field">
<div class="product-image-control">
<div v-if="item.productImagePreviewUrl" class="product-image-preview"><img :src="item.productImagePreviewUrl" :alt="`${item.cargoNameCn || `货物 ${index + 1}`}产品图片`" /><el-button circle type="danger" size="small" :aria-label="`移除货物 ${index + 1} 产品图片`" @click="removeCargoProductImage(item)"><Trash2 :size="14" /></el-button></div>
<div v-else class="product-image-placeholder"><ImagePlus :size="22" /><span>{{ item.productImageUploading ? '上传中...' : '暂无图片' }}</span></div>
<el-upload :show-file-list="false" accept=".jpg,.jpeg,.png,image/jpeg,image/png" :disabled="item.productImageUploading" :http-request="(options: UploadRequestOptions) => uploadCargoProductImage(item, options)"><el-button :loading="item.productImageUploading"><ImagePlus :size="15" />{{ item.productImageFileId ? '替换图片' : '上传图片' }}</el-button></el-upload>
<small>JPG、PNG,最大 5MB</small>
<span v-if="item.productImageError" class="product-image-error" role="alert">{{ item.productImageError }}</span>
</div>
</el-form-item>
<el-form-item label="UPC NO." :prop="`items.${index}.upc`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入 UPC NO.') : undefined"><el-input v-model="item.upc" maxlength="64" placeholder="UPC NO." /></el-form-item>
</div>
</section>
<div class="order-section-heading cargo-table-heading"><div><h3>货物明细</h3></div></div>
<div class="cargo-table-toolbar">
<div class="cargo-import-actions"><input ref="cargoImportInput" class="cargo-import-input" type="file" accept=".xls,application/vnd.ms-excel" @change="importCargoTemplate" /><el-button class="cargo-upload-button" @click="cargoImportInput?.click()"><Upload :size="16" />上传 Excel</el-button><el-button class="cargo-download-button" @click="downloadCargoTemplate"><Download :size="16" />下载模板</el-button></div>
<div class="cargo-table-actions"><el-button @click="addCargoItem"><Plus :size="16" />添加行</el-button><el-button type="danger" plain :disabled="!selectedCargoIds.length" @click="removeSelectedCargoItems"><Trash2 :size="15" />删除所选行</el-button></div>
</div>
<div class="cargo-table-wrap">
<table class="cargo-edit-table">
<thead><tr>
<th class="cargo-check-cell"><el-checkbox :model-value="allCargoSelected" :indeterminate="someCargoSelected" aria-label="全选货物" @change="toggleAllCargoItems" /></th><th class="cargo-index-cell">#</th>
<th>SKU NO.<small>FBA必填</small></th><th>中文品名</th><th class="cargo-image-column">图片</th><th>英文品名</th><th>报关 HS CODE</th><th>清关 HS CODE</th><th>包装类型</th>
<th class="cargo-number-column">箱规长 cm</th><th class="cargo-number-column">箱规宽 cm</th><th class="cargo-number-column">箱规高 cm</th><th class="cargo-number-column">每箱数量</th><th class="cargo-number-column">箱数</th><th class="cargo-number-column">总毛重</th><th class="cargo-number-column">总体积</th><th class="cargo-number-column">总数量</th><th class="cargo-number-column">总净重</th>
<th>清关申报单价 USD</th><th>清关申报金额 USD</th><th>材质</th><th>用途</th><th>型号</th><th>品牌</th><th class="cargo-dimensions-column">产品规格 长×宽×高</th><th>危险品</th><th>货件编号<small>FBA必填</small></th><th>跟踪编号<small>FBA必填</small></th><th>UPC NO.<small>FBA必填</small></th><th>备注</th><th class="cargo-operation-column">操作</th>
</tr></thead>
<tbody><tr v-for="(item, index) in form.items" :key="item.id">
<td class="cargo-check-cell"><el-checkbox :model-value="selectedCargoIds.includes(item.id)" :aria-label="`选择货物 ${index + 1}`" @change="toggleCargoItem(item.id, $event)" /></td><td class="cargo-index-cell">{{ index + 1 }}</td>
<td><el-form-item :prop="`items.${index}.sku`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入 SKU NO.') : undefined"><el-input v-model="item.sku" maxlength="64" placeholder="SKU编号" /></el-form-item></td>
<td><el-form-item :prop="`items.${index}.cargoNameCn`" required :rules="requiredTextRules('请输入中文品名')"><el-input v-model="item.cargoNameCn" maxlength="128" placeholder="中文品名" /></el-form-item></td>
<td class="cargo-image-column"><div class="cargo-table-image"><img v-if="item.productImagePreviewUrl" :src="item.productImagePreviewUrl" :alt="`${item.cargoNameCn || `货物 ${index + 1}`}产品图片`" /><el-upload v-else :show-file-list="false" accept=".jpg,.jpeg,.png,image/jpeg,image/png" :disabled="item.productImageUploading" :http-request="(options: UploadRequestOptions) => uploadCargoProductImage(item, options)"><el-button text :loading="item.productImageUploading" aria-label="上传图片"><ImagePlus :size="17" /></el-button></el-upload><el-button v-if="item.productImagePreviewUrl" class="cargo-image-remove" text type="danger" aria-label="移除图片" @click="removeCargoProductImage(item)"><Trash2 :size="12" /></el-button></div></td>
<td><el-input v-model="item.cargoNameEn" maxlength="128" placeholder="English Name" /></td><td><el-input v-model="item.hsCode" maxlength="20" placeholder="HS Code" /></td><td><el-input v-model="item.customsHsCode" maxlength="20" placeholder="HS Code" /></td>
<td><el-form-item :prop="`items.${index}.packageType`" required :rules="requiredSelectRules('请选择包装类型')"><el-select v-model="item.packageType"><el-option label="纸箱" value="CARTON" /><el-option label="托盘" value="PALLET" /><el-option label="木箱" value="WOODEN_CASE" /></el-select></el-form-item></td>
<td><el-input-number v-model="item.lengthCm" :min="0" :precision="2" :controls="false" /></td><td><el-input-number v-model="item.widthCm" :min="0" :precision="2" :controls="false" /></td><td><el-input-number v-model="item.heightCm" :min="0" :precision="2" :controls="false" /></td>
<td><el-form-item :prop="`items.${index}.unitsPerPackage`" required :rules="positiveNumberRules('每箱数量必须大于 0', true)"><el-input-number v-model="item.unitsPerPackage" :min="1" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.packageCount`" required :rules="positiveNumberRules('箱数必须大于 0', true)"><el-input-number v-model="item.packageCount" :min="1" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.grossWeightKg`" required :rules="positiveNumberRules('总毛重必须大于 0')"><el-input-number v-model="item.grossWeightKg" :min="0" :precision="3" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.volumeCbm`" required :rules="positiveNumberRules('总体积必须大于 0')"><el-input-number v-model="item.volumeCbm" :min="0" :precision="4" :controls="false" /></el-form-item></td><td><el-form-item :prop="`items.${index}.pieces`" required :rules="positiveNumberRules('总数量必须大于 0', true)"><el-input-number v-model="item.pieces" :min="1" :controls="false" /></el-form-item></td><td><el-input-number v-model="item.netWeightKg" :min="0" :precision="3" :controls="false" /></td>
<td><el-input-number v-model="item.unitDeclaredValue" :min="0" :precision="2" :controls="false" /></td><td><el-input-number v-model="item.declaredValue" :min="0" :precision="2" :controls="false" /></td><td><el-input v-model="item.material" maxlength="128" placeholder="材质" /></td><td><el-input v-model="item.usage" maxlength="128" placeholder="用途" /></td><td><el-input v-model="item.model" maxlength="64" placeholder="型号" /></td><td><el-input v-model="item.brand" maxlength="64" placeholder="品牌" /></td>
<td class="cargo-dimensions-column"><div class="cargo-table-dimensions"><el-input-number v-model="item.productLengthCm" :min="0" :precision="2" :controls="false" /><span>×</span><el-input-number v-model="item.productWidthCm" :min="0" :precision="2" :controls="false" /><span>×</span><el-input-number v-model="item.productHeightCm" :min="0" :precision="2" :controls="false" /></div></td><td class="cargo-danger-cell"><el-switch v-model="item.dangerousGoods" /></td>
<td><el-form-item :prop="`items.${index}.shipmentId`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('亚马逊货物请输入 Shipment ID') : undefined"><el-input v-model="item.shipmentId" maxlength="64" placeholder="SHIPMENT ID" /></el-form-item></td><td><el-form-item :prop="`items.${index}.referenceId`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('亚马逊货物请输入 Reference ID') : undefined"><el-input v-model="item.referenceId" maxlength="64" placeholder="REFERENCE ID" /></el-form-item></td><td><el-form-item :prop="`items.${index}.upc`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入 UPC NO.') : undefined"><el-input v-model="item.upc" maxlength="64" placeholder="UPC NO." /></el-form-item></td><td><el-input v-model="item.remark" maxlength="500" placeholder="备注" /></td><td class="cargo-operation-column"><el-button link type="danger" :aria-label="`删除货物 ${index + 1}`" @click="removeCargoItem(index)"><Trash2 :size="15" />删除</el-button></td>
</tr></tbody>
</table>
</div>
<div class="cargo-table-hint"><span>FBA 货物的 SKU NO.、UPC NO. 为必填项</span><i></i><span>亚马逊货物的货件编号和跟踪编号为必填项</span></div>
</section>
<section class="cargo-field-section">
<h4>箱规、数量与重量</h4>
<div class="cargo-form-grid">
<el-form-item label="包装类型" :prop="`items.${index}.packageType`" required :rules="requiredSelectRules('请选择包装类型')"><el-select v-model="item.packageType"><el-option label="纸箱" value="CARTON" /><el-option label="托盘" value="PALLET" /><el-option label="木箱" value="WOODEN_CASE" /></el-select></el-form-item>
<el-form-item label="箱规长 cm"><el-input-number v-model="item.lengthCm" :min="0" :precision="2" controls-position="right" /></el-form-item>
<el-form-item label="箱规宽 cm"><el-input-number v-model="item.widthCm" :min="0" :precision="2" controls-position="right" /></el-form-item>
<el-form-item label="箱规高 cm"><el-input-number v-model="item.heightCm" :min="0" :precision="2" controls-position="right" /></el-form-item>
<el-form-item label="每箱数量 QTY/CTN" :prop="`items.${index}.unitsPerPackage`" required :rules="positiveNumberRules('每箱数量必须为大于 0 的整数', true)"><el-input-number v-model="item.unitsPerPackage" :min="1" controls-position="right" /></el-form-item>
<el-form-item label="箱数 CARTONS" :prop="`items.${index}.packageCount`" required :rules="positiveNumberRules('箱数必须为大于 0 的整数', true)"><el-input-number v-model="item.packageCount" :min="1" controls-position="right" /></el-form-item>
<el-form-item label="总毛重 T.K.GW" :prop="`items.${index}.grossWeightKg`" required :rules="positiveNumberRules('总毛重必须大于 0')"><el-input-number v-model="item.grossWeightKg" :min="0" :precision="3" controls-position="right" /></el-form-item>
<el-form-item label="总体积 T.CBM" :prop="`items.${index}.volumeCbm`" required :rules="positiveNumberRules('总体积必须大于 0')"><el-input-number v-model="item.volumeCbm" :min="0" :precision="4" controls-position="right" /></el-form-item>
<el-form-item label="总数量 T.QTY" :prop="`items.${index}.pieces`" required :rules="positiveNumberRules('总数量必须为大于 0 的整数', true)"><el-input-number v-model="item.pieces" :min="1" controls-position="right" /></el-form-item>
<el-form-item label="总净重 T.QNW"><el-input-number v-model="item.netWeightKg" :min="0" :precision="3" controls-position="right" /></el-form-item>
</div>
</section>
<section class="cargo-field-section">
<h4>报关与产品资料</h4>
<div class="cargo-form-grid">
<el-form-item label="报关 HS CODE"><el-input v-model="item.hsCode" maxlength="20" placeholder="报关 HS CODE" /></el-form-item>
<el-form-item label="清关 HS CODE"><el-input v-model="item.customsHsCode" maxlength="20" placeholder="清关 HS CODE" /></el-form-item>
<el-form-item label="清关申报单价 USD"><el-input-number v-model="item.unitDeclaredValue" :min="0" :precision="2" controls-position="right" /></el-form-item>
<el-form-item label="清关申报金额 USD"><el-input-number v-model="item.declaredValue" :min="0" :precision="2" controls-position="right" /></el-form-item>
<el-form-item label="中英文材质 MATERIAL"><el-input v-model="item.material" maxlength="128" placeholder="材质 / Material" /></el-form-item>
<el-form-item label="中英文用途 USE FOR"><el-input v-model="item.usage" maxlength="128" placeholder="用途 / Use for" /></el-form-item>
<el-form-item label="型号 MODEL"><el-input v-model="item.model" maxlength="64" placeholder="型号" /></el-form-item>
<el-form-item label="品牌 LOGO"><el-input v-model="item.brand" maxlength="64" placeholder="品牌" /></el-form-item>
<el-form-item label="产品规格 长×宽×高" class="product-dimensions-field"><div class="dimension-inputs"><el-input-number v-model="item.productLengthCm" :min="0" :precision="2" placeholder="长 cm" controls-position="right" /><span>×</span><el-input-number v-model="item.productWidthCm" :min="0" :precision="2" placeholder="宽 cm" controls-position="right" /><span>×</span><el-input-number v-model="item.productHeightCm" :min="0" :precision="2" placeholder="高 cm" controls-position="right" /></div></el-form-item>
<el-form-item label="危险品"><el-switch v-model="item.dangerousGoods" /></el-form-item>
</div>
</section>
<section class="cargo-field-section">
<h4>货件与备注</h4>
<div class="cargo-form-grid">
<el-form-item label="货件编号 SHIPMENT ID" :prop="`items.${index}.shipmentId`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('亚马逊货物请输入 Shipment ID') : undefined"><el-input v-model="item.shipmentId" maxlength="64" placeholder="SHIPMENT ID" /></el-form-item>
<el-form-item label="跟踪编号 REFERENCE ID" :prop="`items.${index}.referenceId`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('亚马逊货物请输入 Reference ID') : undefined"><el-input v-model="item.referenceId" maxlength="64" placeholder="REFERENCE ID" /></el-form-item>
<el-form-item label="备注" class="cargo-remark-field"><el-input v-model="item.remark" maxlength="500" placeholder="选填,例如:注意防潮" /></el-form-item>
</div>
</section>
</article>
<aside class="price-estimate" aria-live="polite">
<div class="price-estimate__heading"><div><strong>参考运费</strong><span>仅供参考,最终以实际结算为准</span></div><el-button v-if="priceEstimateError" link type="primary" @click="loadPriceEstimate">重新估价</el-button></div>
<div v-if="priceEstimateLoading" class="price-estimate__state">正在匹配当前生效价格...</div>
<div v-else-if="priceEstimate" class="price-estimate__result"><strong>{{ priceEstimate.currency }} {{ Number(priceEstimate.estimatedAmount).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</strong><span>计费量 {{ priceEstimate.chargeableQuantity }} {{ priceEstimate.chargeUnit }} · 单价 {{ priceEstimate.currency }} {{ priceEstimate.unitPrice }}</span><small>{{ priceEstimate.disclaimer || '仅供参考,最终以实际结算为准' }}</small></div>
<div v-else-if="priceEstimateError" class="price-estimate__state is-error">暂未取得参考价:{{ priceEstimateError }}。您仍可正常保存订单草稿。</div>
<div v-else class="price-estimate__state">选择收货仓、渠道和目的仓,并填写货物重量与体积后显示参考价。</div>
</aside>
<section class="order-form-section prototype-section cost-estimate-section" aria-label="费用预估">
<div class="order-section-heading"><div><h3>费用预估</h3></div></div>
<div class="cost-estimate-cards">
<article class="cost-estimate-card">
<span class="cost-estimate-badge is-fastest">最快</span>
<strong class="cost-estimate-days">10天</strong>
<div class="cost-estimate-price"><small>¥</small><strong>3,310</strong></div>
<p>优先配送,时效保障</p>
</article>
<article class="cost-estimate-card is-recommended">
<span class="cost-estimate-badge is-recommend">推荐</span>
<strong class="cost-estimate-days">13天</strong>
<div class="cost-estimate-price"><small>¥</small><strong>2,990</strong></div>
<p>性价比之选</p>
</article>
<article class="cost-estimate-card">
<span class="cost-estimate-badge is-economy">经济</span>
<strong class="cost-estimate-days">16天</strong>
<div class="cost-estimate-price"><small>¥</small><strong>2,500</strong></div>
<p>经济实惠</p>
</article>
</div>
<p class="cost-estimate-note">以上费用为静态预估展示,实际费用以最终结算结果为准。</p>
</section>
</el-form>
<template #footer><div class="editor-actions"><el-button :disabled="saving" @click="closeEditor">返回</el-button><el-button v-if="editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" type="primary" :loading="saving" @click="submitOrder"><Send :size="16" />{{ companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' }}</el-button></div></template>
@@ -1444,63 +1536,132 @@ async function searchOrders(model: QueryModel) {
:global(.first-leg-editor .el-drawer__header) { flex: 0 0 auto; min-height: 58px; margin-bottom: 0; padding: 17px 22px; border-bottom: 1px solid var(--border); }
:global(.first-leg-editor .el-drawer__body) { min-height: 0; padding: 18px 22px 32px; overscroll-behavior: contain; }
:global(.first-leg-editor .el-drawer__footer) { flex: 0 0 auto; padding: 12px 22px; border-top: 1px solid var(--border); background: #fff; }
.order-form { width: min(100%, 1000px); margin: 0 auto; padding: 0 0 12px; }
.is-create-route { gap: 0; }
.is-create-route .first-leg-editor-page { width: 100%; }
.is-create-route .supporting-data-error, .is-create-route > .el-alert { margin-bottom: 14px; }
.is-create-route .prototype-order-form { width: 100%; margin: 0; padding: 0 0 16px; }
.is-create-route .prototype-section:not(.cargo-section):not(.cost-estimate-section):not(.destination-section) { width: min(100%, 900px); }
.order-form { width: min(100%, 1000px); margin: 0; padding: 0 0 12px; }
.order-form :deep(.el-form-item__error) { position: static; padding-top: 6px; overflow-wrap: anywhere; line-height: 1.45; }
.order-form-section + .order-form-section { margin-top: 24px; padding-top: 22px; border-top: 1px solid var(--border); }
.order-form-section + .order-form-section { margin-top: 30px; padding-top: 28px; border-top: 1px solid #edf0f5; }
.order-form-section h3 { margin: 0 0 16px; color: var(--navy); font-size: 16px; }
.order-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 18px; }
.order-form-grid > *, .cargo-form-grid > * { min-width: 0; }
.order-form :deep(.el-select), .order-form :deep(.el-date-editor), .order-form :deep(.el-input-number) { width: 100%; }
.form-span-full { grid-column: 1 / -1; }
.prototype-section { padding: 2px 2px 0; }
.segmented-options { display: flex; width: min(100%, 620px); }
.prototype-section { padding: 4px 0 0; }
.segmented-options { display: flex; width: min(100%, 560px); overflow: hidden; border: 1px solid #e3e8f0; border-radius: 5px; background: #fff; }
.segmented-options :deep(.el-radio-button) { flex: 1; }
.segmented-options :deep(.el-radio-button__inner) { display: flex; width: 100%; min-height: 42px; box-sizing: border-box; align-items: center; justify-content: center; padding: 11px 16px; }
.warehouse-options { display: flex; flex-wrap: wrap; gap: 8px; }
.warehouse-options :deep(.el-radio-button__inner) { display: flex; min-height: 40px; box-sizing: border-box; align-items: center; justify-content: center; border: 1px solid var(--border); border-radius: 6px !important; box-shadow: none !important; }
.warehouse-options :deep(.el-radio-button.is-active .el-radio-button__inner) { border-color: var(--primary); }
.selected-warehouse { display: flex; align-items: flex-start; gap: 10px; margin-top: 0; padding: 11px 13px; border: 1px solid #d9def8; background: #f6f7fe; color: var(--primary); }
.selected-warehouse svg { flex: 0 0 17px; margin-top: 2px; }
.selected-warehouse div { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
.selected-warehouse span, .selected-warehouse small { color: var(--muted); font-size: 11px; line-height: 1.5; }
.order-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.segmented-options :deep(.el-radio-button__inner) { display: flex; width: 100%; min-height: 42px; box-sizing: border-box; align-items: center; justify-content: center; padding: 10px 16px; border: 0; border-right: 1px solid #e3e8f0; border-radius: 0; box-shadow: none; color: #526070; font-size: 13px; }
.segmented-options :deep(.el-radio-button:last-child .el-radio-button__inner) { border-right: 0; }
.segmented-options :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) { border-color: var(--primary); background: var(--primary); box-shadow: none; color: #fff; }
.warehouse-options { display: flex; width: 100%; align-items: center; flex-wrap: wrap; gap: 10px 22px; padding-top: 9px; }
.warehouse-options :deep(.el-radio) { height: auto; margin-right: 0; color: #4d5a6c; }
.warehouse-options :deep(.el-radio__label) { padding-left: 7px; font-size: 13px; }
.order-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 18px; }
.order-section-heading h3 { margin-bottom: 5px; }
.order-section-heading p { margin: 0; color: var(--muted); font-size: 11px; }
.prototype-form-flow { display: flex; max-width: 900px; flex-direction: column; padding-top: 4px; }
.prototype-form-row { display: grid; width: 100%; grid-template-columns: 140px minmax(0, 560px); align-items: start; margin-bottom: 18px; }
.prototype-form-row--wide { grid-template-columns: 140px minmax(0, 720px); }
.prototype-form-row--compact { grid-template-columns: 140px minmax(0, 420px); }
.prototype-form-row--short { grid-template-columns: 140px minmax(0, 280px); }
.prototype-form-row--tabs { grid-template-columns: 140px minmax(0, 520px); margin-bottom: 22px; }
.prototype-form-row :deep(.el-form-item__label) { width: auto !important; min-height: 40px; justify-content: flex-end; padding: 0 20px 0 0; line-height: 40px; }
.order-section-heading p { margin: 0; color: #8b96a6; font-size: 12px; }
.prototype-form-flow { display: flex; max-width: 860px; flex-direction: column; padding-top: 2px; }
.prototype-form-row { display: grid; width: 100%; grid-template-columns: 114px minmax(0, 560px); align-items: start; margin-bottom: 16px; }
.prototype-form-row--wide { grid-template-columns: 114px minmax(0, 760px); }
.prototype-form-row--compact { grid-template-columns: 114px minmax(0, 420px); }
.prototype-form-row--short { grid-template-columns: 114px minmax(0, 280px); }
.prototype-form-row--tabs { grid-template-columns: 114px minmax(0, 560px); margin-bottom: 18px; }
.prototype-form-row :deep(.el-form-item__label) { width: auto !important; min-height: 40px; justify-content: flex-end; padding: 0 18px 0 0; line-height: 40px; color: #4d5a6c; font-size: 13px; }
.prototype-form-row :deep(.el-form-item__content) { display: block; min-width: 0; line-height: normal; }
.prototype-form-row :deep(.el-input__wrapper), .prototype-form-row :deep(.el-select__wrapper) { min-height: 40px; }
.prototype-form-row :deep(.el-input__wrapper), .prototype-form-row :deep(.el-select__wrapper) { min-height: 40px; box-shadow: 0 0 0 1px #dfe5ed inset; }
.address-book-control { display: grid; grid-template-columns: minmax(260px, 1fr) auto auto; gap: 10px; align-items: center; }
.address-book-control :deep(.el-button) { min-height: 40px; margin-left: 0; }
.field-guidance { display: block; margin-top: 5px; color: var(--muted); line-height: 1.45; }
.field-guidance--error { color: #b54708; }
.price-estimate { margin-top: 16px; padding: 15px 16px; border: 1px solid #cbd5da; background: #f7faf9; }
.price-estimate__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.price-estimate__heading > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
.price-estimate__heading strong { color: var(--navy); font-size: 13px; }
.price-estimate__heading span, .price-estimate__state, .price-estimate__result span, .price-estimate__result small { color: var(--muted); font-size: 11px; line-height: 1.5; }
.price-estimate__state { padding-top: 12px; }
.price-estimate__state.is-error { color: #b54708; }
.price-estimate__result { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: baseline; gap: 5px 14px; padding-top: 12px; }
.price-estimate__result > strong { color: var(--primary); font-size: 20px; }
.price-estimate__result > small { grid-column: 1 / -1; }
.cost-estimate-section { width: 100%; }
.cost-estimate-section .order-section-heading { margin-bottom: 12px; }
.cost-estimate-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.cost-estimate-card { position: relative; display: flex; min-height: 158px; box-sizing: border-box; align-items: center; justify-content: center; flex-direction: column; padding: 24px 18px 18px; border: 1px solid #dde4f2; border-radius: 8px; background: #fff; box-shadow: 0 2px 8px rgba(15, 31, 58, .025); text-align: center; }
.cost-estimate-card.is-recommended { border: 2px solid #637fec; background: #f8f8ff; box-shadow: 0 8px 22px rgba(99, 127, 236, .13); }
.cost-estimate-badge { position: absolute; top: -9px; right: 14px; padding: 3px 11px; border-radius: 12px; color: #fff; font-size: 10px; font-weight: 650; line-height: 1.4; }
.cost-estimate-badge.is-fastest, .cost-estimate-badge.is-recommend { background: #5368c6; }
.cost-estimate-badge.is-economy { background: #71858e; }
.cost-estimate-days { color: #30343b; font-size: 21px; }
.cost-estimate-price { display: flex; align-items: baseline; gap: 5px; margin-top: 13px; color: #637fec; }
.cost-estimate-price small { font-size: 15px; font-weight: 700; }
.cost-estimate-price strong { font-size: 29px; line-height: 1; }
.cost-estimate-card p { margin: 12px 0 0; color: #98a0ad; font-size: 11px; }
.cost-estimate-note { margin: 10px 0 0; color: #98a0ad; font-size: 10px; text-align: right; }
.selected-destination { display: flex; width: min(100%, 720px); align-items: flex-start; gap: 9px; margin: -4px 0 18px 140px; padding: 10px 12px; background: #f2f7f6; color: var(--primary); }
.selected-destination svg { flex: 0 0 17px; margin-top: 2px; }
.selected-destination div { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
.selected-destination span { color: var(--navy); font-size: 12px; font-weight: 600; }
.selected-destination small { color: var(--muted); font-size: 11px; line-height: 1.5; }
.destination-section { width: 100%; }
.destination-section-heading { margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #edf0f5; }
.destination-section-heading h3 { margin-bottom: 0; }
.destination-form-flow { width: min(100%, 900px); padding-left: 0; }
.destination-type-row { grid-template-columns: 114px minmax(0, 300px); margin-bottom: 16px; }
.destination-type-options { width: 300px; }
.destination-type-options :deep(.el-radio-button__inner) { min-height: 36px; padding: 8px 14px; font-size: 12px; }
.destination-form-flow > .prototype-form-row:not(.destination-type-row):not(.prototype-form-row--wide):not(.prototype-form-row--compact):not(.prototype-form-row--short) { grid-template-columns: 114px minmax(0, 360px); }
.flow-divider { width: min(100%, 860px); margin: 4px 0 22px; border-top: 1px solid var(--border); }
.compact-transport-grid { margin-top: 18px; }
.cargo-entry { padding: 16px; border: 1px solid var(--border); background: #fbfcfc; }
.compact-transport-grid { width: min(100%, 820px); grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 28px; margin: 20px 0 0 114px; }
.compact-transport-grid :deep(.el-form-item__label) { color: #4d5a6c; font-size: 13px; }
.shipping-form-flow { max-width: 900px; }
.prototype-order-form > .prototype-section:first-child { width: 100%; }
.shipping-form-flow .prototype-form-row { margin-bottom: 10px; }
.shipping-form-flow .prototype-form-row--tabs { margin-bottom: 12px; }
.compact-transport-grid { display: grid; width: 100%; grid-template-columns: 1fr; gap: 0; margin: 6px 0 0; }
.compact-transport-grid > :deep(.el-form-item) { display: grid; width: 100%; grid-template-columns: 114px minmax(0, 560px); align-items: start; margin-bottom: 10px; }
.compact-transport-grid > :deep(.el-form-item:nth-child(2)) { display: none; }
.compact-transport-grid > :deep(.el-form-item:nth-child(1)) { grid-template-columns: 114px minmax(0, 360px); }
.compact-transport-grid > :deep(.el-form-item:nth-child(6)) { grid-template-columns: 114px minmax(0, 360px); }
.compact-transport-grid > :deep(.el-form-item:nth-child(7)) { grid-template-columns: 114px minmax(0, 620px); margin-top: 2px; margin-bottom: 0; }
.compact-transport-grid > :deep(.el-form-item__label) { width: auto !important; min-height: 40px; justify-content: flex-end; padding: 0 18px 0 0; line-height: 40px; }
.compact-transport-grid > :deep(.el-form-item__content) { display: block; min-width: 0; line-height: normal; }
.compact-transport-grid > :deep(.el-form-item .el-input__wrapper), .compact-transport-grid > :deep(.el-form-item .el-select__wrapper) { min-height: 40px; }
.cargo-section { min-width: 0; padding-bottom: 4px; }
.cargo-table-heading { margin-bottom: 8px; }
.cargo-table-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 10px; }
.cargo-import-actions { display: flex; align-items: center; gap: 10px; }
.cargo-import-input { position: absolute; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
.cargo-import-actions :deep(.el-button) { min-height: 38px; margin-left: 0; padding: 8px 17px; }
.cargo-import-actions .cargo-upload-button { border-style: dashed; border-width: 2px; border-color: #b8c5ff; color: #637fec; }
.cargo-import-actions .cargo-upload-button:hover, .cargo-import-actions .cargo-upload-button:focus-visible { border-color: var(--primary); background: #f5f7ff; color: var(--primary); }
.cargo-import-actions .cargo-download-button { color: #4d5a6c; }
.cargo-import-actions .cargo-download-button :deep(svg) { color: #09a8d8; }
.cargo-table-actions { display: flex; align-items: center; gap: 8px; }
.cargo-table-actions :deep(.el-button) { margin-left: 0; }
.cargo-table-wrap { width: 100%; overflow-x: auto; border: 1px solid #e6eaf0; border-radius: 6px; background: #fff; scrollbar-color: #c8ced8 #f1f3f6; scrollbar-width: thin; }
.cargo-edit-table { width: max-content; min-width: 100%; border-collapse: collapse; table-layout: fixed; color: #4d5a6c; font-size: 12px; }
.cargo-edit-table th { height: 44px; padding: 7px 8px; border-bottom: 1px solid #e6eaf0; background: #f4f5fb; color: #637fec; font-size: 11px; font-weight: 650; text-align: left; white-space: nowrap; }
.cargo-edit-table th small { margin-left: 3px; color: #e8a20c; font-size: 9px; font-weight: 600; }
.cargo-edit-table td { width: 130px; height: 58px; padding: 7px 6px; border-bottom: 1px solid #edf0f5; background: #fff; vertical-align: top; }
.cargo-edit-table tbody tr:last-child td { border-bottom: 0; }
.cargo-edit-table :deep(.el-form-item) { margin-bottom: 0; }
.cargo-edit-table :deep(.el-form-item__content) { display: block; line-height: normal; }
.cargo-edit-table :deep(.el-form-item__error) { width: 100%; padding: 3px 2px 0; font-size: 9px; white-space: normal; }
.cargo-edit-table :deep(.el-input), .cargo-edit-table :deep(.el-select), .cargo-edit-table :deep(.el-input-number) { width: 100%; }
.cargo-edit-table :deep(.el-input__wrapper), .cargo-edit-table :deep(.el-select__wrapper) { min-height: 34px; padding: 1px 8px; box-shadow: none; }
.cargo-edit-table :deep(.el-input__wrapper:hover), .cargo-edit-table :deep(.el-select__wrapper:hover) { box-shadow: 0 0 0 1px #b9c8f5 inset; }
.cargo-edit-table :deep(.el-input__inner) { font-size: 11px; text-align: left; }
.cargo-check-cell, .cargo-index-cell { width: 38px !important; text-align: center !important; vertical-align: middle !important; }
.cargo-image-column { width: 62px !important; text-align: center !important; }
.cargo-number-column { width: 92px !important; }
.cargo-dimensions-column { width: 240px !important; }
.cargo-operation-column { width: 76px !important; text-align: center !important; }
.cargo-danger-cell { text-align: center; vertical-align: middle !important; }
.cargo-table-image { position: relative; display: flex; width: 34px; height: 34px; align-items: center; justify-content: center; margin: 0 auto; border: 1px dashed #d6dce5; border-radius: 4px; color: #788493; }
.cargo-table-image img { width: 100%; height: 100%; border-radius: 3px; object-fit: contain; }
.cargo-table-image :deep(.el-button) { width: 30px; height: 30px; padding: 0; }
.cargo-table-image .cargo-image-remove { position: absolute; top: -9px; right: -9px; width: 19px; height: 19px; border: 1px solid #ffd7d7; border-radius: 50%; background: #fff; }
.cargo-table-dimensions { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr); gap: 3px; align-items: center; }
.cargo-table-dimensions > span { color: #9aa4b2; }
.cargo-table-hint { display: flex; align-items: center; flex-wrap: wrap; gap: 9px; padding: 10px 2px 0; color: #8b96a6; font-size: 11px; }
.cargo-table-hint::before { color: #e5a512; content: '●'; font-size: 8px; }
.cargo-table-hint i { width: 3px; height: 3px; border-radius: 50%; background: #aab2bd; }
.cargo-entry { padding: 18px 20px; border: 1px solid #e2e7ee; background: #fff; box-shadow: 0 2px 8px rgba(15, 31, 58, .025); }
.cargo-entry + .cargo-entry { margin-top: 12px; }
.cargo-entry > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.cargo-field-section + .cargo-field-section { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); }
.cargo-field-section h4 { margin: 0 0 12px; color: var(--navy); font-size: 12px; }
.cargo-entry > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
.cargo-field-section + .cargo-field-section { margin-top: 18px; padding-top: 16px; border-top: 1px solid #edf0f5; }
.cargo-field-section h4 { margin: 0 0 14px; color: #516071; font-size: 12px; }
.cargo-form-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); column-gap: 12px; }
.cargo-form-grid--basic { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.product-image-field { grid-row: span 2; }
@@ -1620,10 +1781,14 @@ async function searchOrders(model: QueryModel) {
.address-book-control :deep(.el-select) { grid-column: 1 / -1; }
.segmented-options { width: 100%; align-items: stretch; flex-direction: column; }
.segmented-options :deep(.el-radio-button__inner) { border: 1px solid var(--border); border-radius: 6px !important; box-shadow: none !important; }
.warehouse-options { display: grid; width: 100%; grid-template-columns: 1fr; }
.warehouse-options :deep(.el-radio-button), .warehouse-options :deep(.el-radio-button__inner) { width: 100%; }
.price-estimate__result { grid-template-columns: 1fr; }
.price-estimate__result > small { grid-column: auto; }
.warehouse-options { width: 100%; gap: 12px 18px; }
.compact-transport-grid > :deep(.el-form-item), .compact-transport-grid > :deep(.el-form-item:nth-child(7)) { display: block; margin-bottom: 14px; }
.compact-transport-grid > :deep(.el-form-item:nth-child(2)) { display: none; }
.compact-transport-grid > :deep(.el-form-item__label) { min-height: auto; justify-content: flex-start; padding: 0 0 8px; line-height: 1.4; }
.cargo-table-toolbar { align-items: stretch; flex-direction: column; }
.cargo-import-actions, .cargo-table-actions { flex-wrap: wrap; }
.cost-estimate-cards { grid-template-columns: 1fr; }
.cost-estimate-card { min-height: 140px; }
:global(.first-leg-editor .el-drawer__header) { min-height: 56px; padding: 15px 20px; }
:global(.first-leg-editor .el-drawer__body) { padding: 16px 20px 28px; }
:global(.first-leg-editor .el-drawer__footer) { padding: 10px 20px; }
+2 -2
View File
@@ -29,13 +29,13 @@
.client-nav-node { min-width: 0; }
.client-nav-node.has-divider-before { margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, .09); }
.client-sidebar-group-trigger,
.client-sidebar-link { display: flex; width: 100%; height: 44px; min-width: 0; align-items: center; gap: 11px; margin: 2px 0; padding: 0 12px; border-radius: 10px; color: rgba(255, 255, 255, .72); font-size: 13px; font-weight: 500; transition: color .2s ease, background .2s ease, box-shadow .2s ease; }
.client-sidebar-link { display: flex; width: 100%; height: 46px; min-width: 0; align-items: center; gap: 12px; margin: 2px 0; padding: 0 14px; border-radius: 10px; color: rgba(255, 255, 255, .72); font-size: 14px; font-weight: 600; line-height: 1.35; transition: color .2s ease, background .2s ease, box-shadow .2s ease; }
.client-sidebar-group-trigger { border: 0; background: transparent; cursor: pointer; text-align: left; }
.client-sidebar-link { text-decoration: none; }
.client-sidebar-group-trigger:hover,
.client-sidebar-link:hover { background: rgba(255, 255, 255, .08); color: #fff; }
.client-nav-menu > .client-nav-node > .client-sidebar-link[aria-current="page"] { background: linear-gradient(90deg, #1565c0, #1e88e5); box-shadow: 0 6px 16px rgba(3, 15, 35, .28); color: #fff; font-weight: 650; }
.client-nav-submenu .client-sidebar-link { height: 40px; margin: 1px 0; padding: 0 12px; border-radius: 9px; color: rgba(255, 255, 255, .58); font-size: 12px; }
.client-nav-submenu .client-sidebar-link { height: 42px; margin: 1px 0; padding: 0 14px 0 22px; border-radius: 9px; color: rgba(255, 255, 255, .62); font-size: 13px; font-weight: 550; }
.client-sidebar .client-nav-submenu .client-sidebar-node-icon { width: 16px; height: 16px; flex: 0 0 16px; opacity: .86; }
.client-nav-submenu .client-sidebar-link:hover { background: rgba(255, 255, 255, .07); color: #fff; }
.client-nav-submenu .client-sidebar-link[aria-current="page"] { background: rgba(30, 136, 229, .25); box-shadow: inset 0 0 0 1px rgba(109, 185, 255, .14); color: #fff; font-weight: 650; }