/**
* VP-TSX 运行时 —— 可执行 builder:h() 把 JSX 树直接组装成 vpproj 工程 JSON 节点。
*
* 定位:只服务「从零生成工程」。TSX 是受约束的编译器输入(组件面 = 平台 widget
* 目录,类型自动生成自 widget schema),不是通用前端代码;产物是编辑器可导入的
* `{ project, connections, connectionData, dataRules }` 信封,真源仍是工程 JSON。
*
* 相对直接手写 JSON 的红利:类型即时纠错(臆造属性名/错误枚举编译报错)、
* 可用循环/变量/函数消除重复(8 张 KPI 卡是一个 map() 而非 800 行 JSON)、可写注释。
*/
import './types.generated'
// piece 判别联合(types.generated 生成):TSX 里模块顶层构造 pieces 的辅助函数
// 请标注返回类型(如 VpScene3dPiece[]),否则字面量拓宽后失去编译期校验。
export type {
VpKnownPiece,
VpPieceNode,
VpPieceOptionsMap,
VpHostPieceMap,
VpScene3dPiece,
VpMap2dPiece,
VpScada2dPiece,
} from './types.generated'
// --- 节点与信封形态 ---------------------------------------------------------
/**
* builder 子图元的宽松节点形态(VpNode 输出面用)。
* TSX 里写 pieces 时用的是 types.generated 的判别联合(VpKnownPiece / VpHostPieceMap,
* 按 type 收窄校验选项键与枚举值),此处仅作为工程 JSON 的落地形态。
*/
export interface VpPiece {
uid: string
type: string
name?: string
enabled?: boolean
locked?: boolean
options?: Record<string, unknown>
/** 嵌套子图元(如 map-2d 的图层挂在 map-body 下) */
pieces?: VpPiece[]
}
export interface VpNode {
uid: string
type: string
name?: string
options: Record<string, unknown>
widgets?: VpNode[]
/** builder 型(map-2d / scada-2d / scene-3d)子图元 */
pieces?: VpPiece[]
/** data-repeater 专用:单元模板(group-panel 节点,与 options 平级) */
unitTemplate?: VpNode
conditionalStyles?: unknown
stateOverrides?: Record<string, unknown>
sizeModeOverrides?: Record<string, unknown>
states?: unknown
/** 当前激活状态 uid(多状态) */
state?: string
enabled?: boolean
locked?: boolean
/** 来源模板标注(属性栏展示用) */
templateSubtypeLabel?: string
templateAlias?: string
/** 组合组件实例(母版-实例复用):masterId + 实例 props/overrides + 内嵌母版 master */
masterId?: string
masterVersion?: unknown
props?: Record<string, unknown>
overrides?: Record<string, unknown>
master?: unknown
/** 组件私有数据表 */
privateData?: unknown
/** 二次开发脚本(代码面板) */
extensions?: unknown
/** 页面根:待处理/暂存区组件子树 */
pending?: unknown
/** 页面根:编辑参考线 */
guideLines?: Record<string, unknown>
}
/** 字体复合值(宽松:编辑器接受渐变串等扩展键) */
export interface VpFont {
family?: string
size?: number
color?: string
bold?: boolean
italic?: boolean
underline?: boolean
weight?: string | number
[k: string]: unknown
}
/** 字段绑定三元组:[连接uid, 表uid, 字段uid];多选放多个三元组 */
export type VpFieldTriple = [string, string, string]
export type VpFieldValue = VpFieldTriple[] | VpFieldTriple
/** 所有组件共有的节点级 props(与 options 平级的顶层字段 + JSX 语法件) */
export interface VpCommonProps {
uid?: string
name?: string
children?: unknown
/** 事件交互(触发器→动作),写入 options.interactions;用 interactions()/on()/act() 组装 */
interactions?: unknown
/** 条件样式(节点顶层,与 options 平级):{paths, cases} */
conditionalStyles?: unknown
stateOverrides?: Record<string, unknown>
sizeModeOverrides?: Record<string, unknown>
states?: unknown
/** data-repeater 专用:单元模板(必须是 group-panel 节点) */
unitTemplate?: VpNode
// 注意:pieces 不在公共 props 里——只有 builder 组件(map-2d / scada-2d / scene-3d)
// 才有 pieces,其类型由 types.generated 按宿主生成(VpHostPieceMap,判别联合强校验)。
/** 当前激活状态 uid(多状态) */
state?: string
enabled?: boolean
locked?: boolean
templateSubtypeLabel?: string
templateAlias?: string
/** 组合组件实例(母版-实例复用,type=component-instance):masterId + props/overrides + 内嵌母版 master */
masterId?: string
masterVersion?: unknown
props?: Record<string, unknown>
overrides?: Record<string, unknown>
master?: unknown
/** 组件私有数据表 */
privateData?: unknown
/** 二次开发脚本(代码面板) */
extensions?: unknown
/** 页面根:待处理/暂存区组件子树 */
pending?: unknown
/** 页面根:编辑参考线 */
guideLines?: Record<string, unknown>
}
/** 页面/窗口 props:常用项带类型,其余选项直接透传 options(页面选项不在 widget schema 里) */
export interface VpScreenProps {
uid?: string
name?: string
children?: unknown
/** 布局画像(必写):固定大屏 canvas-scale / 百分比 canvas-percent / 流式 flow-fluid */
'layout-profile'?: 'canvas-scale' | 'canvas-percent' | 'flow-fluid'
/** 铺满方式(必写;演示推荐 fill-uniform + fill-auto-expand):全部 8 值见 envelope.md §4.1 */
'fullscreen-mode'?:
| 'scaling'
| 'stretch'
| 'adaptive'
| 'responsive'
| 'fill-uniform'
| 'fit-width'
| 'fit-height'
| (string & {})
/** 仅 fill-uniform / responsive:把无锚点的主视觉拉满剩余画布 */
'fill-auto-expand'?: boolean
size?: [number, number]
'background-color'?: string
'container-layout'?: 'static' | 'fluid'
'flex-direction'?: 'row' | 'column'
'fluid-wrap'?: 'wrap' | 'nowrap'
padding?: [number, number, number, number]
[k: string]: unknown
}
// --- h 工厂 -----------------------------------------------------------------
export const Fragment = '#fragment'
/**
* 透传到节点顶层的持久化字段(与 options 平级,VpNode/工程 JSON 同名)。
* 覆盖复用与编辑器全部持久化面:组合组件母版-实例(masterId/masterVersion/props/overrides/master)、
* 二开脚本 extensions、私有数据 privateData、多状态 state、enabled/locked、模板来源标注、
* 页面根 pending(暂存区)/guideLines(参考线)。
* ⚠️ 与 core serializeTsx 的 NODE_EXTRA_PROPS、diffProject 的 NODE_BLOB_PROPS 三处同步;
* 漏一处 = 导出丢字段 / diff 盲区(曾发生在 masterId 上)。
*/
const NODE_PASSTHROUGH = [
'unitTemplate',
'pieces',
'conditionalStyles',
'stateOverrides',
'sizeModeOverrides',
'states',
'state',
'enabled',
'locked',
'templateSubtypeLabel',
'templateAlias',
'masterId',
'masterVersion',
'props',
'overrides',
'master',
'privateData',
'extensions',
'pending',
'guideLines',
] as const
/** 节点级保留 props:不进 options,落节点顶层字段 */
const NODE_PROPS = new Set<string>(['uid', 'name', 'children', 'key', ...NODE_PASSTHROUGH])
type VpChild = VpNode | VpChild[] | null | undefined | false
const counters = new Map<string, number>()
/** 重置自动 uid 计数(同进程编译多份工程时用;同一输入产出确定性 uid) */
export function resetUids(): void {
counters.clear()
}
function autoUid(type: string): string {
const n = (counters.get(type) ?? 0) + 1
counters.set(type, n)
return `${type}-${n}`
}
function flattenChildren(children: VpChild[], into: VpNode[]): void {
for (const c of children) {
if (c == null || c === false) continue
if (Array.isArray(c)) {
flattenChildren(c, into)
continue
}
if (c.type === Fragment) {
into.push(...(c.widgets ?? []))
continue
}
into.push(c)
}
}
/**
* JSX 工厂(tsconfig: jsxFactory=h, jsxFragmentFactory=Fragment)。
* 除保留 props 外的所有属性原样进 options(kebab-case 选项名与 schema 一致);
* `interactions` 也写入 options.interactions(工程 JSON 的真实位置)。
*/
export function h(type: string, props?: Record<string, unknown> | null, ...children: VpChild[]): VpNode {
const widgets: VpNode[] = []
flattenChildren(children, widgets)
if (props && props.children !== undefined) flattenChildren([props.children as VpChild], widgets)
if (type === Fragment) {
return { uid: autoUid('fragment'), type: Fragment, options: {}, widgets }
}
const p = props ?? {}
const options: Record<string, unknown> = {}
for (const [k, v] of Object.entries(p)) {
if (NODE_PROPS.has(k) || v === undefined) continue
options[k] = v
}
const node: VpNode = {
uid: typeof p.uid === 'string' && p.uid ? p.uid : autoUid(type),
type,
options,
}
if (typeof p.name === 'string' && p.name) node.name = p.name
if (widgets.length) node.widgets = widgets
for (const k of NODE_PASSTHROUGH) {
if (p[k] !== undefined) (node as unknown as Record<string, unknown>)[k] = p[k]
}
return node
}
// --- 数据绑定与连接 ----------------------------------------------------------
/** 字段绑定:bind(conn, tbl, 'A') → [[conn,tbl,'A']];bind(conn, tbl, 'A', 'B') 多选 */
export function bind(conn: string, table: string, ...fields: [string, ...string[]]): VpFieldTriple[] {
return fields.map((f) => [conn, table, f] as VpFieldTriple)
}
export interface VpConnectionField {
uid: string
alias?: string
type?: 'string' | 'number' | 'boolean' | (string & {})
[k: string]: unknown
}
/**
* 内联静态数据连接(connectorType:"json",字段 uid=列名)。
* 返回 [connection, connectionData 片段],defineProject 里直接展开。
*/
export function jsonConnection(spec: {
uid: string
name: string
table: { uid: string; name: string; fields: VpConnectionField[] }
rows: Array<Record<string, unknown>>
}): { connection: Record<string, unknown>; connectionData: Record<string, unknown> } {
return {
connection: {
uid: spec.uid,
name: spec.name,
type: 'json',
connectorType: 'json',
config: { copyToRoot: true, fileName: '' },
tables: [{ uid: spec.table.uid, name: spec.table.name, fields: spec.table.fields }],
},
connectionData: {
[spec.uid]: [{
tableId: spec.table.uid,
tableName: spec.table.name,
fields: spec.table.fields,
rows: spec.rows,
}],
},
}
}
// --- 事件交互(触发器→动作) -------------------------------------------------
export interface VpTrigger {
name: string
options?: Record<string, unknown>
}
export interface VpAction {
name: string
options?: Record<string, unknown>
}
/** 动作:act('switchScreen', { screen: [...] }) */
export function act(name: string, options: Record<string, unknown> = {}): VpAction {
return { name, options }
}
/** 一组交互:on('click', act(...), act(...));trigger 可带 options */
export function on(trigger: string | VpTrigger, ...actions: VpAction[]) {
const t: VpTrigger = typeof trigger === 'string' ? { name: trigger, options: {} } : trigger
return {
trigger: t,
interactions: actions.map((action) => ({ enable: true, trigger: t, action })),
}
}
/** options.interactions 的完整值:interactions(on('click', act(...)), on('appear', ...)) */
export function interactions(...groups: ReturnType<typeof on>[]): { version: 2; groups: ReturnType<typeof on>[] } {
return { version: 2, groups }
}
// --- 工程信封 -----------------------------------------------------------------
export interface VpProjectEnvelope {
project: Record<string, unknown>
connections: unknown[]
connectionData: Record<string, unknown>
dataRules: unknown[]
}
/**
* 组装可导入信封。screen/screens/windows 传 <screen>/<window> JSX 节点。
* connections/connectionData 可用 jsonConnection() 的返回展开。
*/
export function defineProject(spec: {
name: string
screen: VpNode
screens?: VpNode[]
windows?: VpNode[]
foregroundScreen?: VpNode
backgroundScreen?: VpNode
params?: Record<string, unknown>
settings?: Record<string, unknown>
connections?: unknown[]
connectionData?: Record<string, unknown>
dataRules?: unknown[]
}): VpProjectEnvelope {
const project: Record<string, unknown> = {
name: spec.name,
screen: spec.screen,
screens: spec.screens ?? [],
params: spec.params ?? {},
settings: spec.settings ?? {},
}
if (spec.windows?.length) project.windows = spec.windows
if (spec.foregroundScreen) project.foregroundScreen = spec.foregroundScreen
if (spec.backgroundScreen) project.backgroundScreen = spec.backgroundScreen
return {
project,
connections: spec.connections ?? [],
connectionData: spec.connectionData ?? {},
dataRules: spec.dataRules ?? [],
}
}