mcpField
F
frappe-impl-ui-components

frappe-impl-ui-components

github.com/impertio-studio开发198111 次安装30 周前更新
代码仓库

关于 frappe-impl-ui-components

面向 Frappe 界面开发者的客户端 UI 组件与实时更新实现技能,汇集常见工作流和 API 指引,涵盖对话框、页面、列表视图及事件订阅。

主要功能

  1. 01对话框与表单输入模式
  2. 02消息、提醒与确认操作
  3. 03列表视图自定义
  4. 04自定义页面与日历
  5. 05看板配置
  6. 06实时事件与扫描器 API

适用场景

  1. 01在对话框中收集输入
  2. 02自定义 DocType 列表视图
  3. 03构建自定义应用页面
  4. 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/
.gitkeep
anti-patterns.md
controls-api.md
decision-tree.md
examples.md
tree-view.md
workflows.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。无需编写代码:

  1. 打开列表视图 → 侧边栏 → Kanban → New Kanban Board
  2. 选择 Select 字段(例如 status)——选项会成为列
  3. 保存——卡片可在各列之间拖动

规则:绝不要为不含 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.TreeView API、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 — 页面和路由的钩子注册

评论

支持 Markdown