mcpField
U
ui-patterns

ui-patterns

github.com/lubusin设计与 UI69108 次安装12 周前更新
代码仓库

关于 ui-patterns

面向 Frappe 应用开发者的界面设计技能,参考官方 Frappe 应用中的模式,涵盖布局、状态、组件和响应式设计。

主要功能

  1. 01应用外壳与导航模式
  2. 02列表、看板和详情视图
  3. 03表单与活动动态
  4. 04空状态、加载和状态规范
  5. 05响应式设计指导

适用场景

  1. 01设计新的 Frappe 应用界面
  2. 02使用 Frappe UI 构建 CRUD 页面
  3. 03沿用 CRM 或 Helpdesk 交互模式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/lubusin/frappe-skills --skill ui-patterns

安装后在 Claude Code 中输入/ui-patterns确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
app-shell-patterns.md
component-patterns.md
mobile-patterns.md

详细介绍

Frappe UI 模式

从官方 Frappe 应用中提取的 UI/UX 模式和设计指南。

适用场景

  • 为新的 Frappe 应用设计 UI
  • 使用 Frappe UI 构建 CRUD 界面
  • 实现列表视图、详情面板或表单
  • 确保与 CRM、Helpdesk、HRMS 的 UX 一致
  • 选择组件模式和布局

所需输入

  • 应用类型(类似 CRM、类似 Helpdesk、数据管理)
  • 关键实体及其关系
  • 主要用户工作流

参考应用

可研究以下官方应用中的模式:

应用Repo关键模式
Frappe CRMgithub.com/frappe/crm潜在客户/商机管道、看板、活动动态
Frappe Helpdeskgithub.com/frappe/helpdesk工单队列、SLA 指示器、客服视图
Frappe HRMSgithub.com/frappe/hrms员工自助服务、审批、仪表板
Frappe Insightsgithub.com/frappe/insights查询构建器、可视化、仪表板
Frappe Buildergithub.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: 前缀
  • 缺少状态:确保处理加载、空和错误状态

升级处理

  • 组件实现 → frontend-development
  • 后端 API 集成 → api-development
  • 企业级工作流 → enterprise-patterns

参考资料

防护措施

  • 先研究官方应用:设计 UI 之前,查看 CRM、Helpdesk 或相关官方应用,了解其模式
  • 使用 Frappe UI 组件:Frappe UI 有对应组件时,切勿创建自定义组件
  • 遵循间距规范:统一使用内边距和外边距(以 4px 为增量)
  • 提供所有状态:每个视图都需要加载、空和错误状态
  • 保持导航一致:侧边栏结构应与官方应用保持一致
  • 进行响应式测试:确保移动端体验正常

常见错误

错误失败原因修正方法
自定义应用外壳设计用户不熟悉这种 UX复制 CRM/Helpdesk 外壳结构
缺少空状态没有数据时用户会感到困惑添加带操作的 EmptyState 组件
用微调器代替骨架屏加载体验突兀使用 Skeleton 组件显示加载状态
状态颜色不一致用户会感到困惑遵循颜色规范表
深层嵌套但没有面包屑用户容易迷失添加面包屑导航
过度使用模态框打断工作流详情视图优先使用侧边面板
没有键盘导航存在无障碍问题确保关键流程支持 Tab/Enter

评论

支持 Markdown