diff --git a/examples/sites/demos/apis/tree-select.js b/examples/sites/demos/apis/tree-select.js new file mode 100644 index 000000000..9b8c4e66f --- /dev/null +++ b/examples/sites/demos/apis/tree-select.js @@ -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', + 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 +} +` + } + ] +} diff --git a/examples/sites/demos/pc/app/tree-select/basic-usage-composition-api.vue b/examples/sites/demos/pc/app/tree-select/basic-usage-composition-api.vue new file mode 100644 index 000000000..b76f14668 --- /dev/null +++ b/examples/sites/demos/pc/app/tree-select/basic-usage-composition-api.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tree-select/basic-usage.spec.ts b/examples/sites/demos/pc/app/tree-select/basic-usage.spec.ts new file mode 100644 index 000000000..9c01f07ba --- /dev/null +++ b/examples/sites/demos/pc/app/tree-select/basic-usage.spec.ts @@ -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/) +}) diff --git a/examples/sites/demos/pc/app/tree-select/basic-usage.vue b/examples/sites/demos/pc/app/tree-select/basic-usage.vue new file mode 100644 index 000000000..322f0e7be --- /dev/null +++ b/examples/sites/demos/pc/app/tree-select/basic-usage.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tree-select/webdoc/tree-select.cn.md b/examples/sites/demos/pc/app/tree-select/webdoc/tree-select.cn.md new file mode 100644 index 000000000..15272e60c --- /dev/null +++ b/examples/sites/demos/pc/app/tree-select/webdoc/tree-select.cn.md @@ -0,0 +1,7 @@ +--- +title: TreeSelect 树形选择器 +--- + +# TreeSelect 树形选择器 + +结合了 BaseSelect 和 Tree 组件的选择器,用于从一个下拉树中选择一个或多个选项。 diff --git a/examples/sites/demos/pc/app/tree-select/webdoc/tree-select.en.md b/examples/sites/demos/pc/app/tree-select/webdoc/tree-select.en.md new file mode 100644 index 000000000..7df84dc75 --- /dev/null +++ b/examples/sites/demos/pc/app/tree-select/webdoc/tree-select.en.md @@ -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. diff --git a/examples/sites/demos/pc/app/tree-select/webdoc/tree-select.js b/examples/sites/demos/pc/app/tree-select/webdoc/tree-select.js new file mode 100644 index 000000000..774ed3c8d --- /dev/null +++ b/examples/sites/demos/pc/app/tree-select/webdoc/tree-select.js @@ -0,0 +1,18 @@ +export default { + column: '2', + owner: '', + demos: [ + { + demoId: 'basic-usage', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic Usage' + }, + desc: { + 'zh-CN': '

最基础的用法,通过 tree-op 设置下拉树的数据源,v-model 设置绑定值。

', + 'en-US': '' + }, + codeFiles: ['basic-usage.vue'] + } + ] +} diff --git a/examples/sites/demos/pc/menus.js b/examples/sites/demos/pc/menus.js index 8b5702865..42ca28db9 100644 --- a/examples/sites/demos/pc/menus.js +++ b/examples/sites/demos/pc/menus.js @@ -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' } + } ] }, { diff --git a/packages/modules.json b/packages/modules.json index c0afc12bf..9cea4eb5d 100644 --- a/packages/modules.json +++ b/packages/modules.json @@ -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", diff --git a/packages/renderless/src/tree-select/index.ts b/packages/renderless/src/tree-select/index.ts new file mode 100644 index 000000000..3e39cc628 --- /dev/null +++ b/packages/renderless/src/tree-select/index.ts @@ -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] + } + }) + ) + } + } diff --git a/packages/renderless/src/tree-select/vue.ts b/packages/renderless/src/tree-select/vue.ts new file mode 100644 index 000000000..426a8fdde --- /dev/null +++ b/packages/renderless/src/tree-select/vue.ts @@ -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 +} diff --git a/packages/vue/package.json b/packages/vue/package.json index e7d69213b..814fc8db5 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -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:~", diff --git a/packages/vue/src/tree-select/index.ts b/packages/vue/src/tree-select/index.ts new file mode 100644 index 000000000..9220f024c --- /dev/null +++ b/packages/vue/src/tree-select/index.ts @@ -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 diff --git a/packages/vue/src/tree-select/package.json b/packages/vue/src/tree-select/package.json new file mode 100644 index 000000000..e6724f953 --- /dev/null +++ b/packages/vue/src/tree-select/package.json @@ -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" +} \ No newline at end of file diff --git a/packages/vue/src/tree-select/src/pc.vue b/packages/vue/src/tree-select/src/pc.vue new file mode 100644 index 000000000..5f4f27926 --- /dev/null +++ b/packages/vue/src/tree-select/src/pc.vue @@ -0,0 +1,64 @@ + + +