使用 wot-design-uni 的 wd-form 组件编写表单页的标准规范 - 提供表单结构、wd-picker 选择器、校验规则、提交处理的完整规范。 触发条件(满足任意一项即触发): - 页面包含 <wd-form> 组件 - 需要实现表单页面(创建、编辑、提交等) - 需要添加选择功能(必须使用 wd-picker 而非 wd-radio-group) - 需要添加表单分区标题(必须使用 FormSectionTitle) - 用户提及"表单"、"wd-form"、"表单校验"、"表单提交"等关键词 - 从 Vue2 迁移表单页面 - 需要实现单选/多选功能(单选用 wd-picker,多选用 wd-checkbox-group) 必须协同的技能: - beautiful-component-design(FormSectionTitle、图标、美化) - api-migration(如果有接口调用) - api-error-handling(如果有接口调用) - code-migration + component-migration(从 Vue2 迁移时) 禁止项: - 禁止使用 wd-radio-group 实现单选(应使用 wd-picker) - 禁止使用 <view class="section-title"> 代替 FormSectionTitle - 禁止 wd-cell 包裹 wd-picker(正确:wd-picker 包裹 wd-cell) - 禁止使用不存在的 #value 插槽 - 禁止单选初始化为数组(应为空字符串) - 禁止多选初始化为字符串(应为空数组) 覆盖场景:维修工单创建、房屋申请、投诉录单、活动报名、用户信息编辑等所有表单页面。
本技能文件定义了在本项目中使用 wot-design-uni 组件库的 <wd-form> 组件编写表单页的标准规范。所有表单页面必须遵循此规范,确保代码风格统一、美观且易于维护。
表单页面通常需要同时使用:
beautiful-component-design - FormSectionTitle、图标、美化api-migration - 如果有接口调用api-error-handling - 如果有接口调用从 Vue2 迁移表单:
code-migration + component-migration - 代码和组件迁移参阅 .claude/skills/check-trigger.md 了解完整的技能触发检查流程。
参考示例:
src/pages-sub/repair/pool-dispatch.vue - 完整表单示例src/pages-sub/repair/pool-finish.vue - 复杂表单示例组件文档:
表单必须:
<wd-form> 包裹所有表单项<wd-cell-group> 分组表单项formRules 校验规则FormInstance 类型导入表单必须使用以下结构:
<template>
<view class="page-container">
<wd-form ref="formRef" :model="model" :rules="formRules">
<!-- 表单内容分组 -->
<view class="section-title"> 分组标题 </view>
<wd-cell-group border>
<!-- 具体的表单项 -->
<wd-input
v-model="model.fieldName"
label="字段标签"
:label-width="LABEL_WIDTH"
prop="fieldName"
placeholder="请输入..."
clearable
:rules="formRules.fieldName"
/>
<!-- 更多表单项... -->
</wd-cell-group>
<!-- 提交按钮 -->
<view class="mt-6 px-3 pb-6">
<wd-button block type="success" size="large" @click="handleSubmit"> 提交 </wd-button>
</view>
</wd-form>
</view>
</template>
关键要求:
<wd-form> 组件必须包含三个必需属性:
ref="formRef" - 组件引用,用于调用表单方法:model="model" - 表单数据双向绑定:rules="formRules" - 表单校验规则使用 wd-cell-group 和 wd-cell 组织表单项:
wd-cell-group 包裹表单项组wd-cell-group 必须添加 border 属性以增强美观度wd-cell-group分组标题:
<view class="section-title"> 作为每组表单项的标题wd-cell-group 之前<script setup lang="ts">
import type { FormRules } from "wot-design-uni/components/wd-form/types";
import { reactive, ref } from "vue";
/** 表单引用 */
const formRef = ref();
/** 表单标签统一宽度(可选,但推荐使用以保持统一) */
const LABEL_WIDTH = "80px";
/** 表单数据模型 */
const model = reactive({
fieldName: "",
// 更多字段...
});
/** 表单校验规则 */
const formRules: FormRules = {
fieldName: [{ required: true, message: "请填写字段" }],
// 更多规则...
};
/** 提交表单 */
async function handleSubmit() {
formRef.value
.validate()
.then(async ({ valid, errors }: { valid: boolean; errors: any[] }) => {
if (!valid) {
console.error("表单校验失败:", errors);
return;
}
// 提交逻辑...
})
.catch((error: any) => {
console.error("表单校验异常:", error);
});
}
</script>
关键要求:
FormRules 类型(从 wot-design-uni/components/wd-form/types)formRef 引用model 响应式数据对象(使用 reactive)formRules 校验规则对象(类型为 FormRules)LABEL_WIDTH 常量,保持表单标签宽度一致<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
}
.section-title {
margin: 0;
font-weight: 400;
font-size: 14px;
color: rgba(69, 90, 100, 0.6);
padding: 20px 15px 10px;
}
/** 自定义样式(如需要) */
:deep(.custom-class) {
/* 样式规则 */
}
</style>
<template>
<wd-cell-group border>
<wd-input
v-model="model.username"
label="用户名"
:label-width="LABEL_WIDTH"
prop="username"
placeholder="请输入用户名"
clearable
:rules="formRules.username"
/>
</wd-cell-group>
</template>
❌ 错误用法 - 使用了不存在的 #value 插槽:
<!-- ❌ 禁止这样写!使用了不存在的 #value 插槽,会导致无法显示和点击 -->
<template>
<wd-cell-group border>
<wd-cell :title-width="LABEL_WIDTH" center>
<template #title>
<text>商品类型</text>
</template>
<template #value>
<!-- ❌ 错误1: wd-cell 组件没有 #value 插槽!#value 是 CellGroup 的插槽 -->
<!-- ❌ 错误2: wd-picker 被 wd-cell 包裹,点击事件被阻挡 -->
<wd-picker v-model="selectedIndex" :columns="options" label-key="name" value-key="id">
<text class="text-blue-500">
{{ options[selectedIndex]?.name || "请选择" }}
</text>
</wd-picker>
</template>
</wd-cell>
</wd-cell-group>
</template>
问题原因:
wd-cell 组件没有 #value 插槽!根据官方文档,wd-cell 组件支持的插槽只有:title、default(右侧内容)、icon、label。#value 插槽是 wd-cell-group 组件的插槽,不是 wd-cell 的插槽。wd-cell 包裹 wd-picker 也会导致点击事件被阻挡,选择器无法正常弹出。<template>
<wd-cell-group border>
<wd-picker
v-model="model.category"
label="分类"
:label-width="LABEL_WIDTH"
prop="category"
:columns="categoryOptions"
label-key="name"
value-key="id"
:rules="formRules.category"
/>
</wd-cell-group>
</template>
使用场景:绝大多数情况下使用此方式,简洁明了。
当需要动态标题或自定义选中值显示时,使用自定义插槽方式。注意:wd-picker 包裹 wd-cell,而不是反过来!
<template>
<wd-cell-group border>
<wd-picker v-model="model.feeFlag" :columns="feeOptions" label-key="name" value-key="id" @confirm="handleFeeChange">
<wd-cell :title="dynamicTitle" :title-width="LABEL_WIDTH" is-link center custom-value-class="cell-value-left">
<text :class="model.feeFlag ? 'text-gray-900' : 'text-gray-400'">
{{ selectedLabel || "请选择" }}
</text>
</wd-cell>
</wd-picker>
</wd-cell-group>
</template>
<style lang="scss" scoped>
/** wd-cell 值靠左对齐 - 确保选择器选中值与其他表单项对齐 */
:deep(.cell-value-left) {
flex: 1;
text-align: left !important;
}
</style>
关键要点:
wd-picker 包裹 wd-cell(而不是反过来):title-width="LABEL_WIDTH" - 与其他表单项保持一致的标签宽度center - 使内容垂直居中custom-value-class="cell-value-left" - 确保选中值左对齐:deep(.cell-value-left) 样式使用场景:仅在需要动态标题或复杂自定义显示时使用。
⚠️ 核心规则:严格遵循统一的组件选型标准,确保用户体验一致性。
| 场景 | 必须使用的组件 | 数据类型 | 说明 |
|---|---|---|---|
| 单选 | wd-picker | string | 无论数据是否动态,统一使用 wd-picker |
| 多选 | wd-checkbox-group | string[] | 无论数据是否动态,统一使用 wd-checkbox-group + wd-checkbox |
❌ 禁止使用:
wd-radio-group - 即使是单选场景,也应该使用 wd-pickerwd-picker 的多列选择模式1. 单选场景 - 使用 wd-picker
<template>
<wd-cell-group border>
<FormSectionTitle title="基本信息" />
<!-- ✅ 正确:单选使用 wd-picker -->
<wd-picker
v-model="model.category"
label="分类"
:label-width="LABEL_WIDTH"
:columns="categoryOptions"
label-key="name"
value-key="id"
/>
</wd-cell-group>
</template>
<script setup lang="ts">
const categoryOptions = [
{ id: "1", name: "选项A" },
{ id: "2", name: "选项B" },
{ id: "3", name: "选项C" },
];
const model = reactive({
category: "", // 单选:string 类型
});
</script>
2. 动态单选场景 - 仍然使用 wd-picker
即使选项是动态从后端获取的,仍然使用 wd-picker:
<template>
<wd-cell-group border>
<FormSectionTitle :title="item.itemTitle" />
<!-- ✅ 正确:动态单选仍使用 wd-picker -->
<wd-picker
v-if="item.titleType === '1001'"
v-model="item.value"
label="请选择"
:label-width="LABEL_WIDTH"
:columns="
item.options.map((opt) => ({
label: opt.name,
value: opt.id,
}))
"
label-key="label"
value-key="value"
/>
</wd-cell-group>
</template>
<script setup lang="ts">
/** 动态表单项数据初始化 */
onLoadSuccess((data) => {
titleList.value = data.data?.list || [];
titleList.value.forEach((item) => {
if (item.titleType === "1001") {
// 单选:初始化为空字符串
item.value = "";
}
});
});
</script>
3. 多选场景 - 使用 wd-checkbox-group
<template>
<wd-cell-group border>
<FormSectionTitle title="选择功能" />
<!-- ✅ 正确:多选使用 wd-checkbox-group -->
<view class="p-3">
<wd-checkbox-group v-model="model.features" @change="handleFeaturesChange">
<wd-checkbox v-for="feature in featureOptions" :key="feature.id" :value="feature.id">
{{ feature.name }}
</wd-checkbox>
</wd-checkbox-group>
</view>
</wd-cell-group>
</template>
<script setup lang="ts">
const featureOptions = [
{ id: "feature1", name: "功能1" },
{ id: "feature2", name: "功能2" },
{ id: "feature3", name: "功能3" },
];
const model = reactive({
features: [] as string[], // 多选:string[] 类型
});
/** 多选变更处理 */
function handleFeaturesChange(event: { value: string[] }) {
model.features = event.value;
console.log("已选择:", model.features);
}
</script>
4. 动态多选场景 - 仍然使用 wd-checkbox-group
<template>
<wd-cell-group border>
<FormSectionTitle :title="item.itemTitle" />
<!-- ✅ 正确:动态多选使用 wd-checkbox-group -->
<view v-if="item.titleType === '2002'" class="p-3">
<wd-checkbox-group v-model="item.values" @change="(event) => handleCheckboxChange(event.value, item)">
<wd-checkbox v-for="(opt, idx) in item.options" :key="idx" :value="opt.id">
{{ opt.name }}
</wd-checkbox>
</wd-checkbox-group>
</view>
</wd-cell-group>
</template>
<script setup lang="ts">
/** 多选变更处理 */
function handleCheckboxChange(values: string[], item: any) {
item.values = values;
console.log(`${item.itemTitle} 已选择:`, values);
}
/** 动态表单项数据初始化 */
onLoadSuccess((data) => {
titleList.value = data.data?.list || [];
titleList.value.forEach((item) => {
if (item.titleType === "2002") {
// 多选:初始化为空数组
item.values = [];
}
});
});
</script>
5. 完整示例:单选和多选混合场景
参考 src/pages-sub/inspection/execute-single.vue:316-345
<template>
<view class="inspection-execute-single">
<wd-form ref="formRef" :model="formData" :rules="formRules">
<!-- 动态表单项 -->
<wd-cell-group v-for="(item, index) in titleList" :key="index" border :class="index > 0 ? 'mt-3' : ''">
<FormSectionTitle
:title="item.itemTitle"
icon="checkbox-checked"
icon-class="i-carbon-checkbox-checked text-blue-500"
/>
<!-- 单选 -->
<wd-picker
v-if="item.titleType === '1001'"
v-model="item.radio as string"
label="请选择"
:label-width="LABEL_WIDTH"
:columns="
item.inspectionItemTitleValueDtos.map((v) => ({
label: v.itemValue,
value: v.itemValue,
}))
"
label-key="label"
value-key="value"
/>
<!-- 多选 -->
<view v-else-if="item.titleType === '2002'" class="p-3">
<wd-checkbox-group
v-model="item.radio as string[]"
@change="(event) => handleCheckboxChange(event.value, item)"
>
<wd-checkbox
v-for="(valueItem, valueIndex) in item.inspectionItemTitleValueDtos"
:key="valueIndex"
:value="valueItem.itemValue"
>
{{ valueItem.itemValue }}
</wd-checkbox>
</wd-checkbox-group>
</view>
<!-- 文本输入 -->
<wd-textarea v-else v-model="item.radio as string" placeholder="请回答" :maxlength="512" show-word-limit />
</wd-cell-group>
</wd-form>
</view>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from "wot-design-uni/components/wd-form/types";
import type { InspectionItemTitle } from "@/types/inspection";
import { onMounted, reactive, ref } from "vue";
import { useRequest } from "alova/client";
import { getInspectionItemTitles } from "@/api/inspection";
import FormSectionTitle from "@/components/common/form-section-title/index.vue";
/** 表单标签统一宽度 */
const LABEL_WIDTH = "80px";
/** 表单实例 */
const formRef = ref<FormInstance>();
/** 巡检项标题列表(动态表单项) */
const titleList = ref<InspectionItemTitle[]>([]);
/** 加载巡检项标题 - 链式回调写法 */
const { send: sendLoadTitles } = useRequest(
() =>
getInspectionItemTitles({
itemId: itemId.value,
page: 1,
row: 100,
}),
{
immediate: false,
},
).onSuccess((data) => {
titleList.value = data.data?.list || [];
// 初始化 radio 字段
titleList.value.forEach((item) => {
if (item.titleType === "1001") {
// 单选:初始化为空字符串
item.radio = "";
} else if (item.titleType === "2002") {
// 多选:初始化为空数组
item.radio = [];
}
});
});
/** 多选 Checkbox 变更 */
function handleCheckboxChange(values: string[], item: InspectionItemTitle) {
item.radio = values;
}
</script>
6. 组件选型决策树
需要选择功能?
├─ 单选(只能选一个)?
│ └─ 使用 wd-picker
│ - 数据类型: string
│ - 静态数据: 直接定义 columns
│ - 动态数据: map 转换为 columns 格式
│
└─ 多选(可以选多个)?
└─ 使用 wd-checkbox-group + wd-checkbox
- 数据类型: string[]
- 事件: @change="(event) => handler(event.value)"
- 初始化: 空数组 []
7. 常见错误
| ❌ 错误写法 | ✅ 正确写法 | 说明 |
|---|---|---|
<wd-radio-group> 用于单选 | <wd-picker> | 单选统一使用 wd-picker |
多选初始化为 '' | 多选初始化为 [] | 多选数据类型必须是数组 |
单选初始化为 [] | 单选初始化为 '' | 单选数据类型必须是字符串 |
@update:model-value 处理多选 | @change 处理多选 | wd-checkbox-group 使用 @change |
多选直接赋值 item.values = '1' | item.values = ['1'] | 多选赋值必须是数组 |
<template>
<wd-cell-group border>
<wd-datetime-picker
v-model="model.appointmentDate"
type="date"
label="预约日期"
:label-width="LABEL_WIDTH"
prop="appointmentDate"
:min-date="Date.now()"
:rules="formRules.appointmentDate"
/>
<wd-datetime-picker
v-model="model.appointmentTime"
type="time"
label="预约时间"
:label-width="LABEL_WIDTH"
prop="appointmentTime"
:rules="formRules.appointmentTime"
/>
</wd-cell-group>
</template>
<template>
<wd-cell-group border>
<wd-textarea
v-model="model.description"
label="描述"
:label-width="LABEL_WIDTH"
prop="description"
placeholder="请输入描述"
:maxlength="500"
show-word-limit
:rules="formRules.description"
/>
</wd-cell-group>
</template>
用于实现选择跳转、显示只读信息等场景:
<template>
<wd-cell-group border>
<wd-cell
title="选择地址"
:title-width="LABEL_WIDTH"
is-link
center
custom-value-class="cell-value-left"
@click="handleSelectAddress"
>
<text :class="model.address ? '' : 'text-gray-400'">
{{ model.address || "请选择地址" }}
</text>
</wd-cell>
<!-- 只读信息显示 -->
<wd-cell title="收费标准" :title-width="LABEL_WIDTH" :value="priceInfo" center />
</wd-cell-group>
</template>
<style lang="scss" scoped>
/** wd-cell 值靠左对齐 */
:deep(.cell-value-left) {
flex: 1;
text-align: left !important;
}
</style>
<template>
<view class="section-title"> 相关图片 </view>
<view class="bg-white p-3">
<wd-upload
v-model:file-list="model.photos"
:limit="9"
:max-size="10 * 1024 * 1024"
:before-upload="handleBeforeUpload"
@success="handleUploadSuccess"
@fail="handleUploadFail"
/>
</view>
</template>
<script setup lang="ts">
import type { UploadBeforeUpload, UploadFile } from "wot-design-uni/components/wd-upload/types";
import { useGlobalToast } from "@/hooks/useGlobalToast";
const toast = useGlobalToast();
const model = reactive({
photos: [] as UploadFile[],
});
/** 图片上传前处理 */
const handleBeforeUpload: UploadBeforeUpload = ({ files, resolve }) => {
const file = files[0];
const maxSize = 10 * 1024 * 1024;
if (file.size && file.size > maxSize) {
toast.warning("图片大小不能超过10MB");
resolve(false);
return;
}
resolve(true);
};
/** 图片上传成功 */
function handleUploadSuccess(response: any) {
console.log("图片上传成功:", response);
}
/** 图片上传失败 */
function handleUploadFail(error: any) {
toast.error("图片上传失败");
console.error("图片上传失败:", error);
}
</script>
const formRules: FormRules = {
// 必填校验
username: [{ required: true, message: "请填写用户名" }],
// 手机号校验
phone: [
{ required: true, message: "请填写手机号" },
{ required: false, pattern: /^1[3-9]\d{9}$/, message: "手机号格式不正确" },
],
// 自定义校验器
appointmentDate: [
{
required: true,
message: "请选择预约日期",
validator: (value) => {
return value && typeof value === "number" ? Promise.resolve() : Promise.reject(new Error("请选择预约日期"));
},
},
],
};
import { useGlobalLoading } from "@/hooks/useGlobalLoading";
import { useGlobalToast } from "@/hooks/useGlobalToast";
const toast = useGlobalToast();
const loading = useGlobalLoading();
/** 提交表单 */
async function handleSubmit() {
// 表单校验
formRef.value
.validate()
.then(async ({ valid, errors }: { valid: boolean; errors: any[] }) => {
if (!valid) {
console.error("表单校验失败:", errors);
return;
}
loading.loading("提交中...");
try {
// 提交逻辑
await submitForm(model);
loading.close();
toast.success("提交成功");
} catch (error) {
loading.close();
toast.error("提交失败");
}
})
.catch((error: any) => {
console.error("表单校验异常:", error);
});
}
对于表单组件不支持的动态校验(如依赖其他字段的校验),在提交时手动校验:
/**
* 位置信息校验
* @returns 返回错误信息,如果验证通过则返回空字符串
*/
function validateLocation(): string {
if (someCondition && !model.fieldA) {
return '请选择字段A'
}
if (anotherCondition && !model.fieldB) {
return '请选择字段B'
}
return ''
}
/** 提交表单 */
async function handleSubmit() {
// 自定义校验
const locationError = validateLocation()
if (locationError) {
toast.warning(locationError)
return
}
// 表单校验
formRef.value.validate().then(...)
}
完整的表单页面实现示例,请参考:
src/pages-sub/repair/add-order.vue该文件展示了:
表单组件声明:
<wd-form ref="formRef" :model="model" :rules="formRules">布局组织:
wd-cell-group 包裹表单项wd-cell-group 添加 border 属性wd-form 下放置表单组件,不使用 wd-cell-group分组标题:
<view class="section-title"> 作为分组标题wd-cell-group 之前标签宽度:
LABEL_WIDTH 常量(如 '80px'):label-width="LABEL_WIDTH"TypeScript 类型:
FormRules 类型model 使用 reactiveformRef 使用 ref()useGlobalToast 和 useGlobalLoading 提供用户反馈prop 属性clearable 属性提升用户体验label-key 和 value-key:maxlength 和 show-word-limit 限制文本输入长度每个表单页面必须在文件顶部提供注释,说明业务名称和访问地址:
<!--
表单页面名称
功能:页面功能描述
访问地址: http://localhost:3000/#/pages-sub/xxx/xxx
建议携带参数: ?param1=xxx¶m2=xxx
完整示例: http://localhost:3000/#/pages-sub/xxx/xxx?param1=xxx¶m2=xxx
-->
uno.config.ts 中为业务特定样式创建 shortcutsv-show 而非 v-if 控制显示useGlobalToast - 全局提示消息useGlobalLoading - 全局加载状态useRequest (from alova/client) - 接口请求管理在编写表单时,充分利用 TypeScript 类型定义:
import type { FormRules } from "wot-design-uni/components/wd-form/types";
import type { UploadBeforeUpload, UploadFile } from "wot-design-uni/components/wd-upload/types";
遵循本规范编写表单页面,可以确保:
在实际开发中,请始终参考 src/pages-sub/repair/add-order.vue 作为标准范例。