第 7 篇:进阶------校验、联动、列表页
本篇目标
在第 6 篇的单表单之上,补齐三块:
- 提交时统一校验(而不是输入时就报错)
- 字段联动显隐(
showWhen) - 把「表单页」升级为「列表 + 新增/编辑」的完整页面
1. 提交时校验
第 6 篇的错误是「输入即显示」,更常用的交互是「点保存时才校验」。
给 CustomerPage 加一个 submitted 状态:
vue
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import SchemaForm from '@/components/form/SchemaForm.vue'
import { Button } from '@/components/ui/button'
import type { PageSchema } from '@/components/form/schema'
const props = defineProps<{ schema: PageSchema }>()
const form = reactive<Record<string, any>>({})
for (const f of props.schema.fields) {
if (f.default !== undefined) form[f.name] = f.default
}
const submitted = ref(false)
// 是否通过:所有 required 已填 + pattern 通过
const valid = computed(() =>
props.schema.fields.every((f) => {
const v = form[f.name]
if (f.required && (v === undefined || v === null || v === '')) return false
if (f.pattern && typeof v === 'string' && v && !new RegExp(f.pattern).test(v)) return false
return true
}),
)
function submit() {
submitted.value = true
if (!valid.value) return
console.log('提交数据', form)
}
</script>
<template>
<div>
<SchemaForm v-model="form" :fields="schema.fields" :columns="schema.columns" :submitted="submitted" />
<div class="mt-4 flex justify-end">
<Button @click="submit">保存</Button>
</div>
</div>
</template>
配套地,把 SchemaForm.vue 的错误显示改为「仅 submitted 后显示」:
vue
<!-- 替换原来 errorOf(field) 那两行 -->
<p v-if="submitted && errorOf(field)" class="mt-1 text-[var(--text-caption)] text-destructive">
{{ errorOf(field) }}
</p>
并在 props 增加:
ts
defineProps<{
fields: FieldSchema[]
modelValue: Record<string, any>
columns?: number
submitted?: boolean // 新增:点保存后才显示错误
}>()
说明:这是最简单可靠的校验路径。字段很多、校验规则很复杂时,
可引入 vee-validate + zod 生成校验(真实常用组合),把 schema 的 required/pattern 映射成 zod 规则。
2. 字段联动(showWhen)
Schema 里已经支持 showWhen,渲染器第 6 篇的 visible() 已实现。用法:
json
{
"name": "contactType",
"label": "联系方式",
"type": "select",
"options": [
{ "label": "手机", "value": "phone" },
{ "label": "邮箱", "value": "email" }
]
},
{
"name": "email",
"label": "邮箱",
"type": "input",
"showWhen": { "field": "contactType", "equals": "email" }
}
效果:联系人选「邮箱」时才显示邮箱输入框。零 JS,纯 schema 声明。
3. 列表页:DataList + 表格
表单页有了,还差「列表展示已有数据」。做一个通用列表页骨架,
用简单表格(HTML table + Tailwind,不引额外依赖,保证可落地):
创建 src/components/list/DataList.vue:
vue
<script setup lang="ts">
import type { ColumnSchema } from '@/components/form/schema'
// 简化列定义:{ key, label, width? }
defineProps<{
columns: ColumnSchema[]
rows: Record<string, any>[]
}>()
const emit = defineEmits<{
(e: 'edit', row: Record<string, any>): void
}>()
</script>
<template>
<div class="overflow-x-auto rounded-[var(--radius-lg)] border border-border">
<table class="w-full min-w-[640px] text-left">
<thead class="bg-muted">
<tr>
<th
v-for="c in columns"
:key="c.key"
class="px-4 py-2.5 text-[var(--text-caption)] font-medium text-muted-foreground"
>
{{ c.label }}
</th>
<th class="px-4 py-2.5 text-right text-[var(--text-caption)] text-muted-foreground">操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, i) in rows"
:key="i"
class="border-t border-border hover:bg-muted/40"
>
<td v-for="c in columns" :key="c.key" class="px-4 py-2.5 text-[var(--text-body)]">
{{ row[c.key] ?? '-' }}
</td>
<td class="px-4 py-2.5 text-right">
<Button variant="ghost" size="sm" @click="emit('edit', row)">编辑</Button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
在 src/components/form/schema.ts 补充列表列类型:
ts
export interface ColumnSchema {
key: string
label: string
width?: string
}
4. 组合:一个完整的「台账页」
vue
<script setup lang="ts">
import { ref } from 'vue'
import PageShell from '@/components/layout/PageShell.vue'
import PageHeader from '@/components/layout/PageHeader.vue'
import SectionCard from '@/components/layout/SectionCard.vue'
import DataList from '@/components/list/DataList.vue'
import SchemaForm from '@/components/form/SchemaForm.vue'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import type { PageSchema, ColumnSchema } from '@/components/form/schema'
const columns: ColumnSchema[] = [
{ key: 'name', label: '姓名' },
{ key: 'dept', label: '部门' },
{ key: 'phone', label: '手机号' },
]
const rows = ref([
{ name: '张三', dept: '研发', phone: '13800000001' },
{ name: '李四', dept: '产品', phone: '13800000002' },
])
const formSchema: PageSchema = {
title: '编辑客户',
columns: 2,
fields: [
{ name: 'name', label: '姓名', type: 'input', required: true },
{ name: 'dept', label: '部门', type: 'select', options: [{ label: '研发', value: 'rd' }, { label: '产品', value: 'pm' }] },
{ name: 'phone', label: '手机号', type: 'input', pattern: '^1\\d{10}$' },
],
}
const dialogOpen = ref(false)
const editing = ref<Record<string, any>>({})
function openEdit(row: Record<string, any>) {
editing.value = { ...row }
dialogOpen.value = true
}
</script>
<template>
<PageShell>
<PageHeader title="客户台账" description="列表 + 编辑弹窗">
<template #actions>
<Button @click="openEdit({})">新增</Button>
</template>
</PageHeader>
<SectionCard>
<DataList :columns="columns" :rows="rows" @edit="openEdit" />
</SectionCard>
<Dialog v-model:open="dialogOpen">
<DialogContent>
<DialogHeader><DialogTitle>{{ formSchema.title }}</DialogTitle></DialogHeader>
<SchemaForm v-model="editing" :fields="formSchema.fields" :columns="formSchema.columns" />
</DialogContent>
</Dialog>
</PageShell>
</template>
提示:Dialog 组件第 2 篇没装,先执行
pnpm dlx shadcn-vue@latest add dialog。也可不加弹窗,直接把 SchemaForm 放在列表下方做「行内编辑」。
5. 验证
- 必填留空点保存:不弹错误前不提交;填完再点能通过。
- 联系方式的联动字段:切换 select 值,邮箱框显隐正确。
- 列表能渲染、点「编辑」能带出数据进弹窗。
- 窄屏下表出现横向滚动而非挤破。
本篇小结
- 校验时机、字段联动、列表 + 编辑,三块拼成一个完整后台页面
- 全部仍是「固定壳 + schema 数据」的组合
- 最后一步:怎么让 AI 稳定地只产出 schema------第 8 篇:AI 工作流与完整案例