20260913231607

This commit is contained in:
oneao committed 2026-09-13 23:16:08 +08:00
1 parent f3ad727f1c
commit c31329d387
179 files changed
+10846 -2684

No files matched your search

@@ -0,0 +1,238 @@
---
name: fms-module-list 查询区升级为常用条件网格+可用的高级查询
overview: 把 `fms-module-list` 的查询区改造成 dashboard 的布局(常用条件 4 列网格 + 更多条件展开 + 左侧高级查询面板),并让高级查询真正生效(AST → SQL → `/data/page`)。常用与高级复用同一套查询构造器:常用条件按字段类型取默认操作符(文本一律 like),高级条件由用户选操作符并支持嵌套条件组。全程只改前端,后端零改动。
design:
architecture:
framework: vue
styleKeywords:
- 企业级简约
- 白卡分区
- 定宽网格
- 轻阴影
- 微交互反馈
fontSystem:
fontFamily: PingFang SC
heading:
size: 15px
weight: 600
subheading:
size: 13px
weight: 600
body:
size: 13px
weight: 400
colorSystem:
primary:
- "#1677FF"
- "#EDF5FF"
background:
- "#F5F6F8"
- "#FFFFFF"
- "#FAFBFC"
text:
- "#1F2329"
- "#646A73"
- "#A3ACB8"
functional:
- "#F53F3F"
- "#FF7D00"
- "#D8E5F7"
todos:
- id: query-ast-builder
content: 在 queryUtils.js 实现统一 AST 构造器(OPERATORS、defaultOperator、buildConditionSql、buildGroupSql、astToSql、buildQuickAst、buildAdvancedAst),并让 buildQueryCondition 内部转发保持兼容
status: completed
- id: toolbar-quick-layout
content: 改造 FmsQueryToolbar 为 dashboard 布局:常用条件 4 列网格 + 更多条件展开 + 生效条件 chips 条,emit 改为 { mode, ast }
status: completed
dependencies:
- query-ast-builder
- id: toolbar-advanced-panel
content: 新增高级查询面板:条件行(复用 FmsQueryControl 按操作符渲染)、条件组与嵌套逻辑、根逻辑、条件计数与应用查询
status: completed
dependencies:
- toolbar-quick-layout
- id: page-ast-wiring
content: FmsModuleListPage 接入 AST:fetchData 用 astToSql 生成条件,resetSearch 回到默认条件,保留 fixedSearchCondition 组合
status: completed
dependencies:
- toolbar-advanced-panel
- id: user-pref
content: 新增 useModulePref 实现偏好读写与合并(query 分区顺序、view 列显隐/顺序/宽度),按默认→偏好→权限裁剪顺序合并并落库 s_user_module_pref
status: completed
dependencies:
- page-ast-wiring
- id: query-settings-drawer
content: 实现查询设置抽屉:常用/高级/隐藏分区、拖拽排序、恢复默认、无查询权限字段灰显,改动在关闭时一次性保存
status: completed
dependencies:
- user-pref
- id: dashboard-cleanup-and-tests
content: 删除 dashboard 中重复的 AST computed,补充 query-ast / module-pref 单测并跑通 fms-vue 全量测试
status: completed
dependencies:
- query-settings-drawer
---
## 产品概述
把 `fms-module-list`(生产通用列表组件)的查询区改造为 dashboard 原型已验证的形态,并让高级查询**真正生效**。改造后:常用条件以 4 列网格平铺、超出部分「更多条件」展开;高级查询提供可选操作符、可嵌套条件组、条件摘要 chips;两种模式共用同一套查询构造逻辑,最终都生成同一份 `search_condition` 交给 `/data/page` 执行。
## 核心功能
- **常用条件区**:4 列定宽网格(容器查询在窄容器降为 3 列),默认展示 4 个条件,其余收进「更多条件」并带生效条件数角标;支持查询、重置。
- **高级查询**:根逻辑(满足全部/任一)+ 条件行(字段 + 操作符 + 值)+ 条件组(组内 and/or,可嵌套、可增删)+ 「已配置 N 个条件」+ 应用查询,点击后真正请求数据。
- **统一构造**:常用条件用默认操作符(文本 `like`、日期/数值区间 `between`、枚举 `eq`),高级条件用用户选择的操作符,二者生成同一种 AST,经同一个构造器产出 SQL。
- **生效条件 chips 条**:展示当前生效条件,可单个移除、可全部清除;树/页面固定条件也以 chip 呈现(保持现有 `fixedSearchCondition` 语义)。
- **查询设置(用户个性化)**:每个字段可设为「常用/仅高级/隐藏」并拖拽排序,只影响当前用户;无查询权限的字段灰显且不可调整。
- **个性化持久化**:只记布局(查询字段分区与顺序、列显隐/顺序/宽度),不记条件值;语言、主题保持本地存储,不做表格密度。
- **后端零改动**:沿用通用 `/data/loaddata`、`/data/page`、`/data/saveobjt`。
## 技术栈
- 前端:Vue 3 `<script setup>` + 现有自研 `fms-ui`(`@/components/ui` 的 Button / Input / Select / RangePicker / Dropdown / FormItem)
- 图标:`@lucide/vue`(项目既有用法)
- 数据接口:通用 `loadDataApi` / `pageDataApi` / `saveObjectApi`(`fms-vue/src/services/api.js`)
- 测试:Vitest(`fms-vue/tests`)
- 后端:Spring Boot `fms-api` —— **本次零改动**
## 实现思路
### 1. 一套 AST,一个 SQL 构造器(核心)
把「常用条件」和「高级条件」的差异收敛为**操作符从哪来**:常用条件取 `defaultOperator(field)`,高级条件取用户选择,之后共用同一条链路。
```mermaid
flowchart LR
A[常用条件表单值] -->|operator = defaultOperator| C[统一 AST]
B[高级条件节点/分组] -->|operator = 用户选择 + 分组 logic| C
C --> D[astToSql]
D --> E[search_condition]
E --> F[/data/page]
G[fixedSearchCondition] --> E
```
- `OPERATORS`:按 `b_type` 分组的操作符枚举,唯一定义处(文本 `like/not_like/eq/ne/is_null/is_not_null`;数值/金钱 `eq/ne/gt/ge/lt/le/between`;日期 `between/ge/le/is_null/is_not_null`;枚举/下拉/复选 `eq/ne/in/not_in`)。
- `defaultOperator(field)`:文本 → `like`(即「几乎都是 like」),日期/数值区间 → `between`,其余 → `eq`。
- `buildConditionSql(node, fieldMap)`:单条件 → SQL,由现有 `buildQueryCondition` 抽出来,`sqlLiteral` / `escapeSqlText` / `isRangeQuery` 等既有逻辑全部复用。
- `buildGroupSql(group, fieldMap)`:递归;组内按 `logic` 连接,子节点多于 1 个时整体加括号;空值条件跳过。
- `astToSql(ast, fieldMap)`:唯一出口,根节点为空时返回 `''`。
- `buildQuickAst(items, values)` / `buildAdvancedAst({ nodes, groups, logic })`:两个入口。
- `buildQueryCondition(item, value)` **保留签名并内部转发**,现有三个调用方(`FmsModuleListPage`、`FmsQueryToolbar`、`FileListPanel`)不动。
### 2. 工具栏改造(`FmsQueryToolbar.vue`)
- 移除「单字段快搜框 + 顶部摊平的高级面板」,改为:
- 常用条件区:`.list-search` 网格 `repeat(4, 264px)`,`@container (max-width: 1130px)` 降 3 列;`QUICK_VISIBLE_LIMIT = 4`,其余「更多条件」展开并带角标。
- 生效条件 chips 条:单项可移除、全部清除。
- 「高级查询」入口 → 切换 `activeQueryMode`。
- 高级面板:左栏常驻 360px(dashboard 已验证:条件行两行堆叠、条件组左侧粗竖线表达嵌套、底部「已配置 N 个条件 / 取消 / 应用查询」)。
- **值控件复用 `FmsQueryControl`**:高级条件行按节点操作符构造临时 item(`{ config: { ...item.config, b_operator: node.operator }, field, options }`)传入,不重写一套输入控件;改变字段/操作符时按 `emptyQueryValue` 重置值。
- emit 契约由 `{ mode, entries }` 改为 `{ mode, ast }`,父页面用 `astToSql` 落地。
### 3. 页面接入(`FmsModuleListPage.vue`)
- `searchConditions`(SQL 片段数组)替换为 `queryAst`(shallowRef);`fetchData()` 用 `astToSql(queryAst, fieldMap)` 生成条件,再与 `fixedSearchCondition` 以 AND 组合。
- `resetSearch()` 改为**重建默认 AST**(依据模块配置的 `defaultValue`),修掉现有「重置会丢掉默认条件」的问题。
- 初始化时读取用户偏好,把 `query.area/order` 下发给工具栏、把 `view.hidden/order/width` 应用到 `listConfig`(在 `applyColumnSettings` 结果之上再覆盖)。
- 工具栏设置提交时调用 pref 保存。
### 4. 用户个性化(`fms-module-list/useModulePref.js`,新增)
- 读取:`loadDataApi('s_user_module_pref', "b_user_id = N'..' AND b_module_id = N'..'")`。
- 保存:`saveObjectApi([{ table: 's_user_module_pref', key_field: 'b_user_id,b_module_id', updates | inserts: [row] }])`;先查后写,捕获主键冲突时改走 `updates` 兜底(后端无 MERGE,属已知非原子点,影响可忽略)。
- 数据按规范成对维护 `pref` / `pref_org`,关闭设置抽屉时 diff 一次保存,不随每次拖拽写库。
- 合并顺序严格为 **模块默认 → 用户偏好 → 权限裁剪**,权限放最后,确保「偏好只能调布局、不得放宽权限」(`开发规范.md` 通用原则 10)。
- 用户编码取 `useAuthStore().userInfo.account`(缺失时回退 `loginInfo.user?.b_id`);`b_updated_by` / `b_updated_at` 前端赋值。
### 5. 后端改动清单
**零改动**。`search_condition` 仍由前端拼装(通用原则 5 明确不为此增加安全防护层);数据范围与字段查询权限现阶段在前端拼装(后端尚无权限引擎,属过渡实现),代码注释中写明约束,不写 TODO / 占位(通用原则 1)。
## 实现要点(防回归)
- `buildQueryCondition` 是既有公共函数,必须保留兼容,`FileListPanel.vue` 不能受影响。
- `FmsQueryControl` 的区间/多选判定来自 `item.config.b_operator`(`isRangeQuery` / `isMultipleSelect`),高级条件行复用它时必须同步 `b_operator`,否则控件形态错误。
- 现有「保存栏位设置」写的是 `s_module_schema`(模块级默认,管理员行为),与用户级 pref 是两条通道:渲染时 pref 覆盖在模块默认之上,二者不互相写回。
- 高级模式下不重复渲染常用条件区(dashboard 已验证:搜索卡隐藏、表格占满剩余宽高)。
- 只在模块初始化时读一次 pref;设置变更在抽屉关闭/完成时写一次,避免高频写库。
- AST → SQL 为纯函数、条件数量级在数十以内,每次查询构建一次即可,无需缓存。
## 目录结构
```
fms-vue/src/components/fms-module-common/
└── queryUtils.js # [MODIFY] 新增 OPERATORS / defaultOperator / buildConditionSql /
# buildGroupSql / astToSql / buildQuickAst / buildAdvancedAst /
# hasConditionValue;buildQueryCondition 转为内部转发,签名不变
fms-vue/src/components/fms-module-list/
├── FmsQueryToolbar.vue # [MODIFY] 改 dashboard 形态:常用条件 4 列网格 + 更多条件展开 +
# 生效条件 chips + 高级查询左栏常驻面板(条件行/条件组/操作符可选/
# 应用查询)+ 查询设置抽屉(常用/高级/隐藏分区 + 拖拽排序 + 恢复默认);
# emit('search', { mode, ast })
├── FmsModuleListPage.vue # [MODIFY] searchConditions → queryAst;fetchData 用 astToSql;
# resetSearch 回到默认条件;加载并下发 pref;保存 pref
└── useModulePref.js # [NEW] 偏好读写与合并:loadModulePref / mergeQueryLayout /
# mergeViewLayout / saveModulePref,含权限裁剪与 pref_org 基线
fms-vue/src/views/dashboard/
└── index.vue # [MODIFY] 仅删除与公共构造器重复的 quickQueryAst / advancedQueryAst
# (当前无调用点),其余保持原型不变
fms-vue/tests/unit/
├── query-ast.spec.js # [NEW] AST 构造与 SQL 生成用例:默认操作符、空值跳过、between 单端、
# 嵌套括号、转义、buildQueryCondition 兼容
└── module-pref.spec.js # [NEW] 偏好合并顺序、权限裁剪优先、diff 保存请求体
```
## 关键结构
```js
// 统一 AST(两种模式共用)
// { type: 'group', logic: 'and' | 'or', children: [Condition | Group] }
// { type: 'condition', field: 'b_no', operator: 'like', value: 'FMS' }
// queryUtils.js 新增签名(均为纯函数)
function defaultOperator(field: object): string
function hasConditionValue(value: unknown): boolean
function buildConditionSql(node: object, fieldMap: Map<string, object>): string
function buildGroupSql(group: object, fieldMap: Map<string, object>): string
function astToSql(ast: object | null, fieldMap: Map<string, object>): string
function buildQuickAst(items: object[], values: object): object
function buildAdvancedAst(state: { nodes: object[], groups: object[], logic: string }): object
```
```
{
"schemaVersion": 1,
"view": { "default": { "hidden": ["b_bz"], "order": ["b_no", "b_name"], "width": { "b_no": 160 } } },
"query": { "default": { "area": { "b_remark": "quick", "b_score": "hidden" }, "order": ["b_no", "b_status"] } }
}
```
## 设计风格
沿用项目现有设计语言(`--fms-card` 白卡 + 6px 圆角 + 轻阴影 + 8px 灰缝),与 dashboard 布局原型保持一致:浅灰画布上「搜索卡」与「表格卡」两张白卡纵向堆叠,搜索卡高度随内容、表格卡撑满剩余高度。查询区采用顶部标签 + 定宽网格,保证字段位置稳定、英文标签变长也不漂移。整体为克制的现代企业级风格(Enterprise Minimal),交互反馈靠主色 hover / 淡主色底,不使用重投影或渐变。
## 页面区块
1. **常用条件区(搜索卡)**:4 列 × 264px 定宽网格,标签在上控件在下;超过 4 个的条件收进「更多条件」展开,按钮上带生效条件数角标;操作区分「查询/重置」「更多条件/高级查询」两组,配置组钉在行尾;右上角「查询设置」入口绝对定位不占流内高度。
2. **高级查询面板**:左栏常驻 360px 白卡(与树形页左栏同一槽位),头部「高级查询 + 设置 + 关闭」,body 内根逻辑选择、条件行(字段 + 关联徽标 / 操作符 / 值,两行堆叠)、条件组(浅蓝左边框 + 组内逻辑 + 删除组)、底部「已配置 N 个条件 / 取消 / 应用查询」;无遮罩,表格保持可交互。
3. **生效条件 chips 条**:搜索卡底部细分隔线下方一行,浅蓝底圆角 chip 展示每个生效条件文本,可单个 × 移除,右侧「清除全部」;未应用查询时保留条位避免跳动。
4. **查询设置抽屉**:右侧 420px 窄抽屉(浅遮罩),按「页面常用条件 / 仅高级条件 / 已隐藏字段 / 不可用字段」四组罗列,行内拖拽手柄排序 + 「设为第一个 / 移到高级 / 隐藏 / 恢复」文字操作,底部「恢复默认 / 完成设置」;无查询权限字段整行灰显并标注「无查询权限」。
## 交互
条件编辑即时影响本地状态,「应用查询」才固化生效态;切换高级模式时常用条件以 chip 形式保留可见可清除;拖拽排序以源行半透明 + 目标行顶部插入指示线反馈。
## 响应式
搜索卡使用容器查询(`@container`):内容宽度不足 4 列时降为 3 列;高级模式下取消页面最小宽度限制,表格自行横向滚动。
## Agent Extensions
### Skill
- **ui-ux-pro-max**
- 用途:在改造 `FmsQueryToolbar` 的查询区(4 列网格、chips 条、高级条件面板、设置抽屉)时,核对布局节奏、间距层级、拖拽反馈与可访问性细节。
- 预期结果:产出与 dashboard 原型一致、且在企业级密集信息场景下可读可用的查询区视觉与交互规范,避免列宽、角标、chip 溢出等细节返工。
@@ -0,0 +1,265 @@
---
name: 组件库动效体系落地
overview: 按动效规范为 fms-vue 组件库补齐动画体系:9 个 motion token 落进 tokens.css,浮层组件(popover/modal/drawer)补离场状态机,纯 CSS 过渡组件批量补课,修复 Spin 组件不旋转的缺陷并收敛业务页手写 keyframes,全站适配 prefers-reduced-motion。
todos:
- id: motion-tokens
content: 用 [subagent:code-explorer] 盘点组件样式入口,并把 9 个动效 token 与 reduced-motion 归零块写入 tokens.css
status: completed
- id: popover-motion
content: 给 popover 补进出场过渡,覆盖 dropdown / tooltip / select 并确认 select 是否继承
status: completed
dependencies:
- motion-tokens
- id: modal-drawer-motion
content: modal 与 drawer 分层进出场,Drawer 用组件级变量承载 240/200ms
status: completed
dependencies:
- motion-tokens
- id: button-input-motion
content: button 补颜色过渡与按下缩放,input 补聚焦过渡
status: completed
dependencies:
- motion-tokens
- id: form-control-motion
content: switch、checkbox、radio、segmented、tabs 补控件微交互过渡
status: completed
dependencies:
- motion-tokens
- id: collapse-motion
content: FormGroup 与树节点用 grid 1fr 到 0fr 实现折叠过渡
status: completed
dependencies:
- motion-tokens
- id: feedback-motion
content: message 与 notification 用 TransitionGroup 补进出场,scrollbar 补淡出
status: completed
dependencies:
- motion-tokens
- id: spin-motion
content: 修复 Spin 旋转并收敛业务页面两处手写 keyframes
status: completed
dependencies:
- motion-tokens
- id: motion-verify
content: 用 [skill:ui-ux-pro-max] 核对动效与无障碍,跑 oxlint 与编译确认无裸写时长
status: completed
dependencies:
- popover-motion
- modal-drawer-motion
- button-input-motion
- form-control-motion
- collapse-motion
- feedback-motion
- spin-motion
---
## 产品概述
为 `fms-vue` 内置组件库(`src/components/ui`,26 个组件)建立统一的动效体系:此前组件库里已有的动画代码被移除过,现按用户给定的动效规范全部恢复并补齐。目标是在高频后台操作场景下,让状态变化"可被感知但不拖慢操作"。
## 核心功能
- **动效契约**:9 个动效 token(3 档时长、3 档缓动、2 档位移幅度)写入 `src/theme/tokens.css`,沿用 `--fms-` 命名空间
- **浮层进出场**:popover、modal、drawer、下拉菜单、tooltip、select 具备进场与退场动画(遮罩与内容分层、进场错峰、退场同时走)
- **控件微交互**:按钮 hover 变色 + 按下缩放、输入框聚焦、开关滑块位移、分段控制器与标签页指示器滑动、勾选态变化
- **折叠展开**:表单分组与树节点用网格行高过渡实现展开收起
- **反馈提示**:message / notification 进出场、滚动条淡出
- **加载指示器**:修复 Spin 组件不旋转的缺陷,并将业务页面中各自手写的旋转动画收敛回组件
- **无障碍**:系统开启"减少动态效果"时时长归零(保留状态变化、去掉过渡),浮层动画期间不移动焦点
## 视觉与体验效果
按钮按下时有极轻微的收缩反馈;浮层从遮罩淡入到内容浮起有清晰的先后层次,关闭时干脆利落;开关拨动、指示器切换跟随手指;折叠区域平滑展开。整体节奏偏快(100~200ms),不出现回弹、位移、页面转场等干扰性效果。
## 技术栈
- 框架与样式:Vue 3(`script setup`)+ SCSS(沿用 `@use './index.scss'` 拆分约定)
- 变量体系:CSS 自定义属性,复用现有 `--fms-` 命名空间与 `src/theme/tokens.css` 单源
- 过渡机制:CSS `transition` + Vue 内置 Transition / TransitionGroup 组件
- 校验:`pnpm exec oxlint` + Vite 编译检查(dev server 端口 5082)
## 实现方案
### 分层策略
1. **契约层**:9 个 token 落到 `tokens.css` 的 `:root`,并追加 `prefers-reduced-motion` 归零块。所有组件只引用变量、不写裸时长,保证"改一处、全站一致"。
2. **行为层**:浮层组件用 Vue 内置 Transition 承载进出场 —— 这是本次工作量最大、也最容易出 bug 的部分,先做 popover(dropdown / tooltip / select 的共同基座)。选择:全局开关只做系统级自动适配,不新建 ConfigProvider、不引入手动开关状态。
3. **表现层**:纯 CSS 补控件微交互,无卸载时序风险,可并行推进。
### 关键决策与理由
**决策 1:浮层离场用 Vue 内置 Transition,不手写 `isLeaving` 状态类 + `setTimeout`**
规范给出的 `.is-leaving` 写法是框架无关方案;本项目是 Vue 3,用内置过渡组件更省代码。更关键的理由是:**reduced-motion 下 token 归零会让 `transition-duration` 变成 `0ms`,此时 `transitionend` 事件不会触发** —— 若卸载靠该事件驱动,浮层会卡在页面上关不掉。Vue 的 Transition 会读取计算样式,时长为 0 时直接结束过渡,天然不会卡死。代价是过渡类名变成 `.fms-xxx-enter-active` 形态,但时长与缓动参数完全按规范取值。
**决策 2:Drawer 的 240ms / 200ms 用组件级 CSS 变量承载,不裸写数字**
规范建议这两个值写死在组件里,但那样会逃出 reduced-motion 的变量归零覆盖,无障碍要求落空。改为组件级变量(默认值保持 240/200 的视觉意图),并在组件级补一条 reduced-motion 覆盖,兼顾"不进全局 token"与"可被关闭"。
**决策 3:只动 opacity / transform / color 系**
所有新增过渡严格限定这三类属性,走合成层,不触发布局与重绘。表格行、数字滚动、页面转场、`transition: all` 一律不做。
**决策 4:折叠展开用网格 `1fr → 0fr`**
`height: auto` 无法过渡。FormGroup 当前已是 `v-show` + grid 结构,直接可用;树节点同步采用同一手法,避免 JS 测高。
**决策 5:补齐规范对照表遗漏的场景**
Switch 滑块、Segmented 滑块、Scrollbar 淡出、Spin 循环旋转(为"禁止无意义图标旋转"开显式豁免)、Tooltip 用更快的档位(高频鼠标扫过场景,避免拖残影);同时明确位移幅度取值用途:下拉 / 提示用 4px、Modal 用 8px、Drawer 用整屏位移。
**决策 6:按钮 hover 采用规范原值(base 档同时管进出)**
此前工具栏 ghost 按钮的"背景色闪烁"真因是性能浮层压住按钮命中区(已单独修复),与过渡时长无关。此处不设"离开即时"之类的例外规则,保持全站一致。
### 性能与可靠性
- 只改合成层属性,无布局抖动;无新增 DOM 节点(Transition 不额外渲染)
- 每个组件改动互相独立,浮层与纯 CSS 组分批推进,避免一次性全量改动难以定位问题
- 退场时序统一由 Transition 管理,不引入自定义定时器,杜绝"浮层关不掉 / 提前卸载闪烁"两类经典 bug
### 验证方式
用户明确要求不自动启动浏览器实测。本次以静态检查为主:`oxlint` + Vite 编译 + 逐文件核对"无裸写时长数字、无禁用属性、reduced-motion 覆盖完整"。
## 实现注意
- 严格沿用存量风格:SCSS 独立文件用 `@use './index.scss'` 引入;注释用中文说明"为什么这样做",与现有代码一致
- 除 Transition 包裹层外不改动组件 DOM 结构与既有 props / emits,保证向后兼容
- popover 是 dropdown / tooltip / select 的基座,优先改造收益最大;select 是否完全复用 popover 需在实施时确认,若为独立实现在其内部补同样处理
- 控件类动画不影响键盘操作与焦点顺序;浮层禁止在动画期间抢焦点
- 不为动画而动画:无状态变化的组件(栅格、分割器等拖拽即时类)不强行加过渡
## 架构设计
本次改动是"在既有组件库上加一层动效契约",不引入新的架构模式:
```mermaid
graph TD
A["tokens.css<br/>9 个动效 token + reduced-motion 归零"] --> B["浮层组件<br/>popover / modal / drawer"]
A --> C["表单控件<br/>button / input / switch / checkbox / radio"]
A --> D["导航与容器<br/>tabs / segmented / form-group / tree / scrollbar"]
A --> E["反馈与加载<br/>message / notification / spin"]
B --> B1["dropdown / tooltip / select<br/>经 popover 继承"]
F["业务页面手写旋转动画"] --> E
```
- **契约层**:`src/theme/tokens.css` 提供唯一动效参数来源
- **行为层**:浮层组件内置过渡状态,dropdown / tooltip / select 通过基座继承
- **表现层**:各组件 SCSS 引用 token,声明自己用哪几档
## 目录结构
本次改动为既有项目补动效,涉及文件如下(未列出的文件不动):
```
fms-vue/
├── src/
│ ├── theme/
│ │ └── tokens.css # [MODIFY] 追加 9 个动效 token(3 档时长 / 3 档缓动 / 2 档位移)到 :root,
│ │ # 并追加 @media (prefers-reduced-motion: reduce) 块把三档时长归零。
│ │ # 深色 .dark 块不需要覆盖(动效参数与色彩无关)。
│ ├── components/ui/
│ │ ├── popover/
│ │ │ ├── popover.vue # [MODIFY] 浮层基座:把 v-if 内容包进 Transition,补进退场。
│ │ │ │ # 进场 base + ease-out,退场 base + ease-in;定位逻辑不动。
│ │ │ └── index.scss # [MODIFY] 补 enter/leave 过渡类(opacity + 4px 位移)
│ │ ├── modal/
│ │ │ ├── modal.vue # [MODIFY] 遮罩与内容分层过渡,进场遮罩先铺、内容延迟 20ms 落
│ │ │ └── index.scss # [MODIFY] 进场 slow + ease-out(scale 0.96→1),退场 base + ease-in;
│ │ │ # 遮罩进场 base + ease-out、退场 base + ease-in
│ │ ├── drawer/
│ │ │ ├── drawer.vue # [MODIFY] 补进场/退场过渡,复用与 modal 相同的分层写法
│ │ │ └── index.scss # [MODIFY] 用组件级变量承载 240ms 进 / 200ms 出(不裸写),
│ │ │ # 并在组件级加 reduced-motion 覆盖
│ │ ├── select/
│ │ │ ├── select.vue # [MODIFY] 确认是否复用 popover;若独立实现则补下拉进出场
│ │ │ └── index.scss # [MODIFY] 同上,补选项面板过渡类
│ │ ├── dropdown/index.scss # [MODIFY] 下拉项 hover 底色过渡(菜单容器过渡由 popover 提供)
│ │ ├── tooltip/index.scss # [MODIFY] 用更快档位(100ms),高频鼠标扫过场景避免拖残影
│ │ ├── button/index.scss # [MODIFY] 补 background-color / border-color / box-shadow(base + standard)
│ │ │ # 与 transform(fast + standard),补 :active 的 scale 0.97
│ │ ├── input/
│ │ │ ├── input.vue # [MODIFY] 核对聚焦/校验态切换所依赖的 class 是否齐备
│ │ │ └── index.scss # [MODIFY] 补边框色与阴影的聚焦过渡(base + standard)
│ │ ├── switch/index.scss # [MODIFY] 滑块 translateX 过渡(fast + standard),轨道颜色同步
│ │ ├── checkbox/index.scss # [MODIFY] 勾选态颜色/描边过渡
│ │ ├── radio/index.scss # [MODIFY] 选中态过渡,与 checkbox 同参数
│ │ ├── segmented/index.scss # [MODIFY] 滑块位移过渡(base + standard),与 tabs 指示器同类
│ │ ├── tabs/
│ │ │ ├── tabs.vue # [MODIFY] 若已有滑动指示器元素,补其位移态;无则仅补 pane 淡入
│ │ │ └── index.scss # [MODIFY] 指示器位移过渡(base + standard)
│ │ ├── form/
│ │ │ ├── FormGroup.vue # [MODIFY] 折叠容器改为 grid 1fr↔0fr 过渡(分组图标同步旋转)
│ │ │ └── index.scss # [MODIFY] 折叠过渡类(base + standard),禁止动 height
│ │ ├── tree/
│ │ │ ├── tree-node.vue # [MODIFY] 子节点容器套用 grid 1fr↔0fr 手法
│ │ │ └── index.scss # [MODIFY] 展开收起过渡 + 展开箭头旋转(fast + standard)
│ │ ├── message/
│ │ │ ├── message-container.vue # [MODIFY] 列表渲染改为 TransitionGroup,使项进出有过渡
│ │ │ └── index.scss # [MODIFY] 进场 base + ease-out、退场 base + ease-in
│ │ ├── notification/
│ │ │ ├── notification-container.vue # [MODIFY] 同 message 的 TransitionGroup 处理
│ │ │ └── index.scss # [MODIFY] 同 message 参数
│ │ ├── scrollbar/scrollbar.vue # [MODIFY] leave 模式下滚动条淡出(opacity 过渡,样式为组件内联)
│ │ ├── spin/index.scss # [MODIFY] 补 @keyframes 旋转动画到 .spin-indicator,
│ │ │ # 时长用组件级变量承载以便 reduced-motion 归零
│ │ ├── tag/index.scss # [MODIFY] 关闭/删除类交互的轻量过渡(若存在)
│ │ └── pagination/index.scss # [MODIFY] 页码切换态过渡(若存在)
│ └── views/module/
│ ├── module-management/index.vue # [MODIFY] 移除手写 .mm-config-state__spinner 与 @keyframes mm-config-spin,
│ │ # 改用 Spin 组件,复用统一旋转参数
│ └── menu-management/index.vue # [MODIFY] 同上,移除 @keyframes mm-spin
```
## 关键代码结构
动效契约(`src/theme/tokens.css`)是全部组件依赖的唯一来源,必须精确定义:
```css
:root {
/* 时长:跟手操作 / 同层小元素 / 占满屏幕 三档封顶 */
--fms-duration-fast: 100ms;
--fms-duration-base: 150ms;
--fms-duration-slow: 200ms;
/* 缓动:进场用 out、退场用 in、纯颜色变化用 standard */
--fms-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
--fms-ease-out: cubic-bezier(0, 0, 0.2, 1);
--fms-ease-in: cubic-bezier(0.4, 0, 1, 1);
/* 位移幅度:下拉 / 提示用 sm,Modal 用 md */
--fms-offset-sm: 4px;
--fms-offset-md: 8px;
}
/* 无障碍:时长归零保留状态变化,去掉过渡过程 */
@media (prefers-reduced-motion: reduce) {
:root {
--fms-duration-fast: 0ms;
--fms-duration-base: 0ms;
--fms-duration-slow: 0ms;
}
}
/* 组件级变量示例(Drawer):不进全局 token,但同样可被上面的覆盖命中 */
.fms-drawer {
--fms-drawer-duration-in: 240ms;
--fms-drawer-duration-out: 200ms;
}
@media (prefers-reduced-motion: reduce) {
.fms-drawer {
--fms-drawer-duration-in: 0ms;
--fms-drawer-duration-out: 0ms;
}
}
```
## Agent Extensions
### SubAgent
- **code-explorer**
- Purpose: 在动工前一次性盘点 `src/components/ui` 下 26 个组件的样式入口、可动画属性与浮层显隐结构,确认"哪些组件走 Transition、哪些纯 CSS 补课、select 是否复用 popover",避免逐文件试探。
- Expected outcome: 输出一份带路径的改动清单与每类组件的处理方式,作为后续实现的任务依据。
### Skill
- **ui-ux-pro-max**
- Purpose: 核对本次动效参数与无障碍设计的合理性(时长是否落在后台高频场景的舒适区间、缓动方向是否正确、reduced-motion 与焦点处理是否到位),并检查是否有遗漏的交互态。
- Expected outcome: 一份针对动效参数与无障碍的核对结论,确认无系统性偏差后再收尾。
+31 -31
View File
@@ -116,7 +116,7 @@ s_module
| 表单布局 | JSON | 支持分组、栅格、选项卡和条件显示 |
| 查询配置 | JSON | 支持同字段多条件、条件组和嵌套逻辑 |
| 字段分组 | 布局 JSON 节点 | 不再建立独立字段分组表 |
| 用户偏好 | JSON 偏离量 | 只保存用户相对默认配置的修改 |
| 用户偏好 | JSON 偏离量 | 只保存用户相对系统默认的修改 |
| 多语言 | 关系表 | 支持按语言查询、导入、导出和回退 |
| 用户与部门 | 关系表 | 用户归属部门,部门支持层级树 |
| 权限 | 关系表 | 见第 14 节,不与界面布局混合 |
@@ -341,6 +341,21 @@ query
当前阶段不维护同一类型的多套命名配置。若未来确实需要多套列表、表单或查询方案,再增加配置编码字段,或在 JSON 内增加方案节点。
### 6.0 配置两层继承(系统默认 → 个人)
模块 UI 配置采用两层继承,由两张字符结构一致的表(`s_module_schema` 系统默认 + `s_user_module_pref` 个人覆盖)表达。配置层级由**表本身**体现,不在 JSON 内部用 `default`、`system`、`user` 等节点表示层级。
```text
系统默认(s_module_schema) 产品/开发者提供的标准 Schema
↓ 覆盖
个人配置(s_user_module_pref) 用户在系统默认之上的个人调整
```
- `view`、`edit`、`query` 是三种一致的 `b_schema_type`,两层共用同一命名,不拆成独立表,也不引入部门/角色/租户配置层。
- 优先级:个人 > 系统默认。个人层无记录时即系统默认。
- **保存为通用**:把当前查询布局回写为新的系统默认——直接更新 `s_module_schema.query` 的 quick 顺序并标记隐藏字段,同时清空该用户的个人覆盖,所有用户默认即在改后配置。
- 覆盖方式:`s_module_schema` 保存完整默认 Schema;`s_user_module_pref` 只保存相对系统默认的**增量覆盖**,不全量复制,因此系统默认修改后能自然继承。
### 6.1 列表配置示例
```json
@@ -427,57 +442,42 @@ query
}
```
## 7. 用户个性化
## 7. 用户个性化(个人层)
原有列表字段偏好和查询字段偏好合并为一张表。用户偏好只保存相对模块默认配置的偏离量,不复制完整模块配置。
个人配置是两层继承的最上层,只保存相对系统默认(`s_module_schema`)的偏离量,不复制完整模块配置。与 `s_module_schema` 统一使用 `b_schema_type` + `b_schema_json`,按 schema 类型分别存一条。
```sql
-- 用户模块偏好表
-- 用户模块偏好表(个人层)
create table dbo.s_user_module_pref (
b_user_id varchar(50) not null, -- 用户编码
b_user_id varchar(50) not null, -- 用户编码(关联 b_user.b_id)
b_module_id varchar(50) not null, -- 模块编码
b_pref_json nvarchar(max) not null, -- 用户偏离配置 JSON
b_schema_type varchar(20) not null, -- view / edit / query
b_schema_json nvarchar(max) not null, -- 个人覆盖 JSON(相对系统默认)
b_updated_by varchar(50) null, -- 最后修改人
b_updated_at datetime2 null, -- 最后修改时间
primary key (b_user_id, b_module_id)
primary key (b_user_id, b_module_id, b_schema_type)
);
create index ix_s_user_module_pref_module
on dbo.s_user_module_pref (b_module_id, b_user_id);
```
偏好示例:
查询布局偏好示例(平铺、增量,不使用 `view.default` / `query.default` 节点):
```json
{
"schemaVersion": 1,
"view": {
"default": {
"hidden": ["b_bz"],
"order": ["b_no", "b_name", "b_status"],
"width": {
"b_no": 160,
"b_name": 240
}
}
},
"query": {
"default": {
"quick": ["b_no", "b_status"]
}
}
"area": { "b_no": "quick", "b_bz": "hidden" },
"order": { "b_no": 10, "b_name": 20 }
}
```
偏好 JSON 中的 `view.default`、`query.default` 是当前唯一方案的占位,与第 6 节“当前阶段不维护同一类型的多套命名配置”一致;将来出现多套方案时才用该层级区分。
合并规则(两层依次应用):
合并规则:
1. 读取 `s_module_schema` 中的模块默认配置;
2. 读取当前用户的 `s_user_module_pref.b_pref_json`;
3. 按配置类型应用偏离量;
4. 已停用、已删除或无权限的字段不得因用户偏好重新出现;
5. 恢复默认时删除当前模块偏好记录,或删除 JSON 中对应配置片段。
1. 读取 `s_module_schema` 系统默认(query 布局由 `quick` 列表 + 条件推导);
2. 读取当前用户的 `s_user_module_pref.b_schema_json`,按 schema 类型叠加个人偏离量,得到最终配置;
3. 已停用、已删除或无权限的字段(`s_user_field_power`)不得因配置重新出现;权限过滤在配置合并之后执行,永远优先于配置;
4. 恢复默认(恢复通用)= 删除 `s_user_module_pref` 对应行,回落到系统默认;「恢复默认」是删除覆盖,不是新增配置层。
## 8. 自动编码
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

