Form-to-App Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 从 FormSnapshot 生成 AppSpec,并在设计器里预览「列表 + 表单」CRUD;同时能导出 List/Form/api 三份 SFC。
Architecture: 纯函数 buildAppSpec(snapshot, overrides) 决定列、筛选、路由、权限码。运行时 FcAppView 只认 AppSpec + HostAdapter。无 adapter 时用 createMemoryAdapter 的 records 方法(本计划补齐)。不生成菜单、仪表盘。
Tech Stack: Vue3、Element Plus Table/Pagination、现有 form-create 渲染器、getSFC() 仅作参考不复用其提交空实现。
Depends on: 2026-08-23-00-shared-foundation.md
File map
- Create:
designer3.0/src/modules/appgen/buildAppSpec.js - Create:
designer3.0/src/modules/appgen/generateAppSfc.js - Create:
designer3.0/tests/modules/appgen/buildAppSpec.spec.js - Create:
designer3.0/src/designer/appgen/FcAppView.vue - Create:
designer3.0/src/designer/appgen/AppPreviewTab.vue - Modify:
designer3.0/src/modules/shared/adapter.js— memory adapter 补 records - Modify:
designer3.0/src/designer/FcDesigner.vue— 预览增加「应用」页、getAppSpec/getAppSFC - Modify:
designer3.0/types/index.d.ts - Modify:
designer3.0/src/locale/zh-cn.js - Create:
fc-design-pro/src/help/feature/form-app.md
Task 1: Memory adapter 补 records
Files:
Modify:
designer3.0/src/modules/shared/adapter.jsModify:
designer3.0/tests/modules/shared/adapter.spec.js[ ] Step 1: 写失败测试
it('supports crud records', async () => {
const adapter = createMemoryAdapter();
const created = await adapter.saveRecord({ formId: 'f1', name: '张三' });
expect(created.id).toBeTruthy();
const found = await adapter.getRecord(created.id);
expect(found.name).toBe('张三');
const page = await adapter.listRecords({ formId: 'f1', page: 1, pageSize: 10 });
expect(page.total).toBe(1);
await adapter.deleteRecord(created.id);
const empty = await adapter.listRecords({ formId: 'f1', page: 1, pageSize: 10 });
expect(empty.total).toBe(0);
});- [ ] Step 2: 实现
records 用 Map。saveRecord:有 id 则更新,无则分配 rec_ + 自增。listRecords 按 formId 过滤,支持 keyword 对所有字符串字段 includes。分页:slice((page-1)*pageSize, page*pageSize)。
- [ ] Step 3: 跑测试并提交
pnpm test tests/modules/shared/adapter.spec.jsExpected: PASS
git add src/modules/shared/adapter.js tests/modules/shared/adapter.spec.js
git commit -m "feat: memory adapter CRUD records"Task 2: buildAppSpec
Files:
Create:
designer3.0/src/modules/appgen/buildAppSpec.jsCreate:
designer3.0/tests/modules/appgen/buildAppSpec.spec.js[ ] Step 1: 写失败测试
import { describe, it, expect } from 'vitest';
import { buildAppSpec } from '../../../src/modules/appgen/buildAppSpec.js';
const snapshot = {
format: '1.0.0',
formId: 'leave',
name: '请假',
rule: [
{ type: 'input', field: 'name', title: '姓名', _fc_drag_tag: 'input' },
{ type: 'input', field: 'reason', title: '事由', _fc_drag_tag: 'textarea' },
{ type: 'date', field: 'start', title: '开始', _fc_drag_tag: 'date' },
{ type: 'html', title: '说明' },
],
options: { formName: '请假' },
schema: [],
createdAt: '',
};
describe('buildAppSpec', () => {
it('uses first five value fields as list columns', () => {
const spec = buildAppSpec(snapshot);
expect(spec.formId).toBe('leave');
expect(spec.list.columns).toEqual(['name', 'reason', 'start']);
expect(spec.routes.map((r) => r.name)).toEqual(['list', 'create', 'edit', 'detail']);
expect(spec.permissions.create).toBe('leave:create');
});
it('allows overrides', () => {
const spec = buildAppSpec(snapshot, { columns: ['name'], filters: ['start'] });
expect(spec.list.columns).toEqual(['name']);
expect(spec.list.filters).toEqual(['start']);
});
});- [ ] Step 2: 实现
列候选:递归 rule,收集带 field 且 type 不在 ['html','title','divider','alert','button'] 的节点,最多 5 个。筛选默认取 type 为 select/date/switch 的前 3 个。权限码:${formId}:${action}。pageSize 默认 20。
- [ ] Step 3: 跑测试并提交
pnpm test tests/modules/appgen/buildAppSpec.spec.jsExpected: PASS
git add src/modules/appgen tests/modules/appgen
git commit -m "feat: derive AppSpec from form snapshot"Task 3: FcAppView 运行时
Files:
Create:
designer3.0/src/designer/appgen/FcAppView.vueCreate:
designer3.0/src/designer/appgen/AppPreviewTab.vue[ ] Step 1: FcAppView 行为
Props:spec、adapter、rule、options。
默认视图
list:el-table按spec.list.columns取 title(从 rule 反查),工具栏「新建」。「新建 / 编辑 / 详情」:用现有
formCreate渲染同一份rule。详情时api.disabled(true)或走阅读模式(与现有 preview read 相同 API)。提交:
adapter.saveRecord({ formId: spec.formId, ...formData, id })。删除:
el-popconfirm后adapter.deleteRecord。权限:若
formCreate.getData('$permissions')存在且不包含对应 permission,隐藏按钮。无$permissions则全部显示(与现有权限公式一致:没注入就不拦)。[ ] Step 2: 预览 Tab
找到预览弹窗里 sqlMode / sfcMode 的切换实现(FcDesigner.vue 预览相关),增加 appMode。config.showPreviewApp !== false 且本功能发布后默认对 Pro 开放;为了兼容,第一期默认 showPreviewApp: false,examples 打开。
- [ ] Step 3: 暴露 API
getAppSpec(overrides) {
return buildAppSpec(methods.getSnapshot(), overrides);
}
getAppSFC(overrides) {
return generateAppSfc(methods.getAppSpec(overrides), methods.getJson(), methods.getOptionsJson());
}- [ ] Step 4: 手动验收
examples 里:
adapter: createMemoryAdapter(),
showPreviewApp: true,预览 → 应用 → 新建一条 → 回到列表能看到 → 编辑 → 删除。
- [ ] Step 5: Commit
git add src/designer/appgen src/designer/FcDesigner.vue types/index.d.ts src/locale
git commit -m "feat: preview CRUD app from current form"Task 4: 导出 SFC
Files:
Create:
designer3.0/src/modules/appgen/generateAppSfc.jsCreate:
designer3.0/tests/modules/appgen/generateAppSfc.spec.js[ ] Step 1: 写失败测试
import { generateAppSfc } from '../../../src/modules/appgen/generateAppSfc.js';
it('emits three files with adapter hooks', () => {
const files = generateAppSfc(
{ formId: 'leave', list: { columns: ['name'], filters: [], pageSize: 20 }, routes: [], permissions: {} },
'[]',
'{}'
);
expect(files.map((f) => f.name).sort()).toEqual(['FormPage.vue', 'List.vue', 'api.js']);
expect(files.find((f) => f.name === 'api.js').code).toContain('export async function listRecords');
expect(files.find((f) => f.name === 'List.vue').code).toContain('listRecords');
expect(files.find((f) => f.name === 'FormPage.vue').code).toContain('form-create');
});- [ ] Step 2: 生成内容要点
api.js 只留函数签名 + throw new Error('implement HostAdapter'),让客户接线。List.vue / FormPage.vue 从这些函数导入。FormPage.vue 用 form-create + JSON.parse 规则,提交调用 saveRecord。禁止再生成「todo 提交表单」空注释。
[ ] Step 3: 预览弹窗「生成应用」页用
el-tabs展示三个文件,复用现有 SFC 页的复制按钮。[ ] Step 4: 跑测试 + 文档 + 提交
pnpm test tests/modules/appgenExpected: PASS
fc-design-pro/src/help/feature/form-app.md:AppSpec 字段、adapter 方法、导出文件怎么接到 Vue Router。
git add src/modules/appgen src/designer/appgen src/help
git commit -m "feat: export CRUD SFC from AppSpec"

