From 40fcb6fc7bc88cc7c57d6948e44391552f5dd615 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 18 Jul 2026 07:50:25 +0800 Subject: [PATCH] refine Semi UI mileage workspace --- .../apps/web/src/v2/pages/StatisticsPage.tsx | 75 +-- .../apps/web/src/v2/styles/workspace.css | 450 ++++++++++++++++++ 2 files changed, 491 insertions(+), 34 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx index dfd271be..c2c5d59f 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/StatisticsPage.tsx @@ -1,7 +1,7 @@ import { IconArrowDown, IconArrowUp, IconClose, IconDownload, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons'; import { Button, Card, CardGroup, Empty, Input, SideSheet, Spin, Switch, Table, Tag } from '@douyinfe/semi-ui'; import { useQuery } from '@tanstack/react-query'; -import { FormEvent, type RefObject, useEffect, useMemo, useRef, useState } from 'react'; +import { type CSSProperties, FormEvent, memo, type RefObject, useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { DailyMileageRow, MileageStatistics, Page, VehicleRow } from '../../api/types'; @@ -233,22 +233,25 @@ function VehicleMultiSelect({ value, onChange }: { value: VehicleOption[]; onCha function SummaryRail({ data, criteria, fleetTotal, loading }: { data?: MileageStatistics; criteria: Criteria; fleetTotal?: number; loading: boolean }) { const days = inclusiveDays(criteria.dateFrom, criteria.dateTo); const vehicleCount = criteria.vehicles.length ? data?.vehicleCount ?? 0 : fleetTotal ?? 0; + const summaryRange = criteria.dateFrom.slice(0, 4) === criteria.dateTo.slice(0, 4) + ? `${criteria.dateFrom.replace(/-/g, '/')} — ${criteria.dateTo.slice(5).replace('-', '/')}` + : `${criteria.dateFrom.replace(/-/g, '/')} — ${criteria.dateTo.replace(/-/g, '/')}`; const items = loading ? [ ['已绑定主车辆', '—', '正在按新筛选范围查询'], - ['统计天数', `${days} 天`, `${criteria.dateFrom} 至 ${criteria.dateTo}`], + ['统计天数', `${days} 天`, summaryRange], ['区间总里程', '—', '正在计算区间汇总'], ['日均里程', '—', '正在计算有效车辆日'] ] : [ ['已绑定主车辆', `${vehicleCount} 辆`, criteria.vehicles.length ? `已选择 ${criteria.vehicles.length} 辆` : `档案口径 · ${data?.vehicleCount ?? 0} 辆有里程`], - ['统计天数', `${days} 天`, `${criteria.dateFrom} 至 ${criteria.dateTo}`], + ['统计天数', `${days} 天`, summaryRange], ['区间总里程', `${formatKm(data?.periodMileageKm)} km`, `${data?.recordCount ?? 0} 条车辆日记录`], ['日均里程', `${formatKm(data?.averageDailyMileageKm)} km`, '按有效车辆日平均'] ]; const primary = items[2]; const secondary = [items[0], items[1], items[3]]; return - {primary[0]}{primary[1]}{primary[2]} - {secondary.map(([label, value, note]) => {label}{value}{note})} + {primary[0]}{primary[1]}{primary[2]} + {secondary.map(([label, value, note], index) => {label}{value}{note})} ; } @@ -270,37 +273,41 @@ function dateLabel(date: string) { return `${Number(month)}/${Number(day)}`; } -function MileageTable({ rows, dates, scrollRef }: { rows: VehicleMileageMatrix[]; dates: string[]; scrollRef: RefObject }) { - let maxDailyMileage = 1; - for (const row of rows) { - for (const mileage of row.days.values()) maxDailyMileage = Math.max(maxDailyMileage, mileage); - } - const columns = [ - { title: '车牌', dataIndex: 'plate', className: 'is-plate', width: 120, render: (_value: string, row: VehicleMileageMatrix) => {row.plate || '未绑定'} }, - ...dates.map((date) => ({ - title: dateLabel(date), dataIndex: date, className: 'is-number is-date', width: 96, - onHeaderCell: () => ({ title: date }), - onCell: (row?: VehicleMileageMatrix) => { - const mileage = row?.days.get(date); - const intensity = mileage && mileage > 0 ? .035 + mileage / maxDailyMileage * .13 : 0; - return { - className: `is-number is-date${mileage != null ? ' is-daily' : ' is-empty'}`, - title: mileage != null ? `来源:${row?.sources.get(date) || '—'}` : undefined, - style: intensity ? { backgroundColor: `rgba(37, 99, 235, ${intensity.toFixed(3)})` } : undefined - }; - }, - render: (_value: unknown, row: VehicleMileageMatrix) => { - const mileage = row.days.get(date); - return mileage != null ? `${formatKm(mileage)} km` : '—'; - } - })), - { title: '区间总里程', dataIndex: 'totalMileageKm', className: 'is-number is-total', width: 128, onCell: () => ({ className: 'is-number is-period is-total' }), render: (value: number) => `${formatKm(value)} km` } - ]; - const tableWidth = 120 + dates.length * 96 + 128; - return
+const MileageTable = memo(function MileageTable({ rows, dates, scrollRef }: { rows: VehicleMileageMatrix[]; dates: string[]; scrollRef: RefObject }) { + const { columns, tableWidth } = useMemo(() => { + let maxDailyMileage = 1; + for (const row of rows) { + for (const mileage of row.days.values()) maxDailyMileage = Math.max(maxDailyMileage, mileage); + } + return { + columns: [ + { title: '车牌', dataIndex: 'plate', className: 'is-plate', width: 120, render: (_value: string, row: VehicleMileageMatrix) => {row.plate || '未绑定'} }, + ...dates.map((date) => ({ + title: dateLabel(date), dataIndex: date, className: 'is-number is-date', width: 96, + onHeaderCell: () => ({ title: date }), + onCell: (row?: VehicleMileageMatrix) => { + const mileage = row?.days.get(date); + const intensity = mileage && mileage > 0 ? .035 + mileage / maxDailyMileage * .13 : 0; + return { + className: `is-number is-date${mileage != null ? ' is-daily' : ' is-empty'}`, + title: mileage != null ? `来源:${row?.sources.get(date) || '—'}` : undefined, + style: intensity ? { backgroundColor: `rgba(37, 99, 235, ${intensity.toFixed(3)})` } : undefined + }; + }, + render: (_value: unknown, row: VehicleMileageMatrix) => { + const mileage = row.days.get(date); + return mileage != null ? `${formatKm(mileage)} km` : '—'; + } + })), + { title: '区间总里程', dataIndex: 'totalMileageKm', className: 'is-number is-total', width: 128, onCell: () => ({ className: 'is-number is-period is-total' }), render: (value: number) => `${formatKm(value)} km` } + ], + tableWidth: 120 + dates.length * 96 + 128 + }; + }, [dates, rows]); + return
; -} +}); export default function StatisticsPage() { const [searchParams, setSearchParams] = useSearchParams(); diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css index e299bf1f..52746e3c 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -10208,3 +10208,453 @@ min-height: 94px; } } + +/* + * Semi UI mileage workspace convergence. + * Keep a single query/summary surface, one table scroll owner and the same + * restrained control, radius and evidence language used by the other + * operations workspaces. + */ +.v2-mileage-page { + gap: 10px; + padding: 14px 18px 16px; +} + +.v2-mileage-query-panel.v2-workspace-filter-panel.semi-card { + overflow: visible; + border-color: #d8e3ef; + border-radius: 12px; + box-shadow: 0 8px 26px rgba(31, 53, 80, .055); +} + +.v2-mileage-query-panel .v2-workspace-filter-heading { + min-height: 44px; + border-bottom-color: #e3eaf2; + background: linear-gradient(100deg, #f7faff 0%, #f9fbfd 58%, #fff 100%); + padding-inline: 13px; +} + +.v2-mileage-query-panel .v2-mileage-filter { + gap: 10px; + background: #fff; + padding: 11px 12px 12px; +} + +.v2-mileage-filter > label { + gap: 5px; + color: #607187; + font-size: 11px; + font-weight: 650; +} + +.v2-mileage-multiselect, +.v2-mileage-filter > label > .semi-input-wrapper { + min-height: 38px; + border-color: #d8e2ed; + border-radius: 8px; + background: #fbfcfe; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .72); +} + +.v2-mileage-multiselect:hover, +.v2-mileage-filter > label > .semi-input-wrapper:hover { + border-color: #b9c9dc; + background: #fff; +} + +.v2-mileage-multiselect:focus-within, +.v2-mileage-filter > label > .semi-input-wrapper-focus { + border-color: #75a5ed; + background: #fff; + box-shadow: 0 0 0 3px rgba(37, 99, 235, .09); +} + +.v2-mileage-selection > .semi-input-wrapper .semi-input, +.v2-mileage-filter > label > .semi-input-wrapper .semi-input { + color: #2d4058; + font-size: 12px; +} + +.v2-mileage-filter > .v2-primary-button.semi-button { + min-height: 38px; + border-radius: 8px; + padding-inline: 19px; + font-size: 12px; + font-weight: 680; + box-shadow: 0 4px 12px rgba(18, 104, 223, .16); +} + +.v2-mileage-source-trigger.semi-button { + min-height: 38px; + border-color: #d8e3ef; + border-radius: 8px; + background: #f8fbff; + color: #49647f; + font-size: 11px; +} + +.v2-mileage-source-trigger.semi-button:hover, +.v2-mileage-source-trigger.semi-button[aria-expanded="true"] { + border-color: #aec6e4; + background: #f2f7ff; + color: #245da8; +} + +.v2-mileage-source-trigger em { + color: #3d6594; + font-size: 9px; +} + +.v2-mileage-ranges { + min-height: 38px; + gap: 2px; + border: 1px solid #e2e8f0; + border-radius: 9px; + background: #f5f7fa; + padding: 3px; +} + +.v2-mileage-ranges .semi-button { + min-height: 30px; + border-radius: 6px; + font-size: 11px; + font-weight: 650; +} + +.v2-mileage-ranges .semi-button:not(.semi-button-primary):hover { + background: #fff; + color: #1268df; +} + +.v2-mileage-query-panel .v2-mileage-summary.semi-card { + border: 0; + border-top: 1px solid #e2e9f2; + border-radius: 0 0 11px 11px; + background: #fff; + box-shadow: none; +} + +.v2-mileage-summary > .semi-card-body > .v2-mileage-summary-card.is-primary > .semi-card-body { + background: linear-gradient(128deg, #0c61e7 0%, #1c73ee 68%, #2c82ef 100%); +} + +.v2-mileage-summary > .semi-card-body > .is-primary .v2-mileage-summary-value { + letter-spacing: -.035em; +} + +.v2-mileage-summary-secondary.semi-card-group > .v2-mileage-summary-card > .semi-card-body { + justify-content: center; + padding: 11px 15px !important; +} + +.v2-mileage-summary-secondary .v2-mileage-summary-card small { + color: #78879a; + font-size: 10px; + font-weight: 560; +} + +.v2-mileage-summary-secondary .v2-mileage-summary-card strong { + margin: 5px 0 4px; + color: #22354d; + font-size: clamp(19px, 1.55vw, 25px); + font-weight: 760; + line-height: 1.08; +} + +.v2-mileage-summary-secondary .v2-mileage-summary-card span { + display: -webkit-box; + min-height: 2.6em; + overflow: hidden; + color: #8492a4; + font-size: 9px; + line-height: 1.3; + text-overflow: ellipsis; + white-space: normal; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.v2-mileage-results.semi-card { + border-color: #d8e3ef; + border-radius: 12px; + box-shadow: 0 8px 26px rgba(31, 53, 80, .05); +} + +.v2-mileage-results > .semi-card-body > .v2-workspace-panel-header { + min-height: 50px; + border-bottom-color: #e2e9f1; + background: linear-gradient(90deg, #fff 0%, #fbfcfe 100%); + padding-inline: 14px; +} + +.v2-mileage-results .v2-workspace-panel-copy > h5.semi-typography { + color: #23384f; + font-size: 15px; +} + +.v2-mileage-results .v2-workspace-panel-copy > .semi-typography, +.v2-mileage-results .v2-workspace-result-meta { + color: #748499; + font-size: 10px; +} + +.v2-mileage-result-actions > .semi-button { + min-height: 32px; + border-radius: 8px; + font-size: 11px; + font-weight: 650; +} + +.v2-mileage-table-wrap { + scrollbar-color: #9eb0c4 #f2f5f9; + scrollbar-gutter: stable; + scrollbar-width: auto; +} + +.v2-mileage-table-wrap::-webkit-scrollbar { + width: 14px; + height: 14px; +} + +.v2-mileage-table-wrap::-webkit-scrollbar-track { + border-radius: 999px; + background: #f2f5f9; +} + +.v2-mileage-table-wrap::-webkit-scrollbar-thumb { + min-width: 56px; + min-height: 56px; + border: 3px solid #f2f5f9; + border-radius: 999px; + background: #9eb0c4; +} + +.v2-mileage-table-wrap::-webkit-scrollbar-thumb:hover { + background: #7f95ae; +} + +.v2-mileage-table-wrap::-webkit-scrollbar-corner { + background: #f2f5f9; +} + +.v2-mileage-table .semi-table-thead > .semi-table-row > .semi-table-row-head { + height: 42px; + border-bottom-color: #dde6f0; + background: #f6f8fb; + color: #5a6d84; + font-size: 11px; +} + +.v2-mileage-table .semi-table-tbody > .semi-table-row > .semi-table-row-cell { + height: 52px; + border-bottom-color: #e9eef4; + color: #43566d; + font-size: 12px; +} + +.v2-mileage-table .semi-table-tbody > .semi-table-row:hover { + background: #f6faff; +} + +.v2-mileage-table .semi-table-row-cell.is-plate strong { + color: #243950; + font-size: 12px; + font-weight: 720; +} + +.v2-mileage-table .semi-table-row-head.is-total { + background: #edf5ff; + color: #175fbc; +} + +.v2-mileage-table.semi-table-wrapper :is(.semi-table-row-head,.semi-table-row-cell).is-total, +.v2-mileage-table :is(.semi-table-row-head,.semi-table-row-cell).semi-table-cell-fixed-right { + border-left-color: #d5e2f1; + box-shadow: -6px 0 14px rgba(38, 71, 111, .045); +} + +.v2-mileage-table .semi-table-row-cell.is-period { + color: #0e62cf; + font-size: 12px; + font-weight: 780; +} + +.v2-mileage-results > .semi-card-body > footer { + min-height: 40px; + border-top-color: #e3eaf2; + background: #fbfcfe; +} + +.v2-mileage-evidence { + min-height: 20px; + align-items: center; + justify-content: flex-start; + gap: 0; + color: #8290a2; + font-size: 9px; +} + +.v2-mileage-evidence > span { + position: relative; + overflow: hidden; + padding-inline: 10px; + text-overflow: ellipsis; +} + +.v2-mileage-evidence > span:first-child { + padding-left: 2px; +} + +.v2-mileage-evidence > span + span::before { + position: absolute; + top: 50%; + left: 0; + width: 3px; + height: 3px; + border-radius: 50%; + background: #b4c0ce; + content: ""; + transform: translateY(-50%); +} + +@media (min-width: 681px) and (max-width: 1050px) { + .v2-mileage-query-panel .v2-mileage-filter { + row-gap: 10px; + } + + .v2-mileage-summary > .semi-card-body { + grid-template-columns: minmax(285px, 1fr) minmax(0, 2fr); + } +} + +@media (max-width: 680px) { + .v2-mileage-page { + gap: 7px; + padding: 8px; + } + + .v2-mileage-query-panel.v2-workspace-filter-panel.semi-card { + border-radius: 11px; + box-shadow: 0 5px 18px rgba(31, 53, 80, .05); + } + + .v2-mileage-query-panel .v2-mileage-filter { + gap: 9px; + padding: 10px; + } + + .v2-mileage-filter > label > .semi-input-wrapper, + .v2-mileage-multiselect, + .v2-mileage-source-trigger.semi-button, + .v2-mileage-filter > .v2-primary-button.semi-button { + min-height: 42px; + } + + .v2-mileage-query-panel .v2-mileage-summary.semi-card { + border-top: 0; + border-radius: 10px; + } + + .v2-mileage-summary-secondary .v2-mileage-summary-card strong { + font-size: 16px; + } + + .v2-mileage-summary-secondary .v2-mileage-summary-card span { + display: block; + min-height: 0; + overflow: hidden; + font-size: 8px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-mileage-results > .semi-card-body > .v2-workspace-panel-header { + display: grid; + min-height: 54px; + height: 54px; + grid-template-columns: 82px minmax(0, 1fr); + gap: 6px; + padding: 7px 9px; + } + + .v2-mileage-results .v2-workspace-panel-copy { + width: 82px; + min-width: 82px; + max-width: 82px; + flex: none; + } + + .v2-mileage-results .v2-workspace-panel-copy > h5.semi-typography { + width: auto; + overflow: visible; + font-size: 13px; + text-overflow: clip; + } + + .v2-mileage-results .v2-workspace-panel-copy > .semi-typography:last-child, + .v2-mileage-results .v2-workspace-result-meta > span:last-child { + display: none; + } + + .v2-mileage-results > .semi-card-body > .v2-workspace-panel-header > .v2-mileage-result-actions { + display: flex; + width: 100%; + min-width: 0; + max-width: none; + height: auto; + flex: none; + align-items: center; + justify-content: flex-end; + gap: 4px; + } + + .v2-mileage-results .v2-mileage-result-actions > .v2-workspace-panel-meta { + flex: 0 0 auto; + font-size: 8px; + } + + .v2-mileage-result-actions > .semi-button { + width: auto; + min-width: 0; + height: 32px; + flex: 0 0 auto; + padding-inline: 7px; + font-size: 9px; + } + + .v2-mileage-table table { + width: max(100%, var(--v2-mileage-mobile-table-width)) !important; + } + + .v2-mileage-table col.is-plate, + .v2-mileage-table :is(.semi-table-row-head,.semi-table-row-cell).is-plate { + width: 96px !important; + min-width: 96px !important; + max-width: 96px !important; + } + + .v2-mileage-table col.is-date, + .v2-mileage-table :is(.semi-table-row-head,.semi-table-row-cell).is-date { + width: 78px !important; + min-width: 78px !important; + max-width: 78px !important; + } + + .v2-mileage-table col.is-total, + .v2-mileage-table :is(.semi-table-row-head,.semi-table-row-cell).is-total { + width: 104px !important; + min-width: 104px !important; + max-width: 104px !important; + } + + .v2-mileage-table .semi-table-thead > .semi-table-row > .semi-table-row-head { + height: 38px; + font-size: 10px; + } + + .v2-mileage-table .semi-table-tbody > .semi-table-row > .semi-table-row-cell { + height: 44px; + font-size: 11px; + } +}