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:
Kagol 2024-06-26 16:56:50 +08:00 committed by GitHub
parent d160913047
commit 196ab84bee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 484 additions and 1 deletions

View File

@ -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
}
`
}
]
}

View File

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

View File

@ -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/)
})

View File

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

View File

@ -0,0 +1,7 @@
---
title: TreeSelect 树形选择器
---
# TreeSelect 树形选择器
结合了 BaseSelect 和 Tree 组件的选择器,用于从一个下拉树中选择一个或多个选项。

View File

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

View File

@ -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']
}
]
}

View File

@ -152,7 +152,13 @@ export const cmpMenus = [
{ 'nameCn': '开关', 'name': 'Switch', 'key': 'switch' },
{ 'nameCn': '时间选择器', 'name': 'TimePicker', 'key': 'time-picker' },
{ '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' }
}
]
},
{

View File

@ -2991,6 +2991,19 @@
"type": "template",
"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": {
"path": "vue/src/upload/index.ts",
"type": "component",

View File

@ -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]
}
})
)
}
}

View File

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

View File

@ -242,6 +242,7 @@
"@opentiny/vue-transfer-panel": "workspace:~",
"@opentiny/vue-tree": "workspace:~",
"@opentiny/vue-tree-menu": "workspace:~",
"@opentiny/vue-tree-select": "workspace:~",
"@opentiny/vue-upload": "workspace:~",
"@opentiny/vue-upload-dragger": "workspace:~",
"@opentiny/vue-upload-list": "workspace:~",

View File

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

View File

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

View File

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