frappe-ui-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFrappe UI Patterns
Frappe UI 模式
UI/UX patterns and design guidelines extracted from official Frappe applications.
从Frappe官方应用中提炼的UI/UX模式与设计指南。
When to use
适用场景
- Designing UI for a new Frappe app
- Building CRUD interfaces with Frappe UI
- Implementing list views, detail panels, or forms
- Ensuring consistent UX with CRM, Helpdesk, HRMS
- Choosing component patterns and layouts
- 为新Frappe应用设计UI
- 基于Frappe UI构建CRUD界面
- 实现列表视图、详情面板或表单
- 确保与CRM、Helpdesk、HRMS的UX一致性
- 选择组件模式与布局方案
Inputs required
所需输入
- App type (CRM-like, Helpdesk-like, data management)
- Key entities and their relationships
- Primary user workflows
- 应用类型(类CRM、类Helpdesk、数据管理类)
- 核心实体及其关系
- 主要用户工作流
Reference apps
参考应用
Study these official apps for patterns:
| App | Repo | Key Patterns |
|---|---|---|
| Frappe CRM | github.com/frappe/crm | Lead/Deal pipelines, Kanban, activity feeds |
| Frappe Helpdesk | github.com/frappe/helpdesk | Ticket queues, SLA indicators, agent views |
| Frappe HRMS | github.com/frappe/hrms | Employee self-service, approvals, dashboards |
| Frappe Insights | github.com/frappe/insights | Query builders, visualizations, dashboards |
| Frappe Builder | github.com/frappe/builder | Drag-drop interfaces, property panels |
可研究这些官方应用获取设计模式:
| 应用 | 仓库 | 核心模式 |
|---|---|---|
| Frappe CRM | github.com/frappe/crm | 线索/客户机会流水线、看板、活动流 |
| Frappe Helpdesk | github.com/frappe/helpdesk | 工单队列、SLA指标、Agent视图 |
| Frappe HRMS | github.com/frappe/hrms | 员工自助服务、审批流程、数据看板 |
| Frappe Insights | github.com/frappe/insights | 查询构建器、数据可视化、数据看板 |
| Frappe Builder | github.com/frappe/builder | 拖拽式界面、属性面板 |
Procedure
实施步骤
0) App shell structure
0) 应用外壳结构
All Frappe apps follow a consistent shell:
┌─────────────────────────────────────────────────────────────┐
│ Header (App title, search, user menu) │
├──────────────┬──────────────────────────────────────────────┤
│ │ │
│ Sidebar │ Main Content │
│ │ │
│ - Nav items │ ┌─────────────────┬──────────────────────┐ │
│ - Filters │ │ List View │ Detail Panel │ │
│ - Actions │ │ │ │ │
│ │ │ │ │ │
│ │ └─────────────────┴──────────────────────┘ │
│ │ │
└──────────────┴──────────────────────────────────────────────┘Implementation:
vue
<template>
<div class="flex h-screen">
<!-- Sidebar -->
<Sidebar />
<!-- Main content with optional split view -->
<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>所有Frappe应用遵循统一的外壳布局:
┌─────────────────────────────────────────────────────────────┐
│ Header (App title, search, user menu) │
├──────────────┬──────────────────────────────────────────────┤
│ │ │
│ Sidebar │ Main Content │
│ │ │
│ - Nav items │ ┌─────────────────┬──────────────────────┐ │
│ - Filters │ │ List View │ Detail Panel │ │
│ - Actions │ │ │ │ │
│ │ │ │ │ │
│ │ └─────────────────┴──────────────────────┘ │
│ │ │
└──────────────┴──────────────────────────────────────────────┘实现代码:
vue
<template>
<div class="flex h-screen">
<!-- Sidebar -->
<Sidebar />
<!-- Main content with optional split view -->
<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) Sidebar patterns
1) 侧边栏模式
Standard structure:
vue
<template>
<aside class="w-56 border-r bg-gray-50 flex flex-col">
<!-- App logo/title -->
<div class="p-4 border-b">
<h1 class="font-semibold">My App</h1>
</div>
<!-- Primary navigation -->
<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>
<!-- Quick filters (context-dependent) -->
<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>
<!-- User/settings at bottom -->
<div class="p-2 border-t">
<UserMenu />
</div>
</aside>
</template>CRM example nav items:
- Leads (with count badge)
- Deals (with count badge)
- Contacts
- Organizations
- Activities
- Settings
标准结构:
vue
<template>
<aside class="w-56 border-r bg-gray-50 flex flex-col">
<!-- App logo/title -->
<div class="p-4 border-b">
<h1 class="font-semibold">My App</h1>
</div>
<!-- Primary navigation -->
<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>
<!-- Quick filters (context-dependent) -->
<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>
<!-- User/settings at bottom -->
<div class="p-2 border-t">
<UserMenu />
</div>
</aside>
</template>CRM示例导航项:
- 线索(带数量徽章)
- 客户机会(带数量徽章)
- 联系人
- 组织
- 活动
- 设置
2) List view patterns
2) 列表视图模式
Standard list with filters:
vue
<template>
<div class="flex-1 flex flex-col">
<!-- Toolbar -->
<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>
<!-- View modes -->
<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>List row structure:
vue
<template>
<div class="flex items-center p-3 hover:bg-gray-50 cursor-pointer border-b">
<!-- Selection checkbox (for bulk actions) -->
<Checkbox v-if="selectable" v-model="selected" class="mr-3" />
<!-- Avatar/icon -->
<Avatar :label="row.name" :image="row.image" class="mr-3" />
<!-- Primary content -->
<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>
<!-- Status badge -->
<Badge :variant="statusVariant">{{ row.status }}</Badge>
<!-- Metadata -->
<span class="text-sm text-gray-500 ml-4">{{ timeAgo(row.modified) }}</span>
<!-- Actions dropdown -->
<Dropdown :options="rowActions" class="ml-2">
<Button variant="ghost" icon="more-horizontal" />
</Dropdown>
</div>
</template>带筛选的标准列表:
vue
<template>
<div class="flex-1 flex flex-col">
<!-- Toolbar -->
<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>
<!-- View modes -->
<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>列表行结构:
vue
<template>
<div class="flex items-center p-3 hover:bg-gray-50 cursor-pointer border-b">
<!-- Selection checkbox (for bulk actions) -->
<Checkbox v-if="selectable" v-model="selected" class="mr-3" />
<!-- Avatar/icon -->
<Avatar :label="row.name" :image="row.image" class="mr-3" />
<!-- Primary content -->
<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>
<!-- Status badge -->
<Badge :variant="statusVariant">{{ row.status }}</Badge>
<!-- Metadata -->
<span class="text-sm text-gray-500 ml-4">{{ timeAgo(row.modified) }}</span>
<!-- Actions dropdown -->
<Dropdown :options="rowActions" class="ml-2">
<Button variant="ghost" icon="more-horizontal" />
</Dropdown>
</div>
</template>3) Kanban view patterns
3) 看板视图模式
Used in: CRM (Deals), Helpdesk (Tickets by status)
vue
<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"
>
<!-- Column header -->
<div class="p-3 font-medium flex items-center justify-between">
<span>{{ column.label }}</span>
<Badge>{{ column.items.length }}</Badge>
</div>
<!-- Cards -->
<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>
<!-- Add new in column -->
<Button variant="ghost" class="w-full" @click="addTo(column)">
+ Add {{ column.singular }}
</Button>
</div>
</div>
</template>Kanban card structure:
vue
<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>应用场景: CRM(客户机会)、Helpdesk(按状态分类的工单)
vue
<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"
>
<!-- Column header -->
<div class="p-3 font-medium flex items-center justify-between">
<span>{{ column.label }}</span>
<Badge>{{ column.items.length }}</Badge>
</div>
<!-- Cards -->
<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>
<!-- Add new in column -->
<Button variant="ghost" class="w-full" @click="addTo(column)">
+ Add {{ column.singular }}
</Button>
</div>
</div>
</template>看板卡片结构:
vue
<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) Detail panel / side panel
4) 详情面板/侧边面板
Split view pattern (CRM/Helpdesk style):
vue
<template>
<aside class="w-[480px] border-l bg-white flex flex-col">
<!-- Header with close -->
<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 -->
<Tabs v-model="activeTab">
<Tab name="details" label="Details" />
<Tab name="activity" label="Activity" />
<Tab name="notes" label="Notes" />
</Tabs>
<!-- Tab content -->
<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>
<!-- Footer actions -->
<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>分栏视图模式(CRM/Helpdesk风格):
vue
<template>
<aside class="w-[480px] border-l bg-white flex flex-col">
<!-- Header with close -->
<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 -->
<Tabs v-model="activeTab">
<Tab name="details" label="Details" />
<Tab name="activity" label="Activity" />
<Tab name="notes" label="Notes" />
</Tabs>
<!-- Tab content -->
<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>
<!-- Footer actions -->
<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) Form patterns
5) 表单模式
Standard form layout:
vue
<template>
<div class="max-w-2xl mx-auto p-6">
<!-- Form header -->
<div class="mb-6">
<h1 class="text-xl font-semibold">{{ isNew ? 'New' : 'Edit' }} {{ doctype }}</h1>
</div>
<!-- Sections -->
<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>
<!-- More fields -->
</FormSection>
<!-- Actions -->
<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 component:
vue
<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>标准表单布局:
vue
<template>
<div class="max-w-2xl mx-auto p-6">
<!-- Form header -->
<div class="mb-6">
<h1 class="text-xl font-semibold">{{ isNew ? 'New' : 'Edit' }} {{ doctype }}</h1>
</div>
<!-- Sections -->
<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>
<!-- More fields -->
</FormSection>
<!-- Actions -->
<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组件:
vue
<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) Activity feed pattern
6) 活动流模式
Used across all apps for tracking changes:
vue
<template>
<div class="space-y-4">
<!-- Add comment -->
<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>
<!-- Activity items -->
<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>
<!-- Different activity types -->
<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>所有应用中用于跟踪变更:
vue
<template>
<div class="space-y-4">
<!-- Add comment -->
<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>
<!-- Activity items -->
<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>
<!-- Different activity types -->
<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) Empty states
7) 空状态模式
Always provide helpful empty states:
vue
<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>
<!-- Usage -->
<EmptyState
title="No leads yet"
description="Create your first lead to get started"
:action="{ label: 'Create Lead', handler: createLead }"
/>始终提供有帮助的空状态:
vue
<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>
<!-- Usage -->
<EmptyState
title="No leads yet"
description="Create your first lead to get started"
:action="{ label: 'Create Lead', handler: createLead }"
/>8) Loading states
8) 加载状态模式
Skeleton loaders for perceived performance:
vue
<!-- List skeleton -->
<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>使用骨架屏提升感知性能:
vue
<!-- List skeleton -->
<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) Color and status conventions
9) 颜色与状态约定
| Status Type | Color | Usage |
|---|---|---|
| Success/Active | Green ( | Completed, Active, Resolved |
| Warning/Pending | Yellow ( | Pending, In Progress, Due Soon |
| Error/Blocked | Red ( | Failed, Blocked, Overdue |
| Info/Default | Blue ( | New, Open, Info |
| Neutral | Gray ( | Draft, Cancelled, Closed |
Badge component usage:
vue
<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>| 状态类型 | 颜色 | 使用场景 |
|---|---|---|
| 成功/活跃 | 绿色 ( | 已完成、活跃、已解决 |
| 警告/待处理 | 黄色 ( | 待处理、进行中、即将到期 |
| 错误/阻塞 | 红色 ( | 失败、阻塞、已逾期 |
| 信息/默认 | 蓝色 ( | 新建、打开、信息提示 |
| 中性 | 灰色 ( | 草稿、已取消、已关闭 |
Badge组件用法:
vue
<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) Responsive patterns
10) 响应式模式
Mobile-first considerations:
vue
<template>
<!-- Hide sidebar on mobile, show as drawer -->
<Sidebar v-if="!isMobile" />
<Drawer v-else v-model="sidebarOpen">
<Sidebar />
</Drawer>
<!-- Stack list and detail on mobile -->
<div :class="isMobile ? 'flex-col' : 'flex'">
<ListView v-show="!isMobile || !selectedDoc" />
<DetailPanel v-if="selectedDoc" :fullScreen="isMobile" />
</div>
</template>移动端优先注意事项:
vue
<template>
<!-- Hide sidebar on mobile, show as drawer -->
<Sidebar v-if="!isMobile" />
<Drawer v-else v-model="sidebarOpen">
<Sidebar />
</Drawer>
<!-- Stack list and detail on mobile -->
<div :class="isMobile ? 'flex-col' : 'flex'">
<ListView v-show="!isMobile || !selectedDoc" />
<DetailPanel v-if="selectedDoc" :fullScreen="isMobile" />
</div>
</template>Component reference
组件参考
Use these Frappe UI components consistently:
| Component | Usage |
|---|---|
| All actions, with variants: solid, subtle, ghost |
| Text inputs, search fields |
| Form fields with labels, validation |
| Dropdowns, status selectors |
| Boolean inputs, bulk selection |
| User images, entity icons |
| Status indicators, counts |
| Action menus, context menus |
| Modal confirmations, forms |
| Content organization |
| Helpful hints, truncated text |
请持续使用以下Frappe UI组件:
| 组件 | 用途 |
|---|---|
| 所有操作按钮,支持变体:solid、subtle、ghost |
| 文本输入框、搜索框 |
| 带标签、验证的表单字段 |
| 下拉选择框、状态选择器 |
| 布尔输入、批量选择 |
| 用户头像、实体图标 |
| 状态指示器、计数徽章 |
| 操作菜单、上下文菜单 |
| 模态确认框、弹窗表单 |
| 内容分类展示 |
| 提示信息、截断文本展开 |
Verification
验证清单
- App shell matches standard layout (sidebar + main + optional detail)
- List views have search, filters, view toggle, create button
- Detail panel has tabs (Details, Activity, Notes)
- Empty states are helpful with actions
- Loading states use skeletons, not spinners
- Status colors follow conventions
- Forms are sectioned and consistent
- Mobile experience is considered
- 应用外壳符合标准布局(侧边栏+主内容+可选详情面板)
- 列表视图包含搜索、筛选、视图切换、创建按钮
- 详情面板包含标签页(详情、活动、备注)
- 空状态提供操作指引
- 加载状态使用骨架屏而非加载动画
- 状态颜色遵循约定规范
- 表单采用分栏式一致布局
- 适配移动端体验
Failure modes / debugging
故障模式与调试
- Inconsistent spacing: Use TailwindCSS spacing scale (p-2, p-4, gap-2, gap-4)
- Wrong component: Check Frappe UI docs for correct component and props
- Broken responsiveness: Test at mobile breakpoints; use ,
sm:,md:prefixeslg: - Missing states: Ensure loading, empty, and error states are handled
- 间距不一致:使用TailwindCSS间距体系(p-2、p-4、gap-2、gap-4)
- 组件使用错误:查阅Frappe UI文档确认正确组件与属性
- 响应式失效:在移动端断点测试;使用、
sm:、md:前缀lg: - 状态缺失:确保处理加载、空数据与错误状态
Escalation
升级路径
- For component implementation →
frappe-frontend-development - For backend API integration →
frappe-api-development - For enterprise workflows →
frappe-enterprise-patterns
- 组件实现问题 →
frappe-frontend-development - 后端API集成问题 →
frappe-api-development - 企业级工作流问题 →
frappe-enterprise-patterns
References
参考资料
- references/app-shell-patterns.md — Detailed shell layouts
- references/component-patterns.md — Component usage guide
- references/mobile-patterns.md — Responsive design
- references/app-shell-patterns.md — 详细外壳布局
- references/component-patterns.md — 组件使用指南
- references/mobile-patterns.md — 响应式设计
Guardrails
约束规则
- Study official apps first: Before designing UI, review CRM, Helpdesk, or relevant official app for patterns
- Use Frappe UI components: Never create custom components when Frappe UI has an equivalent
- Follow spacing conventions: Use consistent padding/margins (4px increments)
- Provide all states: Every view needs loading, empty, and error states
- Keep navigation consistent: Sidebar structure should match official apps
- Test responsively: Ensure mobile experience works
- 优先研究官方应用:设计UI前,先参考CRM、Helpdesk或相关官方应用的模式
- 使用Frappe UI组件:Frappe UI已有对应组件时,切勿自定义
- 遵循间距约定:使用统一的内边距/外边距(4px递增)
- 覆盖所有状态:每个视图都需要加载、空数据与错误状态
- 保持导航一致:侧边栏结构需与官方应用匹配
- 响应式测试:确保移动端体验可用
Common Mistakes
常见错误
| Mistake | Why It Fails | Fix |
|---|---|---|
| Custom app shell design | Unfamiliar UX for users | Copy CRM/Helpdesk shell structure |
| Missing empty states | Users confused when no data | Add EmptyState component with action |
| Spinner instead of skeleton | Jarring loading experience | Use Skeleton components for loading |
| Inconsistent status colors | User confusion | Follow color conventions table |
| Deep nesting without breadcrumbs | Users get lost | Add breadcrumb navigation |
| Modal overuse | Disruptive workflow | Prefer side panels for detail views |
| No keyboard navigation | Accessibility issues | Ensure Tab/Enter work for key flows |
| 错误 | 问题原因 | 修复方案 |
|---|---|---|
| 自定义应用外壳设计 | 用户对UX不熟悉 | 复制CRM/Helpdesk的外壳结构 |
| 缺失空状态 | 无数据时用户困惑 | 添加带操作按钮的EmptyState组件 |
| 使用加载动画而非骨架屏 | 加载体验突兀 | 使用Skeleton组件实现加载状态 |
| 状态颜色不一致 | 造成用户混淆 | 遵循颜色约定表 |
| 深层嵌套无面包屑 | 用户迷路 | 添加面包屑导航 |
| 过度使用模态框 | 打断工作流 | 优先使用侧边面板展示详情 |
| 无键盘导航 | 可访问性问题 | 确保Tab/Enter键可操作核心流程 |