Halo Console 自定义 FormKit 输入组件完全指南:内置组件、Schema 使用与插件扩展
【免费下载链接】haloHalo 是一款强大易用的开源建站工具,从个人博客、知识库,到企业官网、在线商城,Halo 都能助您轻松实现,一站式满足您的多样化建站需求。项目地址: https://gitcode.com/GitHub_Trending/ha/halo
导读
Halo 的控制台(Console)所有表单都基于 FormKit 构建,但 FormKit 内置的 Input 组件无法覆盖附件选择、菜单选择、密钥管理等建站场景,因此 Halo 在ui/src/formkit中沉淀了一套自定义 FormKit 输入组件体系。本文将完整梳理这些内置组件的类型、参数与使用方式,讲解如何在 Vue SFC 和 FormKit Schema(插件 / 主题设置表单)中调用它们,并深入源码说明插件如何通过formkit.inputs注册自定义输入类型以及 Halo 的冲突处理机制。
为什么 Halo 要自定义 FormKit 输入组件
在 ui/docs/custom-formkit-input/README.md 中明确说明了背景:目前 Console 端的所有表单都使用了 FormKit,但 FormKit 内置的 Input 组件并不满足所有的需求,因此需要自定义一些 Input 组件。此外,为了插件和主题能够更方便地使用系统内的一些数据(如文章、分类、标签、菜单、附件等),同样需要自定义一些带数据的选择组件。
从源码看,这些自定义输入组件集中在 ui/src/formkit/inputs 目录下,并在 ui/src/formkit/inputs/index.ts 中统一导出为builtinFormKitInputs,最终通过 ui/src/formkit/formkit.config.ts 中的inputs: builtinFormKitInputs注入 FormKit 全局配置,使这些类型在整个 Console 与 UC 端(用户中心)可用。
内置自定义输入组件总览
Halo 已提供以下输入类型(来源:ui/src/formkit/inputs/index.ts):
| 类型 | 用途 | 关键参数 |
|---|---|---|
code | 代码编辑器 | language、height |
attachment | 附件选择 | accepts |
repeater | 可视化操作对象集合 | min、max、addLabel、addButton等 |
list | 动态数组列表 | itemType、min、max、addLabel等 |
menuCheckbox | 选择一组菜单 | — |
menuRadio | 选择一个菜单 | — |
menuSelect | 通用菜单选择(单选、多选、排序) | — |
menuItemSelect | 选择菜单项 | — |
postSelect | 选择文章 | — |
singlePageSelect | 选择自定义页面 | — |
categorySelect | 选择分类 | multiple |
categoryCheckbox | 选择多个分类 | — |
tagSelect | 选择标签 | multiple |
tagCheckbox | 选择多个标签 | — |
verificationForm | 远程验证一组数据 | action、label、buttonAttrs |
secret | 选择或管理密钥(Secret) | requiredKeys、descriptionPreset |
select | 通用选择器(单选 / 多选,静态 / 远程动态数据) | options、action、requestOption、remote等 |
从源码看,除上述文档列出的类型外,builtinFormKitInputs还注册了array、color、iconify、password、switch、toggle、roleSelect、userSelect、attachmentInput、attachmentGroupSelect、attachmentPolicySelect等类型(见 ui/src/formkit/inputs/index.ts),可满足颜色、图标、密码、开关、角色、用户等更多场景的表单需求。
输入型组件详解
code:代码编辑器
code组件基于 CodeMirror 实现(见 ui/src/formkit/inputs/code/CodeInput.vue),支持以下参数:
language:编辑器语法高亮语言,目前支持yaml、html、css、javascript、json。height:编辑器高度,如100px。
从 ui/src/formkit/inputs/code/index.ts 的源码看,该组件通过createInput(CodeInput, { type: "input", props: ["height", "language"], forceTypeProp: "textarea" })注册,forceTypeProp: "textarea"表示强制以 textarea 语义提交数据(保留换行与缩进,适合配置类内容)。
CodeInput.vue还内置了全屏编辑能力:点击右下角的全屏按钮后,编辑器会通过 Teleport 固定到视口,并显示带标题的页头工具栏,按Escape键或点击"退出全屏"按钮即可返回(源码见 CodeInput.vue)。
attachment:附件选择
attachment组件用于选择或上传附件,支持一个参数:
accepts:允许上传的文件类型,如image/*。
该组件提供了附件列表、上传、自定义链接等多种交互入口(相关组件见 ui/src/formkit/inputs/attachment 目录),适合在设置表单中让用户选择图片或文件资源。
verificationForm:远程验证
verificationForm用于远程验证一组数据是否符合某个规则,例如验证邮箱、域名所有权等。支持参数:
action:对目标数据进行验证的接口地址。label:验证按钮文本。buttonAttrs:验证按钮的额外属性。
从 ui/src/formkit/inputs/verify-form/index.ts 的源码看,该组件类型为group(type: "group"),内部由verifyInput(验证区块)、messages(消息区)与actions(verificationButton())(动作按钮区)组成,并通过verifyFeature与disablesChildren两个 feature 驱动验证逻辑与子组件禁用状态。其 props 定义为["action", "label", "buttonAttrs"],验证按钮通过bind: "$buttonAttrs"透传额外属性。
secret:密钥选择与管理
secret组件用于选择或者管理 Kubernetes 风格的密钥(Secret),支持参数:
requiredKeys:数组类型,用于确认所需密钥的字段名称,每个元素包含key和help两个属性。descriptionPreset:创建密钥时的备注预设。打开创建弹窗时会预填为备注预设 - 当前时间,用户可编辑后保存。
从 ui/src/formkit/inputs/secret/index.ts 的源码看,该组件通过createInput(defineAsyncComponent(() => import("./SecretSelect.vue")), { type: "input", props: ["requiredKeys", "descriptionPreset"], features: [initialValue, secretFeature] })注册,内部采用异步组件懒加载,配合SecretListModal、SecretCreationModal、SecretEditModal、SecretForm等弹窗组件完成密钥的浏览、创建与编辑(见 ui/src/formkit/inputs/secret/components 目录)。
数据绑定型选择组件
这类组件用于将系统数据(菜单、文章、分类、标签等)直接绑定为表单选项,插件和主题设置表单可以直接引用:
menuCheckbox:选择一组菜单。menuRadio:选择一个菜单。menuSelect:通用菜单选择组件,支持单选、多选、排序。menuItemSelect:选择菜单项。postSelect:选择文章。singlePageSelect:选择自定义页面(单页)。categorySelect:选择分类,参数multiple控制是否多选,默认为false。categoryCheckbox:选择多个分类。tagSelect:选择标签,参数multiple控制是否多选,默认为false。tagCheckbox:选择多个标签。
对应的实现文件位于 ui/src/formkit/inputs 目录下,例如menu-checkbox.ts、menu-radio.ts、menu-select.ts、menu-item-select.ts、post-select.ts、singlePage-select.ts、category-checkbox.ts、category-select、tag-checkbox.ts、tag-select等。这些组件封装了对应的 Console API 请求,插件与主题无需关心数据获取细节即可在设置表单中使用系统数据。
集合 / 数组操作组件:list 与 repeater
list:动态数组列表
list组件定义一个数组列表,让使用者可视化地添加、删除、上移、下移、插入数组项。支持参数:
itemType:列表项的数据类型,用于初始化数据类型,可选string、number、boolean、object,默认为string。min:最小数量,默认为0。max:最大数量,默认为Infinity,即无限制。addLabel:添加按钮的文本,默认为添加。addButton:是否显示添加按钮,默认为true。upControl:是否显示上移按钮,默认为true。downControl:是否显示下移按钮,默认为true。insertControl:是否显示插入按钮,默认为true。removeControl:是否显示删除按钮,默认为true。
从 ui/src/formkit/inputs/list/index.ts 的源码看,list的类型为type: "list",props 包含min、max、upControl、downControl、removeControl、insertControl、addLabel、addButton、itemType,并通过features: [lists, disablesChildren, renamesRadios]实现列表操作逻辑。
在 Vue SFC 中使用的示例:
<script lang="ts" setup> const users = ref([]); </script> <template> <FormKit :min="1" :max="3" type="list" label="Users" add-label="Add User" item-type="string" > <template #default="{ index }"> <FormKit type="text" :index="index" validation="required" /> </template> </FormKit> </template>在 FormKit Schema(YAML)中使用的示例:
- $formkit: list name: users label: Users addLabel: Add User min: 1 max: 3 itemType: string children: - $formkit: text index: "$index" validation: required[!NOTE]
list组件有且只有一个子节点,并且必须为子节点传递index属性。若想提供多个字段,则建议使用group组件包裹。
最终得到的数据类似于:
{ "users": [ "Jack", "John" ] }repeater:对象集合
repeater组件定义一个对象集合,让使用者可视化地操作集合。支持参数与list基本一致:
min:最小数量,默认为0。max:最大数量,默认为Infinity,即无限制。addLabel:添加按钮的文本,默认为添加。addButton:是否显示添加按钮,默认为true。upControl:是否显示上移按钮,默认为true。downControl:是否显示下移按钮,默认为true。insertControl:是否显示插入按钮,默认为true。removeControl:是否显示删除按钮,默认为true。
在 Vue SFC 中使用的示例:
<script lang="ts" setup> const users = ref([]); </script> <template> <FormKit v-model="users" :min="1" :max="3" addLabel="Add User" type="repeater" label="Users" > <FormKit type="text" label="Full Name" name="full_name" validation="required" /> <FormKit type="email" label="Email" name="email" validation="required|email" /> </FormKit> </template>在 FormKit Schema 中使用的示例:
- $formkit: repeater name: users label: Users addLabel: Add User min: 1 max: 3 items: - $formkit: text name: full_name label: Full Name validation: required - $formkit: email name: email label: Email validation: required|email最终得到的数据类似于:
[ { "full_name": "Jack", "email": "jack@example.com" }, { "full_name": "John", "email": "john@example.com" } ][!NOTE] 从 ui/src/formkit/inputs/repeater/index.ts 的源码注释可以看到,
repeater已被标记为@deprecated,官方推荐使用array输入组件替代。array同样注册在builtinFormKitInputs中(见 ui/src/formkit/inputs/index.ts),新开发的插件 / 主题设置表单建议优先考虑array。
select:通用选择器组件
select是 Halo 自定义的选择器组件,用于在备选项中选择一个或多个选项,同时支持静态数据及远程动态数据加载。选项对象至少需要包含label与value,此外还可以提供:
icon:图标图片地址,会以<img>渲染。description:显示在label下方的说明文字,同时参与本地静态选项搜索。attrs:附加属性,其中attrs.disabled可置灰该选项(见 ui/src/formkit/inputs/select/types.ts)。
参数说明
options:静态数据源。当action或remote存在时,此参数无效。action:远程动态数据源的接口地址。requestOption:动态数据源的请求参数,通过此参数指定如何获取数据,适配不同的接口。当action存在时有效。remote:标识当前是否由用户自定义的远程数据源。remoteOption:当remote为true时,此配置项必须存在,用于为 Select 组件提供处理搜索及查询键值对的方法(需实现search与findOptionsByValues)。remoteOptimize:是否开启远程数据源优化,默认为true。开启后会对远程数据源进行优化,减少请求次数。仅在动态数据源下有效。allowCreate:是否允许创建新选项,默认为false。仅在静态数据源下有效,需要同时开启searchable。clearable:是否允许清空选项,默认为false。multiple:是否多选,默认为false。maxCount:多选时最大可选数量,默认为Infinity。仅在多选时有效。sortable:是否支持拖动排序,默认为false。仅在多选时有效。searchable:是否支持搜索内容,默认为false。autoSelect:当 value 不存在时,是否自动选择第一个选项,默认为true。仅在单选时有效。
以上 props 在 ui/src/formkit/inputs/select/index.ts 的类型定义中均有对应声明;SelectActionRequest(即requestOption的类型定义)的字段及默认值详见 ui/src/formkit/inputs/select/types.ts,包括pageField(默认page)、sizeField(默认size)、totalField(默认total)、itemsField(默认items)、labelField(默认label)、valueField(默认value)、iconField、descriptionField、fieldSelectorKey(默认metadata.name)等。
静态数据源:在 Vue SFC 中以组件形式使用
<script lang="ts" setup></script> <template> <FormKit type="select" label="What country makes the best food?" name="countries" placeholder="Select a country" allow-create clearable sortable multiple searchable :options="[ { label: 'China', value: 'China', icon: '/assets/flags/cn.svg', description: 'Chinese cuisine with rich regional styles', }, { label: 'USA', value: 'USA', icon: '/assets/flags/us.svg', description: 'American cuisine with diverse influences', }, { label: 'Japan', value: 'Japan' }, { label: 'Korea', value: 'Korea' }, { label: 'France', value: 'France' }, { label: 'Italy', value: 'Italy' }, { label: 'Germany', value: 'Germany' }, { label: 'UK', value: 'UK' }, { label: 'Canada', value: 'Canada' }, { label: 'Australia', value: 'Australia' }, ]" help="Don’t worry, you can’t get this one wrong." /> </template>远程动态数据源(remote):自定义请求逻辑
当后端接口无法直接通过requestOption的字段映射适配时,可以使用remote模式,自行实现search与findOptionsByValues方法。search接收{ keyword, page, size }参数并返回{ options, total, page, size };findOptionsByValues根据默认选项的值查询对应选项数据(用于回显,类型定义见 ui/src/formkit/inputs/select/types.ts)。示例中通过consoleApiClient.user.listUsers加载用户列表,同时过滤掉系统内置的anonymousUser与ghost用户:
<script lang="ts" setup> const ANONYMOUSUSER_NAME = "anonymousUser"; const DELETEDUSER_NAME = "ghost"; const handleSelectPostAuthorRemote = { search: async ({ keyword, page, size }) => { const { data } = await consoleApiClient.user.listUsers({ page, size, keyword, fieldSelector: [ `name!=${ANONYMOUSUSER_NAME}`, `name!=${DELETEDUSER_NAME}`, ], }); return { options: data.items.map((item) => ({ label: item.user.spec.displayName, value: item.user.metadata.name, icon: item.user.spec.avatar, description: item.user.spec.email, })), total: data.total, page: data.page, size: data.size, }; }, findOptionsByValues: () => { return []; }, }; </script> <template> <FormKit type="select" label="The author of the post is?" name="post_author" placeholder="Select a user" searchable remote :remote-option="handleSelectPostAuthorRemote" /> </template>静态数据源:在 FormKit Schema 中使用
- $formkit: select name: countries label: What country makes the best food? sortable: true multiple: true clearable: true placeholder: Select a country options: - label: China value: cn icon: /assets/flags/cn.svg description: Chinese cuisine with rich regional styles - label: France value: fr icon: /assets/flags/fr.svg description: French cuisine and bakery classics - label: Germany value: de - label: Spain value: es - label: Italy value: ie - label: Greece value: gr远程动态数据源:在 FormKit Schema 中使用
select支持通过action和requestOption参数指定远程数据获取方式。请求的接口会自动拼接page、size与keyword参数,其中keyword为搜索关键词。
- $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 iconField: post.spec.cover descriptionField: post.status.excerpt fieldSelectorKey: metadata.name[!NOTE] 当远程数据具有分页时,可能会出现默认选项不在第一页的情况,此时 Select 组件将会发送另一个查询请求,以获取默认选项的数据。此接口会携带如下参数
fieldSelector: ${requestOption.fieldSelectorKey}=(value1,value2,value3)。其中,value1, value2, value3 为默认选项的值。返回值与查询一致,通过
requestOption解析。
使用方式:Vue SFC 与 FormKit Schema
Halo 的自定义输入组件支持两种使用方式,与原生 FormKit 用法一致。
在 Vue 单组件中使用
<script lang="ts" setup> const postName = ref(""); </script> <template> <FormKit v-model="postName" placeholder="请选择文章" label="文章" type="postSelect" validation="required" /> </template>在 FormKit Schema 中使用(插件 / 主题设置表单定义)
- $formkit: menuRadio name: menus label: 底部菜单组这是插件与主题设置表单最常用的声明方式:只需在 Schema(YAML)中声明$formkit类型与name、label等属性,FormKit 就会自动渲染对应的自定义输入组件,并纳入表单校验与数据收集流程。
插件扩展 FormKit 输入组件
注册机制
插件可以通过 UI 入口中的formkit.inputs注册自己的 FormKit 输入类型。注册后的类型可以在插件的 FormKit Schema 中通过$formkit使用,并会走 FormKit 的输入生命周期。
import { createInput } from "@formkit/vue"; import { definePlugin } from "@halo-dev/ui-shared"; import { defineAsyncComponent } from "vue"; export default definePlugin({ formkit: { inputs: { myPluginInput: createInput( defineAsyncComponent(() => import("./MyPluginInput.vue")) ), }, }, });- $formkit: myPluginInput name: customField label: 自定义字段formkit.inputs仅支持同步对象。如果输入组件需要懒加载,可以在输入定义内部使用defineAsyncComponent(上述示例即采用该方式),这一点与 Halo 内置secret组件使用defineAsyncComponent懒加载的实现思路一致(见 ui/src/formkit/inputs/secret/index.ts)。
冲突处理与命名建议
如果插件注册的输入类型名称与 Halo 内置类型或更早加载的插件类型重复,Halo 会保留已有类型,跳过冲突的插件类型并在控制台输出警告。
从 ui/src/formkit/plugin-inputs.ts 的源码可以看到,Halo 通过collectPluginFormKitInputs函数完成这一合并逻辑:它先将 Halo 内置类型(registeredInputs)记入inputOwners映射,然后逐个遍历插件模块的formkit.inputs;当某个输入名称已存在所有者(内置类型或更早的插件)时,会输出形如Skipped FormKit input "xxx" from plugin "yyy" because it conflicts with zzz.的警告并跳过该类型(见 ui/src/formkit/plugin-inputs.ts)。同时,collectPluginFormKitInputs还会校验输入定义是否合法(必须是包含type且带有schema或component的记录),非法定义同样会被跳过并输出警告(见 ui/src/formkit/plugin-inputs.ts)。
在运行时,该函数由 ui/src/setup/setupModules.ts 在插件模块注册完成后调用,将收集到的插件输入与内置输入合并后交给setupComponents注入应用。
因此建议插件使用带插件标识的类型名称(例如myPluginInput),以降低与 Halo 内置类型或其他插件冲突的概率。
从源码理解整体架构
Halo 的自定义 FormKit 输入体系可以概括为三层结构:
输入定义层:
ui/src/formkit/inputs目录下的每个子目录对应一类输入组件,通过FormKitTypeDefinition(Schema 式定义)或createInput(组件式定义)导出,最终汇总到 ui/src/formkit/inputs/index.ts 的builtinFormKitInputs。配置注入层:ui/src/formkit/formkit.config.ts 将
builtinFormKitInputs注入 FormKit 的inputs配置,同时启用了一系列插件:radioAlt(radio 备选)、stopImplicitSubmission(阻止隐式提交)、passwordPreventAutocomplete(阻止密码自动填充)、requiredAsterisk(必填星号)、autoScrollToErrors(错误自动滚动)以及createAutoHeightTextareaPlugin(textarea 自动高度),并为中英文(zh、en)配置了 i18n,默认语言为zh。插件扩展层:ui/src/setup/setupModules.ts 在插件注册流程中调用 collectPluginFormKitInputs,将插件通过
formkit.inputs声明的类型与内置类型合并;同时 ui/src/setup/setupFormKitRuntime.ts 会将@formkit/core与@formkit/vue暴露到全局运行时,供 UI 插件(ESM 模块)使用 FormKit API 注册与渲染自定义输入。
对插件与主题开发者而言,这意味着:无需修改 Halo 源码,只需要在插件 UI 入口声明formkit.inputs,并在主题 / 插件设置表单的 Schema 中引用即可获得完整的 FormKit 生命周期(校验、联动、值收集、i18n 等)支持。
结语
Halo Console 的自定义 FormKit 输入组件体系,既为 Console 自身的复杂表单(如设置、菜单、分类、密钥管理等)提供了统一的能力底座,也为插件与主题开发者提供了一套可复用、可扩展的表单基础设施。开发者既可以直接在 FormKit Schema 中零成本使用postSelect、categorySelect、secret等内置类型,也可以通过formkit.inputs注册自己的输入组件并遵循冲突处理约定,从而构建出与 Halo 原生表单体验一致的自定义配置界面。
【免费下载链接】haloHalo 是一款强大易用的开源建站工具,从个人博客、知识库,到企业官网、在线商城,Halo 都能助您轻松实现,一站式满足您的多样化建站需求。项目地址: https://gitcode.com/GitHub_Trending/ha/halo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考