Skip to content

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.js

  • Modify: designer3.0/tests/modules/shared/adapter.spec.js

  • [ ] Step 1: 写失败测试

js
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: 跑测试并提交
bash
pnpm test tests/modules/shared/adapter.spec.js

Expected: PASS

bash
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.js

  • Create: designer3.0/tests/modules/appgen/buildAppSpec.spec.js

  • [ ] Step 1: 写失败测试

js
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: 跑测试并提交
bash
pnpm test tests/modules/appgen/buildAppSpec.spec.js

Expected: PASS

bash
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.vue

  • Create: 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
js
getAppSpec(overrides) {
    return buildAppSpec(methods.getSnapshot(), overrides);
}
getAppSFC(overrides) {
    return generateAppSfc(methods.getAppSpec(overrides), methods.getJson(), methods.getOptionsJson());
}
  • [ ] Step 4: 手动验收

examples 里:

js
adapter: createMemoryAdapter(),
showPreviewApp: true,

预览 → 应用 → 新建一条 → 回到列表能看到 → 编辑 → 删除。

  • [ ] Step 5: Commit
bash
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.js

  • Create: designer3.0/tests/modules/appgen/generateAppSfc.spec.js

  • [ ] Step 1: 写失败测试

js
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: 跑测试 + 文档 + 提交

bash
pnpm test tests/modules/appgen

Expected: PASS

fc-design-pro/src/help/feature/form-app.md:AppSpec 字段、adapter 方法、导出文件怎么接到 Vue Router。

bash
git add src/modules/appgen src/designer/appgen src/help
git commit -m "feat: export CRUD SFC from AppSpec"