Merge remote-tracking branch 'upstream/master' into dev

This commit is contained in:
diamont1001
2024-06-27 16:10:30 +08:00
63 changed files with 2491 additions and 2643 deletions

View File

@@ -1,9 +1,46 @@
# 2.11.5 (2024/6/18)
## ✨ feat
- 支持后端文件导入([#142](https://github.com/youlaitech/vue3-element-admin/pull/142)) [@cshaptx4869](https://github.com/cshaptx4869)
## 🐛 fix
- vue-dev-tools 插件导致菜单路由切换卡死,暂时关闭 ([28349e](https://github.com/youlaitech/vue3-element-admin/commit/28349efe147afab36531ba148eaac3a448fe6c71)) [@haoxianrui](https://github.com/haoxianrui)
# 2.11.4 (2024/6/16)
## ✨ feat
- 操作栏增加render配置参数([#138](https://github.com/youlaitech/vue3-element-admin/pull/140)) [@cshaptx4869](https://github.com/cshaptx4869)
- 左侧工具栏增加type配置参数([#141](https://github.com/youlaitech/vue3-element-admin/pull/141)) [@diamont1001](https://github.com/diamont1001)
## ♻️ refactor
- 更换权限分配弹窗类型为 drawer 并添加父子联动开关([2d9193](https://github.com/youlaitech/vue3-element-admin/commit/2d9193c47fd224f01f82b9c0b2bbeb5e7cb33584)) [@haoxianrui](https://github.com/haoxianrui)
# 2.11.3 (2024/6/11)
## ✨ feat
- 支持默认工具栏的导入([#138](https://github.com/youlaitech/vue3-element-admin/pull/138)) [@cshaptx4869](https://github.com/cshaptx4869)
- 添加CURD导入示例([19e7bb](https://github.com/youlaitech/vue3-element-admin/commit/eab91effd6a01d5a3d9257249c8d06aa252b3bf8)) [@cshaptx4869](https://github.com/cshaptx4869)
## ♻️ refactor
- 修改导出全量数据选项文本([904fec](https://github.com/youlaitech/vue3-element-admin/commit/904fecad65217650482fcdbb10ffb7f3d27eb9ea)) [@cshaptx4869](https://github.com/cshaptx4869)
## 🐛 fix
- 菜单列表未适配el-icon导致图标不显示问题修复([e72b68](https://github.com/youlaitech/vue3-element-admin/commit/e72b68337562b5a7ea24ad55bbe00023e1266b40)) [@haoxianrui](https://github.com/haoxianrui)
# 2.11.2 (2024/6/8)
## ✨ feat
- 支持表格远程筛选([#131](https://github.com/youlaitech/vue3-element-admin/pull/119)) [@cshaptx4869](https://github.com/cshaptx4869)
- 支持表格远程筛选([#131](https://github.com/youlaitech/vue3-element-admin/pull/131)) [@cshaptx4869](https://github.com/cshaptx4869)
- 支持标签输入框([#132](https://github.com/youlaitech/vue3-element-admin/pull/132)) [@cshaptx4869](https://github.com/cshaptx4869)
- 表单项支持tips配置([#133](https://github.com/youlaitech/vue3-element-admin/pull/133)) [@cshaptx4869](https://github.com/cshaptx4869)
- 前端导出支持全量数据([#134](https://github.com/youlaitech/vue3-element-admin/pull/134)) [@cshaptx4869](https://github.com/cshaptx4869)

View File

@@ -1,6 +1,6 @@
<div align="center">
<img src="https://img.shields.io/badge/Vue-3.4.27-brightgreen.svg"/>
<img src="https://img.shields.io/badge/Vite-5.2.11-green.svg"/>
<img src="https://img.shields.io/badge/Vue-3.4.29-brightgreen.svg"/>
<img src="https://img.shields.io/badge/Vite-5.3.1-green.svg"/>
<img src="https://img.shields.io/badge/Element Plus-2.7.5-blue.svg"/>
<img src="https://img.shields.io/badge/license-MIT-green.svg"/>
<a href="https://gitee.com/youlaiorg" target="_blank">

View File

@@ -1,6 +1,6 @@
<div align="center">
<img src="https://img.shields.io/badge/Vue-3.4.27-brightgreen.svg"/>
<img src="https://img.shields.io/badge/Vite-5.2.11-green.svg"/>
<img src="https://img.shields.io/badge/Vue-3.4.29-brightgreen.svg"/>
<img src="https://img.shields.io/badge/Vite-5.3.1-green.svg"/>
<img src="https://img.shields.io/badge/Element Plus-2.7.5-blue.svg"/>
<img src="https://img.shields.io/badge/license-MIT-green.svg"/>
<a href="https://gitee.com/youlaiorg" target="_blank">
@@ -30,7 +30,7 @@
- **基础设施**:动态路由、按钮权限、国际化、代码规范、Git 提交规范、常用组件封装。
- **持续更新**:自2021年起,该项目持续开源更新,实时更新工具和依赖,积累了广泛的用户群体。
- **持续更新**:项目持续开源更新,实时更新工具和依赖。
@@ -44,17 +44,18 @@
## 项目地址
| 项目 | Gitee | Github | GitCode |
| ---- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 项目 | Gitee | Github |
| ---- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 前端 | [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) | [vue3-element-admin](https://gitcode.net/youlai/vue3-element-admin) |
| 后端 | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot.git) | [youlai-boot](https://gitcode.net/youlai/youlai-boot) |
| 精简版 | [vue3-element-admin-thin](https://gitee.com/cshaptx4869/vue3-element-admin-thin) | [vue3-element-admin-thin](https://github.com/youlaitech/vue3-element-admin-thin) |
| 后端 | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot.git) |
## 环境准备
| 环境 | 名称版本 | 下载地址 |
| -------------------- | :----------------------------------------------------------- | ------------------------------------------------------------ |
| **开发工具** | VSCode | [下载](https://code.visualstudio.com/Download) |
| **运行环境** | Node ≥18 | [下载](http://nodejs.cn/download) |
| **开发工具** | VSCode | [下载](https://code.visualstudio.com/Download) |
| **运行环境** | Node ≥18 (其中 20.6.0 版本不可用) | [下载](http://nodejs.cn/download) |
## 项目启动

View File

@@ -36,6 +36,7 @@ export default defineMock([
id: 1,
parentId: 0,
name: "有来技术",
code: "YOULAI",
sort: 1,
status: 1,
children: [
@@ -43,6 +44,7 @@ export default defineMock([
id: 2,
parentId: 1,
name: "研发部门",
code: "RD001",
sort: 1,
status: 1,
children: [],
@@ -53,6 +55,7 @@ export default defineMock([
id: 3,
parentId: 1,
name: "测试部门",
code: "QA001",
sort: 1,
status: 1,
children: [],
@@ -126,6 +129,7 @@ const deptMap: Record<string, any> = {
1: {
id: 1,
name: "有来技术",
code: "YOULAI",
parentId: 0,
status: 1,
sort: 1,
@@ -133,6 +137,7 @@ const deptMap: Record<string, any> = {
2: {
id: 2,
name: "研发部门",
code: "RD001",
parentId: 1,
status: 1,
sort: 1,
@@ -140,6 +145,7 @@ const deptMap: Record<string, any> = {
3: {
id: 3,
name: "测试部门",
code: "QA001",
parentId: 1,
status: 1,
sort: 1,

View File

@@ -5,11 +5,11 @@ export default defineMock([
url: "dict/:code/options",
method: ["GET"],
body: ({ params }) => {
const typeCode = params.code;
const code = params.code;
let list = null;
if (typeCode == "gender") {
if (code == "gender") {
list = [
{
value: "1",
@@ -21,7 +21,7 @@ export default defineMock([
},
{
value: "0",
label: "未知",
label: "保密",
},
];
}
@@ -35,7 +35,7 @@ export default defineMock([
},
{
url: "dict/types/page",
url: "dict/page",
method: ["GET"],
body: {
code: "00000",
@@ -46,104 +46,37 @@ export default defineMock([
name: "性别",
code: "gender",
status: 1,
},
{
id: 2,
name: "状态",
code: "status",
status: 1,
dictItems: [
{
id: 1,
name: "男",
value: "1",
sort: 1,
status: 1,
},
{
id: 2,
name: "女",
value: "2",
sort: 2,
status: 1,
},
{
id: 3,
name: "保密",
value: "0",
sort: 3,
status: 1,
},
],
},
],
total: 2,
total: 1,
},
msg: "一切ok",
},
},
{
url: "dict/page",
method: ["GET"],
body: {
code: "00000",
data: {
list: [
{
id: 1,
name: "男",
value: "1",
status: 1,
},
{
id: 2,
name: "女",
value: "2",
status: 1,
},
{
id: 3,
name: "未知",
value: "0",
status: 1,
},
],
total: 3,
},
msg: "一切ok",
},
},
// 新增字典类型
{
url: "dict/types",
method: ["POST"],
body({ body }) {
return {
code: "00000",
data: null,
msg: "新增字典类型" + body.name + "成功",
};
},
},
// 获取字典类型表单数据
{
url: "dict/types/:id/form",
method: ["GET"],
body: ({ params }) => {
return {
code: "00000",
data: dictTypeMap[params.id],
msg: "一切ok",
};
},
},
// 修改字典类型
{
url: "dict/types/:id",
method: ["PUT"],
body({ body }) {
return {
code: "00000",
data: null,
msg: "修改字典类型" + body.name + "成功",
};
},
},
// 删除字典类型
{
url: "dict/types/:id",
method: ["DELETE"],
body({ params }) {
return {
code: "00000",
data: null,
msg: "删除字典类型" + params.id + "成功",
};
},
},
// 新增字典
{
url: "dict",
@@ -178,7 +111,7 @@ export default defineMock([
return {
code: "00000",
data: null,
msg: "修改字典类型" + body.name + "成功",
msg: "修改字典" + body.name + "成功",
};
},
},
@@ -197,51 +130,39 @@ export default defineMock([
},
]);
// 字典类型映射表数据
const dictTypeMap: Record<string, any> = {
1: {
id: 1,
name: "性别",
code: "gender",
status: 1,
remark: null,
},
2: {
id: 2,
name: "状态",
code: "status",
status: 1,
remark: null,
},
};
// 字典映射表数据
const dictMap: Record<string, any> = {
1: {
id: 1,
typeCode: "gender",
name: "男",
value: "1",
status: 1,
sort: 1,
remark: null,
},
2: {
id: 2,
typeCode: "gender",
name: "女",
value: "2",
status: 1,
sort: 2,
remark: null,
},
3: {
id: 3,
typeCode: "gender",
name: "未知",
value: "0",
status: 1,
sort: 1,
remark: null,
code: "00000",
data: {
id: 1,
name: "性别",
code: "gender",
status: 1,
dictItems: [
{
id: 1,
name: "男",
value: "1",
sort: 1,
status: 1,
},
{
id: 2,
name: "女",
value: "2",
sort: 2,
status: 1,
},
{
id: 3,
name: "未知",
value: "0",
sort: 3,
status: 1,
},
],
},
msg: "一切ok",
},
};

View File

@@ -454,7 +454,8 @@ export default defineMock([
parentId: 0,
name: "系统管理",
type: "CATALOG",
path: "/system",
routeName: "",
routePath: "/system",
component: "Layout",
sort: 1,
visible: 1,
@@ -467,7 +468,8 @@ export default defineMock([
parentId: 1,
name: "用户管理",
type: "MENU",
path: "user",
routeName: "User",
routePath: "user",
component: "system/user/index",
sort: 1,
visible: 1,
@@ -475,12 +477,28 @@ export default defineMock([
redirect: null,
perm: null,
children: [
{
id: 105,
parentId: 2,
name: "用户查询",
type: "BUTTON",
routeName: null,
routePath: "",
component: null,
sort: 0,
visible: 1,
icon: "",
redirect: null,
perm: "sys:user:query",
children: [],
},
{
id: 31,
parentId: 2,
name: "用户新增",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 1,
visible: 1,
@@ -494,7 +512,8 @@ export default defineMock([
parentId: 2,
name: "用户编辑",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 2,
visible: 1,
@@ -508,7 +527,8 @@ export default defineMock([
parentId: 2,
name: "用户删除",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 3,
visible: 1,
@@ -522,7 +542,8 @@ export default defineMock([
parentId: 2,
name: "重置密码",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 4,
visible: 1,
@@ -531,6 +552,36 @@ export default defineMock([
perm: "sys:user:password:reset",
children: [],
},
{
id: 106,
parentId: 2,
name: "用户导入",
type: "BUTTON",
routeName: null,
routePath: "",
component: null,
sort: 5,
visible: 1,
icon: "",
redirect: null,
perm: "sys:user:import",
children: [],
},
{
id: 107,
parentId: 2,
name: "用户导出",
type: "BUTTON",
routeName: null,
routePath: "",
component: null,
sort: 6,
visible: 1,
icon: "",
redirect: null,
perm: "sys:user:export",
children: [],
},
],
},
{
@@ -538,7 +589,8 @@ export default defineMock([
parentId: 1,
name: "角色管理",
type: "MENU",
path: "role",
routeName: "Role",
routePath: "role",
component: "system/role/index",
sort: 2,
visible: 1,
@@ -551,7 +603,8 @@ export default defineMock([
parentId: 3,
name: "角色新增",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 1,
visible: 1,
@@ -565,7 +618,8 @@ export default defineMock([
parentId: 3,
name: "角色编辑",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 2,
visible: 1,
@@ -579,7 +633,8 @@ export default defineMock([
parentId: 3,
name: "角色删除",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 3,
visible: 1,
@@ -595,7 +650,8 @@ export default defineMock([
parentId: 1,
name: "菜单管理",
type: "MENU",
path: "menu",
routeName: "Menu",
routePath: "menu",
component: "system/menu/index",
sort: 3,
visible: 1,
@@ -608,7 +664,8 @@ export default defineMock([
parentId: 4,
name: "菜单新增",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 1,
visible: 1,
@@ -617,26 +674,13 @@ export default defineMock([
perm: "sys:menu:add",
children: [],
},
{
id: 74,
parentId: 4,
name: "菜单编辑",
type: "BUTTON",
path: "",
component: null,
sort: 3,
visible: 1,
icon: "",
redirect: null,
perm: "sys:menu:edit",
children: [],
},
{
id: 75,
parentId: 4,
name: "菜单删除",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 3,
visible: 1,
@@ -645,6 +689,21 @@ export default defineMock([
perm: "sys:menu:delete",
children: [],
},
{
id: 74,
parentId: 4,
name: "菜单编辑",
type: "BUTTON",
routeName: null,
routePath: "",
component: null,
sort: 3,
visible: 1,
icon: "",
redirect: null,
perm: "sys:menu:edit",
children: [],
},
],
},
{
@@ -652,7 +711,8 @@ export default defineMock([
parentId: 1,
name: "部门管理",
type: "MENU",
path: "dept",
routeName: "Dept",
routePath: "dept",
component: "system/dept/index",
sort: 4,
visible: 1,
@@ -665,7 +725,8 @@ export default defineMock([
parentId: 5,
name: "部门新增",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 1,
visible: 1,
@@ -679,7 +740,8 @@ export default defineMock([
parentId: 5,
name: "部门编辑",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 2,
visible: 1,
@@ -693,7 +755,8 @@ export default defineMock([
parentId: 5,
name: "部门删除",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 3,
visible: 1,
@@ -709,7 +772,8 @@ export default defineMock([
parentId: 1,
name: "字典管理",
type: "MENU",
path: "dict",
routeName: "Dict",
routePath: "dict",
component: "system/dict/index",
sort: 5,
visible: 1,
@@ -722,7 +786,8 @@ export default defineMock([
parentId: 6,
name: "字典类型新增",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 1,
visible: 1,
@@ -736,7 +801,8 @@ export default defineMock([
parentId: 6,
name: "字典类型编辑",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 2,
visible: 1,
@@ -750,7 +816,8 @@ export default defineMock([
parentId: 6,
name: "字典类型删除",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 3,
visible: 1,
@@ -764,7 +831,8 @@ export default defineMock([
parentId: 6,
name: "字典数据新增",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 4,
visible: 1,
@@ -778,7 +846,8 @@ export default defineMock([
parentId: 6,
name: "字典数据编辑",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 5,
visible: 1,
@@ -792,7 +861,8 @@ export default defineMock([
parentId: 6,
name: "字典数据删除",
type: "BUTTON",
path: "",
routeName: null,
routePath: "",
component: null,
sort: 6,
visible: 1,
@@ -810,7 +880,8 @@ export default defineMock([
parentId: 0,
name: "接口文档",
type: "CATALOG",
path: "/api",
routeName: null,
routePath: "/api",
component: "Layout",
sort: 7,
visible: 1,
@@ -823,7 +894,8 @@ export default defineMock([
parentId: 40,
name: "Apifox",
type: "MENU",
path: "apifox",
routeName: null,
routePath: "apifox",
component: "demo/api/apifox",
sort: 1,
visible: 1,
@@ -832,34 +904,6 @@ export default defineMock([
perm: null,
children: [],
},
{
id: 103,
parentId: 40,
name: "Swagger",
type: "MENU",
path: "swagger",
component: "demo/api/swagger",
sort: 2,
visible: 0,
icon: "api",
redirect: "",
perm: null,
children: [],
},
{
id: 104,
parentId: 40,
name: "Knife4j",
type: "MENU",
path: "knife4j",
component: "demo/api/knife4j",
sort: 3,
visible: 0,
icon: "api",
redirect: "",
perm: null,
children: [],
},
],
},
{
@@ -867,12 +911,13 @@ export default defineMock([
parentId: 0,
name: "平台文档",
type: "CATALOG",
path: "/doc",
routeName: null,
routePath: "/doc",
component: "Layout",
sort: 8,
visible: 1,
icon: "document",
redirect: null,
redirect: "https://juejin.cn/post/7228990409909108793",
perm: null,
children: [
{
@@ -880,7 +925,8 @@ export default defineMock([
parentId: 26,
name: "平台文档(内嵌)",
type: "EXTLINK",
path: "internal-doc",
routeName: null,
routePath: "internal-doc",
component: "demo/internal-doc",
sort: 1,
visible: 1,
@@ -894,7 +940,8 @@ export default defineMock([
parentId: 26,
name: "平台文档(外链)",
type: "EXTLINK",
path: "https://juejin.cn/post/7228990409909108793",
routeName: null,
routePath: "https://juejin.cn/post/7228990409909108793",
component: "",
sort: 2,
visible: 1,
@@ -910,12 +957,13 @@ export default defineMock([
parentId: 0,
name: "多级菜单",
type: "CATALOG",
path: "/multi-level",
routeName: null,
routePath: "/multi-level",
component: "Layout",
sort: 9,
visible: 1,
icon: "cascader",
redirect: "/multi-level/multi-level1",
redirect: "",
perm: null,
children: [
{
@@ -923,12 +971,13 @@ export default defineMock([
parentId: 20,
name: "菜单一级",
type: "MENU",
path: "multi-level1",
routeName: null,
routePath: "multi-level1",
component: "demo/multi-level/level1",
sort: 1,
visible: 1,
icon: "",
redirect: "/multi-level/multi-level2",
redirect: "",
perm: null,
children: [
{
@@ -936,12 +985,13 @@ export default defineMock([
parentId: 21,
name: "菜单二级",
type: "MENU",
path: "multi-level2",
routeName: null,
routePath: "multi-level2",
component: "demo/multi-level/children/level2",
sort: 1,
visible: 1,
icon: "",
redirect: "/multi-level/multi-level2/multi-level3-1",
redirect: null,
perm: null,
children: [
{
@@ -949,7 +999,8 @@ export default defineMock([
parentId: 22,
name: "菜单三级-1",
type: "MENU",
path: "multi-level3-1",
routeName: null,
routePath: "multi-level3-1",
component: "demo/multi-level/children/children/level3-1",
sort: 1,
visible: 1,
@@ -963,7 +1014,8 @@ export default defineMock([
parentId: 22,
name: "菜单三级-2",
type: "MENU",
path: "multi-level3-2",
routeName: null,
routePath: "multi-level3-2",
component: "demo/multi-level/children/children/level3-2",
sort: 2,
visible: 1,
@@ -983,7 +1035,8 @@ export default defineMock([
parentId: 0,
name: "组件封装",
type: "CATALOG",
path: "/component",
routeName: null,
routePath: "/component",
component: "Layout",
sort: 10,
visible: 1,
@@ -991,14 +1044,45 @@ export default defineMock([
redirect: "",
perm: null,
children: [
{
id: 108,
parentId: 36,
name: "增删改查",
type: "MENU",
routeName: null,
routePath: "curd",
component: "demo/curd/index",
sort: 0,
visible: 1,
icon: "",
redirect: "",
perm: null,
children: [],
},
{
id: 109,
parentId: 36,
name: "列表选择器",
type: "MENU",
routeName: null,
routePath: "table-select",
component: "demo/table-select/index",
sort: 1,
visible: 1,
icon: "",
redirect: "",
perm: null,
children: [],
},
{
id: 37,
parentId: 36,
name: "富文本编辑器",
type: "MENU",
path: "wang-editor",
routeName: null,
routePath: "wang-editor",
component: "demo/wang-editor",
sort: 1,
sort: 2,
visible: 1,
icon: "",
redirect: "",
@@ -1010,22 +1094,9 @@ export default defineMock([
parentId: 36,
name: "图片上传",
type: "MENU",
path: "upload",
routeName: null,
routePath: "upload",
component: "demo/upload",
sort: 2,
visible: 1,
icon: "",
redirect: "",
perm: null,
children: [],
},
{
id: 39,
parentId: 36,
name: "图标选择器",
type: "MENU",
path: "icon-selector",
component: "demo/icon-selector",
sort: 3,
visible: 1,
icon: "",
@@ -1038,7 +1109,8 @@ export default defineMock([
parentId: 36,
name: "字典组件",
type: "MENU",
path: "dict-demo",
routeName: null,
routePath: "dict-demo",
component: "demo/dict",
sort: 4,
visible: 1,
@@ -1048,30 +1120,63 @@ export default defineMock([
children: [],
},
{
id: 93,
id: 39,
parentId: 36,
name: "签名",
name: "图标选择器",
type: "MENU",
path: "signature",
component: "demo/signature",
sort: 6,
routeName: null,
routePath: "icon-selector",
component: "demo/icon-selector",
sort: 4,
visible: 1,
icon: "",
redirect: "",
perm: null,
children: [],
},
],
},
{
id: 110,
parentId: 0,
name: "路由参数",
type: "CATALOG",
routeName: null,
routePath: "/route-param",
component: "Layout",
sort: 11,
visible: 1,
icon: "el-icon-ElementPlus",
redirect: null,
perm: null,
children: [
{
id: 94,
parentId: 36,
name: "表格",
id: 111,
parentId: 110,
name: "参数(type=1)",
type: "MENU",
path: "table",
component: "demo/table",
sort: 7,
routeName: null,
routePath: "route-param-type1",
component: "demo/route-param",
sort: 1,
visible: 1,
icon: "",
redirect: "",
icon: "el-icon-Star",
redirect: null,
perm: null,
children: [],
},
{
id: 112,
parentId: 110,
name: "参数(type=2)",
type: "MENU",
routeName: null,
routePath: "route-param-type2",
component: "demo/route-param",
sort: 2,
visible: 1,
icon: "el-icon-StarFilled",
redirect: null,
perm: null,
children: [],
},
@@ -1082,9 +1187,10 @@ export default defineMock([
parentId: 0,
name: "功能演示",
type: "CATALOG",
path: "/function",
routeName: null,
routePath: "/function",
component: "Layout",
sort: 11,
sort: 12,
visible: 1,
icon: "menu",
redirect: "",
@@ -1095,11 +1201,12 @@ export default defineMock([
parentId: 89,
name: "Icons",
type: "MENU",
path: "icon-demo",
routeName: null,
routePath: "icon-demo",
component: "demo/icons",
sort: 2,
visible: 1,
icon: "el-icon-edit",
icon: "el-icon-Notification",
redirect: "",
perm: null,
children: [],
@@ -1109,7 +1216,8 @@ export default defineMock([
parentId: 89,
name: "Websocket",
type: "MENU",
path: "/function/websocket",
routeName: null,
routePath: "/function/websocket",
component: "demo/websocket",
sort: 3,
visible: 1,
@@ -1123,7 +1231,8 @@ export default defineMock([
parentId: 89,
name: "敬请期待...",
type: "CATALOG",
path: "other",
routeName: null,
routePath: "other/:id",
component: "demo/other",
sort: 4,
visible: 1,
@@ -1153,6 +1262,10 @@ export default defineMock([
value: 2,
label: "用户管理",
children: [
{
value: 105,
label: "用户查询",
},
{
value: 31,
label: "用户新增",
@@ -1169,6 +1282,14 @@ export default defineMock([
value: 88,
label: "重置密码",
},
{
value: 106,
label: "用户导入",
},
{
value: 107,
label: "用户导出",
},
],
},
{
@@ -1197,14 +1318,14 @@ export default defineMock([
value: 73,
label: "菜单新增",
},
{
value: 74,
label: "菜单编辑",
},
{
value: 75,
label: "菜单删除",
},
{
value: 74,
label: "菜单编辑",
},
],
},
{
@@ -1265,14 +1386,6 @@ export default defineMock([
value: 41,
label: "Apifox",
},
{
value: 103,
label: "Swagger",
},
{
value: 104,
label: "Knife4j",
},
],
},
{
@@ -1319,6 +1432,14 @@ export default defineMock([
value: 36,
label: "组件封装",
children: [
{
value: 108,
label: "增删改查",
},
{
value: 109,
label: "列表选择器",
},
{
value: 37,
label: "富文本编辑器",
@@ -1327,21 +1448,27 @@ export default defineMock([
value: 38,
label: "图片上传",
},
{
value: 39,
label: "图标选择器",
},
{
value: 95,
label: "字典组件",
},
{
value: 93,
label: "签名",
value: 39,
label: "图标选择器",
},
],
},
{
value: 110,
label: "路由参数",
children: [
{
value: 111,
label: "参数(type=1)",
},
{
value: 94,
label: "表格",
value: 112,
label: "参数(type=2)",
},
],
},
@@ -1428,7 +1555,8 @@ const menuMap: Record<string, any> = {
parentId: 0,
name: "系统管理",
type: "CATALOG",
path: "/system",
routeName: "",
routePath: "/system",
component: "Layout",
perm: null,
visible: 1,
@@ -1437,13 +1565,15 @@ const menuMap: Record<string, any> = {
redirect: "/system/user",
keepAlive: null,
alwaysShow: null,
params: null,
},
2: {
id: 2,
parentId: 1,
name: "用户管理",
type: "MENU",
path: "user",
routeName: "User",
routePath: "user",
component: "system/user/index",
perm: null,
visible: 1,
@@ -1458,7 +1588,8 @@ const menuMap: Record<string, any> = {
parentId: 1,
name: "角色管理",
type: "MENU",
path: "role",
routeName: "Role",
routePath: "role",
component: "system/role/index",
perm: null,
visible: 1,
@@ -1473,7 +1604,8 @@ const menuMap: Record<string, any> = {
parentId: 1,
name: "菜单管理",
type: "MENU",
path: "menu",
routeName: "Menu",
routePath: "menu",
component: "system/menu/index",
perm: null,
visible: 1,
@@ -1488,7 +1620,8 @@ const menuMap: Record<string, any> = {
parentId: 1,
name: "部门管理",
type: "MENU",
path: "dept",
routeName: "Dept",
routePath: "dept",
component: "system/dept/index",
perm: null,
visible: 1,
@@ -1503,7 +1636,8 @@ const menuMap: Record<string, any> = {
parentId: 1,
name: "字典管理",
type: "MENU",
path: "dict",
routeName: "Dict",
routePath: "dict",
component: "system/dict/index",
perm: null,
visible: 1,

View File

@@ -1,6 +1,6 @@
{
"name": "vue3-element-admin",
"version": "2.11.3",
"version": "2.12.0",
"private": true,
"type": "module",
"scripts": {
@@ -63,7 +63,7 @@
"sockjs-client": "1.6.1",
"sortablejs": "^1.15.2",
"stompjs": "^2.3.3",
"vue": "^3.4.27",
"vue": "^3.4.29",
"vue-i18n": "9.9.1",
"vue-router": "^4.3.3"
},
@@ -73,14 +73,14 @@
"@iconify-json/ep": "^1.1.15",
"@types/color": "^3.0.6",
"@types/lodash": "^4.17.5",
"@types/node": "^20.14.2",
"@types/node": "^20.14.3",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.2",
"@types/sockjs-client": "^1.5.4",
"@types/sortablejs": "^1.15.8",
"@types/stompjs": "^2.3.9",
"@typescript-eslint/eslint-plugin": "^7.13.0",
"@typescript-eslint/parser": "^7.13.0",
"@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1",
"@vitejs/plugin-vue": "^5.0.5",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.19",
@@ -98,7 +98,7 @@
"postcss-html": "^1.7.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.3.2",
"sass": "^1.77.5",
"sass": "^1.77.6",
"stylelint": "^16.6.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^4.6.0",
@@ -111,10 +111,10 @@
"unplugin-auto-import": "^0.17.6",
"unplugin-icons": "^0.18.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.2.13",
"vite": "^5.3.1",
"vite-plugin-mock-dev-server": "^1.5.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-devtools": "^7.2.1",
"vite-plugin-vue-devtools": "^7.3.0",
"vue-tsc": "^2.0.21"
},
"repository": "https://gitee.com/youlaiorg/vue3-element-admin.git",

72
src/api/auth.ts Normal file
View File

@@ -0,0 +1,72 @@
import request from "@/utils/request";
const AUTH_BASE_URL = "/api/v1/auth";
class AuthAPI {
/** 登录 接口*/
static login(data: LoginData) {
const formData = new FormData();
formData.append("username", data.username);
formData.append("password", data.password);
formData.append("captchaKey", data.captchaKey);
formData.append("captchaCode", data.captchaCode);
return request<any, LoginResult>({
url: `${AUTH_BASE_URL}/login`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
}
/** 注销 接口*/
static logout() {
return request({
url: `${AUTH_BASE_URL}/logout`,
method: "delete",
});
}
/** 获取验证码 接口*/
static getCaptcha() {
return request<any, CaptchaResult>({
url: `${AUTH_BASE_URL}/captcha`,
method: "get",
});
}
}
export default AuthAPI;
/** 登录请求参数 */
export interface LoginData {
/** 用户名 */
username: string;
/** 密码 */
password: string;
/** 验证码缓存key */
captchaKey: string;
/** 验证码 */
captchaCode: string;
}
/** 登录响应 */
export interface LoginResult {
/** 访问token */
accessToken?: string;
/** 过期时间(单位:毫秒) */
expires?: number;
/** 刷新token */
refreshToken?: string;
/** token 类型 */
tokenType?: string;
}
/** 验证码响应 */
export interface CaptchaResult {
/** 验证码缓存key */
captchaKey: string;
/** 验证码图片Base64字符串 */
captchaBase64: string;
}

View File

@@ -1,48 +0,0 @@
import request from "@/utils/request";
import { CaptchaResult, LoginData, LoginResult } from "./model";
class AuthAPI {
/**
* 登录API
*
* @param data {LoginData}
* @returns
*/
static login(data: LoginData) {
const formData = new FormData();
formData.append("username", data.username);
formData.append("password", data.password);
formData.append("captchaKey", data.captchaKey || "");
formData.append("captchaCode", data.captchaCode || "");
return request<any, LoginResult>({
url: "/api/v1/auth/login",
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
}
/**
* 注销API
*/
static logout() {
return request({
url: "/api/v1/auth/logout",
method: "delete",
});
}
/**
* 获取验证码
*/
static getCaptcha() {
return request<any, CaptchaResult>({
url: "/api/v1/auth/captcha",
method: "get",
});
}
}
export default AuthAPI;

View File

@@ -1,59 +0,0 @@
/**
* 登录请求参数
*/
export interface LoginData {
/**
* 用户名
*/
username: string;
/**
* 密码
*/
password: string;
/**
* 验证码缓存key
*/
captchaKey?: string;
/**
* 验证码
*/
captchaCode?: string;
}
/**
* 登录响应
*/
export interface LoginResult {
/**
* 访问token
*/
accessToken?: string;
/**
* 过期时间(单位:毫秒)
*/
expires?: number;
/**
* 刷新token
*/
refreshToken?: string;
/**
* token 类型
*/
tokenType?: string;
}
/**
* 验证码响应
*/
export interface CaptchaResult {
/**
* 验证码缓存key
*/
captchaKey: string;
/**
* 验证码图片Base64字符串
*/
captchaBase64: string;
}

130
src/api/dept.ts Normal file
View File

@@ -0,0 +1,130 @@
import request from "@/utils/request";
const DEPT_BASE_URL = "/api/v1/dept";
class DeptAPI {
/**
* 获取部门列表
*
* @param queryParams 查询参数(可选)
* @returns 部门树形表格数据
*/
static getList(queryParams?: DeptQuery) {
return request<any, DeptVO[]>({
url: `${DEPT_BASE_URL}`,
method: "get",
params: queryParams,
});
}
/** 获取部门下拉列表 */
static getOptions() {
return request<any, OptionType[]>({
url: `${DEPT_BASE_URL}/options`,
method: "get",
});
}
/**
* 获取部门详情表单数据
*
* @param id 部门ID
* @returns 部门详情表单数据
*/
static getFormData(id: number) {
return request<any, DeptForm>({
url: `${DEPT_BASE_URL}/${id}/form`,
method: "get",
});
}
/**
* 新增部门
*
* @param data 部门表单数据
* @returns 请求结果
*/
static add(data: DeptForm) {
return request({
url: `${DEPT_BASE_URL}`,
method: "post",
data: data,
});
}
/**
* 修改部门
*
* @param id 部门ID
* @param data 部门表单数据
* @returns 请求结果
*/
static update(id: number, data: DeptForm) {
return request({
url: `${DEPT_BASE_URL}/${id}`,
method: "put",
data: data,
});
}
/**
* 删除部门
*
* @param ids 部门ID,多个以英文逗号(,)分隔
* @returns 请求结果
*/
static deleteByIds(ids: string) {
return request({
url: `${DEPT_BASE_URL}/${ids}`,
method: "delete",
});
}
}
export default DeptAPI;
/** 部门查询参数 */
export interface DeptQuery {
/** 搜索关键字 */
keywords?: string;
/** 状态 */
status?: number;
}
/** 部门类型 */
export interface DeptVO {
/** 子部门 */
children?: DeptVO[];
/** 创建时间 */
createTime?: Date;
/** 部门ID */
id?: number;
/** 部门名称 */
name?: string;
/** 部门编号 */
code?: string;
/** 父部门ID */
parentId?: number;
/** 排序 */
sort?: number;
/** 状态(1:启用;0:禁用) */
status?: number;
/** 修改时间 */
updateTime?: Date;
}
/** 部门表单类型 */
export interface DeptForm {
/** 部门ID(新增不填) */
id?: number;
/** 部门名称 */
name?: string;
/** 部门编号 */
code?: string;
/** 父部门ID */
parentId: number;
/** 排序 */
sort?: number;
/** 状态(1:启用;0:禁用) */
status?: number;
}

View File

@@ -1,80 +0,0 @@
import request from "@/utils/request";
import { DeptForm, DeptQuery, DeptVO } from "./model";
class DeptAPI {
/**
* 部门树形表格
*
* @param queryParams
*/
static getList(queryParams?: DeptQuery) {
return request<any, DeptVO[]>({
url: "/api/v1/dept",
method: "get",
params: queryParams,
});
}
/**
* 部门下拉列表
*/
static getOptions() {
return request<any, OptionType[]>({
url: "/api/v1/dept/options",
method: "get",
});
}
/**
* 获取部门详情
*
* @param id
*/
static getFormData(id: number) {
return request<any, DeptForm>({
url: "/api/v1/dept/" + id + "/form",
method: "get",
});
}
/**
* 新增部门
*
* @param data
*/
static add(data: DeptForm) {
return request({
url: "/api/v1/dept",
method: "post",
data: data,
});
}
/**
* 修改部门
*
* @param id
* @param data
*/
static update(id: number, data: DeptForm) {
return request({
url: "/api/v1/dept/" + id,
method: "put",
data: data,
});
}
/**
* 删除部门
*
* @param ids
*/
static deleteByIds(ids: string) {
return request({
url: "/api/v1/dept/" + ids,
method: "delete",
});
}
}
export default DeptAPI;

View File

@@ -1,71 +0,0 @@
/**
* 部门查询参数
*/
export interface DeptQuery {
keywords?: string;
status?: number;
}
/**
* 部门类型
*/
export interface DeptVO {
/**
* 子部门
*/
children?: DeptVO[];
/**
* 创建时间
*/
createTime?: Date;
/**
* 部门ID
*/
id?: number;
/**
* 部门名称
*/
name?: string;
/**
* 父部门ID
*/
parentId?: number;
/**
* 排序
*/
sort?: number;
/**
* 状态(1:启用;0:禁用)
*/
status?: number;
/**
* 修改时间
*/
updateTime?: Date;
}
/**
* 部门表单类型
*/
export interface DeptForm {
/**
* 部门ID(新增不填)
*/
id?: number;
/**
* 部门名称
*/
name?: string;
/**
* 父部门ID
*/
parentId: number;
/**
* 排序
*/
sort?: number;
/**
* 状态(1:启用;0:禁用)
*/
status?: number;
}

183
src/api/dict.ts Normal file
View File

@@ -0,0 +1,183 @@
import request from "@/utils/request";
const DICT_BASE_URL = "/api/v1/dict";
class DictAPI {
/**
* 获取字典分页列表
*
* @param queryParams 查询参数
* @returns 字典分页结果
*/
static getPage(queryParams: DictPageQuery) {
return request<any, PageResult<DictPageVO[]>>({
url: `${DICT_BASE_URL}/page`,
method: "get",
params: queryParams,
});
}
/**
* 获取字典表单数据
*
* @param id 字典ID
* @returns 字典表单数据
*/
static getFormData(id: number) {
return request<any, ResponseData<DictForm>>({
url: `${DICT_BASE_URL}/${id}/form`,
method: "get",
});
}
/**
* 新增字典
*
* @param data 字典表单数据
* @returns 请求结果
*/
static add(data: DictForm) {
return request({
url: `${DICT_BASE_URL}`,
method: "post",
data: data,
});
}
/**
* 修改字典
*
* @param id 字典ID
* @param data 字典表单数据
* @returns 请求结果
*/
static update(id: number, data: DictForm) {
return request({
url: `${DICT_BASE_URL}/${id}`,
method: "put",
data: data,
});
}
/**
* 删除字典
*
* @param ids 字典ID,多个以英文逗号(,)分隔
* @returns 请求结果
*/
static deleteByIds(ids: string) {
return request({
url: `${DICT_BASE_URL}/${ids}`,
method: "delete",
});
}
/**
* 获取字典的数据项
*
* @param typeCode 字典编码
* @returns 字典数据项
*/
static getOptions(code: string) {
return request<any, OptionType[]>({
url: `${DICT_BASE_URL}/${code}/options`,
method: "get",
});
}
}
export default DictAPI;
/**
* 字典查询参数
*/
export interface DictPageQuery extends PageQuery {
/**
* 关键字(字典名称/编码)
*/
keywords?: string;
}
/**
* 字典分页对象
*/
export interface DictPageVO {
/**
* 字典ID
*/
id: number;
/**
* 字典名称
*/
name: string;
/**
* 字典编码
*/
code: string;
/**
* 字典状态(1-启用,0-禁用)
*/
status: number;
/**
* 字典项列表
*/
dictItems: DictItem[];
}
/**
* 字典项
*/
export interface DictItem {
/**
* 字典项ID
*/
id?: number;
/**
* 字典项名称
*/
name?: string;
/**
* 字典项值
*/
value?: string;
/**
* 排序
*/
sort?: number;
/**
* 状态(1-启用,0-禁用)
*/
status?: number;
}
// TypeScript 类型声明
/**
* 字典
*/
export interface DictForm {
/**
* 字典ID
*/
id?: number;
/**
* 字典名称
*/
name?: string;
/**
* 字典编码
*/
code?: string;
/**
* 字典状态(1-启用,0-禁用)
*/
status?: number;
/**
* 备注
*/
remark?: string;
/**
* 字典数据项列表
*/
dictItems?: DictItem[];
}

View File

@@ -1,149 +0,0 @@
import request from "@/utils/request";
import {
DictTypeQuery,
DictTypePageResult,
DictTypeForm,
DictQuery,
DictForm,
DictPageResult,
} from "./model";
class DictAPI {
/**
* 字典类型分页列表
*
* @param queryParams
*/
static getDictTypePage(queryParams: DictTypeQuery) {
return request<any, DictTypePageResult>({
url: "/api/v1/dict/types/page",
method: "get",
params: queryParams,
});
}
/**
* 字典类型表单数据
*
* @param id
*/
static getDictTypeForm(id: number) {
return request<any, ResponseData<DictTypeForm>>({
url: "/api/v1/dict/types/" + id + "/form",
method: "get",
});
}
/**
* 新增字典类型
*
* @param data
*/
static addDictType(data: DictTypeForm) {
return request({
url: "/api/v1/dict/types",
method: "post",
data: data,
});
}
/**
* 修改字典类型
*
* @param id
* @param data
*/
static updateDictType(id: number, data: DictTypeForm) {
return request({
url: "/api/v1/dict/types/" + id,
method: "put",
data: data,
});
}
/**
* 删除字典类型
*/
static deleteDictTypes(ids: string) {
return request({
url: "/api/v1/dict/types/" + ids,
method: "delete",
});
}
/**
* 获取字典类型的数据项
*
* @param typeCode 字典类型编码
*/
static getDictOptions(typeCode: string) {
return request<any, OptionType[]>({
url: "/api/v1/dict/" + typeCode + "/options",
method: "get",
});
}
/**
* 字典分页列表
*/
static getDictPage(queryParams: DictQuery) {
return request<any, DictPageResult>({
url: "/api/v1/dict/page",
method: "get",
params: queryParams,
});
}
/**
* 获取字典表单数据
*
* @param id
*/
static getDictFormData(id: number) {
return request<any, DictForm>({
url: "/api/v1/dict/" + id + "/form",
method: "get",
});
}
/**
* 新增字典
*
* @param data
*/
static addDict(data: DictForm) {
return request({
url: "/api/v1/dict",
method: "post",
data: data,
});
}
/**
* 修改字典项
*
* @param id
* @param data
*/
static updateDict(id: number, data: DictForm) {
return request({
url: "/api/v1/dict/" + id,
method: "put",
data: data,
});
}
/**
* 删除字典
*
* @param ids 字典项ID,多个以英文逗号(,)分割
*/
static deleteDictByIds(ids: string) {
return request({
url: "/api/v1/dict/" + ids,
method: "delete",
});
}
}
export default DictAPI;

View File

@@ -1,142 +0,0 @@
/**
* 字典类型查询参数
*/
export interface DictTypeQuery extends PageQuery {
/**
* 关键字(字典类型名称/编码)
*/
keywords?: string;
}
/**
* 字典类型分页对象
*/
export interface DictTypePageVO {
/**
* 字典类型ID
*/
id: number;
/**
* 类型编码
*/
code: string;
/**
* 类型名称
*/
name: string;
/**
* 状态(1:启用;0:禁用)
*/
status?: number;
/**
* 备注
*/
remark?: string;
}
/**
* 字典分页项类型声明
*/
export type DictTypePageResult = PageResult<DictTypePageVO[]>;
/**
* 字典表单类型声明
*/
export interface DictTypeForm {
/**
* 字典类型ID
*/
id?: number;
/**
* 类型名称
*/
name?: string;
/**
* 类型编码
*/
code?: string;
/**
* 类型状态:1:启用;0:禁用
*/
status: number;
/**
* 备注
*/
remark?: string;
}
/**
* 字典查询参数
*/
export interface DictQuery extends PageQuery {
/**
* 字典项名称
*/
name?: string;
/**
* 字典类型编码
*/
typeCode?: string;
}
/**
* 字典分页对象
*/
export interface DictPageVO {
/**
* 字典ID
*/
id?: number;
/**
* 字典名称
*/
name?: string;
/**
* 状态(1:启用;0:禁用)
*/
status?: number;
/**
* 字典值
*/
value?: string;
}
/**
* 字典分页
*/
export type DictPageResult = PageResult<DictPageVO[]>;
/**
* 字典表单
*/
export interface DictForm {
/**
* 字典ID
*/
id?: number;
/**
* 字典名称
*/
name?: string;
/**
* 排序
*/
sort?: number;
/**
* 状态(1:启用;0:禁用)
*/
status?: number;
/**
* 类型编码
*/
typeCode?: string;
/**
* 值
*/
value?: string;
/**
* 备注
*/
remark?: string;
}

View File

@@ -1,5 +1,4 @@
import request from "@/utils/request";
import { FileInfo } from "./model";
class FileAPI {
/**
@@ -35,3 +34,13 @@ class FileAPI {
}
export default FileAPI;
/**
* 文件API类型声明
*/
export interface FileInfo {
/** 文件名 */
name: string;
/** 文件路径 */
url: string;
}

View File

@@ -1,7 +0,0 @@
/**
* 文件API类型声明
*/
export interface FileInfo {
name: string;
url: string;
}

209
src/api/menu.ts Normal file
View File

@@ -0,0 +1,209 @@
import request from "@/utils/request";
// 菜单基础URL
const MENU_BASE_URL = "/api/v1/menus";
class MenuAPI {
/**
* 获取路由列表
*
* @returns 路由列表
*/
static getRoutes() {
return request<any, RouteVO[]>({
url: `${MENU_BASE_URL}/routes`,
method: "get",
});
}
/**
* 获取菜单树形列表
*
* @param queryParams 查询参数
* @returns 菜单树形列表
*/
static getList(queryParams: MenuQuery) {
return request<any, MenuVO[]>({
url: `${MENU_BASE_URL}`,
method: "get",
params: queryParams,
});
}
/**
* 获取菜单下拉数据源
*
* @returns 菜单下拉数据源
*/
static getOptions() {
return request<any, OptionType[]>({
url: `${MENU_BASE_URL}/options`,
method: "get",
});
}
/**
* 获取菜单表单数据
*
* @param id 菜单ID
* @returns 菜单表单数据
*/
static getFormData(id: number) {
return request<any, MenuForm>({
url: `${MENU_BASE_URL}/${id}/form`,
method: "get",
});
}
/**
* 添加菜单
*
* @param data 菜单表单数据
* @returns 请求结果
*/
static add(data: MenuForm) {
return request({
url: `${MENU_BASE_URL}`,
method: "post",
data: data,
});
}
/**
* 修改菜单
*
* @param id 菜单ID
* @param data 菜单表单数据
* @returns 请求结果
*/
static update(id: string, data: MenuForm) {
return request({
url: `${MENU_BASE_URL}/${id}`,
method: "put",
data: data,
});
}
/**
* 删除菜单
*
* @param id 菜单ID
* @returns 请求结果
*/
static deleteById(id: number) {
return request({
url: `${MENU_BASE_URL}/${id}`,
method: "delete",
});
}
}
export default MenuAPI;
import { MenuTypeEnum } from "@/enums/MenuTypeEnum";
/** 菜单查询参数 */
export interface MenuQuery {
/** 搜索关键字 */
keywords?: string;
}
/** 菜单视图对象 */
export interface MenuVO {
/** 子菜单 */
children?: MenuVO[];
/** 组件路径 */
component?: string;
/** ICON */
icon?: string;
/** 菜单ID */
id?: number;
/** 菜单名称 */
name?: string;
/** 父菜单ID */
parentId?: number;
/** 按钮权限标识 */
perm?: string;
/** 跳转路径 */
redirect?: string;
/** 路由名称 */
routeName?: string;
/** 路由相对路径 */
routePath?: string;
/** 菜单排序(数字越小排名越靠前) */
sort?: number;
/** 菜单 */
type?: MenuTypeEnum;
/** 菜单是否可见(1:显示;0:隐藏) */
visible?: number;
}
/** 菜单表单对象 */
export interface MenuForm {
/** 菜单ID */
id?: string;
/** 父菜单ID */
parentId?: number;
/** 菜单名称 */
name?: string;
/** 菜单是否可见(1-是 0-否) */
visible: number;
/** ICON */
icon?: string;
/** 排序 */
sort?: number;
/** 路由名称 */
routeName?: string;
/** 路由路径 */
routePath?: string;
/** 组件路径 */
component?: string;
/** 跳转路由路径 */
redirect?: string;
/** 菜单 */
type?: MenuTypeEnum;
/** 权限标识 */
perm?: string;
/** 【菜单】是否开启页面缓存 */
keepAlive?: number;
/** 【目录】只有一个子路由是否始终显示 */
alwaysShow?: number;
/** 参数 */
params?: KeyValue[];
}
interface KeyValue {
key: string;
value: string;
}
/** RouteVO,路由对象 */
export interface RouteVO {
/** 子路由列表 */
children: RouteVO[];
/** 组件路径 */
component?: string;
/** 路由属性 */
meta?: Meta;
/** 路由名称 */
name?: string;
/** 路由路径 */
path?: string;
/** 跳转链接 */
redirect?: string;
}
/** Meta,路由属性 */
export interface Meta {
/** 【目录】只有一个子路由是否始终显示 */
alwaysShow?: boolean;
/** 是否隐藏(true-是 false-否) */
hidden?: boolean;
/** ICON */
icon?: string;
/** 【菜单】是否开启页面缓存 */
keepAlive?: boolean;
/** 拥有路由权限的角色编码 */
roles?: string[];
/** 路由title */
title?: string;
}

View File

@@ -1,90 +0,0 @@
import request from "@/utils/request";
import { MenuQuery, MenuVO, MenuForm, RouteVO } from "./model";
class MenuAPI {
/**
* 获取路由列表
*/
static getRoutes() {
return request<any, RouteVO[]>({
url: "/api/v1/menus/routes",
method: "get",
});
}
/**
* 获取菜单树形列表
*
* @param queryParams
*/
static getList(queryParams: MenuQuery) {
return request<any, MenuVO[]>({
url: "/api/v1/menus",
method: "get",
params: queryParams,
});
}
/**
* 获取菜单下拉数据源
*/
static getOptions() {
return request<any, OptionType[]>({
url: "/api/v1/menus/options",
method: "get",
});
}
/**
* 获取菜单表单数据
*
* @param id
*/
static getFormData(id: number) {
return request<any, MenuForm>({
url: "/api/v1/menus/" + id + "/form",
method: "get",
});
}
/**
* 添加菜单
*
* @param data
*/
static add(data: MenuForm) {
return request({
url: "/api/v1/menus",
method: "post",
data: data,
});
}
/**
* 修改菜单
*
* @param id
* @param data
*/
static update(id: string, data: MenuForm) {
return request({
url: "/api/v1/menus/" + id,
method: "put",
data: data,
});
}
/**
* 删除菜单
*
* @param id 菜单ID
*/
static deleteById(id: number) {
return request({
url: "/api/v1/menus/" + id,
method: "delete",
});
}
}
export default MenuAPI;

View File

@@ -1,188 +0,0 @@
import { MenuTypeEnum } from "@/enums/MenuTypeEnum";
/**
* 菜单查询参数类型
*/
export interface MenuQuery {
keywords?: string;
}
/**
* 菜单视图对象类型
*/
export interface MenuVO {
/**
* 子菜单
*/
children?: MenuVO[];
/**
* 组件路径
*/
component?: string;
/**
* ICON
*/
icon?: string;
/**
* 菜单ID
*/
id?: number;
/**
* 菜单名称
*/
name?: string;
/**
* 父菜单ID
*/
parentId?: number;
/**
* 按钮权限标识
*/
perm?: string;
/**
* 跳转路径
*/
redirect?: string;
/**
* 路由名称
*/
routeName?: string;
/**
* 路由相对路径
*/
routePath?: string;
/**
* 菜单排序(数字越小排名越靠前)
*/
sort?: number;
/**
* 菜单类型
*/
type?: MenuTypeEnum;
/**
* 菜单是否可见(1:显示;0:隐藏)
*/
visible?: number;
}
/**
* 菜单表单对象类型
*/
export interface MenuForm {
/**
* 菜单ID
*/
id?: string;
/**
* 父菜单ID
*/
parentId?: number;
/**
* 菜单名称
*/
name?: string;
/**
* 菜单是否可见(1:是;0:否;)
*/
visible: number;
icon?: string;
/**
* 排序
*/
sort?: number;
/**
* 组件路径
*/
component?: string;
/**
* 路由路径
*/
path?: string;
/**
* 跳转路由路径
*/
redirect?: string;
/**
* 菜单类型
*/
type?: MenuTypeEnum;
/**
* 权限标识
*/
perm?: string;
/**
* 【菜单】是否开启页面缓存
*/
keepAlive?: number;
/**
* 【目录】只有一个子路由是否始终显示
*/
alwaysShow?: number;
params?: KeyValue[];
}
interface KeyValue {
key: string;
value: string;
}
/**
* RouteVO,路由对象
*/
export interface RouteVO {
/**
* 子路由列表
*/
children: RouteVO[];
/**
* 组件路径
*/
component?: string;
meta?: Meta;
/**
* 路由名称
*/
name?: string;
/**
* 路由路径
*/
path?: string;
/**
* 跳转链接
*/
redirect?: string;
}
/**
* Meta,路由属性类型
*/
export interface Meta {
/**
* 【目录】只有一个子路由是否始终显示
*/
alwaysShow?: boolean;
/**
* 是否隐藏(true-是 false-否)
*/
hidden?: boolean;
/**
* ICON
*/
icon?: string;
/**
* 【菜单】是否开启页面缓存
*/
keepAlive?: boolean;
/**
* 拥有路由权限的角色编码
*/
roles?: string[];
/**
* 路由title
*/
title?: string;
}

140
src/api/role.ts Normal file
View File

@@ -0,0 +1,140 @@
import request from "@/utils/request";
const ROLE_BASE_URL = "/api/v1/roles";
class RoleAPI {
/** 获取角色分页数据 */
static getPage(queryParams?: RolePageQuery) {
return request<any, PageResult<RolePageVO[]>>({
url: `${ROLE_BASE_URL}/page`,
method: "get",
params: queryParams,
});
}
/** 获取角色下拉数据源 */
static getOptions() {
return request<any, OptionType[]>({
url: `${ROLE_BASE_URL}/options`,
method: "get",
});
}
/**
* 获取角色的菜单ID集合
*
* @param roleId 角色ID
* @returns 角色的菜单ID集合
*/
static getRoleMenuIds(roleId: number) {
return request<any, number[]>({
url: `${ROLE_BASE_URL}/${roleId}/menuIds`,
method: "get",
});
}
/**
* 分配菜单权限
*
* @param roleId 角色ID
* @param data 菜单ID集合
* @returns 请求结果
*/
static updateRoleMenus(roleId: number, data: number[]) {
return request({
url: `${ROLE_BASE_URL}/${roleId}/menus`,
method: "put",
data: data,
});
}
/**
* 获取角色表单数据
*
* @param id 角色ID
* @returns 角色表单数据
*/
static getFormData(id: number) {
return request<any, RoleForm>({
url: `${ROLE_BASE_URL}/${id}/form`,
method: "get",
});
}
/** 添加角色 */
static add(data: RoleForm) {
return request({
url: `${ROLE_BASE_URL}`,
method: "post",
data: data,
});
}
/**
* 更新角色
*
* @param id 角色ID
* @param data 角色表单数据
*/
static update(id: number, data: RoleForm) {
return request({
url: `${ROLE_BASE_URL}/${id}`,
method: "put",
data: data,
});
}
/**
* 批量删除角色,多个以英文逗号(,)分割
*
* @param ids 角色ID字符串,多个以英文逗号(,)分割
*/
static deleteByIds(ids: string) {
return request({
url: `${ROLE_BASE_URL}/${ids}`,
method: "delete",
});
}
}
export default RoleAPI;
/** 角色分页查询参数 */
export interface RolePageQuery extends PageQuery {
/** 搜索关键字 */
keywords?: string;
}
/** 角色分页对象 */
export interface RolePageVO {
/** 角色编码 */
code?: string;
/** 角色ID */
id?: number;
/** 角色名称 */
name?: string;
/** 排序 */
sort?: number;
/** 角色状态 */
status?: number;
/** 创建时间 */
createTime?: Date;
/** 修改时间 */
updateTime?: Date;
}
/** 角色表单对象 */
export interface RoleForm {
/** 角色ID */
id?: number;
/** 角色编码 */
code: string;
/** 数据权限 */
dataScope?: number;
/** 角色名称 */
name: string;
/** 排序 */
sort?: number;
/** 角色状态(1-正常;0-停用) */
status?: number;
}

View File

@@ -1,108 +0,0 @@
import request from "@/utils/request";
import { RoleQuery, RolePageResult, RoleForm } from "./model";
class RoleAPI {
/**
* 获取角色分页数据
*
* @param queryParams
*/
static getPage(queryParams?: RoleQuery) {
return request<any, RolePageResult>({
url: "/api/v1/roles/page",
method: "get",
params: queryParams,
});
}
/**
* 获取角色下拉数据源
*
* @param queryParams
*/
static getOptions(queryParams?: RoleQuery) {
return request<any, OptionType[]>({
url: "/api/v1/roles/options",
method: "get",
params: queryParams,
});
}
/**
* 获取角色的菜单ID集合
*
* @param queryParams
*/
static getRoleMenuIds(roleId: number) {
return request<any, number[]>({
url: "/api/v1/roles/" + roleId + "/menuIds",
method: "get",
});
}
/**
* 分配菜单权限给角色
*
* @param queryParams
*/
static updateRoleMenus(roleId: number, data: number[]) {
return request({
url: "/api/v1/roles/" + roleId + "/menus",
method: "put",
data: data,
});
}
/**
* 获取角色表单数据
*
* @param id
*/
static getFormData(id: number) {
return request<any, RoleForm>({
url: "/api/v1/roles/" + id + "/form",
method: "get",
});
}
/**
* 添加角色
*
* @param data
*/
static add(data: RoleForm) {
return request({
url: "/api/v1/roles",
method: "post",
data: data,
});
}
/**
* 更新角色
*
* @param id
* @param data
*/
static update(id: number, data: RoleForm) {
return request({
url: "/api/v1/roles/" + id,
method: "put",
data: data,
});
}
/**
* 批量删除角色,多个以英文逗号(,)分割
*
* @param ids
*/
static deleteByIds(ids: string) {
return request({
url: "/api/v1/roles/" + ids,
method: "delete",
});
}
}
export default RoleAPI;

View File

@@ -1,78 +0,0 @@
/**
* 角色查询参数
*/
export interface RoleQuery extends PageQuery {
keywords?: string;
}
/**
* 角色分页对象
*/
export interface RolePageVO {
/**
* 角色编码
*/
code?: string;
/**
* 角色ID
*/
id?: number;
/**
* 角色名称
*/
name?: string;
/**
* 排序
*/
sort?: number;
/**
* 角色状态
*/
status?: number;
/**
* 创建时间
*/
createTime?: Date;
/**
* 修改时间
*/
updateTime?: Date;
}
/**
* 角色分页
*/
export type RolePageResult = PageResult<RolePageVO[]>;
/**
* 角色表单对象
*/
export interface RoleForm {
/**
* 角色ID
*/
id?: number;
/**
* 角色编码
*/
code: string;
/**
* 数据权限
*/
dataScope?: number;
/**
* 角色名称
*/
name: string;
/**
* 排序
*/
sort?: number;
/**
* 角色状态(1-正常;0-停用)
*/
status?: number;
}

232
src/api/user.ts Normal file
View File

@@ -0,0 +1,232 @@
import request from "@/utils/request";
const USER_BASE_URL = "/api/v1/users";
class UserAPI {
/**
* 获取当前登录用户信息
*
* @returns 登录用户昵称、头像信息,包括角色和权限
*/
static getInfo() {
return request<any, UserInfo>({
url: `${USER_BASE_URL}/me`,
method: "get",
});
}
/**
* 获取用户分页列表
*
* @param queryParams 查询参数
*/
static getPage(queryParams: UserPageQuery) {
return request<any, PageResult<UserPageVO[]>>({
url: `${USER_BASE_URL}/page`,
method: "get",
params: queryParams,
});
}
/**
* 获取用户表单详情
*
* @param userId 用户ID
* @returns 用户表单详情
*/
static getFormData(userId: number) {
return request<any, UserForm>({
url: `${USER_BASE_URL}/${userId}/form`,
method: "get",
});
}
/**
* 添加用户
*
* @param data 用户表单数据
*/
static add(data: UserForm) {
return request({
url: `${USER_BASE_URL}`,
method: "post",
data: data,
});
}
/**
* 修改用户
*
* @param id 用户ID
* @param data 用户表单数据
*/
static update(id: number, data: UserForm) {
return request({
url: `${USER_BASE_URL}/${id}`,
method: "put",
data: data,
});
}
/**
* 修改用户密码
*
* @param id 用户ID
* @param password 新密码
*/
static updatePassword(id: number, password: string) {
return request({
url: `${USER_BASE_URL}/${id}/password`,
method: "patch",
params: { password: password },
});
}
/**
* 批量删除用户,多个以英文逗号(,)分割
*
* @param ids 用户ID字符串,多个以英文逗号(,)分割
*/
static deleteByIds(ids: string) {
return request({
url: `${USER_BASE_URL}/${ids}`,
method: "delete",
});
}
/** 下载用户导入模板 */
static downloadTemplate() {
return request({
url: `${USER_BASE_URL}/template`,
method: "get",
responseType: "arraybuffer",
});
}
/**
* 导出用户
*
* @param queryParams 查询参数
*/
static export(queryParams: UserPageQuery) {
return request({
url: `${USER_BASE_URL}/export`,
method: "get",
params: queryParams,
responseType: "arraybuffer",
});
}
/**
* 导入用户
*
* @param deptId 部门ID
* @param file 导入文件
*/
static import(deptId: number, file: File) {
const formData = new FormData();
formData.append("file", file);
return request({
url: `${USER_BASE_URL}/import`,
method: "post",
params: { deptId: deptId },
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
}
}
export default UserAPI;
/** 登录用户信息 */
export interface UserInfo {
/** 用户ID */
userId?: number;
/** 用户名 */
username?: string;
/** 昵称 */
nickname?: string;
/** 头像URL */
avatar?: string;
/** 角色 */
roles: string[];
/** 权限 */
perms: string[];
}
/**
* 用户分页查询对象
*/
export interface UserPageQuery extends PageQuery {
/** 搜索关键字 */
keywords?: string;
/** 用户状态 */
status?: number;
/** 部门ID */
deptId?: number;
/** 开始时间 */
startTime?: string;
/** 结束时间 */
endTime?: string;
}
/** 用户分页对象 */
export interface UserPageVO {
/** 用户头像URL */
avatar?: string;
/** 创建时间 */
createTime?: Date;
/** 部门名称 */
deptName?: string;
/** 用户邮箱 */
email?: string;
/** 性别 */
genderLabel?: string;
/** 用户ID */
id?: number;
/** 手机号 */
mobile?: string;
/** 用户昵称 */
nickname?: string;
/** 角色名称,多个使用英文逗号(,)分割 */
roleNames?: string;
/** 用户状态(1:启用;0:禁用) */
status?: number;
/** 用户名 */
username?: string;
}
/** 用户表单类型 */
export interface UserForm {
/** 用户头像 */
avatar?: string;
/** 部门ID */
deptId?: number;
/** 邮箱 */
email?: string;
/** 性别 */
gender?: number;
/** 用户ID */
id?: number;
/** 手机号 */
mobile?: string;
/** 昵称 */
nickname?: string;
/** 角色ID集合 */
roleIds?: number[];
/** 用户状态(1:正常;0:禁用) */
status?: number;
/** 用户名 */
username?: string;
}

View File

@@ -1,141 +0,0 @@
import request from "@/utils/request";
import { UserForm, UserInfo, UserPageVO, UserQuery } from "./model";
class UserAPI {
/**
* 登录成功后获取用户信息(昵称、头像、权限集合和角色集合)
*/
static getInfo() {
return request<any, UserInfo>({
url: "/api/v1/users/me",
method: "get",
});
}
/**
* 获取用户分页列表
*
* @param queryParams
*/
static getPage(queryParams: UserQuery) {
return request<any, PageResult<UserPageVO[]>>({
url: "/api/v1/users/page",
method: "get",
params: queryParams,
});
}
/**
* 获取用户表单详情
*
* @param userId
*/
static getFormData(userId: number) {
return request<any, UserForm>({
url: "/api/v1/users/" + userId + "/form",
method: "get",
});
}
/**
* 添加用户
*
* @param data
*/
static add(data: UserForm) {
return request({
url: "/api/v1/users",
method: "post",
data: data,
});
}
/**
* 修改用户
*
* @param id
* @param data
*/
static update(id: number, data: UserForm) {
return request({
url: "/api/v1/users/" + id,
method: "put",
data: data,
});
}
/**
* 修改用户密码
*
* @param id
* @param password
*/
static updatePassword(id: number, password: string) {
return request({
url: "/api/v1/users/" + id + "/password",
method: "patch",
params: { password: password },
});
}
/**
* 删除用户
*
* @param ids
*/
static deleteByIds(ids: string) {
return request({
url: "/api/v1/users/" + ids,
method: "delete",
});
}
/**
* 下载用户导入模板
*
* @returns
*/
static downloadTemplate() {
return request({
url: "/api/v1/users/template",
method: "get",
responseType: "arraybuffer",
});
}
/**
* 导出用户
*
* @param queryParams
* @returns
*/
static export(queryParams: UserQuery) {
return request({
url: "/api/v1/users/export",
method: "get",
params: queryParams,
responseType: "arraybuffer",
});
}
/**
* 导入用户
*
* @param file
*/
static import(deptId: number, file: File) {
const formData = new FormData();
formData.append("file", file);
return request({
url: "/api/v1/users/import",
method: "post",
params: { deptId: deptId },
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
}
}
export default UserAPI;

View File

@@ -1,115 +0,0 @@
/**
* 登录用户信息
*/
export interface UserInfo {
userId?: number;
username?: string;
nickname?: string;
avatar?: string;
roles: string[];
perms: string[];
}
/**
* 用户查询对象类型
*/
export interface UserQuery extends PageQuery {
keywords?: string;
status?: number;
deptId?: number;
startTime?: string;
endTime?: string;
}
/**
* 用户分页对象
*/
export interface UserPageVO {
/**
* 用户头像地址
*/
avatar?: string;
/**
* 创建时间
*/
createTime?: Date;
/**
* 部门名称
*/
deptName?: string;
/**
* 用户邮箱
*/
email?: string;
/**
* 性别
*/
genderLabel?: string;
/**
* 用户ID
*/
id?: number;
/**
* 手机号
*/
mobile?: string;
/**
* 用户昵称
*/
nickname?: string;
/**
* 角色名称,多个使用英文逗号(,)分割
*/
roleNames?: string;
/**
* 用户状态(1:启用;0:禁用)
*/
status?: number;
/**
* 用户名
*/
username?: string;
}
/**
* 用户表单类型
*/
export interface UserForm {
/**
* 用户头像
*/
avatar?: string;
/**
* 部门ID
*/
deptId?: number;
/**
* 邮箱
*/
email?: string;
/**
* 性别
*/
gender?: number;
/**
* 用户ID
*/
id?: number;
mobile?: string;
/**
* 昵称
*/
nickname?: string;
/**
* 角色ID集合
*/
roleIds?: number[];
/**
* 用户状态(1:正常;0:禁用)
*/
status?: number;
/**
* 用户名
*/
username?: string;
}

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -29,11 +29,22 @@
删除
</el-button>
</template>
<!-- 导入 -->
<template v-else-if="item === 'import'">
<el-button
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
type="default"
icon="upload"
@click="handleToolbar(item)"
>
导入
</el-button>
</template>
<!-- 导出 -->
<template v-else-if="item === 'export'">
<el-button
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
type="primary"
type="default"
icon="download"
@click="handleToolbar(item)"
>
@@ -407,28 +418,28 @@
</el-dialog>
<!-- 导入弹窗 -->
<el-dialog
v-model="importsModalVisible"
v-model="importModalVisible"
:align-center="true"
title="导入数据"
width="600px"
style="padding-right: 0"
@close="handleCloseImportsModal"
@close="handleCloseImportModal"
>
<!-- 滚动 -->
<el-scrollbar max-height="60vh">
<!-- 表单 -->
<el-form
ref="importsFormRef"
ref="importFormRef"
label-width="auto"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importsFormData"
:rules="importsFormRules"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
class="w-full"
ref="uploadRef"
v-model:file-list="importsFormData.files"
v-model:file-list="importFormData.files"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
@@ -443,7 +454,7 @@
<div class="el-upload__tip">
*.xlsx / *.xls
<el-link
v-if="contentConfig.importsTemplate"
v-if="contentConfig.importTemplate"
type="primary"
icon="download"
:underline="false"
@@ -462,12 +473,12 @@
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button
type="primary"
:disabled="importsFormData.files.length === 0"
@click="handleImportsSubmit"
:disabled="importFormData.files.length === 0"
@click="handleImportSubmit"
>
确 定
</el-button>
<el-button @click="handleCloseImportsModal">取 消</el-button>
<el-button @click="handleCloseImportModal">取 消</el-button>
</div>
</template>
</el-dialog>
@@ -575,8 +586,8 @@ function handleSelectionChange(selection: any[]) {
}
// 刷新
function handleRefresh() {
fetchPageData(lastFormData);
function handleRefresh(isRestart = false) {
fetchPageData(lastFormData, isRestart);
}
// 删除
@@ -595,7 +606,7 @@ function handleDelete(id?: number | string) {
if (props.contentConfig.deleteAction) {
props.contentConfig.deleteAction(ids).then(() => {
ElMessage.success("删除成功");
fetchPageData({}, true);
handleRefresh(true);
});
} else {
ElMessage.error("未配置deleteAction");
@@ -695,20 +706,22 @@ function handleExports() {
}
// 导入表单
let isFileImport = false;
const uploadRef = ref<UploadInstance>();
const importsModalVisible = ref(false);
const importsFormRef = ref<FormInstance>();
const importsFormData = reactive<{
const importModalVisible = ref(false);
const importFormRef = ref<FormInstance>();
const importFormData = reactive<{
files: UploadUserFile[];
}>({
files: [],
});
const importsFormRules: FormRules = {
const importFormRules: FormRules = {
files: [{ required: true, message: "请选择文件" }],
};
// 打开导入弹窗
function handleOpenImportsModal() {
importsModalVisible.value = true;
function handleOpenImportModal(isFile: boolean = false) {
importModalVisible.value = true;
isFileImport = isFile;
}
// 覆盖前一个文件
function handleFileExceed(files: File[]) {
@@ -719,11 +732,11 @@ function handleFileExceed(files: File[]) {
}
// 下载导入模板
function handleDownloadTemplate() {
const importsTemplate = props.contentConfig.importsTemplate;
if (typeof importsTemplate === "string") {
window.open(importsTemplate);
} else if (typeof importsTemplate === "function") {
importsTemplate().then((response) => {
const importTemplate = props.contentConfig.importTemplate;
if (typeof importTemplate === "string") {
window.open(importTemplate);
} else if (typeof importTemplate === "function") {
importTemplate().then((response) => {
const fileData = response.data;
const fileName = decodeURI(
response.headers["content-disposition"].split(";")[1].split("=")[1]
@@ -731,21 +744,40 @@ function handleDownloadTemplate() {
saveXlsx(fileData, fileName);
});
} else {
ElMessage.error("未配置importsTemplate");
ElMessage.error("未配置importTemplate");
}
}
// 导入确认
const handleImportsSubmit = useThrottleFn(() => {
importsFormRef.value?.validate((valid: boolean) => {
valid && handleImports();
const handleImportSubmit = useThrottleFn(() => {
importFormRef.value?.validate((valid: boolean) => {
if (valid) {
if (isFileImport) {
handleImport();
} else {
handleImports();
}
}
});
}, 3000);
// 关闭导入弹窗
function handleCloseImportsModal() {
importsModalVisible.value = false;
importsFormRef.value?.resetFields();
function handleCloseImportModal() {
importModalVisible.value = false;
importFormRef.value?.resetFields();
nextTick(() => {
importsFormRef.value?.clearValidate();
importFormRef.value?.clearValidate();
});
}
// 文件导入
function handleImport() {
const importAction = props.contentConfig.importAction;
if (importAction === undefined) {
ElMessage.error("未配置importAction");
return;
}
importAction(importFormData.files[0].raw as File).then(() => {
ElMessage.success("导入数据成功");
handleCloseImportModal();
handleRefresh(true);
});
}
// 导入
@@ -756,7 +788,7 @@ function handleImports() {
return;
}
// 获取选择的文件
const file = importsFormData.files[0].raw as File;
const file = importFormData.files[0].raw as File;
// 创建Workbook实例
const workbook = new ExcelJS.Workbook();
// 使用FileReader对象来读取文件内容
@@ -803,7 +835,8 @@ function handleImports() {
}
importsAction(data).then(() => {
ElMessage.success("导入数据成功");
handleCloseImportsModal();
handleCloseImportModal();
handleRefresh(true);
});
})
.catch((error) => console.log(error));
@@ -823,7 +856,7 @@ function handleToolbar(name: string) {
handleOpenExportsModal();
break;
case "imports":
handleOpenImportsModal();
handleOpenImportModal();
break;
case "search":
emit("searchClick");
@@ -834,6 +867,9 @@ function handleToolbar(name: string) {
case "delete":
handleDelete();
break;
case "import":
handleOpenImportModal(true);
break;
case "export":
emit("exportClick");
break;
@@ -878,11 +914,11 @@ function handleModify(
// 分页切换
function handleSizeChange(value: number) {
pagination.pageSize = value;
fetchPageData(lastFormData);
handleRefresh();
}
function handleCurrentChange(value: number) {
pagination.currentPage = value;
fetchPageData(lastFormData);
handleRefresh();
}
// 远程数据筛选

View File

@@ -92,8 +92,10 @@ export interface IContentConfig<T = any> {
exportAction?: (queryParams: T) => Promise<any>;
// 前端全量导出的网络请求函数(需返回promise)
exportsAction?: (queryParams: T) => Promise<IObject[]>;
// 前端导入模板
importsTemplate?: string | (() => Promise<any>);
// 导入模板
importTemplate?: string | (() => Promise<any>);
// 后端导入的网络请求函数(需返回promise)
importAction?: (file: File) => Promise<any>;
// 前端导入的网络请求函数(需返回promise)
importsAction?: (data: IObject[]) => Promise<any>;
// 主键名(默认为id)
@@ -102,6 +104,7 @@ export interface IContentConfig<T = any> {
toolbar?: Array<
| "add"
| "delete"
| "import"
| "export"
| {
auth: string;

View File

@@ -20,9 +20,9 @@ import DictAPI from "@/api/dict";
const props = defineProps({
/**
* 字典类型编码(eg: 性别-gender)
* 字典编码(eg: 性别-gender)
*/
typeCode: {
code: {
type: String,
required: true,
},
@@ -39,14 +39,17 @@ const props = defineProps({
},
});
const emits = defineEmits(["update:modelValue"]); // 父组件监听事件,同步子组件值的变化给父组件
const emits = defineEmits(["update:modelValue"]);
const options: Ref<OptionType[]> = ref([]); // 字典下拉数据源
const options: Ref<OptionType[]> = ref([]);
const selectedValue = ref<string | number | undefined>();
watch([options, () => props.modelValue], ([newOptions, newModelValue]) => {
if (newOptions.length === 0) return; // 下拉数据源加载未完成不回显
if (newOptions.length === 0) {
// 下拉数据源加载未完成不回显
return;
}
if (newModelValue == undefined) {
selectedValue.value = undefined;
return;
@@ -65,8 +68,8 @@ function handleChange(val?: string | number | undefined) {
}
onBeforeMount(() => {
// 根据字典类型编码(typeCode)获取字典选项
DictAPI.getDictOptions(props.typeCode).then((data) => {
// 根据字典编码获取字典项
DictAPI.getOptions(props.code).then((data) => {
options.value = data;
});
});

View File

@@ -1,6 +1,7 @@
<template>
<!-- 如果菜单项没有隐藏则显示 -->
<div v-if="!item.meta || !item.meta.hidden">
<!-- 显示具有单个子路由的菜单项或没有子路由的父路由 -->
<!-- 显示只有一个子路由或没有子路由的菜单项 -->
<template
v-if="
hasOneShowingChild(item.children, item as RouteRecordRaw) &&
@@ -59,7 +60,7 @@ import { RouteRecordRaw } from "vue-router";
const props = defineProps({
/**
* 路由(eg:user)123
* 当前路由项对象
*/
item: {
type: Object,
@@ -67,47 +68,46 @@ const props = defineProps({
},
/**
* 父层级完整路由路径(eg:/system)
* 父层级完整路由路径
*/
basePath: {
type: String,
required: true,
},
/**
* 是否为嵌套路由
*/
isNest: {
type: Boolean,
default: false,
},
});
const onlyOneChild = ref(); // 临时变量,唯一子路由
const onlyOneChild = ref();
/**
* 判断当前路由是否只有一个子路由
*
* 1:如果只有一个子路由: 返回 true
* 2:如果无子路由 :返回 true
* 判断当前路由是否只有一个显示的子路由
*
* @param children 子路由数组
* @param parent 当前路由
* @param parent 父级路由对象
* @returns 布尔值,表示是否只有一个显示的子路由
*/
function hasOneShowingChild(
children: RouteRecordRaw[] = [],
parent: RouteRecordRaw
) {
// 子路由集合
// 筛选出需要显示的子路由
const showingChildren = children.filter((route: RouteRecordRaw) => {
if (route.meta?.hidden) {
// 过滤不显示的子路由
return false;
} else {
route.meta!.hidden = false;
// 临时变量(多个子路由 onlyOneChild 变量是用不上的)
onlyOneChild.value = route;
return true;
}
});
// 如果只有一个子路由, 返回 true
// 如果只有一个或没有显示的子路由
if (showingChildren.length === 1) {
return true;
}
@@ -121,9 +121,10 @@ function hasOneShowingChild(
}
/**
* 解析路由路径(相对路径 → 绝对路径)
* 解析路由路径,将相对路径转换为绝对路径
*
* @param routePath 路由路径
* @returns 绝对路径
*/
function resolvePath(routePath: string) {
if (isExternal(routePath)) {

View File

@@ -41,9 +41,14 @@ const appStore = useAppStore();
const permissionStore = usePermissionStore();
const router = useRouter();
// 避免 activeTopMenuPath 缓存被清理,从当前路由路径获取顶部菜单路径,eg. /system/user → /system
const activeTopMenuPath = useRoute().path.replace(/\/[^\/]+$/, "") || "/";
appStore.activeTopMenu(activeTopMenuPath);
// 激活的顶部菜单路径
const activePath = computed(() => appStore.activeTopMenuPath);
// 顶部菜单集合
// 混合模式顶部菜单集合
const mixTopMenus = ref<RouteRecordRaw[]>([]);
/**

View File

@@ -9,7 +9,7 @@
ref="tagRef"
v-for="tag in visitedViews"
:key="tag.fullPath"
:class="'tags-item ' + (isActive(tag) ? 'active' : '')"
:class="'tags-item ' + (tagsViewStore.isActive(tag) ? 'active' : '')"
:to="{ path: tag.path, query: tag.query }"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openContentMenu(tag, $event)"
@@ -190,10 +190,6 @@ function moveToCurrentTag() {
});
}
function isActive(tag: TagView) {
return tag.path === route.path;
}
function isAffix(tag: TagView) {
return tag?.affix;
}
@@ -228,26 +224,10 @@ function refreshSelectedTag(view: TagView) {
});
}
function toLastView(visitedViews: TagView[], view?: TagView) {
const latestView = visitedViews.slice(-1)[0];
if (latestView && latestView.fullPath) {
router.push(latestView.fullPath);
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view?.name === "Dashboard") {
// to reload home page
router.replace("/redirect" + view.fullPath);
} else {
router.push("/");
}
}
}
function closeSelectedTag(view: TagView) {
tagsViewStore.delView(view).then((res: any) => {
if (isActive(view)) {
toLastView(res.visitedViews, view);
if (tagsViewStore.isActive(view)) {
tagsViewStore.toLastView(res.visitedViews, view);
}
});
}
@@ -255,14 +235,14 @@ function closeSelectedTag(view: TagView) {
function closeLeftTags() {
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
if (!res.visitedViews.find((item: any) => item.path === route.path)) {
toLastView(res.visitedViews);
tagsViewStore.toLastView(res.visitedViews);
}
});
}
function closeRightTags() {
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
if (!res.visitedViews.find((item: any) => item.path === route.path)) {
toLastView(res.visitedViews);
tagsViewStore.toLastView(res.visitedViews);
}
});
}
@@ -276,7 +256,7 @@ function closeOtherTags() {
function closeAllTags(view: TagView) {
tagsViewStore.delAllViews().then((res: any) => {
toLastView(res.visitedViews, view);
tagsViewStore.toLastView(res.visitedViews, view);
});
}

View File

@@ -32,14 +32,14 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: "dashboard",
component: () => import("@/views/dashboard/index.vue"),
name: "Dashboard", // 用于 keep-alive, 必须与SFC自动推导或者显示声明的组件name一致
// https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude
// 用于 keep-alive 功能,需要与 SFC 中自动推导或显式声明的组件名称一致
// 参考文档: https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude
name: "Dashboard",
meta: {
title: "dashboard",
icon: "homepage",
affix: true,
keepAlive: true,
alwaysShow: false,
},
},
{

View File

@@ -7,19 +7,23 @@ import { store } from "@/store";
import { DeviceEnum } from "@/enums/DeviceEnum";
import { SidebarStatusEnum } from "@/enums/SidebarStatusEnum";
// setup
export const useAppStore = defineStore("app", () => {
// state
// 设备类型
const device = useStorage("device", DeviceEnum.DESKTOP);
// 布局大小
const size = useStorage("size", defaultSettings.size);
// 语言
const language = useStorage("language", defaultSettings.language);
// 侧边栏状态
const sidebarStatus = useStorage("sidebarStatus", SidebarStatusEnum.CLOSED);
const sidebar = reactive({
opened: sidebarStatus.value === SidebarStatusEnum.OPENED,
withoutAnimation: false,
});
// 顶部菜单激活路径
const activeTopMenuPath = useStorage("activeTopMenuPath", "");
/**
* 根据语言标识读取对应的语言包
*/
@@ -31,7 +35,7 @@ export const useAppStore = defineStore("app", () => {
}
});
// actions
// 切换侧边栏
function toggleSidebar() {
sidebar.opened = !sidebar.opened;
sidebarStatus.value = sidebar.opened
@@ -39,20 +43,28 @@ export const useAppStore = defineStore("app", () => {
: SidebarStatusEnum.CLOSED;
}
// 关闭侧边栏
function closeSideBar() {
sidebar.opened = false;
sidebarStatus.value = SidebarStatusEnum.CLOSED;
}
// 打开侧边栏
function openSideBar() {
sidebar.opened = true;
sidebarStatus.value = SidebarStatusEnum.OPENED;
}
// 切换设备
function toggleDevice(val: string) {
device.value = val;
}
/**
* 改变布局大小
*
* @param val 布局大小 default | small | large
*/
function changeSize(val: string) {
size.value = val;
}

View File

@@ -1,8 +1,7 @@
import { RouteRecordRaw } from "vue-router";
import { constantRoutes } from "@/router";
import { store } from "@/store";
import MenuAPI from "@/api/menu";
import { RouteVO } from "@/api/menu/model";
import MenuAPI, { RouteVO } from "@/api/menu";
const modules = import.meta.glob("../../views/**/**.vue");
const Layout = () => import("@/layout/index.vue");

View File

@@ -1,7 +1,8 @@
export const useTagsViewStore = defineStore("tagsView", () => {
const visitedViews = ref<TagView[]>([]);
const cachedViews = ref<string[]>([]);
const router = useRouter();
const route = useRoute();
/**
* 添加已访问视图到已访问视图列表中
*/
@@ -189,6 +190,44 @@ export const useTagsViewStore = defineStore("tagsView", () => {
});
}
/**
* 关闭当前tagView
*/
function closeCurrentView() {
const tags: TagView = {
name: route.name as string,
title: route.meta.title as string,
path: route.path,
fullPath: route.fullPath,
affix: route.meta?.affix,
keepAlive: route.meta?.keepAlive,
query: route.query,
};
delView(tags).then((res: any) => {
if (isActive(tags)) {
toLastView(res.visitedViews, tags);
}
});
}
function isActive(tag: TagView) {
return tag.path === route.path;
}
function toLastView(visitedViews: TagView[], view?: TagView) {
const latestView = visitedViews.slice(-1)[0];
if (latestView && latestView.fullPath) {
router.push(latestView.fullPath);
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view?.name === "Dashboard") {
// to reload home page
router.replace("/redirect" + view.fullPath);
} else {
router.push("/");
}
}
}
return {
visitedViews,
cachedViews,
@@ -207,5 +246,8 @@ export const useTagsViewStore = defineStore("tagsView", () => {
delAllViews,
delAllVisitedViews,
delAllCachedViews,
closeCurrentView,
isActive,
toLastView,
};
});

View File

@@ -3,8 +3,8 @@ import UserAPI from "@/api/user";
import { resetRouter } from "@/router";
import { store } from "@/store";
import { LoginData } from "@/api/auth/model";
import { UserInfo } from "@/api/user/model";
import { LoginData } from "@/api/auth";
import { UserInfo } from "@/api/user";
import { TOKEN_KEY } from "@/enums/CacheEnum";
export const useUserStore = defineStore("user", () => {

81
src/styles/login.scss Normal file
View File

@@ -0,0 +1,81 @@
.login-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow-y: auto;
background: url("@/assets/images/login-background-light.jpg") no-repeat center
right;
.top-bar {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
padding: 10px;
}
.login-card {
width: 400px;
background: transparent;
border: none;
border-radius: 4%;
@media (width <= 640px) {
width: 340px;
}
.input-wrapper {
display: flex;
align-items: center;
width: 100%;
}
.captcha-image {
height: 48px;
cursor: pointer;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
}
.icp-info {
position: absolute;
bottom: 4px;
font-size: 12px;
text-align: center;
}
.el-form-item {
background: var(--el-input-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 5px;
}
.el-input {
.el-input__wrapper {
padding: 0;
background-color: transparent;
box-shadow: none;
&.is-focus,
&:hover {
box-shadow: none !important;
}
input:-webkit-autofill {
/* 通过延时渲染背景色变相去除背景颜色 */
transition: background-color 1000s ease-in-out 0s;
}
}
}
}
html.dark .login-container {
background: url("@/assets/images/login-background-dark.jpg") no-repeat center
right;
}

View File

@@ -13,6 +13,7 @@ declare module "vue" {
Breadcrumb: (typeof import("./../components/Breadcrumb/index.vue"))["default"];
CURD: (typeof import("./../components/CURD/index.vue"))["default"];
DeptTree: (typeof import("./../views/system/user/components/dept-tree.vue"))["default"];
UserImport: (typeof import("./../views/system/user/components/user-import.vue"))["default"];
Dictionary: (typeof import("./../components/Dictionary/index.vue"))["default"];
DictItem: (typeof import("./../views/system/dict/components/dict-item.vue"))["default"];
ElBacktop: (typeof import("element-plus/es"))["ElBacktop"];

60
src/types/router.d.ts vendored Normal file
View File

@@ -0,0 +1,60 @@
import "vue-router";
declare module "vue-router" {
// https://router.vuejs.org/zh/guide/advanced/meta.html#typescript
// 可以通过扩展 RouteMeta 接口来输入 meta 字段
interface RouteMeta {
/**
* 菜单名称
* @example 'Dashboard'
*/
title?: string;
/**
* 菜单图标
* @example 'el-icon-edit'
*/
icon?: string;
/**
* 是否隐藏菜单项
* true 隐藏, false 显示
* @default false
*/
hidden?: boolean;
/**
* 始终显示父级菜单,即使只有一个子菜单
* true 显示父级菜单, false 隐藏父级菜单,显示唯一子节点
* @default false
*/
alwaysShow?: boolean;
/**
* 是否固定在页签上
* true 固定, false 不固定
* @default false
*/
affix?: boolean;
/**
* 是否缓存页面
* true 缓存, false 不缓存
* @default false
*/
keepAlive?: boolean;
/**
* 是否在面包屑导航中隐藏
* true 隐藏, false 显示
* @default false
*/
breadcrumb?: boolean;
/**
* 拥有访问该菜单权限的角色编码集合
* @example ['admin', 'editor']
*/
roles?: string[];
}
}

View File

@@ -1,24 +0,0 @@
import "vue-router";
declare module "vue-router" {
// https://router.vuejs.org/zh/guide/advanced/meta.html#typescript
// 可以通过扩展 RouteMeta 接口来输入 meta 字段
interface RouteMeta {
/** 菜单名称 */
title?: string;
/** 菜单图标 */
icon?: string;
/** 菜单是否隐藏 */
hidden?: boolean;
/** 只有一个子路由是否始终显示 */
alwaysShow?: boolean;
/** 是否固定页签 */
affix?: boolean;
/** 是否缓存页面 */
keepAlive?: boolean;
/** 是否在面包屑上隐藏 */
breadcrumb?: boolean;
/** 拥有菜单权限的角色编码集合 */
roles?: string[];
}
}

View File

@@ -1,7 +1,6 @@
import DeptAPI from "@/api/dept";
import RoleAPI from "@/api/role";
import UserAPI from "@/api/user";
import type { UserForm } from "@/api/user/model";
import UserAPI, { UserForm } from "@/api/user";
import type { IModalConfig } from "@/components/CURD/types";
const modalConfig: IModalConfig<UserForm> = {

View File

@@ -1,9 +1,9 @@
import UserAPI from "@/api/user";
import RoleAPI from "@/api/role";
import type { UserQuery } from "@/api/user/model";
import type { UserPageQuery } from "@/api/user";
import type { IContentConfig } from "@/components/CURD/types";
const contentConfig: IContentConfig<UserQuery> = {
const contentConfig: IContentConfig<UserPageQuery> = {
pageName: "sys:user",
table: {
border: true,
@@ -25,8 +25,11 @@ const contentConfig: IContentConfig<UserQuery> = {
return UserAPI.getPage(params);
},
deleteAction: UserAPI.deleteByIds,
importAction(file) {
return UserAPI.import(1, file);
},
exportAction: UserAPI.export,
importsTemplate: UserAPI.downloadTemplate,
importTemplate: UserAPI.downloadTemplate,
importsAction(data) {
// 模拟导入数据
console.log("importsAction", data);
@@ -42,13 +45,8 @@ const contentConfig: IContentConfig<UserQuery> = {
toolbar: [
"add",
"delete",
"import",
"export",
{
name: "import",
icon: "upload",
text: "导入",
auth: "import",
},
{
name: "custom1",
icon: "plus",

View File

@@ -1,7 +1,7 @@
import DeptAPI from "@/api/dept";
import RoleAPI from "@/api/role";
import UserAPI from "@/api/user";
import type { UserForm } from "@/api/user/model";
import type { UserForm } from "@/api/user";
import type { IModalConfig } from "@/components/CURD/types";
import { DeviceEnum } from "@/enums/DeviceEnum";
import { useAppStore } from "@/store";

View File

@@ -49,7 +49,7 @@
@submit-click="handleSubmitClick"
>
<template #gender="scope">
<dictionary v-model="scope.formData[scope.prop]" type-code="gender" />
<dictionary v-model="scope.formData[scope.prop]" code="gender" />
</template>
</page-modal>
@@ -60,7 +60,7 @@
@submit-click="handleSubmitClick"
>
<template #gender="scope">
<dictionary v-model="scope.formData[scope.prop]" type-code="gender" />
<dictionary v-model="scope.formData[scope.prop]" code="gender" />
</template>
</page-modal>
</template>
@@ -113,8 +113,8 @@ async function handleEditClick(row: IObject) {
// 其他工具栏
function handleToolbarClick(name: string) {
console.log(name);
if (name === "import") {
ElMessage.success("点击了导入按钮");
if (name === "custom1") {
ElMessage.success("点击了自定义1按钮");
}
}
// 其他操作列

View File

@@ -15,14 +15,14 @@ const numberValue = ref(1); // 性别(值为Number)
>
<el-form>
<el-form-item label="性别">
<dictionary v-model="stringValue" type-code="gender" />
<dictionary v-model="stringValue" code="gender" />
<el-link :underline="false" type="primary" class="ml-5"
>值为String: const value = ref("1");
</el-link>
</el-form-item>
<el-form-item label="性别">
<dictionary v-model="numberValue" type-code="gender" />
<dictionary v-model="numberValue" code="gender" />
<el-link :underline="false" type="success" class="ml-5"
>值为Number: const value = ref(1);
</el-link>

View File

@@ -1,7 +1,7 @@
<template>
<div class="login-container">
<!-- 顶部 -->
<div class="absolute-lt flex-x-end p-3 w-full">
<!-- 顶部工具栏 -->
<div class="top-bar">
<el-switch
v-model="isDark"
inline-prompt
@@ -12,7 +12,7 @@
<lang-select class="ml-2 cursor-pointer" />
</div>
<!-- 登录表单 -->
<el-card class="!border-none !bg-transparent !rounded-4% w-100 <sm:w-85">
<el-card class="login-card">
<div class="text-center relative">
<h2>{{ defaultSettings.title }}</h2>
<el-tag class="ml-2 absolute-rt">{{ defaultSettings.version }}</el-tag>
@@ -26,7 +26,7 @@
>
<!-- 用户名 -->
<el-form-item prop="username">
<div class="flex-y-center w-full">
<div class="input-wrapper">
<svg-icon icon-class="user" class="mx-2" />
<el-input
ref="username"
@@ -46,7 +46,7 @@
placement="right"
>
<el-form-item prop="password">
<div class="flex-y-center w-full">
<div class="input-wrapper">
<svg-icon icon-class="lock" class="mx-2" />
<el-input
v-model="loginData.password"
@@ -54,7 +54,7 @@
type="password"
name="password"
@keyup="checkCapslock"
@keyup.enter="handleLogin"
@keyup.enter="handleLoginSubmit"
size="large"
class="h-[48px] pr-2"
show-password
@@ -65,7 +65,7 @@
<!-- 验证码 -->
<el-form-item prop="captchaCode">
<div class="flex-y-center w-full">
<div class="input-wrapper">
<svg-icon icon-class="captcha" class="mx-2" />
<el-input
v-model="loginData.captchaCode"
@@ -73,13 +73,13 @@
size="large"
class="flex-1"
:placeholder="$t('login.captchaCode')"
@keyup.enter="handleLogin"
@keyup.enter="handleLoginSubmit"
/>
<el-image
@click="getCaptcha"
:src="captchaBase64"
class="rounded-tr-md rounded-br-md cursor-pointer h-[48px]"
class="captcha-image"
/>
</div>
</el-form-item>
@@ -90,7 +90,7 @@
type="primary"
size="large"
class="w-full"
@click.prevent="handleLogin"
@click.prevent="handleLoginSubmit"
>{{ $t("login.login") }}
</el-button>
@@ -103,7 +103,7 @@
</el-card>
<!-- ICP备案 -->
<div class="absolute bottom-1 text-[10px] text-center" v-show="icpVisible">
<div class="icp-info" v-show="icpVisible">
<p>
Copyright © 2021 - 2024 youlai.tech All Rights Reserved. 有来技术
版权所有
@@ -114,35 +114,48 @@
</template>
<script setup lang="ts">
import { useSettingsStore, useUserStore } from "@/store";
import AuthAPI from "@/api/auth";
import { LoginData } from "@/api/auth/model";
import type { FormInstance } from "element-plus";
// 外部库和依赖
import { LocationQuery, useRoute } from "vue-router";
// 内部依赖
import { useSettingsStore, useUserStore } from "@/store";
import AuthAPI, { LoginData } from "@/api/auth";
import router from "@/router";
import defaultSettings from "@/settings";
import { ThemeEnum } from "@/enums/ThemeEnum";
// 类型定义
import type { FormInstance } from "element-plus";
// 导入 login.scss 文件
import "@/styles/login.scss";
// 使用导入的依赖和库
const userStore = useUserStore();
const settingsStore = useSettingsStore();
const route = useRoute();
// Internationalization
// 窗口高度
const { height } = useWindowSize();
// 国际化 Internationalization
const { t } = useI18n();
// Reactive states
// 是否暗黑模式
const isDark = ref(settingsStore.theme === ThemeEnum.DARK);
// 是否显示 ICP 备案信息
const icpVisible = ref(true);
const loading = ref(false); // 按钮loading
const isCapslock = ref(false); // 是否大写锁定
const captchaBase64 = ref(); // 验证码图片Base64字符串
const loginFormRef = ref<FormInstance>(); // 登录表单ref
const { height } = useWindowSize();
// 按钮 loading 状态
const loading = ref(false);
// 是否大写锁定
const isCapslock = ref(false);
// 验证码图片Base64字符串
const captchaBase64 = ref();
// 登录表单ref
const loginFormRef = ref<FormInstance>();
const loginData = ref<LoginData>({
username: "admin",
password: "123456",
});
} as LoginData);
const loginRules = computed(() => {
return {
@@ -183,8 +196,8 @@ function getCaptcha() {
});
}
// 登录
function handleLogin() {
/** 登录表单提交 */
function handleLoginSubmit() {
loginFormRef.value?.validate((valid: boolean) => {
if (valid) {
loading.value = true;
@@ -204,7 +217,7 @@ function handleLogin() {
});
}
// 解析 redirect 字符串 为 path 和 queryParams
/** 解析 redirect 字符串 为 path 和 queryParams */
function parseRedirect(): {
path: string;
queryParams: Record<string, string>;
@@ -252,43 +265,4 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
html.dark .login-container {
background: url("@/assets/images/login-bg-dark.jpg") no-repeat center right;
}
.login-container {
overflow-y: auto;
background: url("@/assets/images/login-bg.jpg") no-repeat center right;
@apply wh-full flex-center;
.login-form {
padding: 30px 10px;
}
}
.el-form-item {
background: var(--el-input-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 5px;
}
:deep(.el-input) {
.el-input__wrapper {
padding: 0;
background-color: transparent;
box-shadow: none;
&.is-focus,
&:hover {
box-shadow: none !important;
}
input:-webkit-autofill {
/* 通过延时渲染背景色变相去除背景颜色 */
transition: background-color 1000s ease-in-out 0s;
}
}
}
</style>
<style lang="scss" scoped></style>

View File

@@ -38,7 +38,7 @@
<el-button
v-hasPerm="['sys:dept:add']"
type="success"
@click="openDialog(0, undefined)"
@click="handleOpenDialog(0, undefined)"
><i-ep-plus />新增</el-button
>
<el-button
@@ -60,6 +60,7 @@
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="name" label="部门名称" min-width="200" />
<el-table-column prop="code" label="部门编号" width="200" />
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag v-if="scope.row.status == 1" type="success">正常</el-tag>
@@ -76,7 +77,7 @@
type="primary"
link
size="small"
@click.stop="openDialog(scope.row.id, undefined)"
@click.stop="handleOpenDialog(scope.row.id, undefined)"
><i-ep-plus />新增
</el-button>
<el-button
@@ -84,12 +85,12 @@
type="primary"
link
size="small"
@click.stop="openDialog(scope.row.parentId, scope.row.id)"
@click.stop="handleOpenDialog(scope.row.parentId, scope.row.id)"
><i-ep-edit />编辑
</el-button>
<el-button
v-hasPerm="['sys:dept:delete']"
type="primary"
type="danger"
link
size="small"
@click.stop="handleDelete(scope.row.id)"
@@ -105,7 +106,7 @@
v-model="dialog.visible"
:title="dialog.title"
width="600px"
@closed="closeDialog"
@closed="handleCloseDialog"
>
<el-form
ref="deptFormRef"
@@ -126,6 +127,9 @@
<el-form-item label="部门名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入部门名称" />
</el-form-item>
<el-form-item label="部门编号" prop="code">
<el-input v-model="formData.code" placeholder="请输入部门编号" />
</el-form-item>
<el-form-item label="显示排序" prop="sort">
<el-input-number
v-model="formData.sort"
@@ -145,7 +149,7 @@
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit"> 确 定 </el-button>
<el-button @click="closeDialog"> 取 消 </el-button>
<el-button @click="handleCloseDialog"> 取 消 </el-button>
</div>
</template>
</el-dialog>
@@ -158,8 +162,7 @@ defineOptions({
inheritAttrs: false,
});
import DeptAPI from "@/api/dept";
import { DeptVO, DeptForm, DeptQuery } from "@/api/dept/model";
import DeptAPI, { DeptVO, DeptForm, DeptQuery } from "@/api/dept";
const queryFormRef = ref(ElForm);
const deptFormRef = ref(ElForm);
@@ -183,12 +186,15 @@ const formData = reactive<DeptForm>({
});
const rules = reactive({
parentId: [{ required: true, message: "上级部门不能为空", trigger: "blur" }],
parentId: [
{ required: true, message: "上级部门不能为空", trigger: "change" },
],
name: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
code: [{ required: true, message: "部门编号不能为空", trigger: "blur" }],
sort: [{ required: true, message: "显示排序不能为空", trigger: "blur" }],
});
/** 查询 */
/** 查询部门 */
function handleQuery() {
loading.value = true;
DeptAPI.getList(queryParams).then((data) => {
@@ -197,7 +203,7 @@ function handleQuery() {
});
}
/**重置查询 */
/** 重置查询 */
function handleResetQuery() {
queryFormRef.value.resetFields();
handleQuery();
@@ -208,27 +214,23 @@ function handleSelectionChange(selection: any) {
ids.value = selection.map((item: any) => item.id);
}
/** 获取部门下拉数据 */
async function loadDeptOptions() {
DeptAPI.getOptions().then((data) => {
deptOptions.value = [
{
value: 0,
label: "顶级部门",
children: data,
},
];
});
}
/**
* 打开弹窗
* 打开部门弹窗
*
* @param parentId 父部门ID
* @param deptId 部门ID
*/
async function openDialog(parentId?: number, deptId?: number) {
await loadDeptOptions();
async function handleOpenDialog(parentId?: number, deptId?: number) {
// 加载部门下拉数据
const data = await DeptAPI.getOptions();
deptOptions.value = [
{
value: 0,
label: "顶级部门",
children: data,
},
];
dialog.visible = true;
if (deptId) {
dialog.title = "修改部门";
@@ -241,17 +243,17 @@ async function openDialog(parentId?: number, deptId?: number) {
}
}
/** 表单提交 */
/** 提交部门表单 */
function handleSubmit() {
deptFormRef.value.validate((valid: any) => {
if (valid) {
const deptId = formData.id;
loading.value = true;
const deptId = formData.id;
if (deptId) {
DeptAPI.update(deptId, formData)
.then(() => {
ElMessage.success("修改成功");
closeDialog();
handleCloseDialog();
handleQuery();
})
.finally(() => (loading.value = false));
@@ -259,7 +261,7 @@ function handleSubmit() {
DeptAPI.add(formData)
.then(() => {
ElMessage.success("新增成功");
closeDialog();
handleCloseDialog();
handleQuery();
})
.finally(() => (loading.value = false));
@@ -281,22 +283,26 @@ function handleDelete(deptId?: number) {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
DeptAPI.deleteByIds(deptIds).then(() => {
ElMessage.success("删除成功");
handleResetQuery();
});
});
}).then(
() => {
loading.value = true;
DeptAPI.deleteByIds(deptIds)
.then(() => {
ElMessage.success("删除成功");
handleResetQuery();
})
.finally(() => (loading.value = false));
},
() => {
ElMessage.info("已取消删除");
}
);
}
/** 关闭弹窗 */
function closeDialog() {
function handleCloseDialog() {
dialog.visible = false;
resetForm();
}
/** 重置表单 */
function resetForm() {
deptFormRef.value.resetFields();
deptFormRef.value.clearValidate();

View File

@@ -1,308 +0,0 @@
<!-- 字典数据 -->
<template>
<div class="app-container">
<div class="search-container">
<!-- 搜索表单 -->
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="关键字" prop="name">
<el-input
v-model="queryParams.name"
placeholder="字典名称"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery"
><i-ep-search />搜索</el-button
>
<el-button @click="handleResetQuery"> <i-ep-refresh />重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="never">
<template #header>
<el-button
v-hasPerm="['sys:dict:add']"
type="success"
@click="openDialog()"
><i-ep-plus />新增</el-button
>
<el-button
v-hasPerm="['sys:dict:delete']"
type="danger"
:disabled="ids.length === 0"
@click="handleDelete()"
><i-ep-delete />删除</el-button
>
</template>
<!-- 数据表格 -->
<el-table
v-loading="loading"
:data="dictList"
border
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" />
<el-table-column label="字典名称" prop="name" />
<el-table-column label="字典值" prop="value" />
<el-table-column label="状态" align="center">
<template #default="scope">
<el-tag v-if="scope.row.status === 1" type="success">启用</el-tag>
<el-tag v-else type="info">禁用</el-tag>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" align="center">
<template #default="scope">
<el-button
v-hasPerm="['sys:dict:edit']"
type="primary"
link
@click="openDialog(scope.row.id)"
><i-ep-edit />编辑</el-button
>
<el-button
v-hasPerm="['sys:dict:delete']"
type="primary"
link
@click.stop="handleDelete(scope.row.id)"
><i-ep-delete />删除</el-button
>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="handleQuery"
/>
</el-card>
<!-- 表单弹窗 -->
<el-dialog
v-model="dialog.visible"
:title="dialog.title"
width="500px"
@close="closeDialog"
>
<el-form
ref="dataFormRef"
:model="formData"
:rules="rules"
label-width="100px"
>
<el-form-item label="字典名称">{{ typeName }}</el-form-item>
<el-form-item label="字典名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入字典名称" />
</el-form-item>
<el-form-item label="字典值" prop="value">
<el-input v-model="formData.value" placeholder="字典值" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="formData.sort"
controls-position="right"
:min="0"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="closeDialog">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "DictData",
inheritAttrs: false,
});
import DictAPI from "@/api/dict";
import { DictPageVO, DictForm, DictQuery } from "@/api/dict/model";
const props = defineProps({
typeCode: {
type: String,
default: () => {
return "";
},
},
typeName: {
type: String,
default: () => {
return "";
},
},
});
watch(
() => props.typeCode,
(newVal: string) => {
queryParams.typeCode = newVal;
formData.typeCode = newVal;
handleResetQuery();
}
);
const queryFormRef = ref(ElForm);
const dataFormRef = ref(ElForm);
const loading = ref(false);
const ids = ref<number[]>([]);
const total = ref(0);
const queryParams = reactive<DictQuery>({
pageNum: 1,
pageSize: 10,
typeCode: props.typeCode,
});
const dictList = ref<DictPageVO[]>();
const dialog = reactive({
title: "",
visible: false,
});
const formData = reactive<DictForm>({
status: 1,
typeCode: props.typeCode,
sort: 1,
});
const rules = reactive({
name: [{ required: true, message: "请输入字典名称", trigger: "blur" }],
value: [{ required: true, message: "请输入字典值", trigger: "blur" }],
});
/** 查询 */
function handleQuery() {
if (queryParams.typeCode) {
loading.value = true;
DictAPI.getDictPage(queryParams)
.then((data) => {
dictList.value = data.list;
total.value = data.total;
})
.finally(() => (loading.value = false));
}
}
/** 重置查询 */
function handleResetQuery() {
queryFormRef.value.resetFields();
queryParams.pageNum = 1;
handleQuery();
}
/**
* 行checkbox change事件
*
* @param selection
*/
function handleSelectionChange(selection: any) {
ids.value = selection.map((item: any) => item.id);
}
/**
* 打开字典表单弹窗
*
* @param dictId 字典ID
*/
function openDialog(dictId?: number) {
dialog.visible = true;
if (dictId) {
dialog.title = "修改字典";
DictAPI.getDictFormData(dictId).then((data) => {
Object.assign(formData, data);
});
} else {
dialog.title = "新增字典";
}
}
/** 字典表单提交 */
function handleSubmit() {
dataFormRef.value.validate((isValid: boolean) => {
if (isValid) {
loading.value = false;
const dictId = formData.id;
if (dictId) {
DictAPI.updateDict(dictId, formData)
.then(() => {
ElMessage.success("修改成功");
closeDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
} else {
DictAPI.addDict(formData)
.then(() => {
ElMessage.success("新增成功");
closeDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
}
}
});
}
/** 关闭弹窗 */
function closeDialog() {
dialog.visible = false;
resetForm();
}
/** 重置表单 */
function resetForm() {
dataFormRef.value.resetFields();
dataFormRef.value.clearValidate();
formData.id = undefined;
formData.status = 1;
formData.sort = 1;
formData.typeCode = props.typeCode;
}
/** 删除字典 */
function handleDelete(dictId?: number) {
const dictIds = [dictId || ids.value].join(",");
if (!dictIds) {
ElMessage.warning("请勾选删除项");
return;
}
ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
DictAPI.deleteDictByIds(dictIds).then(() => {
ElMessage.success("删除成功");
handleResetQuery();
});
});
}
onMounted(() => {
handleQuery();
});
</script>

View File

@@ -1,5 +1,4 @@
<!--字典类型-->
<!-- 分类字典 -->
<template>
<div class="app-container">
<div class="search-container">
@@ -7,7 +6,7 @@
<el-form-item label="关键字" prop="name">
<el-input
v-model="queryParams.keywords"
placeholder="字典类型名称/编码"
placeholder="字典名称"
clearable
@keyup.enter="handleQuery"
/>
@@ -16,19 +15,16 @@
<el-button type="primary" @click="handleQuery()"
><i-ep-search />搜索</el-button
>
<el-button @click="handleResetQuery()"
<el-button @click="handleResetClick()"
><i-ep-refresh />重置</el-button
>
</el-form-item>
</el-form>
</div>
<el-card shadow="never" class="table-container">
<template #header>
<el-button
v-hasPerm="['sys:dict_type:add']"
type="success"
@click="openDialog()"
<el-card shadow="never">
<div class="mb-[10px]">
<el-button type="success" @click="handleAddClick()"
><i-ep-plus />新增</el-button
>
<el-button
@@ -37,44 +33,46 @@
@click="handleDelete()"
><i-ep-delete />删除</el-button
>
</template>
</div>
<el-table
v-loading="loading"
highlight-current-row
:data="dictTypeList"
:data="tableData"
border
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="字典类型名称" prop="name" width="200" />
<el-table-column label="字典类型编码" prop="code" width="200" />
<el-table-column label="状态" align="center" width="100">
<template #default="scope">
<el-tag v-if="scope.row.status === 1" type="success">启用</el-tag>
<el-tag v-else type="info">禁用</el-tag>
<el-table-column type="expand" label="字典项列表" width="100">
<template #default="props">
<el-table :data="props.row.dictItems">
<el-table-column label="字典项键" prop="name" width="200" />
<el-table-column label="字典项值" prop="value" align="center" />
<el-table-column label="排序" prop="sort" align="center" />
</el-table>
</template>
</el-table-column>
<el-table-column label="字典名称" prop="name" />
<el-table-column label="字典编码" prop="code" />
<el-table-column label="状态" prop="status">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
{{ scope.row.status === 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" align="center" />
<el-table-column fixed="right" label="操作" align="center" width="220">
<template #default="scope">
<el-button
type="primary"
link
size="small"
@click.stop="openDictDialog(scope.row)"
><i-ep-Collection />字典数据</el-button
>
<el-button
v-hasPerm="['sys:dict_type:edit']"
type="primary"
link
size="small"
@click.stop="openDialog(scope.row.id)"
@click.stop="handleEditClick(scope.row.id, scope.row.name)"
><i-ep-edit />编辑</el-button
>
<el-button
v-hasPerm="['sys:dict_type:delete']"
type="primary"
type="danger"
link
size="small"
@click.stop="handleDelete(scope.row.id)"
@@ -93,71 +91,124 @@
/>
</el-card>
<el-dialog
<!--字典弹窗-->
<el-drawer
v-model="dialog.visible"
:title="dialog.title"
width="500px"
@close="closeDialog"
size="70%"
@close="handleCloseDialog"
>
<el-form
ref="dataFormRef"
:model="formData"
:rules="rules"
label-width="80px"
:rules="computedRules"
label-width="100px"
:inline="true"
>
<el-form-item label="字典名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入字典名称" />
</el-form-item>
<el-form-item label="字典编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入字典编码" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
placeholder="字典类型备注"
:autosize="{ minRows: 2, maxRows: 4 }"
/>
</el-form-item>
<el-card shadow="never">
<el-form-item label="字典名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入字典名称" />
</el-form-item>
<el-form-item label="字典编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入字典编码" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="formData.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-card>
<el-card shadow="never" class="mt-5">
<template #header>
<div class="flex-x-between">
<span>字典项</span>
<el-button
type="primary"
size="small"
@click.stop="handleAddAttrClick"
><i-ep-plus />新增字典</el-button
>
</div>
</template>
<el-table
v-loading="loading"
highlight--currentrow
:data="formData.dictItems"
@selection-change="handleSelectionChange"
>
<el-table-column label="字典项名称" width="200">
<template #default="scope">
<el-form-item :prop="'dictItems.' + scope.$index + '.name'">
<el-input v-model="scope.row.name" />
</el-form-item>
</template>
</el-table-column>
<el-table-column label="字典项值" width="200">
<template #default="scope">
<el-form-item :prop="'dictItems.' + scope.$index + '.value'">
<el-input v-model="scope.row.value" />
</el-form-item>
</template>
</el-table-column>
<el-table-column label="排序">
<template #default="scope">
<el-form-item :prop="'dictItems.' + scope.$index + '.sort'">
<el-input v-model="scope.row.sort" />
</el-form-item>
</template>
</el-table-column>
<el-table-column label="状态" prop="status">
<template #default="scope">
<el-form-item :prop="'dictItems.' + scope.$index + '.status'">
<el-switch
v-model="scope.row.status"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column
fixed="right"
label="操作"
align="center"
width="120"
>
<template #default="scope">
<el-button
type="danger"
link
size="small"
@click.stop="handleDeleteAttrClick(scope.$index)"
><i-ep-delete />删除</el-button
>
</template>
</el-table-column>
</el-table>
</el-card>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="closeDialog">取 消</el-button>
<el-button type="primary" @click="handleSubmitClick">确 定</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-dialog>
<!--字典数据弹窗-->
<el-dialog
v-model="dictDataDialog.visible"
:title="dictDataDialog.title"
width="1000px"
@close="closeDictDialog"
>
<dict-item
v-model:typeCode="selectedDictType.typeCode"
v-model:typeName="selectedDictType.typeName"
/>
</el-dialog>
</el-drawer>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "DictType",
inheritAttrs: false,
name: "Dict",
inherititems: false,
});
import DictAPI from "@/api/dict";
import { DictTypePageVO, DictTypeQuery, DictTypeForm } from "@/api/dict/model";
import DictAPI, { DictPageQuery, DictPageVO, DictForm } from "@/api/dict";
const queryFormRef = ref(ElForm);
const dataFormRef = ref(ElForm);
@@ -166,33 +217,42 @@ const loading = ref(false);
const ids = ref<number[]>([]);
const total = ref(0);
const queryParams = reactive<DictTypeQuery>({
const queryParams = reactive<DictPageQuery>({
pageNum: 1,
pageSize: 10,
});
const dictTypeList = ref<DictTypePageVO[]>();
const tableData = ref<DictPageVO[]>();
// 字典弹窗
const dialog = reactive({
title: "",
visible: false,
});
const formData = reactive<DictTypeForm>({
status: 1,
const formData = reactive<DictForm>({});
const computedRules = computed(() => {
const rules: Partial<Record<string, any>> = {
name: [{ required: true, message: "请输入字典名称", trigger: "blur" }],
code: [{ required: true, message: "请输入字典编码", trigger: "blur" }],
};
if (formData.dictItems) {
formData.dictItems.forEach((attr, index) => {
rules[`dictItems.${index}.name`] = [
{ required: true, message: "请输入字典项名称", trigger: "blur" },
];
});
}
return rules;
});
const rules = reactive({
name: [{ required: true, message: "请输入字典类型名称", trigger: "blur" }],
code: [{ required: true, message: "请输入字典类型编码", trigger: "blur" }],
});
/** 查询 */
// 查询
function handleQuery() {
loading.value = true;
DictAPI.getDictTypePage(queryParams)
DictAPI.getPage(queryParams)
.then((data) => {
dictTypeList.value = data.list;
tableData.value = data.list;
total.value = data.total;
})
.finally(() => {
@@ -200,56 +260,57 @@ function handleQuery() {
});
}
/**
* 重置查询
*/
function handleResetQuery() {
// 重置查询
function handleResetClick() {
queryFormRef.value.resetFields();
queryParams.pageNum = 1;
handleQuery();
}
/** 行复选框选中 */
// 行选择
function handleSelectionChange(selection: any) {
ids.value = selection.map((item: any) => item.id);
}
/**
* 打开字典类型表单弹窗
*
* @param dicTypeId 字典类型ID
*/
function openDialog(dicTypeId?: number) {
// 新增字典
function handleAddClick() {
dialog.visible = true;
if (dicTypeId) {
dialog.title = "修改字典类型";
DictAPI.getDictTypeForm(dicTypeId).then((data) => {
Object.assign(formData, data);
});
} else {
dialog.title = "新增字典类型";
}
dialog.title = "新增字典";
}
/** 字典类型表单提交 */
function handleSubmit() {
/**
* 编辑字典
*
* @param id 字典ID
*/
function handleEditClick(id: number, name: string) {
dialog.visible = true;
dialog.title = "【" + name + "】字典修改";
DictAPI.getFormData(id).then((data) => {
Object.assign(formData, data);
});
}
// 提交字典表单
function handleSubmitClick() {
dataFormRef.value.validate((isValid: boolean) => {
console.log("isValid", isValid);
if (isValid) {
loading.value = false;
const dictTypeId = formData.id;
if (dictTypeId) {
DictAPI.updateDictType(dictTypeId, formData)
loading.value = true;
const id = formData.id;
if (id) {
DictAPI.update(id, formData)
.then(() => {
ElMessage.success("修改成功");
closeDialog();
handleCloseDialog();
handleQuery();
})
.finally(() => (loading.value = false));
} else {
DictAPI.addDictType(formData)
DictAPI.add(formData)
.then(() => {
ElMessage.success("新增成功");
closeDialog();
handleCloseDialog();
handleQuery();
})
.finally(() => (loading.value = false));
@@ -258,63 +319,62 @@ function handleSubmit() {
});
}
/** 关闭字典类型弹窗 */
function closeDialog() {
/** 关闭字典弹窗 */
function handleCloseDialog() {
dialog.visible = false;
resetForm();
}
/** 重置字典类型表单 */
function resetForm() {
dataFormRef.value.resetFields();
dataFormRef.value.clearValidate();
formData.id = undefined;
formData.status = 1;
}
/** 删除字典类型 */
function handleDelete(dictTypeId?: number) {
const dictTypeIds = [dictTypeId || ids.value].join(",");
if (!dictTypeIds) {
/**
* 删除字典
*
* @param id 字典ID
*/
function handleDelete(id?: number) {
const attrGroupIds = [id || ids.value].join(",");
if (!attrGroupIds) {
ElMessage.warning("请勾选删除项");
return;
}
ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
DictAPI.deleteDictTypes(dictTypeIds).then(() => {
ElMessage.success("删除成功");
handleResetQuery();
});
});
}).then(
() => {
DictAPI.deleteByIds(attrGroupIds).then(() => {
ElMessage.success("删除成功");
handleResetClick();
});
},
() => {
ElMessage.info("已取消删除");
}
);
}
const dictDataDialog = reactive({
title: "",
visible: false,
});
const selectedDictType = reactive({ typeCode: "", typeName: "" }); // 当前选中的字典类型
/** 打开字典数据弹窗 */
function openDictDialog(row: DictTypePageVO) {
dictDataDialog.visible = true;
dictDataDialog.title = "【" + row.name + "】字典数据";
selectedDictType.typeCode = row.code;
selectedDictType.typeName = row.name;
// 新增字典
function handleAddAttrClick() {
formData.dictItems = formData.dictItems ?? [];
formData.dictItems.push({ sort: 1, status: 1 });
}
/** 关闭字典数据弹窗 */
function closeDictDialog() {
dictDataDialog.visible = false;
// 删除字典
function handleDeleteAttrClick(index: number) {
if (formData.dictItems && formData.dictItems.length > 0) {
formData.dictItems.splice(index, 1);
}
}
onMounted(() => {
handleQuery();
});
</script>
<style scoped lang="scss">
.el-form--inline .el-form-item {
margin-top: 18px;
}
</style>

View File

@@ -27,7 +27,7 @@
<el-button
v-hasPerm="['sys:menu:add']"
type="success"
@click="handleDialogOpen(0)"
@click="handleOpenDialog(0)"
>
<template #icon><i-ep-plus /></template>
新增</el-button
@@ -84,11 +84,18 @@
</template>
</el-table-column>
<el-table-column
label="路由名称"
align="left"
width="150"
prop="routeName"
/>
<el-table-column
label="路由路径"
align="left"
width="150"
prop="path"
prop="routePath"
/>
<el-table-column
@@ -122,7 +129,7 @@
type="primary"
link
size="small"
@click.stop="handleDialogOpen(scope.row.id)"
@click.stop="handleOpenDialog(scope.row.id)"
>
<i-ep-plus />新增
</el-button>
@@ -132,7 +139,7 @@
type="primary"
link
size="small"
@click.stop="handleDialogOpen(undefined, scope.row.id)"
@click.stop="handleOpenDialog(undefined, scope.row.id)"
>
<i-ep-edit />编辑
</el-button>
@@ -150,19 +157,17 @@
</el-table>
</el-card>
<el-dialog
<el-drawer
v-model="dialog.visible"
:title="dialog.title"
destroy-on-close
append-to-body
width="1000px"
@close="closeDialog"
@close="handleCloseDialog"
size="50%"
>
<el-form
ref="menuFormRef"
:model="formData"
:rules="rules"
label-width="160px"
label-width="100px"
>
<el-form-item label="父级菜单" prop="parentId">
<el-tree-select
@@ -196,7 +201,29 @@
label="外链地址"
prop="path"
>
<el-input v-model="formData.path" placeholder="请输入外链完整路径" />
<el-input
v-model="formData.routePath"
placeholder="请输入外链完整路径"
/>
</el-form-item>
<el-form-item
v-if="formData.type == MenuTypeEnum.MENU"
prop="routeName"
>
<template #label>
<div>
路由名称
<el-tooltip placement="bottom" effect="light">
<template #content>
如果需要开启缓存,需保证页面 defineOptions 中的 name
与此处一致,建议使用驼峰。
</template>
<i-ep-QuestionFilled class="inline-block" />
</el-tooltip>
</div>
</template>
<el-input v-model="formData.routeName" placeholder="User" />
</el-form-item>
<el-form-item
@@ -204,8 +231,7 @@
formData.type == MenuTypeEnum.CATALOG ||
formData.type == MenuTypeEnum.MENU
"
label=""
prop="path"
prop="routePath"
>
<template #label>
<div>
@@ -222,10 +248,10 @@
</template>
<el-input
v-if="formData.type == MenuTypeEnum.CATALOG"
v-model="formData.path"
v-model="formData.routePath"
placeholder="system"
/>
<el-input v-else v-model="formData.path" placeholder="user" />
<el-input v-else v-model="formData.routePath" placeholder="user" />
</el-form-item>
<el-form-item
@@ -405,10 +431,10 @@
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="closeDialog">取 消</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-dialog>
</el-drawer>
</div>
</template>
@@ -418,8 +444,7 @@ defineOptions({
inheritAttrs: false,
});
import MenuAPI from "@/api/menu";
import { MenuQuery, MenuForm, MenuVO } from "@/api/menu/model";
import MenuAPI, { MenuQuery, MenuForm, MenuVO } from "@/api/menu";
import { MenuTypeEnum } from "@/enums/MenuTypeEnum";
const queryFormRef = ref(ElForm);
@@ -458,7 +483,8 @@ const rules = reactive({
parentId: [{ required: true, message: "请选择顶级菜单", trigger: "blur" }],
name: [{ required: true, message: "请输入菜单名称", trigger: "blur" }],
type: [{ required: true, message: "请选择菜单类型", trigger: "blur" }],
path: [{ required: true, message: "请输入路由路径", trigger: "blur" }],
routeName: [{ required: true, message: "请输入路由名称", trigger: "blur" }],
routePath: [{ required: true, message: "请输入路由路径", trigger: "blur" }],
component: [{ required: true, message: "请输入组件路径", trigger: "blur" }],
visible: [{ required: true, message: "请输入路由路径", trigger: "blur" }],
});
@@ -496,7 +522,7 @@ function handleRowClick(row: MenuVO) {
* @param parentId 父菜单ID
* @param menuId 菜单ID
*/
function handleDialogOpen(parentId?: number, menuId?: number) {
function handleOpenDialog(parentId?: number, menuId?: number) {
MenuAPI.getOptions()
.then((data) => {
menuOptions.value = [{ value: 0, label: "顶级菜单", children: data }];
@@ -541,13 +567,13 @@ function submitForm() {
if (menuId) {
MenuAPI.update(menuId, formData.value).then(() => {
ElMessage.success("修改成功");
closeDialog();
handleCloseDialog();
handleQuery();
});
} else {
MenuAPI.add(formData.value).then(() => {
ElMessage.success("新增成功");
closeDialog();
handleCloseDialog();
handleQuery();
});
}
@@ -566,18 +592,26 @@ function handleDelete(menuId: number) {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
MenuAPI.deleteById(menuId).then(() => {
ElMessage.success("删除成功");
handleQuery();
});
})
.catch(() => ElMessage.info("已取消删除"));
}).then(
() => {
loading.value = true;
MenuAPI.deleteById(menuId)
.then(() => {
ElMessage.success("删除成功");
handleQuery();
})
.finally(() => {
loading.value = false;
});
},
() => {
ElMessage.info("已取消删除");
}
);
}
// 关闭弹窗
function closeDialog() {
function handleCloseDialog() {
dialog.visible = false;
menuFormRef.value.resetFields();
menuFormRef.value.clearValidate();

View File

@@ -22,7 +22,7 @@
<el-card shadow="never" class="table-container">
<template #header>
<el-button type="success" @click="openDialog()"
<el-button type="success" @click="handleOpenDialog()"
><i-ep-plus />新增</el-button
>
<el-button
@@ -60,7 +60,7 @@
type="primary"
size="small"
link
@click="openMenuDialog(scope.row)"
@click="handleOpenAssignPermDialog(scope.row)"
>
<i-ep-position />分配权限
</el-button>
@@ -68,12 +68,12 @@
type="primary"
size="small"
link
@click="openDialog(scope.row.id)"
@click="handleOpenDialog(scope.row.id)"
>
<i-ep-edit />编辑
</el-button>
<el-button
type="primary"
type="danger"
size="small"
link
@click="handleDelete(scope.row.id)"
@@ -98,7 +98,7 @@
v-model="dialog.visible"
:title="dialog.title"
width="500px"
@close="closeDialog"
@close="handleCloseDialog"
>
<el-form
ref="roleFormRef"
@@ -143,74 +143,111 @@
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="closeDialog">取 消</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 分配菜单弹窗 -->
<el-dialog
v-model="menuDialogVisible"
<!-- 分配权限弹窗 -->
<el-drawer
v-model="assignPermDialogVisible"
:title="'【' + checkedRole.name + '】权限分配'"
width="800px"
size="500"
>
<el-scrollbar v-loading="loading" max-height="600px">
<el-tree
ref="menuRef"
node-key="value"
show-checkbox
:data="menuList"
:default-expand-all="true"
<div class="flex-x-between">
<el-input
v-model="permKeywords"
clearable
class="w-[200px]"
placeholder="菜单权限名称"
>
<template #default="{ data }">
{{ data.label }}
<template #prefix>
<i-ep-search />
</template>
</el-tree>
</el-scrollbar>
</el-input>
<div class="flex-center">
<el-button type="primary" size="small" plain @click="togglePermTree"
><i-ep-switch />{{ isExpanded ? "收缩" : "展开" }}</el-button
>
<el-checkbox
v-model="parentChildLinked"
@change="handleparentChildLinkedChange"
class="ml-5"
>父子联动
</el-checkbox>
<el-tooltip placement="bottom">
<template #content>
如果只需勾选菜单权限,不需要勾选子菜单或者按钮权限,请关闭父子联动
</template>
<i-ep-QuestionFilled
class="ml-1 color-[--el-color-primary] inline-block cursor-pointer"
/>
</el-tooltip>
</div>
</div>
<el-tree
ref="permTreeRef"
node-key="value"
show-checkbox
:data="menuPermOptions"
:filter-node-method="handlePermFilter"
:default-expand-all="true"
:check-strictly="!parentChildLinked"
class="mt-5"
>
<template #default="{ data }">
{{ data.label }}
</template>
</el-tree>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleRoleMenuSubmit"
<el-button type="primary" @click="handleAssignPermSubmit"
>确 定</el-button
>
<el-button @click="menuDialogVisible = false">取 消</el-button>
<el-button @click="assignPermDialogVisible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import RoleAPI from "@/api/role";
import MenuAPI from "@/api/menu";
import { RolePageVO, RoleForm, RoleQuery } from "@/api/role/model";
defineOptions({
name: "Role",
inheritAttrs: false,
});
import RoleAPI, { RolePageVO, RoleForm, RolePageQuery } from "@/api/role";
import MenuAPI from "@/api/menu";
const queryFormRef = ref(ElForm);
const roleFormRef = ref(ElForm);
const menuRef = ref(ElTree);
const permTreeRef = ref<InstanceType<typeof ElTree>>();
const loading = ref(false);
const ids = ref<number[]>([]);
const total = ref(0);
const queryParams = reactive<RoleQuery>({
const queryParams = reactive<RolePageQuery>({
pageNum: 1,
pageSize: 10,
});
// 角色表格数据
const roleList = ref<RolePageVO[]>();
// 菜单权限下拉
const menuPermOptions = ref<OptionType[]>([]);
// 弹窗
const dialog = reactive({
title: "",
visible: false,
});
// 角色表单
const formData = reactive<RoleForm>({
sort: 1,
status: 1,
@@ -225,15 +262,18 @@ const rules = reactive({
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
});
const menuDialogVisible = ref(false);
const menuList = ref<OptionType[]>([]);
// 选中的角色
interface CheckedRole {
id?: number;
name?: string;
}
let checkedRole: CheckedRole = reactive({});
const checkedRole = ref<CheckedRole>({});
const assignPermDialogVisible = ref(false);
const permKeywords = ref("");
const isExpanded = ref(true);
const parentChildLinked = ref(true);
/** 查询 */
function handleQuery() {
@@ -254,13 +294,13 @@ function handleResetQuery() {
handleQuery();
}
/** 行checkbox 选中事件 */
/** 行复选框选中记录选中ID集合 */
function handleSelectionChange(selection: any) {
ids.value = selection.map((item: any) => item.id);
}
/** 打开角色表单弹窗 */
function openDialog(roleId?: number) {
/** 打开角色弹窗 */
function handleOpenDialog(roleId?: number) {
dialog.visible = true;
if (roleId) {
dialog.title = "修改角色";
@@ -272,7 +312,7 @@ function openDialog(roleId?: number) {
}
}
/** 角色保存提交 */
/** 提交角色表单 */
function handleSubmit() {
roleFormRef.value.validate((valid: any) => {
if (valid) {
@@ -282,7 +322,7 @@ function handleSubmit() {
RoleAPI.update(roleId, formData)
.then(() => {
ElMessage.success("修改成功");
closeDialog();
handleCloseDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
@@ -290,7 +330,7 @@ function handleSubmit() {
RoleAPI.add(formData)
.then(() => {
ElMessage.success("新增成功");
closeDialog();
handleCloseDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
@@ -299,14 +339,10 @@ function handleSubmit() {
});
}
/** 关闭表单弹窗 */
function closeDialog() {
/** 关闭角色弹窗 */
function handleCloseDialog() {
dialog.visible = false;
resetForm();
}
/** 重置表单 */
function resetForm() {
roleFormRef.value.resetFields();
roleFormRef.value.clearValidate();
@@ -327,37 +363,41 @@ function handleDelete(roleId?: number) {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
loading.value = true;
RoleAPI.deleteByIds(roleIds)
.then(() => {
ElMessage.success("删除成功");
handleResetQuery();
})
.finally(() => (loading.value = false));
});
}).then(
() => {
loading.value = true;
RoleAPI.deleteByIds(roleIds)
.then(() => {
ElMessage.success("删除成功");
handleResetQuery();
})
.finally(() => (loading.value = false));
},
() => {
ElMessage.info("已取消删除");
}
);
}
/** 打开分配菜单弹窗 */
async function openMenuDialog(row: RolePageVO) {
/** 打开分配菜单权限弹窗 */
async function handleOpenAssignPermDialog(row: RolePageVO) {
const roleId = row.id;
if (roleId) {
checkedRole = {
id: roleId,
name: row.name,
};
menuDialogVisible.value = true;
assignPermDialogVisible.value = true;
loading.value = true;
checkedRole.value.id = roleId;
checkedRole.value.name = row.name;
// 获取所有的菜单
menuList.value = await MenuAPI.getOptions();
menuPermOptions.value = await MenuAPI.getOptions();
// 回显角色已拥有的菜单
RoleAPI.getRoleMenuIds(roleId)
.then((data) => {
const checkedMenuIds = data;
checkedMenuIds.forEach((menuId) =>
menuRef.value.setChecked(menuId, true, false)
permTreeRef.value!.setChecked(menuId, true, false)
);
})
.finally(() => {
@@ -366,19 +406,19 @@ async function openMenuDialog(row: RolePageVO) {
}
}
/** 角色分配菜单保存提交 */
function handleRoleMenuSubmit() {
const roleId = checkedRole.id;
/** 分配菜单权限提交 */
function handleAssignPermSubmit() {
const roleId = checkedRole.value.id;
if (roleId) {
const checkedMenuIds: number[] = menuRef.value
.getCheckedNodes(false, true)
const checkedMenuIds: number[] = permTreeRef
.value!.getCheckedNodes(false, true)
.map((node: any) => node.value);
loading.value = true;
RoleAPI.updateRoleMenus(roleId, checkedMenuIds)
.then(() => {
ElMessage.success("分配权限成功");
menuDialogVisible.value = false;
assignPermDialogVisible.value = false;
handleResetQuery();
})
.finally(() => {
@@ -387,6 +427,40 @@ function handleRoleMenuSubmit() {
}
}
/** 展开/收缩 菜单权限树 */
function togglePermTree() {
isExpanded.value = !isExpanded.value;
if (permTreeRef.value) {
Object.values(permTreeRef.value.store.nodesMap).forEach((node: any) => {
if (isExpanded.value) {
node.expand();
} else {
node.collapse();
}
});
}
}
/** 权限筛选 */
watch(permKeywords, (val) => {
permTreeRef.value!.filter(val);
});
function handlePermFilter(
value: string,
data: {
[key: string]: any;
}
) {
if (!value) return true;
return data.label.includes(value);
}
/** 父子菜单节点是否联动 change*/
function handleparentChildLinkedChange(val: any) {
parentChildLinked.value = val;
}
onMounted(() => {
handleQuery();
});

View File

@@ -0,0 +1,145 @@
<template>
<el-dialog
v-model="dialogVisible"
:align-center="true"
title="导入数据"
width="600px"
@close="handleClose"
>
<el-scrollbar max-height="60vh">
<el-form
ref="importFormRef"
label-width="auto"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
class="w-full"
ref="uploadRef"
v-model:file-list="importFormData.files"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
*.xlsx / *.xls
<el-link
type="primary"
icon="download"
:underline="false"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleSubmit"
>
确 定
</el-button>
<el-button @click="handleClose">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { type UploadUserFile } from "element-plus";
import UserAPI from "@/api/user";
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:visible", "import-success"]);
const dialogVisible = computed({
get: () => props.visible,
set: (val) => {
emit("update:visible", val);
},
});
const importFormRef = ref(null);
const uploadRef = ref(null);
const importFormData = reactive<{
files: UploadUserFile[];
}>({
files: [],
});
const importFormRules = {
files: [{ required: true, message: "文件不能为空", trigger: "blur" }],
};
const handleClose = () => {
dialogVisible.value = false;
};
const handleSubmit = async () => {
if (!importFormData.files.length) {
ElMessage.warning("请选择文件");
return;
}
try {
await UserAPI.import(1, importFormData.files[0].raw as File);
ElMessage.success("上传成功");
emit("import-success");
handleClose();
} catch (error) {
ElMessage.error("上传失败");
}
};
const handleFileExceed = () => {
ElMessage.warning("只能上传一个文件");
};
const handleDownloadTemplate = () => {
UserAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(
response.headers["content-disposition"].split(";")[1].split("=")[1]
);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
};
</script>

View File

@@ -59,12 +59,12 @@
<el-card shadow="never" class="table-container">
<template #header>
<div class="flex justify-between">
<div class="flex-x-between">
<div>
<el-button
v-hasPerm="['sys:user:add']"
type="success"
@click="openDialog('user-form')"
@click="handleOpenDialog()"
><i-ep-plus />新增</el-button
>
<el-button
@@ -76,19 +76,10 @@
>
</div>
<div>
<el-dropdown split-button>
导入
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="downloadTemplate">
<i-ep-download />下载模板</el-dropdown-item
>
<el-dropdown-item @click="openDialog('user-import')">
<i-ep-top />导入数据</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button class="ml-3" @click="handleOpenImportDialog"
><template #icon><i-ep-upload /></template>导入</el-button
>
<el-button class="ml-3" @click="handleExport"
><template #icon><i-ep-download /></template>导出</el-button
>
@@ -162,7 +153,7 @@
type="primary"
size="small"
link
@click="resetPassword(scope.row)"
@click="hancleResetPassword(scope.row)"
><i-ep-refresh-left />重置密码</el-button
>
<el-button
@@ -170,12 +161,12 @@
type="primary"
link
size="small"
@click="openDialog('user-form', scope.row.id)"
@click="handleOpenDialog(scope.row.id)"
><i-ep-edit />编辑</el-button
>
<el-button
v-hasPerm="['sys:user:delete']"
type="primary"
type="danger"
link
size="small"
@click="handleDelete(scope.row.id)"
@@ -196,17 +187,15 @@
</el-col>
</el-row>
<!-- 弹窗 -->
<el-dialog
<!-- 用户表单弹窗 -->
<el-drawer
v-model="dialog.visible"
:title="dialog.title"
:width="dialog.width"
append-to-body
@close="closeDialog"
@close="handleCloseDialog"
>
<!-- 用户新增/编辑表单 -->
<el-form
v-if="dialog.type === 'user-form'"
ref="userFormRef"
:model="formData"
:rules="rules"
@@ -228,7 +217,7 @@
<el-tree-select
v-model="formData.deptId"
placeholder="请选择所属部门"
:data="deptList"
:data="deptOptions"
filterable
check-strictly
:render-after-expand="false"
@@ -236,13 +225,13 @@
</el-form-item>
<el-form-item label="性别" prop="gender">
<dictionary v-model="formData.gender" type-code="gender" />
<dictionary v-model="formData.gender" code="gender" />
</el-form-item>
<el-form-item label="角色" prop="roleIds">
<el-select v-model="formData.roleIds" multiple placeholder="请选择">
<el-option
v-for="item in roleList"
v-for="item in roleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
@@ -274,55 +263,19 @@
</el-form-item>
</el-form>
<!-- 用户导入表单 -->
<el-form
v-else-if="dialog.type === 'user-import'"
:model="importData"
label-width="100px"
>
<el-form-item label="部门">
<el-tree-select
v-model="importData.deptId"
placeholder="请选择部门"
:data="deptList"
filterable
check-strictly
/>
</el-form-item>
<el-form-item label="Excel文件">
<el-upload
ref="uploadRef"
action=""
drag
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:limit="1"
:auto-upload="false"
:file-list="importData.fileList"
:on-change="handleFileChange"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload">
<i-ep-upload-filled />
</el-icon>
<div class="el-upload__text">
将文件拖到此处,或
<em>点击上传</em>
</div>
<template #tip>
<div>xls/xlsx files</div>
</template>
</el-upload>
</el-form-item>
</el-form>
<!-- 弹窗底部操作按钮 -->
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="closeDialog">取 消</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-dialog>
</el-drawer>
<!-- 用户导入弹窗 -->
<user-import
v-model:visible="importDialogVisible"
@import-success="handleOpenImportDialogSuccess"
/>
</div>
</template>
@@ -332,30 +285,28 @@ defineOptions({
inheritAttrs: false,
});
import UserAPI from "@/api/user";
import UserAPI, { UserForm, UserPageQuery, UserPageVO } from "@/api/user";
import DeptAPI from "@/api/dept";
import RoleAPI from "@/api/role";
import { UserForm, UserQuery, UserPageVO } from "@/api/user/model";
import type { UploadInstance } from "element-plus";
import { genFileId } from "element-plus";
const queryFormRef = ref(ElForm);
const userFormRef = ref(ElForm);
const queryFormRef = ref(ElForm); // 查询表单
const userFormRef = ref(ElForm); // 用户表单
const uploadRef = ref<UploadInstance>(); // 上传组件
const loading = ref(false); // 加载状态
const removeIds = ref([]); // 删除用户ID集合 用于批量删除
const queryParams = reactive<UserQuery>({
const loading = ref(false);
const removeIds = ref([]);
const total = ref(0);
const pageData = ref<UserPageVO[]>();
/** 部门下拉选项 */
const deptOptions = ref<OptionType[]>();
/** 角色下拉选项 */
const roleOptions = ref<OptionType[]>();
/** 用户查询参数 */
const queryParams = reactive<UserPageQuery>({
pageNum: 1,
pageSize: 10,
});
const dateTimeRange = ref("");
const total = ref(0); // 数据总数
const pageData = ref<UserPageVO[]>(); // 用户分页数据
const deptList = ref<OptionType[]>(); // 部门下拉数据源
const roleList = ref<OptionType[]>(); // 角色下拉数据源
const dateTimeRange = ref("");
watch(dateTimeRange, (newVal) => {
if (newVal) {
queryParams.startTime = newVal[0];
@@ -363,27 +314,21 @@ watch(dateTimeRange, (newVal) => {
}
});
// 弹窗对象
/** 用户弹窗对象 */
const dialog = reactive({
visible: false,
type: "user-form",
width: 800,
title: "",
});
/** 导入弹窗显示状态 */
const importDialogVisible = ref(false);
// 用户表单数据
const formData = reactive<UserForm>({
status: 1,
});
// 用户导入数据
const importData = reactive({
deptId: undefined,
file: undefined,
fileList: [],
});
// 校验规则
/** 用户表单校验规则 */
const rules = reactive({
username: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
nickname: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
@@ -430,13 +375,13 @@ function handleResetQuery() {
handleQuery();
}
/** 行选中 */
/** 行复选框选中记录选中ID集合 */
function handleSelectionChange(selection: any) {
removeIds.value = selection.map((item: any) => item.id);
}
/** 重置密码 */
function resetPassword(row: { [key: string]: any }) {
function hancleResetPassword(row: { [key: string]: any }) {
ElMessageBox.prompt(
"请输入用户「" + row.username + "」的新密码",
"重置密码",
@@ -456,110 +401,63 @@ function resetPassword(row: { [key: string]: any }) {
});
}
/** 加载角色下拉数据源 */
async function loadRoleOptions() {
RoleAPI.getOptions().then((data) => {
roleList.value = data;
});
}
/** 加载部门下拉数据源 */
async function loadDeptOptions() {
DeptAPI.getOptions().then((data) => {
deptList.value = data;
});
}
/**
* 打开弹窗
*
* @param type 弹窗类型 用户表单:user-form | 用户导入:user-import
* @param id 用户ID
*/
async function openDialog(type: string, id?: number) {
async function handleOpenDialog(id?: number) {
dialog.visible = true;
dialog.type = type;
// 加载角色下拉数据源
roleOptions.value = await RoleAPI.getOptions();
// 加载部门下拉数据源
deptOptions.value = await DeptAPI.getOptions();
if (dialog.type === "user-form") {
// 用户表单弹窗
await loadDeptOptions();
await loadRoleOptions();
if (id) {
dialog.title = "修改用户";
UserAPI.getFormData(id).then((data) => {
Object.assign(formData, { ...data });
});
} else {
dialog.title = "新增用户";
}
} else if (dialog.type === "user-import") {
// 用户导入弹窗
dialog.title = "导入用户";
dialog.width = 600;
loadDeptOptions();
if (id) {
dialog.title = "修改用户";
UserAPI.getFormData(id).then((data) => {
Object.assign(formData, { ...data });
});
} else {
dialog.title = "新增用户";
}
}
/**
* 关闭弹窗
*
* @param type 弹窗类型 用户表单:user-form | 用户导入:user-import
*/
function closeDialog() {
/** 关闭弹窗 */
function handleCloseDialog() {
dialog.visible = false;
if (dialog.type === "user-form") {
userFormRef.value.resetFields();
userFormRef.value.clearValidate();
userFormRef.value.resetFields();
userFormRef.value.clearValidate();
formData.id = undefined;
formData.status = 1;
} else if (dialog.type === "user-import") {
importData.file = undefined;
importData.fileList = [];
}
formData.id = undefined;
formData.status = 1;
}
/** 表单提交 */
const handleSubmit = useThrottleFn(() => {
if (dialog.type === "user-form") {
userFormRef.value.validate((valid: any) => {
if (valid) {
const userId = formData.id;
loading.value = true;
if (userId) {
UserAPI.update(userId, formData)
.then(() => {
ElMessage.success("修改用户成功");
closeDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
} else {
UserAPI.add(formData)
.then(() => {
ElMessage.success("新增用户成功");
closeDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
}
userFormRef.value.validate((valid: any) => {
if (valid) {
const userId = formData.id;
loading.value = true;
if (userId) {
UserAPI.update(userId, formData)
.then(() => {
ElMessage.success("修改用户成功");
handleCloseDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
} else {
UserAPI.add(formData)
.then(() => {
ElMessage.success("新增用户成功");
handleCloseDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
}
});
} else if (dialog.type === "user-import") {
if (!importData?.deptId) {
ElMessage.warning("请选择部门");
return false;
}
if (!importData?.file) {
ElMessage.warning("上传Excel文件不能为空");
return false;
}
UserAPI.import(importData?.deptId, importData?.file).then((data) => {
ElMessage.success("导入用户成功");
closeDialog();
handleResetQuery();
});
}
});
}, 3000);
/** 删除用户 */
@@ -574,51 +472,29 @@ function handleDelete(id?: number) {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(function () {
UserAPI.deleteByIds(userIds).then(() => {
ElMessage.success("删除成功");
handleResetQuery();
});
});
}).then(
function () {
loading.value = true;
UserAPI.deleteByIds(userIds)
.then(() => {
ElMessage.success("删除成功");
handleResetQuery();
})
.finally(() => (loading.value = false));
},
function () {
ElMessage.info("已取消删除");
}
);
}
/** 打开导入弹窗 */
function handleOpenImportDialog() {
importDialogVisible.value = true;
}
/** 下载导入模板 */
function downloadTemplate() {
UserAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(
response.headers["content-disposition"].split(";")[1].split("=")[1]
);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
}
/** Excel文件 Change */
function handleFileChange(file: any) {
importData.file = file.raw;
}
/** Excel文件 Exceed */
function handleFileExceed(files: any) {
uploadRef.value!.clearFiles();
const file = files[0];
file.uid = genFileId();
uploadRef.value!.handleStart(file);
importData.file = file;
/** 导入用户成功 */
function handleOpenImportDialogSuccess() {
handleQuery();
}
/** 导出用户 */

View File

@@ -12,24 +12,17 @@
"lib": ["esnext", "dom"],
"baseUrl": ".",
"allowJs": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client", "unplugin-icons/types/vue", "element-plus/global"],
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"allowSyntheticDefaultImports": true /* 允许默认导入 */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
"types": ["vite/client", "unplugin-icons/types/vue", "element-plus/global"]
},
"include": [
"src/**/*.ts",
"src/**/*.vue",
"src/typings/**/*.d.ts",
"mock/**/*.ts",
"vite.config.ts"
],
"include": ["mock/**/*.ts", "src/**/*.ts", "src/**/*.vue", "vite.config.ts"],
"exclude": ["node_modules", "dist", "**/*.js"]
}

View File

@@ -100,7 +100,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
vueTemplate: true,
// 指定自动导入函数TS类型声明文件路径 (false:关闭自动生成)
dts: false,
// dts: "src/typings/auto-imports.d.ts",
// dts: "src/types/auto-imports.d.ts",
}),
Components({
resolvers: [
@@ -116,7 +116,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
dirs: ["src/components", "src/**/components"],
// 指定自动导入组件TS类型声明文件路径 (false:关闭自动生成)
dts: false,
// dts: "src/typings/components.d.ts",
// dts: "src/types/components.d.ts",
}),
Icons({
// 自动安装图标库
@@ -128,9 +128,9 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
// 指定symbolId格式
symbolId: "icon-[dir]-[name]",
}),
VueDevTools({
/* VueDevTools({
openInEditorHost: `http://localhost:${env.VITE_APP_PORT}`,
}),
}), */
],
// 预加载项目必需的组件
optimizeDeps: {