feat(tree-select): [tree-select] add tree-select component (#1683)
* feat(tree-select): add tree-select component * refactor(tree-select): obtain updateSelectedData/hidePanel from baseSelectRef
This commit is contained in:
parent
d160913047
commit
196ab84bee
|
@ -0,0 +1,118 @@
|
||||||
|
export default {
|
||||||
|
mode: ['pc'],
|
||||||
|
apis: [
|
||||||
|
{
|
||||||
|
name: 'tree-select',
|
||||||
|
type: 'component',
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'clearable',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: 'false',
|
||||||
|
desc: {
|
||||||
|
'zh-CN': '是否启用一键清除的功能',
|
||||||
|
'en-US': 'Whether to display the one click clear button, only applicable to radio selection'
|
||||||
|
},
|
||||||
|
mode: ['pc'],
|
||||||
|
pcDemo: 'filter'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'filter-method',
|
||||||
|
type: '(query: string) => void',
|
||||||
|
defaultValue: '',
|
||||||
|
desc: {
|
||||||
|
'zh-CN': '自定义过滤方法',
|
||||||
|
'en-US': 'Custom filtering method'
|
||||||
|
},
|
||||||
|
mode: ['pc'],
|
||||||
|
pcDemo: 'filter'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'filterable',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: 'false',
|
||||||
|
desc: {
|
||||||
|
'zh-CN': '是否可搜索',
|
||||||
|
'en-US': 'Is it searchable'
|
||||||
|
},
|
||||||
|
mode: ['pc'],
|
||||||
|
pcDemo: 'filter'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'modelValue / v-model',
|
||||||
|
type: 'string | number | Array<string|number>',
|
||||||
|
defaultValue: '',
|
||||||
|
desc: {
|
||||||
|
'zh-CN': '绑定值',
|
||||||
|
'en-US': 'Bind value'
|
||||||
|
},
|
||||||
|
mode: ['pc'],
|
||||||
|
pcDemo: 'basic-usage'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'multiple',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: 'false',
|
||||||
|
desc: {
|
||||||
|
'zh-CN': '是否允许选择多个选项',
|
||||||
|
'en-US': 'Allow multiple options to be selected'
|
||||||
|
},
|
||||||
|
mode: ['pc'],
|
||||||
|
pcDemo: 'multiple'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'text-field',
|
||||||
|
type: 'string',
|
||||||
|
defaultValue: "'label'",
|
||||||
|
desc: {
|
||||||
|
'zh-CN': '显示值字段',
|
||||||
|
'en-US': 'Show Value Fields'
|
||||||
|
},
|
||||||
|
mode: ['pc'],
|
||||||
|
pcDemo: 'map-field'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tree-op',
|
||||||
|
typeAnchorName: 'ITreeOption',
|
||||||
|
type: 'ITreeOption',
|
||||||
|
defaultValue: '',
|
||||||
|
desc: {
|
||||||
|
'zh-CN': '下拉树时,内置树组件的配置,用法同 Tree 组件。',
|
||||||
|
'en-US':
|
||||||
|
'When pulling down a tree, the configuration of the built-in tree component is the same as that of the Tree component. To be used in conjunction with the render type attribute'
|
||||||
|
},
|
||||||
|
mode: ['pc'],
|
||||||
|
pcDemo: 'basic-usage'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'value-field',
|
||||||
|
type: 'string',
|
||||||
|
defaultValue: "'value'",
|
||||||
|
desc: {
|
||||||
|
'zh-CN': '绑定值字段',
|
||||||
|
'en-US': 'Bind Value Field'
|
||||||
|
},
|
||||||
|
mode: ['pc'],
|
||||||
|
pcDemo: 'map-field'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
types: [
|
||||||
|
{
|
||||||
|
name: 'ITreeOption',
|
||||||
|
type: 'interface',
|
||||||
|
code: `
|
||||||
|
interface ITreeNode {
|
||||||
|
label: string // 默认树节点的文本字段
|
||||||
|
id: number|string // 树节点唯一标识
|
||||||
|
children: ITreeNode[] // 子节点
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITreeOption {
|
||||||
|
data: ITreeNode[] // 树数据,用法同 Tree
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<tiny-tree-select v-model="value" :tree-op="treeOp"></tiny-tree-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { TreeSelect as TinyTreeSelect } from '@opentiny/vue'
|
||||||
|
|
||||||
|
const value = ref('')
|
||||||
|
|
||||||
|
const treeOp = ref({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: '一级 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
label: '二级 1-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 9,
|
||||||
|
label: '三级 1-1-1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 10,
|
||||||
|
label: '三级 1-1-2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: '一级 2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 5,
|
||||||
|
label: '二级 2-1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 6,
|
||||||
|
label: '二级 2-2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tiny-tree-select {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
|
||||||
|
test('测试基本用法', async ({ page }) => {
|
||||||
|
page.on('pageerror', (exception) => expect(exception).toBeNull())
|
||||||
|
await page.goto('tree-select#basic-usage')
|
||||||
|
|
||||||
|
const wrap = page.locator('#basic-usage')
|
||||||
|
const select = wrap.locator('.tiny-tree-select').nth(0)
|
||||||
|
const input = select.locator('.tiny-input__inner')
|
||||||
|
const dropdown = page.locator('body > .tiny-select-dropdown')
|
||||||
|
const treeNode = dropdown.locator('.tiny-tree-node')
|
||||||
|
|
||||||
|
await input.click()
|
||||||
|
await expect(treeNode).toHaveCount(7)
|
||||||
|
|
||||||
|
await treeNode.filter({ hasText: /^二级 2-1$/ }).click()
|
||||||
|
await expect(input).toHaveValue('二级 2-1')
|
||||||
|
await input.click()
|
||||||
|
await expect(treeNode.filter({ hasText: /^二级 2-1$/ })).toHaveClass(/is-current/)
|
||||||
|
})
|
|
@ -0,0 +1,61 @@
|
||||||
|
<template>
|
||||||
|
<tiny-tree-select v-model="value" :tree-op="treeOp"></tiny-tree-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { TreeSelect } from '@opentiny/vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
TinyTreeSelect: TreeSelect
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
treeOp: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: '一级 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
label: '二级 1-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 9,
|
||||||
|
label: '三级 1-1-1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 10,
|
||||||
|
label: '三级 1-1-2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: '一级 2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 5,
|
||||||
|
label: '二级 2-1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 6,
|
||||||
|
label: '二级 2-2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tiny-tree-select {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
title: TreeSelect 树形选择器
|
||||||
|
---
|
||||||
|
|
||||||
|
# TreeSelect 树形选择器
|
||||||
|
|
||||||
|
结合了 BaseSelect 和 Tree 组件的选择器,用于从一个下拉树中选择一个或多个选项。
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
title: TreeSelect
|
||||||
|
---
|
||||||
|
|
||||||
|
# TreeSelect
|
||||||
|
|
||||||
|
A selector that combines the BaseSelect and Tree components to select one or more options from a drop-down tree.
|
|
@ -0,0 +1,18 @@
|
||||||
|
export default {
|
||||||
|
column: '2',
|
||||||
|
owner: '',
|
||||||
|
demos: [
|
||||||
|
{
|
||||||
|
demoId: 'basic-usage',
|
||||||
|
name: {
|
||||||
|
'zh-CN': '基本用法',
|
||||||
|
'en-US': 'Basic Usage'
|
||||||
|
},
|
||||||
|
desc: {
|
||||||
|
'zh-CN': '<p>最基础的用法,通过 <code>tree-op</code> 设置下拉树的数据源,<code>v-model</code> 设置绑定值。</p>',
|
||||||
|
'en-US': ''
|
||||||
|
},
|
||||||
|
codeFiles: ['basic-usage.vue']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -152,7 +152,13 @@ export const cmpMenus = [
|
||||||
{ 'nameCn': '开关', 'name': 'Switch', 'key': 'switch' },
|
{ 'nameCn': '开关', 'name': 'Switch', 'key': 'switch' },
|
||||||
{ 'nameCn': '时间选择器', 'name': 'TimePicker', 'key': 'time-picker' },
|
{ 'nameCn': '时间选择器', 'name': 'TimePicker', 'key': 'time-picker' },
|
||||||
{ 'nameCn': '时间选择', 'name': 'TimeSelect', 'key': 'time-select' },
|
{ 'nameCn': '时间选择', 'name': 'TimeSelect', 'key': 'time-select' },
|
||||||
{ 'nameCn': '穿梭框', 'name': 'Transfer', 'key': 'transfer' }
|
{ 'nameCn': '穿梭框', 'name': 'Transfer', 'key': 'transfer' },
|
||||||
|
{
|
||||||
|
'nameCn': '树形选择器',
|
||||||
|
'name': 'TreeSelect',
|
||||||
|
'key': 'tree-select',
|
||||||
|
'mark': { 'type': 'warning', 'text': 'Beta' }
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -2991,6 +2991,19 @@
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"exclude": false
|
"exclude": false
|
||||||
},
|
},
|
||||||
|
"TreeSelect": {
|
||||||
|
"path": "vue/src/tree-select/index.ts",
|
||||||
|
"type": "component",
|
||||||
|
"exclude": false,
|
||||||
|
"mode": [
|
||||||
|
"pc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"TreeSelectPc": {
|
||||||
|
"path": "vue/src/tree-select/src/pc.vue",
|
||||||
|
"type": "template",
|
||||||
|
"exclude": false
|
||||||
|
},
|
||||||
"Upload": {
|
"Upload": {
|
||||||
"path": "vue/src/upload/index.ts",
|
"path": "vue/src/upload/index.ts",
|
||||||
"type": "component",
|
"type": "component",
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
export const filter =
|
||||||
|
({ vm }) =>
|
||||||
|
(value) => {
|
||||||
|
vm.$refs.treeRef.filter(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const nodeClick =
|
||||||
|
({ props, vm }) =>
|
||||||
|
(data) => {
|
||||||
|
if (!props.multiple) {
|
||||||
|
vm.$refs.baseSelectRef.updateSelectedData({
|
||||||
|
...data,
|
||||||
|
currentLabel: data[props.textField],
|
||||||
|
value: data[props.valueField],
|
||||||
|
state: {
|
||||||
|
currentLabel: data[props.textField]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vm.$refs.baseSelectRef.hidePanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const check =
|
||||||
|
({ props }) =>
|
||||||
|
(data, { checkedNodes }) => {
|
||||||
|
if (props.multiple) {
|
||||||
|
vm.$refs.baseSelectRef.updateSelectedData(
|
||||||
|
checkedNodes.map((node) => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
currentLabel: node[props.textField],
|
||||||
|
value: node[props.valueField]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { filter, nodeClick, check } from './index'
|
||||||
|
|
||||||
|
export const api = ['state', 'filter', 'nodeClick', 'check']
|
||||||
|
|
||||||
|
export const renderless = (props, { reactive }, { vm }) => {
|
||||||
|
const api = {}
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
value: props.modelValue,
|
||||||
|
treeData: props.treeOp.data
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.assign(api, {
|
||||||
|
state,
|
||||||
|
filter: filter({ vm }),
|
||||||
|
nodeClick: nodeClick({ props, vm }),
|
||||||
|
check: check({ props })
|
||||||
|
})
|
||||||
|
|
||||||
|
return api
|
||||||
|
}
|
|
@ -242,6 +242,7 @@
|
||||||
"@opentiny/vue-transfer-panel": "workspace:~",
|
"@opentiny/vue-transfer-panel": "workspace:~",
|
||||||
"@opentiny/vue-tree": "workspace:~",
|
"@opentiny/vue-tree": "workspace:~",
|
||||||
"@opentiny/vue-tree-menu": "workspace:~",
|
"@opentiny/vue-tree-menu": "workspace:~",
|
||||||
|
"@opentiny/vue-tree-select": "workspace:~",
|
||||||
"@opentiny/vue-upload": "workspace:~",
|
"@opentiny/vue-upload": "workspace:~",
|
||||||
"@opentiny/vue-upload-dragger": "workspace:~",
|
"@opentiny/vue-upload-dragger": "workspace:~",
|
||||||
"@opentiny/vue-upload-list": "workspace:~",
|
"@opentiny/vue-upload-list": "workspace:~",
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2022 - present TinyVue Authors.
|
||||||
|
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license.
|
||||||
|
*
|
||||||
|
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
|
||||||
|
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
import TreeSelect from './src/pc.vue'
|
||||||
|
import { version } from './package.json'
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
TreeSelect.install = function (Vue) {
|
||||||
|
Vue.component(TreeSelect.name, TreeSelect)
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeSelect.version = version
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (process.env.BUILD_TARGET === 'runtime') {
|
||||||
|
if (typeof window !== 'undefined' && window.Vue) {
|
||||||
|
TreeSelect.install(window.Vue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TreeSelect
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "@opentiny/vue-tree-select",
|
||||||
|
"version": "3.16.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"module": "index.ts",
|
||||||
|
"sideEffects": false,
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@opentiny-internal/vue-test-utils": "workspace:*",
|
||||||
|
"vitest": "^0.31.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm -w build:ui $npm_package_name",
|
||||||
|
"//postversion": "pnpm build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@opentiny/vue-common": "workspace:~",
|
||||||
|
"@opentiny/vue-renderless": "workspace:~",
|
||||||
|
"@opentiny/vue-theme": "workspace:~",
|
||||||
|
"@opentiny/vue-base-select": "workspace:~",
|
||||||
|
"@opentiny/vue-tree": "workspace:~"
|
||||||
|
},
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<tiny-base-select
|
||||||
|
ref="baseSelectRef"
|
||||||
|
class="tiny-tree-select"
|
||||||
|
v-model="state.value"
|
||||||
|
:multiple="multiple"
|
||||||
|
:filterable="filterable"
|
||||||
|
:clearable="clearable"
|
||||||
|
:filter-method="filter"
|
||||||
|
>
|
||||||
|
<template #panel>
|
||||||
|
<tiny-tree
|
||||||
|
ref="treeRef"
|
||||||
|
:data="state.treeData"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:icon-trigger-click-node="false"
|
||||||
|
:default-expand-all="true"
|
||||||
|
:props="{ label: textField }"
|
||||||
|
:node-key="valueField"
|
||||||
|
:show-checkbox="multiple"
|
||||||
|
:filter-node-method="filterMethod"
|
||||||
|
@node-click="nodeClick"
|
||||||
|
@check="check"
|
||||||
|
></tiny-tree>
|
||||||
|
</template>
|
||||||
|
</tiny-base-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { $prefix, defineComponent, setup } from '@opentiny/vue-common'
|
||||||
|
import { renderless, api } from '@opentiny/vue-renderless/tree-select/vue'
|
||||||
|
import Tree from '@opentiny/vue-tree'
|
||||||
|
import BaseSelect from '@opentiny/vue-base-select'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: $prefix + 'TreeSelect',
|
||||||
|
components: {
|
||||||
|
TinyTree: Tree,
|
||||||
|
TinyBaseSelect: BaseSelect
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
clearable: Boolean,
|
||||||
|
filterable: Boolean,
|
||||||
|
filterMethod: Function,
|
||||||
|
modelValue: {},
|
||||||
|
multiple: Boolean,
|
||||||
|
textField: {
|
||||||
|
type: String,
|
||||||
|
default: 'label'
|
||||||
|
},
|
||||||
|
treeOp: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
valueField: {
|
||||||
|
type: String,
|
||||||
|
default: 'value'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props, context) {
|
||||||
|
return setup({ props, context, renderless, api })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
Loading…
Reference in New Issue