Language 中文 English
ThinkPHP 也能这么爽——用 ThinkPHP + Vue 复刻 Filament 体验
News 2026-07-27 · About 32 min read

ThinkPHP 也能这么爽——用 ThinkPHP + Vue 复刻 Filament 体验

前两篇,我们用 Laravel + Filament 论证了"服务端驱动 UI 对 AI 更友好"。但一个现实问题摆在面前:国内大量团队的主力框架是 ThinkPHP,不是 Laravel。难道"默认不分"的红利,被框架绑死了? 本文给出一个明确的答案:红利属于"声明式后端驱动 UI"这一架构模式,而不属于某个具体框架。我们用 ThinkPHP 8 + Vue 3 复刻 Filament 的体验,并坦诚说明:在"Agent 友好度"上,国内线确实存在差距,但差距可弥补,且框架风险已被降维。

导语

前两篇,我们用 Laravel + Filament 论证了"服务端驱动 UI 对 AI 更友好"。但一个现实问题摆在面前:国内大量团队的主力框架是 ThinkPHP,不是 Laravel。难道"默认不分"的红利,被框架绑死了?

本文给出一个明确的答案:红利属于"声明式后端驱动 UI"这一架构模式,而不属于某个具体框架。我们用 ThinkPHP 8 + Vue 3 复刻 Filament 的体验,并坦诚说明:在"Agent 友好度"上,国内线确实存在差距,但差距可弥补,且框架风险已被降维。

一、单一事实源:一份 Schema 通吃两条线

Filament 体验的核心,是"用一份声明描述一个后台模块"。要复刻它,第一步是定义单一事实源:一份 JSON Schema 元数据契约。

它收敛了模块的全部结构——字段、类型、组件、校验、关联、权限点、菜单。关键不在"它是 JSON",而在它的角色

  • 它是配置面板读写的对象(可视化编辑器直接读写,不另起内部 DTO,零漂移);
  • 它是生成器的输入;
  • 它是二次生成的输入(重跑不丢结构);
  • 它是给开发者看的文档,也是给 AI 当上下文的载体。

更重要的是:国际线(Laravel Stubs)与国内线(ThinkPHP Stubs)共用同一份 Schema,避免元数据分叉成两套。这意味着,描述"订单模块"的那份结构定义,不管最终落到 Laravel 还是 ThinkPHP,都只写一次——框架是皮肤,Schema 是骨骼。

// raise.json(节选:一份 Schema 同时驱动国际线 / 国内线)
{
  "module": "posts",
  "model": "Post",
  "fields": [
    { "name": "title", "type": "string", "required": true, "inTable": true },
    { "name": "body",  "type": "richtext", "inForm": true },
    { "name": "published", "type": "boolean", "inTable": true }
  ],
  "permissions": ["posts.create", "posts.edit", "posts.publish", "posts.delete"]
}

并且,元数据在生成阶段就被编译进物理代码(PHP 声明类 + 模板渲染的 Vue 外壳配置),运行时不存在任何"读配置表去渲染任意 UI"的逻辑。这是与"低代码运行时"的根本分界线:我们生成的是代码,不是解释配置。

二、免编译资产:复刻 Filament 的"基本不编译"

Filament 之所以"爽",很大程度来自它的"基本不编译":前端外壳预编译好随 Composer 包发布,终端用户只写 PHP 声明类、跑 php artisan filament:assets 复制资产,全程无 Node / npm。

