🔖 项目重命名.

This commit is contained in:
lijiahangmax
2024-05-16 00:03:30 +08:00
parent f7189e34cb
commit d3a045ec20
1511 changed files with 4199 additions and 4128 deletions

View File

@@ -0,0 +1,213 @@
<template>
<table class="grant-table" border="1">
<thead>
<tr class="header-row">
<th class="parent-view-header">父页面</th>
<th class="child-view-header">子页面</th>
<th>功能</th>
</tr>
</thead>
<tbody>
<a-checkbox-group v-model="checkedKeys" style="display: contents;">
<template v-for="parentMenu in menuData" :key="parentMenu.id">
<!-- 有子菜单 -->
<template v-if="parentMenu.children?.length">
<tr v-for="(childrenMenu, i) in parentMenu.children"
:key="childrenMenu.id">
<!-- 父菜单 -->
<td v-if="i === 0" :rowspan="parentMenu.children.length">
<a-checkbox :value="parentMenu.id">
{{ parentMenu.name }}
</a-checkbox>
</td>
<!-- 子菜单 -->
<td>
<a-checkbox :value="childrenMenu.id">
{{ childrenMenu.name }}
</a-checkbox>
</td>
<!-- 功能 -->
<td>
<a-row v-if="childrenMenu.children && childrenMenu.children.length">
<a-col v-for="item in childrenMenu.children"
:key="item.id"
:span="8">
<a-checkbox :value="item.id">
{{ item.name }}
</a-checkbox>
</a-col>
</a-row>
</td>
</tr>
</template>
<!-- 无子菜单 -->
<template v-else>
<tr>
<!-- 父菜单 -->
<td>
<a-checkbox :value="parentMenu.id">
{{ parentMenu.name }}
</a-checkbox>
</td>
<!-- 子菜单 -->
<td>
</td>
<!-- 功能 -->
<td>
</td>
</tr>
</template>
</template>
</a-checkbox-group>
</tbody>
</table>
</template>
<script lang="ts">
export default {
name: 'menuGrantTable'
};
</script>
<script lang="ts" setup>
import type { MenuQueryResponse } from '@/api/system/menu';
import { useCacheStore } from '@/store';
import { ref, watch } from 'vue';
import { findNode, flatNodeKeys, flatNodes } from '@/utils/tree';
const cacheStore = useCacheStore();
const unTriggerChange = ref(false);
const menuData = ref<Array<MenuQueryResponse>>([]);
const checkedKeys = ref<Array<number>>([]);
// 初始化
const init = (keys: Array<number>) => {
unTriggerChange.value = true;
checkedKeys.value = keys;
if (!menuData.value.length) {
cacheStore.loadMenus().then(menus => {
menuData.value = [...menus];
});
}
};
// 获取值
const getValue = () => {
return checkedKeys.value;
};
// 通过规则 选择/取消选择
const checkOrUncheckByFilter = (filter: undefined | ((perm: string) => boolean), check: boolean) => {
unTriggerChange.value = true;
const nodes: Array<MenuQueryResponse> = [];
flatNodes(menuData.value, nodes);
if (filter) {
const ruleNodes = nodes.filter(s => s.permission)
.filter(s => filter(s?.permission))
.map(s => s.id)
.filter(Boolean);
if (check) {
// 选择
checkedKeys.value = [...new Set([...checkedKeys.value, ...ruleNodes])];
} else {
// 取消选择
checkedKeys.value = [...checkedKeys.value].filter(s => !ruleNodes.includes(s));
}
} else {
if (check) {
// 选择
checkedKeys.value = nodes.map(s => s.id).filter(Boolean);
} else {
// 取消选择
checkedKeys.value = [];
}
}
};
defineExpose({ init, getValue, checkOrUncheckByFilter });
// 监听级联变化
watch(checkedKeys, (after: Array<number>, before: Array<number>) => {
if (unTriggerChange.value) {
unTriggerChange.value = false;
return;
}
const afterLength = after.length;
const beforeLength = before.length;
if (afterLength > beforeLength) {
// 选择 一定是最后一个
checkOrUncheckMenu(after[afterLength - 1], true);
} else if (afterLength < beforeLength) {
// 取消
let uncheckedId = null;
for (let i = 0; i < afterLength; i++) {
if (after[i] !== before[i]) {
uncheckedId = before[i];
break;
}
}
if (uncheckedId == null) {
uncheckedId = before[beforeLength - 1];
}
checkOrUncheckMenu(uncheckedId, false);
}
});
// 级联选择/取消选择菜单
const checkOrUncheckMenu = (id: number, check: boolean) => {
unTriggerChange.value = true;
// 查询当前节点
const node = findNode(id, menuData.value, 'id');
if (!node) {
return;
}
const childrenId: number[] = [];
// 获取所在子节点id
flatNodeKeys(node.children, childrenId, 'id');
if (check) {
// 选中
checkedKeys.value = [...new Set([...checkedKeys.value, ...childrenId])];
} else {
// 取消选择
checkedKeys.value = checkedKeys.value.filter(s => !childrenId.includes(s));
}
};
</script>
<style lang="less" scoped>
.grant-table {
width: 100%;
border: 1px solid var(--color-fill-3);
text-indent: initial;
border-spacing: 2px;
border-collapse: collapse;
user-select: none;
tbody {
td {
padding: 6px 16px;
border: 1px solid var(--color-fill-3);
}
}
}
.header-row {
th {
font-size: 17px;
padding: 4px;
font-weight: 500;
text-align: center;
background-color: var(--color-fill-1);
color: var(--color-text-2);
}
.parent-view-header, .child-view-header {
width: 200px;
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<a-tree-select v-model:model-value="value"
:data="treeData"
:disabled="disabled"
:loading="loading"
:allow-search="true"
:filter-tree-node="titleFilter"
placeholder="请选择菜单" />
</template>
<script lang="ts">
export default {
name: 'menuTreeSelector'
};
</script>
<script lang="ts" setup>
import type { TreeNodeData } from '@arco-design/web-vue';
import { useCacheStore } from '@/store';
import { computed, onBeforeMount, ref } from 'vue';
import { MenuType } from '@/views/system/menu/types/const';
import { titleFilter } from '@/types/form';
import useLoading from '@/hooks/loading';
const props = defineProps<{
modelValue: number;
disabled: boolean;
}>();
const emits = defineEmits(['update:modelValue']);
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
const treeData = ref<Array<TreeNodeData>>([]);
const value = computed({
get() {
return props.modelValue;
},
set(e) {
emits('update:modelValue', e);
}
});
onBeforeMount(async () => {
let render = (arr: any[]): TreeNodeData[] => {
return arr.map((s) => {
// 为 function 返回空
if (s.type === MenuType.FUNCTION) {
return null as unknown as TreeNodeData;
}
// 当前节点
const node = {
key: s.id,
title: s.name,
children: undefined as unknown
} as TreeNodeData;
// 子节点
if (s.children && s.children.length) {
node.children = render(s.children);
}
return node;
}).filter(Boolean);
};
// 加载数据
try {
setLoading(true);
const menus = await cacheStore.loadMenus();
treeData.value = [{
key: 0,
title: '根目录',
children: render([...menus])
}];
} catch (e) {
} finally {
setLoading(false);
}
});
</script>
<style lang="less" scoped>
</style>;

View File

@@ -0,0 +1,186 @@
<template>
<a-menu class="full"
:mode="topMenu ? 'horizontal' : 'vertical'"
v-model:collapsed="collapsed"
v-model:open-keys="openKeys"
v-model:selected-keys="selectedKey"
:show-collapse-button="appStore.device !== 'mobile'"
:auto-open="false"
:auto-open-selected="true"
:level-indent="34"
@collapse="setCollapse">
<template v-for="menu in menuTree">
<!-- 一级菜单 -->
<a-menu-item v-if="!menu.children?.length"
:key="menu.name"
@click="(e) => goto(e, menu)">
<!-- 图标 -->
<template #icon>
<component v-if="menu.meta?.icon" :is="menu.meta?.icon" />
</template>
<!-- 名称 -->
{{ menu.meta?.locale || '' }}
</a-menu-item>
<!-- 父菜单 -->
<a-sub-menu v-else :key="menu.name">
<!-- 图标 -->
<template #icon>
<component v-if="menu.meta?.icon" :is="menu.meta?.icon" />
</template>
<!-- 名称 -->
<template #title>
{{ menu.meta?.locale || '' }}
</template>
<!-- 子菜单 -->
<a-menu-item v-for="child in menu.children"
:key="child.name"
@click="(e) => goto(e, child)">
<!-- 图标 -->
<template #icon v-if="child.meta?.icon">
<component :is="child.meta?.icon" />
</template>
<!-- 名称 -->
{{ child.meta?.locale || '' }}
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</template>
<script lang="ts">
export default {
name: 'systemMenuTree'
};
</script>
<script lang="ts" setup>
import type { RouteMeta, RouteRecordRaw } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { computed, ref } from 'vue';
import { useAppStore } from '@/store';
import { listenerRouteChange } from '@/utils/route-listener';
import { openWindow, regexUrl } from '@/utils';
import { openNewRoute } from '@/router';
import useMenuTree from './use-menu-tree';
const emits = defineEmits(['collapse']);
const appStore = useAppStore();
const router = useRouter();
const route = useRoute();
const { menuTree } = useMenuTree();
const collapsed = computed({
get() {
if (appStore.device === 'desktop') return appStore.menuCollapse;
return false;
},
set(value: boolean) {
appStore.updateSettings({ menuCollapse: value });
},
});
const topMenu = computed(() => appStore.topMenu);
const openKeys = ref<string[]>([]);
const selectedKey = ref<string[]>([]);
// 跳转路由
const goto = (e: any, item: RouteRecordRaw) => {
// 打开外链
if (regexUrl.test(item.path)) {
openWindow(item.path);
return;
}
const { hideInMenu, activeMenu, newWindow } = item.meta as RouteMeta;
// 新页面打开
if (newWindow || e.ctrlKey) {
openNewRoute({
name: item.name,
});
return;
}
// 设置 selectedKey
if (route.name === item.name && !hideInMenu && !activeMenu) {
selectedKey.value = [item.name as string];
return;
}
// 触发跳转
router.push({
name: item.name,
});
};
const findMenuOpenKeys = (target: string) => {
const result: string[] = [];
let isFind = false;
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
if (item.name === target) {
isFind = true;
result.push(...keys);
return;
}
if (item.children?.length) {
item.children.forEach((el) => {
backtrack(el, [...keys, el.name as string]);
});
}
};
menuTree.value.forEach((el: RouteRecordRaw) => {
if (isFind) return;
backtrack(el, [el.name as string]);
});
return result;
};
// 监听路由 设置打开的 key
listenerRouteChange((newRoute) => {
const { activeMenu, hideInMenu } = newRoute.meta;
if (!hideInMenu || activeMenu) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];
selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);
// 展开菜单
const setCollapse = (val: boolean) => {
if (appStore.device === 'desktop')
appStore.updateSettings({ menuCollapse: val });
};
</script>
<style lang="less" scoped>
:deep(.arco-menu-inner) {
.arco-menu-inline-header {
display: flex;
align-items: center;
}
.arco-icon {
&:not(.arco-icon-down) {
font-size: 18px;
}
}
.arco-menu-icon {
margin-right: 10px !important;
}
.arco-menu-indent-list {
width: 28px;
display: inline-block;
}
.arco-menu-title {
user-select: none;
}
}
</style>

View File

@@ -0,0 +1,67 @@
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
import { computed } from 'vue';
import { useMenuStore } from '@/store';
import { cloneDeep } from 'lodash';
export default function useMenuTree() {
const menuStore = useMenuStore();
const appRoute = computed(() => {
return menuStore.appMenus;
});
const menuTree = computed<RouteRecordNormalized[]>(() => {
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
// 排序
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
return (a.meta.order || 0) - (b.meta.order || 0);
});
function travel(_routes: RouteRecordRaw[], layer: number) {
if (!_routes) return null;
const collector: any = _routes.map((element) => {
// 隐藏子目录
if (element.meta?.hideInMenu || !element.children) {
element.children = [];
if (element.meta?.hideInMenu) {
// 如果隐藏菜单 则不显示
return null;
} else {
return element;
}
}
// 过滤不显示的菜单
element.children = element.children.filter(
(x) => x.meta?.hideInMenu !== true
);
// 关联子节点
const subItem = travel(element.children, layer + 1);
if (subItem.length) {
element.children = subItem;
return element;
}
// 第二层 (子目录)
if (layer > 1) {
element.children = subItem;
return element;
}
// 是否隐藏目录
if (element.meta?.hideInMenu === false) {
return element;
}
return null;
});
return collector.filter(Boolean);
}
return travel(copyRouter, 0);
});
return {
menuTree,
};
}