React + Ant Design 通用企业数据统计模块实战

一、模块通用功能概述

本方案为通用后台数据统计联动看板标准化实现模板,可适配工单审核、客户评价、任务质检、流程巡检等各类业务统计场景,核心通用能力:

  1. 周期聚合统计:默认近30天业务数据自动分组聚合,按评分/等级分为三类统计维度(优/达标/待优化),输出各分组总量、全量数据明细
  2. 卡片联动筛选:顶部统计卡片点击切换筛选条件,再次点击清空筛选,下方列表实时匹配对应分组数据
  3. 权限隔离渲染:基于后台权限标识控制模块整体显隐,无权限用户完全不加载组件
  4. 响应式联动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统计接口,统一参数拼接、信封响应解析、异常捕获逻辑

  1. 统一接口前缀脱敏:/v3/p/platform/v1/business/record/statistics
  2. 使用URLSearchParams安全拼接时间戳参数,规避字符串拼接漏洞
  3. 全局统一响应信封(envelope)封装,所有后端接口标准化返回格式
  4. 数据空值主动抛出业务异常,上层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三态:加载中/数据/错误

  1. useCallback缓存请求函数,避免重复渲染触发重复请求
  2. useEffect组件挂载自动请求,对外暴露refresh手动刷新方法
  3. 异常信息分层降级:后端返回错误文案 > 全局默认提示
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

管理当前激活筛选分组,控制表格数据渲染,支持开关启用、清空筛选重置

  1. activeFilter存储当前选中分组,null代表无筛选
  2. 提供enabled入参,支持模块权限关闭时停止数据请求
  3. 筛选置空时直接清空列表,无需等待接口响应,交互更顺滑
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 通用统计卡片组件

纯展示交互组件,仅接收外部传入筛选状态与回调,无内部数据请求,完全解耦

核心交互逻辑:

  1. 加载中展示Antd Spin占位
  2. 数量为0的分组按钮置灰禁用,禁止点击
  3. 当前选中分组高亮样式,再次点击同一分组自动清空筛选
  4. 完善无障碍属性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模拟服务通用设计(脱敏)

本地调试模拟接口数据,逻辑通用,可直接迁移至任意统计业务:

  1. 模拟登录操作人信息,从Cookie读取当前登录账号
  2. 自动维护记录评分、总分、平均分、最低分聚合字段
  3. 统一追加操作时间线日志,保证Mock数据结构和线上接口完全对齐
    核心逻辑:新增一条评分记录后,自动重算分组聚合数值,无需手动维护统计量

八、通用权限控制方案

  1. 后台统一返回用户权限标识字符串(示例通用权限key:platform.biz.stats
  2. 全局权限工具函数checkPermission(权限标识)判断用户权限
  3. 无权限时直接不渲染整个统计模块,而非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 通用性能优化手段

  1. 数据复用:统计卡片、表格共用同一份接口返回明细,不重复请求
  2. 权限条件渲染:无权限直接销毁组件,减少JS执行与DOM节点
  3. 筛选快速重置:清空筛选时直接置空表格状态,不等待接口返回
  4. 纯函数工具:计数、过滤逻辑无副作用,配合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    # 本地模拟接口服务

整体方案通用复用总结

该套实现完全剥离专属业务名词、接口路径、权限标识、业务字段,仅保留统计+筛选联动 通用技术逻辑,可一键复用于:质检统计、工单评价、客户回访评分、巡检打分等任意后台数据看板场景。

核心技术落地要点:

  1. 分层解耦:UI、逻辑、请求、数据模型四层分离,单一职责;
  2. 前端本地筛选:优化交互体验,降低后端接口压力;
  3. TypeScript泛型抽象:工具函数跨业务复用,保障类型安全;
  4. 完整状态兜底:加载、空数据、报错、禁用、选中多状态全覆盖;
  5. 配置化扩展:新增统计分组、自定义统计周期无需改动核心组件代码。
相关推荐
悟空瞎说3 小时前
Three.js 架构实践:用 MVC 模式组织你的 3D 应用
前端·javascript
光影少年3 小时前
react navite本地存储:AsyncStorage、MMKV、文件存储
前端·react native·react.js
曹牧3 小时前
C#:问号
前端·数据库·c#
windliang3 小时前
Claude Code 源码分析(十一):一段会话怎样保存、恢复与续写
前端·javascript·人工智能
掘金一周3 小时前
掘友们,你们现在下班了都玩啥游戏?| 沸点周刊 8.13
前端·人工智能·后端
HjhIron3 小时前
别再傻等后端接口了!React + Vite + Mock 打造真正的前后端分离开发体验
前端
用户921080262863 小时前
从 CSS 到 Vue Scoped:一篇讲透前端样式的工程化认知
前端
扫一屋5 小时前
Element Plus 源码级深度定制:不 Fork、不改包,用 Vite 插件实现组件行为运行时改造
前端
BigTopOne6 小时前
KOOM 学习计划
前端
自进化Agent智能体6 小时前
Hermes 技能系统详解——安装、使用与浏览
前端