03 · 架构与工程化

3 阅读3分钟

大屏项目的架构核心是 「配置驱动渲染」:把布局、组件、数据源都抽成 JSON Schema,让一份代码跑多套大屏。

1. 目录结构

src/
├─ app/                  应用入口、全局布局
│   ├─ App.vue / App.tsx
│   └─ providers/        ThemeProvider、SocketProvider…
├─ screens/              一个文件夹 = 一个大屏
│   ├─ ops-monitor/      运维监控大屏
│   │   ├─ schema.ts     该大屏的配置(布局 + 组件 + 数据源)
│   │   ├─ index.vue
│   │   └─ widgets/      该大屏专属组件
│   ├─ leader-report/
│   └─ city-3d/
├─ widgets/              通用大屏组件(柱图、饼图、地图、KPI…)
│   ├─ KpiCard/
│   ├─ LineChart/
│   ├─ BarChart/
│   ├─ MapHeat/
│   └─ FlyLine/
├─ renderer/             Schema → 实际组件树的渲染引擎
│   ├─ Renderer.tsx
│   └─ resolveDataSource.ts
├─ data/                 数据层
│   ├─ http.ts
│   ├─ socket.ts
│   ├─ stores/           Pinia/Zustand 模块
│   └─ services/         按域划分的请求封装
├─ utils/
├─ themes/               主题(深色蓝、橙红、紫银…)
├─ assets/               图标、Lottie、纹理、视频
└─ main.ts

2. 配置驱动渲染(Schema-driven)

2.1 核心 Schema 形态

type DashboardSchema = {
  id: string;
  resolution: { w: number; h: number };  // 设计稿基准分辨率
  theme: 'dark-blue' | 'red' | string;
  background?: { type: 'image' | 'video' | 'color'; src?: string; color?: string };
  refreshInterval?: number;              // 全局兜底刷新
  widgets: WidgetSchema[];
};

type WidgetSchema = {
  id: string;
  type: 'kpi' | 'line' | 'bar' | 'map-heat' | 'fly-line' | 'three-city' | string;
  rect: { x: number; y: number; w: number; h: number };  // 设计稿坐标
  props?: Record<string, unknown>;        // 组件自身配置(颜色、轴等)
  dataSource?: DataSource;
};

type DataSource =
  | { kind: 'static'; data: unknown }
  | { kind: 'http';   url: string; method?: string; intervalMs?: number; transform?: string }
  | { kind: 'ws';     channel: string; transform?: string }
  | { kind: 'sse';    url: string; transform?: string };

2.2 渲染器伪代码

function Renderer({ schema }: { schema: DashboardSchema }) {
  return (
    <Stage resolution={schema.resolution} theme={schema.theme} bg={schema.background}>
      {schema.widgets.map(w => (
        <Positioned key={w.id} rect={w.rect}>
          <WidgetHost widget={w} />
        </Positioned>
      ))}
    </Stage>
  );
}

function WidgetHost({ widget }) {
  const data = useDataSource(widget.dataSource);   // 屏蔽 http/ws/sse 差异
  const Comp = widgetRegistry[widget.type];
  return <Comp data={data} {...widget.props} />;
}

好处:

  • 新增大屏 = 写一份 schema.ts,不写组件代码。
  • 切换数据源协议(HTTP → WebSocket)只改 schema,不改组件。
  • 后期可扩 GUI 编辑器,把 schema 可视化生成。

2.3 何时用 Schema、何时直接写

  • 多个大屏同质化高、组件复用度高 → Schema 驱动。
  • 单个大屏视觉极特殊、组件高度定制(如 3D 城市) → 直接写组件树,不要硬塞 Schema。

3. 状态管理

层级内容推荐方案
全局当前主题、当前大屏 ID、用户、Token、时钟Pinia store / Zustand
会话实时推送通道、连接状态单例 Manager(见 05
局部组件 props 与派生数据组件内 useState / ref
服务拉取/缓存数据TanStack Query(vue-query / react-query)

大屏不要把所有数据都塞 store。指标级数据放 query 缓存即可,store 只放跨组件、跨大屏共享的状态。

4. 数据源解析层

useDataSource(ds) 是关键抽象:

function useDataSource(ds?: DataSource) {
  switch (ds?.kind) {
    case 'static': return ds.data;
    case 'http':   return useHttpPolling(ds.url, ds.intervalMs);
    case 'ws':     return useWsChannel(ds.channel);
    case 'sse':    return useSseStream(ds.url);
    default:       return null;
  }
}

transform 字段(字符串表达式或 JSONata/jq 风格)允许 schema 里写简单数据变形,复杂逻辑仍写在组件里。

5. 主题与设计 Token

themes/
├─ dark-blue.ts
├─ red.ts
└─ index.ts
export const darkBlue = {
  bg:        '#020A1A',
  panel:     'rgba(15, 40, 90, 0.6)',
  text:      '#E6F2FF',
  textDim:   '#7FA1C8',
  primary:   '#3FA8FF',
  accent:    '#00FFE0',
  series:    ['#3FA8FF', '#00FFE0', '#FFC53F', '#FF6B6B', '#A0FF6B'],
  fontFamily: '"DIN Alternate", "PingFang SC", sans-serif',
};

ECharts 用 echarts.registerTheme('dark-blue', echartsThemeFromTokens(darkBlue)),所有图表统一调用 <EChart theme="dark-blue" />,避免一个个改 option。

6. 工程化要点

  • 构建:Vite + TS + Vue3/React + esbuild;首屏 HMR 必须 < 1s 才能高效调试大屏布局。
  • 路径别名@widgets@renderer@data@themes 让 schema 可读。
  • lint:eslint + stylelint + 大屏专属规则(禁用 position: fixed、禁用 100vh 单位等,避免拼接屏踩坑)。
  • 类型:所有 schema 必须有 TS 类型;考虑用 zod 做运行时校验(用户配错立刻报)。
  • 测试:Storybook 做 widget 开发与回归;Playwright 做整屏视觉回归。
  • mockvite-plugin-mock 出一份「演示模式」数据,断网也能演讲。

7. 演示模式(Demo Mode)

大屏经常要在断网的发布会、车里、户外演讲。一定要有 Demo Mode:

if (import.meta.env.VITE_DEMO === 'true') {
  // 接管所有数据源,吐预设动画数据
  enableMockSocket();
  enableMockHttp();
}

预设数据要带「故事感」:刻意制造高峰、回落、告警、恢复的曲线,让汇报有戏剧张力。

8. 反模式

  • 把 widget 直接 import 到大屏页面:复用低,新增大屏要复制粘贴。走注册表 + Schema
  • 数据源全塞进 widget 内部:同一个 widget 换数据源要改源码;抽到 schema
  • 把主题写进组件:换主题改 N 处;走 token + ECharts theme
  • 没有 Demo Mode:现场断网就当场翻车。