diff --git a/ui/docs/custom-formkit-input/README.md b/ui/docs/custom-formkit-input/README.md index 50e48dbb6..3cd9a0a89 100644 --- a/ui/docs/custom-formkit-input/README.md +++ b/ui/docs/custom-formkit-input/README.md @@ -57,6 +57,20 @@ - `secret`: 用于选择或者管理密钥(Secret) - 参数 1. requiredKey:用于确认所需密钥的字段名称 +- `select`: 自定义的选择器组件,用于在备选项中选择一个或多个选项 + - 参数 + 1. `options`:静态数据源。当 `action` 或 `remote` 存在时,此参数无效。 + 2. `action`:远程动态数据源的接口地址。 + 3. `requestOption`: 动态数据源的请求参数,可以通过此参数来指定如何获取数据,适配不同的接口。当 `action` 存在时,此参数有效。 + 4. `remote`:标识当前是否由用户自定义的远程数据源。 + 5. `remoteOption`:当 `remote` 为 `true` 时,此配置项必须存在,用于为 Select 组件提供处理搜索及查询键值对的方法。 + 6. `remoteOptimize`:是否开启远程数据源优化,默认为 `true`。开启后,将会对远程数据源进行优化,减少请求次数。仅在动态数据源下有效。 + 7. `allowCreate`:是否允许创建新选项,默认为 `false`。仅在静态数据源下有效。 + 8. `clearable`:是否允许清空选项,默认为 `false`。 + 9. `multiple`:是否多选,默认为 `false`。 + 10. `maxCount`:多选时最大可选数量,默认为 `Infinity`。仅在多选时有效。 + 11. `sortable`:是否支持拖动排序,默认为 `false`。仅在多选时有效。 + 12. `searchable`:是否支持搜索内容,默认为 `false`。 在 Vue 单组件中使用: @@ -84,6 +98,143 @@ const postName = ref(""); label: 底部菜单组 ``` +### select + +select 是一个选择器类型的输入组件,使用者可以从一批待选数据中选择一个或多个选项。它支持单选、多选操作,并且支持静态数据及远程动态数据加载等多种方式。 + +#### 在 Vue SFC 中以组件形式使用 + +静态数据源: + +```vue + + + + +``` + +动态数据源: + +```vue + + + + +``` + +#### 在 FormKit Schema 中使用 + +静态数据源: + +```yaml +- $formkit: select + name: countries + label: What country makes the best food? + sortable: true + multiple: true + clearable: true + placeholder: Select a country + options: + - label: France + value: fr + - label: Germany + value: de + - label: Spain + value: es + - label: Italy + value: ie + - label: Greece + value: gr +``` + +远程动态数据源: + +支持远程动态数据源,通过 `action` 和 `requestOption` 参数来指定如何获取数据。 + +请求的接口将会自动拼接 `page`、`size` 与 `keyword` 参数,其中 `keyword` 为搜索关键词。 + +```yaml +- $formkit: select + name: postName + label: Choose an post + clearable: true + action: /apis/api.console.halo.run/v1alpha1/posts + requestOption: + method: GET + pageField: page + sizeField: size + totalField: total + itemsField: items + labelField: post.spec.title + valueField: post.metadata.name +``` + +> [!NOTE] +> 当远程数据具有分页时,可能会出现默认选项不在第一页的情况,此时 Select 组件将会发送另一个查询请求,以获取默认选项的数据。此接口会携带如下参数 `labelSelector: ${requestOption.valueField}=in(value1,value2,value3)`。 + +> 其中,value1, value2, value3 为默认选项的值。返回值与查询一致,通过 `requestOption` 解析。 + ### list list 是一个数组类型的输入组件,可以让使用者可视化的操作数组。它支持动态添加、删除、上移、下移、插入数组项等操作。 diff --git a/ui/package.json b/ui/package.json index f76deb68a..6c5af2e39 100644 --- a/ui/package.json +++ b/ui/package.json @@ -52,6 +52,7 @@ "@codemirror/view": "^6.5.1", "@emoji-mart/data": "^1.2.1", "@formkit/core": "^1.5.9", + "@formkit/drag-and-drop": "^0.1.6", "@formkit/i18n": "^1.5.9", "@formkit/inputs": "^1.5.9", "@formkit/themes": "^1.5.9", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 9e1d9f461..e432c819d 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@formkit/core': specifier: ^1.5.9 version: 1.5.9 + '@formkit/drag-and-drop': + specifier: ^0.1.6 + version: 0.1.6 '@formkit/i18n': specifier: ^1.5.9 version: 1.5.9 @@ -2351,6 +2354,9 @@ packages: '@formkit/dev@1.5.9': resolution: {integrity: sha512-aeD53iH6WD/3jKiYyGmZgvocGQv77NHHD4MF5+I/DvApu0IP1gTArsmBFaBDEVr7t5o/xO2zH06Up7sJcA0+mA==} + '@formkit/drag-and-drop@0.1.6': + resolution: {integrity: sha512-wZyxvk7WTbQ12q8ZGvLoYner1ktBOUf+lCblJT3P0LyqpjGCKTfQMKJtwToKQzJgTbhvow4LBu+yP92Mux321w==} + '@formkit/i18n@1.5.9': resolution: {integrity: sha512-4FVqE1YciXSwl2KUuGRvpizZXBnwZACVRMrNjSn2WokVsOPYdmgwP1+35nG6LVU6i8bcOv/8fASCLUO3ADe7mw==} @@ -12685,6 +12691,8 @@ snapshots: '@formkit/core': 1.5.9 '@formkit/utils': 1.5.9 + '@formkit/drag-and-drop@0.1.6': {} + '@formkit/i18n@1.5.9': dependencies: '@formkit/core': 1.5.9 diff --git a/ui/src/formkit/formkit.config.ts b/ui/src/formkit/formkit.config.ts index 868b760a2..0428c4561 100644 --- a/ui/src/formkit/formkit.config.ts +++ b/ui/src/formkit/formkit.config.ts @@ -23,6 +23,8 @@ import { singlePageSelect } from "./inputs/singlePage-select"; import { tagCheckbox } from "./inputs/tag-checkbox"; import { tagSelect } from "./inputs/tag-select"; import { verificationForm } from "./inputs/verify-form"; +import { select as nativeSelect } from "@formkit/inputs"; +import { select } from "./inputs/select"; import theme from "./theme"; import { userSelect } from "./inputs/user-select"; @@ -67,6 +69,8 @@ const config: DefaultConfigOptions = { tagSelect, verificationForm, userSelect, + nativeSelect, + select, }, locales: { zh, en }, locale: "zh", diff --git a/ui/src/formkit/inputs/attachment-group-select.ts b/ui/src/formkit/inputs/attachment-group-select.ts index 76e2dceba..59c5d29e2 100644 --- a/ui/src/formkit/inputs/attachment-group-select.ts +++ b/ui/src/formkit/inputs/attachment-group-select.ts @@ -21,6 +21,6 @@ function optionsHandler(node: FormKitNode) { export const attachmentGroupSelect: FormKitTypeDefinition = { ...select, props: ["placeholder"], - forceTypeProp: "select", + forceTypeProp: "nativeSelect", features: [optionsHandler, selects, defaultIcon("select", "select")], }; diff --git a/ui/src/formkit/inputs/attachment-policy-select.ts b/ui/src/formkit/inputs/attachment-policy-select.ts index 97d495c73..fb85f3411 100644 --- a/ui/src/formkit/inputs/attachment-policy-select.ts +++ b/ui/src/formkit/inputs/attachment-policy-select.ts @@ -18,6 +18,6 @@ function optionsHandler(node: FormKitNode) { export const attachmentPolicySelect: FormKitTypeDefinition = { ...select, props: ["placeholder"], - forceTypeProp: "select", + forceTypeProp: "nativeSelect", features: [optionsHandler, selects, defaultIcon("select", "select")], }; diff --git a/ui/src/formkit/inputs/menu-item-select.ts b/ui/src/formkit/inputs/menu-item-select.ts index b0aba46e8..1f089f271 100644 --- a/ui/src/formkit/inputs/menu-item-select.ts +++ b/ui/src/formkit/inputs/menu-item-select.ts @@ -20,6 +20,6 @@ function optionsHandler(node: FormKitNode) { export const menuItemSelect: FormKitTypeDefinition = { ...select, props: ["placeholder", "menuItems"], - forceTypeProp: "select", + forceTypeProp: "nativeSelect", features: [optionsHandler, selects, defaultIcon("select", "select")], }; diff --git a/ui/src/formkit/inputs/post-select.ts b/ui/src/formkit/inputs/post-select.ts index bfa08853d..65ca8d77e 100644 --- a/ui/src/formkit/inputs/post-select.ts +++ b/ui/src/formkit/inputs/post-select.ts @@ -24,6 +24,6 @@ function optionsHandler(node: FormKitNode) { export const postSelect: FormKitTypeDefinition = { ...select, props: ["placeholder"], - forceTypeProp: "select", + forceTypeProp: "nativeSelect", features: [optionsHandler, selects, defaultIcon("select", "select")], }; diff --git a/ui/src/formkit/inputs/role-select.ts b/ui/src/formkit/inputs/role-select.ts index ff98269c4..717a80e26 100644 --- a/ui/src/formkit/inputs/role-select.ts +++ b/ui/src/formkit/inputs/role-select.ts @@ -35,6 +35,6 @@ function optionsHandler(node: FormKitNode) { export const roleSelect: FormKitTypeDefinition = { ...select, props: ["placeholder"], - forceTypeProp: "select", + forceTypeProp: "nativeSelect", features: [optionsHandler, selects, defaultIcon("select", "select")], }; diff --git a/ui/src/formkit/inputs/select/MultipleOverflow.vue b/ui/src/formkit/inputs/select/MultipleOverflow.vue new file mode 100644 index 000000000..6b239cdd6 --- /dev/null +++ b/ui/src/formkit/inputs/select/MultipleOverflow.vue @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/formkit/inputs/select/MultipleOverflowItem.vue b/ui/src/formkit/inputs/select/MultipleOverflowItem.vue new file mode 100644 index 000000000..c768d49a3 --- /dev/null +++ b/ui/src/formkit/inputs/select/MultipleOverflowItem.vue @@ -0,0 +1,9 @@ + + + + + + + diff --git a/ui/src/formkit/inputs/select/MultipleSelect.vue b/ui/src/formkit/inputs/select/MultipleSelect.vue new file mode 100644 index 000000000..9fa8ec28c --- /dev/null +++ b/ui/src/formkit/inputs/select/MultipleSelect.vue @@ -0,0 +1,85 @@ + + + + + + + + {{ option.label }} + + + + + diff --git a/ui/src/formkit/inputs/select/MultipleSelectItem.vue b/ui/src/formkit/inputs/select/MultipleSelectItem.vue new file mode 100644 index 000000000..368d69649 --- /dev/null +++ b/ui/src/formkit/inputs/select/MultipleSelectItem.vue @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/ui/src/formkit/inputs/select/MultipleSelectSearchInput.vue b/ui/src/formkit/inputs/select/MultipleSelectSearchInput.vue new file mode 100644 index 000000000..90322d372 --- /dev/null +++ b/ui/src/formkit/inputs/select/MultipleSelectSearchInput.vue @@ -0,0 +1,69 @@ + + + + + + + + {{ inputMirrorValue }} + + + + + diff --git a/ui/src/formkit/inputs/select/MultipleSelectSelector.vue b/ui/src/formkit/inputs/select/MultipleSelectSelector.vue new file mode 100644 index 000000000..ef8d507fe --- /dev/null +++ b/ui/src/formkit/inputs/select/MultipleSelectSelector.vue @@ -0,0 +1,113 @@ + + + + emit('deleteItem', index, option)" + @sort="(options) => emit('sort', options)" + /> + + + + + + + {{ placeholder }} + + + + + + diff --git a/ui/src/formkit/inputs/select/SelectContainer.vue b/ui/src/formkit/inputs/select/SelectContainer.vue new file mode 100644 index 000000000..3ca131dfe --- /dev/null +++ b/ui/src/formkit/inputs/select/SelectContainer.vue @@ -0,0 +1,311 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/formkit/inputs/select/SelectDropdownContainer.vue b/ui/src/formkit/inputs/select/SelectDropdownContainer.vue new file mode 100644 index 000000000..1a3438017 --- /dev/null +++ b/ui/src/formkit/inputs/select/SelectDropdownContainer.vue @@ -0,0 +1,102 @@ + + + + + + + + + emit('selected', option)" + @load-more="handleLoadMore" + > + + + + + + diff --git a/ui/src/formkit/inputs/select/SelectMain.vue b/ui/src/formkit/inputs/select/SelectMain.vue new file mode 100644 index 000000000..99948f8f2 --- /dev/null +++ b/ui/src/formkit/inputs/select/SelectMain.vue @@ -0,0 +1,654 @@ + + + + diff --git a/ui/src/formkit/inputs/select/SelectOption.vue b/ui/src/formkit/inputs/select/SelectOption.vue new file mode 100644 index 000000000..d09a2d989 --- /dev/null +++ b/ui/src/formkit/inputs/select/SelectOption.vue @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + diff --git a/ui/src/formkit/inputs/select/SelectOptionItem.vue b/ui/src/formkit/inputs/select/SelectOptionItem.vue new file mode 100644 index 000000000..bb15d9579 --- /dev/null +++ b/ui/src/formkit/inputs/select/SelectOptionItem.vue @@ -0,0 +1,11 @@ + + + + + {{ option.label }} + + diff --git a/ui/src/formkit/inputs/select/SelectSearchInput.vue b/ui/src/formkit/inputs/select/SelectSearchInput.vue new file mode 100644 index 000000000..0f1f6490d --- /dev/null +++ b/ui/src/formkit/inputs/select/SelectSearchInput.vue @@ -0,0 +1,40 @@ + + + + + + emit('search', inputValue, event)" + /> + + + + + + diff --git a/ui/src/formkit/inputs/select/SelectSelector.vue b/ui/src/formkit/inputs/select/SelectSelector.vue new file mode 100644 index 000000000..34611d685 --- /dev/null +++ b/ui/src/formkit/inputs/select/SelectSelector.vue @@ -0,0 +1,56 @@ + + + + + + + {{ selectLabel || placeholder }} + + + + diff --git a/ui/src/formkit/inputs/select/index.ts b/ui/src/formkit/inputs/select/index.ts new file mode 100644 index 000000000..3b2f7553d --- /dev/null +++ b/ui/src/formkit/inputs/select/index.ts @@ -0,0 +1,62 @@ +import type { FormKitTypeDefinition } from "@formkit/core"; +import { + help, + icon, + inner, + label, + message, + messages, + outer, + prefix, + suffix, + wrapper, +} from "@formkit/inputs"; +import SelectMain from "./SelectMain.vue"; +import { SelectSection } from "./sections/index"; + +/** + * Custom Select component. + * + * features: + * + * 1. Supports both single and multiple selection, controlled by the `multiple` prop. The display format of the input differs between single and multiple selection modes. + * 2. Supports passing in an `options` prop to render dropdown options, or using the `action` prop to pass a function for dynamically retrieving options. The handling differs based on the method: + * a. If `options` is passed, it will be used directly to render the dropdown options. + * b. If the `action` prop is provided, it should be used to fetch options from an API, and additional features like pagination and search may also be enabled. + * 3. Supports sorting functionality. If sorting is enabled, the list allows drag-and-drop sorting, and the current position of the node can be displayed in the dropdown. + * 4. Supports an add feature. If the target content is not in the list, it allows adding the currently entered content as a `value`. + * 5. Allows restricting the maximum number of selections, controlled by the `maxCount` prop. + */ +export const select: FormKitTypeDefinition = { + schema: outer( + wrapper( + label("$label"), + inner(icon("prefix"), prefix(), SelectSection(), suffix(), icon("suffix")) + ), + help("$help"), + messages(message("$message.value")) + ), + + type: "input", + + props: [ + "clearable", + "multiple", + "maxCount", + "sortable", + "action", + "requestOption", + "placeholder", + "remote", + "remoteOption", + "allowCreate", + "remoteOptimize", + "searchable", + ], + + library: { + SelectMain: SelectMain, + }, + + schemaMemoKey: "custom-select", +}; diff --git a/ui/src/formkit/inputs/select/sections/index.ts b/ui/src/formkit/inputs/select/sections/index.ts new file mode 100644 index 000000000..6ce728a1a --- /dev/null +++ b/ui/src/formkit/inputs/select/sections/index.ts @@ -0,0 +1,8 @@ +import { createSection } from "@formkit/inputs"; + +export const SelectSection = createSection("SelectMain", () => ({ + $cmp: "SelectMain", + props: { + context: "$node.context", + }, +})); diff --git a/ui/src/formkit/inputs/singlePage-select.ts b/ui/src/formkit/inputs/singlePage-select.ts index 3e6ee27cd..2073589ef 100644 --- a/ui/src/formkit/inputs/singlePage-select.ts +++ b/ui/src/formkit/inputs/singlePage-select.ts @@ -24,6 +24,6 @@ function optionsHandler(node: FormKitNode) { export const singlePageSelect: FormKitTypeDefinition = { ...select, props: ["placeholder"], - forceTypeProp: "select", + forceTypeProp: "nativeSelect", features: [optionsHandler, selects, defaultIcon("select", "select")], }; diff --git a/ui/src/formkit/inputs/user-select.ts b/ui/src/formkit/inputs/user-select.ts index 8d264766a..83a2d19b8 100644 --- a/ui/src/formkit/inputs/user-select.ts +++ b/ui/src/formkit/inputs/user-select.ts @@ -21,6 +21,6 @@ function optionsHandler(node: FormKitNode) { export const userSelect: FormKitTypeDefinition = { ...select, props: ["placeholder"], - forceTypeProp: "select", + forceTypeProp: "nativeSelect", features: [optionsHandler, selects, defaultIcon("select", "select")], }; diff --git a/ui/src/formkit/theme.ts b/ui/src/formkit/theme.ts index 499ae0231..19f38ddeb 100644 --- a/ui/src/formkit/theme.ts +++ b/ui/src/formkit/theme.ts @@ -25,6 +25,13 @@ const buttonClassification = { "bg-blue-500 hover:bg-blue-700 text-white text-sm font-normal py-3 px-5 rounded", }; +const selectClassification = { + label: textClassification.label, + wrapper: textClassification.wrapper, + inner: + "group/select py-0.5 min-h-[36px] inline-flex items-center w-full relative box-border border border-gray-300 formkit-invalid:border-red-500 rounded-base overflow-hidden focus-within:border-primary focus-within:shadow-sm w-full sm:max-w-lg transition-all", +}; + const theme: Record> = { global: { form: "divide-y divide-gray-100", @@ -64,7 +71,8 @@ const theme: Record> = { "form-range appearance-none w-full h-2 p-0 bg-gray-200 rounded-full focus:outline-none focus:ring-0 focus:shadow-none", }, search: textClassification, - select: textClassification, + select: selectClassification, + nativeSelect: textClassification, submit: buttonClassification, tel: textClassification, text: textClassification, diff --git a/ui/src/locales/en.yaml b/ui/src/locales/en.yaml index 72b365769..08c63a95f 100644 --- a/ui/src/locales/en.yaml +++ b/ui/src/locales/en.yaml @@ -1705,6 +1705,8 @@ core: content_cache: toast_recovered: Recovered unsaved content from cache formkit: + select: + no_data: No data category_select: creation_label: Create {text} category tag_select: diff --git a/ui/src/locales/zh-CN.yaml b/ui/src/locales/zh-CN.yaml index 038e4c731..b1c6fd93d 100644 --- a/ui/src/locales/zh-CN.yaml +++ b/ui/src/locales/zh-CN.yaml @@ -1617,6 +1617,8 @@ core: content_cache: toast_recovered: 已从缓存中恢复未保存的内容 formkit: + select: + no_data: 暂无数据 category_select: creation_label: 创建 {text} 分类 tag_select: diff --git a/ui/src/locales/zh-TW.yaml b/ui/src/locales/zh-TW.yaml index 50271eb0b..bdc18ee88 100644 --- a/ui/src/locales/zh-TW.yaml +++ b/ui/src/locales/zh-TW.yaml @@ -1574,6 +1574,8 @@ core: content_cache: toast_recovered: 已從緩存中恢復未保存的內容 formkit: + select: + no_data: 暫無數據 category_select: creation_label: 創建 {text} 分類 tag_select: