1.完成新版菜单管理

This commit is contained in:
猿小天
2023-10-27 16:59:25 +08:00
parent 7159253c6b
commit bd8fef9a04
15 changed files with 570 additions and 206 deletions

View File

@@ -9,7 +9,7 @@ import { request } from '/@/utils/service';
//扩展包
import { FsExtendsEditor,FsExtendsUploader } from '@fast-crud/fast-extends';
import '@fast-crud/fast-extends/dist/style.css';
import { successMessage, successNotification } from '/@/utils/message';
import { ElMessage } from "element-plus";
export default {
async install(app: any, options: any) {
// 先安装ui
@@ -18,9 +18,9 @@ export default {
app.use(FastCrud, {
//i18n, //i18n配置可选默认使用中文具体用法请看demo里的 src/i18n/index.js 文件
// 此处配置公共的dictRequest字典请求
async dictRequest({ dict }: any) {
async dictRequest({ url }: any) {
//根据dict的url异步返回一个字典数组
return await request({ url: dict.url, params: dict.params || {} }).then((res:any)=>{
return await request({ url: url, }).then((res:any)=>{
return res.data
});
},
@@ -41,14 +41,21 @@ export default {
transformRes: ({ res }: any) => {
//将pageRequest的返回数据转换为fast-crud所需要的格式
//return {records,currentPage,pageSize,total};
return { records: res.data, currentPage: res.page, pageSize: res.limit, total: res.total };
if(res.page){
return { records: res.data, currentPage: res.page, pageSize: res.limit, total: res.total };
}else{
return { records: res.data,currentPage: 1, pageSize: res.data.length, total: res.data.length };
}
},
},
form: {
afterSubmit(ctx: any) {
// 增加crud提示
if (ctx.res.code == 2000) {
successNotification(ctx.res.msg);
afterSubmit(ctx:any ) {
const {mode} = ctx
if (mode === "add") {
ElMessage.success({ message: "添加成功" });
} else if (mode === "edit") {
ElMessage.success({ message: "保存成功" });
}
},
},
@@ -100,10 +107,12 @@ export default {
});
},
successHandle(ret) {
console.log(111,ret)
// 上传完成后的结果处理, 此处应返回格式为{url:xxx,key:xxx}
return {
url: getBaseURL() + ret.data.url,
key: ret.data.id
url: ret.data.url,
key: ret.data.id,
...ret.data
};
}
}

View File

@@ -1,11 +1,19 @@
import { toRaw } from 'vue';
import { DictionaryStore } from '/@/stores/dictionary';
/**
* @method 获取指定name字典
/**
* @method 获取指定name字典
*/
export const dictionary = (name: string) => {
export const dictionary = (key: string,value:string|number) => {
const dict = DictionaryStore()
const dictionary = toRaw(dict.data)
return dictionary[name]
}
if(value!==null || value !==''){
for (let item of dictionary[key]) {
if (item.value === value) {
return item.label
}
}
return ''
}
return dictionary[key]
}

View File

@@ -12,5 +12,5 @@ export const scanAndInstallPlugins = (app: any) => {
pluginNames.add(pluginsName);
}
pluginsAll = Array.from(pluginNames);
console.log('已发现插件:', pluginsAll);
console.table('已注册插件:', pluginsAll);
};

View File

@@ -1,8 +1,6 @@
<template>
<el-drawer size="70%" v-model="drawer" direction="rtl" destroy-on-close :before-close="handleClose">
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</el-drawer>
</template>
@@ -25,6 +23,7 @@ const handleClose = (done: () => void) => {
})
.then(() => {
done();
})
.catch(() => {
// catch error

View File

@@ -5,12 +5,19 @@ export const apiPrefix = '/api/system/menu/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
url: apiPrefix+"tree/",
method: 'get',
params: query,
});
}
export function GetChildren(id: InfoReq) {
return request({
url: apiPrefix + id + '/getChildren/',
method: 'get',
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id + '/',

View File

@@ -0,0 +1,364 @@
import * as api from './api';
import { CreateCrudOptionsProps, CreateCrudOptionsRet, dict, useCompute } from '@fast-crud/fast-crud';
const { compute } = useCompute();
import { shallowRef } from "vue";
import IconSelector from "/@/components/IconSelector/index.vue"
export default function ({ crudExpose, onAddCatalog, onAddChildren, onAddButton }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }) => {
form.id = row.id;
await api.UpdateObj(form);
if (row.parent) {
//刷新父节点的状态
reloadTreeChildren(row.parent);
}
};
const delRequest = async ({ row }:any) => {
await api.DelObj(row.id);
if (row.parent) {
//刷新父节点的状态
reloadTreeChildren(row.parent);
}
};
const addRequest = async (context:any) => {
return await api.AddObj(context.form);
};
//刷新父节点状态
function reloadTreeChildren(parent:string|number) {
const data = crudExpose.getBaseTableRef().store.states.treeData;
if (data.value != null) {
const item = data.value[parent];
if (item != null) {
item.loaded = false;
item.expanded = false;
}
}
}
const createFilter = (queryString: string) => {
return (file: any) => {
return file.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1;
};
};
// 获取组件地址
const getCompoent = (queryString: string, cb: any) => {
const files: any = import.meta.glob('@views/**/*.vue');
let fileLists: Array<any> = [];
Object.keys(files).forEach((queryString: string) => {
fileLists.push({
label: queryString.replace(/(\.\/|\.vue)/g, ''),
value: queryString.replace(/(\.\/|\.vue)/g, ''),
});
});
const results = queryString ? fileLists.filter(createFilter(queryString)) : fileLists;
// 统一去掉/src/views/前缀
results.forEach((val) => {
val.label = val.label.replace('/src/views/', '');
val.value = val.value.replace('/src/views/', '');
});
cb(results);
};
// 验证路由地址
const { getFormData } = crudExpose;
const validateWebPath = (rule: any, value: string, callback: Function) => {
let pattern = /^\/.*?/;
let patternUrl = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
const reg = getFormData().is_link ? patternUrl.test(value) : pattern.test(value);
if (reg) {
callback();
} else {
callback(new Error('请输入正确的地址'));
}
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
pagination: {
show: false,
},
form: {
labelWidth: '120px',
row: { gutter: 20 },
// group: {
// groupType: 'tabs', //collapse tabs
// accordion: false,
// groups: {
// catalog: {
// label: '目录',
// icon: 'el-icon-goods',
// columns: ['name', 'icon', 'sort', 'status'],
// },
// menu: {
// label: '菜单',
// icon: 'el-icon-price-tag',
// columns: ['parent', 'name', 'icon', 'is_link', 'web_path', 'component', 'cache', 'visible', 'frame_out', 'sort', 'status'],
// },
// info: {
// label: '按钮',
// collapsed: true, //默认折叠
// icon: 'el-icon-warning-outline',
// columns: ['name', 'component'],
// },
// },
// },
},
table: {
lazy: true,
load: async (row: any, treeNode: unknown, resolve: (date: any[]) => void) => {
//懒加载,更新和删除后,需要刷新父节点的状态,见上方
const obj = await api.GetChildren(row.id);
resolve([...obj.data]);
},
},
columns: {
id: {
title: 'ID',
key: 'id',
type: 'number',
column: {
width: 100,
},
form: {
show: false,
},
},
menu_type: {
title: '类型',
type: 'dict-radio',
dict: dict({
data: [
{ label: '目录', value: 0 },
{ label: '菜单', value: 1 },
{ label: '按钮', value: 2 },
],
}),
form: {
value:0,
valueChange({ form, value, getComponentRef }) {
if (value) {
getComponentRef("parent").reloadDict(); // 执行city的select组件的reloadDict()方法触发“city”重新加载字典
}
}
},
column: {
show: false,
},
},
parent: {
title: '父级',
dict: dict({
prototype: true,
url({form}){
if(form && form.menu_type===1){
return '/api/system/menu/tree/?menu_type=0'
}else{
return `/api/system/menu/tree/?menu_type=1`
}
return undefined
},
label: 'name',
value: 'id',
}),
type: 'dict-select',
form: {
show:compute(({form})=>{
return [1,2].includes(form.menu_type);
}),
rules: [{ required: true, message: '必填项' }],
component: {},
},
column: {
show: false,
},
},
name: {
title: '名称',
search: { show: true },
type: 'text',
form: {
rules: [{ required: true, message: '请输入名称' }],
component: {
placeholder: '请输入名称',
},
},
},
icon: {
title: '图标',
form:{
show:compute(({form})=>{
return [0,1].includes(form.menu_type);
}),
component:{
name: shallowRef(IconSelector),
vModel: "modelValue",
}
},
column: {
component: {
style: 'font-size:18px',
},
},
},
sort: {
title: '排序',
type: 'number',
form: {
value: 1,
show:compute(({form})=>{
return [0,1].includes(form.menu_type);
}),
},
},
is_link: {
title: '外链接',
type: 'dict-switch',
dict: dict({
data: [
{ label: '是', value: true },
{ label: '否', value: false },
],
}),
form: {
value: false,
show:compute(({form})=>{
return [1].includes(form.menu_type);
}),
},
},
web_path: {
title: '路由地址',
form: {
show:compute(({form})=>{
return [1].includes(form.menu_type);
}),
helper: compute(({ form }) => {
return form.is_link ? '请输入http开头的地址' : '浏览器中url的地址,请以/开头';
}),
rules: [{ required: true, message: '请输入路由地址', validator: validateWebPath, trigger: 'blur' }],
component: {
placeholder: '请输入路由地址',
},
},
column: {
show: false,
},
},
component: {
title: '组件地址',
form: {
show: compute(({ form }) => {
return [1,2].includes(form.menu_type)
}),
helper: compute(({ form }) => {
return form.menu_type === 1 ? 'src/views下的文件夹地址' : '按钮权限值是唯一的标识';
}),
rules: [{ required: true, message: '请输入组件地址', trigger: 'blur' }],
component: {
style: {
width: '100%',
},
disabled: compute(({ form }) => {
if(form.is_link&&form.menu_type===1){
form.component ="无"
return form.is_link
}
form.component =null
return false
}),
name: compute(({ form }) => {
return [1,2].includes(form.menu_type)? 'el-autocomplete' : 'el-input';
}),
triggerOnFocus: false,
fetchSuggestions: (query, cb) => {
return getCompoent(query, cb);
},
},
},
column: {
show: false,
},
},
visible: {
title: '侧边可见',
search: { show: true },
type: 'dict-radio',
dict: dict({
data: [
{ label: '是', value: true },
{ label: '否', value: false },
],
}),
form: {
value: true,
show:compute(({form})=>{
return [1].includes(form.menu_type)
}),
},
},
cache: {
title: '是否缓存',
search: { show: true },
type: 'dict-radio',
dict: dict({
data: [
{ label: '是', value: true },
{ label: '否', value: false },
],
}),
form: {
value: false,
show:compute(({form})=>{
return [1].includes(form.menu_type)
}),
},
},
frame_out: {
title: '主框架外展示',
search: { show: true },
type: 'dict-radio',
dict: dict({
data: [
{ label: '是', value: true },
{ label: '否', value: false },
],
}),
form: {
value: false,
show:compute(({form})=>{
return [1].includes(form.menu_type)
}),
},
},
status: {
title: '状态',
search: { show: true },
type: 'dict-radio',
dict: dict({
data: [
{ label: '启用', value: true },
{ label: '禁用', value: false },
],
}),
form: {
value: true,
show:compute(({form})=>{
return [0,1].includes(form.menu_type);
}),
},
},
},
},
};
}

View File

@@ -1,142 +1,124 @@
<template>
<fs-page>
<el-row class="menu-el-row">
<el-col :span="6">
<div class="menu-box menu-left-box">
<MenuTreeCom
ref="menuTreeRef"
:treeData="menuTreeData"
@treeClick="handleTreeClick"
@updateDept="handleUpdateMenu"
@deleteDept="handleDeleteMenu"
/>
</div>
</el-col>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding">
<el-col :span="18">
<div class="menu-box menu-right-box">
<MenuButtonCom ref="menuButtonRef" />
</div>
</el-col>
</el-row>
<el-drawer v-model="drawerVisible" title="菜单配置" direction="rtl" size="500px" :close-on-click-modal="false" :before-close="handleDrawerClose">
<MenuFormCom
v-if="drawerVisible"
:initFormData="drawerFormData"
:cacheData="menuTreeCacheData"
:treeData="menuTreeData"
@drawerClose="handleDrawerClose"
/>
</el-drawer>
</fs-page>
</fs-crud>
<fs-form-wrapper ref="addChildrenRef" />
</fs-page>
</template>
<script lang="ts" setup name="menuPages">
import { ref, onMounted } from 'vue';
import XEUtils from 'xe-utils';
import { ElMessageBox } from 'element-plus';
import MenuTreeCom from './components/MenuTreeCom/index.vue';
import MenuButtonCom from './components/MenuButtonCom/index.vue';
import MenuFormCom from './components/MenuFormCom/index.vue';
import { GetList, DelObj } from './api';
import { successNotification } from '/@/utils/message';
import { APIResponseData, MenuTreeItemType } from './types';
<script lang="ts" setup>
import {ref, onMounted, reactive, computed} from "vue";
import createCrudOptions from "./crud";
import {useExpose, useCrud, useCompute} from "@fast-crud/fast-crud";
import {AddObj} from "./api";
import {ElMessage } from "element-plus"
let menuTreeData = ref([]);
let menuTreeCacheData = ref<MenuTreeItemType[]>([]);
let drawerVisible = ref(false);
let drawerFormData = ref<Partial<MenuTreeItemType>>({});
let menuTreeRef = ref<InstanceType<typeof MenuTreeCom> | null>(null);
let menuButtonRef = ref<InstanceType<typeof MenuButtonCom> | null>(null);
const getData = () => {
GetList({}).then((ret: APIResponseData) => {
const responseData = ret.data;
const result = XEUtils.toArrayTree(responseData, {
parentKey: 'parent',
children: 'children',
strict: true,
});
menuTreeData.value = result;
});
};
/**
* 菜单的点击事件
*/
const handleTreeClick = (record: MenuTreeItemType) => {
menuButtonRef.value?.handleRefreshTable(record);
};
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding });
/**
* 部门的 新增 or 编辑 事件
*/
const handleUpdateMenu = (type: string, record?: MenuTreeItemType) => {
if (type === 'update' && record) {
const parentData = menuTreeRef.value?.treeRef?.currentNode.parent.data || {};
menuTreeCacheData.value = [parentData];
drawerFormData.value = record;
}
drawerVisible.value = true;
};
const handleDrawerClose = (type?: string) => {
if (type === 'submit') {
getData();
}
drawerVisible.value = false;
drawerFormData.value = {};
};
/**
* 部门的删除事件
*/
const handleDeleteMenu = (id: string, callback: Function) => {
ElMessageBox.confirm('您确认删除该菜单项吗?', '温馨提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
const res: APIResponseData = await DelObj(id);
callback();
if (res?.code === 2000) {
successNotification(res.msg as string);
getData();
}
});
};
// 添加目录
const onAddCatalog = (row:any)=>{
const childrenOptions = ref();
childrenOptions.value = {...crudBinding.value.addForm};
childrenOptions.value.columns.parent_name.show = false
childrenOptions.value.columns.icon.show = true
childrenOptions.value.columns.web_path.show = false
childrenOptions.value.columns.is_link.show = false
childrenOptions.value.columns.cache.show = false
childrenOptions.value.columns.web_path.show = false
childrenOptions.value.columns.visible.show = false
childrenOptions.value.columns.frame_out.show = false
childrenOptions.value.columns.sort.show = true
childrenOptions.value.columns.status.show = true
childrenOptions.value.columns.component.show = false
childrenOptions.value.initialForm = { menu_type:0,web_path:'/' };
//覆盖提交方法
childrenOptions.value.doSubmit=({ form })=>{
AddObj(form).then(res=>{
ElMessage.success('添加成功')
childrenOptions.value.onSuccess()
addChildrenRef.value.close()
})
}
addChildrenRef.value.open(childrenOptions.value);
}
// 添加子级
const addChildrenRef = ref();
const onAddChildren = (row:any)=>{
const childrenOptions = ref();
childrenOptions.value = {...crudBinding.value.addForm};
childrenOptions.value.columns.parent_name.show = true
childrenOptions.value.columns.is_link.show = true
childrenOptions.value.columns.cache.show = true
childrenOptions.value.columns.web_path.show = true
childrenOptions.value.columns.component.show = true
childrenOptions.value.columns.component.title = "组件地址"
childrenOptions.value.columns.visible.show = true
childrenOptions.value.columns.frame_out.show = true
childrenOptions.value.columns.status.show = true
childrenOptions.value.initialForm = { parent_name: row.name,menu_type:1 };
//覆盖提交方法
childrenOptions.value.doSubmit=({ form })=>{
form.parent = row.id
AddObj(form).then(res=>{
ElMessage.success('添加成功')
childrenOptions.value.onSuccess()
addChildrenRef.value.close()
})
}
addChildrenRef.value.open(childrenOptions.value);
}
// 添加按钮
const onAddButton = (row:any)=>{
const childrenOptions = ref();
childrenOptions.value = {...crudBinding.value.addForm};
childrenOptions.value.columns.parent_name.show = true
childrenOptions.value.columns.icon.show = false
childrenOptions.value.columns.web_path.show = false
childrenOptions.value.columns.is_link.show = false
childrenOptions.value.columns.cache.show = false
childrenOptions.value.columns.web_path.show = false
childrenOptions.value.columns.visible.show = false
childrenOptions.value.columns.frame_out.show = false
childrenOptions.value.columns.sort.show = false
childrenOptions.value.columns.status.show = false
childrenOptions.value.columns.component.show = true
childrenOptions.value.columns.component.title = "按钮权限值"
childrenOptions.value.initialForm = { parent_name: row.name,menu_type:2 };
//覆盖提交方法
childrenOptions.value.doSubmit=({ form })=>{
form.parent = row.id
AddObj(form).then(res=>{
ElMessage.success('添加成功')
childrenOptions.value.onSuccess()
addChildrenRef.value.close()
})
}
addChildrenRef.value.open(childrenOptions.value);
}
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose,onAddCatalog,onAddChildren,onAddButton });
// 初始化crud配置
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
// 你可以调用此方法重新初始化crud配置
// resetCrudOptions(options)
// 页面打开后获取列表数据
onMounted(() => {
getData();
crudExpose.doRefresh();
});
</script>
<style lang="scss" scoped>
.menu-el-row {
height: 100%;
overflow: hidden;
.el-col {
height: 100%;
padding: 10px 0;
box-sizing: border-box;
}
}
.menu-box {
height: 100%;
padding: 10px;
background-color: #fff;
box-sizing: border-box;
}
.menu-left-box {
position: relative;
border-radius: 0 8px 8px 0;
margin-right: 10px;
}
.menu-right-box {
border-radius: 8px 0 0 8px;
}
</style>