项目:html5-dataviz | 版本:v1.0 | 生成日期:2026-10-05 | 依据:源码分析
本项目是纯前端应用,"接口"分四层:①案例与框架之间的注册契约(manifest 清单 + JSON 元数据 + SFC 约定);②数据访问层 src/api/chart-examples.ts 的函数接口;③渲染链组件(EChartDemo / ChartExampleRenderer / VueSfcRenderer / SourceCodeModal)的 props/emits/expose 接口;④内部工具与组合函数接口。本章逐一给出字段表与真实示例。
ChartExampleLocalMeta定义于 src/chart-examples/manifest.ts(头部声明),是本地 SFC 的注册格式。echartsLocalManifest / antvLocalManifest 写在 manifest.ts(由 server 侧 gen-charts-local.ts 生成,⚠️ 该 server 仓库未随本项目提供,重新生成命令见文件头注释),threeLocalManifest 写在 src/chart-examples/three-manifest.ts(由 scripts/gen-three-registry.mjs 生成)。三个清单通过文件尾部的 LOCAL_LIBRARY_MANIFESTS 常量统一暴露:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | 示例唯一标识,同时约定为 SFC 文件名 {id}.vue 与 JSON 条目 id;跨库唯一(gen-three-registry.mjs 第 4 步校验) |
title | string | 是 | 中文展示名(列表卡片与详情页标题) |
category | string | 是 | 分类名,驱动侧边栏分类树与列表筛选 |
tags | string[] | 是 | 标签数组,参与搜索命中与详情页胶囊展示 |
globals | Record<string,string> | 否 | three 库专用:资产根路径注入(如 {"ASSET_HOST":"/threejs-official/"}),详情页挂载时透传给示例组件 |
// src/chart-examples/manifest.ts(节选)
{
id: 'area-simple',
title: '大数据量面积图',
category: '折线图',
tags: ['折线图', '数据缩放', '提示框', '工具箱', '图形组件'],
}
// src/chart-examples/three-manifest.ts(节选,含 globals)
{
id: 'webxr_xr_haptics',
title: 'three.js xr - haptics',
category: 'WebXR',
tags: ['Three.js', 'Three.js', '轨道控制', '空间音频(WebAudio)', 'WebXR沉浸', '实时阴影'],
globals: { ASSET_HOST: '/threejs-official/' },
}ChartExample定义于 src/types/chart-examples.ts,是列表/详情数据集(src/data/charts/{echarts,antv,three}.json)的条目契约,共 19 个字段(详细字段说明见 04-数据模型)。渲染相关的关键判别字段是 loaderKind,实际取值与含义:
| loaderKind | 库 | 条数 | 渲染路径 |
|---|---|---|---|
local | three | 989 | 详情页本地 SFC 直挂(JSON 仅作列表元信息,无代码字段) |
option | echarts | 211 | 兜底时 ChartExampleRenderer.renderEchartsOption 求值 optionCode |
api-async | echarts | 137 | 同上(脚本自带异步取数/初始化逻辑) |
antv-config | antv | 343 | renderAntvConfig 求值出 {plotClass, options} 后实例化 G2Plot |
component | antv | 8 | VueSfcRenderer 运行时编译 componentCode |
代码字段(optionCode/componentCode/initCode)遵循 scripts/slim-chart-json.mjs 的瘦身规则:有本地 SFC 的条目一律删除(详情页源码弹窗直接读 SFC 的 ?raw 源码);仅 71 个兜底条目(54 echarts + 17 antv)保留。该守恒关系被 src/data/charts/__tests__/charts-data.test.ts 锁定(CODE_KEYS 至少存在一个的判定只针对无本地 SFC 的条目)。
本地 SFC 的"接口"是目录与命名约定,而非显式注册调用:
src/chart-examples/{library}/{category-dir}/{id}.vue(echarts/antv 按分类建目录;three 镜像迁移源目录结构,相对导入原样生效,见 scripts/gen-three-registry.mjs 头注释)。EChartDemo,权威规范见 docs/CODE_TO_VCHART_GUIDE.md):<template>
<div class="demo-container"><EChartDemo :option="option" /></div>
</template>
<script setup>
import EChartDemo from '../../EChartDemo.vue'
const option = { /* ECharts option,支持 ref */ }
</script>onMounted 中 new Plot(containerRef.value, options) + render(),无需任何宿主组件。ExampleScene(或 CSS3D/SVG/WebGPU/WebXR 变体壳,均位于 src/chart-examples/three/)为根,@ready="onReady(ctx)" 中操作 ctx.scene / ctx.addUpdateCallback。/echarts-data/**(EChartDemo.fetchJson 固定拼接前缀);GeoJSON 走 /map-geojson/**;three 资产根由 manifest 的 globals.ASSET_HOST 注入。三个导出函数语义对齐原后端 REST 接口,是视图层取数的唯一入口:
| 函数 | 签名 | 入参 | 返回 | 行为要点 |
|---|---|---|---|---|
getChartExamples | (query: FindChartExamplesQuery) => Promise<PaginatedChartExampleListResult> | library 必填;page? limit? category? search? | { data: ChartExample[], pagination: { total, page, limit, totalPages } } | 内存过滤(category 精确等值;search 小写包含匹配 title+titleEn+description+tags);page 最小 1、limit 上限 200、默认 20 |
getChartExample | (id: string, library?: string) => Promise<ChartExample> | id;library 可选(详情页传库避免全库扫描) | 单条元数据 | 未命中抛 Error('图表示例不存在: {id}');省略库时按 DATASET_LOADERS 键序扫描 |
getChartCategories | (library: string) => Promise<{ data: CategoryStat[] }> | library | [{ category, count }] | Map 计数后按 count 降序排序 |
配套类型(src/types/chart-examples.ts):FindChartExamplesQuery、PaginatedChartExampleListResult、CategoryStat。懒加载实现:DATASET_LOADERS 每库一个动态 import() JSON 的加载器,datasetCache: Map<string, Promise<ChartExample[]>> 缓存 Promise(失败即清除允许重试);未知库直接返回空集(与原 filterLibrary 语义一致)。
src/chart-examples/EChartDemo.vue)| 类别 | 名称 | 类型 | 说明 |
|---|---|---|---|
| props | option | Record<string, unknown> | Ref<Record<string, unknown>> | 静态 option,支持 Ref 自动解包(normalizeOption 判 'value' in o) |
| props | onReady | (ctx: ChartContext) => Promise<Record<string,unknown> | void> | ... | 异步初始化钩子,返回 option 则代为 setOption |
| props | setup | ChartSetup = (ctx) => void | Promise<void | (() => void)> | 业务钩子;返回的清理函数在重试/卸载时统一调用 |
| props | scripts | string[] | 执行前预加载的本地脚本(/echarts-data/lib/...) |
| emits | error | [message: string] | 渲染失败时上报 |
| expose | — | — | 无 defineExpose(重试由内部按钮触发 run()) |
ChartContext(同文件导出)字段:echarts(echarts 命名空间)、myChart(底层实例延迟代理,支持 setOption/getOption/resize/on/off/dispose 及任意属性转发)、setOption(opt, opts?)、registerMap(name, geoJson)、registerTransform(t)、loadScript(src)、fetchJson(path)(固定挂 /echarts-data 前缀,非 2xx 抛错)。
src/components/chart-examples/ChartExampleRenderer.vue)| 类别 | 名称 | 类型 | 说明 |
|---|---|---|---|
| props | example | ChartExample | 必填,渲染依据(loaderKind + 代码字段 + requiresGl/requiresMap) |
| props | showToolbar | boolean,默认 true | 右上悬浮工具条(刷新/下载源码/全屏);详情页传 false(工具在页头) |
| emits | — | — | 无(错误走内部 error 态展示) |
| expose | refresh() | () => void | 清理实例后重跑 renderExample |
| 模板插槽 | — | — | 经 VueSfcRenderer 透传 #loading / #fallback |
src/components/chart-examples/VueSfcRenderer.vue)| 类别 | 名称 | 类型 | 说明 |
|---|---|---|---|
| props | source | { raw: string, componentPath?: string } | SFC 源码与虚拟路径(相对导入解析基准;缺失且非 card 模式显示"缺失 componentPath"红章) |
| props | demoId | string | 缓存键 / scopeId 组成部分 |
| props | lazy | boolean | 惰性编译:IntersectionObserver(rootMargin 600px)+ mouseenter 双触发 |
| props | mode | 'card' | 'detail' | card 态注入强制的 100% 宽高样式 |
| props | inferredComponentPath | string | componentPath 缺省时的推断值 |
| emits | error | [message: string] | 编译/渲染错误 |
| expose | refresh() | () => void | 重新编译 |
| 插槽 | loading / fallback(作用域 {error}) / placeholder | — | 三态自定义呈现 |
src/components/chart-examples/SourceCodeModal.vue)props:open(v-model:open)、title、code、language('vue' | 'javascript')、filename;emits:update:open。Teleport 到 body,遮罩点击/Escape/关闭按钮三种关闭途径,打开期间锁 documentElement 滚动;内部复用 src/components/common/code/CodeBlock.vue(Shiki 高亮 + 行号 + 复制)。
| 模块 | 导出 | 签名要点 |
|---|---|---|
src/utils/v-chart.ts | VChart | defineAsyncComponent:先 import('echarts') 再 import('vue-echarts');供 main.ts 全局注册 |
src/chart-examples/loadScript.ts | loadScript(src) | 幂等加载脚本(Map 缓存 Promise);加载期间临时屏蔽 window.define 防 UMD 走 AMD 分支不挂全局 |
src/utils/sfcRuntime.ts | compileSfcToComponent(code, options) | 入参 SfcCodeInput {raw/template/script/style} + SfcCompileOptions {demoId, componentPath, skipCache};返回 {component, styles};模块级编译缓存 |
src/utils/sfcRuntime.ts | compileSfc | 上述的 never-throw 包装(返回 {component, styles, error}) |
src/utils/sfcRuntime.ts | setSfcCompiler / loadSfcCompiler | 编译器注入与懒加载(测试可注入替身) |
src/utils/sfcRuntime.ts | injectSfcStyles / removeSfcStyles | 样式注入与统一清理(id 去重) |
src/utils/sfc-modules.ts | registerSfcComponent(name, comp) / getComponentRegistry() / initSfcComponentRegistry(app) | 沙箱组件注册表(VueSfcRenderer 首次编译前懒注册 v-chart/VChart) |
src/utils/sfc-modules.ts | ensureModule(source, componentPath?) | 模块解析:npm 白名单 → @/ 别名 glob → 资源 URL → MissingModuleStub 兜底 |
src/composables/useResourceFetch.ts | useResourceFetch(fetcher, options) | options:{immediate, watch, onError, onSuccess, initial};返回 {data, loading, error, refresh, execute};requestId + AbortController 竞态保护,AbortError 不视为错误 |
src/composables/useListFilter.ts | useListFilter<T>({initial, debounce=300, syncUrl=true, urlKeys}) | 返回 {filters, debouncedFilters, reset, clearAll};URL 恢复优先于 initial |
src/composables/usePagination.ts | usePagination({total, pageSize=24, initialPage=1, resetOnTotalChange=true}) | 返回 {page, pageSize, totalPages, setPage(clamp), setPageSize(回1), reset} |
src/composables/useMapGeoJson.ts | useMapGeoJson(mapName, primaryUrl, fallbackUrl?) | 返回 {geoJson, error, isLoading, load};GeoJSON 结构校验(FeatureCollection + 非空 features)+ loadId 竞态 + 自动 echarts.registerMap |
src/composables/useTheme.ts | useTheme() | 返回 {theme, themeSetting, isDark(), isLight(), toggleTheme, setTheme, setThemeSetting, reapplyTheme};localStorage 键 devcanvas-theme |
src/utils/echarts-theme.ts | darkEchartsTheme | 大屏暗色主题对象(8 色板 + 轴/图例/tooltip 样式) |
src/config/chartLibraries.ts | CHART_LIBRARIES: ChartLibraryDef[] | {id, name, description, nav?: 'dropdown'|'standalone'};导航与库页单一真源 |
上述接口并非仅靠文档约束,多处有回归测试钉死:
src/data/charts/__tests__/charts-data.test.ts:锁定三库 JSON 条数(348/351/989)、19 个必需键、代码字段守恒(有本地 SFC 必无代码字段、兜底条目必有至少一个)、SFC 文件与 JSON 条目互相不缺;src/chart-examples/__tests__/EChartDemo.test.ts:锁定 EChartDemo 执行契约(option/onReady/setup 语义);src/components/chart-examples/__tests__/ChartExampleRenderer.test.ts 与 VueSfcRenderer.test.ts:锁定 loaderKind 分派与沙箱编译行为;src/config/__tests__/bigscreenComponents.test.ts:锁定大屏清单(bigscreenData.ts)与组件映射(bigscreenComponents.ts)一一对应;src/views/__tests__/ChartExampleDetailPage.breadcrumb.test.ts:以真实路由表校验卡片跳转路径不落 404(routes.ts 头注释说明的设计意图)。因此,任何对第 1~3 节接口的变更都必须同步调整对应测试——这是接手维护者最先要建立的心智模型。