feat 菜单管理移植

This commit is contained in:
xxm
2022-10-14 23:48:20 +08:00
parent c420ec5be1
commit a434ec8bdb
16 changed files with 440 additions and 31 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 894 B

After

Width:  |  Height:  |  Size: 723 B

View File

@@ -5,6 +5,7 @@ export enum ResultEnum {
SUCCESS = 0, SUCCESS = 0,
ERROR = -1, ERROR = -1,
TIMEOUT = 401, TIMEOUT = 401,
NOT_LOGIN = 10004,
TYPE = 'success', TYPE = 'success',
} }

View File

@@ -89,7 +89,7 @@
listenerRouteChange((route) => { listenerRouteChange((route) => {
const { name } = route const { name } = route
console.log(route) // console.log(route)
if (name === REDIRECT_NAME || !route || !userStore.getToken) { if (name === REDIRECT_NAME || !route || !userStore.getToken) {
return return
} }

View File

@@ -25,7 +25,8 @@ export function createPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
if ( if (
from.path === ROOT_PATH && from.path === ROOT_PATH &&
to.path === PageEnum.BASE_HOME && to.path === PageEnum.BASE_HOME
&&
// TODO 没有用户首页配置这个字段 // TODO 没有用户首页配置这个字段
userStore.getUserInfo.homePath && userStore.getUserInfo.homePath &&
userStore.getUserInfo.homePath !== PageEnum.BASE_HOME userStore.getUserInfo.homePath !== PageEnum.BASE_HOME

View File

@@ -29,7 +29,6 @@ function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
item.component = null item.component = null
} else { } else {
// 内部打开, 开是否是 Iframe 方式 // 内部打开, 开是否是 Iframe 方式
console.log(item.component)
if ((item.component as string).toUpperCase() === 'IFRAME') { if ((item.component as string).toUpperCase() === 'IFRAME') {
// item.meta.frameSrc = item.iframeUrl // item.meta.frameSrc = item.iframeUrl
item.meta.frameSrc = 'https://vvbin.cn/doc-next/' item.meta.frameSrc = 'https://vvbin.cn/doc-next/'

View File

@@ -23,15 +23,7 @@ const dashboard: AppRouteModule = {
title: t('routes.dashboard.analysis'), title: t('routes.dashboard.analysis'),
}, },
}, },
{ ]
path: 'workbench',
name: 'Workbench',
component: () => import('/@/views/dashboard/workbench/index.vue'),
meta: {
title: t('routes.dashboard.workbench'),
},
},
],
} }
export default dashboard export default dashboard

View File

@@ -38,7 +38,7 @@ const setting: ProjectConfig = {
// 灰度迷失 // 灰度迷失
grayMode: false, grayMode: false,
// 色模式 // 色模式
colorWeak: false, colorWeak: false,
// 是否取消菜单,顶部,多选项卡页面显示,对于可能嵌入其他系统 // 是否取消菜单,顶部,多选项卡页面显示,对于可能嵌入其他系统
@@ -117,12 +117,13 @@ const setting: ProjectConfig = {
mixSideFixed: false, mixSideFixed: false,
}, },
// Multi-label // 多标签配置
multiTabsSetting: { multiTabsSetting: {
// 缓存
cache: false, cache: false,
// Turn on // Turn on
show: true, show: true,
// Is it possible to drag and drop sorting tabs // 是否可以拖放排序标签
canDrag: true, canDrag: true,
// Turn on quick actions // Turn on quick actions
showQuick: true, showQuick: true,

View File

@@ -22,6 +22,7 @@ import { useMessage } from '/@/hooks/web/useMessage'
import { PageEnum } from '/@/enums/pageEnum' import { PageEnum } from '/@/enums/pageEnum'
import { getAppEnvConfig } from '/@/utils/env' import { getAppEnvConfig } from '/@/utils/env'
import { PermMenu } from '/@/api/sys/model/menuModel' import { PermMenu } from '/@/api/sys/model/menuModel'
import dashboard from '/@/router/routes/modules/dashboard'
interface PermissionState { interface PermissionState {
// Permission code list // Permission code list
@@ -118,7 +119,7 @@ export const usePermissionStore = defineStore({
* 转换权限菜单为系统中的菜单 * 转换权限菜单为系统中的菜单
*/ */
convertMenus(permMenus: PermMenu[]): AppRouteRecordRaw[] { convertMenus(permMenus: PermMenu[]): AppRouteRecordRaw[] {
return permMenus.map((o) => { return permMenus?.map((o) => {
const menu = { const menu = {
name: o.name, name: o.name,
path: o.path, path: o.path,
@@ -136,8 +137,7 @@ export const usePermissionStore = defineStore({
}, },
children: this.convertMenus(o.children), children: this.convertMenus(o.children),
} as AppRouteRecordRaw } as AppRouteRecordRaw
if (o.component.toUpperCase()){ if (o.component.toUpperCase()) {
} }
return menu return menu
}) })
@@ -228,12 +228,9 @@ export const usePermissionStore = defineStore({
// 动态引入组件 // 动态引入组件
routeList = transformObjToRoute(routeList) routeList = transformObjToRoute(routeList)
console.log(routeList)
// 后台路由到菜单结构 // 后台路由到菜单结构
const backMenuList = transformRouteToMenu(routeList) const backMenuList = transformRouteToMenu(routeList)
this.setBackMenuList(backMenuList) this.setBackMenuList(backMenuList)
// 删除 meta.ignoreRoute 项 // 删除 meta.ignoreRoute 项
routeList = filter(routeList, routeRemoveIgnoreFilter) routeList = filter(routeList, routeRemoveIgnoreFilter)
routeList = routeList.filter(routeRemoveIgnoreFilter) routeList = routeList.filter(routeRemoveIgnoreFilter)
@@ -244,6 +241,7 @@ export const usePermissionStore = defineStore({
} }
routes.push(ERROR_LOG_ROUTE) routes.push(ERROR_LOG_ROUTE)
routes.push(dashboard)
patchHomeAffix(routes) patchHomeAffix(routes)
return routes return routes
}, },

View File

@@ -58,10 +58,11 @@ const transform: AxiosTransform = {
// 在此处根据自己项目的实际情况对不同的code执行不同的操作 // 在此处根据自己项目的实际情况对不同的code执行不同的操作
// 如果不希望中断当前请求请return数据否则直接抛出异常即可 // 如果不希望中断当前请求请return数据否则直接抛出异常即可
let timeoutMsg = '' let timeoutMsg = ''
const userStore = useUserStoreWithOut()
switch (code) { switch (code) {
case ResultEnum.TIMEOUT: case ResultEnum.TIMEOUT:
case ResultEnum.NOT_LOGIN:
timeoutMsg = '登录超时,请重新登录!' timeoutMsg = '登录超时,请重新登录!'
const userStore = useUserStoreWithOut()
userStore.setToken(undefined) userStore.setToken(undefined)
userStore.logout(true) userStore.logout(true)
break break

View File

@@ -39,8 +39,9 @@
<a href="javascript:" @click="edit(row)">编辑</a> <a href="javascript:" @click="edit(row)">编辑</a>
</span> </span>
<a-divider type="vertical" /> <a-divider type="vertical" />
<a-popconfirm title="是否删除" @confirm="remove(row)" okText="是" cancelText="否"> <a-popconfirm :disabled="row.system" title="是否删除" @confirm="remove(row)" okText="是" cancelText="否">
<a href="javascript:" style="color: red">删除</a> <a href="javascript:" v-if="!row.system" style="color: red">删除</a>
<a href="javascript:" v-else disabled>删除</a>
</a-popconfirm> </a-popconfirm>
</template> </template>
</vxe-column> </vxe-column>
@@ -67,7 +68,6 @@
import { STRING } from '/@/components/Bootx/Query/SuperQueryCode' import { STRING } from '/@/components/Bootx/Query/SuperQueryCode'
import { FormEditType } from '/@/enums/formTypeEnum' import { FormEditType } from '/@/enums/formTypeEnum'
import { useMessage } from '/@/hooks/web/useMessage' import { useMessage } from '/@/hooks/web/useMessage'
import { getAppEnvConfig } from "/@/utils/env";
// 使用hooks // 使用hooks
const { handleTableChange, pageQueryResHandel, resetQueryParams, pagination, pages, model, loading } = useTablePage(queryPage) const { handleTableChange, pageQueryResHandel, resetQueryParams, pagination, pages, model, loading } = useTablePage(queryPage)

View File

@@ -46,7 +46,8 @@
</span> </span>
<a-divider type="vertical" /> <a-divider type="vertical" />
<a-popconfirm title="是否删除" @confirm="remove(row)" okText="是" cancelText="否"> <a-popconfirm title="是否删除" @confirm="remove(row)" okText="是" cancelText="否">
<a href="javascript:" style="color: red">删除</a> <a href="javascript:" v-if="!row.system" style="color: red">删除</a>
<a href="javascript:" v-else disabled>删除</a>
</a-popconfirm> </a-popconfirm>
</template> </template>
</vxe-column> </vxe-column>
@@ -66,13 +67,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { del, page } from './LoginType.api' import { del, LoginType, page } from "./LoginType.api";
import useTablePage from '/@/hooks/bootx/useTablePage' import useTablePage from '/@/hooks/bootx/useTablePage'
import LoginTypeEdit from './LoginTypeEdit.vue' import LoginTypeEdit from './LoginTypeEdit.vue'
import BQuery from '/@/components/Bootx/Query/BQuery.vue' import BQuery from '/@/components/Bootx/Query/BQuery.vue'
import { FormEditType } from '/@/enums/formTypeEnum' import { FormEditType } from '/@/enums/formTypeEnum'
import { useMessage } from '/@/hooks/web/useMessage' import { useMessage } from '/@/hooks/web/useMessage'
import { STRING } from '/@/components/Bootx/Query/SuperQueryCode' import { STRING } from '/@/components/Bootx/Query/SuperQueryCode'
import { BaseEntity } from "/#/web";
// 使用hooks // 使用hooks
const { handleTableChange, pageQueryResHandel, resetQueryParams, pagination, pages, model, loading } = useTablePage(queryPage) const { handleTableChange, pageQueryResHandel, resetQueryParams, pagination, pages, model, loading } = useTablePage(queryPage)
@@ -102,11 +104,11 @@
loginTypeEdit.value.init(null, FormEditType.Add) loginTypeEdit.value.init(null, FormEditType.Add)
} }
// 查看 // 查看
function edit(record) { function edit(record: LoginType) {
loginTypeEdit.value.init(record.id, FormEditType.Edit) loginTypeEdit.value.init(record.id, FormEditType.Edit)
} }
// 查看 // 查看
function show(record) { function show(record: LoginType) {
loginTypeEdit.value.init(record.id, FormEditType.Show) loginTypeEdit.value.init(record.id, FormEditType.Show)
} }

View File

@@ -0,0 +1,97 @@
import { defHttp } from '/@/utils/http/axios'
import { PageResult, Result } from '/#/axios'
import { BaseEntity } from '/#/web'
/**
* 树列表
*/
export const menuTree = (clientCode: string) => {
return defHttp.get<Result<Array<Menu>>>({
url: '/perm/menu/menuTree',
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: obj,
})
}
/**
* 更新
*/
export const update = (obj: Menu) => {
return defHttp.post({
url: '/perm/menu/update',
data: obj,
})
}
/**
* 删除
*/
export const del = (id) => {
return defHttp.delete({
url: '/perm/menu/delete',
params: { id },
})
}
/**
* 权限_菜单
*/
export interface Menu extends BaseEntity {
// 终端code
clientCode?: string
// 父id
parentId: number | null
// 菜单名称
title: string
// 路由名称
name: string
// 菜单权限编码
permCode: string
// 菜单图标
icon: string
// 是否隐藏
hidden: boolean
// 是否隐藏子菜单
hideChildrenInMenu: boolean
// 组件
component: string
// 组件名字
componentName: string
// 路径
path: string
// 菜单跳转地址(重定向)
redirect: string
// 菜单排序
sortNo: number
// 类型0一级菜单1子菜单 2按钮权限
menuType: number
// 是否缓存页面
keepAlive: boolean
// 是否外部打开方式
targetOutside: boolean
// 隐藏的标题内容
hiddenHeaderContent: boolean
// 系统菜单
admin?: boolean
// 描述
remark?: string
}

View File

@@ -0,0 +1,128 @@
<template>
<a-drawer :title="title" width="50%" :mask-closable="showable" @close="handleCancel" :visible="visible" :confirmLoading="confirmLoading">
<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="菜单名称" name="title">
<a-input v-model:value="form.title" :disabled="showable" placeholder="请输入title" />
</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="path">
<a-input v-model:value="form.path" :disabled="showable" placeholder="请输入访问路径" />
</a-form-item>
<a-form-item label="组件名称" name="component">
<a-input v-model:value="form.component" :disabled="showable" placeholder="请输入组件名称" />
</a-form-item>
<a-form-item label="菜单跳转地址(重定向)" name="redirect">
<a-input v-model:value="form.redirect" :disabled="showable" placeholder="请输入重定向" />
</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>
</a-spin>
</a-drawer>
</template>
<script lang="ts" setup>
import useFormEdit from '/@/hooks/bootx/useFormEdit'
import { nextTick, reactive, ref } from 'vue'
import { Rule } from 'ant-design-vue/lib/form'
import { $ref } from 'vue/macros'
import { add, get, Menu, update } from '/@/views/modules/system/menu/Menu.api'
import { FormEditType } from '/@/enums/formTypeEnum'
const {
initFormModel,
handleCancel,
search,
labelCol,
wrapperCol,
title,
modalWidth,
confirmLoading,
visible,
editable,
showable,
formEditType,
} = useFormEdit()
const formRef = ref()
let form = $ref({
parentId: null,
title: '',
name: '',
path: '',
component: '',
redirect: '',
sortNo: 0,
icon: '',
hidden: false,
hideChildrenInMenu: false,
keepAlive: true,
hiddenHeaderContent: false,
targetOutside: false,
menuType: 0,
} as Menu)
const rules = reactive({
title: [{ required: true, message: '请输入菜单或权限名称' }],
name: [{ required: true, message: '请输入路由名称' }],
path: [{ required: true, message: '请输入菜单路径' }],
url: [{ required: true, message: '请输入菜单路径' }],
} as Record<string, Rule[]>)
// 事件
const emits = defineEmits(['ok'])
// 入口
function init(id, editType: FormEditType) {
console.log(id)
initFormModel(id, editType)
resetForm()
getInfo(id, editType)
}
// 获取信息
function getInfo(id, editType: FormEditType) {
// this.initLoginTypes()
if ([FormEditType.Edit, FormEditType.Show].includes(editType)) {
confirmLoading.value = true
get(id).then(({ data }) => {
form = 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)
} else if (formEditType.value === FormEditType.Edit) {
await update(form)
}
confirmLoading.value = false
handleCancel()
emits('ok')
})
}
// 重置表单的校验
function resetForm() {
nextTick(() => {
formRef.value!.resetFields()
})
}
defineExpose({
init,
})
</script>
<style scoped></style>

View File

@@ -0,0 +1,189 @@
<template>
<div>
<div class="m-3 p-3 pt-5 bg-white">
<a-form class="query" layout="inline">
<a-row :gutter="10">
<a-col :md="6" :sm="24">
<a-form-item label="终端">
<a-select v-model:value="clientCode" @change="init" :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 zoom :refresh="{ query: init }">
<template #buttons>
<a-button type="primary" @click="add()"> 新建 </a-button>
<a-button style="margin-left: 8px" @click="allTreeExpand(true)">展开所有</a-button>
<a-button style="margin-left: 8px" @click="allTreeExpand(false)">关闭所有</a-button>
</template>
</vxe-toolbar>
<vxe-table
resizable
:stripe="false"
show-overflow
border="inner"
ref="xTree"
:loading="loading"
:tree-config="{ children: 'children' }"
:data="tableData"
>
<vxe-column field="title" title="菜单名称" tree-node />
<vxe-column field="name" title="路由名称" />
<vxe-column field="menuType" title="菜单类型">
<template #default="{ row }">
<span v-show="String(row.menuType) === '0'">一级菜单</span>
<span v-show="String(row.menuType) === '1'">子菜单</span>
<span v-show="String(row.menuType) === '2'">按钮/权限</span>
</template>
</vxe-column>
<vxe-column field="path" title="请求路径" />
<vxe-column field="sortNo" title="排序" :visible="false" />
<vxe-column field="component" title="组件" />
<vxe-column field="icon" title="图标" :visible="false">
<template #default="{ row }">
<div v-if="row.icon !== ''">
<!-- <a-icon :type="row.icon" />-->
</div>
</template>
</vxe-column>
<vxe-column title="操作" fixed="right" width="240" :showOverflow="false">
<template #default="{ row }">
<a href="javascript:" @click="show(row)">查看</a>
<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 href="javascript:" @click="resourceList(row)">权限资源</a>
<a-divider type="vertical" />
<a-dropdown>
<a class="ant-dropdown-link"> 更多 </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-popconfirm title="是否删除菜单或权限" @confirm="remove(row)" okText="是" cancelText="否">
<a href="javascript:" v-if="!row.admin" style="color: red">删除</a>
<a href="javascript:" v-else disabled>删除</a>
</a-popconfirm>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</vxe-column>
</vxe-table>
<menu-edit ref="menuEdit" @ok="init" />
</div>
</div>
</template>
<script lang="ts" setup>
import { getAppEnvConfig } from '/@/utils/env'
import { $ref } from 'vue/macros'
import { nextTick, onMounted, ref } from 'vue'
import { Client, findAll } from '/@/views/modules/system/client/Client.api'
import XEUtils from 'xe-utils'
import { menuTree, Menu } from './Menu.api'
import { FormEditType } from '/@/enums/formTypeEnum'
import MenuEdit from './MenuEdit.vue'
const { VITE_GLOB_APP_CLIENT } = getAppEnvConfig()
let clientCode = $ref(VITE_GLOB_APP_CLIENT)
let searchName = $ref()
let loading = $ref(false)
let treeExpand = $ref(false)
let clients = $ref([] as Client[])
let remoteTableData = $ref([] as Menu[])
let tableData = $ref([] as Menu[])
let xTree = ref()
let menuEdit = ref()
onMounted(() => {
initClients()
init()
})
async function initClients() {
const { data } = await findAll()
clients = data
}
async function init() {
loading = true
menuTree(clientCode).then((res) => {
remoteTableData = res.data
search()
loading = false
})
}
function add() {
menuEdit.value.init(null, FormEditType.Add)
}
function edit(record: Menu) {
menuEdit.value.init(record.id, FormEditType.Edit)
}
function show(record: Menu) {
menuEdit.value.init(record.id, FormEditType.Show)
}
/**
* 搜索
*/
function search() {
const search = XEUtils.toValueString(searchName).trim().toLowerCase()
if (search) {
const searchProps = ['name', 'title', 'path', 'component']
tableData = XEUtils.searchTree(remoteTableData, (item) =>
searchProps.some((key) => XEUtils.toValueString(item[key]).toLowerCase().indexOf(search) > -1),
)
// 搜索状态默认展开
treeExpand = true
} else {
tableData = remoteTableData
}
nextTick(() => {
xTree.value.setAllTreeExpand(treeExpand)
})
}
/**
* 展开or关闭
*/
function allTreeExpand(value) {
nextTick(() => {
xTree.value.setAllTreeExpand(treeExpand)
})
treeExpand = value
}
</script>
<style lang="less" scoped>
.query {
.ant-form-item {
margin-bottom: 8px;
}
.ant-row {
width: 100%;
}
}
</style>

2
types/config.d.ts vendored
View File

@@ -160,5 +160,5 @@ export interface GlobEnvConfig {
// Upload url // Upload url
VITE_GLOB_UPLOAD_URL?: string VITE_GLOB_UPLOAD_URL?: string
// 终端类型 // 终端类型
VITE_GLOB_APP_CLIENT?: string VITE_GLOB_APP_CLIENT: string
} }

2
types/web.d.ts vendored
View File

@@ -35,5 +35,5 @@ export interface TablePageModel<T = any> {
* 基础实体对象 * 基础实体对象
*/ */
export interface BaseEntity { export interface BaseEntity {
id: number | null id?: number | null | undefined
} }