Merge origin/release/test and preserve local frontend changes

This commit is contained in:
2026-10-20 11:48:38 +08:00
265 changed files with 14131 additions and 5723 deletions
@@ -44,12 +44,12 @@ describe('inbound fee-detail API', () => {
it('keeps an empty supplier category as null and rejects an unknown non-empty category', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [
{ id: 31, feeTypeName: '拖车费', feeCode: 'TRUCKING', supplierCategory: 'TRUCKING', warehousePayable: true },
{ id: 31, feeTypeName: '拖车费', feeCode: 'TRUCKING', defaultPrice: '60.25', supplierCategory: 'TRUCKING', warehousePayable: true },
{ id: 33, feeTypeName: '缺少类别', feeCode: 'MISSING', warehousePayable: true },
] } })
await expect(listInboundFeeItemOptions()).resolves.toMatchObject([
{ id: 31, feeCode: 'TRUCKING', supplierCategory: 'TRUCKING' },
{ id: 33, feeCode: 'MISSING', supplierCategory: null },
{ id: 31, feeCode: 'TRUCKING', defaultPrice: 60.25, supplierCategory: 'TRUCKING' },
{ id: 33, feeCode: 'MISSING', defaultPrice: null, supplierCategory: null },
])
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [
{ id: 32, feeTypeName: '未知费用', feeCode: 'UNMAPPED', supplierCategory: 'UNKNOWN', warehousePayable: true },
@@ -53,6 +53,7 @@ export interface SaveInboundFeeDetailRequest {
}
export interface InboundFeeItemOption {
defaultPrice: number | null
id: string | number
feeTypeName: string
feeCode: string
@@ -123,6 +124,7 @@ export async function listInboundFeeItemOptions() {
return (unwrapApiResponse(response).records || []).map((raw): InboundFeeItemOption => ({
id: raw.id as string | number,
feeTypeName: String(raw.feeTypeName || ''),
defaultPrice: raw.defaultPrice == null ? null : Number(raw.defaultPrice),
feeCode: String(raw.feeCode || ''),
supplierCategory: supplierCategory(raw.supplierCategory),
warehousePayable: Boolean(raw.warehousePayable),
@@ -42,10 +42,106 @@ function mountManager(permissions: string[]) {
}
describe('InboundFeeDetailManager', () => {
it.each([80, null])('refreshes the default price on reopening the editor: %s', async (defaultPrice) => {
const item = { id: '31', feeTypeName: '操作费', feeCode: 'OPERATION', defaultPrice: 60, supplierCategory: null, warehouseReceivable: true, warehousePayable: false }
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([item])
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
await flushPromises()
const state = (wrapper.vm.$ as unknown as { setupState: {
openEditor(mode: string, row?: InboundFeeDetail): Promise<void>
editorOpen: boolean
form: { feeItemId: string; unitPrice: number; amount: number }
} }).setupState
await state.openEditor('create'); await flushPromises()
state.editorOpen = false; await flushPromises()
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ ...item, defaultPrice }])
await state.openEditor('create'); await flushPromises()
const select = wrapper.findAllComponents({ name: 'ElFormItem' }).find((field) => field.props('prop') === 'feeItemId')!.findComponent({ name: 'ElSelect' })
select.vm.$emit('update:modelValue', '31'); select.vm.$emit('change', '31')
await flushPromises()
expect(state.form).toMatchObject({ unitPrice: defaultPrice ?? 0, amount: defaultPrice ?? 0 })
expect(listInboundFeeItemOptions).toHaveBeenCalledTimes(2)
state.editorOpen = false; await flushPromises()
await state.openEditor('edit', pending); await flushPromises()
expect(state.form).toMatchObject({ unitPrice: 10, amount: 100 })
})
it('clears cached choices and blocks saving while refresh is pending or has failed', async () => {
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
await flushPromises()
const state = (wrapper.vm.$ as unknown as { setupState: {
openEditor(mode: string, row?: InboundFeeDetail): Promise<void>
save(): Promise<void>
editorOpen: boolean
feeItems: unknown[]
feeItemsLoading: boolean
feeItemsError: { message: string } | null
actionError: { message: string } | null
form: { unitPrice: number; amount: number }
} }).setupState
await state.openEditor('edit', pending); await flushPromises()
expect(state.feeItems).toHaveLength(1)
state.editorOpen = false; await flushPromises()
let rejectRefresh!: (reason: Error) => void
vi.mocked(listInboundFeeItemOptions).mockImplementationOnce(() => new Promise((_, reject) => { rejectRefresh = reject }))
const opening = state.openEditor('edit', pending)
await flushPromises()
expect(state.feeItems).toEqual([])
expect(state.feeItemsLoading).toBe(true)
const select = wrapper.findAllComponents({ name: 'ElFormItem' }).find((field) => field.props('prop') === 'feeItemId')!.findComponent({ name: 'ElSelect' })
expect(select.props('disabled')).toBe(true)
await state.save()
expect(state.actionError?.message).toContain('正在加载')
rejectRefresh(new Error('加载失败'))
await opening; await flushPromises()
expect(state.feeItems).toEqual([])
expect(state.feeItemsError).not.toBeNull()
expect(state.form).toMatchObject({ unitPrice: 10, amount: 100 })
await state.save()
expect(updateInboundFeeDetail).not.toHaveBeenCalled()
state.feeItemsError = null
await state.save()
expect(updateInboundFeeDetail).not.toHaveBeenCalled()
expect(state.actionError?.message).toContain('不可用')
})
it('prefills only on user selection and saves a manually overridden price', async () => {
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([
{ id: '31', feeTypeName: '操作费', feeCode: 'OPERATION', defaultPrice: 60, supplierCategory: null, warehouseReceivable: true, warehousePayable: false },
{ id: '32', feeTypeName: '手工费', feeCode: 'MANUAL', defaultPrice: null, supplierCategory: null, warehouseReceivable: true, warehousePayable: false },
{ id: '33', feeTypeName: '零元费', feeCode: 'ZERO', defaultPrice: 0, supplierCategory: null, warehouseReceivable: true, warehousePayable: false },
])
const wrapper = mountManager(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view'])
await flushPromises()
const state = (wrapper.vm.$ as unknown as { setupState: {
openEditor(mode: string, row?: InboundFeeDetail): Promise<void>
save(): Promise<void>
form: { feeItemId: string; unitPrice: number; amount: number }
} }).setupState
await state.openEditor('edit', pending)
await flushPromises()
expect(state.form).toMatchObject({ unitPrice: 10, amount: 100 })
await state.openEditor('create')
await flushPromises()
const select = wrapper.findAllComponents({ name: 'ElFormItem' }).find((item) => item.props('prop') === 'feeItemId')!.findComponent({ name: 'ElSelect' })
for (const [id, price] of [['31', 60], ['32', 0], ['33', 0]] as const) {
select.vm.$emit('update:modelValue', id)
select.vm.$emit('change', id)
await flushPromises()
expect(state.form).toMatchObject({ unitPrice: price, amount: price })
}
select.vm.$emit('update:modelValue', '31')
select.vm.$emit('change', '31')
await flushPromises()
state.form.unitPrice = 65; state.form.amount = 75
await state.save(); await flushPromises()
expect(createInboundFeeDetail).toHaveBeenCalledWith('100', '88', expect.objectContaining({ feeItemId: '31', unitPrice: 65, amount: 75 }))
})
beforeEach(() => {
vi.clearAllMocks(); document.body.innerHTML = ''
vi.mocked(listInboundFeeDetails).mockResolvedValue([pending, approved])
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ id: '31', feeTypeName: '操作费', feeCode: 'OPERATION', supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true }])
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ id: '31', feeTypeName: '操作费', feeCode: 'OPERATION', defaultPrice: null, supplierCategory: 'TRUCKING', warehouseReceivable: true, warehousePayable: true }])
vi.mocked(listInboundFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '测试供应商', category: 'TRUCKING' }])
vi.mocked(createInboundFeeDetail).mockResolvedValue(pending)
vi.mocked(updateInboundFeeDetail).mockResolvedValue(pending)
@@ -227,7 +323,7 @@ describe('InboundFeeDetailManager', () => {
})
it('allows any loaded ACTIVE supplier when a payable fee item has no supplier category', async () => {
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ id: '31', feeTypeName: '未映射费用', feeCode: 'UNMAPPED', supplierCategory: null, warehouseReceivable: false, warehousePayable: true }])
vi.mocked(listInboundFeeItemOptions).mockResolvedValue([{ id: '31', feeTypeName: '未映射费用', feeCode: 'UNMAPPED', defaultPrice: null, supplierCategory: null, warehouseReceivable: false, warehousePayable: true }])
vi.mocked(listInboundFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '测试供应商', category: 'CUSTOMS' }])
const wrapper = mountManager([
'firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit',
@@ -145,6 +145,8 @@ async function load() {
async function loadFeeItems() {
if (!canViewFeeItems.value || feeItemsLoading.value) return
// 刷新期间移除旧候选,加载失败时不能继续使用过期默认价格。
feeItems.value = []
feeItemsLoading.value = true; feeItemsError.value = null
try { feeItems.value = await listInboundFeeItemOptions() }
catch (error) { feeItemsError.value = getApiErrorDetails(error, '费用项目加载失败') }
@@ -181,11 +183,19 @@ function assignForm(row?: InboundFeeDetail) {
voucherError.value = null
}
// 仅在用户主动选费时带价,编辑回显不覆盖已保存的单价和金额。
function applyFeeItemDefaultPrice() {
const price = selectedFeeItem.value?.defaultPrice ?? 0
form.unitPrice = price
form.amount = price
}
async function openEditor(mode: 'create' | 'edit' | 'adjust', row?: InboundFeeDetail) {
editorMode.value = mode; target.value = row || null; assignForm(row); actionError.value = null
feeItemsError.value = null; suppliersError.value = null
editorOpen.value = true
if (!feeItems.value.length) await loadFeeItems()
// 每次进入录入窗口都读取当前项目配置,不修改已有费用的回显金额。
await loadFeeItems()
if (form.direction === 'PAYABLE') await loadSuppliers()
}
@@ -225,7 +235,9 @@ async function finishAction(message: string) {
async function save() {
if (!await formRef.value?.validate()) return
if (!canViewFeeItems.value) { actionError.value = { message: '当前账号缺少费用项目查看权限,无法保存费用' }; return }
if (feeItemsLoading.value || feeItemsError.value) { actionError.value = { message: feeItemsLoading.value ? '费用项目正在加载,请稍候' : '费用项目加载失败,请重新打开费用窗口后重试' }; return }
const selectedItem = feeItems.value.find((item) => String(item.id) === String(form.feeItemId))
if (!selectedItem) { actionError.value = { message: '所选费用项目不可用,请重新选择' }; return }
if (form.direction === 'PAYABLE' && !canViewSuppliers.value) { actionError.value = { message: '当前账号缺少供应商查看权限,无法维护应付费用' }; return }
if (form.direction === 'PAYABLE' && suppliersError.value) { actionError.value = { message: '供应商列表加载失败,无法校验应付费用供应商' }; return }
if (form.direction === 'PAYABLE' && loadedSupplierCategory.value !== selectedItem?.supplierCategory) { actionError.value = { message: '供应商候选尚未按费用项目类别加载完成,无法保存' }; return }
@@ -332,27 +344,30 @@ onMounted(() => void load())
<el-alert v-if="feeItemsError" type="error" :closable="true" @close="feeItemsError = null"><template #title>{{ feeItemsError.message }}</template><ApiErrorMeta :details="feeItemsError" /></el-alert>
<el-alert v-if="suppliersError" type="error" :closable="true" @close="suppliersError = null"><template #title>{{ suppliersError.message }}</template><ApiErrorMeta :details="suppliersError" /></el-alert>
<el-alert v-if="actionError" type="error" :closable="true" @close="actionError = null"><template #title>{{ actionError.message }}</template><ApiErrorMeta :details="actionError" /></el-alert>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="fee-form density-form">
<el-form-item label="应收/应付" prop="direction"><el-segmented v-model="form.direction" :options="directionOptions" /></el-form-item>
<el-form-item label="费用项目" prop="feeItemId"><el-select v-model="form.feeItemId" filterable :loading="feeItemsLoading" :disabled="!canViewFeeItems"><el-option v-for="item in applicableItems" :key="item.id" :label="item.feeTypeName" :value="item.id" /></el-select></el-form-item>
<el-form-item v-if="form.direction === 'PAYABLE'" label="供应商" required><el-select v-model="form.supplierId" filterable :loading="suppliersLoading" :disabled="!canViewSuppliers || !selectedFeeItem || !!suppliersError" placeholder="请先选择费用项目"><el-option v-for="supplier in suppliers" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" /></el-select></el-form-item>
<el-form-item v-if="form.direction === 'PAYABLE'" label="结算方式" required><el-radio-group v-model="settlementModeModel"><el-radio-button value="ON_ACCOUNT">账期应付</el-radio-button><el-radio-button value="DIRECT_PAID">已付成本</el-radio-button></el-radio-group></el-form-item>
<template v-if="form.direction === 'PAYABLE' && form.settlementMode === 'DIRECT_PAID'">
<el-form-item label="已付时间" required><el-date-picker v-model="form.paidAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择实际付款时间" /></el-form-item>
<el-form-item label="付款方式" required><el-select v-model="form.paymentMethod" placeholder="请选择付款方式"><el-option label="线上支付" value="ONLINE" /><el-option label="银行转账" value="BANK_TRANSFER" /><el-option label="现金" value="CASH" /><el-option label="其他" value="OTHER" /></el-select></el-form-item>
<el-form-item label="付款流水号"><el-input v-model="form.paymentReferenceNo" maxlength="128" placeholder="流水号与付款凭证至少填写一项" /></el-form-item>
<el-form-item label="付款凭证" class="voucher-field">
<div class="voucher-control"><label class="voucher-picker" :class="{ 'is-disabled': voucherUploading }"><FileUp :size="15" /><span>{{ voucherUploading ? '上传并校验中' : '选择凭证' }}</span><input type="file" accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png" :disabled="voucherUploading" @change="selectVoucher" /></label><span class="voucher-hint">FINANCE_VOUCHER · 最多 20 个</span></div>
<el-alert v-if="voucherError" type="error" :closable="true" show-icon @close="voucherError = null"><template #title>{{ voucherError.message }}</template><ApiErrorMeta :details="voucherError" /></el-alert>
<div v-if="voucherFiles.length" class="voucher-list"><span v-for="file in voucherFiles" :key="file.fileId"><span :title="file.fileName">{{ file.fileName }}</span><button type="button" :aria-label="`移除${file.fileName}`" @click="removeVoucher(file.fileId)"><X :size="14" /></button></span></div>
</el-form-item>
</template>
<el-form-item label="业务发生日" prop="businessDate"><el-date-picker v-model="form.businessDate" type="date" value-format="YYYY-MM-DD" /></el-form-item>
<el-form-item label="货币" prop="currency"><el-input model-value="CNY" readonly aria-label="货币 CNY" /></el-form-item>
<el-form-item label="单价" prop="unitPrice"><el-input-number v-model="form.unitPrice" :min="0" :precision="4" placeholder="请输入单价" /></el-form-item>
<el-form-item label="金额" prop="amount"><el-input-number v-model="form.amount" :min="0" :precision="2" placeholder="请输入金额" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注(选填)" /></el-form-item>
</el-form>
<section class="density-dialog-section" data-tone="work">
<h3 class="density-dialog-section__title">费用资料</h3>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="fee-form density-form">
<el-form-item label="应收/应付" prop="direction"><el-segmented v-model="form.direction" :options="directionOptions" /></el-form-item>
<el-form-item label="费用项目" prop="feeItemId"><el-select v-model="form.feeItemId" filterable :loading="feeItemsLoading" :disabled="!canViewFeeItems || feeItemsLoading" @change="applyFeeItemDefaultPrice"><el-option v-for="item in applicableItems" :key="item.id" :label="item.feeTypeName" :value="item.id" /></el-select></el-form-item>
<el-form-item v-if="form.direction === 'PAYABLE'" label="供应商" required><el-select v-model="form.supplierId" filterable :loading="suppliersLoading" :disabled="!canViewSuppliers || !selectedFeeItem || !!suppliersError" placeholder="请先选择费用项目"><el-option v-for="supplier in suppliers" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" /></el-select></el-form-item>
<el-form-item v-if="form.direction === 'PAYABLE'" label="结算方式" required><el-radio-group v-model="settlementModeModel"><el-radio-button value="ON_ACCOUNT">账期应付</el-radio-button><el-radio-button value="DIRECT_PAID">已付成本</el-radio-button></el-radio-group></el-form-item>
<template v-if="form.direction === 'PAYABLE' && form.settlementMode === 'DIRECT_PAID'">
<el-form-item label="已付时间" required><el-date-picker v-model="form.paidAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择实际付款时间" /></el-form-item>
<el-form-item label="付款方式" required><el-select v-model="form.paymentMethod" placeholder="请选择付款方式"><el-option label="线上支付" value="ONLINE" /><el-option label="银行转账" value="BANK_TRANSFER" /><el-option label="现金" value="CASH" /><el-option label="其他" value="OTHER" /></el-select></el-form-item>
<el-form-item label="付款流水号"><el-input v-model="form.paymentReferenceNo" maxlength="128" placeholder="流水号与付款凭证至少填写一项" /></el-form-item>
<el-form-item label="付款凭证" class="voucher-field">
<div class="voucher-control"><label class="voucher-picker" :class="{ 'is-disabled': voucherUploading }"><FileUp :size="15" /><span>{{ voucherUploading ? '上传并校验中' : '选择凭证' }}</span><input type="file" accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png" :disabled="voucherUploading" @change="selectVoucher" /></label><span class="voucher-hint">FINANCE_VOUCHER · 最多 20 个</span></div>
<el-alert v-if="voucherError" type="error" :closable="true" show-icon @close="voucherError = null"><template #title>{{ voucherError.message }}</template><ApiErrorMeta :details="voucherError" /></el-alert>
<div v-if="voucherFiles.length" class="voucher-list"><span v-for="file in voucherFiles" :key="file.fileId"><span :title="file.fileName">{{ file.fileName }}</span><button type="button" :aria-label="`移除${file.fileName}`" @click="removeVoucher(file.fileId)"><X :size="14" /></button></span></div>
</el-form-item>
</template>
<el-form-item label="业务发生日" prop="businessDate"><el-date-picker v-model="form.businessDate" type="date" value-format="YYYY-MM-DD" /></el-form-item>
<el-form-item label="货币" prop="currency"><el-input model-value="CNY" readonly aria-label="货币 CNY" /></el-form-item>
<el-form-item label="单价" prop="unitPrice"><el-input-number v-model="form.unitPrice" :min="0" :precision="4" placeholder="请输入单价" /></el-form-item>
<el-form-item label="金额" prop="amount"><el-input-number v-model="form.amount" :min="0" :precision="2" placeholder="请输入金额" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注(选填)" /></el-form-item>
</el-form>
</section>
</AppDialog>
<AppDialog class="admin-density-dialog" v-model="rejectOpen" title="驳回费用" :confirm-loading="saving" confirm-text="确认驳回" confirm-type="warning" @confirm="reject">
<el-alert v-if="actionError" type="error" :closable="true" @close="actionError = null"><template #title>{{ actionError.message }}</template><ApiErrorMeta :details="actionError" /></el-alert>
@@ -367,7 +382,7 @@ onMounted(() => void load())
.fee-toolbar > div { display: flex; flex-wrap: wrap; gap: var(--admin-space-3); }
.fee-toolbar :deep(.el-button + .el-button) { margin-left: 0; }
.fee-refresh-button .is-spinning { animation: fee-refresh-spin 0.8s linear infinite; }
.fee-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--admin-space-6); margin-top: var(--admin-space-5); }
.fee-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--admin-space-6); }
.fee-form :deep(.el-select), .fee-form :deep(.el-date-editor), .fee-form :deep(.el-input-number) { width: 100%; }
.fee-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
.voucher-field { grid-column: 1 / -1; }.voucher-control { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-4); width: 100%; }.voucher-picker { display: inline-flex; align-items: center; gap: var(--admin-space-2); padding: var(--admin-space-2) var(--admin-space-5); border: 1px solid var(--cc-border); border-radius: var(--admin-radius); color: var(--el-color-primary); cursor: pointer; }.voucher-picker.is-disabled { cursor: not-allowed; opacity: .6; }.voucher-picker input { position: absolute; width: 1px; height: 1px; opacity: 0; }.voucher-hint { color: var(--cc-muted); font-size: var(--admin-font-caption); }.voucher-list { display: grid; width: 100%; gap: var(--admin-space-2); margin-top: var(--admin-space-3); }.voucher-list > span { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-3); padding: var(--admin-space-2) var(--admin-space-3); border-radius: var(--admin-radius); background: var(--el-fill-color-light); }.voucher-list > span > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.voucher-list button { display: inline-flex; padding: 2px; border: 0; background: transparent; color: var(--el-color-danger); cursor: pointer; }
@@ -60,6 +60,7 @@ describe('OrderDeclarationEditorDialog', () => {
createWrapper(true)
await flushPromises()
const parent = document.querySelector<HTMLElement>('.el-dialog.admin-density-dialog')!
expect([...parent.querySelectorAll('.density-dialog-section__title')].map(title => title.textContent)).toEqual(['原订单资料', '订单申报资料', '货品申报明细 不允许新增、删除或调整明细序号'])
expect(parent.style.getPropertyValue('--el-dialog-width')).toBe('min(1180px, calc(100vw - 36px))')
expect(parent.querySelector('.density-descriptions')).not.toBeNull()
parent.querySelector<HTMLButtonElement>('.cargo-table .density-action')!.click()
@@ -74,6 +75,14 @@ describe('OrderDeclarationEditorDialog', () => {
name.value = '编辑后的台灯'
name.dispatchEvent(new Event('input', { bubbles: true }))
await flushPromises()
cargo.querySelector<HTMLButtonElement>('button[aria-label="展开全屏"]')!.click()
await flushPromises()
expect(cargo.classList.contains('is-fullscreen')).toBe(true)
expect(name.value).toBe('编辑后的台灯')
cargo.querySelector<HTMLButtonElement>('button[aria-label="退出全屏"]')!.click()
await flushPromises()
expect(cargo.classList.contains('is-fullscreen')).toBe(false)
expect(name.value).toBe('编辑后的台灯')
const apply = [...cargo.querySelectorAll<HTMLButtonElement>('button')].find(button => button.textContent?.trim() === '应用本项')!
apply.click()
await flushPromises()
@@ -222,7 +222,8 @@ async function save() {
<template>
<AppDialog class="admin-density-dialog"
:model-value="modelValue"
title="编辑客户申报资料"
:title="`编辑客户申报资料${detail.summary.inboundCode ? ' · ' + detail.summary.inboundCode : ''}`"
fullscreenable
width="min(1180px, calc(100vw - 36px))"
confirm-text="保存申报资料"
:confirm-loading="saving"
@@ -231,7 +232,7 @@ async function save() {
@update:model-value="emit('update:modelValue', $event)"
@confirm="save"
>
<div class="declaration-editor">
<div class="declaration-editor density-dialog-sections">
<el-alert type="warning" :closable="false" title="仅修改客户原始申报资料;仓库/PDA 实测数据不会被覆盖。变更渠道不作废原箱码,订单会标记为“曾变更渠道”。国内收货仓和中转仓不可修改,如需变更请重新下单。" />
<LoadErrorState v-if="submitError" compact title="保存失败,当前填写内容已保留" :message="submitError.message" :show-retry="false">
<template #details><ApiErrorMeta :details="submitError" /></template>
@@ -240,71 +241,80 @@ async function save() {
<template #details><ApiErrorMeta :details="channelsError" /></template>
</LoadErrorState>
<el-descriptions :column="4" border class="readonly-facts density-descriptions">
<el-descriptions-item label="入仓码">{{ detail.summary.inboundCode || '—' }}</el-descriptions-item>
<el-descriptions-item label="国内收货仓">{{ detail.summary.receivingWarehouseName || '—' }}</el-descriptions-item>
<el-descriptions-item label="目的仓/中转仓">{{ detail.summary.destinationWarehouseName || '—' }}</el-descriptions-item>
<el-descriptions-item label="版本">{{ form.expectedVersion }}</el-descriptions-item>
</el-descriptions>
<section class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">原订单资料</h3>
<el-descriptions :column="4" border class="readonly-facts density-descriptions">
<el-descriptions-item label="入仓码">{{ detail.summary.inboundCode || '—' }}</el-descriptions-item>
<el-descriptions-item label="国内收货仓">{{ detail.summary.receivingWarehouseName || '—' }}</el-descriptions-item>
<el-descriptions-item label="目的仓/中转仓">{{ detail.summary.destinationWarehouseName || '—' }}</el-descriptions-item>
<el-descriptions-item label="版本">{{ form.expectedVersion }}</el-descriptions-item>
</el-descriptions>
</section>
<el-form label-position="top" class="order-fields density-form">
<el-row :gutter="14">
<el-col :span="7"><el-form-item label="渠道名称" required><el-select v-model="form.channelCode" aria-label="渠道名称" filterable clearable :loading="channelsLoading" placeholder="请选择正式渠道" @change="selectChannel"><el-option v-for="channel in channels" :key="channel.channelCode" :label="`${channel.channelName} · ${channel.channelCode}`" :value="channel.channelCode" /></el-select></el-form-item></el-col>
<el-col :span="4"><el-form-item label="渠道编码"><el-input :model-value="form.channelCode" readonly aria-label="渠道编码" /></el-form-item></el-col>
<el-col :span="4"><el-form-item label="预估体积(m³)"><el-input-number v-model="form.estimatedVolumeCbm" :min="0.0001" :precision="4" :controls="false" placeholder="请输入预估体积(选填)" /></el-form-item></el-col>
<el-col :span="5"><el-form-item label="预计到货时间"><el-date-picker v-model="form.expectedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm" clearable /></el-form-item></el-col>
<el-col :span="4"><el-form-item label="客户备注"><el-input v-model="form.customerRemark" maxlength="1000" show-word-limit placeholder="请输入客户备注(选填)" /></el-form-item></el-col>
</el-row>
</el-form>
<section class="density-dialog-section" data-tone="work">
<h3 class="density-dialog-section__title">订单申报资料</h3>
<el-form label-position="top" class="order-fields density-form">
<el-row :gutter="14">
<el-col :span="8"><el-form-item label="渠道名称" required><el-select v-model="form.channelCode" aria-label="渠道名称" filterable clearable :loading="channelsLoading" placeholder="请选择正式渠道" @change="selectChannel"><el-option v-for="channel in channels" :key="channel.channelCode" :label="`${channel.channelName} · ${channel.channelCode}`" :value="channel.channelCode" /></el-select></el-form-item></el-col>
<el-col :span="8"><el-form-item label="渠道编码"><el-input :model-value="form.channelCode" readonly aria-label="渠道编码" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="预估体积(m³)"><el-input-number v-model="form.estimatedVolumeCbm" :min="0.0001" :precision="4" :controls="false" placeholder="请输入预估体积(选填)" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="预计到货时间"><el-date-picker v-model="form.expectedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm" clearable /></el-form-item></el-col>
<el-col :span="16"><el-form-item label="客户备注"><el-input v-model="form.customerRemark" maxlength="1000" show-word-limit placeholder="请输入客户备注(选填)" /></el-form-item></el-col>
</el-row>
</el-form>
</section>
<h3>货品申报明细 <small>不允许新增、删除或调整明细序号</small></h3>
<el-table :data="form.cargoes" border stripe size="small" class="cargo-table">
<el-table-column label="操作" width="82" fixed="left"><template #default="{ $index }"><el-button class="density-action" data-tone="neutral" @click="editCargo($index)">编辑</el-button></template></el-table-column>
<el-table-column prop="itemNo" label="项" width="58" align="center" />
<el-table-column label="货品" min-width="190"><template #default="{ row }"><strong class="cell-primary" :title="row.cargoName">{{ row.cargoName }}</strong><small :title="row.cargoNameEn || row.skuNo">{{ row.cargoNameEn || row.skuNo || '—' }}</small></template></el-table-column>
<el-table-column label="箱数 / 数量" width="118" align="right"><template #default="{ row }">{{ row.packageCount }} 箱 / {{ row.totalQuantity }}</template></el-table-column>
<el-table-column label="申报尺寸(cm)" width="148"><template #default="{ row }">{{ row.lengthCm }} × {{ row.widthCm }} × {{ row.heightCm }}</template></el-table-column>
<el-table-column label="毛重 / 净重(kg)" width="145"><template #default="{ row }">{{ row.grossWeightKg }} / {{ row.netWeightKg ?? '—' }}</template></el-table-column>
<el-table-column prop="volumeCbm" label="体积(m³)" width="100" align="right" />
<el-table-column label="申报金额" width="125" align="right"><template #default="{ row }">{{ row.currency || '—' }} {{ row.declaredValue ?? '—' }}</template></el-table-column>
<el-table-column label="材质 / 用途" min-width="170"><template #default="{ row }"><span :title="[row.material, row.usage].filter(Boolean).join(' / ')">{{ [row.material, row.usage].filter(Boolean).join(' / ') || '—' }}</span></template></el-table-column>
</el-table>
<section class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">货品申报明细 <small>不允许新增、删除或调整明细序号</small></h3>
<el-table :data="form.cargoes" border stripe size="small" class="cargo-table">
<el-table-column label="操作" width="82" fixed="left"><template #default="{ $index }"><el-button class="density-action" data-tone="neutral" @click="editCargo($index)">编辑</el-button></template></el-table-column>
<el-table-column prop="itemNo" label="项" width="58" align="center" />
<el-table-column label="货品" min-width="190"><template #default="{ row }"><strong class="cell-primary" :title="row.cargoName">{{ row.cargoName }}</strong><small :title="row.cargoNameEn || row.skuNo">{{ row.cargoNameEn || row.skuNo || '—' }}</small></template></el-table-column>
<el-table-column label="箱数 / 数量" width="118" align="right"><template #default="{ row }">{{ row.packageCount }} 箱 / {{ row.totalQuantity }}</template></el-table-column>
<el-table-column label="申报尺寸(cm)" width="148"><template #default="{ row }">{{ row.lengthCm }} × {{ row.widthCm }} × {{ row.heightCm }}</template></el-table-column>
<el-table-column label="毛重 / 净重(kg)" width="145"><template #default="{ row }">{{ row.grossWeightKg }} / {{ row.netWeightKg ?? '—' }}</template></el-table-column>
<el-table-column prop="volumeCbm" label="体积(m³)" width="100" align="right" />
<el-table-column label="申报金额" width="125" align="right"><template #default="{ row }">{{ row.currency || '—' }} {{ row.declaredValue ?? '—' }}</template></el-table-column>
<el-table-column label="材质 / 用途" min-width="170"><template #default="{ row }"><span :title="[row.material, row.usage].filter(Boolean).join(' / ')">{{ [row.material, row.usage].filter(Boolean).join(' / ') || '—' }}</span></template></el-table-column>
</el-table>
</section>
</div>
</AppDialog>
<AppDialog class="admin-density-dialog" v-model="cargoEditorOpen" title="编辑货品申报" width="min(980px, calc(100vw - 36px))" confirm-text="应用本项" :close-on-click-modal="false" @confirm="applyCargo">
<div v-if="cargoDraft" class="cargo-editor-grid">
<label><span>中文品名 *</span><el-input v-model="cargoDraft.cargoName" maxlength="128" placeholder="请输入中文品名" /></label>
<label><span>英文品名</span><el-input v-model="cargoDraft.cargoNameEn" maxlength="128" placeholder="请输入英文品名(选填)" /></label>
<label><span>SKU NO.</span><el-input v-model="cargoDraft.skuNo" maxlength="64" placeholder="请输入 SKU(选填)" /></label>
<label><span>型号</span><el-input v-model="cargoDraft.model" maxlength="64" placeholder="请输入型号(选填)" /></label>
<label><span>箱数 *</span><el-input-number v-model="cargoDraft.packageCount" :min="1" :precision="0" controls-position="right" placeholder="请输入箱数" /></label>
<label><span>总数量 *</span><el-input-number v-model="cargoDraft.totalQuantity" :min="1" :precision="0" controls-position="right" placeholder="请输入总数量" /></label>
<label><span>每箱数量</span><el-input-number v-model="cargoDraft.unitsPerPackage" :min="1" :precision="0" controls-position="right" placeholder="请输入每箱数量(选填)" /></label>
<label><span>币种</span><el-input :model-value="cargoDraft.currency || '—'" readonly aria-label="币种" /></label>
<label><span>长(cm) *</span><el-input-number v-model="cargoDraft.lengthCm" :min="0.01" :precision="2" controls-position="right" placeholder="请输入长度" /></label>
<label><span>宽(cm) *</span><el-input-number v-model="cargoDraft.widthCm" :min="0.01" :precision="2" controls-position="right" placeholder="请输入宽度" /></label>
<label><span>高(cm) *</span><el-input-number v-model="cargoDraft.heightCm" :min="0.01" :precision="2" controls-position="right" placeholder="请输入高度" /></label>
<label><span>体积(m³) *</span><el-input-number v-model="cargoDraft.volumeCbm" :min="0.0001" :precision="4" controls-position="right" placeholder="请输入体积" /></label>
<label><span>毛重(kg) *</span><el-input-number v-model="cargoDraft.grossWeightKg" :min="0.001" :precision="3" controls-position="right" placeholder="请输入毛重" /></label>
<label><span>净重(kg)</span><el-input-number v-model="cargoDraft.netWeightKg" :min="0.001" :precision="3" controls-position="right" placeholder="请输入净重(选填)" /></label>
<label><span>单价</span><el-input-number v-model="cargoDraft.unitDeclaredValue" :min="0" :precision="2" controls-position="right" placeholder="请输入申报单价(选填)" /></label>
<label><span>申报总价</span><el-input-number v-model="cargoDraft.declaredValue" :min="0" :precision="2" controls-position="right" placeholder="请输入申报总价(选填)" /></label>
<label><span>报关 HS CODE</span><el-input v-model="cargoDraft.hsCode" maxlength="20" placeholder="请输入报关 HS CODE(选填)" /></label>
<label><span>清关 HS CODE</span><el-input v-model="cargoDraft.customsHsCode" maxlength="20" placeholder="请输入清关 HS CODE(选填)" /></label>
<label><span>材质</span><el-input v-model="cargoDraft.material" maxlength="128" placeholder="请输入材质(选填)" /></label>
<label><span>用途</span><el-input v-model="cargoDraft.usage" maxlength="128" placeholder="请输入用途(选填)" /></label>
<label><span>品牌</span><el-input v-model="cargoDraft.brand" maxlength="64" placeholder="请输入品牌(选填)" /></label>
<label class="wide"><span>货品备注</span><el-input v-model="cargoDraft.cargoRemark" maxlength="500" placeholder="请输入货品备注(选填)" /></label>
</div>
<AppDialog class="admin-density-dialog" v-model="cargoEditorOpen" :title="`编辑货品申报${detail.summary.inboundCode ? ' · ' + detail.summary.inboundCode : ''}`" fullscreenable width="min(980px, calc(100vw - 36px))" confirm-text="应用本项" :close-on-click-modal="false" @confirm="applyCargo">
<section v-if="cargoDraft" class="density-dialog-section" data-tone="work">
<h3 class="density-dialog-section__title">货品申报资料</h3>
<div class="cargo-editor-grid">
<label><span>中文品名 *</span><el-input v-model="cargoDraft.cargoName" maxlength="128" placeholder="请输入中文品名" /></label>
<label><span>英文品名</span><el-input v-model="cargoDraft.cargoNameEn" maxlength="128" placeholder="请输入英文品名(选填)" /></label>
<label><span>SKU NO.</span><el-input v-model="cargoDraft.skuNo" maxlength="64" placeholder="请输入 SKU(选填)" /></label>
<label><span>型号</span><el-input v-model="cargoDraft.model" maxlength="64" placeholder="请输入型号(选填)" /></label>
<label><span>箱数 *</span><el-input-number v-model="cargoDraft.packageCount" :min="1" :precision="0" controls-position="right" placeholder="请输入箱数" /></label>
<label><span>总数量 *</span><el-input-number v-model="cargoDraft.totalQuantity" :min="1" :precision="0" controls-position="right" placeholder="请输入总数量" /></label>
<label><span>每箱数量</span><el-input-number v-model="cargoDraft.unitsPerPackage" :min="1" :precision="0" controls-position="right" placeholder="请输入每箱数量(选填)" /></label>
<label><span>币种</span><el-input :model-value="cargoDraft.currency || '—'" readonly aria-label="币种" /></label>
<label><span>长(cm) *</span><el-input-number v-model="cargoDraft.lengthCm" :min="0.01" :precision="2" controls-position="right" placeholder="请输入长度" /></label>
<label><span>宽(cm) *</span><el-input-number v-model="cargoDraft.widthCm" :min="0.01" :precision="2" controls-position="right" placeholder="请输入宽度" /></label>
<label><span>高(cm) *</span><el-input-number v-model="cargoDraft.heightCm" :min="0.01" :precision="2" controls-position="right" placeholder="请输入高度" /></label>
<label><span>体积(m³) *</span><el-input-number v-model="cargoDraft.volumeCbm" :min="0.0001" :precision="4" controls-position="right" placeholder="请输入体积" /></label>
<label><span>毛重(kg) *</span><el-input-number v-model="cargoDraft.grossWeightKg" :min="0.001" :precision="3" controls-position="right" placeholder="请输入毛重" /></label>
<label><span>净重(kg)</span><el-input-number v-model="cargoDraft.netWeightKg" :min="0.001" :precision="3" controls-position="right" placeholder="请输入净重(选填)" /></label>
<label><span>单价</span><el-input-number v-model="cargoDraft.unitDeclaredValue" :min="0" :precision="2" controls-position="right" placeholder="请输入申报单价(选填)" /></label>
<label><span>申报总价</span><el-input-number v-model="cargoDraft.declaredValue" :min="0" :precision="2" controls-position="right" placeholder="请输入申报总价(选填)" /></label>
<label><span>报关 HS CODE</span><el-input v-model="cargoDraft.hsCode" maxlength="20" placeholder="请输入报关 HS CODE(选填)" /></label>
<label><span>清关 HS CODE</span><el-input v-model="cargoDraft.customsHsCode" maxlength="20" placeholder="请输入清关 HS CODE(选填)" /></label>
<label><span>材质</span><el-input v-model="cargoDraft.material" maxlength="128" placeholder="请输入材质(选填)" /></label>
<label><span>用途</span><el-input v-model="cargoDraft.usage" maxlength="128" placeholder="请输入用途(选填)" /></label>
<label><span>品牌</span><el-input v-model="cargoDraft.brand" maxlength="64" placeholder="请输入品牌(选填)" /></label>
<label class="wide"><span>货品备注</span><el-input v-model="cargoDraft.cargoRemark" maxlength="500" placeholder="请输入货品备注(选填)" /></label>
</div>
</section>
</AppDialog>
</template>
<style scoped>
.declaration-editor { display: grid; gap: var(--admin-space-5); }
.readonly-facts { margin-top: 2px; }
.order-fields :deep(.el-date-editor), .order-fields :deep(.el-input) { width: 100%; }
h3 { margin: 0; color: #344054; font-size: var(--admin-font-section); } h3 small { margin-left: var(--admin-space-3); color: #98a2b3; font-weight: 400; }
.cargo-table { width: 100%; }
.cargo-table small, .cargo-table .cell-primary { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cargo-table small { margin-top: 3px; color: #98a2b3; }
@@ -371,7 +371,7 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain("row.nextAction === 'APPROVE_ORDER'")
expect(source).toContain("can('platform:order:approve')")
expect(source).toContain('await approvePlatformUnifiedOrder(tenantId, orderId)')
expect(source).toContain('title="审核订单"')
expect(source).toContain("orderDialogTitle('审核订单', approvalOrder?.inboundCode)")
expect(source).toContain('confirm-text="审核通过"')
expect(source).not.toContain('assignPlatformUnifiedOrderCarrier')
expect(source).not.toContain('listPlatformPartners')
@@ -424,7 +424,7 @@ describe('UnifiedOrdersView contract', () => {
})
it('keeps customer remarks read-only and edits the three collaboration remarks together', () => {
expect(source).toContain('title="编辑订单备注"')
expect(source).toContain("orderDialogTitle('编辑订单备注', noteOrder?.inboundCode)")
expect(source).toContain('label="客户备注(只读)"')
expect(source).toContain(':model-value="noteOrder?.customerRemark')
expect(source).toContain('v-model="noteForm.internalRemark"')
@@ -2382,34 +2382,59 @@ onBeforeUnmount(() => {
releaseMeasurementPreview()
releaseCargoImagePreviews()
})
function orderDialogTitle(action: string, inboundCode?: string | null) {
return inboundCode ? `${action} · ${inboundCode}` : action
}
</script>
<template>
<AdminPage>
<section class="business-page unified-orders-page admin-density">
<AppDialog class="admin-density-dialog" v-model="feeConfirmOpen" title="编辑计费数据" confirm-text="确认并发送" :confirm-loading="confirmFeeSending || feeConfirmReferenceLoading" width="min(920px, 94vw)" :teleported="false" @confirm="executeConfirmAndSendFee">
<AppDialog class="admin-density-dialog" v-model="feeConfirmOpen" :title="orderDialogTitle('编辑计费数据', detail?.summary.inboundCode)" confirm-text="确认并发送" :confirm-loading="confirmFeeSending || feeConfirmReferenceLoading" width="min(920px, 94vw)" fullscreenable :teleported="false" @confirm="executeConfirmAndSendFee">
<el-alert class="fee-confirm-price-alert" title="价格表单价仅作为参考;本次对客单价可人工修改,客户将一次确认计费量、单价和费用合计。" type="info" :closable="false" show-icon />
<el-form label-position="top" class="fee-confirm-form">
<el-form-item label="入仓码"><el-input :model-value="detail?.summary.inboundCode || '—'" disabled /></el-form-item>
<el-form-item label="渠道"><el-input :model-value="detail?.summary.channelName || detail?.summary.channelCode || '—'" disabled /></el-form-item>
<el-form-item label="计费毛重"><div class="fee-confirm-number-wrap"><el-input-number v-model="feeConfirmWeight" :min="0" :precision="3" :controls="false" class="fee-confirm-number" placeholder="请输入计费毛重" /><span class="fee-confirm-unit">kg</span></div></el-form-item>
<el-form-item label="计费体积"><div class="fee-confirm-number-wrap"><el-input-number v-model="feeConfirmVolume" :min="0" :precision="6" :controls="false" class="fee-confirm-number" placeholder="请输入计费体积" /><span class="fee-confirm-unit">m³</span></div></el-form-item>
<el-form-item label="价格表参考单价"><div class="fee-confirm-number-wrap"><el-input-number :model-value="feeConfirmPriceMissing || feeConfirmReference?.unitPrice == null ? undefined : Number(feeConfirmReference.unitPrice)" :precision="4" :controls="false" disabled class="fee-confirm-number" :placeholder="feeConfirmPriceMissing ? '未命中' : '正在读取参考单价'" /><span class="fee-confirm-unit">{{ feeConfirmPriceUnit }}</span></div></el-form-item>
<el-form-item label="本次对客单价"><div class="fee-confirm-number-wrap"><el-input-number v-model="feeConfirmUnitPrice" :min="0" :precision="4" :controls="false" :disabled="feeConfirmReferenceLoading" class="fee-confirm-number" placeholder="请输入对客单价" /><span class="fee-confirm-unit">{{ feeConfirmPriceUnit }}</span></div></el-form-item>
<el-form-item v-if="feeConfirmPriceChanged" :label="feeConfirmPriceMissing ? '人工定价原因' : '单价调整原因'" class="fee-confirm-price-reason"><el-input v-model="feeConfirmPriceReason" type="textarea" :rows="2" maxlength="500" show-word-limit placeholder="请说明本次人工定价的原因" /></el-form-item>
</el-form>
<dl v-if="feeConfirmReference" class="fee-confirm-price-summary">
<div><dt>参考计费量</dt><dd>{{ formatNumber(Number(feeConfirmReference.chargeableQuantity), feeConfirmReference.roundingScale) }} {{ feeConfirmReference.chargeUnit }}</dd></div>
<div><dt>按当前单价预计合计</dt><dd>{{ feeConfirmEstimatedTotal == null ? '—' : feeConfirmMoney(feeConfirmEstimatedTotal, feeConfirmReference.currency) }}</dd></div>
</dl>
<h4 class="fee-confirm-reference-title">参考数据</h4>
<el-table :data="feeConfirmRows" border size="small">
<el-table-column prop="source" label="数据来源" />
<el-table-column prop="count" label="件数" />
<el-table-column label="总毛重(T.K.GW)" align="right"><template #default="{ row }">{{ row.weight == null ? '—' : formatNumber(row.weight, 2) }}</template></el-table-column>
<el-table-column label="总体积(m³)"><template #default="{ row }">{{ row.volume == null ? '—' : formatNumber(row.volume, 4) }}</template></el-table-column>
<el-table-column label="总材积重"><template #default="{ row }">{{ row.volume == null ? '—' : formatNumber(Number(row.volume) * 167, 2) }}</template></el-table-column>
</el-table>
<section class="density-dialog-section" data-tone="work">
<h3 class="density-dialog-section__title">本次计费数据 <small>毛重、体积和对客单价可修改</small></h3>
<table class="density-table density-table--editable fee-confirm-table" aria-label="本次计费数据">
<colgroup><col class="fee-confirm-label-column" /><col /><col class="fee-confirm-label-column" /><col /></colgroup>
<tbody>
<tr>
<th scope="row">入仓码</th><td>{{ detail?.summary.inboundCode || '—' }}</td>
<th scope="row">渠道</th><td>{{ detail?.summary.channelName || detail?.summary.channelCode || '—' }}</td>
</tr>
<tr>
<th scope="row"><label for="fee-confirm-weight">计费毛重</label></th>
<td><div class="density-table-editor"><el-input-number id="fee-confirm-weight" v-model="feeConfirmWeight" :min="0" :precision="3" :controls="false" placeholder="请输入计费毛重" /><span>kg</span></div></td>
<th scope="row"><label for="fee-confirm-volume">计费体积</label></th>
<td><div class="density-table-editor"><el-input-number id="fee-confirm-volume" v-model="feeConfirmVolume" :min="0" :precision="6" :controls="false" placeholder="请输入计费体积" /><span>m³</span></div></td>
</tr>
<tr>
<th scope="row">价格表参考单价</th>
<td>{{ feeConfirmPriceMissing ? '未命中' : feeConfirmReference?.unitPrice == null ? '正在读取参考单价' : `${formatNumber(Number(feeConfirmReference.unitPrice), 4)} ${feeConfirmPriceUnit}` }}</td>
<th scope="row"><label for="fee-confirm-unit-price">本次对客单价</label></th>
<td><div class="density-table-editor"><el-input-number id="fee-confirm-unit-price" v-model="feeConfirmUnitPrice" :min="0" :precision="4" :controls="false" :disabled="feeConfirmReferenceLoading" placeholder="请输入对客单价" /><span>{{ feeConfirmPriceUnit }}</span></div></td>
</tr>
<tr v-if="feeConfirmReference">
<th scope="row">参考计费量</th><td>{{ formatNumber(Number(feeConfirmReference.chargeableQuantity), feeConfirmReference.roundingScale) }} {{ feeConfirmReference.chargeUnit }}</td>
<th scope="row">按当前单价预计合计</th><td><strong>{{ feeConfirmEstimatedTotal == null ? '—' : feeConfirmMoney(feeConfirmEstimatedTotal, feeConfirmReference.currency) }}</strong></td>
</tr>
<tr v-if="feeConfirmPriceChanged">
<th scope="row"><label for="fee-confirm-price-reason">{{ feeConfirmPriceMissing ? '人工定价原因' : '单价调整原因' }}</label></th>
<td colspan="3"><el-input id="fee-confirm-price-reason" v-model="feeConfirmPriceReason" type="textarea" :rows="2" maxlength="500" show-word-limit placeholder="请说明本次人工定价的原因" /></td>
</tr>
</tbody>
</table>
</section>
<section class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">参考数据</h3>
<el-table :data="feeConfirmRows" border size="small">
<el-table-column prop="source" label="数据来源" />
<el-table-column prop="count" label="件数" />
<el-table-column label="总毛重(T.K.GW)" align="right"><template #default="{ row }">{{ row.weight == null ? '—' : formatNumber(row.weight, 2) }}</template></el-table-column>
<el-table-column label="总体积(m³)"><template #default="{ row }">{{ row.volume == null ? '—' : formatNumber(row.volume, 4) }}</template></el-table-column>
<el-table-column label="总材积重"><template #default="{ row }">{{ row.volume == null ? '—' : formatNumber(Number(row.volume) * 167, 2) }}</template></el-table-column>
</el-table>
</section>
</AppDialog>
<template v-if="isDetailRoute">
<DetailPageLayout ariaLabel="统一订单详情">
@@ -2692,7 +2717,7 @@ onBeforeUnmount(() => {
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(actualMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(actualMeasurementRows).weightVolume, 6) }}</span></div>
<section class="weight-evidence" aria-label="称重图片与服务端确认重量">
<div class="weight-evidence__heading">
<div><h4>称重图片与确认重量</h4><p>称重图片独立展示;重量值直接采用服务端确认结果。</p></div>
<div><h4>称重图片与确认重量</h4></div>
<dl><dt>实际总毛重</dt><dd>{{ formatNumber(measurementDetail?.actualGrossWeightKg) }} kg</dd></dl>
</div>
<div v-if="measurementDetail?.weightResults.length" class="weight-evidence-grid">
@@ -2907,17 +2932,23 @@ onBeforeUnmount(() => {
</ListPageLayout>
<AppDialog class="admin-density-dialog" v-model="legacyImportOpen" title="导入旧系统订单" width="min(760px, calc(100vw - 28px))" confirm-text="开始导入" :confirm-loading="legacyImportSaving" :confirm-disabled="!legacyImportFile || legacyImportSaving || !!legacyImportResult" destroy-on-close @confirm="submitLegacyImport">
<div class="legacy-import-content">
<dl class="legacy-import-facts">
<div><dt>固定归属客户</dt><dd>IT测试公司</dd></div>
<div><dt>导入结果状态</dt><dd>待审核</dd></div>
</dl>
<el-upload :auto-upload="false" :limit="1" accept=".xlsx" :on-change="selectLegacyImportFile">
<el-button><FileUp :size="16" />选择 Excel 文件</el-button>
<template #tip><div class="el-upload__tip">仅支持旧系统“订单统计导出” .xlsx 文件,最多 200 条、5MB;入仓码保持不变。</div></template>
</el-upload>
<div class="legacy-import-content density-dialog-sections">
<section class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">导入规则</h3>
<dl class="legacy-import-facts">
<div><dt>固定归属客户</dt><dd>IT测试公司</dd></div>
<div><dt>导入结果状态</dt><dd>待审核</dd></div>
</dl>
</section>
<section class="density-dialog-section" data-tone="work">
<h3 class="density-dialog-section__title">导入文件</h3>
<el-upload :auto-upload="false" :limit="1" accept=".xlsx" :on-change="selectLegacyImportFile">
<el-button><FileUp :size="16" />选择 Excel 文件</el-button>
<template #tip><div class="el-upload__tip">仅支持旧系统“订单统计导出” .xlsx 文件,最多 200 条、5MB;入仓码保持不变。</div></template>
</el-upload>
</section>
<LoadErrorState v-if="legacyImportError" compact title="旧系统订单导入失败" :message="legacyImportError.message" :show-retry="false"><template #details><ApiErrorMeta :details="legacyImportError" /></template></LoadErrorState>
<div v-if="legacyImportResult" class="legacy-import-result">
<div v-if="legacyImportResult" class="legacy-import-result density-dialog-section" data-tone="summary">
<p>共 {{ legacyImportResult.total }} 条,成功 {{ legacyImportResult.succeeded }} 条,失败 {{ legacyImportResult.failed }} 条。</p>
<el-table v-if="legacyImportResult.failed" :data="legacyImportResult.items.filter((item) => !item.success)" border stripe max-height="280">
<el-table-column prop="excelRow" label="Excel 行" width="90" />
@@ -2928,25 +2959,34 @@ onBeforeUnmount(() => {
</div>
</AppDialog>
<SmartLoadingDialog v-model="smartLoadingOpen" />
<AppDialog class="admin-density-dialog" v-model="batchApprovalOpen" title="批量审核订单" confirm-text="确认批量审核" confirm-type="success" width="min(1100px, calc(100vw - 28px))" :loading="batchApprovalLoading" :confirm-loading="batchApprovalSaving" :confirm-disabled="batchApprovalLoading || !batchApprovalSelectedRows.length" destroy-on-close @confirm="batchApproveOrders">
<AppDialog class="admin-density-dialog" v-model="batchApprovalOpen" title="批量审核订单" confirm-text="确认批量审核" confirm-type="success" width="min(1100px, calc(100vw - 28px))" fullscreenable :loading="batchApprovalLoading" :confirm-loading="batchApprovalSaving" :confirm-disabled="batchApprovalLoading || !batchApprovalSelectedRows.length" destroy-on-close @confirm="batchApproveOrders">
<el-alert title="请逐票核对入仓码、客户、中文品名、渠道和目的仓;审核通过后订单进入待测量。" type="info" :closable="false" show-icon />
<LoadErrorState v-if="batchApprovalError" compact title="批量审核预览加载失败" :message="batchApprovalError.message" :loading="batchApprovalLoading" @retry="openBatchApproval"><template #details><ApiErrorMeta :details="batchApprovalError" /></template></LoadErrorState>
<template v-else>
<div class="batch-approval-summary"><span>预览 {{ batchApprovalPreviewRows.length }} 票</span><strong>已勾选 {{ batchApprovalSelectedRows.length }} 票</strong></div>
<el-table ref="batchApprovalTableRef" class="batch-approval-table" :data="batchApprovalPreviewRows" row-key="orderId" border stripe max-height="520" empty-text="暂无可审核订单" @selection-change="handleBatchApprovalSelection">
<el-table-column type="selection" width="48" fixed="left" :selectable="(row: UnifiedOrderBatchApprovalPreviewItem) => row.canApprove" />
<el-table-column label="入仓码" min-width="160" show-overflow-tooltip><template #default="{ row }"><strong>{{ row.inboundCode || '—' }}</strong></template></el-table-column>
<el-table-column label="客户" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.customerName || '—' }}</template></el-table-column>
<el-table-column label="中文品名" min-width="220" show-overflow-tooltip><template #default="{ row }">{{ row.chineseGoodsNames.length ? row.chineseGoodsNames.join('、') : '—' }}</template></el-table-column>
<el-table-column label="渠道" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ row.channelName || row.channelCode || '—' }}</template></el-table-column>
<el-table-column label="目的仓" min-width="180" show-overflow-tooltip><template #default="{ row }">{{ row.destinationWarehouseName || row.destinationWarehouseCode || '—' }}</template></el-table-column>
<el-table-column label="校验结果" min-width="180" show-overflow-tooltip><template #default="{ row }"><el-tag :type="row.canApprove ? 'success' : 'danger'" effect="light">{{ row.canApprove ? '可审核' : row.blockReason || '不可审核' }}</el-tag></template></el-table-column>
</el-table>
<section class="density-dialog-section" data-tone="summary">
<h3 class="density-dialog-section__title">本次审核范围</h3>
<div class="batch-approval-summary"><span>预览 {{ batchApprovalPreviewRows.length }} 票</span><strong>已勾选 {{ batchApprovalSelectedRows.length }} 票</strong></div>
</section>
<section class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">待审核订单</h3>
<el-table ref="batchApprovalTableRef" class="batch-approval-table" :data="batchApprovalPreviewRows" row-key="orderId" border stripe max-height="520" empty-text="暂无可审核订单" @selection-change="handleBatchApprovalSelection">
<el-table-column type="selection" width="48" fixed="left" :selectable="(row: UnifiedOrderBatchApprovalPreviewItem) => row.canApprove" />
<el-table-column label="入仓码" min-width="160" show-overflow-tooltip><template #default="{ row }"><strong>{{ row.inboundCode || '—' }}</strong></template></el-table-column>
<el-table-column label="客户" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.customerName || '—' }}</template></el-table-column>
<el-table-column label="中文品名" min-width="220" show-overflow-tooltip><template #default="{ row }">{{ row.chineseGoodsNames.length ? row.chineseGoodsNames.join('、') : '—' }}</template></el-table-column>
<el-table-column label="渠道" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ row.channelName || row.channelCode || '—' }}</template></el-table-column>
<el-table-column label="目的仓" min-width="180" show-overflow-tooltip><template #default="{ row }">{{ row.destinationWarehouseName || row.destinationWarehouseCode || '—' }}</template></el-table-column>
<el-table-column label="校验结果" min-width="180" show-overflow-tooltip><template #default="{ row }"><el-tag :type="row.canApprove ? 'success' : 'danger'" effect="light">{{ row.canApprove ? '可审核' : row.blockReason || '不可审核' }}</el-tag></template></el-table-column>
</el-table>
</section>
</template>
</AppDialog>
<AppDialog class="admin-density-dialog" v-model="coLoadCreateOpen" title="合柜报关" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="!canCreateCoLoadGroup" @confirm="createCoLoadGroup">
<el-alert type="warning" :closable="false" show-icon :title="`所选 ${selectedOrders.length} 票须属于同一国内仓、同一渠道,并全量进入同一柜;目的仓或目的港可以不同。资格由服务端按完整入仓片段校验。`" />
<div class="coload-selection"><span v-for="row in selectedOrders" :key="String(row.orderId)">{{ row.inboundCode || '待生成入仓码' }}</span></div>
<section class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">所选入仓码</h3>
<div class="coload-selection"><span v-for="row in selectedOrders" :key="String(row.orderId)">{{ row.inboundCode || '待生成入仓码' }}</span></div>
</section>
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
</AppDialog>
<AppDialog class="admin-density-dialog" v-model="coLoadDetailOpen" title="同柜组成员" :show-confirm="false" cancel-text="关闭" width="min(720px, 94vw)" :loading="coLoadDetailLoading" :confirm-loading="coLoadCancelSaving">
@@ -2954,12 +2994,15 @@ onBeforeUnmount(() => {
<template v-else-if="coLoadDetailSummary">
<div class="coload-detail-summary"><el-tag size="small" :type="coLoadDetailSummary.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroupSummary(coLoadDetailSummary) }}</el-tag><span>{{ coLoadDetailSummary.memberCount }} 个成员</span></div>
<el-alert v-if="coLoadDetailHasMissingInboundCodes" type="error" :closable="false" show-icon title="同柜组成员入仓码尚未返回,请刷新后重试" />
<div v-else-if="coLoadDetailMembers.length" class="coload-detail-scroll">
<table class="coload-detail-table">
<thead><tr><th>入仓码</th></tr></thead>
<tbody><tr v-for="member in coLoadDetailMembers" :key="member.inboundCode"><td><strong :title="member.inboundCode">{{ member.inboundCode }}</strong></td></tr></tbody>
</table>
</div>
<section v-else-if="coLoadDetailMembers.length" class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">同柜成员</h3>
<div class="coload-detail-scroll">
<table class="coload-detail-table">
<thead><tr><th>入仓码</th></tr></thead>
<tbody><tr v-for="member in coLoadDetailMembers" :key="member.inboundCode"><td><strong :title="member.inboundCode">{{ member.inboundCode }}</strong></td></tr></tbody>
</table>
</div>
</section>
<el-empty v-else description="暂无同柜组成员" :image-size="56" />
<div v-if="coLoadCancelError" class="coload-error" role="alert"><span>{{ coLoadCancelError.message }}</span><ApiErrorMeta :details="coLoadCancelError" /></div>
</template>
@@ -2976,7 +3019,7 @@ onBeforeUnmount(() => {
/>
</AppDrawer>
<AppDialog class="admin-density-dialog" v-model="noteOpen" title="编辑订单备注" width="min(680px, calc(100vw - 28px))" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
<AppDialog class="admin-density-dialog" v-model="noteOpen" :title="orderDialogTitle('编辑订单备注', noteOrder?.inboundCode)" width="min(680px, calc(100vw - 28px))" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
<el-form label-position="top" class="order-remarks-form">
<el-form-item label="客户备注(只读)"><el-input :model-value="noteOrder?.customerRemark || '—'" disabled /></el-form-item>
<el-form-item label="内部备注"><el-input v-model="noteForm.internalRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空内部备注" /></el-form-item>
@@ -2985,19 +3028,22 @@ onBeforeUnmount(() => {
</el-form>
</AppDialog>
<AppDialog class="admin-density-dialog" v-model="holdDialogOpen" :title="holdAction === 'HOLD' ? '确认扣货' : '确认解除扣货'" :confirm-text="holdAction === 'HOLD' ? '确认扣货' : '确认解除'" :confirm-type="holdAction === 'HOLD' ? 'danger' : 'primary'" :confirm-loading="holdSaving" :confirm-disabled="!holdReason.trim() || holdReason.trim().length > 1000" width="min(600px, calc(100vw - 28px))" destroy-on-close @confirm="submitHoldAction">
<AppDialog class="admin-density-dialog" v-model="holdDialogOpen" :title="orderDialogTitle(holdAction === 'HOLD' ? '确认扣货' : '确认解除扣货', detail?.summary.inboundCode)" :confirm-text="holdAction === 'HOLD' ? '确认扣货' : '确认解除'" :confirm-type="holdAction === 'HOLD' ? 'danger' : 'primary'" :confirm-loading="holdSaving" :confirm-disabled="!holdReason.trim() || holdReason.trim().length > 1000" width="min(600px, calc(100vw - 28px))" destroy-on-close @confirm="submitHoldAction">
<el-alert :title="holdAction === 'HOLD' ? '扣货后服务端将阻止后续实物履约操作。' : '解除后是否可继续履约仍以服务端状态为准。'" :type="holdAction === 'HOLD' ? 'warning' : 'info'" :closable="false" show-icon />
<el-form label-position="top" class="hold-command-form"><el-form-item :label="holdAction === 'HOLD' ? '扣货原因' : '解除原因'" required><el-input v-model="holdReason" type="textarea" :rows="4" maxlength="1000" show-word-limit :placeholder="holdAction === 'HOLD' ? '请输入扣货原因' : '请输入解除扣货原因'" aria-label="扣货操作原因" /></el-form-item><el-form-item label="关联文件(选填)"><el-upload :key="holdEvidenceUploadKey" :auto-upload="false" :limit="1" accept=".txt,.pdf,.jpg,.jpeg,.png,text/plain,application/pdf,image/jpeg,image/png" :disabled="holdSaving" :on-change="selectHoldEvidenceFile" :on-remove="removeHoldEvidenceFile"><el-button :disabled="holdSaving"><FileUp :size="15" />选择文件</el-button><template #tip><div class="el-upload__tip">仅支持 TXT、PDF、JPG、PNG,单文件不超过 10MB;服务端将校验文件真实内容。</div></template></el-upload></el-form-item><LoadErrorState v-if="holdEvidenceError" compact title="关联文件处理失败" :message="holdEvidenceError.message" :show-retry="false"><template #details><ApiErrorMeta :details="holdEvidenceError" /></template></LoadErrorState></el-form>
</AppDialog>
<AppDialog class="admin-density-dialog" v-model="approvalOpen" title="审核订单" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" destroy-on-close @confirm="approveOrder" @cancel="closeApproval">
<dl v-if="approvalOrder" class="approval-facts">
<div><dt>入仓码</dt><dd>{{ approvalOrder.inboundCode || '—' }}</dd></div>
<div><dt>客户</dt><dd>{{ customerName(approvalOrder) }}</dd></div>
<div><dt>渠道</dt><dd>{{ approvalOrder.channelName || '—' }}</dd></div>
<div><dt>目的仓</dt><dd>{{ approvalOrder.destinationWarehouseName || '—' }}</dd></div>
<div><dt>总箱数</dt><dd>{{ approvalOrder.totalPackageCount ?? '—' }} 箱</dd></div>
</dl>
<AppDialog class="admin-density-dialog" v-model="approvalOpen" :title="orderDialogTitle('审核订单', approvalOrder?.inboundCode)" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" destroy-on-close @confirm="approveOrder" @cancel="closeApproval">
<section v-if="approvalOrder" class="density-dialog-section" data-tone="summary">
<h3 class="density-dialog-section__title">订单审核资料</h3>
<dl class="approval-facts">
<div><dt>入仓码</dt><dd>{{ approvalOrder.inboundCode || '—' }}</dd></div>
<div><dt>客户</dt><dd>{{ customerName(approvalOrder) }}</dd></div>
<div><dt>渠道</dt><dd>{{ approvalOrder.channelName || '—' }}</dd></div>
<div><dt>目的仓</dt><dd>{{ approvalOrder.destinationWarehouseName || '—' }}</dd></div>
<div><dt>总箱数</dt><dd>{{ approvalOrder.totalPackageCount ?? '—' }} 箱</dd></div>
</dl>
</section>
<p class="approval-hint">确认订单资料无误后审核通过,订单将进入待测量阶段。</p>
</AppDialog>
@@ -3013,6 +3059,7 @@ onBeforeUnmount(() => {
v-model="measurementPreviewOpen"
:title="measurementPreviewTitle"
width="min(920px, calc(100vw - 28px))"
fullscreenable
:loading="measurementPreviewLoading"
:show-footer="false"
destroy-on-close
@@ -3079,7 +3126,7 @@ onBeforeUnmount(() => {
.table-card :deep(.el-table__row.is-coload-group-row > .el-table__cell) { background: #fff6df; color: #7a4b00; }
.table-card :deep(.el-table__row.is-coload-group-row:hover > .el-table__cell) { background: #ffedbd; }
.table-card :deep(.el-table__row.is-coload-group-row > .el-table__cell:first-child) { box-shadow: inset 3px 0 0 #e6a23c; }
.coload-selection { display: flex; flex-wrap: wrap; gap: var(--admin-space-2, 6px); margin-top: var(--admin-space-5, 12px); }
.coload-selection { display: flex; flex-wrap: wrap; gap: var(--admin-space-2, 6px); }
.coload-selection > span { padding: var(--admin-space-1, 4px) 7px; border: 1px solid var(--cc-border); border-radius: var(--admin-radius, 4px); background: var(--cc-surface-muted); font-size: var(--admin-font-caption, 12px); }
.coload-detail-summary { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-5, 12px); margin-bottom: var(--admin-space-4, 10px); }
.coload-detail-scroll { overflow-x: auto; border: 1px solid var(--cc-border); border-radius: var(--admin-radius, 4px); }
@@ -3096,9 +3143,8 @@ onBeforeUnmount(() => {
.subsequent-fulfillment { margin-top: var(--admin-space-5, 12px); padding-top: var(--admin-space-4, 10px); border-top: 1px solid var(--cc-border); }
.subsequent-fulfillment h3 { margin: 0 0 var(--admin-space-3, 8px); font-size: var(--admin-font-section, 14px); }
.fulfillment-facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); overflow: hidden; }
.batch-approval-summary { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-6, 16px); margin: var(--admin-space-5, 12px) 0 var(--admin-space-3, 8px); color: var(--cc-muted); font-size: var(--admin-font-body, 13px); white-space: nowrap; }
.batch-approval-summary { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-6, 16px); margin: 0; color: var(--cc-muted); font-size: var(--admin-font-body, 13px); white-space: nowrap; }
.batch-approval-summary strong { color: var(--cc-text); }
.batch-approval-table :deep(.cell) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fulfillment-facts > div { display: flex; align-items: baseline; gap: var(--admin-space-4, 10px); min-width: 0; padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); background: var(--admin-surface, #fff); }
.fulfillment-facts > div.wide { grid-column: span 2; }
.fulfillment-facts dt { flex: 0 0 auto; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
@@ -3136,20 +3182,14 @@ onBeforeUnmount(() => {
.panel-title > svg { color: var(--cc-primary); }
.measurement-title-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--admin-space-3, 8px); }
.measurement-title-actions :deep(.el-button) { margin-left: 0; }
.fee-confirm-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 22px; }
.fee-confirm-price-alert { margin-bottom: 14px; }
.fee-confirm-number-wrap { position: relative; width: 100%; }
.fee-confirm-number { width: 100%; }
.fee-confirm-number :deep(.el-input__wrapper) { padding-right: 92px; }
.fee-confirm-unit { position: absolute; top: 50%; right: 12px; z-index: 1; transform: translateY(-50%); color: var(--cc-muted); pointer-events: none; }
.fee-confirm-reference-title { margin: var(--admin-space-3, 8px) 0 var(--admin-space-5, 12px); color: var(--cc-text); font-size: var(--admin-font-section, 14px); }
.fee-confirm-price-reason { grid-column: 1 / -1; }
.fee-confirm-price-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0 0 var(--admin-space-6, 16px); border: 1px solid var(--cc-border); background: var(--admin-label-bg, #f8fafc); }
.fee-confirm-price-summary > div { min-width: 0; padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); }
.fee-confirm-price-summary > div + div { border-left: 1px solid var(--cc-border); }
.fee-confirm-price-summary dt { color: var(--cc-muted); font-size: var(--admin-font-caption, 11px); }
.fee-confirm-price-summary dd { margin: var(--admin-space-1, 4px) 0 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); font-weight: 650; }
@media (max-width: 680px) { .fee-confirm-form { grid-template-columns: 1fr; } .fee-confirm-unit { right: 12px; } }
.fee-confirm-price-alert { margin-bottom: var(--admin-space-5); }
.fee-confirm-table { table-layout: fixed; }
.fee-confirm-label-column { width: 18%; }
@media (max-width: 680px) {
.fee-confirm-table colgroup { display: none; }
.fee-confirm-table tr { display: grid; grid-template-columns: 36% minmax(0, 1fr); }
.fee-confirm-table :is(th, td) { min-width: 0; }
}
.fee-section-title { margin: var(--admin-space-5, 12px) 0 var(--admin-space-3, 8px); color: #344054; font-size: var(--admin-font-section, 14px); line-height: 1.4; }
.fee-overview { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border: 1px solid var(--cc-border); }
.fee-overview > div { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--admin-space-1, 4px) var(--admin-space-5, 12px); padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); }
@@ -3204,7 +3244,6 @@ onBeforeUnmount(() => {
.hold-summary dt { flex: 0 0 auto; margin: 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.hold-summary dd { margin: 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); overflow-wrap: anywhere; }
.hold-actions { display: flex; flex: 0 0 auto; gap: var(--admin-space-3, 8px); }
.legacy-import-content { display: grid; gap: var(--admin-space-5, 12px); }
.legacy-import-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border: 1px solid var(--cc-border); }
.legacy-import-facts > div { display: grid; grid-template-columns: 120px minmax(0, 1fr); align-items: center; padding-block: var(--admin-space-2, 6px); }
.legacy-import-facts dt { padding: 8px 10px; color: var(--cc-text-muted); }
@@ -28,7 +28,7 @@
.unified-orders-page .fulfillment-tree-panel .tree-summary > div { display: flex; align-items: baseline; gap: var(--admin-space-3, 8px); }
.unified-orders-page .fulfillment-tree-panel .tree-summary strong { overflow: visible; white-space: normal; overflow-wrap: anywhere; font-size: var(--admin-font-body, 13px); font-weight: 600; }
.admin-density-dialog.el-dialog .batch-approval-table .cell { white-space: normal; overflow-wrap: anywhere; }
.admin-density-dialog.el-dialog .batch-approval-table td .cell.el-tooltip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal; }
.admin-density-dialog.el-dialog .batch-approval-table .el-tag { height: auto; min-height: 24px; white-space: normal; line-height: 20px; }
@media (max-width: 960px) {