1
0
mirror of synced 2025-11-06 04:20:54 +08:00

tree组件

This commit is contained in:
qianlishi
2024-12-30 13:24:38 +08:00
parent 763e64d01b
commit 2abe3b22d6
28 changed files with 483 additions and 17 deletions

View File

@@ -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

View File

@@ -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);
});

View 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,
});
}

View File

@@ -23,3 +23,11 @@ export function reqUpdatePassword(data) {
data,
});
}
// 数字字典
export function getAllDict() {
return http.request({
url: '/gaeaDict/all',
method: 'GET',
});
}

View File

@@ -0,0 +1 @@
export { default as Crud } from './src/Crud.vue';

View 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>

View File

@@ -0,0 +1,2 @@
export { default as JsqSelect } from './src/Jsq-select.vue';
export { useSelect } from './src/hooks/useSelect';

View File

@@ -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>

View File

@@ -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];
};

View 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,
},
};

View File

@@ -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];

View File

@@ -0,0 +1,2 @@
export { default as JsqTree } from './src/Jsq-tree.vue';
export { useTree } from './src/hooks/useTree';

View 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>

View File

@@ -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];
};

View File

@@ -0,0 +1,8 @@
import { treeProps } from 'naive-ui';
export const basicProps = {
...treeProps,
treeTitle: {
type: String,
default: '所属菜单',
},
};

View 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]

View File

@@ -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'; // 全局数字字典名称

View File

@@ -63,6 +63,7 @@ const transform: AxiosTransform = {
// 这里 coderesultmessage为 后台统一的字段,需要修改为项目自己的接口返回格式
const { code, message } = data;
// 请求成功
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS;
// 是否显示提示信息

View File

@@ -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];
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>