refactor(projects): 页面布局调整为rdms风格
This commit is contained in:
224
AGENTS.md
Normal file
224
AGENTS.md
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
本文件为后续编码代理提供 `cn-rdms-web` 的稳定仓库上下文。
|
||||||
|
在修改代码前请先阅读。
|
||||||
|
|
||||||
|
## 适用范围
|
||||||
|
|
||||||
|
本说明适用于以 `C:\code\gitea\rdms\cn-rdms-web` 为根目录的整个仓库。
|
||||||
|
|
||||||
|
描述仓库现状时,以当前代码、当前配置、当前文档中可直接验证的事实为准;除非用户明确要求,不引入历史实现、过渡方案或猜测来解释当前行为。
|
||||||
|
|
||||||
|
默认回答保持精简,优先给结论、改动点、验证方式和必要风险;如果用户只要求分析、审阅或方案,就停留在分析层,不主动扩展到实现层。
|
||||||
|
|
||||||
|
## 交互与执行原则
|
||||||
|
|
||||||
|
- 进入实施阶段前,先说明目标、涉及模块、预计改动点和验证方式。
|
||||||
|
- 先定义验证方式,再执行修改和校验;如果没有实际运行命令,需要明确说明只做了静态检查。
|
||||||
|
- 只在当前任务需要的最小范围内改动,避免把无关重构混入同一次修改。
|
||||||
|
|
||||||
|
## 项目概览
|
||||||
|
|
||||||
|
- 应用类型:RDMS 系统的 Vue 3 后台前端
|
||||||
|
- 包管理器:`pnpm`
|
||||||
|
- 运行时与工具链:Vite 7、TypeScript、Pinia、Element Plus、UnoCSS
|
||||||
|
- 工作区包位于 `packages/*`,通过 `@sa/*` 引用
|
||||||
|
- Node 版本要求:`>=20.19.0`
|
||||||
|
- pnpm 版本要求:`>=8.7.0`
|
||||||
|
|
||||||
|
## 项目骨架主线
|
||||||
|
|
||||||
|
当前项目不是边写边拼的页面集合,而是已经形成闭环的后台前端骨架。后续改动优先顺着这几条主线做,而不是平行再起一套:
|
||||||
|
|
||||||
|
- 路由来源统一:页面文件与自定义路由作为源头,经 `elegant-router` 生成路由产物,再由 `build/plugins/router.ts` 集中补齐 `meta`
|
||||||
|
- 权限入口统一:常量路由和权限路由明确分流,`route store` 负责初始化、菜单生成、缓存路由和面包屑
|
||||||
|
- 请求入口统一:所有业务请求默认走 `src/service/request/index.ts`
|
||||||
|
- 页面套路统一:列表页通常拆为搜索区、表格区、操作弹层/抽屉和 `modules/*` 子组件
|
||||||
|
- 衍生资产统一:页面资源白名单从路由结构生成,不手工维护第二份页面清单
|
||||||
|
|
||||||
|
## 环境与构建说明
|
||||||
|
|
||||||
|
- Vite 路径别名:`@` -> `src`
|
||||||
|
- Vite 路径别名:`~` -> 仓库根目录
|
||||||
|
- 开发服务器默认端口:`9527`
|
||||||
|
- 预览服务器默认端口:`9725`
|
||||||
|
- 环境文件包括 `.env`、`.env.dev`、`.env.prod`
|
||||||
|
|
||||||
|
## 关键目录与文件
|
||||||
|
|
||||||
|
- `src/views`:业务页面
|
||||||
|
- `src/components`:共享组件
|
||||||
|
- `src/layouts`:应用壳、头部、侧栏、菜单、标签页、主题抽屉
|
||||||
|
- `src/store/modules`:Pinia 模块,包含 app、auth、route、tab、theme
|
||||||
|
- `src/service/api`:接口封装与请求参数拼装
|
||||||
|
- `src/service/request`:统一请求实例、鉴权头、加密、错误处理、token 刷新
|
||||||
|
- `src/router/routes`:自定义路由定义
|
||||||
|
- `src/router/elegant`:自动生成的路由产物
|
||||||
|
- `src/theme/settings.ts`:默认主题与布局设置
|
||||||
|
- `build/plugins/router.ts`:elegant-router 配置与路由元信息生成逻辑
|
||||||
|
- `src/hooks/common/table.ts`:列表页表格 hook 主入口
|
||||||
|
- `src/hooks/common/form.ts`:表单校验与表单实例 hook
|
||||||
|
- `src/styles/scss/element-plus.scss`:当前项目表格、弹层、按钮、表单密度与公共壳样式标准
|
||||||
|
- `packages/*`:项目内本地共享库
|
||||||
|
- `docs/`:当前工作上下文的一部分,做架构级、权限级、页面规范级改动前优先查阅
|
||||||
|
|
||||||
|
## 生成文件
|
||||||
|
|
||||||
|
除非有非常明确的理由并且同步维护生成流程,否则不要手工修改生成文件。
|
||||||
|
|
||||||
|
- `src/router/elegant/imports.ts`
|
||||||
|
- `src/router/elegant/routes.ts`
|
||||||
|
- `src/router/elegant/transform.ts`
|
||||||
|
- `src/typings/elegant-router.d.ts`
|
||||||
|
- `src/typings/components.d.ts`
|
||||||
|
- `docs/frontend-page-resource-manifest.json`
|
||||||
|
|
||||||
|
如果路由生成产物过期或不一致,执行 `pnpm gen-route`。
|
||||||
|
如果页面资源清单需要同步,执行 `pnpm gen:page-resource-manifest`。
|
||||||
|
|
||||||
|
## 路由与导航开发口径
|
||||||
|
|
||||||
|
- 新增业务页面时,优先通过页面文件与 `build/plugins/router.ts` 补齐路由,不要手工在多个位置重复注册同一页面。
|
||||||
|
- 路由 `meta` 的中心落点是 `build/plugins/router.ts`;新增业务页的 `icon`、`order`、`roles`、`keepAlive` 优先在那里集中维护。
|
||||||
|
- 当前代码链路仍保留 `i18nKey` 兼容字段,但它是兼容保留项,不是新增业务页面必须补齐的默认要求。
|
||||||
|
- `meta.constant = true` 的路由属于常量路由;其余默认属于权限路由。
|
||||||
|
- 常量路由维护入口优先是 `build/plugins/router.ts` 和 `src/router/routes/custom-routes.ts`,不要把常量路由散落到业务页面逻辑里。
|
||||||
|
- 菜单图标约定属于路由契约的一部分:`meta.icon` 表示 Iconify 图标,`meta.localIcon` 表示本地 SVG 图标;不要混用字段语义。
|
||||||
|
|
||||||
|
## 分层职责约束
|
||||||
|
|
||||||
|
### `src/views`
|
||||||
|
|
||||||
|
- 页面层负责页面编排、交互状态、表单行为和对 store/service 的组合调用。
|
||||||
|
- 不要在页面组件里散落 URL 拼接、token 注入、统一错误提示或权限路由推导逻辑。
|
||||||
|
|
||||||
|
### `src/components`
|
||||||
|
|
||||||
|
- 共享组件负责可复用 UI 或局部业务部件。
|
||||||
|
- 不要把只服务于单个页面的复杂流程长期堆在公共组件目录中。
|
||||||
|
|
||||||
|
### `src/service/api`
|
||||||
|
|
||||||
|
- API 层负责接口封装、请求参数归一化、查询字符串拼装和返回类型对齐。
|
||||||
|
- 不要在 `views`、`store` 或 `components` 中重复手写同一接口地址和参数序列化逻辑。
|
||||||
|
|
||||||
|
### `src/service/request`
|
||||||
|
|
||||||
|
- 请求层负责统一请求实例、鉴权头、接口加密、成功码判定、token 刷新和通用错误处理。
|
||||||
|
- 除非任务明确需要,不要平行引入新的 `axios`/`fetch` 调用链绕开现有封装。
|
||||||
|
|
||||||
|
### `src/store/modules`
|
||||||
|
|
||||||
|
- Store 负责跨页面共享状态,例如认证、路由、标签页、主题、布局和全局 UI 状态。
|
||||||
|
- 临时性的页面局部状态优先留在页面组件或 composable 中,不要无边界堆进全局 store。
|
||||||
|
|
||||||
|
### `src/router` 与 `build/plugins/router.ts`
|
||||||
|
|
||||||
|
- 路由、菜单、权限标识、首页配置和路由元信息优先沿用当前 elegant-router 与 route store 链路。
|
||||||
|
- 不要只在页面里临时写条件分支来替代正式的路由、菜单或权限配置。
|
||||||
|
|
||||||
|
### `src/layouts` 与 `src/theme`
|
||||||
|
|
||||||
|
- 布局壳和主题设置是全局行为源头,相关改动要同时检查布局组件、theme store 和默认设置。
|
||||||
|
- 不要在业务页面里复制一套平行的布局状态或主题状态。
|
||||||
|
|
||||||
|
## 业务页面开发风格
|
||||||
|
|
||||||
|
- 页面组件保持“编排层薄”。页面文件主要负责搜索参数、表格 hook、列定义、弹层开关、接口调用编排,不把大量表单细节和重复交互直接堆在页面根组件里。
|
||||||
|
- 列表页优先拆出同目录下的 `modules/*` 子组件,例如搜索组件、操作弹层、详情抽屉、资源面板等。
|
||||||
|
- 系统管理下现有 `user`、`role`、`menu`、`dict` 页面可以作为参考实现,新增同类页面优先沿用它们的拆分方式。
|
||||||
|
- 搜索组件优先复用 `src/components/custom/table-search-panel.vue` 作为外壳。搜索模块本身应尽量只接收 `model`,只向外发出 `reset` / `search`,不直接承载列表请求逻辑。
|
||||||
|
- 列表能力优先复用 `src/hooks/common/table.ts` 中的 `useUIPaginatedTable`、`useTableOperate`、`defaultTransform`。
|
||||||
|
- 表单能力优先复用 `src/hooks/common/form.ts` 中的 `useForm`、`useFormRules`。
|
||||||
|
- 当前项目的真实业务口径是“内网中文优先”。新增业务页不必为了形式强行补全国际化键;但如果是在已有大量 `$t(...)` 的页面或模块内继续开发,优先保持该局部代码风格一致,不要半页中文直写、半页国际化混用。
|
||||||
|
|
||||||
|
## 表格、搜索区与操作列约束
|
||||||
|
|
||||||
|
- 搜索区按钮组保持在最右侧;存在折叠项时,按钮顺序保持为“展开/收起 -> 重置 -> 查询”。
|
||||||
|
- 不要在每个页面重新拼一套搜索区骨架,优先延续 `TableSearchPanel` 的结构和交互。
|
||||||
|
- 表格操作列优先复用 `src/components/custom/business-table-action-cell.tsx`。
|
||||||
|
- 操作数 `<= 2` 时默认直出;操作数 `> 2` 时优先收敛为 `1 个直出主按钮 + 1 个更多按钮`。
|
||||||
|
- 表格、按钮、弹层、表单的尺寸和间距标准优先由 `src/styles/scss/element-plus.scss` 和公共组件承接,不在业务页面散落写新的局部尺寸作为事实标准。
|
||||||
|
|
||||||
|
## 表单与弹层约束
|
||||||
|
|
||||||
|
- 新增、编辑能力优先沿用 `ElDialog / ElDrawer / ElForm / ElScrollbar / #footer` 这一套标准组合,不额外创造新的弹层交互模型。
|
||||||
|
- 轻中量表单优先复用 `src/components/custom/business-form-dialog.vue`;字段较多、需要保留列表上下文或承载重型控件时,再考虑 `src/components/custom/business-form-drawer.vue`。
|
||||||
|
- 表单分组优先复用 `src/components/custom/business-form-section.vue`。
|
||||||
|
- 现有公共壳组件已内置尺寸预设:`dialog` 的 `sm/md/lg` 对应 `520px/640px/720px`,`drawer` 的 `md/lg/xl` 对应 `480px/720px/960px`;优先使用预设值而不是页面内重复硬编码宽度。
|
||||||
|
- 常规 CRUD 表单优先使用 `label-position="top"`、`ElRow + ElCol` 双列布局、`gutter=16`;普通字段优先 `span=12`,长文本或重量级字段优先 `span=24`。
|
||||||
|
- 底部按钮顺序固定为“取消 -> 确认”,并保持右对齐。
|
||||||
|
- 单选组和开关类字段优先复用仓库既有样式钩子,例如 `business-form-radio-group`、`business-form-switch-field`。
|
||||||
|
|
||||||
|
## 接口、路由与权限约束
|
||||||
|
|
||||||
|
- 默认沿用 `src/service/request/index.ts` 中现有请求链路,不要另造一套鉴权、加密、错误处理或 token 刷新机制。
|
||||||
|
- 接口前缀、服务常量优先复用现有常量定义,例如 `src/constants/service.ts`。
|
||||||
|
- 后端契约变化时,至少同步检查 `src/service/api/*`、`src/typings/api/*`、相关页面调用和说明文档是否一致。
|
||||||
|
- 涉及路由、菜单、权限的改动时,同时检查 `build/plugins/router.ts`、`src/router/routes/*`、`src/store/modules/route/*` 和相关文档。
|
||||||
|
- 对于可再生的路由产物,优先修改源配置并执行 `pnpm gen-route`,不要把手工修补生成文件当成常规方案。
|
||||||
|
|
||||||
|
## 页面资源与菜单目录约束
|
||||||
|
|
||||||
|
- 页面组件键、页面资源、菜单目录是三层不同概念,不要把它们当成同一个值。
|
||||||
|
- `component` 决定“渲染哪个页面组件”;菜单目录决定“挂在哪个业务目录下”和最终 URL;页面资源主要用于从白名单中选择并回填组件信息。
|
||||||
|
- 不要因为组件键是 `view.system_dict`,就推导它只能挂在 `/system/dict`;同一个页面组件允许挂在新的业务目录下复用。
|
||||||
|
- 页面资源白名单中的标准路径是参考路径,不应反向覆盖当前菜单树已经确定的最终 URL。
|
||||||
|
- 涉及菜单编辑器或页面资源选择逻辑时,优先保证“组件可解析、资源合法、最终 URL 由菜单树决定”,不要强绑页面资源标准路径和父级目录前缀。
|
||||||
|
|
||||||
|
## 代码约定
|
||||||
|
|
||||||
|
- 优先使用现有别名导入(`@/...`、`~/...`),避免过长的相对路径。
|
||||||
|
- 保持与 TypeScript 严格模式兼容。
|
||||||
|
- 遵循仓库现有的 Vue SFC 风格:`script setup`、类型化 store、职责单一的小型 composable/helper。
|
||||||
|
- 修改界面时优先延续 `src/layouts` 和 `src/theme` 中已有的 UI 模式,不要平行引入另一套设计体系。
|
||||||
|
- 注释保持克制,只在代码本身不够直观时补充必要说明。
|
||||||
|
|
||||||
|
## 注释与编码
|
||||||
|
|
||||||
|
- 新增或修改代码时,关键分支、关键约束和非直观实现可以补充简洁中文注释。
|
||||||
|
- 不要为了省事删除原有有效注释,也不要添加没有信息量的注释。
|
||||||
|
- 写入中文内容时保持 UTF-8 编码,并自行确认显示正常;不要用改成英文来规避编码问题。
|
||||||
|
|
||||||
|
## 校验建议
|
||||||
|
|
||||||
|
对有实际影响的代码改动,优先执行:
|
||||||
|
|
||||||
|
- `pnpm typecheck`
|
||||||
|
- `pnpm lint`
|
||||||
|
|
||||||
|
如果改动涉及路由,额外执行:
|
||||||
|
|
||||||
|
- `pnpm gen-route`
|
||||||
|
|
||||||
|
如果改动影响页面资源清单、菜单资源选择或页面白名单,额外执行:
|
||||||
|
|
||||||
|
- `pnpm gen:page-resource-manifest`
|
||||||
|
|
||||||
|
静态校验时,至少自查以下几点:
|
||||||
|
|
||||||
|
- 调用链是否闭环,改动是否落在正确的分层位置
|
||||||
|
- 路由、菜单、权限标识、主题状态或资源注册是否前后一致
|
||||||
|
- 改动范围是否控制在当前任务所需的最小集合内
|
||||||
|
- 文档、类型定义、接口封装或生成产物是否需要同步更新
|
||||||
|
|
||||||
|
## 提交与脚本约束
|
||||||
|
|
||||||
|
- `pre-commit` 会执行 `pnpm typecheck && pnpm lint && git diff --exit-code`,因此“代码能跑”不等于“可以提交”。
|
||||||
|
- `pnpm lint` 实际会执行 `eslint . --fix`;提交失败后要检查是否有被自动修复但尚未重新暂存的文件。
|
||||||
|
- 提交规范说明以 `docs/前端提交规范与示例.md` 为准;最稳妥的提交方式是执行 `pnpm commit:zh`,按交互选择 `type`、`scope` 和 `description`。
|
||||||
|
- `commit-msg` 钩子会校验 Conventional Commits;推荐使用 `pnpm commit:zh` 生成提交信息。
|
||||||
|
- 如果手动提交,执行 `git commit -m "type(scope): 描述"`,并确保 `type`、`scope`、描述写法与 `docs/前端提交规范与示例.md` 保持一致。
|
||||||
|
- 提交信息基础格式遵循 `type(scope): 描述`。
|
||||||
|
- 写 Node ESM 脚本时,避免沿用 `__filename`、`__dirname` 这类下划线悬挂命名。
|
||||||
|
- 能并发的批量异步任务优先 `Promise.all(...)`,不要默认在循环体里直接 `await`。
|
||||||
|
- 手写 `new Promise(...)` 时优先使用 block 写法,不要把 executor 写成隐式返回值的单表达式箭头函数。
|
||||||
|
- 一个函数如果开始同时承担“判断 + 转换 + 组装 + 递归”,优先拆 helper,避免把复杂度堆到单个函数里。
|
||||||
|
|
||||||
|
## 代理工作说明
|
||||||
|
|
||||||
|
- 编辑前先检查当前 `git diff`,仓库中可能已经存在用户进行中的修改。
|
||||||
|
- 在工作树不干净时,不要回退与当前任务无关的变更。
|
||||||
|
- 修改布局或主题行为时,同时检查 `src/layouts/*` 和 `src/store/modules/theme/*`,因为相关逻辑分散在界面层和状态层。
|
||||||
|
- 修改路由或菜单时,同时检查 `build/plugins/router.ts` 和 `src/router/routes/*`。
|
||||||
|
- 做架构级、权限级或页面规范级修改前,优先查阅 `docs/` 中现有说明,避免与当前文档约定冲突。
|
||||||
393
route.json
Normal file
393
route.json
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "",
|
||||||
|
"data": {
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"id": "900000",
|
||||||
|
"name": "system",
|
||||||
|
"path": "/system",
|
||||||
|
"component": "layout.base",
|
||||||
|
"redirect": "/system/user",
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "权限中心",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "carbon:cloud-service-management",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 9,
|
||||||
|
"keepAlive": false,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "900001",
|
||||||
|
"name": "system_user",
|
||||||
|
"path": "/system/user",
|
||||||
|
"component": "view.system_user",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "用户管理",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "ic:round-manage-accounts",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 1,
|
||||||
|
"keepAlive": false,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "900002",
|
||||||
|
"name": "system_role",
|
||||||
|
"path": "/system/role",
|
||||||
|
"component": "view.system_role",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "角色管理",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "carbon:user-role",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 2,
|
||||||
|
"keepAlive": false,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2036723586735665153",
|
||||||
|
"name": "system_post",
|
||||||
|
"path": "/system/post",
|
||||||
|
"component": "view.system_post",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "岗位管理",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "mdi:account-group-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 3,
|
||||||
|
"keepAlive": false,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "900003",
|
||||||
|
"name": "system_menu",
|
||||||
|
"path": "/system/menu",
|
||||||
|
"component": "view.system_menu",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "菜单管理",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "material-symbols:route",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 4,
|
||||||
|
"keepAlive": false,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "legacy-system",
|
||||||
|
"path": "/legacy-system",
|
||||||
|
"component": "layout.base",
|
||||||
|
"redirect": "/legacy-system/dict",
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "系统管理",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "ep:tools",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 10,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "900004",
|
||||||
|
"name": "system_dict",
|
||||||
|
"path": "/legacy-system/dict",
|
||||||
|
"component": "view.system_dict",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "字典管理",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "mdi:book-open-page-variant-outline",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 4,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "108",
|
||||||
|
"name": "legacy_system_log",
|
||||||
|
"path": "/legacy-system/log",
|
||||||
|
"component": "layout.base",
|
||||||
|
"redirect": "/legacy-system/log/operate-log",
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "审计日志",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "ep:document-copy",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 9,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "500",
|
||||||
|
"name": "SystemOperateLog",
|
||||||
|
"path": "/legacy-system/log/operate-log",
|
||||||
|
"component": "system/operatelog/index",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "操作日志",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "ep:position",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 1,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "501",
|
||||||
|
"name": "SystemLoginLog",
|
||||||
|
"path": "/legacy-system/log/login-log",
|
||||||
|
"component": "system/loginlog/index",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "登录日志",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "ep:promotion",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 2,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "infra",
|
||||||
|
"path": "/infra",
|
||||||
|
"component": "layout.base",
|
||||||
|
"redirect": "/infra/log",
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "基础设施",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "ep:monitor",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 20,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "1083",
|
||||||
|
"name": "infra_log",
|
||||||
|
"path": "/infra/log",
|
||||||
|
"component": "layout.base",
|
||||||
|
"redirect": "/infra/log/api-access-log",
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "API 日志",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "fa:tasks",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 4,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "1078",
|
||||||
|
"name": "InfraApiAccessLog",
|
||||||
|
"path": "/infra/log/api-access-log",
|
||||||
|
"component": "infra/apiAccessLog/index",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "访问日志",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "ep:place",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 1,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1084",
|
||||||
|
"name": "InfraApiErrorLog",
|
||||||
|
"path": "/infra/log/api-error-log",
|
||||||
|
"component": "infra/apiErrorLog/index",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "错误日志",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "ep:warning-filled",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 2,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1243",
|
||||||
|
"name": "infra_file",
|
||||||
|
"path": "/infra/file",
|
||||||
|
"component": "layout.base",
|
||||||
|
"redirect": "/infra/file/file-config",
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "文件管理",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "ep:files",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 6,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "1237",
|
||||||
|
"name": "InfraFileConfig",
|
||||||
|
"path": "/infra/file/file-config",
|
||||||
|
"component": "infra/fileConfig/index",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "文件配置",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "fa-solid:file-signature",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 0,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1090",
|
||||||
|
"name": "InfraFile",
|
||||||
|
"path": "/infra/file/file",
|
||||||
|
"component": "infra/file/index",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "文件列表",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "ep:upload-filled",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 5,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "106",
|
||||||
|
"name": "InfraConfig",
|
||||||
|
"path": "/infra/config",
|
||||||
|
"component": "infra/config/index",
|
||||||
|
"redirect": null,
|
||||||
|
"props": null,
|
||||||
|
"meta": {
|
||||||
|
"title": "配置管理",
|
||||||
|
"i18nKey": null,
|
||||||
|
"icon": "fa:connectdevelop",
|
||||||
|
"localIcon": null,
|
||||||
|
"order": 8,
|
||||||
|
"keepAlive": true,
|
||||||
|
"hideInMenu": false,
|
||||||
|
"activeMenu": null,
|
||||||
|
"multiTab": null,
|
||||||
|
"fixedIndexInTab": null
|
||||||
|
},
|
||||||
|
"children": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"home": "system_user"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,18 +16,18 @@ defineOptions({ name: 'BaseLayout' });
|
|||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
|
const { childLevelMenus } = setupMixMenuContext();
|
||||||
|
|
||||||
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
|
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
|
||||||
|
|
||||||
const layoutMode = computed(() => {
|
const layoutMode = computed(() => {
|
||||||
const vertical: LayoutMode = 'vertical';
|
const vertical: LayoutMode = 'vertical';
|
||||||
const horizontal: LayoutMode = 'horizontal';
|
const horizontal: LayoutMode = 'horizontal';
|
||||||
return themeStore.layout.mode.includes(vertical) ? vertical : horizontal;
|
return themeStore.layoutMode.includes(vertical) ? vertical : horizontal;
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerProps = computed(() => {
|
const headerProps = computed(() => {
|
||||||
const { mode, reverseHorizontalMix } = themeStore.layout;
|
const mode = themeStore.layoutMode;
|
||||||
|
|
||||||
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
|
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
|
||||||
vertical: {
|
vertical: {
|
||||||
@@ -48,31 +48,26 @@ const headerProps = computed(() => {
|
|||||||
'horizontal-mix': {
|
'horizontal-mix': {
|
||||||
showLogo: true,
|
showLogo: true,
|
||||||
showMenu: true,
|
showMenu: true,
|
||||||
showMenuToggler: reverseHorizontalMix && isActiveFirstLevelMenuHasChildren.value
|
showMenuToggler: false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return headerPropsConfig[mode];
|
return headerPropsConfig[mode];
|
||||||
});
|
});
|
||||||
|
|
||||||
const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
|
const siderVisible = computed(() => themeStore.layoutMode !== 'horizontal');
|
||||||
|
|
||||||
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
|
const isVerticalMix = computed(() => themeStore.layoutMode === 'vertical-mix');
|
||||||
|
|
||||||
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
|
const isHorizontalMix = computed(() => themeStore.layoutMode === 'horizontal-mix');
|
||||||
|
|
||||||
const siderWidth = computed(() => getSiderWidth());
|
const siderWidth = computed(() => getSiderWidth());
|
||||||
|
|
||||||
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
|
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
|
||||||
|
|
||||||
function getSiderWidth() {
|
function getSiderWidth() {
|
||||||
const { reverseHorizontalMix } = themeStore.layout;
|
|
||||||
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
|
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
|
||||||
|
|
||||||
if (isHorizontalMix.value && reverseHorizontalMix) {
|
|
||||||
return isActiveFirstLevelMenuHasChildren.value ? width : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width;
|
let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width;
|
||||||
|
|
||||||
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
||||||
@@ -83,13 +78,8 @@ function getSiderWidth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSiderCollapsedWidth() {
|
function getSiderCollapsedWidth() {
|
||||||
const { reverseHorizontalMix } = themeStore.layout;
|
|
||||||
const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = themeStore.sider;
|
const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = themeStore.sider;
|
||||||
|
|
||||||
if (isHorizontalMix.value && reverseHorizontalMix) {
|
|
||||||
return isActiveFirstLevelMenuHasChildren.value ? collapsedWidth : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let w = isVerticalMix.value || isHorizontalMix.value ? mixCollapsedWidth : collapsedWidth;
|
let w = isVerticalMix.value || isHorizontalMix.value ? mixCollapsedWidth : collapsedWidth;
|
||||||
|
|
||||||
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
|
||||||
@@ -110,7 +100,7 @@ function getSiderCollapsedWidth() {
|
|||||||
:full-content="appStore.fullContent"
|
:full-content="appStore.fullContent"
|
||||||
:fixed-top="themeStore.fixedHeaderAndTab"
|
:fixed-top="themeStore.fixedHeaderAndTab"
|
||||||
:header-height="themeStore.header.height"
|
:header-height="themeStore.header.height"
|
||||||
:tab-visible="themeStore.tab.visible"
|
:tab-visible="themeStore.tabVisible"
|
||||||
:tab-height="themeStore.tab.height"
|
:tab-height="themeStore.tab.height"
|
||||||
:content-class="appStore.contentXScrollable ? 'overflow-x-hidden' : ''"
|
:content-class="appStore.contentXScrollable ? 'overflow-x-hidden' : ''"
|
||||||
:sider-visible="siderVisible"
|
:sider-visible="siderVisible"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useRouterPush } from '@/hooks/common/router';
|
|||||||
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
|
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
|
||||||
|
|
||||||
function useMixMenu() {
|
function useMixMenu() {
|
||||||
const route = useRoute();
|
|
||||||
const routeStore = useRouteStore();
|
const routeStore = useRouteStore();
|
||||||
const { selectedKey } = useMenu();
|
const { selectedKey } = useMenu();
|
||||||
|
|
||||||
@@ -19,9 +18,9 @@ function useMixMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getActiveFirstLevelMenuKey() {
|
function getActiveFirstLevelMenuKey() {
|
||||||
const [firstLevelRouteName] = selectedKey.value.split('_');
|
const [firstLevelMenuKey = ''] = routeStore.getSelectedMenuKeyPath(selectedKey.value);
|
||||||
|
|
||||||
setActiveFirstLevelMenuKey(firstLevelRouteName);
|
setActiveFirstLevelMenuKey(firstLevelMenuKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
|
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
|
||||||
@@ -49,7 +48,7 @@ function useMixMenu() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.name,
|
[selectedKey, allMenus],
|
||||||
() => {
|
() => {
|
||||||
getActiveFirstLevelMenuKey();
|
getActiveFirstLevelMenuKey();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ withDefaults(defineProps<Props>(), {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterLink to="/" class="w-full flex-center nowrap-hidden">
|
<RouterLink to="/" class="h-full w-full flex-y-center justify-start gap-8px nowrap-hidden px-12px">
|
||||||
<SystemLogo class="text-32px text-primary" />
|
<SystemLogo class="shrink-0 text-32px text-primary" />
|
||||||
<h2 v-show="showTitle" class="pl-8px text-16px text-primary font-bold transition duration-300 ease-in-out">
|
<h2
|
||||||
|
v-show="showTitle"
|
||||||
|
class="min-w-0 flex-1-hidden ellipsis-text text-16px text-primary font-bold transition duration-300 ease-in-out"
|
||||||
|
>
|
||||||
{{ $t('system.title') }}
|
{{ $t('system.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import VerticalMenu from './modules/vertical-menu.vue';
|
|||||||
import VerticalMixMenu from './modules/vertical-mix-menu.vue';
|
import VerticalMixMenu from './modules/vertical-mix-menu.vue';
|
||||||
import HorizontalMenu from './modules/horizontal-menu.vue';
|
import HorizontalMenu from './modules/horizontal-menu.vue';
|
||||||
import HorizontalMixMenu from './modules/horizontal-mix-menu.vue';
|
import HorizontalMixMenu from './modules/horizontal-mix-menu.vue';
|
||||||
import ReversedHorizontalMixMenu from './modules/reversed-horizontal-mix-menu.vue';
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'GlobalMenu'
|
name: 'GlobalMenu'
|
||||||
@@ -21,13 +20,13 @@ const activeMenu = computed(() => {
|
|||||||
vertical: VerticalMenu,
|
vertical: VerticalMenu,
|
||||||
'vertical-mix': VerticalMixMenu,
|
'vertical-mix': VerticalMixMenu,
|
||||||
horizontal: HorizontalMenu,
|
horizontal: HorizontalMenu,
|
||||||
'horizontal-mix': themeStore.layout.reverseHorizontalMix ? ReversedHorizontalMixMenu : HorizontalMixMenu
|
'horizontal-mix': HorizontalMixMenu
|
||||||
};
|
};
|
||||||
|
|
||||||
return menuMap[themeStore.layout.mode];
|
return menuMap[themeStore.layoutMode];
|
||||||
});
|
});
|
||||||
|
|
||||||
const reRenderVertical = computed(() => themeStore.layout.mode === 'vertical' && appStore.isMobile);
|
const reRenderVertical = computed(() => themeStore.layoutMode === 'vertical' && appStore.isMobile);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,43 +1,111 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { RouteKey } from '@elegant-router/types';
|
import { computed } from 'vue';
|
||||||
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
|
import { useRouteStore } from '@/store/modules/route';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||||||
import { useMenu, useMixMenuContext } from '../../../context';
|
import { useMenu, useMixMenuContext } from '../../../context';
|
||||||
import MenuItem from '../components/menu-item.vue';
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'HorizontalMixMenu'
|
name: 'HorizontalMixMenu'
|
||||||
});
|
});
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
const routeStore = useRouteStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
const { routerPushByKeyWithMetaQuery } = useRouterPush();
|
||||||
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
|
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
|
||||||
const { selectedKeyDummy, handleSelect } = useMenu();
|
const { selectedKey } = useMenu();
|
||||||
|
|
||||||
|
const activeFirstLevelMenu = computed(
|
||||||
|
() => allMenus.value.find(menu => menu.key === activeFirstLevelMenuKey.value) || null
|
||||||
|
);
|
||||||
|
const headerMenuHeight = computed(() => `${themeStore.header.height}px`);
|
||||||
|
const selectedMenuKeyPath = computed(() => routeStore.getSelectedMenuKeyPath(selectedKey.value));
|
||||||
|
|
||||||
function handleSelectMixMenu(menu: App.Global.Menu) {
|
function handleSelectMixMenu(menu: App.Global.Menu) {
|
||||||
setActiveFirstLevelMenuKey(menu.key);
|
setActiveFirstLevelMenuKey(menu.key);
|
||||||
|
|
||||||
if (!menu.children?.length) {
|
|
||||||
routerPushByKeyWithMetaQuery(menu.routeKey);
|
routerPushByKeyWithMetaQuery(menu.routeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickNavItem(menu: App.Global.Menu) {
|
||||||
|
routerPushByKeyWithMetaQuery(menu.routeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickDomainAnchor() {
|
||||||
|
if (!activeFirstLevelMenu.value) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routerPushByKeyWithMetaQuery(activeFirstLevelMenu.value.routeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMenuActive(menu: App.Global.Menu) {
|
||||||
|
return selectedMenuKeyPath.value.includes(menu.key);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||||||
<ElMenu
|
<div class="mix-header-nav size-full min-w-0 flex-y-center">
|
||||||
ellipsis
|
<button
|
||||||
class="w-full"
|
v-if="activeFirstLevelMenu"
|
||||||
mode="horizontal"
|
type="button"
|
||||||
:default-active="selectedKeyDummy"
|
class="domain-anchor h-full flex-y-center gap-8px px-8px text-left"
|
||||||
@select="val => handleSelect(val as RouteKey)"
|
@click="handleClickDomainAnchor"
|
||||||
>
|
>
|
||||||
<MenuItem v-for="item in childLevelMenus" :key="item.key" :item="item" :index="item.key" />
|
<component :is="activeFirstLevelMenu.icon" v-if="activeFirstLevelMenu.icon" class="text-icon" />
|
||||||
</ElMenu>
|
<span class="domain-anchor__label">{{ activeFirstLevelMenu.label }}</span>
|
||||||
|
</button>
|
||||||
|
<div v-if="childLevelMenus.length" class="mx-12px h-20px w-1px shrink-0 bg-[var(--el-border-color)]"></div>
|
||||||
|
<div v-if="childLevelMenus.length" class="header-nav-list h-full min-w-0 flex-1">
|
||||||
|
<template v-for="item in childLevelMenus" :key="item.key">
|
||||||
|
<button
|
||||||
|
v-if="!item.children?.length"
|
||||||
|
type="button"
|
||||||
|
class="header-nav-item"
|
||||||
|
:class="{ 'is-active': isMenuActive(item) }"
|
||||||
|
@click="handleClickNavItem(item)"
|
||||||
|
>
|
||||||
|
<span class="header-nav-item__label">{{ item.label }}</span>
|
||||||
|
</button>
|
||||||
|
<ElDropdown
|
||||||
|
v-else
|
||||||
|
trigger="hover"
|
||||||
|
placement="bottom"
|
||||||
|
popper-class="header-nav-dropdown"
|
||||||
|
:show-timeout="120"
|
||||||
|
:hide-timeout="120"
|
||||||
|
:teleported="true"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="header-nav-item header-nav-item--dropdown"
|
||||||
|
:class="{ 'is-active': isMenuActive(item) }"
|
||||||
|
>
|
||||||
|
<span class="header-nav-item__label">{{ item.label }}</span>
|
||||||
|
<icon-ep:arrow-down class="header-nav-item__arrow" />
|
||||||
|
</button>
|
||||||
|
<template #dropdown>
|
||||||
|
<ElDropdownMenu>
|
||||||
|
<ElDropdownItem
|
||||||
|
v-for="child in item.children"
|
||||||
|
:key="child.key"
|
||||||
|
class="header-nav-dropdown__item"
|
||||||
|
:class="{ 'is-active-route': isMenuActive(child) }"
|
||||||
|
@click="handleClickNavItem(child)"
|
||||||
|
>
|
||||||
|
{{ child.label }}
|
||||||
|
</ElDropdownItem>
|
||||||
|
</ElDropdownMenu>
|
||||||
|
</template>
|
||||||
|
</ElDropdown>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<FirstLevelMenu
|
<FirstLevelMenu
|
||||||
@@ -52,4 +120,138 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
|
|||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.mix-header-nav {
|
||||||
|
height: v-bind(headerMenuHeight);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-anchor {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
margin: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
font: inherit;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 0;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-anchor:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-anchor__label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 12rem;
|
||||||
|
line-height: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav-item {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav-item:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav-item__label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav-item__arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav-item.is-active {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav-item.is-active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 0;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.header-nav-dropdown.el-popper) {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 14px;
|
||||||
|
background-color: rgb(255 255 255 / 98%);
|
||||||
|
box-shadow:
|
||||||
|
0 12px 28px rgb(15 23 42 / 10%),
|
||||||
|
0 2px 8px rgb(15 23 42 / 6%);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.header-nav-dropdown .el-popper__arrow) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.header-nav-dropdown .el-dropdown-menu) {
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid rgb(226 232 240 / 90%);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.header-nav-dropdown .el-dropdown-menu__item) {
|
||||||
|
height: 40px;
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 40px;
|
||||||
|
color: rgb(15 23 42 / 88%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.header-nav-dropdown .el-dropdown-menu__item:hover) {
|
||||||
|
background-color: rgb(99 102 241 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.header-nav-dropdown .el-dropdown-menu__item.is-active-route) {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
background-color: rgb(99 102 241 / 10%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ defineOptions({ name: 'GlobalSider' });
|
|||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
|
const isVerticalMix = computed(() => themeStore.layoutMode === 'vertical-mix');
|
||||||
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
|
const isHorizontalMix = computed(() => themeStore.layoutMode === 'horizontal-mix');
|
||||||
const darkMenu = computed(() => !themeStore.darkMode && !isHorizontalMix.value && themeStore.sider.inverted);
|
const darkMenu = computed(() => !themeStore.darkMode && !isHorizontalMix.value && themeStore.sider.inverted);
|
||||||
const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
|
const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
|
||||||
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
|
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import DarkMode from './modules/dark-mode.vue';
|
import DarkMode from './modules/dark-mode.vue';
|
||||||
import LayoutMode from './modules/layout-mode.vue';
|
|
||||||
import ThemeColor from './modules/theme-color.vue';
|
import ThemeColor from './modules/theme-color.vue';
|
||||||
import PageFun from './modules/page-fun.vue';
|
import PageFun from './modules/page-fun.vue';
|
||||||
import ConfigOperation from './modules/config-operation.vue';
|
import ConfigOperation from './modules/config-operation.vue';
|
||||||
@@ -15,7 +14,6 @@ const appStore = useAppStore();
|
|||||||
<template>
|
<template>
|
||||||
<ElDrawer v-model="appStore.themeDrawerVisible" :title="$t('theme.themeDrawerTitle')" :size="360">
|
<ElDrawer v-model="appStore.themeDrawerVisible" :title="$t('theme.themeDrawerTitle')" :size="360">
|
||||||
<DarkMode />
|
<DarkMode />
|
||||||
<LayoutMode />
|
|
||||||
<ThemeColor />
|
<ThemeColor />
|
||||||
<PageFun />
|
<PageFun />
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function handleColourWeaknessChange(value: boolean) {
|
|||||||
themeStore.setColourWeakness(value);
|
themeStore.setColourWeakness(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layout.mode.includes('vertical'));
|
const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layoutMode.includes('vertical'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { themePageAnimationModeOptions, themeScrollModeOptions, themeTabModeOptions } from '@/constants/app';
|
import { themePageAnimationModeOptions, themeScrollModeOptions } from '@/constants/app';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { translateOptions } from '@/utils/common';
|
import { translateOptions } from '@/utils/common';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
@@ -10,7 +10,7 @@ defineOptions({ name: 'PageFun' });
|
|||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
const layoutMode = computed(() => themeStore.layout.mode);
|
const layoutMode = computed(() => themeStore.layoutMode);
|
||||||
|
|
||||||
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix'));
|
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix'));
|
||||||
|
|
||||||
@@ -55,25 +55,6 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
|
|||||||
<SettingItem v-if="themeStore.header.breadcrumb.visible" key="4-1" :label="$t('theme.header.breadcrumb.showIcon')">
|
<SettingItem v-if="themeStore.header.breadcrumb.visible" key="4-1" :label="$t('theme.header.breadcrumb.showIcon')">
|
||||||
<ElSwitch v-model="themeStore.header.breadcrumb.showIcon" />
|
<ElSwitch v-model="themeStore.header.breadcrumb.showIcon" />
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem key="5" :label="$t('theme.tab.visible')">
|
|
||||||
<ElSwitch v-model="themeStore.tab.visible" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="themeStore.tab.visible" key="5-1" :label="$t('theme.tab.cache')">
|
|
||||||
<ElSwitch v-model="themeStore.tab.cache" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="themeStore.tab.visible" key="5-2" :label="$t('theme.tab.height')">
|
|
||||||
<ElInputNumber v-model="themeStore.tab.height" size="small" :step="1" class="w-120px" />
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="themeStore.tab.visible" key="5-3" :label="$t('theme.tab.mode.title')">
|
|
||||||
<ElSelect v-model="themeStore.tab.mode" size="small" class="w-120px">
|
|
||||||
<ElOption
|
|
||||||
v-for="{ label, value } in translateOptions(themeTabModeOptions)"
|
|
||||||
:key="value"
|
|
||||||
:label="label"
|
|
||||||
:value="value"
|
|
||||||
/>
|
|
||||||
</ElSelect>
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem v-if="layoutMode === 'vertical'" key="6-1" :label="$t('theme.sider.width')">
|
<SettingItem v-if="layoutMode === 'vertical'" key="6-1" :label="$t('theme.sider.width')">
|
||||||
<ElInputNumber v-model="themeStore.sider.width" size="small" :step="1" class="w-120px" />
|
<ElInputNumber v-model="themeStore.sider.width" size="small" :step="1" class="w-120px" />
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|||||||
@@ -83,11 +83,10 @@ export const useAppStore = defineStore(SetupStoreId.App, () => {
|
|||||||
if (newValue) {
|
if (newValue) {
|
||||||
// backup theme setting before is mobile
|
// backup theme setting before is mobile
|
||||||
localStg.set('backupThemeSettingBeforeIsMobile', {
|
localStg.set('backupThemeSettingBeforeIsMobile', {
|
||||||
layout: themeStore.layout.mode,
|
layout: themeStore.layoutMode,
|
||||||
siderCollapse: siderCollapse.value
|
siderCollapse: siderCollapse.value
|
||||||
});
|
});
|
||||||
|
|
||||||
themeStore.setThemeLayout('vertical');
|
|
||||||
setSiderCollapse(true);
|
setSiderCollapse(true);
|
||||||
} else {
|
} else {
|
||||||
// when is not mobile, recover the backup theme setting
|
// when is not mobile, recover the backup theme setting
|
||||||
@@ -95,7 +94,6 @@ export const useAppStore = defineStore(SetupStoreId.App, () => {
|
|||||||
|
|
||||||
if (backup) {
|
if (backup) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
themeStore.setThemeLayout(backup.layout);
|
|
||||||
setSiderCollapse(backup.siderCollapse);
|
setSiderCollapse(backup.siderCollapse);
|
||||||
|
|
||||||
localStg.remove('backupThemeSettingBeforeIsMobile');
|
localStg.remove('backupThemeSettingBeforeIsMobile');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { computed, effectScope, onScopeDispose, ref, toRefs, watch } from 'vue';
|
import { computed, effectScope, onScopeDispose, ref, toRefs, watch } from 'vue';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { usePreferredColorScheme } from '@vueuse/core';
|
import { breakpointsTailwind, useBreakpoints, usePreferredColorScheme } from '@vueuse/core';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { getPaletteColorByNumber } from '@sa/color';
|
import { getPaletteColorByNumber } from '@sa/color';
|
||||||
import { localStg } from '@/utils/storage';
|
import { localStg } from '@/utils/storage';
|
||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
||||||
const scope = effectScope();
|
const scope = effectScope();
|
||||||
const osTheme = usePreferredColorScheme();
|
const osTheme = usePreferredColorScheme();
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||||
|
const isMobile = breakpoints.smaller('sm');
|
||||||
|
|
||||||
/** Theme settings */
|
/** Theme settings */
|
||||||
const settings: Ref<App.Theme.ThemeSetting> = ref(initThemeSettings());
|
const settings: Ref<App.Theme.ThemeSetting> = ref(initThemeSettings());
|
||||||
@@ -51,6 +53,12 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
/** UI theme */
|
/** UI theme */
|
||||||
const uiTheme = computed(() => getNaiveTheme(themeColors.value, settings.value.recommendColor));
|
const uiTheme = computed(() => getNaiveTheme(themeColors.value, settings.value.recommendColor));
|
||||||
|
|
||||||
|
/** Product layout mode */
|
||||||
|
const layoutMode = computed<UnionKey.ThemeLayoutMode>(() => (isMobile.value ? 'vertical' : 'horizontal-mix'));
|
||||||
|
|
||||||
|
/** Product tab visible */
|
||||||
|
const tabVisible = computed(() => (isMobile.value ? settings.value.tab.visible : false));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings json
|
* Settings json
|
||||||
*
|
*
|
||||||
@@ -216,6 +224,8 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
darkMode,
|
darkMode,
|
||||||
themeColors,
|
themeColors,
|
||||||
uiTheme,
|
uiTheme,
|
||||||
|
layoutMode,
|
||||||
|
tabVisible,
|
||||||
settingsJson,
|
settingsJson,
|
||||||
setGrayscale,
|
setGrayscale,
|
||||||
setColourWeakness,
|
setColourWeakness,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
|||||||
},
|
},
|
||||||
isInfoFollowPrimary: true,
|
isInfoFollowPrimary: true,
|
||||||
layout: {
|
layout: {
|
||||||
mode: 'vertical',
|
mode: 'horizontal-mix',
|
||||||
scrollMode: 'content',
|
scrollMode: 'content',
|
||||||
reverseHorizontalMix: false
|
reverseHorizontalMix: false
|
||||||
},
|
},
|
||||||
|
|||||||
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@@ -82,6 +82,7 @@ declare module 'vue' {
|
|||||||
IconCarbonPlay: typeof import('~icons/carbon/play')['default']
|
IconCarbonPlay: typeof import('~icons/carbon/play')['default']
|
||||||
IconCarbonStop: typeof import('~icons/carbon/stop')['default']
|
IconCarbonStop: typeof import('~icons/carbon/stop')['default']
|
||||||
'IconCharm:download': typeof import('~icons/charm/download')['default']
|
'IconCharm:download': typeof import('~icons/charm/download')['default']
|
||||||
|
'IconEp:arrowDown': typeof import('~icons/ep/arrow-down')['default']
|
||||||
IconEpRemoveFilled: typeof import('~icons/ep/remove-filled')['default']
|
IconEpRemoveFilled: typeof import('~icons/ep/remove-filled')['default']
|
||||||
IconEpSuccessFilled: typeof import('~icons/ep/success-filled')['default']
|
IconEpSuccessFilled: typeof import('~icons/ep/success-filled')['default']
|
||||||
'IconF7:circleFill': typeof import('~icons/f7/circle-fill')['default']
|
'IconF7:circleFill': typeof import('~icons/f7/circle-fill')['default']
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ const treeProps = {
|
|||||||
label: 'name'
|
label: 'name'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
function applyCheckedKeys(keys: number[]) {
|
||||||
|
checkedKeys.value = [...keys];
|
||||||
|
treeRef.value?.setCheckedKeys(keys);
|
||||||
|
}
|
||||||
|
|
||||||
function getTagType(type: Api.SystemManage.MenuType): UI.ThemeColor {
|
function getTagType(type: Api.SystemManage.MenuType): UI.ThemeColor {
|
||||||
const tagMap: Record<Api.SystemManage.MenuType, UI.ThemeColor> = {
|
const tagMap: Record<Api.SystemManage.MenuType, UI.ThemeColor> = {
|
||||||
1: 'info',
|
1: 'info',
|
||||||
@@ -83,8 +88,7 @@ function collectExpandableNodeIds(nodes: Api.SystemManage.MenuSimple[]) {
|
|||||||
|
|
||||||
async function loadRoleMenus() {
|
async function loadRoleMenus() {
|
||||||
if (!props.role) {
|
if (!props.role) {
|
||||||
checkedKeys.value = [];
|
applyCheckedKeys([]);
|
||||||
treeRef.value?.setCheckedKeys([]);
|
|
||||||
treeRef.value?.filter(filterKeyword.value);
|
treeRef.value?.filter(filterKeyword.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -96,14 +100,14 @@ async function loadRoleMenus() {
|
|||||||
permissionLoading.value = false;
|
permissionLoading.value = false;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
checkedKeys.value = [];
|
applyCheckedKeys([]);
|
||||||
treeRef.value?.setCheckedKeys([]);
|
|
||||||
treeRef.value?.filter(filterKeyword.value);
|
treeRef.value?.filter(filterKeyword.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkedKeys.value = data;
|
// Role-menu bindings are exact IDs from the backend, so tree echo must not
|
||||||
treeRef.value?.setCheckedKeys(data);
|
// cascade parent checks down to unrelated descendants.
|
||||||
|
applyCheckedKeys(data);
|
||||||
treeRef.value?.filter(filterKeyword.value);
|
treeRef.value?.filter(filterKeyword.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +135,7 @@ async function handleSave() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkedKeys.value = menuIds;
|
checkedKeys.value = [...menuIds];
|
||||||
|
|
||||||
window.$message?.success($t('common.modifySuccess'));
|
window.$message?.success($t('common.modifySuccess'));
|
||||||
emit('saved');
|
emit('saved');
|
||||||
@@ -153,7 +157,7 @@ watch(
|
|||||||
() => props.menuTree.length,
|
() => props.menuTree.length,
|
||||||
value => {
|
value => {
|
||||||
if (value && props.role) {
|
if (value && props.role) {
|
||||||
treeRef.value?.setCheckedKeys(checkedKeys.value);
|
applyCheckedKeys(checkedKeys.value);
|
||||||
treeRef.value?.filter(filterKeyword.value);
|
treeRef.value?.filter(filterKeyword.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,6 +207,7 @@ watch(
|
|||||||
ref="treeRef"
|
ref="treeRef"
|
||||||
node-key="id"
|
node-key="id"
|
||||||
show-checkbox
|
show-checkbox
|
||||||
|
check-strictly
|
||||||
:default-expanded-keys="defaultExpandedKeys"
|
:default-expanded-keys="defaultExpandedKeys"
|
||||||
:data="menuTree"
|
:data="menuTree"
|
||||||
:props="treeProps"
|
:props="treeProps"
|
||||||
|
|||||||
@@ -15,20 +15,21 @@
|
|||||||
* - 叶子节点:基层员工,没有下级
|
* - 叶子节点:基层员工,没有下级
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {nextTick, onMounted, reactive, ref} from 'vue';
|
import { nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
import type {ElTree} from 'element-plus';
|
import type { ElTree } from 'element-plus';
|
||||||
import {ElButton, ElPopconfirm, ElTag} from 'element-plus';
|
import { ElButton, ElPopconfirm, ElTag } from 'element-plus';
|
||||||
import {useBoolean} from '@sa/hooks';
|
import { useBoolean } from '@sa/hooks';
|
||||||
import {
|
import {
|
||||||
fetchBatchDeleteUserManagementRelation,
|
fetchBatchDeleteUserManagementRelation,
|
||||||
fetchDeleteUserManagementRelation, fetchGetUserListByDeptId,
|
fetchDeleteUserManagementRelation,
|
||||||
|
fetchGetUserListByDeptId,
|
||||||
fetchGetUserManagementRelationQuery,
|
fetchGetUserManagementRelationQuery,
|
||||||
fetchGetUserManagementRelationTree,
|
fetchGetUserManagementRelationTree
|
||||||
} from '@/service/api';
|
} from '@/service/api';
|
||||||
import RelationOperateDialog from './modules/relation-operate-dialog.vue';
|
import RelationOperateDialog from './modules/relation-operate-dialog.vue';
|
||||||
import RelationSearch from './modules/relation-search.vue';
|
import RelationSearch from './modules/relation-search.vue';
|
||||||
|
|
||||||
defineOptions({name: 'UserManagementRelation'});
|
defineOptions({ name: 'UserManagementRelation' });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件 userQuery 定义
|
* 组件 userQuery 定义
|
||||||
@@ -40,8 +41,8 @@ interface userQuery {
|
|||||||
deptId?: number | null;
|
deptId?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//从user的index组件访问带人关系,fromUserIndex为true,否则false; dept=100是灿能电力的id
|
// 从user的index组件访问带人关系,fromUserIndex为true,否则false; dept=100是灿能电力的id
|
||||||
const {fromUserIndex = false, deptId = 100} = defineProps<userQuery>()
|
const { fromUserIndex = false, deptId = 100 } = defineProps<userQuery>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化搜索参数
|
* 初始化搜索参数
|
||||||
@@ -86,7 +87,7 @@ const treeProps: any = {
|
|||||||
* 获取用户简单列表,供搜索组件和对话框组件共享使用
|
* 获取用户简单列表,供搜索组件和对话框组件共享使用
|
||||||
*/
|
*/
|
||||||
async function loadUserList() {
|
async function loadUserList() {
|
||||||
const {data, error} = await fetchGetUserListByDeptId(deptId);
|
const { data, error } = await fetchGetUserListByDeptId(deptId);
|
||||||
if (!error) {
|
if (!error) {
|
||||||
userList.value = data || [];
|
userList.value = data || [];
|
||||||
}
|
}
|
||||||
@@ -103,10 +104,10 @@ async function loadTreeData() {
|
|||||||
try {
|
try {
|
||||||
// 默认不是来自user的index组件访问且deptId=100,查询灿能电力及其以下所有部门的用户的带人关系
|
// 默认不是来自user的index组件访问且deptId=100,查询灿能电力及其以下所有部门的用户的带人关系
|
||||||
const query: Api.SystemManage.UserManagementRelationQueryReqVO = {
|
const query: Api.SystemManage.UserManagementRelationQueryReqVO = {
|
||||||
fromUserIndex: fromUserIndex,
|
fromUserIndex,
|
||||||
deptId: deptId
|
deptId
|
||||||
};
|
};
|
||||||
const {data, error} = await fetchGetUserManagementRelationTree(query);
|
const { data, error } = await fetchGetUserManagementRelationTree(query);
|
||||||
|
|
||||||
if (!error) {
|
if (!error) {
|
||||||
treeData.value = data || [];
|
treeData.value = data || [];
|
||||||
@@ -127,7 +128,7 @@ async function loadTreeDataByQuery(query: Api.SystemManage.UserManagementRelatio
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {data, error} = await fetchGetUserManagementRelationQuery(query);
|
const { data, error } = await fetchGetUserManagementRelationQuery(query);
|
||||||
|
|
||||||
if (!error) {
|
if (!error) {
|
||||||
treeData.value = data || [];
|
treeData.value = data || [];
|
||||||
@@ -164,8 +165,8 @@ async function handleSearch() {
|
|||||||
// 有搜索条件,调用查询接口
|
// 有搜索条件,调用查询接口
|
||||||
const query: Api.SystemManage.UserManagementRelationQueryReqVO = {
|
const query: Api.SystemManage.UserManagementRelationQueryReqVO = {
|
||||||
subordinateUserId: searchParams.subordinateUserId,
|
subordinateUserId: searchParams.subordinateUserId,
|
||||||
fromUserIndex: fromUserIndex,
|
fromUserIndex,
|
||||||
deptId: deptId
|
deptId
|
||||||
};
|
};
|
||||||
await loadTreeDataByQuery(query);
|
await loadTreeDataByQuery(query);
|
||||||
} else {
|
} else {
|
||||||
@@ -188,7 +189,7 @@ function resetSearchParams() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 对话框相关状态
|
// 对话框相关状态
|
||||||
const {bool: operateVisible, setTrue: openOperateModal, setFalse: closeOperateModal} = useBoolean();
|
const { bool: operateVisible, setTrue: openOperateModal, setFalse: closeOperateModal } = useBoolean();
|
||||||
const operateType = ref<UI.TableOperateType>('add');
|
const operateType = ref<UI.TableOperateType>('add');
|
||||||
const editingData = ref<Api.SystemManage.UserManagementRelation | null>(null);
|
const editingData = ref<Api.SystemManage.UserManagementRelation | null>(null);
|
||||||
|
|
||||||
@@ -243,7 +244,7 @@ function openEdit(item: Api.SystemManage.UserManagementRelationTreeRespVO) {
|
|||||||
* @param item 要删除的关系记录
|
* @param item 要删除的关系记录
|
||||||
*/
|
*/
|
||||||
async function handleDelete(item: Api.SystemManage.UserManagementRelationTreeRespVO) {
|
async function handleDelete(item: Api.SystemManage.UserManagementRelationTreeRespVO) {
|
||||||
const {error} = await fetchDeleteUserManagementRelation(item.id);
|
const { error } = await fetchDeleteUserManagementRelation(item.id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return;
|
return;
|
||||||
@@ -263,7 +264,7 @@ async function handleBatchDelete() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {error} = await fetchBatchDeleteUserManagementRelation(checkedNodeKeys.value);
|
const { error } = await fetchBatchDeleteUserManagementRelation(checkedNodeKeys.value);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return;
|
return;
|
||||||
@@ -350,23 +351,23 @@ onMounted(async () => {
|
|||||||
<div class="flex items-center gap-10px">
|
<div class="flex items-center gap-10px">
|
||||||
<ElButton plain type="primary" @click="openAdd()">
|
<ElButton plain type="primary" @click="openAdd()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-ic-round-plus class="text-icon"/>
|
<icon-ic-round-plus class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
新增
|
新增
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<!-- <ElPopconfirm title="确认删除选中的关系吗?" @confirm="handleBatchDelete">-->
|
<!-- <ElPopconfirm title="确认删除选中的关系吗?" @confirm="handleBatchDelete">-->
|
||||||
<!-- <template #reference>-->
|
<!-- <template #reference>-->
|
||||||
<!-- <ElButton type="danger" plain :disabled="checkedNodeKeys.length === 0">-->
|
<!-- <ElButton type="danger" plain :disabled="checkedNodeKeys.length === 0">-->
|
||||||
<!-- <template #icon>-->
|
<!-- <template #icon>-->
|
||||||
<!-- <icon-ic-round-delete class="text-icon"/>-->
|
<!-- <icon-ic-round-delete class="text-icon"/>-->
|
||||||
<!-- </template>-->
|
<!-- </template>-->
|
||||||
<!-- 批量删除-->
|
<!-- 批量删除-->
|
||||||
<!-- </ElButton>-->
|
<!-- </ElButton>-->
|
||||||
<!-- </template>-->
|
<!-- </template>-->
|
||||||
<!-- </ElPopconfirm>-->
|
<!-- </ElPopconfirm>-->
|
||||||
<ElButton @click="reloadTreeData">
|
<ElButton @click="reloadTreeData">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-ic-round-refresh class="text-icon"/>
|
<icon-ic-round-refresh class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
刷新
|
刷新
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@@ -390,18 +391,18 @@ onMounted(async () => {
|
|||||||
<div class="flex flex-1 items-center justify-between">
|
<div class="flex flex-1 items-center justify-between">
|
||||||
<span class="flex items-center gap-8px">
|
<span class="flex items-center gap-8px">
|
||||||
<span>{{ node.label }}</span>
|
<span>{{ node.label }}</span>
|
||||||
<!-- <ElTag v-if="data.managerNickname" size="small" type="info">上级:{{ data.managerNickname }}</ElTag>-->
|
<!-- <ElTag v-if="data.managerNickname" size="small" type="info">上级:{{ data.managerNickname }}</ElTag>-->
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ElButton link type="primary" size="default" @click.stop="openAdd(data)">
|
<ElButton link type="primary" size="default" @click.stop="openAdd(data)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-ic-round-plus class="text-icon"/>
|
<icon-ic-round-plus class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
新增
|
新增
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton link type="primary" size="small" @click.stop="openEdit(data)">
|
<ElButton link type="primary" size="small" @click.stop="openEdit(data)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-ic-round-edit class="text-icon"/>
|
<icon-ic-round-edit class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
编辑
|
编辑
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@@ -413,7 +414,7 @@ onMounted(async () => {
|
|||||||
<template #reference>
|
<template #reference>
|
||||||
<ElButton link type="danger" size="small">
|
<ElButton link type="danger" size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-ic-round-delete class="text-icon"/>
|
<icon-ic-round-delete class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
删除
|
删除
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|||||||
@@ -284,26 +284,26 @@ watch(visible, value => {
|
|||||||
</ElRow>
|
</ElRow>
|
||||||
<ElRow :gutter="16">
|
<ElRow :gutter="16">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="生效开始时间" prop="effectiveFrom" style="width:100%">
|
<ElFormItem label="生效开始时间" prop="effectiveFrom" style="width: 100%">
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-model="model.effectiveFrom"
|
v-model="model.effectiveFrom"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
type="datetime"
|
type="datetime"
|
||||||
placeholder="请选择生效开始时间"
|
placeholder="请选择生效开始时间"
|
||||||
value-format="x"
|
value-format="x"
|
||||||
style="width:100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="生效结束时间" prop="effectiveUntil" style="width:100%">
|
<ElFormItem label="生效结束时间" prop="effectiveUntil" style="width: 100%">
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-model="model.effectiveUntil"
|
v-model="model.effectiveUntil"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
type="datetime"
|
type="datetime"
|
||||||
placeholder="请选择生效结束时间"
|
placeholder="请选择生效结束时间"
|
||||||
value-format="x"
|
value-format="x"
|
||||||
style="width:100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
@@ -319,6 +319,4 @@ watch(visible, value => {
|
|||||||
</BusinessFormDialog>
|
</BusinessFormDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
* - 用户列表通过 props 传入,由父组件统一管理
|
* - 用户列表通过 props 传入,由父组件统一管理
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {defineEmits, defineModel, defineOptions} from 'vue';
|
import { defineEmits, defineModel, defineOptions } from 'vue';
|
||||||
|
|
||||||
defineOptions({name: 'RelationSearch'});
|
defineOptions({ name: 'RelationSearch' });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件 Emits 定义
|
* 组件 Emits 定义
|
||||||
@@ -43,7 +43,7 @@ defineProps<Props>();
|
|||||||
/**
|
/**
|
||||||
* 搜索参数模型,支持双向绑定
|
* 搜索参数模型,支持双向绑定
|
||||||
*/
|
*/
|
||||||
const model = defineModel<Api.SystemManage.UserManagementRelationQueryReqVO>('model', {required: true});
|
const model = defineModel<Api.SystemManage.UserManagementRelationQueryReqVO>('model', { required: true });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置搜索
|
* 重置搜索
|
||||||
@@ -83,7 +83,7 @@ function search() {
|
|||||||
<ElCol :lg="8" :md="12" :sm="12">
|
<ElCol :lg="8" :md="12" :sm="12">
|
||||||
<ElFormItem label="用户名" prop="subordinateUserId" style="margin-left: -50px">
|
<ElFormItem label="用户名" prop="subordinateUserId" style="margin-left: -50px">
|
||||||
<ElSelect v-model="model.subordinateUserId" class="w-full" placeholder="请选择用户名" clearable filterable>
|
<ElSelect v-model="model.subordinateUserId" class="w-full" placeholder="请选择用户名" clearable filterable>
|
||||||
<ElOption v-for="user in userList" :key="user.id" :label="user.nickname" :value="user.id"/>
|
<ElOption v-for="user in userList" :key="user.id" :label="user.nickname" :value="user.id" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import {computed, nextTick, onMounted, reactive, ref, watch} from 'vue';
|
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import type {TableInstance} from 'element-plus';
|
import type { TableInstance } from 'element-plus';
|
||||||
import {ElButton, ElPopconfirm, ElSwitch, ElTag} from 'element-plus';
|
import { ElButton, ElPopconfirm, ElSwitch, ElTag } from 'element-plus';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import type {FlatResponseData} from '@sa/axios';
|
import type { FlatResponseData } from '@sa/axios';
|
||||||
import {userGenderRecord} from '@/constants/business';
|
import { userGenderRecord } from '@/constants/business';
|
||||||
import {
|
import {
|
||||||
fetchBatchDeleteUser,
|
fetchBatchDeleteUser,
|
||||||
fetchDeleteDept,
|
fetchDeleteDept,
|
||||||
@@ -17,11 +17,12 @@ import {
|
|||||||
fetchUpdateUser,
|
fetchUpdateUser,
|
||||||
fetchUpdateUserStatus
|
fetchUpdateUserStatus
|
||||||
} from '@/service/api';
|
} from '@/service/api';
|
||||||
import {useUIPaginatedTable} from '@/hooks/common/table';
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
|
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
|
||||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||||
import {$t} from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import {buildMenuTree} from '@/views/system/shared/menu-tree';
|
import { buildMenuTree } from '@/views/system/shared/menu-tree';
|
||||||
|
import UserManagementRelation from '@/views/system/user-management-relation/index.vue';
|
||||||
import UserOperateDialog from './modules/user-operate-dialog.vue';
|
import UserOperateDialog from './modules/user-operate-dialog.vue';
|
||||||
import UserOrgLeaderDialog from './modules/user-org-leader-dialog.vue';
|
import UserOrgLeaderDialog from './modules/user-org-leader-dialog.vue';
|
||||||
import UserOrgOperateDialog from './modules/user-org-operate-dialog.vue';
|
import UserOrgOperateDialog from './modules/user-org-operate-dialog.vue';
|
||||||
@@ -29,9 +30,8 @@ import UserOrgPanel from './modules/user-org-panel.vue';
|
|||||||
import UserResignedDialog from './modules/user-resigned-dialog.vue';
|
import UserResignedDialog from './modules/user-resigned-dialog.vue';
|
||||||
import UserResetPasswordDialog from './modules/user-reset-password-dialog.vue';
|
import UserResetPasswordDialog from './modules/user-reset-password-dialog.vue';
|
||||||
import UserSearch from './modules/user-search.vue';
|
import UserSearch from './modules/user-search.vue';
|
||||||
import UserManagementRelation from '@/views/system/user-management-relation/index.vue';
|
|
||||||
|
|
||||||
defineOptions({name: 'UserManage'});
|
defineOptions({ name: 'UserManage' });
|
||||||
|
|
||||||
function getInitSearchParams(): Api.SystemManage.UserSearchParams {
|
function getInitSearchParams(): Api.SystemManage.UserSearchParams {
|
||||||
return {
|
return {
|
||||||
@@ -154,7 +154,7 @@ const deptTree = computed(() => buildMenuTree(deptList.value));
|
|||||||
const currentDept = computed(() => deptList.value.find(item => item.id === currentDeptId.value) ?? null);
|
const currentDept = computed(() => deptList.value.find(item => item.id === currentDeptId.value) ?? null);
|
||||||
const deptCount = computed(() => deptList.value.length);
|
const deptCount = computed(() => deptList.value.length);
|
||||||
|
|
||||||
const {columns, columnChecks, data, loading, getDataByPage, mobilePagination} = useUIPaginatedTable<
|
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
||||||
FlatResponseData<any, Api.SystemManage.UserList>,
|
FlatResponseData<any, Api.SystemManage.UserList>,
|
||||||
Api.SystemManage.User
|
Api.SystemManage.User
|
||||||
>({
|
>({
|
||||||
@@ -178,9 +178,9 @@ const {columns, columnChecks, data, loading, getDataByPage, mobilePagination} =
|
|||||||
searchParams.pageSize = params.pageSize ?? 10;
|
searchParams.pageSize = params.pageSize ?? 10;
|
||||||
},
|
},
|
||||||
columns: () => [
|
columns: () => [
|
||||||
{prop: 'selection', type: 'selection', width: 48},
|
{ prop: 'selection', type: 'selection', width: 48 },
|
||||||
{prop: 'index', type: 'index', label: $t('common.index'), width: 64},
|
{ prop: 'index', type: 'index', label: $t('common.index'), width: 64 },
|
||||||
{prop: 'username', label: $t('page.system.user.userName'), minWidth: 140, showOverflowTooltip: true},
|
{ prop: 'username', label: $t('page.system.user.userName'), minWidth: 140, showOverflowTooltip: true },
|
||||||
{
|
{
|
||||||
prop: 'nickname',
|
prop: 'nickname',
|
||||||
label: $t('page.system.user.nickName'),
|
label: $t('page.system.user.nickName'),
|
||||||
@@ -260,9 +260,9 @@ const {columns, columnChecks, data, loading, getDataByPage, mobilePagination} =
|
|||||||
formatter: row => {
|
formatter: row => {
|
||||||
const state = getUserResignedState(row);
|
const state = getUserResignedState(row);
|
||||||
const stateMap: Record<UserResignedState, { type: UI.ThemeColor; label: App.I18n.I18nKey }> = {
|
const stateMap: Record<UserResignedState, { type: UI.ThemeColor; label: App.I18n.I18nKey }> = {
|
||||||
active: {type: 'success', label: 'page.system.user.resignedStateEnum.active'},
|
active: { type: 'success', label: 'page.system.user.resignedStateEnum.active' },
|
||||||
pending: {type: 'warning', label: 'page.system.user.resignedStateEnum.pending'},
|
pending: { type: 'warning', label: 'page.system.user.resignedStateEnum.pending' },
|
||||||
resigned: {type: 'info', label: 'page.system.user.resignedStateEnum.resigned'}
|
resigned: { type: 'info', label: 'page.system.user.resignedStateEnum.resigned' }
|
||||||
};
|
};
|
||||||
|
|
||||||
return <ElTag type={stateMap[state].type}>{$t(stateMap[state].label)}</ElTag>;
|
return <ElTag type={stateMap[state].type}>{$t(stateMap[state].label)}</ElTag>;
|
||||||
@@ -323,7 +323,7 @@ const {columns, columnChecks, data, loading, getDataByPage, mobilePagination} =
|
|||||||
async function loadDeptTree() {
|
async function loadDeptTree() {
|
||||||
deptLoading.value = true;
|
deptLoading.value = true;
|
||||||
|
|
||||||
const {error, data: deptItems} = await fetchGetDeptList({
|
const { error, data: deptItems } = await fetchGetDeptList({
|
||||||
status: 0
|
status: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -425,7 +425,7 @@ function openOrgLeader(row: Api.SystemManage.Dept) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteDeptAction(row: Api.SystemManage.Dept) {
|
async function handleDeleteDeptAction(row: Api.SystemManage.Dept) {
|
||||||
const {error} = await fetchDeleteDept(row.id);
|
const { error } = await fetchDeleteDept(row.id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return;
|
return;
|
||||||
@@ -450,7 +450,7 @@ async function handleDeleteAction(row: Api.SystemManage.User) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {error} = await fetchDeleteUser(row.id);
|
const { error } = await fetchDeleteUser(row.id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return;
|
return;
|
||||||
@@ -469,7 +469,7 @@ async function updateUserResignedAt(userId: number, value: number | null) {
|
|||||||
|
|
||||||
const user = detailResult.data;
|
const user = detailResult.data;
|
||||||
|
|
||||||
const {error} = await fetchUpdateUser({
|
const { error } = await fetchUpdateUser({
|
||||||
id: userId,
|
id: userId,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
nickname: user.nickname ?? null,
|
nickname: user.nickname ?? null,
|
||||||
@@ -521,7 +521,7 @@ async function handleBatchDelete() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {error} = await fetchBatchDeleteUser(userCheckedRowKeys.value);
|
const { error } = await fetchBatchDeleteUser(userCheckedRowKeys.value);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return;
|
return;
|
||||||
@@ -534,7 +534,7 @@ async function handleBatchDelete() {
|
|||||||
async function handleToggleStatus(row: Api.SystemManage.User, enabled: boolean) {
|
async function handleToggleStatus(row: Api.SystemManage.User, enabled: boolean) {
|
||||||
statusLoadingIds.value = [...statusLoadingIds.value, row.id];
|
statusLoadingIds.value = [...statusLoadingIds.value, row.id];
|
||||||
|
|
||||||
const {error} = await fetchUpdateUserStatus({
|
const { error } = await fetchUpdateUserStatus({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
status: enabled ? 0 : 1
|
status: enabled ? 0 : 1
|
||||||
});
|
});
|
||||||
@@ -643,13 +643,13 @@ onMounted(async () => {
|
|||||||
<template #default>
|
<template #default>
|
||||||
<ElButton plain type="primary" :disabled="!currentDept" @click="openAdd">
|
<ElButton plain type="primary" :disabled="!currentDept" @click="openAdd">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-ic-round-plus class="text-icon"/>
|
<icon-ic-round-plus class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
{{ $t('common.add') }}
|
{{ $t('common.add') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton plain type="primary" :disabled="!currentDept" @click="userManagementRelationVisible = true">
|
<ElButton plain type="primary" :disabled="!currentDept" @click="userManagementRelationVisible = true">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-ic-round-plus class="text-icon"/>
|
<icon-ic-round-plus class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
带人关系
|
带人关系
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@@ -657,7 +657,7 @@ onMounted(async () => {
|
|||||||
<template #reference>
|
<template #reference>
|
||||||
<ElButton type="danger" plain :disabled="userCheckedRowKeys.length === 0">
|
<ElButton type="danger" plain :disabled="userCheckedRowKeys.length === 0">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-ic-round-delete class="text-icon"/>
|
<icon-ic-round-delete class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
{{ $t('common.batchDelete') }}
|
{{ $t('common.batchDelete') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@@ -679,7 +679,7 @@ onMounted(async () => {
|
|||||||
:data="data"
|
:data="data"
|
||||||
@selection-change="handleUserSelectionChange"
|
@selection-change="handleUserSelectionChange"
|
||||||
>
|
>
|
||||||
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col"/>
|
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
|
||||||
</ElTable>
|
</ElTable>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-20px flex justify-end">
|
<div class="mt-20px flex justify-end">
|
||||||
@@ -694,7 +694,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-else class="h-full flex items-center justify-center">
|
<div v-else class="h-full flex items-center justify-center">
|
||||||
<ElEmpty :description="$t('page.system.user.emptyOrg')"/>
|
<ElEmpty :description="$t('page.system.user.emptyOrg')" />
|
||||||
</div>
|
</div>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
@@ -733,7 +733,7 @@ onMounted(async () => {
|
|||||||
@submitted="handleDeptSubmitted"
|
@submitted="handleDeptSubmitted"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UserOrgLeaderDialog v-model:visible="orgLeaderVisible" :dept="leaderDeptData"/>
|
<UserOrgLeaderDialog v-model:visible="orgLeaderVisible" :dept="leaderDeptData" />
|
||||||
|
|
||||||
<BusinessFormDialog
|
<BusinessFormDialog
|
||||||
v-model="userManagementRelationVisible"
|
v-model="userManagementRelationVisible"
|
||||||
@@ -742,7 +742,7 @@ onMounted(async () => {
|
|||||||
:show-footer="false"
|
:show-footer="false"
|
||||||
max-body-height="70vh"
|
max-body-height="70vh"
|
||||||
>
|
>
|
||||||
<UserManagementRelation :fromUserIndex="true" :deptId="currentDeptId"/>
|
<UserManagementRelation :from-user-index="true" :dept-id="currentDeptId" />
|
||||||
</BusinessFormDialog>
|
</BusinessFormDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user