npm package: @qynpm/form
Description: 基于 Vue 3 和 Element Plus 的 schema 表单工具包。
Latest version: 0.0.3

# @qynpm/form

基于 Vue 3 + Element Plus 的 schema 驱动表单工具包。

当前提供：

- `useFormItem`
- `useSave`
- `useSyncQueryParams`

## 安装

```bash
pnpm add @qynpm/form
```

Peer Dependencies：

- `vue >= 3.4`
- `element-plus >= 2.0`
- `@element-plus/icons-vue >= 2.0`
- `pinia >= 2.0`
- `vue-router >= 4.0`
- `@qynpm/ui >= 1.0.0`

## 引入

这是一个以 hooks 为主的工具包，通常按需引入即可：

```ts
import { useFormItem, useSave, useSyncQueryParams } from '@qynpm/form'
```

## 设计说明

`useFormItem` 采用 schema 驱动模式：

- 第一参数 `model` 用来描述字段结构、默认值、控件类型和渲染方式
- 第二参数 `options` 用来描述 query 同步、缓存、紧凑布局、保存能力等
- 内部维护响应式 `modelForm`
- 页面侧通过 `formItem` / `formItems` 输出表单项
- 提交时通过 `modelForm` 或 `getModelForm()` 获取当前值

补充说明：

- `QyDialog` 现在通过 `@qynpm/ui` 提供，`createInstance` 仍属于包内运行时实现
- 当前版本更适合作为体系内拆包使用，还不是完全脱离宿主约定的通用表单库
- 图标能力仍然默认复用宿主现有方案
- `imageInput` 暂时仍然是过渡能力，后续可以继续拆成独立包

## 快速上手

```vue
<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'` | 控件类型 |
| `value` | `any` | 默认值 |
| `props` | `Record<string, any>` | 透传给控件的 props |
| `rules` | `FormItemRule[]` | Element Plus 校验规则 |
| `options` | `Array \| Ref` | `select` / `radio` 选项 |
| `render` | `Function` | 自定义渲染函数 |

#### 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>` | 获取当前表单值快照 |
| `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)`

用于保存和恢复表单条件：

```ts
import { useSave } from '@qynpm/form'

const { save, apply, SaveComponent } = useSave({
  id: 'query-form',
  modelForm,
  showFilter: true
})
```

### `useSyncQueryParams(queryParams, options?)`

用于查询表单与地址栏联动：

```ts
const form = useFormItem(schema, {
  syncQuery: true
})
```

当前保持的行为包括：

- query 初始化回填
- 数组值默认序列化 / 反序列化
- `date` 时间区间转换
- 尝试同步更新 tagsView 中的 query

## 使用示例

### 基础表单

```vue
<component :is="form.formItem" prop="shopName" />
<component :is="form.formItem" prop="shopType" />
```

### 批量渲染

```vue
<component :is="form.formItems" />
```

### includes / excludes

```vue
<component :is="form.formItems" includes="keyword,status,channel" />
<component :is="form.formItems" excludes="remark" />
```

### query 表单

```ts
const queryForm = useFormItem(schema, {
  syncQuery: true,
  id: 'query-form'
})
```

### 启用缓存

```ts
const form = useFormItem(schema, {
  cache: {
    key: 'shop-form-cache'
  }
})
```

### 自定义渲染

```ts
{
  label: '自定义字段',
  key: 'custom',
  type: 'string',
  render: (h, { modelForm, Component }) =>
    h('div', { class: 'flex gap-2' }, [
      Component,
      h('span', modelForm.custom || '')
    ])
}
```

### 保存 / 应用按钮

```vue
<template>
  <component :is="form.SaveComponent" />
</template>
```

## 样式与运行时说明

- 宿主项目需要引入 `element-plus/dist/index.css`
- `@qynpm/form` 当前会自动注入包内样式
- 宿主项目需要提供 `@element-plus/icons-vue`
- 如果使用 `syncQuery`，宿主项目需要接入 `pinia` 和 `vue-router`
- 包内图标仍然默认复用宿主图标体系
- `imageInput` 需要显式传入 `api`
- `tokenKey` 不传时默认使用 `Admin-Token`

## 本地开发

```bash
pnpm dev
pnpm build
```

- `pnpm dev`：启动 playground 预览页
- `pnpm build`：构建到 `dist/`

Workspace 联调：

```json
{
  "dependencies": {
    "@qynpm/form": "workspace:*"
  }
}
```

## 已知限制

1. `useFormItem` 和 `useSave` 目前仍然是配套关系，后续可以再评估是否继续拆开。
2. `createInstance` 仍在包内，后续可根据拆包策略决定是否继续下沉。
3. 当前图标能力仍然偏体系内约定。
4. `imageInput` 仍然依赖宿主传入上传接口，只是 `tokenKey` 提供了默认值。

## 当前联调说明

当前不再通过手动修改 `form` / `ui` 源码 import 来切换联调模式，而是统一使用顶层 Vite 开关控制。

开关位置：

- [vite.config.ts](/vite.config.ts)

当前配置：

```ts
const ENABLE_LOCAL_QIYIN_DEBUG = true
```

规则如下：

1. 当值为 `true` 时
- `@qiyin/form` / `@qynpm/form` 映射到本地源码入口 [@qiyin/form/index.ts](/@qiyin/form/index.ts)
- `@qynpm/ui` 映射到本地源码入口 [@qiyin/UI/lib/index.js](/@qiyin/UI/lib/index.js)
- 适合 workspace 本地联调

2. 当值为 `false` 时
- 走各自包的正常入口
- 更接近线上/发包后的使用方式

注意：

1. 修改开关后需要重启对应项目的 dev 服务