20260722222510

This commit is contained in:
oneao committed 2026-07-22 22:25:10 +08:00
1 parent d04fad24ed
commit 59b3ccfb1c
36 files changed
+2753 -3064

No files matched your search

@@ -0,0 +1,190 @@
---
name: AppTabs 操作按钮组与应用内最大化
overview: 在页签栏 AppTabs 最左侧新增「一体的、带分割线」的三图标按钮组(返回控制台 / 刷新当前路由 / 留空占位),最右侧新增「应用内最大化」按钮;最大化时隐藏侧边栏+顶栏+页签栏,内容区占满窗口,并提供退出浮窗。
design:
styleKeywords:
- 企业级扁平
- 一体化分段工具栏
- 竖向分割线
- 蓝色主色高亮
- 轻量 hover
- 桌面端
fontSystem:
fontFamily: PingFang SC
heading:
size: 14px
weight: 600
subheading:
size: 13px
weight: 500
body:
size: 12px
weight: 400
colorSystem:
primary:
- "#1677ff"
- "#0958d9"
background:
- "#ffffff"
- "#f5f7fa"
text:
- "#1f2329"
- "#646a73"
functional:
- "#e5e8eb"
- "#f2f3f5"
- "#e6f4ff"
- "#c9cdd4"
todos:
- id: store-state
content: 在 app store 新增 contentMaximized 与 routeRefreshKey 状态及切换/刷新方法(不持久化)
status: completed
- id: apptabs-buttons
content: AppTabs 新增左侧三按钮一体化组(返回/刷新/占位+分割线)与右侧最大化按钮并接线
status: completed
dependencies:
- store-state
- id: default-layout
content: DefaultLayout 应用内最大化隐藏三块布局、加浮动退出按钮、路由刷新 key 接入
status: completed
dependencies:
- store-state
- id: verify
content: 校验返回/刷新/最大化/退出交互与各页面正常且未触发整页刷新
status: completed
dependencies:
- apptabs-buttons
- default-layout
---
## 用户需求
在页签栏 `AppTabs` 上新增操作按钮:
- 最左侧新增一组「一体化的三个图标按钮」,按钮之间以分割线分隔,视觉上是一个整体。
- 第 1 个(最左):返回控制台按钮,点击跳转到 `/dashboard`(工作台)。
- 第 2 个(中间):刷新按钮,要求**重新挂载当前路由组件**(各页面 `onMounted` 重新执行、重新拉数据),**不能**刷新整站(`router.go(0)` / `location.reload()` 禁止)。
- 第 3 个(最右):暂不确定功能,先**留空占位**——保留按钮槽位与分割线,禁用状态、无图标。
- 最右侧新增一个「收缩全屏」按钮,用户确认行为为**应用内最大化**:隐藏侧边栏 + 顶栏 + 页签栏,让内容区占满整个窗口(仍是网页,不调用浏览器 Fullscreen API)。该按钮放在 `AppTabs` 最右侧。
## 产品概述
为内部管理系统页签栏补充快捷操作:左侧一组统一风格的工具按钮(返回控制台 / 刷新路由 / 占位),右侧一个「应用内最大化」开关。最大化后,侧边栏、顶栏、页签栏全部收起,仅留内容区;因承载开关的页签栏随之隐藏,需在内容区右上角浮动一个「退出最大化」按钮用于恢复。
## 核心特性
- 左侧三按钮为「一体容器 + 分割线」的视觉整体,风格对齐现有 `tab-nav-button`。
- 返回控制台:`router.push('/dashboard')`。
- 刷新路由:通过 `appStore.routeRefreshKey` 自增,驱动 `DefaultLayout` 中 `<component :key>` 变化以重挂载当前路由(非整页刷新)。
- 应用内最大化:由 `appStore.contentMaximized` 控制,`DefaultLayout` 用 `v-show` 收起三块布局,内容区右上角出现浮动退出按钮。
- 占位按钮:禁用、无图标、保留分割线,后续可随时接入功能。
## 技术栈
- 前端:Vue 3 `<script setup>` + Vite + antdv-next + Pinia(沿用现有项目栈)
- 图标:`@lucide/vue`(复用现有图标习惯,新增 `House` / `RotateCw` / `Maximize2` / `Minimize2`)
- 样式:组件内 `scoped lang="scss"`,复用全局 `--fms-*` 主题变量,遵循「少量 CSS / 公用集中 / 不兼容移动端」CSS 规范
## 实现方案
### 策略
1. **状态下沉到 `useAppStore`**:新增 `contentMaximized`(`ref(false)`)+ `toggleContentMaximized()`,以及 `routeRefreshKey`(`ref(0)`)+ `refreshRoute()`(自增计数)。两者均**不加入** `persist.pick`(当前只持久化 `locale`),避免刷新/最大化状态被持久化。
2. **`AppTabs` 接线**:
- 标签轨道前插入「左侧操作组」`.tab-actions`(一体化容器,含 3 个按钮 + 2 条竖向分割线)。按钮 1 调 `router.push('/dashboard')`;按钮 2 调 `appStore.refreshRoute()`;按钮 3 禁用、无图标。
- 右滚动按钮后插入「最大化」按钮,调 `appStore.toggleContentMaximized()`,图标按 `contentMaximized` 在 `Maximize2` / `Minimize2` 间切换(退出时页签栏已隐藏,此图标仅作状态提示,真正退出走浮动按钮)。
3. **`DefaultLayout` 收起布局 + 路由刷新**:
- `AppSidebar` / `AppToolbar` / `AppTabs` 包裹 `v-show="!appStore.contentMaximized"`(用 `v-show` 而非 `v-if`,保留页签与折叠状态,避免重挂载开销)。
- 内容区 `<component :is="Component" :key="route.fullPath + ':' + appStore.routeRefreshKey">`——`routeRefreshKey` 变化即强制重挂载当前路由组件,达成「刷新当前路由」。
- 内容区右上角新增 `fixed` 浮动「退出最大化」按钮(`v-if="appStore.contentMaximized"`),点击 `toggleContentMaximized()` 恢复。
### 关键技术决策
- **刷新用 key 自增而非整页 reload**:`keep-alive` 下组件按 `:key` 缓存,`route.fullPath + ':' + routeRefreshKey` 变化会创建新实例并触发 `onMounted`,等价于「重进当前页」而不重载网站;原有过渡/缓存结构不变。
- **最大化用 `v-show`**:与 `v-if` 相比不销毁 `AppTabs`/`AppSidebar`,保留 `visitedTabs`、`sidebarCollapsed` 等内存状态,退出后零重新加载;`display:none` 不影响键盘/焦点无障碍(内部按钮仍可达)。
- **退出入口与开关解耦**:因最大化时 `AppTabs` 被隐藏,退出动作改由内容区浮动按钮承载,保证「进入必有出口」。
- **风格对齐**:左侧操作组直接复用 `tab-nav-button` 的尺寸/圆角/配色(26×28、radius 4、`--tabs-idle-bg`/`--fms-text-secondary`),仅在外层加统一个边框与 `1px` 分割线,满足「一体 + 分割线」。
### 性能与可靠性
- 路由刷新为低频用户操作:每次刷新在 `keep-alive` 中产生一个新缓存槽。`keep-alive` 当前为裸标签(无 `max`),理论上可无限增长;本场景为人工触发、频率极低,可接受。若后续需要可加 `:max` 限制缓存数——本次不改以避免影响现有缓存语义。
- `v-show` 切换为纯 CSS `display` 切换,无重渲染、无额外网络请求;最大化状态不持久化,刷新页面后默认恢复常规布局。
- 不改通用接口、不改其他页面、不改权限逻辑;仅触及 `app.js` / `DefaultLayout.vue` / `AppTabs.vue` 三处。
## 实现注意
- `app.js` 的 `persist.pick` 保持 `["locale"]` 不变,新增状态不持久化。
- `DefaultLayout.vue` 当前无 `<script setup>`,需补 `import { useRoute } from 'vue-router'` 与 `import { useAppStore }` 并取 `route`/`appStore`,再改 `:key` 与加 `v-show`/浮动按钮。
- 返回控制台按钮在 `/dashboard` 时点击应无副作用(vue-router 对相同路径 `push` 静默处理)。
- 左侧操作组与右侧最大化按钮均需 `aria-label`,占位按钮加 `disabled` 与 `title="敬请期待"`。
- 样式保持 `scoped`,与现有 `tab-header` 高度(34px)、间距(gap 4px)一致;分割线用 `border-left` + `margin`。
## 架构设计
```mermaid
flowchart TD
A[AppTabs 左侧操作组] -->|返回控制台| B[router.push /dashboard]
A -->|刷新路由| C[appStore.refreshRoute 自增 routeRefreshKey]
A -->|占位 禁用| Z[无操作]
D[AppTabs 右侧最大化按钮] -->|toggleContentMaximized| E[appStore.contentMaximized=true]
E --> F[DefaultLayout v-show 隐藏 Sidebar/Toolbar/Tabs]
E --> G[内容区浮动 退出最大化 按钮显示]
G -->|toggleContentMaximized| H[恢复常规布局]
C --> I[DefaultLayout :key 变化 重挂载路由组件]
```
## 目录结构
```
fms-vue/src/
├── stores/
│ └── app.js # [MODIFY] 新增 contentMaximized/toggleContentMaximized 与
│ # routeRefreshKey/refreshRoute;persist.pick 仍为 ["locale"]。
├── layouts/
│ ├── DefaultLayout.vue # [MODIFY] 引入 useRoute/useAppStore;三块布局加
│ # v-show="!contentMaximized";<component :key> 加入
│ # routeRefreshKey;内容区加浮动退出最大化按钮。
│ └── components/
│ └── AppTabs.vue # [MODIFY] 标签轨道前加左侧一体三按钮组(返回/刷新/占位+分割线);
│ # 右滚动按钮后加最大化按钮;import 新图标并接线。
```
## 关键代码结构
```js
// src/stores/app.js 新增状态
const contentMaximized = ref(false);
const routeRefreshKey = ref(0);
function toggleContentMaximized() { contentMaximized.value = !contentMaximized.value; }
function refreshRoute() { routeRefreshKey.value++; }
// 返回中需导出 contentMaximized, routeRefreshKey, toggleContentMaximized, refreshRoute
// DefaultLayout.vue 中 <component> 的 key
<component :is="Component" :key="route.fullPath + ':' + appStore.routeRefreshKey" />
```
## 设计风格
延续现有 FMS 企业级扁平风格(白底、蓝色主色 #1677ff、细边框、轻量 hover)。在 34px 高的页签栏内新增两类控件,与既有 `tab-nav-button` 视觉一致,不引入新色彩或拟物风格。
## 左侧一体化按钮组
- 一个统一的外层容器 `.tab-actions`:浅边框、4px 圆角、与页签栏同高,内含 3 个等宽图标按钮,按钮之间以 1px 竖向分割线隔开,整体观感如一个「分段工具栏」。
- 按钮 1(House 图标):返回控制台;hover 时文字/图标转主色蓝、背景转 `--tabs-active-bg`。
- 按钮 2(RotateCw 图标):刷新当前路由;hover 态同上。
- 按钮 3(占位):禁用态、无图标、保留分割线,视觉稍淡(opacity 0.4),`title="敬请期待"`。
- 图标尺寸 14px(对齐 `.lucide` 习惯),按钮 26×28,圆角 4px。
## 右侧最大化按钮
- 位于右滚动箭头之后,独立图标按钮(Maximize2 / 退出时 Minimize2),样式复用 `tab-nav-button`。
- 最大化激活后:侧边栏、顶栏、页签栏整体 `display:none`,内容区占满窗口;内容区右上角浮现一个 fixed 圆角胶囊「退出最大化」按钮(退出箭头图标 + 文字),hover 浅色背景,点击恢复三块布局。
## 交互与响应式
- 纯桌面端,不做移动端媒体查询(遵循 CSS 规范第 4 条)。
- 所有按钮带 `a-tooltip` 文字提示与 `aria-label`;hover/active 有平滑颜色过渡(0.15s ease)。
- 分割线与按钮间距使用现有 `gap:4px` 节奏,保持与页签栏其他元素对齐。
@@ -0,0 +1,121 @@
---
name: 模块管理树节点点击懒加载
overview: 将模块管理从“挂载时一次性加载全部表”改为“挂载时仅加载树(b_module),点击树节点时才懒加载该模块的字段/权限/翻译/用户分配数据”。
todos:
- id: split-load
content: 拆分 loadAll 为 loadModules 与 loadModuleDetail,新增翻译条件过滤器
status: pending
- id: wire-lazy
content: 将树选中/刷新、onMounted、保存/删除后刷新接入懒加载,保留 draft 与级联逻辑
status: pending
dependencies:
- split-load
- id: verify-scopes
content: 回归校验新建/取消/保存/删除/刷新在各场景下数据正确且不触发全量加载
status: pending
dependencies:
- wire-lazy
---
## 用户需求
模块管理页面进入时,不要立即把所有模块的全部数据一次性拉取下来;应当在用户点击(或右键)左侧树节点时,才加载该节点对应模块的详情数据。
## 产品概述
模块管理采用「左侧模块树 + 右侧详情」布局。当前实现在页面挂载时即通过 `loadAll()` 一次性并发加载 6 张表(b_module / b_module_field / b_module_power / b_i18n / b_user_module / b_user_power)的全部数据并缓存,再按选中节点从内存过滤。需要改为:挂载时只加载树所需的 b_module;选中某个节点时才按该模块懒加载其字段、权限、翻译及用户分配数据。
## 核心特性
- 页面挂载仅加载模块树(b_module),用于渲染树、上级模块选项与编码重复校验。
- 点击/右键树节点时才加载该模块的字段定义、操作权限、多语言翻译、用户模块/权限分配,并构建编辑草稿。
- 保存、删除、刷新后只刷新树与「当前选中模块」的详情,不再全量加载。
- 新建模块、取消编辑保持原交互;保存/删除的级联逻辑在懒加载下依然正确。
## 技术栈
- 前端:Vue 3 `<script setup>` + Vite + antdv-next + Pinia(沿用现有项目栈)
- 数据访问:复用通用端点 `loadDataApi(viewName, searchCondition, orderBy)`(POST `/api/data/loaddata`),不新增后端 URL
- 复用现有组件 `ModuleTreePanel` / `ModuleBasicInfoPanel` / `ModuleFieldDefinitionPanel` / `ModulePowerPanel`,仅调整 `index.vue` 的加载编排
## 实现方案
### 策略
将当前的 `loadAll()` 拆分为「树加载」与「模块详情懒加载」两步:
1. `loadModules()`:仅 `loadDataApi("b_module", null, "b_xh ASC")` 填充 `data.modules`(树、上级选项、编码校验所需)。
2. `loadModuleDetail(moduleCode)`:先定位 `data.modules` 中的模块,再按 `b_module_id` 条件并发加载 `b_module_field`、`b_module_power`、`b_i18n`(按模块编码过滤)、`b_user_module`;随后以已加载的权限 id 列表为条件加载 `b_user_power`。加载完成后调用现有 `selectModule(moduleCode)` 从已加载的 `data` 构建 `draft`。
3. 树节点 `@select` / 右键 `@select` 改为调用 `loadModuleDetail`;`@refresh`、保存后、删除后统一复用 `loadModules()` +(必要时)`loadModuleDetail(selectedKey)`。
### 关键技术决策
- **翻译按模块过滤**:多语言键规则为 `module.<code>`、`field.<code>.<字段>`、`power.<code>.<权限>`。用条件 `b_key = 'module.<code>' OR b_key LIKE 'field.<code>.%' OR b_key LIKE 'power.<code>.%'` 在后端过滤,避免一次性加载全量 b_i18n。`.%` 后缀确保不会被 `m_page` 误匹配到 `m_page_x` 等。code 来自数据库且为小写字母数字下划线,仍做单引号转义防御。
- **用户分配按当前模块加载**:`b_user_module` 用 `b_module_id = <id>` 过滤;`b_user_power` 依赖权限 id,故在 powers 加载完成后用 `b_power_id IN (...)` 二次加载。保证 `saveModule` 的权限回收与 `deleteModule` 的级联删除逻辑数据完整。
- **保留草稿构建逻辑**:`selectModule` 仅负责从已加载的 `data`(fields/powers/translations)按模块过滤并构建 `draft`,不含网络请求,最小化改动面;`resetDraft` 直接复用它从已加载数据重建。
- **不回归**:`parentOptions`/`directoryParentOptions`/`validate()` 依赖 `data.modules`(已加载);新建、取消、保存、删除交互保持不变。
### 性能与可靠性
- 挂载请求由 6 个并发降为 1 个(b_module),点击节点时按模块加载 4~5 个按需查询,随模块规模线性而非全量。
- `loading` 复用现有 `a-spin` 同时驱动树刷新按钮与右侧工作区,加载期间提供反馈。
- 对 `handleSelect` 增加 `loading` 守卫,避免并发重复加载。
- bigint 的 `b_module_id`/`b_power_id` 以字符串拼接条件(与原 `diffRows` 中 `String(item.b_id)` 处理一致),避免精度问题。
## 实现注意
- 仅修改 `fms-vue/src/views/module-management/index.vue`,不触碰 `api.js`、通用端点、全局组件与其他页面。
- `data` 响应式结构(modules/fields/powers/translations/userModules/userPowers)保持不变;`clearDraft` 仍负责清空选中态与详情。
- 删除当前模块后 `selectedKey` 对应的模块已从 `data.modules` 移除,应走 `clearDraft` 分支;删除非当前模块则重新加载当前详情。
- 新建模块期间刷新:只刷新树(loadModules),不要因 `selectedKey` 不存在而清空新建草稿。
## 架构设计
```mermaid
flowchart TD
A[onMounted] --> B[loadModules 仅 b_module]
B --> C[渲染模块树]
D[点击/右键树节点] --> E[loadModuleDetail 按模块懒加载]
E --> F[并发: b_module_field / b_module_power / b_i18n / b_user_module]
F --> G[按权限id加载 b_user_power]
G --> H[selectModule 构建 draft]
H --> I[右侧展示详情]
J[保存/删除/刷新] --> B
J --> K{有选中且非新建?}
K -->|是| E
K -->|否| L[clearDraft]
```
## 目录结构
```
fms-vue/src/views/module-management/
└── index.vue # [MODIFY] 模块管理主视图。拆分 loadAll 为 loadModules(仅加载 b_module 树数据)与
# loadModuleDetail(按选中模块懒加载字段/权限/翻译/用户分配并构建 draft);
# 新增 translationSearchCondition 按模块编码过滤 b_i18n;
# 树 @select/@refresh、onMounted、saveModule/deleteModule 末尾统一调用新加载函数;
# 保留 selectModule/resetDraft/clearDraft 与级联保存删除逻辑。
```
## 关键代码结构
```js
// 按模块编码过滤多语言键(精确前缀,避免误匹配)
function translationSearchCondition(code) {
const c = String(code).replace(/'/g, "''");
return `b_key = 'module.${c}' OR b_key LIKE 'field.${c}.%' OR b_key LIKE 'power.${c}.%'`;
}
// 树加载:仅 b_module
async function loadModules() { /* loadDataApi("b_module", null, "b_xh ASC") -> data.modules */ }
// 详情懒加载:选中节点时按模块加载字段/权限/翻译/用户分配
async function loadModuleDetail(moduleCode) {
const module = data.modules.find((i) => i.b_code === moduleCode);
if (!module) return;
// 并发加载 b_module_field(b_module_id=id)、b_module_power(b_module_id=id)、
// b_i18n(translationSearchCondition(code))、b_user_module(b_module_id=id)
// 再以权限 id 列表加载 b_user_power(b_power_id IN (...))
// 写入 data 后调用 selectModule(moduleCode)
}
```