关于 frappe-impl-ui-components
面向 Frappe 界面开发者的客户端 UI 组件与实时更新实现技能,汇集常见工作流和 API 指引,涵盖对话框、页面、列表视图及事件订阅。
主要功能
- 01对话框与表单输入模式
- 02消息、提醒与确认操作
- 03列表视图自定义
- 04自定义页面与日历
- 05看板配置
- 06实时事件与扫描器 API
适用场景
- 01在对话框中收集输入
- 02自定义 DocType 列表视图
- 03构建自定义应用页面
- 04根据实时事件更新界面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-impl-ui-components安装后在 Claude Code 中输入/frappe-impl-ui-components确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/.gitkeepanti-patterns.mdcontrols-api.mddecision-tree.mdexamples.mdtree-view.mdworkflows.md详细介绍
Frappe UI 组件与实时功能 — 实现工作流
用于构建客户端 UI 的分步工作流。表单脚本请参见 frappe-impl-clientscripts。服务器端 API 请参见 frappe-syntax-serverscripts。
版本:v14/v15/v16 | 注意:v15+ 使用 Bootstrap 5;Dialog API 在所有版本中均保持稳定。
快速选择:该使用哪个 UI 组件?
WHAT do you need?
├── Prompt user for input → frappe.prompt (simple) or frappe.ui.Dialog (complex)
├── Show a message/alert → frappe.msgprint / frappe.show_alert / frappe.throw
├── Confirm an action → frappe.confirm
├── Multi-field data entry popup → frappe.ui.Dialog with fields
├── Select from a list of records → frappe.ui.form.MultiSelectDialog
├── Full custom page (not a form) → frappe.ui.Page
├── Customize list columns/colors → frappe.listview_settings
├── Visual board for workflow → Kanban Board (Select field based)
├── Date-based record view → Calendar View ({doctype}_calendar.js)
├── Hierarchical data display → Tree View (is_tree DocType)
├── Live updates without refresh → frappe.publish_realtime + frappe.realtime.on
├── Show background job progress → frappe.publish_progress
├── Scan barcode/QR code → frappe.ui.Scanner
└── Custom cell formatting → formatters in listview_settings or form
完整的决策树请参见 references/decision-tree.md。
工作流 1:对话框(frappe.ui.Dialog)
简单对话框
let d = new frappe.ui.Dialog({
title: "Enter Details",
fields: [
{ label: "Full Name", fieldname: "full_name", fieldtype: "Data", reqd: 1 },
{ label: "Email", fieldname: "email", fieldtype: "Data", options: "Email" },
{ label: "Role", fieldname: "role", fieldtype: "Select",
options: "Developer\nManager\nDesigner" },
],
size: "small", // "small", "large", or "extra-large"
primary_action_label: "Create",
primary_action(values) {
frappe.call({
method: "myapp.api.create_user",
args: values,
callback(r) {
if (!r.exc) {
frappe.show_alert({ message: "User created", indicator: "green" });
d.hide();
}
}
});
}
});
d.show();
规则:务必在回调中调用 d.hide(),绝不要在异步调用完成之前调用。
带表格字段的对话框
let d = new frappe.ui.Dialog({
title: "Add Items",
fields: [
{ label: "Customer", fieldname: "customer", fieldtype: "Link",
options: "Customer", reqd: 1 },
{ fieldtype: "Section Break" },
{ label: "Items", fieldname: "items", fieldtype: "Table",
in_place_edit: true, reqd: 1,
fields: [
{ fieldname: "item", label: "Item", fieldtype: "Link",
options: "Item", in_list_view: 1, reqd: 1 },
{ fieldname: "qty", label: "Qty", fieldtype: "Int",
in_list_view: 1, default: 1 },
{ fieldname: "rate", label: "Rate", fieldtype: "Currency",
in_list_view: 1 },
],
},
],
primary_action_label: "Submit",
primary_action(values) {
console.log(values); // { customer: "...", items: [{item, qty, rate}] }
d.hide();
}
});
d.show();
规则:务必在要显示的表格子字段上设置 in_list_view: 1。未设置该属性的字段会在网格中隐藏。
多步对话框
let d = new frappe.ui.Dialog({
title: "Setup Wizard",
fields: [
// Page 1
{ fieldtype: "Section Break", label: "Step 1: Basic Info",
collapsible: 0 },
{ label: "Name", fieldname: "name", fieldtype: "Data", reqd: 1 },
// Page 2
{ fieldtype: "Section Break", label: "Step 2: Configuration",
collapsible: 0 },
{ label: "Option", fieldname: "option", fieldtype: "Select",
options: "A\nB\nC" },
],
primary_action_label: "Finish",
primary_action(values) {
d.hide();
}
});
d.show();
主要 Dialog 方法
| 方法 | 用途 |
|---|---|
d.show() | 显示对话框 |
d.hide() | 关闭对话框 |
d.get_values() | 获取所有字段值组成的对象 |
d.set_values({field: val}) | 设置字段值 |
d.get_field("name") | 获取指定字段控件 |
d.set_df_property("name", "hidden", 1) | 动态显示/隐藏字段 |
d.disable_primary_action() | 禁用提交按钮 |
d.enable_primary_action() | 重新启用提交按钮 |
工作流 2:消息与提醒
frappe.msgprint:模态消息
// Simple message
frappe.msgprint("Record saved successfully");
// With options
frappe.msgprint({
title: "Warning",
message: "This action cannot be undone",
indicator: "orange", // green, blue, orange, red
primary_action: {
label: "Proceed",
action() { do_something(); }
}
});
// List of messages
frappe.msgprint({
title: "Validation Errors",
message: "Please fix the following:",
as_list: true,
indicator: "red",
});
frappe.throw:抛出异常并显示错误
// Client-side: shows msgprint and stops execution
frappe.throw("Amount cannot be negative");
# Server-side: raises ValidationError, shown as red msgprint
frappe.throw("Amount cannot be negative")
frappe.throw("Not Permitted", frappe.PermissionError) # specific exception
规则:验证错误务必使用 frappe.throw。绝不要使用 frappe.msgprint 显示错误——它不会停止执行。
frappe.confirm:是/否对话框
frappe.confirm(
"Are you sure you want to delete this record?",
() => { /* Yes callback */ delete_record(); },
() => { /* No callback (optional) */ }
);
frappe.prompt:快速单字段输入
frappe.prompt(
{ label: "Reason", fieldname: "reason", fieldtype: "Small Text", reqd: 1 },
(values) => {
console.log(values.reason);
},
"Enter Reason", // dialog title
"Submit" // primary action label
);
// Multiple fields
frappe.prompt([
{ label: "Reason", fieldname: "reason", fieldtype: "Small Text", reqd: 1 },
{ label: "Priority", fieldname: "priority", fieldtype: "Select",
options: "Low\nMedium\nHigh" },
], (values) => { console.log(values); }, "Details");
frappe.show_alert:Toast 通知
// Simple
frappe.show_alert("Saved");
// With indicator and duration
frappe.show_alert({ message: "Email sent", indicator: "green" }, 5);
// Duration in seconds (default: 7)
规则:使用 frappe.show_alert 显示非阻塞的成功消息。需要用户确认时,使用 frappe.msgprint。
工作流 3:自定义列表视图
在 DocType 目录中创建 {doctype_name}_list.js:
// myapp/doctype/task/task_list.js
frappe.listview_settings["Task"] = {
// Extra fields to fetch (beyond standard)
add_fields: ["priority", "status", "assigned_to"],
// Hide the name column
hide_name_column: true,
// Row indicator (colored dot)
get_indicator(doc) {
// MUST return [label, color, comma-separated-filter]
if (doc.status === "Completed") return ["Completed", "green", "status,=,Completed"];
if (doc.status === "Overdue") return ["Overdue", "red", "status,=,Overdue"];
return ["Open", "orange", "status,=,Open"];
},
// Custom column formatters
formatters: {
priority(val) {
const colors = { High: "red", Medium: "orange", Low: "green" };
return `<span class="indicator-pill ${colors[val] || ""}">${val}</span>`;
}
},
// Row action button
button: {
show(doc) { return doc.status === "Open"; },
get_label() { return __("Complete"); },
get_description(doc) { return __("Mark {0} as complete", [doc.name]); },
action(doc) {
frappe.xcall("myapp.api.complete_task", { task: doc.name })
.then(() => cur_list.refresh());
}
},
// Lifecycle hooks
onload(listview) {
listview.page.add_inner_button("Export", () => export_tasks());
},
refresh(listview) {
// Runs on every list refresh
},
// Default filters
filters: [["status", "!=", "Cancelled"]],
};
规则:get_indicator 务必返回一个包含 3 个元素的数组。第三个元素是点击后用于筛选的过滤字符串。
工作流 4:自定义页面(frappe.ui.Page)
步骤 1:在 hooks.py 中注册
# hooks.py
page_js = { "my-custom-page": "public/js/my_custom_page.js" }
步骤 2:创建页面定义
// myapp/myapp/my_custom_page/my_custom_page.js
frappe.pages["my-custom-page"].on_page_load = function(wrapper) {
let page = frappe.ui.make_app_page({
parent: wrapper,
title: "My Custom Page",
single_column: true,
});
// Primary action button
page.set_primary_action("Create", () => create_new(), "octicon octicon-plus");
// Secondary action
page.set_secondary_action("Refresh", () => refresh_data());
// Dropdown menu
page.add_menu_item("Export CSV", () => export_csv());
page.add_menu_item("Settings", () => frappe.set_route("Form", "My Settings"));
// Inner toolbar buttons
page.add_inner_button("Update All", () => update_all());
page.add_inner_button("New Post", () => new_post(), "Make"); // grouped
// Toolbar filter fields
let status_field = page.add_field({
label: "Status",
fieldtype: "Select",
fieldname: "status",
options: ["", "Open", "Closed", "Cancelled"],
change() { refresh_data(); }
});
// Status indicator
page.set_indicator("Active", "green");
// Content area
$(page.body).html(`<div class="my-page-content"></div>`);
// Load initial data
refresh_data();
};
主要页面方法
| 方法 | 用途 |
|---|---|
page.set_title(title) | 设置页面标题 |
page.set_indicator(label, color) | 状态徽标(绿色/红色/橙色/蓝色) |
page.set_primary_action(label, fn, icon) | 主操作按钮 |
page.set_secondary_action(label, fn) | 次要按钮 |
page.add_menu_item(label, fn) | 下拉菜单项 |
page.add_inner_button(label, fn, group) | 工具栏按钮(可选分组) |
page.add_field({...}) | 向工具栏添加筛选/输入字段 |
page.get_form_values() | 获取所有工具栏字段值 |
page.clear_fields() | 移除所有工具栏字段 |
page.clear_primary_action() | 移除主按钮 |
工作流 5:日历视图
在 DocType 目录中创建 {doctype}_calendar.js:
// myapp/doctype/event/event_calendar.js
frappe.views.calendar["Event"] = {
field_map: {
start: "starts_on",
end: "ends_on",
id: "name",
title: "subject",
allDay: "all_day",
color: "color",
},
gantt: true, // Enable Gantt view toggle
get_events_method: "myapp.api.get_events", // Optional custom event source
filters: [
{ fieldtype: "Link", fieldname: "event_type", label: "Type",
options: "Event Type" }
],
};
规则:务必将 start 和 end 映射到 DocType 上实际的 Date 或 Datetime 字段。缺少映射会导致日历空白。
工作流 6:看板
看板适用于任何包含 Select 字段的 DocType。无需编写代码:
- 打开列表视图 → 侧边栏 → Kanban → New Kanban Board
- 选择 Select 字段(例如
status)——选项会成为列 - 保存——卡片可在各列之间拖动
规则:绝不要为不含 Select 字段的 DocType 创建看板。程序化配置请参见 references/examples.md。
工作流 7:实时更新(Socket.IO)
服务端:发布事件
# Broadcast to all users
frappe.publish_realtime("task_updated", {"task": task.name, "status": "Done"})
# Send to specific user
frappe.publish_realtime("notification", {"msg": "Your report is ready"},
user="[email protected]")
# Send to users viewing a specific document
frappe.publish_realtime("doc_updated", {"field": "status"},
doctype="Task", docname="TASK-001")
# ALWAYS use after_commit=True in document events
frappe.publish_realtime("order_created", message, after_commit=True)
客户端:订阅事件
// Listen for events
frappe.realtime.on("task_updated", (data) => {
frappe.show_alert({ message: `Task ${data.task}: ${data.status}`, indicator: "green" });
cur_list && cur_list.refresh();
});
// Stop listening
frappe.realtime.off("task_updated");
进度指示器
# Server: publish progress during long operations
def process_items(items):
total = len(items)
for i, item in enumerate(items):
process(item)
frappe.publish_progress(
percent=(i + 1) / total * 100,
title="Processing Items",
description=f"Processing {item.name}",
)
规则:从文档事件发布时,务必使用 after_commit=True。否则,即使事务回滚,事件仍会触发。
实时房间
| 房间 | 受众 | 用例 |
|---|---|---|
| (default) | 所有系统用户 | 全局通知 |
user:{email} | 单个用户 | 个人提醒 |
doctype:{dt} | 正在查看列表的用户 | 列表刷新触发器 |
doc:{dt}/{name} | 正在查看文档的用户 | 文档变更提醒 |
website | 所有用户,包括访客 | 公开公告 |
工作流 8:扫描器 API(条形码/二维码)
// Single scan — closes after first scan
new frappe.ui.Scanner({
dialog: true, multiple: false,
on_scan(data) {
frappe.set_route("Form", "Item", data.decodedText);
}
});
// Continuous scanning — stays open for multiple scans
let scanner = new frappe.ui.Scanner({
dialog: true, multiple: true,
on_scan(data) { add_item_to_list(data.decodedText); }
});
// Stop: scanner.stop_scan() or close the dialog
规则:对于单项查询,始终将 multiple: false。完整的条形码与 Stock-Entry 示例请参阅 references/examples.md。
反模式摘要
| 反模式 | 正确做法 |
|---|---|
使用 frappe.msgprint 显示错误 | 使用 frappe.throw — 它会停止执行 |
| 异步操作完成前隐藏对话框 | 在回调中隐藏:callback() { d.hide(); } |
| 在对话框中进行同步 API 调用 | 始终使用 frappe.call / frappe.xcall(异步) |
表格字段缺少 in_list_view | 对可见列设置 in_list_view: 1 |
使用 publish_realtime 时未设置 after_commit | 在文档事件中始终使用 after_commit=True |
| DocType 上没有 Select 字段却使用看板 | 看板要求使用 Select 字段作为列 |
| 日历的 field_map 缺少开始/结束字段 | 始终同时映射 start 和 end 字段 |
get_indicator 返回 2 个元素的数组 | 始终返回 3 个元素:[标签、颜色、筛选条件] |
参考文件
references/controls-api.md— 通过frappe.ui.form.make_control()使用独立控件、完整的控件类型参考、控件方法和事件references/tree-view.md— Tree DocType 配置、frappe.views.TreeViewAPI、frappe.ui.Tree底层 API、树节点操作references/workflows.md— 扩展工作流指南references/examples.md— 完整代码示例references/decision-tree.md— 完整的 UI 组件决策树references/anti-patterns.md— 附带代码示例的扩展反模式说明
另请参阅
frappe-impl-clientscripts— 表单级客户端脚本frappe-syntax-clientscripts— 客户端 API 语法参考frappe-impl-hooks— 页面和路由的钩子注册