# 应用骨架与模块脚手架

> 返回 [SKILL.md](../../SKILL.md)。下文结构提炼自官方参考实现（park 能源站、safety 罐区两套应用，模块结构相同）。

> **包名**：外部工程用发布包名 **`vjvpsdk`**（`"file:./vendor/vjvpsdk"` 或私有 registry 版本号），拿到包即可用，无需构建 SDK。下文示例的 `@vp/sdk` 与 `vjvpsdk` 等价互换；CSS 子路径分别为 `@vp/sdk/vp-runtime.css` 与 `vjvpsdk/vp-runtime.css`。

## 1. 应用骨架（npm 消费形态，默认推荐）

四个文件起步：

**package.json**（依赖 `vjvpsdk` + `vue`；`vjvpsdk` 也可换成私有 registry 版本号）：

```jsonc
{
  "name": "<name>",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "typecheck": "vue-tsc --noEmit"
  },
  "dependencies": { "vjvpsdk": "file:./vendor/vjvpsdk", "vue": "^3.5.13" },
  "devDependencies": { "@vitejs/plugin-vue": "^5.2.1", "typescript": "^5.6.3", "vite": "^5.4.11", "vue-tsc": "^2.1.10" }
}
```

**vite.config.ts**（**不配置任何别名**——`import 'vjvpsdk'` 走包入口解析到 `dist/vp-runtime.mjs`）：

```ts
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
  base: './',
  plugins: [vue()],
  server: { port: 5181, host: true },
})
```

**index.html**：`<div id="app"></div>` + `<script type="module" src="/src/main.ts"></script>`（深色底 body 样式防白闪）。

**src/main.ts**（样式经包子路径引入；自定义组件经挂载选项注册）：

```ts
import { createVpViewer } from 'vjvpsdk'
import 'vjvpsdk/vp-runtime.css'
import { appInit } from './project'
import { buildAll } from './build'
import { myWidgetModule } from './widgets/myWidget'

createVpViewer('#app', appInit, {
  widgets: [{ type: 'my-widget', module: myWidgetModule }],
  onReady: (ctx) => void buildAll(ctx),
})
```

⚠️ 外部工程用 `vjvpsdk` 包无需构建 SDK。类型解析走包内 `dist/vp-runtime.d.ts`（发布包 types 字段所指）。

## 2. 模块目录（配置层 / 代码层）

```
src/
├── main.ts               # 入口：注册自定义组件 + createVpViewer + onReady
├── project/              # ◆配置层：数据源
│   ├── connections.ts    #   连接/表/字段定义（uid=alias 中文字段，helper 压缩重复）
│   ├── connectionData.ts #   初始行（测点初值从 sim 单一事实源导入，静态报表手写）
│   └── index.ts          #   body 外壳（页面级选项 + 首页空壳）+ mergeFieldSchema
├── scenes/               # ◆配置层：重场景 pieces 静态定义
│   ├── scene3dPieces.ts  #   三维场景（导出 UID 常量 + pieces() + interactions()）
│   ├── scadaPieces.ts    #   组态工艺图（node/link 用 helper 生成）
│   └── mapPieces.ts      #   2D 地图（map-body + 数据源 + 图层）
├── sim/                  # ◆代码层：模拟引擎与业务状态机（纯函数 + 单测）
│   ├── points.ts         #   测点/设备/映射表 单一事实源（含 DEVICE_PIECE_MAP 类映射）
│   ├── simulator.ts      #   物理模型 tick（纯函数，注入 rng）
│   ├── alarms.ts         #   告警状态机（raise/resolve/ack + 派生视图）
│   ├── <domain>.ts       #   领域状态机（隐患/作业票/演练…）
│   └── runtime.ts        #   接线：setInterval tick → ctx.setData；对页面暴露控制 API
├── pages/                # ◆代码层：每页一个 builder（ctx/screen/sim 注入）
├── widgets/              # ◆代码层：自定义组件（模型类 + 渲染 + 纯函数内核分离）
├── ui.ts                 # 主题常量 + addPanel/addCartesian/addPie/addTable/addButton/addValueText + buildNav/buildHeader
├── windows.ts            # 工程级窗口（趋势/详情/应急……内容组件带 uid 供程序化刷写）
└── build.ts              # 总装：启动 sim → addScreen 建页 → 逐页 builder → 窗口 → 跨页联动
```

## 3. 数据层模板要点

- **connections.ts**：用 `F(uid)`/`S(uid)`/`conn(...)` 小 helper 压缩；字段 `uid = alias`（中文列名直接当字段 id，绑定与行键一致零转换）。
- **connectionData.ts**：测点初值 **必须** 从 `sim/points.ts` 的 `initialPoints()` 导入生成（单一事实源，防两处漂移）；静态报表（月度/分项）手写；历史窗口可用初始 sim 状态回放 N 拍生成，与实时数据无缝衔接。
- **index.ts**：内核 `VpDataTable` 要求每张表带 `fields/tableName`（图表 `getFieldMeta` 直读），`mergeFieldSchema` 从 connections 合并进 connectionData。
- **body 外壳**：只带数据层 + 一个空首页（页面级选项 `layout-profile: canvas-percent` + `fullscreen-mode: responsive` + 1920×1080 + 深色渐变背景）；其余页面全部 `ctx.addScreen` 程序化创建。

## 4. 页面构建要点

- 全部几何按 **1920×1080 设计基准 + `'geometry-unit': 'percent'`** 写，全屏自适应。
- 导航条每页重建（当前页高亮、其余 `btn.onClick(() => ctx.setActiveScreen(id))`）；`PAGES` 清单集中定义。
- 面板套路：`addPanel`（group-panel 百分比几何）→ `addPanelHeader`（光条+标题+分隔线）→ 内容组件（图表绑 `category-field`/`metric-fields`，表格绑 `table-data-fields`，指标文本绑 `value-field`）。
- 程序化刷写文本：创建时给 uid，回调里 `ctx.getWidget(uid)?.setOption('text', ...)`。
- 表格点行：`table.onSelect((data, index) => ...)`——`data` 是**选中维度** `{别名:值}`（取联动键 / `table-key-fields` / `select-sync-fields` 各列，都未配时为被点击列；再点同行取消时为 `undefined`），**不是**行 payload；要 `{rowNumber(1基), rowIndex(0基), columnUID, value, row:{别名:值}, …}` 用 `table.onValueChange(v => …)` 或 `table.getValue()`（即 `currentValue`）。业务主键（如 `_id`）要出现在 `data` 里，就把它配进 `table-key-fields`；数据行本身另带运行时注入的 `_index` 字段（1 基行号，vxe `keyField` 也用它）。
