mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-28 06:49:24 +08:00
initial commit
This commit is contained in:
3
src/components/Tree/index.ts
Normal file
3
src/components/Tree/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as BasicTree } from './src/BasicTree';
|
||||
export * from './src/types';
|
||||
export type { ContextMenuItem } from '/@/hooks/web/useContextMenu';
|
270
src/components/Tree/src/BasicTree.tsx
Normal file
270
src/components/Tree/src/BasicTree.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
import { defineComponent, reactive, computed, unref, ref, watchEffect } from 'vue';
|
||||
import { Tree } from 'ant-design-vue';
|
||||
import { extendSlots } from '/@/utils/helper/tsxHelper';
|
||||
import { useContextMenu, ContextMenuItem } from '/@/hooks/web/useContextMenu';
|
||||
import { basicProps } from './props';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
import { omit } from 'lodash-es';
|
||||
import { DownOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import type { ReplaceFields, TreeItem, Keys, CheckKeys, InsertNodeParams } from './types';
|
||||
import { tryTsxEmit } from '/@/utils/helper/vueHelper';
|
||||
|
||||
import './index.less';
|
||||
import { forEach } from '/@/utils/helper/treeHelper';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
interface State {
|
||||
expandedKeys: Keys;
|
||||
selectedKeys: Keys;
|
||||
checkedKeys: CheckKeys;
|
||||
}
|
||||
const prefixCls = 'basic-tree';
|
||||
export default defineComponent({
|
||||
name: 'BasicTree',
|
||||
props: basicProps,
|
||||
emits: ['update:expandedKeys', 'update:selectedKeys', 'update:value'],
|
||||
setup(props, { attrs, slots, emit }) {
|
||||
const state = reactive<State>({
|
||||
expandedKeys: props.expandedKeys || [],
|
||||
selectedKeys: props.selectedKeys || [],
|
||||
checkedKeys: props.checkedKeys || [],
|
||||
});
|
||||
|
||||
const treeDataRef = ref<TreeItem[]>([]);
|
||||
|
||||
const [createContextMenu] = useContextMenu();
|
||||
|
||||
const getReplaceFields = computed(
|
||||
(): Required<ReplaceFields> => {
|
||||
const { replaceFields } = props;
|
||||
return {
|
||||
children: 'children',
|
||||
title: 'title',
|
||||
key: 'key',
|
||||
...replaceFields,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const getTreeData = computed(() => {
|
||||
return unref(treeDataRef);
|
||||
});
|
||||
|
||||
// 渲染操作按钮
|
||||
function renderAction(node: TreeItem) {
|
||||
const { actionList } = props;
|
||||
|
||||
if (!actionList || actionList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return actionList.map((item, index) => {
|
||||
return (
|
||||
<span key={index} class={`${prefixCls}__action`}>
|
||||
{item.render(node)}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
}
|
||||
// 渲染树节点
|
||||
function renderTreeNode({ data }: { data: TreeItem[] | undefined }) {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.map((item) => {
|
||||
const { title: titleField, key: keyField, children: childrenField } = unref(
|
||||
getReplaceFields
|
||||
);
|
||||
const propsData = omit(item, 'title');
|
||||
const anyItem = item as any;
|
||||
return (
|
||||
<Tree.TreeNode {...propsData} key={keyField && anyItem[keyField]}>
|
||||
{{
|
||||
title: () => (
|
||||
<span class={`${prefixCls}-title`}>
|
||||
{titleField && anyItem[titleField]}
|
||||
{renderAction(item)}
|
||||
</span>
|
||||
),
|
||||
default: () => renderTreeNode({ data: childrenField ? anyItem[childrenField] : [] }),
|
||||
}}
|
||||
</Tree.TreeNode>
|
||||
);
|
||||
});
|
||||
}
|
||||
// 处理右键事件
|
||||
async function handleRightClick({ event, node }: any) {
|
||||
const { rightMenuList: menuList = [], beforeRightClick } = props;
|
||||
let rightMenuList: ContextMenuItem[] = [];
|
||||
if (beforeRightClick && isFunction(beforeRightClick)) {
|
||||
rightMenuList = await beforeRightClick(node);
|
||||
} else {
|
||||
rightMenuList = menuList;
|
||||
}
|
||||
if (!rightMenuList.length) return;
|
||||
createContextMenu({
|
||||
event,
|
||||
items: rightMenuList,
|
||||
});
|
||||
}
|
||||
|
||||
function setExpandedKeys(keys: string[]) {
|
||||
state.expandedKeys = keys;
|
||||
}
|
||||
|
||||
function getExpandedKeys() {
|
||||
return state.expandedKeys;
|
||||
}
|
||||
function setSelectedKeys(keys: string[]) {
|
||||
state.selectedKeys = keys;
|
||||
}
|
||||
|
||||
function getSelectedKeys() {
|
||||
return state.selectedKeys;
|
||||
}
|
||||
|
||||
function setCheckedKeys(keys: CheckKeys) {
|
||||
state.checkedKeys = keys;
|
||||
}
|
||||
|
||||
function getCheckedKeys() {
|
||||
return state.checkedKeys;
|
||||
}
|
||||
|
||||
// 展开指定级别
|
||||
function filterByLevel(level = 1, list?: TreeItem[], currentLevel = 1) {
|
||||
if (!level) {
|
||||
return [];
|
||||
}
|
||||
const res: (string | number)[] = [];
|
||||
const data = list || props.treeData || [];
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
const item = data[index] as any;
|
||||
|
||||
const { key: keyField, children: childrenField } = unref(getReplaceFields);
|
||||
const key = keyField ? item[keyField] : '';
|
||||
const children = childrenField ? item[childrenField] : [];
|
||||
res.push(key);
|
||||
if (children && children.length && currentLevel < level) {
|
||||
currentLevel += 1;
|
||||
res.push(...filterByLevel(level, children, currentLevel));
|
||||
}
|
||||
}
|
||||
return res as string[] | number[];
|
||||
}
|
||||
/**
|
||||
* 添加节点
|
||||
*/
|
||||
function insertNodeByKey({ parentKey = null, node, push = 'push' }: InsertNodeParams) {
|
||||
const treeData: any = cloneDeep(unref(treeDataRef));
|
||||
if (!parentKey) {
|
||||
treeData[push](node);
|
||||
return;
|
||||
}
|
||||
const { key: keyField, children: childrenField } = unref(getReplaceFields);
|
||||
forEach(treeData, (treeItem) => {
|
||||
if (treeItem[keyField] === parentKey) {
|
||||
treeItem[childrenField] = treeItem[childrenField] || [];
|
||||
treeItem[childrenField][push](node);
|
||||
}
|
||||
});
|
||||
treeDataRef.value = treeData;
|
||||
}
|
||||
|
||||
// 删除节点
|
||||
function deleteNodeByKey(key: string, list: TreeItem[]) {
|
||||
if (!key) return;
|
||||
const treeData = list || unref(treeDataRef);
|
||||
const { key: keyField, children: childrenField } = unref(getReplaceFields);
|
||||
|
||||
for (let index = 0; index < treeData.length; index++) {
|
||||
const element: any = treeData[index];
|
||||
const children = element[childrenField];
|
||||
|
||||
if (element[keyField] === key) {
|
||||
treeData.splice(index, 1);
|
||||
break;
|
||||
} else if (children && children.length) {
|
||||
deleteNodeByKey(key, element[childrenField]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新节点
|
||||
function updateNodeByKey(key: string, node: TreeItem, list: TreeItem[]) {
|
||||
if (!key) return;
|
||||
const treeData = list || unref(treeDataRef);
|
||||
const { key: keyField, children: childrenField } = unref(getReplaceFields);
|
||||
|
||||
for (let index = 0; index < treeData.length; index++) {
|
||||
const element: any = treeData[index];
|
||||
const children = element[childrenField];
|
||||
|
||||
if (element[keyField] === key) {
|
||||
treeData[index] = { ...treeData[index], ...node };
|
||||
break;
|
||||
} else if (children && children.length) {
|
||||
updateNodeByKey(key, node, element[childrenField]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
treeDataRef.value = props.treeData as TreeItem[];
|
||||
state.expandedKeys = props.expandedKeys;
|
||||
state.selectedKeys = props.selectedKeys;
|
||||
state.checkedKeys = props.checkedKeys;
|
||||
});
|
||||
|
||||
tryTsxEmit((currentInstance) => {
|
||||
currentInstance.setExpandedKeys = setExpandedKeys;
|
||||
currentInstance.getExpandedKeys = getExpandedKeys;
|
||||
currentInstance.setSelectedKeys = setSelectedKeys;
|
||||
currentInstance.getSelectedKeys = getSelectedKeys;
|
||||
currentInstance.setCheckedKeys = setCheckedKeys;
|
||||
currentInstance.getCheckedKeys = getCheckedKeys;
|
||||
currentInstance.insertNodeByKey = insertNodeByKey;
|
||||
currentInstance.deleteNodeByKey = deleteNodeByKey;
|
||||
currentInstance.updateNodeByKey = updateNodeByKey;
|
||||
currentInstance.filterByLevel = (level: number) => {
|
||||
state.expandedKeys = filterByLevel(level);
|
||||
};
|
||||
});
|
||||
return () => {
|
||||
let propsData: any = {
|
||||
blockNode: true,
|
||||
...attrs,
|
||||
...props,
|
||||
expandedKeys: state.expandedKeys,
|
||||
selectedKeys: state.selectedKeys,
|
||||
checkedKeys: state.checkedKeys,
|
||||
replaceFields: unref(getReplaceFields),
|
||||
'onUpdate:expandedKeys': (v: Keys) => {
|
||||
state.expandedKeys = v;
|
||||
emit('update:expandedKeys', v);
|
||||
},
|
||||
'onUpdate:selectedKeys': (v: Keys) => {
|
||||
state.selectedKeys = v;
|
||||
emit('update:selectedKeys', v);
|
||||
},
|
||||
check: (v: CheckKeys) => {
|
||||
state.checkedKeys = v;
|
||||
emit('update:value', v);
|
||||
},
|
||||
onRightClick: handleRightClick,
|
||||
};
|
||||
propsData = omit(propsData, 'treeData');
|
||||
return (
|
||||
<Tree {...propsData} class={prefixCls}>
|
||||
{{
|
||||
switcherIcon: () => <DownOutlined />,
|
||||
default: () => renderTreeNode({ data: unref(getTreeData) }),
|
||||
...extendSlots(slots),
|
||||
}}
|
||||
</Tree>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
20
src/components/Tree/src/index.less
Normal file
20
src/components/Tree/src/index.less
Normal file
@@ -0,0 +1,20 @@
|
||||
.basic-tree {
|
||||
position: relative;
|
||||
|
||||
&-title {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding-right: 10px;
|
||||
|
||||
.basic-tree__action {
|
||||
display: none;
|
||||
float: right;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.basic-tree__action {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
56
src/components/Tree/src/props.ts
Normal file
56
src/components/Tree/src/props.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { PropType } from 'vue';
|
||||
import type { ReplaceFields, TreeItem, ActionItem, Keys, CheckKeys } from './types';
|
||||
import type { ContextMenuItem } from '/@/hooks/web/useContextMenu';
|
||||
|
||||
export const basicProps = {
|
||||
replaceFields: {
|
||||
type: Object as PropType<ReplaceFields>,
|
||||
},
|
||||
|
||||
treeData: {
|
||||
type: Array as PropType<TreeItem[]>,
|
||||
},
|
||||
|
||||
actionList: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
expandedKeys: {
|
||||
type: Array as PropType<Keys>,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
selectedKeys: {
|
||||
type: Array as PropType<Keys>,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
checkedKeys: {
|
||||
type: Array as PropType<CheckKeys>,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
beforeRightClick: {
|
||||
type: Function as PropType<(...arg: any) => ContextMenuItem[]>,
|
||||
default: null,
|
||||
},
|
||||
|
||||
rightMenuList: {
|
||||
type: Array as PropType<ContextMenuItem[]>,
|
||||
},
|
||||
};
|
||||
|
||||
export const treeNodeProps = {
|
||||
actionList: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
replaceFields: {
|
||||
type: Object as PropType<ReplaceFields>,
|
||||
},
|
||||
treeData: {
|
||||
type: Array as PropType<TreeItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
};
|
118
src/components/Tree/src/types.ts
Normal file
118
src/components/Tree/src/types.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
export interface ActionItem {
|
||||
render: (record: any) => any;
|
||||
}
|
||||
|
||||
export interface TreeItem {
|
||||
/**
|
||||
* Class
|
||||
* @description className
|
||||
* @type string
|
||||
*/
|
||||
class?: string;
|
||||
|
||||
/**
|
||||
* Style
|
||||
* @description style of tree node
|
||||
* @type string | object
|
||||
*/
|
||||
style?: string | object;
|
||||
|
||||
/**
|
||||
* Disable Checkbox
|
||||
* @description Disables the checkbox of the treeNode
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
disableCheckbox?: boolean;
|
||||
|
||||
/**
|
||||
* Disabled
|
||||
* @description Disabled or not
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* Icon
|
||||
* @description customize icon. When you pass component, whose render will receive full TreeNode props as component props
|
||||
* @type any (slot | slot-scope)
|
||||
*/
|
||||
icon?: any;
|
||||
|
||||
/**
|
||||
* Is Leaf?
|
||||
* @description Leaf node or not
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
isLeaf?: boolean;
|
||||
|
||||
/**
|
||||
* Key
|
||||
* @description Required property, should be unique in the tree
|
||||
* (In tree: Used with (default)ExpandedKeys / (default)CheckedKeys / (default)SelectedKeys)
|
||||
* @default internal calculated position of treeNode or undefined
|
||||
* @type string | number
|
||||
*/
|
||||
key: string | number;
|
||||
|
||||
/**
|
||||
* Selectable
|
||||
* @description Set whether the treeNode can be selected
|
||||
* @default true
|
||||
* @type boolean
|
||||
*/
|
||||
selectable?: boolean;
|
||||
|
||||
/**
|
||||
* Title
|
||||
* @description Content showed on the treeNodes
|
||||
* @default '---'
|
||||
* @type any (string | slot)
|
||||
*/
|
||||
title: any;
|
||||
|
||||
/**
|
||||
* Value
|
||||
* @description Will be treated as treeNodeFilterProp by default, should be unique in the tree
|
||||
* @default undefined
|
||||
* @type string
|
||||
*/
|
||||
value?: string;
|
||||
children?: TreeItem[];
|
||||
slots?: any;
|
||||
scopedSlots?: any;
|
||||
}
|
||||
|
||||
export interface ReplaceFields {
|
||||
children?: string;
|
||||
title?: string;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export type Keys = string[] | number[];
|
||||
export type CheckKeys =
|
||||
| string[]
|
||||
| number[]
|
||||
| { checked: string[] | number[]; halfChecked: string[] | number[] };
|
||||
|
||||
export interface TreeActionType {
|
||||
setExpandedKeys: (keys: Keys) => void;
|
||||
getExpandedKeys: () => Keys;
|
||||
setSelectedKeys: (keys: Keys) => void;
|
||||
getSelectedKeys: () => Keys;
|
||||
setCheckedKeys: (keys: CheckKeys) => void;
|
||||
getCheckedKeys: () => CheckKeys;
|
||||
filterByLevel: (level: number) => void;
|
||||
insertNodeByKey: (opt: InsertNodeParams) => void;
|
||||
deleteNodeByKey: (key: string) => void;
|
||||
updateNodeByKey: (key: string, node: Omit<TreeItem, 'key'>) => void;
|
||||
}
|
||||
|
||||
export interface InsertNodeParams {
|
||||
parentKey: string | null;
|
||||
node: TreeItem;
|
||||
list?: TreeItem[];
|
||||
push?: 'push' | 'unshift';
|
||||
}
|
Reference in New Issue
Block a user