This commit is contained in:
2026-10-20 23:34:11 +08:00
parent f155976f52
commit 13e4aa56ec
321 changed files with 6756 additions and 2110 deletions
@@ -1,6 +1,7 @@
<script setup lang="ts">
import InlineHelp from '@/shared/components/InlineHelp.vue'
import '@/styles/adminDensity.css'
import '@/styles/adminWorkbench.css'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { FileUp, Plus, RefreshCw, X } from '@lucide/vue'
@@ -301,7 +302,7 @@ onMounted(() => void load())
</script>
<template>
<section class="fee-detail-manager admin-density" aria-label="运单费用维护">
<section class="fee-detail-manager admin-density admin-workbench" aria-label="运单费用维护">
<div class="fee-toolbar">
<span>共 {{ rows.length }} 条应收应付费用明细</span>
<div>
@@ -337,7 +338,7 @@ onMounted(() => void load())
</div>
</template>
<AppDialog class="admin-density-dialog" v-model="editorOpen" :title="editorMode === 'create' ? '新增费用' : editorMode === 'adjust' ? '调整已审核费用' : '编辑费用'" width="min(1000px, calc(100vw - 28px))" :confirm-loading="saving" confirm-text="保存" @confirm="save">
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="editorOpen" :title="editorMode === 'create' ? '新增费用' : editorMode === 'adjust' ? '调整已审核费用' : '编辑费用'" width="min(1000px, calc(100vw - 28px))" :confirm-loading="saving" confirm-text="保存" @confirm="save">
<el-alert v-if="editorMode === 'adjust'" title="调整将生成新版本并重新进入待审核状态" type="warning" :closable="false" show-icon />
<el-alert v-if="!canViewFeeItems" title="当前账号缺少费用项目查看权限,无法新增或修改费用" type="warning" :closable="false" show-icon />
@@ -370,7 +371,7 @@ onMounted(() => void load())
</el-form>
</section>
</AppDialog>
<AppDialog class="admin-density-dialog" v-model="rejectOpen" title="驳回费用" :confirm-loading="saving" confirm-text="确认驳回" confirm-type="warning" width="min(880px, calc(100vw - 28px))" @confirm="reject">
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="rejectOpen" title="驳回费用" :confirm-loading="saving" confirm-text="确认驳回" confirm-type="warning" width="min(880px, calc(100vw - 28px))" @confirm="reject">
<el-alert v-if="actionError" type="error" :closable="true" @close="actionError = null"><template #title>{{ actionError.message }}</template><ApiErrorMeta :details="actionError" /></el-alert>
<el-form class="density-form" label-position="top"><el-form-item label="驳回原因" required><el-input v-model="rejectReason" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入驳回原因" /></el-form-item></el-form>
</AppDialog>
@@ -386,7 +387,7 @@ onMounted(() => void load())
.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; }
.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(--workbench-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(--workbench-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; }
.fee-table-shell { min-width: 0; max-width: 100%; overflow: hidden; }
.fee-table-shell :deep(.el-table) { width: 100%; max-width: 100%; }
.private-delivery-tag { margin-left: var(--admin-space-2); vertical-align: middle; }
@@ -1,5 +1,6 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import '@/styles/adminWorkbench.css'
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AppDialog from '@/shared/components/AppDialog.vue'
@@ -220,7 +221,7 @@ async function save() {
</script>
<template>
<AppDialog class="admin-density-dialog"
<AppDialog class="admin-density-dialog admin-workbench is-dialog"
:model-value="modelValue"
:title="`编辑客户申报资料${detail.summary.inboundCode ? ' · ' + detail.summary.inboundCode : ''}`"
fullscreenable
@@ -254,7 +255,7 @@ async function save() {
<section class="density-dialog-section" data-tone="work">
<h3 class="density-dialog-section__title">订单申报资料</h3>
<el-form label-position="left" label-width="110px" class="order-fields density-form">
<table class="density-table density-table--editable order-declaration-fields" aria-label="订单申报资料">
<div class="workbench-evidence"><table class="density-table density-table--editable order-declaration-fields" aria-label="订单申报资料">
<colgroup><col style="width: 40%"><col style="width: 27%"><col style="width: 33%"></colgroup>
<tbody>
<tr>
@@ -267,7 +268,7 @@ async function save() {
<td colspan="2"><el-form-item label="客户备注"><el-input v-model="form.customerRemark" maxlength="1000" show-word-limit aria-label="客户备注" placeholder="请输入客户备注(选填)" /></el-form-item></td>
</tr>
</tbody>
</table>
</table></div>
</el-form>
</section>
@@ -289,7 +290,7 @@ async function save() {
</div>
</AppDialog>
<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">
<AppDialog class="admin-density-dialog admin-workbench is-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">
@@ -1,4 +1,6 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import '@/styles/adminWorkbench.css'
import { onMounted, ref } from 'vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -24,7 +26,7 @@ onMounted(() => { void load() })
</script>
<template>
<div class="fulfillment-tree-panel" :class="{ compact }" aria-label="拆分履约聚合">
<div class="fulfillment-tree-panel admin-workbench" :class="{ compact }" aria-label="拆分履约聚合">
<LoadErrorState v-if="error" title="拆分履约暂时无法加载" :message="error.message" :loading="loading" @retry="load">
<ApiErrorMeta :details="error" />
</LoadErrorState>
@@ -53,11 +55,11 @@ onMounted(() => { void load() })
<style scoped>
.fulfillment-tree-panel { display: grid; gap: 16px; }
.tree-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; padding: 14px; border: 1px solid var(--cc-border); border-radius: 8px; background: var(--cc-surface-muted); }
.tree-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; padding: 14px; border: 1px solid var(--workbench-border); border-radius: 8px; background: var(--workbench-surface-soft); }
.tree-summary div { display: grid; gap: 4px; min-width: 0; }
.tree-summary span { color: var(--cc-muted); font-size: 12px; }
.tree-summary strong { overflow: hidden; color: var(--cc-text); text-overflow: ellipsis; white-space: nowrap; }
.tree-loading { min-height: 120px; display: grid; place-items: center; color: var(--cc-muted); }
small { color: var(--cc-muted); font-weight: 400; }
.tree-summary span { color: var(--workbench-muted); font-size: 12px; }
.tree-summary strong { overflow: hidden; color: var(--workbench-text); text-overflow: ellipsis; white-space: nowrap; }
.tree-loading { min-height: 120px; display: grid; place-items: center; color: var(--workbench-muted); }
small { color: var(--workbench-muted); font-weight: 400; }
@media (max-width: 720px) { .tree-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
</style>
@@ -8,7 +8,8 @@ import UnifiedOrdersView from './UnifiedOrdersView.vue'
vi.mock('vue-router', () => ({ useRoute: () => ({ params: {}, query: {} }), useRouter: () => ({ push: vi.fn() }) }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
vi.mock('@/modules/iam', async importOriginal => ({
...await importOriginal<typeof import('@/modules/iam')>(), listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
@@ -11,7 +11,8 @@ vi.mock('vue-router', () => ({
}))
const { canPermission } = vi.hoisted(() => ({ canPermission: vi.fn((..._permissions: string[]) => true) }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) }))
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
vi.mock('@/modules/iam', async importOriginal => ({
...await importOriginal<typeof import('@/modules/iam')>(), listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
vi.mock('@/modules/inbounds', () => ({
downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn(),
@@ -12,7 +12,8 @@ vi.mock('vue-router', () => ({
useRouter: () => ({ push: vi.fn() }),
}))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
vi.mock('@/modules/iam', async importOriginal => ({
...await importOriginal<typeof import('@/modules/iam')>(), listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
vi.mock('@/modules/inbounds', () => ({
downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn(),
@@ -165,12 +166,15 @@ describe('UnifiedOrdersView list request coordination', () => {
await wrapper.findAll('[role="tab"]').find((tab) => tab.text() === '测量数据审核')!.trigger('click')
await flushPromises()
expect(wrapper.getComponent({ name: 'InboundTaskList' }).props()).toMatchObject({ mode: 'review', title: '订单列表' })
expect(wrapper.getComponent({ name: 'InboundTaskList' }).classes()).toContain('is-embedded')
expect(wrapper.get('main').classes()).not.toContain('is-embedded')
expect(wrapper.get('[aria-selected="true"]').text()).toBe('测量数据审核')
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
await wrapper.findAll('[role="tab"]').find((tab) => tab.text() === '待测量数据')!.trigger('click')
await flushPromises()
expect(wrapper.findComponent({ name: 'InboundTaskList' }).exists()).toBe(false)
expect(wrapper.find('.is-embedded').exists()).toBe(false)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'WAIT_MEASURE' }))
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
})
@@ -70,6 +70,11 @@ describe('UnifiedOrdersView contract', () => {
})
it('renders accessible current-status tabs with keyboard navigation', () => {
expect(source.match(/class="orders-status-toolbar"/g)).toHaveLength(2)
expect(source).toContain('.orders-status-toolbar :deep(.workbench-table.is-page-surface > .data-table > .data-table__toolbar > .data-table__toolbar-start)')
expect(source).toContain('flex: 0 0 100%;')
expect(source).toContain('.orders-status-toolbar :deep(.workbench-table.is-page-surface > .data-table > .data-table__toolbar > .data-table__toolbar-actions)')
expect(source).toContain('margin-left: auto;')
expect(source).toContain('role="tablist"')
expect(source).toContain('role="tab"')
expect(source).toContain(':aria-selected="selectedTab === tab.value"')
@@ -236,13 +241,21 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain("detail.summary.internalRemark || detail.summary.latestInternalNote || '—'")
})
it('keeps the title, search, errors and results in the integrated list', () => {
it('keeps status tabs, search, errors and results in the integrated list', () => {
expect(source).toContain('<ListPageLayout v-else ariaLabel="统一订单列表">')
expect(source).toContain('<template #filters>')
expect(source).toContain('<template #notices>')
expect(source).toContain('<template #default>')
expect(source).toContain(':results-visible="!loadError"')
expect(source).toContain('<template #toolbar-start><h1>订单列表</h1></template>')
const toolbar = source.match(/<template #toolbar-start>([\s\S]*?)<\/template>/)?.[1]
expect(toolbar).toContain('class="status-tabs density-status-tabs workbench-list-tabs"')
expect(toolbar).toContain('@click="selectStatus(tab.value)"')
expect(toolbar).toContain('@keydown="handleTabKeydown($event, index)"')
expect(toolbar).toContain('tabCount(tab.value)')
expect(toolbar).toContain('statusCountsError.message')
expect(toolbar).toContain('@click="loadStatusCounts"')
expect(toolbar).not.toContain('<h1>')
expect(source.match(/class="status-tabs density-status-tabs workbench-list-tabs"/g)).toHaveLength(2)
expect(source).toContain('appearance="integrated"')
expect(source).not.toContain('class="orders-load-error"')
expect(source).not.toContain('.orders-load-error { margin-top: 0; }')
@@ -1,4 +1,5 @@
<script setup lang="ts">
import '@/styles/adminWorkbench.css'
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
import { ArrowLeft, ChevronDown, ChevronUp, CircleCheck, Download, FileUp, GitBranch, Link2, PackageOpen, Printer, Send, ShieldAlert, SlidersHorizontal, StickyNote, Undo2 } from '@lucide/vue'
@@ -37,6 +38,7 @@ import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/
import { createPlatformExportTask } from '@/modules/downloads'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, getPlatformInboundCodeAssociation, type LoadingAssociationSummary, type OverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
import InlineHelp from '@/shared/components/InlineHelp.vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
@@ -2388,13 +2390,13 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</script>
<template>
<AdminPage>
<AdminPage class="admin-workbench admin-density">
<section class="business-page unified-orders-page admin-density">
<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">
<AppDialog class="admin-density-dialog admin-workbench is-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 />
<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="本次计费数据">
<div class="workbench-evidence"><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>
@@ -2422,7 +2424,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<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>
</table></div>
</section>
<section class="density-dialog-section" data-tone="source">
@@ -2437,7 +2439,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</section>
</AppDialog>
<template v-if="isDetailRoute">
<DetailPageLayout ariaLabel="统一订单详情">
<DetailPageLayout class="workbench-panel is-flush is-page-surface" ariaLabel="统一订单详情">
<header class="order-detail-header density-detail-header">
<div class="order-detail-heading density-detail-heading">
<el-button link class="back-button density-back" @click="fromMeasurementReview ? returnToMeasurementReviews() : closeDetail()"><ArrowLeft :size="16" />{{ fromMeasurementReview ? '返回测量审核列表' : '返回列表' }}</el-button>
@@ -2764,7 +2766,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</template>
</section>
<section class="other-notes-section" aria-label="内部备注">
<div class="panel-title"><div><h2>内部备注</h2><p>仅平台内部可见,独立保存</p></div><el-button v-if="can('platform:order:note')" type="primary" size="small" @click="openNoteDialog()"><StickyNote :size="15" />添加备注</el-button></div>
<div class="panel-title"><div><h2>内部备注<InlineHelp title="备注可见范围"><p>仅平台内部可见,独立保存。</p></InlineHelp></h2></div><el-button v-if="can('platform:order:note')" type="primary" size="small" @click="openNoteDialog()"><StickyNote :size="15" />添加备注</el-button></div>
<p v-if="detail.order.internalNotes.length === 0" class="compact-empty">暂无内部备注</p>
<ul v-else class="notes-list" aria-label="内部备注"><li v-for="item in detail.order.internalNotes" :key="String(item.id)"><p>{{ item.content }}</p><span>操作人 {{ item.createdBy || '系统' }} · {{ formatDate(item.createdAt) }}</span></li></ul>
</section>
@@ -2776,10 +2778,10 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</template>
<template v-else>
<div v-if="measurementReviewSelected" id="unified-orders-results" role="tabpanel">
<InboundTaskList mode="review" title="订单列表" @changed="loadStatusCounts">
<div v-if="measurementReviewSelected" id="unified-orders-results" class="orders-status-toolbar" role="tabpanel">
<InboundTaskList mode="review" title="订单列表" class="is-embedded" @changed="loadStatusCounts">
<template #status-tabs>
<div ref="statusTabList" class="status-tabs density-status-tabs" role="tablist" aria-label="订单主状态" :aria-busy="statusCountsLoading">
<div ref="statusTabList" class="status-tabs density-status-tabs workbench-list-tabs" role="tablist" aria-label="订单主状态" :aria-busy="statusCountsLoading">
<button
v-for="(tab, index) in statusTabs"
:key="tab.value || 'ALL'"
@@ -2808,11 +2810,9 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<template #default>
<div id="unified-orders-results" role="tabpanel"><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError" v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table-v2" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" :row-class-name="unifiedOrderRowClassName" row-key="orderId" border empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleOrderSelectionChange">
<template #toolbar-start><h1>订单列表</h1></template>
<template #filters>
<div class="orders-query-panel integrated-query-group">
<div ref="statusTabList" class="status-tabs density-status-tabs" role="tablist" aria-label="订单主状态" :aria-busy="statusCountsLoading">
<div id="unified-orders-results" class="orders-status-toolbar" role="tabpanel"><div class="workbench-table is-page-surface"><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError" v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table-v2" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" :row-class-name="unifiedOrderRowClassName" row-key="orderId" border empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleOrderSelectionChange">
<template #toolbar-start>
<div ref="statusTabList" class="status-tabs density-status-tabs workbench-list-tabs" role="tablist" aria-label="订单主状态" :aria-busy="statusCountsLoading">
<button
v-for="(tab, index) in statusTabs"
:key="tab.value || 'ALL'"
@@ -2833,6 +2833,9 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<el-button link type="primary" @click="loadStatusCounts">重试数量</el-button>
<ApiErrorMeta :details="statusCountsError" />
</div>
</template>
<template #filters>
<div class="orders-query-panel integrated-query-group">
<QueryToolbar id="unified-orders-advanced-filters" v-model="filters" class="orders-query-toolbar" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query-v2" :search-on-reset="false" @search="search" @reset="reset">
<template #actions>
<el-button :aria-expanded="showAdvancedFilters" aria-controls="unified-orders-advanced-filters" @click="showAdvancedFilters = !showAdvancedFilters">
@@ -2927,11 +2930,11 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<template #expectedShipDate="{ row }">{{ formatDate((row as UnifiedOrderResource).expectedShipDate) }}</template>
<template #updatedAt="{ row }">{{ formatDate((row as UnifiedOrderResource).updatedAt) }}</template>
<template #pagination-left="{ total: tableTotal }"><span>共 {{ tableTotal }} 条</span></template>
</DataTable></div>
</DataTable></div></div>
</template>
</ListPageLayout>
<AppDialog class="admin-density-dialog" v-model="legacyImportOpen" title="导入旧系统订单" width="min(880px, calc(100vw - 28px))" confirm-text="开始导入" :confirm-loading="legacyImportSaving" :confirm-disabled="!legacyImportFile || legacyImportSaving || !!legacyImportResult" destroy-on-close @confirm="submitLegacyImport">
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="legacyImportOpen" title="导入旧系统订单" width="min(880px, calc(100vw - 28px))" confirm-text="开始导入" :confirm-loading="legacyImportSaving" :confirm-disabled="!legacyImportFile || legacyImportSaving || !!legacyImportResult" destroy-on-close @confirm="submitLegacyImport">
<div class="legacy-import-content density-dialog-sections">
<section class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">导入规则</h3>
@@ -2959,7 +2962,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</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))" fullscreenable :loading="batchApprovalLoading" :confirm-loading="batchApprovalSaving" :confirm-disabled="batchApprovalLoading || !batchApprovalSelectedRows.length" destroy-on-close @confirm="batchApproveOrders">
<AppDialog class="admin-density-dialog admin-workbench is-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>
@@ -2981,7 +2984,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</section>
</template>
</AppDialog>
<AppDialog class="admin-density-dialog" v-model="coLoadCreateOpen" title="合柜报关" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="!canCreateCoLoadGroup" width="min(880px, calc(100vw - 28px))" @confirm="createCoLoadGroup">
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="coLoadCreateOpen" title="合柜报关" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="!canCreateCoLoadGroup" width="min(880px, calc(100vw - 28px))" @confirm="createCoLoadGroup">
<el-alert type="warning" :closable="false" show-icon :title="`所选 ${selectedOrders.length} 票须属于同一国内仓、同一渠道,并全量进入同一柜;目的仓或目的港可以不同。资格由服务端按完整入仓片段校验。`" />
<section class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">所选入仓码</h3>
@@ -2989,7 +2992,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</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(880px, 94vw)" :loading="coLoadDetailLoading" :confirm-loading="coLoadCancelSaving">
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="coLoadDetailOpen" title="同柜组成员" :show-confirm="false" cancel-text="关闭" width="min(880px, 94vw)" :loading="coLoadDetailLoading" :confirm-loading="coLoadCancelSaving">
<LoadErrorState v-if="coLoadDetailError" title="同柜组成员加载失败" :message="coLoadDetailError.message" :loading="coLoadDetailLoading" @retry="loadCoLoadDetail"><template #details><ApiErrorMeta :details="coLoadDetailError" /></template></LoadErrorState>
<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>
@@ -2997,10 +3000,10 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<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">
<div class="workbench-evidence"><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>
</table></div>
</div>
</section>
<el-empty v-else description="暂无同柜组成员" :image-size="56" />
@@ -3010,7 +3013,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</AppDialog>
</template>
<AppDrawer class="admin-density-drawer" v-model="relationOpen" title="父子订单关联" size="min(860px, 94vw)" :show-footer="false" destroy-on-close>
<AppDrawer class="admin-density-drawer admin-workbench is-drawer" v-model="relationOpen" title="父子订单关联" size="min(860px, 94vw)" :show-footer="false" destroy-on-close>
<OrderFulfillmentTreePanel
v-if="relationOrder"
:tenant-id="String(relationOrder.tenantId)"
@@ -3019,7 +3022,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
/>
</AppDrawer>
<AppDialog class="admin-density-dialog" v-model="noteOpen" :title="orderDialogTitle('编辑订单备注', noteOrder?.inboundCode)" width="min(1000px, calc(100vw - 28px))" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="noteOpen" :title="orderDialogTitle('编辑订单备注', noteOrder?.inboundCode)" width="min(1000px, 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 /></el-form-item>
@@ -3028,12 +3031,12 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</el-form>
</AppDialog>
<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(880px, calc(100vw - 28px))" destroy-on-close @confirm="submitHoldAction">
<AppDialog class="admin-density-dialog admin-workbench is-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(880px, 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="orderDialogTitle('审核订单', approvalOrder?.inboundCode)" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" width="min(880px, calc(100vw - 28px))" destroy-on-close @confirm="approveOrder" @cancel="closeApproval">
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="approvalOpen" :title="orderDialogTitle('审核订单', approvalOrder?.inboundCode)" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" width="min(880px, calc(100vw - 28px))" 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">
@@ -3055,7 +3058,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
/>
<AppDialog
class="admin-density-dialog"
class="admin-density-dialog admin-workbench is-dialog"
v-model="measurementPreviewOpen"
:title="measurementPreviewTitle"
width="min(920px, calc(100vw - 28px))"
@@ -3089,13 +3092,23 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</template>
<style scoped>
.detail-tabs { border: 1px solid var(--cc-border); border-block: 0; }
/* Order statuses need a full row before the business actions in both list modes. */
@media (min-width: 721px) {
.unified-orders-page .orders-status-toolbar :deep(.workbench-table.is-page-surface > .data-table > .data-table__toolbar > .data-table__toolbar-start) {
flex: 0 0 100%;
}
.unified-orders-page .orders-status-toolbar :deep(.workbench-table.is-page-surface > .data-table > .data-table__toolbar > .data-table__toolbar-actions) {
margin-left: auto;
}
}
.panel-title > div:first-child { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--admin-space-1, 4px) var(--admin-space-5, 12px); }
.compact-empty { margin: 0; padding: 14px var(--admin-space-4, 10px); color: var(--cc-muted); text-align: center; font-size: var(--admin-font-body, 13px); }
.approval-facts { margin: 0; border-top: 1px solid var(--cc-border); }
.approval-facts > div { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: var(--admin-space-5, 12px); padding: var(--admin-space-3, 8px) 0; border-bottom: 1px solid var(--cc-border); }
.approval-facts dt { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.compact-empty { margin: 0; padding: 14px var(--admin-space-4, 10px); color: var(--workbench-muted); text-align: center; font-size: var(--admin-font-body, 13px); }
.approval-facts { margin: 0; border-top: 1px solid var(--workbench-border); }
.approval-facts > div { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: var(--admin-space-5, 12px); padding: var(--admin-space-3, 8px) 0; border-bottom: 1px solid var(--workbench-border); }
.approval-facts dt { color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
.approval-facts dd { min-width: 0; margin: 0; overflow-wrap: anywhere; font-size: var(--admin-font-body, 13px); }
.approval-hint { margin: var(--admin-space-5, 12px) 0 0; color: #475467; font-size: var(--admin-font-body, 13px); }
.detail-panel :deep(.el-table) { font-size: var(--admin-font-body, 13px); --el-table-header-bg-color: var(--admin-label-bg, #f8fafc); }
@@ -3104,7 +3117,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
.detail-panel :deep(.el-table__empty-block) { min-height: 44px; }
.detail-panel :deep(.el-table__empty-text) { line-height: 44px; }
.history-table :deep(.cell) { white-space: normal; }
.measurement-section-title:has([aria-expanded="false"]) { border-bottom: 1px solid var(--cc-border); }
.measurement-section-title:has([aria-expanded="false"]) { border-bottom: 1px solid var(--workbench-border); }
.fulfillment-facts + .fulfillment-facts { border-top: 0; }
@media (min-width: 961px) {
.table-card :deep(.data-table__pagination) { min-height: 44px; padding: var(--admin-space-2, 6px) var(--admin-space-5, 12px); }
@@ -3112,8 +3125,8 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
.unified-orders-page { min-width: 0; gap: var(--admin-space-5, 12px); --cc-list-table-font-size: 13px; --cc-list-query-font-size: 13px; --cc-list-action-font-size: 13px; }
.page-intro { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--admin-space-6, 16px); margin-bottom: var(--admin-space-6, 16px); }
.page-intro h1 { margin: 0; color: var(--cc-text); font-size: 22px; line-height: 1.35; }
.page-intro p { margin: 5px 0 0; color: var(--cc-muted); font-size: var(--admin-font-body, 13px); line-height: 1.6; }
.page-intro h1 { margin: 0; color: var(--workbench-text); font-size: 22px; line-height: 1.35; }
.page-intro p { margin: 5px 0 0; color: var(--workbench-muted); font-size: var(--admin-font-body, 13px); line-height: 1.6; }
.page-intro :deep(.el-button) { flex: 0 0 auto; }
.orders-query-panel { min-width: 0; }
.orders-query-toolbar { border: 0; padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); }
@@ -3127,34 +3140,34 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
.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); }
.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-selection > span { padding: var(--admin-space-1, 4px) 7px; border: 1px solid var(--workbench-border); border-radius: var(--admin-radius, 4px); background: var(--workbench-surface-soft); 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); }
.coload-detail-scroll { overflow-x: auto; border: 1px solid var(--workbench-border); border-radius: var(--admin-radius, 4px); }
.coload-detail-table { width: 100%; border-collapse: collapse; font-size: var(--admin-font-caption, 12px); }
.coload-detail-table th,.coload-detail-table td { padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border-bottom: 1px solid var(--cc-border); text-align: left; vertical-align: top; }
.coload-detail-table th { background: var(--cc-surface-muted); color: var(--cc-muted); font-weight: 600; }
.coload-detail-table th,.coload-detail-table td { padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border-bottom: 1px solid var(--workbench-border); text-align: left; vertical-align: top; }
.coload-detail-table th { background: var(--workbench-surface-soft); color: var(--workbench-muted); font-weight: 600; }
.coload-detail-table tbody tr:last-child td { border-bottom: 0; }
.coload-detail-table td { min-width: 210px; }
.coload-detail-table td:last-child { min-width: 80px; white-space: nowrap; }
.coload-detail-table td strong,.coload-detail-table td span { display: block; overflow-wrap: anywhere; }
.coload-detail-table td span { color: var(--cc-muted); }
.coload-detail-table td span { color: var(--workbench-muted); }
.coload-detail-table :deep(.el-button) { min-height: 24px; padding: 2px 0; }
.coload-toolbar-action { display: flex; min-width: 0; align-items: center; gap: var(--admin-space-3, 8px); }
.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 { margin-top: var(--admin-space-5, 12px); padding-top: var(--admin-space-4, 10px); border-top: 1px solid var(--workbench-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: 0; color: var(--cc-muted); font-size: var(--admin-font-body, 13px); white-space: nowrap; }
.batch-approval-summary strong { color: var(--cc-text); }
.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 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--workbench-border); border-left: 1px solid var(--workbench-border); overflow: hidden; }
.batch-approval-summary { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-6, 16px); margin: 0; color: var(--workbench-muted); font-size: var(--admin-font-body, 13px); white-space: nowrap; }
.batch-approval-summary strong { color: var(--workbench-text); }
.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(--workbench-border); border-bottom: 1px solid var(--workbench-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); }
.fulfillment-facts dt { flex: 0 0 auto; color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
.fulfillment-facts dd { min-width: 0; margin: 0; overflow-wrap: anywhere; font-size: var(--admin-font-body, 13px); }
.fulfillment-empty { margin: 0; padding: var(--admin-space-5, 12px); border: 1px dashed var(--cc-border); border-radius: 6px; color: var(--cc-muted); text-align: center; }
.fulfillment-empty { margin: 0; padding: var(--admin-space-5, 12px); border: 1px dashed var(--workbench-border); border-radius: 6px; color: var(--workbench-muted); text-align: center; }
.row-actions { display: flex; align-items: center; gap: var(--admin-space-2, 6px); white-space: nowrap; }
.row-actions :deep(.el-button) { margin-left: 0; }
.table-card :deep(.data-table__system-cell .row-actions .el-dropdown) { margin-left: 0; }
.planning-cell { display: grid; min-width: 0; gap: var(--admin-space-1, 4px); }
.planning-cell > small { color: var(--cc-muted); font-size: var(--admin-font-caption, 11px); }
.planning-cell > small { color: var(--workbench-muted); font-size: var(--admin-font-caption, 11px); }
.planning-volume-input,.planning-arrival-input { width: 100%; max-width: 100%; }
.planning-cell :deep(.el-input__wrapper) { min-height: var(--admin-control-height, 32px); padding-right: 9px; padding-left: 9px; }
.planning-cell :deep(.el-input__inner) { min-width: 0; text-align: left; }
@@ -3164,8 +3177,8 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
.order-relation-link :deep(span) { min-width: 0; white-space: normal; text-align: left; }
.primary-cell { display: grid; min-width: 0; gap: var(--admin-space-1, 4px); }
.primary-cell strong { overflow-wrap: anywhere; }
.primary-cell span { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); overflow-wrap: anywhere; }
.order-detail-header { border: 1px solid var(--cc-border); border-bottom: 0; border-radius: 4px 4px 0 0; }
.primary-cell span { color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); overflow-wrap: anywhere; }
.back-button { flex: 0 0 auto; padding: 0; }
.order-title { min-width: 0; }
@@ -3174,12 +3187,12 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
.detail-header-actions :deep(.el-button + .el-button) { margin-left: 0; }
.detail-load-error { min-height: 280px; }
.order-detail-content { min-width: 0; }
.detail-overview { border: 1px solid var(--cc-border); border-top: 0; }
.detail-panel { min-width: 0; padding: var(--admin-space-5, 12px); border: 1px solid var(--cc-border); border-radius: 0 0 4px 4px; background: var(--admin-surface, #fff); }
.detail-panel { min-width: 0; padding: var(--admin-space-5, 12px); }
.panel-title { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-5, 12px); min-height: var(--admin-action-height, 28px); margin-bottom: var(--admin-space-3, 8px); }
.panel-title h2 { margin: 0; color: #344054; font-size: var(--admin-font-section, 14px); font-weight: 600; }
.panel-title p { margin: 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); line-height: 1.5; }
.panel-title > svg { color: var(--cc-primary); }
.panel-title p { margin: 0; color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); line-height: 1.5; }
.panel-title > svg { color: var(--workbench-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-price-alert { margin-bottom: var(--admin-space-5); }
@@ -3191,60 +3204,60 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
.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 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border: 1px solid var(--workbench-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); }
.fee-overview > div + div { border-left: 1px solid var(--cc-border); }
.fee-overview dt, .fee-overview small { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.fee-overview > div + div { border-left: 1px solid var(--workbench-border); }
.fee-overview dt, .fee-overview small { color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
.fee-overview dd { margin: 0; font-size: var(--admin-font-title, 16px); font-weight: 600; font-variant-numeric: tabular-nums; }
.fee-overview small { margin-left: auto; }
.fee-section-title--secondary { margin-top: var(--admin-space-5, 12px); padding-top: var(--admin-space-4, 10px); border-top: 1px solid var(--cc-border); }
.fee-section-title--secondary { margin-top: var(--admin-space-5, 12px); padding-top: var(--admin-space-4, 10px); border-top: 1px solid var(--workbench-border); }
.order-info-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; }
.order-info-grid > div.wide { grid-column: span 2; }
.detail-table-wrap { overflow-x: auto; }
.detail-table-wrap :deep(.el-table) { min-width: 720px; --el-table-header-bg-color: var(--admin-label-bg, #f8fafc); }
.cargo-image { width: 40px; height: var(--admin-control-height, 32px); border: 1px solid var(--cc-border); border-radius: 2px; background: var(--admin-label-bg, #f8fafc); cursor: zoom-in; }
.cargo-image { width: 40px; height: var(--admin-control-height, 32px); border: 1px solid var(--workbench-border); border-radius: 2px; background: var(--admin-label-bg, #f8fafc); cursor: zoom-in; }
.measurement-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; }
.measurement-section { margin-top: var(--admin-space-5, 12px); }
.measurement-section-title { display: flex; width: 100%; min-height: var(--admin-row-height, 36px); align-items: center; gap: var(--admin-space-5, 12px); padding: var(--admin-space-1, 4px) var(--admin-space-4, 10px); border: 1px solid var(--cc-border); border-bottom: 0; background: var(--admin-label-bg, #f8fafc); color: var(--cc-text); }
.measurement-section-title { display: flex; width: 100%; min-height: var(--admin-row-height, 36px); align-items: center; gap: var(--admin-space-5, 12px); padding: var(--admin-space-1, 4px) var(--admin-space-4, 10px); border: 1px solid var(--workbench-border); border-bottom: 0; background: var(--admin-label-bg, #f8fafc); color: var(--workbench-text); }
.measurement-section-title:hover { background: #f2f6fb; }
.measurement-section-title h3 { flex: 0 0 auto; margin: 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); font-weight: 600; }
.measurement-section-summary { display: flex; min-width: 0; flex: 1; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--admin-space-1, 4px) var(--admin-space-6, 16px); color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.measurement-section-title h3 { flex: 0 0 auto; margin: 0; color: var(--workbench-text); font-size: var(--admin-font-body, 13px); font-weight: 600; }
.measurement-section-summary { display: flex; min-width: 0; flex: 1; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--admin-space-1, 4px) var(--admin-space-6, 16px); color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
.measurement-section-toggle { display: flex; min-width: 0; min-height: var(--admin-action-height, 28px); flex: 1; align-items: center; gap: var(--admin-space-6, 16px); padding: 0; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.measurement-section-toggle:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: 2px; }
.measurement-section-toggle > svg { flex: 0 0 auto; margin-left: auto; color: var(--cc-muted); }
.measurement-section-toggle:focus-visible { outline: 2px solid var(--workbench-primary); outline-offset: 2px; }
.measurement-section-toggle > svg { flex: 0 0 auto; margin-left: auto; color: var(--workbench-muted); }
.measurement-section-mode { flex: 0 0 190px; }
.measurement-section-title > .measurement-section-mode { display: none; }
.measurement-rule-selector { display: flex; flex-wrap: wrap; align-items: center; gap: var(--admin-space-4, 10px); margin: 0; padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border: 1px solid var(--cc-border); border-top: 0; background: var(--admin-surface, #fff); }
.measurement-rule-selector__label { color: var(--cc-text); font-size: var(--admin-font-caption, 12px); font-weight: 650; white-space: nowrap; }
.measurement-rule-selector { display: flex; flex-wrap: wrap; align-items: center; gap: var(--admin-space-4, 10px); margin: 0; padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border: 1px solid var(--workbench-border); border-top: 0; background: var(--admin-surface, #fff); }
.measurement-rule-selector__label { color: var(--workbench-text); font-size: var(--admin-font-caption, 12px); font-weight: 650; white-space: nowrap; }
.measurement-rule-selector__control { width: 270px; }
.measurement-rule-selector__hint { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.measurement-rule-selector__hint { color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
.measurement-outer-dimension-input { width: 128px; max-width: 100%; }
.measurement-total { display: flex; min-height: var(--admin-control-height, 32px); flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--admin-space-1, 4px) 20px; padding: var(--admin-space-2, 6px) var(--admin-space-4, 10px); border: 1px solid var(--cc-border); border-top: 0; background: var(--admin-label-bg, #f8fafc); color: #475467; font-size: var(--admin-font-caption, 12px); font-variant-numeric: tabular-nums; }
.measurement-total { display: flex; min-height: var(--admin-control-height, 32px); flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--admin-space-1, 4px) 20px; padding: var(--admin-space-2, 6px) var(--admin-space-4, 10px); border: 1px solid var(--workbench-border); border-top: 0; background: var(--admin-label-bg, #f8fafc); color: #475467; font-size: var(--admin-font-caption, 12px); font-variant-numeric: tabular-nums; }
.measurement-total strong { margin-right: auto; color: var(--cc-text); font-size: var(--admin-font-body, 13px); }
.weight-evidence { margin-top: var(--admin-space-5, 12px); padding: var(--admin-space-4, 10px) 0 0; border-top: 1px solid var(--cc-border); background: var(--admin-surface, #fff); }
.measurement-total strong { margin-right: auto; color: var(--workbench-text); font-size: var(--admin-font-body, 13px); }
.weight-evidence { margin-top: var(--admin-space-5, 12px); padding: var(--admin-space-4, 10px) 0 0; border-top: 1px solid var(--workbench-border); background: var(--admin-surface, #fff); }
.weight-evidence__heading { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-6, 16px); margin-bottom: var(--admin-space-3, 8px); }
.weight-evidence__heading h4 { margin: 0; color: var(--cc-text); font-size: var(--admin-font-section, 14px); }
.weight-evidence__heading p { margin: 5px 0 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.weight-evidence__heading h4 { margin: 0; color: var(--workbench-text); font-size: var(--admin-font-section, 14px); }
.weight-evidence__heading p { margin: 5px 0 0; color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
.weight-evidence__heading dl { margin: 0; text-align: right; }
.weight-evidence__heading dt { color: var(--cc-muted); font-size: var(--admin-font-caption, 11px); }
.weight-evidence__heading dd { margin: 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); font-weight: 600; }
.weight-evidence__heading dt { color: var(--workbench-muted); font-size: var(--admin-font-caption, 11px); }
.weight-evidence__heading dd { margin: 0; color: var(--workbench-text); font-size: var(--admin-font-body, 13px); font-weight: 600; }
.weight-evidence-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--admin-space-3, 8px); }
.weight-evidence-card { display: grid; grid-template-columns: 96px minmax(0, 1fr); align-items: center; min-width: 0; padding: var(--admin-space-3, 8px); border: 1px solid var(--cc-border); background: var(--admin-surface, #fff); }
.weight-evidence-card { display: grid; grid-template-columns: 96px minmax(0, 1fr); align-items: center; min-width: 0; padding: var(--admin-space-3, 8px); border: 1px solid var(--workbench-border); background: var(--admin-surface, #fff); }
.weight-evidence-card > dl { display: grid; gap: var(--admin-space-1, 4px); margin: 0; padding: 0 0 0 var(--admin-space-3, 8px); }
.weight-evidence-card > dl > div { display: flex; min-width: 0; justify-content: space-between; gap: var(--admin-space-5, 12px); }
.weight-evidence-card dt { color: var(--cc-muted); font-size: var(--admin-font-caption, 11px); }
.weight-evidence-card dd { min-width: 0; margin: 0; color: var(--cc-text); font-size: var(--admin-font-caption, 12px); font-weight: 650; text-align: right; overflow-wrap: anywhere; }
.weight-evidence-card dt { color: var(--workbench-muted); font-size: var(--admin-font-caption, 11px); }
.weight-evidence-card dd { min-width: 0; margin: 0; color: var(--workbench-text); font-size: var(--admin-font-caption, 12px); font-weight: 650; text-align: right; overflow-wrap: anywhere; }
.measurement-original-preview { display: block; width: 100%; max-height: min(72vh, 760px); object-fit: contain; border-radius: 8px; background: #f2f4f7; }
.hold-section { padding-bottom: var(--admin-space-5, 12px); border-bottom: 1px solid var(--cc-border); }
.hold-section { padding-bottom: var(--admin-space-5, 12px); border-bottom: 1px solid var(--workbench-border); }
.other-notes-section { padding-top: var(--admin-space-4, 10px); }
.hold-summary { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-6, 16px); padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border: 1px solid var(--cc-border); background: var(--admin-surface, #fff); }
.hold-summary { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-6, 16px); padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border: 1px solid var(--workbench-border); background: var(--admin-surface, #fff); }
.hold-summary dl { display: grid; min-width: 0; flex: 1; grid-template-columns: 210px minmax(280px, 1fr); gap: var(--admin-space-5, 12px); margin: 0; }
.hold-summary dl > div { display: flex; align-items: baseline; gap: var(--admin-space-4, 10px); min-width: 0; }
.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-summary dt { flex: 0 0 auto; margin: 0; color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
.hold-summary dd { margin: 0; color: var(--workbench-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-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border: 1px solid var(--cc-border); }
.legacy-import-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border: 1px solid var(--workbench-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); }
.legacy-import-facts dd { margin: 0; padding: 8px 10px; font-weight: 600; }
@@ -3254,18 +3267,18 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
.hold-history-title { margin: var(--admin-space-6, 16px) 0 var(--admin-space-3, 8px); color: #344054; font-size: var(--admin-font-section, 14px); }
.hold-history-table { min-width: 0; overflow-x: auto; }
.hold-command-form { margin-top: var(--admin-space-6, 16px); }
.hold-evidence-hint { margin: -var(--admin-space-1, 4px) 0 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.hold-evidence-hint { margin: -var(--admin-space-1, 4px) 0 0; color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
.notes-list { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }
.notes-list p { margin: var(--admin-space-1, 4px) 0 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); line-height: 1.55; overflow-wrap: anywhere; }
.notes-list p { margin: var(--admin-space-1, 4px) 0 0; color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); line-height: 1.55; overflow-wrap: anywhere; }
.notes-list li { padding: 14px 0; border-bottom: 1px solid #edf0f4; }
.notes-list li:first-child { padding-top: 0; }
.notes-list li:last-child { padding-bottom: 0; border-bottom: 0; }
.notes-list li > p { margin: 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); }
.notes-list span { display: block; margin-top: 5px; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.notes-list li > p { margin: 0; color: var(--workbench-text); font-size: var(--admin-font-body, 13px); }
.notes-list span { display: block; margin-top: 5px; color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
@media (max-width: 960px) { .order-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .measurement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .table-actions :deep(.el-button) { width: 100%; min-height: 40px; } .row-actions { flex-wrap: wrap; } .order-detail-header { align-items: stretch; flex-direction: column; } .order-detail-heading { flex-wrap: wrap; } .detail-header-actions { display: grid; grid-template-columns: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .order-info-grid, .measurement-grid { grid-template-columns: 1fr; } .order-info-grid > div.wide { grid-column: auto; } .detail-tabs button { padding: 0 18px; } .detail-panel { padding: 16px; } .measurement-section-title { align-items: flex-start; flex-wrap: wrap; padding: 12px; } .measurement-section-summary { width: 100%; flex-basis: calc(100% - 34px); flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; white-space: normal; } .measurement-section-title > svg { margin-left: auto; } .measurement-total { flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; } .measurement-total strong { width: 100%; } }
@media (max-width: 720px) { .weight-evidence__heading { flex-direction: column; } .weight-evidence__heading dl { text-align: left; } }
@media (max-width: 720px) { .fee-overview { grid-template-columns: 1fr; } .fee-overview > div + div { border-top: 1px solid var(--cc-border); border-left: 0; } }
@media (max-width: 720px) { .fee-overview { grid-template-columns: 1fr; } .fee-overview > div + div { border-top: 1px solid var(--workbench-border); border-left: 0; } }
@media (max-width: 720px) { .measurement-section-title { align-items: stretch; flex-direction: column; } .measurement-section-toggle { align-items: flex-start; flex-wrap: wrap; } .measurement-section-toggle > svg { margin-left: auto; } .measurement-section-mode { width: 100%; flex-basis: auto; } }
@media (max-width: 420px) { .weight-evidence { padding: 10px; } .weight-evidence-grid { grid-template-columns: 1fr; } }
</style>