@qynpm/form
| Install | |
|---|---|
npm install @qynpm/form |
|
| Latest Version: | 1.1.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 26, 2026 |
| Links: | npm |
@qynpm/form
基于 Vue 3 + Element Plus 的 schema 驱动表单工具包。
当前提供:
useFormItemQyQueryFormdefineFormModeluseSaveuseSyncQueryParamsconfigureQyForm
安装
pnpm add @qynpm/form
Peer Dependencies:
vue >= 3.4element-plus >= 2.0@element-plus/icons-vue >= 2.0pinia >= 2.0vue-router >= 4.0@qynpm/ui
引入
这是一个以 hooks 为主的工具包,通常按需引入即可:
import {
QyQueryForm,
defineFormModel,
useFormItem,
useSave,
useSyncQueryParams,
configureQyForm
} from '@qynpm/form'
当前稳定导出
构建产物将已有依赖 vuedraggable 外部化,ESM/CJS 消费由宿主 bundler 或 Node 解析包依赖;直接通过 <script> 使用 UMD 时,需先加载它并提供全局变量 vuedraggable。
当前建议作为公共包稳定使用的导出面如下:
| 导出项 | 类型 | 说明 |
|---|---|---|
useFormItem |
hook | schema 表单核心能力 |
QyQueryForm |
component | 查询表单外壳,兼容旧查询表单布局、保存筛选和拖拽排序 |
defineFormModel |
helper | 兼容旧 qy-query-form 的 model 定义方式 |
useSave |
hook | 保存 / 应用筛选条件 |
useSyncQueryParams |
hook | query 参数同步 |
configureQyForm |
function | 宿主全局注册运行时配置 |
说明:
QyQueryForm、defineFormModel可以作为旧查询表单迁移入口使用。configureQyForm目前只承载筛选条件存储扩展,不建议继续塞入其他业务能力。- README 中未列出的内部文件、临时兼容实现,不应作为稳定 API 依赖。
宿主接入
@qynpm/form 默认使用 localStorage 保存筛选条件。
如果宿主项目需要复用旧 useUserCache 一类的“用户级缓存”能力,不建议把接口逻辑直接写死进包内,而是由宿主在启动阶段全局注册一次:
import { configureQyForm, type FormPresetOption } from '@qynpm/form'
configureQyForm({
presetStorage: {
async load(id) {
// 这里接宿主自己的读取逻辑
return [] as FormPresetOption[]
},
async save(id, presets) {
// 这里接宿主自己的保存逻辑
}
}
})
建议接入位置:
main.ts- 项目公共初始化文件,例如
setupQyForm.ts - UI / 表单体系统一入口
这样页面层不需要逐个传 adapter,后续所有 QyQueryForm / useSave 都会走这套全局实现。
迁移页面前,建议先阅读:
设计说明
useFormItem 采用 schema 驱动模式:
- 第一参数
model用来描述字段结构、默认值、控件类型和渲染方式 - 第二参数
options用来描述 query 同步、缓存、紧凑布局、保存能力等 - 内部维护响应式
modelForm - 页面侧通过
formItem/formItems输出表单项 - 提交时通过
modelForm或getModelForm()获取当前值
补充说明:
QyDialog现在通过@qynpm/ui提供,createInstance仍属于包内运行时实现- 当前版本更适合作为体系内拆包使用,还不是完全脱离宿主约定的通用表单库
- 图标能力仍然默认复用宿主现有方案
imageInput继续作为旧 schema 兼容入口,内部由@qynpm/ui/QyRemarkImage承接
快速上手
<template>
<el-form v-bind="form.getFormProps()">
<component :is="form.formItem" prop="shopName" />
<component :is="form.formItem" prop="shopType" />
<el-form-item>
<el-button type="primary" @click="handleSubmit">提交</el-button>
<el-button @click="form.reset">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { useFormItem } from '@qynpm/form'
const form = useFormItem(
[
{
label: '店铺名称',
key: 'shopName',
type: 'string',
rules: [{ required: true, message: '请输入店铺名称', trigger: 'blur' }]
},
{
label: '店铺类型',
key: 'shopType',
type: 'select',
value: 'erp',
options: [
{ label: 'ERP', value: 'erp' },
{ label: 'Production', value: 'production' }
]
}
],
{
id: 'shop-form'
}
)
function handleSubmit() {
console.log(form.getModelForm())
}
</script>
API
useFormItem(model, options?)
model
类型:FormItemSet<Key>[]
| 字段 | 类型 | 说明 |
|---|---|---|
label |
string |
表单项标题 |
key |
string |
字段名,对应 modelForm 的 key |
asKey |
string |
没有 key 时的备用唯一标识 |
type |
'string' | 'number' | 'imageInput' | 'select' | 'date' | 'inputNumber' | 'radio' | 'qyMoneyInput' | 'qyRemoteSelect' | 'qyTreeSelect' | 'qyImageUpload' | 'qyDatePicker' | 'qyInputNumber' |
控件类型 |
value |
any |
默认值 |
props |
Record<string, any> |
透传给控件的 props |
rules |
FormItemRule[] |
Element Plus 校验规则 |
options |
Array | Ref |
select / radio 选项 |
render |
Function |
自定义渲染函数 |
类型映射
@qynpm/form 保留旧 schema 类型,但内部基础组件已经切到 @qynpm/ui:
| schema 类型 | 默认组件 | 说明 |
|---|---|---|
string |
QyInput |
普通文本输入,type="number" 仍按输入框字符串语义处理 |
number |
QyInput |
兼容旧 ElInput 用法,不等同于数字步进器 |
select |
QySelect |
保留旧 options: { ref, label, value } 映射和数字值转换 |
radio |
QyRadioGroup |
保留旧 options 映射,支持普通 radio 和 button radio |
date |
QyDatePicker |
保留 datetimerange 结束时间归一逻辑 |
inputNumber |
QyInputNumber |
普通数字输入,默认值仍为 0 |
imageInput |
QyRemarkImage |
备注文本 + 单图协议,走特殊适配路径 |
显式 qy* 类型只保留语义和旧基础类型明显不同的组件:
| schema 类型 | 默认组件 | 使用场景 |
|---|---|---|
qyMoneyInput |
QyMoneyInput |
金额、费用、税费 |
qyRemoteSelect |
QyRemoteSelect |
远程筛选下拉 |
qyTreeSelect |
QyTreeSelect |
分类、组织、品类等树形筛选;支持 data、label-key、value-key 等树选择参数 |
qyImageUpload |
QyImageUpload |
单图字段;配置 multiple 后用于财审凭证、付款凭证等多图数组 |
qyDatePicker |
QyDatePicker |
需要显式表达 UI 日期组件时使用 |
qyInputNumber |
QyInputNumber |
需要显式表达 UI 数字输入时使用 |
qyTreeSelect 会直接渲染 QyTreeSelect,适合把旧 render + el-tree-select 收敛回 schema:
{
label: '分类',
key: 'cateList',
type: 'qyTreeSelect',
props: {
data: catesList,
'label-key': 'cateName',
'value-key': 'cateProductCode',
multiple: true,
clearable: true,
filterable: true,
'render-after-expand': false
}
}
不再额外暴露 qyInput、qySelect、qyRadioGroup。对应能力已经分别收敛到 string、select、radio,避免 schema API 重复。
imageInput 暂不改名为 remarkImage。当前项目内仍存在老组件 @qiyin/components/remarkImage,而 @qynpm/ui 中也有 QyRemarkImage。为了避免出现第三套命名,form 继续使用旧 schema 名 imageInput 承接备注图文协议。
imageInput 的值仍是旧协议字符串:普通备注文本和单张图片地址合并保存,图片部分通过 [url:xxx] 标记。useFormItem 渲染前会用 parseRemarkText 拆成 defaultValue/defaultImage 传给 QyRemarkImage,组件 change 后再把完整协议字符串写回 modelForm[key]。reset() 会重置 modelForm 并强制重新挂载 imageInput,避免备注组件内部未触发 blur 的临时文字或图片状态残留。
options
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
form |
Record<string, any> |
{} |
初始化表单值,会覆盖 schema 默认值 |
syncQuery |
boolean |
false |
是否和 URL query 同步 |
compact |
boolean |
false |
是否启用紧凑布局 class |
cache |
{ key: string } |
undefined |
启用 localStorage 缓存 |
customSetterGetter |
Record<string, CustomSetterGetter> |
{} |
query 同步时的自定义序列化 / 反序列化 |
showFilter |
boolean |
true |
useSave 是否展示快捷筛选项 |
id |
string |
undefined |
useSave 使用的唯一标识 |
返回值
| 字段 | 类型 | 说明 |
|---|---|---|
model |
FormItemSet[] |
原始 schema |
modelForm |
Reactive<Record<string, any>> |
响应式表单值对象 |
formProps |
ComputedRef |
表单 props 计算结果 |
getModelForm() |
() => Record<string, any> |
获取当前表单值快照;文本字段去除首尾空白(date、qyDatePicker、imageInput 除外),不修改 modelForm |
formItem() |
({ prop | itemKey }) => VNode |
渲染单个表单项 |
formItems() |
(props) => VNode[] |
批量渲染表单项,支持 includes / excludes |
reset() |
() => void |
重置到默认值,并清理缓存 |
getFormProps() |
() => { model, rules } |
返回可直接绑定到 el-form 的 props |
save() |
(buttonProps?) => VNode | null |
保存按钮 |
apply() |
(buttonProps?) => VNode | null |
应用按钮 |
SaveComponent() |
() => JSX.Element |
保存 / 应用按钮组 |
getFormItemVNode() |
(item) => VNode |
获取单个表单项 VNode |
useSave(options)
用于保存和恢复表单条件:
import { useSave } from '@qynpm/form'
const { save, apply, SaveComponent } = useSave({
id: 'query-form',
modelForm,
showFilter: true
})
默认行为:
- 未注册
presetStorage时,使用localStorage - 已注册
presetStorage时,统一走宿主提供的读写实现
QyQueryForm
用于承接旧 qy-query-form 的查询表单外壳能力。
<template>
<QyQueryForm
v-model="queryForm"
:model="model"
:options="{
syncQuery: true,
compact: true,
id: 'query-form'
}"
:query-loading="loading"
label-position="top"
size="small"
item-draggable
@query="handleQuery"
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { QyQueryForm, defineFormModel } from '@qynpm/form'
const loading = ref(false)
const queryForm = ref({})
const model = defineFormModel(
computed(() => [
{ label: '店铺名称', key: 'shopName', type: 'string' },
{ label: '店铺类型', key: 'shopType', type: 'select' }
])
)
function handleQuery() {}
</script>
当前承接的能力包括:
- 查询 / 重置按钮
SaveComponent保存筛选itemDraggable拖拽排序spare / _spare备用字段区域options.id对应的 localStorage 排序缓存
适用场景:
- 页面原来已经使用旧
<qy-query-form> - 页面依赖保存筛选、拖拽排序、备用字段
- 页面迁移时希望尽量少改模板结构
如果页面只需要普通 schema 表单,不需要查询外壳,优先直接使用 useFormItem。
Props
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
model |
FormItemSet[] |
- | 查询表单 schema,支持 spare 和运行时 _spare |
options |
Options |
{} |
透传给内部 useFormItem,其中 options.id 同时用于排序缓存和保存筛选 |
itemDraggable |
boolean |
false |
是否开启拖拽排序和备用字段区域;开启后必须提供 options.id |
queryLoading |
boolean |
false |
查询按钮 loading 状态 |
modelValue |
Record<string, any> |
{} |
查询表单当前值,和内部 modelForm 双向同步。初始化优先级:URL query(syncQuery)> 初始 modelValue > options.form > 缓存 > schema 默认值,值为 undefined 的键视为未传;整体替换为新对象时,新对象缺少的 schema 字段回到 schema 默认值 |
Events
| 事件 | 参数 | 说明 |
|---|---|---|
update:modelValue |
(value: Record<string, any>) |
内部表单值变化时触发 |
query |
() |
点击查询按钮或保存筛选快捷项回填后触发 |
Slots
| 插槽 | 说明 |
|---|---|
default |
追加到查询按钮区域后的自定义内容 |
行为约束
reset只重置表单值,不会额外触发query- 点击“重置排序”会回到 schema 中
spare定义的默认分组 - 已保存排序时,新增字段会优先根据
_spare恢复到 active / spare 区域 itemDraggable开启时,如果缺少options.id,组件会报错提示- 排序缓存使用
localStorage,key 格式为qyQueryForm_${options.id}
defineFormModel(formModel)
兼容旧查询表单的 model 定义方式,本身只做透传。
import { computed } from 'vue'
import { defineFormModel } from '@qynpm/form'
const model = defineFormModel(
computed(() => [
{ label: '关键词', key: 'keyword', type: 'string' }
])
)
作用:
- 让旧页面迁移时保持
defineFormModel(...)写法不变 - 方便后续统一替换旧
@qiyin/components/qyQueryForm入口
configureQyForm(config)
用于给整个包注册运行时配置。当前主要用于接管“保存筛选条件”的存储实现。
import { configureQyForm } from '@qynpm/form'
configureQyForm({
presetStorage: {
async load(id) {
return []
},
async save(id, presets) {}
}
})
presetStorage
类型:
type FormPresetOption = {
name: string
data: string
}
type QyFormPresetStorage = {
load: (cacheId: string) => Promise<FormPresetOption[]> | FormPresetOption[]
save: (
cacheId: string,
presets: FormPresetOption[]
) => Promise<void> | void
}
说明:
load:读取某个筛选组 id 下的全部已保存条件save:整体保存该 id 当前的全部条件,删除场景也通过这里落库- 包内不会要求宿主逐页传入;只需要全局注册一次
useSyncQueryParams(queryParams, options?)
用于查询表单与地址栏联动:
const form = useFormItem(schema, {
syncQuery: true
})
当前保持的行为包括:
- query 初始化回填
- 数组值默认用逗号序列化 / 反序列化
- 日期字段按类型序列化(见下表)
- 尝试同步更新 tagsView 中的 query
URL 中的值无法按字段配置还原时(例如旧版本写出的错误日期链接、超出范围的时间戳),保留字段初始化值,不会覆盖成 undefined 或错误数组;无法序列化的值(例如不完整的日期区间)不写入 URL。
日期字段的 URL 格式
date、qyDatePicker 字段按实际生效的 type 与 valueFormat 选择格式:componentProps 覆盖 props(显式写 undefined 也算覆盖),value-format 与 valueFormat 两种写法均可。
| 字段类型 | valueFormat |
URL 中的值 | 还原为 |
|---|---|---|---|
单值(date、datetime、week、month、year) |
日期格式,如 YYYY-MM-DD |
原值,如 2024-01-05 |
原字符串;必须能按该格式解析,否则视为无效 |
| 单值 | x |
毫秒时间戳 | 数字 |
| 单值 | X |
秒级时间戳 | 字符串 |
| 单值 | 未设置(控件值为 Date) |
毫秒时间戳 | Date |
区间(daterange、datetimerange、monthrange) |
任意 | 两个毫秒时间戳,逗号分隔 | ['YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss'],与 valueFormat 无关,沿用既有格式 |
多选(dates) |
同单值 | 各项按单值规则,逗号分隔;任一项本身含逗号时改用 JSON 数组 | 数组,各项同单值 |
自定义转换
customSetterGetter 中配置的字段优先于以上默认规则。实际调用方式以此为准:get(value) 把表单值转成写入 URL 的字符串,set(text) 把 URL 中的字符串还原为表单值。也可以把字段配置为字符串 'date',表示按日期区间处理。
使用示例
基础表单
<component :is="form.formItem" prop="shopName" />
<component :is="form.formItem" prop="shopType" />
批量渲染
<component :is="form.formItems" />
includes / excludes
<component :is="form.formItems" includes="keyword,status,channel" />
<component :is="form.formItems" excludes="remark" />
query 表单
const queryForm = useFormItem(schema, {
syncQuery: true,
id: 'query-form'
})
启用缓存
const form = useFormItem(schema, {
cache: {
key: 'shop-form-cache'
}
})
自定义渲染
{
label: '自定义字段',
key: 'custom',
type: 'string',
render: (h, { modelForm, Component }) =>
h('div', { class: 'flex gap-2' }, [
Component,
h('span', modelForm.custom || '')
])
}
保存 / 应用按钮
<template>
<component :is="form.SaveComponent" />
</template>
宿主全局注册示例
下面是适合在宿主项目里落地的最小示例:
import { configureQyForm, type FormPresetOption } from '@qynpm/form'
async function getUserPresets(cacheId: string): Promise<FormPresetOption[]> {
// 示例:替换成宿主自己的接口
return []
}
async function saveUserPresets(
cacheId: string,
presets: FormPresetOption[]
) {
// 示例:替换成宿主自己的接口
}
configureQyForm({
presetStorage: {
load: getUserPresets,
save: saveUserPresets
}
})
如果要兼容旧 useUserCache 语义,可以在这个全局注册层里做:
load(id)中调用旧缓存读取接口save(id, presets)中把整份筛选条件数组写回
这样替换旧 qyQueryForm 时,页面本身不需要改调用方式。
样式与运行时说明
- 宿主项目需要引入
element-plus/dist/index.css @qynpm/form当前会自动注入包内样式- 宿主项目需要提供
@element-plus/icons-vue - 如果使用
syncQuery,宿主项目需要接入pinia和vue-router - 包内图标仍然默认复用宿主图标体系
imageInput默认使用/permission-api/common/uploads和Admin-Token,如宿主上传接口不同,需要显式传入api/tokenKey
本地开发
pnpm dev
pnpm build
pnpm dev:启动 playground 预览页pnpm build:构建到dist/
Workspace 联调:
{
"dependencies": {
"@qynpm/form": "workspace:*"
}
}
已知限制
useFormItem和useSave目前仍然是配套关系,后续可以再评估是否继续拆开。createInstance仍在包内,后续可根据拆包策略决定是否继续下沉。- 当前图标能力仍然偏体系内约定。
imageInput是备注图文协议,默认上传接口只适合当前起印体系;跨项目使用时需要确认api/tokenKey。
当前联调说明
当前不再通过手动修改 form / ui 源码 import 来切换联调模式,而是统一使用顶层 Vite 开关控制。
开关位置:
qy-workspace顶层vite.config.ts
当前配置:
const ENABLE_LOCAL_QIYIN_DEBUG = true
规则如下:
- 当值为
true时
@qiyin/form/@qynpm/form映射到本地源码入口 index.ts@qynpm/ui映射到本地源码入口 ../UI/lib/index.js- 适合 workspace 本地联调
- 当值为
false时
- 走各自包的正常入口
- 更接近线上/发包后的使用方式
注意:
- 修改开关后需要重启对应项目的 dev 服务
Related Packages
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.1.0 | ^3.4.21 | MIT | |
| 1.0.7 | ^3.4.21 | MIT | |
| 1.0.6 | ^3.4.21 | MIT | |
| 1.0.5 | ^3.4.21 | MIT | |
| 1.0.4 | ^3.4.21 | MIT | |
| 1.0.3 | ^3.4.21 | MIT | |
| 1.0.2 | ^3.4.21 | MIT | |
| 1.0.1 | ^3.4.21 | MIT | |
| 0.0.3 | ^3.4.21 | MIT |