用途
该技能用于构建或编辑 Frappe Studio 应用与页面,涉及区块树、数据绑定、页面脚本、设计与导出的标准应用。资料建议编写绑定、事件或样式前先阅读 RULES.md,构建或重设页面样式前先阅读 DESIGN.md;可用组件需查看 frontend/src/data/components.ts 与 frontend/src/data/componentFamilies.ts;导航、重定向、守卫或启动数据需阅读 ROUTING.md。
标准应用与自定义应用
模式分为两种:
- 自定义(Custom):编辑内容位于站点数据库中的 Studio 文档,页面脚本为 Studio Page.script 中的裸函数体,本地工作流为在 Studio 中编辑并预览。
- 标准(Standard):编辑内容位于关联 Frappe 应用下的导出文件,并同步到 Studio 文档,页面脚本为带 setup(context) 的配套 .ts 模块,本地工作流为编辑文件、运行 watch-studio、检查 /dev/<app_route>。
应使用 Studio App 上已设置的既有模式。两种脚本形式与编辑工作流不可互换。
建立应用
对于标准(导出)应用,先在 Studio 中创建应用和页面,然后通过 StudioApp.enable_app_export(target_app) 启用导出,这会设置 is_standard 和 frappe_app、写入 JSON,并将现有页面脚本移入 .ts 文件。文档保存仅在开发者模式下导出,参见 studio/export.py:can_export。
文件位于关联的 Frappe 应用下,例如参考已有导出 frappe/studio/note/ 后再直接创建:
<frappe_app>/studio/<studio_app>/<studio_app>.json <frappe_app>/studio/<studio_app>/studio_page/<scrubbed_title>/<scrubbed_title>.json <frappe_app>/studio/<studio_app>/studio_page/<scrubbed_title>/<scrubbed_title>.ts <frappe_app>/studio/<studio_app>/studio_components/<component_name>.json
.ts 文件可选。页面 JSON 中,顶层 name 为经过 scrub 的标题;page_name 为 Studio 与脚本包使用的真实文档 ID,需保持稳定。若磁盘上标题变化,需将 JSON 与 .ts 文件移动到新的 frappe.scrub(page_title) 词干。导出子行中的 fields 和 filters 等资源字段为 JSON 编码字符串。
应用路由为 URL 前缀;页面路由以 / 开头并相对于该前缀。app_home 保存页面文档 ID。公开路由需要已发布页面,访客访问还需 allow_guest。
构建页面
draft_blocks 保存编辑器的 JSON 数组,仅含一个根区块;blocks 保存已发布副本。每个根/容器需要 originalElement(根为 "body",容器为 "div"),否则其及子级将渲染为空白。组件区块不得有 originalElement。
存储的 JSON 使用 componentName、componentProps、baseStyles / mobileStyles / tabletStyles、componentSlots、children、componentEvents 以及唯一的 componentId 值。AI 工具使用紧凑别名,如 name、props、style 和 c;直接写 JSON 文件时应以现有导出为模板。
资源位于 Studio Page.resources。配置 Document List 或 Document 源前,先阅读实际 DocType 字段。列表暴露 {{ source.data }},文档暴露 {{ source.doc }},Repeater 子级可见 dataItem。过滤器接受 {"status": "Open"} 或 {"status": ["!=", "Closed"]};多值需用 ["in", [...]],而非裸列表。
页面代码与组件
自定义(仅数据库)页面使用 Studio Page.script 中的裸脚本函数体:无 import 或 export。页面资源和变量已在作用域中,变量为 refs。
标准页面使用同目录 .ts 模块,形式为 export default function setup(context) { ... }。需显式导入 Vue 与 Frappe UI API。context 提供页面资源、变量、路由、router、call、toast 和 socket(应用的 socket.io 连接)。需返回绑定与事件使用的每个状态值或处理函数;状态保留在代码中而非 Studio 变量。切勿混用两种脚本形式。
标准应用的共享代码属于其 Studio 应用文件夹,可通过 @app/... 导入。
Tailwind 仅扫描 studio/ 应用文件夹内的文件。若应用导入了该文件夹之外(例如 Frappe 应用前端也使用的 shared/ 包)的代码,该代码中使用的 Tailwind 类不会进入构建的 CSS。解决方法是在 tsconfig.json 旁添加 tailwind.config.js,并在 content 中列出额外路径。路径相对于配置文件。node_modules 会被自动跳过。仅使用 content(glob 数组)与 safelist;theme 与 plugins 被忽略。编辑器会将每个应用的配置合并为一个样式表,因此 safelist 应保持简短。添加或修改该文件后需重启 Vite 开发服务器。
export default { content: ["../../shared/src/**/*.{vue,ts}"], safelist: ["prose-sm"], }
自定义 Vue 组件
标准应用可将自己的 Vue 组件用作区块。当内置组件与区块组合无法清晰表达 UI 时使用:有状态控件(评分输入、拖放看板、画布或 SVG 绘图)、第三方库,或否则页面需用大量区块与沉重脚本粘合重建的标记。不要用它来重设内置组件已能实现的内容样式。对于由普通区块构成的可复用布局,应改用 Studio Component(studio_components/*.json)。自定义(仅数据库)应用不能使用 .vue 文件。
将单文件组件放在 <frappe_app>/studio/<studio_app>/components/<ComponentName>.vue。Studio 会查找 Studio 应用文件夹下的任何 .vue 文件,文件名即区块的 componentName。
按常规编写 Vue 单文件组件。按 DESIGN.md 使用 Tailwind 类与 espresso token 设置样式,并尽量保持单一根元素(或将 $attrs 绑定到其上),以便区块样式生效。
从区块引用它时设置 isCustomVueComponent: true 且无 originalElement。Props、{{ }} 绑定、{"$type": "variable"} v-model 绑定、用于其 emit 的 componentEvents 以及用于其具名插槽的 componentSlots 均与内置组件一致:
{ "componentId": "RatingInput-ab12cd", "componentName": "RatingInput", "isCustomVueComponent": true, "componentProps": {"max": 5, "modelValue": {"$type": "variable", "name": "rating"}}, "componentEvents": { "change": {"action": "Run Script", "event": "change", "script": "saveRating()"} } }
在 Vite 开发服务器运行时,添加、删除或编辑 .vue 文件会更新编辑器的组件面板与画布,无需重启。页面 .ts 脚本也可直接导入它(import Board from "@app/components/Board.vue")并用 h() 渲染。
同步、运行与验证
本地标准应用编辑时,运行 Studio 的 Vite 开发服务器和 bench --site <site> watch-studio。watcher 会自动导入变更的应用、页面和组件 JSON。它在启动时发现 studio/ 文件夹,因此若目标文件夹是之后创建的需重启它。Vite 热重载页面 .ts 文件;watcher 不同步删除。
检查由 Vite 提供的 /dev/<app_route> 预览及其页面路由。预览可显示草稿区块而无需发布或构建。不要在编辑并预览的循环中运行 bench migrate 或 build-studio-app。仅在需要验证生产资源或构建路由时才构建。
Studio 保存可将标准文档导出回磁盘。在 Studio 中编辑该文档前,等待磁盘变更同步;若编辑器报告外部变更,保存前先刷新。
跨应用工作
保持同级页面视觉一致:构建新页面前先阅读现有页面的树,并复用其色板、间距节奏与区块结构。路由以 / 开头且在应用内唯一。一次完整构建一个页面后再开始下一个;在链接到新页面的页面上接好导航,而不仅是在新页面本身。