关于 frappe-ui-patterns
frappe-ui-patterns 是整理自官方 Frappe 应用的 UI/UX 模式与设计指南,面向构建 Frappe 应用界面的开发者。内容涵盖常见布局、组件使用、状态呈现及响应式设计注意事项。
主要功能
- 01应用外壳与导航布局模式
- 02列表、看板和详情面板模式
- 03表单与活动动态模式
- 04空状态、加载状态和状态颜色指南
- 05响应式设计与组件参考
适用场景
- 01为新 Frappe 应用设计界面
- 02构建 CRUD 列表、详情和表单
- 03统一 CRM 或 Helpdesk 风格的交互
- 04检查加载、空数据及移动端状态
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ lubusin/ frappe-skills --skill frappe-ui-patterns安装后在 Claude Code 中输入/frappe-ui-patterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/app-shell-patterns.mdcomponent-patterns.mdmobile-patterns.md详细介绍
Frappe UI 模式
从官方 Frappe 应用中提取的 UI/UX 模式和设计指南。
适用场景
- 为新的 Frappe 应用设计 UI
- 使用 Frappe UI 构建 CRUD 界面
- 实现列表视图、详情面板或表单
- 确保与 CRM、Helpdesk、HRMS 的 UX 一致
- 选择组件模式和布局
所需输入
- 应用类型(类似 CRM、类似 Helpdesk、数据管理)
- 关键实体及其关系
- 主要用户工作流
参考应用
可研究以下官方应用中的模式:
| 应用 | Repo | 关键模式 |
|---|---|---|
| Frappe CRM | github.com/frappe/crm | 潜在客户/商机管道、看板、活动动态 |
| Frappe Helpdesk | github.com/frappe/helpdesk | 工单队列、SLA 指示器、客服视图 |
| Frappe HRMS | github.com/frappe/hrms | 员工自助服务、审批、仪表板 |
| Frappe Insights | github.com/frappe/insights | 查询构建器、可视化、仪表板 |
| Frappe Builder | github.com/frappe/builder | 拖放式界面、属性面板 |
流程
0) 应用外壳结构
所有 Frappe 应用都遵循一致的外壳结构:
┌─────────────────────────────────────────────────────────────┐
│ Header (App title, search, user menu) │
├──────────────┬──────────────────────────────────────────────┤
│ │ │
│ Sidebar │ Main Content │
│ │ │
│ - Nav items │ ┌─────────────────┬──────────────────────┐ │
│ - Filters │ │ List View │ Detail Panel │ │
│ - Actions │ │ │ │ │
│ │ │ │ │ │
│ │ └─────────────────┴──────────────────────┘ │
│ │ │
└──────────────┴──────────────────────────────────────────────┘
实现:
<template>
<div class="flex h-screen">
<Sidebar />
<div class="flex-1 flex">
<ListView
:class="selectedDoc ? 'w-1/2' : 'w-full'"
@select="selectDoc"
/>
<DetailPanel
v-if="selectedDoc"
:doc="selectedDoc"
class="w-1/2 border-l"
/>
</div>
</div>
</template>
1) 侧边栏模式
标准结构:
<template>
<aside class="w-56 border-r bg-gray-50 flex flex-col">
<div class="p-4 border-b">
<h1 class="font-semibold">My App</h1>
</div>
<nav class="flex-1 p-2">
<SidebarLink
v-for="item in navItems"
:key="item.name"
:label="item.label"
:icon="item.icon"
:to="item.route"
:count="item.count"
/>
</nav>
<div v-if="filters.length" class="p-2 border-t">
<p class="text-xs text-gray-500 px-2 mb-1">Filters</p>
<SidebarLink
v-for="filter in filters"
:key="filter.name"
:label="filter.label"
:count="filter.count"
@click="applyFilter(filter)"
/>
</div>
<div class="p-2 border-t">
<UserMenu />
</div>
</aside>
</template>
CRM 导航项示例:
- 潜在客户(带数量徽标)
- 商机(带数量徽标)
- 联系人
- 组织
- 活动
- 设置
2) 列表视图模式
带筛选条件的标准列表:
<template>
<div class="flex-1 flex flex-col">
<div class="flex items-center justify-between p-4 border-b">
<div class="flex items-center gap-2">
<Input
type="search"
placeholder="Search..."
v-model="searchQuery"
:debounce="300"
/>
<FilterDropdown :filters="availableFilters" v-model="activeFilters" />
</div>
<div class="flex items-center gap-2">
<ViewToggle v-model="viewMode" :options="['list', 'kanban', 'grid']" />
<Button variant="solid" @click="createNew">
<template #prefix><FeatherIcon name="plus" /></template>
New
</Button>
</div>
</div>
<ListView v-if="viewMode === 'list'" :data="items" @row-click="select" />
<KanbanView v-else-if="viewMode === 'kanban'" :data="items" :columns="stages" />
<GridView v-else :data="items" @card-click="select" />
</div>
</template>
列表行结构:
<template>
<div class="flex items-center p-3 hover:bg-gray-50 cursor-pointer border-b">
<Checkbox v-if="selectable" v-model="selected" class="mr-3" />
<Avatar :label="row.name" :image="row.image" class="mr-3" />
<div class="flex-1 min-w-0">
<p class="font-medium truncate">{{ row.title }}</p>
<p class="text-sm text-gray-500 truncate">{{ row.subtitle }}</p>
</div>
<Badge :variant="statusVariant">{{ row.status }}</Badge>
<span class="text-sm text-gray-500 ml-4">{{ timeAgo(row.modified) }}</span>
<Dropdown :options="rowActions" class="ml-2">
<Button variant="ghost" icon="more-horizontal" />
</Dropdown>
</div>
</template>
3) 看板视图模式
使用场景: CRM(商机)、Helpdesk(按状态分类的工单)
<template>
<div class="flex overflow-x-auto p-4 gap-4">
<div
v-for="column in columns"
:key="column.name"
class="flex-shrink-0 w-72 bg-gray-100 rounded-lg"
>
<div class="p-3 font-medium flex items-center justify-between">
<span>{{ column.label }}</span>
<Badge>{{ column.items.length }}</Badge>
</div>
<div class="p-2 space-y-2 min-h-[200px]">
<KanbanCard
v-for="item in column.items"
:key="item.name"
:data="item"
@click="select(item)"
draggable
@dragend="handleDrop"
/>
</div>
<Button variant="ghost" class="w-full" @click="addTo(column)">
+ Add {{ column.singular }}
</Button>
</div>
</div>
</template>
看板卡片结构:
<template>
<div class="bg-white rounded-lg p-3 shadow-sm border cursor-pointer hover:shadow">
<p class="font-medium mb-1">{{ data.title }}</p>
<p class="text-sm text-gray-500 mb-2">{{ data.subtitle }}</p>
<div class="flex items-center justify-between">
<Avatar :label="data.assigned_to" size="sm" />
<span class="text-xs text-gray-400">{{ data.due_date }}</span>
</div>
</div>
</template>
4) 详情面板 / 侧边面板
分栏视图模式(CRM/Helpdesk 风格):
<template>
<aside class="w-[480px] border-l bg-white flex flex-col">
<div class="flex items-center justify-between p-4 border-b">
<h2 class="font-semibold">{{ doc.name }}</h2>
<Button variant="ghost" icon="x" @click="$emit('close')" />
</div>
<Tabs v-model="activeTab">
<Tab name="details" label="Details" />
<Tab name="activity" label="Activity" />
<Tab name="notes" label="Notes" />
</Tabs>
<div class="flex-1 overflow-auto p-4">
<DetailsTab v-if="activeTab === 'details'" :doc="doc" />
<ActivityFeed v-else-if="activeTab === 'activity'" :doctype="doctype" :name="doc.name" />
<NotesTab v-else :doctype="doctype" :name="doc.name" />
</div>
<div class="p-4 border-t flex justify-end gap-2">
<Button @click="edit">Edit</Button>
<Button variant="solid" @click="primaryAction">{{ primaryActionLabel }}</Button>
</div>
</aside>
</template>
5) 表单模式
标准表单布局:
<template>
<div class="max-w-2xl mx-auto p-6">
<div class="mb-6">
<h1 class="text-xl font-semibold">{{ isNew ? 'New' : 'Edit' }} {{ doctype }}</h1>
</div>
<FormSection title="Basic Information">
<div class="grid grid-cols-2 gap-4">
<FormControl label="Name" v-model="doc.name" :required="true" />
<FormControl label="Status" type="select" v-model="doc.status" :options="statusOptions" />
</div>
<FormControl label="Description" type="textarea" v-model="doc.description" class="mt-4" />
</FormSection>
<FormSection title="Details" collapsible>
</FormSection>
<div class="flex justify-end gap-2 mt-6 pt-4 border-t">
<Button @click="cancel">Cancel</Button>
<Button variant="solid" @click="save" :loading="saving">Save</Button>
</div>
</div>
</template>
FormSection 组件:
<template>
<div class="mb-6">
<div
class="flex items-center justify-between mb-3 cursor-pointer"
@click="collapsible && (collapsed = !collapsed)"
>
<h3 class="font-medium text-gray-700">{{ title }}</h3>
<FeatherIcon v-if="collapsible" :name="collapsed ? 'chevron-down' : 'chevron-up'" />
</div>
<div v-show="!collapsed">
<slot />
</div>
</div>
</template>
6) 活动动态模式
所有应用都使用该模式跟踪变更:
<template>
<div class="space-y-4">
<div class="flex gap-3">
<Avatar :label="$user.name" />
<div class="flex-1">
<Textarea v-model="newComment" placeholder="Add a comment..." rows="2" />
<Button class="mt-2" @click="addComment" :disabled="!newComment">Comment</Button>
</div>
</div>
<div v-for="item in activities" :key="item.name" class="flex gap-3">
<Avatar :label="item.owner" size="sm" />
<div class="flex-1">
<div class="flex items-baseline gap-2">
<span class="font-medium text-sm">{{ item.owner }}</span>
<span class="text-xs text-gray-400">{{ timeAgo(item.creation) }}</span>
</div>
<CommentContent v-if="item.type === 'comment'" :content="item.content" />
<StatusChange v-else-if="item.type === 'status'" :from="item.from" :to="item.to" />
<FieldChange v-else-if="item.type === 'change'" :field="item.field" :value="item.value" />
</div>
</div>
</div>
</template>
7) 空状态
务必提供有帮助的空状态:
<template>
<div class="flex flex-col items-center justify-center h-64 text-center">
<FeatherIcon name="inbox" class="w-12 h-12 text-gray-300 mb-4" />
<h3 class="font-medium text-gray-700 mb-1">{{ title }}</h3>
<p class="text-sm text-gray-500 mb-4">{{ description }}</p>
<Button v-if="action" variant="solid" @click="action.handler">
{{ action.label }}
</Button>
</div>
</template>
<EmptyState
title="No leads yet"
description="Create your first lead to get started"
:action="{ label: 'Create Lead', handler: createLead }"
/>
8) 加载状态
使用骨架屏提升感知性能:
<template>
<div v-if="loading" class="space-y-2 p-4">
<div v-for="i in 5" :key="i" class="flex items-center gap-3 p-3">
<Skeleton class="w-10 h-10 rounded-full" />
<div class="flex-1">
<Skeleton class="h-4 w-1/3 mb-2" />
<Skeleton class="h-3 w-1/2" />
</div>
</div>
</div>
<ListView v-else :data="data" />
</template>
9) 颜色和状态约定
| 状态类型 | 颜色 | 用途 |
|---|---|---|
| 成功/活动 | 绿色(bg-green-100 text-green-700) | 已完成、活动中、已解决 |
| 警告/待处理 | 黄色(bg-yellow-100 text-yellow-700) | 待处理、进行中、即将到期 |
| 错误/阻止 | 红色(bg-red-100 text-red-700) | 失败、已阻止、已逾期 |
| 信息/默认 | 蓝色(bg-blue-100 text-blue-700) | 新建、打开、信息 |
| 中性 | 灰色(bg-gray-100 text-gray-700) | 草稿、已取消、已关闭 |
Badge 组件用法:
<Badge variant="success">Active</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="error">Overdue</Badge>
<Badge variant="info">New</Badge>
<Badge variant="subtle">Draft</Badge>
10) 响应式模式
移动端优先考量:
<template>
<Sidebar v-if="!isMobile" />
<Drawer v-else v-model="sidebarOpen">
<Sidebar />
</Drawer>
<div :class="isMobile ? 'flex-col' : 'flex'">
<ListView v-show="!isMobile || !selectedDoc" />
<DetailPanel v-if="selectedDoc" :fullScreen="isMobile" />
</div>
</template>
组件参考
请一致地使用以下 Frappe UI 组件:
| 组件 | 用途 |
|---|---|
<Button> | 所有操作,变体包括:solid、subtle、ghost |
<Input> | 文本输入、搜索框 |
<FormControl> | 带标签和验证的表单字段 |
<Select> | 下拉菜单、状态选择器 |
<Checkbox> | 布尔值输入、批量选择 |
<Avatar> | 用户图片、实体图标 |
<Badge> | 状态指示器、数量 |
<Dropdown> | 操作菜单、上下文菜单 |
<Dialog> | 模态确认框、表单 |
<Tabs> | 内容组织 |
<Tooltip> | 有用提示、截断文本 |
验证
- 应用外壳符合标准布局(侧边栏 + 主区域 + 可选详情面板)
- 列表视图包含搜索、筛选条件、视图切换和创建按钮
- 详情面板包含标签页(详情、活动、备注)
- 空状态提供有用信息和操作
- 加载状态使用骨架屏,而非旋转指示器
- 状态颜色遵循约定
- 表单分区清晰且风格一致
- 已考虑移动端体验
故障模式 / 调试
- 间距不一致:使用 TailwindCSS 间距尺度(p-2、p-4、gap-2、gap-4)
- 组件错误:查阅 Frappe UI 文档,确认正确的组件和属性
- 响应式布局异常:在移动端断点进行测试;使用
sm:、md:、lg:前缀 - 缺少状态:确保处理加载、空内容和错误状态
升级处理
- 组件实现 →
frappe-frontend-development - 后端 API 集成 →
frappe-api-development - 企业级工作流 →
frappe-enterprise-patterns
参考资料
- references/app-shell-patterns.md — Shell 布局详解
- references/component-patterns.md — 组件使用指南
- references/mobile-patterns.md — 响应式设计
注意事项
- 先研究官方应用:设计 UI 前,先查看 CRM、Helpdesk 或相关官方应用,了解其模式
- 使用 Frappe UI 组件:如果 Frappe UI 有对应组件,切勿创建自定义组件
- 遵循间距规范:使用一致的内边距和外边距(以 4px 为增量)
- 提供所有状态:每个视图都需要加载、空内容和错误状态
- 保持导航一致:侧边栏结构应与官方应用一致
- 进行响应式测试:确保移动端体验良好
常见错误
| 错误 | 问题所在 | 修复方法 |
|---|---|---|
| 自定义应用 Shell 设计 | 用户不熟悉这种 UX | 复制 CRM/Helpdesk 的 Shell 结构 |
| 缺少空状态 | 没有数据时用户会感到困惑 | 添加带操作的 EmptyState 组件 |
| 用旋转加载图标代替骨架屏 | 加载体验突兀 | 使用 Skeleton 组件表示加载状态 |
| 状态颜色不一致 | 用户会感到困惑 | 遵循颜色规范表 |
| 深层嵌套但没有面包屑 | 用户容易迷失方向 | 添加面包屑导航 |
| 过度使用模态框 | 打断工作流 | 详情视图优先使用侧边面板 |
| 不支持键盘导航 | 存在无障碍问题 | 确保关键流程支持 Tab/Enter 操作 |
评论
支持 Markdown