我们照搬同一模型,关键在编译发生在"生成器仓库"一次,而非"用户侧"每次

  1. 生成器仓库(开发期) 用 Vite 把通用 CrudTable / CrudForm + 自研 Inertia 客户端 + Element Plus 编译为 public/admin/app.jsapp.css(含版本 hash 做 cache busting)。Vite 仅存在于生成器仓库,不进入生成项目
  2. 生成项目时,上述编译产物直接写入用户项目的 public/admin/
  3. 用户加模块只写 PHP Resource 声明类 → props 驱动预编译外壳 → 零编译、零 Node、零 npm
  4. 资产发布命令镜像 Filament:php think raise:publish-assets(把生成器内的 admin-dist/* 同步到 public/admin/)。
# 用户侧:拉起资产,全程无 Node
php think raise:publish-assets
# 等价于 Laravel 侧的
# php artisan filament:assets

这一步把"前端构建"从用户的日常里彻底抹掉,正是"不分离"体验在国内线上的对应物。

三、Inertia 桥接 + Page / Resource 声明模型

桥接层是一个薄薄的 Inertia-for-TP 适配器:后端控制器 render 返回 {component, props},没有 REST 契约缝;后端拥有路由与数据。协议很小,关键在于"不引入第二份契约"。

// 后端控制器:render 返回 {component, props},无 REST 契约
$page = new PostResource();
return inertia()->renderPage(PostResource::class, [
    'rows' => Post::paginate(10),
]);
// 适配器内部:反射 extends 基类 → 推导骨架 → 序列化声明 + 运行时数据
// → 返回 { component: 'Crud/Index', props: { resource, rows, meta } }

声明模型与 Filament 同构:PHP 只声明"内容",前端固定骨架写死"结构"

一个 Resource 声明"列 / 过滤器 / 工具栏 / 表单 / 行操作"这五个内容槽位;前端固定的 CrudPage 骨架按固定顺序拼装这些区块(PageHeader → FilterBar → Toolbar → CrudTable → Pagination → DialogForm)。PHP 只决定"区块里有什么",绝不描述"区块怎么排布"——这既守住了"代码生成器而非低代码"的定位,也让 Agent 的维护面极小。

// app/admin/resources/PostResource.php(声明"内容",不描述布局)
class PostResource extends Resource
{
    public function table(): array
    {
        return [
            Column::make('title')->searchable(),
            Column::make('author.name')->label('作者'),
            Column::make('published')->boolean(),
        ];
    }

    public function filters(): array
    {
        return [ Filter::make('author')->relationship() ];
    }

    public function toolbar(): array
    {
        return [ Action::make('export')->can('posts.export') ];
    }

    public function form(): array
    {
        return [
            Field::make('title')->required(),
            Field::make('body')->richEditor(),
        ];
    }

    public function rowActions(): array
    {
        return [ Action::make('edit'), Action::make('delete')->danger() ];
    }
}

ThinkPHP+Vue 架构图

"内容驱动、结构固定"——这正是 Filament 的 Resource + 自定义 Page 模型在 ThinkPHP 上的映射。语法不同,骨架同构。

四、分层覆盖:给确定性一个出口

"通用组件省掉 80% 重复"不能变成"卡住 20% 定制"。四级台阶,确定性解析(约定优于配置):

| 台阶 | 场景 | 怎么做 | 生成 / 手写 | | ------ | ----------------- | ------------------------------------- | ----------------- | | ① 通用默认 | 80% 标准模块 | CrudTable / CrudForm 读 PHP 声明自动渲染 | 仅生成 PHP | | ② 轻定制 | 自定义列 / 动作 / 提交前校验 | 通用组件暴露 slot + hook;注册小组件占 slot | 零整页 Vue;可选 1 个小组件 | | ③ 重定制 | 复杂交互 / 专属流程 | 放 modules/X/Index.vue 真实 SFC 覆盖通用 | 手写真实 Vue | | ④ 双侧覆盖 | 业务逻辑也要改 | PHP 侧子类 Controller / 自定义 action 覆盖 | 手写 PHP |

解析顺序一行代码确定:modules/{Module}/Index.vue 存在 ? 用它 : 用通用 CrudTable。这把"定制"从"破坏结构"变成"在确定的出口上替换"——Agent 知道去哪改,人也知道去哪找。

五、坦诚对比:Agent 友好度的差距与弥补

必须诚实:国内线(ThinkPHP + Vue 分离产物)在"Agent 友好度"上,确实低于国际线(Laravel + Filament + Boost)。核心差距是缺 Boost 那一类的 MCP / Skills 工具生态——没有 php artisan boost:mcp 提供的 15+ 工具,下游 Agent 接手时"看项目"的成本更高。

弥补路径不推翻双轨制,而是补齐上下文层:

  • .ai/guidelines:固化声明约定、数据作用域、权限点命名 {module}.{action},让 Agent 零成本理解项目;
  • 补可选的"服务端组件模板":把常见的定制沉淀为可复用、可生成的模板,降低手写 SFC 的比例;
  • 不推翻双轨制:两条线共用同一份 JSON Schema,差异只在"编译到哪套 Stubs",工具链各自演进互不影响。

更关键的一层认知是:框架风险已被降维。因为真正的资产是"声明式 Page 体系 + 预编译分发 + 插件架构",而非某个具体后端框架——今日绑在 ThinkPHP 上的,明天若想平移到 Python + Vue,唯一绑定点只是"用户声明类"和"Inertia 适配器"两处,90% 的装配逻辑可直接平移。换框架,从"重写整个应用"降维为"换一套生成模板"。

金句:框架会过时,但"声明式页面装配模型"不会;你真正该押注的,是后者。

结语:回到第一性原理

三篇文章走到这里,可以收一个尾。

第一篇,我们质疑"默认分离",给出公式 Agent 产出质量 ≈ 1 /(语言数 × 接缝数 × 结构不确定性);第二篇,用 Laravel + Filament 证明"单语言 + 服务端驱动 + 自带上下文"能把这个分母压到极低;第三篇,用 ThinkPHP + Vue 证明这条路不绑定语言——它的红利属于架构模式,不属于某个框架。

三条线汇成同一句话:在 Vibe Coding 时代,把"默认分离"的肌肉记忆,换成"默认单语言 + 服务端驱动",不是怀旧,而是第一性原理。而当你真要动手,无论选 Laravel 还是 ThinkPHP,请记得提前为"AI 接手维护"买单——固化约定、接好工具、写好 guidelines,让每一次生成与交接,都站在确定性的肩膀上。