diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx index b8f994d2..e2e1f07f 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx @@ -6,7 +6,7 @@ import { Link, useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { AccessProtocolStatus, AccessQuery, AccessSummary, AccessThresholdConfig, AccessThresholdUpdate, AccessUnresolvedIdentity, AccessVehicleRow, Page } from '../../api/types'; import { accessRowsToCSV, formatAccessTime, formatSeconds, updateProtocolThreshold } from '../domain/access'; -import { InlineError } from '../shared/AsyncState'; +import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState'; import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { MetricActionButton } from '../shared/MetricActionButton'; import { PageHeader } from '../shared/PageHeader'; @@ -239,8 +239,8 @@ export default function AccessPage() { {mobileLayout ?
{rows.map((row) => )}
: } - {vehiclesQuery.isFetching ?
: null} - {!vehiclesQuery.isFetching && !rows.length ? : null} + {vehiclesQuery.isFetching ? : null} + {!vehiclesQuery.isFetching && !rows.length ? : null}
setOffset((next - 1) * limit)} pageSize={limit} pageSizeLabel="每页车辆数" onPageSizeChange={(next) => { setLimit(next); setOffset(0); }} pageSizeOptions={[{ value: 20, label: '20 辆/页' }, { value: 50, label: '50 辆/页' }, { value: 100, label: '100 辆/页' }]} />
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx index f634ca53..0f7d85df 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx @@ -6,7 +6,7 @@ import { Link, useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { AlertEvent, AlertNotification, AlertQuery, AlertRule, AlertRuleInput, AlertStatus, MetricDefinition, Page } from '../../api/types'; import { actionLabels, alertValue, canAct, formatAlertTime, operatorLabels, ruleCondition, severityLabels, statusLabels, thresholdText } from '../domain/alert'; -import { InlineError } from '../shared/AsyncState'; +import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState'; import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { MetricActionButton } from '../shared/MetricActionButton'; import { PageHeader } from '../shared/PageHeader'; @@ -215,8 +215,8 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e {mobileLayout ?
{rows.map((event) => )}
: setSelection({ scope: eventScope, id })} />} - {events.isFetching ?
: null} - {!events.isFetching && !rows.length ? : null} + {events.isFetching ? : null} + {!events.isFetching && !rows.length ? : null}
setOffset((next - 1) * limit)} pageSize={limit} onPageSizeChange={(next) => { setLimit(next); setOffset(0); }} pageSizeOptions={[{ value: 20, label: '20 条/页' }, { value: 50, label: '50 条/页' }]} />
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx index 696cbc2b..0ec073eb 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx @@ -74,6 +74,27 @@ test('preserves the exact monitor return after changing history filters', async expect(await screen.findByRole('link', { name: /返回全局监控/ })).toHaveAttribute('href', monitorPath); }); +test('renders the shared empty state when the API returns nullable empty collections', async () => { + mocks.historyMetricCatalog.mockResolvedValue({ categories: [{ key: 'location', label: '位置数据' }], metrics: [metric] }); + mocks.historyExports.mockResolvedValue([]); + mocks.historyData.mockResolvedValue({ + category: 'location', + columns: null, + rows: null, + summary: null, + total: 0, + limit: 50, + offset: 0, + asOf: '2026-07-18 00:53:00' + } as unknown as HistoryDataResponse); + + const view = renderPage('/history?keywords=LTESTNOEXIST00001&dateFrom=2026-07-18T00%3A00&dateTo=2026-07-18T00%3A53'); + + expect(await screen.findByText('当前条件没有历史记录')).toBeInTheDocument(); + expect(view.container.querySelector('.v2-history-empty.v2-state-surface')).toBeInTheDocument(); + expect(screen.queryByText('当前模块暂时无法显示')).not.toBeInTheDocument(); +}); + test('clears stale rows, charts, and evidence as soon as the history scope changes', async () => { let resolveNewData!: (value: HistoryDataResponse) => void; let resolveNewSeries!: (value: HistorySeriesResponse) => void; diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx index a057ee45..f731d0f0 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx @@ -7,7 +7,7 @@ import { api } from '../../api/client'; import type { HistoryDataResponse, HistoryDataRow, HistoryExportRequest, HistoryMetricDefinition, HistorySeriesResponse } from '../../api/types'; import { buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, formatSeriesGrain, historyExportPollInterval, parseHistoryKeywords } from '../domain/history'; import { downloadBlob } from '../domain/download'; -import { InlineError } from '../shared/AsyncState'; +import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { PageHeader } from '../shared/PageHeader'; @@ -275,6 +275,8 @@ export default function HistoryPage() { gcTime: QUERY_MEMORY.highVolumeGcTime }); const result = dataQuery.data; + const resultRows = result?.rows ?? []; + const resultSummary = result?.summary; const allMetrics = result?.columns ?? catalogQuery.data?.metrics.filter((metric) => metric.category === criteria.category) ?? []; const visibleKeys = visibleByCategory[criteria.category] ?? allMetrics.filter((metric) => metric.defaultVisible).map((metric) => metric.key); const visibleMetrics = allMetrics.filter((metric) => visibleKeys.includes(metric.key)); @@ -292,7 +294,7 @@ export default function HistoryPage() { const selectedRow = scopedSelectedRowRef?.id === '' ? undefined : scopedSelectedRowRef - ? result?.rows.find((row) => row.id === scopedSelectedRowRef.id) + ? resultRows.find((row) => row.id === scopedSelectedRowRef.id) : undefined; const selectRow = useCallback((row: HistoryDataRow) => { setSelectedRowRef({ scope: dataScope, id: row.id }); @@ -325,7 +327,7 @@ export default function HistoryPage() { const setVisibleMetrics = (keys: string[]) => setVisibleByCategory((current) => ({ ...current, [criteria.category]: keys })); const totalPages = Math.max(1, Math.ceil((result?.total ?? 0) / limit)); const page = Math.floor(offset / limit) + 1; - const summarySources = result?.summary.sources.join('、') || '—'; + const summarySources = resultSummary?.sources?.join('、') || '—'; return
@@ -342,7 +344,7 @@ export default function HistoryPage() {
时间范围
setDraft((current) => ({ ...current, dateFrom: value }))} /> setDraft((current) => ({ ...current, dateTo: value }))} />
-
{exportAllowed ? : 只读}
+
{exportAllowed ? : 只读}
@@ -358,16 +360,20 @@ export default function HistoryPage() { {dataQuery.isError ? dataQuery.refetch()} /> : null}
-
结果行数{result?.total.toLocaleString('zh-CN') ?? '—'}
车辆数{result?.summary.vehicleCount ?? '—'}
数据源{summarySources}
查询耗时{result ? `${result.summary.queryDurationMs} ms` : '—'}
+
结果行数{result?.total?.toLocaleString('zh-CN') ?? '—'}
车辆数{resultSummary?.vehicleCount ?? '—'}
数据源{summarySources}
查询耗时{resultSummary ? `${resultSummary.queryDurationMs} ms` : '—'}
setTrendExpanded((value) => !value)} /> {exportAllowed ? : null}{!mobileLayout ? } showClear value={customerKeyword} onChange={setCustomerKeyword} placeholder="搜索名称、账号或客户标识" /> expect(vehicles).toHaveBeenCalledTimes(1)); + expect(screen.getByRole('heading', { name: '最近上报车辆', level: 5 })).toBeInTheDocument(); + expect(await screen.findByText('当前显示 1 辆')).toBeInTheDocument(); + expect(screen.getAllByRole('listitem', { name: `打开 ${initialRealtime.plate} 车辆档案` })).toHaveLength(1); fireEvent.focus(input); expect(input).toHaveAttribute('aria-expanded', 'true'); fireEvent.change(input, { target: { value: initialRealtime.plate } }); - await waitFor(() => expect(vehicles).toHaveBeenCalled()); + await waitFor(() => expect(vehicles).toHaveBeenCalledTimes(2)); + expect(screen.getByRole('heading', { name: '匹配车辆快捷入口', level: 5 })).toBeInTheDocument(); const option = await screen.findByRole('option', { name: `${initialRealtime.plate} ${initialRealtime.vin} JT808 选择` }); expect(view.container.querySelector('#v2-vehicle-search-options.v2-vehicle-candidate-list')).toBeInTheDocument(); expect(view.container.querySelector('#v2-vehicle-search-options')).toHaveAttribute('aria-busy', 'false'); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx index 2e2a49be..6cc8259f 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx @@ -43,6 +43,12 @@ function resetWorkspaceScroll() { content.scrollTo?.({ top: 0, left: 0, behavior: 'auto' }); } +function vehicleLastSeenLabel(value?: string) { + if (!value) return '暂无上报时间'; + const normalized = value.replace('T', ' '); + return normalized.length >= 16 ? `${normalized.slice(5, 16)} 更新` : normalized; +} + function VehicleSearch() { const navigate = useNavigate(); const { session } = usePlatformSession(); @@ -60,11 +66,11 @@ function VehicleSearch() { const candidates = useQuery({ queryKey: ['vehicle-search-options', candidateParams.toString()], queryFn: ({ signal }) => api.vehicles(candidateParams, signal), - enabled: candidatesOpen, staleTime: 30_000, gcTime: QUERY_MEMORY.optionGcTime }); const options = useMemo(() => mergeVehicleCandidates(candidates.data?.items ?? []), [candidates.data?.items]); + const quickVehicles = useMemo(() => options.slice(0, mobileLayout ? 5 : 6), [mobileLayout, options]); useEffect(() => () => window.clearTimeout(closeTimerRef.current), []); const openCandidates = () => { window.clearTimeout(closeTimerRef.current); @@ -136,6 +142,41 @@ function VehicleSearch() {
{canAdminister(session) ? : null} + + + {candidates.isFetching && !candidates.data + ?
正在读取授权车辆…
+ : candidates.isError + ?
{candidates.error instanceof Error ? candidates.error.message : '授权车辆加载失败'}
+ : quickVehicles.length + ?
+ {quickVehicles.map((vehicle) => )} +
+ : } +
{syncOpen ? 正在加载主档同步工具…}> setSyncOpen(false)} /> : null} ; } diff --git a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts index 13519285..aa21bbdc 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -128,8 +128,8 @@ describe('V2 production entry', () => { expect(corePageSources.HistoryPage).toContain('
'); expect(corePageSources.HistoryPage).not.toContain('job) => { expect(corePageSources.AlertsPage).toContain(' { expect(corePageSources.AccessPage).toContain('车辆'); @@ -226,7 +226,7 @@ describe('V2 production entry', () => { expect(corePageSources.UsersPage).toContain(''); @@ -325,8 +325,8 @@ describe('V2 production entry', () => { expect(corePageSources.StatisticsPage).toContain(' { + const view = render(<> + + + ); + + expect(screen.getAllByRole('status')).toHaveLength(2); + expect(screen.getByText('正在加载轨迹回放')).toBeInTheDocument(); + expect(screen.getByText('正在查询里程')).toBeInTheDocument(); + expect(view.container.querySelector('.v2-state-surface.is-page.is-loading')).toBeInTheDocument(); + expect(view.container.querySelector('.v2-state-surface.is-panel.is-loading')).toBeInTheDocument(); +}); + +test('keeps empty states consistent while allowing a clear next action', () => { + const action = vi.fn(); + const view = render(<> + 创建账号} /> + + ); + + expect(view.container.querySelectorAll('.v2-state-empty.semi-empty')).toHaveLength(2); + fireEvent.click(screen.getByRole('button', { name: '创建账号' })); + expect(action).toHaveBeenCalledTimes(1); +}); + +test('announces recoverable errors and exposes a Semi retry action', () => { + const retry = vi.fn(); + render(); + + expect(screen.getByRole('alert')).toHaveTextContent('数据暂时无法加载'); + expect(screen.getByRole('alert')).toHaveTextContent('服务暂时不可用'); + const button = screen.getByRole('button', { name: '重试' }); + expect(button).toHaveClass('semi-button'); + fireEvent.click(button); + expect(retry).toHaveBeenCalledTimes(1); +}); diff --git a/vehicle-data-platform/apps/web/src/v2/shared/AsyncState.tsx b/vehicle-data-platform/apps/web/src/v2/shared/AsyncState.tsx index 06699eb6..b1f7d83a 100644 --- a/vehicle-data-platform/apps/web/src/v2/shared/AsyncState.tsx +++ b/vehicle-data-platform/apps/web/src/v2/shared/AsyncState.tsx @@ -1,23 +1,88 @@ import { IconAlertTriangle, IconRefresh } from '@douyinfe/semi-icons'; import { Button, Empty, Spin, Typography } from '@douyinfe/semi-ui'; +import type { ReactNode } from 'react'; -export function PageLoading({ label = '正在加载车辆数据' }: { label?: string }) { - return
-
{label}导航与筛选状态已保留
-
+type StateScope = 'page' | 'panel' | 'inline'; +type StateKind = 'loading' | 'empty' | 'error'; + +function stateRole(kind: StateKind) { + return kind === 'error' ? 'alert' : 'status'; +} + +function StateSurface({ + kind, + scope, + title, + description, + className = '', + action, + compact = false +}: { + kind: StateKind; + scope: StateScope; + title: string; + description?: string; + className?: string; + action?: ReactNode; + compact?: boolean; +}) { + const classes = ['v2-state-surface', `is-${kind}`, `is-${scope}`, compact ? 'is-compact' : '', className].filter(Boolean).join(' '); + + return
+ {kind === 'empty' + ? {action} + :
+ {kind === 'loading' ? : } +
{title}{description ? {description} : null}
+ {action ?
{action}
: null} +
} + {kind === 'loading' ? : null}
; } +export function PageLoading({ label = '正在加载车辆数据' }: { label?: string }) { + return ; +} + +export function PanelLoading({ + title = '正在读取数据', + description = '当前结果返回后会自动更新。', + className = '', + compact = false +}: { + title?: string; + description?: string; + className?: string; + compact?: boolean; +}) { + return ; +} + +export function PanelEmpty({ + title, + description, + className = '', + action +}: { + title: string; + description?: string; + className?: string; + action?: ReactNode; +}) { + return ; +} + export function InlineError({ message, onRetry }: { message: string; onRetry?: () => void }) { - return ( -
- - {message} - {onRetry ? : null} -
- ); + return } aria-label="重试" onClick={onRetry}>重试 : null} + />; } export function EmptyState({ title = '暂无符合条件的车辆' }: { title?: string }) { - return
; + return ; } diff --git a/vehicle-data-platform/apps/web/src/v2/styles/v2.css b/vehicle-data-platform/apps/web/src/v2/styles/v2.css index 675d9cfd..e057e5e4 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/v2.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/v2.css @@ -10087,12 +10087,14 @@ button, a { -webkit-tap-highlight-color: transparent; } .v2-vehicle-search-page { align-content: start; place-items: start center; + gap: 14px; overflow: auto; background: var(--v2-bg); - padding: clamp(32px, 6vh, 64px) var(--v2-page-gutter) 32px; + padding: clamp(24px, 4vh, 40px) var(--v2-page-gutter) 32px; } -.v2-vehicle-search-card.semi-card { - width: min(900px, 100%); +.v2-vehicle-search-card.semi-card, +.v2-vehicle-recent-card.semi-card { + width: min(1040px, 100%); border-color: var(--v2-surface-border); border-radius: 14px; box-shadow: var(--v2-surface-shadow); @@ -10201,6 +10203,105 @@ button, a { -webkit-tap-highlight-color: transparent; } width: max-content; margin: 16px 0 0 auto; } +.v2-vehicle-recent-card > .semi-card-body { + overflow: hidden; + border-radius: inherit; +} +.v2-vehicle-recent-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + padding: 12px; +} +.v2-vehicle-recent-item.semi-button { + display: block; + width: 100%; + min-width: 0; + height: auto; + min-height: 82px; + border: 1px solid #e3eaf2; + border-radius: 10px; + background: #fbfcfe; + padding: 11px 34px 10px 12px; + color: #26374d; + text-align: left; + transition: border-color .16s ease, background-color .16s ease, box-shadow .16s ease, transform .16s ease; +} +.v2-vehicle-recent-item.semi-button:hover, +.v2-vehicle-recent-item.semi-button:focus-visible { + border-color: #b8d1fb; + background: #f5f9ff; + box-shadow: 0 6px 18px rgba(33, 102, 217, .08); + transform: translateY(-1px); +} +.v2-vehicle-recent-item .semi-button-content { + position: relative; + display: grid; + min-width: 0; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + align-items: center; + gap: 8px; +} +.v2-vehicle-recent-identity { + display: grid; + min-width: 0; + gap: 3px; +} +.v2-vehicle-recent-identity strong { + overflow: hidden; + color: #182438; + font-size: 14px; + font-weight: 720; + text-overflow: ellipsis; + white-space: nowrap; +} +.v2-vehicle-recent-identity small, +.v2-vehicle-recent-status small { + overflow: hidden; + color: #8391a4; + font-size: 9px; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; +} +.v2-vehicle-recent-status { + display: grid; + justify-items: end; + gap: 4px; +} +.v2-vehicle-recent-protocols { + display: flex; + min-width: 0; + gap: 4px; +} +.v2-vehicle-recent-protocols .semi-tag, +.v2-vehicle-recent-status .semi-tag { + margin: 0; +} +.v2-vehicle-recent-arrow { + position: absolute; + top: 50%; + right: -24px; + color: #9aabba; + transform: translateY(-50%); +} +.v2-vehicle-recent-state { + display: flex; + min-height: 128px; + align-items: center; + justify-content: center; + gap: 9px; + color: #718096; + font-size: 12px; +} +.v2-vehicle-recent-state.is-error { + flex-direction: column; + color: var(--semi-color-danger); +} +.v2-vehicle-recent-empty.semi-empty { + padding: 22px 16px 28px; +} @media (max-width: 680px) { .v2-vehicle-search-page { @@ -10212,7 +10313,8 @@ button, a { -webkit-tap-highlight-color: transparent; } width: auto; flex: 1 1 100%; } - .v2-vehicle-search-card.semi-card { + .v2-vehicle-search-card.semi-card, + .v2-vehicle-recent-card.semi-card { width: 100%; min-width: 0; border-radius: 12px; @@ -10290,6 +10392,34 @@ button, a { -webkit-tap-highlight-color: transparent; } justify-content: center; margin-top: 12px; } + .v2-vehicle-recent-grid { + grid-template-columns: 1fr; + gap: 0; + padding: 4px 10px 8px; + } + .v2-vehicle-recent-item.semi-button { + min-height: 62px; + border: 0; + border-bottom: 1px solid #edf1f6; + border-radius: 0; + background: transparent; + padding: 8px 28px 8px 2px; + } + .v2-vehicle-recent-item.semi-button:last-child { + border-bottom: 0; + } + .v2-vehicle-recent-item.semi-button:hover, + .v2-vehicle-recent-item.semi-button:focus-visible { + background: #f5f9ff; + box-shadow: none; + transform: none; + } + .v2-vehicle-recent-identity strong { + font-size: 13px; + } + .v2-vehicle-recent-arrow { + right: -20px; + } } /* Semi UI history hierarchy: keep evidence rows primary and aggregate trends explicitly on demand. */ 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 0e17319a..94453d5f 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -2358,3 +2358,286 @@ font-size: 10px; } } + +/* Unified Semi UI state language: page, panel and inline recovery surfaces. */ +.v2-state-surface { + box-sizing: border-box; + width: 100%; + min-width: 0; + color: #607086; +} + +.v2-state-surface.is-page { + display: grid; + min-height: 100%; + align-content: center; + justify-items: center; + gap: 24px; + background: + radial-gradient(circle at 50% 35%, rgba(18, 104, 243, .06), transparent 32%), + var(--v2-bg); + padding: clamp(24px, 5vw, 54px); +} + +.v2-state-surface.is-panel { + position: relative; + inset: auto; + display: grid; + min-height: 164px; + align-content: center; + justify-items: center; + gap: 18px; + border: 0; + background: linear-gradient(180deg, rgba(249, 251, 254, .72), rgba(255, 255, 255, .98)); + padding: 22px; +} + +.v2-state-surface.is-inline { + display: flex; + min-height: 52px; + align-items: center; + border: 1px solid #dfe7f1; + border-radius: 10px; + background: rgba(255, 255, 255, .96); + padding: 9px 12px; +} + +.v2-state-surface.is-error.is-inline { + border-color: #f3c7c4; + background: #fff8f7; + color: #b42318; +} + +.v2-state-message { + display: flex; + min-width: 0; + align-items: center; + justify-content: center; + gap: 12px; +} + +.v2-state-icon { + display: grid; + width: 38px; + height: 38px; + flex: 0 0 auto; + place-items: center; + border-radius: 11px; + background: #edf4ff; + color: var(--v2-blue); +} + +.v2-state-surface.is-error .v2-state-icon { + background: #fff0ef; + color: #d92d20; +} + +.v2-state-copy { + display: grid; + min-width: 0; + gap: 4px; +} + +.v2-state-copy > .semi-typography { + display: block; + margin: 0; +} + +.v2-state-copy > .semi-typography-primary { + color: #293a50; + font-size: 13px; + font-weight: 700; +} + +.v2-state-copy > .semi-typography-tertiary, +.v2-state-copy > .semi-typography-danger { + overflow: hidden; + color: #7e8c9f; + font-size: 10px; + line-height: 1.55; + text-overflow: ellipsis; +} + +.v2-state-copy > .semi-typography-danger { + color: #a63a32; +} + +.v2-state-action { + display: flex; + flex: 0 0 auto; + margin-left: 10px; +} + +.v2-state-action > .semi-button { + min-height: 34px; + border-radius: 8px; + font-weight: 650; +} + +.v2-state-skeleton { + display: grid; + width: min(780px, 100%); + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.v2-state-skeleton > i { + min-height: 58px; + border: 1px solid #eaf0f6; + border-radius: 10px; + background: linear-gradient(100deg, #f3f6fa 24%, #eaf1f8 38%, #f3f6fa 54%); + background-size: 300% 100%; + animation: v2-skeleton 1.4s ease infinite; +} + +.v2-state-surface.is-page .v2-state-skeleton { + width: min(960px, 100%); +} + +.v2-state-surface.is-page .v2-state-skeleton > i { + min-height: 92px; +} + +.v2-state-surface.is-page .v2-state-skeleton > i:last-child { + min-height: 240px; + grid-column: 1 / -1; +} + +.v2-state-surface.is-panel.is-compact { + position: sticky; + z-index: 3; + top: 0; + display: flex; + width: 100%; + min-height: 42px; + justify-content: center; + border-bottom: 1px solid #dfe8f2; + background: rgba(248, 251, 255, .94); + padding: 5px 12px; + backdrop-filter: blur(8px); +} + +.v2-state-surface.is-panel.is-compact .v2-state-icon { + width: 28px; + height: 28px; + border-radius: 8px; +} + +.v2-state-surface.is-panel.is-compact .v2-state-copy { + display: flex; + align-items: baseline; + gap: 7px; +} + +.v2-state-surface.is-panel.is-compact .v2-state-skeleton { + display: none; +} + +.v2-state-empty.semi-empty { + box-sizing: border-box; + width: 100%; + min-height: 140px; + justify-content: center; + padding: 20px; +} + +.v2-state-empty .semi-empty-image { + height: 54px; + margin-bottom: 10px; + opacity: .8; +} + +.v2-state-empty .semi-empty-title { + color: #33465d; + font-size: 13px; + font-weight: 700; +} + +.v2-state-empty .semi-empty-description { + max-width: 440px; + margin-top: 6px; + color: #8491a3; + font-size: 10px; + line-height: 1.6; +} + +.v2-state-empty .semi-empty-content { + margin-top: 14px; +} + +.v2-state-surface.is-inline .v2-state-empty.semi-empty { + min-height: 50px; + align-items: flex-start; + padding: 4px 0; +} + +.v2-state-surface.is-inline .v2-state-empty .semi-empty-image, +.v2-state-surface.is-inline .v2-state-empty .semi-empty-description { + display: none; +} + +@media (max-width: 680px) { + .v2-state-surface.is-page { + gap: 18px; + padding: 18px 12px 88px; + } + + .v2-state-surface.is-panel { + min-height: 142px; + padding: 18px 12px; + } + + .v2-state-message { + max-width: 100%; + gap: 9px; + } + + .v2-state-icon { + width: 34px; + height: 34px; + border-radius: 9px; + } + + .v2-state-copy > .semi-typography-primary { + font-size: 12px; + } + + .v2-state-copy > .semi-typography-tertiary, + .v2-state-copy > .semi-typography-danger { + font-size: 9px; + } + + .v2-state-skeleton { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .v2-state-skeleton > i:last-child { + grid-column: 1 / -1; + } + + .v2-state-surface.is-page .v2-state-skeleton > i { + min-height: 72px; + } + + .v2-state-surface.is-page .v2-state-skeleton > i:last-child { + min-height: 190px; + } + + .v2-state-surface.is-inline { + align-items: stretch; + padding: 9px; + } + + .v2-state-surface.is-inline .v2-state-message { + width: 100%; + align-items: flex-start; + } + + .v2-state-surface.is-inline .v2-state-action { + margin-left: auto; + } + + .v2-state-surface.is-panel.is-compact .v2-state-copy > .semi-typography-tertiary { + display: none; + } +}