tree组件
This commit is contained in:
7
Report-V3-TS/pnpm-lock.yaml
generated
7
Report-V3-TS/pnpm-lock.yaml
generated
@@ -37,6 +37,9 @@ dependencies:
|
||||
element-resize-detector:
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4
|
||||
js-md5:
|
||||
specifier: ^0.8.3
|
||||
version: 0.8.3
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -4096,6 +4099,10 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/js-md5@0.8.3:
|
||||
resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==}
|
||||
dev: false
|
||||
|
||||
/js-stringify@1.0.2:
|
||||
resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==}
|
||||
dev: true
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useDesignSettingStore } from '@/store/modules/designSetting';
|
||||
import { lighten } from '@/utils/index';
|
||||
import { getAllDict } from '@/api/login';
|
||||
import { storage } from '@/utils/Storage';
|
||||
import { GLOBAL_DICT_CODE_NAME } from '@/enums/common'
|
||||
|
||||
const route = useRoute();
|
||||
const useScreenLock = useScreenLockStore();
|
||||
@@ -73,7 +76,15 @@
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 初始化字典项
|
||||
const getAllDictData = async () => {
|
||||
const { code, data } = await getAllDict();
|
||||
if (code != 200) return;
|
||||
storage.set(GLOBAL_DICT_CODE_NAME, data);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getAllDictData();
|
||||
document.addEventListener('mousedown', timekeeping);
|
||||
});
|
||||
|
||||
|
||||
16
Report-V3-TS/src/api/access/accessAuthority.ts
Normal file
16
Report-V3-TS/src/api/access/accessAuthority.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* @Description:
|
||||
* @Author: qianlishi
|
||||
* @Date: 2024-12-30 01:22:56
|
||||
* @LastEditors: qianlishi
|
||||
* @LastEditTime: 2024-12-30 01:23:21
|
||||
*/
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
export function getAuthorityTree(data = {}) {
|
||||
return http.request({
|
||||
url: 'accessAuthority/menuTree',
|
||||
method: 'get',
|
||||
data,
|
||||
});
|
||||
}
|
||||
@@ -23,3 +23,11 @@ export function reqUpdatePassword(data) {
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 数字字典
|
||||
export function getAllDict() {
|
||||
return http.request({
|
||||
url: '/gaeaDict/all',
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
1
Report-V3-TS/src/components/Base/Jsq-crud/index.ts
Normal file
1
Report-V3-TS/src/components/Base/Jsq-crud/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Crud } from './src/Crud.vue';
|
||||
41
Report-V3-TS/src/components/Base/Jsq-crud/src/Jsq-crud.vue
Normal file
41
Report-V3-TS/src/components/Base/Jsq-crud/src/Jsq-crud.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="view-container">
|
||||
<div class="view-container-left">
|
||||
<n-space vertical :size="12">
|
||||
<n-input v-model:value="pattern" placeholder="搜索" />
|
||||
<h3>所属菜单</h3>
|
||||
<n-tree
|
||||
default-expand-all
|
||||
:show-irrelevant-nodes="showIrrelevantNodes"
|
||||
:pattern="pattern"
|
||||
:data="data"
|
||||
block-line
|
||||
/>
|
||||
</n-space>
|
||||
</div>
|
||||
<div class="view-container-right">右</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup></script>
|
||||
<style lang="less" scoped>
|
||||
.view-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 118px);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
&-left {
|
||||
width: 20%;
|
||||
height: calc(100vh - 118px);
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
padding: 20px 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
&-right {
|
||||
background: #fff;
|
||||
flex: 1;
|
||||
padding: 20px 10px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
2
Report-V3-TS/src/components/Base/Jsq-select/index.ts
Normal file
2
Report-V3-TS/src/components/Base/Jsq-select/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as JsqSelect } from './src/Jsq-select.vue';
|
||||
export { useSelect } from './src/hooks/useSelect';
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<n-select v-bind="getBindValue" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, unref, useAttrs, computed, onMounted, watch } from 'vue';
|
||||
import type { SelectProps } from 'naive-ui/lib/select';
|
||||
import { basicProps } from './props';
|
||||
import { selectActionType } from './types';
|
||||
import { deepMerge, getDictName } from '@/utils';
|
||||
|
||||
const props = defineProps({ ...basicProps });
|
||||
const attrs = useAttrs();
|
||||
const emit = defineEmits(['register']);
|
||||
const selectOptions = ref<any[]>([]);
|
||||
|
||||
const propsRef = ref<Partial<SelectProps>>({});
|
||||
// 将标签传递的属性与hooks传递的参数合并
|
||||
const getProps = computed(() => {
|
||||
return { ...props, ...(unref(propsRef) as any) };
|
||||
});
|
||||
|
||||
const getOptions = computed(() => {
|
||||
const { api, dictCode, localOptions } = unref(getProps);
|
||||
const options = localOptions
|
||||
? localOptions
|
||||
: dictCode
|
||||
? getDictName(dictCode)
|
||||
: selectOptions.value;
|
||||
return { options };
|
||||
});
|
||||
|
||||
const getBindValue = computed(
|
||||
() => ({ ...attrs, ...props, ...unref(getProps), ...unref(getOptions) } as Recordable),
|
||||
);
|
||||
|
||||
const setProps = async (selectProps: Partial<SelectProps>): Promise<void> => {
|
||||
propsRef.value = deepMerge(unref(propsRef) || {}, selectProps);
|
||||
};
|
||||
|
||||
const selectMethods: selectActionType = {
|
||||
setProps,
|
||||
};
|
||||
|
||||
watch(
|
||||
() => unref(getProps).api,
|
||||
(old, val) => {
|
||||
console.log(old);
|
||||
console.log(val);
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
emit('register', selectMethods);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
...selectMethods,
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ref, unref, nextTick, onUnmounted, watch } from 'vue';
|
||||
import type { SelectProps } from 'naive-ui/lib/select';
|
||||
import { getDynamicProps } from '@/utils';
|
||||
import { isProdMode } from '@/utils/env';
|
||||
import { JsqSelectProps, UseSelectReturnType, selectActionType } from '../types';
|
||||
import type { DynamicProps } from '/#/utils';
|
||||
|
||||
type props = Partial<DynamicProps<JsqSelectProps>>;
|
||||
export const useSelect = (props?: props): UseSelectReturnType => {
|
||||
const selectRef = ref<Nullable<selectActionType>>(null);
|
||||
|
||||
const getSelect = async () => {
|
||||
const select = unref(selectRef);
|
||||
if (!select) {
|
||||
console.error('tree节点尚未获取');
|
||||
}
|
||||
await nextTick();
|
||||
return select as selectActionType;
|
||||
};
|
||||
|
||||
const register = (instance: selectActionType) => {
|
||||
isProdMode() &&
|
||||
onUnmounted(() => {
|
||||
selectRef.value = null;
|
||||
});
|
||||
if (unref(selectRef) && isProdMode() && instance === unref(selectRef)) return;
|
||||
|
||||
selectRef.value = instance;
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
() => {
|
||||
props && instance.setProps(getDynamicProps(props));
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const methods: selectActionType = {
|
||||
setProps: async (selectProps: Partial<SelectProps>) => {
|
||||
const select = await getSelect();
|
||||
await select.setProps(selectProps);
|
||||
},
|
||||
};
|
||||
|
||||
return [register, methods];
|
||||
};
|
||||
17
Report-V3-TS/src/components/Base/Jsq-select/src/props.ts
Normal file
17
Report-V3-TS/src/components/Base/Jsq-select/src/props.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { selectProps } from 'naive-ui';
|
||||
|
||||
export const basicProps = {
|
||||
...selectProps,
|
||||
labelField: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { SelectProps } from 'naive-ui/lib/select';
|
||||
|
||||
type options = {
|
||||
[key: string]: string | number;
|
||||
};
|
||||
|
||||
export type JsqSelectProps = {
|
||||
api?: () => Promise<any>; // api接口
|
||||
dictCode?: String; // 数字字典
|
||||
localOptions?: options[]; // 静态数据
|
||||
};
|
||||
|
||||
// 对外暴露的方法
|
||||
export interface selectActionType {
|
||||
setProps: (JsqSelectProps: Partial<SelectProps>) => Promise<void>;
|
||||
}
|
||||
|
||||
export type selectRegisterFn = (treeInstance: selectActionType) => void;
|
||||
|
||||
export type UseSelectReturnType = [selectRegisterFn, selectActionType];
|
||||
2
Report-V3-TS/src/components/Base/Jsq-tree/index.ts
Normal file
2
Report-V3-TS/src/components/Base/Jsq-tree/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as JsqTree } from './src/Jsq-tree.vue';
|
||||
export { useTree } from './src/hooks/useTree';
|
||||
64
Report-V3-TS/src/components/Base/Jsq-tree/src/Jsq-tree.vue
Normal file
64
Report-V3-TS/src/components/Base/Jsq-tree/src/Jsq-tree.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<n-space vertical :size="12">
|
||||
<n-input v-model:value="pattern" placeholder="搜索" clearable />
|
||||
<h3>{{ treeTitle }}</h3>
|
||||
<n-tree
|
||||
v-bind="getBindValue"
|
||||
:show-irrelevant-nodes="showIrrelevantNodes"
|
||||
:pattern="pattern"
|
||||
:data="treeData"
|
||||
block-line
|
||||
/>
|
||||
</n-space>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, useAttrs, onMounted, unref } from 'vue';
|
||||
import type { TreeOption } from 'naive-ui';
|
||||
import type { TreeActionType } from './types/index';
|
||||
import type { TreeProps } from 'naive-ui/lib/tree';
|
||||
|
||||
import { basicProps } from './props';
|
||||
import { deepMerge, removeTreeNotValChildren } from '@/utils';
|
||||
|
||||
const props = defineProps({ ...basicProps });
|
||||
const attrs = useAttrs();
|
||||
const emit = defineEmits(['register']);
|
||||
|
||||
const pattern = ref('');
|
||||
const showIrrelevantNodes = ref(false);
|
||||
const treeData = ref<TreeOption[]>([]);
|
||||
|
||||
const propsRef = ref<Partial<TreeProps>>({});
|
||||
|
||||
const getProps = computed(() => {
|
||||
return { ...props, ...(unref(propsRef) as any) };
|
||||
});
|
||||
const getBindValue = computed(() => ({ ...attrs, ...props, ...unref(getProps) } as Recordable));
|
||||
|
||||
const setProps = async (treeProps: Partial<TreeProps>): Promise<void> => {
|
||||
propsRef.value = deepMerge(unref(propsRef) || {}, treeProps);
|
||||
};
|
||||
|
||||
// 查询tree数据
|
||||
const loadData = async (params?: any) => {
|
||||
const { api } = getBindValue.value;
|
||||
if (!api) return;
|
||||
const { code, data } = await api(params);
|
||||
if (code != 200) return;
|
||||
treeData.value = removeTreeNotValChildren(data);
|
||||
};
|
||||
|
||||
const TreeMethods: TreeActionType = {
|
||||
setProps,
|
||||
loadData,
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
emit('register', TreeMethods);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
...TreeMethods,
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ref, unref, nextTick, onUnmounted, watch } from 'vue';
|
||||
import { getDynamicProps } from '@/utils';
|
||||
import { isProdMode } from '@/utils/env';
|
||||
import type { JsqTreeProps, TreeActionType, UseTreeReturnType } from '../types/index';
|
||||
import type { DynamicProps } from '/#/utils';
|
||||
import type { TreeProps } from 'naive-ui/lib/tree';
|
||||
|
||||
type Props = Partial<DynamicProps<JsqTreeProps>>;
|
||||
export const useTree = (props?: Props): UseTreeReturnType => {
|
||||
const treeRef = ref<Nullable<TreeActionType>>(null);
|
||||
|
||||
const getTree = async () => {
|
||||
const tree = unref(treeRef);
|
||||
if (!tree) {
|
||||
console.error('tree节点尚未获取');
|
||||
}
|
||||
await nextTick();
|
||||
return tree as TreeActionType;
|
||||
};
|
||||
|
||||
const register = (instance: TreeActionType) => {
|
||||
isProdMode() &&
|
||||
onUnmounted(() => {
|
||||
treeRef.value = null;
|
||||
});
|
||||
if (unref(treeRef) && isProdMode() && instance === unref(treeRef)) return;
|
||||
|
||||
treeRef.value = instance;
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
() => {
|
||||
props && instance.setProps(getDynamicProps(props));
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const methods: TreeActionType = {
|
||||
setProps: async (treeProps: Partial<TreeProps>) => {
|
||||
const tree = await getTree();
|
||||
await tree.setProps(treeProps);
|
||||
},
|
||||
loadData: async (params) => {
|
||||
const tree = await getTree();
|
||||
await tree.loadData(params);
|
||||
},
|
||||
};
|
||||
|
||||
return [register, methods];
|
||||
};
|
||||
8
Report-V3-TS/src/components/Base/Jsq-tree/src/props.ts
Normal file
8
Report-V3-TS/src/components/Base/Jsq-tree/src/props.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { treeProps } from 'naive-ui';
|
||||
export const basicProps = {
|
||||
...treeProps,
|
||||
treeTitle: {
|
||||
type: String,
|
||||
default: '所属菜单',
|
||||
},
|
||||
};
|
||||
14
Report-V3-TS/src/components/Base/Jsq-tree/src/types/index.ts
Normal file
14
Report-V3-TS/src/components/Base/Jsq-tree/src/types/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { TreeProps } from 'naive-ui/lib/tree';
|
||||
export type JsqTreeProps = {
|
||||
api?: () => Promise<any>; // api接口
|
||||
};
|
||||
|
||||
// 对外暴露的方法
|
||||
export interface TreeActionType {
|
||||
setProps: (treeProps: Partial<TreeProps>) => Promise<void>;
|
||||
loadData: (params: any) => Promise<void>;
|
||||
}
|
||||
|
||||
export type TreeRegisterFn = (treeInstance: TreeActionType) => void
|
||||
|
||||
export type UseTreeReturnType = [TreeRegisterFn, TreeActionType]
|
||||
@@ -2,3 +2,4 @@ export const ACCESS_TOKEN = 'ACCESS-TOKEN'; // 用户token
|
||||
export const CURRENT_USER = 'CURRENT-USER'; // 当前用户信息
|
||||
export const IS_LOCKSCREEN = 'IS-LOCKSCREEN'; // 是否锁屏
|
||||
export const TABS_ROUTES = 'TABS-ROUTES'; // 标签页
|
||||
export const GLOBAL_DICT_CODE_NAME = 'REPORTDICT'; // 全局数字字典名称
|
||||
|
||||
@@ -63,6 +63,7 @@ const transform: AxiosTransform = {
|
||||
|
||||
// 这里 code,result,message为 后台统一的字段,需要修改为项目自己的接口返回格式
|
||||
const { code, message } = data;
|
||||
|
||||
// 请求成功
|
||||
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS;
|
||||
// 是否显示提示信息
|
||||
|
||||
@@ -4,6 +4,8 @@ import { NIcon, NTag } from 'naive-ui';
|
||||
import { PageEnum } from '@/enums/pageEnum';
|
||||
import { isObject } from './is/index';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { storage } from './Storage';
|
||||
import { GLOBAL_DICT_CODE_NAME } from '@/enums/common';
|
||||
/**
|
||||
* render 图标
|
||||
* */
|
||||
@@ -47,7 +49,7 @@ export function renderNew(type = 'warning', text = 'New', color: object = newTag
|
||||
size: 'small',
|
||||
color,
|
||||
},
|
||||
{ default: () => text }
|
||||
{ default: () => text },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,7 +230,7 @@ export function lighten(color: string, amount: number) {
|
||||
amount = Math.trunc((255 * amount) / 100);
|
||||
return `#${addLight(color.substring(0, 2), amount)}${addLight(
|
||||
color.substring(2, 4),
|
||||
amount
|
||||
amount,
|
||||
)}${addLight(color.substring(4, 6), amount)}`;
|
||||
}
|
||||
|
||||
@@ -238,3 +240,27 @@ export function lighten(color: string, amount: number) {
|
||||
export function isUrl(url: string) {
|
||||
return /^(http|https):\/\//g.test(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除树节点 children为 null | [] 字段
|
||||
*/
|
||||
export function removeTreeNotValChildren(data: any) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const item = data[i];
|
||||
if (item.children && item.children.length > 0) {
|
||||
removeTreeNotValChildren(item.children);
|
||||
} else {
|
||||
delete item.children;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// 根据数字字典名称获取数据
|
||||
export function getDictName(dictName: string) {
|
||||
if (!dictName) {
|
||||
console.error('必须要传递数字字典名称');
|
||||
}
|
||||
const allDictCode = storage.get(GLOBAL_DICT_CODE_NAME);
|
||||
return allDictCode[dictName];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script lang='ts' setup>
|
||||
import { useAttrs } from 'vue'
|
||||
const attr = useAttrs()
|
||||
console.log(attr)
|
||||
</script>
|
||||
<style lang='less' scoped></style>
|
||||
@@ -1,15 +1,78 @@
|
||||
<!--
|
||||
* @Description:
|
||||
* @Author: qianlishi
|
||||
* @Date: 2024-12-08 16:34:50
|
||||
* @Date: 2024-12-08 17:38:28
|
||||
* @LastEditors: qianlishi
|
||||
* @LastEditTime: 2024-12-08 16:36:34
|
||||
* @LastEditTime: 2024-12-30 05:43:45
|
||||
-->
|
||||
<template>
|
||||
<div>权限管理</div>
|
||||
<div class="view-container">
|
||||
<div class="view-container-left">
|
||||
<JsqTree
|
||||
default-expand-all
|
||||
key-field="id"
|
||||
label-field="label"
|
||||
@register="register"
|
||||
:node-props="nodeProps"
|
||||
/>
|
||||
</div>
|
||||
<div class="view-container-right">
|
||||
<JsqSelect @register="register1" />
|
||||
<Test v-bind="obj" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import JsqTree from '@/components/Base/Jsq-tree/src/Jsq-tree.vue';
|
||||
import JsqSelect from '@/components/Base/Jsq-select/src/Jsq-select.vue';
|
||||
import Test from './components/test.vue';
|
||||
|
||||
import { getAuthorityTree } from '@/api/access/accessAuthority';
|
||||
|
||||
import { useTree } from '@/components/Base/Jsq-tree';
|
||||
import { useSelect } from '@/components/Base/Jsq-select';
|
||||
|
||||
const [register1, {}] = useSelect({
|
||||
api: getAuthorityTree,
|
||||
});
|
||||
|
||||
const obj = ref({ a: 1, b: 2, c: 3 });
|
||||
|
||||
const [register, { loadData }] = useTree({
|
||||
api: getAuthorityTree,
|
||||
});
|
||||
const nodeProps = ({ option }: { option: any }) => {
|
||||
return {
|
||||
onClick() {
|
||||
console.log(11, option);
|
||||
console.log(22, option);
|
||||
},
|
||||
};
|
||||
};
|
||||
onMounted(() => {
|
||||
loadData({ a: 1 });
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.view-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 118px);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
&-left {
|
||||
width: 20%;
|
||||
height: calc(100vh - 118px);
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
padding: 20px 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
&-right {
|
||||
background: #fff;
|
||||
flex: 1;
|
||||
padding: 20px 10px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
<!--
|
||||
* @Description:
|
||||
* @Author: qianlishi
|
||||
* @Date: 2024-12-08 16:36:34
|
||||
* @LastEditors: qianlishi
|
||||
* @LastEditTime: 2024-12-08 16:36:57
|
||||
-->
|
||||
<template>
|
||||
<div>角色管理</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
<script lang="ts" setup></script>
|
||||
<style lang="less" scoped></style>
|
||||
|
||||
Reference in New Issue
Block a user