Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c982ecee68 | ||
|
|
ff19c2a0fb | ||
|
|
aa3f114bf4 | ||
|
|
ec44ed1ddc | ||
|
|
38dfbc20dc | ||
|
|
c3477b3a5a | ||
|
|
21fc08193e | ||
|
|
5f837700e9 | ||
|
|
728fdbfe0e | ||
|
|
27851836dc | ||
|
|
58e32c468f | ||
|
|
87bc5bea09 | ||
|
|
8a3ff74833 | ||
|
|
33fb49d683 | ||
|
|
71e00e560c | ||
|
|
4bdffaf4b7 | ||
|
|
4f5f4a7846 | ||
|
|
bcd4a55656 | ||
|
|
b7d8b4e426 | ||
|
|
5f8d919f03 | ||
|
|
97726edc60 | ||
|
|
5e2f228e64 | ||
|
|
d54c058085 | ||
|
|
60ba3158d5 | ||
|
|
be1f2ac382 |
@@ -1,9 +1,13 @@
|
||||
---
|
||||
name: frontend-api-types
|
||||
description: 前端 API 与类型定义专家。用于当前项目中的 src/api 层、types.ts、返回结构、Query/Form/VO/InfoVO 定义,以及前后端接口映射任务。
|
||||
description: 前端 API 与类型定义专家。用于 plus-ui 前端项目中的 src/api 层、types.ts、返回结构、Query/Form/VO/InfoVO 定义,以及前后端接口映射任务;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
||||
---
|
||||
|
||||
你负责当前前端项目中的 API 层和类型定义。
|
||||
你负责 plus-ui 前端项目中的 API 层和类型定义。
|
||||
|
||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
||||
默认分支:`6.X-Vue`
|
||||
远端引用必须同时标记仓库、分支和文件路径;不要写本机绝对路径。
|
||||
|
||||
## 核心原则
|
||||
|
||||
@@ -25,4 +29,4 @@ description: 前端 API 与类型定义专家。用于当前项目中的 src/api
|
||||
|
||||
- API 路径是否与后端一致
|
||||
- 类型是否覆盖接口真实结构
|
||||
- 是否不必要地把类型写宽了
|
||||
- 是否不必要地把类型写宽了
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
---
|
||||
name: frontend-crud-coding
|
||||
description: 前端总入口。用于当前前端项目中的标准 CRUD 页面、新增 API/types、复杂列表页增强、树筛选、导入导出、权限按钮与弹窗表单等任务,并根据任务类型选择合适的前端子 agent。
|
||||
description: 前端总入口。用于 plus-ui 前端项目中的标准 CRUD 页面、新增 API/types、复杂列表页增强、树筛选、导入导出、权限按钮与弹窗表单等任务,并根据任务类型选择合适的前端子 agent;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
||||
---
|
||||
|
||||
你是当前前端项目的总入口 agent。
|
||||
你是 plus-ui 前端项目的总入口 agent。
|
||||
|
||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
||||
默认分支:`6.X-Vue`
|
||||
远端引用必须同时标记仓库、分支和文件路径;不要在 agent 文档中写本机绝对路径。
|
||||
|
||||
先判断任务类型,再按下面规则处理:
|
||||
|
||||
@@ -14,5 +18,5 @@ description: 前端总入口。用于当前前端项目中的标准 CRUD 页面
|
||||
通用要求:
|
||||
|
||||
- 先读当前目录下最近似页面和 API,再动代码。
|
||||
- 冲突时优先相信当前项目真实页面,其次是公共组件和工具,再其次才是关联后端工程的 generator 模板。
|
||||
- 默认直接产出可落地代码,而不是只给抽象建议。
|
||||
- 冲突时优先相信当前项目真实页面,其次是公共组件和工具,再其次才是 plus-ui Gitee 仓库 `6.X-Vue` 分支的 `gen` 模板。
|
||||
- 默认直接产出可落地代码,而不是只给抽象建议。
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
---
|
||||
name: frontend-crud-page
|
||||
description: 前端标准 CRUD 页面专家。用于当前项目中的新建列表页、弹窗表单页、标准 API/types/index.vue 骨架,以及 gen 模板到项目风格的落地任务。
|
||||
description: 前端标准 CRUD 页面专家。用于 plus-ui 前端项目中的新建列表页、弹窗表单页、标准 API/types/index.vue 骨架,以及 gen 模板到项目风格的落地任务;默认参考 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
||||
---
|
||||
|
||||
你负责当前前端项目中的标准 CRUD 页面实现。
|
||||
你负责 plus-ui 前端项目中的标准 CRUD 页面实现。
|
||||
|
||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
||||
默认分支:`6.X-Vue`
|
||||
远端模板引用必须同时标记仓库、分支和路径,例如 `branch=6.X-Vue, path=gen/index.vue.ftl`;不要写本机绝对路径。
|
||||
|
||||
## 核心原则
|
||||
|
||||
1. 先看当前模块最近似页面。
|
||||
2. 再参考关联后端工程中的 generator 模板。
|
||||
2. 再参考 plus-ui Gitee 仓库 `6.X-Vue` 分支中的 `gen` 模板。
|
||||
3. 默认同时维护:
|
||||
`src/api/<module>/<business>/index.ts`
|
||||
`src/api/<module>/<business>/types.ts`
|
||||
@@ -34,4 +38,4 @@ description: 前端标准 CRUD 页面专家。用于当前项目中的新建列
|
||||
|
||||
- API 路径是否与后端一致
|
||||
- `index.ts` 与 `types.ts` 是否同步补齐
|
||||
- 页面是否只是模板裸输出,如果是要继续补强到当前项目风格
|
||||
- 页面是否只是模板裸输出,如果是要继续补强到当前项目风格
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
---
|
||||
name: frontend-page-enhancement
|
||||
description: 复杂前端页面增强专家。用于修改当前项目中已经存在的列表页、树筛选页、带导入导出和更多菜单的页面,强调增量修改和保留现有交互能力。
|
||||
description: 复杂前端页面增强专家。用于修改 plus-ui 前端项目中已经存在的列表页、树筛选页、带导入导出和更多菜单的页面,强调增量修改和保留现有交互能力;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支。
|
||||
---
|
||||
|
||||
你负责当前前端项目中已有页面的增强,不是重写页面。
|
||||
你负责 plus-ui 前端项目中已有页面的增强,不是重写页面。
|
||||
|
||||
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
|
||||
默认分支:`6.X-Vue`
|
||||
远端引用必须同时标记仓库、分支和文件路径;不要写本机绝对路径。
|
||||
|
||||
## 核心原则
|
||||
|
||||
@@ -24,4 +28,4 @@ description: 复杂前端页面增强专家。用于修改当前项目中已经
|
||||
|
||||
- 是否破坏了原页面结构和样式
|
||||
- 是否误删了已有权限控制或交互能力
|
||||
- 是否应该拆成子组件而不是继续堆主页面
|
||||
- 是否应该拆成子组件而不是继续堆主页面
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
---
|
||||
name: frontend-crud-coding
|
||||
description: 在当前 plus-ui-new 前端项目中按真实 Vue 3 + TypeScript + Element Plus + oxlint/oxfmt 代码风格生成或修改页面、API、types、hooks 接入和样式壳。用于新增或修改标准 CRUD 列表页、树表页、系统管理页、监控页、workflow 页面、demo 页面,补齐与后端接口对应的 src/api、types 和 src/views 代码;触发后应先读取适用 references,再阅读目标模块真实代码和项目内 gen 代码生成模板。
|
||||
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 代码生成模板。
|
||||
---
|
||||
|
||||
# 前端编码规范
|
||||
|
||||
先对齐当前前端项目里的真实实现,再参考项目内 `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,7 +1,7 @@
|
||||
interface:
|
||||
display_name: "前端编码"
|
||||
short_description: "按 plus-ui-new 真实代码和 gen 模板编写前端 CRUD"
|
||||
default_prompt: "使用 $frontend-crud-coding 先读取适用 reference,再按当前 plus-ui-new 真实页面、hooks、API/types 约定和项目内 gen 模板实现前端修改。"
|
||||
short_description: "按 plus-ui 6.X-Vue 真实代码和 gen 模板编写前端 CRUD"
|
||||
default_prompt: "使用 $frontend-crud-coding 先读取适用 reference,再按 plus-ui Gitee 仓库 6.X-Vue 分支的真实页面、hooks、API/types 约定和项目内 gen 模板实现前端修改。"
|
||||
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
```text
|
||||
使用 $frontend-crud-coding 为 system/client 补一套前端 CRUD 页面。
|
||||
后端接口已经有 /system/client/list、/system/client/{id}、POST /system/client、PUT /system/client、DELETE /system/client/{ids}。
|
||||
请参考项目内 gen 模板、src/views/demo/demo/index.vue 和现有 system/client 风格实现。
|
||||
请参考 plus-ui Gitee 仓库 6.X-Vue 分支的项目内 gen 模板、src/views/demo/demo/index.vue 和现有 system/client 风格实现。
|
||||
```
|
||||
|
||||
### 期望执行方式
|
||||
@@ -107,7 +107,7 @@
|
||||
2. 新增状态筛选和导出
|
||||
3. API 路径沿用后端接口
|
||||
4. 参考 system/config 的工具栏与导出交互
|
||||
5. 参考 gen 模板补齐缺失 types
|
||||
5. 参考 plus-ui Gitee 仓库 6.X-Vue 分支的 gen 模板补齐缺失 types
|
||||
```
|
||||
|
||||
## 不推荐的任务描述
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
## 优先参考的代码来源
|
||||
|
||||
- 默认远端基线:`https://gitee.com/JavaLionLi/plus-ui`,分支 `6.X-Vue`。引用远端文件时必须同时写明 `branch=6.X-Vue` 和仓库内路径。
|
||||
- 当前目标目录下最近似页面、API、types。
|
||||
- 标准单表:`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`。
|
||||
@@ -22,6 +23,7 @@
|
||||
- `.editorconfig` 要求 UTF-8、LF、2 空格缩进。
|
||||
- 当前仓库没有 `.prettierrc`;格式化使用 `pnpm run fmt`,lint 使用 `pnpm lint`。
|
||||
- 不要在一个页面里混入与仓库不一致的格式和写法。
|
||||
- 不要把本机绝对路径写进 skill 输出或参考文档;跨环境参考统一使用 Gitee 仓库 URL、分支和仓库内相对路径。
|
||||
|
||||
## API 文件规则
|
||||
|
||||
@@ -127,7 +129,8 @@
|
||||
|
||||
## 与 gen 模板的关系
|
||||
|
||||
- `gen` 是当前前端项目内的生成模板,优先于外部后端工程拷贝的模板。
|
||||
- `gen` 是 plus-ui 前端项目内的生成模板;默认远端位置是 `https://gitee.com/JavaLionLi/plus-ui/tree/6.X-Vue/gen`,分支必须标记为 `6.X-Vue`。
|
||||
- 本地仓库存在对应文件时优先读本地 `gen`,但不要把本地绝对路径写入 skill 或交付内容。
|
||||
- 新增标准单表页面时读取 `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` 模板是标准骨架,不是最终答案;落地时仍要对照目标模块真实页面和公共 hooks。
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
|
||||
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
|
||||
|
||||
# 后台请求地址
|
||||
VITE_API_TARGET=http://localhost:8080
|
||||
|
||||
# 开发环境配置
|
||||
VITE_APP_ENV = 'development'
|
||||
|
||||
|
||||
+3
-2
@@ -1,5 +1,7 @@
|
||||
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
|
||||
<#if !table.tree>
|
||||
import type { PageResult } from '@/api/types';
|
||||
</#if>
|
||||
import type { AxiosPromise } from '@/utils/api-types';
|
||||
import request from '@/utils/request';
|
||||
|
||||
@@ -9,7 +11,7 @@ import request from '@/utils/request';
|
||||
* @returns {*}
|
||||
*/
|
||||
|
||||
export const list${BusinessName} = (query?: ${BusinessName}Query): AxiosPromise<PageResult<${BusinessName}VO>> => {
|
||||
export const list${BusinessName} = (query?: ${BusinessName}Query): AxiosPromise<<#if table.tree>${BusinessName}VO[]<#else>PageResult<${BusinessName}VO></#if>> => {
|
||||
return request({
|
||||
url: '/${moduleName}/${businessName}/list',
|
||||
method: 'get',
|
||||
@@ -100,4 +102,3 @@ export const del${BusinessName} = (${pkColumn.javaField}: string | number | Arra
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</#if>
|
||||
</el-select>
|
||||
</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-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
|
||||
<el-option label="请选择字典生成" value="" />
|
||||
@@ -312,7 +312,7 @@
|
||||
></el-option>
|
||||
</el-select>
|
||||
</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-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
|
||||
<el-option label="请选择字典生成" value="" />
|
||||
@@ -329,7 +329,7 @@
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</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-checkbox-group v-model="form.${column.javaField}">
|
||||
<el-checkbox>请选择字典生成</el-checkbox>
|
||||
@@ -349,7 +349,7 @@
|
||||
>{{dict.label}}</el-radio>
|
||||
</el-radio-group>
|
||||
</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-radio-group v-model="form.${column.javaField}">
|
||||
<el-radio value="1">请选择字典生成</el-radio>
|
||||
@@ -689,4 +689,3 @@ onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+14
-10
@@ -45,7 +45,7 @@
|
||||
</#if>
|
||||
</el-select>
|
||||
</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-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
|
||||
<el-option label="请选择字典生成" value="" />
|
||||
@@ -60,7 +60,7 @@
|
||||
placeholder="请选择${column.columnLabel}"
|
||||
/>
|
||||
</el-form-item>
|
||||
<#elseif column.htmlType == "datetime" && column.queryType == "BETWEEN">
|
||||
<#elseif column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
|
||||
<el-form-item label="${column.columnLabel}" style="width: 308px">
|
||||
<el-date-picker
|
||||
v-model="dateRange${column.capJavaField}"
|
||||
@@ -233,7 +233,8 @@
|
||||
<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">
|
||||
<#list columns as column>
|
||||
<#if (column.insert || column.edit) && !column.pk>`n<#if column.htmlType == "input">
|
||||
<#if (column.insert || column.edit) && !column.pk>
|
||||
<#if column.htmlType == "input">
|
||||
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
|
||||
<el-input v-model="form.${column.javaField}" placeholder="请输入${column.columnLabel}" />
|
||||
</el-form-item>
|
||||
@@ -268,7 +269,7 @@
|
||||
></el-option>
|
||||
</el-select>
|
||||
</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-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
|
||||
<el-option label="请选择字典生成" value="" />
|
||||
@@ -285,7 +286,7 @@
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</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-checkbox-group v-model="form.${column.javaField}">
|
||||
<el-checkbox>请选择字典生成</el-checkbox>
|
||||
@@ -305,7 +306,7 @@
|
||||
>{{dict.label}}</el-radio>
|
||||
</el-radio-group>
|
||||
</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-radio-group v-model="form.${column.javaField}">
|
||||
<el-radio value="1">请选择字典生成</el-radio>
|
||||
@@ -380,6 +381,9 @@ import { useTableSelection } from '@/hooks/table/useTableSelection';
|
||||
<#if needDict>
|
||||
import { useDict } from '@/utils/dict';
|
||||
</#if>
|
||||
<#if needParseTime>
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
</#if>
|
||||
import modal from '@/plugins/modal';
|
||||
<#if enableExport>
|
||||
import { download as requestDownload } from '@/utils/request';
|
||||
@@ -400,7 +404,7 @@ const { loading, withLoading } = useLoading(true);
|
||||
const { showSearch } = useSearchToggle();
|
||||
const total = ref(0);
|
||||
<#list columns as column>
|
||||
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
|
||||
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
|
||||
const {
|
||||
dateRange: dateRange${column.capJavaField},
|
||||
applyDateRange: apply${column.capJavaField}DateRange,
|
||||
@@ -472,7 +476,7 @@ const getList = async () => {
|
||||
<#if needAddDateRange>
|
||||
let params = queryParams.value;
|
||||
<#list columns as column>
|
||||
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
|
||||
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
|
||||
params = apply${column.capJavaField}DateRange(params);
|
||||
</#if>
|
||||
</#list>
|
||||
@@ -505,7 +509,7 @@ const { resetQuery } = useSearchReset({
|
||||
initialPageSize: 10,
|
||||
resetExtras: () => {
|
||||
<#list columns as column>
|
||||
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
|
||||
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
|
||||
reset${column.capJavaField}DateRange();
|
||||
</#if>
|
||||
</#list>
|
||||
@@ -565,8 +569,8 @@ const handleDelete = async (row?: Partial<${BusinessName}VO>) => {
|
||||
await getList();
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
<#if enableExport>
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
requestDownload(
|
||||
'${moduleName}/${businessName}/export',
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { BaseEntity<#if !table.tree>, PageQuery</#if> } from '@/api/types';
|
||||
|
||||
export interface ${BusinessName}VO {
|
||||
<#list columns as column>
|
||||
<#if column.list>
|
||||
|
||||
+5
-1
@@ -9,9 +9,10 @@
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
<!--[if lt IE 11]>
|
||||
<script>
|
||||
window.location.href = '/html/ie.html';
|
||||
window.location.href = '/ie.html';
|
||||
</script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
@@ -209,6 +210,9 @@
|
||||
<div class="load_title">正在加载系统资源,请耐心等待</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/tac/load.min.js"></script>
|
||||
<script src="/tac/css/tac.css"></script>
|
||||
<script src="/tac/js/tac.min.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+26
-30
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "ruoyi-vue-plus",
|
||||
"version": "6.0.0-BETA",
|
||||
"version": "6.0.0",
|
||||
"description": "RuoYi-Vue-Plus后台管理系统",
|
||||
"license": "MIT",
|
||||
"author": "LionLi",
|
||||
@@ -9,8 +9,10 @@
|
||||
"type": "git",
|
||||
"url": "https://gitee.com/JavaLionLi/plus-ui.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.34.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build --mode production",
|
||||
"build:dev": "vite build --mode development",
|
||||
"build:prod": "vite build --mode production",
|
||||
"dev": "vite serve --mode development",
|
||||
@@ -24,48 +26,49 @@
|
||||
"@highlightjs/vue-plugin": "2.1.2",
|
||||
"@iconify/vue": "^5.0.1",
|
||||
"@vueuse/core": "14.3.0",
|
||||
"@wangeditor-next/editor": "5.7.10",
|
||||
"@wangeditor-next/editor": "5.7.16",
|
||||
"@wangeditor-next/editor-for-vue": "5.1.14",
|
||||
"animate.css": "4.1.1",
|
||||
"await-to-js": "3.0.0",
|
||||
"axios": "1.17.0",
|
||||
"axios": "1.18.1",
|
||||
"crypto-js": "4.2.0",
|
||||
"echarts": "6.1.0",
|
||||
"element-plus": "2.14.1",
|
||||
"element-plus": "2.14.3",
|
||||
"highlight.js": "11.11.1",
|
||||
"image-conversion": "2.1.1",
|
||||
"jsencrypt": "3.5.4",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "3.0.4",
|
||||
"vue": "3.5.35",
|
||||
"pinia": "4.0.2",
|
||||
"vue": "3.5.40",
|
||||
"vue-cropper": "1.1.4",
|
||||
"vue-i18n": "11.4.5",
|
||||
"vue-i18n": "11.4.8",
|
||||
"vue-json-pretty": "2.6.0",
|
||||
"vue-router": "5.1.0",
|
||||
"vue-router": "5.2.0",
|
||||
"vue-types": "7.0.0",
|
||||
"vxe-table": "4.19.10"
|
||||
"vxe-pc-ui": "4.16.22",
|
||||
"vxe-table": "4.20.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/node": "^25.9.2",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/preset-attributify": "66.7.0",
|
||||
"@unocss/preset-wind3": "66.7.0",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vue/compiler-sfc": "3.5.35",
|
||||
"autoprefixer": "10.5.0",
|
||||
"oxfmt": "^0.53.0",
|
||||
"oxlint": "^1.68.0",
|
||||
"sass": "1.100.0",
|
||||
"@unocss/preset-attributify": "66.7.5",
|
||||
"@unocss/preset-wind3": "66.7.5",
|
||||
"@vitejs/plugin-vue": "^6.0.8",
|
||||
"@vue/compiler-sfc": "3.5.40",
|
||||
"autoprefixer": "10.5.4",
|
||||
"oxfmt": "^0.60.0",
|
||||
"oxlint": "^1.75.0",
|
||||
"sass": "1.102.0",
|
||||
"typescript": "^6.0.3",
|
||||
"unocss": "66.7.0",
|
||||
"unocss": "66.7.5",
|
||||
"unplugin-auto-import": "21.0.0",
|
||||
"unplugin-vue-components": "32.1.0",
|
||||
"unplugin-vue-setup-extend-plus": "1.0.1",
|
||||
"vite": "^8.0.16",
|
||||
"vite-plugin-svg-icons-ng": "^1.9.1",
|
||||
"vitest": "4.1.8",
|
||||
"vue-tsc": "^3.3.4"
|
||||
"vite": "^8.1.5",
|
||||
"vite-plugin-svg-icons-ng": "^1.9.2",
|
||||
"vitest": "4.1.10",
|
||||
"vue-tsc": "^3.3.8"
|
||||
},
|
||||
"browserslist": [
|
||||
"Chrome >= 87",
|
||||
@@ -76,12 +79,5 @@
|
||||
"engines": {
|
||||
"node": ">=20.19.0",
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"es5-ext",
|
||||
"esbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1133
-1364
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
packages:
|
||||
- '.'
|
||||
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
es5-ext: true
|
||||
esbuild: true
|
||||
|
||||
overrides:
|
||||
esbuild: 0.28.1
|
||||
@@ -0,0 +1,7 @@
|
||||
#tianai-captcha-parent{box-shadow:0 0 11px 0 #999;width:318px;height:318px;overflow:hidden;position:relative;z-index:997;box-sizing:border-box;border-radius:5px;padding:8px}#tianai-captcha-parent #tianai-captcha-box{height:260px;width:100%;position:relative;overflow:hidden}#tianai-captcha-parent #tianai-captcha-box .loading{width:120px;height:20px;-webkit-mask:linear-gradient(90deg, #000 70%, rgba(0, 0, 0, 0) 0) 0/20%;background:linear-gradient(#f7b645 0 0) 0/0% no-repeat rgba(221,221,221,.4196078431);animation:cartoon 1s infinite steps(6);margin:120px auto}@keyframes cartoon{100%{background-size:120%}}#tianai-captcha-parent #tianai-captcha-box #tianai-captcha{transform-style:preserve-3d;will-change:transform;transition-duration:.45s;transform:translateX(-300px)}#tianai-captcha-parent #tianai-captcha-bg-img{background-color:#fff;background-position:top;background-size:cover;z-index:-1;width:100%;height:100%;top:0;left:0;position:absolute;border-radius:6px}#tianai-captcha-parent .slider-bottom{height:19px;width:100%}#tianai-captcha-parent .slider-bottom .close-btn{width:20px;height:20px;background-image:url(../../tac/images/icon.png);background-repeat:no-repeat;background-position:0 -14px;float:right;margin-right:2px;cursor:pointer}#tianai-captcha-parent .slider-bottom .refresh-btn{width:20px;height:20px;background-image:url(../../tac/images/icon.png);background-position:0 -167px;background-repeat:no-repeat;float:right;margin-right:10px;cursor:pointer}#tianai-captcha-parent .slider-bottom .logo{height:30px;float:left}#tianai-captcha-parent .slider-move-shadow{animation:myanimation 2s infinite;height:100%;width:5px;background-color:#fff;position:absolute;top:0;left:0;filter:opacity(0.5);box-shadow:1px 1px 1px #fff;border-radius:50%}#tianai-captcha-parent #tianai-captcha-slider-move-track-mask{border-width:1px;border-style:solid;border-color:#00f4ab;width:0;height:32px;background-color:#a9ffe5;opacity:.5;position:absolute;top:-1px;left:-1px;border-radius:5px}
|
||||
#tianai-captcha{text-align:left;box-sizing:content-box;width:300px;height:260px;z-index:999}#tianai-captcha .slider-bottom .logo{height:30px}#tianai-captcha .slider-bottom{height:19px;width:100%}#tianai-captcha .content .tianai-captcha-tips{height:25px;width:100%;position:absolute;bottom:-25px;left:0;z-index:999;font-size:15px;line-height:25px;color:#fff;text-align:center;transition:bottom .3s ease-in-out}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-error{background-color:#ff5d39}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-success{background-color:#39c522}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-on{bottom:0}#tianai-captcha .content #tianai-captcha-loading{z-index:9999;background-color:#f5f5f5;text-align:center;height:100%;overflow:hidden;position:relative;display:flex;justify-content:center;align-items:center}#tianai-captcha .content #tianai-captcha-loading img{display:block;width:45px;height:45px}#tianai-captcha #tianai-captcha-slider-bg-canvas{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:5px}#tianai-captcha #tianai-captcha-slider-bg-div{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:5px}#tianai-captcha #tianai-captcha-slider-bg-div .tianai-captcha-slider-bg-div-slice{position:absolute}@keyframes myanimation{from{left:0}to{left:289px}}
|
||||
#tianai-captcha.tianai-captcha-slider{z-index:999;position:absolute;left:0;top:0;user-select:none}#tianai-captcha.tianai-captcha-slider .content{width:100%;height:180px;position:relative;overflow:hidden}#tianai-captcha.tianai-captcha-slider .bg-img-div{width:100%;height:100%;position:absolute;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .bg-img-div img{height:100%;width:100%;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-img-div{height:100%;position:absolute;left:0;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .slider-img-div #tianai-captcha-slider-move-img{height:100%}#tianai-captcha.tianai-captcha-slider .slider-move{height:34px;width:100%;margin:11px 0;position:relative}#tianai-captcha.tianai-captcha-slider .slider-move-track{position:relative;height:32px;line-height:32px;text-align:center;background:#f5f5f5;color:#999;transition:0s;font-size:14px;box-sizing:content-box;border:1px solid #f5f5f5;border-radius:4px}#tianai-captcha.tianai-captcha-slider .refresh-btn,#tianai-captcha.tianai-captcha-slider .close-btn{display:inline-block}#tianai-captcha.tianai-captcha-slider .slider-move{line-height:38px;font-size:14px;text-align:center;white-space:nowrap;color:#88949d;-moz-user-select:none;-webkit-user-select:none;user-select:none;filter:opacity(0.8)}#tianai-captcha.tianai-captcha-slider .slider-move .slider-move-btn{transform:translate(0px, 0px);position:absolute;top:-6px;left:0;width:63px;height:45px;background-color:#fff;background-repeat:no-repeat;background-size:contain;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-tip{margin-bottom:5px;font-weight:bold;font-size:15px;line-height:normal;color:#000}#tianai-captcha.tianai-captcha-slider .slider-move-btn:hover{cursor:pointer}
|
||||
#tianai-captcha.tianai-captcha-rotate .rotate-img-div{height:100%;text-align:center}#tianai-captcha.tianai-captcha-rotate .rotate-img-div img{height:100%;transform:rotate(0deg);display:inline-block}
|
||||
#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-img-div{background-size:100% 180px;position:absolute;transform:translate(0px, 0px);z-index:1;width:100%}#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-bg-img{width:100%;height:100%;position:absolute;transform:translate(0px, 0px);background-size:100% 180px}
|
||||
#tianai-captcha.tianai-captcha-disable{z-index:999;position:absolute;left:0;top:0}#tianai-captcha.tianai-captcha-disable .content{width:100%;height:180px;position:relative;overflow:hidden}#tianai-captcha.tianai-captcha-disable .content .bg-img-div{background-image:url(../../tac/images/dun.jpeg);width:100%;height:100%;overflow:hidden}#tianai-captcha.tianai-captcha-disable .content .bg-img-div #content-span{color:#fff;overflow:hidden;margin-top:132px;display:block;text-align:center}
|
||||
#tianai-captcha.tianai-captcha-word-click{box-sizing:border-box}#tianai-captcha.tianai-captcha-word-click .click-tip{position:relative;height:40px;width:100%}#tianai-captcha.tianai-captcha-word-click .click-tip .tip-img{height:35px;position:absolute;right:15px}#tianai-captcha.tianai-captcha-word-click .click-tip #tianai-captcha-click-track-font{font-size:18px;display:inline-block;height:40px;line-height:40px;position:absolute}#tianai-captcha.tianai-captcha-word-click .slider-bottom{position:relative;top:6px}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask{width:100%;height:100%;position:absolute;left:0;top:0}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask .click-span{position:absolute;left:0;top:0;border-radius:50px;background-color:#409eff;width:20px;height:20px;text-align:center;line-height:20px;color:#fff;border:2px solid #fff;box-sizing:content-box}#tianai-captcha.tianai-captcha-word-click .click-confirm-btn{width:100%;height:35px;border-radius:4px;background-image:linear-gradient(173deg, hsl(38.09, 91%, 57.89%) 0%, hsl(38.09, 89.38%, 71.74%) 100%);font-size:15px;text-align:center;box-sizing:border-box;line-height:35px;color:#fff;margin-top:3px}#tianai-captcha.tianai-captcha-word-click .click-confirm-btn:hover{cursor:pointer}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
||||
const Math=window.Math,head=document.getElementsByTagName("head")[0],TIMEOUT=1e4,TAC_LOADING_DIV='<div id="tac-loading" style="\n border: 1px solid #eee;\n /* background-color: #409EFF; */\n border-radius: 5px;\n width: 318px;\n height: 318px;\n line-height: 318px;\n color: #606266;\n text-align: center;\n position: relative;\n box-sizing: border-box;\n">璇风◢绛�...</div>';function showLoading(e){var t=document.querySelector(e);t&&(t.innerHTML=TAC_LOADING_DIV)}function hideLoading(e){let t=document.querySelector(e);t&&(t.innerHTML="")}function loadCaptchaScript(e,t,n,r,o){const i=e.scriptUrls,c=e.cssUrls,l=e.timeout||TIMEOUT;let s=i.length+c.length;function d(e,i){if(s--,e&&0===s){if(hideLoading(t.bindEl),!window.TAC)throw new Error("TAC鏈姞杞斤紝璇锋鏌ュ湴鍧€鏄惁姝g‘");r(new TAC(t,n))}else e||(hideLoading(t.bindEl),o(i))}setTimeout(()=>{0!==s&&showLoading(t.bindEl)},10),i.forEach(function(e){loadResource("string"==typeof e?{url:e}:e,d,"script",l)}),c.forEach(function(e){loadResource("string"==typeof e?{url:e}:e,d,"link",l)})}function loadResource(e,t,n="script",r){if(document.querySelector(`${n}[${"script"===n?"src":"href"}="${e.url}"]`))return void t(!0,e);let o=!1;const i=document.createElement(n);"link"===n?i.rel="stylesheet":i.async=!0,i["script"===n?"src":"href"]=e.url;let c;i.onload=i.onreadystatechange=(()=>{o||i.readyState&&"loaded"!==i.readyState&&"complete"!==i.readyState||function t(n){e.checkOnReady?c=setTimeout(()=>{e.checkOnReady()?n():t(n)},10):n()}(()=>{o=!0,setTimeout(()=>t(o,e),0)})}),i.onerror=(()=>{t(o=!1,e)}),head.appendChild(i),setTimeout(()=>{o||(c&&clearTimeout(c),i.onload=i.onerror=null,i.remove&&i.remove(),t(o,e))},r||TIMEOUT)}function loadTAC(e,t,n){return new Promise((r,o)=>{let i={..."string"==typeof e?{url:e}:e};i.url&&(i.url.endsWith("/")||(i.url+="/"),i.scriptUrls||(i.scriptUrls=[i.url+"js/tac.min.js"]),i.cssUrls||(i.cssUrls=[i.url+"css/tac.css"])),i.scriptUrls&&i.cssUrls?loadCaptchaScript(i,t,n,r,o):o("璇锋寜鐓ф枃妗i厤缃畉ac")})}setTimeout(()=>{let e=document.scripts,t=null;for(let n=0;n<e.length;n++)if(e[n].src.indexOf("load.js")>1||e[n].src.indexOf("load.min.js")>1){t=e[n].src.substring(e[n].src.indexOf("/"),e[n].src.lastIndexOf("/"));break}},100),window.loadCaptchaScript=loadCaptchaScript,window.loadTAC=loadTAC,window.initTAC=loadTAC;
|
||||
+1
-1
@@ -73,7 +73,7 @@ export function logout() {
|
||||
*/
|
||||
export function getCodeImg(): AxiosPromise<VerifyCodeResult> {
|
||||
return request({
|
||||
url: '/auth/code',
|
||||
url: '/resource/captcha/image',
|
||||
headers: {
|
||||
isToken: false
|
||||
},
|
||||
|
||||
@@ -36,7 +36,8 @@ export interface UserVO extends BaseEntity {
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
gender: string;
|
||||
avatar: string;
|
||||
avatar?: string | number;
|
||||
avatarUrl?: string;
|
||||
status: string;
|
||||
delFlag: string;
|
||||
loginIp: string;
|
||||
@@ -67,6 +68,7 @@ export interface UserForm {
|
||||
gender?: string;
|
||||
status: string;
|
||||
remark?: string;
|
||||
avatar?: string | number;
|
||||
postIds: string[];
|
||||
roleIds: string[];
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { VxeTableInstance } from 'vxe-table';
|
||||
import type { VxeTableInstance } from 'vxe-table';
|
||||
import animateConfig from '@/animate';
|
||||
import api from '@/api/system/role';
|
||||
import { RoleVO, RoleQuery } from '@/api/system/role/types';
|
||||
|
||||
@@ -19,33 +19,38 @@
|
||||
</template>
|
||||
<template v-if="!modelCollapsed">
|
||||
<el-input v-model="filterText" :placeholder="placeholder" prefix-icon="Search" clearable />
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
class="mt-2 dept-tree"
|
||||
:node-key="nodeKey"
|
||||
:data="data"
|
||||
:props="mergedTreeProps as any"
|
||||
:expand-on-click-node="false"
|
||||
:filter-node-method="internalFilterNode"
|
||||
highlight-current
|
||||
default-expand-all
|
||||
@node-click="onNodeClick"
|
||||
>
|
||||
<template #default="{ data: nodeData }">
|
||||
<span class="tree-node-label" :class="{ 'is-disabled': isNodeDisabled(nodeData) }">
|
||||
<span>{{ getNodeLabel(nodeData) }}</span>
|
||||
<el-tooltip v-if="isNodeDisabled(nodeData)" content="停用" placement="top">
|
||||
<el-icon class="tree-node-disabled-icon"><CircleCloseFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
<div ref="treeWrapRef" class="dept-tree-wrap">
|
||||
<el-tree-v2
|
||||
ref="treeRef"
|
||||
class="dept-tree"
|
||||
:data="treeData"
|
||||
:props="mergedTreeProps as any"
|
||||
:height="treeHeight"
|
||||
:item-size="28"
|
||||
:expand-on-click-node="false"
|
||||
:default-expanded-keys="defaultExpandedKeys"
|
||||
:filter-method="internalFilterNode"
|
||||
highlight-current
|
||||
@node-click="onNodeClick"
|
||||
>
|
||||
<template #default="{ node }">
|
||||
<span class="tree-node-label" :class="{ 'is-disabled': isNodeDisabled(node.data) }">
|
||||
<span>{{ node.label }}</span>
|
||||
<el-tooltip v-if="isNodeDisabled(node.data)" content="停用" placement="top">
|
||||
<el-icon class="tree-node-disabled-icon"><CircleCloseFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree-v2>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TreeKey, TreeV2Instance } from 'element-plus';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 面板标题 */
|
||||
@@ -91,23 +96,34 @@ const modelCollapsed = computed({
|
||||
});
|
||||
|
||||
const filterText = ref('');
|
||||
const treeRef = ref<ElTreeInstance>();
|
||||
const treeRef = ref<TreeV2Instance>();
|
||||
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 label = props.treeProps?.label ?? 'label';
|
||||
const children = props.treeProps?.children ?? 'children';
|
||||
const value = props.treeProps?.value ?? props.nodeKey;
|
||||
if (!props.disabledField) {
|
||||
return props.treeProps;
|
||||
return {
|
||||
...props.treeProps,
|
||||
value,
|
||||
label,
|
||||
children
|
||||
};
|
||||
}
|
||||
return {
|
||||
...props.treeProps,
|
||||
value,
|
||||
label,
|
||||
children,
|
||||
disabled: props.treeProps?.disabled ?? props.disabledField
|
||||
};
|
||||
});
|
||||
|
||||
const getNodeLabel = (data: any) => {
|
||||
const labelField = props.treeProps?.label ?? 'label';
|
||||
return data?.[labelField] ?? '';
|
||||
};
|
||||
|
||||
const isNodeDisabled = (data: any) => {
|
||||
if (!props.disabledField) {
|
||||
return false;
|
||||
@@ -119,6 +135,23 @@ const toggleCollapsed = () => {
|
||||
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) => {
|
||||
if (props.filterNodeMethod) {
|
||||
return props.filterNodeMethod(value, data);
|
||||
@@ -127,16 +160,32 @@ const internalFilterNode = (value: string, data: any) => {
|
||||
return data[props.filterField]?.indexOf(value) !== -1;
|
||||
};
|
||||
|
||||
watchEffect(
|
||||
() => {
|
||||
treeRef.value?.filter(filterText.value);
|
||||
const filterTree = useDebounceFn(() => {
|
||||
treeRef.value?.filter(filterText.value);
|
||||
}, 200);
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ flush: 'post' }
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const onNodeClick = (data: any, node: any, component: any) => {
|
||||
if (isNodeDisabled(data)) {
|
||||
treeRef.value?.setCurrentKey(undefined);
|
||||
treeRef.value?.setCurrentKey(undefined as any);
|
||||
return;
|
||||
}
|
||||
emit('node-click', data, node, component);
|
||||
@@ -224,14 +273,17 @@ $mobile-breakpoint: 900px;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.dept-tree {
|
||||
padding-top: 6px;
|
||||
.dept-tree-wrap {
|
||||
margin-top: 8px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 180px;
|
||||
max-height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dept-tree {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-right: 4px;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { VxeTableInstance } from 'vxe-table';
|
||||
import type { VxeTableInstance } from 'vxe-table';
|
||||
import animateConfig from '@/animate';
|
||||
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
|
||||
import api from '@/api/system/user';
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
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,12 +13,14 @@
|
||||
|
||||
<script setup name="AppMain" lang="ts">
|
||||
import animateConfig from '@/animate';
|
||||
import { useFullHeightTable } from '@/hooks/table/useFullHeightTable';
|
||||
import { useSettingsStore } from '@/store/modules/settings';
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView';
|
||||
import IframeToggle from './IframeToggle/index.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
useFullHeightTable();
|
||||
|
||||
// 随机动画集合
|
||||
const animate = ref<string>('');
|
||||
@@ -62,6 +64,14 @@ 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 {
|
||||
padding-top: 76px;
|
||||
min-height: calc(100vh - 76px);
|
||||
|
||||
@@ -154,6 +154,13 @@
|
||||
</span>
|
||||
</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-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
|
||||
@@ -260,6 +267,7 @@ const saveSetting = () => {
|
||||
settings.value.theme = storeSettings.value.theme;
|
||||
settings.value.navType = storeSettings.value.navType;
|
||||
settings.value.radiusBase = storeSettings.value.radiusBase;
|
||||
settings.value.fullHeightTable = storeSettings.value.fullHeightTable;
|
||||
setTimeout(() => {
|
||||
modal.closeLoading();
|
||||
}, 1000);
|
||||
|
||||
+7
-4
@@ -1,13 +1,15 @@
|
||||
import HighLight from '@highlightjs/vue-plugin';
|
||||
import { ElDialog } from 'element-plus';
|
||||
import { createApp } from 'vue';
|
||||
import VXETable from 'vxe-table';
|
||||
import VxeUIPlugin, { VxeUI } from 'vxe-pc-ui';
|
||||
import VxeTablePlugin from 'vxe-table';
|
||||
import 'virtual:uno.css';
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||
import '@/assets/styles/index.scss';
|
||||
import 'highlight.js/styles/atom-one-dark.css';
|
||||
import 'highlight.js/lib/common';
|
||||
import 'virtual:svg-icons-register';
|
||||
import 'vxe-pc-ui/lib/style.css';
|
||||
import 'vxe-table/lib/style.css';
|
||||
import i18n from '@/lang/index';
|
||||
import ElementIcons from '@/plugins/svgicon';
|
||||
@@ -18,7 +20,7 @@ import './permission';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
|
||||
VXETable.setConfig({
|
||||
VxeUI.setConfig({
|
||||
zIndex: 999999
|
||||
});
|
||||
|
||||
@@ -28,10 +30,11 @@ const app = createApp(App);
|
||||
|
||||
app.use(HighLight);
|
||||
app.use(ElementIcons);
|
||||
app.use(router);
|
||||
app.use(store);
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
app.use(VXETable);
|
||||
app.use(VxeUIPlugin);
|
||||
app.use(VxeTablePlugin);
|
||||
app.use(plugins);
|
||||
directive(app);
|
||||
|
||||
|
||||
+6
-1
@@ -81,6 +81,11 @@ const setting: DefaultSettings = {
|
||||
/**
|
||||
* 页面圆角大小
|
||||
*/
|
||||
radiusBase: 14
|
||||
radiusBase: 14,
|
||||
|
||||
/**
|
||||
* 表格全高内部滚动
|
||||
*/
|
||||
fullHeightTable: true
|
||||
};
|
||||
export default setting;
|
||||
|
||||
@@ -16,7 +16,8 @@ export const useSettingsStore = defineStore('setting', () => {
|
||||
sideTheme: defaultSettings.sideTheme,
|
||||
theme: defaultSettings.theme,
|
||||
navType: defaultSettings.navType,
|
||||
radiusBase: defaultSettings.radiusBase
|
||||
radiusBase: defaultSettings.radiusBase,
|
||||
fullHeightTable: defaultSettings.fullHeightTable
|
||||
});
|
||||
const title = ref<string>(defaultSettings.title);
|
||||
const theme = ref<string>(storageSetting.value.theme);
|
||||
@@ -32,6 +33,7 @@ export const useSettingsStore = defineStore('setting', () => {
|
||||
const dark = ref<boolean>(defaultSettings.dark);
|
||||
const navType = ref<NavTypeEnum>(storageSetting.value.navType || NavTypeEnum.LEFT);
|
||||
const radiusBase = ref<number>(storageSetting.value.radiusBase ?? defaultSettings.radiusBase);
|
||||
const fullHeightTable = ref<boolean>(storageSetting.value.fullHeightTable ?? defaultSettings.fullHeightTable);
|
||||
|
||||
const setTitle = (value: string) => {
|
||||
title.value = value;
|
||||
@@ -52,6 +54,7 @@ export const useSettingsStore = defineStore('setting', () => {
|
||||
dark,
|
||||
navType,
|
||||
radiusBase,
|
||||
fullHeightTable,
|
||||
setTitle
|
||||
};
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
const user = data.user;
|
||||
const profile = user.avatar == '' || user.avatar == null ? defAva : user.avatar;
|
||||
const profile = user.avatarUrl == '' || user.avatarUrl == null ? defAva : user.avatarUrl;
|
||||
|
||||
if (data.roles && data.roles.length > 0) {
|
||||
// 验证返回的roles是否是一个非空数组
|
||||
|
||||
Vendored
+22
@@ -3,6 +3,23 @@ import { LanguageEnum } from '@/enums/LanguageEnum';
|
||||
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/**
|
||||
* 天艾验证码初始化方法
|
||||
* @param assetPath 静态资源目录,public下tac文件夹填 "/tac"
|
||||
* @param config 验证码配置项
|
||||
*/
|
||||
initTAC: (
|
||||
assetPath: string,
|
||||
captchaConfig: {
|
||||
requestCaptchaDataUrl: string;
|
||||
validCaptchaUrl: string;
|
||||
bindEl: string;
|
||||
validSuccess: (res, c, t) => void
|
||||
}
|
||||
, style: { logoUrl: null }) => Promise<{ init: () => void; destroyWindow: () => void }>;
|
||||
}
|
||||
|
||||
/** vue Instance */
|
||||
type ComponentInternalInstance = ComponentInstance;
|
||||
|
||||
@@ -131,6 +148,11 @@ declare global {
|
||||
* 页面圆角大小
|
||||
*/
|
||||
radiusBase: number;
|
||||
|
||||
/**
|
||||
* 表格全高内部滚动
|
||||
*/
|
||||
fullHeightTable: boolean;
|
||||
}
|
||||
|
||||
interface DefaultSettings extends LayoutSetting {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// 返回状态定义
|
||||
export type CaptchaResult = -1 | 0 | 1
|
||||
type CaptchaStatusCallback = (status: CaptchaResult) => void
|
||||
|
||||
/**
|
||||
* 唤起 tianai TAC行为验证码
|
||||
* ✅ 动态创建全局遮罩容器,无需模板写div
|
||||
* ✅ 弹窗视口永久居中,全局最高层级
|
||||
* ✅ 完全对齐官方loadTAC配置参数
|
||||
* @param onStatusChange 状态回调,0=未完成
|
||||
* @returns Promise<CaptchaResult> 1成功,-1失败/关闭/加载异常
|
||||
*/
|
||||
export async function showTACCaptcha(onStatusChange?: CaptchaStatusCallback): Promise<CaptchaResult> {
|
||||
await nextTick()
|
||||
return new Promise((resolve) => {
|
||||
// ========= 动态创建全局全屏遮罩容器 =========
|
||||
const maskWrap = document.createElement('div')
|
||||
const tempId = 'tac-captcha-temp-' + Date.now()
|
||||
maskWrap.id = tempId
|
||||
|
||||
// 全局顶层全屏遮罩 + flex居中
|
||||
maskWrap.style.position = 'fixed'
|
||||
maskWrap.style.top = '0'
|
||||
maskWrap.style.left = '0'
|
||||
maskWrap.style.width = '100vw'
|
||||
maskWrap.style.height = '100vh'
|
||||
maskWrap.style.zIndex = '99999'
|
||||
maskWrap.style.backgroundColor = 'rgba(0,0,0,0.5)'
|
||||
maskWrap.style.display = 'flex'
|
||||
maskWrap.style.alignItems = 'center'
|
||||
maskWrap.style.justifyContent = 'center'
|
||||
maskWrap.style.margin = '0'
|
||||
maskWrap.style.padding = '0'
|
||||
maskWrap.style.overflow = 'hidden'
|
||||
document.body.appendChild(maskWrap)
|
||||
|
||||
const bindSelector = `#${tempId}`
|
||||
// 打开弹窗,状态=未完成
|
||||
onStatusChange?.(0)
|
||||
|
||||
const captchaConfig = {
|
||||
// 请求验证码接口
|
||||
requestCaptchaDataUrl: "http://localhost:8080/resource/captcha/behavior/generate",
|
||||
// 验证验证码接口
|
||||
validCaptchaUrl: "http://localhost:8080/resource/captcha/behavior/verify",
|
||||
// 绑定的div
|
||||
bindEl: bindSelector,
|
||||
// 验证成功回调函数
|
||||
validSuccess: (res, c, t) => {
|
||||
console.log("验证码验证成功回调...")
|
||||
// 销毁验证码
|
||||
t.destroyWindow();
|
||||
document.body.removeChild(maskWrap)
|
||||
console.log(res)
|
||||
onStatusChange?.(1)
|
||||
resolve(1) // ✅ 成功,返回1,await解除阻塞
|
||||
},
|
||||
// 验证失败的回调函数(可忽略,如果不自定义 validFail 方法时,会使用默认的)
|
||||
validFail: (res, c, t) => {
|
||||
console.log("验证码验证失败回调...");
|
||||
// 验证失败后重新拉取验证码
|
||||
t.reloadCaptcha();
|
||||
},
|
||||
// 刷新按钮回调事件
|
||||
btnRefreshFun: (el, tac) => {
|
||||
console.log("刷新按钮触发事件...");
|
||||
tac.reloadCaptcha();
|
||||
},
|
||||
// 关闭按钮回调事件
|
||||
btnCloseFun: (el, tac) => {
|
||||
console.log("关闭按钮触发事件...");
|
||||
tac.destroyWindow();
|
||||
document.body.removeChild(maskWrap)
|
||||
onStatusChange?.(-1)
|
||||
resolve(-1) // ✅ 用户关闭弹窗,返回 -1
|
||||
}
|
||||
}
|
||||
// 一些样式配置, 可不传
|
||||
let style = {
|
||||
logoUrl: null// 去除logo
|
||||
}
|
||||
// 参数1 为 tac文件是目录地址, 目录里包含 tac的js和css等文件
|
||||
// 参数2 为 tac验证码相关配置
|
||||
// 参数3 为 tac窗口一些样式配置
|
||||
window.initTAC("./tac", captchaConfig, style).then(tac => {
|
||||
tac.init(); // 调用init则显示验证码
|
||||
}).catch(e => {
|
||||
console.log("初始化tac失败", e);
|
||||
document.body.removeChild(maskWrap)
|
||||
onStatusChange?.(-1)
|
||||
resolve(-1) // ✅ 初始化异常,返回 -1
|
||||
})
|
||||
})
|
||||
}
|
||||
+39
-2
@@ -9,6 +9,9 @@ import { parsePushMessage, resolveNoticeGroup, resolveNoticeTitle, shouldAppendN
|
||||
|
||||
let closePushConnection: (() => void) | undefined;
|
||||
let stopPushWatchers: Array<() => void> = [];
|
||||
const KICKED_MESSAGE = 'kicked';
|
||||
let pushKicked = false;
|
||||
let resumePushTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const formatNoticeTime = (timestamp?: number | string) => {
|
||||
const time = timestamp ? new Date(timestamp) : new Date();
|
||||
@@ -44,6 +47,15 @@ const appendNotice = (raw: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handlePushMessage = (raw: string) => {
|
||||
if (raw === KICKED_MESSAGE) {
|
||||
pushKicked = true;
|
||||
closePush();
|
||||
return;
|
||||
}
|
||||
appendNotice(raw);
|
||||
};
|
||||
|
||||
const toNoticeItem = (item: MessageVO) => {
|
||||
const userId = useUserStore().userId;
|
||||
const timestamp = item.createTime ? new Date(item.createTime).getTime() : Date.now();
|
||||
@@ -91,7 +103,7 @@ const initSsePush = (url: string) => {
|
||||
|
||||
const stopDataWatch = watch(data, () => {
|
||||
if (!data.value) return;
|
||||
appendNotice(data.value);
|
||||
handlePushMessage(data.value);
|
||||
data.value = null;
|
||||
});
|
||||
stopPushWatchers.push(stopErrorWatch, stopDataWatch);
|
||||
@@ -115,7 +127,7 @@ const initWsPush = (url: string) => {
|
||||
if (String(e.data) === 'pong') {
|
||||
return;
|
||||
}
|
||||
appendNotice(String(e.data));
|
||||
handlePushMessage(String(e.data));
|
||||
}
|
||||
});
|
||||
closePushConnection = close;
|
||||
@@ -126,6 +138,7 @@ export const initPush = () => {
|
||||
if (import.meta.env.VITE_APP_MESSAGE_ENABLED === 'false') {
|
||||
return;
|
||||
}
|
||||
pushKicked = false;
|
||||
const path = import.meta.env.VITE_APP_MESSAGE_PATH || '/resource/message';
|
||||
const transport = import.meta.env.VITE_APP_MESSAGE_TRANSPORT || 'sse';
|
||||
if (transport.toLowerCase() === 'websocket') {
|
||||
@@ -153,3 +166,27 @@ export const closePush = () => {
|
||||
stopPushWatchers.forEach(stop => stop());
|
||||
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);
|
||||
|
||||
+19
-354
@@ -1,111 +1,29 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<section class="hero-panel">
|
||||
<div class="hero-copy">
|
||||
<h1>RuoYi-Vue-Plus 控制台</h1>
|
||||
<p>
|
||||
企业级后台管理系统 重写 RuoYi-Vue 所有功能 集成 Sa-Token、Mybatis-Plus、WarmFlow、SpringDoc、Hutool、OSS
|
||||
等组件
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<el-button type="primary" @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">查看源码</el-button>
|
||||
<el-button plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')">
|
||||
更新日志
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="content-grid">
|
||||
<section class="section-card">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>项目矩阵</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-list">
|
||||
<article v-for="product in products" :key="product.name" class="product-card">
|
||||
<div class="product-top">
|
||||
<div>
|
||||
<h3>{{ product.name }}</h3>
|
||||
<p>{{ product.summary }}</p>
|
||||
</div>
|
||||
<span class="product-version">{{ product.version }}</span>
|
||||
</div>
|
||||
<div class="product-tags">
|
||||
<el-tag v-for="tag in product.tags" :key="tag" effect="plain">{{ tag }}</el-tag>
|
||||
</div>
|
||||
<div class="product-actions">
|
||||
<el-button type="primary" plain @click="goTarget(product.primaryUrl)">
|
||||
{{ product.primaryLabel }}
|
||||
</el-button>
|
||||
<el-button plain @click="goTarget(product.secondaryUrl)">{{ product.secondaryLabel }}</el-button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-card capability-card">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>能力地图</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="capability-groups">
|
||||
<article v-for="group in capabilityGroups" :key="group.title" class="capability-group">
|
||||
<h3>{{ group.title }}</h3>
|
||||
<ul>
|
||||
<li v-for="item in group.items" :key="item">{{ item }}</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<button @click="login">登录</button>
|
||||
<div id="captcha-box"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Index" lang="ts">
|
||||
const products = [
|
||||
{
|
||||
name: 'RuoYi-Vue-Plus',
|
||||
version: 'v6.0.0',
|
||||
summary: '面向分布式集群场景的后台管理系统,保持现有业务接口与权限逻辑,适合先完成前端壳升级。',
|
||||
tags: ['Vue 3', 'Element Plus', 'Spring Boot', 'Sa-Token'],
|
||||
primaryLabel: '访问 GitHub',
|
||||
primaryUrl: 'https://github.com/dromara/RuoYi-Vue-Plus',
|
||||
secondaryLabel: '查看更新日志',
|
||||
secondaryUrl: 'https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog'
|
||||
},
|
||||
{
|
||||
name: 'RuoYi-Cloud-Plus',
|
||||
version: 'v6.0.0',
|
||||
summary: '微服务通用权限管理系统,适合更复杂的服务治理场景,也可以沿用同样的前端升级思路。',
|
||||
tags: ['Spring Cloud', 'Gateway', 'Nacos', 'Dubbo'],
|
||||
primaryLabel: '访问 GitHub',
|
||||
primaryUrl: 'https://github.com/dromara/RuoYi-Cloud-Plus',
|
||||
secondaryLabel: '查看更新日志',
|
||||
secondaryUrl: 'https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog'
|
||||
}
|
||||
];
|
||||
<script setup lang="ts">
|
||||
|
||||
const capabilityGroups = [
|
||||
{
|
||||
title: '后端基建',
|
||||
items: ['Spring Boot / Spring Cloud', 'Sa-Token 认证与权限', 'MySQL / Redis', '代码生成器']
|
||||
},
|
||||
{
|
||||
title: '平台能力',
|
||||
items: ['动态菜单与按钮权限', '监控、日志、在线用户', '任务调度与工作流', '文件存储与多云适配']
|
||||
},
|
||||
{
|
||||
title: '前端方向',
|
||||
items: ['UI 卡片化', '主题与布局统一', '通用页面容器规范化', '企业化布局']
|
||||
}
|
||||
];
|
||||
|
||||
const goTarget = (url: string) => {
|
||||
window.open(url, '__blank');
|
||||
};
|
||||
import {CaptchaResult,showTACCaptcha} from "@/utils/captcha";
|
||||
|
||||
async function login() {
|
||||
const captchaRes: CaptchaResult = await showTACCaptcha((status) => {
|
||||
console.log('验证码实时状态:', status)
|
||||
})
|
||||
switch (captchaRes) {
|
||||
case 1:
|
||||
console.log("✅验证码校验成功,拿token调用登录接口")
|
||||
// todo: 在这里调用登录,带上后端返回的token
|
||||
break
|
||||
case -1:
|
||||
console.log("❌ 关闭弹窗 / 加载失败")
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -114,257 +32,4 @@ const goTarget = (url: string) => {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.hero-panel,
|
||||
.section-card {
|
||||
border-radius: 28px;
|
||||
border: 1px solid var(--app-surface-border);
|
||||
background: var(--app-surface-bg);
|
||||
box-shadow: var(--app-shadow-sm);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.3fr) minmax(280px, 0.7fr);
|
||||
gap: 18px;
|
||||
padding: 30px;
|
||||
background: radial-gradient(circle at top left, rgba(53, 109, 255, 0.16), transparent 30%), var(--app-surface-bg);
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(30px, 4vw, 46px);
|
||||
line-height: 1.06;
|
||||
letter-spacing: -0.04em;
|
||||
color: var(--app-text-title);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
max-width: 760px;
|
||||
color: var(--app-text-muted);
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(53, 109, 255, 0.12);
|
||||
color: var(--app-accent-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 18px 20px;
|
||||
border-radius: 22px;
|
||||
background: var(--app-elevated-soft-bg);
|
||||
border: 1px solid var(--app-surface-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
strong {
|
||||
color: var(--app-text-title);
|
||||
font-size: 24px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--app-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(300px, 0.85fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
|
||||
h2 {
|
||||
margin: 6px 0 0;
|
||||
color: var(--app-text-title);
|
||||
font-size: 26px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
}
|
||||
|
||||
.section-kicker {
|
||||
color: var(--app-text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.product-list,
|
||||
.capability-groups {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
padding: 22px;
|
||||
border-radius: 24px;
|
||||
background: var(--app-elevated-soft-bg);
|
||||
border: 1px solid var(--app-surface-border);
|
||||
transition:
|
||||
transform 0.25s ease,
|
||||
box-shadow 0.25s ease,
|
||||
border-color 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--app-shadow-sm);
|
||||
border-color: rgba(53, 109, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.product-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px;
|
||||
color: var(--app-text-title);
|
||||
font-size: 22px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--app-text-muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
.product-version {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(53, 109, 255, 0.12);
|
||||
color: var(--app-accent-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.product-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.capability-card {
|
||||
background: radial-gradient(circle at top right, rgba(14, 165, 233, 0.12), transparent 28%), var(--app-surface-bg);
|
||||
}
|
||||
|
||||
.capability-group {
|
||||
padding: 18px 18px 18px 20px;
|
||||
border-radius: 22px;
|
||||
background: var(--app-elevated-soft-bg);
|
||||
border: 1px solid var(--app-surface-border);
|
||||
|
||||
h3 {
|
||||
margin: 0 0 12px;
|
||||
color: var(--app-text-title);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
padding-left: 16px;
|
||||
color: var(--app-text-muted);
|
||||
line-height: 1.7;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 10px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--app-accent-strong);
|
||||
box-shadow: 0 0 0 5px rgba(53, 109, 255, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.hero-panel,
|
||||
.content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-panel,
|
||||
.section-card {
|
||||
padding: 20px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.product-top {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
html.dark {
|
||||
.hero-panel {
|
||||
background: radial-gradient(circle at top left, rgba(53, 109, 255, 0.18), transparent 30%), var(--app-surface-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -121,6 +121,7 @@ import { authRouterUrl } from '@/api/system/social/auth';
|
||||
import { LoginData } from '@/api/types';
|
||||
import { HttpStatus } from '@/enums/RespEnum';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import {CaptchaResult, showTACCaptcha} from "@/utils/captcha";
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE;
|
||||
const currentYear = new Date().getFullYear();
|
||||
@@ -185,6 +186,26 @@ const handleLogin = () => {
|
||||
loginRef.value?.validate(async (valid: boolean, fields: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
const captchaRes: CaptchaResult = await showTACCaptcha((status) => {
|
||||
console.log('验证码实时状态:', status)
|
||||
})
|
||||
console.log("captchaRes结果:", captchaRes) // 打印看实际返回值!重点调试
|
||||
|
||||
switch (captchaRes) {
|
||||
case 1:
|
||||
console.log("✅验证码校验成功,拿token调用登录接口")
|
||||
break // 跳出switch,继续往下执行登录
|
||||
case -1:
|
||||
console.log("❌ 关闭弹窗 / 加载失败")
|
||||
loading.value = false;
|
||||
return; // 退出当前async回调,不再执行后面登录代码
|
||||
default:
|
||||
console.log("⚠️ 验证码其他状态", captchaRes)
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// ========== 验证码成功才会执行到这里 ==========
|
||||
if (loginForm.value.rememberMe) {
|
||||
localStorage.setItem('username', String(loginForm.value.username));
|
||||
localStorage.setItem('rememberMe', String(loginForm.value.rememberMe));
|
||||
@@ -210,6 +231,7 @@ const handleLogin = () => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const getCode = async () => {
|
||||
const res = await getCodeImg();
|
||||
const { data } = res;
|
||||
|
||||
@@ -356,7 +356,7 @@ const handleExport = () => {
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`config_${new Date().getTime()}.xlsx`
|
||||
`operlog_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
onMounted(() => {
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="220">
|
||||
<template #default="scope">
|
||||
<div class="menu-name-cell">
|
||||
<svg-icon v-if="scope.row.icon" :icon-class="scope.row.icon" />
|
||||
<svg-icon v-if="isMenuIconVisible(scope.row.icon)" :icon-class="scope.row.icon" />
|
||||
<span class="menu-name-text">{{ scope.row.menuName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -431,6 +431,11 @@ const { dialog, openDialog, closeDialog, setTitle } = useDialogState();
|
||||
|
||||
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 } => {
|
||||
if (menu.menuType === MenuTypeEnum.F) {
|
||||
return { label: '按钮', type: 'warning' };
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<template>
|
||||
<!-- 导入表 -->
|
||||
<el-dialog v-model="visible" title="导入表" width="1100px" top="5vh" append-to-body>
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="数据源" prop="dataName">
|
||||
|
||||
@@ -170,8 +170,8 @@
|
||||
<el-tab-pane
|
||||
v-for="(value, key) in preview.data"
|
||||
:key="value"
|
||||
:label="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
|
||||
:name="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
|
||||
:label="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.ftl'))"
|
||||
:name="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.ftl'))"
|
||||
>
|
||||
<el-link
|
||||
v-copyText="value"
|
||||
|
||||
+11
-2
@@ -1,5 +1,5 @@
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import createPlugins from './vite/plugins';
|
||||
import createPlugins from './vite/plugins/index';
|
||||
import autoprefixer from 'autoprefixer'; // css自动添加兼容性前缀
|
||||
|
||||
export default defineConfig(({ mode, command }) => {
|
||||
@@ -15,13 +15,22 @@ export default defineConfig(({ mode, command }) => {
|
||||
},
|
||||
// https://cn.vitejs.dev/config/#resolve-extensions
|
||||
plugins: createPlugins(env, command === 'build'),
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
rolldownOptions: {
|
||||
checks: {
|
||||
invalidAnnotation: false,
|
||||
pluginTimings: false
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: Number(env.VITE_APP_PORT),
|
||||
open: true,
|
||||
proxy: {
|
||||
[env.VITE_APP_BASE_API]: {
|
||||
target: 'http://localhost:8080',
|
||||
target: env.VITE_API_TARGET,
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewrite: path => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
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,6 +6,7 @@ import createComponents from './components';
|
||||
import createSvgIconsPlugin from './svg-icon';
|
||||
import createCompression from './compression';
|
||||
import createSetupExtend from './setup-extend';
|
||||
import { viteCheckTransitionPlugin } from "./check-transition";
|
||||
|
||||
export default (viteEnv: any, isBuild = false): [] => {
|
||||
const vitePlugins: any = [];
|
||||
@@ -16,5 +17,6 @@ export default (viteEnv: any, isBuild = false): [] => {
|
||||
vitePlugins.push(createCompression(viteEnv));
|
||||
vitePlugins.push(createSvgIconsPlugin());
|
||||
vitePlugins.push(createSetupExtend());
|
||||
vitePlugins.push(viteCheckTransitionPlugin())
|
||||
return vitePlugins;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user