Skip to content

Commit 6c45a02

Browse files
kagolgitee-org
authored andcommitted
!191 新增tree-select特性
Merge pull request !191 from Minglye/dev
2 parents 5a41058 + 735f14c commit 6c45a02

File tree

6 files changed

+636
-0
lines changed

6 files changed

+636
-0
lines changed

devui/tree-select/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { App } from 'vue'
2+
import TreeSelect from './src/tree-select'
3+
4+
TreeSelect.install = function(app: App): void {
5+
app.component(TreeSelect.name, TreeSelect)
6+
}
7+
8+
export { TreeSelect }
9+
10+
export default {
11+
title: 'TreeSelect 树形选择框',
12+
category: '数据录入',
13+
status: undefined, // TODO: 组件若开发完成则填入"已完成",并删除该注释
14+
install(app: App): void {
15+
16+
app.use(TreeSelect as any)
17+
}
18+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { PropType, ExtractPropTypes } from 'vue'
2+
3+
export interface TreeItem {
4+
id?: number | string
5+
label?: string
6+
data?: any
7+
parent?: TreeItem | null
8+
children?: Array<TreeItem> | null
9+
level?: number
10+
loading?: boolean
11+
isOpen?: boolean
12+
isChecked?: boolean
13+
disabled?: boolean
14+
15+
[prop: string]: any
16+
}
17+
18+
export type TreeData = Array<TreeItem>
19+
20+
export type ModelValue = number | string | Array<number | string>;
21+
22+
export const treeSelectProps = {
23+
modelValue: {
24+
type: [String, Number, Array] as PropType<ModelValue>,
25+
default: '',
26+
},
27+
treeData: {
28+
type: Array as PropType<TreeData>,
29+
default: () => [],
30+
},
31+
placeholder: {
32+
type: String,
33+
default: '请选择',
34+
},
35+
disabled: {
36+
type: Boolean,
37+
default: false
38+
},
39+
expandTree: {
40+
type: Boolean,
41+
default: false
42+
},
43+
multiple: {
44+
type: Boolean,
45+
default: false,
46+
},
47+
leafOnly: {
48+
type: Boolean,
49+
default: false,
50+
},
51+
searchable: {
52+
type: Boolean,
53+
default: false,
54+
},
55+
allowClear: {
56+
type: Boolean,
57+
default: false
58+
},
59+
onToggleChange: {
60+
type: Function as PropType<(bool: boolean) => void>,
61+
default: undefined,
62+
},
63+
onValueChange: {
64+
type: Function as PropType<(item: TreeItem, index: number) => void>,
65+
default: undefined,
66+
},
67+
} as const
68+
69+
export type TreeSelectProps = ExtractPropTypes<typeof treeSelectProps>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
@import '../../style/mixins/index';
2+
@import '../../style/theme/color';
3+
@import '../../style/theme/corner';
4+
5+
$tree-select-input-height: 28px;
6+
$tree-select-dropdown-max-height: 300px;
7+
$tree-select-item-min-height: 36px;
8+
$tree-select-item-font-size: 16px;
9+
10+
.devui-tree-select {
11+
position: relative;
12+
width: 100%;
13+
}
14+
15+
.devui-tree-select-disabled {
16+
cursor: not-allowed;
17+
background-color: $devui-disabled-bg;
18+
border-color: $devui-disabled-line;
19+
color: $devui-disabled-text;
20+
21+
.devui-tree-select-input {
22+
cursor: not-allowed;
23+
background-color: $devui-disabled-bg;
24+
border-color: $devui-disabled-line;
25+
color: $devui-disabled-text;
26+
}
27+
28+
.devui-tree-select-arrow {
29+
cursor: not-allowed;
30+
color: $devui-disabled-text;
31+
}
32+
}
33+
34+
.devui-tree-select-open {
35+
.devui-tree-select-arrow {
36+
transform: rotate3d(0, 0, 1, 180deg);
37+
}
38+
}
39+
40+
.devui-tree-select-input {
41+
cursor: pointer;
42+
width: 100%;
43+
height: $tree-select-input-height;
44+
padding: 4px $tree-select-input-height 4px 10px;
45+
color: $devui-text;
46+
vertical-align: middle;
47+
border: 1px solid $devui-form-control-line;
48+
border-radius: $devui-border-radius;
49+
outline: none;
50+
background-color: $devui-base-bg;
51+
}
52+
53+
.devui-tree-select-dropdown {
54+
border-radius: $devui-border-radius;
55+
background: $devui-base-bg;
56+
box-shadow: 0 2px 5px 0 $devui-shadow;
57+
}
58+
59+
.devui-tree-select-dropdown-list {
60+
max-height: $tree-select-dropdown-max-height;
61+
overflow-y: auto;
62+
padding: 0;
63+
margin: 0;
64+
}
65+
66+
.devui-tree-select-item {
67+
font-size: $tree-select-item-font-size;
68+
display: block;
69+
min-height: $tree-select-item-min-height;
70+
line-height: 1.5;
71+
width: 100%;
72+
padding: 10px;
73+
clear: both;
74+
overflow: hidden;
75+
white-space: nowrap;
76+
text-overflow: ellipsis;
77+
border: 0;
78+
color: $devui-text;
79+
cursor: pointer;
80+
81+
&:hover:not(.active):not(.disabled) {
82+
color: $devui-list-item-hover-text;
83+
background-color: $devui-list-item-hover-bg;
84+
}
85+
}
86+
87+
.devui-tree-select-clearable:hover {
88+
.devui-tree-select-clear {
89+
display: inline-flex;
90+
}
91+
92+
.devui-tree-select-arrow {
93+
display: none;
94+
}
95+
}
96+
97+
.devui-tree-select-clearable:hover {
98+
.devui-tree-select-clear {
99+
display: inline-flex;
100+
}
101+
102+
.devui-tree-select-arrow {
103+
display: none;
104+
}
105+
}
106+
107+
.devui-tree-select-clear,
108+
.devui-tree-select-arrow {
109+
position: absolute;
110+
right: 0;
111+
height: 100%;
112+
width: $tree-select-input-height;
113+
display: inline-flex;
114+
justify-content: center;
115+
align-items: center;
116+
}
117+
118+
.devui-tree-select-clear {
119+
display: none;
120+
121+
&:hover {
122+
cursor: pointer;
123+
color: $devui-icon-fill-active;
124+
}
125+
}
126+
127+
.devui-tree-select-arrow-expand {
128+
display: inline-flex;
129+
justify-content: center;
130+
align-items: center;
131+
transform: rotate3d(0, 0, 1, 270deg);
132+
}
133+
134+
.devui-tree-select-arrow-open {
135+
transform: rotate3d(0, 0, 1, 0deg);
136+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import './tree-select.scss'
2+
3+
import { defineComponent, ref, reactive, toRefs, computed } from 'vue'
4+
import { treeSelectProps, TreeSelectProps } from './tree-select-types'
5+
import { className } from './utils'
6+
7+
export default defineComponent({
8+
name: 'DTreeSelect',
9+
props: treeSelectProps,
10+
emits: ['toggleChange', 'valueChange', 'update:modelValue'],
11+
setup(props: TreeSelectProps, ctx) {
12+
13+
const visible = ref<boolean>(false)
14+
const origin = ref()
15+
const position = reactive({
16+
originX: 'left',
17+
originY: 'bottom',
18+
overlayX: 'left',
19+
overlayY: 'top'
20+
})
21+
const inputValue = ref<string>('')
22+
23+
const { treeData } = toRefs(props)
24+
25+
const mergeClearable = computed<boolean>(() => {
26+
return !props.disabled && props.allowClear && inputValue.value.length > 0;
27+
})
28+
29+
function toggleChange() {
30+
if (props.disabled) return
31+
visible.value = !visible.value
32+
ctx.emit('toggleChange', visible.value)
33+
}
34+
35+
function valueChange(data) {
36+
if (data.isOpen !== undefined) {
37+
data.isOpen = !data.isOpen
38+
} else {
39+
inputValue.value = data.label
40+
visible.value = false
41+
ctx.emit('update:modelValue', data.label)
42+
ctx.emit('toggleChange', visible.value)
43+
}
44+
}
45+
46+
function handleClear(e: MouseEvent) {
47+
e.preventDefault()
48+
e.stopPropagation()
49+
if (props.multiple) {
50+
ctx.emit('update:modelValue', [])
51+
} else {
52+
ctx.emit('update:modelValue', '')
53+
inputValue.value = ''
54+
}
55+
}
56+
57+
return {
58+
visible,
59+
origin,
60+
position,
61+
inputValue,
62+
mergeClearable,
63+
treeData,
64+
handleClear,
65+
toggleChange,
66+
valueChange,
67+
}
68+
},
69+
render() {
70+
const {
71+
origin,
72+
position,
73+
inputValue,
74+
mergeClearable,
75+
treeData,
76+
placeholder,
77+
disabled,
78+
handleClear,
79+
toggleChange,
80+
valueChange
81+
} = this
82+
83+
const treeSelectCls = className('devui-tree-select', {
84+
'devui-tree-select-open': this.visible,
85+
'devui-tree-select-disabled': disabled,
86+
})
87+
88+
const renderNode = (item) => (
89+
<div
90+
class="devui-tree-select-item"
91+
style={{ paddingLeft: `${20 * (item.level - 1)}px` }}
92+
onClick={(e: MouseEvent) => {
93+
e.preventDefault()
94+
e.stopPropagation()
95+
valueChange(item)
96+
}}>
97+
{ item.children ?
98+
<span class={['devui-tree-select-arrow-expand', item.isOpen ? 'devui-tree-select-arrow-open' : '']}>
99+
<d-icon name="select-arrow" />
100+
</span> : <span>{'\u00A0\u00A0\u00A0'}</span>}
101+
{item.label}
102+
</div>
103+
)
104+
105+
const renderTree = (treeData) => {
106+
return treeData.map(item => {
107+
if (item.children) {
108+
return (
109+
<>
110+
{ renderNode(item) }
111+
{ item.isOpen && renderTree(item.children) }
112+
</>
113+
)
114+
}
115+
return renderNode(item)
116+
})
117+
}
118+
119+
return (
120+
<div class={treeSelectCls}>
121+
<div class={mergeClearable ? 'devui-tree-select-clearable' : ''} ref="origin" onClick={toggleChange}>
122+
<input
123+
value={inputValue}
124+
type="text"
125+
class="devui-tree-select-input"
126+
placeholder={placeholder}
127+
readonly
128+
disabled={disabled}
129+
/>
130+
<span onClick={handleClear} class="devui-tree-select-clear">
131+
<d-icon name="close" />
132+
</span>
133+
<span class="devui-tree-select-arrow">
134+
<d-icon name="select-arrow" />
135+
</span>
136+
</div>
137+
<d-flexible-overlay origin={origin} v-model={[this.visible, 'visible']} position={position}>
138+
<div class="devui-tree-select-dropdown">
139+
<ul class="devui-tree-select-dropdown-list">{renderTree(treeData)}</ul>
140+
</div>
141+
</d-flexible-overlay>
142+
</div>
143+
)
144+
}
145+
})

devui/tree-select/src/utils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* 动态获取class字符串
3+
* @param classStr 是一个字符串,固定的class名
4+
* @param classOpt 是一个对象,key表示class名,value为布尔值,true则添加,否则不添加
5+
* @returns 最终的class字符串
6+
*/
7+
export function className(
8+
classStr: string,
9+
classOpt?: { [key: string]: boolean; }
10+
): string {
11+
let classname = classStr;
12+
if (typeof classOpt === 'object') {
13+
Object.keys(classOpt).forEach((key) => {
14+
classOpt[key] && (classname += ` ${key}`);
15+
});
16+
}
17+
18+
return classname;
19+
}

0 commit comments

Comments
 (0)