forked from opentiny/tiny-vue
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': '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' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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-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:~",
|
||||
|
|
|
@ -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