refactor: unify embedded BI states

This commit is contained in:
kkfluous
2026-08-07 16:37:53 +08:00
parent 671a7a8224
commit 55297a6240
6 changed files with 48 additions and 33 deletions
+1
View File
@@ -150,6 +150,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0)
- 已完成版本化指标目录和四模块入口。
- 已完成里程、电能、氢能主要 URL 状态。
- 已完成 ETC 记录/账单视图、日期、搜索和分页 URL 状态,以及超范围页码自动纠正。
- 已统一共享加载、空数据和故障组件的顶层卡片与明细内联边界,消除能源明细中的嵌套状态卡片。
- 已完成并发同参 GET 去重。
### 阶段 B:核心下钻闭环
+25 -4
View File
@@ -233,9 +233,20 @@ export function SkeletonBlock({ className }: { className?: string }) {
return <div className={cn('overflow-hidden rounded-xl bg-slate-100 shimmer', className)} />;
}
export function LoadingState({ label = '数据加载中' }: { label?: string }) {
type StateVariant = 'card' | 'inline';
export function LoadingState({
label = '数据加载中',
variant = 'card',
}: {
label?: string;
variant?: StateVariant;
}) {
return (
<div className="rounded-2xl border border-slate-100 bg-white p-8 text-center shadow-sm">
<div className={cn(
'p-8 text-center',
variant === 'card' && 'rounded-2xl border border-slate-100 bg-white shadow-sm',
)}>
<Loader2 className="mx-auto mb-3 animate-spin text-blue-500" size={22} />
<div className="text-xs font-black text-slate-500">{label}</div>
</div>
@@ -245,12 +256,17 @@ export function LoadingState({ label = '数据加载中' }: { label?: string })
export function EmptyState({
title = '暂无数据',
description = '换个筛选条件或稍后再试',
variant = 'card',
}: {
title?: string;
description?: string;
variant?: StateVariant;
}) {
return (
<div className="rounded-2xl border border-slate-100 bg-white p-8 text-center shadow-sm">
<div className={cn(
'p-8 text-center',
variant === 'card' && 'rounded-2xl border border-slate-100 bg-white shadow-sm',
)}>
<SearchX className="mx-auto mb-3 text-slate-300" size={26} />
<div className="text-sm font-black text-slate-700">{title}</div>
<div className="mt-1 text-xs font-bold text-slate-400">{description}</div>
@@ -263,14 +279,19 @@ export function ErrorState({
title = '加载失败',
onRetry,
retrying = false,
variant = 'card',
}: {
message: string;
title?: string;
onRetry?: () => void;
retrying?: boolean;
variant?: StateVariant;
}) {
return (
<div className="rounded-2xl border border-rose-100 bg-rose-50 p-4 text-rose-700 shadow-sm">
<div className={cn(
'bg-rose-50 p-4 text-rose-700',
variant === 'card' && 'rounded-2xl border border-rose-100 shadow-sm',
)}>
<div className="flex items-start gap-2">
<AlertCircle size={18} className="mt-0.5 shrink-0" />
<div className="min-w-0 flex-1">
+4 -5
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { ChevronLeft, ChevronRight, Loader2, ReceiptText, Route, Search } from 'lucide-react';
import { ChevronLeft, ChevronRight, ReceiptText, Route, Search } from 'lucide-react';
import Blur from '../../components/Blur';
import { ErrorState, LoadingState } from '../../components/ui/surface';
import { fetchEtcBills, fetchEtcRecords } from './api';
import type { EtcBillResponse, EtcTollRecordResponse } from './types';
import type { EtcDetailView, EtcDrillContext } from './etc-drill-context';
@@ -165,11 +166,9 @@ export default function ETCDetails({
) : null}
{error ? (
<div className="m-4 rounded-lg border border-rose-100 bg-rose-50 p-3 text-xs font-bold text-rose-700">{error}</div>
<ErrorState message={error} variant="inline" />
) : loading && !activeData ? (
<div className="flex items-center justify-center gap-2 py-14 text-xs font-bold text-slate-400">
<Loader2 size={16} className="animate-spin" /> ETC
</div>
<LoadingState label="正在加载 ETC 明细" variant="inline" />
) : view === 'records' && records ? (
records.items.length === 0 ? (
<div className="py-14 text-center">
+6 -6
View File
@@ -311,11 +311,11 @@ export default function ElectricDaily() {
<span className="text-right"></span>
</div>
{error ? (
<div className="p-3"><ErrorState message={error} /></div>
<ErrorState message={error} variant="inline" />
) : months === null ? (
<div className="p-3"><LoadingState label="正在加载充电明细" /></div>
<LoadingState label="正在加载充电明细" variant="inline" />
) : months.length === 0 ? (
<div className="p-3"><EmptyState title="暂无充电数据" description="请切换时间范围或车辆归属" /></div>
<EmptyState title="暂无充电数据" description="请切换时间范围或车辆归属" variant="inline" />
) : months.map(m => {
const open = openMonths.has(m.month);
return (
@@ -369,11 +369,11 @@ export default function ElectricDaily() {
{selectedDate === d.date && (
<div className="border-t border-blue-100 bg-blue-50/40 px-3 py-3 md:pl-9">
{ordersError ? (
<ErrorState message={ordersError} />
<ErrorState message={ordersError} variant="inline" />
) : !orders || orders.date !== d.date ? (
<LoadingState label="正在加载充电订单" />
<LoadingState label="正在加载充电订单" variant="inline" />
) : orders.items.length === 0 ? (
<EmptyState title="当日无充电订单" description="当前车辆归属下没有订单明细" />
<EmptyState title="当日无充电订单" description="当前车辆归属下没有订单明细" variant="inline" />
) : (
<div className="overflow-hidden rounded-lg border border-blue-100 bg-white">
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-blue-100 bg-blue-50 px-3 py-2 text-[10px] font-bold text-blue-700">
+2 -2
View File
@@ -472,9 +472,9 @@ export default function HydrogenDaily() {
</div>
{/* 主行 + 子行 */}
{rows === null ? (
<div className="p-3"><LoadingState label="正在加载加氢明细" /></div>
<LoadingState label="正在加载加氢明细" variant="inline" />
) : rows.length === 0 ? (
<div className="p-3"><EmptyState title="暂无加氢数据" description="请切换时间范围或车辆归属" /></div>
<EmptyState title="暂无加氢数据" description="请切换时间范围或车辆归属" variant="inline" />
) : rows.map(r => {
const open = expanded.has(r.date);
const isAbnormal = Math.abs(r.chainPct) >= 0.3;
+10 -16
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { ChevronLeft, ChevronRight, Loader2, ReceiptText, RefreshCw, X } from 'lucide-react';
import { ChevronLeft, ChevronRight, ReceiptText, X } from 'lucide-react';
import Blur from '../../components/Blur';
import { EmptyState, ErrorState, LoadingState } from '../../components/ui/surface';
import { fetchHydrogenOrders } from './api';
import type { CustomerType, HydrogenOrderResponse } from './types';
@@ -81,23 +82,16 @@ export default function HydrogenOrders({
) : null}
{error ? (
<div className="flex flex-col items-center px-4 py-8 text-center">
<div className="text-xs font-black text-rose-700"></div>
<div className="mt-1 text-[10px] font-bold text-rose-500">{error}</div>
<button
type="button"
onClick={() => setRetryKey(key => key + 1)}
className="mt-3 inline-flex h-8 items-center gap-1.5 rounded-lg border border-rose-100 bg-rose-50 px-3 text-[10px] font-black text-rose-700"
>
<RefreshCw size={12} />
</button>
</div>
<ErrorState
title="订单数据暂不可用"
message={error}
onRetry={() => setRetryKey(key => key + 1)}
variant="inline"
/>
) : loading && !data ? (
<div className="flex items-center justify-center gap-2 py-10 text-xs font-bold text-slate-400">
<Loader2 size={15} className="animate-spin" />
</div>
<LoadingState label="正在加载加氢订单" variant="inline" />
) : data?.items.length === 0 ? (
<div className="py-10 text-center text-xs font-bold text-slate-400"></div>
<EmptyState title="当前筛选下无加氢订单" description="请切换日期、车辆归属或实体范围" variant="inline" />
) : data ? (
<>
<div className="hidden overflow-x-auto md:block">