feat 菜单和各类权限处理

This commit is contained in:
DaxPay
2024-07-10 20:19:40 +08:00
parent 082fc22c68
commit 2119580b5e
52 changed files with 3171 additions and 108 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -8,7 +8,7 @@ export interface PermMenu {
effect: boolean
icon: string
hidden: boolean
hideChildrenInMenu: boolean
hideChildrenMenu: boolean
component: string
path: string
iframeUrl: string

View File

@@ -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))
}
})

View File

@@ -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;

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}
/**

View File

@@ -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),

View File

@@ -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>

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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()) {

View File

@@ -8,7 +8,7 @@ const about: AppRouteModule = {
component: LAYOUT,
redirect: '/about/index',
meta: {
hideChildrenInMenu: true,
hideChildrenMenu: true,
icon: 'simple-icons:aboutdotme',
title: '关于',
orderNo: 100000,

View File

@@ -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]

View File

@@ -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
}

View File

@@ -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,
// 是否显示搜索按钮

View File

@@ -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/'

View File

@@ -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
View 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
})
}

View File

@@ -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(

View File

@@ -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 ''
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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({

View File

@@ -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)
})

View File

@@ -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)
}
// 删除

View 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
}

View 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>

View 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>

View 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[]
}

View 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>

View 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>

View 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[]
}

View 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>

View 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>

View 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[]
}

View 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>

View 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>

View 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[]
}

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="less">
</style>

View 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),
},
})
}

View File

@@ -4,8 +4,7 @@
<div class="flex justify-between items-center">
<span class="flex-1">
<a :href="GITHUB_URL" target="_blank">{{ name }}</a>
一个基于Vue3.0Vite Ant-Design-Vue TypeScript
的后台解决方案目标是为中大型项目开发,提供现成的开箱解决方案及丰富的示例,原则上不会限制任何代码用于商用
DaxPay多商户支付管理系统的前端项目基于Vue3.0Vite Ant-Design-Vue TypeScript实现
</span>
</div>
</template>

View File

@@ -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