mirror of
https://github.com/vbenjs/gf-vben-admin.git
synced 2025-01-23 11:50:20 +08:00
refactor(tree): Refactor tree to support antv3.0
This commit is contained in:
parent
50cf2d0b8f
commit
52257f061d
@ -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',
|
||||
},
|
||||
});
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -132,6 +132,7 @@
|
||||
"brotli",
|
||||
"tailwindcss",
|
||||
"sider",
|
||||
"pnpm"
|
||||
"pnpm",
|
||||
"antd"
|
||||
]
|
||||
}
|
||||
|
@ -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;
|
||||
// },
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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",
|
||||
|
752
pnpm-lock.yaml
generated
752
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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';
|
||||
|
@ -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 searchText = searchState.searchText;
|
||||
const { highlight } = unref(props);
|
||||
return data.map((item) => {
|
||||
const treeData = computed(() => {
|
||||
const data = cloneDeep(getTreeData.value);
|
||||
data.forEach((item) => {
|
||||
const searchText = searchState.searchText;
|
||||
const { highlight } = unref(props);
|
||||
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,41 +381,34 @@
|
||||
) : (
|
||||
title
|
||||
);
|
||||
|
||||
return (
|
||||
<Tree.TreeNode {...propsData} node={toRaw(item)} key={get(item, keyField)}>
|
||||
{{
|
||||
title: () => (
|
||||
<span
|
||||
class={`${prefixCls}-title pl-2`}
|
||||
onClick={handleClickNode.bind(null, item[keyField], item[childrenField])}
|
||||
>
|
||||
{item.slots?.title ? (
|
||||
getSlot(slots, item.slots?.title, item)
|
||||
) : (
|
||||
<>
|
||||
{icon && <TreeIcon icon={icon} />}
|
||||
{titleDom}
|
||||
{/*{get(item, titleField)}*/}
|
||||
<span class={`${prefixCls}__actions`}>
|
||||
{renderAction({ ...item, level })}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
default: () => renderTreeNode({ data: children, level: level + 1 }),
|
||||
}}
|
||||
</Tree.TreeNode>
|
||||
item.title = (
|
||||
<span
|
||||
class={`${bem('title')} pl-2`}
|
||||
onClick={handleClickNode.bind(null, item[keyField], item[childrenField])}
|
||||
>
|
||||
{item.slots?.title ? (
|
||||
getSlot(slots, item.slots?.title, item)
|
||||
) : (
|
||||
<>
|
||||
{icon && <TreeIcon icon={icon} />}
|
||||
{titleDom}
|
||||
<span class={bem('actions')}>{renderAction(item)}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
}
|
||||
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>
|
||||
|
@ -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,148 +32,140 @@
|
||||
</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,
|
||||
const [bem] = createBEM('tree-header');
|
||||
|
||||
// eslint-disable vue/valid-define-emits
|
||||
const props = defineProps({
|
||||
helpMessage: {
|
||||
type: [String, Array] as PropType<string | string[]>,
|
||||
default: '',
|
||||
},
|
||||
props: {
|
||||
helpMessage: {
|
||||
type: [String, Array] as PropType<string | string[]>,
|
||||
default: '',
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
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 getInputSearchCls = computed(() => {
|
||||
const titleExists = slots.headerTitle || props.title;
|
||||
return [
|
||||
'mr-1',
|
||||
'w-full',
|
||||
{
|
||||
['ml-5']: titleExists,
|
||||
},
|
||||
title: propTypes.string,
|
||||
toolbar: propTypes.bool,
|
||||
checkable: propTypes.bool,
|
||||
search: propTypes.bool,
|
||||
checkAll: propTypes.func,
|
||||
expandAll: propTypes.func,
|
||||
searchText: propTypes.string,
|
||||
},
|
||||
emits: ['strictly-change', 'search'],
|
||||
setup(props, { emit, slots }) {
|
||||
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,
|
||||
},
|
||||
];
|
||||
});
|
||||
const toolbarList = computed(() => {
|
||||
const { checkable } = props;
|
||||
const defaultToolbarList = [
|
||||
{ label: t('component.tree.expandAll'), value: ToolbarEnum.EXPAND_ALL },
|
||||
{
|
||||
label: t('component.tree.unExpandAll'),
|
||||
value: ToolbarEnum.UN_EXPAND_ALL,
|
||||
divider: checkable,
|
||||
},
|
||||
];
|
||||
|
||||
const toolbarList = computed(() => {
|
||||
const { checkable } = props;
|
||||
const defaultToolbarList = [
|
||||
{ label: t('component.tree.expandAll'), value: ToolbarEnum.EXPAND_ALL },
|
||||
return checkable
|
||||
? [
|
||||
{ label: t('component.tree.selectAll'), value: ToolbarEnum.SELECT_ALL },
|
||||
{
|
||||
label: t('component.tree.unExpandAll'),
|
||||
value: ToolbarEnum.UN_EXPAND_ALL,
|
||||
label: t('component.tree.unSelectAll'),
|
||||
value: ToolbarEnum.UN_SELECT_ALL,
|
||||
divider: checkable,
|
||||
},
|
||||
];
|
||||
|
||||
return checkable
|
||||
? [
|
||||
{ label: t('component.tree.selectAll'), value: ToolbarEnum.SELECT_ALL },
|
||||
{
|
||||
label: t('component.tree.unSelectAll'),
|
||||
value: ToolbarEnum.UN_SELECT_ALL,
|
||||
divider: checkable,
|
||||
},
|
||||
...defaultToolbarList,
|
||||
{ label: t('component.tree.checkStrictly'), value: ToolbarEnum.CHECK_STRICTLY },
|
||||
{ label: t('component.tree.checkUnStrictly'), value: ToolbarEnum.CHECK_UN_STRICTLY },
|
||||
]
|
||||
: defaultToolbarList;
|
||||
});
|
||||
|
||||
function handleMenuClick(e: MenuInfo) {
|
||||
const { key } = e;
|
||||
switch (key) {
|
||||
case ToolbarEnum.SELECT_ALL:
|
||||
props.checkAll?.(true);
|
||||
break;
|
||||
case ToolbarEnum.UN_SELECT_ALL:
|
||||
props.checkAll?.(false);
|
||||
break;
|
||||
case ToolbarEnum.EXPAND_ALL:
|
||||
props.expandAll?.(true);
|
||||
break;
|
||||
case ToolbarEnum.UN_EXPAND_ALL:
|
||||
props.expandAll?.(false);
|
||||
break;
|
||||
case ToolbarEnum.CHECK_STRICTLY:
|
||||
emit('strictly-change', false);
|
||||
break;
|
||||
case ToolbarEnum.CHECK_UN_STRICTLY:
|
||||
emit('strictly-change', true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function emitChange(value?: string): void {
|
||||
emit('search', value);
|
||||
}
|
||||
const debounceEmitChange = useDebounceFn(emitChange, 200);
|
||||
|
||||
watch(
|
||||
() => searchValue.value,
|
||||
(v) => {
|
||||
debounceEmitChange(v);
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.searchText,
|
||||
(v) => {
|
||||
if (v !== searchValue.value) {
|
||||
searchValue.value = v;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return { t, toolbarList, handleMenuClick, searchValue, getInputSearchCls };
|
||||
},
|
||||
...defaultToolbarList,
|
||||
{ label: t('component.tree.checkStrictly'), value: ToolbarEnum.CHECK_STRICTLY },
|
||||
{ label: t('component.tree.checkUnStrictly'), value: ToolbarEnum.CHECK_UN_STRICTLY },
|
||||
]
|
||||
: defaultToolbarList;
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.basic-tree-header {
|
||||
border-bottom: 1px solid @border-color-base;
|
||||
|
||||
function handleMenuClick(e: { key: ToolbarEnum }) {
|
||||
const { key } = e;
|
||||
switch (key) {
|
||||
case ToolbarEnum.SELECT_ALL:
|
||||
props.checkAll?.(true);
|
||||
break;
|
||||
case ToolbarEnum.UN_SELECT_ALL:
|
||||
props.checkAll?.(false);
|
||||
break;
|
||||
case ToolbarEnum.EXPAND_ALL:
|
||||
props.expandAll?.(true);
|
||||
break;
|
||||
case ToolbarEnum.UN_EXPAND_ALL:
|
||||
props.expandAll?.(false);
|
||||
break;
|
||||
case ToolbarEnum.CHECK_STRICTLY:
|
||||
emit('strictly-change', false);
|
||||
break;
|
||||
case ToolbarEnum.CHECK_UN_STRICTLY:
|
||||
emit('strictly-change', true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
function emitChange(value?: string): void {
|
||||
emit('search', value);
|
||||
}
|
||||
|
||||
const debounceEmitChange = useDebounceFn(emitChange, 200);
|
||||
|
||||
watch(
|
||||
() => searchValue.value,
|
||||
(v) => {
|
||||
debounceEmitChange(v);
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.searchText,
|
||||
(v) => {
|
||||
if (v !== searchValue.value) {
|
||||
searchValue.value = v;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
@ -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' });
|
||||
|
@ -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: () => [],
|
||||
},
|
||||
};
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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++) {
|
||||
|
49
src/components/Tree/style/index.less
Normal file
49
src/components/Tree/style/index.less
Normal 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;
|
||||
}
|
||||
}
|
1
src/components/Tree/style/index.ts
Normal file
1
src/components/Tree/style/index.ts
Normal file
@ -0,0 +1 @@
|
||||
import './index.less';
|
@ -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
52
src/utils/bem.ts
Normal 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
185
src/utils/props.ts
Normal 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
42
src/utils/types.ts
Normal 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] };
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user