diff --git a/packages/devui-vue/devui/tree-select/__tests__/tree-select.spec.ts b/packages/devui-vue/devui/tree-select/__tests__/tree-select.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f35fcbe2bff578f3c8db05069812a092c546123e --- /dev/null +++ b/packages/devui-vue/devui/tree-select/__tests__/tree-select.spec.ts @@ -0,0 +1,57 @@ +import { mount } from '@vue/test-utils'; +import { ref } from 'vue'; +import DTreeSelect from '../src/tree-select'; + +describe('tree-select', () => { + + it('tree-select should render correctly', async () => { + const value = ref(''); + const data = ref([{ + label: '一级 1', + children: [{ + label: '二级 1-1', + children: [{ + label: '三级 1-1-1', + }] + }] + }, { + label: '一级 2', + children: [{ + label: '二级 2-1', + children: [{ + label: '三级 2-1-1', + }] + }, { + label: '二级 2-2', + children: [{ + label: '三级 2-2-1', + }] + }] + }, { + label: '一级 3', + children: [{ + label: '二级 3-1', + children: [{ + label: '三级 3-1-1', + }] + }, { + label: '二级 3-2', + children: [{ + label: '三级 3-2-1', + }] + }] + }]); + const wrapper = mount({ + components: { DTreeSelect }, + template: ``, + setup() { + return { + value, + data, + }; + }, + }); + + expect(wrapper.classes()).toContain('devui-tree-select'); + }); +}) \ No newline at end of file diff --git a/packages/devui-vue/devui/tree-select/assets/close.svg b/packages/devui-vue/devui/tree-select/assets/close.svg new file mode 100644 index 0000000000000000000000000000000000000000..2bb8e7f8194373d0c40e23af4cc80c684c9c140a --- /dev/null +++ b/packages/devui-vue/devui/tree-select/assets/close.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/packages/devui-vue/devui/tree-select/assets/open.svg b/packages/devui-vue/devui/tree-select/assets/open.svg new file mode 100644 index 0000000000000000000000000000000000000000..a69af88b169b95da702114d77e49b9a510ecfd21 --- /dev/null +++ b/packages/devui-vue/devui/tree-select/assets/open.svg @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/packages/devui-vue/devui/tree-select/hooks/use-clear.ts b/packages/devui-vue/devui/tree-select/hooks/use-clear.ts new file mode 100644 index 0000000000000000000000000000000000000000..0891d489f4ee7c0b43ebebcc04b9cc8c579877f5 --- /dev/null +++ b/packages/devui-vue/devui/tree-select/hooks/use-clear.ts @@ -0,0 +1,26 @@ +import { computed } from 'vue' +import type { SetupContext, Ref } from 'vue' +import { TreeSelectProps } from '../src/tree-select-types' + +export default function useClear(props: TreeSelectProps, ctx: SetupContext, data: Ref): any { + + const isClearable = computed(() => { + return !props.disabled && props.allowClear && data.value.length > 0; + }) + + const handleClear = (e: MouseEvent) => { + e.preventDefault() + e.stopPropagation() + if (props.multiple) { + ctx.emit('update:modelValue', []) + } else { + ctx.emit('update:modelValue', '') + data.value = '' + } + } + + return { + isClearable, + handleClear, + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui/tree-select/hooks/use-select.ts b/packages/devui-vue/devui/tree-select/hooks/use-select.ts new file mode 100644 index 0000000000000000000000000000000000000000..24d0370065b8504118ccf5ac6cbb50c434f7ab33 --- /dev/null +++ b/packages/devui-vue/devui/tree-select/hooks/use-select.ts @@ -0,0 +1,72 @@ +import { ref } from 'vue' +import { TreeSelectProps, TreeItem } from '../src/tree-select-types' + +export default function useSelect(props: TreeSelectProps): any { + const inputValue = ref('') + const selectedCache = new Set() + + const selectValue = (item: TreeItem) => { + if(!props.multiple) { + inputValue.value = item.label + } else { + item.checked = !item.checked + if(item.halfchecked) item.halfchecked = false + useCache(item) + searchUp(item) + searchDown(item) + inputValue.value = [...selectedCache].toString() + } + } + + const useCache = (item: TreeItem) => { + item.checked === true + ? selectedCache.add(item.label) + : (selectedCache.has(item.label) && selectedCache.delete(item.label)) + } + + const searchUp = (item: TreeItem) => { + + if(!item.parent) return + let state = '' + const checkedArr = item.parent.children.filter((el) => el.checked === true) + switch(checkedArr.length) { + case 0: + state = 'none' + break; + case item.parent.children.length: + state = 'checked' + break + default: + state = 'halfchecked' + break + } + + if(state === 'checked') { + item.parent.checked = true + item.parent.halfchecked = false + } else if(state === 'halfchecked') { + item.parent.halfchecked = true + item.parent.checked = false + } else { + item.parent.checked = false + item.parent.halfchecked = false + } + + useCache(item.parent) + searchUp(item.parent) + } + + const searchDown = (item: TreeItem) => { + if(!item.children) return + item.children.forEach((el) => { + el.checked = item.checked + useCache(el) + searchDown(el) + }) + } + + return { + inputValue, + selectValue + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui/tree-select/hooks/use-toggle.ts b/packages/devui-vue/devui/tree-select/hooks/use-toggle.ts new file mode 100644 index 0000000000000000000000000000000000000000..6be5a707c94cb4a5d0c7b5da4fcb9d908c2e62f7 --- /dev/null +++ b/packages/devui-vue/devui/tree-select/hooks/use-toggle.ts @@ -0,0 +1,23 @@ +import { ref } from 'vue' +import { TreeSelectProps, TreeItem } from '../src/tree-select-types' + +export default function useToggle(props: TreeSelectProps): any { + const visible = ref(false) + + const selectToggle = () => { + if(props.disabled) return + visible.value = !visible.value + } + + const treeToggle = (e: MouseEvent, item: TreeItem) => { + e.preventDefault() + e.stopPropagation() + item.opened = ! item.opened + } + + return { + visible, + selectToggle, + treeToggle, + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui/tree-select/index.ts b/packages/devui-vue/devui/tree-select/index.ts index 4a3ed884e921042dc112face7b8c7dd4ec550d09..1c269221309716b9b9b3d113ca32260972ec4bb8 100644 --- a/packages/devui-vue/devui/tree-select/index.ts +++ b/packages/devui-vue/devui/tree-select/index.ts @@ -10,9 +10,8 @@ export { TreeSelect } export default { title: 'TreeSelect 树形选择框', category: '数据录入', - status: undefined, // TODO: 组件若开发完成则填入"已完成",并删除该注释 + status: '20%', // TODO: 组件若开发完成则填入"已完成",并删除该注释 install(app: App): void { - app.use(TreeSelect as any) } } diff --git a/packages/devui-vue/devui/tree-select/src/tree-select-types.ts b/packages/devui-vue/devui/tree-select/src/tree-select-types.ts index fcb84dccdb48e718a957106f532610d0f3aca596..0809b4da2b5a56e4c15828ec05553e1b116c540c 100644 --- a/packages/devui-vue/devui/tree-select/src/tree-select-types.ts +++ b/packages/devui-vue/devui/tree-select/src/tree-select-types.ts @@ -1,15 +1,15 @@ import type { PropType, ExtractPropTypes } from 'vue' export interface TreeItem { - id?: number | string - label?: string - data?: any - parent?: TreeItem | null - children?: Array | null + id: number | string + label: string + parent?: TreeItem + children?: Array level?: number loading?: boolean - isOpen?: boolean - isChecked?: boolean + opened?: boolean + checked?: boolean + halfchecked?: boolean disabled?: boolean [prop: string]: any @@ -36,10 +36,6 @@ export const treeSelectProps = { type: Boolean, default: false }, - expandTree: { - type: Boolean, - default: false - }, multiple: { type: Boolean, default: false, diff --git a/packages/devui-vue/devui/tree-select/src/tree-select.scss b/packages/devui-vue/devui/tree-select/src/tree-select.scss index 9cae05128d0cbd928cd48ce97aaad4945d6605b1..85b3a7836fb6af9489bf8924e21182ff8fe61e77 100644 --- a/packages/devui-vue/devui/tree-select/src/tree-select.scss +++ b/packages/devui-vue/devui/tree-select/src/tree-select.scss @@ -65,7 +65,7 @@ $tree-select-item-font-size: 16px; .devui-tree-select-item { font-size: $tree-select-item-font-size; - display: block; + display: inline-flex; min-height: $tree-select-item-min-height; line-height: 1.5; width: 100%; @@ -77,6 +77,7 @@ $tree-select-item-font-size: 16px; border: 0; color: $devui-text; cursor: pointer; + align-items: center; &:hover:not(.active):not(.disabled) { color: $devui-list-item-hover-text; @@ -84,24 +85,21 @@ $tree-select-item-font-size: 16px; } } -.devui-tree-select-clearable:hover { - .devui-tree-select-clear { - display: inline-flex; - } - - .devui-tree-select-arrow { - display: none; +.devui-tree-select-clearable { + position: relative; + &:hover { + .devui-tree-select-clear { + display: inline-flex; + } + + .devui-tree-select-arrow { + display: none; + } } } -.devui-tree-select-clearable:hover { - .devui-tree-select-clear { - display: inline-flex; - } - - .devui-tree-select-arrow { - display: none; - } +.devui-tree-select-notclearable { + position: relative; } .devui-tree-select-clear, @@ -124,13 +122,3 @@ $tree-select-item-font-size: 16px; } } -.devui-tree-select-arrow-expand { - display: inline-flex; - justify-content: center; - align-items: center; - transform: rotate3d(0, 0, 1, 270deg); -} - -.devui-tree-select-arrow-open { - transform: rotate3d(0, 0, 1, 0deg); -} diff --git a/packages/devui-vue/devui/tree-select/src/tree-select.tsx b/packages/devui-vue/devui/tree-select/src/tree-select.tsx index ed99e34b6d3d37764ac7386e3ac40509c3cf5260..dfa35e0825b60f9c25fb5da47d826b871fb640c6 100644 --- a/packages/devui-vue/devui/tree-select/src/tree-select.tsx +++ b/packages/devui-vue/devui/tree-select/src/tree-select.tsx @@ -1,87 +1,40 @@ import './tree-select.scss' -import { defineComponent, ref, reactive, toRefs, computed } from 'vue' -import { treeSelectProps, TreeSelectProps } from './tree-select-types' -import { className } from './utils' +import { defineComponent, ref, Transition } from 'vue' +import type { SetupContext } from 'vue' +import { treeSelectProps, TreeSelectProps, TreeItem } from './tree-select-types' +import { attributeExtension, className } from './utils' +import useToggle from '../hooks/use-toggle' +import useSelect from '../hooks/use-select' +import useClear from '../hooks/use-clear' +import IconOpen from '../assets/open.svg' +import IconClose from '../assets/close.svg' +import Checkbox from '../../checkbox/src/checkbox' export default defineComponent({ name: 'DTreeSelect', props: treeSelectProps, emits: ['toggleChange', 'valueChange', 'update:modelValue'], - setup(props: TreeSelectProps, ctx) { - - const visible = ref(false) - const origin = ref() - const position = reactive({ - originX: 'left', - originY: 'bottom', - overlayX: 'left', - overlayY: 'top' - }) - const inputValue = ref('') - - const { treeData } = toRefs(props) - - const mergeClearable = computed(() => { - return !props.disabled && props.allowClear && inputValue.value.length > 0; - }) - - function toggleChange() { - if (props.disabled) return - visible.value = !visible.value - ctx.emit('toggleChange', visible.value) - } + setup(props: TreeSelectProps, ctx: SetupContext) { + const { treeData, placeholder, disabled, multiple, leafOnly } = props + const { visible, selectToggle, treeToggle} = useToggle(props) + const { inputValue, selectValue } = useSelect(props) + const { isClearable, handleClear} = useClear(props, ctx, inputValue) - function valueChange(data) { - if (data.isOpen !== undefined) { - data.isOpen = !data.isOpen + const clickNode = (item: TreeItem)=> { + if(!leafOnly) { + selectValue(item) + !multiple && selectToggle(item) } else { - inputValue.value = data.label - visible.value = false - ctx.emit('update:modelValue', data.label) - ctx.emit('toggleChange', visible.value) - } - } - - function handleClear(e: MouseEvent) { - e.preventDefault() - e.stopPropagation() - if (props.multiple) { - ctx.emit('update:modelValue', []) - } else { - ctx.emit('update:modelValue', '') - inputValue.value = '' + if(!item.children) { + selectValue(item) + !multiple && selectToggle(item) + } } } - return { - visible, - origin, - position, - inputValue, - mergeClearable, - treeData, - handleClear, - toggleChange, - valueChange, - } - }, - render() { - const { - origin, - position, - inputValue, - mergeClearable, - treeData, - placeholder, - disabled, - handleClear, - toggleChange, - valueChange - } = this - const treeSelectCls = className('devui-tree-select', { - 'devui-tree-select-open': this.visible, + 'devui-tree-select-open': visible.value, 'devui-tree-select-disabled': disabled, }) @@ -89,16 +42,19 @@ export default defineComponent({
{ - e.preventDefault() - e.stopPropagation() - valueChange(item) - }}> - { item.children ? - - - : {'\u00A0\u00A0\u00A0'}} - {item.label} + onClick={() => clickNode(item)} + > + { item.children + ? item.opened + ? treeToggle(e, item)}/> + : treeToggle(e, item)} /> + :{'\u00A0\u00A0\u00A0'} + } + { multiple + ? item.halfchecked + ? + : + : (item.label)}
) @@ -108,7 +64,7 @@ export default defineComponent({ return ( <> { renderNode(item) } - { item.isOpen && renderTree(item.children) } + { item.opened && renderTree(item.children) } ) } @@ -116,30 +72,34 @@ export default defineComponent({ }) } - return ( -
-
- - - - - - - -
- -
-
    {renderTree(treeData)}
+ return () => { + return ( +
+
selectToggle()}> + + handleClear(e)} class="devui-tree-select-clear"> + + + + +
- -
- ) - } + +
+
    {renderTree(attributeExtension(treeData))}
+
+
+
+ ) + } + }, }) diff --git a/packages/devui-vue/devui/tree-select/src/utils.ts b/packages/devui-vue/devui/tree-select/src/utils.ts index 3092da7789a04ffc4321311f268cccd1b802bb9b..30f3136a3c4d392022e2346acf325f417aa346e2 100644 --- a/packages/devui-vue/devui/tree-select/src/utils.ts +++ b/packages/devui-vue/devui/tree-select/src/utils.ts @@ -1,3 +1,26 @@ +import { TreeData } from '../src/tree-select-types' + +export function attributeExtension(data: TreeData): any { + data.forEach((el) => { + let level = 1 + el.level = level + const nodeQueue = [] + nodeQueue.push(el) + while(nodeQueue.length !== 0) { + const node = nodeQueue.shift() + if(node.children) { + node.children.forEach((el) => { + el.level = level + 1 + el.parent = node + nodeQueue.push(el) + }) + } + level += 1 + } + }) + return data +} + /** * 动态获取class字符串 * @param classStr 是一个字符串,固定的class名 diff --git a/packages/devui-vue/docs/components/tree-select/index.md b/packages/devui-vue/docs/components/tree-select/index.md index 03a6a90407e78c74ecc4972b7b7d71627871c9d5..97f4017f206f0be2c6002d69fb3de404d9dfdba8 100644 --- a/packages/devui-vue/docs/components/tree-select/index.md +++ b/packages/devui-vue/docs/components/tree-select/index.md @@ -8,7 +8,7 @@ ```vue ``` -::: \ No newline at end of file +::: + +### 多选 + +:::demo + +```vue + + +``` + +::: + +### 仅叶子节点可选 + +:::demo + +```vue + + +``` + +::: + +### API + +d-select-tree 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 | +| ------------ | ------- | ----- | ------------------------------------------------------------------- | ------------------------------------ | +| treeData | TreeData | | 必选,树形选择框的内容 | [基本用法](#基本用法) | +| placeholder | string | '请选择'| 可选,修改输入框的默认显示内容 | [基本用法](#基本用法) | +| disabled | boolean | false | 可选,值为 true 时禁止用户使用 | [禁用](#禁用) | +| allowClear | boolean | false | 可选,值为 true 时可以清空输入框内容 | [可清空](#可清空) | +| multiple | boolean | false | 可选,值为 true 时可选择多个项 | [多选](#多选) | +| leafOnly | boolean | false | 可选,值为 true 时仅可选择叶子节点 | [仅叶子节点可选](#仅叶子节点可选) | + + +### 接口 & 类型定义 + + +```ts +interface TreeItem { + id: number | string + label: string + parent?: TreeItem + children?: Array + level?: number + loading?: boolean + opened?: boolean + checked?: boolean + halfchecked?: boolean + disabled?: boolean + + [prop: string]: any +} + +type TreeData = Array +``` \ No newline at end of file