一、模块通用功能概述
本方案为通用后台数据统计联动看板标准化实现模板,可适配工单审核、客户评价、任务质检、流程巡检等各类业务统计场景,核心通用能力:
- 周期聚合统计:默认近30天业务数据自动分组聚合,按评分/等级分为三类统计维度(优/达标/待优化),输出各分组总量、全量数据明细
- 卡片联动筛选:顶部统计卡片点击切换筛选条件,再次点击清空筛选,下方列表实时匹配对应分组数据
- 权限隔离渲染:基于后台权限标识控制模块整体显隐,无权限用户完全不加载组件
- 响应式联动UI:统计卡片、数据表格双向联动,统一处理加载、空数据、异常报错状态
二、通用分层架构与数据流
页面组件层级
bash
┌─────────────────────────────────────────────────────────┐
│ DashboardPage 工作台首页 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ DataStatsBar 顶部统计卡片栏 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 优档分组 │ │达标分组 │ │待优化分组│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │ onFilterChange 筛选回调 │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ FilteredDataTable 筛选数据表格 │ │
│ │ ┌──────┬──────┬──────┬──────┬──────┬──────┐ │ │
│ │ │编号 │名称 │等级分│备注 │操作人│时间 │ │ │
│ │ └──────┴──────┴──────┴──────┴──────┴──────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
标准数据流链路
接口请求层 → 全局统计数据Hook → 顶部统计卡片组件 → 筛选数据Hook → 业务数据表格组件
核心设计:一次请求获取全量聚合数据+明细数据,前端内存筛选,无需重复调接口
三、接口层通用标准化设计(脱敏通用命名)
3.1 通用TS类型定义
剥离业务专属名词,抽象通用数据结构,适配所有评分类统计场景
typescript
// src/api/business-stats.ts 通用业务统计接口
// 单条业务明细通用结构
export type BizRecordItem = {
id: number;
record_id: string;
record_name: string;
operator_uid: string;
operator_name: string;
score: number; // 分级评分核心字段
remark: string;
update_timestamp: number;
};
// 接口返回顶层统一结构:聚合统计+原始明细
export type BizStatsResponse = {
score_group_count: { score: number; count: number }[];
total_record_num: number;
record_list: BizRecordItem[];
};
// 接口请求时间区间参数
export type BizStatsQueryParams = {
start_time?: number;
end_time?: number;
};
// 筛选分组枚举:通用三级分类
export type ScoreGroupKey = "high" | "normal" | "low";
3.2 基础统计查询接口封装
通用GET统计接口,统一参数拼接、信封响应解析、异常捕获逻辑
- 统一接口前缀脱敏:
/v3/p/platform/v1/business/record/statistics - 使用
URLSearchParams安全拼接时间戳参数,规避字符串拼接漏洞 - 全局统一响应信封(envelope)封装,所有后端接口标准化返回格式
- 数据空值主动抛出业务异常,上层Hook统一捕获展示
typescript
const STATS_API_PATH = "/v3/p/platform/v1/business/record/statistics";
export async function fetchBizStatsData(params?: BizStatsQueryParams): Promise<BizStatsResponse> {
const searchParams = new URLSearchParams();
if (params?.start_time) searchParams.set("start_time", String(params.start_time));
if (params?.end_time) searchParams.set("end_time", String(params.end_time));
const queryStr = searchParams.toString();
const finalUrl = queryStr ? `${STATS_API_PATH}?${queryStr}` : STATS_API_PATH;
// 全局统一请求封装,处理token、响应拦截
const resEnvelope = await requestWithGlobalWrapper<BizStatsResponse>(finalUrl, { method: "GET" });
if (!resEnvelope.data) throw new Error("获取业务统计数据为空");
return resEnvelope.data;
}
3.3 前端内存筛选封装函数
不复发网络请求,基于已拉取全量明细做本地过滤,提升交互速度
typescript
export async function getFilteredRecordList(group: ScoreGroupKey) {
// 复用基础统计接口,只请求一次全量数据
const allStats = await fetchBizStatsData(getDefault30DayRange());
// 模型层通用过滤工具处理分组
const filterList = filterRecordByScoreGroup(allStats.record_list, group);
return { list: filterList, total: filterList.length };
}
四、通用自定义Hook分层设计
4.1 useBizStats 全局统计数据Hook
负责拉取周期统计基础数据,封装完整UI三态:加载中/数据/错误
useCallback缓存请求函数,避免重复渲染触发重复请求useEffect组件挂载自动请求,对外暴露refresh手动刷新方法- 异常信息分层降级:后端返回错误文案 > 全局默认提示
typescript
// src/dashboard/hooks/use-biz-stats.ts
export function useBizStats() {
const [state, setState] = useState<{
data: BizStatsResponse | null;
loading: boolean;
error: string | null;
}>({ data: null, loading: false, error: null });
const loadStats = useCallback(async () => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const data = await fetchBizStatsData(getDefault30DayRange());
setState({ data, loading: false, error: null });
} catch (err) {
const errMsg = err instanceof Error ? err.message : "统计数据加载失败";
setState(prev => ({ ...prev, loading: false, error: errMsg }));
}
}, []);
useEffect(() => { void loadStats(); }, [loadStats]);
return { ...state, refresh: loadStats };
}
通用30天时间范围计算工具(通用工具函数)
统一和后端约定秒级Unix时间戳,自动计算近30天起止时间,兼容任意业务统计周期需求
typescript
export function getDefault30DayRange(): BizStatsQueryParams {
const today = new Date();
// 结束:当日23:59:59
const endDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59);
// 起始:往前推29天,合计30天数据
const startDate = new Date(endDate);
startDate.setDate(startDate.getDate() - 29);
return {
start_time: Math.floor(startDate.getTime() / 1000),
end_time: Math.floor(endDate.getTime() / 1000),
};
}
4.2 useFilteredRecordList 筛选联动Hook
管理当前激活筛选分组,控制表格数据渲染,支持开关启用、清空筛选重置
activeFilter存储当前选中分组,null代表无筛选- 提供
enabled入参,支持模块权限关闭时停止数据请求 - 筛选置空时直接清空列表,无需等待接口响应,交互更顺滑
typescript
export function useFilteredRecordList(options?: { enabled?: boolean }) {
const { enabled = true } = options || {};
const [activeFilterKey, setActiveFilterKey] = useState<ScoreGroupKey | null>(null);
const [tableState, setTableState] = useState({ list: [], loading: false, error: null, total: 0 });
const fetchFilterData = useCallback(async (group: ScoreGroupKey) => {
setTableState(prev => ({ ...prev, loading: true, error: null }));
try {
const res = await getFilteredRecordList(group);
setTableState({ list: res.list, loading: false, error: null, total: res.total });
} catch (err) {
const msg = err instanceof Error ? err.message : "筛选数据加载失败";
setTableState(prev => ({ ...prev, loading: false, error: msg }));
}
}, []);
// 监听筛选标识变化,自动重新过滤
useEffect(() => {
if (!enabled || !activeFilterKey) {
setTableState({ list: [], loading: false, error: null, total: 0 });
return;
}
void fetchFilterData(activeFilterKey);
}, [activeFilterKey, enabled, fetchFilterData]);
// 封装筛选切换方法,对外暴露
const changeFilter = (key: ScoreGroupKey | null) => setActiveFilterKey(key);
return { ...tableState, activeFilterKey, changeFilter };
}
五、通用模型层与工具函数(业务解耦核心)
完全抽离分组规则、文案、过滤逻辑,实现业务可配置、快速扩展,不侵入组件与接口
typescript
// src/models/business/score-group.ts
// 1. 分组分值区间配置(可全局配置修改)
export const SCORE_GROUP_RANGE_MAP: Record<ScoreGroupKey, [number, number]> = {
high: [5, 5], // 高分优档
normal: [3, 4], // 达标中档
low: [1, 2], // 低分待优化
};
// 2. 分组展示文案映射,支持国际化抽取
export const SCORE_GROUP_LABEL_MAP: Record<ScoreGroupKey, string> = {
high: "优档",
normal: "达标",
low: "待优化",
};
// 3. 分组数组,循环渲染卡片使用
export const ALL_SCORE_GROUPS: ScoreGroupKey[] = ["high", "normal", "low"];
// 泛型通用过滤函数:只要包含score字段的数组均可复用
export function filterRecordByScoreGroup<T extends { score: number }>(list: T[], group: ScoreGroupKey): T[] {
const [min, max] = SCORE_GROUP_RANGE_MAP[group];
return list.filter(item => item.score >= min && item.score <= max);
}
// 泛型通用计数函数
export function countRecordByScoreGroup<T extends { score: number }>(list: T[], group: ScoreGroupKey): number {
const [min, max] = SCORE_GROUP_RANGE_MAP[group];
return list.filter(item => item.score >= min && item.score <= max).length;
}
六、通用UI组件标准化实现
6.1 DataStatsBar 通用统计卡片组件
纯展示交互组件,仅接收外部传入筛选状态与回调,无内部数据请求,完全解耦
核心交互逻辑:
- 加载中展示Antd Spin占位
- 数量为0的分组按钮置灰禁用,禁止点击
- 当前选中分组高亮样式,再次点击同一分组自动清空筛选
- 完善无障碍属性
role/aria-label,满足WCAG规范
typescript
// src/dashboard/components/data-stats-bar/index.tsx
type StatsBarProps = {
activeFilter: ScoreGroupKey | null;
onFilterChange: (key: ScoreGroupKey | null) => void;
};
export function DataStatsBar(props: StatsBarProps) {
const { activeFilter, onFilterChange } = props;
const { data, loading } = useBizStats();
if (loading || !data) {
return <div className={styles.loadWrap}><Spin size="small" /> 统计数据加载中</div>;
}
// 基于模型层批量生成卡片配置
const cardConfigList = ALL_SCORE_GROUPS.map(key => ({
key,
label: SCORE_GROUP_LABEL_MAP[key],
count: countRecordByScoreGroup(data.record_list, key),
}));
// 卡片点击逻辑
const handleCardClick = (item: typeof cardConfigList[0]) => {
if (item.count === 0) return;
// 重复点击取消筛选
const nextKey = activeFilter === item.key ? null : item.key;
onFilterChange(nextKey);
};
return (
<div className={styles.barWrap} role="group" aria-label="业务数据统计分组">
<div className={styles.cardContainer}>
{cardConfigList.map(item => (
<button
key={item.key}
className={clsx(
styles.statCard,
activeFilter === item.key && styles.cardActive,
item.count === 0 && styles.cardDisabled
)}
onClick={() => handleCardClick(item)}
title={`${item.label},共${item.count}条数据`}
>
<span className={styles.colorDot} />
<span className={styles.cardLabel}>{item.label}</span>
<span className={styles.cardNum}>{item.count}</span>
</button>
))}
</div>
<span className={styles.tipText}>*默认统计近30天全部分类数据</span>
</div>
);
}
6.2 通用评分标签组件
统一分数-视觉映射规则,全局复用在表格、卡片、详情页
typescript
// src/common/components/score-tag.tsx
type ScoreTagProps = { score: number };
export function ScoreTag({ score }: ScoreTagProps) {
const getTagColor = (s: number) => {
if (s === 5) return "gold";
if (s >= 3) return "success";
return "error";
};
return <Tag color={getTagColor(score)}>{score}分</Tag>;
}
七、Mock模拟服务通用设计(脱敏)
本地调试模拟接口数据,逻辑通用,可直接迁移至任意统计业务:
- 模拟登录操作人信息,从Cookie读取当前登录账号
- 自动维护记录评分、总分、平均分、最低分聚合字段
- 统一追加操作时间线日志,保证Mock数据结构和线上接口完全对齐
核心逻辑:新增一条评分记录后,自动重算分组聚合数值,无需手动维护统计量
八、通用权限控制方案
- 后台统一返回用户权限标识字符串(示例通用权限key:
platform.biz.stats) - 全局权限工具函数
checkPermission(权限标识)判断用户权限 - 无权限时直接不渲染整个统计模块,而非CSS隐藏,减少DOM渲染开销
tsx
// 工作台页面权限使用
const canViewStats = checkPermission("platform.biz.stats");
return (
<div className="dashboard-container">
{canViewStats && (
<>
<DataStatsBar activeFilter={filterKey} onFilterChange={changeFilter} />
<FilteredRecordTable />
</>
)}
{/* 其他页面模块 */}
</div>
);
九、标准化分层架构与技术最佳实践
9.1 四层分层规范(通用企业前端标准)
bash
┌─────────────────────────────────────┐
│ Components 展示层 │ 纯UI渲染,不处理请求、业务计算
├─────────────────────────────────────┤
│ Custom Hooks 逻辑层 │ 状态管理、接口调用、交互逻辑封装
├─────────────────────────────────────┤
│ API Layer 请求层 │ 统一接口地址、参数、响应解析、异常
├─────────────────────────────────────┤
│ Models 模型工具层 │ 类型定义、分组规则、通用计算函数
└─────────────────────────────────────┘
9.2 核心技术选型(通用无业务绑定)
React + Ant Design + TypeScript + CSS Modules + Vite
- TS泛型:通用工具函数跨业务复用,全链路类型约束
- CSS Modules:样式隔离,避免多业务模块样式冲突
- useState+useCallback:轻量状态管理,无Redux等重型状态库冗余
9.3 技术设计决策通用思路
| 技术选择 | 实现方案 | 通用优势 |
|---|---|---|
| 数据筛选 | 前端内存过滤 | 一次请求全量数据,切换筛选无网络延迟,减轻后端查询压力 |
| 状态管理 | 组件内本地state | 单模块独立,无全局状态污染,维护成本低 |
| 异常处理 | 三层降级提示 | 接口异常 → 业务错误文案 → 兜底默认提示,用户友好 |
| 周期统计 | 内置30天计算工具 | 可扩展自定义起止时间,适配7天/90天等其他统计需求 |
| 扩展能力 | 配置化分组模型 | 新增统计分组仅修改SCORE_GROUP_RANGE_MAP,无需修改组件/接口 |
9.4 通用性能优化手段
- 数据复用:统计卡片、表格共用同一份接口返回明细,不重复请求
- 权限条件渲染:无权限直接销毁组件,减少JS执行与DOM节点
- 筛选快速重置:清空筛选时直接置空表格状态,不等待接口返回
- 纯函数工具:计数、过滤逻辑无副作用,配合React缓存减少重计算
十、通用目录结构(脱敏,适配所有后台统计模块)
bash
src/
├── api/
│ └── business-stats.ts # 通用统计接口、请求函数、顶层类型
├── models/
│ └── business/
│ └── score-group.ts # 分组配置、泛型过滤/计数工具
├── dashboard/
│ ├── hooks/
│ │ ├── use-biz-stats.ts # 统计基础数据Hook
│ │ └── use-filtered-record-list.ts # 筛选联动Hook
│ └── components/
│ └── data-stats-bar/
│ ├── index.tsx # 通用统计卡片组件
│ └── index.module.less # 隔离样式
├── common/
│ └── components/
│ └── score-tag.tsx # 全局通用评分标签
└── mock/
└── business/
└── record-service.ts # 本地模拟接口服务
整体方案通用复用总结
该套实现完全剥离专属业务名词、接口路径、权限标识、业务字段,仅保留统计+筛选联动 通用技术逻辑,可一键复用于:质检统计、工单评价、客户回访评分、巡检打分等任意后台数据看板场景。
核心技术落地要点:
- 分层解耦:UI、逻辑、请求、数据模型四层分离,单一职责;
- 前端本地筛选:优化交互体验,降低后端接口压力;
- TypeScript泛型抽象:工具函数跨业务复用,保障类型安全;
- 完整状态兜底:加载、空数据、报错、禁用、选中多状态全覆盖;
- 配置化扩展:新增统计分组、自定义统计周期无需改动核心组件代码。