This commit is contained in:
sheng
2023-07-25 15:25:57 +08:00
parent 8f57a240db
commit 7ccbcebe77
373 changed files with 41578 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/menu/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id + '/',
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(obj: DelReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'delete',
});
}
export function GetAllMenu(query: UserPageQuery) {
return request({
url: apiPrefix + 'get_all_menu/',
method: 'get',
params: query,
});
}
export function lazyLoadMenu(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function dragMenu(obj: AddReq) {
return request({
url: apiPrefix + 'drag_menu/',
method: 'post',
data: obj,
});
}

View File

@@ -0,0 +1,41 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/menu_button/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: any) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,214 @@
import {
CrudOptions,
AddReq,
DelReq,
EditReq,
dict,
CrudExpose,
CreateCrudOptionsRet,
CreateCrudOptionsProps,
UserPageQuery,
} from '@fast-crud/fast-crud';
import _ from 'lodash-es';
import * as api from './api';
import { request } from '/@/utils/service';
//此处为crudOptions配置
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
if(context!.selectOptions.value.id){
return await api.GetList({ menu: context!.selectOptions.value.id } as any);
}else{
return undefined
}
};
const editRequest = async ({ form, row }: EditReq) => {
return await api.UpdateObj({ ...form, menu: row.menu });
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj({ ...form, ...{ menu: context!.selectOptions.value.id } });
};
return {
crudOptions: {
search: {
container: {
action: {
//按钮栏配置
col: {
span: 8
}
}
},
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
form: {
col: { span: 24 },
labelWidth: '100px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
search: {
title: '关键词',
column: { show: false },
type: 'text',
search: { show: true },
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
id: {
title: 'ID',
type: 'text',
column: { show: false },
search: { show: false },
form: { show: false },
},
name: {
title: '权限名称',
type: 'text',
search: { show: true },
column: {
minWidth: 120,
sortable: true,
},
form: {
rules: [{ required: true, message: '权限名称必填' }],
component: {
placeholder: '输入权限名称搜索',
props: {
clearable: true,
allowCreate: true,
filterable: true,
},
},
helper: {
render(h) {
return <el-alert title="手动输入" type="warning" description="页面中按钮的名称或者自定义一个名称" />;
},
},
},
},
value: {
title: '权限值',
type: 'text',
search: { show: false },
column: {
width: 120,
sortable: true,
},
form: {
rules: [{ required: true, message: '权限标识必填' }],
placeholder: '输入权限标识',
helper: {
render(h) {
return <el-alert title="唯一值" type="warning" description="用于判断前端按钮权限或接口权限" />;
},
},
},
},
method: {
title: '请求方式',
search: { show: false },
type: 'dict-select',
column: {
width: 120,
sortable: true,
},
dict: dict({
data: [
{ label: 'GET', value: 0 },
{ label: 'POST', value: 1, color: 'success' },
{ label: 'PUT', value: 2, color: 'warning' },
{ label: 'DELETE', value: 3, color: 'danger' },
],
}),
form: {
rules: [{ required: true, message: '必填项' }],
},
},
api: {
title: '接口地址',
search: { show: false },
type: 'dict-select',
dict: dict({
getData() {
return request({ url: '/swagger.json' }).then((res: any) => {
const ret = Object.keys(res.paths);
const data = [];
for (const item of ret) {
const obj: any = {};
obj.label = item;
obj.value = item;
data.push(obj);
}
return data;
});
},
}),
column: {
minWidth: 200,
sortable: true,
},
form: {
rules: [{ required: true, message: '必填项' }],
component: {
props: {
allowCreate: true,
filterable: true,
clearable: true,
},
},
helper: {
render(h) {
return <el-alert title="请正确填写,以免请求时被拦截。匹配单例使用正则,例如:/api/xx/.*?/" type="warning" />;
},
},
},
},
},
},
};
};

View File

@@ -0,0 +1,29 @@
<template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</template>
<script lang="ts" setup>
import { ref, defineProps, watch } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
// 当前选择的菜单信息
let selectOptions: any = ref({ name: null });
const props = defineProps<{
selectMenu: object;
}>();
const { crudRef, crudBinding, crudExpose, context } = useFs({ createCrudOptions, context: { selectOptions } });
const { doRefresh,setTableData } = crudExpose;
watch(props.selectMenu, (val: any) => {
if (!val.is_catalog && val.id) {
selectOptions.value = val;
doRefresh();
}else{
//清空表格数据
setTableData([])
}
});
defineExpose({ selectOptions });
</script>

View File

@@ -0,0 +1,31 @@
import * as api from './api';
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from '@fast-crud/fast-crud';
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
columns: {},
},
};
}

View File

