+
| 箱规类型 | 内径长(cm) | 内径宽(cm) | 内径高(cm) | 单边加宽(cm) | 备注 |
| {{ row.boxTypeName }} | | | | | |
-
箱规按权重系数计算长、宽、高任一项超出当前挡位时进入下一挡;命中规格的一个系数统一应用于长、宽、高。
| 箱规类型 | 内径长(cm) | 内径宽(cm) | 内径高(cm) | 权重系数 | 重量(kg) | 备注 |
|---|
| {{ BOX_NAMES[row.boxType] }} | | | | | | |
|---|
+
箱规按权重系数计算长、宽、高任一项超出当前挡位时进入下一挡;命中规格的一个系数统一应用于长、宽、高。
| 箱规类型 | 内径长(cm) | 内径宽(cm) | 内径高(cm) | 权重系数 | 重量(kg) | 备注 |
|---|
| {{ BOX_NAMES[row.boxType] }} | | | | | | |
|---|
@@ -434,25 +434,25 @@ onBeforeUnmount(stopInitializationPolling)
diff --git a/src/modules/customers/customerDensity.css b/src/modules/customers/customerDensity.css
index 6889581c..89082960 100644
--- a/src/modules/customers/customerDensity.css
+++ b/src/modules/customers/customerDensity.css
@@ -4,12 +4,12 @@
.customer-density-dialog.admin-density-dialog .el-button .lucide-copy { display: none; }
.customer-density.admin-density.customer-detail-page .detail-page-layout { gap: var(--admin-space-5, 12px); }
-.customer-density.admin-density.customer-detail-page .detail-header { padding: var(--admin-space-3, 8px) var(--admin-space-5, 12px); background: var(--admin-surface, #fff); border: 1px solid var(--cc-border); }
+.customer-density.admin-density.customer-detail-page .detail-header { padding: var(--admin-space-3, 8px) var(--admin-space-5, 12px); background: var(--workbench-surface); border: 1px solid var(--workbench-border); }
.customer-density.admin-density .detail-page-toolbar h1 { font-size: var(--admin-font-title, 16px); line-height: 24px; }
.customer-density.admin-density .detail-page-toolbar p { margin-top: 2px; font-size: var(--admin-font-caption, 12px); }
-.customer-density.admin-density .tab-panel { padding-top: 0; min-height: 0; background: var(--admin-surface, #fff); border: 1px solid var(--cc-border); border-top: 0; }
+.customer-density.admin-density .tab-panel { padding-top: 0; min-height: 0; background: var(--workbench-surface); border: 1px solid var(--workbench-border); border-top: 0; }
.customer-density.admin-density .basic-layout { grid-template-columns: 1fr; gap: 0; }
-.customer-density.admin-density .detail-panel { padding: var(--admin-space-5, 12px); border: 0; border-bottom: 1px solid var(--cc-border); border-radius: 0; background: var(--admin-surface, #fff); box-shadow: none; }
+.customer-density.admin-density .detail-panel { padding: var(--admin-space-5, 12px); border: 0; border-bottom: 1px solid var(--workbench-border); border-radius: 0; background: var(--workbench-surface); box-shadow: none; }
.customer-density.admin-density .detail-panel:last-child { border-bottom: 0; }
.customer-density.admin-density .module-panel { min-height: 0; }
.customer-density.admin-density .panel-title { align-items: center; gap: var(--admin-space-5, 12px); margin-bottom: var(--admin-space-3, 8px); padding-bottom: var(--admin-space-3, 8px); }
@@ -17,17 +17,17 @@
.customer-density.admin-density .panel-title p { margin-top: 3px; font-size: var(--admin-font-caption, 12px); }
.customer-density.admin-density .info-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.customer-density.admin-density .info-grid--three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
-.customer-density.admin-density .qualification-list { gap: 0; border: 1px solid var(--cc-border); }
-.customer-density.admin-density .qualification-list button { grid-template-columns: minmax(0, 1fr) 100px 120px 48px; gap: var(--admin-space-5, 12px); min-height: 40px; padding: var(--admin-space-2, 6px) var(--admin-space-4, 10px); border: 0; border-bottom: 1px solid var(--cc-border); border-radius: 0; }
+.customer-density.admin-density .qualification-list { gap: 0; border: 1px solid var(--workbench-border); }
+.customer-density.admin-density .qualification-list button { grid-template-columns: minmax(0, 1fr) 100px 120px 48px; gap: var(--admin-space-5, 12px); min-height: 40px; padding: var(--admin-space-2, 6px) var(--admin-space-4, 10px); border: 0; border-bottom: 1px solid var(--workbench-border); border-radius: 0; }
.customer-density.admin-density .qualification-list button:last-child { border-bottom: 0; }
.customer-density.admin-density .qualification-list strong { font-size: var(--admin-font-body, 13px); }
.customer-density.admin-density .qualification-list small { overflow-wrap: anywhere; font-size: var(--admin-font-caption, 12px); }
.customer-density.admin-density .file-date { font-size: var(--admin-font-caption, 12px); }
-.customer-density.admin-density .file-preview-label { color: var(--cc-primary); font-size: var(--admin-font-body, 13px); }
+.customer-density.admin-density .file-preview-label { color: var(--workbench-primary); font-size: var(--admin-font-body, 13px); }
.customer-density.admin-density .section-empty,.customer-density.admin-density .module-empty { min-height: var(--admin-empty-height, 64px); padding: var(--admin-space-5, 12px); gap: var(--admin-space-1, 4px); }
.customer-density.admin-density .section-empty > svg,.customer-density.admin-density .module-empty > svg { display: none; }
.customer-density.admin-density .section-empty span { font-size: var(--admin-font-caption, 12px); }
-.customer-density.admin-density .user-table-wrap { border: 1px solid var(--cc-border); border-radius: 0; }
+.customer-density.admin-density .user-table-wrap { border: 1px solid var(--workbench-border); border-radius: 0; }
.customer-density.admin-density .user-table-wrap table { font-size: var(--admin-font-body, 13px); }
.customer-density.admin-density .user-table-wrap th,.customer-density.admin-density .user-table-wrap td { height: var(--admin-row-height, 36px); padding: var(--admin-space-2, 6px) var(--admin-space-4, 10px); }
.customer-density.admin-density .user-identity > span { display: none; }
@@ -46,7 +46,6 @@
.customer-density.admin-density .rule-heading,.customer-density.admin-density .rule-card-heading,.customer-density.admin-density .panel-heading { min-height: 0; padding: var(--admin-space-3, 8px) 0; gap: var(--admin-space-5, 12px); }
.customer-density.admin-density .rule-heading h2,.customer-density.admin-density .rule-card-heading h2,.customer-density.admin-density .panel-heading h2 { font-size: var(--admin-font-section, 14px); }
.customer-density.admin-density .rule-heading p,.customer-density.admin-density .panel-heading p { margin-top: 3px; font-size: var(--admin-font-caption, 12px); }
-.customer-density.admin-density .formula-band { min-height: var(--admin-row-height, 36px); padding: var(--admin-space-2, 6px) var(--admin-space-4, 10px); gap: var(--admin-space-2, 6px) var(--admin-space-7, 24px); }
.customer-density.admin-density .rule-state,.customer-density.admin-density .rule-error,.customer-density.admin-density .loading-state,.customer-density.admin-density .fee-loading,.customer-density.admin-density .fee-empty { min-height: var(--admin-empty-height, 64px); padding: var(--admin-space-5, 12px); }
.customer-density.admin-density .rule-state > svg { display: none; }
.customer-density.admin-density .charge-rule-card { margin-top: var(--admin-space-5, 12px); }
diff --git a/src/modules/customers/customerFeeDialogs.test.ts b/src/modules/customers/customerFeeDialogs.test.ts
index d4af79bd..193b138f 100644
--- a/src/modules/customers/customerFeeDialogs.test.ts
+++ b/src/modules/customers/customerFeeDialogs.test.ts
@@ -102,7 +102,7 @@ describe('customer overseas fee dialog entry points', () => {
const confirmation = vi.spyOn(ElMessageBox, 'confirm').mockRejectedValueOnce('cancel')
document.querySelector
('.customer-density-dialog .el-dialog__headerbtn')!.click()
await flushPromises()
- expect(confirmation).toHaveBeenCalledWith(expect.stringContaining('尚未保存'), '放弃编辑 · 费用测试客户', expect.objectContaining({ customClass: 'admin-density-confirm' }))
+ expect(confirmation).toHaveBeenCalledWith(expect.stringContaining('尚未保存'), '放弃编辑 · 费用测试客户', expect.objectContaining({ customClass: 'admin-density-confirm admin-workbench' }))
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true)
expect(price.value).toBe('9.123456')
const done = vi.fn()
diff --git a/src/modules/customers/pages/CustomerDetailView.vue b/src/modules/customers/pages/CustomerDetailView.vue
index 21d8f68a..244302f3 100644
--- a/src/modules/customers/pages/CustomerDetailView.vue
+++ b/src/modules/customers/pages/CustomerDetailView.vue
@@ -1,4 +1,5 @@
-
+
-
+
客户列表
@@ -1083,9 +1084,9 @@ onMounted(() => { void loadCustomers() })
-
+
- 基础资料
+ 基础资料
| |
| |
@@ -1095,7 +1096,7 @@ onMounted(() => { void loadCustomers() })
- 联系与商务仅用于企业业务联系,不会修改企业管理员登录手机号;登录手机号请在成员/账号管理中单独维护,修改会影响登录。
+ 联系与商务仅用于企业业务联系,不会修改企业管理员登录手机号;登录手机号请在成员/账号管理中单独维护,修改会影响登录。
| |
| |
@@ -1104,7 +1105,7 @@ onMounted(() => { void loadCustomers() })
- 企业认证资料营业执照和18位统一社会信用代码为必填业务项;法人身份证、货代资质证书和银行开户许可证均为选填。
支持 PDF、JPG、PNG,单个文件不超过 5MB;* 为提交审核必填项。
{{ statusMeta(editorStatus).label }}
+ 企业认证资料营业执照和18位统一社会信用代码为必填业务项;法人身份证、货代资质证书和银行开户许可证均为选填。
支持 PDF、JPG、PNG,单个文件不超过 5MB;* 为提交审核必填项。
{{ statusMeta(editorStatus).label }}
| {{ form.unifiedSocialCreditCode }}{{ creditCodeEstablished ? '已登记的统一社会信用代码不可直接修改。' : '必须与营业执照一致,并随营业执照共同保存;法人身份证为选填。' }} |
{{ qualificationLoadError }}重试
@@ -1128,7 +1129,7 @@ onMounted(() => { void loadCustomers() })
-
+
{{ qualificationPreviewName }}
![]()
@@ -1136,10 +1137,10 @@ onMounted(() => { void loadCustomers() })
-
+
请立即保存以下一次性凭证临时密码只在本次{{ credentialSource === 'creation' ? '创建' : '审核' }}响应中返回,关闭后无法再次查看。
-
+
| 登录账号 | {{ approvalResult.administratorUsername || '未返回' }}
|
| 临时密码 | {{ approvalResult.temporaryPassword || '未返回' }}
|
@@ -1147,9 +1148,9 @@ onMounted(() => { void loadCustomers() })
我已保存,关闭
-
+
-
+
企业基本信息 待审核
| 客户名称 | {{ selectedReview.tenantName || '未填写' }} | 客户编号 | {{ selectedReview.tenantCode || '未生成' }} |
@@ -1157,7 +1158,7 @@ onMounted(() => { void loadCustomers() })
| 联系邮箱 | {{ selectedReview.contactEmail || '未填写' }} | 提交时间 | {{ formatDate(selectedReview.submittedAt || selectedReview.createdAt) }} |
-
+
资质文件
营业执照为必填;法人身份证及其他资质为选填。共 {{ reviewQualifications.length }} 份
正在加载资质文件...
@@ -1179,12 +1180,12 @@ onMounted(() => { void loadCustomers() })
-
+
{{ reviewPreviewName }}
-
+
正在重置企业管理员登录密码{{ passwordTenant.tenantName }}
客户编号:{{ passwordTenant.tenantCode }}
随机生成
@@ -1195,14 +1196,14 @@ onMounted(() => { void loadCustomers() })
-
+
{{ impersonationTenant.tenantName }}将以企业管理员身份打开客户工作台
客户编号:{{ impersonationTenant.tenantCode }}
本操作会生成一次性、短时有效的客户会话,并记录代登录原因;不会替换当前 Admin 会话。
-
+
删除是逻辑归档:历史业务数据会保留,客户账号会被停用。是否允许归档由服务端最终判断。
{{ archiveTenant.tenantName }}请输入客户编号 {{ archiveTenant.tenantCode }} 以确认。
@@ -1211,25 +1212,24 @@ onMounted(() => { void loadCustomers() })
diff --git a/src/modules/customs/pages/CustomsCasesView.test.ts b/src/modules/customs/pages/CustomsCasesView.test.ts
index 6594b8c7..eb44db69 100644
--- a/src/modules/customs/pages/CustomsCasesView.test.ts
+++ b/src/modules/customs/pages/CustomsCasesView.test.ts
@@ -42,9 +42,9 @@ describe('CustomsCasesView', () => {
expect(source).not.toContain("label: '入库单标识'")
})
it('keeps the detail dialog responsive with internal scrolling', () => {
- expect(source).toContain('width:min(900px,calc(100vw - 28px))!important')
- expect(source).toContain('max-height:90vh')
- expect(source).toContain('overflow-y:auto')
+ expect(source).toContain('width="min(900px, calc(100vw - 28px))"')
+ expect(source).toContain('admin-density-dialog admin-workbench is-dialog')
+ expect(source).not.toContain(':global(.el-dialog:has(')
})
it('uses a bordered view action instead of a text link', () => {
expect(source).toContain('size="small" type="primary" plain')
diff --git a/src/modules/customs/pages/CustomsCasesView.vue b/src/modules/customs/pages/CustomsCasesView.vue
index 93448f38..ffa4c2aa 100644
--- a/src/modules/customs/pages/CustomsCasesView.vue
+++ b/src/modules/customs/pages/CustomsCasesView.vue
@@ -1,4 +1,6 @@
-
-{{ detail.customsCase.customsNo }}{{ detail.customsCase.checklistCode }} / {{ detail.customsCase.checklistVersionNo }}{{ detail.documents.length }} / {{ detail.receipts.length }}重新加载登记线下提交提交批次
报关代理:{{ batch.customsAgentName || '服务端未返回名称快照' }}状态历史
{{ customsHistoryActionLabel(entry.action) }}:{{ customsStatusLabel(entry.fromStatus) }} → {{ customsStatusLabel(entry.toStatus) }}{{ entry.note || '' }}
-
+
+{{ detail.customsCase.customsNo }}{{ detail.customsCase.checklistCode }} / {{ detail.customsCase.checklistVersionNo }}{{ detail.documents.length }} / {{ detail.receipts.length }}重新加载登记线下提交提交批次
报关代理:{{ batch.customsAgentName || '服务端未返回名称快照' }}状态历史
{{ customsHistoryActionLabel(entry.action) }}:{{ customsStatusLabel(entry.fromStatus) }} → {{ customsStatusLabel(entry.toStatus) }}{{ entry.note || '' }}
+
diff --git a/src/modules/dashboard/operation-matrix.md b/src/modules/dashboard/operation-matrix.md
new file mode 100644
index 00000000..81ee2a6a
--- /dev/null
+++ b/src/modules/dashboard/operation-matrix.md
@@ -0,0 +1,13 @@
+# 首页数据看板逐操作矩阵
+
+依据:2026-10-20 用户确认全 Admin 推广清爽蓝白风格;视觉参考为工作区 `docs/界面参考/蓝白卡片式工作台风格参考.md` 与其中的用户参考图。当前页面入口为 `pages/DashboardView.vue`,路由 `/dashboard`;本轮只调整展示,不改变指标口径、权限和接口。
+
+| 页面 | 操作 | 当前承载与依据 | 本轮结论与验证 |
+| --- | --- | --- | --- |
+| 首页数据看板 | 查看核心指标 | `DashboardView.vue` 的 `metrics` 与核心业务指标区域;四列指标卡,按读取权限显示 | 接入公共工作台卡片、图标、数值和状态样式;正常统计口径收进可聚焦的信息提示,箱型未知仍可见;加载中、未知和真实零保持原义 |
+| 首页数据看板 | 刷新数据 | `loadMetrics`,顶部原地只读刷新按钮 | 保留按钮位置、加载禁用、部分请求失败提示与成功结果;不增加轮询或缓存 |
+| 首页数据看板 | 全屏展示/退出 | `toggleFullscreen` 与 `dashboardRef`,同一页面根进入浏览器全屏 | 保留状态同步和 `aria-pressed`,全屏继续继承公共主题 |
+| 首页数据看板 | 无指标权限 | `hasMetrics` 与现有授权菜单提示 | 不发无权限指标请求;提示保留,不显示假指标或新增业务入口 |
+| 首页数据看板 | 详情、新增、编辑、审核、文件预览、删除 | 当前页面无上述操作 | 不新增操作或浮层 |
+
+自动回归入口:`pages/DashboardView.test.ts`、`api/dashboard.test.ts`。本模块未独立执行检查,统一由本轮总控完成。视觉待人工 `1920×1080`、浏览器缩放 `100%` 截图确认。
diff --git a/src/modules/dashboard/pages/DashboardView.test.ts b/src/modules/dashboard/pages/DashboardView.test.ts
index db73c65c..5de1ed58 100644
--- a/src/modules/dashboard/pages/DashboardView.test.ts
+++ b/src/modules/dashboard/pages/DashboardView.test.ts
@@ -1,4 +1,5 @@
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
+import { ElButton, ElTooltip } from 'element-plus'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import DashboardView from './DashboardView.vue'
import { getTodayNewOrderCount, getActiveCustomerCount, getDashboardOperationalMetrics } from '../api/dashboard'
@@ -20,6 +21,10 @@ vi.mock('../api/dashboard', () => ({
enableAutoUnmount(afterEach)
+function mountDashboard() {
+ return mount(DashboardView, { global: { components: { ElButton, ElTooltip } } })
+}
+
describe('DashboardView', () => {
beforeEach(() => {
vi.clearAllMocks()
@@ -28,7 +33,7 @@ describe('DashboardView', () => {
it('makes no metric requests for a minimal pool sales account and gives an authorized-menu hint', async () => {
access.permissions = new Set(['platform:fleet:capacity-offer:list', 'platform:fleet:capacity-offer:inquiry-create', 'platform:fleet:capacity-offer:inquiry-edit', 'platform:fleet:capacity-offer:inquiry-cancel'])
- const wrapper = mount(DashboardView); await flushPromises()
+ const wrapper = mountDashboard(); await flushPromises()
expect(getTodayNewOrderCount).not.toHaveBeenCalled(); expect(getActiveCustomerCount).not.toHaveBeenCalled(); expect(getDashboardOperationalMetrics).not.toHaveBeenCalled()
expect(wrapper.findAll('.dashboard-metric')).toHaveLength(0)
expect(wrapper.text()).toContain('请从左侧菜单进入已授权业务')
@@ -37,28 +42,30 @@ describe('DashboardView', () => {
it('requests only granted metric sources and requires both container and customs permissions', async () => {
access.permissions = new Set(['platform:customer:list', 'platform:firstleg:container:list'])
- const wrapper = mount(DashboardView); await flushPromises()
+ const wrapper = mountDashboard(); await flushPromises()
expect(getActiveCustomerCount).toHaveBeenCalledOnce(); expect(getTodayNewOrderCount).not.toHaveBeenCalled(); expect(getDashboardOperationalMetrics).not.toHaveBeenCalled()
- expect(wrapper.findAll('.dashboard-metric p').map(node => node.text())).toEqual(['活跃客户'])
+ expect(wrapper.findAll('.workbench-metric-label').map(node => node.text())).toEqual(['活跃客户'])
await wrapper.findAll('button').find(button => button.text().trim() === '刷新数据')!.trigger('click'); await flushPromises()
expect(getActiveCustomerCount).toHaveBeenCalledTimes(2); expect(getDashboardOperationalMetrics).not.toHaveBeenCalled()
})
it('renders only the four live metrics', async () => {
- const wrapper = mount(DashboardView)
+ const wrapper = mountDashboard()
await flushPromises()
expect(wrapper.find('.dashboard-context > p').exists()).toBe(false)
+ expect(wrapper.classes()).toContain('admin-workbench')
expect(wrapper.text()).toContain('数据更新时间:2026-09-11 12:00')
expect(wrapper.findAll('.dashboard-metric')).toHaveLength(4)
- expect(wrapper.findAll('.dashboard-metric p').map(label => label.text())).toEqual(['今日新运单', '在途柜量', '活跃客户', '待清关票数'])
+ expect(wrapper.findAll('.workbench-metric-label').map(label => label.text())).toEqual(['今日新运单', '在途柜量', '活跃客户', '待清关票数'])
expect(wrapper.find('.density-fields').exists()).toBe(false)
- expect(wrapper.findAll('.dashboard-metric-mark')).toHaveLength(4)
+ expect(wrapper.findAll('.dashboard-metric.workbench-metric .workbench-icon')).toHaveLength(4)
+ expect(wrapper.findAll('button[aria-label^="查看"]')).toHaveLength(4)
expect(wrapper.text()).toContain('12 票')
expect(wrapper.text()).toContain('4.25 TEU')
expect(wrapper.text()).toContain('8 家')
expect(wrapper.text()).toContain('7 票')
- expect(wrapper.text()).toContain('1 柜箱型未知')
+ expect(wrapper.get('.dashboard-metric .workbench-badge[data-tone="amber"]').text()).toBe('1 柜箱型未知')
expect(wrapper.text()).not.toContain('近 30 天运单量趋势')
expect(wrapper.text()).not.toContain('运单状态分布')
expect(wrapper.text()).not.toContain('TOP10 航线货量')
@@ -69,7 +76,7 @@ describe('DashboardView', () => {
it('keeps successful metrics when one request fails and refreshes through the visible control', async () => {
vi.mocked(getTodayNewOrderCount).mockRejectedValueOnce(new Error('指标加载失败'))
- const wrapper = mount(DashboardView)
+ const wrapper = mountDashboard()
await flushPromises()
expect(wrapper.text()).toContain('部分核心指标加载失败')
expect(wrapper.get('.dashboard-metric strong').text()).toContain('—')
@@ -82,7 +89,7 @@ describe('DashboardView', () => {
})
it('uses the same visualization root when entering and exiting fullscreen', async () => {
- const wrapper = mount(DashboardView)
+ const wrapper = mountDashboard()
await flushPromises()
let fullscreen: Element | null = null
const previousElement = Object.getOwnPropertyDescriptor(document, 'fullscreenElement')
diff --git a/src/modules/dashboard/pages/DashboardView.vue b/src/modules/dashboard/pages/DashboardView.vue
index 2e63f8a1..9e1483c9 100644
--- a/src/modules/dashboard/pages/DashboardView.vue
+++ b/src/modules/dashboard/pages/DashboardView.vue
@@ -2,12 +2,19 @@
import dayjs from 'dayjs'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import {
+ ClipboardCheck,
Expand,
+ FileText,
Minimize,
RefreshCw,
+ Ship,
+ Users,
} from '@lucide/vue'
import AdminPage from '@/shared/components/AdminPage.vue'
+import InlineHelp from '@/shared/components/InlineHelp.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
+import '@/styles/adminDensity.css'
+import '@/styles/adminWorkbench.css'
import {
getActiveCustomerCount,
getDashboardOperationalMetrics,
@@ -36,17 +43,19 @@ function displayNumber(value: number | null | undefined, maximumFractionDigits =
}
const metrics = computed(() => [
- { label: '今日新运单', value: displayNumber(todayNewOrderCount.value), unit: '票', note: '按今日创建时间统计', tone: 'blue', visible: canReadOrders.value },
+ { label: '今日新运单', value: displayNumber(todayNewOrderCount.value), unit: '票', note: '按今日创建时间统计', warning: '', icon: FileText, tone: 'blue', visible: canReadOrders.value },
{
label: '在途柜量', value: displayNumber(operationalMetrics.value?.inTransitTeu, 2), unit: 'TEU',
note: operationalMetrics.value
? `已开船未到仓 · ${operationalMetrics.value.inTransitContainerCount} 柜${operationalMetrics.value.unknownContainerTypeCount ? ` · ${operationalMetrics.value.unknownContainerTypeCount} 柜箱型未知` : ''}`
: '已开船且尚未到仓',
+ warning: operationalMetrics.value?.unknownContainerTypeCount ? `${operationalMetrics.value.unknownContainerTypeCount} 柜箱型未知` : '',
+ icon: Ship,
tone: 'green',
visible: canReadOperations.value,
},
- { label: '活跃客户', value: displayNumber(activeCustomerCount.value), unit: '家', note: '客户状态为启用', tone: 'purple', visible: canReadCustomers.value },
- { label: '待清关票数', value: displayNumber(operationalMetrics.value?.pendingCustomsOrderCount), unit: '票', note: '已进入清关且尚未完成', tone: 'rose', visible: canReadOperations.value },
+ { label: '活跃客户', value: displayNumber(activeCustomerCount.value), unit: '家', note: '客户状态为启用', warning: '', icon: Users, tone: 'purple', visible: canReadCustomers.value },
+ { label: '待清关票数', value: displayNumber(operationalMetrics.value?.pendingCustomsOrderCount), unit: '票', note: '已进入清关且尚未完成', warning: '', icon: ClipboardCheck, tone: 'rose', visible: canReadOperations.value },
].filter(metric => metric.visible))
async function toggleFullscreen() {
@@ -84,30 +93,33 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
-
-
+
+
- {{ !hasMetrics ? '当前账号暂无可查看的首页指标,请从左侧菜单进入已授权业务。' : '部分核心指标加载失败,可刷新重试。' }}
+ {{ !hasMetrics ? '当前账号暂无可查看的首页指标,请从左侧菜单进入已授权业务。' : '部分核心指标加载失败,可刷新重试。' }}
- 数据更新时间:{{ metricsUpdatedAt ? dayjs(metricsUpdatedAt).format('YYYY-MM-DD HH:mm') : '加载中' }}
-
-
+
-
-
- {{ metric.label }}
- {{ metric.value }} {{ metric.unit }}
- {{ metric.note }}
+
+
+
+
{{ metric.label }}
+
{{ metric.note }}
+
+ {{ metric.value }} {{ metric.unit }}
+ {{ metric.warning }}
@@ -115,24 +127,14 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
diff --git a/src/modules/domestic-containers/pages/DomesticContainerInventoryDetailView.vue b/src/modules/domestic-containers/pages/DomesticContainerInventoryDetailView.vue
index db524c72..eb4e3947 100644
--- a/src/modules/domestic-containers/pages/DomesticContainerInventoryDetailView.vue
+++ b/src/modules/domestic-containers/pages/DomesticContainerInventoryDetailView.vue
@@ -1,5 +1,6 @@
-
-
-
+
+
+
@@ -77,7 +80,7 @@ onMounted(load)
出库单数量{{ detail.outboundOrderCount }}
入库箱数{{ detail.inventory.inboundBoxes }} 箱
已出库箱数{{ detail.inventory.outboundBoxes }} 箱
- 当前库存{{ detail.inventory.availableBoxes }} 箱
+ 当前库存{{ detail.inventory.availableBoxes }} 箱
累计入库体积{{ detail.inventory.inboundVolumeCbm.toFixed(2) }} m³
最后更新时间{{ displayTime(detail.inventory.updatedAt) }}
库存版本{{ detail.inventory.version }}
@@ -88,25 +91,25 @@ onMounted(load)
进销存交易记录
-
-
-
+
+ {{ row.businessType === 'INBOUND' ? '入库' : '出库' }}
+
diff --git a/src/modules/domestic-containers/pages/DomesticContainersView.test.ts b/src/modules/domestic-containers/pages/DomesticContainersView.test.ts
index 246f5008..d0725d2d 100644
--- a/src/modules/domestic-containers/pages/DomesticContainersView.test.ts
+++ b/src/modules/domestic-containers/pages/DomesticContainersView.test.ts
@@ -15,7 +15,20 @@ describe('DomesticContainersView', () => {
it('gates create actions with backend permissions', () => { expect(source).toContain("access.can('platform:domestic-container:inbound:create')"); expect(source).toContain("access.can('platform:domestic-container:outbound:create')") })
it('loads only active domestic warehouse candidates and surfaces errors', () => { expect(source).toContain("listAllPlatformWarehouses({ warehouseType: 'DOMESTIC', status: 'ACTIVE' })"); expect(source).toContain('国内仓候选加载失败') })
it('keeps idempotency key stable through retry and handles conflicts by refreshing inventory', () => { expect(source).toContain('idempotencyKey.value'); expect(source).toContain("submitError.value.code === 'A0409'"); expect(source).toContain('await loadCandidates()'); expect(source).not.toContain('localStorage') })
- it('reuses the order-list tab panel and keyboard interaction', () => { expect(source).toContain('class="business-query-panel integrated-query-group"'); expect(source).toContain('role="tablist"'); expect(source).toContain('role="tab"'); expect(source).toContain('handleTabKeydown'); expect(source).toContain('class="status-tabs density-status-tabs"'); expect(source).not.toContain(' {
+ const toolbar = source.match(/([\s\S]*?)<\/template>/)?.[1]
+ const filters = source.match(/([\s\S]*?)<\/template>/)?.[1]
+ expect(toolbar).toContain('class="status-tabs density-status-tabs workbench-list-tabs"')
+ expect(toolbar).toContain('role="tablist"')
+ expect(toolbar).toContain('role="tab"')
+ expect(toolbar).toContain('@click="switchTab(tab.value)"')
+ expect(toolbar).toContain('@keydown="handleTabKeydown($event, index)"')
+ expect(toolbar).not.toContain('')
+ expect(filters).toContain('class="business-query-panel integrated-query-group"')
+ expect(filters).toContain(' { expect(source).toContain("key: 'warehouseId', type: 'select', width: 210"); expect(source).toContain("key: 'orderNo', type: 'input', width: 220"); expect(source).toContain("key: 'containerNo', type: 'input', width: 220"); expect(source).toContain("key: 'cargoName', type: 'input', width: 220"); expect(source).toContain("key: 'status', type: 'select', width: 180") })
it('uses the shared query toolbar flow without a page-level layout override', () => { expect(source).not.toContain('.query-toolbar__filters') })
it('keeps all four inventory facts in the approved compact summary', () => {
diff --git a/src/modules/domestic-containers/pages/DomesticContainersView.vue b/src/modules/domestic-containers/pages/DomesticContainersView.vue
index 55c26bc1..5eb990f0 100644
--- a/src/modules/domestic-containers/pages/DomesticContainersView.vue
+++ b/src/modules/domestic-containers/pages/DomesticContainersView.vue
@@ -1,5 +1,6 @@
-
+
-
- 整柜业务
-
+
+
+
入库箱数{{ summary.inboundBoxes }}
已出库箱数{{ summary.outboundBoxes }}
当前库存{{ summary.availableBoxes }}
累计入库体积{{ summary.inboundVolumeCbm.toFixed(2) }} m³
-新建入库单新建出库单查看—
-
+新建入库单新建出库单查看—
+
-
+
@@ -117,8 +118,8 @@ watch([page, pageSize], load); onMounted(async () => { await Promise.all([loadWa
|
|
-
+
-
+
diff --git a/src/modules/downloads/pages/DownloadCenterView.vue b/src/modules/downloads/pages/DownloadCenterView.vue
index 21405f69..90dd0036 100644
--- a/src/modules/downloads/pages/DownloadCenterView.vue
+++ b/src/modules/downloads/pages/DownloadCenterView.vue
@@ -1,4 +1,5 @@
-
+
-
+
下载中心
@@ -314,7 +315,7 @@ onBeforeUnmount(() => {
-
{
.download-error { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-6); }
.download-error { padding: var(--admin-space-6); }
.download-error strong { color: var(--cc-danger); font-size: var(--admin-font-body); }
-.download-error p { margin: 5px 0; color: var(--cc-text); font-size: var(--admin-font-body); }
+.download-error p { margin: 5px 0; color: var(--workbench-text); font-size: var(--admin-font-body); }
.download-sync-warning { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-5); padding: var(--admin-space-4) var(--admin-space-6); border-bottom: 1px solid #fedf89; background: #fffaeb; color: #93370d; font-size: var(--admin-font-caption); }
.failure-cell { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.failure-message { color: var(--cc-danger); }
-small { display: block; margin-top: 3px; color: var(--cc-muted); font-size: var(--admin-font-caption); }
+small { display: block; margin-top: 3px; color: var(--workbench-muted); font-size: var(--admin-font-caption); }
@media (max-width: 720px) {
.download-error, .download-sync-warning { align-items: stretch; flex-direction: column; }
.table-actions :deep(.el-button) { min-height: 44px; }
diff --git a/src/modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue b/src/modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue
index 4f486857..c4757459 100644
--- a/src/modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue
+++ b/src/modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue
@@ -1,6 +1,7 @@
-
-
+
+