mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-15 16:28:33 +08:00
Compare commits
No commits in common. "6.X-Vue" and "v6.0.0-BETA-Vue" have entirely different histories.
6.X-Vue
...
v6.0.0-BET
@ -1,13 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: frontend-api-types
|
name: frontend-api-types
|
||||||
description: 前端 API 与类型定义专家。用于 plus-ui 前端项目中的 src/api 层、types.ts、返回结构、Query/Form/VO/InfoVO 定义,以及前后端接口映射任务;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
description: 前端 API 与类型定义专家。用于当前项目中的 src/api 层、types.ts、返回结构、Query/Form/VO/InfoVO 定义,以及前后端接口映射任务。
|
||||||
---
|
---
|
||||||
|
|
||||||
你负责 plus-ui 前端项目中的 API 层和类型定义。
|
你负责当前前端项目中的 API 层和类型定义。
|
||||||
|
|
||||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
|
||||||
默认分支:`6.X-Vue`
|
|
||||||
远端引用必须同时标记仓库、分支和文件路径;不要写本机绝对路径。
|
|
||||||
|
|
||||||
## 核心原则
|
## 核心原则
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: frontend-crud-coding
|
name: frontend-crud-coding
|
||||||
description: 前端总入口。用于 plus-ui 前端项目中的标准 CRUD 页面、新增 API/types、复杂列表页增强、树筛选、导入导出、权限按钮与弹窗表单等任务,并根据任务类型选择合适的前端子 agent;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
description: 前端总入口。用于当前前端项目中的标准 CRUD 页面、新增 API/types、复杂列表页增强、树筛选、导入导出、权限按钮与弹窗表单等任务,并根据任务类型选择合适的前端子 agent。
|
||||||
---
|
---
|
||||||
|
|
||||||
你是 plus-ui 前端项目的总入口 agent。
|
你是当前前端项目的总入口 agent。
|
||||||
|
|
||||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
|
||||||
默认分支:`6.X-Vue`
|
|
||||||
远端引用必须同时标记仓库、分支和文件路径;不要在 agent 文档中写本机绝对路径。
|
|
||||||
|
|
||||||
先判断任务类型,再按下面规则处理:
|
先判断任务类型,再按下面规则处理:
|
||||||
|
|
||||||
@ -18,5 +14,5 @@ description: 前端总入口。用于 plus-ui 前端项目中的标准 CRUD 页
|
|||||||
通用要求:
|
通用要求:
|
||||||
|
|
||||||
- 先读当前目录下最近似页面和 API,再动代码。
|
- 先读当前目录下最近似页面和 API,再动代码。
|
||||||
- 冲突时优先相信当前项目真实页面,其次是公共组件和工具,再其次才是 plus-ui Gitee 仓库 `6.X-Vue` 分支的 `gen` 模板。
|
- 冲突时优先相信当前项目真实页面,其次是公共组件和工具,再其次才是关联后端工程的 generator 模板。
|
||||||
- 默认直接产出可落地代码,而不是只给抽象建议。
|
- 默认直接产出可落地代码,而不是只给抽象建议。
|
||||||
@ -1,18 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: frontend-crud-page
|
name: frontend-crud-page
|
||||||
description: 前端标准 CRUD 页面专家。用于 plus-ui 前端项目中的新建列表页、弹窗表单页、标准 API/types/index.vue 骨架,以及 gen 模板到项目风格的落地任务;默认参考 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
description: 前端标准 CRUD 页面专家。用于当前项目中的新建列表页、弹窗表单页、标准 API/types/index.vue 骨架,以及 gen 模板到项目风格的落地任务。
|
||||||
---
|
---
|
||||||
|
|
||||||
你负责 plus-ui 前端项目中的标准 CRUD 页面实现。
|
你负责当前前端项目中的标准 CRUD 页面实现。
|
||||||
|
|
||||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
|
||||||
默认分支:`6.X-Vue`
|
|
||||||
远端模板引用必须同时标记仓库、分支和路径,例如 `branch=6.X-Vue, path=gen/index.vue.ftl`;不要写本机绝对路径。
|
|
||||||
|
|
||||||
## 核心原则
|
## 核心原则
|
||||||
|
|
||||||
1. 先看当前模块最近似页面。
|
1. 先看当前模块最近似页面。
|
||||||
2. 再参考 plus-ui Gitee 仓库 `6.X-Vue` 分支中的 `gen` 模板。
|
2. 再参考关联后端工程中的 generator 模板。
|
||||||
3. 默认同时维护:
|
3. 默认同时维护:
|
||||||
`src/api/<module>/<business>/index.ts`
|
`src/api/<module>/<business>/index.ts`
|
||||||
`src/api/<module>/<business>/types.ts`
|
`src/api/<module>/<business>/types.ts`
|
||||||
|
|||||||
@ -1,13 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: frontend-page-enhancement
|
name: frontend-page-enhancement
|
||||||
description: 复杂前端页面增强专家。用于修改 plus-ui 前端项目中已经存在的列表页、树筛选页、带导入导出和更多菜单的页面,强调增量修改和保留现有交互能力;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
description: 复杂前端页面增强专家。用于修改当前项目中已经存在的列表页、树筛选页、带导入导出和更多菜单的页面,强调增量修改和保留现有交互能力。
|
||||||
---
|
---
|
||||||
|
|
||||||
你负责 plus-ui 前端项目中已有页面的增强,不是重写页面。
|
你负责当前前端项目中已有页面的增强,不是重写页面。
|
||||||
|
|
||||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
|
||||||
默认分支:`6.X-Vue`
|
|
||||||
远端引用必须同时标记仓库、分支和文件路径;不要写本机绝对路径。
|
|
||||||
|
|
||||||
## 核心原则
|
## 核心原则
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: frontend-crud-coding
|
name: frontend-crud-coding
|
||||||
description: 在 plus-ui 前端项目中按真实 Vue 3 + TypeScript + Element Plus + oxlint/oxfmt 代码风格生成或修改页面、API、types、hooks 接入和样式壳。用于新增或修改标准 CRUD 列表页、树表页、系统管理页、监控页、workflow 页面、demo 页面,补齐与后端接口对应的 src/api、types 和 src/views 代码;默认参考 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支,触发后应先读取适用 references,再阅读目标模块真实代码和项目内 gen 代码生成模板。
|
description: 在当前 plus-ui-new 前端项目中按真实 Vue 3 + TypeScript + Element Plus + oxlint/oxfmt 代码风格生成或修改页面、API、types、hooks 接入和样式壳。用于新增或修改标准 CRUD 列表页、树表页、系统管理页、监控页、workflow 页面、demo 页面,补齐与后端接口对应的 src/api、types 和 src/views 代码;触发后应先读取适用 references,再阅读目标模块真实代码和项目内 gen 代码生成模板。
|
||||||
---
|
---
|
||||||
|
|
||||||
# 前端编码规范
|
# 前端编码规范
|
||||||
|
|
||||||
先对齐当前前端项目里的真实实现,再参考项目内 `gen` 目录下的代码生成模板。不要只套通用 Vue 模板,也不要把 generator 模板原样复制进来而忽略当前项目已经演进出的 hooks、页面壳、类型入口和下载方式。
|
先对齐当前前端项目里的真实实现,再参考项目内 `gen` 目录下的代码生成模板。不要只套通用 Vue 模板,也不要把 generator 模板原样复制进来而忽略当前项目已经演进出的 hooks、页面壳、类型入口和下载方式。
|
||||||
|
|
||||||
## 项目基线
|
|
||||||
|
|
||||||
- 基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
|
||||||
- 默认分支:`6.X-Vue`
|
|
||||||
- 远端引用必须同时标记仓库、分支和文件路径,例如 `https://gitee.com/JavaLionLi/plus-ui/blob/6.X-Vue/gen/index.vue.ftl` 或 `branch=6.X-Vue, path=gen/index.vue.ftl`。
|
|
||||||
- 不要在 skill、reference 或 agent 文档中写本机绝对路径;本地文件读取以运行时工作目录为准。
|
|
||||||
- 如果用户指定其他分支,先按用户分支读取同一仓库的对应文件,并在结果中说明使用的分支。
|
|
||||||
|
|
||||||
## 执行流程
|
## 执行流程
|
||||||
|
|
||||||
1. 判断任务类型:新增标准 CRUD、树表、已有页面增强、复杂业务页、只补 API/types。
|
1. 判断任务类型:新增标准 CRUD、树表、已有页面增强、复杂业务页、只补 API/types。
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
interface:
|
interface:
|
||||||
display_name: "前端编码"
|
display_name: "前端编码"
|
||||||
short_description: "按 plus-ui 6.X-Vue 真实代码和 gen 模板编写前端 CRUD"
|
short_description: "按 plus-ui-new 真实代码和 gen 模板编写前端 CRUD"
|
||||||
default_prompt: "使用 $frontend-crud-coding 先读取适用 reference,再按 plus-ui Gitee 仓库 6.X-Vue 分支的真实页面、hooks、API/types 约定和项目内 gen 模板实现前端修改。"
|
default_prompt: "使用 $frontend-crud-coding 先读取适用 reference,再按当前 plus-ui-new 真实页面、hooks、API/types 约定和项目内 gen 模板实现前端修改。"
|
||||||
|
|
||||||
policy:
|
policy:
|
||||||
allow_implicit_invocation: true
|
allow_implicit_invocation: true
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
```text
|
```text
|
||||||
使用 $frontend-crud-coding 为 system/client 补一套前端 CRUD 页面。
|
使用 $frontend-crud-coding 为 system/client 补一套前端 CRUD 页面。
|
||||||
后端接口已经有 /system/client/list、/system/client/{id}、POST /system/client、PUT /system/client、DELETE /system/client/{ids}。
|
后端接口已经有 /system/client/list、/system/client/{id}、POST /system/client、PUT /system/client、DELETE /system/client/{ids}。
|
||||||
请参考 plus-ui Gitee 仓库 6.X-Vue 分支的项目内 gen 模板、src/views/demo/demo/index.vue 和现有 system/client 风格实现。
|
请参考项目内 gen 模板、src/views/demo/demo/index.vue 和现有 system/client 风格实现。
|
||||||
```
|
```
|
||||||
|
|
||||||
### 期望执行方式
|
### 期望执行方式
|
||||||
@ -107,7 +107,7 @@
|
|||||||
2. 新增状态筛选和导出
|
2. 新增状态筛选和导出
|
||||||
3. API 路径沿用后端接口
|
3. API 路径沿用后端接口
|
||||||
4. 参考 system/config 的工具栏与导出交互
|
4. 参考 system/config 的工具栏与导出交互
|
||||||
5. 参考 plus-ui Gitee 仓库 6.X-Vue 分支的 gen 模板补齐缺失 types
|
5. 参考 gen 模板补齐缺失 types
|
||||||
```
|
```
|
||||||
|
|
||||||
## 不推荐的任务描述
|
## 不推荐的任务描述
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
## 优先参考的代码来源
|
## 优先参考的代码来源
|
||||||
|
|
||||||
- 默认远端基线:`https://gitee.com/JavaLionLi/plus-ui`,分支 `6.X-Vue`。引用远端文件时必须同时写明 `branch=6.X-Vue` 和仓库内路径。
|
|
||||||
- 当前目标目录下最近似页面、API、types。
|
- 当前目标目录下最近似页面、API、types。
|
||||||
- 标准单表:`src/views/demo/demo/index.vue`、`src/api/demo/demo/index.ts`、`src/api/demo/demo/types.ts`。
|
- 标准单表:`src/views/demo/demo/index.vue`、`src/api/demo/demo/index.ts`、`src/api/demo/demo/types.ts`。
|
||||||
- 树表:`src/views/demo/tree/index.vue`、`src/views/workflow/category/index.vue`。
|
- 树表:`src/views/demo/tree/index.vue`、`src/views/workflow/category/index.vue`。
|
||||||
@ -23,7 +22,6 @@
|
|||||||
- `.editorconfig` 要求 UTF-8、LF、2 空格缩进。
|
- `.editorconfig` 要求 UTF-8、LF、2 空格缩进。
|
||||||
- 当前仓库没有 `.prettierrc`;格式化使用 `pnpm run fmt`,lint 使用 `pnpm lint`。
|
- 当前仓库没有 `.prettierrc`;格式化使用 `pnpm run fmt`,lint 使用 `pnpm lint`。
|
||||||
- 不要在一个页面里混入与仓库不一致的格式和写法。
|
- 不要在一个页面里混入与仓库不一致的格式和写法。
|
||||||
- 不要把本机绝对路径写进 skill 输出或参考文档;跨环境参考统一使用 Gitee 仓库 URL、分支和仓库内相对路径。
|
|
||||||
|
|
||||||
## API 文件规则
|
## API 文件规则
|
||||||
|
|
||||||
@ -129,8 +127,7 @@
|
|||||||
|
|
||||||
## 与 gen 模板的关系
|
## 与 gen 模板的关系
|
||||||
|
|
||||||
- `gen` 是 plus-ui 前端项目内的生成模板;默认远端位置是 `https://gitee.com/JavaLionLi/plus-ui/tree/6.X-Vue/gen`,分支必须标记为 `6.X-Vue`。
|
- `gen` 是当前前端项目内的生成模板,优先于外部后端工程拷贝的模板。
|
||||||
- 本地仓库存在对应文件时优先读本地 `gen`,但不要把本地绝对路径写入 skill 或交付内容。
|
|
||||||
- 新增标准单表页面时读取 `gen/index.vue.ftl`、`gen/api.ts.ftl`、`gen/types.ts.ftl`。
|
- 新增标准单表页面时读取 `gen/index.vue.ftl`、`gen/api.ts.ftl`、`gen/types.ts.ftl`。
|
||||||
- 新增树表页面时读取 `gen/index-tree.vue.ftl`、`gen/api.ts.ftl`、`gen/types.ts.ftl`。
|
- 新增树表页面时读取 `gen/index-tree.vue.ftl`、`gen/api.ts.ftl`、`gen/types.ts.ftl`。
|
||||||
- `gen` 模板是标准骨架,不是最终答案;落地时仍要对照目标模块真实页面和公共 hooks。
|
- `gen` 模板是标准骨架,不是最终答案;落地时仍要对照目标模块真实页面和公共 hooks。
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
## 平台简介
|
## 平台简介
|
||||||
|
|
||||||
- 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
|
- 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
|
||||||
- 官方项目: 基于 React + Ant Design 版本前端项目 [plus-ui-react](https://gitee.com/JavaLionLi/plus-ui/tree/6.X-React/)
|
- 官方项目: 基于 React + Ant Design 版本前端项目 [plus-ui-react](https://gitee.com/JavaLionLi/plus-ui/tree/6.X-react/)
|
||||||
- 成员项目: 基于 vben5(ant-design-vue) 的前端项目 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)
|
- 成员项目: 基于 vben5(ant-design-vue) 的前端项目 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)
|
||||||
- 成员项目: 基于soybean 的前端项目 [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)
|
- 成员项目: 基于soybean 的前端项目 [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
|
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
|
||||||
<#if !table.tree>
|
|
||||||
import type { PageResult } from '@/api/types';
|
import type { PageResult } from '@/api/types';
|
||||||
</#if>
|
|
||||||
import type { AxiosPromise } from '@/utils/api-types';
|
import type { AxiosPromise } from '@/utils/api-types';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
|
|
||||||
@ -11,7 +9,7 @@ import request from '@/utils/request';
|
|||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const list${BusinessName} = (query?: ${BusinessName}Query): AxiosPromise<<#if table.tree>${BusinessName}VO[]<#else>PageResult<${BusinessName}VO></#if>> => {
|
export const list${BusinessName} = (query?: ${BusinessName}Query): AxiosPromise<PageResult<${BusinessName}VO>> => {
|
||||||
return request({
|
return request({
|
||||||
url: '/${moduleName}/${businessName}/list',
|
url: '/${moduleName}/${businessName}/list',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -102,3 +100,4 @@ export const del${BusinessName} = (${pkColumn.javaField}: string | number | Arra
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,7 @@
|
|||||||
</#if>
|
</#if>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && !(column.dictType?has_content)>
|
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && column.dictType?has_content>
|
||||||
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
||||||
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
|
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
|
||||||
<el-option label="请选择字典生成" value="" />
|
<el-option label="请选择字典生成" value="" />
|
||||||
@ -312,7 +312,7 @@
|
|||||||
></el-option>
|
></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<#elseif column.htmlType == "select" && !(column.dictType?has_content)>
|
<#elseif column.htmlType == "select" && column.dictType?has_content>
|
||||||
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
||||||
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
|
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
|
||||||
<el-option label="请选择字典生成" value="" />
|
<el-option label="请选择字典生成" value="" />
|
||||||
@ -329,7 +329,7 @@
|
|||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<#elseif column.htmlType == "checkbox" && !(column.dictType?has_content)>
|
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
|
||||||
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
||||||
<el-checkbox-group v-model="form.${column.javaField}">
|
<el-checkbox-group v-model="form.${column.javaField}">
|
||||||
<el-checkbox>请选择字典生成</el-checkbox>
|
<el-checkbox>请选择字典生成</el-checkbox>
|
||||||
@ -349,7 +349,7 @@
|
|||||||
>{{dict.label}}</el-radio>
|
>{{dict.label}}</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<#elseif column.htmlType == "radio" && !(column.dictType?has_content)>
|
<#elseif column.htmlType == "radio" && column.dictType?has_content>
|
||||||
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
||||||
<el-radio-group v-model="form.${column.javaField}">
|
<el-radio-group v-model="form.${column.javaField}">
|
||||||
<el-radio value="1">请选择字典生成</el-radio>
|
<el-radio value="1">请选择字典生成</el-radio>
|
||||||
@ -689,3 +689,4 @@ onMounted(() => {
|
|||||||
getList();
|
getList();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,7 @@
|
|||||||
</#if>
|
</#if>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && !(column.dictType?has_content)>
|
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && column.dictType?has_content>
|
||||||
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
||||||
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
|
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
|
||||||
<el-option label="请选择字典生成" value="" />
|
<el-option label="请选择字典生成" value="" />
|
||||||
@ -60,7 +60,7 @@
|
|||||||
placeholder="请选择${column.columnLabel}"
|
placeholder="请选择${column.columnLabel}"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<#elseif column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
|
<#elseif column.htmlType == "datetime" && column.queryType == "BETWEEN">
|
||||||
<el-form-item label="${column.columnLabel}" style="width: 308px">
|
<el-form-item label="${column.columnLabel}" style="width: 308px">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="dateRange${column.capJavaField}"
|
v-model="dateRange${column.capJavaField}"
|
||||||
@ -233,8 +233,7 @@
|
|||||||
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
|
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
|
||||||
<el-form ref="${businessName}FormRef" :model="form" :rules="rules" label-width="80px">
|
<el-form ref="${businessName}FormRef" :model="form" :rules="rules" label-width="80px">
|
||||||
<#list columns as column>
|
<#list columns as column>
|
||||||
<#if (column.insert || column.edit) && !column.pk>
|
<#if (column.insert || column.edit) && !column.pk>`n<#if column.htmlType == "input">
|
||||||
<#if column.htmlType == "input">
|
|
||||||
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
||||||
<el-input v-model="form.${column.javaField}" placeholder="请输入${column.columnLabel}" />
|
<el-input v-model="form.${column.javaField}" placeholder="请输入${column.columnLabel}" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -269,7 +268,7 @@
|
|||||||
></el-option>
|
></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<#elseif column.htmlType == "select" && !(column.dictType?has_content)>
|
<#elseif column.htmlType == "select" && column.dictType?has_content>
|
||||||
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
||||||
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
|
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
|
||||||
<el-option label="请选择字典生成" value="" />
|
<el-option label="请选择字典生成" value="" />
|
||||||
@ -286,7 +285,7 @@
|
|||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<#elseif column.htmlType == "checkbox" && !(column.dictType?has_content)>
|
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
|
||||||
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
||||||
<el-checkbox-group v-model="form.${column.javaField}">
|
<el-checkbox-group v-model="form.${column.javaField}">
|
||||||
<el-checkbox>请选择字典生成</el-checkbox>
|
<el-checkbox>请选择字典生成</el-checkbox>
|
||||||
@ -306,7 +305,7 @@
|
|||||||
>{{dict.label}}</el-radio>
|
>{{dict.label}}</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<#elseif column.htmlType == "radio" && !(column.dictType?has_content)>
|
<#elseif column.htmlType == "radio" && column.dictType?has_content>
|
||||||
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
||||||
<el-radio-group v-model="form.${column.javaField}">
|
<el-radio-group v-model="form.${column.javaField}">
|
||||||
<el-radio value="1">请选择字典生成</el-radio>
|
<el-radio value="1">请选择字典生成</el-radio>
|
||||||
@ -381,9 +380,6 @@ import { useTableSelection } from '@/hooks/table/useTableSelection';
|
|||||||
<#if needDict>
|
<#if needDict>
|
||||||
import { useDict } from '@/utils/dict';
|
import { useDict } from '@/utils/dict';
|
||||||
</#if>
|
</#if>
|
||||||
<#if needParseTime>
|
|
||||||
import { parseTime } from '@/utils/ruoyi';
|
|
||||||
</#if>
|
|
||||||
import modal from '@/plugins/modal';
|
import modal from '@/plugins/modal';
|
||||||
<#if enableExport>
|
<#if enableExport>
|
||||||
import { download as requestDownload } from '@/utils/request';
|
import { download as requestDownload } from '@/utils/request';
|
||||||
@ -404,7 +400,7 @@ const { loading, withLoading } = useLoading(true);
|
|||||||
const { showSearch } = useSearchToggle();
|
const { showSearch } = useSearchToggle();
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
<#list columns as column>
|
<#list columns as column>
|
||||||
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
|
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
|
||||||
const {
|
const {
|
||||||
dateRange: dateRange${column.capJavaField},
|
dateRange: dateRange${column.capJavaField},
|
||||||
applyDateRange: apply${column.capJavaField}DateRange,
|
applyDateRange: apply${column.capJavaField}DateRange,
|
||||||
@ -476,7 +472,7 @@ const getList = async () => {
|
|||||||
<#if needAddDateRange>
|
<#if needAddDateRange>
|
||||||
let params = queryParams.value;
|
let params = queryParams.value;
|
||||||
<#list columns as column>
|
<#list columns as column>
|
||||||
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
|
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
|
||||||
params = apply${column.capJavaField}DateRange(params);
|
params = apply${column.capJavaField}DateRange(params);
|
||||||
</#if>
|
</#if>
|
||||||
</#list>
|
</#list>
|
||||||
@ -509,7 +505,7 @@ const { resetQuery } = useSearchReset({
|
|||||||
initialPageSize: 10,
|
initialPageSize: 10,
|
||||||
resetExtras: () => {
|
resetExtras: () => {
|
||||||
<#list columns as column>
|
<#list columns as column>
|
||||||
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
|
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
|
||||||
reset${column.capJavaField}DateRange();
|
reset${column.capJavaField}DateRange();
|
||||||
</#if>
|
</#if>
|
||||||
</#list>
|
</#list>
|
||||||
@ -569,8 +565,8 @@ const handleDelete = async (row?: Partial<${BusinessName}VO>) => {
|
|||||||
await getList();
|
await getList();
|
||||||
};
|
};
|
||||||
|
|
||||||
<#if enableExport>
|
|
||||||
/** 导出按钮操作 */
|
/** 导出按钮操作 */
|
||||||
|
<#if enableExport>
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
requestDownload(
|
requestDownload(
|
||||||
'${moduleName}/${businessName}/export',
|
'${moduleName}/${businessName}/export',
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import type { BaseEntity<#if !table.tree>, PageQuery</#if> } from '@/api/types';
|
||||||
|
|
||||||
export interface ${BusinessName}VO {
|
export interface ${BusinessName}VO {
|
||||||
<#list columns as column>
|
<#list columns as column>
|
||||||
<#if column.list>
|
<#if column.list>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<title>%VITE_APP_TITLE%</title>
|
<title>%VITE_APP_TITLE%</title>
|
||||||
<!--[if lt IE 11]>
|
<!--[if lt IE 11]>
|
||||||
<script>
|
<script>
|
||||||
window.location.href = '/ie.html';
|
window.location.href = '/html/ie.html';
|
||||||
</script>
|
</script>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
67
package.json
67
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package",
|
"$schema": "https://json.schemastore.org/package",
|
||||||
"name": "ruoyi-vue-plus",
|
"name": "ruoyi-vue-plus",
|
||||||
"version": "6.0.0",
|
"version": "6.0.0-BETA",
|
||||||
"description": "RuoYi-Vue-Plus后台管理系统",
|
"description": "RuoYi-Vue-Plus后台管理系统",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "LionLi",
|
"author": "LionLi",
|
||||||
@ -9,10 +9,8 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitee.com/JavaLionLi/plus-ui.git"
|
"url": "https://gitee.com/JavaLionLi/plus-ui.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.34.5",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build --mode production",
|
|
||||||
"build:dev": "vite build --mode development",
|
"build:dev": "vite build --mode development",
|
||||||
"build:prod": "vite build --mode production",
|
"build:prod": "vite build --mode production",
|
||||||
"dev": "vite serve --mode development",
|
"dev": "vite serve --mode development",
|
||||||
@ -25,49 +23,49 @@
|
|||||||
"@element-plus/icons-vue": "2.3.2",
|
"@element-plus/icons-vue": "2.3.2",
|
||||||
"@highlightjs/vue-plugin": "2.1.2",
|
"@highlightjs/vue-plugin": "2.1.2",
|
||||||
"@iconify/vue": "^5.0.1",
|
"@iconify/vue": "^5.0.1",
|
||||||
"@noble/ciphers": "^2.4.0",
|
"@vueuse/core": "14.3.0",
|
||||||
"@vueuse/core": "14.4.0",
|
"@wangeditor-next/editor": "5.7.10",
|
||||||
"@wangeditor-next/editor": "6.4.1",
|
"@wangeditor-next/editor-for-vue": "5.1.14",
|
||||||
"@wangeditor-next/editor-for-vue": "6.4.1",
|
|
||||||
"animate.css": "4.1.1",
|
"animate.css": "4.1.1",
|
||||||
"await-to-js": "3.0.0",
|
"await-to-js": "3.0.0",
|
||||||
"axios": "1.20.0",
|
"axios": "1.17.0",
|
||||||
|
"crypto-js": "4.2.0",
|
||||||
"echarts": "6.1.0",
|
"echarts": "6.1.0",
|
||||||
"element-plus": "2.14.5",
|
"element-plus": "2.14.1",
|
||||||
"highlight.js": "11.12.0",
|
"highlight.js": "11.11.1",
|
||||||
"image-conversion": "2.1.1",
|
"image-conversion": "2.1.1",
|
||||||
"jsencrypt": "3.5.4",
|
"jsencrypt": "3.5.4",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"pinia": "4.0.3",
|
"pinia": "3.0.4",
|
||||||
"vue": "3.5.42",
|
"vue": "3.5.35",
|
||||||
"vue-cropper": "1.1.4",
|
"vue-cropper": "1.1.4",
|
||||||
"vue-i18n": "11.4.10",
|
"vue-i18n": "11.4.5",
|
||||||
"vue-json-pretty": "2.6.0",
|
"vue-json-pretty": "2.6.0",
|
||||||
"vue-router": "5.3.1",
|
"vue-router": "5.1.0",
|
||||||
"vue-types": "7.0.0",
|
"vue-types": "7.0.0",
|
||||||
"vxe-pc-ui": "4.18.9",
|
"vxe-table": "4.19.10"
|
||||||
"vxe-table": "4.21.9"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^26.1.1",
|
"@types/crypto-js": "4.2.2",
|
||||||
|
"@types/node": "^25.9.2",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@unocss/preset-attributify": "66.10.1",
|
"@unocss/preset-attributify": "66.7.0",
|
||||||
"@unocss/preset-wind3": "66.10.1",
|
"@unocss/preset-wind3": "66.7.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.8",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"@vue/compiler-sfc": "3.5.42",
|
"@vue/compiler-sfc": "3.5.35",
|
||||||
"autoprefixer": "10.5.5",
|
"autoprefixer": "10.5.0",
|
||||||
"oxfmt": "^0.67.0",
|
"oxfmt": "^0.53.0",
|
||||||
"oxlint": "^1.82.0",
|
"oxlint": "^1.68.0",
|
||||||
"sass": "1.104.0",
|
"sass": "1.100.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"unocss": "66.10.1",
|
"unocss": "66.7.0",
|
||||||
"unplugin-auto-import": "21.1.0",
|
"unplugin-auto-import": "21.0.0",
|
||||||
"unplugin-vue-components": "32.1.0",
|
"unplugin-vue-components": "32.1.0",
|
||||||
"unplugin-vue-setup-extend-plus": "1.0.1",
|
"unplugin-vue-setup-extend-plus": "1.0.1",
|
||||||
"vite": "^8.2.2",
|
"vite": "^8.0.16",
|
||||||
"vite-plugin-svg-icons-ng": "^1.9.3",
|
"vite-plugin-svg-icons-ng": "^1.9.1",
|
||||||
"vitest": "4.1.11",
|
"vitest": "4.1.8",
|
||||||
"vue-tsc": "^3.3.11"
|
"vue-tsc": "^3.3.4"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"Chrome >= 87",
|
"Chrome >= 87",
|
||||||
@ -78,5 +76,12 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0",
|
"node": ">=20.19.0",
|
||||||
"pnpm": ">=10.0.0"
|
"pnpm": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@parcel/watcher",
|
||||||
|
"es5-ext",
|
||||||
|
"esbuild"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3450
pnpm-lock.yaml
generated
3450
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,10 +0,0 @@
|
|||||||
packages:
|
|
||||||
- '.'
|
|
||||||
|
|
||||||
allowBuilds:
|
|
||||||
'@parcel/watcher': true
|
|
||||||
es5-ext: true
|
|
||||||
esbuild: true
|
|
||||||
|
|
||||||
overrides:
|
|
||||||
esbuild: 0.28.1
|
|
||||||
@ -36,8 +36,7 @@ export interface UserVO extends BaseEntity {
|
|||||||
email: string;
|
email: string;
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
gender: string;
|
gender: string;
|
||||||
avatar?: string | number;
|
avatar: string;
|
||||||
avatarUrl?: string;
|
|
||||||
status: string;
|
status: string;
|
||||||
delFlag: string;
|
delFlag: string;
|
||||||
loginIp: string;
|
loginIp: string;
|
||||||
@ -68,7 +67,6 @@ export interface UserForm {
|
|||||||
gender?: string;
|
gender?: string;
|
||||||
status: string;
|
status: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
avatar?: string | number;
|
|
||||||
postIds: string[];
|
postIds: string[];
|
||||||
roleIds: string[];
|
roleIds: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,11 +77,11 @@
|
|||||||
.query-form .el-input-number,
|
.query-form .el-input-number,
|
||||||
.query-form .el-input-number__decrease,
|
.query-form .el-input-number__decrease,
|
||||||
.query-form .el-input-number__increase {
|
.query-form .el-input-number__increase {
|
||||||
border-radius: var(--app-radius-md) !important;
|
border-radius: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-form .el-button {
|
.query-form .el-button {
|
||||||
border-radius: var(--app-radius-md) !important;
|
border-radius: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .query-form .el-button--primary {
|
html.dark .query-form .el-button--primary {
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
height: 28px !important;
|
height: 28px !important;
|
||||||
min-width: 28px !important;
|
min-width: 28px !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
border-radius: var(--app-radius-md) !important;
|
border-radius: 10px !important;
|
||||||
background: var(--app-accent-soft) !important;
|
background: var(--app-accent-soft) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -80,5 +80,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-panel .toolbar-actions .el-button:not(.is-circle):not(.is-link) {
|
.table-panel .toolbar-actions .el-button:not(.is-circle):not(.is-link) {
|
||||||
border-radius: var(--app-radius-md) !important;
|
border-radius: 10px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,8 +26,8 @@
|
|||||||
--tags-view-active-border-color: var(--el-color-primary);
|
--tags-view-active-border-color: var(--el-color-primary);
|
||||||
|
|
||||||
// --- 应用壳:圆角、阴影、表面、导航、强调色与文案色 ---
|
// --- 应用壳:圆角、阴影、表面、导航、强调色与文案色 ---
|
||||||
--app-radius-sm: max(0px, calc(var(--app-radius-base) - 8px));
|
--app-radius-sm: calc(var(--app-radius-base) - 8px);
|
||||||
--app-radius-md: max(0px, calc(var(--app-radius-base) - 4px));
|
--app-radius-md: calc(var(--app-radius-base) - 4px);
|
||||||
--app-radius-lg: var(--app-radius-base);
|
--app-radius-lg: var(--app-radius-base);
|
||||||
--app-radius-base: 14px;
|
--app-radius-base: 14px;
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
.el-radio-button__inner,
|
.el-radio-button__inner,
|
||||||
.el-checkbox-button__inner {
|
.el-checkbox-button__inner {
|
||||||
border-radius: var(--app-radius-md) !important;
|
border-radius: 10px !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,5 +69,5 @@
|
|||||||
.el-checkbox-button:first-child .el-checkbox-button__inner,
|
.el-checkbox-button:first-child .el-checkbox-button__inner,
|
||||||
.el-radio-button:last-child .el-radio-button__inner,
|
.el-radio-button:last-child .el-radio-button__inner,
|
||||||
.el-checkbox-button:last-child .el-checkbox-button__inner {
|
.el-checkbox-button:last-child .el-checkbox-button__inner {
|
||||||
border-radius: var(--app-radius-md) !important;
|
border-radius: 10px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,19 +54,6 @@ html.dark {
|
|||||||
border-right: 0;
|
border-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全高表格的外壳会延伸到分页区域,左边框改由实际表头/数据行首列绘制,避免少量数据时边框悬空。
|
|
||||||
.el-table.is-full-height-table.el-table--border {
|
|
||||||
border-left: 0;
|
|
||||||
|
|
||||||
> .el-table__inner-wrapper > .el-table__header-wrapper,
|
|
||||||
> .el-table__inner-wrapper > .el-table__body-wrapper,
|
|
||||||
> .el-table__inner-wrapper > .el-table__footer-wrapper {
|
|
||||||
tr > .el-table__cell:first-child {
|
|
||||||
border-left: var(--el-table-border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 表格内操作列:固定宽按钮、下拉链接色 ---
|
// --- 表格内操作列:固定宽按钮、下拉链接色 ---
|
||||||
.el-table .fixed-width .el-button--small {
|
.el-table .fixed-width .el-button--small {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|||||||
@ -104,7 +104,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { VxeTableInstance } from 'vxe-table';
|
import { VxeTableInstance } from 'vxe-table';
|
||||||
import animateConfig from '@/animate';
|
import animateConfig from '@/animate';
|
||||||
import api from '@/api/system/role';
|
import api from '@/api/system/role';
|
||||||
import { RoleVO, RoleQuery } from '@/api/system/role/types';
|
import { RoleVO, RoleQuery } from '@/api/system/role/types';
|
||||||
|
|||||||
@ -19,38 +19,33 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-if="!modelCollapsed">
|
<template v-if="!modelCollapsed">
|
||||||
<el-input v-model="filterText" :placeholder="placeholder" prefix-icon="Search" clearable />
|
<el-input v-model="filterText" :placeholder="placeholder" prefix-icon="Search" clearable />
|
||||||
<div ref="treeWrapRef" class="dept-tree-wrap">
|
<el-tree
|
||||||
<el-tree-v2
|
ref="treeRef"
|
||||||
ref="treeRef"
|
class="mt-2 dept-tree"
|
||||||
class="dept-tree"
|
:node-key="nodeKey"
|
||||||
:data="treeData"
|
:data="data"
|
||||||
:props="mergedTreeProps as any"
|
:props="mergedTreeProps as any"
|
||||||
:height="treeHeight"
|
:expand-on-click-node="false"
|
||||||
:item-size="28"
|
:filter-node-method="internalFilterNode"
|
||||||
:expand-on-click-node="false"
|
highlight-current
|
||||||
:default-expanded-keys="defaultExpandedKeys"
|
default-expand-all
|
||||||
:filter-method="internalFilterNode"
|
@node-click="onNodeClick"
|
||||||
highlight-current
|
>
|
||||||
@node-click="onNodeClick"
|
<template #default="{ data: nodeData }">
|
||||||
>
|
<span class="tree-node-label" :class="{ 'is-disabled': isNodeDisabled(nodeData) }">
|
||||||
<template #default="{ node }">
|
<span>{{ getNodeLabel(nodeData) }}</span>
|
||||||
<span class="tree-node-label" :class="{ 'is-disabled': isNodeDisabled(node.data) }">
|
<el-tooltip v-if="isNodeDisabled(nodeData)" content="停用" placement="top">
|
||||||
<span>{{ node.label }}</span>
|
<el-icon class="tree-node-disabled-icon"><CircleCloseFilled /></el-icon>
|
||||||
<el-tooltip v-if="isNodeDisabled(node.data)" content="停用" placement="top">
|
</el-tooltip>
|
||||||
<el-icon class="tree-node-disabled-icon"><CircleCloseFilled /></el-icon>
|
</span>
|
||||||
</el-tooltip>
|
</template>
|
||||||
</span>
|
</el-tree>
|
||||||
</template>
|
|
||||||
</el-tree-v2>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TreeKey, TreeV2Instance } from 'element-plus';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
/** 面板标题 */
|
/** 面板标题 */
|
||||||
@ -96,34 +91,23 @@ const modelCollapsed = computed({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const filterText = ref('');
|
const filterText = ref('');
|
||||||
const treeRef = ref<TreeV2Instance>();
|
const treeRef = ref<ElTreeInstance>();
|
||||||
const treeWrapRef = ref<HTMLElement>();
|
|
||||||
const treeData = shallowRef<any[]>([]);
|
|
||||||
const defaultExpandedKeys = shallowRef<TreeKey[]>([]);
|
|
||||||
const { height: treeWrapHeight } = useElementSize(treeWrapRef);
|
|
||||||
const treeHeight = computed(() => Math.max(Math.floor(treeWrapHeight.value), 180));
|
|
||||||
|
|
||||||
const mergedTreeProps = computed(() => {
|
const mergedTreeProps = computed(() => {
|
||||||
const label = props.treeProps?.label ?? 'label';
|
|
||||||
const children = props.treeProps?.children ?? 'children';
|
|
||||||
const value = props.treeProps?.value ?? props.nodeKey;
|
|
||||||
if (!props.disabledField) {
|
if (!props.disabledField) {
|
||||||
return {
|
return props.treeProps;
|
||||||
...props.treeProps,
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
children
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...props.treeProps,
|
...props.treeProps,
|
||||||
value,
|
|
||||||
label,
|
|
||||||
children,
|
|
||||||
disabled: props.treeProps?.disabled ?? props.disabledField
|
disabled: props.treeProps?.disabled ?? props.disabledField
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getNodeLabel = (data: any) => {
|
||||||
|
const labelField = props.treeProps?.label ?? 'label';
|
||||||
|
return data?.[labelField] ?? '';
|
||||||
|
};
|
||||||
|
|
||||||
const isNodeDisabled = (data: any) => {
|
const isNodeDisabled = (data: any) => {
|
||||||
if (!props.disabledField) {
|
if (!props.disabledField) {
|
||||||
return false;
|
return false;
|
||||||
@ -135,23 +119,6 @@ const toggleCollapsed = () => {
|
|||||||
modelCollapsed.value = !modelCollapsed.value;
|
modelCollapsed.value = !modelCollapsed.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNodeKey = (data: any) => data?.[props.nodeKey] as TreeKey | undefined;
|
|
||||||
|
|
||||||
const collectExpandedKeys = (nodes: any[], keys: TreeKey[] = []) => {
|
|
||||||
const childrenField = props.treeProps?.children ?? 'children';
|
|
||||||
for (const node of nodes) {
|
|
||||||
const children = node?.[childrenField];
|
|
||||||
if (Array.isArray(children) && children.length > 0) {
|
|
||||||
const key = getNodeKey(node);
|
|
||||||
if (key !== undefined) {
|
|
||||||
keys.push(key);
|
|
||||||
}
|
|
||||||
collectExpandedKeys(children, keys);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return keys;
|
|
||||||
};
|
|
||||||
|
|
||||||
const internalFilterNode = (value: string, data: any) => {
|
const internalFilterNode = (value: string, data: any) => {
|
||||||
if (props.filterNodeMethod) {
|
if (props.filterNodeMethod) {
|
||||||
return props.filterNodeMethod(value, data);
|
return props.filterNodeMethod(value, data);
|
||||||
@ -160,32 +127,16 @@ const internalFilterNode = (value: string, data: any) => {
|
|||||||
return data[props.filterField]?.indexOf(value) !== -1;
|
return data[props.filterField]?.indexOf(value) !== -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterTree = useDebounceFn(() => {
|
watchEffect(
|
||||||
treeRef.value?.filter(filterText.value);
|
() => {
|
||||||
}, 200);
|
treeRef.value?.filter(filterText.value);
|
||||||
|
|
||||||
watch(filterText, () => filterTree());
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.data,
|
|
||||||
(data) => {
|
|
||||||
const rawData = toRaw(data);
|
|
||||||
treeData.value = rawData;
|
|
||||||
defaultExpandedKeys.value = collectExpandedKeys(rawData);
|
|
||||||
nextTick(() => {
|
|
||||||
treeRef.value?.setData(rawData);
|
|
||||||
treeRef.value?.setExpandedKeys(defaultExpandedKeys.value);
|
|
||||||
if (filterText.value) {
|
|
||||||
filterTree();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ flush: 'post' }
|
||||||
);
|
);
|
||||||
|
|
||||||
const onNodeClick = (data: any, node: any, component: any) => {
|
const onNodeClick = (data: any, node: any, component: any) => {
|
||||||
if (isNodeDisabled(data)) {
|
if (isNodeDisabled(data)) {
|
||||||
treeRef.value?.setCurrentKey(undefined as any);
|
treeRef.value?.setCurrentKey(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit('node-click', data, node, component);
|
emit('node-click', data, node, component);
|
||||||
@ -273,17 +224,14 @@ $mobile-breakpoint: 900px;
|
|||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dept-tree-wrap {
|
.dept-tree {
|
||||||
margin-top: 8px;
|
padding-top: 6px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dept-tree {
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
padding-right: 4px;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -167,7 +167,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { VxeTableInstance } from 'vxe-table';
|
import { VxeTableInstance } from 'vxe-table';
|
||||||
import animateConfig from '@/animate';
|
import animateConfig from '@/animate';
|
||||||
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
|
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
|
||||||
import api from '@/api/system/user';
|
import api from '@/api/system/user';
|
||||||
|
|||||||
@ -1,298 +0,0 @@
|
|||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useSettingsStore } from '@/store/modules/settings';
|
|
||||||
|
|
||||||
interface VueBackedHTMLElement extends HTMLElement {
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableComponentHost {
|
|
||||||
proxy?: {
|
|
||||||
doLayout?: () => void;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableState {
|
|
||||||
height: string;
|
|
||||||
maxHeight: string;
|
|
||||||
bodyHeight: string;
|
|
||||||
bodyMaxHeight: string;
|
|
||||||
bodyOverflowY: string;
|
|
||||||
bodyOverflowX: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableStates = new WeakMap<HTMLElement, TableState>();
|
|
||||||
const vueParentComponentKey = '__vueParentComponent';
|
|
||||||
|
|
||||||
const getTableComponentHost = (table: HTMLElement): TableComponentHost | undefined => {
|
|
||||||
const host = (table as VueBackedHTMLElement)[vueParentComponentKey];
|
|
||||||
if (!host || typeof host !== 'object') return undefined;
|
|
||||||
return host as TableComponentHost;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOuterHeight = (element: HTMLElement) => {
|
|
||||||
const style = window.getComputedStyle(element);
|
|
||||||
const marginTop = Number.parseFloat(style.marginTop) || 0;
|
|
||||||
const marginBottom = Number.parseFloat(style.marginBottom) || 0;
|
|
||||||
return element.getBoundingClientRect().height + marginTop + marginBottom;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isVisible = (element: HTMLElement) => {
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
return rect.width > 0 && rect.height > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFollowingHeight = (table: HTMLElement) => {
|
|
||||||
const parent = table.parentElement;
|
|
||||||
if (!parent) return 0;
|
|
||||||
|
|
||||||
const children = Array.from(parent.children) as HTMLElement[];
|
|
||||||
const tableIndex = children.indexOf(table);
|
|
||||||
if (tableIndex === -1) return 0;
|
|
||||||
|
|
||||||
return children.slice(tableIndex + 1).reduce((height, child) => {
|
|
||||||
if (!isVisible(child)) return height;
|
|
||||||
return height + getOuterHeight(child);
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBottomPadding = (table: HTMLElement) => {
|
|
||||||
const parent = table.parentElement;
|
|
||||||
if (!parent) return 0;
|
|
||||||
const parentStyle = window.getComputedStyle(parent);
|
|
||||||
return Number.parseFloat(parentStyle.paddingBottom) || 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBodyWrapper = (table: HTMLElement) => {
|
|
||||||
return table.querySelector<HTMLElement>('.el-table__body-wrapper');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTableComponent = (table: HTMLElement) => {
|
|
||||||
return getTableComponentHost(table)?.proxy;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPageOverflowHeight = () => {
|
|
||||||
const documentElement = document.documentElement;
|
|
||||||
return Math.max(0, documentElement.scrollHeight - documentElement.clientHeight);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rememberTableState = (table: HTMLElement) => {
|
|
||||||
if (tableStates.has(table)) return;
|
|
||||||
|
|
||||||
const bodyWrapper = getBodyWrapper(table);
|
|
||||||
tableStates.set(table, {
|
|
||||||
height: table.style.height,
|
|
||||||
maxHeight: table.style.maxHeight,
|
|
||||||
bodyHeight: bodyWrapper?.style.height ?? '',
|
|
||||||
bodyMaxHeight: bodyWrapper?.style.maxHeight ?? '',
|
|
||||||
bodyOverflowY: bodyWrapper?.style.overflowY ?? '',
|
|
||||||
bodyOverflowX: bodyWrapper?.style.overflowX ?? ''
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetTable = (table: HTMLElement) => {
|
|
||||||
const originalState = tableStates.get(table);
|
|
||||||
if (!originalState) return;
|
|
||||||
|
|
||||||
const bodyWrapper = getBodyWrapper(table);
|
|
||||||
table.classList.remove('is-full-height-table');
|
|
||||||
table.style.height = originalState.height;
|
|
||||||
table.style.maxHeight = originalState.maxHeight;
|
|
||||||
if (bodyWrapper) {
|
|
||||||
bodyWrapper.style.height = originalState.bodyHeight;
|
|
||||||
bodyWrapper.style.maxHeight = originalState.bodyMaxHeight;
|
|
||||||
bodyWrapper.style.overflowY = originalState.bodyOverflowY;
|
|
||||||
bodyWrapper.style.overflowX = originalState.bodyOverflowX;
|
|
||||||
}
|
|
||||||
tableStates.delete(table);
|
|
||||||
getTableComponent(table)?.doLayout?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetAllTables = () => {
|
|
||||||
document.querySelectorAll<HTMLElement>('.is-full-height-table').forEach(resetTable);
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateTableHeight = (table: HTMLElement) => {
|
|
||||||
const rect = table.getBoundingClientRect();
|
|
||||||
const viewportBottom = window.innerHeight;
|
|
||||||
const followingHeight = getFollowingHeight(table);
|
|
||||||
const bottomPadding = getBottomPadding(table);
|
|
||||||
const bottomOffset = window.innerWidth < 768 ? 28 : 32;
|
|
||||||
const minHeight = window.innerWidth < 768 ? 220 : 260;
|
|
||||||
|
|
||||||
return Math.max(minHeight, Math.floor(viewportBottom - rect.top - followingHeight - bottomPadding - bottomOffset));
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyTableHeightValue = (table: HTMLElement, tableHeight: number) => {
|
|
||||||
const nextHeight = `${tableHeight}px`;
|
|
||||||
if (table.style.height !== nextHeight) {
|
|
||||||
table.style.height = nextHeight;
|
|
||||||
}
|
|
||||||
if (table.style.maxHeight !== nextHeight) {
|
|
||||||
table.style.maxHeight = nextHeight;
|
|
||||||
}
|
|
||||||
table.classList.add('is-full-height-table');
|
|
||||||
|
|
||||||
const bodyWrapper = getBodyWrapper(table);
|
|
||||||
if (bodyWrapper) {
|
|
||||||
const headerHeight = table.querySelector<HTMLElement>('.el-table__header-wrapper')?.offsetHeight ?? 0;
|
|
||||||
const footerHeight = table.querySelector<HTMLElement>('.el-table__footer-wrapper')?.offsetHeight ?? 0;
|
|
||||||
const bodyHeight = `${Math.max(120, tableHeight - headerHeight - footerHeight)}px`;
|
|
||||||
if (bodyWrapper.style.height !== bodyHeight) {
|
|
||||||
bodyWrapper.style.height = bodyHeight;
|
|
||||||
}
|
|
||||||
if (bodyWrapper.style.maxHeight !== bodyHeight) {
|
|
||||||
bodyWrapper.style.maxHeight = bodyHeight;
|
|
||||||
}
|
|
||||||
bodyWrapper.style.overflowY = 'auto';
|
|
||||||
bodyWrapper.style.overflowX = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
getTableComponent(table)?.doLayout?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyTableHeight = (table: HTMLElement) => {
|
|
||||||
rememberTableState(table);
|
|
||||||
|
|
||||||
const minHeight = window.innerWidth < 768 ? 220 : 260;
|
|
||||||
const tableHeight = calculateTableHeight(table);
|
|
||||||
applyTableHeightValue(table, tableHeight);
|
|
||||||
|
|
||||||
const overflowHeight = getPageOverflowHeight();
|
|
||||||
if (overflowHeight > 0 && tableHeight > minHeight) {
|
|
||||||
applyTableHeightValue(table, Math.max(minHeight, tableHeight - overflowHeight - 4));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const canManageTable = (table: HTMLElement) => {
|
|
||||||
if (!isVisible(table)) return false;
|
|
||||||
if (!table.closest('.app-main')) return false;
|
|
||||||
if (!table.closest('.table-panel')) return false;
|
|
||||||
if (table.closest('.el-dialog')) return false;
|
|
||||||
if (table.dataset.fullHeightTable === 'false') return false;
|
|
||||||
|
|
||||||
const hasManualHeight = Boolean(table.style.height || table.style.maxHeight);
|
|
||||||
return !hasManualHeight || tableStates.has(table);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getManagedTables = () => {
|
|
||||||
return Array.from(document.querySelectorAll<HTMLElement>('.app-main .table-panel .data-table.el-table')).filter(
|
|
||||||
canManageTable
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useFullHeightTable() {
|
|
||||||
const route = useRoute();
|
|
||||||
const settingsStore = useSettingsStore();
|
|
||||||
const observedElements = new Set<Element>();
|
|
||||||
let resizeObserver: ResizeObserver | undefined;
|
|
||||||
let mutationObserver: MutationObserver | undefined;
|
|
||||||
let animationFrame = 0;
|
|
||||||
const timerIds = new Set<number>();
|
|
||||||
|
|
||||||
const refreshObservedElements = () => {
|
|
||||||
if (!resizeObserver) return;
|
|
||||||
|
|
||||||
const nextElements = new Set<Element>([
|
|
||||||
...Array.from(document.querySelectorAll('.app-main, .app-main .p-2, .app-main .table-panel')),
|
|
||||||
...Array.from(document.querySelectorAll('.app-main .search-panel, .app-main .tree-panel-shell')),
|
|
||||||
...getManagedTables()
|
|
||||||
]);
|
|
||||||
|
|
||||||
observedElements.forEach(element => {
|
|
||||||
if (!nextElements.has(element)) {
|
|
||||||
resizeObserver?.unobserve(element);
|
|
||||||
observedElements.delete(element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
nextElements.forEach(element => {
|
|
||||||
if (!observedElements.has(element)) {
|
|
||||||
resizeObserver?.observe(element);
|
|
||||||
observedElements.add(element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const recalculate = () => {
|
|
||||||
animationFrame = 0;
|
|
||||||
if (!settingsStore.fullHeightTable) {
|
|
||||||
resetAllTables();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tables = getManagedTables();
|
|
||||||
document.querySelectorAll<HTMLElement>('.is-full-height-table').forEach(table => {
|
|
||||||
if (!tables.includes(table)) {
|
|
||||||
resetTable(table);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
tables.forEach(applyTableHeight);
|
|
||||||
refreshObservedElements();
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleRecalculate = (delay = 0) => {
|
|
||||||
if (delay > 0) {
|
|
||||||
const timerId = window.setTimeout(() => {
|
|
||||||
timerIds.delete(timerId);
|
|
||||||
scheduleRecalculate();
|
|
||||||
}, delay);
|
|
||||||
timerIds.add(timerId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (animationFrame) return;
|
|
||||||
animationFrame = window.requestAnimationFrame(recalculate);
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleLayoutRecheck = () => {
|
|
||||||
scheduleRecalculate();
|
|
||||||
[80, 180, 360].forEach(scheduleRecalculate);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
resizeObserver = new ResizeObserver(() => scheduleLayoutRecheck());
|
|
||||||
mutationObserver = new MutationObserver(() => {
|
|
||||||
refreshObservedElements();
|
|
||||||
scheduleLayoutRecheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
const appMain = document.querySelector('.app-main');
|
|
||||||
if (appMain) {
|
|
||||||
mutationObserver.observe(appMain, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['class', 'style'],
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
appMain.addEventListener('transitionend', scheduleLayoutRecheck);
|
|
||||||
}
|
|
||||||
window.addEventListener('resize', scheduleLayoutRecheck);
|
|
||||||
|
|
||||||
refreshObservedElements();
|
|
||||||
scheduleLayoutRecheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (animationFrame) {
|
|
||||||
window.cancelAnimationFrame(animationFrame);
|
|
||||||
}
|
|
||||||
timerIds.forEach(timerId => window.clearTimeout(timerId));
|
|
||||||
timerIds.clear();
|
|
||||||
resizeObserver?.disconnect();
|
|
||||||
mutationObserver?.disconnect();
|
|
||||||
document.querySelector('.app-main')?.removeEventListener('transitionend', scheduleLayoutRecheck);
|
|
||||||
window.removeEventListener('resize', scheduleLayoutRecheck);
|
|
||||||
resetAllTables();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => settingsStore.fullHeightTable,
|
|
||||||
() => scheduleLayoutRecheck()
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.fullPath,
|
|
||||||
() => {
|
|
||||||
nextTick(() => scheduleLayoutRecheck());
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -13,14 +13,12 @@
|
|||||||
|
|
||||||
<script setup name="AppMain" lang="ts">
|
<script setup name="AppMain" lang="ts">
|
||||||
import animateConfig from '@/animate';
|
import animateConfig from '@/animate';
|
||||||
import { useFullHeightTable } from '@/hooks/table/useFullHeightTable';
|
|
||||||
import { useSettingsStore } from '@/store/modules/settings';
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
import { useTagsViewStore } from '@/store/modules/tagsView';
|
import { useTagsViewStore } from '@/store/modules/tagsView';
|
||||||
import IframeToggle from './IframeToggle/index.vue';
|
import IframeToggle from './IframeToggle/index.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const tagsViewStore = useTagsViewStore();
|
const tagsViewStore = useTagsViewStore();
|
||||||
useFullHeightTable();
|
|
||||||
|
|
||||||
// 随机动画集合
|
// 随机动画集合
|
||||||
const animate = ref<string>('');
|
const animate = ref<string>('');
|
||||||
@ -64,14 +62,6 @@ function addIframe() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main:not(.with-fixed-header) {
|
|
||||||
min-height: calc(100vh - 64px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-main.with-tags-view:not(.with-fixed-header) {
|
|
||||||
min-height: calc(100vh - 105px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-main.with-fixed-header {
|
.app-main.with-fixed-header {
|
||||||
padding-top: 76px;
|
padding-top: 76px;
|
||||||
min-height: calc(100vh - 76px);
|
min-height: calc(100vh - 76px);
|
||||||
|
|||||||
@ -154,13 +154,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-item">
|
|
||||||
<span>全高表格</span>
|
|
||||||
<span class="comp-style">
|
|
||||||
<el-switch v-model="settingsStore.fullHeightTable" class="drawer-switch" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider />
|
<el-divider />
|
||||||
|
|
||||||
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
|
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
|
||||||
@ -267,7 +260,6 @@ const saveSetting = () => {
|
|||||||
settings.value.theme = storeSettings.value.theme;
|
settings.value.theme = storeSettings.value.theme;
|
||||||
settings.value.navType = storeSettings.value.navType;
|
settings.value.navType = storeSettings.value.navType;
|
||||||
settings.value.radiusBase = storeSettings.value.radiusBase;
|
settings.value.radiusBase = storeSettings.value.radiusBase;
|
||||||
settings.value.fullHeightTable = storeSettings.value.fullHeightTable;
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
modal.closeLoading();
|
modal.closeLoading();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|||||||
11
src/main.ts
11
src/main.ts
@ -1,15 +1,13 @@
|
|||||||
import HighLight from '@highlightjs/vue-plugin';
|
import HighLight from '@highlightjs/vue-plugin';
|
||||||
import { ElDialog } from 'element-plus';
|
import { ElDialog } from 'element-plus';
|
||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import VxeUIPlugin, { VxeUI } from 'vxe-pc-ui';
|
import VXETable from 'vxe-table';
|
||||||
import VxeTablePlugin from 'vxe-table';
|
|
||||||
import 'virtual:uno.css';
|
import 'virtual:uno.css';
|
||||||
import 'element-plus/theme-chalk/dark/css-vars.css';
|
import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||||
import '@/assets/styles/index.scss';
|
import '@/assets/styles/index.scss';
|
||||||
import 'highlight.js/styles/atom-one-dark.css';
|
import 'highlight.js/styles/atom-one-dark.css';
|
||||||
import 'highlight.js/lib/common';
|
import 'highlight.js/lib/common';
|
||||||
import 'virtual:svg-icons-register';
|
import 'virtual:svg-icons-register';
|
||||||
import 'vxe-pc-ui/lib/style.css';
|
|
||||||
import 'vxe-table/lib/style.css';
|
import 'vxe-table/lib/style.css';
|
||||||
import i18n from '@/lang/index';
|
import i18n from '@/lang/index';
|
||||||
import ElementIcons from '@/plugins/svgicon';
|
import ElementIcons from '@/plugins/svgicon';
|
||||||
@ -20,7 +18,7 @@ import './permission';
|
|||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
VxeUI.setConfig({
|
VXETable.setConfig({
|
||||||
zIndex: 999999
|
zIndex: 999999
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -30,11 +28,10 @@ const app = createApp(App);
|
|||||||
|
|
||||||
app.use(HighLight);
|
app.use(HighLight);
|
||||||
app.use(ElementIcons);
|
app.use(ElementIcons);
|
||||||
app.use(store);
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
app.use(store);
|
||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
app.use(VxeUIPlugin);
|
app.use(VXETable);
|
||||||
app.use(VxeTablePlugin);
|
|
||||||
app.use(plugins);
|
app.use(plugins);
|
||||||
directive(app);
|
directive(app);
|
||||||
|
|
||||||
|
|||||||
@ -81,11 +81,6 @@ const setting: DefaultSettings = {
|
|||||||
/**
|
/**
|
||||||
* 页面圆角大小
|
* 页面圆角大小
|
||||||
*/
|
*/
|
||||||
radiusBase: 14,
|
radiusBase: 14
|
||||||
|
|
||||||
/**
|
|
||||||
* 表格全高内部滚动
|
|
||||||
*/
|
|
||||||
fullHeightTable: true
|
|
||||||
};
|
};
|
||||||
export default setting;
|
export default setting;
|
||||||
|
|||||||
@ -16,8 +16,7 @@ export const useSettingsStore = defineStore('setting', () => {
|
|||||||
sideTheme: defaultSettings.sideTheme,
|
sideTheme: defaultSettings.sideTheme,
|
||||||
theme: defaultSettings.theme,
|
theme: defaultSettings.theme,
|
||||||
navType: defaultSettings.navType,
|
navType: defaultSettings.navType,
|
||||||
radiusBase: defaultSettings.radiusBase,
|
radiusBase: defaultSettings.radiusBase
|
||||||
fullHeightTable: defaultSettings.fullHeightTable
|
|
||||||
});
|
});
|
||||||
const title = ref<string>(defaultSettings.title);
|
const title = ref<string>(defaultSettings.title);
|
||||||
const theme = ref<string>(storageSetting.value.theme);
|
const theme = ref<string>(storageSetting.value.theme);
|
||||||
@ -33,7 +32,6 @@ export const useSettingsStore = defineStore('setting', () => {
|
|||||||
const dark = ref<boolean>(defaultSettings.dark);
|
const dark = ref<boolean>(defaultSettings.dark);
|
||||||
const navType = ref<NavTypeEnum>(storageSetting.value.navType || NavTypeEnum.LEFT);
|
const navType = ref<NavTypeEnum>(storageSetting.value.navType || NavTypeEnum.LEFT);
|
||||||
const radiusBase = ref<number>(storageSetting.value.radiusBase ?? defaultSettings.radiusBase);
|
const radiusBase = ref<number>(storageSetting.value.radiusBase ?? defaultSettings.radiusBase);
|
||||||
const fullHeightTable = ref<boolean>(storageSetting.value.fullHeightTable ?? defaultSettings.fullHeightTable);
|
|
||||||
|
|
||||||
const setTitle = (value: string) => {
|
const setTitle = (value: string) => {
|
||||||
title.value = value;
|
title.value = value;
|
||||||
@ -54,7 +52,6 @@ export const useSettingsStore = defineStore('setting', () => {
|
|||||||
dark,
|
dark,
|
||||||
navType,
|
navType,
|
||||||
radiusBase,
|
radiusBase,
|
||||||
fullHeightTable,
|
|
||||||
setTitle
|
setTitle
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
const user = data.user;
|
const user = data.user;
|
||||||
const profile = user.avatarUrl == '' || user.avatarUrl == null ? defAva : user.avatarUrl;
|
const profile = user.avatar == '' || user.avatar == null ? defAva : user.avatar;
|
||||||
|
|
||||||
if (data.roles && data.roles.length > 0) {
|
if (data.roles && data.roles.length > 0) {
|
||||||
// 验证返回的roles是否是一个非空数组
|
// 验证返回的roles是否是一个非空数组
|
||||||
|
|||||||
5
src/types/global.d.ts
vendored
5
src/types/global.d.ts
vendored
@ -131,11 +131,6 @@ declare global {
|
|||||||
* 页面圆角大小
|
* 页面圆角大小
|
||||||
*/
|
*/
|
||||||
radiusBase: number;
|
radiusBase: number;
|
||||||
|
|
||||||
/**
|
|
||||||
* 表格全高内部滚动
|
|
||||||
*/
|
|
||||||
fullHeightTable: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DefaultSettings extends LayoutSetting {
|
interface DefaultSettings extends LayoutSetting {
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { ecb } from '@noble/ciphers/aes.js';
|
import * as CryptoJSModule from 'crypto-js';
|
||||||
|
|
||||||
|
const CryptoJS = ('default' in CryptoJSModule ? CryptoJSModule.default : CryptoJSModule) as typeof CryptoJSModule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 随机生成32位的字符串
|
* 随机生成32位的字符串
|
||||||
@ -12,58 +14,41 @@ const generateRandomString = (): string => {
|
|||||||
.slice(0, 32);
|
.slice(0, 32);
|
||||||
};
|
};
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
|
|
||||||
const bytesToBase64 = (bytes: Uint8Array): string => {
|
|
||||||
let binary = '';
|
|
||||||
for (const byte of bytes) {
|
|
||||||
binary += String.fromCharCode(byte);
|
|
||||||
}
|
|
||||||
return btoa(binary);
|
|
||||||
};
|
|
||||||
|
|
||||||
const base64ToBytes = (str: string): Uint8Array => {
|
|
||||||
const binary = atob(str);
|
|
||||||
const bytes = new Uint8Array(binary.length);
|
|
||||||
for (let i = 0; i < binary.length; i++) {
|
|
||||||
bytes[i] = binary.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 随机生成aes 密钥
|
* 随机生成aes 密钥
|
||||||
* @returns {Uint8Array}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export const generateAesKey = (): Uint8Array => {
|
export const generateAesKey = (): CryptoJSModule.lib.WordArray => {
|
||||||
return encoder.encode(generateRandomString());
|
return CryptoJS.enc.Utf8.parse(generateRandomString());
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加密base64
|
* 加密base64
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export const encryptBase64 = (bytes: Uint8Array): string => {
|
export const encryptBase64 = (str: CryptoJSModule.lib.WordArray): string => {
|
||||||
return bytesToBase64(bytes);
|
return CryptoJS.enc.Base64.stringify(str);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解密base64
|
* 解密base64
|
||||||
*/
|
*/
|
||||||
export const decryptBase64 = (str: string): Uint8Array => {
|
export const decryptBase64 = (str: string) => {
|
||||||
return base64ToBytes(str);
|
return CryptoJS.enc.Base64.parse(str);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用密钥对数据进行加密 (AES/ECB/PKCS7Padding 与后端保持一致)
|
* 使用密钥对数据进行加密
|
||||||
* @param message
|
* @param message
|
||||||
* @param aesKey
|
* @param aesKey
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export const encryptWithAes = (message: string, aesKey: Uint8Array): string => {
|
export const encryptWithAes = (message: string, aesKey: CryptoJSModule.lib.WordArray): string => {
|
||||||
const encrypted = ecb(aesKey).encrypt(encoder.encode(message));
|
const encrypted = CryptoJS.AES.encrypt(message, aesKey, {
|
||||||
return bytesToBase64(encrypted);
|
mode: CryptoJS.mode.ECB,
|
||||||
|
padding: CryptoJS.pad.Pkcs7
|
||||||
|
});
|
||||||
|
return encrypted.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,7 +57,10 @@ export const encryptWithAes = (message: string, aesKey: Uint8Array): string => {
|
|||||||
* @param aesKey
|
* @param aesKey
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export const decryptWithAes = (message: string, aesKey: Uint8Array): string => {
|
export const decryptWithAes = (message: string, aesKey: CryptoJSModule.lib.WordArray): string => {
|
||||||
const decrypted = ecb(aesKey).decrypt(base64ToBytes(message));
|
const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
|
||||||
return decoder.decode(decrypted);
|
mode: CryptoJS.mode.ECB,
|
||||||
|
padding: CryptoJS.pad.Pkcs7
|
||||||
|
});
|
||||||
|
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,9 +9,6 @@ import { parsePushMessage, resolveNoticeGroup, resolveNoticeTitle, shouldAppendN
|
|||||||
|
|
||||||
let closePushConnection: (() => void) | undefined;
|
let closePushConnection: (() => void) | undefined;
|
||||||
let stopPushWatchers: Array<() => void> = [];
|
let stopPushWatchers: Array<() => void> = [];
|
||||||
const KICKED_MESSAGE = 'kicked';
|
|
||||||
let pushKicked = false;
|
|
||||||
let resumePushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
||||||
|
|
||||||
const formatNoticeTime = (timestamp?: number | string) => {
|
const formatNoticeTime = (timestamp?: number | string) => {
|
||||||
const time = timestamp ? new Date(timestamp) : new Date();
|
const time = timestamp ? new Date(timestamp) : new Date();
|
||||||
@ -47,15 +44,6 @@ const appendNotice = (raw: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePushMessage = (raw: string) => {
|
|
||||||
if (raw === KICKED_MESSAGE) {
|
|
||||||
pushKicked = true;
|
|
||||||
closePush();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
appendNotice(raw);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toNoticeItem = (item: MessageVO) => {
|
const toNoticeItem = (item: MessageVO) => {
|
||||||
const userId = useUserStore().userId;
|
const userId = useUserStore().userId;
|
||||||
const timestamp = item.createTime ? new Date(item.createTime).getTime() : Date.now();
|
const timestamp = item.createTime ? new Date(item.createTime).getTime() : Date.now();
|
||||||
@ -103,7 +91,7 @@ const initSsePush = (url: string) => {
|
|||||||
|
|
||||||
const stopDataWatch = watch(data, () => {
|
const stopDataWatch = watch(data, () => {
|
||||||
if (!data.value) return;
|
if (!data.value) return;
|
||||||
handlePushMessage(data.value);
|
appendNotice(data.value);
|
||||||
data.value = null;
|
data.value = null;
|
||||||
});
|
});
|
||||||
stopPushWatchers.push(stopErrorWatch, stopDataWatch);
|
stopPushWatchers.push(stopErrorWatch, stopDataWatch);
|
||||||
@ -127,7 +115,7 @@ const initWsPush = (url: string) => {
|
|||||||
if (String(e.data) === 'pong') {
|
if (String(e.data) === 'pong') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handlePushMessage(String(e.data));
|
appendNotice(String(e.data));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
closePushConnection = close;
|
closePushConnection = close;
|
||||||
@ -138,7 +126,6 @@ export const initPush = () => {
|
|||||||
if (import.meta.env.VITE_APP_MESSAGE_ENABLED === 'false') {
|
if (import.meta.env.VITE_APP_MESSAGE_ENABLED === 'false') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pushKicked = false;
|
|
||||||
const path = import.meta.env.VITE_APP_MESSAGE_PATH || '/resource/message';
|
const path = import.meta.env.VITE_APP_MESSAGE_PATH || '/resource/message';
|
||||||
const transport = import.meta.env.VITE_APP_MESSAGE_TRANSPORT || 'sse';
|
const transport = import.meta.env.VITE_APP_MESSAGE_TRANSPORT || 'sse';
|
||||||
if (transport.toLowerCase() === 'websocket') {
|
if (transport.toLowerCase() === 'websocket') {
|
||||||
@ -166,27 +153,3 @@ export const closePush = () => {
|
|||||||
stopPushWatchers.forEach(stop => stop());
|
stopPushWatchers.forEach(stop => stop());
|
||||||
stopPushWatchers = [];
|
stopPushWatchers = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const resumePushIfNeeded = () => {
|
|
||||||
if (!pushKicked || !getToken() || document.visibilityState !== 'visible') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (resumePushTimer) {
|
|
||||||
clearTimeout(resumePushTimer);
|
|
||||||
}
|
|
||||||
resumePushTimer = setTimeout(async () => {
|
|
||||||
resumePushTimer = undefined;
|
|
||||||
if (!pushKicked || !getToken() || document.visibilityState !== 'visible') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await initMessageBox();
|
|
||||||
} finally {
|
|
||||||
initPush();
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('focus', resumePushIfNeeded);
|
|
||||||
document.addEventListener('visibilitychange', resumePushIfNeeded);
|
|
||||||
window.addEventListener('online', resumePushIfNeeded);
|
|
||||||
|
|||||||
@ -356,7 +356,7 @@ const handleExport = () => {
|
|||||||
{
|
{
|
||||||
...queryParams.value
|
...queryParams.value
|
||||||
},
|
},
|
||||||
`operlog_${new Date().getTime()}.xlsx`
|
`config_${new Date().getTime()}.xlsx`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@ -75,7 +75,7 @@
|
|||||||
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="220">
|
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="220">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div class="menu-name-cell">
|
<div class="menu-name-cell">
|
||||||
<svg-icon v-if="isMenuIconVisible(scope.row.icon)" :icon-class="scope.row.icon" />
|
<svg-icon v-if="scope.row.icon" :icon-class="scope.row.icon" />
|
||||||
<span class="menu-name-text">{{ scope.row.menuName }}</span>
|
<span class="menu-name-text">{{ scope.row.menuName }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -431,11 +431,6 @@ const { dialog, openDialog, closeDialog, setTitle } = useDialogState();
|
|||||||
|
|
||||||
type MenuTagType = 'warning' | 'primary' | 'success' | 'danger';
|
type MenuTagType = 'warning' | 'primary' | 'success' | 'danger';
|
||||||
|
|
||||||
const isMenuIconVisible = (icon?: string) => {
|
|
||||||
const normalizedIcon = icon?.trim();
|
|
||||||
return !!normalizedIcon && normalizedIcon !== '#';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMenuTypeMeta = (menu: Partial<MenuVO>): { label: string; type: MenuTagType } => {
|
const getMenuTypeMeta = (menu: Partial<MenuVO>): { label: string; type: MenuTagType } => {
|
||||||
if (menu.menuType === MenuTypeEnum.F) {
|
if (menu.menuType === MenuTypeEnum.F) {
|
||||||
return { label: '按钮', type: 'warning' };
|
return { label: '按钮', type: 'warning' };
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- 导入表 -->
|
||||||
<el-dialog v-model="visible" title="导入表" width="1100px" top="5vh" append-to-body>
|
<el-dialog v-model="visible" title="导入表" width="1100px" top="5vh" append-to-body>
|
||||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||||
<el-form-item label="数据源" prop="dataName">
|
<el-form-item label="数据源" prop="dataName">
|
||||||
|
|||||||
@ -170,8 +170,8 @@
|
|||||||
<el-tab-pane
|
<el-tab-pane
|
||||||
v-for="(value, key) in preview.data"
|
v-for="(value, key) in preview.data"
|
||||||
:key="value"
|
:key="value"
|
||||||
:label="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.ftl'))"
|
:label="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
|
||||||
:name="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.ftl'))"
|
:name="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
|
||||||
>
|
>
|
||||||
<el-link
|
<el-link
|
||||||
v-copyText="value"
|
v-copyText="value"
|
||||||
|
|||||||
@ -15,15 +15,6 @@ export default defineConfig(({ mode, command }) => {
|
|||||||
},
|
},
|
||||||
// https://cn.vitejs.dev/config/#resolve-extensions
|
// https://cn.vitejs.dev/config/#resolve-extensions
|
||||||
plugins: createPlugins(env, command === 'build'),
|
plugins: createPlugins(env, command === 'build'),
|
||||||
build: {
|
|
||||||
chunkSizeWarningLimit: 1500,
|
|
||||||
rolldownOptions: {
|
|
||||||
checks: {
|
|
||||||
invalidAnnotation: false,
|
|
||||||
pluginTimings: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: Number(env.VITE_APP_PORT),
|
port: Number(env.VITE_APP_PORT),
|
||||||
|
|||||||
@ -1,117 +0,0 @@
|
|||||||
import type { PluginOption } from 'vite';
|
|
||||||
|
|
||||||
import { normalizePath } from 'vite';
|
|
||||||
import { parse } from 'vue/compiler-sfc';
|
|
||||||
|
|
||||||
/** 模板 AST 元素节点类型 */
|
|
||||||
const NODE_TYPE_ELEMENT = 1;
|
|
||||||
/** 模板 AST 注释节点类型 */
|
|
||||||
const NODE_TYPE_COMMENT = 3;
|
|
||||||
/** 模板 AST 指令属性类型 */
|
|
||||||
const NODE_TYPE_DIRECTIVE = 7;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断节点是否为元素节点
|
|
||||||
*/
|
|
||||||
function isElement(node: any): boolean {
|
|
||||||
return node?.type === NODE_TYPE_ELEMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断节点是否为注释节点
|
|
||||||
* @description 注释节点同样是 transition 的一个子节点,会导致多节点
|
|
||||||
*/
|
|
||||||
function isComment(node: any): boolean {
|
|
||||||
return node?.type === NODE_TYPE_COMMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节点用于报错展示的标签文本
|
|
||||||
*/
|
|
||||||
function nodeLabel(node: any): string {
|
|
||||||
return isComment(node) ? '<!--注释-->' : `<${node.tag}>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断元素是否带有指定指令(如 v-if / v-else-if / v-else)
|
|
||||||
*/
|
|
||||||
function hasDirective(node: any, name: string): boolean {
|
|
||||||
return (node.props || []).some(
|
|
||||||
(prop: any) => prop.type === NODE_TYPE_DIRECTIVE && prop.name === name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断多个元素子节点是否构成合法的 v-if / v-else-if / v-else 分支链
|
|
||||||
* @description 分支链任意时刻只渲染一个节点,对外层 transition 是合法的
|
|
||||||
*/
|
|
||||||
function isConditionalChain(elements: any[]): boolean {
|
|
||||||
// 首个节点必须是 v-if,后续节点必须都是 v-else-if 或 v-else
|
|
||||||
if (!hasDirective(elements[0], 'if')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return elements
|
|
||||||
.slice(1)
|
|
||||||
.every((el) => hasDirective(el, 'else-if') || hasDirective(el, 'else'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检测页面模板根节点是否存在多标签的 vite 插件
|
|
||||||
* @description 外层路由/页面切换已用 <transition> 包裹,每个页面即为 transition 的子节点。
|
|
||||||
* Vue 的 <transition> 只能管理单个子节点,因此页面模板出现多个根节点会导致过渡失效。
|
|
||||||
* 该插件在编译 src/views 下的页面时检测此类写法并直接报错。
|
|
||||||
*/
|
|
||||||
export const viteCheckTransitionPlugin = (): PluginOption => {
|
|
||||||
// 仅检测 views 目录下的页面
|
|
||||||
const targetDir = normalizePath('/src/views/');
|
|
||||||
|
|
||||||
return {
|
|
||||||
enforce: 'pre',
|
|
||||||
name: 'vite:check-transition',
|
|
||||||
transform(code, id) {
|
|
||||||
const normalizedId = normalizePath(id);
|
|
||||||
|
|
||||||
// 跳过非 views 下的文件、非 .vue 文件以及带 query 的子请求
|
|
||||||
if (
|
|
||||||
normalizedId.includes('?') ||
|
|
||||||
!normalizedId.endsWith('.vue') ||
|
|
||||||
!normalizedId.includes(targetDir)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { descriptor, errors: parseErrors } = parse(code, {
|
|
||||||
filename: id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 解析失败交由 vue 插件处理,这里不重复报错
|
|
||||||
if (parseErrors.length > 0 || !descriptor.template?.ast) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计根级元素节点与注释节点,二者都是 transition 的子节点,忽略空白文本
|
|
||||||
const rootNodes = (descriptor.template.ast.children || []).filter(
|
|
||||||
(child: any) => isElement(child) || isComment(child),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 仅元素节点参与 v-if 分支链判断(注释不属于分支链)
|
|
||||||
const rootElements = rootNodes.filter((node: any) => isElement(node));
|
|
||||||
const isChain =
|
|
||||||
rootNodes.length === rootElements.length &&
|
|
||||||
isConditionalChain(rootElements);
|
|
||||||
|
|
||||||
if (rootNodes.length > 1 && !isChain) {
|
|
||||||
const relativePath = normalizedId.slice(
|
|
||||||
normalizedId.indexOf('/src/') + 1,
|
|
||||||
);
|
|
||||||
const tags = rootNodes.map((node: any) => nodeLabel(node)).join(' ');
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`[check-transition] ${relativePath} 存在多个根节点: ${tags}\n 页面外层已包裹 <transition>,模板必须只有单个根节点,请用一个容器元素(如 div)包裹,并删除多余的注释/空标签。`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -6,7 +6,6 @@ import createComponents from './components';
|
|||||||
import createSvgIconsPlugin from './svg-icon';
|
import createSvgIconsPlugin from './svg-icon';
|
||||||
import createCompression from './compression';
|
import createCompression from './compression';
|
||||||
import createSetupExtend from './setup-extend';
|
import createSetupExtend from './setup-extend';
|
||||||
import { viteCheckTransitionPlugin } from './check-transition';
|
|
||||||
|
|
||||||
export default (viteEnv: any, isBuild = false): [] => {
|
export default (viteEnv: any, isBuild = false): [] => {
|
||||||
const vitePlugins: any = [];
|
const vitePlugins: any = [];
|
||||||
@ -17,6 +16,5 @@ export default (viteEnv: any, isBuild = false): [] => {
|
|||||||
vitePlugins.push(createCompression(viteEnv));
|
vitePlugins.push(createCompression(viteEnv));
|
||||||
vitePlugins.push(createSvgIconsPlugin());
|
vitePlugins.push(createSvgIconsPlugin());
|
||||||
vitePlugins.push(createSetupExtend());
|
vitePlugins.push(createSetupExtend());
|
||||||
vitePlugins.push(viteCheckTransitionPlugin())
|
|
||||||
return vitePlugins;
|
return vitePlugins;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user