@@ -0,0 +1,400 @@
<template>
<fs-page>
<splitpanes>
<pane max-size="20" min-size="16">
<el-card :body-style="{ height: '100%' }">
<p class="font-mono font-black text-center text-xl pb-5">
菜单列表
<el-tooltip effect="dark" :content="content" placement="right">
<el-icon>
<QuestionFilled/>
</el-icon>
</el-tooltip>
</p>
<el-input v-model="filterText" :placeholder="placeholder"/>
<el-tree ref="treeRef" class="font-mono font-bold leading-6 text-7xl" :data="data"
:props="treeProps"
:filter-node-method="filterNode" :load="loadNode" :allow-drag="allowDrag"
:allow-drop="allowDrop"
@node-drop="nodeDrop" lazy icon="ArrowRightBold" :indent="12" draggable
@node-click="handleNodeClick">
<template #default="{ node, data }">
<span v-if="data.status" class="text-center font-black font-normal">
<SvgIcon :name="node.data.icon"/>&nbsp;{{ node.label }}
</span>
<span v-else class="text-center font-black text-red-700 font-normal">
<SvgIcon :name="node.data.icon"/>&nbsp;{{ node.label }}
</span>
</template>
</el-tree>
</el-card>
</pane>
<pane min-size="30">
<el-card :body-style="{ height: '100%' }">
<el-form ref="formRef" :rules="rules" :model="form" label-width="80px"
label-position="right">
<el-alert :title="content" type="success" effect="dark" :closable="false" center/>
<el-divider>
<strong>菜单配置</strong>
</el-divider>
<el-form-item label="菜单ID" prop="id">
<el-input v-model="form.id" disabled/>
</el-form-item>
<el-form-item label="父级ID" prop="parent">
<el-input v-model="form.parent"/>
</el-form-item>
<el-form-item required label="菜单名称" prop="name">
<el-input v-model="form.name"/>
</el-form-item>
<el-form-item label="组件地址" prop="component">
<el-autocomplete class="w-full" v-model="form.component"
:fetch-suggestions="querySearch" :trigger-on-focus="false"
clearable
debounce="100"
placeholder="输入组件地址"/>
</el-form-item>
<el-form-item required label="Url" prop="web_path">
<el-input v-model="form.web_path"/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" controls-position="right"/>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="侧边可见">
<el-radio-group v-model="form.visible">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="缓存">
<el-radio-group v-model="form.cache">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="图标" prop="icon">
<IconSelector clearable v-model="form.icon"/>
</el-form-item>
</el-form>
<el-divider></el-divider>
<div>
<el-button @click="saveMenu()" type="primary" round>保存</el-button>
<el-button @click="newMenu()" type="success" round :disabled="!form.id">新建</el-button>
<el-button @click="addChildMenu()" type="warning" round :disabled="!form.id">添加子级
</el-button>
<!-- <el-button @click="addSameLevelMenu()" type="warning" round>添加同级</el-button>-->
<el-button @click="deleteMenu()" type="danger" round :disabled="!form.id">删除菜单
</el-button>
</div>
</el-card>
</pane>
<pane min-size="30">
<el-card :body-style="{ height: '100%' }">
<menuButton :select-menu="form"/>
</el-card>
</pane>
</splitpanes>
</fs-page>
</template>
<script lang="ts" setup name="menu">
import {Splitpanes, Pane} from 'splitpanes';
import 'splitpanes/dist/splitpanes.css';
import * as api from './api';
import * as menuButoonApi from './components/menuButton/api';
import {ElForm, ElTree, FormRules, ElMessageBox} from 'element-plus';
import {ref, onMounted, watch, reactive, toRaw, defineAsyncComponent, nextTick, shallowRef, onActivated} from 'vue';
import XEUtils from 'xe-utils';
import {errorMessage, successMessage} from '../../../utils/message';
interface Tree {
id: number;
name: string;
status: boolean;
children?: Tree[];
}
interface APIResponseData {
code?: number;
data: [];
msg?: string;
}
interface Form<T> {
[key: string]: T;
}
interface ComponentFileItem {
value: string;
label: string;
}
// 引入组件
const menuButton = defineAsyncComponent(() => import('./components/menuButton/index.vue'));
const IconSelector = defineAsyncComponent(() => import('/@/components/iconSelector/index.vue'));
const SvgIcon = defineAsyncComponent(() => import('/@/components/svgIcon/index.vue'));
const placeholder = ref('请输入菜单名称');
const filterText = ref('');
const treeRef = ref<InstanceType<typeof ElTree>>();
const treeProps = {
children: 'children',
label: 'name',
icon: 'icon',
isLeaf: (data: Tree[], node: Node) => {
// @ts-ignore
if (node.data.is_catalog) {
return false;
} else {
return true;
}
},
};
const validateWebPath = (rule: string, value: string, callback: Function) => {
let pattern = /^\/.*?/;
if (!pattern.test(value)) {
callback(new Error('请输入正确的地址'));
} else {
callback();
}
};
watch(filterText, (val) => {
treeRef.value!.filter(val);
});
const filterNode = (value: string, data: Tree) => {
if (!value) return true;
return toRaw(data).name.indexOf(value) !== -1;
};
// 懒加载
const loadNode = (node: Node, resolve: (data: Tree[]) => void) => {
// @ts-ignore
if (node.level !== 0) {
// @ts-ignore
api.lazyLoadMenu({parent: node.data.id}).then((res: APIResponseData) => {
resolve(res.data);
});
}
};
// 判断是否可以拖动
const allowDrag = (node: Node) => {
// @ts-ignore
if (node.data.is_catalog) {
return false;
} else {
return true;
}
};
// 判断是否可以被放置
const allowDrop = (draggingNode: Node, dropNode: Node, type: string) => {
// @ts-ignore
if (!dropNode.isLeaf) {
return true;
}
};
const nodeDrop = (draggingNode: Node, dropNode: Node, dropType: string, event: any) => {
// @ts-ignore
if (!dropNode.isLeaf) {
// @ts-ignore
api.dragMenu({menu_id: draggingNode.data.id, parent_id: dropNode.data.id}).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
}
};
let data = ref([]);
let isAddNewMenu = ref(false); // 判断当前是新增菜单,还是更新保存当前菜单
const permissionDrawerVisible = ref(false);
const content = `
1.红色菜单代表状态禁用;
2.添加菜单,如果是目录,组件地址为空即可;
3.添加根节点菜单父级ID为空即可;
4.支持拖拽菜单;
`;
let form: Form<any> = reactive({
id: '',
parent: '',
name: '',
component: '',
web_path: '',
sort: '',
status: true,
is_catalog: false,
permission: '',
icon: '',
visible: true,
cache: true,
});
let menuPermissonList = ref([]);
const formRef = ref<InstanceType<typeof ElForm>>();
const querySearch = (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 createFilter = (queryString: string) => {
return (file: ComponentFileItem) => {
return file.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1;
};
};
const rules = reactive<FormRules>({
// @ts-ignore
web_path: [{validator: validateWebPath, trigger: 'blur'}],
});
const getData = () => {
api.GetList({}).then((ret: APIResponseData) => {
const responseData = ret.data;
const result = XEUtils.toArrayTree(responseData, {
parentKey: 'parent',
children: 'children',
strict: true,
});
data.value = result;
});
};
const getPermissions = (menu: object) => {
menuButoonApi.GetList(menu).then((res: APIResponseData) => {
menuPermissonList.value = res.data;
});
};
const saveMenu = () => {
formRef.value?.validate((valid, fields) => {
if (valid) {
if (!isAddNewMenu.value) {
// 保存菜单
form.component == '' ? (form.is_catalog = true) : (form.is_catalog = false);
api.UpdateObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
} else {
// 新增菜单
form.component == '' ? (form.is_catalog = true) : (form.is_catalog = false);
api.AddObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
}
} else {
errorMessage('请填写检查表单');
}
});
};
const newMenu = () => {
formRef.value?.resetFields();
isAddNewMenu.value = true;
};
const addChildMenu = () => {
let parentId = form.id;
formRef.value?.resetFields();
form.parent = parentId;
isAddNewMenu.value = true;
};
const addSameLevelMenu = () => {
let parentId = form.parent;
formRef.value?.resetFields();
form.parent = parentId;
isAddNewMenu.value = true;
};
const deleteMenu = () => {
ElMessageBox.confirm(
'您确认删除该菜单项吗?',
'温馨提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
api.DelObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
})
};
const handleNodeClick = (data: any, node: any, prop: any) => {
Object.keys(toRaw(data)).forEach((key: string) => {
form[key] = data[key];
});
delete form.component_name;
form.id = data.id;
isAddNewMenu.value = false;
// 点击tree node时加载对应的权限菜单
// getPermissions({ menu: form.id });
};
const addPermission = () => {
!form.is_catalog ? (permissionDrawerVisible.value = true) : errorMessage('目录没有菜单权限');
};
const drawerClose = () => {
permissionDrawerVisible.value = false;
};
// 页面打开后获取列表数据
onMounted(() => {
getData();
});
onActivated(() => {
console.log('keep-alive成功')
});
</script>
<style lang="scss" scoped>
.el-row {
height: 100%;
.el-col {
height: 100%;
}
}
.el-card {
height: 100%;
}
.font-normal {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
}
</style>