feat(base-select): [select,base-select] add panel slot (#1663)

* feat(base-select): add panel slot

* feat(base-select): add multiple tree demo

* docs(base-select): add filterable tree demo

* test(base-select): optimize slot panel e2e test
This commit is contained in:
Kagol 2024-06-07 11:40:31 +08:00 committed by GitHub
parent de7adb69e5
commit b72b21da0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 386 additions and 17 deletions

View File

@ -765,6 +765,16 @@ export default {
mode: ['pc', 'mobile-first'],
pcDemo: 'slot-reference',
mfDemo: 'slot-reference'
},
{
name: 'panel',
desc: {
'zh-CN': '下拉面板插槽',
'en-US': 'Panel slot'
},
mode: ['pc', 'mobile-first'],
pcDemo: 'slot-panel',
mfDemo: 'slot-panel'
}
]
},

View File

@ -0,0 +1,143 @@
<template>
<div>场景1下拉树单选</div>
<tiny-base-select v-model="value">
<template #panel="{ methods: { updateSelectedData, hidePanel } }">
<tiny-tree
:data="treeData"
:expand-on-click-node="false"
:icon-trigger-click-node="false"
:default-expand-all="true"
@node-click="
(data) => {
updateSelectedData({
...data,
currentLabel: data.label,
value: data.id,
state: {
currentLabel: data.label
}
})
hidePanel()
}
"
></tiny-tree>
</template>
</tiny-base-select>
<div>场景2下拉树多选</div>
<tiny-base-select v-model="value2" multiple>
<template #panel="{ methods: { updateSelectedData } }">
<tiny-tree
:data="treeData"
:expand-on-click-node="false"
:icon-trigger-click-node="false"
:default-expand-all="true"
:show-checkbox="true"
@check="
(data, { checkedNodes }) => {
updateSelectedData(
checkedNodes.map((node) => {
return {
...node,
currentLabel: node.label,
value: node.id
}
})
)
}
"
></tiny-tree>
</template>
</tiny-base-select>
<div>场景3下拉树可搜索</div>
<tiny-base-select v-model="value3" filterable clearable :filter-method="filterMethod">
<template #panel="{ methods: { updateSelectedData, hidePanel } }">
<tiny-tree
ref="treeRef"
:data="treeData"
:expand-on-click-node="false"
:icon-trigger-click-node="false"
:default-expand-all="true"
:filter-node-method="filter"
@node-click="
(data) => {
updateSelectedData({
...data,
currentLabel: data.label,
value: data.id,
state: {
currentLabel: data.label
}
})
hidePanel()
}
"
></tiny-tree>
</template>
</tiny-base-select>
</template>
<script setup>
import { ref } from 'vue'
import { BaseSelect as TinyBaseSelect, Tree as TinyTree } from '@opentiny/vue'
const treeRef = ref()
const value = ref('')
const value2 = ref([])
const value3 = ref('')
const treeData = ref([
{
id: 1,
label: '一级 1',
children: [
{
id: 4,
label: '二级 1-1',
children: [
{
id: 9,
label: '三级 1-1-1'
},
{
id: 10,
label: '三级 1-1-2'
}
]
}
]
},
{
id: 2,
label: '一级 2',
children: [
{
id: 5,
label: '二级 2-1'
},
{
id: 6,
label: '二级 2-2'
}
]
}
])
const filterMethod = (value) => {
treeRef.value.filter(value)
}
const filter = (value, data) => {
if (!value) return true
return data.label.includes(value)
}
</script>
<style scoped>
.tiny-base-select {
width: 280px;
}
</style>

View File

