refactor(tree): Refactor tree to support antv3.0

This commit is contained in:
vben 2021-11-30 00:53:26 +08:00
parent 50cf2d0b8f
commit 52257f061d
21 changed files with 1146 additions and 842 deletions

View File

@ -62,6 +62,7 @@ module.exports = defineConfig({
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off',
'vue/require-explicit-emits': 'off',
'vue/html-self-closing': [
'error',
{
@ -74,6 +75,6 @@ module.exports = defineConfig({
math: 'always',
},
],
'vue/multi-word-component-names': 'off'
'vue/multi-word-component-names': 'off',
},
});

View File

@ -132,6 +132,7 @@
"brotli",
"tailwindcss",
"sider",
"pnpm"
"pnpm",
"antd"
]
}

View File

@ -9,17 +9,17 @@ import type { Plugin } from 'vite';
export function configHmrPlugin(): Plugin {
return {
name: 'singleHMR',
handleHotUpdate({ modules, file }) {
if (file.match(/xml$/)) return [];
// handleHotUpdate({ modules, file }) {
// if (file.match(/xml$/)) return [];
modules.forEach((m) => {
if (!m.url.match(/\.(css|less)/)) {
m.importedModules = new Set();
m.importers = new Set();
}
});
// modules.forEach((m) => {
// if (!m.url.match(/\.(css|less)/)) {
// m.importedModules = new Set();
// m.importers = new Set();
// }
// });
return modules;
},
// return modules;
// },
};
}

View File

