mirror of
https://gitee.com/bootx/dax-pay-ui.git
synced 2025-10-14 22:27:05 +00:00
feat 菜单和各类权限处理
This commit is contained in:
@@ -14,7 +14,7 @@ VITE_GLOB_API_URL=/admin
|
||||
VITE_GLOB_API_URL_PREFIX =
|
||||
|
||||
# 终端类型
|
||||
VITE_GLOB_APP_CLIENT=admin
|
||||
VITE_GLOB_APP_CLIENT=dax-pay-admin
|
||||
|
||||
# 超时时间
|
||||
VITE_GLOB_API_TIMEOUT=30000
|
||||
|
@@ -11,10 +11,10 @@ VITE_PUBLIC_PATH=/
|
||||
VITE_GLOB_API_URL=/merchant
|
||||
|
||||
# 接口前缀
|
||||
VITE_GLOB_API_URL_PREFIX =
|
||||
VITE_GLOB_API_URL_PREFIX=
|
||||
|
||||
# 终端类型
|
||||
VITE_GLOB_APP_CLIENT=merchant
|
||||
VITE_GLOB_APP_CLIENT=dax-pay-merchant
|
||||
|
||||
# 超时时间
|
||||
VITE_GLOB_API_TIMEOUT=30000
|
||||
|
@@ -8,7 +8,7 @@ export interface PermMenu {
|
||||
effect: boolean
|
||||
icon: string
|
||||
hidden: boolean
|
||||
hideChildrenInMenu: boolean
|
||||
hideChildrenMenu: boolean
|
||||
component: string
|
||||
path: string
|
||||
iframeUrl: string
|
||||
|
@@ -69,7 +69,7 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref, emit: A
|
||||
if (
|
||||
!hideMenu &&
|
||||
reg.test(name?.toLowerCase() ?? '') &&
|
||||
(!children?.length || meta?.hideChildrenInMenu)
|
||||
(!children?.length || meta?.hideChildrenMenu)
|
||||
) {
|
||||
const chars: { char: string; highlight: boolean }[] = []
|
||||
|
||||
@@ -151,7 +151,7 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref, emit: A
|
||||
icon,
|
||||
})
|
||||
}
|
||||
if (!meta?.hideChildrenInMenu && Array.isArray(children) && children.length) {
|
||||
if (!meta?.hideChildrenMenu && Array.isArray(children) && children.length) {
|
||||
ret.push(...handlerSearchResult(children, reg, item))
|
||||
}
|
||||
})
|
||||
|
@@ -47,7 +47,7 @@
|
||||
position: relative;
|
||||
padding-left: 7px;
|
||||
color: @text-color-base;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import SvgIcon from './src/SvgIcon.vue'
|
||||
import IconPicker from './src/IconPicker.vue'
|
||||
import Icon from './Icon.vue'
|
||||
|
||||
export { IconPicker, SvgIcon }
|
||||
export { IconPicker, SvgIcon, Icon }
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import ALink from './Link.vue'
|
||||
import { withInstall } from '@/utils'
|
||||
import link from './Link.vue'
|
||||
|
||||
export const Link = withInstall(link)
|
||||
const Link = withInstall(ALink)
|
||||
export { ALink, Link }
|
||||
|
@@ -30,7 +30,7 @@
|
||||
const getShowMenu = computed(() => !props.item.meta?.hideMenu)
|
||||
function menuHasChildren(menuTreeItem: MenuType): boolean {
|
||||
return (
|
||||
!menuTreeItem.meta?.hideChildrenInMenu &&
|
||||
!menuTreeItem.meta?.hideChildrenMenu &&
|
||||
Reflect.has(menuTreeItem, 'children') &&
|
||||
!!menuTreeItem.children &&
|
||||
menuTreeItem.children.length > 0
|
||||
|
@@ -29,7 +29,7 @@
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { REDIRECT_NAME } from '@/router/constant'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { isFunction, isOutsideUrl } from '@/utils/is'
|
||||
import { isFunction, getOutsideUrl } from '@/utils/is'
|
||||
import { openWindow } from '@/utils'
|
||||
import { useOpenKeys } from './useOpenKeys'
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
async function handleSelect(key: string) {
|
||||
// 判断是否需要通过外部打开
|
||||
const path = isOutsideUrl(key)
|
||||
const path = getOutsideUrl(key)
|
||||
if (path) {
|
||||
openWindow(path)
|
||||
return
|
||||
|
@@ -91,7 +91,7 @@
|
||||
|
||||
function menuHasChildren(menuTreeItem: Menu): boolean {
|
||||
return (
|
||||
!menuTreeItem.meta?.hideChildrenInMenu &&
|
||||
!menuTreeItem.meta?.hideChildrenMenu &&
|
||||
Reflect.has(menuTreeItem, 'children') &&
|
||||
!!menuTreeItem.children &&
|
||||
menuTreeItem.children.length > 0
|
||||
|
@@ -41,7 +41,6 @@ import {
|
||||
Descriptions,
|
||||
Space,
|
||||
} from 'ant-design-vue'
|
||||
import VXETable from 'vxe-table'
|
||||
|
||||
export function registerGlobComp(app: App) {
|
||||
app.use(Input)
|
||||
@@ -85,5 +84,4 @@ export function registerGlobComp(app: App) {
|
||||
app.use(Spin)
|
||||
app.use(Dropdown)
|
||||
app.use(Input)
|
||||
app.use(VXETable)
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { reactive, toRefs } from 'vue'
|
||||
import { FormEditType } from '@/enums/formTypeEnum'
|
||||
import { setIcon } from 'vxe-table'
|
||||
|
||||
export default function () {
|
||||
const model = reactive({
|
||||
@@ -61,9 +62,12 @@ export default function () {
|
||||
*/
|
||||
function handleCancel() {
|
||||
visible.value = false
|
||||
addable.value = false
|
||||
editable.value = false
|
||||
showable.value = false
|
||||
// 防止可以看到界面的的变化
|
||||
setTimeout(() => {
|
||||
addable.value = false
|
||||
editable.value = false
|
||||
showable.value = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -50,7 +50,7 @@ function createConfirm(options: ModalOptionsEx) {
|
||||
const iconType = options.iconType || 'warning'
|
||||
Reflect.deleteProperty(options, 'iconType')
|
||||
const opt: ModalFuncProps = {
|
||||
centered: true,
|
||||
centered: false,
|
||||
icon: getIcon(iconType),
|
||||
...options,
|
||||
content: renderContent(options),
|
||||
|
@@ -10,7 +10,7 @@
|
||||
<router-link v-else to="" @click="handleClick(routeItem)">
|
||||
{{ (routeItem.meta.title || routeItem.name) as string }}
|
||||
</router-link>
|
||||
<template v-if="routeItem.children && !routeItem.meta?.hideChildrenInMenu" #overlay>
|
||||
<template v-if="routeItem.children && !routeItem.meta?.hideChildrenMenu" #overlay>
|
||||
<Menu>
|
||||
<template v-for="childItem in routeItem.children" :key="childItem.name">
|
||||
<MenuItem>
|
||||
|
@@ -74,7 +74,10 @@ export function createPermissionGuard(router: Router) {
|
||||
const routes = await permissionStore.buildRoutesAction()
|
||||
routes.forEach((route) => {
|
||||
try {
|
||||
router.addRoute(route as RouteRecordRaw)
|
||||
// 如果路径不为空 且 不是 / 开头的不添加到路由表中
|
||||
if (!route.path || route.path.startsWith('/')) {
|
||||
router.addRoute(route as RouteRecordRaw)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { AppRouteModule } from '@/router/types'
|
||||
import type { MenuModule, Menu, AppRouteRecordRaw } from '@/router/types'
|
||||
import { findPath, treeMap } from '@/utils/helper/treeHelper'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { isOutsideUrl } from '@/utils/is'
|
||||
import { getOutsideUrl } from '@/utils/is'
|
||||
import { RouteParams } from 'vue-router'
|
||||
import { toRaw } from 'vue'
|
||||
|
||||
@@ -18,8 +18,7 @@ function joinParentPath(menus: Menu[], parentPath = '') {
|
||||
for (let index = 0; index < menus.length; index++) {
|
||||
const menu = menus[index]
|
||||
// 请注意,以 / 开头的嵌套路径将被视为根路径。这允许你利用组件嵌套,而无需使用嵌套 URL。
|
||||
if (!(menu.path.startsWith('/') || isOutsideUrl(menu.path))) {
|
||||
// path doesn't start with /, nor is it a url, join parent path
|
||||
if (!(menu.path.startsWith('/') || getOutsideUrl(menu.path))) {
|
||||
// 路径不以 / 开头,也不是外部打开的路径,加入父路径
|
||||
menu.path = `${parentPath}/${menu.path}`
|
||||
}
|
||||
@@ -33,7 +32,6 @@ function joinParentPath(menus: Menu[], parentPath = '') {
|
||||
* 解析菜单模块
|
||||
*/
|
||||
export function transformMenuModule(menuModule: MenuModule): Menu {
|
||||
// const { menu } = menuModule
|
||||
const { menu } = menuModule
|
||||
const menuList = [menu]
|
||||
|
||||
@@ -48,10 +46,9 @@ export function transformRouteToMenu(routeModList: AppRouteModule[], routerMappi
|
||||
// 借助 lodash 深拷贝
|
||||
const cloneRouteModList = cloneDeep(routeModList)
|
||||
const routeList: AppRouteRecordRaw[] = []
|
||||
|
||||
// 对路由项进行修改
|
||||
cloneRouteModList.forEach((item) => {
|
||||
if (routerMapping && item.meta.hideChildrenInMenu && typeof item.redirect === 'string') {
|
||||
if (routerMapping && item.meta.hideChildrenMenu && typeof item.redirect === 'string') {
|
||||
item.path = item.redirect
|
||||
}
|
||||
|
||||
@@ -66,19 +63,19 @@ export function transformRouteToMenu(routeModList: AppRouteModule[], routerMappi
|
||||
const list = treeMap(routeList, {
|
||||
conversion: (node: AppRouteRecordRaw) => {
|
||||
const { meta: { title, hideMenu = false } = {} } = node
|
||||
|
||||
return {
|
||||
...(node.meta || {}),
|
||||
meta: node.meta,
|
||||
targetOutside: node.targetOutside,
|
||||
fullScreen: node.fullScreen,
|
||||
name: title,
|
||||
hideMenu,
|
||||
path: node.path,
|
||||
path: getPath(node),
|
||||
...(node.redirect ? { redirect: node.redirect } : {}),
|
||||
}
|
||||
},
|
||||
})
|
||||
// 路径处理
|
||||
joinParentPath(list)
|
||||
return cloneDeep(list)
|
||||
}
|
||||
|
||||
@@ -106,3 +103,26 @@ export function configureDynamicParamsMenu(menu: Menu, params: RouteParams) {
|
||||
// children
|
||||
menu.children?.forEach((item) => configureDynamicParamsMenu(item, params))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单路径
|
||||
* @param node
|
||||
*/
|
||||
function getPath(node: AppRouteRecordRaw) {
|
||||
// 是否从外部打开
|
||||
let path = node.path
|
||||
if (node?.targetOutside) {
|
||||
path = `outside://${node.path}`
|
||||
}
|
||||
// 全屏打开
|
||||
if (node?.fullScreen) {
|
||||
if (getOutsideUrl(node.path)) {
|
||||
path = `${node.path}?onlytab=1&__full__`
|
||||
} else {
|
||||
path = `outside://${node.path}?onlytab=1&__full__`
|
||||
}
|
||||
}
|
||||
//
|
||||
node.path = '/'
|
||||
return path
|
||||
}
|
||||
|
@@ -37,13 +37,16 @@ const staticMenus: Menu[] = []
|
||||
})()
|
||||
|
||||
/**
|
||||
* 获取菜单
|
||||
* 获取菜单数据
|
||||
*/
|
||||
async function getAsyncMenus() {
|
||||
const permissionStore = usePermissionStore()
|
||||
return permissionStore.getBackMenuList.filter((item) => !item.meta?.hideMenu && !item.hideMenu)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单数据
|
||||
*/
|
||||
export const getMenus = async (): Promise<Menu[]> => {
|
||||
const menus = await getAsyncMenus()
|
||||
if (isRoleMode()) {
|
||||
@@ -74,7 +77,7 @@ export async function getShallowMenus(): Promise<Menu[]> {
|
||||
export async function getChildrenMenus(parentPath: string) {
|
||||
const menus = await getMenus()
|
||||
const parent = menus.find((item) => item.path === parentPath)
|
||||
if (!parent || !parent.children || !!parent?.meta?.hideChildrenInMenu) {
|
||||
if (!parent || !parent.children || !!parent?.meta?.hideChildrenMenu) {
|
||||
return [] as Menu[]
|
||||
}
|
||||
if (isRoleMode()) {
|
||||
|
@@ -8,7 +8,7 @@ const about: AppRouteModule = {
|
||||
component: LAYOUT,
|
||||
redirect: '/about/index',
|
||||
meta: {
|
||||
hideChildrenInMenu: true,
|
||||
hideChildrenMenu: true,
|
||||
icon: 'simple-icons:aboutdotme',
|
||||
title: '关于',
|
||||
orderNo: 100000,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { AppRouteModule } from '@/router/types'
|
||||
import { LAYOUT } from '@/router/constant'
|
||||
|
||||
const IFrame = () => import('@/views/sys/iframe/FrameBlank.vue')
|
||||
/**
|
||||
* 位于主框架内的页面, 通常需要登录
|
||||
*/
|
||||
@@ -20,7 +21,16 @@ export const INTERNAL: AppRouteModule = {
|
||||
// meta: { title: '个人设置' },
|
||||
// },
|
||||
{
|
||||
path: '/about/index',
|
||||
path: '/antv',
|
||||
name: 'Antv',
|
||||
component: IFrame,
|
||||
meta: {
|
||||
frameSrc: 'https://www.antdv.com/docs/vue/introduce-cn/',
|
||||
title: 'antv',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'AboutPage',
|
||||
component: () => import('@/views/sys/about/index.vue'),
|
||||
meta: { title: '关于' },
|
||||
@@ -45,25 +55,7 @@ export const OUTSIDE: AppRouteModule = {
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO 临时功能开发
|
||||
*/
|
||||
export const TEMP: AppRouteModule = {
|
||||
path: '/temp',
|
||||
name: 'PROJECT_TEMP',
|
||||
component: LAYOUT,
|
||||
meta: { title: '临时功能' },
|
||||
children: [
|
||||
{
|
||||
path: 'dict',
|
||||
name: 'Dict',
|
||||
component: () => import('@/views/baseapi/dict/DictList.vue'),
|
||||
meta: { title: '字典' },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目基础路由
|
||||
*/
|
||||
export const PROJECT_BASE = [INTERNAL, OUTSIDE, TEMP]
|
||||
export const PROJECT_BASE = [INTERNAL, OUTSIDE]
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import type { RouteRecordRaw, RouteMeta } from 'vue-router'
|
||||
import { RoleEnum } from '@/enums/roleEnum'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export type Component<T = any> =
|
||||
@@ -12,6 +11,7 @@ export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
|
||||
name: string
|
||||
meta: RouteMeta
|
||||
targetOutside?: boolean
|
||||
fullScreen?: boolean
|
||||
redirect?: string
|
||||
iframeUrl?: string
|
||||
component?: Component | string
|
||||
@@ -36,7 +36,6 @@ export interface Menu {
|
||||
|
||||
path: string
|
||||
|
||||
// path contains param, auto assignment.
|
||||
paramPath?: string
|
||||
|
||||
disabled?: boolean
|
||||
@@ -45,12 +44,14 @@ export interface Menu {
|
||||
|
||||
orderNo?: number
|
||||
|
||||
roles?: RoleEnum[]
|
||||
|
||||
meta?: Partial<RouteMeta>
|
||||
|
||||
tag?: MenuTag
|
||||
|
||||
targetOutside?: boolean
|
||||
|
||||
fullScreen?: boolean
|
||||
|
||||
hideMenu?: boolean
|
||||
}
|
||||
|
||||
|
@@ -19,7 +19,7 @@ const setting: ProjectConfig = {
|
||||
// 是否显示主题切换按钮
|
||||
showDarkModeToggle: true,
|
||||
|
||||
// `Settings` 按钮位置
|
||||
// 按钮位置
|
||||
settingButtonPosition: SettingButtonPositionEnum.AUTO,
|
||||
|
||||
// 权限模式
|
||||
@@ -34,7 +34,7 @@ const setting: ProjectConfig = {
|
||||
// 颜色
|
||||
themeColor: APP_PRESET_COLOR_LIST[0],
|
||||
|
||||
// 灰度迷失
|
||||
// 灰度模式
|
||||
grayMode: false,
|
||||
|
||||
// 色弱模式
|
||||
@@ -50,7 +50,7 @@ const setting: ProjectConfig = {
|
||||
showLogo: true,
|
||||
|
||||
// 是否显示页脚
|
||||
showFooter: false,
|
||||
showFooter: true,
|
||||
|
||||
// 页头设置
|
||||
headerSetting: {
|
||||
@@ -65,7 +65,7 @@ const setting: ProjectConfig = {
|
||||
// 是否显示全屏按钮
|
||||
showFullScreen: true,
|
||||
// 是否显示文档按钮
|
||||
showDoc: true,
|
||||
showDoc: false,
|
||||
// 是否显示通知按钮
|
||||
showNotice: false,
|
||||
// 是否显示搜索按钮
|
||||
|
@@ -1,8 +1,8 @@
|
||||
// github repo url
|
||||
export const GITHUB_URL = 'https://github.com/anncwb/vue-vben-admin'
|
||||
export const GITHUB_URL = 'https://gitee.com/dromara/dax-pay'
|
||||
|
||||
// vue-vben-admin-next-doc
|
||||
export const DOC_URL = 'https://doc.vvbin.cn/'
|
||||
export const DOC_URL = 'https://doc.daxpay.cn/'
|
||||
|
||||
// site url
|
||||
export const SITE_URL = 'https://vben.vvbin.cn/'
|
||||
export const SITE_URL = 'https://doc.daxpay.cn/'
|
||||
|
@@ -90,10 +90,6 @@ export const usePermissionStore = defineStore({
|
||||
return menus
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取菜单
|
||||
*/
|
||||
|
||||
/**
|
||||
* 转换路由为系统中的菜单
|
||||
*/
|
||||
@@ -112,7 +108,7 @@ export const usePermissionStore = defineStore({
|
||||
title: o.title,
|
||||
icon: o.icon,
|
||||
hideMenu: o.hidden,
|
||||
hideChildrenInMenu: o.hideChildrenInMenu,
|
||||
hideChildrenMenu: o.hideChildrenMenu,
|
||||
ignoreKeepAlive: !o.keepAlive,
|
||||
},
|
||||
children: this.convertMenus(o.children),
|
||||
@@ -187,6 +183,7 @@ export const usePermissionStore = defineStore({
|
||||
}
|
||||
// 后端获取的菜单如果为空, 不进行处理
|
||||
if (routeList) {
|
||||
// 将后端对象变成路由对象
|
||||
routeList = transformObjToRoute(routeList)
|
||||
// 后台路由到菜单结构
|
||||
const backMenuList = transformRouteToMenu(routeList)
|
||||
|
72
src/utils/dataUtil.ts
Normal file
72
src/utils/dataUtil.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { unref } from 'vue'
|
||||
import { LabeledValue } from 'ant-design-vue/lib/select'
|
||||
|
||||
/**
|
||||
* 树形数据转换
|
||||
* @param data 树状数据
|
||||
* @param key 值名称/主键名称
|
||||
* @param title 标题名称
|
||||
* @param children 孩子列表字段名
|
||||
* @returns {*[]}
|
||||
*/
|
||||
export function treeDataTranslate(data, key = 'value', title = 'title', children = 'children') {
|
||||
const temp = [] as Tree[]
|
||||
if (!data) {
|
||||
return []
|
||||
}
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const p = {
|
||||
key: data[i][key],
|
||||
title: data[i][title],
|
||||
value: String(data[i][key]),
|
||||
raw: data[i],
|
||||
children: [],
|
||||
} as Tree
|
||||
if (data[i][children] && data[i][children].length > 0) {
|
||||
p.children = treeDataTranslate(data[i][children], key, title, children)
|
||||
}
|
||||
temp.push(p)
|
||||
}
|
||||
return temp
|
||||
}
|
||||
export interface Tree {
|
||||
key: string
|
||||
title: string
|
||||
value: unknown
|
||||
// 关联原始数据
|
||||
raw: unknown
|
||||
children?: Tree[] | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 从列表中获取指定的对象
|
||||
* @param list 列表
|
||||
* @param fieldValue 要查询的字段值
|
||||
* @param fieldName 字段名称
|
||||
*/
|
||||
export function findOneByField(list: any, fieldValue: object, fieldName: string) {
|
||||
const item = unref(list)?.filter((o) => {
|
||||
return o[fieldName] === fieldValue
|
||||
})
|
||||
if (item && item.length > 0) {
|
||||
return item[0]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换成下拉框数据格式
|
||||
* @param list 数据列表
|
||||
* @param labelName label名称
|
||||
* @param valueName 值名称
|
||||
*/
|
||||
export function dropdownTranslate(list: any, labelName = 'name', valueName = 'code') {
|
||||
return unref(list)?.map((o) => {
|
||||
return {
|
||||
label: o[labelName],
|
||||
value: o[valueName],
|
||||
} as LabeledValue
|
||||
})
|
||||
}
|
@@ -166,7 +166,6 @@ export function forEach<T = any>(
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Extract tree specified structure
|
||||
* @description: 提取树指定结构
|
||||
*/
|
||||
export function treeMap<T = any>(treeData: T[], opt: { children?: string; conversion: Fn }): T[] {
|
||||
@@ -174,7 +173,6 @@ export function treeMap<T = any>(treeData: T[], opt: { children?: string; conver
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Extract tree specified structure
|
||||
* @description: 提取树指定结构
|
||||
*/
|
||||
export function treeMapEach(
|
||||
|
@@ -82,18 +82,19 @@ export function isUrl(path: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否从外部打开的链接
|
||||
* 是否从外部打开的链接, 如果是网址
|
||||
* 如果是则返回打开的地址, 否则返回空字符串
|
||||
* @return 打开的地址, 为空字符则说明无法打开
|
||||
*/
|
||||
export function isOutsideUrl(path: string): string {
|
||||
export function getOutsideUrl(path: string): string {
|
||||
if (isUrl(path)) {
|
||||
return path
|
||||
}
|
||||
if (path.startsWith('outside://')) {
|
||||
// 转换成项目内路由地址
|
||||
const routerPath = path.substring(10)
|
||||
const to = router.resolve(routerPath)
|
||||
return to.href
|
||||
return path.substring(10)
|
||||
// const to = router.resolve(routerPath)
|
||||
// return to.href
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
@@ -82,13 +82,13 @@ export function existsByCodeNotId(code, id) {
|
||||
*/
|
||||
export interface Dict extends BaseEntity {
|
||||
// 编码
|
||||
code: string
|
||||
code?: string
|
||||
// 名称
|
||||
name: string
|
||||
name?: string
|
||||
// 是否启用
|
||||
enable?: boolean
|
||||
// 分类标签
|
||||
groupTag: string
|
||||
groupTag?: string
|
||||
// 备注
|
||||
remark: string
|
||||
remark?: string
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
:loading="confirmLoading"
|
||||
:width="modalWidth"
|
||||
:title="title"
|
||||
:visible="visible"
|
||||
:open="visible"
|
||||
:mask-closable="showable"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
@@ -76,9 +76,9 @@
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
modalWidth,
|
||||
visible,
|
||||
title,
|
||||
confirmLoading,
|
||||
visible,
|
||||
showable,
|
||||
formEditType,
|
||||
} = useFormEdit()
|
||||
|
@@ -4,7 +4,7 @@
|
||||
:loading="confirmLoading"
|
||||
:width="modalWidth"
|
||||
:title="title"
|
||||
:visible="visible"
|
||||
:open="visible"
|
||||
:mask-closable="showable"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
@@ -60,7 +60,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, ref } from 'vue'
|
||||
import { nextTick, reactive, ref, unref } from "vue";
|
||||
import useFormEdit from '@/hooks/bootx/useFormEdit'
|
||||
import { add, get, update, existsByCode, existsByCodeNotId, DictItem } from './DictItem.api'
|
||||
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
|
||||
@@ -108,8 +108,8 @@
|
||||
function init(id, editType: FormEditType, dict: Dict) {
|
||||
initFormEditType(editType)
|
||||
resetForm()
|
||||
form.dictId = dict.id as number
|
||||
form.dictCode = dict.code
|
||||
form.dictId = unref(dict).id as number
|
||||
form.dictCode = unref(dict).code
|
||||
getInfo(id, editType)
|
||||
}
|
||||
// 获取信息
|
||||
@@ -148,7 +148,7 @@
|
||||
// 重置表单的校验
|
||||
function resetForm() {
|
||||
nextTick(() => {
|
||||
formRef?.resetFields()
|
||||
formRef.value?.resetFields()
|
||||
})
|
||||
}
|
||||
defineExpose({
|
||||
|
@@ -4,7 +4,7 @@
|
||||
v-bind="$attrs"
|
||||
title="字典列表"
|
||||
width="60%"
|
||||
:visible="visible"
|
||||
:open="visible"
|
||||
@close="visible = false"
|
||||
>
|
||||
<vxe-toolbar ref="xToolbar" custom :refresh="{ queryMethod: queryPage }">
|
||||
@@ -14,7 +14,7 @@
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-toolbar>
|
||||
<vxe-table row-id="id" ref="xTable" :data="pagination.records" :loading="loading">
|
||||
<vxe-table ey-field="id" ref="xTable" :data="pagination.records" :loading="loading">
|
||||
<vxe-column type="seq" width="60" />
|
||||
<vxe-column field="code" title="字典项编码" />
|
||||
<vxe-column field="name" title="字典项名称" />
|
||||
@@ -51,12 +51,12 @@
|
||||
:total="pagination.total"
|
||||
@page-change="handleTableChange"
|
||||
/>
|
||||
<dict-item-edit ref="dictItemEdit" @ok="queryPage" />
|
||||
<DictItemEdit ref="dictItemEdit" @ok="queryPage" />
|
||||
</basic-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { nextTick, ref, unref } from "vue";
|
||||
import { del, page } from './DictItem.api'
|
||||
import useTablePage from '@/hooks/bootx/useTablePage'
|
||||
import DictItemEdit from './DictItemEdit.vue'
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
// 查询条件
|
||||
let visible = ref(false)
|
||||
let dictInfo = ref<Dict>()
|
||||
let dictInfo: Dict
|
||||
|
||||
const xTable = ref<VxeTableInstance>()
|
||||
const xToolbar = ref<VxeToolbarInstance>()
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
function init(dict) {
|
||||
visible.value = true
|
||||
dictInfo.value = dict
|
||||
dictInfo = unref(dict)
|
||||
queryPage()
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
page({
|
||||
...model.queryParam,
|
||||
...pages,
|
||||
dictId: dictInfo.value?.id,
|
||||
dictId: dictInfo?.id,
|
||||
}).then(({ data }) => {
|
||||
pageQueryResHandel(data)
|
||||
})
|
||||
|
@@ -81,6 +81,13 @@
|
||||
import { QueryField, STRING } from '@/components/Bootx/Query/Query'
|
||||
import DictItemList from './DictItemList.vue'
|
||||
|
||||
interface RowVO {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
const tableData = ref<RowVO[]>([{ id: 10001, name: 'Test1' }])
|
||||
|
||||
// 使用hooks
|
||||
const {
|
||||
handleTableChange,
|
||||
@@ -91,7 +98,7 @@
|
||||
model,
|
||||
loading,
|
||||
} = useTablePage(queryPage)
|
||||
const { notification, createMessage } = useMessage()
|
||||
const { createMessage } = useMessage()
|
||||
|
||||
// 查询条件
|
||||
const fields = [
|
||||
@@ -100,17 +107,17 @@
|
||||
{ field: 'groupTag', type: STRING, name: '分组标签', placeholder: '请输入分组标签' },
|
||||
] as QueryField[]
|
||||
|
||||
const xTable = $ref<VxeTableInstance>()
|
||||
const xToolbar = $ref<VxeToolbarInstance>()
|
||||
const dictEdit = $ref<any>()
|
||||
const dictItemList = $ref<any>()
|
||||
const xTable = ref<VxeTableInstance>()
|
||||
const xToolbar = ref<VxeToolbarInstance>()
|
||||
const dictEdit = ref<any>()
|
||||
const dictItemList = ref<any>()
|
||||
|
||||
onMounted(() => {
|
||||
vxeBind()
|
||||
queryPage()
|
||||
})
|
||||
function vxeBind() {
|
||||
xTable?.connect(xToolbar as VxeToolbarInstance)
|
||||
xTable.value?.connect(xToolbar.value as VxeToolbarInstance)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
@@ -126,19 +133,19 @@
|
||||
}
|
||||
// 新增
|
||||
function add() {
|
||||
dictEdit.init(null, FormEditType.Add)
|
||||
dictEdit.value.init(null, FormEditType.Add)
|
||||
}
|
||||
// 查看
|
||||
function edit(record) {
|
||||
dictEdit.init(record.id, FormEditType.Edit)
|
||||
dictEdit.value.init(record.id, FormEditType.Edit)
|
||||
}
|
||||
// 查看
|
||||
function show(record) {
|
||||
dictEdit.init(record.id, FormEditType.Show)
|
||||
dictEdit.value.init(record.id, FormEditType.Show)
|
||||
}
|
||||
// 明细列表查看
|
||||
function itemList(record) {
|
||||
dictItemList.init(record)
|
||||
dictItemList.value.init(record)
|
||||
}
|
||||
|
||||
// 删除
|
||||
|
93
src/views/iam/client/Client.api.ts
Normal file
93
src/views/iam/client/Client.api.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { defHttp } from '@/utils/http/axios'
|
||||
import { PageResult, Result } from '#/axios'
|
||||
import { BaseEntity } from '#/web'
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*/
|
||||
export const page = (params) => {
|
||||
return defHttp.get<Result<PageResult<Client>>>({
|
||||
url: '/client/page',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条
|
||||
*/
|
||||
export const get = (id) => {
|
||||
return defHttp.get<Result<Client>>({
|
||||
url: '/client/findById',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*/
|
||||
export const add = (obj: Client) => {
|
||||
return defHttp.post({
|
||||
url: '/client/add',
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*/
|
||||
export const update = (obj: Client) => {
|
||||
return defHttp.post({
|
||||
url: '/client/update',
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
export const del = (id) => {
|
||||
return defHttp.delete({
|
||||
url: '/client/delete',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部
|
||||
*/
|
||||
export const findAll = () => {
|
||||
return defHttp.get<Result<Array<Client>>>({
|
||||
url: '/client/findAll',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码是否被使用
|
||||
*/
|
||||
export const existsByCode = (code: string) => {
|
||||
return defHttp.get<Result<boolean>>({
|
||||
url: '/client/existsByCode',
|
||||
method: 'GET',
|
||||
params: { code },
|
||||
})
|
||||
}
|
||||
export const existsByCodeNotId = (code: string, id) => {
|
||||
return defHttp.get<Result<boolean>>({
|
||||
url: '/client/existsByCodeNotId',
|
||||
params: { code, id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 终端
|
||||
*/
|
||||
export interface Client extends BaseEntity {
|
||||
// 编码
|
||||
code: string
|
||||
// 名称
|
||||
name: string
|
||||
// 是否系统内置
|
||||
internal: boolean
|
||||
// 描述
|
||||
remark: string
|
||||
}
|
149
src/views/iam/client/ClientEdit.vue
Normal file
149
src/views/iam/client/ClientEdit.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<basic-modal
|
||||
:loading="confirmLoading"
|
||||
v-bind="$attrs"
|
||||
:width="750"
|
||||
:title="title"
|
||||
:open="visible"
|
||||
:mask-closable="showable"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-spin :spinning="confirmLoading">
|
||||
<a-form
|
||||
class="small-from-item"
|
||||
:model="form"
|
||||
ref="formRef"
|
||||
:rules="rules"
|
||||
:label-col="labelCol"
|
||||
:wrapper-col="wrapperCol"
|
||||
>
|
||||
<a-form-item label="主键" name="id" :hidden="true">
|
||||
<a-input v-model:value="form.id" :disabled="showable" />
|
||||
</a-form-item>
|
||||
<a-form-item label="编码" validate-first name="code">
|
||||
<a-input v-model:value="form.code" :disabled="showable" placeholder="请输入编码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="名称" validate-first name="name">
|
||||
<a-input v-model:value="form.name" :disabled="showable" placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="系统内置" name="internal" v-if="!addable">
|
||||
<a-tag v-if="form.internal" color="green">是</a-tag>
|
||||
<a-tag v-else color="red">否</a-tag>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="form.remark" :disabled="showable" placeholder="请输入描述" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button key="cancel" @click="handleCancel">取消</a-button>
|
||||
<a-button
|
||||
v-if="!showable"
|
||||
key="forward"
|
||||
:loading="confirmLoading"
|
||||
type="primary"
|
||||
@click="handleOk"
|
||||
>保存</a-button
|
||||
>
|
||||
</a-space>
|
||||
</template>
|
||||
</basic-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, ref } from 'vue'
|
||||
import useFormEdit from '@/hooks/bootx/useFormEdit'
|
||||
import { add, Client, existsByCode, existsByCodeNotId, get, update } from './Client.api'
|
||||
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
|
||||
import { FormEditType } from '@/enums/formTypeEnum'
|
||||
import { useValidate } from '@/hooks/bootx/useValidate'
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue'
|
||||
|
||||
const {
|
||||
initFormEditType,
|
||||
handleCancel,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
title,
|
||||
confirmLoading,
|
||||
visible,
|
||||
addable,
|
||||
showable,
|
||||
formEditType,
|
||||
} = useFormEdit()
|
||||
const { existsByServer } = useValidate()
|
||||
|
||||
const form = ref({
|
||||
id: null,
|
||||
code: '',
|
||||
name: '',
|
||||
} as Client)
|
||||
// 校验状态
|
||||
const rules = reactive({
|
||||
code: [
|
||||
{ required: true, message: '请输入终端编码' },
|
||||
{ validator: validateCode, trigger: 'blur' },
|
||||
],
|
||||
name: [{ required: true, message: '请输入终端名称', trigger: ['blur', 'change'] }],
|
||||
} as Record<string, Rule[]>)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 事件
|
||||
const emits = defineEmits(['ok'])
|
||||
|
||||
// 入口
|
||||
function init(id, editType: FormEditType) {
|
||||
initFormEditType(editType)
|
||||
resetForm()
|
||||
getInfo(id, editType)
|
||||
}
|
||||
// 获取信息
|
||||
function getInfo(id, editType: FormEditType) {
|
||||
if ([FormEditType.Edit, FormEditType.Show].includes(editType)) {
|
||||
confirmLoading.value = true
|
||||
get(id).then(({ data }) => {
|
||||
form.value = data
|
||||
confirmLoading.value = false
|
||||
})
|
||||
} else {
|
||||
confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
// 保存
|
||||
function handleOk() {
|
||||
formRef.value?.validate().then(async () => {
|
||||
confirmLoading.value = true
|
||||
if (formEditType.value === FormEditType.Add) {
|
||||
await add(form.value)
|
||||
} else if (formEditType.value === FormEditType.Edit) {
|
||||
await update(form.value)
|
||||
}
|
||||
confirmLoading.value = false
|
||||
handleCancel()
|
||||
emits('ok')
|
||||
})
|
||||
}
|
||||
|
||||
// 重置表单的校验
|
||||
function resetForm() {
|
||||
nextTick(() => {
|
||||
formRef.value?.resetFields()
|
||||
})
|
||||
}
|
||||
|
||||
// 校验编码重复
|
||||
async function validateCode() {
|
||||
const { code, id } = form.value
|
||||
return existsByServer(code, id, formEditType, existsByCode, existsByCodeNotId)
|
||||
}
|
||||
defineExpose({
|
||||
init,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.vben-basic-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
143
src/views/iam/client/ClientList.vue
Normal file
143
src/views/iam/client/ClientList.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="m-3 p-3 pt-5 bg-white">
|
||||
<b-query
|
||||
:query-params="model.queryParam"
|
||||
:fields="fields"
|
||||
@query="queryPage"
|
||||
@reset="resetQueryParams"
|
||||
/>
|
||||
</div>
|
||||
<div class="m-3 p-3 bg-white">
|
||||
<vxe-toolbar ref="xToolbar" custom :refresh="{ queryMethod: queryPage }">
|
||||
<template #buttons>
|
||||
<a-space>
|
||||
<a-button type="primary" pre-icon="ant-design:plus-outlined" @click="add"
|
||||
>新建</a-button
|
||||
>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-toolbar>
|
||||
<vxe-table ref="xTable" key-field="id" :data="pagination.records" :loading="loading">
|
||||
<vxe-column type="seq" width="60" />
|
||||
<vxe-column field="code" title="编码" />
|
||||
<vxe-column field="name" title="名称" />
|
||||
<vxe-column field="internal" title="系统内置">
|
||||
<template #default="{ row }">
|
||||
<a-tag v-if="row.internal" color="green">是</a-tag>
|
||||
<a-tag v-else color="red">否</a-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="remark" title="备注" />
|
||||
<vxe-column field="createTime" title="创建时间" />
|
||||
<vxe-column fixed="right" width="150" :showOverflow="false" title="操作">
|
||||
<template #default="{ row }">
|
||||
<span>
|
||||
<a-link @click="show(row)">查看</a-link>
|
||||
</span>
|
||||
<a-divider type="vertical" />
|
||||
<span>
|
||||
<a-link @click="edit(row)">编辑</a-link>
|
||||
</span>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
:disabled="row.internal"
|
||||
title="是否删除"
|
||||
@confirm="remove(row)"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<a-link v-if="!row.internal" style="color: red">删除</a-link>
|
||||
<a-link v-else disabled>删除</a-link>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
<vxe-pager
|
||||
size="medium"
|
||||
:loading="loading"
|
||||
:current-page="pagination.current"
|
||||
:page-size="pagination.size"
|
||||
:total="pagination.total"
|
||||
@page-change="handleTableChange"
|
||||
/>
|
||||
<ClientEdit ref="clientEdit" @ok="queryPage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { del, page } from './Client.api'
|
||||
import useTablePage from '@/hooks/bootx/useTablePage'
|
||||
import ClientEdit from './ClientEdit.vue'
|
||||
import BQuery from '@/components/Bootx/Query/BQuery.vue'
|
||||
import { STRING } from '@/components/Bootx/Query/Query'
|
||||
import { FormEditType } from '@/enums/formTypeEnum'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { VxeTableInstance, VxeToolbarInstance } from 'vxe-table'
|
||||
import ALink from '@/components/Link/Link.vue'
|
||||
|
||||
// 使用hooks
|
||||
const {
|
||||
handleTableChange,
|
||||
pageQueryResHandel,
|
||||
resetQueryParams,
|
||||
pagination,
|
||||
pages,
|
||||
model,
|
||||
loading,
|
||||
} = useTablePage(queryPage)
|
||||
// 查询条件
|
||||
const fields = [
|
||||
{ field: 'code', formType: STRING, name: '编码', placeholder: '请输入终端编码' },
|
||||
{ field: 'name', formType: STRING, name: '名称', placeholder: '请输入终端名称' },
|
||||
]
|
||||
const xTable = ref<VxeTableInstance>()
|
||||
const xToolbar = ref<VxeToolbarInstance>()
|
||||
const clientEdit: any = ref()
|
||||
|
||||
onMounted(() => {
|
||||
vxeBind()
|
||||
queryPage()
|
||||
})
|
||||
|
||||
function vxeBind() {
|
||||
xTable.value?.connect(xToolbar.value as VxeToolbarInstance)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
function queryPage() {
|
||||
loading.value = true
|
||||
page({
|
||||
...model.queryParam,
|
||||
...pages,
|
||||
}).then(({ data }) => {
|
||||
pageQueryResHandel(data)
|
||||
})
|
||||
return Promise.resolve()
|
||||
}
|
||||
// 新增
|
||||
function add() {
|
||||
clientEdit.value.init(null, FormEditType.Add)
|
||||
}
|
||||
// 查看
|
||||
function edit(record) {
|
||||
clientEdit.value.init(record.id, FormEditType.Edit)
|
||||
}
|
||||
// 查看
|
||||
function show(record) {
|
||||
clientEdit.value.init(record.id, FormEditType.Show)
|
||||
}
|
||||
|
||||
// 删除
|
||||
const { notification } = useMessage()
|
||||
function remove(record) {
|
||||
del(record.id).then(() => {
|
||||
notification.success({ message: '删除成功' })
|
||||
queryPage()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
104
src/views/iam/perm/code/PermCode.api.ts
Normal file
104
src/views/iam/perm/code/PermCode.api.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { defHttp } from '@/utils/http/axios'
|
||||
import { Result } from '#/axios'
|
||||
import { BaseEntity } from '#/web'
|
||||
import { unref } from 'vue'
|
||||
|
||||
/**
|
||||
* 权限码树列表
|
||||
*/
|
||||
export const codeTree = () => {
|
||||
return defHttp.get<Result<Array<PermCode>>>({
|
||||
url: '/perm/code/tree',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限码目录树
|
||||
*/
|
||||
export const catalogTree = () => {
|
||||
return defHttp.get<Result<Array<PermCode>>>({
|
||||
url: '/perm/code/catalogTree',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条
|
||||
*/
|
||||
export const get = (id) => {
|
||||
return defHttp.get<Result<PermCode>>({
|
||||
url: '/perm/code/findById',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*/
|
||||
export const add = (obj: PermCode) => {
|
||||
return defHttp.post({
|
||||
url: '/perm/code/add',
|
||||
data: unref(obj),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*/
|
||||
export const update = (obj: PermCode) => {
|
||||
return defHttp.post({
|
||||
url: '/perm/code/update',
|
||||
data: unref(obj),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
export const del = (id) => {
|
||||
return defHttp.delete({
|
||||
url: '/perm/code/delete',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码是否被使用
|
||||
*/
|
||||
export const existsByCode = (code: string) => {
|
||||
return defHttp.get<Result<boolean>>({
|
||||
url: '/perm/code/existsByCode',
|
||||
method: 'GET',
|
||||
params: { code },
|
||||
})
|
||||
}
|
||||
export const existsByCodeNotId = (code: string, id) => {
|
||||
return defHttp.get<Result<boolean>>({
|
||||
url: '/perm/code/existsByCodeNotId',
|
||||
params: { code, id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限码
|
||||
*/
|
||||
export interface PermCode extends BaseEntity {
|
||||
// 权限码权限编码
|
||||
code: string
|
||||
// 权限码名称
|
||||
name: string
|
||||
// 父id
|
||||
pid?: string
|
||||
// 描述
|
||||
remark?: string
|
||||
// 是否为子节点
|
||||
leaf: boolean
|
||||
// 显示标题, 纯显示用
|
||||
title?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限码树
|
||||
*/
|
||||
export interface PermCodeTree extends PermCode {
|
||||
children: PermCodeTree[]
|
||||
}
|
190
src/views/iam/perm/code/PermCodeEdit.vue
Normal file
190
src/views/iam/perm/code/PermCodeEdit.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<basic-modal
|
||||
v-bind="$attrs"
|
||||
:width="modalWidth"
|
||||
:loading="confirmLoading"
|
||||
:title="title"
|
||||
:open="visible"
|
||||
:mask-closable="false"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
class="small-from-item"
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="labelCol"
|
||||
:wrapper-col="wrapperCol"
|
||||
>
|
||||
<a-form-item name="id" :hidden="true">
|
||||
<a-input v-model="form.id" :disabled="showable" />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" name="leaf">
|
||||
<a-radio-group :disabled="!addable" v-model:value="form.leaf">
|
||||
<a-radio :value="false">目录</a-radio>
|
||||
<a-radio :value="true">权限码</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="上级目录" name="pid">
|
||||
<a-tree-select
|
||||
allowClear
|
||||
style="width: 100%"
|
||||
:tree-data="treeData"
|
||||
v-model:value="form.pid"
|
||||
placeholder="请选择上级目录"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.leaf" validate-first label="权限码" name="code">
|
||||
<a-input v-model:value="form.code" :disabled="showable" placeholder="请输入权限标识" />
|
||||
</a-form-item>
|
||||
<a-form-item label="名称" name="name">
|
||||
<a-input v-model:value="form.name" :disabled="showable" placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="form.remark" :disabled="showable" placeholder="请输入备注" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button key="cancel" @click="handleCancel">取消</a-button>
|
||||
<a-button
|
||||
v-if="!showable"
|
||||
key="forward"
|
||||
:loading="confirmLoading"
|
||||
type="primary"
|
||||
@click="handleOk"
|
||||
>保存</a-button
|
||||
>
|
||||
</a-space>
|
||||
</template>
|
||||
</basic-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BasicModal } from '@/components/Modal'
|
||||
import useFormEdit from '@/hooks/bootx/useFormEdit'
|
||||
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
|
||||
import {
|
||||
add,
|
||||
get,
|
||||
PermCode,
|
||||
update,
|
||||
existsByCode,
|
||||
existsByCodeNotId,
|
||||
catalogTree,
|
||||
} from './PermCode.api'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { FormEditType } from '@/enums/formTypeEnum'
|
||||
import { useValidate } from '@/hooks/bootx/useValidate'
|
||||
import { treeDataTranslate } from '@/utils/dataUtil'
|
||||
|
||||
const { existsByServer } = useValidate()
|
||||
|
||||
const treeData = ref<any[]>()
|
||||
const {
|
||||
initFormEditType,
|
||||
handleCancel,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
modalWidth,
|
||||
title,
|
||||
confirmLoading,
|
||||
showable,
|
||||
addable,
|
||||
visible,
|
||||
formEditType,
|
||||
} = useFormEdit()
|
||||
|
||||
// 表单
|
||||
const formRef = ref<FormInstance>()
|
||||
let form = ref({
|
||||
id: null,
|
||||
code: '',
|
||||
name: '',
|
||||
remark: '',
|
||||
leaf: false,
|
||||
} as PermCode)
|
||||
// 校验
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
code: [
|
||||
{ required: form.value.leaf, message: '权限编码必填' },
|
||||
{ validator: validateCode, trigger: 'blur' },
|
||||
],
|
||||
name: [{ required: true, message: '名称必填', trigger: ['blur', 'change'] }],
|
||||
} as Record<string, Rule[]>
|
||||
})
|
||||
// 事件
|
||||
const emits = defineEmits(['ok'])
|
||||
/**
|
||||
* 入口
|
||||
*/
|
||||
function init(id, editType: FormEditType, row: PermCode) {
|
||||
initRoleTree()
|
||||
initFormEditType(editType)
|
||||
resetForm()
|
||||
getInfo(id, editType, row)
|
||||
}
|
||||
/**
|
||||
* 查询权限目录树
|
||||
*/
|
||||
function initRoleTree() {
|
||||
catalogTree().then((res) => {
|
||||
treeData.value = treeDataTranslate(res.data, 'id', 'name')
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 获取信息
|
||||
*/
|
||||
function getInfo(id, editType: FormEditType, row: PermCode) {
|
||||
if ([FormEditType.Edit, FormEditType.Show].includes(editType)) {
|
||||
get(id).then(({ data }) => {
|
||||
form.value = data
|
||||
confirmLoading.value = false
|
||||
})
|
||||
} else {
|
||||
console.log(row)
|
||||
if (row) {
|
||||
form.value.pid = row.id as string
|
||||
}
|
||||
confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 校验编码重复
|
||||
*/
|
||||
async function validateCode() {
|
||||
const { code, id } = form.value
|
||||
return existsByServer(code, id, formEditType, existsByCode, existsByCodeNotId)
|
||||
}
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
function handleOk() {
|
||||
formRef.value?.validate().then(async () => {
|
||||
confirmLoading.value = true
|
||||
if (formEditType.value === FormEditType.Add) {
|
||||
await add(form.value)
|
||||
} else if (formEditType.value === FormEditType.Edit) {
|
||||
await update(form.value)
|
||||
}
|
||||
confirmLoading.value = false
|
||||
handleCancel()
|
||||
emits('ok')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表单的校验
|
||||
*/
|
||||
function resetForm() {
|
||||
nextTick(() => {
|
||||
formRef.value?.resetFields()
|
||||
})
|
||||
}
|
||||
defineExpose({
|
||||
init,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
188
src/views/iam/perm/code/PermCodeList.vue
Normal file
188
src/views/iam/perm/code/PermCodeList.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="m-3 p-3 pt-5 bg-white">
|
||||
<a-form class="page-query">
|
||||
<a-row :gutter="10">
|
||||
<a-col :md="8" :sm="24">
|
||||
<a-form-item label="查询">
|
||||
<a-input-search
|
||||
v-model:value="searchName"
|
||||
@search="search"
|
||||
@keyup.enter="search"
|
||||
allow-clear
|
||||
placeholder="请输入名称、编码查询"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="m-3 p-3 bg-white">
|
||||
<vxe-toolbar ref="xToolbar" custom zoom :refresh="{ queryMethod: queryPage }">
|
||||
<template #buttons>
|
||||
<a-space>
|
||||
<a-button type="primary" pre-icon="ant-design:plus-outlined" @click="add()">
|
||||
新建
|
||||
</a-button>
|
||||
<a-button @click="allTreeExpand(true)">展开所有</a-button>
|
||||
<a-button @click="allTreeExpand(false)">关闭所有</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-toolbar>
|
||||
<vxe-table
|
||||
resizable
|
||||
:height="650"
|
||||
ref="xTable"
|
||||
border="inner"
|
||||
:stripe="false"
|
||||
:loading="loading"
|
||||
:tree-config="{ childrenField: 'children' }"
|
||||
:data="tableData"
|
||||
>
|
||||
<vxe-column field="name" title="名称" tree-node />
|
||||
<vxe-column field="code" title="权限码" />
|
||||
<vxe-column title="操作" fixed="right" width="220" :showOverflow="false">
|
||||
<template #default="{ row }">
|
||||
<a href="javascript:" @click="show(row)">查看</a>
|
||||
<template v-if="String(row.menuType) !== '2'">
|
||||
<a-divider type="vertical" />
|
||||
<a href="javascript:" v-if="!row.admin" @click="edit(row)">编辑</a>
|
||||
<a href="javascript:" v-else disabled>编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-dropdown>
|
||||
<a> 更多 <icon icon="ant-design:down-outlined" :size="12" /> </a>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
<a @click="addChildren(row)">添加下级</a>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a
|
||||
href="javascript:"
|
||||
v-if="!row.admin"
|
||||
@click="remove(row)"
|
||||
style="color: red"
|
||||
>删除</a
|
||||
>
|
||||
<a href="javascript:" v-else disabled>删除</a>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
<PermCodeEdit ref="codeEdit" @ok="queryPage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { Client, findAll } from '@/views/iam/client/Client.api'
|
||||
import XEUtils from 'xe-utils'
|
||||
import { codeTree, PermCode, del } from './PermCode.api'
|
||||
import { FormEditType } from '@/enums/formTypeEnum'
|
||||
import PermCodeEdit from './PermCodeEdit.vue'
|
||||
import { VxeTableInstance, VxeToolbarInstance } from 'vxe-table'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { Icon } from '@/components/Icon'
|
||||
|
||||
const { createConfirm, notification } = useMessage()
|
||||
|
||||
const searchName = ref()
|
||||
const loading = ref(false)
|
||||
const treeExpand = ref(false)
|
||||
const clients = ref([] as Client[])
|
||||
const remoteTableData = ref([] as PermCode[])
|
||||
const tableData = ref([] as PermCode[])
|
||||
const xTable = ref<VxeTableInstance>()
|
||||
const xToolbar = ref<VxeToolbarInstance>()
|
||||
let codeEdit = ref<any>()
|
||||
|
||||
onMounted(() => {
|
||||
vxeBind()
|
||||
initClients()
|
||||
queryPage()
|
||||
})
|
||||
|
||||
function vxeBind() {
|
||||
xTable.value?.connect(xToolbar.value as VxeToolbarInstance)
|
||||
}
|
||||
|
||||
async function initClients() {
|
||||
const { data } = await findAll()
|
||||
clients.value = data
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
async function queryPage() {
|
||||
loading.value = true
|
||||
const { data } = await codeTree()
|
||||
remoteTableData.value = data
|
||||
search()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function add() {
|
||||
codeEdit.value.init(null, FormEditType.Add)
|
||||
}
|
||||
function edit(record: PermCode) {
|
||||
codeEdit.value.init(record.id, FormEditType.Edit)
|
||||
}
|
||||
function show(record: PermCode) {
|
||||
codeEdit.value.init(record.id, FormEditType.Show)
|
||||
}
|
||||
function addChildren(row: PermCode) {
|
||||
codeEdit.value.init(null, FormEditType.Add, row)
|
||||
}
|
||||
function remove(record: PermCode) {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '警告',
|
||||
content: '是否删除该条数据',
|
||||
onOk: () => {
|
||||
del(record.id).then(() => {
|
||||
notification.success({ message: '删除成功' })
|
||||
queryPage()
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
function search() {
|
||||
const search = XEUtils.toValueString(searchName.value).trim().toLowerCase()
|
||||
if (search) {
|
||||
const searchProps = ['name', 'title', 'path', 'component']
|
||||
tableData.value = XEUtils.searchTree(remoteTableData.value, (item) =>
|
||||
searchProps.some(
|
||||
(key) => XEUtils.toValueString(item[key]).toLowerCase().indexOf(search) > -1,
|
||||
),
|
||||
)
|
||||
// 搜索状态默认展开
|
||||
treeExpand.value = true
|
||||
} else {
|
||||
tableData.value = remoteTableData.value
|
||||
}
|
||||
nextTick(() => {
|
||||
xTable.value?.setAllTreeExpand(treeExpand.value)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 展开or关闭
|
||||
*/
|
||||
function allTreeExpand(value) {
|
||||
nextTick(() => {
|
||||
xTable.value?.setAllTreeExpand(treeExpand.value)
|
||||
})
|
||||
treeExpand.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
103
src/views/iam/perm/menu/Menu.api.ts
Normal file
103
src/views/iam/perm/menu/Menu.api.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { defHttp } from '@/utils/http/axios'
|
||||
import { Result } from '#/axios'
|
||||
import { BaseEntity } from '#/web'
|
||||
import { unref } from 'vue'
|
||||
|
||||
/**
|
||||
* 菜单树列表
|
||||
*/
|
||||
export const menuTree = (clientCode: string) => {
|
||||
return defHttp.get<Result<Array<Menu>>>({
|
||||
url: '/perm/menu/tree',
|
||||
params: { clientCode },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条
|
||||
*/
|
||||
export const get = (id) => {
|
||||
return defHttp.get<Result<Menu>>({
|
||||
url: '/perm/menu/findById',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*/
|
||||
export const add = (obj: Menu) => {
|
||||
return defHttp.post({
|
||||
url: '/perm/menu/add',
|
||||
data: unref(obj),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*/
|
||||
export const update = (obj: Menu) => {
|
||||
return defHttp.post({
|
||||
url: '/perm/menu/update',
|
||||
data: unref(obj),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
export const del = (id) => {
|
||||
return defHttp.delete({
|
||||
url: '/perm/menu/delete',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限_菜单
|
||||
*/
|
||||
export interface Menu extends BaseEntity {
|
||||
// 终端code
|
||||
clientCode?: string
|
||||
// 是否是一级菜单
|
||||
root: boolean
|
||||
// 父id
|
||||
pid: number | null | string
|
||||
// 菜单名称
|
||||
title: string
|
||||
// 路由名称
|
||||
name: string
|
||||
// 菜单图标
|
||||
icon: string
|
||||
// 是否隐藏
|
||||
hidden: boolean
|
||||
// 是否隐藏子菜单
|
||||
hideChildrenMenu: boolean
|
||||
// 组件
|
||||
component: string
|
||||
// 组件名字
|
||||
componentName: string
|
||||
// 路径
|
||||
path: string
|
||||
// 菜单跳转地址(重定向)
|
||||
redirect: string
|
||||
// 菜单排序
|
||||
sortNo: number
|
||||
// 是否缓存页面
|
||||
keepAlive: boolean
|
||||
// 是否外部打开方式
|
||||
targetOutside: boolean
|
||||
// 是否外部打开方式
|
||||
fullScreen: boolean
|
||||
// 系统菜单
|
||||
admin?: boolean
|
||||
// 描述
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单树
|
||||
*/
|
||||
export interface MenuTree extends Menu {
|
||||
children: MenuTree[]
|
||||
}
|
298
src/views/iam/perm/menu/MenuEdit.vue
Normal file
298
src/views/iam/perm/menu/MenuEdit.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<basic-drawer
|
||||
showFooter
|
||||
v-bind="$attrs"
|
||||
width="50%"
|
||||
:title="title"
|
||||
:mask-closable="showable"
|
||||
:open="visible"
|
||||
@close="handleCancel"
|
||||
>
|
||||
<a-spin :spinning="confirmLoading">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="small-from-item"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:labelCol="labelCol"
|
||||
:wrapperCol="wrapperCol"
|
||||
>
|
||||
<a-form-item name="id" :hidden="true">
|
||||
<a-input v-model="form.id" :disabled="showable" />
|
||||
</a-form-item>
|
||||
<a-form-item label="菜单类型">
|
||||
<a-radio-group :disabled="showable" v-model:value="form.root">
|
||||
<a-radio :value="true">一级菜单</a-radio>
|
||||
<a-radio :value="false">子菜单</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item v-show="!form.root" label="上级菜单" name="pid">
|
||||
<a-tree-select
|
||||
style="width: 100%"
|
||||
:tree-data="treeData"
|
||||
v-model:value="form.pid"
|
||||
placeholder="请选择父级菜单"
|
||||
:disabled="showable"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="菜单名称" name="title">
|
||||
<a-input v-model:value="form.title" :disabled="showable" placeholder="请输入菜单名称" />
|
||||
</a-form-item>
|
||||
<a-form-item name="name">
|
||||
<template #label>
|
||||
<basic-title
|
||||
helpMessage="此处名称应和vue组件的name属性保持一致。
|
||||
路由名称不能重复,主要用于路由缓存功能。
|
||||
如果路由名称和vue组件的name属性不一致,则会导致路由缓存失效。不填则随机自动生成"
|
||||
>路由名称</basic-title
|
||||
>
|
||||
</template>
|
||||
<a-input v-model:value="form.name" :disabled="showable" placeholder="请输入路由名称" />
|
||||
</a-form-item>
|
||||
<a-form-item name="path">
|
||||
<template #label>
|
||||
<basic-title helpMessage="如果访问路径不是网址,则需要以'/'开头,否则将无法被注册为路由"
|
||||
>访问路径</basic-title
|
||||
>
|
||||
</template>
|
||||
<a-input v-model:value="form.path" :disabled="showable" placeholder="请输入访问路径" />
|
||||
</a-form-item>
|
||||
<a-form-item name="component">
|
||||
<template #label>
|
||||
<basic-title
|
||||
help-message="Layout和Iframe可以直接输入,新页面打开访问地址不需要填写,自定义组件需要输入/src/views/下的全路径"
|
||||
>
|
||||
组件
|
||||
</basic-title>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="form.component"
|
||||
:disabled="showable"
|
||||
placeholder="请输入组件名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item name="redirect">
|
||||
<template #label>
|
||||
<basic-title help-message="组件是Iframe的情况下,此配置为内嵌页面中请求地址">
|
||||
默认跳转地址
|
||||
</basic-title>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="form.redirect"
|
||||
:disabled="showable"
|
||||
placeholder="请输入跳转地址(重定向)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="菜单图标" name="icon">
|
||||
<icon-picker v-model:value="form.icon" :disabled="showable" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sortNo">
|
||||
<a-input-number
|
||||
placeholder="请输入菜单排序,可以是小数"
|
||||
:disabled="showable"
|
||||
v-model:value="form.sortNo"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="隐藏菜单" name="hidden">
|
||||
<a-switch
|
||||
:disabled="showable"
|
||||
checkedChildren="是"
|
||||
unCheckedChildren="否"
|
||||
v-model:checked="form.hidden"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="隐藏子菜单" name="hideChildrenMenu">
|
||||
<a-switch
|
||||
:disabled="showable"
|
||||
checkedChildren="是"
|
||||
unCheckedChildren="否"
|
||||
v-model:checked="form.hideChildrenMenu"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否缓存路由" name="keepAlive">
|
||||
<a-switch
|
||||
:disabled="showable"
|
||||
checkedChildren="是"
|
||||
unCheckedChildren="否"
|
||||
v-model:checked="form.keepAlive"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否外部打开" name="targetOutside">
|
||||
<template #label>
|
||||
<basic-title help-message="对Iframe组件无效"> 是否外部打开 </basic-title>
|
||||
</template>
|
||||
<a-switch
|
||||
:disabled="showable"
|
||||
checkedChildren="是"
|
||||
unCheckedChildren="否"
|
||||
v-model:checked="form.targetOutside"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否全屏显示" name="fullScreen">
|
||||
<template #label>
|
||||
<basic-title help-message="默认从新窗口打开,对Iframe组件无效">
|
||||
是否全屏显示
|
||||
</basic-title>
|
||||
</template>
|
||||
<a-switch
|
||||
:disabled="showable"
|
||||
checkedChildren="是"
|
||||
unCheckedChildren="否"
|
||||
v-model:checked="form.fullScreen"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button key="cancel" @click="handleCancel">取消</a-button>
|
||||
<a-button
|
||||
v-if="!showable"
|
||||
key="forward"
|
||||
:loading="confirmLoading"
|
||||
type="primary"
|
||||
@click="handleOk"
|
||||
>保存</a-button
|
||||
>
|
||||
</a-space>
|
||||
</template>
|
||||
</basic-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useFormEdit from '@/hooks/bootx/useFormEdit'
|
||||
import { computed, defineComponent, nextTick, ref, unref } from 'vue'
|
||||
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
|
||||
import { add, get, Menu, menuTree, update } from './Menu.api'
|
||||
import { FormEditType } from '@/enums/formTypeEnum'
|
||||
import { treeDataTranslate } from '@/utils/dataUtil'
|
||||
import { IconPicker } from '@/components/Icon'
|
||||
import { BasicTitle } from '@/components/Basic'
|
||||
import { BasicDrawer } from '@/components/Drawer'
|
||||
|
||||
defineComponent({
|
||||
name: 'MenuEdit',
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const {
|
||||
initFormEditType,
|
||||
handleCancel,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
title,
|
||||
confirmLoading,
|
||||
visible,
|
||||
showable,
|
||||
formEditType,
|
||||
} = useFormEdit()
|
||||
|
||||
const form = ref({
|
||||
root: true,
|
||||
pid: null,
|
||||
title: '',
|
||||
clientCode: '',
|
||||
name: '',
|
||||
path: '',
|
||||
component: '',
|
||||
redirect: '',
|
||||
sortNo: 0,
|
||||
icon: '',
|
||||
hidden: false,
|
||||
hideChildrenMenu: false,
|
||||
keepAlive: true,
|
||||
targetOutside: false,
|
||||
fullScreen: false,
|
||||
} as Menu)
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
pid: [
|
||||
{
|
||||
required: !form.value.root,
|
||||
message: '请选择父级菜单',
|
||||
},
|
||||
],
|
||||
title: [{ required: true, message: '请输入菜单名称' }],
|
||||
path: [{ required: true, message: '请输入菜单路径' }],
|
||||
url: [{ required: true, message: '请输入菜单路径' }],
|
||||
} as Record<string, Rule[]>
|
||||
})
|
||||
let treeData = ref<any[]>()
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 事件
|
||||
const emits = defineEmits(['ok'])
|
||||
// 入口
|
||||
function init(id: any, editType: FormEditType, clientCode: string | undefined, row: Menu) {
|
||||
initMenuTree(unref(clientCode))
|
||||
initFormEditType(editType)
|
||||
resetForm()
|
||||
form.value.clientCode = clientCode
|
||||
getInfo(id, editType, row)
|
||||
}
|
||||
// 查询树
|
||||
function initMenuTree(clientCode) {
|
||||
menuTree(clientCode).then((res) => {
|
||||
treeData.value = treeDataTranslate(res.data, 'id', 'title')
|
||||
})
|
||||
}
|
||||
|
||||
// 获取信息
|
||||
async function getInfo(id, editType: FormEditType, row: Menu) {
|
||||
// 编辑或查看
|
||||
if ([FormEditType.Edit, FormEditType.Show].includes(editType)) {
|
||||
confirmLoading.value = true
|
||||
get(id).then(({ data }) => {
|
||||
form.value = data as Menu
|
||||
confirmLoading.value = false
|
||||
})
|
||||
// 新增
|
||||
} else if (editType === FormEditType.Add) {
|
||||
confirmLoading.value = false
|
||||
} else {
|
||||
// 添加下级
|
||||
if (row) {
|
||||
// 添加下级
|
||||
form.value.root = false
|
||||
nextTick(() => {
|
||||
form.value.pid = row.id as string
|
||||
form.value.path = row.path
|
||||
}).then()
|
||||
} else {
|
||||
// 复制
|
||||
const { data } = await get(id)
|
||||
delete data.id
|
||||
form.value = data as Menu
|
||||
confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
// 保存
|
||||
function handleOk() {
|
||||
formRef.value?.validate().then(async () => {
|
||||
confirmLoading.value = true
|
||||
if ([FormEditType.Add, FormEditType.Other].includes(formEditType.value)) {
|
||||
await add(form.value)
|
||||
} else if (formEditType.value === FormEditType.Edit) {
|
||||
await update(form.value)
|
||||
}
|
||||
confirmLoading.value = false
|
||||
handleCancel()
|
||||
emits('ok')
|
||||
})
|
||||
}
|
||||
|
||||
// 重置表单的校验
|
||||
function resetForm() {
|
||||
nextTick(() => {
|
||||
formRef.value?.resetFields()
|
||||
})
|
||||
}
|
||||
defineExpose({
|
||||
init,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
217
src/views/iam/perm/menu/MenuList.vue
Normal file
217
src/views/iam/perm/menu/MenuList.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="m-3 p-3 pt-5 bg-white">
|
||||
<a-form class="page-query">
|
||||
<a-row :gutter="10">
|
||||
<a-col :md="4" :sm="24">
|
||||
<a-form-item label="终端">
|
||||
<a-select
|
||||
v-model:value="clientCode"
|
||||
@change="queryPage"
|
||||
:default-value="clientCode"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option v-for="o in clients" :key="o.code">{{ o.name }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="8" :sm="24">
|
||||
<a-form-item label="查询">
|
||||
<a-input-search
|
||||
v-model:value="searchName"
|
||||
@search="search"
|
||||
@keyup.enter="search"
|
||||
allow-clear
|
||||
placeholder="请输入菜单名称、路由名称、请求路径或组件名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="m-3 p-3 bg-white">
|
||||
<vxe-toolbar ref="xToolbar" custom zoom :refresh="{ queryMethod: queryPage }">
|
||||
<template #buttons>
|
||||
<a-space>
|
||||
<a-button type="primary" pre-icon="ant-design:plus-outlined" @click="add()">
|
||||
新建
|
||||
</a-button>
|
||||
<a-button @click="allTreeExpand(true)">展开所有</a-button>
|
||||
<a-button @click="allTreeExpand(false)">关闭所有</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-toolbar>
|
||||
<vxe-table
|
||||
resizable
|
||||
:height="650"
|
||||
ref="xTable"
|
||||
border="inner"
|
||||
:stripe="false"
|
||||
:loading="loading"
|
||||
:tree-config="{ childrenField: 'children' }"
|
||||
:data="tableData"
|
||||
>
|
||||
<vxe-column field="title" title="菜单名称" tree-node />
|
||||
<vxe-column field="name" title="路由名称" />
|
||||
<vxe-column field="path" title="请求路径" />
|
||||
<vxe-column field="sortNo" title="排序" :visible="false" />
|
||||
<vxe-column field="component" title="组件" />
|
||||
<vxe-column field="icon" title="图标">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.icon">
|
||||
<icon :icon="row.icon" />
|
||||
</div>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="操作" fixed="right" width="220" :showOverflow="false">
|
||||
<template #default="{ row }">
|
||||
<a href="javascript:" @click="show(row)">查看</a>
|
||||
<template v-if="String(row.menuType) !== '2'">
|
||||
<a-divider type="vertical" />
|
||||
<a href="javascript:" v-if="!row.internal" @click="edit(row)">编辑</a>
|
||||
<a href="javascript:" v-else disabled>编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-dropdown>
|
||||
<a> 更多 <icon icon="ant-design:down-outlined" :size="12" /> </a>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
<a @click="addChildren(row)">添加下级</a>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a @click="copy(row.id)">复制</a>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a
|
||||
href="javascript:"
|
||||
v-if="!row.internal"
|
||||
@click="remove(row)"
|
||||
style="color: red"
|
||||
>删除</a
|
||||
>
|
||||
<a href="javascript:" v-else disabled>删除</a>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
<MenuEdit ref="menuEdit" @ok="queryPage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getAppEnvConfig } from '@/utils/env'
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { Client, findAll } from '@/views/iam/client/Client.api'
|
||||
import XEUtils from 'xe-utils'
|
||||
import { menuTree, Menu, del } from './Menu.api'
|
||||
import { FormEditType } from '@/enums/formTypeEnum'
|
||||
import MenuEdit from './MenuEdit.vue'
|
||||
import { VxeTableInstance, VxeToolbarInstance } from 'vxe-table'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { Icon } from '@/components/Icon'
|
||||
|
||||
const { VITE_GLOB_APP_CLIENT } = getAppEnvConfig()
|
||||
const { createConfirm, notification } = useMessage()
|
||||
|
||||
const clientCode = ref(VITE_GLOB_APP_CLIENT)
|
||||
const searchName = ref()
|
||||
const loading = ref(false)
|
||||
const treeExpand = ref(false)
|
||||
const clients = ref([] as Client[])
|
||||
const remoteTableData = ref([] as Menu[])
|
||||
const tableData = ref([] as Menu[])
|
||||
const xTable = ref<VxeTableInstance>()
|
||||
const xToolbar = ref<VxeToolbarInstance>()
|
||||
let menuEdit = ref<any>()
|
||||
|
||||
onMounted(() => {
|
||||
vxeBind()
|
||||
initClients()
|
||||
queryPage()
|
||||
})
|
||||
|
||||
function vxeBind() {
|
||||
xTable.value?.connect(xToolbar.value as VxeToolbarInstance)
|
||||
}
|
||||
|
||||
async function initClients() {
|
||||
const { data } = await findAll()
|
||||
clients.value = data
|
||||
}
|
||||
|
||||
async function queryPage() {
|
||||
loading.value = true
|
||||
const { data } = await menuTree(clientCode.value)
|
||||
remoteTableData.value = data
|
||||
search()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function add() {
|
||||
menuEdit.value.init(null, FormEditType.Add, clientCode.value)
|
||||
}
|
||||
function edit(record: Menu) {
|
||||
menuEdit.value.init(record.id, FormEditType.Edit, clientCode.value)
|
||||
}
|
||||
function show(record: Menu) {
|
||||
menuEdit.value.init(record.id, FormEditType.Show, clientCode.value)
|
||||
}
|
||||
function addChildren(row: Menu) {
|
||||
menuEdit.value.init(null, FormEditType.Other, clientCode.value, row)
|
||||
}
|
||||
function copy(id) {
|
||||
menuEdit.value.init(id, FormEditType.Other, clientCode.value)
|
||||
}
|
||||
|
||||
function remove(record: Menu) {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '警告',
|
||||
content: '是否删除该条数据',
|
||||
onOk: () => {
|
||||
del(record.id).then(() => {
|
||||
notification.success({ message: '删除成功' })
|
||||
queryPage()
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
function search() {
|
||||
const search = XEUtils.toValueString(searchName.value).trim().toLowerCase()
|
||||
if (search) {
|
||||
const searchProps = ['name', 'title', 'path', 'component']
|
||||
tableData.value = XEUtils.searchTree(remoteTableData.value, (item) =>
|
||||
searchProps.some(
|
||||
(key) => XEUtils.toValueString(item[key]).toLowerCase().indexOf(search) > -1,
|
||||
),
|
||||
)
|
||||
// 搜索状态默认展开
|
||||
treeExpand.value = true
|
||||
} else {
|
||||
tableData.value = remoteTableData.value
|
||||
}
|
||||
nextTick(() => {
|
||||
xTable.value?.setAllTreeExpand(treeExpand.value)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 展开or关闭
|
||||
*/
|
||||
function allTreeExpand(value) {
|
||||
nextTick(() => {
|
||||
xTable.value?.setAllTreeExpand(treeExpand.value)
|
||||
})
|
||||
treeExpand.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
56
src/views/iam/perm/path/PermPath.api.ts
Normal file
56
src/views/iam/perm/path/PermPath.api.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { defHttp } from '@/utils/http/axios'
|
||||
import { Result } from '#/axios'
|
||||
import { BaseEntity } from '#/web'
|
||||
|
||||
/**
|
||||
* 请求资源树
|
||||
*/
|
||||
export const tree = (clientCode) => {
|
||||
return defHttp.get<Result<PermPath[]>>({
|
||||
url: '/perm/path/tree',
|
||||
params: { clientCode },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条
|
||||
*/
|
||||
export const get = (id) => {
|
||||
return defHttp.get<Result<PermPath>>({
|
||||
url: '/perm/path/findById',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步
|
||||
*/
|
||||
export function syncSystem() {
|
||||
return defHttp.post({
|
||||
url: `/perm/path/sync`,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求权限
|
||||
*/
|
||||
export interface PermPath extends BaseEntity {
|
||||
// 标识
|
||||
code: string
|
||||
// 上级标识
|
||||
parentCode: string
|
||||
// 权限名称
|
||||
name: string
|
||||
// 请求类型
|
||||
method: string
|
||||
// 请求路径
|
||||
path: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求权限树
|
||||
*/
|
||||
export interface PermPathTree extends PermPath {
|
||||
// 子级
|
||||
children: PermPathTree[]
|
||||
}
|
31
src/views/iam/perm/path/PermPathInfo.vue
Normal file
31
src/views/iam/perm/path/PermPathInfo.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template></template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, ref } from 'vue'
|
||||
import useFormEdit from '@/hooks/bootx/useFormEdit'
|
||||
import { add, get, update, PermPath } from './PermPath.api'
|
||||
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
|
||||
import { FormEditType } from '@/enums/formTypeEnum'
|
||||
|
||||
const {
|
||||
initFormEditType,
|
||||
handleCancel,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
modalWidth,
|
||||
title,
|
||||
confirmLoading,
|
||||
visible,
|
||||
showable,
|
||||
formEditType,
|
||||
} = useFormEdit()
|
||||
// 表单
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
function show(id) {}
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
172
src/views/iam/perm/path/PermPathList.vue
Normal file
172
src/views/iam/perm/path/PermPathList.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="m-3 p-3 pt-5 bg-white">
|
||||
<a-form class="page-query">
|
||||
<a-row :gutter="10">
|
||||
<a-col :md="4" :sm="24">
|
||||
<a-form-item label="终端">
|
||||
<a-select
|
||||
v-model:value="clientCode"
|
||||
@change="queryPage"
|
||||
:default-value="clientCode"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option v-for="o in clients" :key="o.code">{{ o.name }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="8" :sm="24">
|
||||
<a-form-item label="查询">
|
||||
<a-input-search
|
||||
v-model:value="searchName"
|
||||
@search="search"
|
||||
@keyup.enter="search"
|
||||
allow-clear
|
||||
placeholder="请输入路径或标识"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="m-3 p-3 bg-white">
|
||||
<vxe-toolbar ref="xToolbar" custom zoom :refresh="{ queryMethod: queryPage }">
|
||||
<template #buttons>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="sync" pre-icon="ant-design:sync-outlined"
|
||||
>同步系统资源</a-button
|
||||
>
|
||||
<a-button @click="allTreeExpand(true)">展开所有</a-button>
|
||||
<a-button @click="allTreeExpand(false)">关闭所有</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-toolbar>
|
||||
<vxe-table
|
||||
resizable
|
||||
:height="650"
|
||||
ref="xTable"
|
||||
border="inner"
|
||||
:stripe="false"
|
||||
:loading="loading"
|
||||
:tree-config="{ childrenField: 'children' }"
|
||||
:data="tableData"
|
||||
>
|
||||
<vxe-column field="name" title="名称" tree-node />
|
||||
<vxe-column field="path" title="请求路径" />
|
||||
<vxe-column title="操作" fixed="right" width="220" :showOverflow="false">
|
||||
<template #default="{ row }">
|
||||
<a href="javascript:" @click="show(row)">查看</a>
|
||||
<template v-if="String(row.menuType) !== '2'"> </template>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</div>
|
||||
<PermPathInfo ref="permPathInfo" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { tree, syncSystem, PermPath } from './PermPath.api'
|
||||
import { VxeTableInstance, VxeToolbarInstance } from 'vxe-table'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import PermPathInfo from './PermPathInfo.vue'
|
||||
import { Client, findAll } from '@/views/iam/client/Client.api'
|
||||
import XEUtils from 'xe-utils'
|
||||
import { getAppEnvConfig } from '@/utils/env'
|
||||
|
||||
const { createMessage, createConfirm } = useMessage()
|
||||
|
||||
const { VITE_GLOB_APP_CLIENT } = getAppEnvConfig()
|
||||
|
||||
const permPathInfo = ref<any>()
|
||||
const clientCode = ref(VITE_GLOB_APP_CLIENT)
|
||||
const searchName = ref()
|
||||
const loading = ref(false)
|
||||
const treeExpand = ref(false)
|
||||
const clients = ref([] as Client[])
|
||||
const remoteTableData = ref([] as PermPath[])
|
||||
const tableData = ref([] as PermPath[])
|
||||
const xTable = ref<VxeTableInstance>()
|
||||
const xToolbar = ref<VxeToolbarInstance>()
|
||||
|
||||
onMounted(() => {
|
||||
vxeBind()
|
||||
initClients()
|
||||
queryPage()
|
||||
})
|
||||
function vxeBind() {
|
||||
xTable.value?.connect(xToolbar.value as VxeToolbarInstance)
|
||||
}
|
||||
|
||||
async function initClients() {
|
||||
const { data } = await findAll()
|
||||
clients.value = data
|
||||
}
|
||||
|
||||
async function queryPage() {
|
||||
loading.value = true
|
||||
const { data } = await tree(clientCode.value)
|
||||
remoteTableData.value = data
|
||||
search()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
function search() {
|
||||
const search = XEUtils.toValueString(searchName.value).trim().toLowerCase()
|
||||
if (search) {
|
||||
const searchProps = ['name', 'path', 'code', 'component']
|
||||
tableData.value = XEUtils.searchTree(remoteTableData.value, (item) =>
|
||||
searchProps.some(
|
||||
(key) => XEUtils.toValueString(item[key]).toLowerCase().indexOf(search) > -1,
|
||||
),
|
||||
)
|
||||
// 搜索状态默认展开
|
||||
treeExpand.value = true
|
||||
} else {
|
||||
tableData.value = remoteTableData.value
|
||||
}
|
||||
nextTick(() => {
|
||||
xTable.value?.setAllTreeExpand(treeExpand.value)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 展开or关闭
|
||||
*/
|
||||
function allTreeExpand(value) {
|
||||
nextTick(() => {
|
||||
xTable.value?.setAllTreeExpand(treeExpand.value)
|
||||
})
|
||||
treeExpand.value = value
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步
|
||||
*/
|
||||
function sync() {
|
||||
createConfirm({
|
||||
iconType: 'info',
|
||||
title: '提示',
|
||||
content: '是否同步系统资源,将会覆盖当前的数据,确定进行同步?',
|
||||
onOk: () => {
|
||||
createMessage.success('同步中,请稍后......')
|
||||
syncSystem().then(() => {
|
||||
createMessage.success('同步完成')
|
||||
queryPage()
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看
|
||||
*/
|
||||
function show(record: PermPath) {
|
||||
permPathInfo.value.init(record.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
116
src/views/iam/role/Role.api.ts
Normal file
116
src/views/iam/role/Role.api.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { defHttp } from '@/utils/http/axios'
|
||||
import { Result } from '#/axios'
|
||||
import { BaseEntity } from '#/web'
|
||||
|
||||
/**
|
||||
* tree
|
||||
*/
|
||||
export const tree = () => {
|
||||
return defHttp.get<Result<RoleTree[]>>({
|
||||
url: '/role/tree',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条
|
||||
*/
|
||||
export const get = (id) => {
|
||||
return defHttp.get<Result<Role>>({
|
||||
url: '/role/findById',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加角色
|
||||
*/
|
||||
export const add = (obj: Role) => {
|
||||
return defHttp.post({
|
||||
url: '/role/add',
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改角色
|
||||
*/
|
||||
export const update = (obj: Role) => {
|
||||
return defHttp.post({
|
||||
url: '/role/update',
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
export const del = (id) => {
|
||||
return defHttp.delete({
|
||||
url: '/role/delete',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码是否被使用
|
||||
*/
|
||||
export const existsByCode = (code: string) => {
|
||||
return defHttp.get<Result<boolean>>({
|
||||
url: '/role/existsByCode',
|
||||
params: { code },
|
||||
})
|
||||
}
|
||||
export const existsByCodeNotId = (code: string, id) => {
|
||||
return defHttp.get<Result<boolean>>({
|
||||
url: '/role/existsByCodeNotId',
|
||||
params: { code, id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码是否被使用
|
||||
*/
|
||||
export const existsByName = (name: string) => {
|
||||
return defHttp.get<Result<boolean>>({
|
||||
url: '/role/existsByName',
|
||||
params: { name },
|
||||
})
|
||||
}
|
||||
export const existsByNameNotId = (name: string, id) => {
|
||||
return defHttp.get<Result<boolean>>({
|
||||
url: '/role/existsByNameNotId',
|
||||
params: { name, id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部角色
|
||||
*/
|
||||
export const findAll = () => {
|
||||
return defHttp.get<Result<Array<Role>>>({
|
||||
url: '/role/findAll',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色
|
||||
*/
|
||||
export interface Role extends BaseEntity {
|
||||
// 编码
|
||||
code?: string
|
||||
// 父ID
|
||||
pid?: number
|
||||
// 名称
|
||||
name?: string
|
||||
// 是否系统内置
|
||||
internal?: boolean
|
||||
// 说明
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色树
|
||||
*/
|
||||
export interface RoleTree extends Role {
|
||||
children?: RoleTree[]
|
||||
}
|
284
src/views/iam/role/RoleCodeModal.vue
Normal file
284
src/views/iam/role/RoleCodeModal.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<basic-drawer
|
||||
showFooter
|
||||
v-bind="$attrs"
|
||||
title="权限码配置"
|
||||
width="40%"
|
||||
:open="visible"
|
||||
@close="handleCancel"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<a-input
|
||||
style="margin-bottom: 8px"
|
||||
placeholder="筛选"
|
||||
allowClear
|
||||
v-model:value="searchName"
|
||||
@change="search"
|
||||
/>
|
||||
<a-tree
|
||||
:checkable="true"
|
||||
v-model:checkedKeys="checkedKeys"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
:auto-expand-parent="autoExpandParent"
|
||||
:tree-data="treeData"
|
||||
@check="onCheck"
|
||||
@expand="onExpand"
|
||||
>
|
||||
<template #title="{ title }">
|
||||
<span v-if="title?.toLowerCase()?.indexOf(searchName.toLowerCase()) > -1">
|
||||
{{ searchRenderStart(title, searchName) }}
|
||||
<span style="color: #f50">
|
||||
{{ searchRenderMiddle(title, searchName) }}
|
||||
</span>
|
||||
{{ searchRenderEnd(title, searchName) }}
|
||||
</span>
|
||||
<span v-else>{{ title }}</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</a-spin>
|
||||
<template #footer>
|
||||
<a-select style="min-width: 100px" @change="clientSwitch" v-model:value="clientCode">
|
||||
<a-select-option v-for="o in clients" :key="o.code">{{ o.name }}</a-select-option>
|
||||
</a-select>
|
||||
<a-dropdown style="margin-left: 5px" :trigger="['click']" placement="top">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="1" @click="checkALL">全部勾选</a-menu-item>
|
||||
<a-menu-item key="2" @click="cancelCheckALL">取消全选</a-menu-item>
|
||||
<a-menu-item key="3" @click="expandAll">展开所有</a-menu-item>
|
||||
<a-menu-item key="4" @click="closeAll">合并所有</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button post-icon="ant-design:up-outlined"> 操作 </a-button>
|
||||
</a-dropdown>
|
||||
<a-button @click="visible = false" style="margin-right: 0.8rem">取消</a-button>
|
||||
<a-button
|
||||
@click="handleSubmit()"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
style="margin-right: 0.8rem"
|
||||
>保存</a-button
|
||||
>
|
||||
</template>
|
||||
</basic-drawer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { BasicDrawer } from '@/components/Drawer'
|
||||
import { getAppEnvConfig } from '@/utils/env'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { ref } from 'vue'
|
||||
import { RoleTree } from '@/views/iam/role/Role.api'
|
||||
import { Client, findAll as findClients } from '@/views/iam/client/Client.api'
|
||||
import { Tree, treeDataTranslate } from '@/utils/dataUtil'
|
||||
import { findIdsByRoleCode, saveRoleCode, treeByRoleCode } from '@/views/iam/role/RolePerm.api'
|
||||
import XEUtils from 'xe-utils'
|
||||
import { PermCodeTree } from '@/views/iam/perm/code/PermCode.api'
|
||||
|
||||
const { VITE_GLOB_APP_CLIENT } = getAppEnvConfig()
|
||||
const { createMessage, createConfirm } = useMessage()
|
||||
|
||||
let loading = ref(false)
|
||||
let currentRole = ref<RoleTree>({})
|
||||
let visible = ref(false)
|
||||
// 终端列表
|
||||
let clients = ref([] as Client[])
|
||||
let clientCode = ref(VITE_GLOB_APP_CLIENT)
|
||||
let searchName = ref('')
|
||||
// 所有的key
|
||||
let allTreeKeys = ref<string[]>([])
|
||||
// 展开的key
|
||||
let expandedKeys = ref<string[]>([])
|
||||
// 被选中的key
|
||||
let checkedKeys = ref<string[]>([])
|
||||
let autoExpandParent = ref(false)
|
||||
//权限码树信息
|
||||
let treeData = ref<Tree[]>([])
|
||||
let treeList = ref<PermCodeTree[]>([])
|
||||
|
||||
function init(record: RoleTree) {
|
||||
currentRole.value = record
|
||||
initData()
|
||||
initAssign()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化终端列表
|
||||
*/
|
||||
function initData() {
|
||||
findClients().then(({ data }) => {
|
||||
clients.value = data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化权限码分配信息
|
||||
*/
|
||||
async function initAssign() {
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
searchName.value = ''
|
||||
expandedKeys.value = []
|
||||
// 当前角色的权限码
|
||||
await treeByRoleCode(currentRole.value.id).then((res) => {
|
||||
treeData.value = treeDataTranslate(res.data, 'id', 'title')
|
||||
generateTreeList(res.data)
|
||||
})
|
||||
// 当前角色已经选择的
|
||||
await findIdsByRoleCode(currentRole.value.id).then((res) => {
|
||||
checkedKeys.value = res.data
|
||||
})
|
||||
// 所有的key值
|
||||
allTreeKeys.value = treeList.value.map((item) => item.id) as string[]
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
function handleSubmit() {
|
||||
// 是否级联更新子角色
|
||||
if (currentRole.value.children) {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '警告',
|
||||
cancelText: '不应用',
|
||||
okText: '应用',
|
||||
content:
|
||||
'将新增的权限应用到下级子角色中,注意:删除权限时无论如何选择,都将会下级角色的权限级联删除',
|
||||
onOk: () => {
|
||||
save(true)
|
||||
},
|
||||
onCancel: () => {
|
||||
save(false)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
save(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
function save(updateChildren: boolean) {
|
||||
loading.value = true
|
||||
saveRoleCode({
|
||||
roleId: currentRole.value.id,
|
||||
updateChildren,
|
||||
codes: checkedKeys.value,
|
||||
}).then(() => {
|
||||
createMessage.success('保存成功')
|
||||
handleCancel()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
visible.value = false
|
||||
}
|
||||
/**
|
||||
* 树数据铺平
|
||||
*/
|
||||
function generateTreeList(treeData) {
|
||||
if (!treeData) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < treeData.length; i++) {
|
||||
const node = treeData[i]
|
||||
treeList.value.push(node)
|
||||
if (node.children) {
|
||||
generateTreeList(node.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
function search() {
|
||||
const value = XEUtils.toValueString(searchName.value).toLowerCase()
|
||||
expandedKeys.value = treeList.value
|
||||
.map((node) => {
|
||||
if (
|
||||
searchName &&
|
||||
node.pid &&
|
||||
XEUtils.toValueString(node.name)?.toLowerCase()?.indexOf(value) > -1
|
||||
) {
|
||||
return node.name
|
||||
}
|
||||
})
|
||||
.filter((item, i, self) => item && self.indexOf(item) === i) as string[]
|
||||
}
|
||||
/**
|
||||
* 渲染搜索项目数据开始段
|
||||
*/
|
||||
function searchRenderStart(title, searchName) {
|
||||
return title.substring(0, title.toLowerCase().indexOf(searchName.toLowerCase()))
|
||||
}
|
||||
/**
|
||||
* 渲染搜索项目数据中间段
|
||||
*/
|
||||
function searchRenderMiddle(title, searchName) {
|
||||
return title.substring(
|
||||
title.toLowerCase().indexOf(searchName.toLowerCase()),
|
||||
title.toLowerCase().indexOf(searchName.toLowerCase()) + searchName.length,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* 渲染搜索项目数据结束段
|
||||
*/
|
||||
function searchRenderEnd(title, searchName) {
|
||||
return title.substring(
|
||||
title.toLowerCase().indexOf(searchName.toLowerCase()) + searchName.length,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* 展开/收起节点时触发
|
||||
*/
|
||||
function onExpand(keys) {
|
||||
expandedKeys.value = keys
|
||||
autoExpandParent.value = false
|
||||
}
|
||||
/**
|
||||
* 点击复选框触发
|
||||
*/
|
||||
function onCheck(key) {
|
||||
checkedKeys.value = key
|
||||
}
|
||||
/**
|
||||
* 展开全部
|
||||
*/
|
||||
function expandAll() {
|
||||
expandedKeys.value = allTreeKeys.value
|
||||
}
|
||||
/**
|
||||
* 合并全部
|
||||
*/
|
||||
function closeAll() {
|
||||
expandedKeys.value = []
|
||||
}
|
||||
/**
|
||||
* 全选
|
||||
*/
|
||||
function checkALL() {
|
||||
checkedKeys.value = allTreeKeys.value
|
||||
}
|
||||
/**
|
||||
* 全不选
|
||||
*/
|
||||
function cancelCheckALL() {
|
||||
checkedKeys.value = []
|
||||
}
|
||||
/**
|
||||
* 终端切换
|
||||
*/
|
||||
function clientSwitch(item) {
|
||||
clientCode.value = item
|
||||
initAssign()
|
||||
}
|
||||
|
||||
defineExpose({ init })
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
184
src/views/iam/role/RoleEdit.vue
Normal file
184
src/views/iam/role/RoleEdit.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<basic-modal
|
||||
:loading="confirmLoading"
|
||||
v-bind="$attrs"
|
||||
:width="750"
|
||||
:title="title"
|
||||
:open="visible"
|
||||
:mask-closable="showable"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
class="small-from-item"
|
||||
:model="form"
|
||||
ref="formRef"
|
||||
:rules="rules"
|
||||
:label-col="labelCol"
|
||||
:wrapper-col="wrapperCol"
|
||||
>
|
||||
<a-form-item label="主键" name="id" :hidden="true">
|
||||
<a-input v-model:value="form.id" :disabled="showable" />
|
||||
</a-form-item>
|
||||
<a-form-item v-show="addable" label="上级角色" name="pid">
|
||||
<a-tree-select
|
||||
style="width: 100%"
|
||||
:tree-data="treeData"
|
||||
v-model:value="form.pid"
|
||||
placeholder="请选择上级角色"
|
||||
:disabled="showable"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="编码" name="code">
|
||||
<a-input v-model:value="form.code" :disabled="showable" placeholder="请输入编码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="名称" name="name">
|
||||
<a-input v-model:value="form.name" :disabled="showable" placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="说明" name="remark">
|
||||
<a-textarea v-model:value="form.remark" :disabled="showable" placeholder="请输入说明" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button key="cancel" @click="handleCancel">取消</a-button>
|
||||
<a-button
|
||||
v-if="!showable"
|
||||
key="forward"
|
||||
:loading="confirmLoading"
|
||||
type="primary"
|
||||
@click="handleOk"
|
||||
>保存</a-button
|
||||
>
|
||||
</a-space>
|
||||
</template>
|
||||
</basic-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, ref, unref } from 'vue'
|
||||
import useFormEdit from '@/hooks/bootx/useFormEdit'
|
||||
import {
|
||||
add,
|
||||
get,
|
||||
update,
|
||||
existsByCode,
|
||||
existsByCodeNotId,
|
||||
existsByName,
|
||||
existsByNameNotId,
|
||||
Role,
|
||||
tree,
|
||||
} from './Role.api'
|
||||
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
|
||||
import { FormEditType } from '@/enums/formTypeEnum'
|
||||
import { BasicModal } from '@/components/Modal'
|
||||
import { useValidate } from '@/hooks/bootx/useValidate'
|
||||
import { treeDataTranslate } from '@/utils/dataUtil'
|
||||
|
||||
const {
|
||||
initFormEditType,
|
||||
handleCancel,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
title,
|
||||
confirmLoading,
|
||||
visible,
|
||||
addable,
|
||||
showable,
|
||||
formEditType,
|
||||
} = useFormEdit()
|
||||
const { existsByServer } = useValidate()
|
||||
|
||||
// 表单
|
||||
const formRef = ref<FormInstance>()
|
||||
let form = ref({
|
||||
id: null,
|
||||
code: '',
|
||||
pid: undefined,
|
||||
name: '',
|
||||
remark: '',
|
||||
} as Role)
|
||||
|
||||
let treeData = ref<any[]>()
|
||||
|
||||
// 校验
|
||||
const rules = reactive({
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: ['blur', 'change'] },
|
||||
{ validator: validateName, trigger: 'blur' },
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入角色代码', trigger: ['blur', 'change'] },
|
||||
{ validator: validateCode, trigger: 'blur' },
|
||||
],
|
||||
} as Record<string, Rule[]>)
|
||||
// 事件
|
||||
const emits = defineEmits(['ok'])
|
||||
// 入口
|
||||
function init(id, editType: FormEditType, roleId) {
|
||||
initRoleTree()
|
||||
initFormEditType(editType)
|
||||
resetForm()
|
||||
getInfo(id, editType, roleId)
|
||||
}
|
||||
/**
|
||||
* 查询树
|
||||
*/
|
||||
function initRoleTree() {
|
||||
tree().then((res) => {
|
||||
treeData.value = treeDataTranslate(res.data, 'id', 'name')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取信息
|
||||
*/
|
||||
function getInfo(id, editType: FormEditType, roleId) {
|
||||
if ([FormEditType.Edit, FormEditType.Show].includes(editType)) {
|
||||
confirmLoading.value = true
|
||||
get(id).then(({ data }) => {
|
||||
form.value = data
|
||||
confirmLoading.value = false
|
||||
})
|
||||
} else if (editType === FormEditType.Add) {
|
||||
confirmLoading.value = false
|
||||
// 添加下级
|
||||
if (roleId) {
|
||||
nextTick(() => {
|
||||
form.value.pid = roleId
|
||||
}).then()
|
||||
}
|
||||
}
|
||||
}
|
||||
// 保存
|
||||
function handleOk() {
|
||||
formRef.value?.validate().then(async () => {
|
||||
confirmLoading.value = true
|
||||
if (formEditType.value === FormEditType.Add) {
|
||||
await add(unref(form))
|
||||
} else if (formEditType.value === FormEditType.Edit) {
|
||||
await update(unref(form))
|
||||
}
|
||||
confirmLoading.value = false
|
||||
handleCancel()
|
||||
emits('ok')
|
||||
})
|
||||
}
|
||||
|
||||
// 重置表单的校验
|
||||
function resetForm() {
|
||||
nextTick(() => formRef.value?.resetFields())
|
||||
}
|
||||
async function validateCode() {
|
||||
const { code, id } = form.value
|
||||
return existsByServer(code, id, formEditType.value, existsByCode, existsByCodeNotId)
|
||||
}
|
||||
async function validateName() {
|
||||
const { name, id } = form.value
|
||||
return existsByServer(name, id, formEditType.value, existsByName, existsByNameNotId)
|
||||
}
|
||||
defineExpose({
|
||||
init,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
224
src/views/iam/role/RoleList.vue
Normal file
224
src/views/iam/role/RoleList.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="m-3 p-3 pt-5 bg-white">
|
||||
<a-form class="page-query">
|
||||
<a-row :gutter="10">
|
||||
<a-col :md="8" :sm="24">
|
||||
<a-form-item label="查询">
|
||||
<a-input-search
|
||||
v-model:value="searchName"
|
||||
@search="search"
|
||||
@keyup.enter="search"
|
||||
allow-clear
|
||||
placeholder="请输入角色名称或编码"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="m-3 p-3 bg-white">
|
||||
<vxe-toolbar ref="xToolbar" custom :refresh="{ queryMethod: queryPage }">
|
||||
<template #buttons>
|
||||
<a-space>
|
||||
<a-button type="primary" pre-icon="ant-design:plus-outlined" @click="add()">
|
||||
新建
|
||||
</a-button>
|
||||
<a-button @click="allTreeExpand(true)">展开所有</a-button>
|
||||
<a-button @click="allTreeExpand(false)">关闭所有</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-toolbar>
|
||||
<vxe-table
|
||||
resizable
|
||||
:height="650"
|
||||
:stripe="false"
|
||||
ref="xTable"
|
||||
border="inner"
|
||||
:loading="loading"
|
||||
:tree-config="{ childrenField: 'children' }"
|
||||
:data="tableData"
|
||||
>
|
||||
<vxe-column field="name" title="名称" :min-width="200" tree-node>
|
||||
<template #default="{ row }">
|
||||
<a-link @click="show(row)">{{ row.name }}</a-link>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="code" title="编码" :min-width="150" />
|
||||
<vxe-column field="remark" title="备注" :min-width="200" />
|
||||
<vxe-column fixed="right" width="270" :showOverflow="false" title="操作">
|
||||
<template #default="{ row }">
|
||||
<a-link @click="addChildren(row)">添加子角色</a-link>
|
||||
<a-divider type="vertical" />
|
||||
<a-link @click="edit(row)">编辑</a-link>
|
||||
<a-divider type="vertical" />
|
||||
<a-link danger @click="remove(row)">删除</a-link>
|
||||
<a-divider type="vertical" />
|
||||
<a-dropdown>
|
||||
<a> 授权 <Icon icon="ant-design:down-outlined" :size="12" :min-width="280" /> </a>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
<a-link @click="handleRoleMenu(row)">菜单权限</a-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a-link @click="handleRolePath(row)">请求权限</a-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a-link @click="handleRoleCode(row)">权限码</a-link>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
<RoleEdit ref="roleEdit" @ok="queryPage" />
|
||||
<RoleMenuModal ref="roleMenuModal" />
|
||||
<RoleCodeModal ref="roleCodeModal" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, ref, unref } from 'vue'
|
||||
import { del, RoleTree, tree } from './Role.api'
|
||||
import useTablePage from '@/hooks/bootx/useTablePage'
|
||||
import RoleEdit from './RoleEdit.vue'
|
||||
import RoleMenuModal from './RoleMenuModal.vue'
|
||||
import RoleCodeModal from './RoleCodeModal.vue'
|
||||
import { VxeTableInstance, VxeToolbarInstance } from 'vxe-table'
|
||||
import { FormEditType } from '@/enums/formTypeEnum'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import ALink from '@/components/Link/Link.vue'
|
||||
import XEUtils from 'xe-utils'
|
||||
|
||||
// 使用hooks
|
||||
const { loading } = useTablePage(queryPage)
|
||||
const { createMessage, createConfirm } = useMessage()
|
||||
|
||||
let searchName = ref<string>()
|
||||
let tableData = ref<RoleTree[]>([])
|
||||
let remoteTableData = ref<RoleTree[]>([])
|
||||
let treeExpand = ref(false)
|
||||
const roleEdit = ref<any>()
|
||||
const roleMenuModal = ref<any>()
|
||||
const rolePathModal = ref<any>()
|
||||
const roleCodeModal = ref<any>()
|
||||
// 查询条件
|
||||
const xTable = ref<VxeTableInstance>()
|
||||
const xToolbar = ref<VxeToolbarInstance>()
|
||||
|
||||
onMounted(() => {
|
||||
vxeBind()
|
||||
queryPage()
|
||||
})
|
||||
function vxeBind() {
|
||||
xTable.value?.connect(xToolbar.value as VxeToolbarInstance)
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单树查询
|
||||
*/
|
||||
function queryPage() {
|
||||
loading.value = true
|
||||
tree().then(({ data }) => {
|
||||
remoteTableData.value = data
|
||||
search()
|
||||
loading.value = false
|
||||
})
|
||||
return Promise.resolve()
|
||||
}
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
function add() {
|
||||
roleEdit.value.init(null, FormEditType.Add)
|
||||
}
|
||||
/**
|
||||
* 添加子角色
|
||||
*/
|
||||
function addChildren(record) {
|
||||
roleEdit.value.init(null, FormEditType.Add, record.id)
|
||||
}
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
function edit(record) {
|
||||
roleEdit.value.init(record.id, FormEditType.Edit)
|
||||
}
|
||||
/**
|
||||
* 查看
|
||||
*/
|
||||
function show(record) {
|
||||
roleEdit.value.init(record.id, FormEditType.Show)
|
||||
}
|
||||
/**
|
||||
* 菜单授权处理
|
||||
*/
|
||||
function handleRoleMenu(record) {
|
||||
roleMenuModal.value.init(record)
|
||||
}
|
||||
/**
|
||||
* 请求授权处理
|
||||
*/
|
||||
function handleRolePath(record) {
|
||||
rolePathModal.value.init(record)
|
||||
}
|
||||
/**
|
||||
* 权限码处理
|
||||
*/
|
||||
function handleRoleCode(record) {
|
||||
roleCodeModal.value.init(record)
|
||||
}
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
function remove(record) {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '警告',
|
||||
content: '是否删除该数据',
|
||||
onOk: () => {
|
||||
del(record.id).then(() => {
|
||||
createMessage.success('删除成功')
|
||||
queryPage()
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
function search() {
|
||||
const search = XEUtils.toValueString(unref(searchName)).trim().toLowerCase()
|
||||
if (search) {
|
||||
const searchProps = ['name', 'code']
|
||||
tableData.value = XEUtils.searchTree(remoteTableData.value, (item) =>
|
||||
searchProps.some(
|
||||
(key) => XEUtils.toValueString(item[key]).toLowerCase().indexOf(search) > -1,
|
||||
),
|
||||
)
|
||||
// 搜索状态默认展开
|
||||
treeExpand.value = true
|
||||
} else {
|
||||
tableData.value = remoteTableData.value
|
||||
}
|
||||
nextTick(() => {
|
||||
xTable.value?.setAllTreeExpand(treeExpand.value)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 展开or关闭
|
||||
*/
|
||||
function allTreeExpand(value) {
|
||||
nextTick(() => {
|
||||
xTable.value?.setAllTreeExpand(treeExpand.value)
|
||||
})
|
||||
treeExpand.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
291
src/views/iam/role/RoleMenuModal.vue
Normal file
291
src/views/iam/role/RoleMenuModal.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<basic-drawer
|
||||
showFooter
|
||||
v-bind="$attrs"
|
||||
title="角色菜单配置"
|
||||
width="40%"
|
||||
:open="visible"
|
||||
@close="handleCancel"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<a-input
|
||||
style="margin-bottom: 8px"
|
||||
placeholder="筛选"
|
||||
allowClear
|
||||
v-model:value="searchName"
|
||||
@change="search"
|
||||
/>
|
||||
<a-tree
|
||||
:checkable="true"
|
||||
v-model:checkedKeys="checkedKeys"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
:checkStrictly="checkStrictly"
|
||||
:auto-expand-parent="autoExpandParent"
|
||||
:tree-data="treeData"
|
||||
@check="onCheck"
|
||||
@expand="onExpand"
|
||||
>
|
||||
<template #title="{ title }">
|
||||
<span v-if="title.toLowerCase().indexOf(searchName.toLowerCase()) > -1">
|
||||
{{ searchRenderStart(title, searchName) }}
|
||||
<span style="color: #f50">
|
||||
{{ searchRenderMiddle(title, searchName) }}
|
||||
</span>
|
||||
{{ searchRenderEnd(title, searchName) }}
|
||||
</span>
|
||||
<span v-else>{{ title }}</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</a-spin>
|
||||
<template #footer>
|
||||
<a-select style="min-width: 100px" @change="clientSwitch" v-model:value="clientCode">
|
||||
<a-select-option v-for="o in clients" :key="o.code">{{ o.name }}</a-select-option>
|
||||
</a-select>
|
||||
<a-dropdown style="margin-left: 5px" :trigger="['click']" placement="top">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="1" @click="checkALL">全部勾选</a-menu-item>
|
||||
<a-menu-item key="2" @click="cancelCheckALL">取消全选</a-menu-item>
|
||||
<a-menu-item key="3" @click="expandAll">展开所有</a-menu-item>
|
||||
<a-menu-item key="4" @click="closeAll">合并所有</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button post-icon="ant-design:up-outlined"> 操作 </a-button>
|
||||
</a-dropdown>
|
||||
<a-button @click="visible = false" style="margin-right: 0.8rem">取消</a-button>
|
||||
<a-button
|
||||
@click="handleSubmit()"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
style="margin-right: 0.8rem"
|
||||
>保存</a-button
|
||||
>
|
||||
</template>
|
||||
</basic-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BasicDrawer } from '@/components/Drawer'
|
||||
import { findAll as findClients, Client } from '@/views/iam/client/Client.api'
|
||||
import { getAppEnvConfig } from '@/utils/env'
|
||||
import { RoleTree } from './Role.api'
|
||||
import { Tree, treeDataTranslate } from '@/utils/dataUtil'
|
||||
import XEUtils from 'xe-utils'
|
||||
import { MenuTree } from '@/views/iam/perm/menu/Menu.api'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { ref } from 'vue'
|
||||
import { findIdsByRoleMenu, saveRoleMenu, treeByRoleMenu } from './RolePerm.api'
|
||||
|
||||
const { VITE_GLOB_APP_CLIENT } = getAppEnvConfig()
|
||||
const { createMessage, createConfirm } = useMessage()
|
||||
|
||||
let loading = ref(false)
|
||||
let currentRole = ref<RoleTree>({})
|
||||
let visible = ref(false)
|
||||
// 终端列表
|
||||
let clients = ref([] as Client[])
|
||||
let clientCode = ref(VITE_GLOB_APP_CLIENT)
|
||||
let searchName = ref('')
|
||||
// 父子选项默认不受控
|
||||
let checkStrictly = ref(true)
|
||||
// 所有的key
|
||||
let allTreeKeys = ref<string[]>([])
|
||||
// 展开的key
|
||||
let expandedKeys = ref<string[]>([])
|
||||
// 被选中的key
|
||||
let checkedKeys = ref<string[]>([])
|
||||
let autoExpandParent = ref(false)
|
||||
//菜单树信息
|
||||
let treeData = ref<Tree[]>([])
|
||||
let treeList = ref<MenuTree[]>([])
|
||||
|
||||
function init(record: RoleTree) {
|
||||
currentRole.value = record
|
||||
initData()
|
||||
initAssign()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化终端列表
|
||||
*/
|
||||
function initData() {
|
||||
findClients().then(({ data }) => {
|
||||
clients.value = data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化菜单分配信息
|
||||
*/
|
||||
async function initAssign() {
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
searchName.value = ''
|
||||
expandedKeys.value = []
|
||||
// 当前角色的菜单
|
||||
await treeByRoleMenu(currentRole.value.id, clientCode).then((res) => {
|
||||
treeData.value = treeDataTranslate(res.data, 'id', 'title')
|
||||
generateTreeList(res.data)
|
||||
})
|
||||
// 当前角色已经选择的
|
||||
await findIdsByRoleMenu(currentRole.value.id, clientCode).then((res) => {
|
||||
checkedKeys.value = res.data
|
||||
})
|
||||
// 所有的key值
|
||||
allTreeKeys.value = treeList.value.map((item) => item.id) as string[]
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
function handleSubmit() {
|
||||
// 是否级联更新子角色
|
||||
if (currentRole.value.children) {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '警告',
|
||||
cancelText: '不应用',
|
||||
okText: '应用',
|
||||
content:
|
||||
'将新增的权限应用到下级子角色中,注意:删除权限时无论如何选择,都将会下级角色的权限级联删除',
|
||||
onOk: () => {
|
||||
save(true)
|
||||
},
|
||||
onCancel: () => {
|
||||
save(false)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
save(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
function save(updateChildren: boolean) {
|
||||
loading.value = true
|
||||
saveRoleMenu({
|
||||
roleId: currentRole.value.id,
|
||||
clientCode: clientCode.value,
|
||||
updateChildren,
|
||||
menuIds: checkedKeys.value,
|
||||
}).then(() => {
|
||||
createMessage.success('保存成功')
|
||||
handleCancel()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
visible.value = false
|
||||
}
|
||||
/**
|
||||
* 树数据铺平
|
||||
*/
|
||||
function generateTreeList(treeData) {
|
||||
if (!treeData) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < treeData.length; i++) {
|
||||
const node = treeData[i]
|
||||
treeList.value.push(node)
|
||||
if (node.children) {
|
||||
generateTreeList(node.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
function search() {
|
||||
const value = XEUtils.toValueString(searchName.value).toLowerCase()
|
||||
expandedKeys.value = treeList.value
|
||||
.map((node) => {
|
||||
if (
|
||||
searchName &&
|
||||
node.pid &&
|
||||
XEUtils.toValueString(node.title).toLowerCase().indexOf(value) > -1
|
||||
) {
|
||||
return node.pid
|
||||
}
|
||||
})
|
||||
.filter((item, i, self) => item && self.indexOf(item) === i) as string[]
|
||||
}
|
||||
/**
|
||||
* 渲染搜索项目数据开始段
|
||||
*/
|
||||
function searchRenderStart(title, searchName) {
|
||||
return title.substring(0, title.toLowerCase().indexOf(searchName.toLowerCase()))
|
||||
}
|
||||
/**
|
||||
* 渲染搜索项目数据中间段
|
||||
*/
|
||||
function searchRenderMiddle(title, searchName) {
|
||||
return title.substring(
|
||||
title.toLowerCase().indexOf(searchName.toLowerCase()),
|
||||
title.toLowerCase().indexOf(searchName.toLowerCase()) + searchName.length,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* 渲染搜索项目数据结束段
|
||||
*/
|
||||
function searchRenderEnd(title, searchName) {
|
||||
return title.substring(
|
||||
title.toLowerCase().indexOf(searchName.toLowerCase()) + searchName.length,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* 展开/收起节点时触发
|
||||
*/
|
||||
function onExpand(keys) {
|
||||
expandedKeys.value = keys
|
||||
autoExpandParent.value = false
|
||||
}
|
||||
/**
|
||||
* 点击复选框触发
|
||||
*/
|
||||
function onCheck(key) {
|
||||
if (checkStrictly.value) {
|
||||
checkedKeys.value = key.checked
|
||||
} else {
|
||||
checkedKeys.value = key
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 展开全部
|
||||
*/
|
||||
function expandAll() {
|
||||
expandedKeys.value = allTreeKeys.value
|
||||
}
|
||||
/**
|
||||
* 合并全部
|
||||
*/
|
||||
function closeAll() {
|
||||
expandedKeys.value = []
|
||||
}
|
||||
/**
|
||||
* 全选
|
||||
*/
|
||||
function checkALL() {
|
||||
checkedKeys.value = allTreeKeys.value
|
||||
}
|
||||
/**
|
||||
* 全不选
|
||||
*/
|
||||
function cancelCheckALL() {
|
||||
checkedKeys.value = []
|
||||
}
|
||||
/**
|
||||
* 终端切换
|
||||
*/
|
||||
function clientSwitch(item) {
|
||||
clientCode.value = item
|
||||
initAssign()
|
||||
}
|
||||
defineExpose({ init })
|
||||
</script>
|
||||
<style scoped></style>
|
11
src/views/iam/role/RolePathModal.vue
Normal file
11
src/views/iam/role/RolePathModal.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
112
src/views/iam/role/RolePerm.api.ts
Normal file
112
src/views/iam/role/RolePerm.api.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { defHttp } from '@/utils/http/axios'
|
||||
import { Result } from '#/axios'
|
||||
import { unref } from 'vue'
|
||||
import { MenuTree } from '@/views/iam/perm/menu/Menu.api'
|
||||
import { PermPathTree } from '@/views/iam/perm/path/PermPath.api'
|
||||
import { PermCodeTree } from '@/views/iam/perm/code/PermCode.api'
|
||||
|
||||
/**
|
||||
* 添加角色菜单关联关系
|
||||
*/
|
||||
export function saveRoleMenu(obj) {
|
||||
return defHttp.post<Result<void>>({
|
||||
url: '/role/menu/save',
|
||||
data: unref(obj),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定角色下的菜单权限树
|
||||
*/
|
||||
export function treeByRoleMenu(roleId, clientCode) {
|
||||
return defHttp.get<Result<MenuTree[]>>({
|
||||
url: '/role/menu/treeByRole',
|
||||
params: {
|
||||
roleId: unref(roleId),
|
||||
clientCode: unref(clientCode),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前角色已经选择的菜单id
|
||||
*/
|
||||
export function findIdsByRoleMenu(roleId, clientCode) {
|
||||
return defHttp.get<Result<string[]>>({
|
||||
url: '/role/menu/findIdsByRole',
|
||||
params: {
|
||||
roleId: unref(roleId),
|
||||
clientCode: unref(clientCode),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加角色请求路径关联关系
|
||||
*/
|
||||
export function saveRolePath(obj) {
|
||||
return defHttp.post<Result<void>>({
|
||||
url: '/role/path/save',
|
||||
data: unref(obj),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定角色下的请求路径权限树
|
||||
*/
|
||||
export function treeByRolePath(roleId, clientCode) {
|
||||
return defHttp.get<Result<PermPathTree[]>>({
|
||||
url: '/role/path/treeByRole',
|
||||
params: {
|
||||
roleId: unref(roleId),
|
||||
clientCode: unref(clientCode),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前角色已经选择的请求路径id
|
||||
*/
|
||||
export function findIdsByRolePath(roleId, clientCode) {
|
||||
return defHttp.get<Result<string[]>>({
|
||||
url: '/role/path/findIdsByRole',
|
||||
params: {
|
||||
roleId: unref(roleId),
|
||||
clientCode: unref(clientCode),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加角色权限码关联关系
|
||||
*/
|
||||
export function saveRoleCode(obj) {
|
||||
return defHttp.post<Result<void>>({
|
||||
url: '/role/code/save',
|
||||
data: unref(obj),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定角色下的角色权限码树
|
||||
*/
|
||||
export function treeByRoleCode(roleId) {
|
||||
return defHttp.get<Result<PermCodeTree[]>>({
|
||||
url: '/role/code/treeByRole',
|
||||
params: {
|
||||
roleId: unref(roleId),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前角色已经选择的角色权限码id
|
||||
*/
|
||||
export function findIdsByRoleCode(roleId) {
|
||||
return defHttp.get<Result<string[]>>({
|
||||
url: '/role/code/findIdsByRole',
|
||||
params: {
|
||||
roleId: unref(roleId),
|
||||
},
|
||||
})
|
||||
}
|
@@ -4,8 +4,7 @@
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="flex-1">
|
||||
<a :href="GITHUB_URL" target="_blank">{{ name }}</a>
|
||||
是一个基于Vue3.0、Vite、 Ant-Design-Vue 、TypeScript
|
||||
的后台解决方案,目标是为中大型项目开发,提供现成的开箱解决方案及丰富的示例,原则上不会限制任何代码用于商用。
|
||||
是DaxPay多商户支付管理系统的前端项目,基于Vue3.0、Vite、 Ant-Design-Vue 、TypeScript实现。
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
2
types/vue-router.d.ts
vendored
2
types/vue-router.d.ts
vendored
@@ -26,7 +26,7 @@ declare module 'vue-router' {
|
||||
// 路由是否已动态添加 Whether the route has been dynamically added
|
||||
hideBreadcrumb?: boolean
|
||||
// 隐藏子菜单 Hide submenu
|
||||
hideChildrenInMenu?: boolean
|
||||
hideChildrenMenu?: boolean
|
||||
// 携带参数 Carrying parameters
|
||||
carryParam?: boolean
|
||||
// 内部用于标记单层菜单 Used internally to mark single-level menus
|
||||
|
Reference in New Issue
Block a user