@ -0,0 +1,20 @@
import { expect, test } from '@playwright/test'
test('custom-panel', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('base-select#slot-panel')
const wrap = page.locator('#slot-panel')
const select = wrap.locator('.tiny-base-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,149 @@
<template>
<div>场景1下拉树单选</div>
<tiny-base-select v-model="value">
<template #panel="{ methods: { updateSelectedData, hidePanel } }">
<tiny-tree
:data="treeData"
:expand-on-click-node="false"
:icon-trigger-click-node="false"
:default-expand-all="true"
@node-click="
(data) => {
updateSelectedData({
...data,
currentLabel: data.label,
value: data.id,
state: {
currentLabel: data.label
}
})
hidePanel()
}
"
></tiny-tree>
</template>
</tiny-base-select>
<div>场景2下拉树多选</div>
<tiny-base-select v-model="value2" multiple>
<template #panel="{ methods: { updateSelectedData } }">
<tiny-tree
:data="treeData"
:expand-on-click-node="false"
:icon-trigger-click-node="false"
:default-expand-all="true"
:show-checkbox="true"
@check="
(data, { checkedNodes }) => {
updateSelectedData(
checkedNodes.map((node) => {
return {
...node,
currentLabel: node.label,
value: node.id
}
})
)
}
"
></tiny-tree>
</template>
</tiny-base-select>
<div>场景3下拉树可搜索</div>
<tiny-base-select v-model="value3" filterable clearable :filter-method="filterMethod">
<template #panel="{ methods: { updateSelectedData, hidePanel } }">
<tiny-tree
ref="treeRef"
:data="treeData"
:expand-on-click-node="false"
:icon-trigger-click-node="false"
:default-expand-all="true"
:filter-node-method="filter"
@node-click="
(data) => {
updateSelectedData({
...data,
currentLabel: data.label,
value: data.id,
state: {
currentLabel: data.label
}
})
hidePanel()
}
"
></tiny-tree>
</template>
</tiny-base-select>
</template>
<script>
import { BaseSelect, Tree } from '@opentiny/vue'
export default {
components: {
TinyBaseSelect: BaseSelect,
TinyTree: Tree
},
data() {
return {
value: '',
value2: '',
value3: '',
treeData: [
{
id: 1,
label: '一级 1',
children: [
{
id: 4,
label: '二级 1-1',
children: [
{
id: 9,
label: '三级 1-1-1'
},
{
id: 10,
label: '三级 1-1-2'
}
]
}
]
},
{
id: 2,
label: '一级 2',
children: [
{
id: 5,
label: '二级 2-1'
},
{
id: 6,
label: '二级 2-2'
}
]
}
]
}
},
methods: {
filterMethod(value) {
this.$refs.treeRef.filter(value)
},
filter(value, data) {
if (!value) return true
return data.label.includes(value)
}
}
}
</script>
<style scoped>
.tiny-base-select {
width: 280px;
}
</style>

View File

@ -491,6 +491,18 @@ export default {
},
codeFiles: ['slot-reference.vue']
},
{
demoId: 'slot-panel',
name: {
'zh-CN': '下拉面板插槽',
'en-US': 'Panel slot'
},
desc: {
'zh-CN': '<p>通过 <code>panel</code> 插槽自定义下拉面板的内容。</p>\n',
'en-US': '<p>Customize content of the panel through the <code>panel</code> slot.</p>'
},
codeFiles: ['slot-panel.vue']
},
{
demoId: 'slot-label',
name: {

View File

@ -244,20 +244,24 @@ export const getOption =
const isNull = Object.prototype.toString.call(value).toLowerCase() === '[object null]'
const isUndefined = Object.prototype.toString.call(value).toLowerCase() === '[object undefined]'
for (let i = state.cachedOptions.length - 1; i >= 0; i--) {
const cachedOption = state.cachedOptions[i]
const isEqual = isObject
? getObj(cachedOption.value, props.valueKey) === getObj(value, props.valueKey)
: cachedOption.value === value
if (state.cachedOptions.length !== 0) {
for (let i = state.cachedOptions.length - 1; i >= 0; i--) {
const cachedOption = state.cachedOptions[i]
const isEqual = isObject
? getObj(cachedOption.value, props.valueKey) === getObj(value, props.valueKey)
: cachedOption.value === value
if (isEqual) {
option = cachedOption
break
if (isEqual) {
option = cachedOption
break
}
}
}
if (option) {
return option
if (option) {
return option
}
} else if (!isEmptyObject(state.selected)) {
return state.selected
}
if (props.optimization) {
@ -356,7 +360,11 @@ export const setSelected =
? 'checked-sur'
: 'halfselect'
: 'check'
state.selected = result
if (state.selected.length === 0 || !state.selected[0].currentLabel) {
state.selected = result
}
vm.$refs.selectTree && vm.$refs.selectTree.setCheckedNodes && vm.$refs.selectTree.setCheckedNodes(state.selected)
state.tips = state.selected.map((item) => (item.state ? item.state.currentLabel : item.currentLabel)).join(',')
@ -1083,7 +1091,7 @@ export const emptyText =
return props.loadingText || t(I18N.loading)
}
if (props.remote && state.query === '') {
if (props.remote && state.query === '' && props.renderType) {
return remoteEmptyText(props, state)
}
@ -2020,3 +2028,15 @@ export const onClickCollapseTag =
nextTick(api.resetInputHeight)
}
}
export const updateSelectedData =
({ state }) =>
(data) => {
state.selected = data
}
export const hidePanel =
({ state }) =>
() => {
state.visible = false
}

View File

@ -95,7 +95,9 @@ import {
clearNoMatchValue,
handleDebouncedQueryChange,
onClickCollapseTag,
computedIsExpand
computedIsExpand,
updateSelectedData,
hidePanel
} from './index'
import debounce from '../common/deps/debounce'
import { isNumber } from '../common/type'
@ -150,7 +152,9 @@ export const api = [
'getLabelSlotValue',
'updateModelValue',
'clearSearchText',
'onClickCollapseTag'
'onClickCollapseTag',
'updateSelectedData',
'hidePanel'
]
const initState = ({ reactive, computed, props, api, emitter, parent, constants, useBreakpoint, vm, designConfig }) => {
@ -359,7 +363,9 @@ const initApi = ({
computedGetIcon: computedGetIcon({ designConfig, props }),
computedGetTagType: computedGetTagType({ designConfig, props }),
clearSearchText: clearSearchText({ state, api }),
clearNoMatchValue: clearNoMatchValue({ props, emit })
clearNoMatchValue: clearNoMatchValue({ props, emit }),
updateSelectedData: updateSelectedData({ state }),
hidePanel: hidePanel({ state })
})
addApi({ api, props, state, emit, constants, parent, nextTick, dispatch, vm, isMobileFirstMode, designConfig })

View File

@ -351,6 +351,13 @@
<span>{{ topCreateText }}</span>
</div>
</div>
<slot
name="panel"
:methods="{
updateSelectedData,
hidePanel
}"
></slot>
<!-- tiny 新增 可搜索的输入框 -->
<tiny-input
v-if="searchable"
@ -495,7 +502,9 @@
</tiny-option>
</slot>
</tiny-scrollbar>
<template v-if="state.emptyText && (!allowCreate || loading || (allowCreate && state.emptyFlag))">
<template
v-if="!slots.panel && state.emptyText && (!allowCreate || loading || (allowCreate && state.emptyFlag))"
>
<!-- tiny 新增 showEmptyImage功能 -->
<div v-if="loadingText || slots.empty">
<slot name="empty" v-if="slots.empty"></slot>