# panelx **Repository Path**: wanghj348/panelx ## Basic Information - **Project Name**: panelx - **Description**: No description available - **Primary Language**: TypeScript - **License**: MIT - **Default Branch**: develop - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2026-06-16 - **Last Updated**: 2026-07-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # panelx 本仓库使用 **pnpm** 管理依赖,请勿使用 npm / yarn。 ```bash # 安装依赖 pnpm install # 开发 pnpm dev # 构建 pnpm build ``` ## 发布到 npm 日常依赖与脚本仍使用 **pnpm**;发版前请先完成 **`pnpm run build:lib`**,再执行发布。 若本机或项目 **`.npmrc`** 里把 `registry` 配成了**镜像**(例如 npmmirror / 企业内网源,仅用于加速 `install`),则 **`publish` 不应走该镜像**:镜像往往不支持写操作,或不应把公共包推到镜像。发布时请在命令上**显式指定目标 registry**: ```bash pnpm run build:lib # 测试打包正常? pnpm pack # 登录(必须先登录) pnpm login --registry https://registry.npmjs.org/ # 检测登录成功(可选,尽量做一次) pnpm whoami --registry https://registry.npmjs.org/ # 发布(尽量不要有git未提交,已用新的version打包) pnpm publish --access public --registry https://registry.npmjs.org/ ``` 使用 npm 客户端时同理: ```bash npm publish --registry=https://registry.npmjs.org/ ``` 说明: - 发布到 **npm 官方**时,registry 固定为 **`https://registry.npmjs.org/`**(注意末尾无多余路径)。 - 若目标是**私有 registry**,将上述 URL 换成贵司地址即可。 - 首次发布或换源后需先登录对应源,例如:`npm login --registry=https://registry.npmjs.org/` ## 前置规则(后续所有代码编写须遵守) - **设计稿尺寸(px)**:凡用于「设计坐标 ↔ 屏幕/容器实际像素」换算的基准尺寸,在配置里**使用 px**(见下节 **Dashboard 尺寸与坐标系**)。不要把业务样式里的随意数值写成 px(见下一条)。 - **其余单位禁止 px**:除上述“设计尺寸基准”外,**所有**样式与布局单位**不得使用 px**,统一使用相对单位(**rem**、**vh**、**vw** 等),以达到比例尺效果(随视口缩放)。 ## Dashboard 尺寸与坐标系(2D / 3D) 本节对应代码中的约定,避免把「大屏设计稿」「3D 世界单位」「Editor 画布」混为一谈。 ### 1. 2D 部分(`widgets2D` / Editor2D) - **设计稿尺寸**:`config.design.width` / `config.design.height`(如 1920×1080),与 **2D 组件** 的 `layout` 处于**同一设计坐标系**(见 `src/types/dashboard.ts` 中 `WidgetConfig2D` 注释)。 - **渲染到屏幕**:根容器按设计宽高比占位;`SizeManager2D`(`src/core/size/SizeManager2D.ts`)用 `scale = actualWidth / designWidth` 把设计稿矩形换算为实际像素;位置/尺寸再经 `pxToVw` / `pxToVh` / `pxToRem`(`src/utils/viewport.ts`、`src/core/size`)落到样式。 - **要点**:2D 的目标是**保持相对位置与比例**,小屏下整体等比缩小,而不是单独拉伸某轴破坏版式。 ### 2. 3D 部分(背景层 Scene3D / `widgets3D` / Editor3D) - **画布像素 vs 大屏 design**:3D 渲染区域(`Scene3DFramework`、Editor3D 主区域 canvas)**只依赖父容器的实际 CSS 像素**:铺满父容器,并用该宽高比设置相机 aspect。**不参与**用 `config.design` 去“规定”Three.js 视口分辨率。 `config.design` 服务于 **2D 组件** 的 layout 与整屏比例尺;整屏布局完成后,背景层 3D 所在 div 会分到一块**实际像素区域**——这是**布局结果**,不是 Three.js 读取 `config.design` 作为输入。 - **定位与尺度(重要)**:模型在世界中的位置、比例尺、`worldSize`、`designSize3D`(3D 设计稿尺寸)、**3D设计稿坐标系** 与 `origin` 等,才是 **3D 语义**下的关键数据;**不要**用 Dashboard 的 `config.design` 代替 3D 设计稿尺寸去做定位换算。 - **Editor3D**:侧栏只维护 **3D 设计**尺寸与比例尺等;**不展示、不编辑** Dashboard 的 `design`,也不展示 Viewport/DPR/Canvas 像素(避免与「3D 只跟父容器有关」混淆)。导入 JSON 时若含 `design`,仅写入 `config.design` 供导出/大屏 2D 兼容,**不会**用其覆盖侧栏中的 3D 设计稿尺寸。 - **场景世界单位(Three.js)**:**Y 轴向上**,**水平面为 XZ**。**世界原点** `(0,0,0)`;模型 `position`、相机均在**世界坐标**下理解。 - **设计坐标 → 世界 XZ**:Editor3D 用「以左上角为 (0,0) 的输入坐标」映射到世界 **X、Z**(`src/utils/coord3d.ts`),并按 **worldScale**(`world = 3D设计稿尺寸 × scale`)换算。**不是**把 `config.design` 直接当成 Three 里的米制场景尺寸。 - **场景范围 / 相机**:`WidgetConfig3D.worldSize` 描述 3D 设计空间对应的 **世界范围**(正交相机可视等)。正交可视半高由 **sqrt(x²+y²+z²)/2**(包围盒外接球半径)推导,再乘 `ORTHOGRAPHIC_FRUSTUM_SCALE`;轨道相机初始距离取 `minOrthographicOrbitDistanceFromWorldSize`(须大于外接球半径,避免相机落在球内导致近/远裁切)。与 **`config.design`(2D 大屏)语义不同**。 ### 3. 「3D设计稿坐标系」与 Three.js 世界坐标(两套原点) 为避免与 Three.js **世界坐标**(world space,原点在 `(0,0,0)`)混淆,本仓库对「平面/厂区/小区总图上**左上角为 0**」这一套单独命名:**3D设计稿坐标系**(仅指水平面上的布局基准,Y 仍表示高度;水平方向由 XZ 表达)。 - **典型场景**:把一张工厂或住宅小区的**平面图**当作 3D 里摆放设备/标注的参考——图上 **左上角为 (0,0)**,向右、向下为增大;导入 Three.js 后,场景往往以 **世界原点为场景中心**,于是出现 **两套原点**,必须通过 **原点偏移**(Editor3D / `src/utils/coord3d.ts` 中的 `originX`、`originY` 与世界比例尺)做互转。 - **命名约定**:文档与讨论中说到「设计稿上的坐标」「平面左上角为 0」时,优先使用 **3D设计稿坐标系**;说到模型 `position`、相机、物理单位时,指 **Three.js 世界坐标**。 - **换算示例(减少歧义)**:若配置中将 **3D设计稿坐标系的原点**(即平面图左上角 `(0,0)` 落在世界中的位置)设为 **`(-20, 0, -20)`**(世界坐标),则在同一比例尺下,Three.js 世界点 **`(0, 0, 0)`** 对应到 **3D设计稿坐标系**中的 **`(20, 0, 20)`**(即相对「平面图左上角」在水平面上偏移 +20、+20;Y 为高度轴,此处为 0)。 直观理解:世界原点相对「左上角锚点」平移了 `(+20, 0, +20)`,故在「以左上角为 0」的读数下记为 `(20, 0, 20)`。 实现上,`designInputToWorldXZ` / `worldXZToDesignInput` 中的 `originX`、`originY` 即参与上述「锚点」换算;具体数值以导出配置与 Editor3D 当前设置为准。 ### 4. 与旧版 README 表述的关系 原先「3D 场景的设计/世界尺寸」容易误解成「和 `config.design` 是同一个东西」。实际上: - **2D**:几乎总是 `config.design` + `SizeManager2D`。 - **3D**:世界坐标 +(可选)Editor3D 的 3D 设计尺寸与 `worldScale` + `widgets3D[].worldSize`。 若新增功能,请先区分改的是 **2D 设计稿坐标**、**3D设计稿坐标系**(平面左上角为 0)还是 **Three.js 世界坐标**,再选对应工具类与字段。 ## 规范 - **包管理**:仅使用 **pnpm**,不要使用 npm / yarn。 - **样式单位**:除前述“设计稿尺寸”外,禁止使用 `px`,统一使用相对单位(如 **rem**、**vh**、**vw** 等)。 ## 编辑器布局(Editor) 编辑器主界面为左、中、右三栏布局,通过 **CSS Grid** 控制占用比例: - **左侧**:组件列表、尺寸设置、操作按钮(默认 **25%**) - **中间**:标尺 + 画布主区域(默认 **50%**) - **右侧**:属性配置栏(默认 **25%**) 修改比例时,在 `src/editor/Editor.vue` 的样式中调整 `.panelx-editor` 的 `grid-template-columns`,例如: ```css /* 默认:25% 50% 25% */ grid-template-columns: 25% 50% 25%; /* 示例:左侧更窄、主区域更宽 */ grid-template-columns: 20% 60% 20%; ``` ### Editor2D 与 Editor3D 分工及合并导出(低代码衔接) - **分工**:2D 与 3D **各自独立编辑**(`Editor.vue` / `Editor3D.vue`)。运行时大屏上 3D 作为 **Dashboard 背景层**(由 `widgets3D` + `scene3D` 等驱动),与 2D 组件叠放;未来可在 2D 画布中为「某父容器」嵌入 3D 预览,仍复用同一份 `DashboardConfig`。 - **草稿**:在 **Editor3D** 侧栏点击 **「保存草稿」**,将当前 3D 相关配置写入 **`localStorage`**,键名 **`EDITOR_3D_DRAFT`**(见 `src/utils/editor3dDraft.ts`),内容与「导出 JSON」一致(`widgets3D`、`scene3D`、`background` 等)。 - **合并**:在 **Editor2D** 侧栏勾选 **「导出/预览合并 3D 草稿」**(持久化键 `PanelX_EDITOR_ENABLE_3D_MERGE`)。勾选后,**导出配置**与 **预览**会以当前 2D 配置为底,再合并草稿中的 `widgets3D` / `scene3D`(及非空的根 `background`、`debug`)。合并后侧栏会短暂显示**文字提示**(成功/未读到草稿/草稿无实例)。**详细合并日志**(`[Editor2D][merge3D]`)仅在 **`config.debug` 或 `PanelX_DEBUG`** 开启时输出到控制台。 - **与 `backgroundLayer` 的关系**:`Dashboard` 若配置了 **`backgroundLayer`(如图片背景)**,会优先使用该层,**不会**再使用 `widgets3D` 生成的 3D 背景。合并时若草稿里带有 3D 实例(`widgets3D.length > 0`),会**清除**合并结果中的 `backgroundLayer`,以保证 3D 场景能作为背景显示。若你需要「图片叠在 3D 上」等组合,需另行扩展分层策略。 - **典型流程**:Editor3D 调场景 → **保存草稿** → 打开 Editor2D 排 2D → 勾选合并 → **导出** 得到完整 `dashboard-config.json`。 ## 编辑器 Widget 默认配置 每个 widget 拖入画布时需要默认 props 与尺寸;右侧属性栏的字段定义也来自同一套配置。 ### Widget 默认配置所在文件 | 位置 | 作用 | 优先级(拖入时) | |------|------|------------------| | **`src/editor/editor-config/defaultParams.ts`** | 按 **类型** 配置默认参数(如 `stat`、`chart`、`glassChart`),拖入时作为该类型 widget 的初始 props | **最高** | | **`src/editor/editor-config/registeredWidgets.ts`** → `registeredWidgets[].defaultProps` | 每个侧栏项可选的 `defaultProps`,仅当 `defaultParams` 未配置该类型或为空时使用 | 次之 | | **`src/widgets/widgetPropConfig.ts`** → `widgetTypeReg[type].defaultProps` | 代码侧为每种 `WidgetType2D` 写的默认 props,未在 JSON 中配置时兜底 | 兜底 | 编辑器解析顺序:先取 **`widgetPropData.defaultParams[type]`**,若无再取 **`registeredWidgets` 中该 type 的 `defaultProps`**,再无则用 **`getWidgetDefaultProps(type)`**(来自 `widgetPropConfig.ts`)。 右侧「组件属性」的字段列表来自 **`src/widgets/widgetPropConfig.ts`** 的 **`propConfig`**(`getWidgetPropConfig(type)`),与默认值同文件定义。 ### 配置文件示例(editor-config) - **`registeredWidgets`**:侧栏可拖拽列表;每项需 **`type`**、**`label`**、**`defaultSize`**(拖入时的宽高,设计稿 px),可选 **`defaultProps`**、**`sampleImage`**。 - **`widgetPropData.defaultParams`**:按类型集中写默认参数,拖入时优先使用,无需在每条 `registeredWidgets` 里重复。 编辑器启动时从 **`src/editor/editor-config/index.ts`** 加载(见 `Editor2D.vue` 的 `onMounted`);大屏/配置加载视图从同一内置配置取 `datasources`。 ```json { "widgetPropData": { "defaultParams": { "stat": { "value": 0, "label": "指标" }, "chart": { "seriesType": "bar", "options": { ... }, "height": "100%", "width": "100%" } } } } ``` ### 代码兜底(Widget Registry) **`src/widgets/widgetPropConfig.ts`** 中为每种 `WidgetType2D` 配置 **`defaultProps`** 与 **`propConfig`**;**`src/widgets/widgetRegistry.ts`** 对外提供 **`getWidgetDefaultProps(type)`**、**`getWidgetPropConfig(type)`**。 新增 widget 类型时在此维护默认值与属性定义,保证未配置 JSON 时仍有可用默认值及右侧栏字段。 ## Widget 数据集成 Dashboard 与编辑器通过**统一 prop 配置**和**按 widget id 的数据存储**打通配置与运行时数据,便于展示、编辑与后期数据更新。 ### 1. 统一 Prop 配置(Registry) - **类型**(`src/types/widgets.ts`) - **`WidgetPropDef`**:单个属性的定义(`key`、`label`、`type`、`default`),供编辑器展示与解析 config。 - **`WidgetTypeRegItem`**:某类 widget 的 `defaultProps` + `propConfig` 数组。 - **实现**(`src/widgets/widgetPropConfig.ts`) 为每种 `WidgetType2D` 配置 `defaultProps` 与 `propConfig`。 - **`getWidgetDefaultProps(type)`**:拖入画布或解析 config 时使用的默认 props。 - **`getWidgetPropConfig(type)`**:该类型所有可编辑属性的定义,供 Editor 右侧属性栏按 key/label/type 渲染(可后续接入)。 - **入口**(`src/widgets/widgetRegistry.ts`) 对外提供 `getWidgetTypeReg(type)`、`getWidgetDefaultProps`、`getWidgetPropConfig`。 ### 2. Dashboard 按 widget id 的数据 - **`widgetData`**(`src/components/Dashboard.vue`) - 类型:`Ref`,即 `Record>`(键为组件 **`id`**)。 - 配置加载后,由 **`syncWidgetDataFromConfig()`** 根据当前 `config.widgets2D` 填充:每个 widget 的 `widgetData[id] = { ...w.props }`。 - 渲染时通过 **`getWidgetProps(w)`** 取数:优先 `widgetData[w.id]`,无则回退到 `w.props`,模板使用 `v-bind="getWidgetProps(w)"`。 - **provide / inject**(类型见 `src/types/injections.ts`) - **`WidgetDataKey`**:注入后得到 `Ref`,只读当前所有 widget 数据。 - **`SetWidgetDataKey`**:注入后得到 **`SetWidgetDataFn`**,即 `(id, patch) => void`,按 widget id 局部更新数据(合并 patch 到该 id 的 props),便于后期「配置数据更新」而不改 config。 子组件或外部使用示例: ```ts import { inject } from 'vue' import { WidgetDataKey, SetWidgetDataKey } from '@/types/injections' const widgetData = inject(WidgetDataKey) // Ref | undefined const setWidgetData = inject(SetWidgetDataKey) // SetWidgetDataFn | undefined // 按 id 更新某 widget 数据 setWidgetData?.('stat-1', { value: 123, label: '产量' }) ``` - **类型导出**(`src/types/widgets.ts`) **`WidgetDataMap`**、**`SetWidgetDataFn`** 已导出,与 **`WidgetDataKey`**、**`SetWidgetDataKey`** 一起供全项目做类型约束。 ## MarqueeText 走马灯(维护说明) 实现文件:**`src/widgets/MarqueeText.vue`**。用于 2D 单行横向跑马灯;属性注册见 **`src/widgets/widgetPropConfig.ts`**、**`src/widgets/widgetRegistry.ts`**,编辑器默认参数见 **`src/editor/editor-config/defaultParams.ts`**(如有 `marqueeText` 配置)。 ### 行为概要 - **`loopCount ≤ 0`**:无限循环(`animation-iteration-count: infinite`)。 - **`loopCount > 0`**:滚动动画跑完指定次数后进入 **淡出**,再进入 **隐藏**(`opacity: 0`,`pointer-events: none`)。 - **位移与入场**:用测量得到的 **容器宽度**(`--marquee-start`)与 **第一份文案宽度**(`--marquee-shift`,负值)驱动 `@keyframes panelx-marquee-loop`,使内容从右侧进入并无缝循环;**不要**用「百分比位移」代替测量值,否则与双份 DOM 拼接的循环语义容易不一致。 - **高亮片段**:文案中用 `[[...]]` 包裹的片段解析为高亮(`parseTextSegments`)。 ### 状态机(`phase`) | 值 | 含义 | | --- | --- | | `looping` | 正常滚动 | | `exiting` | 有限次数已结束,容器执行淡出动画,轨道 `animation-play-state: paused` | | `done` | 淡出结束,加 `.is-hidden` 彻底不可见 | ### 维护时必读:`scoped` 与 `AnimationEvent.animationName` `