feat: 添加系统管理-菜单管理 (#929)

* feat: 添加系统管理-菜单管理

* chore: update

* chore: add Copyright in login page

* chore: 将页脚放在一屏可视区

* chore: 依赖更新

* chore: update

* chore: update

* chore: 更新依赖

* chore: update `husky v9`

* style: 适配`el-dialog`样式的更新

* style: update `src/layout/components/search/components/SearchResult.vue`

* chore: update

* style: update

* fix: 修复`ReDialog`中点击取消和确定按钮会触发两次关闭回调

* chore: update

* chore: update src/views/system/menu/README.md

* chore: update

* chore: update

* chore: done

* chore: update
This commit is contained in:
xiaoming 2024-02-26 22:17:40 +08:00 committed by GitHub
parent f762587fa7
commit c314b0cd1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 2743 additions and 1118 deletions

2
.nvmrc
View File

@ -1 +1 @@
v20.11.0
v20.11.1

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
src/views/system/menu/README.md

View File

@ -29,6 +29,7 @@ menus:
hssysManagement: System Manage
hsUser: User Manage
hsRole: Role Manage
hsSystemMenu: Menu Manage
hsDept: Dept Manage
hseditor: Editor
hsabnormal: Abnormal Page

View File

@ -29,6 +29,7 @@ menus:
hssysManagement: 系统管理
hsUser: 用户管理
hsRole: 角色管理
hsSystemMenu: 菜单管理
hsDept: 部门管理
hseditor: 编辑器
hsabnormal: 异常页面

View File

@ -11,7 +11,7 @@ import { system, permission, frame, tabs } from "@/router/enums";
const systemRouter = {
path: "/system",
meta: {
icon: "setting",
icon: "ri:settings-3-line",
title: "menus.hssysManagement",
rank: system
},
@ -20,7 +20,7 @@ const systemRouter = {
path: "/system/user/index",
name: "SystemUser",
meta: {
icon: "flUser",
icon: "ri:admin-line",
title: "menus.hsUser",
roles: ["admin"]
}
@ -29,16 +29,25 @@ const systemRouter = {
path: "/system/role/index",
name: "SystemRole",
meta: {
icon: "role",
icon: "ri:admin-fill",
title: "menus.hsRole",
roles: ["admin"]
}
},
{
path: "/system/menu/index",
name: "SystemMenu",
meta: {
icon: "ep:menu",
title: "menus.hsSystemMenu",
roles: ["admin"]
}
},
{
path: "/system/dept/index",
name: "SystemDept",
meta: {
icon: "dept",
icon: "ri:git-branch-line",
title: "menus.hsDept",
roles: ["admin"]
}
@ -50,7 +59,7 @@ const permissionRouter = {
path: "/permission",
meta: {
title: "menus.permission",
icon: "lollipop",
icon: "ep:lollipop",
rank: permission
},
children: [
@ -68,7 +77,11 @@ const permissionRouter = {
meta: {
title: "menus.permissionButton",
roles: ["admin", "common"],
auths: ["btn_add", "btn_edit", "btn_delete"]
auths: [
"permission:btn:add",
"permission:btn:edit",
"permission:btn:delete"
]
}
}
]
@ -77,7 +90,7 @@ const permissionRouter = {
const frameRouter = {
path: "/iframe",
meta: {
icon: "monitor",
icon: "ep:monitor",
title: "menus.hsExternalPage",
rank: frame
},
@ -180,7 +193,7 @@ const frameRouter = {
const tabsRouter = {
path: "/tabs",
meta: {
icon: "tag",
icon: "ri:bookmark-2-line",
title: "menus.hstabs",
rank: tabs
},

View File

@ -140,6 +140,620 @@ export default defineFakeRoute([
};
}
},
// 菜单管理
{
url: "/menu",
method: "post",
response: () => {
return {
success: true,
data: [
// 外部页面
{
parentId: 0,
id: 100,
menuType: 0, // 菜单类型0代表菜单、1代表iframe、2代表外链、3代表按钮
title: "menus.hsExternalPage",
name: "PureIframe",
path: "/iframe",
component: "",
rank: 7,
redirect: "",
icon: "ep:monitor",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 100,
id: 101,
menuType: 0,
title: "menus.hsExternalDoc",
name: "PureIframeExternal",
path: "/iframe/external",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 101,
id: 102,
menuType: 2,
title: "menus.externalLink",
name: "https://yiming_chang.gitee.io/pure-admin-doc",
path: "/external",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 101,
id: 103,
menuType: 2,
title: "menus.pureutilsLink",
name: "https://pure-admin-utils.netlify.app/",
path: "/pureutilsLink",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 100,
id: 104,
menuType: 1,
title: "menus.hsEmbeddedDoc",
name: "PureIframeEmbedded",
path: "/iframe/embedded",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 104,
id: 105,
menuType: 1,
title: "menus.hsEpDocument",
name: "FrameEp",
path: "/iframe/ep",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "https://element-plus.org/zh-CN/",
frameLoading: true,
keepAlive: true,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 104,
id: 106,
menuType: 1,
title: "menus.hsTailwindcssDocument",
name: "FrameTailwindcss",
path: "/iframe/tailwindcss",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "https://tailwindcss.com/docs/installation",
frameLoading: true,
keepAlive: true,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 104,
id: 107,
menuType: 1,
title: "menus.hsVueDocument",
name: "FrameVue",
path: "/iframe/vue3",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "https://cn.vuejs.org/",
frameLoading: true,
keepAlive: true,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 104,
id: 108,
menuType: 1,
title: "menus.hsViteDocument",
name: "FrameVite",
path: "/iframe/vite",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "https://cn.vitejs.dev/",
frameLoading: true,
keepAlive: true,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 104,
id: 109,
menuType: 1,
title: "menus.hsPiniaDocument",
name: "FramePinia",
path: "/iframe/pinia",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "https://pinia.vuejs.org/zh/index.html",
frameLoading: true,
keepAlive: true,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 104,
id: 110,
menuType: 1,
title: "menus.hsRouterDocument",
name: "FrameRouter",
path: "/iframe/vue-router",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "https://router.vuejs.org/zh/",
frameLoading: true,
keepAlive: true,
hiddenTag: false,
showLink: true,
showParent: false
},
// 权限管理
{
parentId: 0,
id: 200,
menuType: 0,
title: "menus.permission",
name: "PurePermission",
path: "/permission",
component: "",
rank: 9,
redirect: "",
icon: "ep:lollipop",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 200,
id: 201,
menuType: 0,
title: "menus.permissionPage",
name: "PermissionPage",
path: "/permission/page/index",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 200,
id: 202,
menuType: 0,
title: "menus.permissionButton",
name: "PermissionButton",
path: "/permission/button/index",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 202,
id: 203,
menuType: 3,
title: "添加",
name: "",
path: "",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "permission:btn:add",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 202,
id: 204,
menuType: 3,
title: "修改",
name: "",
path: "",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "permission:btn:edit",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 202,
id: 205,
menuType: 3,
title: "删除",
name: "",
path: "",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "permission:btn:delete",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
// 系统管理
{
parentId: 0,
id: 300,
menuType: 0,
title: "menus.hssysManagement",
name: "PureSystem",
path: "/system",
component: "",
rank: 10,
redirect: "",
icon: "ri:settings-3-line",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 300,
id: 301,
menuType: 0,
title: "menus.hsUser",
name: "SystemUser",
path: "/system/user/index",
component: "",
rank: null,
redirect: "",
icon: "ri:admin-line",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 300,
id: 302,
menuType: 0,
title: "menus.hsRole",
name: "SystemRole",
path: "/system/role/index",
component: "",
rank: null,
redirect: "",
icon: "ri:admin-fill",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 300,
id: 303,
menuType: 0,
title: "menus.hsSystemMenu",
name: "SystemMenu",
path: "/system/menu/index",
component: "",
rank: null,
redirect: "",
icon: "ep:menu",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 300,
id: 304,
menuType: 0,
title: "menus.hsDept",
name: "SystemDept",
path: "/system/dept/index",
component: "",
rank: null,
redirect: "",
icon: "ri:git-branch-line",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
// 标签页操作
{
parentId: 0,
id: 400,
menuType: 0,
title: "menus.hstabs",
name: "PureTabs",
path: "/tabs",
component: "",
rank: 11,
redirect: "",
icon: "ri:bookmark-2-line",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 400,
id: 401,
menuType: 0,
title: "menus.hstabs",
name: "Tabs",
path: "/tabs/index",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
},
{
parentId: 400,
id: 402,
menuType: 0,
title: "query传参模式",
name: "TabQueryDetail",
path: "/tabs/query-detail",
component: "",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "/tabs/index",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: false,
showParent: false
},
{
parentId: 400,
id: 403,
menuType: 0,
title: "params传参模式",
name: "TabParamsDetail",
path: "/tabs/params-detail/:id",
component: "params-detail",
rank: null,
redirect: "",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "/tabs/index",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: false,
showParent: false
}
]
};
}
},
// 部门管理
{
url: "/dept",

View File

@ -19,7 +19,7 @@
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/",
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
"prepare": "husky install",
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"keywords": [
@ -50,13 +50,13 @@
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@howdyjs/mouse-menu": "2.0.9",
"@logicflow/core": "^1.2.18",
"@logicflow/extension": "^1.2.19",
"@logicflow/core": "^1.2.22",
"@logicflow/extension": "^1.2.22",
"@pureadmin/descriptions": "^1.2.0",
"@pureadmin/table": "^3.0.2",
"@pureadmin/utils": "^2.4.4",
"@vueuse/core": "^10.7.2",
"@vueuse/motion": "^2.0.0",
"@vueuse/core": "^10.8.0",
"@vueuse/motion": "^2.1.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"@zxcvbn-ts/core": "^3.0.4",
@ -65,9 +65,9 @@
"china-area-data": "^5.0.1",
"cropperjs": "^1.6.1",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"echarts": "^5.5.0",
"el-table-infinite-scroll": "^3.0.3",
"element-plus": "^2.5.3",
"element-plus": "^2.5.6",
"intro.js": "^7.2.0",
"js-cookie": "^3.0.5",
"jsbarcode": "^3.11.6",
@ -77,37 +77,37 @@
"nprogress": "^0.2.0",
"path": "^0.12.7",
"pinia": "^2.1.7",
"pinyin-pro": "^3.19.3",
"pinyin-pro": "^3.19.6",
"qrcode": "^1.5.3",
"qs": "^6.11.2",
"responsive-storage": "^2.2.0",
"sortablejs": "^1.15.2",
"swiper": "^11.0.5",
"swiper": "^11.0.6",
"typeit": "8.7.1",
"v-contextmenu": "3.0.0",
"v-contextmenu": "^3.2.0",
"v3-infinite-loading": "^1.3.1",
"version-rocket": "^1.7.1",
"vue": "3.4.14",
"vue-i18n": "^9.9.0",
"vue-i18n": "^9.9.1",
"vue-json-pretty": "^2.3.0",
"vue-pdf-embed": "1.2.1",
"vue-router": "^4.2.5",
"vue-router": "^4.3.0",
"vue-tippy": "^6.4.1",
"vue-types": "^5.1.1",
"vue-virtual-scroller": "2.0.0-beta.8",
"vue-waterfall-plugin-next": "^2.3.1",
"vue3-danmaku": "^1.6.0",
"vuedraggable": "^4.1.0",
"wavesurfer.js": "^7.7.1",
"xgplayer": "^3.0.11",
"wavesurfer.js": "^7.7.3",
"xgplayer": "^3.0.13",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@commitlint/types": "^18.6.0",
"@eslint/js": "^8.56.0",
"@faker-js/faker": "^8.4.0",
"@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.6.2",
"@commitlint/types": "^18.6.1",
"@eslint/js": "^8.57.0",
"@faker-js/faker": "^8.4.1",
"@iconify-icons/ep": "^1.2.12",
"@iconify-icons/ri": "^1.2.10",
"@iconify/vue": "^4.1.1",
@ -116,44 +116,44 @@
"@types/gradient-string": "^1.1.5",
"@types/intro.js": "^5.1.5",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.11.7",
"@types/node": "^20.11.20",
"@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5",
"@types/qs": "^6.9.11",
"@types/sortablejs": "^1.15.7",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"@vitejs/plugin-vue": "^5.0.3",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.17",
"boxen": "^7.1.1",
"cloc": "^2.11.0",
"cssnano": "^6.0.3",
"eslint": "^8.56.0",
"cssnano": "^6.0.5",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^2.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.20.1",
"eslint-plugin-vue": "^9.22.0",
"gradient-string": "^2.0.2",
"husky": "^8.0.3",
"lint-staged": "^15.2.0",
"postcss": "^8.4.33",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"postcss": "^8.4.35",
"postcss-html": "^1.6.0",
"postcss-import": "^15.1.0",
"postcss-import": "^16.0.1",
"postcss-scss": "^4.0.9",
"prettier": "^3.2.4",
"prettier": "^3.2.5",
"rimraf": "^5.0.5",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.70.0",
"stylelint": "^16.2.0",
"stylelint-config-recess-order": "^4.4.0",
"sass": "^1.71.1",
"stylelint": "^16.2.1",
"stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard-scss": "^12.0.0",
"stylelint-config-standard-scss": "^13.0.0",
"stylelint-prettier": "^5.0.0",
"svgo": "^3.2.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite": "^5.1.4",
"vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-fake-server": "^2.1.1",

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
"Grey": false,
"Weak": false,
"HideTabs": false,
"HideFooter": true,
"HideFooter": false,
"SidebarStatus": true,
"EpThemeColor": "#409EFF",
"ShowLogo": true,

View File

@ -43,3 +43,8 @@ export const getRoleList = (data?: object) => {
export const getDeptList = (data?: object) => {
return http.request<Result>("post", "/dept", { data });
};
/** 获取菜单管理列表 */
export const getMenuList = (data?: object) => {
return http.request<Result>("post", "/menu", { data });
};

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,12 +1,19 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { animates } from "./animate";
import { ref, computed, toRef } from "vue";
import { cloneDeep } from "@pureadmin/utils";
defineOptions({
name: "ReAnimateSelector"
});
const props = defineProps({
placeholder: {
type: String,
default: "请选择动画"
}
});
const inputValue = defineModel({ type: String });
const searchVal = ref();
@ -74,7 +81,7 @@ function onMouseleave() {
<el-select
clearable
filterable
placeholder="请选择动画"
:placeholder="props.placeholder"
popper-class="pure-animate-popper"
:model-value="inputValue"
:filter-method="filterMethod"

View File

@ -90,7 +90,7 @@ function handleClose(
v-model="options.visible"
class="pure-dialog"
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
@close="handleClose(options, index)"
@closed="handleClose(options, index)"
@opened="eventsCallBack('open', options, index)"
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"

View File

@ -3,7 +3,7 @@ import type { CSSProperties, VNode, Component } from "vue";
type DoneFn = (cancel?: boolean) => void;
type EventType = "open" | "close" | "openAutoFocus" | "closeAutoFocus";
type ArgsType = {
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页 */
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
command: "cancel" | "sure" | "close";
};
@ -157,7 +157,7 @@ interface DialogOptions extends DialogProps {
options: DialogOptions;
index: number;
}) => void;
/** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或空白页关闭页面时才会触发) */
/** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或空白页或按下了esc键关闭页面时才会触发) */
close?: ({
options,
index
@ -165,7 +165,7 @@ interface DialogOptions extends DialogProps {
options: DialogOptions;
index: number;
}) => void;
/** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页 */
/** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
closeCallBack?: ({
options,
index,

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { IconJson } from "@/components/ReIcon/data";
import { cloneDeep, isAllEmpty } from "@pureadmin/utils";
import { ref, computed, CSSProperties, toRef, watch } from "vue";
import { ref, computed, CSSProperties, watch } from "vue";
import Search from "@iconify-icons/ri/search-eye-line";
type ParameterCSSProperties = (item?: string) => CSSProperties | undefined;

View File

@ -30,7 +30,7 @@ import Table from "@iconify-icons/ri/table-line";
import Info from "@iconify-icons/ri/file-info-line";
import Artboard from "@iconify-icons/ri/artboard-line";
addIcon("ubuntuFill", UbuntuFill);
addIcon("menu", Menu);
addIcon("ep:menu", Menu);
addIcon("edit", Edit);
addIcon("informationLine", InformationLine);
addIcon("setUp", SetUp);
@ -42,14 +42,14 @@ addIcon("listCheck", ListCheck);
addIcon("histogram", Histogram);
addIcon("ppt", Ppt);
addIcon("checkboxCircleLine", CheckboxCircleLine);
addIcon("flUser", FlUser);
addIcon("role", Role);
addIcon("setting", Setting);
addIcon("dept", Dept);
addIcon("ri:admin-line", FlUser);
addIcon("ri:admin-fill", Role);
addIcon("ri:settings-3-line", Setting);
addIcon("ri:git-branch-line", Dept);
addIcon("search", Search);
addIcon("lollipop", Lollipop);
addIcon("monitor", Monitor);
addIcon("tag", Tag);
addIcon("ep:lollipop", Lollipop);
addIcon("ep:monitor", Monitor);
addIcon("ri:bookmark-2-line", Tag);
addIcon("table", Table);
addIcon("info", Info);
addIcon("artboard", Artboard);

View File

@ -29,6 +29,10 @@ const props = {
columns: {
type: Array as PropType<TableColumnList>,
default: () => []
},
isExpandAll: {
type: Boolean,
default: true
}
};
@ -38,10 +42,10 @@ export default defineComponent({
emits: ["refresh"],
setup(props, { emit, slots, attrs }) {
const size = ref("default");
const isExpandAll = ref(true);
const loading = ref(false);
const checkAll = ref(true);
const isIndeterminate = ref(false);
const isExpandAll = ref(props.isExpandAll);
const filterColumns = cloneDeep(props?.columns).filter(column =>
isBoolean(column?.hide)
? !column.hide

View File

@ -12,7 +12,7 @@ export interface OptionsType {
/** 图标属性、样式配置 */
iconAttrs?: iconType;
/** 值 */
value?: string | number;
value?: any;
/** 是否禁用 */
disabled?: boolean;
/** `tooltip` 提示 */

View File

@ -5,14 +5,16 @@ const TITLE = getConfig("Title");
</script>
<template>
<footer class="layout-footer">
MIT © 2020-PRESENT
<footer
class="layout-footer text-[rgba(0,0,0,0.6)] dark:text-[rgba(220,220,242,0.8)]"
>
Copyright © 2020-present
<a
class="ml-1 hover:text-primary"
class="hover:text-primary"
href="https://github.com/pure-admin"
target="_blank"
>
{{ TITLE }}
&nbsp;{{ TITLE }}
</a>
</footer>
</template>
@ -24,6 +26,6 @@ const TITLE = getConfig("Title");
justify-content: center;
width: 100%;
padding: 0 0 8px;
color: #c0c4cc;
font-size: 14px;
}
</style>

View File

@ -4,7 +4,7 @@ export interface ListItem {
datetime: string;
type: string;
description: string;
status?: "" | "success" | "warning" | "info" | "danger";
status?: "primary" | "success" | "warning" | "info" | "danger";
extra?: string;
}

View File

@ -115,7 +115,7 @@ defineExpose({ handleScroll });
cursor: pointer;
border: 0.1px solid #ccc;
border-radius: 4px;
transition: all 0.3s;
transition: font-size 0.16s;
&-title {
display: flex;

View File

@ -1,7 +1,8 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { computed } from "vue";
import { useGlobal } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav";
import MenuFold from "@iconify-icons/ri/menu-fold-fill";
interface Props {
@ -12,7 +13,6 @@ const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const visible = ref(false);
const { tooltipEffect } = useNav();
const iconClass = computed(() => {
@ -42,21 +42,18 @@ const toggleClick = () => {
<template>
<div class="collapse-container">
<el-tooltip
placement="right"
:visible="visible"
:effect="tooltipEffect"
:content="props.isActive ? '点击折叠' : '点击展开'"
>
<IconifyIconOffline
v-tippy="{
content: props.isActive ? '点击折叠' : '点击展开',
theme: tooltipEffect,
hideOnClick: 'toggle',
placement: 'right'
}"
:icon="MenuFold"
:class="[iconClass, themeColor === 'light' ? '' : 'text-primary']"
:style="{ transform: props.isActive ? 'none' : 'rotateY(180deg)' }"
@click="toggleClick"
@mouseenter="visible = true"
@mouseleave="visible = false"
/>
</el-tooltip>
</div>
</template>

View File

@ -3,7 +3,6 @@ import path from "path";
import { getConfig } from "@/config";
import { menuType } from "../../types";
import extraIcon from "./extraIcon.vue";
import { useDark } from "@pureadmin/utils";
import { ReText } from "@/components/ReText";
import { useNav } from "@/layout/hooks/useNav";
import { transformI18n } from "@/plugins/i18n";
@ -16,7 +15,6 @@ import ArrowLeft from "@iconify-icons/ep/arrow-left-bold";
import ArrowRight from "@iconify-icons/ep/arrow-right-bold";
const { layout, isCollapse, tooltipEffect, getDivStyle } = useNav();
const { isDark } = useDark();
const props = defineProps({
item: {
@ -143,7 +141,7 @@ function resolvePath(routePath) {
<ReText
:tippyProps="{
offset: [0, -10],
theme: !isDark ? tooltipEffect : undefined
theme: tooltipEffect
}"
class="!text-inherit"
>
@ -181,7 +179,7 @@ function resolvePath(routePath) {
"
:tippyProps="{
offset: [0, -10],
theme: !isDark ? tooltipEffect : undefined
theme: tooltipEffect
}"
:class="{
'!text-inherit': true,

View File

@ -1,6 +1,6 @@
// 完整版菜单比较多,将 rank 抽离出来,在此方便维护
const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以后端在返回 rank 的时候需要从 1 开始哦
const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以后端在返回 rank 的时候需要从非 0 开始
components = 1,
able = 2,
table = 3,

View File

@ -5,7 +5,7 @@ export default {
path: "/components",
redirect: "/components/dialog",
meta: {
icon: "menu",
icon: "ep:menu",
title: $t("menus.hscomponents"),
rank: components
},

View File

@ -44,15 +44,23 @@
}
.pure-dialog {
.el-dialog__header.show-close {
padding-right: 16px;
}
.el-dialog__headerbtn {
top: 16px;
right: 12px;
width: 24px;
height: 24px;
}
.pure-dialog-svg {
color: var(--el-color-info);
}
.el-dialog__headerbtn {
top: 20px;
right: 14px;
width: 24px;
height: 24px;
.el-dialog__footer {
padding-top: 0;
}
}
@ -148,17 +156,14 @@
display: none;
}
.el-dialog__body {
padding-top: 12px;
padding-bottom: 0;
}
.el-input__inner {
font-size: 1.2em;
}
.el-dialog__footer {
padding-bottom: 10px;
width: calc(100% + 32px);
padding: 10px 20px;
margin: auto -16px -16px;
box-shadow:
0 -1px 0 0 #e0e3e8,
0 -3px 6px 0 rgb(69 98 155 / 12%);

View File

@ -1,6 +1,7 @@
.wave {
position: fixed;
height: 100%;
width: 80%;
left: 0;
bottom: 0;
z-index: -1;

View File

@ -6,10 +6,10 @@ defineOptions({
name: "AnimateCss"
});
const icon = ref("");
const animate = ref("");
watch(icon, () => {
console.log("icon", icon.value);
watch(animate, () => {
console.log("animate", animate.value);
});
</script>
@ -29,6 +29,6 @@ watch(icon, () => {
</span>
</div>
</template>
<ReAnimateSelector v-model="icon" class="!w-[200px]" />
<ReAnimateSelector v-model="animate" class="!w-[200px]" />
</el-card>
</template>

View File

@ -203,7 +203,7 @@ function onCloseCallBackClick() {
} else if (args?.command === "sure") {
text = "您点击了确定按钮";
} else {
text = "您点击了右上角关闭按钮或空白页";
text = "您点击了右上角关闭按钮或空白页或按下了esc键";
}
message(text);
},
@ -301,7 +301,9 @@ function onFormOneClick() {
} else if (args?.command === "sure") {
message(`您点击了确定按钮,当前表单数据为 ${text}`);
} else {
message(`您点击了右上角关闭按钮或者空白页,当前表单数据为 ${text}`);
message(
`您点击了右上角关闭按钮或空白页或按下了esc键当前表单数据为 ${text}`
);
}
}
});

View File

@ -12,8 +12,8 @@ const checked2 = ref(false);
const baseTag = ref("dark");
const tagList = ref([
{
type: "",
text: "Default"
type: "primary",
text: "Primary"
},
{
type: "success",

View File

@ -323,6 +323,18 @@ watch(loginDay, value => {
</div>
</div>
</div>
<div
class="w-full flex-c absolute bottom-3 text-sm text-[rgba(0,0,0,0.6)] dark:text-[rgba(220,220,242,0.8)]"
>
Copyright © 2020-present
<a
class="hover:text-primary"
href="https://github.com/pure-admin"
target="_blank"
>
&nbsp;{{ title }}
</a>
</div>
</div>
</template>

View File

@ -15,19 +15,26 @@ defineOptions({
<div class="card-header">组件方式判断权限</div>
</template>
<el-space wrap>
<Auth value="btn_add">
<Auth value="permission:btn:add">
<el-button plain type="warning">
拥有code'btn_add' 权限可见
拥有code'permission:btn:add' 权限可见
</el-button>
</Auth>
<Auth :value="['btn_edit']">
<Auth :value="['permission:btn:edit']">
<el-button plain type="primary">
拥有code['btn_edit'] 权限可见
拥有code['permission:btn:edit'] 权限可见
</el-button>
</Auth>
<Auth :value="['btn_add', 'btn_edit', 'btn_delete']">
<Auth
:value="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
>
<el-button plain type="danger">
拥有code['btn_add', 'btn_edit', 'btn_delete'] 权限可见
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</Auth>
</el-space>
@ -38,18 +45,25 @@ defineOptions({
<div class="card-header">函数方式判断权限</div>
</template>
<el-space wrap>
<el-button v-if="hasAuth('btn_add')" plain type="warning">
拥有code'btn_add' 权限可见
<el-button v-if="hasAuth('permission:btn:add')" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button v-if="hasAuth(['btn_edit'])" plain type="primary">
拥有code['btn_edit'] 权限可见
<el-button v-if="hasAuth(['permission:btn:edit'])" plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-if="hasAuth(['btn_add', 'btn_edit', 'btn_delete'])"
v-if="
hasAuth([
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
])
"
plain
type="danger"
>
拥有code['btn_add', 'btn_edit', 'btn_delete'] 权限可见
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>
@ -61,18 +75,23 @@ defineOptions({
</div>
</template>
<el-space wrap>
<el-button v-auth="'btn_add'" plain type="warning">
拥有code'btn_add' 权限可见
<el-button v-auth="'permission:btn:add'" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button v-auth="['btn_edit']" plain type="primary">
拥有code['btn_edit'] 权限可见
<el-button v-auth="['permission:btn:edit']" plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-auth="['btn_add', 'btn_edit', 'btn_delete']"
v-auth="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
plain
type="danger"
>
拥有code['btn_add', 'btn_edit', 'btn_delete'] 权限可见
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>

View File

@ -95,7 +95,10 @@ const tableData = [
:columns="columns"
>
<template #tag="{ row }">
<el-tag :type="row.tag === 'Home' ? '' : 'success'" disable-transitions>
<el-tag
:type="row.tag === 'Home' ? null : 'success'"
disable-transitions
>
{{ row.tag }}
</el-tag>
</template>

View File

@ -89,7 +89,7 @@ const {
<pure-table
ref="tableRef"
adaptive
:adaptiveConfig="{ offsetBottom: 32 }"
:adaptiveConfig="{ offsetBottom: 45 }"
align-whole="center"
row-key="id"
showOverflowTooltip
@ -111,20 +111,20 @@ const {
link
type="primary"
:size="size"
:icon="useRenderIcon(AddFill)"
@click="openDialog('新增', { parentId: row.id } as any)"
:icon="useRenderIcon(EditPen)"
@click="openDialog('修改', row)"
>
新增
修改
</el-button>
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('修改', row)"
:icon="useRenderIcon(AddFill)"
@click="openDialog('新增', { parentId: row.id } as any)"
>
修改
新增
</el-button>
<el-popconfirm
:title="`是否确认删除部门名称为${row.name}的这条数据`"
@ -150,6 +150,14 @@ const {
</template>
<style lang="scss" scoped>
:deep(.el-table__inner-wrapper::before) {
height: 0;
}
.main-content {
margin: 24px 24px 0 !important;
}
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;

View File

@ -0,0 +1,27 @@
<!-- 初版,持续完善中 -->
## 字段含义
| 字段 | 说明 |
| :---------------- | :----------------------------------------------------------- |
| `menuType` | 菜单类型(`0`代表菜单、`1`代表`iframe`、`2`代表外链、`3`代表按钮) |
| `parentId` | |
| `title` | 菜单名称(兼容国际化、非国际化,如果用国际化的写法就必须在根目录的`locales`文件夹下对应添加) |
| `name` | 路由名称(必须唯一并且和当前路由`component`字段对应的页面里用`defineOptions`包起来的`name`保持一致) |
| `path` | 路由路径 |
| `component` | 组件路径(传`component`组件路径,那么`path`可以随便写,如果不传,`component`组件路径会跟`path`保持一致) |
| `rank` | 菜单排序(平台规定只有`home`路由的`rank`才能为`0`,所以后端在返回`rank`的时候需要从非`0`开始 [点击查看更多](https://yiming_chang.gitee.io/pure-admin-doc/pages/routerMenu/#%E8%8F%9C%E5%8D%95%E6%8E%92%E5%BA%8F-rank) |
| `redirect` | 路由重定向 |
| `icon` | 菜单图标 |
| `extraIcon` | 右侧图标 |
| `enterTransition` | 进场动画(页面加载动画) |
| `leaveTransition` | 离场动画(页面加载动画) |
| `activePath` | 菜单激活(将某个菜单激活,主要用于通过`query`或`params`传参的路由,当它们通过配置`showLink: false`后不在菜单中显示,就不会有任何菜单高亮,而通过设置`activePath`指定激活菜单即可获得高亮,`activePath`为指定激活菜单的`path` |
| `auths` | 权限标识(按钮级别权限设置) |
| `frameSrc` | 链接地址(需要内嵌的`iframe`链接地址) |
| `frameLoading` | 加载动画(内嵌的`iframe`页面是否开启首次加载动画) |
| `keepAlive` | 缓存页面(是否缓存该路由页面,开启后会保存该页面的整体状态,刷新后会清空状态) |
| `hiddenTag` | 标签页(当前菜单名称或自定义信息禁止添加到标签页) |
| `showLink` | 菜单(是否显示该菜单) |
| `showParent` | 父级菜单(是否显示父级菜单 [点击查看更多](https://yiming_chang.gitee.io/pure-admin-doc/pages/routerMenu/#%E7%AC%AC%E4%B8%80%E7%A7%8D-%E8%AF%A5%E6%A8%A1%E5%BC%8F%E9%92%88%E5%AF%B9%E7%88%B6%E7%BA%A7%E8%8F%9C%E5%8D%95%E4%B8%8B%E5%8F%AA%E6%9C%89%E4%B8%80%E4%B8%AA%E5%AD%90%E8%8F%9C%E5%8D%95%E7%9A%84%E6%83%85%E5%86%B5-%E5%9C%A8%E5%AD%90%E8%8F%9C%E5%8D%95%E7%9A%84-meta-%E5%B1%9E%E6%80%A7%E4%B8%AD%E5%8A%A0%E4%B8%8A-showparent-true-%E5%8D%B3%E5%8F%AF) |

View File

@ -0,0 +1,326 @@
<script setup lang="ts">
import { ref } from "vue";
import ReCol from "@/components/ReCol";
import { formRules } from "./utils/rule";
import { FormProps } from "./utils/types";
import { transformI18n } from "@/plugins/i18n";
import { IconSelect } from "@/components/ReIcon";
import Segmented from "@/components/ReSegmented";
import ReAnimateSelector from "@/components/ReAnimateSelector";
import {
menuTypeOptions,
showLinkOptions,
keepAliveOptions,
hiddenTagOptions,
showParentOptions,
frameLoadingOptions
} from "./utils/enums";
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
menuType: 0,
higherMenuOptions: [],
parentId: 0,
title: "",
name: "",
path: "",
component: "",
rank: 99,
redirect: " ",
icon: "",
extraIcon: "",
enterTransition: "",
leaveTransition: "",
activePath: "",
auths: "",
frameSrc: "",
frameLoading: true,
keepAlive: false,
hiddenTag: false,
showLink: true,
showParent: false
})
});
const ruleFormRef = ref();
const newFormInline = ref(props.formInline);
function getRef() {
return ruleFormRef.value;
}
defineExpose({ getRef });
</script>
<template>
<el-form
ref="ruleFormRef"
:model="newFormInline"
:rules="formRules"
label-width="82px"
>
<el-row :gutter="30">
<re-col>
<el-form-item label="菜单类型">
<Segmented
v-model="newFormInline.menuType"
:options="menuTypeOptions"
/>
</el-form-item>
</re-col>
<re-col>
<el-form-item label="上级菜单">
<el-cascader
v-model="newFormInline.parentId"
class="w-full"
:options="newFormInline.higherMenuOptions"
:props="{
value: 'id',
label: 'title',
emitPath: false,
checkStrictly: true
}"
clearable
filterable
placeholder="请选择上级菜单"
>
<template #default="{ node, data }">
<span>{{ transformI18n(data.title) }}</span>
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
</template>
</el-cascader>
</el-form-item>
</re-col>
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="菜单名称" prop="title">
<el-input
v-model="newFormInline.title"
clearable
placeholder="请输入菜单名称"
/>
</el-form-item>
</re-col>
<re-col v-if="newFormInline.menuType !== 3" :value="12" :xs="24" :sm="24">
<el-form-item label="路由名称" prop="name">
<el-input
v-model="newFormInline.name"
clearable
placeholder="请输入路由名称"
/>
</el-form-item>
</re-col>
<re-col v-if="newFormInline.menuType !== 3" :value="12" :xs="24" :sm="24">
<el-form-item label="路由路径" prop="path">
<el-input
v-model="newFormInline.path"
clearable
placeholder="请输入路由路径"
/>
</el-form-item>
</re-col>
<re-col
v-show="newFormInline.menuType === 0"
:value="12"
:xs="24"
:sm="24"
>
<el-form-item label="组件路径">
<el-input
v-model="newFormInline.component"
clearable
placeholder="请输入组件路径"
/>
</el-form-item>
</re-col>
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="菜单排序">
<el-input-number
v-model="newFormInline.rank"
class="!w-full"
:min="1"
:max="9999"
controls-position="right"
/>
</el-form-item>
</re-col>
<re-col
v-show="newFormInline.menuType === 0"
:value="12"
:xs="24"
:sm="24"
>
<el-form-item label="路由重定向">
<el-input
v-model="newFormInline.redirect"
clearable
placeholder="请输入默认跳转地址"
/>
</el-form-item>
</re-col>
<re-col
v-show="newFormInline.menuType !== 3"
:value="12"
:xs="24"
:sm="24"
>
<el-form-item label="菜单图标">
<IconSelect v-model="newFormInline.icon" class="w-full" />
</el-form-item>
</re-col>
<re-col
v-show="newFormInline.menuType !== 3"
:value="12"
:xs="24"
:sm="24"
>
<el-form-item label="右侧图标">
<el-input
v-model="newFormInline.extraIcon"
clearable
placeholder="菜单名称右侧的额外图标"
/>
</el-form-item>
</re-col>
<re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
<el-form-item label="进场动画">
<ReAnimateSelector
v-model="newFormInline.enterTransition"
placeholder="请选择页面进场加载动画"
/>
</el-form-item>
</re-col>
<re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
<el-form-item label="离场动画">
<ReAnimateSelector
v-model="newFormInline.leaveTransition"
placeholder="请选择页面离场加载动画"
/>
</el-form-item>
</re-col>
<re-col
v-show="newFormInline.menuType === 0"
:value="12"
:xs="24"
:sm="24"
>
<el-form-item label="菜单激活">
<el-input
v-model="newFormInline.activePath"
clearable
placeholder="请输入需要激活的菜单"
/>
</el-form-item>
</re-col>
<re-col v-if="newFormInline.menuType === 3" :value="12" :xs="24" :sm="24">
<!-- 按钮级别权限设置 -->
<el-form-item label="权限标识" prop="auths">
<el-input
v-model="newFormInline.auths"
clearable
placeholder="请输入权限标识"
/>
</el-form-item>
</re-col>
<re-col
v-show="newFormInline.menuType === 1"
:value="12"
:xs="24"
:sm="24"
>
<!-- iframe -->
<el-form-item label="链接地址">
<el-input
v-model="newFormInline.frameSrc"
clearable
placeholder="请输入 iframe 链接地址"
/>
</el-form-item>
</re-col>
<re-col v-if="newFormInline.menuType === 1" :value="12" :xs="24" :sm="24">
<el-form-item label="加载动画">
<Segmented
:modelValue="newFormInline.frameLoading ? 0 : 1"
:options="frameLoadingOptions"
@change="
({ option: { value } }) => {
newFormInline.frameLoading = value;
}
"
/>
</el-form-item>
</re-col>
<re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
<el-form-item label="缓存页面">
<Segmented
:modelValue="newFormInline.keepAlive ? 0 : 1"
:options="keepAliveOptions"
@change="
({ option: { value } }) => {
newFormInline.keepAlive = value;
}
"
/>
</el-form-item>
</re-col>
<re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
<el-form-item label="标签页">
<Segmented
:modelValue="newFormInline.hiddenTag ? 1 : 0"
:options="hiddenTagOptions"
@change="
({ option: { value } }) => {
newFormInline.hiddenTag = value;
}
"
/>
</el-form-item>
</re-col>
<re-col
v-show="newFormInline.menuType !== 3"
:value="12"
:xs="24"
:sm="24"
>
<el-form-item label="菜单">
<Segmented
:modelValue="newFormInline.showLink ? 0 : 1"
:options="showLinkOptions"
@change="
({ option: { value } }) => {
newFormInline.showLink = value;
}
"
/>
</el-form-item>
</re-col>
<re-col
v-show="newFormInline.menuType !== 3"
:value="8"
:xs="24"
:sm="24"
>
<el-form-item label="父级菜单">
<Segmented
:modelValue="newFormInline.showParent ? 0 : 1"
:options="showParentOptions"
@change="
({ option: { value } }) => {
newFormInline.showParent = value;
}
"
/>
</el-form-item>
</re-col>
</el-row>
</el-form>
</template>

View File

@ -0,0 +1,157 @@
<script setup lang="ts">
import { ref } from "vue";
import { useMenu } from "./utils/hook";
import { transformI18n } from "@/plugins/i18n";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import Delete from "@iconify-icons/ep/delete";
import EditPen from "@iconify-icons/ep/edit-pen";
import Refresh from "@iconify-icons/ep/refresh";
import AddFill from "@iconify-icons/ri/add-circle-line";
defineOptions({
name: "SystemMenu"
});
const formRef = ref();
const tableRef = ref();
const {
form,
loading,
columns,
dataList,
onSearch,
resetForm,
openDialog,
handleDelete,
handleSelectionChange
} = useMenu();
</script>
<template>
<div class="main">
<el-form
ref="formRef"
:inline="true"
:model="form"
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
>
<el-form-item label="菜单名称:" prop="title">
<el-input
v-model="form.title"
placeholder="请输入菜单名称"
clearable
class="!w-[180px]"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:icon="useRenderIcon('search')"
:loading="loading"
@click="onSearch"
>
搜索
</el-button>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<PureTableBar
title="菜单管理(初版,持续完善中)"
:columns="columns"
:isExpandAll="false"
:tableRef="tableRef?.getTableRef()"
@refresh="onSearch"
>
<template #buttons>
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog()"
>
新增菜单
</el-button>
</template>
<template v-slot="{ size, dynamicColumns }">
<pure-table
ref="tableRef"
adaptive
:adaptiveConfig="{ offsetBottom: 45 }"
align-whole="center"
row-key="id"
showOverflowTooltip
table-layout="auto"
:loading="loading"
:size="size"
:data="dataList"
:columns="dynamicColumns"
:header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}"
@selection-change="handleSelectionChange"
>
<template #operation="{ row }">
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('修改', row)"
>
修改
</el-button>
<el-button
v-show="row.menuType !== 3"
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(AddFill)"
@click="openDialog('新增', { parentId: row.id } as any)"
>
新增
</el-button>
<el-popconfirm
:title="`是否确认删除菜单名称为${transformI18n(row.title)}的这条数据${row?.children?.length > 0 ? '。注意下级菜单也会一并删除,请谨慎操作' : ''}`"
@confirm="handleDelete(row)"
>
<template #reference>
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Delete)"
>
删除
</el-button>
</template>
</el-popconfirm>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-table__inner-wrapper::before) {
height: 0;
}
.main-content {
margin: 24px 24px 0 !important;
}
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
}
</style>

View File

@ -0,0 +1,94 @@
import type { OptionsType } from "@/components/ReSegmented";
const menuTypeOptions: Array<OptionsType> = [
{
label: "菜单",
value: 0
},
{
label: "iframe",
value: 1
},
{
label: "外链",
value: 2
},
{
label: "按钮",
value: 3
}
];
const showLinkOptions: Array<OptionsType> = [
{
label: "显示",
tip: "会在菜单中显示",
value: true
},
{
label: "隐藏",
tip: "不会在菜单中显示",
value: false
}
];
const keepAliveOptions: Array<OptionsType> = [
{
label: "缓存",
tip: "会保存该页面的整体状态,刷新后会清空状态",
value: true
},
{
label: "不缓存",
tip: "不会保存该页面的整体状态",
value: false
}
];
const hiddenTagOptions: Array<OptionsType> = [
{
label: "允许",
tip: "当前菜单名称或自定义信息允许添加到标签页",
value: false
},
{
label: "禁止",
tip: "当前菜单名称或自定义信息禁止添加到标签页",
value: true
}
];
const showParentOptions: Array<OptionsType> = [
{
label: "显示",
tip: "会显示父级菜单",
value: true
},
{
label: "隐藏",
tip: "不会显示父级菜单",
value: false
}
];
const frameLoadingOptions: Array<OptionsType> = [
{
label: "开启",
tip: "有首次加载动画",
value: true
},
{
label: "关闭",
tip: "无首次加载动画",
value: false
}
];
export {
menuTypeOptions,
showLinkOptions,
keepAliveOptions,
hiddenTagOptions,
showParentOptions,
frameLoadingOptions
};

View File

@ -0,0 +1,223 @@
import editForm from "../form.vue";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message";
import { getMenuList } from "@/api/system";
import { transformI18n } from "@/plugins/i18n";
import { addDialog } from "@/components/ReDialog";
import { reactive, ref, onMounted, h } from "vue";
import type { FormItemProps } from "../utils/types";
import { cloneDeep, isAllEmpty } from "@pureadmin/utils";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
export function useMenu() {
const form = reactive({
title: ""
});
const formRef = ref();
const dataList = ref([]);
const loading = ref(true);
const getMenuType = (type, text = false) => {
switch (type) {
case 0:
return text ? "菜单" : "primary";
case 1:
return text ? "iframe" : "warning";
case 2:
return text ? "外链" : "danger";
case 3:
return text ? "按钮" : "info";
}
};
const columns: TableColumnList = [
{
label: "菜单名称",
prop: "title",
align: "left",
cellRenderer: ({ row }) => (
<>
<span class="inline-block mr-1">
{h(useRenderIcon(row.icon), {
style: { paddingTop: "1px" }
})}
</span>
<span>{transformI18n(row.title)}</span>
</>
)
},
{
label: "菜单类型",
prop: "menuType",
width: 100,
cellRenderer: ({ row, props }) => (
<el-tag
size={props.size}
type={getMenuType(row.menuType)}
effect="plain"
>
{getMenuType(row.menuType, true)}
</el-tag>
)
},
{
label: "路由路径",
prop: "path"
},
{
label: "组件路径",
prop: "component",
formatter: ({ path, component }) =>
isAllEmpty(component) ? path : component
},
{
label: "权限标识",
prop: "auths"
},
{
label: "排序",
prop: "rank",
width: 100
},
{
label: "隐藏",
prop: "showLink",
formatter: ({ showLink }) => (showLink ? "否" : "是"),
width: 100
},
{
label: "操作",
fixed: "right",
width: 210,
slot: "operation"
}
];
function handleSelectionChange(val) {
console.log("handleSelectionChange", val);
}
function resetForm(formEl) {
if (!formEl) return;
formEl.resetFields();
onSearch();
}
async function onSearch() {
loading.value = true;
const { data } = await getMenuList(); // 这里是返回一维数组结构前端自行处理成树结构返回格式要求唯一id加父节点parentIdparentId取父节点id
let newData = data;
if (!isAllEmpty(form.title)) {
// 前端搜索菜单名称
newData = newData.filter(item =>
transformI18n(item.title).includes(form.title)
);
}
dataList.value = handleTree(newData); // 处理成树结构
setTimeout(() => {
loading.value = false;
}, 500);
}
function formatHigherMenuOptions(treeList) {
if (!treeList || !treeList.length) return;
const newTreeList = [];
for (let i = 0; i < treeList.length; i++) {
treeList[i].title = transformI18n(treeList[i].title);
formatHigherMenuOptions(treeList[i].children);
newTreeList.push(treeList[i]);
}
return newTreeList;
}
function openDialog(title = "新增", row?: FormItemProps) {
addDialog({
title: `${title}菜单`,
props: {
formInline: {
menuType: row?.menuType ?? 0,
higherMenuOptions: formatHigherMenuOptions(cloneDeep(dataList.value)),
parentId: row?.parentId ?? 0,
title: row?.title ?? "",
name: row?.name ?? "",
path: row?.path ?? "",
component: row?.component ?? "",
rank: row?.rank ?? 99,
redirect: row?.redirect ?? "",
icon: row?.icon ?? "",
extraIcon: row?.extraIcon ?? "",
enterTransition: row?.enterTransition ?? "",
leaveTransition: row?.leaveTransition ?? "",
activePath: row?.activePath ?? "",
auths: row?.auths ?? "",
frameSrc: row?.frameSrc ?? "",
frameLoading: row?.frameLoading ?? true,
keepAlive: row?.keepAlive ?? false,
hiddenTag: row?.hiddenTag ?? false,
showLink: row?.showLink ?? true,
showParent: row?.showParent ?? false
}
},
width: "45%",
draggable: true,
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(editForm, { ref: formRef }),
beforeSure: (done, { options }) => {
const FormRef = formRef.value.getRef();
const curData = options.props.formInline as FormItemProps;
function chores() {
message(
`${title}了菜单名称为${transformI18n(curData.title)}的这条数据`,
{
type: "success"
}
);
done(); // 关闭弹框
onSearch(); // 刷新表格数据
}
FormRef.validate(valid => {
if (valid) {
console.log("curData", curData);
// 表单规则校验通过
if (title === "新增") {
// 实际开发先调用新增接口,再进行下面操作
chores();
} else {
// 实际开发先调用修改接口,再进行下面操作
chores();
}
}
});
}
});
}
function handleDelete(row) {
message(`您删除了菜单名称为${transformI18n(row.title)}的这条数据`, {
type: "success"
});
onSearch();
}
onMounted(() => {
onSearch();
});
return {
form,
loading,
columns,
dataList,
/** 搜索 */
onSearch,
/** 重置 */
resetForm,
/** 新增、修改菜单 */
openDialog,
/** 删除菜单 */
handleDelete,
handleSelectionChange
};
}

View File

@ -0,0 +1,10 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
/** 自定义表单规则校验 */
export const formRules = reactive(<FormRules>{
title: [{ required: true, message: "菜单名称为必填项", trigger: "blur" }],
name: [{ required: true, message: "路由名称为必填项", trigger: "blur" }],
path: [{ required: true, message: "路由路径为必填项", trigger: "blur" }],
auths: [{ required: true, message: "权限标识为必填项", trigger: "blur" }]
});

View File

@ -0,0 +1,29 @@
interface FormItemProps {
/** 菜单类型0代表菜单、1代表iframe、2代表外链、3代表按钮*/
menuType: number;
higherMenuOptions: Record<string, unknown>[];
parentId: number;
title: string;
name: string;
path: string;
component: string;
rank: number;
redirect: string;
icon: string;
extraIcon: string;
enterTransition: string;
leaveTransition: string;
activePath: string;
auths: string;
frameSrc: string;
frameLoading: boolean;
keepAlive: boolean;
hiddenTag: boolean;
showLink: boolean;
showParent: boolean;
}
interface FormProps {
formInline: FormItemProps;
}
export type { FormItemProps, FormProps };

View File

@ -108,6 +108,7 @@ const {
:loading="loading"
:size="size"
adaptive
:adaptiveConfig="{ offsetBottom: 108 }"
:data="dataList"
:columns="dynamicColumns"
:pagination="pagination"
@ -206,6 +207,10 @@ const {
margin: 0;
}
.main-content {
margin: 24px 24px 0 !important;
}
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;

View File

@ -150,6 +150,7 @@ const {
ref="tableRef"
row-key="id"
adaptive
:adaptiveConfig="{ offsetBottom: 108 }"
align-whole="center"
table-layout="auto"
:loading="loading"
@ -260,6 +261,10 @@ const {
outline: none;
}
.main-content {
margin: 24px 24px 0 !important;
}
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;

View File

@ -96,7 +96,7 @@ defineExpose({ onTreeReset });
<div
v-loading="props.treeLoading"
class="h-full bg-bg_color overflow-auto"
:style="{ minHeight: `calc(100vh - 133px)` }"
:style="{ minHeight: `calc(100vh - 145px)` }"
>
<div class="flex items-center h-[34px]">
<el-input

View File

@ -104,7 +104,7 @@ export function useUser(tableRef: Ref, treeRef: Ref) {
cellRenderer: ({ row, props }) => (
<el-tag
size={props.size}
type={row.sex === 1 ? "danger" : ""}
type={row.sex === 1 ? "danger" : null}
effect="plain"
>
{row.sex === 1 ? "女" : "男"}