@@ -0,0 +1,138 @@
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.util.*;
/**
* 导出旧系统库(FMS)的用户表结构为 Markdown 文档。
*
* 用法:
* javac -encoding UTF-8 -cp "mssql-jdbc-13.4.0.jre11.jar" SchemaDumpMarkdown.java
* java -cp ".;mssql-jdbc-13.4.0.jre11.jar" SchemaDumpMarkdown <输出文件绝对路径> [数据库名,默认 G3HY2025]
*/
public class SchemaDumpMarkdown {
static final String BASE_URL = "jdbc:sqlserver://118.89.70.199:1433;encrypt=true;trustServerCertificate=true";
static final String USER = "sa";
static final String PASSWORD = "zhang520..";
static class Col {
String name, type, len, precScale, nullable, def;
}
public static void main(String[] args) throws Exception {
String out = args.length > 0 ? args[0] : "Fms旧系统表结构.md";
String db = args.length > 1 ? args[1] : "G3HY2025";
String url = BASE_URL + ";databaseName=" + db;
Map<String, List<Col>> cols = new LinkedHashMap<>();
Map<String, String> pk = new LinkedHashMap<>();
Map<String, List<String>> fk = new LinkedHashMap<>();
try (Connection conn = DriverManager.getConnection(url, USER, PASSWORD)) {
// 表清单
List<String> tables = new ArrayList<>();
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES "
+ "WHERE TABLE_TYPE='BASE TABLE' ORDER BY TABLE_NAME")) {
while (rs.next()) tables.add(rs.getString(1));
}
// 字段
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, "
+ "CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, IS_NULLABLE, COLUMN_DEFAULT "
+ "FROM INFORMATION_SCHEMA.COLUMNS ORDER BY TABLE_NAME, ORDINAL_POSITION")) {
while (rs.next()) {
Col c = new Col();
c.name = rs.getString(2);
c.type = rs.getString(3);
int maxLen = rs.getInt(4);
c.len = rs.wasNull() ? "" : (maxLen == -1 ? "MAX" : String.valueOf(maxLen));
Object p = rs.getObject(5);
if (p != null) c.precScale = p + "," + rs.getObject(6);
else c.precScale = "";
c.nullable = rs.getString(7);
c.def = rs.getString(8);
cols.computeIfAbsent(rs.getString(1), k -> new ArrayList<>()).add(c);
}
}
// 主键
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT OBJECT_NAME(k.parent_object_id), c.name "
+ "FROM sys.key_constraints k "
+ "JOIN sys.index_columns ic ON ic.object_id = k.parent_object_id AND ic.index_id = k.unique_index_id "
+ "JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id "
+ "WHERE k.type = 'PK' "
+ "ORDER BY 1, ic.key_ordinal")) {
while (rs.next()) {
String t = rs.getString(1);
pk.merge(t, rs.getString(2), (a, b) -> a + ", " + b);
}
}
// 外键
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT OBJECT_NAME(f.parent_object_id), "
+ "COL_NAME(fc.parent_object_id, fc.parent_column_id), "
+ "OBJECT_NAME(f.referenced_object_id), "
+ "COL_NAME(fc.referenced_object_id, fc.referenced_column_id) "
+ "FROM sys.foreign_keys f JOIN sys.foreign_key_columns fc ON f.object_id = fc.constraint_object_id "
+ "ORDER BY 1, 2")) {
while (rs.next()) {
fk.computeIfAbsent(rs.getString(1), k -> new ArrayList<>())
.add(rs.getString(2) + " -> " + rs.getString(3) + "(" + rs.getString(4) + ")");
}
}
StringBuilder sb = new StringBuilder();
sb.append("# Fms 旧系统表结构(G3HY2025 库)\n\n");
sb.append("> 来源库:`").append(db).append("`(jdbc:sqlserver://118.89.70.199:1433;databaseName=").append(db).append(")\n");
sb.append("> 仅包含用户表(BASE TABLE),不含视图。字段「备注」列留空,供后续补充含义。\n");
sb.append("> 拉取时间:").append(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date())).append("\n");
sb.append("> 约束情况:该库未定义外键约束(`sys.foreign_keys` = 0);")
.append(pk.size()).append(" 张表定义了主键,").append(tables.size() - pk.size())
.append(" 张表无主键。\n\n");
sb.append("共 **").append(tables.size()).append("** 张表。\n\n");
sb.append("## 表清单(索引)\n\n");
sb.append("| # | 表名 | 字段数 | 主键 |\n| --- | --- | --- | --- |\n");
int i = 1;
for (String t : tables) {
List<Col> cs = cols.getOrDefault(t, Collections.emptyList());
sb.append("| ").append(i++).append(" | `").append(t).append("` | ").append(cs.size())
.append(" | ").append(esc(pk.getOrDefault(t, ""))).append(" |\n");
}
sb.append("\n## 各表字段结构\n");
for (String t : tables) {
List<Col> cs = cols.getOrDefault(t, Collections.emptyList());
sb.append("\n### ").append(t).append("\n\n");
if (pk.containsKey(t)) sb.append("**主键:** ").append(esc(pk.get(t))).append("\n\n");
List<String> fs = fk.get(t);
if (fs != null && !fs.isEmpty()) {
sb.append("**外键:**\n");
for (String f : fs) sb.append("- ").append(esc(f)).append("\n");
sb.append("\n");
}
sb.append("| 字段名 | 类型 | 长度 | 精度,小数位 | 可空 | 默认值 | 备注 |\n");
sb.append("| --- | --- | --- | --- | --- | --- | --- |\n");
for (Col c : cs) {
sb.append("| `").append(c.name).append("` | ").append(c.type).append(" | ").append(c.len)
.append(" | ").append(c.precScale).append(" | ").append(c.nullable)
.append(" | ").append(c.def == null ? "" : esc(c.def)).append(" | |\n");
}
}
try (Writer w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(out), StandardCharsets.UTF_8))) {
w.write(sb.toString());
}
System.out.println("已写出 " + tables.size() + " 张表 -> " + out);
}
}
static String esc(String s) {
return s == null ? "" : s.replace("|", "\\|");
}
}
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,393 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e139]:
- generic [ref=e141]:
- button "金地集团 企业版" [ref=e143] [cursor=pointer]:
- generic [ref=e147]:
- generic [ref=e148]: 金地集团
- generic [ref=e149]: 企业版
- navigation "主导航" [ref=e154]:
- link "工作台" [ref=e155] [cursor=pointer]:
- /url: /dashboard
- link "组件库" [ref=e163] [cursor=pointer]:
- /url: /demo
- button "模块中心" [ref=e172] [cursor=pointer]
- separator
- generic [ref=e181]:
- banner [ref=e182]:
- button "折叠侧栏" [ref=e184] [cursor=pointer]
- tablist [ref=e190]:
- tab "工作台" [selected] [ref=e191] [cursor=pointer]
- generic [ref=e193]:
- button "English" [ref=e194] [cursor=pointer]
- button "主题色切换" [ref=e199] [cursor=pointer]
- button "切换到深色模式" [ref=e206] [cursor=pointer]
- button "全屏" [ref=e213] [cursor=pointer]
- button "通知" [ref=e219] [cursor=pointer]
- button "超 超级管理员" [ref=e224] [cursor=pointer]:
- generic [ref=e225]: 超
- generic [ref=e226]: 超级管理员
- generic [ref=e230]:
- generic [ref=e231]:
- button "纯列表" [ref=e232] [cursor=pointer]
- button "树+表格" [ref=e233] [cursor=pointer]
- generic [ref=e234]:
- generic [ref=e235]:
- button "查询设置" [ref=e236] [cursor=pointer]
- generic [ref=e240]:
- generic [ref=e241]:
- generic [ref=e242]: 编号
- textbox [ref=e244]
- generic [ref=e245]:
- generic [ref=e246]: 名称
- textbox [ref=e248]
- generic [ref=e249]:
- generic [ref=e250]: 类型
- combobox [ref=e253]
- generic [ref=e256]:
- generic [ref=e257]: 状态
- combobox [ref=e260]
- generic [ref=e263]:
- generic [ref=e264]:
- button "查询" [ref=e265] [cursor=pointer]
- button "重置" [ref=e266] [cursor=pointer]
- generic [ref=e267]:
- button "更多条件" [ref=e268] [cursor=pointer]
- button "高级查询" [ref=e271] [cursor=pointer]
- generic [ref=e273]:
- generic [ref=e274]:
- generic [ref=e275]:
- button "新增" [ref=e276] [cursor=pointer]
- button "导入" [ref=e278] [cursor=pointer]
- button "删除" [disabled] [ref=e282]
- generic [ref=e286]:
- button "EN" [ref=e287] [cursor=pointer]
- button "刷新数据" [ref=e288] [cursor=pointer]
- button "导出" [ref=e294] [cursor=pointer]
- button "列设置" [ref=e298] [cursor=pointer]
- button "更多" [ref=e301] [cursor=pointer]
- table [ref=e310]:
- rowgroup [ref=e311]:
- row [ref=e312]:
- columnheader
- columnheader [ref=e313]:
- generic [ref=e315] [cursor=pointer]:
- checkbox
- columnheader "序号" [ref=e317]
- columnheader "ID" [ref=e320]
- columnheader "名称" [ref=e323]
- columnheader "类型" [ref=e326]
- columnheader "状态" [ref=e329]
- columnheader "启用" [ref=e332]
- columnheader "评分" [ref=e335]
- columnheader "字段 1" [ref=e338]
- columnheader "字段 2" [ref=e341]
- columnheader "字段 3" [ref=e344]
- columnheader "字段 4" [ref=e347]
- columnheader "字段 5" [ref=e350]
- columnheader "字段 6" [ref=e353]
- columnheader [ref=e356]
- rowgroup [ref=e357]:
- row
- row [ref=e358]:
- cell
- cell [ref=e359]:
- generic [ref=e360] [cursor=pointer]:
- checkbox
- cell "1" [ref=e362]
- cell "1" [ref=e364]
- cell "任务-00001" [ref=e366]
- cell "任务" [ref=e368]
- cell "启用" [ref=e370]
- cell [ref=e372]:
- generic [ref=e373] [cursor=pointer]:
- checkbox "启用"
- cell "85" [ref=e375]
- cell "1-1" [ref=e377]
- cell "1-2" [ref=e379]
- cell "1-3" [ref=e381]
- cell "1-4" [ref=e383]
- cell "1-5" [ref=e385]
- cell "1-6" [ref=e387]
- cell [ref=e389]
- row [ref=e390]:
- cell
- cell [ref=e391]:
- generic [ref=e392] [cursor=pointer]:
- checkbox
- cell "2" [ref=e394]
- cell "2" [ref=e396]
- cell "任务-00002" [ref=e398]
- cell "作业" [ref=e400]
- cell "禁用" [ref=e402]
- cell "启用" [ref=e404]:
- generic [ref=e405] [cursor=pointer]:
- checkbox "启用" [checked]
- cell "56.4" [ref=e407]
- cell "2-1" [ref=e409]
- cell "2-2" [ref=e411]
- cell "2-3" [ref=e413]
- cell "2-4" [ref=e415]
- cell "2-5" [ref=e417]
- cell "2-6" [ref=e419]
- cell [ref=e421]
- row [ref=e422]:
- cell
- cell [ref=e423]:
- generic [ref=e424] [cursor=pointer]:
- checkbox
- cell "3" [ref=e426]
- cell "3" [ref=e428]
- cell "任务-00003" [ref=e430]
- cell "流程" [ref=e432]
- cell "待审" [ref=e434]
- cell [ref=e436]:
- generic [ref=e437] [cursor=pointer]:
- checkbox "启用"
- cell "48" [ref=e439]
- cell "3-1" [ref=e441]
- cell "3-2" [ref=e443]
- cell "3-3" [ref=e445]
- cell "3-4" [ref=e447]
- cell "3-5" [ref=e449]
- cell "3-6" [ref=e451]
- cell [ref=e453]
- row [ref=e454]:
- cell
- cell [ref=e455]:
- generic [ref=e456] [cursor=pointer]:
- checkbox
- cell "4" [ref=e458]
- cell "4" [ref=e460]
- cell "任务-00004" [ref=e462]
- cell "告警" [ref=e464]
- cell "启用" [ref=e466]
- cell "启用" [ref=e468]:
- generic [ref=e469] [cursor=pointer]:
- checkbox "启用" [checked]
- cell "64.9" [ref=e471]
- cell "4-1" [ref=e473]
- cell "4-2" [ref=e475]
- cell "4-3" [ref=e477]
- cell "4-4" [ref=e479]
- cell "4-5" [ref=e481]
- cell "4-6" [ref=e483]
- cell [ref=e485]
- row [ref=e486]:
- cell
- cell [ref=e487]:
- generic [ref=e488] [cursor=pointer]:
- checkbox
- cell "5" [ref=e490]
- cell "5" [ref=e492]
- cell "任务-00005" [ref=e494]
- cell "任务" [ref=e496]
- cell "禁用" [ref=e498]
- cell [ref=e500]:
- generic [ref=e501] [cursor=pointer]:
- checkbox "启用"
- cell "43.7" [ref=e503]
- cell "5-1" [ref=e505]
- cell "5-2" [ref=e507]
- cell "5-3" [ref=e509]
- cell "5-4" [ref=e511]
- cell "5-5" [ref=e513]
- cell "5-6" [ref=e515]
- cell [ref=e517]
- row [ref=e518]:
- cell
- cell [ref=e519]:
- generic [ref=e520] [cursor=pointer]:
- checkbox
- cell "6" [ref=e522]
- cell "6" [ref=e524]
- cell "任务-00006" [ref=e526]
- cell "作业" [ref=e528]
- cell "待审" [ref=e530]
- cell "启用" [ref=e532]:
- generic [ref=e533] [cursor=pointer]:
- checkbox "启用" [checked]
- cell "70.6" [ref=e535]
- cell "6-1" [ref=e537]
- cell "6-2" [ref=e539]
- cell "6-3" [ref=e541]
- cell "6-4" [ref=e543]
- cell "6-5" [ref=e545]
- cell "6-6" [ref=e547]
- cell [ref=e549]
- row [ref=e550]:
- cell
- cell [ref=e551]:
- generic [ref=e552] [cursor=pointer]:
- checkbox
- cell "7" [ref=e554]
- cell "7" [ref=e556]
- cell "任务-00007" [ref=e558]
- cell "流程" [ref=e560]
- cell "启用" [ref=e562]
- cell [ref=e564]:
- generic [ref=e565] [cursor=pointer]:
- checkbox "启用"
- cell "54.5" [ref=e567]
- cell "7-1" [ref=e569]
- cell "7-2" [ref=e571]
- cell "7-3" [ref=e573]
- cell "7-4" [ref=e575]
- cell "7-5" [ref=e577]
- cell "7-6" [ref=e579]
- cell [ref=e581]
- row [ref=e582]:
- cell
- cell [ref=e583]:
- generic [ref=e584] [cursor=pointer]:
- checkbox
- cell "8" [ref=e586]
- cell "8" [ref=e588]
- cell "任务-00008" [ref=e590]
- cell "告警" [ref=e592]
- cell "禁用" [ref=e594]
- cell "启用" [ref=e596]:
- generic [ref=e597] [cursor=pointer]:
- checkbox "启用" [checked]
- cell "67.4" [ref=e599]
- cell "8-1" [ref=e601]
- cell "8-2" [ref=e603]
- cell "8-3" [ref=e605]
- cell "8-4" [ref=e607]
- cell "8-5" [ref=e609]
- cell "8-6" [ref=e611]
- cell [ref=e613]
- row [ref=e614]:
- cell
- cell [ref=e615]:
- generic [ref=e616] [cursor=pointer]:
- checkbox
- cell "9" [ref=e618]
- cell "9" [ref=e620]
- cell "任务-00009" [ref=e622]
- cell "任务" [ref=e624]
- cell "待审" [ref=e626]
- cell [ref=e628]:
- generic [ref=e629] [cursor=pointer]:
- checkbox "启用"
- cell "71.1" [ref=e631]
- cell "9-1" [ref=e633]
- cell "9-2" [ref=e635]
- cell "9-3" [ref=e637]
- cell "9-4" [ref=e639]
- cell "9-5" [ref=e641]
- cell "9-6" [ref=e643]
- cell [ref=e645]
- row [ref=e646]:
- cell
- cell [ref=e647]:
- generic [ref=e648] [cursor=pointer]:
- checkbox
- cell "10" [ref=e650]
- cell "10" [ref=e652]
- cell "任务-00010" [ref=e654]
- cell "作业" [ref=e656]
- cell "启用" [ref=e658]
- cell "启用" [ref=e660]:
- generic [ref=e661] [cursor=pointer]:
- checkbox "启用" [checked]
- cell "5.4" [ref=e663]
- cell "10-1" [ref=e665]
- cell "10-2" [ref=e667]
- cell "10-3" [ref=e669]
- cell "10-4" [ref=e671]
- cell "10-5" [ref=e673]
- cell "10-6" [ref=e675]
- cell [ref=e677]
- row [ref=e678]:
- cell
- cell [ref=e679]:
- generic [ref=e680] [cursor=pointer]:
- checkbox
- cell "11" [ref=e682]
- cell "11" [ref=e684]
- cell "任务-00011" [ref=e686]
- cell "流程" [ref=e688]
- cell "禁用" [ref=e690]
- cell [ref=e692]:
- generic [ref=e693] [cursor=pointer]:
- checkbox "启用"
- cell "25" [ref=e695]
- cell "11-1" [ref=e697]
- cell "11-2" [ref=e699]
- cell "11-3" [ref=e701]
- cell "11-4" [ref=e703]
- cell "11-5" [ref=e705]
- cell "11-6" [ref=e707]
- cell [ref=e709]
- row [ref=e710]:
- cell
- cell [ref=e711]:
- generic [ref=e712] [cursor=pointer]:
- checkbox
- cell "12" [ref=e714]
- cell "12" [ref=e716]
- cell "任务-00012" [ref=e718]
- cell "告警" [ref=e720]
- cell "待审" [ref=e722]
- cell "启用" [ref=e724]:
- generic [ref=e725] [cursor=pointer]:
- checkbox "启用" [checked]
- cell "89.2" [ref=e727]
- cell "12-1" [ref=e729]
- cell "12-2" [ref=e731]
- cell "12-3" [ref=e733]
- cell "12-4" [ref=e735]
- cell "12-5" [ref=e737]
- cell "12-6" [ref=e739]
- cell [ref=e741]
- row [ref=e742]:
- cell
- cell [ref=e743]:
- generic [ref=e744] [cursor=pointer]:
- checkbox
- cell "13" [ref=e746]
- cell "13" [ref=e748]
- cell "任务-00013" [ref=e750]
- cell "任务" [ref=e752]
- cell "启用" [ref=e754]
- cell [ref=e756]:
- generic [ref=e757] [cursor=pointer]:
- checkbox "启用"
- cell "57.5" [ref=e759]
- cell "13-1" [ref=e761]
- cell "13-2" [ref=e763]
- cell "13-3" [ref=e765]
- cell "13-4" [ref=e767]
- cell "13-5" [ref=e769]
- cell "13-6" [ref=e771]
- cell [ref=e773]
- row [ref=e774]
- generic [ref=e775]:
- generic [ref=e776]: 共 10000 条
- navigation "分页" [ref=e777]:
- list [ref=e778]:
- listitem [ref=e779]:
- button "上一页" [disabled] [ref=e780]
- listitem [ref=e783]:
- button "1" [ref=e784] [cursor=pointer]
- listitem [ref=e785]:
- button "2" [ref=e786] [cursor=pointer]
- listitem [ref=e787]:
- button "向后跳 5 页" [ref=e788] [cursor=pointer]
- listitem [ref=e793]:
- button "1000" [ref=e794] [cursor=pointer]
- listitem [ref=e795]:
- button "下一页" [ref=e796] [cursor=pointer]
- generic "每页条数" [ref=e800]:
- combobox [ref=e802]: "10"
- complementary "FPS 性能监控" [ref=e76]:
- generic [ref=e77]:
- generic [ref=e78]: FPS
- strong [ref=e79]: "60"
- button "重置 FPS 统计" [ref=e80] [cursor=pointer]
- generic [ref=e106]:
- generic [ref=e107]: avg 60
- generic [ref=e108]: min 53
- alert [ref=e805]:
- generic [ref=e809]: 登录成功
@@ -0,0 +1,6 @@
- generic [active] [ref=f1e1]:
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,6 @@
- generic [active] [ref=f7e1]:
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=f1e1]:
- generic [ref=f1e3]:
- generic [ref=f1e4]:
- img "FMS" [ref=f1e5]
- generic [ref=f1e6]: FMS 企业管理系统
- generic [ref=f1e7]:
- button "中" [ref=f1e8] [cursor=pointer]
- button [ref=f1e9] [cursor=pointer]
- generic [ref=f1e12]:
- paragraph [ref=f1e22]: 企业管理系统
- generic [ref=f1e24]:
- heading "登录" [level=1] [ref=f1e25]
- paragraph [ref=f1e26]: 欢迎回来,请输入您的账号信息
- generic [ref=f1e27]:
- generic [ref=f1e28]:
- generic [ref=f1e29]:
- text: "*"
- generic [ref=f1e30]: 机构码
- textbox [active] [ref=f1e39]: g3hd
- generic [ref=f1e40]:
- generic [ref=f1e41]:
- text: "*"
- generic [ref=f1e42]: 账号
- textbox [ref=f1e50]: g3soft
- generic [ref=f1e51]:
- generic [ref=f1e52]: 密码
- generic [ref=f1e56]:
- textbox [ref=f1e61]
- button "显示密码" [ref=f1e62] [cursor=pointer]
- generic [ref=f1e66]: 请按住滑块拖动
- button "登录" [ref=f1e74] [cursor=pointer]
- paragraph [ref=f1e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
@@ -0,0 +1,37 @@
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img "FMS" [ref=e5]
- generic [ref=e6]: FMS 企业管理系统
- generic [ref=e7]:
- button "中" [ref=e8] [cursor=pointer]
- button [ref=e9] [cursor=pointer]
- generic [ref=e12]:
- paragraph [ref=e22]: 企业管理系统
- generic [ref=e24]:
- heading "登录" [level=1] [ref=e25]
- paragraph [ref=e26]: 欢迎回来,请输入您的账号信息
- generic [ref=e27]:
- generic [ref=e28]:
- generic [ref=e29]:
- text: "*"
- generic [ref=e30]: 机构码
- textbox [active] [ref=e39]
- generic [ref=e40]:
- generic [ref=e41]:
- text: "*"
- generic [ref=e42]: 账号
- textbox [ref=e50]
- generic [ref=e51]:
- generic [ref=e52]: 密码
- generic [ref=e56]:
- textbox [ref=e61]
- button "显示密码" [ref=e62] [cursor=pointer]
- generic [ref=e66]: 请按住滑块拖动
- button "登录" [ref=e74] [cursor=pointer]
- paragraph [ref=e75]: © 2026 FMS · 企业管理系统
- status:
- generic:
- generic: FMS
- generic: 数字化管理系统
- generic: 正在加载,请稍候…
Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

+26
View File
@@ -0,0 +1,26 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:5082",
"localStorage": [
{
"name": "i18n",
"value": "{\"locale\":\"zh-CN\"}"
},
{
"name": "app",
"value": "{\"darkMode\":false,\"primaryColor\":\"#0f172b\"}"
},
{
"name": "__probe_marker",
"value": "1"
},
{
"name": "fms-user",
"value": "{\"userInfo\":{\"orgId\":\"g3hd\",\"account\":\"g3soft\"}}"
}
]
}
]
}
+5 -5
View File
@@ -30,12 +30,12 @@
}
/* ===== 开屏加载屏 =====
配色对齐 src/theme/tokens.css 浅色主题(默认):背景 #f5f6f8 / 文字 #1f2329 /
次级文字 #86909c / 主色 #0f172b / 边框 #e5e6eb;
配色对齐 src/theme/tokens.css 浅色主题(默认):背景 #eef1f6 / 文字 #1f2329 /
次级文字 #6f7784 / 主色 #0f172b / 边框 #e3e8ee;
深色(html.boot-dark,由上方脚本按持久化偏好写入)对齐 .dark 覆盖值。 */
.app-boot {
display: flex;
background-color: #f5f6f8;
background-color: #eef1f6;
position: fixed;
left: 0;
top: 0;
@@ -114,7 +114,7 @@
.app-boot__sub-title {
margin-top: 4px;
font-size: 14px;
color: #86909c;
color: #6f7784;
}
.boot-dark .app-boot__sub-title,
@@ -153,7 +153,7 @@
.app-boot__hint {
margin-top: 12px;
font-size: 12px;
color: #86909c;
color: #6f7784;
}
/* 内容入场:轻微上浮 + 淡入,逐项错开,避免生硬 */
+193 -35
View File
@@ -1,6 +1,6 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { RotateCcw } from '@lucide/vue'
import { ChevronUp, RotateCcw } from '@lucide/vue'
import { Button } from '@/components/ui'
defineOptions({ inheritAttrs: false })
@@ -21,6 +21,20 @@ const averageFps = ref(0)
const minimumFps = ref(null)
const history = ref([])
/* 折叠态:只留一颗迷你胶囊(状态色 + 当前 FPS),点击可展开完整面板。
*
* 为什么默认折叠、且折叠态整颗胶囊 pointer-events: none:
* 浮层 fixed 在页面右上角,只要参与鼠标命中,就会压住页面右上角的操作区
* (顶栏用户菜单、列表页查询工具栏的 ghost 按钮等),把这些控件的 hover 命中区
* 吞掉;指针在「被遮挡 / 未遮挡」边界来回时 hover 反复开关,表现为背景色闪烁。
* 因此折叠态整体不参与命中,展开入口改在窗口捕获阶段用坐标判断:
* - 未位移抬指 → 展开完整面板
* - 拖 → 移动胶囊位置(与展开态拖动一致) */
const collapsed = ref(true)
const pillEl = ref(null)
let pillDrag = null // 折叠态按下:{ pointerX, pointerY, originX, originY, width, height, moved }
const PILL_DRAG_THRESHOLD = 4
/* 非法入参归一化:采样间隔/帧数/基准值过低时取最小值,避免除零或空图 */
const normalizedSampleInterval = computed(() => Math.max(100, props.sampleInterval))
const normalizedHistorySize = computed(() => Math.max(1, Math.floor(props.historySize)))
@@ -94,10 +108,12 @@ const barStyles = computed(() =>
}),
)
/* ---------- 拖拽移动(固定浮层可能遮挡顶栏操作,允许用户拖开) ---------- */
/* ---------- 拖拽移动(固定浮层可能遮挡页面操作区,允许用户拖开) ---------- */
const rootEl = ref(null)
// 拖动后的 left/top 坐标;null 表示未拖过,使用默认定位(右上角)
const pos = ref(null)
// 拖动中标记:模板用普通变量不具响应性,独立 ref 保证拖动态样式即时生效
const isDragging = ref(false)
let dragStart = null // { pointerX, pointerY, originX, originY, width, height }
/* ---------- 双击隐藏(也可由父级通过 ref 调 show/hide 控制显隐) ---------- */
@@ -118,8 +134,9 @@ function hide() {
defineExpose({ show, hide })
function onPointerDown(e) {
// 重置按钮的点击不触发拖拽
if (e.target.closest('.fps-monitor__reset')) return
// 面板内按钮(收起 / 重置)标记了 data-fps-no-drag:
// 拖拽会在 pointerdown 上 preventDefault,从而吞掉这些按钮的原生 click
if (e.target.closest('[data-fps-no-drag]')) return
const rect = rootEl.value.getBoundingClientRect()
dragStart = {
pointerX: e.clientX,
@@ -129,6 +146,7 @@ function onPointerDown(e) {
width: rect.width,
height: rect.height,
}
isDragging.value = true
window.addEventListener('pointermove', onPointerMove)
window.addEventListener('pointerup', onPointerUp)
e.preventDefault()
@@ -146,20 +164,78 @@ function onPointerMove(e) {
function onPointerUp() {
dragStart = null
isDragging.value = false
window.removeEventListener('pointermove', onPointerMove)
window.removeEventListener('pointerup', onPointerUp)
}
/* ---------- 折叠态交互:捕获阶段判断是否点在胶囊上 ----------
* 胶囊自身 pointer-events: none(不参与页面命中),若不拦截,
* 这次交互会「隔空」落到胶囊下方的页面元素上,所以命中胶囊时需要拦下它。 */
function onPillPointerDownCapture(e) {
if (!collapsed.value || !visible.value || !pillEl.value) return
const rect = pillEl.value.getBoundingClientRect()
if (
e.clientX < rect.left ||
e.clientX > rect.right ||
e.clientY < rect.top ||
e.clientY > rect.bottom
) {
return
}
e.preventDefault()
e.stopPropagation()
pillDrag = {
pointerX: e.clientX,
pointerY: e.clientY,
originX: rect.left,
originY: rect.top,
width: rect.width,
height: rect.height,
moved: false,
}
window.addEventListener('pointermove', onPillPointerMove)
window.addEventListener('pointerup', onPillPointerUp)
}
function onPillPointerMove(e) {
if (!pillDrag) return
const dx = e.clientX - pillDrag.pointerX
const dy = e.clientY - pillDrag.pointerY
// 阈值内的轻微抖动不算拖动,避免「点击展开」被误判
if (!pillDrag.moved && Math.abs(dx) < PILL_DRAG_THRESHOLD && Math.abs(dy) < PILL_DRAG_THRESHOLD) {
return
}
pillDrag.moved = true
isDragging.value = true
// 边界限制:不拖出视口
const x = Math.max(0, Math.min(pillDrag.originX + dx, window.innerWidth - pillDrag.width))
const y = Math.max(0, Math.min(pillDrag.originY + dy, window.innerHeight - pillDrag.height))
pos.value = { x, y }
}
function onPillPointerUp() {
const wasClick = Boolean(pillDrag && !pillDrag.moved)
pillDrag = null
isDragging.value = false
window.removeEventListener('pointermove', onPillPointerMove)
window.removeEventListener('pointerup', onPillPointerUp)
// 原地点击 → 展开完整面板
if (wasClick) collapsed.value = false
}
/* ---------- 生命周期 ---------- */
onMounted(() => {
resetSampleWindow()
document.addEventListener('visibilitychange', handleVisibilityChange)
window.addEventListener('pointerdown', onPillPointerDownCapture, true)
animationFrameId = requestAnimationFrame(tick)
})
onBeforeUnmount(() => {
cancelAnimationFrame(animationFrameId)
document.removeEventListener('visibilitychange', handleVisibilityChange)
window.removeEventListener('pointerdown', onPillPointerDownCapture, true)
})
</script>
@@ -168,41 +244,73 @@ onBeforeUnmount(() => {
v-show="visible"
ref="rootEl"
class="fps-monitor"
:class="{ 'is-dragging': dragStart }"
:class="{ 'is-dragging': isDragging, 'is-collapsed': collapsed }"
:style="pos ? { left: pos.x + 'px', top: pos.y + 'px' } : undefined"
aria-label="FPS 性能监控"
v-bind="$attrs"
@pointerdown="onPointerDown"
@dblclick="onDblClick"
>
<div class="fps-monitor__header">
<span class="fps-monitor__label">FPS</span>
<strong class="fps-monitor__current" :style="{ color: currentFpsColor }">
{{ currentFps || '--' }}
</strong>
<Button
type="ghost"
class="fps-monitor__reset"
title="重置统计"
aria-label="重置 FPS 统计"
@click="reset"
>
<RotateCcw />
</Button>
</div>
<!-- 折叠态:迷你胶囊(状态色 + 当前 FPS)。
碰撞由捕获阶段处理:胶囊整体 pointer-events: none,不参与页面命中,
因此不会吞掉页面按钮的 hover;点击 / 拖动见 onPillPointerDownCapture。 -->
<button
v-if="collapsed"
ref="pillEl"
type="button"
class="fps-monitor__pill"
title="点击展开 FPS 面板"
aria-label="展开 FPS 性能监控面板"
>
<i
class="fps-monitor__pill-dot"
:style="{ backgroundColor: currentFpsColor }"
aria-hidden="true"
/>
<b class="fps-monitor__pill-value">{{ currentFps || '--' }}</b>
</button>
<div class="fps-monitor__chart" aria-hidden="true">
<span v-for="(bar, index) in barStyles" :key="index" class="fps-monitor__bar" :style="bar" />
</div>
<template v-else>
<div class="fps-monitor__header">
<span class="fps-monitor__label">FPS</span>
<strong class="fps-monitor__current" :style="{ color: currentFpsColor }">
{{ currentFps || '--' }}
</strong>
<Button
type="ghost"
class="fps-monitor__icon-button"
data-fps-no-drag
title="收起面板"
aria-label="收起 FPS 面板"
@click="collapsed = true"
>
<ChevronUp />
</Button>
<Button
type="ghost"
class="fps-monitor__icon-button fps-monitor__reset"
data-fps-no-drag
title="重置统计"
aria-label="重置 FPS 统计"
@click="reset"
>
<RotateCcw />
</Button>
</div>
<div class="fps-monitor__summary">
<span
>avg <b>{{ averageFps || '--' }}</b></span
>
<span
>min <b>{{ minimumFps ?? '--' }}</b></span
>
</div>
<div class="fps-monitor__chart" aria-hidden="true">
<span v-for="(bar, index) in barStyles" :key="index" class="fps-monitor__bar" :style="bar" />
</div>
<div class="fps-monitor__summary">
<span
>avg <b>{{ averageFps || '--' }}</b></span
>
<span
>min <b>{{ minimumFps ?? '--' }}</b></span
>
</div>
</template>
</aside>
</template>
@@ -228,7 +336,8 @@ onBeforeUnmount(() => {
border: 1px solid var(--fms-border);
border-radius: var(--fms-control-radius);
box-shadow: var(--fms-popover-shadow);
transition: box-shadow 150ms ease;
/* 拖起时阴影加深,走 token 保证 reduced-motion 下同步降级 */
transition: box-shadow var(--fms-duration-base) var(--fms-ease-standard);
&.is-dragging {
cursor: grabbing;
@@ -236,6 +345,55 @@ onBeforeUnmount(() => {
}
}
/* 折叠态:容器收缩到胶囊尺寸(width:auto + 去内边距 + 去卡片外观),
并整体退出鼠标命中(pointer-events: none)。
容器盒子同样参与命中判定,只缩小内容而留下原尺寸的盒子仍会遮挡页面。 */
.fps-monitor.is-collapsed {
width: auto;
gap: 0;
padding: 0;
background: none;
border: none;
box-shadow: none;
pointer-events: none;
}
/* 折叠态拖动:容器此时无阴影,反馈补到胶囊自身 */
.fps-monitor.is-collapsed.is-dragging .fps-monitor__pill {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
}
/* 折叠胶囊:状态色圆点 + 当前 FPS,点击展开完整面板 */
.fps-monitor__pill {
display: inline-flex;
align-items: center;
gap: 5px;
height: 18px;
padding: 0 7px;
font-family: inherit;
font-size: 11px;
line-height: 1;
color: var(--fms-text-secondary);
cursor: pointer;
background: var(--fms-card);
border: 1px solid var(--fms-border);
border-radius: 9px;
box-shadow: var(--fms-shadow);
}
.fps-monitor__pill-dot {
flex: none;
width: 6px;
height: 6px;
border-radius: 50%;
}
.fps-monitor__pill-value {
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--fms-text);
}
.fps-monitor__header {
display: flex;
align-items: center;
@@ -250,7 +408,7 @@ onBeforeUnmount(() => {
.fps-monitor__current {
flex: 1;
font-size: 18px;
font-size: 16px;
font-variant-numeric: tabular-nums;
line-height: 1;
}
@@ -280,8 +438,8 @@ onBeforeUnmount(() => {
font-variant-numeric: tabular-nums;
}
/* 重置按钮:组件库 Button 无 size 枚举,在 ghost 变体基础上压缩为 20×20 小方钮 */
.fps-monitor .fps-monitor__reset {
/* 面板内图标按钮(收起 / 重置):组件库 Button 无 size 枚举,压缩为 20×20 小方钮 */
.fps-monitor .fps-monitor__icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -0,0 +1,118 @@
<script setup>
/**
* FmsField —— 多态控件渲染器(不含 label)。
*
* 职责单一:给一个 type + 一组属性,渲染出对应的、已接好 v-model 的控件。
* label / 必填 / 校验 / 栅格 交给 FormItem,本组件只做"类型 → 控件"的多态映射,
* 把散落在各处的类型默认值(日期格式、数字输入、checkbox 0/1 归一等)收敛到一处。
*
* 留出的扩展口子:
* - 默认插槽:整体接管控件渲染(忽略 type),最大逃生舱;
* - control-props:一个对象直接喂给内部控件(如 select 的 options、selectId 的 view-name);
* - types:自定义 { [type]: Component } 注册表,新增类型无需改本组件模板;
* - 透传:inheritAttrs=false,class/style/原生事件(如 @keydown)等 fallthrough 属性
* 一律转发到内部控件;
* - v-model:默认转发;selectId 额外支持 v-model:label(id + 显示名 双绑)。
*
* 内置 type:input | textarea | number | money | date | datetime | checkbox | select | selectId
* 未识别的 type 兜底为 Input。
*/
import { computed, useAttrs } from 'vue'
import { Checkbox, DatePicker, Input, Select } from '@/components/ui'
import FmsSelect from '@/components/fms-select/FmsSelect.vue'
defineOptions({ inheritAttrs: false })
const props = defineProps({
/** 控件类型;见文件头内置类型清单,可用 types 扩展 */
type: { type: String, default: 'input' },
/** 双向绑定的值,转发给内部控件 */
modelValue: { type: [String, Number, Boolean, Array, Object], default: undefined },
/** 显示名(仅 selectId 用):v-model:label,与 modelValue(id) 成对 */
label: { type: String, default: '' },
/** 禁用态(所有控件通用) */
disabled: { type: Boolean, default: false },
/** 只读态(仅 Input 类控件生效) */
readonly: { type: Boolean, default: false },
/** 校验状态 '' | error | warning(仅 Input 类控件生效) */
status: { type: String, default: '' },
/** 直接喂给内部控件的额外属性(优先级高于 fallthrough 属性) */
controlProps: { type: Object, default: () => ({}) },
/** 自定义类型注册表 { [type]: Component },命中时优先于内置类型 */
types: { type: Object, default: () => ({}) },
})
const emit = defineEmits(['update:modelValue', 'update:label', 'change'])
const attrs = useAttrs()
// 非 Input 的内置类型 → 组件映射;其余(input/textarea/number/money/未知)统一落到 Input
const BUILTIN_COMPONENTS = {
select: Select,
selectId: FmsSelect,
checkbox: Checkbox,
date: DatePicker,
datetime: DatePicker,
}
const resolvedComponent = computed(
() => props.types[props.type] || BUILTIN_COMPONENTS[props.type] || Input,
)
const isCheckbox = computed(() => props.type === 'checkbox')
const isInputLike = computed(() => resolvedComponent.value === Input)
const isLookup = computed(() => resolvedComponent.value === FmsSelect)
// 各类型的默认控件属性(可被 controlProps 覆盖)
const typeDefaults = computed(() => {
switch (props.type) {
case 'textarea':
return { type: 'textarea' }
case 'number':
case 'money':
return { type: 'number' }
case 'date':
return { valueFormat: 'YYYY-MM-DD' }
case 'datetime':
return { format: 'YYYY-MM-DD HH:mm', valueFormat: 'YYYY-MM-DD HH:mm:ss' }
default:
return {}
}
})
// 汇总喂给内部控件的全部 props/事件:类型默认 < fallthrough 属性 < controlProps,
// 再叠加值绑定、双绑、通用状态与 change 转发。
const bindProps = computed(() => {
const merged = { ...typeDefaults.value, ...attrs, ...props.controlProps }
// 值绑定:checkbox 在 0/1(存储)与 boolean(控件)之间转换,其余类型直通
merged.modelValue = isCheckbox.value ? Number(props.modelValue) === 1 : props.modelValue
merged['onUpdate:modelValue'] = (value) =>
emit('update:modelValue', isCheckbox.value ? (value ? 1 : 0) : value)
// selectId:额外的显示名双绑(v-model:label)
if (isLookup.value) {
merged.label = props.label
merged['onUpdate:label'] = (value) => emit('update:label', value)
}
// 通用状态:disabled 所有控件都吃;readonly / status 仅 Input 支持,避免污染其它控件
merged.disabled = props.disabled
if (isInputLike.value) {
if (props.readonly) merged.readonly = true
if (props.status) merged.status = props.status
}
// 内部控件的 change 原样转发(各控件签名不同,不做强行归一)
merged.onChange = (...args) => emit('change', ...args)
return merged
})
</script>
<template>
<!-- 默认插槽:整体接管控件渲染;未提供时按 type 渲染内置/注册控件 -->
<slot>
<component :is="resolvedComponent" v-bind="bindProps" />
</slot>
</template>
@@ -1,6 +1,6 @@
// 模块数据驱动列表/编辑的「字段展示」工具:解析下拉选项、格式化日期等。
// 供 fms-module-list / fms-module-edit 等按模块元数据渲染的组件共用;
// 查询条件构建(buildQueryCondition 等)见 queryUtils.js。
// 查询条件构建(buildConditionSql / astToSql 等)见 queryUtils.js。
import { isEmptyValue } from './queryUtils'
const padDatePart = (number) => String(number).padStart(2, '0')
Loaded 100 of 179 files, more files were not shown because too many files have changed in this diff. Show more