增加 系统管理-菜单管理 页面

This commit is contained in:
wyy 2023-11-09 16:51:16 +08:00
parent 662ec8510f
commit a7d18b2d5e
6 changed files with 591 additions and 51 deletions

View File

@ -96,7 +96,7 @@ router.beforeEach((to, from, next) => {
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
sessionStorage.setItem('Authorization', JSON.stringify(data.authorities || '[]'))
sessionStorage.setItem('Authorities', JSON.stringify(data.authorities || '[]'))
fnAddDynamicMenuRoutes(data.menuList)
router.options.isAddDynamicMenuRoutes = true
const rList = []
@ -168,7 +168,7 @@ function fnCurrentRouteType (route, globalRoutes = []) {
*/
function fnAddDynamicMenuRoutes (menuList = [], routes = []) {
let temp = []
const modules = import.meta.glob('../views/modules/**/**.vue')
const modules = import.meta.glob('../views/modules/**/index.vue')
for (let i = 0; i < menuList.length; i++) {
if (menuList[i].list && menuList[i].list.length >= 1) {
temp = temp.concat(menuList[i].list)
@ -193,7 +193,7 @@ function fnAddDynamicMenuRoutes (menuList = [], routes = []) {
route.meta.iframeUrl = menuList[i].url
} else {
try {
route.component = modules[`../views/modules/${menuList[i].url}.vue`] || null
route.component = modules[`../views/modules/${menuList[i].url}/index.vue`] || null
} catch (e) {}
}
routes.push(route)

View File

@ -20,7 +20,6 @@ const http = axios.create({
http.interceptors.request.use(
config => {
config.headers.Authorization = cookie.get('Authorization') // 请求头带上token
config.headers.locale = localStorage.getItem('b2cLang') || 'zh_CN'
// 只针对get方式进行序列化
if (config.method === 'get' || config.method === 'GET') {
config.paramsSerializer = function (params) {
@ -68,7 +67,7 @@ http.interceptors.response.use(
// eslint-disable-next-line no-console
console.error('============== 请求异常 ==============', '\n', `接口地址: ${response.config.url.replace(import.meta.env.VITE_APP_BASE_API, '')}`, '\n', `异常信息: ${res}`, '\n', '============== 请求异常 end ==========')
ElMessage({
message: $t('utils.serverErr'),
message: '服务器出了点小差,请稍后再试',
type: 'error',
duration: 1.5 * 1000,
customClass: 'element-error-message-zindex'
@ -94,7 +93,7 @@ http.interceptors.response.use(
break
case 405:
ElMessage({
message: $t('utils.requestErr'),
message: 'http请求方式有误',
type: 'error',
duration: 1500,
customClass: 'element-error-message-zindex'
@ -102,7 +101,7 @@ http.interceptors.response.use(
break
case 500:
ElMessage({
message: $t('utils.serverErr'),
message: '服务器出了点小差,请稍后再试',
type: 'error',
duration: 1500,
customClass: 'element-error-message-zindex'
@ -110,7 +109,7 @@ http.interceptors.response.use(
break
case 501:
ElMessage({
message: $t('utils.serverNoSupp'),
message: '服务器不支持当前请求所需要的某个功能',
type: 'error',
duration: 1500,
customClass: 'element-error-message-zindex'

View File

@ -14,7 +14,7 @@ export function getUUID () {
* @param {*} key
*/
export function isAuth (key) {
const authorities = JSON.parse(sessionStorage.getItem('b2cAuthorities') || '[]')
const authorities = JSON.parse(sessionStorage.getItem('Authorities') || '[]')
if (authorities.length) {
for (const i in authorities) {
const element = authorities[i]
@ -33,3 +33,57 @@ export function clearLoginInfo () {
cookie.remove('Authorization')
router.options.isAddDynamicMenuRoutes = false
}
/**
* 树形数据转换
* @param {*} data
* @param {*} id
* @param {*} pid
*/
export function treeDataTranslate (data, id = 'id', pid = 'parentId') {
const res = []
const temp = {}
for (let i = 0; i < data.length; i++) {
temp[data[i][id]] = data[i]
}
for (let k = 0; k < data.length; k++) {
if (temp[data[k][pid]] && data[k][id] !== data[k][pid]) {
if (!temp[data[k][pid]].children) {
temp[data[k][pid]].children = []
}
if (!temp[data[k][pid]]._level) {
temp[data[k][pid]]._level = 1
}
data[k]._level = temp[data[k][pid]]._level + 1
temp[data[k][pid]].children.push(data[k])
} else {
res.push(data[k])
}
}
return res
}
function idListFromTree (data, val, res = [], id = 'id', children = 'children') {
for (let i = 0; i < data.length; i++) {
const element = data[i]
if (element[children]) {
if (idListFromTree(element[children], val, res, id, children)) {
res.push(element[id])
return true
}
}
if (element[id] === val) {
res.push(element[id])
return true
}
}
}
/**
* 将数组中的parentId列表取出倒序排列
*/
// eslint-disable-next-line no-unused-vars
export function idList (data, val, id = 'id', children = 'children') {
const res = []
idListFromTree(data, val, res, id)
return res
}

View File

@ -146,40 +146,49 @@ const getCaptcha = () => {
background: url('../../../assets/img/login-bg.png') no-repeat;
background-size: cover;
position: fixed;
}
.login .login-box {
position: absolute;
left: 50%;
transform: translateX(-50%);
height: 100%;
padding-top: 10%;
}
.login .login-box .top {
margin-bottom: 30px;
text-align: center;
}
.login .login-box .top .logo {
font-size: 0;
max-width: 50%;
margin: 0 auto;
}
.login .login-box .top :deep(.company) {
font-size: 16px;
margin-top: 10px;
}
.login .login-box .mid {
font-size: 14px;
}
.login .login-box .mid .item-btn {
margin-top: 20px;
}
.login .login-box .mid .item-btn input {
border: 0;
width: 100%;
height: 40px;
background: #1f87e8;
color: #fff;
border-radius: 3px;
.login-box {
position: absolute;
left: 50%;
transform: translateX(-50%);
height: 100%;
padding-top: 10%;
.top {
margin-bottom: 30px;
text-align: center;
.logo {
font-size: 0;
max-width: 50%;
margin: 0 auto;
}
&:deep(.company) {
font-size: 16px;
margin-top: 10px;
}
}
.mid {
font-size: 14px;
.item-btn {
width: 410px;
margin-top: 20px;
input {
border: 0;
width: 100%;
height: 40px;
background: #1f87e8;
color: #fff;
border-radius: 3px;
}
}
}
.bottom {
position: absolute;
bottom: 10%;
width: 100%;
color: #999;
font-size: 12px;
text-align: center;
}
}
}
.info {
width: 410px;
@ -187,12 +196,4 @@ const getCaptcha = () => {
:deep(.login-captcha) {
height: 40px;
}
.login .login-box .bottom {
position: absolute;
bottom: 10%;
width: 100%;
color: #999;
font-size: 12px;
text-align: center;
}
</style>

View File

@ -0,0 +1,290 @@
<template>
<el-dialog
v-model="visible"
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
>
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
>
<el-form-item
label="类型"
prop="type"
>
<el-radio-group v-model="dataForm.type">
<el-radio
v-for="(type, index) in dataForm.typeList"
:key="index"
:label="index"
>
{{ type }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
:label="dataForm.typeList[dataForm.type] + '名称'"
prop="name"
>
<el-input
v-model="dataForm.name"
:placeholder="dataForm.typeList[dataForm.type] + '名称'"
/>
</el-form-item>
<el-form-item label="上级菜单">
<el-cascader
v-model="selectedMenu"
expand-trigger="hover"
:options="menuList"
:props="menuListTreeProps"
change-on-select
@change="handleSelectMenuChange"
/>
</el-form-item>
<el-form-item
v-if="dataForm.type === 1"
label="菜单路由"
prop="url"
>
<el-input
v-model="dataForm.url"
placeholder="菜单路由"
/>
</el-form-item>
<el-form-item
v-if="dataForm.type !== 0"
label="授权标识"
prop="perms"
>
<el-input
v-model="dataForm.perms"
placeholder="多个用逗号分隔, 如: user:list,user:create"
/>
</el-form-item>
<el-form-item
v-if="dataForm.type !== 2"
label="排序号"
prop="orderNum"
>
<el-input-number
v-model="dataForm.orderNum"
controls-position="right"
:min="0"
label="排序号"
/>
</el-form-item>
<el-form-item
v-if="dataForm.type !== 2"
label="菜单图标"
prop="icon"
>
<el-row>
<el-col :span="22">
<el-input
ref="iconInputRef"
v-model="dataForm.icon"
:virtual-ref="iconListPopoverRef"
placeholder="菜单图标名称"
clearable
/>
<el-popover
ref="iconListPopoverRef"
style="width: 390px"
:virtual-ref="iconInputRef"
placement="bottom-start"
trigger="click"
:popper-style="iconPopoverClass"
virtual-triggering
>
<el-button
v-for="(item, index) in iconList"
:key="index"
style="padding: 8px; margin: 8px 0 0 8px"
:class="{ 'is-active': item === dataForm.icon }"
@click="iconActiveHandle(item)"
>
<svg-icon
:icon-class="`${item}`"
/>
</el-button>
</el-popover>
</el-col>
<el-col
:span="2"
class="icon-list__tips"
>
<el-tooltip
placement="top"
effect="light"
>
<template #content>
<div>全站推荐使用SVG Sprite, 详细请参考:icons/index.js 描述</div>
</template>
<i class="el-icon-warning" />
</el-tooltip>
</el-col>
</el-row>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
@click="onSubmit()"
>
确定
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { treeDataTranslate, idList } from '@/utils'
import { ElMessage } from 'element-plus'
const emit = defineEmits(['refreshDataList'])
const iconInputRef = ref(null)
const iconListPopoverRef = ref(null)
const iconPopoverClass = computed(() => {
return {
width: '396px'
}
})
const visible = ref(false)
const dataForm = reactive({
id: 0,
type: 1,
typeList: ['目录', '菜单', '按钮'],
name: '',
parentId: 0,
url: '',
perms: '',
orderNum: 0,
icon: '',
iconList: []
})
const menuList = ref([])
const selectedMenu = ref([])
const menuListTreeProps = {
value: 'menuId',
label: 'name'
}
// eslint-disable-next-line no-unused-vars
const validateUrl = (rule, value, callback) => {
if (dataForm.type === 1 && !/\S/.test(value)) {
callback(new Error('菜单URL不能为空'))
} else {
callback()
}
}
const dataRule = ref({
name: [
{ required: true, message: '菜单名称不能为空', trigger: 'blur' },
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的菜单名称', trigger: 'blur' }
],
url: [
{ validator: validateUrl, trigger: 'blur' }
]
})
onMounted(() => {
onLoadIcons()
})
const iconList = []
//
const onLoadIcons = () => {
const icons = import.meta.glob('@/icons/svg/*.svg')
for (const icon in icons) {
const iconName = icon.split('/src/icons/svg/')[1].split('.svg')[0]
iconList.push(iconName)
}
}
const dataFormRef = ref(null)
const init = (id) => {
dataForm.id = id || 0
http({
url: http.adornUrl('/sys/menu/list'),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
menuList.value = treeDataTranslate(data, 'menuId')
})
.then(() => {
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
})
})
.then(() => {
if (dataForm.id) {
//
http({
url: http.adornUrl(`/sys/menu/info/${dataForm.id}`),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
dataForm.id = data.menuId
dataForm.type = data.type
dataForm.name = data.name
dataForm.parentId = data.parentId
dataForm.url = data.url
dataForm.perms = data.perms
dataForm.orderNum = data.orderNum
dataForm.icon = data.icon
selectedMenu.value = idList(menuList.value, data.parentId, 'menuId', 'children').reverse()
})
} else {
selectedMenu.value = []
}
})
}
defineExpose({ init })
const handleSelectMenuChange = (val) => {
dataForm.parentId = val[val.length - 1]
}
//
const iconActiveHandle = (iconName) => {
dataForm.icon = iconName
}
//
const onSubmit = Debounce(() => {
dataFormRef.value?.validate((valid) => {
if (valid) {
http({
url: http.adornUrl('/sys/menu'),
method: dataForm.id ? 'put' : 'post',
data: http.adornData({
menuId: dataForm.id || undefined,
type: dataForm.type,
name: dataForm.name,
parentId: dataForm.parentId,
url: dataForm.url,
perms: dataForm.perms,
orderNum: dataForm.orderNum,
icon: dataForm.icon
})
})
.then(() => {
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
}
})
})
}
})
})
</script>

View File

@ -0,0 +1,196 @@
<template>
<div class="mod-menu">
<el-form
:inline="true"
:model="dataForm"
>
<el-form-item>
<el-button
v-if="isAuth('sys:menu:save')"
type="primary"
@click="onAddOrUpdate()"
>
新增
</el-button>
</el-form-item>
</el-form>
<el-table
:data="dataList"
border
style="width: 100%;"
row-key="menuId"
>
<el-table-column
prop="name"
header-align="center"
tree-key="menuId"
width="150"
label="名称"
/>
<el-table-column
header-align="center"
align="center"
label="图标"
>
<template #default="scope">
<svg-icon
:icon-class="`icon-${scope.row.icon}`"
/>
</template>
</el-table-column>
<el-table-column
prop="type"
header-align="center"
align="center"
label="类型"
>
<template #default="scope">
<el-tag
v-if="scope.row.type === 0"
>
目录
</el-tag>
<el-tag
v-else-if="scope.row.type === 1"
type="success"
>
菜单
</el-tag>
<el-tag
v-else-if="scope.row.type === 2"
type="info"
>
按钮
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="orderNum"
header-align="center"
align="center"
label="排序号"
/>
<el-table-column
prop="url"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="菜单URL"
>
<template #default="scope">
{{ scope.row.url || '-' }}
</template>
</el-table-column>
<el-table-column
prop="perms"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="授权标识"
>
<template #default="scope">
{{ scope.row.perms || '-' }}
</template>
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作"
>
<template #default="scope">
<el-button
v-if="isAuth('sys:menu:update')"
type="text"
@click="onAddOrUpdate(scope.row.menuId)"
>
修改
</el-button>
<el-button
v-if="isAuth('sys:menu:delete')"
type="text"
@click="onDelete(scope.row.menuId)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
/>
</div>
</template>
<script setup>
import { treeDataTranslate, isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import AddOrUpdate from './add-or-update.vue'
const dataForm = ref({})
onMounted(() => {
getDataList()
})
const dataList = ref([])
/**
* 获取数据列表
*/
const getDataList = () => {
http({
url: http.adornUrl('/sys/menu/table'),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
dataList.value = treeDataTranslate(data, 'menuId')
})
}
const addOrUpdateRef = ref(null)
const addOrUpdateVisible = ref(false)
/**
* 新增 / 修改
* @param id
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
nextTick(() => {
addOrUpdateRef.value?.init(id)
})
}
/**
* 删除
* @param id
*/
const onDelete = (id) => {
ElMessageBox.confirm(`确定对[id=${id}]进行[删除]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
http({
url: http.adornUrl(`/sys/menu/${id}`),
method: 'delete',
data: http.adornData()
}).then(() => {
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
getDataList()
}
})
})
})
}
</script>