Merge remote-tracking branch 'vben/main' into Gf-Vben-Admin

# Conflicts:
#	pnpm-lock.yaml
This commit is contained in:
JinMao 2021-12-01 11:25:54 +08:00
commit b7d2dac3d2
21 changed files with 756 additions and 511 deletions

View File

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

View File

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

View File

@ -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;
}, // },
}; };
} }

View File

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

View File

@ -36,12 +36,12 @@
"@ant-design/colors": "^6.0.0", "@ant-design/colors": "^6.0.0",
"@ant-design/icons-vue": "^6.0.1", "@ant-design/icons-vue": "^6.0.1",
"@iconify/iconify": "^2.1.0", "@iconify/iconify": "^2.1.0",
"@logicflow/core": "^0.7.9", "@logicflow/core": "^0.7.10",
"@logicflow/extension": "^0.7.9", "@logicflow/extension": "^0.7.10",
"@vueuse/core": "^7.0.3", "@vueuse/core": "^7.1.2",
"@vueuse/shared": "^7.0.3", "@vueuse/shared": "^7.1.2",
"@zxcvbn-ts/core": "^1.0.0", "@zxcvbn-ts/core": "^1.0.0",
"ant-design-vue": "3.0.0-alpha.12", "ant-design-vue": "3.0.0-alpha.13",
"axios": "^0.24.0", "axios": "^0.24.0",
"codemirror": "^5.64.0", "codemirror": "^5.64.0",
"cropperjs": "^1.5.12", "cropperjs": "^1.5.12",
@ -62,7 +62,7 @@
"sortablejs": "^1.14.0", "sortablejs": "^1.14.0",
"tinymce": "^5.10.2", "tinymce": "^5.10.2",
"vditor": "^3.8.7", "vditor": "^3.8.7",
"vue": "^3.2.22", "vue": "^3.2.23",
"vue-i18n": "^9.1.9", "vue-i18n": "^9.1.9",
"vue-json-pretty": "^2.0.6", "vue-json-pretty": "^2.0.6",
"vue-router": "^4.0.12", "vue-router": "^4.0.12",
@ -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.0", "@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",
@ -91,9 +91,9 @@
"@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0", "@typescript-eslint/parser": "^5.4.0",
"@vitejs/plugin-legacy": "^1.6.3", "@vitejs/plugin-legacy": "^1.6.3",
"@vitejs/plugin-vue": "^1.10.0", "@vitejs/plugin-vue": "^1.10.1",
"@vitejs/plugin-vue-jsx": "^1.3.0", "@vitejs/plugin-vue-jsx": "^1.3.0",
"@vue/compiler-sfc": "3.2.22", "@vue/compiler-sfc": "3.2.23",
"@vue/test-utils": "^2.0.0-rc.17", "@vue/test-utils": "^2.0.0-rc.17",
"autoprefixer": "^10.4.0", "autoprefixer": "^10.4.0",
"commitizen": "^4.2.4", "commitizen": "^4.2.4",
@ -102,7 +102,7 @@
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"eslint": "^8.3.0", "eslint": "^8.3.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-define-config": "^1.1.4", "eslint-define-config": "^1.2.0",
"eslint-plugin-jest": "^25.3.0", "eslint-plugin-jest": "^25.3.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.1.1", "eslint-plugin-vue": "^8.1.1",
@ -110,38 +110,39 @@
"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",
"postcss": "^8.4.0", "postcss": "^8.4.4",
"postcss-html": "^1.2.0", "postcss-html": "^1.3.0",
"postcss-less": "^5.0.0", "postcss-less": "^5.0.0",
"prettier": "^2.4.1", "prettier": "^2.5.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup-plugin-visualizer": "^5.5.2", "rollup-plugin-visualizer": "^5.5.2",
"stylelint": "^14.1.0", "stylelint": "^14.1.0",
"stylelint-config-html": "^1.0.0", "stylelint-config-html": "^1.0.0",
"stylelint-config-prettier": "^9.0.3", "stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^6.0.0",
"stylelint-config-standard": "^24.0.0", "stylelint-config-standard": "^24.0.0",
"stylelint-order": "^5.0.0", "stylelint-order": "^5.0.0",
"ts-jest": "^27.0.7", "ts-jest": "^27.0.7",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"typescript": "^4.5.2", "typescript": "^4.5.2",
"vite": "^2.7.0-beta.8 ", "vite": "^2.7.0-beta.9",
"vite-plugin-compression": "^0.3.6", "vite-plugin-compression": "^0.3.6",
"vite-plugin-html": "^2.1.1", "vite-plugin-html": "^2.1.1",
"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.1", "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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,56 +0,0 @@
import type { TreeDataItem, CheckEvent as CheckEventOrigin } from 'ant-design-vue/es/tree/Tree';
import { ContextMenuItem } from '/@/hooks/web/useContextMenu';
export interface ActionItem {
render: (record: Recordable) => any;
show?: boolean | ((record: Recordable) => boolean);
}
export interface TreeItem extends TreeDataItem {
icon?: any;
}
export interface ReplaceFields {
children?: string;
title?: string;
key?: string;
}
export type Keys = (string | number)[];
export type CheckKeys =
| (string | number)[]
| { checked: (string | number)[]; halfChecked: (string | number)[] };
export interface TreeActionType {
checkAll: (checkAll: boolean) => void;
expandAll: (expandAll: boolean) => void;
setExpandedKeys: (keys: Keys) => void;
getExpandedKeys: () => Keys;
setSelectedKeys: (keys: Keys) => void;
getSelectedKeys: () => Keys;
setCheckedKeys: (keys: CheckKeys) => void;
getCheckedKeys: () => CheckKeys;
filterByLevel: (level: number) => void;
insertNodeByKey: (opt: InsertNodeParams) => void;
insertNodesByKey: (opt: InsertNodeParams) => void;
deleteNodeByKey: (key: string) => void;
updateNodeByKey: (key: string, node: Omit<TreeDataItem, 'key'>) => void;
setSearchValue: (value: string) => void;
getSearchValue: () => string;
}
export interface InsertNodeParams {
parentKey: string | null;
node: TreeDataItem;
list?: TreeDataItem[];
push?: 'push' | 'unshift';
}
export interface ContextMenuOptions {
icon?: string;
styles?: any;
items?: ContextMenuItem[];
}
export type CheckEvent = CheckEventOrigin;

View File

@ -1,4 +1,4 @@
import type { InsertNodeParams, Keys, ReplaceFields } from './typing'; import type { InsertNodeParams, KeyType, FieldNames } from './tree';
import type { Ref, ComputedRef } from 'vue'; import type { 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++) {

View File

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

View File

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

View File

@ -15,13 +15,6 @@ import { setupGlobDirectives } from '/@/directives';
import { setupI18n } from '/@/locales/setupI18n'; import { 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
View File

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

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

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

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

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

View File

@ -30,7 +30,7 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"tsconfig-paths": "^3.12.0", "tsconfig-paths": "^3.12.0",
"tsup": "^5.9.1", "tsup": "^5.10.0",
"typescript": "^4.5.2" "typescript": "^4.5.2"
} }
} }

View File

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