关于 frappe-desk-customization
面向 Frappe Desk 开发者的界面定制指南,涵盖表单脚本、列表视图、对话框和客户端 API。适用于按站点或应用需求扩展管理界面交互。
主要功能
- 01为表单添加按钮和客户端校验
- 02动态筛选 Link 字段及控制字段显示
- 03构建对话框并调用服务端方法
- 04定制列表视图标记和批量操作
- 05通过 hooks 注入脚本并监听实时事件
适用场景
- 01按条件显示表单操作按钮
- 02根据字段值筛选关联记录
- 03为用户收集输入并调用 RPC
- 04定制列表状态标记或操作
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ lubusin/ frappe-skills --skill frappe-desk-customization安装后在 Claude Code 中输入/frappe-desk-customization确认已加载。
附带文件references/desk.mdreferences/js-api.md
详细介绍
Frappe Desk 自定义
通过表单脚本、列表视图、对话框和客户端 API 自定义 Frappe Desk 管理界面。
使用时机
- 为表单添加自定义按钮或操作
- 动态筛选 Link 字段
- 根据条件切换字段可见性
- 自定义列表视图指示器和批量操作
- 构建交互式对话框和提示词
- 在保存前添加客户端验证
- 通过 hooks 向其他应用的 DocType 注入脚本
所需输入
- 要自定义的目标 DocType
- 脚本是应用级(受版本控制)还是 Client Script(特定站点)
- 要挂接的事件(refresh、validate、字段变更等)
- UI 行为要求(按钮、筛选器、可见性)
流程
0)选择脚本类型
| 类型 | 位置 | 受版本控制 | 使用场景 |
|---|---|---|---|
| 应用级表单脚本 | <app>/<module>/doctype/<doctype>/<doctype>.js | 是 | 标准应用行为 |
| Client Script | DocType:Client Script | 否(DB) | 特定站点的自定义 |
| 通过 hook 注入的脚本 | 通过 hooks.py 中的 doctype_js | 是 | 扩展其他应用的 DocType |
1)编写表单脚本
frappe.ui.form.on("My DocType", {
// Called once during form setup
setup(frm) {
frm.set_query("customer", function() {
return {
filters: { "status": "Active" }
};
});
},
// Called every time form loads or refreshes
refresh(frm) {
if (frm.doc.status === "Draft") {
frm.add_custom_button(__("Submit for Review"), function() {
frappe.call({
method: "my_app.api.submit_for_review",
args: { name: frm.doc.name },
callback(r) {
frm.reload_doc();
}
});
}, __("Actions"));
}
// Toggle field visibility
frm.toggle_display("discount_section", frm.doc.grand_total > 1000);
// Set field properties
frm.set_df_property("notes", "read_only", frm.doc.docstatus === 1);
},
// Called before save — return false to cancel
validate(frm) {
if (frm.doc.end_date < frm.doc.start_date) {
frappe.msgprint(__("End date must be after start date"));
frappe.validated = false;
}
},
// Field change handler (use fieldname as key)
customer(frm) {
if (frm.doc.customer) {
frappe.db.get_value("Customer", frm.doc.customer, "territory",
function(r) {
frm.set_value("territory", r.territory);
}
);
}
},
// Before save hook
before_save(frm) {
frm.doc.full_name = `${frm.doc.first_name} ${frm.doc.last_name}`;
},
// After save hook
after_save(frm) {
frappe.show_alert({
message: __("Document saved successfully"),
indicator: "green"
});
}
});
// Child table events
frappe.ui.form.on("My DocType Item", {
qty(frm, cdt, cdn) {
let row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, "amount", row.qty * row.rate);
calculate_total(frm);
},
items_remove(frm) {
calculate_total(frm);
}
});
function calculate_total(frm) {
let total = 0;
(frm.doc.items || []).forEach(row => {
total += row.amount || 0;
});
frm.set_value("grand_total", total);
}
2)构建对话框和提示词
// Simple prompt
frappe.prompt(
{ fieldname: "reason", fieldtype: "Small Text", label: "Reason", reqd: 1 },
function(values) {
frappe.call({
method: "my_app.api.reject",
args: { name: frm.doc.name, reason: values.reason }
});
},
__("Rejection Reason"),
__("Reject")
);
// Multi-field dialog
let d = new frappe.ui.Dialog({
title: __("Configure Settings"),
fields: [
{ fieldname: "email", fieldtype: "Data", options: "Email", label: "Email", reqd: 1 },
{ fieldname: "frequency", fieldtype: "Select", options: "Daily\nWeekly\nMonthly", label: "Frequency" },
{ fieldname: "active", fieldtype: "Check", label: "Active", default: 1 }
],
primary_action_label: __("Save"),
primary_action(values) {
frappe.call({
method: "my_app.api.save_settings",
args: values,
callback() {
d.hide();
frappe.show_alert({ message: __("Settings saved"), indicator: "green" });
}
});
}
});
d.show();
// Confirmation dialog
frappe.confirm(
__("Are you sure you want to delete this?"),
function() { /* Yes */ },
function() { /* No */ }
);
3)发起服务器调用
// Standard call (callback)
frappe.call({
method: "my_app.api.get_stats",
args: { customer: frm.doc.customer },
freeze: true,
freeze_message: __("Loading..."),
callback(r) {
if (r.message) {
frm.set_value("total_orders", r.message.total);
}
}
});
// Promise-based call
let result = await frappe.xcall("my_app.api.get_stats", {
customer: frm.doc.customer
});
4)自定义列表视图
// my_app/public/js/sample_doc_list.js
// or via hooks: doctype_list_js = {"Sample Doc": "public/js/sample_doc_list.js"}
frappe.listview_settings["Sample Doc"] = {
// Status indicator colors
get_indicator(doc) {
if (doc.status === "Open") return [__("Open"), "orange", "status,=,Open"];
if (doc.status === "Closed") return [__("Closed"), "green", "status,=,Closed"];
return [__("Draft"), "grey", "status,=,Draft"];
},
// Add bulk actions
onload(listview) {
listview.page.add_action_item(__("Mark as Closed"), function() {
let names = listview.get_checked_items(true);
frappe.call({
method: "my_app.api.bulk_close",
args: { names },
callback() { listview.refresh(); }
});
});
},
// Hide default "New" button
hide_name_column: true
};
5)使用实时事件
// Listen for server-side events
frappe.realtime.on("export_complete", function(data) {
frappe.show_alert({
message: __("Export complete: {0} records", [data.count]),
indicator: "green"
});
});
6)通过 hooks 注入脚本
若要扩展其他应用中的 DocType,而不修改它:
# hooks.py
doctype_js = {
"Sales Order": "public/js/sales_order_custom.js"
}
doctype_list_js = {
"Sales Order": "public/js/sales_order_list_custom.js"
}
# Rebuild assets after adding hook scripts
bench build --app my_app
7)导航和路由
// Navigate to a document
frappe.set_route("Form", "Sales Order", "SO-001");
// Navigate to list with filters
frappe.route_options = { "status": "Open" };
frappe.set_route("List", "Sales Order");
// Get current route
let route = frappe.get_route();
验证
- 表单脚本加载时没有 JS 控制台错误
- 自定义按钮在正确的条件下显示
- 字段可见性切换正常
- Link 字段筛选器返回正确选项
- 验证可阻止无效保存
- 列表视图指示器正确显示
- 对话框可打开、收集输入并提交
故障模式 / 调试
- 脚本未加载:检查文件路径是否与 DocType 匹配;运行
bench build - 按钮未显示:检查
refresh中的条件逻辑;验证frm.doc.docstatus - 事件未触发:确认事件名称完全匹配(区分大小写)
- Hook 脚本被忽略:检查
hooks.py路径;重新构建资源 frappe.call失败:检查方法路径;确认服务器端方法使用了@frappe.whitelist()
升级处理
- 服务器端控制器逻辑 →
frappe-doctype-development - RPC 端点实现 →
frappe-api-development - Frappe UI(Vue 3)前端 →
frappe-frontend-development
参考资料
- references/desk.md — Desk UI 视图和脚本编写
- references/js-api.md — JavaScript 客户端 API 参考
使用规范
- 使用
frm.doc,不要直接使用doc:始终通过frm.doc访问文档,以保持一致性和响应性 - 保存前进行验证:在
validate事件中使用frm.validate(),不要在before_save中使用 - 注意异步操作:
frappe.call()是异步的;若要按顺序执行操作,请使用回调或 async/await - 字段变更后刷新:以编程方式修改后调用
frm.refresh_field()或frm.refresh_fields() - 适当检查
frm.is_new():某些操作仅适用于已保存的文档
常见错误
| 错误 | 失败原因 | 修复方法 |
|---|---|---|
set_value 后未调用 frm.refresh_field() | UI 不会更新 | 调用 frm.set_value() 后调用 frm.refresh_field('fieldname') |
| 事件挂钩名称错误 | 事件永远不会触发 | 使用准确名称:refresh、validate、onload、before_save |
| 使用同步调用阻塞 UI | 页面冻结 | 使用 frappe.call(),并设置 async: true(默认值) |
使用 cur_frm 而非 frm | 在对话框/多个表单中会出错 | 始终使用传递给处理程序的 frm 参数 |
未检查 frm.doc.docstatus | 按钮会显示在已提交的文档上 | 显示编辑操作前检查 frm.doc.docstatus == 0 |
console.log(frm.doc) 显示过时数据 | 造成调试困惑 | 使用 frm.reload_doc() 或检查网络响应 |
评论
支持 Markdown