diff --git a/devui/tree-select/index.ts b/devui/tree-select/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a3ed884e921042dc112face7b8c7dd4ec550d09 --- /dev/null +++ b/devui/tree-select/index.ts @@ -0,0 +1,18 @@ +import type { App } from 'vue' +import TreeSelect from './src/tree-select' + +TreeSelect.install = function(app: App): void { + app.component(TreeSelect.name, TreeSelect) +} + +export { TreeSelect } + +export default { + title: 'TreeSelect 树形选择框', + category: '数据录入', + status: undefined, // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + + app.use(TreeSelect as any) + } +} diff --git a/devui/tree-select/src/tree-select-types.ts b/devui/tree-select/src/tree-select-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..fcb84dccdb48e718a957106f532610d0f3aca596 --- /dev/null +++ b/devui/tree-select/src/tree-select-types.ts @@ -0,0 +1,69 @@ +import type { PropType, ExtractPropTypes } from 'vue' + +export interface TreeItem { + id?: number | string + label?: string + data?: any + parent?: TreeItem | null + children?: Array | null + level?: number + loading?: boolean + isOpen?: boolean + isChecked?: boolean + disabled?: boolean + + [prop: string]: any +} + +export type TreeData = Array + +export type ModelValue = number | string | Array; + +export const treeSelectProps = { + modelValue: { + type: [String, Number, Array] as PropType, + default: '', + }, + treeData: { + type: Array as PropType, + default: () => [], + }, + placeholder: { + type: String, + default: '请选择', + }, + disabled: { + type: Boolean, + default: false + }, + expandTree: { + type: Boolean, + default: false + }, + multiple: { + type: Boolean, + default: false, + }, + leafOnly: { + type: Boolean, + default: false, + }, + searchable: { + type: Boolean, + default: false, + }, + allowClear: { + type: Boolean, + default: false + }, + onToggleChange: { + type: Function as PropType<(bool: boolean) => void>, + default: undefined, + }, + onValueChange: { + type: Function as PropType<(item: TreeItem, index: number) => void>, + default: undefined, + }, +} as const + +export type TreeSelectProps = ExtractPropTypes diff --git a/devui/tree-select/src/tree-select.scss b/devui/tree-select/src/tree-select.scss new file mode 100644 index 0000000000000000000000000000000000000000..9cae05128d0cbd928cd48ce97aaad4945d6605b1 --- /dev/null +++ b/devui/tree-select/src/tree-select.scss @@ -0,0 +1,136 @@ +@import '../../style/mixins/index'; +@import '../../style/theme/color'; +@import '../../style/theme/corner'; + +$tree-select-input-height: 28px; +$tree-select-dropdown-max-height: 300px; +$tree-select-item-min-height: 36px; +$tree-select-item-font-size: 16px; + +.devui-tree-select { + position: relative; + width: 100%; +} + +.devui-tree-select-disabled { + cursor: not-allowed; + background-color: $devui-disabled-bg; + border-color: $devui-disabled-line; + color: $devui-disabled-text; + + .devui-tree-select-input { + cursor: not-allowed; + background-color: $devui-disabled-bg; + border-color: $devui-disabled-line; + color: $devui-disabled-text; + } + + .devui-tree-select-arrow { + cursor: not-allowed; + color: $devui-disabled-text; + } +} + +.devui-tree-select-open { + .devui-tree-select-arrow { + transform: rotate3d(0, 0, 1, 180deg); + } +} + +.devui-tree-select-input { + cursor: pointer; + width: 100%; + height: $tree-select-input-height; + padding: 4px $tree-select-input-height 4px 10px; + color: $devui-text; + vertical-align: middle; + border: 1px solid $devui-form-control-line; + border-radius: $devui-border-radius; + outline: none; + background-color: $devui-base-bg; +} + +.devui-tree-select-dropdown { + border-radius: $devui-border-radius; + background: $devui-base-bg; + box-shadow: 0 2px 5px 0 $devui-shadow; +} + +.devui-tree-select-dropdown-list { + max-height: $tree-select-dropdown-max-height; + overflow-y: auto; + padding: 0; + margin: 0; +} + +.devui-tree-select-item { + font-size: $tree-select-item-font-size; + display: block; + min-height: $tree-select-item-min-height; + line-height: 1.5; + width: 100%; + padding: 10px; + clear: both; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + border: 0; + color: $devui-text; + cursor: pointer; + + &:hover:not(.active):not(.disabled) { + color: $devui-list-item-hover-text; + background-color: $devui-list-item-hover-bg; + } +} + +.devui-tree-select-clearable: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-clear, +.devui-tree-select-arrow { + position: absolute; + right: 0; + height: 100%; + width: $tree-select-input-height; + display: inline-flex; + justify-content: center; + align-items: center; +} + +.devui-tree-select-clear { + display: none; + + &:hover { + cursor: pointer; + color: $devui-icon-fill-active; + } +} + +.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/devui/tree-select/src/tree-select.tsx b/devui/tree-select/src/tree-select.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ed99e34b6d3d37764ac7386e3ac40509c3cf5260 --- /dev/null +++ b/devui/tree-select/src/tree-select.tsx @@ -0,0 +1,145 @@ +import './tree-select.scss' + +import { defineComponent, ref, reactive, toRefs, computed } from 'vue' +import { treeSelectProps, TreeSelectProps } from './tree-select-types' +import { className } from './utils' + +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) + } + + function valueChange(data) { + if (data.isOpen !== undefined) { + data.isOpen = !data.isOpen + } 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 = '' + } + } + + 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-disabled': disabled, + }) + + const renderNode = (item) => ( +
{ + e.preventDefault() + e.stopPropagation() + valueChange(item) + }}> + { item.children ? + + + : {'\u00A0\u00A0\u00A0'}} + {item.label} +
+ ) + + const renderTree = (treeData) => { + return treeData.map(item => { + if (item.children) { + return ( + <> + { renderNode(item) } + { item.isOpen && renderTree(item.children) } + + ) + } + return renderNode(item) + }) + } + + return ( +
+
+ + + + + + + +
+ +
+
    {renderTree(treeData)}
+
+
+
+ ) + } +}) diff --git a/devui/tree-select/src/utils.ts b/devui/tree-select/src/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..3092da7789a04ffc4321311f268cccd1b802bb9b --- /dev/null +++ b/devui/tree-select/src/utils.ts @@ -0,0 +1,19 @@ +/** + * 动态获取class字符串 + * @param classStr 是一个字符串,固定的class名 + * @param classOpt 是一个对象,key表示class名,value为布尔值,true则添加,否则不添加 + * @returns 最终的class字符串 + */ +export function className( + classStr: string, + classOpt?: { [key: string]: boolean; } +): string { + let classname = classStr; + if (typeof classOpt === 'object') { + Object.keys(classOpt).forEach((key) => { + classOpt[key] && (classname += ` ${key}`); + }); + } + + return classname; +} diff --git a/docs/components/tree-select/index.md b/docs/components/tree-select/index.md new file mode 100644 index 0000000000000000000000000000000000000000..03a6a90407e78c74ecc4972b7b7d71627871c9d5 --- /dev/null +++ b/docs/components/tree-select/index.md @@ -0,0 +1,249 @@ +# TreeSelect 树形选择框 + +一种从列表中选择嵌套结构数据的组件。 + +### 基本用法 + +:::demo + +```vue + + +``` + +::: + +### 禁用 + +:::demo + +```vue + + +``` + +::: + +### 可清空 + +:::demo + +```vue + + +``` + +::: \ No newline at end of file