mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-01-24 02:00:25 +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/singleline-html-element-content-newline': 'off',
|
||||||
'vue/attribute-hyphenation': 'off',
|
'vue/attribute-hyphenation': 'off',
|
||||||
'vue/require-default-prop': 'off',
|
'vue/require-default-prop': 'off',
|
||||||
|
'vue/require-explicit-emits': 'off',
|
||||||
'vue/html-self-closing': [
|
'vue/html-self-closing': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
@ -74,6 +75,6 @@ module.exports = defineConfig({
|
|||||||
math: 'always',
|
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",
|
"brotli",
|
||||||
"tailwindcss",
|
"tailwindcss",
|
||||||
"sider",
|
"sider",
|
||||||
"pnpm"
|
"pnpm",
|
||||||
|
"antd"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -9,17 +9,17 @@ import type { Plugin } from 'vite';
|
|||||||
export function configHmrPlugin(): Plugin {
|
export function configHmrPlugin(): Plugin {
|
||||||
return {
|
return {
|
||||||
name: 'singleHMR',
|
name: 'singleHMR',
|
||||||
handleHotUpdate({ modules, file }) {
|
// handleHotUpdate({ modules, file }) {
|
||||||
if (file.match(/xml$/)) return [];
|
// if (file.match(/xml$/)) return [];
|
||||||
|
|
||||||
modules.forEach((m) => {
|
// modules.forEach((m) => {
|
||||||
if (!m.url.match(/\.(css|less)/)) {
|
// if (!m.url.match(/\.(css|less)/)) {
|
||||||
m.importedModules = new Set();
|
// m.importedModules = new Set();
|
||||||
m.importers = new Set();
|
// m.importers = new Set();
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
return modules;
|
// return modules;
|
||||||
},
|
// },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
import styleImport from 'vite-plugin-style-import';
|
import styleImport from 'vite-plugin-style-import';
|
||||||
|
|
||||||
export function configStyleImportPlugin(isBuild: boolean) {
|
export function configStyleImportPlugin(_isBuild: boolean) {
|
||||||
if (!isBuild) {
|
// if (!isBuild) {
|
||||||
return [];
|
// return [];
|
||||||
}
|
// }
|
||||||
const styleImportPlugin = styleImport({
|
const styleImportPlugin = styleImport({
|
||||||
libs: [
|
libs: [
|
||||||
{
|
{
|
||||||
@ -19,6 +19,7 @@ export function configStyleImportPlugin(isBuild: boolean) {
|
|||||||
'anchor-link',
|
'anchor-link',
|
||||||
'sub-menu',
|
'sub-menu',
|
||||||
'menu-item',
|
'menu-item',
|
||||||
|
'menu-divider',
|
||||||
'menu-item-group',
|
'menu-item-group',
|
||||||
'breadcrumb-item',
|
'breadcrumb-item',
|
||||||
'breadcrumb-separator',
|
'breadcrumb-separator',
|
||||||
|
@ -72,7 +72,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^15.0.0",
|
"@commitlint/cli": "^15.0.0",
|
||||||
"@commitlint/config-conventional": "^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",
|
"@purge-icons/generated": "^0.7.0",
|
||||||
"@types/codemirror": "^5.60.5",
|
"@types/codemirror": "^5.60.5",
|
||||||
"@types/crypto-js": "^4.0.2",
|
"@types/crypto-js": "^4.0.2",
|
||||||
@ -110,7 +110,7 @@
|
|||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"inquirer": "^8.2.0",
|
"inquirer": "^8.2.0",
|
||||||
"jest": "^27.3.1",
|
"jest": "^27.4.0",
|
||||||
"less": "^4.1.2",
|
"less": "^4.1.2",
|
||||||
"lint-staged": "12.1.2",
|
"lint-staged": "12.1.2",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
@ -135,14 +135,14 @@
|
|||||||
"vite-plugin-imagemin": "^0.4.6",
|
"vite-plugin-imagemin": "^0.4.6",
|
||||||
"vite-plugin-mock": "^2.9.6",
|
"vite-plugin-mock": "^2.9.6",
|
||||||
"vite-plugin-purge-icons": "^0.7.0",
|
"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-style-import": "^1.4.0",
|
||||||
"vite-plugin-svg-icons": "^1.0.5",
|
"vite-plugin-svg-icons": "^1.0.5",
|
||||||
"vite-plugin-theme": "^0.8.1",
|
"vite-plugin-theme": "^0.8.1",
|
||||||
"vite-plugin-vue-setup-extend": "^0.1.0",
|
"vite-plugin-vue-setup-extend": "^0.1.0",
|
||||||
"vite-plugin-windicss": "^1.5.3",
|
"vite-plugin-windicss": "^1.5.3",
|
||||||
"vue-eslint-parser": "^8.0.1",
|
"vue-eslint-parser": "^8.0.1",
|
||||||
"vue-tsc": "^0.29.6"
|
"vue-tsc": "^0.29.7"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"//": "Used to install imagemin dependencies, because imagemin may not be installed in China. If it is abroad, you can delete it",
|
"//": "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 BasicTree from './src/Tree.vue';
|
||||||
|
import './style';
|
||||||
|
|
||||||
export { BasicTree };
|
export { BasicTree };
|
||||||
export type { ContextMenuItem } from '/@/hooks/web/useContextMenu';
|
export type { ContextMenuItem } from '/@/hooks/web/useContextMenu';
|
||||||
export * from './src/typing';
|
export * from './src/tree';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="tsx">
|
<script lang="tsx">
|
||||||
import type { ReplaceFields, Keys, CheckKeys, TreeActionType, TreeItem } from './typing';
|
import type { CSSProperties } from 'vue';
|
||||||
import type { CheckEvent } from './typing';
|
import type { FieldNames, TreeState, TreeItem, KeyType, CheckKeys, TreeActionType } from './tree';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
defineComponent,
|
defineComponent,
|
||||||
@ -11,43 +11,31 @@
|
|||||||
watchEffect,
|
watchEffect,
|
||||||
toRaw,
|
toRaw,
|
||||||
watch,
|
watch,
|
||||||
CSSProperties,
|
|
||||||
onMounted,
|
onMounted,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { Tree, Empty } from 'ant-design-vue';
|
import { Tree, Empty } from 'ant-design-vue';
|
||||||
import { TreeIcon } from './TreeIcon';
|
import { TreeIcon } from './TreeIcon';
|
||||||
import { ScrollContainer } from '/@/components/Container';
|
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 { isArray, isBoolean, isEmpty, isFunction } from '/@/utils/is';
|
||||||
import { extendSlots, getSlot } from '/@/utils/helper/tsxHelper';
|
import { extendSlots, getSlot } from '/@/utils/helper/tsxHelper';
|
||||||
import { filter, treeToList } from '/@/utils/helper/treeHelper';
|
import { filter, treeToList } from '/@/utils/helper/treeHelper';
|
||||||
import { useTree } from './useTree';
|
import { useTree } from './useTree';
|
||||||
import { useContextMenu } from '/@/hooks/web/useContextMenu';
|
import { useContextMenu } from '/@/hooks/web/useContextMenu';
|
||||||
import { useDesign } from '/@/hooks/web/useDesign';
|
|
||||||
import { basicProps } from './props';
|
|
||||||
import { CreateContextOptions } from '/@/components/ContextMenu';
|
import { CreateContextOptions } from '/@/components/ContextMenu';
|
||||||
import TreeHeader from './TreeHeader.vue';
|
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({
|
export default defineComponent({
|
||||||
name: 'BasicTree',
|
name: 'BasicTree',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: basicProps,
|
props: treeProps,
|
||||||
emits: [
|
emits: treeEmits,
|
||||||
'update:expandedKeys',
|
|
||||||
'update:selectedKeys',
|
|
||||||
'update:value',
|
|
||||||
'change',
|
|
||||||
'check',
|
|
||||||
'update:searchValue',
|
|
||||||
],
|
|
||||||
setup(props, { attrs, slots, emit, expose }) {
|
setup(props, { attrs, slots, emit, expose }) {
|
||||||
const state = reactive<State>({
|
const [bem] = createBEM('tree');
|
||||||
|
|
||||||
|
const state = reactive<TreeState>({
|
||||||
checkStrictly: props.checkStrictly,
|
checkStrictly: props.checkStrictly,
|
||||||
expandedKeys: props.expandedKeys || [],
|
expandedKeys: props.expandedKeys || [],
|
||||||
selectedKeys: props.selectedKeys || [],
|
selectedKeys: props.selectedKeys || [],
|
||||||
@ -63,15 +51,14 @@
|
|||||||
const treeDataRef = ref<TreeItem[]>([]);
|
const treeDataRef = ref<TreeItem[]>([]);
|
||||||
|
|
||||||
const [createContextMenu] = useContextMenu();
|
const [createContextMenu] = useContextMenu();
|
||||||
const { prefixCls } = useDesign('basic-tree');
|
|
||||||
|
|
||||||
const getReplaceFields = computed((): Required<ReplaceFields> => {
|
const getFieldNames = computed((): Required<FieldNames> => {
|
||||||
const { replaceFields } = props;
|
const { fieldNames } = props;
|
||||||
return {
|
return {
|
||||||
children: 'children',
|
children: 'children',
|
||||||
title: 'title',
|
title: 'title',
|
||||||
key: 'key',
|
key: 'key',
|
||||||
...replaceFields,
|
...fieldNames,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -84,19 +71,19 @@
|
|||||||
selectedKeys: state.selectedKeys,
|
selectedKeys: state.selectedKeys,
|
||||||
checkedKeys: state.checkedKeys,
|
checkedKeys: state.checkedKeys,
|
||||||
checkStrictly: state.checkStrictly,
|
checkStrictly: state.checkStrictly,
|
||||||
replaceFields: unref(getReplaceFields),
|
filedNames: unref(getFieldNames),
|
||||||
'onUpdate:expandedKeys': (v: Keys) => {
|
'onUpdate:expandedKeys': (v: KeyType[]) => {
|
||||||
state.expandedKeys = v;
|
state.expandedKeys = v;
|
||||||
emit('update:expandedKeys', v);
|
emit('update:expandedKeys', v);
|
||||||
},
|
},
|
||||||
'onUpdate:selectedKeys': (v: Keys) => {
|
'onUpdate:selectedKeys': (v: KeyType[]) => {
|
||||||
state.selectedKeys = v;
|
state.selectedKeys = v;
|
||||||
emit('update:selectedKeys', v);
|
emit('update:selectedKeys', v);
|
||||||
},
|
},
|
||||||
onCheck: (v: CheckKeys, e: CheckEvent) => {
|
onCheck: (v: CheckKeys, e) => {
|
||||||
let currentValue = toRaw(state.checkedKeys) as Keys;
|
let currentValue = toRaw(state.checkedKeys) as KeyType[];
|
||||||
if (isArray(currentValue) && searchState.startSearch) {
|
if (isArray(currentValue) && searchState.startSearch) {
|
||||||
const { key } = unref(getReplaceFields);
|
const { key } = unref(getFieldNames);
|
||||||
currentValue = difference(currentValue, getChildrenKeys(e.node.$attrs.node[key]));
|
currentValue = difference(currentValue, getChildrenKeys(e.node.$attrs.node[key]));
|
||||||
if (e.checked) {
|
if (e.checked) {
|
||||||
currentValue.push(e.node.$attrs.node[key]);
|
currentValue.push(e.node.$attrs.node[key]);
|
||||||
@ -132,7 +119,7 @@
|
|||||||
getAllKeys,
|
getAllKeys,
|
||||||
getChildrenKeys,
|
getChildrenKeys,
|
||||||
getEnabledKeys,
|
getEnabledKeys,
|
||||||
} = useTree(treeDataRef, getReplaceFields);
|
} = useTree(treeDataRef, getFieldNames);
|
||||||
|
|
||||||
function getIcon(params: Recordable, icon?: string) {
|
function getIcon(params: Recordable, icon?: string) {
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
@ -161,14 +148,14 @@
|
|||||||
createContextMenu(contextMenuOptions);
|
createContextMenu(contextMenuOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setExpandedKeys(keys: Keys) {
|
function setExpandedKeys(keys: KeyType[]) {
|
||||||
state.expandedKeys = keys;
|
state.expandedKeys = keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExpandedKeys() {
|
function getExpandedKeys() {
|
||||||
return state.expandedKeys;
|
return state.expandedKeys;
|
||||||
}
|
}
|
||||||
function setSelectedKeys(keys: Keys) {
|
function setSelectedKeys(keys: KeyType[]) {
|
||||||
state.selectedKeys = keys;
|
state.selectedKeys = keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,11 +172,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkAll(checkAll: boolean) {
|
function checkAll(checkAll: boolean) {
|
||||||
state.checkedKeys = checkAll ? getEnabledKeys() : ([] as Keys);
|
state.checkedKeys = checkAll ? getEnabledKeys() : ([] as KeyType[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expandAll(expandAll: boolean) {
|
function expandAll(expandAll: boolean) {
|
||||||
state.expandedKeys = expandAll ? getAllKeys() : ([] as Keys);
|
state.expandedKeys = expandAll ? getAllKeys() : ([] as KeyType[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStrictlyChange(strictly: boolean) {
|
function onStrictlyChange(strictly: boolean) {
|
||||||
@ -227,21 +214,21 @@
|
|||||||
const { filterFn, checkable, expandOnSearch, checkOnSearch, selectedOnSearch } =
|
const { filterFn, checkable, expandOnSearch, checkOnSearch, selectedOnSearch } =
|
||||||
unref(props);
|
unref(props);
|
||||||
searchState.startSearch = true;
|
searchState.startSearch = true;
|
||||||
const { title: titleField, key: keyField } = unref(getReplaceFields);
|
const { title: titleField, key: keyField } = unref(getFieldNames);
|
||||||
|
|
||||||
const matchedKeys: string[] = [];
|
const matchedKeys: string[] = [];
|
||||||
searchState.searchData = filter(
|
searchState.searchData = filter(
|
||||||
unref(treeDataRef),
|
unref(treeDataRef),
|
||||||
(node) => {
|
(node) => {
|
||||||
const result = filterFn
|
const result = filterFn
|
||||||
? filterFn(searchValue, node, unref(getReplaceFields))
|
? filterFn(searchValue, node, unref(getFieldNames))
|
||||||
: node[titleField]?.includes(searchValue) ?? false;
|
: node[titleField]?.includes(searchValue) ?? false;
|
||||||
if (result) {
|
if (result) {
|
||||||
matchedKeys.push(node[keyField]);
|
matchedKeys.push(node[keyField]);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
unref(getReplaceFields),
|
unref(getFieldNames),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (expandOnSearch) {
|
if (expandOnSearch) {
|
||||||
@ -317,15 +304,6 @@
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// watchEffect(() => {
|
|
||||||
// console.log('======================');
|
|
||||||
// console.log(props.value);
|
|
||||||
// console.log('======================');
|
|
||||||
// if (props.value) {
|
|
||||||
// state.checkedKeys = props.value;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
state.checkStrictly = props.checkStrictly;
|
state.checkStrictly = props.checkStrictly;
|
||||||
});
|
});
|
||||||
@ -354,8 +332,6 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expose(instance);
|
|
||||||
|
|
||||||
function renderAction(node: TreeItem) {
|
function renderAction(node: TreeItem) {
|
||||||
const { actionList } = props;
|
const { actionList } = props;
|
||||||
if (!actionList || actionList.length === 0) return;
|
if (!actionList || actionList.length === 0) return;
|
||||||
@ -370,29 +346,25 @@
|
|||||||
if (!nodeShow) return null;
|
if (!nodeShow) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span key={index} class={`${prefixCls}__action`}>
|
<span key={index} class={bem('action')}>
|
||||||
{item.render(node)}
|
{item.render(node)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTreeNode({ data, level }: { data: TreeItem[] | undefined; level: number }) {
|
const treeData = computed(() => {
|
||||||
if (!data) {
|
const data = cloneDeep(getTreeData.value);
|
||||||
return null;
|
data.forEach((item) => {
|
||||||
}
|
const searchText = searchState.searchText;
|
||||||
const searchText = searchState.searchText;
|
const { highlight } = unref(props);
|
||||||
const { highlight } = unref(props);
|
|
||||||
return data.map((item) => {
|
|
||||||
const {
|
const {
|
||||||
title: titleField,
|
title: titleField,
|
||||||
key: keyField,
|
key: keyField,
|
||||||
children: childrenField,
|
children: childrenField,
|
||||||
} = unref(getReplaceFields);
|
} = unref(getFieldNames);
|
||||||
|
|
||||||
const propsData = omit(item, 'title');
|
const icon = getIcon(item, item.icon);
|
||||||
const icon = getIcon({ ...item, level }, item.icon);
|
|
||||||
const children = get(item, childrenField) || [];
|
|
||||||
const title = get(item, titleField);
|
const title = get(item, titleField);
|
||||||
|
|
||||||
const searchIdx = searchText ? title.indexOf(searchText) : -1;
|
const searchIdx = searchText ? title.indexOf(searchText) : -1;
|
||||||
@ -401,7 +373,7 @@
|
|||||||
const highlightStyle = `color: ${isBoolean(highlight) ? '#f50' : highlight}`;
|
const highlightStyle = `color: ${isBoolean(highlight) ? '#f50' : highlight}`;
|
||||||
|
|
||||||
const titleDom = isHighlight ? (
|
const titleDom = isHighlight ? (
|
||||||
<span class={unref(getBindValues)?.blockNode ? `${prefixCls}__content` : ''}>
|
<span class={unref(getBindValues)?.blockNode ? `${bem('content')}` : ''}>
|
||||||
<span>{title.substr(0, searchIdx)}</span>
|
<span>{title.substr(0, searchIdx)}</span>
|
||||||
<span style={highlightStyle}>{searchText}</span>
|
<span style={highlightStyle}>{searchText}</span>
|
||||||
<span>{title.substr(searchIdx + (searchText as string).length)}</span>
|
<span>{title.substr(searchIdx + (searchText as string).length)}</span>
|
||||||
@ -409,41 +381,34 @@
|
|||||||
) : (
|
) : (
|
||||||
title
|
title
|
||||||
);
|
);
|
||||||
|
item.title = (
|
||||||
return (
|
<span
|
||||||
<Tree.TreeNode {...propsData} node={toRaw(item)} key={get(item, keyField)}>
|
class={`${bem('title')} pl-2`}
|
||||||
{{
|
onClick={handleClickNode.bind(null, item[keyField], item[childrenField])}
|
||||||
title: () => (
|
>
|
||||||
<span
|
{item.slots?.title ? (
|
||||||
class={`${prefixCls}-title pl-2`}
|
getSlot(slots, item.slots?.title, item)
|
||||||
onClick={handleClickNode.bind(null, item[keyField], item[childrenField])}
|
) : (
|
||||||
>
|
<>
|
||||||
{item.slots?.title ? (
|
{icon && <TreeIcon icon={icon} />}
|
||||||
getSlot(slots, item.slots?.title, item)
|
{titleDom}
|
||||||
) : (
|
<span class={bem('actions')}>{renderAction(item)}</span>
|
||||||
<>
|
</>
|
||||||
{icon && <TreeIcon icon={icon} />}
|
)}
|
||||||
{titleDom}
|
</span>
|
||||||
{/*{get(item, titleField)}*/}
|
|
||||||
<span class={`${prefixCls}__actions`}>
|
|
||||||
{renderAction({ ...item, level })}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
default: () => renderTreeNode({ data: children, level: level + 1 }),
|
|
||||||
}}
|
|
||||||
</Tree.TreeNode>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
return data;
|
||||||
|
});
|
||||||
|
|
||||||
|
expose(instance);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const { title, helpMessage, toolbar, search, checkable } = props;
|
const { title, helpMessage, toolbar, search, checkable } = props;
|
||||||
const showTitle = title || toolbar || search || slots.headerTitle;
|
const showTitle = title || toolbar || search || slots.headerTitle;
|
||||||
const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' };
|
const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' };
|
||||||
return (
|
return (
|
||||||
<div class={[prefixCls, 'h-full', attrs.class]}>
|
<div class={[bem(), 'h-full', attrs.class]}>
|
||||||
{showTitle && (
|
{showTitle && (
|
||||||
<TreeHeader
|
<TreeHeader
|
||||||
checkable={checkable}
|
checkable={checkable}
|
||||||
@ -461,15 +426,10 @@
|
|||||||
</TreeHeader>
|
</TreeHeader>
|
||||||
)}
|
)}
|
||||||
<ScrollContainer style={scrollStyle} v-show={!unref(getNotFound)}>
|
<ScrollContainer style={scrollStyle} v-show={!unref(getNotFound)}>
|
||||||
<Tree {...unref(getBindValues)} showIcon={false}>
|
<Tree {...unref(getBindValues)} showIcon={false} treeData={treeData.value}>
|
||||||
{{
|
{extendSlots(slots)}
|
||||||
// switcherIcon: () => <DownOutlined />,
|
|
||||||
default: () => renderTreeNode({ data: unref(getTreeData), level: 1 }),
|
|
||||||
...extendSlots(slots),
|
|
||||||
}}
|
|
||||||
</Tree>
|
</Tree>
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
|
|
||||||
<Empty v-show={unref(getNotFound)} image={Empty.PRESENTED_IMAGE_SIMPLE} class="!mt-4" />
|
<Empty v-show={unref(getNotFound)} image={Empty.PRESENTED_IMAGE_SIMPLE} class="!mt-4" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -477,50 +437,3 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
<template>
|
||||||
<div class="flex px-2 py-1.5 items-center basic-tree-header">
|
<div :class="bem()" class="flex px-2 py-1.5 items-center">
|
||||||
<slot name="headerTitle" v-if="$slots.headerTitle"></slot>
|
<slot name="headerTitle" v-if="slots.headerTitle"></slot>
|
||||||
<BasicTitle :helpMessage="helpMessage" v-if="!$slots.headerTitle && title">
|
<BasicTitle :helpMessage="helpMessage" v-if="!slots.headerTitle && title">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</BasicTitle>
|
</BasicTitle>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-1 justify-self-stretch items-center cursor-pointer"
|
class="flex flex-1 justify-self-stretch items-center cursor-pointer"
|
||||||
v-if="search || toolbar"
|
v-if="search || toolbar"
|
||||||
@ -33,148 +32,140 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { PropType } from 'vue';
|
import { computed, ref, watch, useSlots } from 'vue';
|
||||||
import { defineComponent, computed, ref, watch } from 'vue';
|
import { Dropdown, Menu, MenuItem, MenuDivider, InputSearch } from 'ant-design-vue';
|
||||||
|
|
||||||
import { Dropdown, Menu, Input } from 'ant-design-vue';
|
|
||||||
import { Icon } from '/@/components/Icon';
|
import { Icon } from '/@/components/Icon';
|
||||||
import { BasicTitle } from '/@/components/Basic';
|
import { BasicTitle } from '/@/components/Basic';
|
||||||
|
|
||||||
import { propTypes } from '/@/utils/propTypes';
|
|
||||||
|
|
||||||
import { useI18n } from '/@/hooks/web/useI18n';
|
import { useI18n } from '/@/hooks/web/useI18n';
|
||||||
import { useDebounceFn } from '@vueuse/core';
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
|
import { createBEM } from '/@/utils/bem';
|
||||||
|
import { ToolbarEnum } from './tree';
|
||||||
|
|
||||||
enum ToolbarEnum {
|
const searchValue = ref('');
|
||||||
SELECT_ALL,
|
|
||||||
UN_SELECT_ALL,
|
|
||||||
EXPAND_ALL,
|
|
||||||
UN_EXPAND_ALL,
|
|
||||||
CHECK_STRICTLY,
|
|
||||||
CHECK_UN_STRICTLY,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MenuInfo {
|
const [bem] = createBEM('tree-header');
|
||||||
key: ToolbarEnum;
|
|
||||||
}
|
// eslint-disable vue/valid-define-emits
|
||||||
export default defineComponent({
|
const props = defineProps({
|
||||||
name: 'BasicTreeHeader',
|
helpMessage: {
|
||||||
components: {
|
type: [String, Array] as PropType<string | string[]>,
|
||||||
BasicTitle,
|
default: '',
|
||||||
Icon,
|
|
||||||
Dropdown,
|
|
||||||
Menu,
|
|
||||||
MenuItem: Menu.Item,
|
|
||||||
MenuDivider: Menu.Divider,
|
|
||||||
InputSearch: Input.Search,
|
|
||||||
},
|
},
|
||||||
props: {
|
title: {
|
||||||
helpMessage: {
|
type: String,
|
||||||
type: [String, Array] as PropType<string | string[]>,
|
default: '',
|
||||||
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 toolbarList = computed(() => {
|
||||||
const titleExists = slots.headerTitle || props.title;
|
const { checkable } = props;
|
||||||
return [
|
const defaultToolbarList = [
|
||||||
'mr-1',
|
{ label: t('component.tree.expandAll'), value: ToolbarEnum.EXPAND_ALL },
|
||||||
'w-full',
|
{
|
||||||
// titleExists ? 'w-2/3' : 'w-full',
|
label: t('component.tree.unExpandAll'),
|
||||||
{
|
value: ToolbarEnum.UN_EXPAND_ALL,
|
||||||
['ml-5']: titleExists,
|
divider: checkable,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
|
||||||
|
|
||||||
const toolbarList = computed(() => {
|
return checkable
|
||||||
const { checkable } = props;
|
? [
|
||||||
const defaultToolbarList = [
|
{ label: t('component.tree.selectAll'), value: ToolbarEnum.SELECT_ALL },
|
||||||
{ label: t('component.tree.expandAll'), value: ToolbarEnum.EXPAND_ALL },
|
|
||||||
{
|
{
|
||||||
label: t('component.tree.unExpandAll'),
|
label: t('component.tree.unSelectAll'),
|
||||||
value: ToolbarEnum.UN_EXPAND_ALL,
|
value: ToolbarEnum.UN_SELECT_ALL,
|
||||||
divider: checkable,
|
divider: checkable,
|
||||||
},
|
},
|
||||||
];
|
...defaultToolbarList,
|
||||||
|
{ label: t('component.tree.checkStrictly'), value: ToolbarEnum.CHECK_STRICTLY },
|
||||||
return checkable
|
{ label: t('component.tree.checkUnStrictly'), value: ToolbarEnum.CHECK_UN_STRICTLY },
|
||||||
? [
|
]
|
||||||
{ label: t('component.tree.selectAll'), value: ToolbarEnum.SELECT_ALL },
|
: defaultToolbarList;
|
||||||
{
|
|
||||||
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 };
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
|
||||||
<style lang="less" scoped>
|
function handleMenuClick(e: { key: ToolbarEnum }) {
|
||||||
.basic-tree-header {
|
const { key } = e;
|
||||||
border-bottom: 1px solid @border-color-base;
|
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 type { VNode, FunctionalComponent } from 'vue';
|
||||||
|
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { isString } from '/@/utils/is';
|
import { isString } from '@vue/shared';
|
||||||
import { Icon } from '/@/components/Icon';
|
import { Icon } from '/@/components/Icon';
|
||||||
|
|
||||||
export interface ComponentProps {
|
export const TreeIcon: FunctionalComponent = ({ icon }: { icon: VNode | string }) => {
|
||||||
icon: VNode | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TreeIcon: FunctionalComponent = ({ icon }: ComponentProps) => {
|
|
||||||
if (!icon) return null;
|
if (!icon) return null;
|
||||||
if (isString(icon)) {
|
if (isString(icon)) {
|
||||||
return h(Icon, { icon, class: 'mr-1' });
|
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 { Ref, ComputedRef } from 'vue';
|
||||||
import type { TreeDataItem } from 'ant-design-vue/es/tree/Tree';
|
import type { TreeDataItem } from 'ant-design-vue/es/tree/Tree';
|
||||||
|
|
||||||
@ -6,14 +6,11 @@ import { cloneDeep } from 'lodash-es';
|
|||||||
import { unref } from 'vue';
|
import { unref } from 'vue';
|
||||||
import { forEach } from '/@/utils/helper/treeHelper';
|
import { forEach } from '/@/utils/helper/treeHelper';
|
||||||
|
|
||||||
export function useTree(
|
export function useTree(treeDataRef: Ref<TreeDataItem[]>, getFieldNames: ComputedRef<FieldNames>) {
|
||||||
treeDataRef: Ref<TreeDataItem[]>,
|
|
||||||
getReplaceFields: ComputedRef<ReplaceFields>,
|
|
||||||
) {
|
|
||||||
function getAllKeys(list?: TreeDataItem[]) {
|
function getAllKeys(list?: TreeDataItem[]) {
|
||||||
const keys: string[] = [];
|
const keys: string[] = [];
|
||||||
const treeData = list || unref(treeDataRef);
|
const treeData = list || unref(treeDataRef);
|
||||||
const { key: keyField, children: childrenField } = unref(getReplaceFields);
|
const { key: keyField, children: childrenField } = unref(getFieldNames);
|
||||||
if (!childrenField || !keyField) return keys;
|
if (!childrenField || !keyField) return keys;
|
||||||
|
|
||||||
for (let index = 0; index < treeData.length; index++) {
|
for (let index = 0; index < treeData.length; index++) {
|
||||||
@ -24,14 +21,14 @@ export function useTree(
|
|||||||
keys.push(...(getAllKeys(children) as string[]));
|
keys.push(...(getAllKeys(children) as string[]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keys as Keys;
|
return keys as KeyType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// get keys that can be checked and selected
|
// get keys that can be checked and selected
|
||||||
function getEnabledKeys(list?: TreeDataItem[]) {
|
function getEnabledKeys(list?: TreeDataItem[]) {
|
||||||
const keys: string[] = [];
|
const keys: string[] = [];
|
||||||
const treeData = list || unref(treeDataRef);
|
const treeData = list || unref(treeDataRef);
|
||||||
const { key: keyField, children: childrenField } = unref(getReplaceFields);
|
const { key: keyField, children: childrenField } = unref(getFieldNames);
|
||||||
if (!childrenField || !keyField) return keys;
|
if (!childrenField || !keyField) return keys;
|
||||||
|
|
||||||
for (let index = 0; index < treeData.length; index++) {
|
for (let index = 0; index < treeData.length; index++) {
|
||||||
@ -42,13 +39,13 @@ export function useTree(
|
|||||||
keys.push(...(getEnabledKeys(children) as string[]));
|
keys.push(...(getEnabledKeys(children) as string[]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keys as Keys;
|
return keys as KeyType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChildrenKeys(nodeKey: string | number, list?: TreeDataItem[]): Keys {
|
function getChildrenKeys(nodeKey: string | number, list?: TreeDataItem[]) {
|
||||||
const keys: Keys = [];
|
const keys: KeyType[] = [];
|
||||||
const treeData = list || unref(treeDataRef);
|
const treeData = list || unref(treeDataRef);
|
||||||
const { key: keyField, children: childrenField } = unref(getReplaceFields);
|
const { key: keyField, children: childrenField } = unref(getFieldNames);
|
||||||
if (!childrenField || !keyField) return keys;
|
if (!childrenField || !keyField) return keys;
|
||||||
for (let index = 0; index < treeData.length; index++) {
|
for (let index = 0; index < treeData.length; index++) {
|
||||||
const node = treeData[index];
|
const node = treeData[index];
|
||||||
@ -64,14 +61,14 @@ export function useTree(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keys as Keys;
|
return keys as KeyType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update node
|
// Update node
|
||||||
function updateNodeByKey(key: string, node: TreeDataItem, list?: TreeDataItem[]) {
|
function updateNodeByKey(key: string, node: TreeDataItem, list?: TreeDataItem[]) {
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
const treeData = list || unref(treeDataRef);
|
const treeData = list || unref(treeDataRef);
|
||||||
const { key: keyField, children: childrenField } = unref(getReplaceFields);
|
const { key: keyField, children: childrenField } = unref(getFieldNames);
|
||||||
|
|
||||||
if (!childrenField || !keyField) return;
|
if (!childrenField || !keyField) return;
|
||||||
|
|
||||||
@ -98,7 +95,7 @@ export function useTree(
|
|||||||
for (let index = 0; index < data.length; index++) {
|
for (let index = 0; index < data.length; index++) {
|
||||||
const item = data[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 key = keyField ? item[keyField] : '';
|
||||||
const children = childrenField ? item[childrenField] : [];
|
const children = childrenField ? item[childrenField] : [];
|
||||||
res.push(key);
|
res.push(key);
|
||||||
@ -120,7 +117,7 @@ export function useTree(
|
|||||||
treeDataRef.value = treeData;
|
treeDataRef.value = treeData;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { key: keyField, children: childrenField } = unref(getReplaceFields);
|
const { key: keyField, children: childrenField } = unref(getFieldNames);
|
||||||
if (!childrenField || !keyField) return;
|
if (!childrenField || !keyField) return;
|
||||||
|
|
||||||
forEach(treeData, (treeItem) => {
|
forEach(treeData, (treeItem) => {
|
||||||
@ -145,7 +142,7 @@ export function useTree(
|
|||||||
treeData[push](list[i]);
|
treeData[push](list[i]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const { key: keyField, children: childrenField } = unref(getReplaceFields);
|
const { key: keyField, children: childrenField } = unref(getFieldNames);
|
||||||
if (!childrenField || !keyField) return;
|
if (!childrenField || !keyField) return;
|
||||||
|
|
||||||
forEach(treeData, (treeItem) => {
|
forEach(treeData, (treeItem) => {
|
||||||
@ -164,7 +161,7 @@ export function useTree(
|
|||||||
function deleteNodeByKey(key: string, list?: TreeDataItem[]) {
|
function deleteNodeByKey(key: string, list?: TreeDataItem[]) {
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
const treeData = list || unref(treeDataRef);
|
const treeData = list || unref(treeDataRef);
|
||||||
const { key: keyField, children: childrenField } = unref(getReplaceFields);
|
const { key: keyField, children: childrenField } = unref(getFieldNames);
|
||||||
if (!childrenField || !keyField) return;
|
if (!childrenField || !keyField) return;
|
||||||
|
|
||||||
for (let index = 0; index < treeData.length; index++) {
|
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 { setupI18n } from '/@/locales/setupI18n';
|
||||||
import { registerGlobComp } from '/@/components/registerGlobComp';
|
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() {
|
async function bootstrap() {
|
||||||
const app = createApp(App);
|
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: {
|
optimizeDeps: {
|
||||||
// @iconify/iconify: The dependency is dynamically and virtually loaded by @purge-icons/generated, so it needs to be specified explicitly
|
// @iconify/iconify: The dependency is dynamically and virtually loaded by @purge-icons/generated, so it needs to be specified explicitly
|
||||||
include: [
|
include: [
|
||||||
|
'@vue/shared',
|
||||||
'@iconify/iconify',
|
'@iconify/iconify',
|
||||||
'ant-design-vue/es/locale/zh_CN',
|
'ant-design-vue/es/locale/zh_CN',
|
||||||
'ant-design-vue/es/locale/en_US',
|
'ant-design-vue/es/locale/en_US',
|
||||||
|
Loading…
Reference in New Issue
Block a user