frappe-frontend-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFrappe Frontend Development
Frappe前端开发
Build modern frontend applications using Frappe UI (Vue 3 + TailwindCSS) and portal pages.
使用Frappe UI(Vue 3 + TailwindCSS)和门户页面构建现代化前端应用。
When to use
适用场景
- Building a custom SPA frontend for a Frappe app
- Using Frappe UI components (Button, Dialog, ListView, etc.)
- Implementing data fetching with Resource, ListResource, DocumentResource
- Creating portal/public-facing pages
- Setting up Vue 3 frontend tooling inside a Frappe app
- 为Frappe应用构建自定义SPA前端
- 使用Frappe UI组件(Button、Dialog、ListView等)
- 基于Resource、ListResource、DocumentResource实现数据获取
- 创建门户/公开访问页面
- 在Frappe应用内配置Vue 3前端工具链
Inputs required
所需输入
- App name and whether frontend already exists
- Frontend type (full SPA via Frappe UI, or portal pages)
- Authentication requirements (logged-in users, guest access)
- Key components and data resources needed
- 应用名称及前端是否已存在
- 前端类型(基于Frappe UI的完整SPA,或门户页面)
- 认证要求(登录用户访问、访客访问)
- 所需核心组件和数据资源
Procedure
实施步骤
0) Choose frontend approach
0) 选择前端方案
| Approach | When to Use | Stack |
|---|---|---|
| Frappe UI SPA | Custom app frontend | Vue 3, TailwindCSS, Vite |
| Portal pages | Simple public pages | Jinja + HTML, minimal JS |
| Desk extensions | Admin UI enhancements | Form/List scripts (see |
| 方案 | 适用场景 | 技术栈 |
|---|---|---|
| Frappe UI SPA | 自定义应用前端 | Vue 3, TailwindCSS, Vite |
| 门户页面 | 简单公开页面 | Jinja + HTML, 极简JS |
| Desk扩展 | 管理端UI增强 | 表单/列表脚本(参见 |
1) Scaffold Frappe UI frontend
1) 初始化Frappe UI前端项目
bash
undefinedbash
undefinedInside your Frappe app directory
Inside your Frappe app directory
cd apps/my_app
npx degit frappe/frappe-ui-starter frontend
cd apps/my_app
npx degit frappe/frappe-ui-starter frontend
Install dependencies
Install dependencies
cd frontend
yarn
cd frontend
yarn
Start dev server
Start dev server
yarn dev
undefinedyarn dev
undefined2) Configure main.js
2) 配置main.js
javascript
import { createApp } from 'vue'
import {
FrappeUI,
setConfig,
frappeRequest,
resourcesPlugin,
pageMetaPlugin
} from 'frappe-ui'
import App from './App.vue'
import './index.css'
let app = createApp(App)
// Register FrappeUI plugin (components + directives)
app.use(FrappeUI)
// Enable Frappe response parsing
setConfig('resourceFetcher', frappeRequest)
// Optional: Options API resource support
app.use(resourcesPlugin)
// Optional: Reactive page titles
app.use(pageMetaPlugin)
app.mount('#app')javascript
import { createApp } from 'vue'
import {
FrappeUI,
setConfig,
frappeRequest,
resourcesPlugin,
pageMetaPlugin
} from 'frappe-ui'
import App from './App.vue'
import './index.css'
let app = createApp(App)
// Register FrappeUI plugin (components + directives)
app.use(FrappeUI)
// Enable Frappe response parsing
setConfig('resourceFetcher', frappeRequest)
// Optional: Options API resource support
app.use(resourcesPlugin)
// Optional: Reactive page titles
app.use(pageMetaPlugin)
app.mount('#app')3) Fetch data with Resources
3) 使用Resources获取数据
Generic Resource — for custom API calls:
javascript
import { createResource } from 'frappe-ui'
let stats = createResource({
url: 'my_app.api.get_dashboard_stats',
params: { period: 'monthly' },
auto: true,
cache: 'dashboard-stats',
transform(data) {
return { ...data, formatted_total: format_currency(data.total) }
},
onSuccess(data) { console.log('Loaded:', data) },
onError(error) { console.error('Failed:', error) }
})
// Properties
stats.data // Response data
stats.loading // Boolean: request in progress
stats.error // Error object if failed
stats.fetched // Boolean: data fetched at least once
// Methods
stats.fetch() // Trigger request
stats.reload() // Re-fetch
stats.submit({ period: 'weekly' }) // Fetch with new params
stats.reset() // Reset stateList Resource — for DocType lists with pagination:
javascript
import { createListResource } from 'frappe-ui'
let todos = createListResource({
doctype: 'ToDo',
fields: ['name', 'description', 'status'],
filters: { status: 'Open' },
orderBy: 'creation desc',
pageLength: 20,
auto: true,
cache: 'open-todos'
})
// List-specific API
todos.data // Array of records
todos.hasNextPage // Boolean: more pages
todos.next() // Load next page
todos.reload() // Refresh list
// CRUD operations
todos.insert.submit({ description: 'New task' })
todos.setValue.submit({ name: 'TODO-001', status: 'Closed' })
todos.delete.submit('TODO-001')
todos.runDocMethod.submit({ method: 'send_email', name: 'TODO-001' })Document Resource — for single document operations:
javascript
import { createDocumentResource } from 'frappe-ui'
let todo = createDocumentResource({
doctype: 'ToDo',
name: 'TODO-001',
whitelistedMethods: {
sendEmail: 'send_email',
markComplete: 'mark_complete'
},
onSuccess(doc) { console.log('Loaded:', doc.name) }
})
// Document API
todo.doc // Full document object
todo.reload() // Refresh document
// Update fields
todo.setValue.submit({ status: 'Closed' })
// Debounced update (coalesces rapid changes)
todo.setValueDebounced.submit({ description: 'Updated' })
// Call whitelisted methods
todo.sendEmail.submit({ email: 'user@example.com' })
// Delete
todo.delete.submit()通用Resource — 用于自定义API调用:
javascript
import { createResource } from 'frappe-ui'
let stats = createResource({
url: 'my_app.api.get_dashboard_stats',
params: { period: 'monthly' },
auto: true,
cache: 'dashboard-stats',
transform(data) {
return { ...data, formatted_total: format_currency(data.total) }
},
onSuccess(data) { console.log('Loaded:', data) },
onError(error) { console.error('Failed:', error) }
})
// Properties
stats.data // Response data
stats.loading // Boolean: request in progress
stats.error // Error object if failed
stats.fetched // Boolean: data fetched at least once
// Methods
stats.fetch() // Trigger request
stats.reload() // Re-fetch
stats.submit({ period: 'weekly' }) // Fetch with new params
stats.reset() // Reset state列表Resource — 用于带分页的DocType列表:
javascript
import { createListResource } from 'frappe-ui'
let todos = createListResource({
doctype: 'ToDo',
fields: ['name', 'description', 'status'],
filters: { status: 'Open' },
orderBy: 'creation desc',
pageLength: 20,
auto: true,
cache: 'open-todos'
})
// List-specific API
todos.data // Array of records
todos.hasNextPage // Boolean: more pages
todos.next() // Load next page
todos.reload() // Refresh list
// CRUD operations
todos.insert.submit({ description: 'New task' })
todos.setValue.submit({ name: 'TODO-001', status: 'Closed' })
todos.delete.submit('TODO-001')
todos.runDocMethod.submit({ method: 'send_email', name: 'TODO-001' })文档Resource — 用于单个文档操作:
javascript
import { createDocumentResource } from 'frappe-ui'
let todo = createDocumentResource({
doctype: 'ToDo',
name: 'TODO-001',
whitelistedMethods: {
sendEmail: 'send_email',
markComplete: 'mark_complete'
},
onSuccess(doc) { console.log('Loaded:', doc.name) }
})
// Document API
todo.doc // Full document object
todo.reload() // Refresh document
// Update fields
todo.setValue.submit({ status: 'Closed' })
// Debounced update (coalesces rapid changes)
todo.setValueDebounced.submit({ description: 'Updated' })
// Call whitelisted methods
todo.sendEmail.submit({ email: 'user@example.com' })
// Delete
todo.delete.submit()4) Use Frappe UI components
4) 使用Frappe UI组件
vue
<template>
<div class="p-4">
<Button variant="solid" theme="blue" @click="showDialog = true">
Add Todo
</Button>
<ListView :columns="columns" :rows="todos.data">
<template #cell="{ column, row, value }">
<Badge v-if="column.key === 'status'" :theme="value === 'Open' ? 'orange' : 'green'">
{{ value }}
</Badge>
<span v-else>{{ value }}</span>
</template>
</ListView>
<Dialog v-model="showDialog" :options="{ title: 'New Todo' }">
<template #body-content>
<TextInput v-model="newDescription" placeholder="Description" />
</template>
<template #actions>
<Button variant="solid" @click="addTodo">Save</Button>
</template>
</Dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Button, ListView, Badge, Dialog, TextInput, createListResource } from 'frappe-ui'
const showDialog = ref(false)
const newDescription = ref('')
const todos = createListResource({
doctype: 'ToDo',
fields: ['name', 'description', 'status'],
auto: true
})
const columns = [
{ label: 'Description', key: 'description' },
{ label: 'Status', key: 'status', width: 100 }
]
function addTodo() {
todos.insert.submit(
{ description: newDescription.value },
{ onSuccess() { showDialog.value = false; newDescription.value = '' } }
)
}
</script>Available component categories:
| Category | Components |
|---|---|
| Inputs | TextInput, Textarea, Select, Combobox, MultiSelect, Checkbox, Switch, DatePicker, TimePicker, Slider, Password, Rating |
| Display | Alert, Avatar, Badge, Breadcrumbs, Progress, Tooltip, ErrorMessage, LoadingText |
| Navigation | Button, Dropdown, Tabs, Sidebar, Popover |
| Layout | Dialog, ListView, Calendar, Tree |
| Rich Content | TextEditor (TipTap), Charts, FileUploader |
vue
<template>
<div class="p-4">
<Button variant="solid" theme="blue" @click="showDialog = true">
Add Todo
</Button>
<ListView :columns="columns" :rows="todos.data">
<template #cell="{ column, row, value }">
<Badge v-if="column.key === 'status'" :theme="value === 'Open' ? 'orange' : 'green'">
{{ value }}
</Badge>
<span v-else>{{ value }}</span>
</template>
</ListView>
<Dialog v-model="showDialog" :options="{ title: 'New Todo' }">
<template #body-content>
<TextInput v-model="newDescription" placeholder="Description" />
</template>
<template #actions>
<Button variant="solid" @click="addTodo">Save</Button>
</template>
</Dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Button, ListView, Badge, Dialog, TextInput, createListResource } from 'frappe-ui'
const showDialog = ref(false)
const newDescription = ref('')
const todos = createListResource({
doctype: 'ToDo',
fields: ['name', 'description', 'status'],
auto: true
})
const columns = [
{ label: 'Description', key: 'description' },
{ label: 'Status', key: 'status', width: 100 }
]
function addTodo() {
todos.insert.submit(
{ description: newDescription.value },
{ onSuccess() { showDialog.value = false; newDescription.value = '' } }
)
}
</script>可用组件分类:
| 分类 | 组件 |
|---|---|
| 输入组件 | TextInput, Textarea, Select, Combobox, MultiSelect, Checkbox, Switch, DatePicker, TimePicker, Slider, Password, Rating |
| 展示组件 | Alert, Avatar, Badge, Breadcrumbs, Progress, Tooltip, ErrorMessage, LoadingText |
| 导航组件 | Button, Dropdown, Tabs, Sidebar, Popover |
| 布局组件 | Dialog, ListView, Calendar, Tree |
| 富内容组件 | TextEditor (TipTap), Charts, FileUploader |
5) Add directives and utilities
5) 添加指令与工具函数
vue
<script setup>
import { onOutsideClickDirective, visibilityDirective, debounce } from 'frappe-ui'
const vOnOutsideClick = onOutsideClickDirective
const vVisibility = visibilityDirective
const debouncedSearch = debounce((query) => {
// Search logic
}, 500)
</script>
<template>
<div v-on-outside-click="closeDropdown">...</div>
<div v-visibility="onVisible">Lazy loaded content</div>
</template>vue
<script setup>
import { onOutsideClickDirective, visibilityDirective, debounce } from 'frappe-ui'
const vOnOutsideClick = onOutsideClickDirective
const vVisibility = visibilityDirective
const debouncedSearch = debounce((query) => {
// Search logic
}, 500)
</script>
<template>
<div v-on-outside-click="closeDropdown">...</div>
<div v-visibility="onVisible">Lazy loaded content</div>
</template>6) Configure TailwindCSS
6) 配置TailwindCSS
javascript
// tailwind.config.js
module.exports = {
presets: [
require('frappe-ui/src/utils/tailwind.config')
],
content: [
'./index.html',
'./src/**/*.{vue,js,ts}',
'./node_modules/frappe-ui/src/components/**/*.{vue,js,ts}'
]
}javascript
// tailwind.config.js
module.exports = {
presets: [
require('frappe-ui/src/utils/tailwind.config')
],
content: [
'./index.html',
'./src/**/*.{vue,js,ts}',
'./node_modules/frappe-ui/src/components/**/*.{vue,js,ts}'
]
}7) Build for production
7) 生产环境构建
bash
undefinedbash
undefinedBuild frontend assets
Build frontend assets
cd frontend && yarn build
cd frontend && yarn build
Assets are served at /frontend by Frappe
Assets are served at /frontend by Frappe
undefinedundefined8) Portal pages (alternative approach)
8) 门户页面(替代方案)
For simple public pages without a full SPA:
python
undefined适用于无需完整SPA的简单公开页面:
python
undefinedIn your app's website/ or www/ directory
In your app's website/ or www/ directory
my_app/www/my_page.html
my_app/www/my_page.html
{% extends "templates/web.html" %}
{% block page_content %}
<h1>{{ title }}</h1>
<p>Welcome, {{ frappe.session.user }}</p>
{% endblock %}
```
python
undefined{% extends "templates/web.html" %}
{% block page_content %}
<h1>{{ title }}</h1>
<p>Welcome, {{ frappe.session.user }}</p>
{% endblock %}
```
python
undefinedmy_app/www/my_page.py
my_app/www/my_page.py
def get_context(context):
context.title = "My Page"
context.data = frappe.get_all("ToDo", filters={"owner": frappe.session.user})
undefineddef get_context(context):
context.title = "My Page"
context.data = frappe.get_all("ToDo", filters={"owner": frappe.session.user})
undefinedVerification
验证清单
- starts without errors
yarn dev - Components render correctly
- Data resources fetch and display data
- CRUD operations work (insert, update, delete)
- Authentication works (login redirect, session handling)
- completes successfully
yarn build - Production assets serve correctly from Frappe
- 启动无错误
yarn dev - 组件渲染正常
- 数据资源可获取并展示数据
- CRUD操作(插入、更新、删除)正常工作
- 认证功能正常(登录重定向、会话处理)
- 执行成功
yarn build - 生产环境资源可通过Frappe正常访问
Failure modes / debugging
故障排查
- CORS errors: Set for local dev; ensure proper CSRF token in production
ignore_csrf - 404 on API calls: Check method path; verify decorator
@frappe.whitelist() - Component not found: Ensure import path is correct; check version
frappe-ui - Styles broken: Verify TailwindCSS config includes component paths
frappe-ui - Auth issues: Check session cookie; ensure site URL matches in dev proxy config
- CORS错误:本地开发时设置;生产环境确保正确配置CSRF令牌
ignore_csrf - API调用404:检查方法路径;验证是否添加装饰器
@frappe.whitelist() - 组件未找到:确保导入路径正确;检查版本
frappe-ui - 样式失效:验证TailwindCSS配置包含组件路径
frappe-ui - 认证问题:检查会话Cookie;确保开发代理配置中的站点URL匹配
Escalation
问题升级
- For Desk UI scripting →
frappe-desk-customization - For API endpoint implementation →
frappe-api-development - For app architecture →
frappe-app-development - For UI/UX patterns from official apps →
frappe-ui-patterns
- 管理端UI脚本相关 → 参考
frappe-desk-customization - API端点实现相关 → 参考
frappe-api-development - 应用架构相关 → 参考
frappe-app-development - 官方应用UI/UX模式相关 → 参考
frappe-ui-patterns
References
参考资料
- references/frappe-ui.md — Frappe UI framework reference
- references/portal-development.md — Portal pages overview
- references/frappe-ui.md — Frappe UI框架参考文档
- references/portal-development.md — 门户页面开发概述
Guardrails
注意事项
- ALWAYS use Frappe UI for custom frontends: Never use vanilla JS, jQuery, or custom frameworks for app frontends — Frappe UI (Vue 3 + TailwindCSS) is the standard. This ensures consistency with CRM, Helpdesk, and other official Frappe apps.
- Use FrappeUI components: Prefer ,
<Button>,<Input>over custom HTML for consistency<FormControl> - Follow CRM/Helpdesk app shell patterns: For CRUD apps, follow skill which documents sidebar navigation, list views, form layouts, and routing patterns from official Frappe apps
frappe-ui-patterns - Handle loading states: Always show loading indicators during API calls; use
resource.loading - Validate API responses: Check for errors before accessing data; handle responses
exc - Configure proxy correctly: Dev server must proxy API calls to Frappe backend
- Handle authentication: Check and redirect to login when needed
$session.user
- 始终使用Frappe UI开发自定义前端:绝不要使用原生JS、jQuery或其他自定义框架开发应用前端 — Frappe UI(Vue 3 + TailwindCSS)是标准方案。这确保与CRM、Helpdesk等官方Frappe应用保持一致性。
- 优先使用FrappeUI组件:为保证一致性,优先使用、
<Button>、<Input>等组件,而非自定义HTML<FormControl> - 遵循CRM/Helpdesk应用框架模式:对于CRUD类应用,遵循中记录的官方Frappe应用的侧边栏导航、列表视图、表单布局和路由模式
frappe-ui-patterns - 处理加载状态:API调用期间必须显示加载指示器;使用状态
resource.loading - 验证API响应:访问数据前检查错误;处理类型的响应
exc - 正确配置代理:开发服务器必须将API请求代理到Frappe后端
- 处理认证状态:检查,必要时重定向到登录页面
$session.user
Common Mistakes
常见错误
| Mistake | Why It Fails | Fix |
|---|---|---|
| Missing CORS/proxy setup | API calls fail in development | Configure Vite proxy to forward |
| Not handling auth state | App crashes for logged-out users | Check |
| Wrong resource URLs | 404 errors on API calls | Use |
| Hardcoded site URL | Breaks across environments | Use relative URLs or environment variables |
| Not including CSRF token | POST requests fail | Use |
| Missing TailwindCSS config | Frappe UI styles broken | Include |
| Using vanilla JS/jQuery | Inconsistent UX, maintenance burden | Always use Frappe UI for custom frontends |
| Custom app shell design | Inconsistent with ecosystem | Follow CRM/Helpdesk patterns for navigation, lists, forms |
| 错误 | 原因 | 修复方案 |
|---|---|---|
| 缺少CORS/代理配置 | 开发环境下API调用失败 | 配置Vite代理,将 |
| 未处理认证状态 | 未登录用户访问时应用崩溃 | 挂载时调用 |
| Resource URL错误 | API调用出现404错误 | 使用 |
| 硬编码站点URL | 在不同环境下失效 | 使用相对URL或环境变量 |
| 未包含CSRF令牌 | POST请求失败 | 使用 |
| 缺少TailwindCSS配置 | Frappe UI样式失效 | 在Tailwind的content路径中包含 |
| 使用原生JS/jQuery | 用户体验不一致,维护成本高 | 始终使用Frappe UI开发自定义前端 |
| 自定义应用框架设计 | 与生态系统不一致 | 遵循CRM/Helpdesk的导航、列表、表单模式 |