diff --git a/src/core/navigation/adminNavigation.test.ts b/src/core/navigation/adminNavigation.test.ts
index db1a7e2d..84779344 100644
--- a/src/core/navigation/adminNavigation.test.ts
+++ b/src/core/navigation/adminNavigation.test.ts
@@ -36,11 +36,11 @@ describe('adminNavigation', () => {
{ title: '客户服务', children: ['异常订单'] },
{ title: '财务管理', children: ['客户账户', '客户应收应付', '应收款统计表', '客户对账单', '结算单应收', '结算单应付'] },
{ title: '海外仓', children: ['经营总览', '销量分析', '库存分析', '数据采集'] },
- { title: '通用', children: ['费用项目', '汇率管理', '仓库管理', '海外仓报价', '车队报价', '船期价格表', '下载中心'] },
+ { title: '通用', children: ['费用项目', '汇率管理', '仓库管理', '海外仓报价', '车队报价', '船期价格表', '下载中心', '供应商列表'] },
{ title: '系统管理', children: ['用户管理', '角色权限', '菜单权限'] },
])
expect(visible).toHaveLength(13)
- expect(visible.flatMap(node => node.children ?? [])).toHaveLength(56)
+ expect(visible.flatMap(node => node.children ?? [])).toHaveLength(57)
expect(adminNavigation.find((item) => item.key === 'notifications')).toBeUndefined()
})
@@ -196,12 +196,13 @@ describe('adminNavigation', () => {
expect(getNavigationLeaves(filtered).map((item) => item.path)).toEqual(['/dashboard', '/master-data/fee-items'])
})
- it('retains the supplier route permission while omitting its entry from the latest sidebar', () => {
+ it('shows the restored supplier entry under general only with its list permission', () => {
const visible = filterAdminNavigation(adminNavigation, new Set(['platform:supplier:list']))
const missing = filterAdminNavigation(adminNavigation, new Set())
expect(getNavigationLeaves(adminNavigation).find(item => item.path === '/suppliers')?.permission).toBe('platform:supplier:list')
- expect(getNavigationLeaves(visible).map((item) => item.path)).toEqual(['/dashboard'])
+ expect(getNavigationLeaves(visible).map((item) => item.path)).toEqual(['/dashboard', '/suppliers'])
+ expect(visible.find(item => item.title === '通用')?.children?.map(item => item.path)).toEqual(['/suppliers'])
expect(getNavigationLeaves(missing).map((item) => item.path)).toEqual(['/dashboard'])
})
diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts
index 65b13116..c610ee5b 100644
--- a/src/core/navigation/adminNavigation.ts
+++ b/src/core/navigation/adminNavigation.ts
@@ -204,7 +204,7 @@ export const adminNavigation: AdminNavigationNode[] = [
{ key: 'fleet-quotes', title: '车队报价', icon: Truck, path: '/overseas-master-data/fleet-quotes', permission: 'platform:fleet:quote:list' },
{ key: 'fba-carrier-rates', title: '船期价格表', icon: CircleDollarSign, path: '/first-leg-pricing/carrier-rates', permission: 'firstleg:fba-pricing:rate:view' },
{ key: 'download-center', title: '下载中心', icon: Download, path: '/downloads', permission: 'platform:download:list' },
- { key: 'supplier-list', title: '供应商列表', icon: ContactRound, path: '/suppliers', permission: 'platform:supplier:list', hidden: true },
+ { key: 'supplier-list', title: '供应商列表', icon: ContactRound, path: '/suppliers', permission: 'platform:supplier:list' },
{ key: 'express-companies', title: '快递公司', icon: Truck, path: '/master-data/express-companies', permission: 'platform:express:list', hidden: true },
{ key: 'counterparties', title: '往来单位', icon: ContactRound, path: '/master-data/counterparties', permission: 'platform:counterparty:list', hidden: true },
{ key: 'mojia-sku-sync', title: '称重机器', icon: Database, path: '/general/mojia-sku-sync', permission: 'platform:mojia-sku-sync:list', hidden: true },
diff --git a/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md b/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md
index a5b7f32a..7abee3fc 100644
--- a/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md
+++ b/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md
@@ -89,6 +89,19 @@
以上仅为代码接入依据,真实界面验收仍待执行。
+## 2026-10-19 出车记录统计与列表优化
+
+依据:当前对话图1(现状)、图2(原型);用户确认优先在前端优化。沿当前实际发车口径与接口,不恢复原型手填发车事实。
+
+| 页面 | 操作 | 原型承载/布局 | 当前实现 | 本轮结论 |
+|---|---|---|---|---|
+| 出车记录 | 统计与查询 | 图2上方独立四栏饼图,下方记录与筛选 | 图1统计位于列表筛选和表格之间,条形明细内部滚动 | 独立四栏饼图及彩色图例;用户追加确认只改饼图、保留现有日期/目的地统计分组;列表一套日期/关键词/货源/状态查询,同一响应更新统计与记录;币种切换独立展示,顶部均价明确为最近统计日值,不相加或简单平均每日均价;长图例与完整统计按需展开 |
+| 出车记录 | 列表、导出 | 图2列表独立卡片,操作第一列,导出在右上 | integrated列表、字段设置、XLSX下载任务 | 保留公共连续列表,整理默认列;完整字段仍可通过字段设置显示;导出沿原筛选及权限 |
+| 出车记录 | 详情、实测编辑、凭证预览 | 图2仅能确认列表编辑入口;当前操作以既有确认矩阵为准 | 详情独立路由;实测820px居中及按需凭证预览 | 不改变承载、读写权限或请求方式 |
+| 出车记录 | 新增、审核、删除 | 图2有新增记录,未定义审核/删除 | 当前列表无新增/审核/删除 | 不新增业务动作,实际发车事实仍由既有作业链产生 |
+
+本轮只调整前端;自动检查与人工视觉验收分别记录,修改后1920×1080、100%截图待用户确认。
+
## 新版报价与出车记录接入
| 入口 | 原型依据 | 当前承载 | 核对边界 |
diff --git a/src/modules/overseas-master-data/components/TripOperationsCharts.test.ts b/src/modules/overseas-master-data/components/TripOperationsCharts.test.ts
new file mode 100644
index 00000000..3c2ee71b
--- /dev/null
+++ b/src/modules/overseas-master-data/components/TripOperationsCharts.test.ts
@@ -0,0 +1,83 @@
+import { afterEach, expect, it } from 'vitest'
+import { mount, type VueWrapper } from '@vue/test-utils'
+import TripOperationsCharts from './TripOperationsCharts.vue'
+import type { TripOperationsCharts as Charts, TripOperationsSummary } from '../api/tripOperations'
+
+const wrappers: VueWrapper[] = []
+const emptyCharts = (): Charts => ({ physicalPallets: [], destinationTrips: [], externalAgreedAmounts: [], externalPalletRates: [] })
+const summary = (): TripOperationsSummary => ({ trips: 20, inTransit: 5, completed: 15, physicalPallets: 90, palletSpaces: 100, ownPackages: 50, externalPackages: 20, externalAgreedAmounts: [{ currency: 'CNY', amount: 390 }, { currency: 'USD', amount: 100 }], costs: { visible: false, unrecordedTrips: null, unallocatedTrips: null, totals: [] } })
+function render(charts: Charts, totals = summary()) {
+ const wrapper = mount(TripOperationsCharts, { props: { charts, summary: totals }, global: { stubs: { ElButton: { template: '' } } } })
+ wrappers.push(wrapper)
+ return wrapper
+}
+afterEach(() => wrappers.splice(0).forEach(wrapper => wrapper.unmount()))
+
+it('renders all four statistics as pies with matching legends and server totals', () => {
+ const charts = emptyCharts()
+ charts.physicalPallets = [{ label: '每日实运托盘', unit: '托', points: [{ label: '2026-10-02', value: 10 }] }]
+ charts.destinationTrips = [{ label: '目的地涉及车次', unit: '次', points: [{ label: 'ONT8', value: 3 }, { label: 'ABE2', value: 8 }] }]
+ charts.externalAgreedAmounts = [{ label: 'CNY', unit: 'CNY', points: [{ label: '2026-10-02', value: 390 }] }]
+ charts.externalPalletRates = [{ label: 'CNY', unit: 'CNY/标准托位', points: [{ label: '2026-10-07', value: 30 }, { label: '2026-10-02', value: 90 }] }]
+ const original = structuredClone(charts)
+ const wrapper = render(charts)
+ expect(wrapper.findAll('.pie-chart')).toHaveLength(4)
+ expect(wrapper.find('polyline').exists()).toBe(false)
+ expect(wrapper.find('.bar-track').exists()).toBe(false)
+ expect(wrapper.find('.chart--pallets .chart-metric').text()).toBe('90 托')
+ expect(wrapper.find('.chart--rates .chart-metric').text()).toContain('CNY/标准托位 30.00')
+ expect(wrapper.find('.chart--rates .series-caption').text()).toContain('2026-10-07')
+ expect(wrapper.find('.chart--rates').text()).toContain('顶部为最近统计日均价')
+ expect(wrapper.findAll('.chart--rates .pie-slice').map(slice => slice.attributes('aria-label'))).toEqual(['2026-10-02:90.00 CNY/标准托位', '2026-10-07:30.00 CNY/标准托位'])
+ expect(wrapper.find('.chart--destinations .chart-legend .legend-dot').attributes('style')).toContain('rgb(59, 130, 246)')
+ expect(wrapper.find('.chart--destinations .pie-slice').attributes('fill')).toBe('#3b82f6')
+ expect(charts).toEqual(original)
+})
+
+it('switches currency without combining monetary amounts or adding daily rates', async () => {
+ const charts = emptyCharts()
+ charts.externalAgreedAmounts = [{ label: 'CNY', unit: 'CNY', points: [{ label: '2026-10-02', value: 390 }] }, { label: 'USD', unit: 'USD', points: [{ label: '2026-10-03', value: 100 }] }]
+ const wrapper = render(charts)
+ expect(wrapper.find('.chart--amounts .chart-metric').text()).toBe('CNY 390.00')
+ expect(wrapper.find('.chart--amounts').findAll('svg')).toHaveLength(1)
+ const usd = wrapper.findAll('.chart--amounts .series-selector button').find(button => button.text() === 'USD')!
+ await usd.trigger('click')
+ expect(usd.attributes('aria-pressed')).toBe('true')
+ expect(wrapper.find('.chart--amounts .chart-metric').text()).toBe('USD 100.00')
+ expect(wrapper.find('.chart--amounts .pie-slice').attributes('aria-label')).toBe('2026-10-03:100.00 USD')
+ await wrapper.setProps({ charts: { ...charts, externalAgreedAmounts: [charts.externalAgreedAmounts[0]!] } })
+ expect(wrapper.find('.chart--amounts .chart-metric').text()).toBe('CNY 390.00')
+})
+
+it('includes every destination in its pie while collapsing long legends and preserving complete details', () => {
+ const charts = emptyCharts()
+ charts.destinationTrips = [{ label: '目的地涉及车次', unit: '次', points: Array.from({ length: 7 }, (_, index) => ({ label: `FBA-${index}`, value: index + 1 })) }]
+ const wrapper = render(charts)
+ const destination = wrapper.find('.chart--destinations')
+ expect(destination.find('.chart-metric').text()).toBe('7 个目的地')
+ expect(destination.findAll('.pie-slice')).toHaveLength(7)
+ expect(destination.find('.legend-details').attributes('open')).toBeUndefined()
+ expect(destination.find('.legend-details summary').text()).toBe('展开其余 2 项图例')
+ expect(wrapper.find('[aria-label="派送目的地明细"]').findAll('tbody tr')).toHaveLength(7)
+ expect(wrapper.find('[aria-label="派送目的地明细"]').text()).toContain('FBA-0')
+})
+
+it('draws a single positive value as a full pie and retains known zero values without invalid geometry', () => {
+ const charts = emptyCharts()
+ charts.physicalPallets = [{ label: '托盘', unit: '托', points: [{ label: '2026-10-01', value: 0 }, { label: '2026-10-02', value: 2 }] }]
+ charts.externalPalletRates = [{ label: 'USD', unit: 'USD/标准托位', points: [{ label: '2026-10-01', value: 0 }] }]
+ const wrapper = render(charts)
+ expect(wrapper.find('.chart--pallets .pie-slice').attributes('d')).toBe('M 80 8 A 72 72 0 1 1 80 152 A 72 72 0 1 1 80 8 Z')
+ expect(wrapper.find('.chart--rates .pie-empty').exists()).toBe(true)
+ expect(wrapper.find('.chart--rates .chart-metric').text()).toContain('0.00')
+ expect(wrapper.find('.chart--pallets .chart-legend').text()).toContain('2026-10-01')
+ expect(wrapper.html()).not.toMatch(/NaN|Infinity/)
+})
+
+it('shows missing amounts and rates as unknown and omits empty detail controls', () => {
+ const wrapper = render(emptyCharts(), { ...summary(), physicalPallets: 0, externalAgreedAmounts: [] })
+ expect(wrapper.findAll('.chart-empty')).toHaveLength(4)
+ expect(wrapper.find('.chart--amounts .chart-metric').text()).toBe('—')
+ expect(wrapper.find('.chart--rates .chart-metric').text()).toBe('—')
+ expect(wrapper.find('details').exists()).toBe(false)
+})
diff --git a/src/modules/overseas-master-data/components/TripOperationsCharts.vue b/src/modules/overseas-master-data/components/TripOperationsCharts.vue
index 4f1d0a58..0eb3cc98 100644
--- a/src/modules/overseas-master-data/components/TripOperationsCharts.vue
+++ b/src/modules/overseas-master-data/components/TripOperationsCharts.vue
@@ -1,28 +1,137 @@
-
- {{ section.title }}{{ section.note }}
- 暂无对应统计
- {{ series.label }}({{ series.unit }})
-
{{ point.label }}{{ point.value }}
-
-
+
+
+
+
+
+ {{ section.title }}
+
+ {{ number(summary.physicalPallets) }} 托
+ {{ number(destinationCount) }} 个目的地
+
+ {{ section.active.label }} {{ number(amount(section.active.label)!.amount, true) }}
+ —
+
+
+ {{ section.active.unit }} {{ number(section.active.points[section.active.points.length - 1]!.value, true) }}
+ —
+
+
+ {{ section.note }}
+
+ {{ series.label }}
+
+
+ {{ seriesTitle(section.active) }} · {{ section.active.points[section.active.points.length - 1]!.label }}
+
+
+ - {{ point.label }}{{ number(point.value, section.money) }}
+
+
+ 展开其余 {{ section.active.points.length - 5 }} 项图例
+
+ - {{ point.label }}{{ number(point.value, section.money) }}
+
+
+
+ 暂无对应统计
+
+
+
+ 查看完整统计明细
+
+
+ {{ section.title }}
+ 各日均价按确认计价托位加权,排除整单报价;不将每日均价相加或简单平均为期间均价。
+ 暂无对应统计
+
+ {{ seriesTitle(series) }}
+ | {{ section.key === 'destinations' ? '目的地' : '日期(UTC)' }} | 数值 |
+ | {{ point.label }} | {{ number(point.value, section.money) }} |
+
+
+
+
+
+
+
diff --git a/src/modules/overseas-master-data/pages/FleetReportingViews.test.ts b/src/modules/overseas-master-data/pages/FleetReportingViews.test.ts
index b04bbf60..79378948 100644
--- a/src/modules/overseas-master-data/pages/FleetReportingViews.test.ts
+++ b/src/modules/overseas-master-data/pages/FleetReportingViews.test.ts
@@ -2,6 +2,8 @@ import { afterEach, expect, it, vi } from 'vitest'
import { shallowMount, flushPromises, type VueWrapper } from '@vue/test-utils'
import FleetQuotesView from './FleetQuotesView.vue'
import TripOperationsView from './TripOperationsView.vue'
+import QueryToolbar from '@/shared/components/QueryToolbar.vue'
+import TripOperationsCharts from '../components/TripOperationsCharts.vue'
import * as quotes from '../api/fleetQuotes'
import * as reports from '../api/tripOperations'
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (p: string) => p.endsWith(':list') }) }))
@@ -9,10 +11,10 @@ vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
vi.mock('../api/fleetQuotes', () => ({ listFleetQuotes: vi.fn(), setFleetQuoteStatus: vi.fn(), deleteFleetQuote: vi.fn(), exportFleetQuotes: vi.fn(), saveFleetCsv: vi.fn() }))
vi.mock('../api/tripOperations', () => ({ listTripOperations: vi.fn(), exportTripOperations: vi.fn() }))
const wrappers: VueWrapper[] = []
-const layout = { template: '
' }
+const layout = { template: '
' }
function mount(component: object) { const wrapper = shallowMount(component, { global: { stubs: { AdminPage: layout, ListPageLayout: layout, QueryToolbar: true,
DataTable: { name: 'DataTable', props: ['data', 'columns'], template: '
' }, ElButton: { template: '' },
- LoadErrorState: true, FleetQuoteEditor: true, FleetQuoteImport: true, ApiErrorMeta: true } } }); wrappers.push(wrapper); return wrapper }
+ ElTooltip: true, LoadErrorState: true, FleetQuoteEditor: true, FleetQuoteImport: true, ApiErrorMeta: true } } }); wrappers.push(wrapper); return wrapper }
afterEach(() => { wrappers.splice(0).forEach(w => w.unmount()); vi.clearAllMocks() })
it('quotes read-only view fetches a page once and hides all write/export tools', async () => {
vi.mocked(quotes.listFleetQuotes).mockResolvedValue({ records: [], total: 0, page: 1, size: 20, pages: 0 })
@@ -24,4 +26,30 @@ it('operations uses server totals rather than displayed page size', async () =>
vi.mocked(reports.listTripOperations).mockResolvedValue({ page: { records: [], total: 21, page: 1, size: 20, pages: 2 }, summary: { trips: 21, inTransit: 12, completed: 9, physicalPallets: 40, palletSpaces: 40, ownPackages: 70, externalPackages: 10, externalAgreedAmounts: [], costs: { visible: false, unrecordedTrips: null, unallocatedTrips: null, totals: [] } }, charts: { physicalPallets: [], destinationTrips: [], externalAgreedAmounts: [], externalPalletRates: [] } })
const wrapper = mount(TripOperationsView); await flushPromises()
expect(reports.listTripOperations).toHaveBeenCalledTimes(1); expect(wrapper.text()).toContain('实际出车 21 次'); expect(wrapper.text()).not.toContain('导出CSV')
+ expect(wrapper.findComponent(TripOperationsCharts).props('summary').physicalPallets).toBe(40)
+ expect(wrapper.find('.operations-overview').element.compareDocumentPosition(wrapper.findComponent({ name: 'DataTable' }).element) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
+ expect(wrapper.findComponent({ name: 'DataTable' }).props('columns').find((column: { key: string }) => column.key === 'finance')).toMatchObject({ defaultVisible: false })
+})
+it('operations queries charts and records together and resets with only one request', async () => {
+ vi.mocked(reports.listTripOperations).mockResolvedValue({ page: { records: [], total: 0, page: 1, size: 20, pages: 0 }, summary: { trips: 0, inTransit: 0, completed: 0, physicalPallets: 0, palletSpaces: 0, ownPackages: 0, externalPackages: 0, externalAgreedAmounts: [], costs: { visible: false, unrecordedTrips: null, unallocatedTrips: null, totals: [] } }, charts: { physicalPallets: [], destinationTrips: [], externalAgreedAmounts: [], externalPalletRates: [] } })
+ const wrapper = mount(TripOperationsView); await flushPromises()
+ const initial = vi.mocked(reports.listTripOperations).mock.calls[0]![0]
+ const toolbar = wrapper.findComponent(QueryToolbar)
+ toolbar.vm.$emit('update:modelValue', { from: '2026-10-01', to: '2026-10-07', keyword: 'TRIP', sourceType: 'EXTERNAL', status: 'IN_TRANSIT' })
+ toolbar.vm.$emit('search'); await flushPromises()
+ expect(reports.listTripOperations).toHaveBeenLastCalledWith(expect.objectContaining({ from: '2026-10-01T00:00:00Z', to: '2026-10-08T00:00:00.000Z', keyword: 'TRIP', sourceType: 'EXTERNAL', status: 'IN_TRANSIT' }))
+ expect(wrapper.text()).toContain('2026-10-01 至 2026-10-07(UTC)')
+ toolbar.vm.$emit('update:modelValue', { from: '2026-09-01', to: '2026-09-07' }); await flushPromises()
+ expect(wrapper.text()).toContain('2026-10-01 至 2026-10-07(UTC)')
+ toolbar.vm.$emit('reset'); toolbar.vm.$emit('search'); await flushPromises()
+ expect(reports.listTripOperations).toHaveBeenCalledTimes(3)
+ expect(reports.listTripOperations).toHaveBeenLastCalledWith(initial)
+ expect(toolbar.props('modelValue')).toMatchObject({ keyword: '', sourceType: '', status: '' })
+})
+it('operations shows query failures without leaving stale statistics', async () => {
+ vi.mocked(reports.listTripOperations).mockRejectedValue(new Error('统计暂不可用'))
+ const wrapper = mount(TripOperationsView); await flushPromises()
+ expect(wrapper.findComponent(TripOperationsCharts).exists()).toBe(false)
+ expect(wrapper.findComponent({ name: 'LoadErrorState' }).attributes('message')).toBe('运营记录加载失败')
+ expect(wrapper.text()).not.toContain('实际出车 0 次')
})
diff --git a/src/modules/overseas-master-data/pages/TripOperationsView.vue b/src/modules/overseas-master-data/pages/TripOperationsView.vue
index 918fa278..ede8c2e7 100644
--- a/src/modules/overseas-master-data/pages/TripOperationsView.vue
+++ b/src/modules/overseas-master-data/pages/TripOperationsView.vue
@@ -20,48 +20,79 @@ const router = useRouter(), access = useAccessPreviewStore()
const result = ref(null), page = ref(1), size = ref(20), loading = ref(false), exporting = ref(false), error = ref(null)
const recordOpen = ref(false), selected = ref(null)
const today = new Date().toISOString().slice(0, 10), monthAgo = new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10)
-const filters = ref({ from: monthAgo, to: today, keyword: '', sourceType: '', status: '' })
+const defaultFilters = () => ({ from: monthAgo, to: today, keyword: '', sourceType: '', status: '' })
+const filters = ref(defaultFilters())
+const displayedRange = ref('')
let applied: TripOperationsQuery = range(), sequence = 0
const fields: QueryField[] = [
- { key: 'from', label: '实际发车起日(UTC)', type: 'date', width: 185 }, { key: 'to', label: '实际发车止日(UTC)', type: 'date', width: 185 },
- { key: 'keyword', label: '关键词', type: 'input', width: 235, placeholder: '车次 / 发车车牌 / 司机 / 目的仓' },
- { key: 'sourceType', label: '货源', type: 'select', width: 145, options: [{ value: 'OWN', label: '含自有业务' }, { value: 'EXTERNAL', label: '含外接业务' }] },
- { key: 'status', label: '状态', type: 'select', width: 130, options: [{ value: 'IN_TRANSIT', label: '在途' }, { value: 'COMPLETED', label: '货物已结案' }] },
+ { key: 'from', label: '实际发车起日(UTC)', placeholder: '发车起日(UTC)', type: 'date', width: 150, props: { 'aria-label': '实际发车起日(UTC)' } },
+ { key: 'to', label: '实际发车止日(UTC)', placeholder: '发车止日(UTC)', type: 'date', width: 150, props: { 'aria-label': '实际发车止日(UTC)' } },
+ { key: 'keyword', label: '关键词', type: 'input', width: 270, placeholder: '车次 / 车牌 / 司机 / 目的仓', props: { 'aria-label': '车次、车牌、司机或目的仓' } },
+ { key: 'sourceType', label: '货源', placeholder: '全部货源', type: 'select', width: 140, props: { 'aria-label': '货源' }, options: [{ value: 'OWN', label: '含自有业务' }, { value: 'EXTERNAL', label: '含外接业务' }] },
+ { key: 'status', label: '状态', placeholder: '全部状态', type: 'select', width: 140, props: { 'aria-label': '状态' }, options: [{ value: 'IN_TRANSIT', label: '在途' }, { value: 'COMPLETED', label: '货物已结案' }] },
]
-const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 185, fixed: 'left', slotName: 'actions' }, { key: 'tripNo', label: '车次号', minWidth: 220 },
+const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 140, fixed: 'left', slotName: 'actions' }, { key: 'tripNo', label: '车次号', minWidth: 220 },
{ key: 'status', label: '状态', width: 125, slotName: 'status' }, { key: 'vehiclePlate', label: '发车车牌', minWidth: 125 }, { key: 'driverName', label: '发车司机', minWidth: 100 },
{ key: 'warehouseName', label: '起运仓', minWidth: 150 }, { key: 'destinations', label: '目的地', minWidth: 180, slotName: 'destinations' },
- { key: 'departedAt', label: '实际发车(UTC)', minWidth: 190 }, { key: 'completedAt', label: '货物结案(UTC)', minWidth: 190 },
- { key: 'physicalPallets', label: '实物托盘', width: 100 }, { key: 'palletSpaces', label: '标准托位', width: 100 }, { key: 'ownPackages', label: '自有箱数', width: 100 }, { key: 'externalPackages', label: '外接箱数', width: 100 },
- { key: 'agreed', label: '外接约定金额', minWidth: 170, slotName: 'agreed' }, { key: 'distance', label: '实际里程(mile)', width: 140, slotName: 'distance' }, { key: 'fuel', label: '实际油耗(L)', width: 130, slotName: 'fuel' }, { key: 'finance', label: '应收 / 实收', minWidth: 260, slotName: 'finance' }, { key: 'cost', label: '已登记直接成本', minWidth: 200, slotName: 'cost' }, { key: 'allocated', label: '自有 / 外接分摊', minWidth: 240, slotName: 'allocated' }]
+ { key: 'departedAt', label: '实际发车(UTC)', minWidth: 190 }, { key: 'physicalPallets', label: '实物托盘', width: 100 },
+ { key: 'agreed', label: '外接约定金额', minWidth: 170, slotName: 'agreed' },
+ { key: 'completedAt', label: '货物结案(UTC)', minWidth: 190, defaultVisible: false },
+ { key: 'palletSpaces', label: '标准托位', width: 100, defaultVisible: false }, { key: 'ownPackages', label: '自有箱数', width: 100, defaultVisible: false }, { key: 'externalPackages', label: '外接箱数', width: 100, defaultVisible: false },
+ { key: 'distance', label: '实际里程(mile)', width: 140, slotName: 'distance', defaultVisible: false }, { key: 'fuel', label: '实际油耗(L)', width: 130, slotName: 'fuel', defaultVisible: false },
+ { key: 'finance', label: '应收 / 实收', minWidth: 260, slotName: 'finance', defaultVisible: false }, { key: 'cost', label: '已登记直接成本', minWidth: 200, slotName: 'cost', defaultVisible: false }, { key: 'allocated', label: '自有 / 外接分摊', minWidth: 240, slotName: 'allocated', defaultVisible: false }]
function range(): TripOperationsQuery {
const from = String(filters.value.from || monthAgo), to = String(filters.value.to || today)
if (!/^\d{4}-\d{2}-\d{2}$/.test(from) || !/^\d{4}-\d{2}-\d{2}$/.test(to) || Number.isNaN(Date.parse(from)) || Number.isNaN(Date.parse(to))) throw new Error('请输入有效日期 YYYY-MM-DD')
return { from: `${from}T00:00:00Z`, to: new Date(Date.parse(`${to}T00:00:00Z`) + 86400000).toISOString(), keyword: String(filters.value.keyword || '') || undefined, sourceType: (filters.value.sourceType || undefined) as TripOperationsQuery['sourceType'], status: String(filters.value.status || '') || undefined }
}
-async function load() { const current = ++sequence; loading.value = true; error.value = null; try { const data = await listTripOperations({ ...applied, page: page.value, size: size.value }); if (current === sequence) result.value = data } catch (e) { if (current === sequence) { result.value = null; error.value = getApiErrorDetails(e, '运营记录加载失败') } } finally { if (current === sequence) loading.value = false } }
+async function load() {
+ const current = ++sequence, query = { ...applied, page: page.value, size: size.value }
+ loading.value = true; error.value = null
+ try {
+ const data = await listTripOperations(query)
+ if (current === sequence) {
+ result.value = data
+ displayedRange.value = `${query.from.slice(0, 10)} 至 ${new Date(Date.parse(query.to) - 86400000).toISOString().slice(0, 10)}(UTC)`
+ }
+ } catch (e) {
+ if (current === sequence) { result.value = null; error.value = getApiErrorDetails(e, '运营记录加载失败') }
+ } finally { if (current === sequence) loading.value = false }
+}
function search() { try { applied = range(); if (page.value !== 1) page.value = 1; else void load() } catch (e) { error.value = getApiErrorDetails(e, '日期范围无效') } }
+function resetFilters() { filters.value = defaultFilters() }
function openRecord(tripNo: unknown) { const row = result.value?.page.records.find(item => item.tripNo === tripNo); if (row) { selected.value = row; recordOpen.value = true } }
async function download() { exporting.value = true; try { await submitExport('TRIP_OPERATIONS_LIST', '出车记录', applied) } catch (e) { error.value = getApiErrorDetails(e, '导出失败') } finally { exporting.value = false } }
const money = (rows: TripOperationsMoney[]) => rows.length ? rows.map(r => `${r.currency} ${r.amount.toFixed(2)}`).join(' / ') : '—'
watch([page, size], load); onMounted(load); onBeforeUnmount(() => { sequence++ })
+
+
+
+ 出车数据统计
+ {{ loading ? '正在更新统计与记录…' : displayedRange }} · 与下方列表筛选一致
+
+
+
+
+ 已登记直接成本:{{ result.summary.costs.totals.length ? result.summary.costs.totals.map(c => `${c.currency} ${c.directCost.toFixed(2)}`).join(' / ') : '—' }};未登记 {{ result.summary.costs.unrecordedTrips ?? '—' }} 趟,已登记但未能分摊 {{ result.summary.costs.unallocatedTrips ?? '—' }} 趟。
+
+ {{ loading ? '正在加载出车统计…' : '暂无统计数据' }}
+
+
+
+
+
出车记录
-
-
-
-
-
-
- 实际出车 {{ result.summary.trips }} 次在途 {{ result.summary.inTransit }} 次货物结案 {{ result.summary.completed }} 次
- 实物托盘 {{ result.summary.physicalPallets }} 托外接约定 {{ money(result.summary.externalAgreedAmounts) }}
-
-
已登记直接成本:{{ result.summary.costs.totals.length ? result.summary.costs.totals.map(c => `${c.currency} ${c.directCost.toFixed(2)}`).join(' / ') : '—' }};未登记 {{ result.summary.costs.unrecordedTrips }} 趟,已登记但未能分摊 {{ result.summary.costs.unallocatedTrips }} 趟。
-
按实际发车时间查询,每趟车只计一次;货源筛选展示含该类货物的整趟车。约定金额不代表应收或实收,结案不代表车辆与司机已释放。
-
-
+
导出
详情实测记录
{{ row.status === 'COMPLETED' ? '货物已结案' : '在途' }}{{ row.destinations.map((d: TripOperationsRow['destinations'][number]) => d.label).join(' / ') }}
@@ -73,7 +104,12 @@ watch([page, size], load); onMounted(load); onBeforeUnmount(() => { sequence++ }
diff --git a/src/styles/adminDensity.test.ts b/src/styles/adminDensity.test.ts
index 7e197daa..ae7026f5 100644
--- a/src/styles/adminDensity.test.ts
+++ b/src/styles/adminDensity.test.ts
@@ -71,6 +71,7 @@ const generalCatalogConsumers = [
'overseas-master-data/components/CapacityInquiryTable.vue',
'overseas-master-data/components/CapacityOfferSettingsDialog.vue',
'overseas-master-data/pages/TripOperationsView.vue',
+ 'overseas-master-data/components/TripOperationsCharts.vue',
'overseas-master-data/pages/TripDetailView.vue',
'overseas-master-data/components/TripPlanDialog.vue',
'overseas-master-data/pages/DriverDetailView.vue',