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