frappe-frontend-development

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Frappe 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) 选择前端方案

ApproachWhen to UseStack
Frappe UI SPACustom app frontendVue 3, TailwindCSS, Vite
Portal pagesSimple public pagesJinja + HTML, minimal JS
Desk extensionsAdmin UI enhancementsForm/List scripts (see
frappe-desk-customization
)
方案适用场景技术栈
Frappe UI SPA自定义应用前端Vue 3, TailwindCSS, Vite
门户页面简单公开页面Jinja + HTML, 极简JS
Desk扩展管理端UI增强表单/列表脚本(参见
frappe-desk-customization

1) Scaffold Frappe UI frontend

1) 初始化Frappe UI前端项目

bash
undefined
bash
undefined

Inside 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
undefined
yarn dev
undefined

2) 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 state
List 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:
CategoryComponents
InputsTextInput, Textarea, Select, Combobox, MultiSelect, Checkbox, Switch, DatePicker, TimePicker, Slider, Password, Rating
DisplayAlert, Avatar, Badge, Breadcrumbs, Progress, Tooltip, ErrorMessage, LoadingText
NavigationButton, Dropdown, Tabs, Sidebar, Popover
LayoutDialog, ListView, Calendar, Tree
Rich ContentTextEditor (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
undefined
bash
undefined

Build 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

undefined
undefined

8) Portal pages (alternative approach)

8) 门户页面(替代方案)

For simple public pages without a full SPA:
python
undefined
适用于无需完整SPA的简单公开页面:
python
undefined

In 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
undefined

my_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})
undefined
def get_context(context): context.title = "My Page" context.data = frappe.get_all("ToDo", filters={"owner": frappe.session.user})
undefined

Verification

验证清单

  • yarn dev
    starts without errors
  • Components render correctly
  • Data resources fetch and display data
  • CRUD operations work (insert, update, delete)
  • Authentication works (login redirect, session handling)
  • yarn build
    completes successfully
  • Production assets serve correctly from Frappe
  • yarn dev
    启动无错误
  • 组件渲染正常
  • 数据资源可获取并展示数据
  • CRUD操作(插入、更新、删除)正常工作
  • 认证功能正常(登录重定向、会话处理)
  • yarn build
    执行成功
  • 生产环境资源可通过Frappe正常访问

Failure modes / debugging

故障排查

  • CORS errors: Set
    ignore_csrf
    for local dev; ensure proper CSRF token in production
  • 404 on API calls: Check method path; verify
    @frappe.whitelist()
    decorator
  • Component not found: Ensure import path is correct; check
    frappe-ui
    version
  • Styles broken: Verify TailwindCSS config includes
    frappe-ui
    component paths
  • Auth issues: Check session cookie; ensure site URL matches in dev proxy config
  • CORS错误:本地开发时设置
    ignore_csrf
    ;生产环境确保正确配置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>
    ,
    <FormControl>
    over custom HTML for consistency
  • Follow CRM/Helpdesk app shell patterns: For CRUD apps, follow
    frappe-ui-patterns
    skill which documents sidebar navigation, list views, form layouts, and routing patterns from official Frappe apps
  • Handle loading states: Always show loading indicators during API calls; use
    resource.loading
  • Validate API responses: Check for errors before accessing data; handle
    exc
    responses
  • Configure proxy correctly: Dev server must proxy API calls to Frappe backend
  • Handle authentication: Check
    $session.user
    and redirect to login when needed
  • 始终使用Frappe UI开发自定义前端:绝不要使用原生JS、jQuery或其他自定义框架开发应用前端 — Frappe UI(Vue 3 + TailwindCSS)是标准方案。这确保与CRM、Helpdesk等官方Frappe应用保持一致性。
  • 优先使用FrappeUI组件:为保证一致性,优先使用
    <Button>
    <Input>
    <FormControl>
    等组件,而非自定义HTML
  • 遵循CRM/Helpdesk应用框架模式:对于CRUD类应用,遵循
    frappe-ui-patterns
    中记录的官方Frappe应用的侧边栏导航、列表视图、表单布局和路由模式
  • 处理加载状态:API调用期间必须显示加载指示器;使用
    resource.loading
    状态
  • 验证API响应:访问数据前检查错误;处理
    exc
    类型的响应
  • 正确配置代理:开发服务器必须将API请求代理到Frappe后端
  • 处理认证状态:检查
    $session.user
    ,必要时重定向到登录页面

Common Mistakes

常见错误

MistakeWhy It FailsFix
Missing CORS/proxy setupAPI calls fail in developmentConfigure Vite proxy to forward
/api
to Frappe site
Not handling auth stateApp crashes for logged-out usersCheck
call('frappe.auth.get_logged_user')
on mount
Wrong resource URLs404 errors on API callsUse
createResource
with correct method paths
Hardcoded site URLBreaks across environmentsUse relative URLs or environment variables
Not including CSRF tokenPOST requests failUse
frappe.csrf_token
or configure session properly
Missing TailwindCSS configFrappe UI styles brokenInclude
frappe-ui
in Tailwind content paths
Using vanilla JS/jQueryInconsistent UX, maintenance burdenAlways use Frappe UI for custom frontends
Custom app shell designInconsistent with ecosystemFollow CRM/Helpdesk patterns for navigation, lists, forms
错误原因修复方案
缺少CORS/代理配置开发环境下API调用失败配置Vite代理,将
/api
请求转发到Frappe站点
未处理认证状态未登录用户访问时应用崩溃挂载时调用
call('frappe.auth.get_logged_user')
检查登录状态
Resource URL错误API调用出现404错误使用
createResource
时传入正确的方法路径
硬编码站点URL在不同环境下失效使用相对URL或环境变量
未包含CSRF令牌POST请求失败使用
frappe.csrf_token
或正确配置会话
缺少TailwindCSS配置Frappe UI样式失效在Tailwind的content路径中包含
frappe-ui
组件路径
使用原生JS/jQuery用户体验不一致,维护成本高始终使用Frappe UI开发自定义前端
自定义应用框架设计与生态系统不一致遵循CRM/Helpdesk的导航、列表、表单模式