From 87da5df91bd2ef23726678b87356a4304d6fab71 Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Wed, 3 Jun 2026 22:15:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A3=8E=E9=99=A9=E5=8F=AF=E8=A7=81?= =?UTF-8?q?=E6=80=A7=E6=8E=A7=E5=88=B6=E4=B8=8E=E5=B7=AE=E6=97=85=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E4=BA=A4=E4=BA=92=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增风险可见性工具函数与风险日趋势图表组件 - 优化差旅请求详情页费用模型与视图交互 - 完善顶部导航栏样式与应用壳路由逻辑 - 补充风险可见性、风险看板与差旅详情测试覆盖 --- web/src/assets/styles/components/top-bar.css | 170 ++++++++++++ .../views/travel-request-detail-view.css | 26 +- .../components/charts/RiskDailyTrendChart.vue | 16 +- web/src/components/layout/TopBar.vue | 241 +++++++++++------- web/src/composables/useAppShell.js | 131 +++++++--- web/src/composables/useOverviewView.js | 65 ++++- .../composables/useTopBarWorkbenchPopovers.js | 90 +++++++ web/src/scripts/App.js | 3 +- web/src/utils/dateRangeDefaults.js | 14 + web/src/utils/riskVisibility.js | 7 + web/src/views/TravelRequestDetailView.vue | 43 ++-- .../views/scripts/TravelRequestDetailView.js | 2 +- .../travelRequestDetailExpenseModel.js | 27 +- ...p-shell-financial-assistant-entry.test.mjs | 16 +- web/tests/risk-observation-dashboard.test.mjs | 38 +++ web/tests/risk-visibility.test.mjs | 37 +++ ...travel-request-detail-risk-advice.test.mjs | 51 +++- 17 files changed, 809 insertions(+), 168 deletions(-) create mode 100644 web/src/composables/useTopBarWorkbenchPopovers.js create mode 100644 web/src/utils/dateRangeDefaults.js diff --git a/web/src/assets/styles/components/top-bar.css b/web/src/assets/styles/components/top-bar.css index 4527bde..95d1321 100644 --- a/web/src/assets/styles/components/top-bar.css +++ b/web/src/assets/styles/components/top-bar.css @@ -881,6 +881,176 @@ line-height: 1.5; } +.help-wrap { + position: relative; + display: inline-flex; +} + +.help-wrap.is-open .help-btn { + border: 1px solid var(--theme-primary-light-5); + background: var(--theme-primary-light-9); + color: var(--theme-primary-active); +} + +.help-btn { + border: 1px solid transparent; + border-radius: 4px; + transition: + border-color 180ms var(--ease), + background 180ms var(--ease), + color 180ms var(--ease); +} + +.help-panel-enter-active, +.help-panel-leave-active { + transition: + opacity 220ms var(--ease), + transform 220ms var(--ease); +} + +.help-panel-enter-from, +.help-panel-leave-to { + opacity: 0; + transform: translateY(-6px); +} + +@media (prefers-reduced-motion: reduce) { + .help-panel-enter-active, + .help-panel-leave-active { + transition-duration: 1ms; + } + + .help-panel-enter-from, + .help-panel-leave-to { + transform: none; + } +} + +.help-popover { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 60; + width: min(300px, calc(100vw - 32px)); + overflow: hidden; + border: 1px solid #d7e0ea; + border-radius: 4px; + background: #fff; + box-shadow: 0 16px 40px rgba(15, 23, 42, 0.12); +} + +.help-popover::before { + content: ""; + display: block; + height: 3px; + background: linear-gradient( + 90deg, + var(--theme-primary-active) 0%, + var(--theme-primary-light-3, #7eb3d4) 100% + ); +} + +.help-head { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px; + border-bottom: 1px solid #edf2f7; + background: #fafbfd; +} + +.help-head-icon { + width: 32px; + height: 32px; + flex: 0 0 auto; + display: grid; + place-items: center; + border: 1px solid var(--theme-primary-light-6); + border-radius: 4px; + background: #fff; + color: var(--theme-primary-active); + font-size: 17px; +} + +.help-head-copy { + min-width: 0; + flex: 1 1 auto; + display: grid; + gap: 2px; +} + +.help-head-copy strong { + color: #0f172a; + font-size: 13px; + font-weight: 800; +} + +.help-head-copy small { + color: #64748b; + font-size: 12px; + font-weight: 600; +} + +.help-close-btn { + width: 28px; + height: 28px; + flex: 0 0 auto; + display: inline-grid; + place-items: center; + border: 0; + border-radius: 4px; + background: transparent; + color: #64748b; + font-size: 18px; + transition: + background 160ms var(--ease), + color 160ms var(--ease); +} + +.help-close-btn:hover { + background: #eef2f7; + color: #0f172a; +} + +.help-meta { + margin: 0; + padding: 10px 14px 12px; + display: grid; + gap: 8px; +} + +.help-meta-row { + display: grid; + grid-template-columns: 72px minmax(0, 1fr); + gap: 8px; + align-items: start; +} + +.help-meta-row--block { + grid-template-columns: 1fr; + gap: 4px; +} + +.help-meta-row dt { + color: #64748b; + font-size: 12px; + font-weight: 700; +} + +.help-meta-row dd { + margin: 0; + color: #0f172a; + font-size: 13px; + font-weight: 650; + line-height: 1.45; +} + +.help-meta-row--block dd { + color: #475569; + font-size: 12px; + font-weight: 600; +} + .company-switcher { max-width: min(220px, 28vw); height: 38px; diff --git a/web/src/assets/styles/views/travel-request-detail-view.css b/web/src/assets/styles/views/travel-request-detail-view.css index 66c08eb..f4d6f75 100644 --- a/web/src/assets/styles/views/travel-request-detail-view.css +++ b/web/src/assets/styles/views/travel-request-detail-view.css @@ -936,25 +936,33 @@ .detail-expense-table .col-action { width: 9%; } .expense-time { - position: relative; + min-width: 0; } -.expense-time.has-major-risk { - padding-left: 30px; +.expense-time-content { + display: grid; + grid-template-columns: 18px minmax(0, 1fr); + align-items: center; + gap: 6px; + min-width: 0; +} + +.expense-time-value { + min-width: 0; +} + +.expense-risk-indicator, +.expense-risk-indicator-placeholder { + width: 18px; + height: 18px; } .expense-risk-indicator { - position: absolute; - left: 8px; - top: 50%; - width: 18px; - height: 18px; padding: 0; border: 0; background: transparent; display: inline-grid; place-items: center; - transform: translateY(-50%); color: #dc2626; font-size: 18px; line-height: 1; diff --git a/web/src/components/charts/RiskDailyTrendChart.vue b/web/src/components/charts/RiskDailyTrendChart.vue index daad379..9ae0162 100644 --- a/web/src/components/charts/RiskDailyTrendChart.vue +++ b/web/src/components/charts/RiskDailyTrendChart.vue @@ -32,6 +32,12 @@ const totals = computed(() => props.rows.map((item) => Number(item.total || 0))) const highValues = computed(() => props.rows.map((item) => Number(item.highOrAbove || 0))) const maxValue = computed(() => Math.max(...totals.value, ...highValues.value, 1)) const axisMax = computed(() => Math.max(5, Math.ceil(maxValue.value * 1.2))) +const barWidth = computed(() => { + if (labels.value.length >= 12) return 9 + if (labels.value.length >= 8) return 11 + return 14 +}) +const bottomGridSize = computed(() => (labels.value.some((label) => String(label).includes('~')) ? 38 : 28)) const ariaLabel = computed(() => props.rows.map((item) => ( @@ -47,7 +53,7 @@ const chartOptions = computed(() => ({ grid: { top: 34, right: 16, - bottom: 24, + bottom: bottomGridSize.value, left: 28, containLabel: true }, @@ -86,7 +92,10 @@ const chartOptions = computed(() => ({ axisLabel: { color: '#64748b', fontSize: 11, - fontWeight: 700 + fontWeight: 700, + interval: 0, + lineHeight: 14, + formatter: (value) => String(value || '').replace('~', '\n~') } }, yAxis: { @@ -106,7 +115,8 @@ const chartOptions = computed(() => ({ name: '风险观察', type: 'bar', data: totals.value, - barWidth: 14, + barWidth: barWidth.value, + barMaxWidth: 14, itemStyle: { color: themeColors.value.chartPrimary, borderRadius: [4, 4, 0, 0] diff --git a/web/src/components/layout/TopBar.vue b/web/src/components/layout/TopBar.vue index fc7e913..e9ddd46 100644 --- a/web/src/components/layout/TopBar.vue +++ b/web/src/components/layout/TopBar.vue @@ -123,14 +123,14 @@