@ -4,10 +4,10 @@
*/
import styleImport from 'vite-plugin-style-import';
export function configStyleImportPlugin(isBuild: boolean) {
if (!isBuild) {
return [];
}
export function configStyleImportPlugin(_isBuild: boolean) {
// if (!isBuild) {
// return [];
// }
const styleImportPlugin = styleImport({
libs: [
{
@ -19,6 +19,7 @@ export function configStyleImportPlugin(isBuild: boolean) {
'anchor-link',
'sub-menu',
'menu-item',
'menu-divider',
'menu-item-group',
'breadcrumb-item',
'breadcrumb-separator',

View File

@ -72,7 +72,7 @@
"devDependencies": {
"@commitlint/cli": "^15.0.0",
"@commitlint/config-conventional": "^15.0.0",
"@iconify/json": "^2.0.2",
"@iconify/json": "^2.0.3",
"@purge-icons/generated": "^0.7.0",
"@types/codemirror": "^5.60.5",
"@types/crypto-js": "^4.0.2",
@ -110,7 +110,7 @@
"fs-extra": "^10.0.0",
"husky": "^7.0.4",
"inquirer": "^8.2.0",
"jest": "^27.3.1",
"jest": "^27.4.0",
"less": "^4.1.2",
"lint-staged": "12.1.2",
"npm-run-all": "^4.1.5",
@ -135,14 +135,14 @@
"vite-plugin-imagemin": "^0.4.6",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-purge-icons": "^0.7.0",
"vite-plugin-pwa": "^0.11.7",
"vite-plugin-pwa": "^0.11.8",
"vite-plugin-style-import": "^1.4.0",
"vite-plugin-svg-icons": "^1.0.5",
"vite-plugin-theme": "^0.8.1",
"vite-plugin-vue-setup-extend": "^0.1.0",
"vite-plugin-windicss": "^1.5.3",
"vue-eslint-parser": "^8.0.1",
"vue-tsc": "^0.29.6"
"vue-tsc": "^0.29.7"
},
"resolutions": {
"//": "Used to install imagemin dependencies, because imagemin may not be installed in China. If it is abroad, you can delete it",

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
import BasicTree from './src/Tree.vue';
import './style';
export { BasicTree };
export type { ContextMenuItem } from '/@/hooks/web/useContextMenu';
export * from './src/typing';
export * from './src/tree';

View File

@ -1,6 +1,6 @@
<script lang="tsx">
import type { ReplaceFields, Keys, CheckKeys, TreeActionType, TreeItem } from './typing';
import type { CheckEvent } from './typing';
import type { CSSProperties } from 'vue';
import type { FieldNames, TreeState, TreeItem, KeyType, CheckKeys, TreeActionType } from './tree';
import {
defineComponent,
@ -11,43 +11,31 @@
watchEffect,
toRaw,
watch,
CSSProperties,
onMounted,
} from 'vue';
import { Tree, Empty } from 'ant-design-vue';
import { TreeIcon } from './TreeIcon';
import { ScrollContainer } from '/@/components/Container';
import { omit, get, difference } from 'lodash-es';
import { omit, get, difference, cloneDeep } from 'lodash-es';
import { isArray, isBoolean, isEmpty, isFunction } from '/@/utils/is';
import { extendSlots, getSlot } from '/@/utils/helper/tsxHelper';
import { filter, treeToList } from '/@/utils/helper/treeHelper';
import { useTree } from './useTree';
import { useContextMenu } from '/@/hooks/web/useContextMenu';
import { useDesign } from '/@/hooks/web/useDesign';
import { basicProps } from './props';
import { CreateContextOptions } from '/@/components/ContextMenu';
import TreeHeader from './TreeHeader.vue';
import { treeEmits, treeProps } from './tree';
import { createBEM } from '/@/utils/bem';
interface State {
expandedKeys: Keys;
selectedKeys: Keys;
checkedKeys: CheckKeys;
checkStrictly: boolean;
}
export default defineComponent({
name: 'BasicTree',
inheritAttrs: false,
props: basicProps,
emits: [
'update:expandedKeys',
'update:selectedKeys',
'update:value',
'change',
'check',
'update:searchValue',
],
props: treeProps,
emits: treeEmits,
setup(props, { attrs, slots, emit, expose }) {
const state = reactive<State>({
const [bem] = createBEM('tree');
const state = reactive<TreeState>({
checkStrictly: props.checkStrictly,
expandedKeys: props.expandedKeys || [],
selectedKeys: props.selectedKeys || [],
@ -63,15 +51,14 @@
const treeDataRef = ref<TreeItem[]>([]);
const [createContextMenu] = useContextMenu();
const { prefixCls } = useDesign('basic-tree');
const getReplaceFields = computed((): Required<ReplaceFields> => {
const { replaceFields } = props;
const getFieldNames = computed((): Required<FieldNames> => {
const { fieldNames } = props;
return {
children: 'children',
title: 'title',
key: 'key',
...replaceFields,
...fieldNames,
};
});
@ -84,19 +71,19 @@
selectedKeys: state.selectedKeys,
checkedKeys: state.checkedKeys,
checkStrictly: state.checkStrictly,
replaceFields: unref(getReplaceFields),
'onUpdate:expandedKeys': (v: Keys) => {
filedNames: unref(getFieldNames),
'onUpdate:expandedKeys': (v: KeyType[]) => {
state.expandedKeys = v;
emit('update:expandedKeys', v);
},
'onUpdate:selectedKeys': (v: Keys) => {
'onUpdate:selectedKeys': (v: KeyType[]) => {
state.selectedKeys = v;
emit('update:selectedKeys', v);
},
onCheck: (v: CheckKeys, e: CheckEvent) => {
let currentValue = toRaw(state.checkedKeys) as Keys;
onCheck: (v: CheckKeys, e) => {
let currentValue = toRaw(state.checkedKeys) as KeyType[];
if (isArray(currentValue) && searchState.startSearch) {
const { key } = unref(getReplaceFields);
const { key } = unref(getFieldNames);
currentValue = difference(currentValue, getChildrenKeys(e.node.$attrs.node[key]));
if (e.checked) {
currentValue.push(e.node.$attrs.node[key]);
@ -132,7 +119,7 @@
getAllKeys,
getChildrenKeys,
getEnabledKeys,
} = useTree(treeDataRef, getReplaceFields);
} = useTree(treeDataRef, getFieldNames);
function getIcon(params: Recordable, icon?: string) {
if (!icon) {
@ -161,14 +148,14 @@
createContextMenu(contextMenuOptions);
}
function setExpandedKeys(keys: Keys) {
function setExpandedKeys(keys: KeyType[]) {
state.expandedKeys = keys;
}
function getExpandedKeys() {
return state.expandedKeys;
}
function setSelectedKeys(keys: Keys) {
function setSelectedKeys(keys: KeyType[]) {
state.selectedKeys = keys;
}
@ -185,11 +172,11 @@
}
function checkAll(checkAll: boolean) {
state.checkedKeys = checkAll ? getEnabledKeys() : ([] as Keys);
state.checkedKeys = checkAll ? getEnabledKeys() : ([] as KeyType[]);
}
function expandAll(expandAll: boolean) {
state.expandedKeys = expandAll ? getAllKeys() : ([] as Keys);
state.expandedKeys = expandAll ? getAllKeys() : ([] as KeyType[]);
}
function onStrictlyChange(strictly: boolean) {
@ -227,21 +214,21 @@
const { filterFn, checkable, expandOnSearch, checkOnSearch, selectedOnSearch } =
unref(props);
searchState.startSearch = true;
const { title: titleField, key: keyField } = unref(getReplaceFields);
const { title: titleField, key: keyField } = unref(getFieldNames);
const matchedKeys: string[] = [];
searchState.searchData = filter(
unref(treeDataRef),
(node) => {
const result = filterFn
? filterFn(searchValue, node, unref(getReplaceFields))
? filterFn(searchValue, node, unref(getFieldNames))
: node[titleField]?.includes(searchValue) ?? false;
if (result) {
matchedKeys.push(node[keyField]);
}
return result;
},
unref(getReplaceFields),
unref(getFieldNames),
);
if (expandOnSearch) {
@ -317,15 +304,6 @@
},
);
// watchEffect(() => {
// console.log('======================');
// console.log(props.value);
// console.log('======================');
// if (props.value) {
// state.checkedKeys = props.value;
// }
// });
watchEffect(() => {
state.checkStrictly = props.checkStrictly;
});
@ -354,8 +332,6 @@
},
};
expose(instance);
function renderAction(node: TreeItem) {
const { actionList } = props;
if (!actionList || actionList.length === 0) return;
@ -370,29 +346,25 @@
if (!nodeShow) return null;
return (
<span key={index} class={`${prefixCls}__action`}>
<span key={index} class={bem('action')}>
{item.render(node)}
</span>
);
});
}
function renderTreeNode({ data, level }: { data: TreeItem[] | undefined; level: number }) {
if (!data) {
return null;
}
const treeData = computed(() => {
const data = cloneDeep(getTreeData.value);
data.forEach((item) => {
const searchText = searchState.searchText;
const { highlight } = unref(props);
return data.map((item) => {
const {
title: titleField,
key: keyField,
children: childrenField,
} = unref(getReplaceFields);
} = unref(getFieldNames);
const propsData = omit(item, 'title');
const icon = getIcon({ ...item, level }, item.icon);
const children = get(item, childrenField) || [];
const icon = getIcon(item, item.icon);
const title = get(item, titleField);
const searchIdx = searchText ? title.indexOf(searchText) : -1;
@ -401,7 +373,7 @@
const highlightStyle = `color: ${isBoolean(highlight) ? '#f50' : highlight}`;
const titleDom = isHighlight ? (
<span class={unref(getBindValues)?.blockNode ? `${prefixCls}__content` : ''}>
<span class={unref(getBindValues)?.blockNode ? `${bem('content')}` : ''}>
<span>{title.substr(0, searchIdx)}</span>
<span style={highlightStyle}>{searchText}</span>
<span>{title.substr(searchIdx + (searchText as string).length)}</span>
@ -409,13 +381,9 @@
) : (
title
);
return (
<Tree.TreeNode {...propsData} node={toRaw(item)} key={get(item, keyField)}>
{{
title: () => (
item.title = (
<span
class={`${prefixCls}-title pl-2`}
class={`${bem('title')} pl-2`}
onClick={handleClickNode.bind(null, item[keyField], item[childrenField])}
>
{item.slots?.title ? (
@ -424,26 +392,23 @@
<>
{icon && <TreeIcon icon={icon} />}
{titleDom}
{/*{get(item, titleField)}*/}
<span class={`${prefixCls}__actions`}>
{renderAction({ ...item, level })}
</span>
<span class={bem('actions')}>{renderAction(item)}</span>
</>
)}
</span>
),
default: () => renderTreeNode({ data: children, level: level + 1 }),
}}
</Tree.TreeNode>
);
});
}
return data;
});
expose(instance);
return () => {
const { title, helpMessage, toolbar, search, checkable } = props;
const showTitle = title || toolbar || search || slots.headerTitle;
const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' };
return (
<div class={[prefixCls, 'h-full', attrs.class]}>
<div class={[bem(), 'h-full', attrs.class]}>
{showTitle && (
<TreeHeader
checkable={checkable}
@ -461,15 +426,10 @@
</TreeHeader>
)}
<ScrollContainer style={scrollStyle} v-show={!unref(getNotFound)}>
<Tree {...unref(getBindValues)} showIcon={false}>
{{
// switcherIcon: () => <DownOutlined />,
default: () => renderTreeNode({ data: unref(getTreeData), level: 1 }),
...extendSlots(slots),
}}
<Tree {...unref(getBindValues)} showIcon={false} treeData={treeData.value}>
{extendSlots(slots)}
</Tree>
</ScrollContainer>
<Empty v-show={unref(getNotFound)} image={Empty.PRESENTED_IMAGE_SIMPLE} class="!mt-4" />
</div>
);
@ -477,50 +437,3 @@
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-tree';
.@{prefix-cls} {
background-color: @component-background;
.ant-tree-node-content-wrapper {
position: relative;
.ant-tree-title {
position: absolute;
left: 0;
width: 100%;
}
}
&-title {
position: relative;
display: flex;
align-items: center;
width: 100%;
padding-right: 10px;
&:hover {
.@{prefix-cls}__action {
visibility: visible;
}
}
}
&__content {
overflow: hidden;
}
&__actions {
position: absolute;
top: 2px;
right: 3px;
display: flex;
}
&__action {
margin-left: 4px;
visibility: hidden;
}
}
</style>

View File

@ -1,10 +1,9 @@
<template>
<div class="flex px-2 py-1.5 items-center basic-tree-header">
<slot name="headerTitle" v-if="$slots.headerTitle"></slot>
<BasicTitle :helpMessage="helpMessage" v-if="!$slots.headerTitle && title">
<div :class="bem()" class="flex px-2 py-1.5 items-center">
<slot name="headerTitle" v-if="slots.headerTitle"></slot>
<BasicTitle :helpMessage="helpMessage" v-if="!slots.headerTitle && title">
{{ title }}
</BasicTitle>
<div
class="flex flex-1 justify-self-stretch items-center cursor-pointer"
v-if="search || toolbar"
@ -33,66 +32,65 @@
</div>
</div>
</template>
<script lang="ts">
import { PropType } from 'vue';
import { defineComponent, computed, ref, watch } from 'vue';
import { Dropdown, Menu, Input } from 'ant-design-vue';
<script lang="ts" setup>
import { computed, ref, watch, useSlots } from 'vue';
import { Dropdown, Menu, MenuItem, MenuDivider, InputSearch } from 'ant-design-vue';
import { Icon } from '/@/components/Icon';
import { BasicTitle } from '/@/components/Basic';
import { propTypes } from '/@/utils/propTypes';
import { useI18n } from '/@/hooks/web/useI18n';
import { useDebounceFn } from '@vueuse/core';
import { createBEM } from '/@/utils/bem';
import { ToolbarEnum } from './tree';
enum ToolbarEnum {
SELECT_ALL,
UN_SELECT_ALL,
EXPAND_ALL,
UN_EXPAND_ALL,
CHECK_STRICTLY,
CHECK_UN_STRICTLY,
}
const searchValue = ref('');
interface MenuInfo {
key: ToolbarEnum;
}
export default defineComponent({
name: 'BasicTreeHeader',
components: {
BasicTitle,
Icon,
Dropdown,
Menu,
MenuItem: Menu.Item,
MenuDivider: Menu.Divider,
InputSearch: Input.Search,
},
props: {
const [bem] = createBEM('tree-header');
// eslint-disable vue/valid-define-emits
const props = defineProps({
helpMessage: {
type: [String, Array] as PropType<string | string[]>,
default: '',
},
title: propTypes.string,
toolbar: propTypes.bool,
checkable: propTypes.bool,
search: propTypes.bool,
checkAll: propTypes.func,
expandAll: propTypes.func,
searchText: propTypes.string,
title: {
type: String,
default: '',
},
emits: ['strictly-change', 'search'],
setup(props, { emit, slots }) {
toolbar: {
type: Boolean,
default: false,
},
checkable: {
type: Boolean,
default: false,
},
search: {
type: Boolean,
default: false,
},
searchText: {
type: String,
default: '',
},
checkAll: {
type: Function,
default: undefined,
},
expandAll: {
type: Function,
default: undefined,
},
} as const);
const emit = defineEmits(['strictly-change', 'search']);
const slots = useSlots();
const { t } = useI18n();
const searchValue = ref('');
const getInputSearchCls = computed(() => {
const titleExists = slots.headerTitle || props.title;
return [
'mr-1',
'w-full',
// titleExists ? 'w-2/3' : 'w-full',
{
['ml-5']: titleExists,
},
@ -125,7 +123,7 @@
: defaultToolbarList;
});
function handleMenuClick(e: MenuInfo) {
function handleMenuClick(e: { key: ToolbarEnum }) {
const { key } = e;
switch (key) {
case ToolbarEnum.SELECT_ALL:
@ -152,6 +150,7 @@
function emitChange(value?: string): void {
emit('search', value);
}
const debounceEmitChange = useDebounceFn(emitChange, 200);
watch(
@ -160,6 +159,7 @@
debounceEmitChange(v);
},
);
watch(
() => props.searchText,
(v) => {
@ -168,13 +168,4 @@
}
},
);
return { t, toolbarList, handleMenuClick, searchValue, getInputSearchCls };
},
});
</script>
<style lang="less" scoped>
.basic-tree-header {
border-bottom: 1px solid @border-color-base;
}
</style>

View File

@ -1,14 +1,10 @@
import type { VNode, FunctionalComponent } from 'vue';
import { h } from 'vue';
import { isString } from '/@/utils/is';
import { isString } from '@vue/shared';
import { Icon } from '/@/components/Icon';
export interface ComponentProps {
icon: VNode | string;
}
export const TreeIcon: FunctionalComponent = ({ icon }: ComponentProps) => {
export const TreeIcon: FunctionalComponent = ({ icon }: { icon: VNode | string }) => {
if (!icon) return null;
if (isString(icon)) {
return h(Icon, { icon, class: 'mr-1' });

View File

@ -1,108 +0,0 @@
import type { PropType } from 'vue';
import type {
ReplaceFields,
ActionItem,
Keys,
CheckKeys,
ContextMenuOptions,
TreeItem,
} from './typing';
import type { ContextMenuItem } from '/@/hooks/web/useContextMenu';
import type { TreeDataItem } from 'ant-design-vue/es/tree';
import { propTypes } from '/@/utils/propTypes';
export const basicProps = {
value: {
type: [Object, Array] as PropType<Keys | CheckKeys>,
},
renderIcon: {
type: Function as PropType<(params: Recordable) => string>,
},
helpMessage: {
type: [String, Array] as PropType<string | string[]>,
default: '',
},
title: propTypes.string,
toolbar: propTypes.bool,
search: propTypes.bool,
searchValue: propTypes.string,
checkStrictly: propTypes.bool,
clickRowToExpand: propTypes.bool.def(true),
checkable: propTypes.bool.def(false),
defaultExpandLevel: {
type: [String, Number] as PropType<string | number>,
default: '',
},
defaultExpandAll: propTypes.bool.def(false),
replaceFields: {
type: Object as PropType<ReplaceFields>,
},
treeData: {
type: Array as PropType<TreeDataItem[]>,
},
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[] | ContextMenuOptions>,
default: null,
},
rightMenuList: {
type: Array as PropType<ContextMenuItem[]>,
},
// 自定义数据过滤判断方法(注: 不是整个过滤方法而是内置过滤的判断方法用于增强原本仅能通过title进行过滤的方式)
filterFn: {
type: Function as PropType<
(searchValue: any, node: TreeItem, replaceFields: ReplaceFields) => boolean
>,
default: null,
},
// 高亮搜索值仅高亮具体匹配值通过title值为true时使用默认色值值为#xxx时使用此值替代且高亮开启
highlight: {
type: [Boolean, String] as PropType<Boolean | String>,
default: false,
},
// 搜索完成时自动展开结果
expandOnSearch: propTypes.bool.def(false),
// 搜索完成自动选中所有结果,当且仅当 checkable===true 时生效
checkOnSearch: propTypes.bool.def(false),
// 搜索完成自动select所有结果
selectedOnSearch: propTypes.bool.def(false),
};
export const treeNodeProps = {
actionList: {
type: Array as PropType<ActionItem[]>,
default: () => [],
},
replaceFields: {
type: Object as PropType<ReplaceFields>,
},
treeData: {
type: Array as PropType<TreeDataItem[]>,
default: () => [],
},
};

View File

@ -0,0 +1,184 @@
import type { ExtractPropTypes } from 'vue';
import type { TreeDataItem } from 'ant-design-vue/es/tree/Tree';
import { buildProps } from '/@/utils/props';
export enum ToolbarEnum {
SELECT_ALL,
UN_SELECT_ALL,
EXPAND_ALL,
UN_EXPAND_ALL,
CHECK_STRICTLY,
CHECK_UN_STRICTLY,
}
export const treeEmits = [
'update:expandedKeys',
'update:selectedKeys',
'update:value',
'change',
'check',
'update:searchValue',
];
export interface TreeState {
expandedKeys: KeyType[];
selectedKeys: KeyType[];
checkedKeys: CheckKeys;
checkStrictly: boolean;
}
export interface FieldNames {
children?: string;
title?: string;
key?: string;
}
export type KeyType = string | number;
export type CheckKeys =
| KeyType[]
| { checked: string[] | number[]; halfChecked: string[] | number[] };
export const treeProps = buildProps({
value: {
type: [Object, Array] as PropType<KeyType[] | CheckKeys>,
},
renderIcon: {
type: Function as PropType<(params: Recordable) => string>,
},
helpMessage: {
type: [String, Array] as PropType<string | string[]>,
default: '',
},
title: {
type: String,
default: '',
},
toolbar: Boolean,
search: Boolean,
searchValue: {
type: String,
default: '',
},
checkStrictly: Boolean,
clickRowToExpand: {
type: Boolean,
default: false,
},
checkable: Boolean,
defaultExpandLevel: {
type: [String, Number] as PropType<string | number>,
default: '',
},
defaultExpandAll: Boolean,
fieldNames: {
type: Object as PropType<FieldNames>,
},
treeData: {
type: Array as PropType<TreeDataItem[]>,
},
actionList: {
type: Array as PropType<TreeActionItem[]>,
default: () => [],
},
expandedKeys: {
type: Array as PropType<KeyType[]>,
default: () => [],
},
selectedKeys: {
type: Array as PropType<KeyType[]>,
default: () => [],
},
checkedKeys: {
type: Array as PropType<CheckKeys>,
default: () => [],
},
beforeRightClick: {
type: Function as PropType<(...arg: any) => ContextMenuItem[] | ContextMenuOptions>,
default: undefined,
},
rightMenuList: {
type: Array as PropType<ContextMenuItem[]>,
},
// 自定义数据过滤判断方法(注: 不是整个过滤方法而是内置过滤的判断方法用于增强原本仅能通过title进行过滤的方式)
filterFn: {
type: Function as PropType<
(searchValue: any, node: TreeItem, replaceFields: FieldNames) => boolean
>,
default: undefined,
},
// 高亮搜索值仅高亮具体匹配值通过title值为true时使用默认色值值为#xxx时使用此值替代且高亮开启
highlight: {
type: [Boolean, String] as PropType<Boolean | String>,
default: false,
},
// 搜索完成时自动展开结果
expandOnSearch: Boolean,
// 搜索完成自动选中所有结果,当且仅当 checkable===true 时生效
checkOnSearch: Boolean,
// 搜索完成自动select所有结果
selectedOnSearch: Boolean,
});
export type TreeProps = ExtractPropTypes<typeof treeProps>;
export interface ContextMenuItem {
label: string;
icon?: string;
disabled?: boolean;
handler?: Fn;
divider?: boolean;
children?: ContextMenuItem[];
}
export interface ContextMenuOptions {
icon?: string;
styles?: any;
items?: ContextMenuItem[];
}
export interface TreeItem extends TreeDataItem {
icon?: any;
}
export interface TreeActionItem {
render: (record: Recordable) => any;
show?: boolean | ((record: Recordable) => boolean);
}
export interface InsertNodeParams {
parentKey: string | null;
node: TreeDataItem;
list?: TreeDataItem[];
push?: 'push' | 'unshift';
}
export interface TreeActionType {
checkAll: (checkAll: boolean) => void;
expandAll: (expandAll: boolean) => void;
setExpandedKeys: (keys: KeyType[]) => void;
getExpandedKeys: () => KeyType[];
setSelectedKeys: (keys: KeyType[]) => void;
getSelectedKeys: () => KeyType[];
setCheckedKeys: (keys: CheckKeys) => void;
getCheckedKeys: () => CheckKeys;
filterByLevel: (level: number) => void;
insertNodeByKey: (opt: InsertNodeParams) => void;
insertNodesByKey: (opt: InsertNodeParams) => void;
deleteNodeByKey: (key: string) => void;
updateNodeByKey: (key: string, node: Omit<TreeDataItem, 'key'>) => void;
setSearchValue: (value: string) => void;
getSearchValue: () => string;
}

View File

@ -1,56 +0,0 @@
import type { TreeDataItem, CheckEvent as CheckEventOrigin } from 'ant-design-vue/es/tree/Tree';
import { ContextMenuItem } from '/@/hooks/web/useContextMenu';
export interface ActionItem {
render: (record: Recordable) => any;
show?: boolean | ((record: Recordable) => boolean);
}
export interface TreeItem extends TreeDataItem {
icon?: 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 {
checkAll: (checkAll: boolean) => void;
expandAll: (expandAll: boolean) => void;
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;
insertNodesByKey: (opt: InsertNodeParams) => void;
deleteNodeByKey: (key: string) => void;
updateNodeByKey: (key: string, node: Omit<TreeDataItem, 'key'>) => void;
setSearchValue: (value: string) => void;
getSearchValue: () => string;
}
export interface InsertNodeParams {
parentKey: string | null;
node: TreeDataItem;
list?: TreeDataItem[];
push?: 'push' | 'unshift';
}
export interface ContextMenuOptions {
icon?: string;
styles?: any;
items?: ContextMenuItem[];
}
export type CheckEvent = CheckEventOrigin;

View File

@ -1,4 +1,4 @@
import type { InsertNodeParams, Keys, ReplaceFields } from './typing';
import type { InsertNodeParams, KeyType, FieldNames } from './tree';
import type { Ref, ComputedRef } from 'vue';
import type { TreeDataItem } from 'ant-design-vue/es/tree/Tree';
@ -6,14 +6,11 @@ import { cloneDeep } from 'lodash-es';
import { unref } from 'vue';
import { forEach } from '/@/utils/helper/treeHelper';
export function useTree(
treeDataRef: Ref<TreeDataItem[]>,
getReplaceFields: ComputedRef<ReplaceFields>,
) {
export function useTree(treeDataRef: Ref<TreeDataItem[]>, getFieldNames: ComputedRef<FieldNames>) {
function getAllKeys(list?: TreeDataItem[]) {
const keys: string[] = [];
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return keys;
for (let index = 0; index < treeData.length; index++) {
@ -24,14 +21,14 @@ export function useTree(
keys.push(...(getAllKeys(children) as string[]));
}
}
return keys as Keys;
return keys as KeyType[];
}
// get keys that can be checked and selected
function getEnabledKeys(list?: TreeDataItem[]) {
const keys: string[] = [];
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return keys;
for (let index = 0; index < treeData.length; index++) {
@ -42,13 +39,13 @@ export function useTree(
keys.push(...(getEnabledKeys(children) as string[]));
}
}
return keys as Keys;
return keys as KeyType[];
}
function getChildrenKeys(nodeKey: string | number, list?: TreeDataItem[]): Keys {
const keys: Keys = [];
function getChildrenKeys(nodeKey: string | number, list?: TreeDataItem[]) {
const keys: KeyType[] = [];
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return keys;
for (let index = 0; index < treeData.length; index++) {
const node = treeData[index];
@ -64,14 +61,14 @@ export function useTree(
}
}
}
return keys as Keys;
return keys as KeyType[];
}
// Update node
function updateNodeByKey(key: string, node: TreeDataItem, list?: TreeDataItem[]) {
if (!key) return;
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return;
@ -98,7 +95,7 @@ export function useTree(
for (let index = 0; index < data.length; index++) {
const item = data[index];
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
const key = keyField ? item[keyField] : '';
const children = childrenField ? item[childrenField] : [];
res.push(key);
@ -120,7 +117,7 @@ export function useTree(
treeDataRef.value = treeData;
return;
}
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return;
forEach(treeData, (treeItem) => {
@ -145,7 +142,7 @@ export function useTree(
treeData[push](list[i]);
}
} else {
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return;
forEach(treeData, (treeItem) => {
@ -164,7 +161,7 @@ export function useTree(
function deleteNodeByKey(key: string, list?: TreeDataItem[]) {
if (!key) return;
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
const { key: keyField, children: childrenField } = unref(getFieldNames);
if (!childrenField || !keyField) return;
for (let index = 0; index < treeData.length; index++) {

View File

@ -0,0 +1,49 @@
@tree-prefix-cls: ~'@{namespace}-tree';
.@{tree-prefix-cls} {
background-color: @component-background;
.ant-tree-node-content-wrapper {
position: relative;
.ant-tree-title {
position: absolute;
left: 0;
width: 100%;
}
}
&__title {
position: relative;
display: flex;
align-items: center;
width: 100%;
padding-right: 10px;
&:hover {
.@{tree-prefix-cls}__action {
visibility: visible;
}
}
}
&__content {
overflow: hidden;
}
&__actions {
position: absolute;
top: 2px;
right: 3px;
display: flex;
}
&__action {
margin-left: 4px;
visibility: hidden;
}
&-header {
border-bottom: 1px solid @border-color-base;
}
}

View File

@ -0,0 +1 @@
import './index.less';

View File

@ -15,13 +15,6 @@ import { setupGlobDirectives } from '/@/directives';
import { setupI18n } from '/@/locales/setupI18n';
import { registerGlobComp } from '/@/components/registerGlobComp';
// Importing on demand in local development will increase the number of browser requests by around 20%.
// This may slow down the browser refresh speed.
// Therefore, only enable on-demand importing in production environments .
if (import.meta.env.DEV) {
import('ant-design-vue/dist/antd.less');
}
async function bootstrap() {
const app = createApp(App);

52
src/utils/bem.ts Normal file
View File

@ -0,0 +1,52 @@
import { prefixCls } from '/@/settings/designSetting';
type Mod = string | { [key: string]: any };
type Mods = Mod | Mod[];
export type BEM = ReturnType<typeof createBEM>;
function genBem(name: string, mods?: Mods): string {
if (!mods) {
return '';
}
if (typeof mods === 'string') {
return ` ${name}--${mods}`;
}
if (Array.isArray(mods)) {
return mods.reduce<string>((ret, item) => ret + genBem(name, item), '');
}
return Object.keys(mods).reduce((ret, key) => ret + (mods[key] ? genBem(name, key) : ''), '');
}
/**
* bem helper
* b() // 'button'
* b('text') // 'button__text'
* b({ disabled }) // 'button button--disabled'
* b('text', { disabled }) // 'button__text button__text--disabled'
* b(['disabled', 'primary']) // 'button button--disabled button--primary'
*/
export function buildBEM(name: string) {
return (el?: Mods, mods?: Mods): Mods => {
if (el && typeof el !== 'string') {
mods = el;
el = '';
}
el = el ? `${name}__${el}` : name;
return `${el}${genBem(el, mods)}`;
};
}
export function createBEM(name: string) {
return [buildBEM(`${prefixCls}-${name}`)];
}
export function createNamespace(name: string) {
const prefixedName = `${prefixCls}-${name}`;
return [prefixedName, buildBEM(prefixedName)] as const;
}

185
src/utils/props.ts Normal file
View File

@ -0,0 +1,185 @@
// copy from element-plus
import { warn } from 'vue';
import { isObject } from '@vue/shared';
import { fromPairs } from 'lodash-es';
import type { ExtractPropTypes, PropType } from '@vue/runtime-core';
import type { Mutable } from './types';
const wrapperKey = Symbol();
export type PropWrapper<T> = { [wrapperKey]: T };
export const propKey = Symbol();
type ResolveProp<T> = ExtractPropTypes<{
key: { type: T; required: true };
}>['key'];
type ResolvePropType<T> = ResolveProp<T> extends { type: infer V } ? V : ResolveProp<T>;
type ResolvePropTypeWithReadonly<T> = Readonly<T> extends Readonly<Array<infer A>>
? ResolvePropType<A[]>
: ResolvePropType<T>;
type IfUnknown<T, V> = [unknown] extends [T] ? V : T;
export type BuildPropOption<T, D extends BuildPropType<T, V, C>, R, V, C> = {
type?: T;
values?: readonly V[];
required?: R;
default?: R extends true
? never
: D extends Record<string, unknown> | Array<any>
? () => D
: (() => D) | D;
validator?: ((val: any) => val is C) | ((val: any) => boolean);
};
type _BuildPropType<T, V, C> =
| (T extends PropWrapper<unknown>
? T[typeof wrapperKey]
: [V] extends [never]
? ResolvePropTypeWithReadonly<T>
: never)
| V
| C;
export type BuildPropType<T, V, C> = _BuildPropType<
IfUnknown<T, never>,
IfUnknown<V, never>,
IfUnknown<C, never>
>;
type _BuildPropDefault<T, D> = [T] extends [
// eslint-disable-next-line @typescript-eslint/ban-types
Record<string, unknown> | Array<any> | Function,
]
? D
: D extends () => T
? ReturnType<D>
: D;
export type BuildPropDefault<T, D, R> = R extends true
? { readonly default?: undefined }
: {
readonly default: Exclude<D, undefined> extends never
? undefined
: Exclude<_BuildPropDefault<T, D>, undefined>;
};
export type BuildPropReturn<T, D, R, V, C> = {
readonly type: PropType<BuildPropType<T, V, C>>;
readonly required: IfUnknown<R, false>;
readonly validator: ((val: unknown) => boolean) | undefined;
[propKey]: true;
} & BuildPropDefault<BuildPropType<T, V, C>, IfUnknown<D, never>, IfUnknown<R, false>>;
/**
* @description Build prop. It can better optimize prop types
* @description prop
* @example
// limited options
// the type will be PropType<'light' | 'dark'>
buildProp({
type: String,
values: ['light', 'dark'],
} as const)
* @example
// limited options and other types
// the type will be PropType<'small' | 'medium' | number>
buildProp({
type: [String, Number],
values: ['small', 'medium'],
validator: (val: unknown): val is number => typeof val === 'number',
} as const)
@link see more: https://github.com/element-plus/element-plus/pull/3341
*/
export function buildProp<
T = never,
D extends BuildPropType<T, V, C> = never,
R extends boolean = false,
V = never,
C = never,
>(option: BuildPropOption<T, D, R, V, C>, key?: string): BuildPropReturn<T, D, R, V, C> {
// filter native prop type and nested prop, e.g `null`, `undefined` (from `buildProps`)
if (!isObject(option) || !!option[propKey]) return option as any;
const { values, required, default: defaultValue, type, validator } = option;
const _validator =
values || validator
? (val: unknown) => {
let valid = false;
let allowedValues: unknown[] = [];
if (values) {
allowedValues = [...values, defaultValue];
valid ||= allowedValues.includes(val);
}
if (validator) valid ||= validator(val);
if (!valid && allowedValues.length > 0) {
const allowValuesText = [...new Set(allowedValues)]
.map((value) => JSON.stringify(value))
.join(', ');
warn(
`Invalid prop: validation failed${
key ? ` for prop "${key}"` : ''
}. Expected one of [${allowValuesText}], got value ${JSON.stringify(val)}.`,
);
}
return valid;
}
: undefined;
return {
type:
typeof type === 'object' && Object.getOwnPropertySymbols(type).includes(wrapperKey)
? type[wrapperKey]
: type,
required: !!required,
default: defaultValue,
validator: _validator,
[propKey]: true,
} as unknown as BuildPropReturn<T, D, R, V, C>;
}
type NativePropType = [((...args: any) => any) | { new (...args: any): any } | undefined | null];
export const buildProps = <
O extends {
[K in keyof O]: O[K] extends BuildPropReturn<any, any, any, any, any>
? O[K]
: [O[K]] extends NativePropType
? O[K]
: O[K] extends BuildPropOption<infer T, infer D, infer R, infer V, infer C>
? D extends BuildPropType<T, V, C>
? BuildPropOption<T, D, R, V, C>
: never
: never;
},
>(
props: O,
) =>
fromPairs(
Object.entries(props).map(([key, option]) => [key, buildProp(option as any, key)]),
) as unknown as {
[K in keyof O]: O[K] extends { [propKey]: boolean }
? O[K]
: [O[K]] extends NativePropType
? O[K]
: O[K] extends BuildPropOption<
infer T,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
infer _D,
infer R,
infer V,
infer C
>
? BuildPropReturn<T, O[K]['default'], R, V, C>
: never;
};
export const definePropType = <T>(val: any) => ({ [wrapperKey]: val } as PropWrapper<T>);
export const keyOf = <T>(arr: T) => Object.keys(arr) as Array<keyof T>;
export const mutable = <T extends readonly any[] | Record<string, unknown>>(val: T) =>
val as Mutable<typeof val>;
export const componentSize = ['large', 'medium', 'small', 'mini'] as const;

42
src/utils/types.ts Normal file
View File

@ -0,0 +1,42 @@
// copy from element-plus
import type { CSSProperties, Plugin } from 'vue';
type OptionalKeys<T extends Record<string, unknown>> = {
[K in keyof T]: T extends Record<K, T[K]> ? never : K;
}[keyof T];
type RequiredKeys<T extends Record<string, unknown>> = Exclude<keyof T, OptionalKeys<T>>;
type MonoArgEmitter<T, Keys extends keyof T> = <K extends Keys>(evt: K, arg?: T[K]) => void;
type BiArgEmitter<T, Keys extends keyof T> = <K extends Keys>(evt: K, arg: T[K]) => void;
export type EventEmitter<T extends Record<string, unknown>> = MonoArgEmitter<T, OptionalKeys<T>> &
BiArgEmitter<T, RequiredKeys<T>>;
export type AnyFunction<T> = (...args: any[]) => T;
export type PartialReturnType<T extends (...args: unknown[]) => unknown> = Partial<ReturnType<T>>;
export type SFCWithInstall<T> = T & Plugin;
export type Nullable<T> = T | null;
export type RefElement = Nullable<HTMLElement>;
export type CustomizedHTMLElement<T> = HTMLElement & T;
export type Indexable<T> = {
[key: string]: T;
};
export type Hash<T> = Indexable<T>;
export type TimeoutHandle = ReturnType<typeof global.setTimeout>;
export type ComponentSize = 'large' | 'medium' | 'small' | 'mini';
export type StyleValue = string | CSSProperties | Array<StyleValue>;
export type Mutable<T> = { -readonly [P in keyof T]: T[P] };

View File

@ -95,6 +95,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
optimizeDeps: {
// @iconify/iconify: The dependency is dynamically and virtually loaded by @purge-icons/generated, so it needs to be specified explicitly
include: [
'@vue/shared',
'@iconify/iconify',
'ant-design-vue/es/locale/zh_CN',
'ant-design-vue/es/locale/en_US',