关于 odoo-frontend
用于 Odoo 前端开发的技能,涵盖主题生成、版本检测、主题配置及前端开发实践,支持 Odoo 14–19 的版本差异。适合构建和维护 Odoo 网站及主题的开发者。
主要功能
- 01使用 /create-theme 生成主题模块
- 02检测 Odoo 版本并对应 Bootstrap
- 03提供主题变量与动态页面参考
- 04涵盖 JavaScript、PWA、测试和性能实践
- 05支持 Figma 转换与 Chrome DevTools 样式提取
适用场景
- 01为 Odoo 网站搭建主题模块
- 02按 Odoo 版本选择前端实现方式
- 03将 Figma 设计转换为 Odoo 模板
- 04为网站添加离线支持并检查性能
MCP 相关
README 提及 MCP 集成,用于 Figma 设计转换和 Chrome DevTools 样式提取。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ahmed-lakosha/ odoo-plugins --skill odoo-frontend安装后在 Claude Code 中输入/odoo-frontend确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
scripts/bootstrap_mapper.pycreate_theme.pydevtools_extractor.pyfigma_converter.pypwa_generator.pytheme_mirror_generator.pyversion_detector.pytemplates/theme_module/__manifest__.py.template详细介绍
Odoo 前端开发技能 v6.0
概述
此技能提供高级 Odoo 前端开发能力,包括:
- 🎨 /create-theme 命令:生成包含所有文件、可用于生产环境的完整主题模块
- 🔧 主题功能激活:通过
theme.utils模型中的_theme_xxx_post_copy()配置模板 - 📋 完整的动态页面参考:包含 XML ID 的所有 11 种页眉、9 种页脚、商店、产品和博客模板
- 🎯 设计工作流:Figma → Odoo 模板匹配 → 配置 → 增强方法
- 主题开发:完整的
$o-website-values-palettes参考、颜色语义和主题镜像模型 - 自动检测:自动检测 Odoo 版本并映射到正确的 Bootstrap 版本
- publicWidget 框架:全面的模式,包含针对网站构建器的
editableMode处理 - 渐进式 Web 应用:Service Worker、离线支持、推送通知
- 现代 JavaScript:TypeScript、ES2020+、Web Components、Owl v1/v2 模式
- 测试框架:Jest、Cypress、视觉回归测试
- 性能优化:Core Web Vitals、资源提示、关键 CSS
- 无障碍:符合 WCAG 2.1 AA 标准、ARIA 模式、键盘导航
- 实时功能:WebSocket、Server-Sent Events、实时协作
- MCP 集成:Figma 设计转换和 Chrome DevTools 样式提取
- 主题脚手架:按照规范结构生成完整主题模块
- 版本适配:处理 Odoo 14-19 之间的差异(Bootstrap 4/5、Owl v1/v2、Snippet 结构)
- DevOps 就绪:CI/CD 流水线、Vite 构建、自动化测试
🎨 /create-theme 命令
快速开始:生成可用于生产环境的完整 Odoo 主题模块。
用法
# Interactive mode (recommended)
/create-theme
# Quick mode with arguments
/create-theme <theme_name> <project_path>
# Full arguments
/create-theme <theme_name> <project_path> --version=17 --colors="#207AB7,#FB9F54,#F6F4F0,#FFFFFF,#191A19" --font="IBM Plex Sans"
创建内容
theme_<name>/
├── __init__.py
├── __manifest__.py
├── security/
│ └── ir.model.access.csv
├── data/
│ ├── assets.xml
│ ├── menu.xml
│ └── pages/ # Individual page files (BEST PRACTICE!)
│ ├── home_page.xml # Homepage (inherits website.homepage)
│ ├── aboutus_page.xml # About Us page
│ ├── contactus_page.xml # Contact (inherits website.contactus)
│ └── services_page.xml # Services page
├── views/
│ ├── layout/
│ │ ├── header.xml # Header customization (OPTIONAL)
│ │ ├── footer.xml # Footer customization (OPTIONAL)
│ │ └── templates.xml # Base layout templates
│ └── snippets/
│ └── custom_snippets.xml # Custom snippet definitions
└── static/src/
├── scss/
│ ├── primary_variables.scss # Theme variables + fonts
│ ├── bootstrap_overridden.scss # Bootstrap overrides (OPTIONAL)
│ └── theme.scss # Additional custom styles
├── js/
│ ├── theme.js # publicWidget implementations
│ └── snippets_options.js # Snippet options (if needed)
└── img/
💡 简化方案(推荐)
大多数情况下,无需自定义 XML,通过 $o-website-values-palettes 即可完成配置:
'header-template':'default'|'hamburger'|'vertical'|'sidebar''footer-template':'default'|'centered'|'minimalist'|'links'|'descriptive'
适用自定义 header.xml/footer.xml 的情况:
- 设计需要模板中没有的完全自定义布局
- 需要添加模板未提供的 HTML 元素
颜色系统(o-color-1 至 o-color-5)
| 变量 | 语义 | 默认值 |
|---|---|---|
o-color-1 | 品牌主色 | #207AB7 |
o-color-2 | 辅助色/强调色 | #FB9F54 |
o-color-3 | 浅色背景 | #F6F4F0 |
o-color-4 | 白色/主体基色 | #FFFFFF |
o-color-5 | 深色文本/标题 | #191A19 |
版本支持
- Odoo 14-15:Bootstrap 4.5.0,简单 Snippet
- Odoo 16-17:Bootstrap 5.1.3,现代资源包
- Odoo 18-19:Bootstrap 5.1.3,需要 Snippet 组
直接运行脚本
python scripts/create_theme.py <theme_name> <output_path> --version=17 --colors="#207AB7,#FB9F54,#F6F4F0,#FFFFFF,#191A19"
关键规则
- 绝不修改 Odoo 核心代码,即
odoo/或odoo/addons/目录中的内容 - 禁止内联 JS/CSS——创建独立的
.js和.scss文件 - JS 模块:使用
/** @odoo-module **/注解 - 网站主题:仅使用
publicWidget框架(不要使用 Owl 或原生 JS) - Bootstrap:Odoo 16+ 使用 v5.1.3 类(绝不使用 Tailwind)
- 模块命名:使用
snake_case规范 - 翻译:在定义时(而非通过运行时包装器)使用
_t()将 JS 数组/常量中的静态标签标记为可翻译。静态 XML 字符串会自动翻译。
自动检测工作流
步骤 1:检测 Odoo 版本
开始任何前端任务时,首先检测 Odoo 版本:
# Use the version detector script
python scripts/version_detector.py <module_path>
版本检测器将:
- 查找
__manifest__.py或__openerp__.py - 解析版本字段
- 如果未找到,则从父目录检测(例如
odoo17/) - 返回:
- Odoo 版本(例如 “17.0”)
- Bootstrap 版本(例如 “5.1.3”)
- Owl 版本(例如 “2.x” 或 None)
- 模块类型(主题、网站、自定义)
步骤 2:引导版本映射
自动选择引导版本:
- Odoo 14-15:Bootstrap 4.5.0
- Odoo 16-19:Bootstrap 5.1.3
步骤 3:检测 Owl 版本
JavaScript 框架选择:
- Odoo 14-15:Owl 实验版或仅使用 jQuery
- Odoo 16-17:Owl v1
- Odoo 18-19:Owl v2(存在破坏性变更)
步骤 4:检测片段结构
片段注册方式:
- Odoo 14-17:直接插入片段,无需分组
- Odoo 18-19:必须使用片段组(带有分组的
snippet_structure)
主题变量完整参考
本节完整介绍 Odoo 主题中使用的三种核心 SCSS 变量系统。
📚 1. $o-theme-font-configs - Google Fonts 配置
$o-theme-font-configs 用于定义 Odoo 网站主题中可用的字体,并自动导入 Google Fonts。
结构
$o-theme-font-configs: (
'<Font Name>': (
'family': (<CSS font-family list>), // Required: Font family with fallbacks
'url': '<Google Fonts parameter>', // Required*: Google Fonts query param ONLY
'properties': ( // Optional: Per-context CSS overrides
'<font-alias>': (
'<website-value-key>': <value>,
),
),
),
);
⚠️ 重要提示:'url' 键中仅包含查询参数,而非完整 URL!
// ✅ CORRECT - Only the font parameter
'url': 'Poppins:300,300i,400,400i,600,600i,700,700i'
// The system generates: https://fonts.googleapis.com/css?family=Poppins:...&display=swap
// ❌ WRONG - Do not include full URL
'url': 'https://fonts.googleapis.com/css?family=Poppins:300,400,700'
字重指定
// Format: FontName:weight1,weight1i,weight2,weight2i,...
// - Number alone = normal style
// - Number + 'i' = italic style
'url': 'Poppins:300,300i,400,400i,600,600i,700,700i'
// Light Light-i Regular Regular-i SemiBold ...
多单词字体名称
// Replace spaces with +
'url': 'Open+Sans:300,300i,400,400i,700,700i'
'url': 'Source+Sans+Pro:300,300i,400,400i,700,700i'
'url': 'DM+Serif+Display:400,400i'
字体别名(用于 'properties' 键)
| 别名 | 映射到 | 用途 |
|---|---|---|
'base' | 'font' | 正文文本 |
'headings' | 'headings-font' | 所有标题(H1-H6) |
'h2' - 'h6' | 'h2-font' - 'h6-font' | 单个标题 |
'navbar' | 'navbar-font' | 导航菜单 |
'buttons' | 'buttons-font' | 按钮文本 |
'display-1' - 'display-4' | 'display-1-font' - 'display-4-font' | 展示文本 |
完整示例
// ⚠️ STANDALONE definition - NO map-merge with core variables!
$o-theme-font-configs: (
'Poppins': (
'family': ('Poppins', sans-serif),
'url': 'Poppins:300,300i,400,400i,500,500i,600,600i,700,700i',
'properties': (
'base': (
'font-size-base': (15 / 16) * 1rem,
'header-font-size': (15 / 16) * 1rem,
),
),
),
'Playfair Display': (
'family': ('Playfair Display', serif),
'url': 'Playfair+Display:400,400i,700,700i',
),
'Inter': (
'family': ('Inter', sans-serif),
'url': 'Inter:300,400,500,600,700',
),
);
阿拉伯语/RTL 字体支持
$o-theme-font-configs: (
'IBM Plex Sans Arabic': (
'family': ('IBM Plex Sans Arabic', sans-serif),
'url': 'IBM+Plex+Sans+Arabic:100,200,300,400,500,600,700',
),
'Cairo': (
'family': ('Cairo', sans-serif),
'url': 'Cairo:200,300,400,500,600,700,800,900',
),
'Almarai': (
'family': ('Almarai', sans-serif),
'url': 'Almarai:300,400,700,800',
),
);
📚 2. $o-color-palettes - 颜色系统
$o-color-palettes 用于定义所有颜色调色板,其中包含 5 种核心颜色以及组件颜色分配。
五种核心颜色
| 颜色 | 语义 | 典型用途 |
|---|---|---|
o-color-1 | 主色/强调色 | 品牌色、按钮、链接、突出显示 |
o-color-2 | 辅助色 | 互补强调色、辅助按钮 |
o-color-3 | 浅色背景 | 区块背景、卡片、浅色区域 |
o-color-4 | 白色/最浅色 | 主要内容背景(通常为 #FFFFFF) |
o-color-5 | 深色/文本色 | 深色背景、文本颜色、页脚 |
视觉示意
┌─────────────────────────────────────────────────────────┐
│ o-color-4 (White Background) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ o-color-3 (Light Section) │ │
│ │ Text: o-color-5 (Dark) │ │
│ │ Links: o-color-1 (Primary) │ │
│ │ Buttons: o-color-1 (Primary), o-color-2 (Sec)│ │
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ o-color-5 (Dark Section - Footer) │ │
│ │ Text: o-color-4 (White) │ │
│ │ Links: o-color-3 (Light) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
颜色组合(o_cc1 - o_cc5)
颜色组合是预设配色方案,可自动设置背景、文本、标题、链接和按钮的颜色。
| 类 | 背景 | 典型用途 |
|---|---|---|
o_cc1 | o-color-4(白色) | 主要内容区域 |
o_cc2 | o-color-3(浅色) | 交替区块 |
o_cc3 | o-color-2(辅助色) | 强调区块 |
o_cc4 | o-color-1(主色) | 行动号召区块 |
o_cc5 | o-color-5(深色) | 页脚、深色区块 |
颜色调色板结构
$o-color-palettes: map-merge($o-color-palettes, (
'my-palette': (
// Required: The 5 core colors
'o-color-1': #09294F, // Primary (Navy blue)
'o-color-2': #FFA807, // Secondary (Orange)
'o-color-3': #F6F4F0, // Light background
'o-color-4': #FFFFFF, // White
'o-color-5': #1B212C, // Dark
// Component color assignments (1-5 → o_cc1-o_cc5)
'menu': 1, // Menu uses o_cc1
'footer': 5, // Footer uses o_cc5
'copyright': 5, // Copyright uses o_cc5
// Color combination overrides
'o-cc1-text': 'o-color-5',
'o-cc1-headings': 'o-color-5',
'o-cc5-link': 'o-color-4',
'o-cc5-btn-primary': 'o-color-2',
),
));
颜色组合的覆盖语法
// Override specific colors within a combination
'o-cc{n}-{property}': value
// Available properties:
'o-cc1-text': #333333, // Text color
'o-cc1-headings': 'o-color-5', // Headings color
'o-cc1-link': 'o-color-1', // Link color
'o-cc1-btn-primary': 'o-color-1', // Primary button
'o-cc1-btn-secondary': 'o-color-2', // Secondary button
// Example for dark background (o_cc5)
'o-cc5-text': rgba(#fff, .8), // Semi-transparent white
'o-cc5-headings': 'o-color-4', // White headings
'o-cc5-link': 'o-color-4', // White links
'o-cc5-btn-primary': 'o-color-2', // Orange buttons on dark
HTML 用法
<section class="o_cc o_cc1 pt32 pb32">
<div class="container"><h2>Content</h2></div>
</section>
<section class="o_cc o_cc2 pt32 pb32">...</section>
<section class="o_cc o_cc4 pt32 pb32">...</section>
<section class="o_cc o_cc5 pt32 pb32">...</section>
📚 3. $o-website-values-palettes - 完整配置(115+ 个键)
$o-website-values-palettes 是主配置变量,用于控制 Bootstrap 组件、排版、按钮、输入框、页眉/页脚模板等。
按类别快速查阅
| 类别 | 键数量 | 说明 |
|---|---|---|
| 排版与字体 | 13 | 字体系列配置 |
| 字号 | 13 | 基础字号和标题字号 |
| 行高 | 11 | 文本间距 |
| 外边距 | 22 | 标题和段落外边距 |
| 按钮 | 17 | 按钮样式 |
| 输入框 | 12 | 表单字段样式 |
| 页眉 | 13 | 页眉/导航配置 |
| 页脚 | 3 | 页脚配置 |
| 链接 | 1 | 链接下划线行为 |
| 布局 | 3 | 页面布局 |
| 颜色与渐变 | 5 | 调色板和渐变 |
| Google Fonts | 2 | 加载其他字体 |
| 总计 | 115+ |
3.1 排版与字体
| 键 | 类型 | 默认值 | 描述 |
|---|---|---|---|
'font' | 字符串 | 配置中的首个值 | 整个网站的基础字体 |
'headings-font' | 字符串 | 继承 font | H1-H6 标题的字体 |
'navbar-font' | 字符串 | 继承 font | 导航菜单的字体 |
'buttons-font' | 字符串 | 继承 font | 按钮文本的字体 |
'h2-font' - 'h6-font' | 字符串 | 继承标题字体 | 各级标题的字体 |
'display-1-font' - 'display-4-font' | 字符串 | 继承标题字体 | 展示文本的字体 |
3.2 字体大小
| 键 | 类型 | 默认值 | 描述 |
|---|---|---|---|
'font-size-base' | 尺寸 | 1rem | 基础字体大小(16px) |
'small-font-size' | 尺寸 | 0.875rem | 小号文本(14px) |
'h1-font-size' - 'h6-font-size' | 尺寸 | 计算得出 | 标题字号 |
'display-1-font-size' - 'display-6-font-size' | 尺寸 | 5rem-2.5rem | 展示字号 |
3.3 行高与边距
| 键 | 类型 | 默认值 | 描述 |
|---|---|---|---|
'body-line-height' | 数字 | 1.5 | 正文行高 |
'headings-line-height' | 数字 | 1.2 | 所有标题的行高 |
'h2-line-height' - 'h6-line-height' | 数字 | 继承 | 各级标题的行高 |
'paragraph-margin-top' | 尺寸 | 0 | 段落上边距 |
'paragraph-margin-bottom' | 尺寸 | 16px | 段落下边距 |
'headings-margin-top' | 尺寸 | 0 | 标题上边距 |
'headings-margin-bottom' | 尺寸 | 0.5rem | 标题下边距 |
'h2-margin-top' - 'h6-margin-top' | 尺寸 | 继承 | 各级标题的上边距 |
'h2-margin-bottom' - 'h6-margin-bottom' | 尺寸 | 继承 | 各级标题的下边距 |
3.4 按钮(17 个键)
| 键 | 类型 | 默认值 | 描述 |
|---|---|---|---|
'btn-padding-y' | 尺寸 | Bootstrap | 垂直内边距 |
'btn-padding-x' | 尺寸 | Bootstrap | 水平内边距 |
'btn-font-size' | 尺寸 | Bootstrap | 字体大小 |
'btn-padding-y-sm' / 'btn-padding-x-sm' | 尺寸 | Bootstrap | 小号按钮内边距 |
'btn-padding-y-lg' / 'btn-padding-x-lg' | 尺寸 | Bootstrap | 大号按钮内边距 |
'btn-font-size-sm' / 'btn-font-size-lg' | 尺寸 | Bootstrap | 不同尺寸的字号 |
'btn-border-width' | 尺寸 | Bootstrap | 边框粗细 |
'btn-border-radius' | 尺寸 | Bootstrap | 圆角半径 |
'btn-border-radius-sm' / 'btn-border-radius-lg' | 尺寸 | Bootstrap | 不同尺寸的圆角半径 |
'btn-primary-outline' | 布尔值 | false | 将主要按钮设为描边样式 |
'btn-secondary-outline' | 布尔值 | false | 将次要按钮设为描边样式 |
'btn-primary-flat' | 布尔值 | false | 主要按钮的扁平样式 |
'btn-secondary-flat' | 布尔值 | false | 次要按钮的扁平样式 |
'btn-ripple' | 布尔值 | false | Material Design 涟漪效果 |
3.5 输入框与表单(12 个键)
| 键 | 类型 | 默认值 | 描述 |
|---|---|---|---|
'input-padding-y' / 'input-padding-x' | 尺寸 | Bootstrap | 输入框内边距 |
'input-font-size' | 尺寸 | Bootstrap | 输入框字号 |
'input-padding-y-sm' / 'input-padding-x-sm' | 尺寸 | Bootstrap | 小号输入框内边距 |
'input-padding-y-lg' / 'input-padding-x-lg' | 尺寸 | Bootstrap | 大号输入框内边距 |
'input-border-width' | 尺寸 | Bootstrap | 边框粗细 |
'input-border-radius' | 尺寸 | Bootstrap | 圆角半径 |
'input-border-radius-sm' / 'input-border-radius-lg' | 尺寸 | Bootstrap | 不同尺寸的圆角半径 |
3.6 页眉与导航(13 个键)
| 键 | 类型 | 值 | 默认值 | 描述 |
|---|---|---|---|---|
'header-template' | 字符串 | 见下文 | 'default' | 页眉布局 |
'header-links-style' | 字符串 | 见下文 | 'default' | 导航链接样式 |
'header-font-size' | 尺寸 | CSS 长度 | Bootstrap | 页眉文本大小 |
'logo-height' | 尺寸 | CSS 长度 | 导航栏高度 | Logo 高度 |
'fixed-logo-height' | 尺寸 | CSS 长度 | 更小 | 固定时的 Logo 高度 |
'hamburger-position' | 字符串 | 'left'/'center'/'right' | 'left' | 桌面端位置 |
'hamburger-position-mobile' | 字符串 | 'left'/'center'/'right' | 'left' | 移动端位置 |
'menu-border-width' | 尺寸 | CSS 长度 | null | 菜单边框 |
'menu-border-radius' | 尺寸 | CSS 长度 | null | 菜单圆角 |
'menu-box-shadow' | CSS | 阴影/无 | null | 菜单阴影 |
'sidebar-width' | 尺寸 | CSS 长度 | 18.75rem | 侧边栏宽度 |
页眉模板选项:
'default'- 标准水平导航栏'hamburger'- 汉堡菜单(折叠)'vertical'- 垂直侧边栏导航'sidebar'- 完整侧边栏布局
页眉链接样式选项:
'default'- 标准链接'fill'- 悬停时填充背景'outline'- 悬停时显示轮廓边框'pills'- 圆角药丸形链接'block'- 块状链接'border-bottom'- 悬停时显示下划线边框
3.7 页脚(3 个键)
| 键 | 类型 | 值 | 默认值 | 描述 |
|---|---|---|---|---|
'footer-template' | 字符串 | 模板名称 | 'default' | 页脚布局 |
'footer-effect' | 字符串 | 见下文 | null | 动画效果 |
'footer-scrolltop' | 布尔值 | true/false | false | 回到顶部按钮 |
页脚模板选项:
'default'- 标准页脚'centered'- 居中对齐'minimalist'- 简洁极简'links'- 链接较多'descriptive'- 详细描述
页脚效果:
null- 无效果(静态)'slideout_slide_hover'- 悬停时滑出'slideout_shadow'- 滚动时显示阴影
3.8 链接(1 个键)
| 键 | 类型 | 值 | 默认值 | 描述 |
|---|---|---|---|---|
'link-underline' | 字符串 | 'never'/'hover'/'always' | 'hover' | 下划线行为 |
3.9 布局(3 个键)
| 键 | 类型 | 值 | 默认值 | 描述 |
|---|---|---|---|---|
'layout' | 字符串 | 'full'/'boxed' | 'full' | 页面布局 |
'body-image' | URL | 图片路径 | null | 背景图片 |
'body-image-type' | 字符串 | 'image'/'pattern' | 'image' | 背景类型 |
3.10 颜色与渐变(5 个键)
| 键 | 类型 | 默认值 | 描述 |
|---|---|---|---|
'color-palettes-name' | 字符串 | null | 当前颜色调色板名称 |
'menu-gradient' | CSS 渐变 | null | 菜单背景渐变 |
'menu-secondary-gradient' | CSS 渐变 | null | 次级菜单渐变 |
'footer-gradient' | CSS 渐变 | null | 页脚背景渐变 |
'copyright-gradient' | CSS 渐变 | null | 版权区域渐变 |
3.11 Google Fonts(2 个键)
| 键 | 类型 | 默认值 | 描述 |
|---|---|---|---|
'google-fonts' | 列表 | null | 要加载的其他 Google 字体 |
'google-local-fonts' | 映射 | null | 本地托管的字体 |
完整示例 - 现代企业主题
$o-website-values-palettes: (
(
// === REQUIRED ===
'color-palettes-name': 'my-corporate-palette',
// === TYPOGRAPHY ===
'font': 'Inter',
'headings-font': 'Inter',
'navbar-font': 'Inter',
'buttons-font': 'Inter',
'font-size-base': 1rem,
'headings-line-height': 1.3,
'body-line-height': 1.6,
// === HEADER (NO custom header.xml needed!) ===
'header-template': 'default', // or 'hamburger', 'vertical', 'sidebar'
'header-links-style': 'default', // or 'pills', 'fill', 'border-bottom'
'logo-height': 48px,
'fixed-logo-height': 36px,
// === BUTTONS ===
'btn-padding-y': 0.75rem,
'btn-padding-x': 1.5rem,
'btn-padding-y-lg': 1rem,
'btn-padding-x-lg': 2rem,
'btn-border-radius': 8px,
'btn-border-radius-lg': 12px,
'btn-ripple': true,
// === INPUTS ===
'input-padding-y': 0.75rem,
'input-padding-x': 1rem,
'input-border-radius': 8px,
// === FOOTER (NO custom footer.xml needed!) ===
'footer-template': 'default', // or 'centered', 'minimalist', 'links'
'footer-scrolltop': true,
// === LINKS ===
'link-underline': 'hover', // or 'never', 'always'
// === LAYOUT ===
'layout': 'full', // or 'boxed'
),
);
⚠️ 严重:Odoo 主题中的 SCSS 加载顺序
主题 SCSS 文件会在核心 Odoo 变量定义之前加载!
在清单的资产包中使用 prepend 时,你的 SCSS 会在 Odoo 核心 primary_variables.scss 之前执行:
LOAD ORDER:
1. YOUR theme's primary_variables.scss (via prepend) ← FIRST
2. Odoo core primary_variables.scss ← SECOND
3. Other SCSS files
⛔ 严重限制:
- 不能对核心变量使用 map-merge()(这些变量此时尚不存在!)
- 加载主题时,
$o-color-palettes、$o-theme-color-palettes、$o-theme-font-configs均未定义
❌ 错误(会导致“Undefined variable”错误):
$o-color-palettes: map-merge($o-color-palettes, (...)); // ERROR!
$o-theme-font-configs: map-merge($o-theme-font-configs, (...)); // ERROR!
✅ 正确(作为独立项定义):
// Standalone font config (no map-merge!)
$o-theme-font-configs: (
'Poppins': (
'family': ('Poppins', sans-serif),
'url': 'Poppins:300,300i,400,400i,500,500i,600,600i,700,700i',
),
);
// Reference existing palette by name
$o-website-values-palettes: (
(
'color-palettes-name': 'default-1', // Use existing palette name!
'font': 'Poppins',
// ...other values
),
);
❌ 错误:主题中不要使用 ir.asset 记录来加载 Google Fonts,否则会导致 URL 格式错误!
主题页面创建规范
单个页面文件模式(推荐)
创建单独的页面文件,而不是单个 pages.xml:
theme_name/
├── data/
│ ├── home.xml # Homepage template + page
│ ├── about.xml # About page template + page
│ ├── contact.xml # Contact page (inherits website.contactus)
│ ├── services.xml # Services page template + page
│ └── menu.xml # Menu configuration
└── views/
└── templates.xml # Shared templates and layout
主页(继承 website.homepage)
<template id="view_home" inherit_id="website.homepage" name="Home">
<xpath expr="//div[@id='wrap']" position="replace">
<div id="wrap" class="oe_structure">
</div>
</xpath>
</template>
联系页面(继承 website.contactus)
<template id="view_contact" inherit_id="website.contactus" name="Contact">
<xpath expr="//h1" position="replace">
<h1>Get in Touch</h1>
</xpath>
</template>
自定义页面(theme.website.page)
<template id="view_about" name="About">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<section class="s_title pt96 pb48">
<div class="container">
<h1>About Us</h1>
</div>
</section>
</div>
</t>
</template>
<record id="page_about" model="theme.website.page">
<field name="view_id" ref="view_about"/>
<field name="is_published" eval="True"/>
<field name="url">/about</field>
<field name="name">About</field>
</record>
主题镜像模型架构
主题如何安装到页面
Theme Module XML
↓
theme.ir.ui.view (Template View)
↓
theme.website.page (Template Page)
↓ (Theme Installation)
↓
ir.ui.view (Actual View with website_id)
↓
website.page (Actual Page with website_id)
要点
- 主题模块包含
theme.*模型(模板) - 安装时,这些模型会转换为带有
website_id的实际模型 - 每个网站都会获得独立副本,从而可以重复使用主题
website_id会在视图创建层级分配
主题脚手架命令
命令:搭建完整主题模块
触发条件:用户要求“创建主题”“搭建主题”或“生成主题模块”
工作流:
-
检测上下文
# Determine current Odoo version cd <project_path> python -c "import sys; sys.path.insert(0, 'helpers'); from version_detector import detect_version; print(detect_version('.'))" -
创建模块结构
theme_<name>/ ├── __init__.py ├── __manifest__.py ├── security/ │ └── ir.model.access.csv ├── data/ │ ├── assets.xml │ ├── menu.xml │ └── pages/ │ ├── home_page.xml │ └── aboutus_page.xml ├── views/ │ ├── layout/ │ │ ├── header.xml # Header customization (OPTIONAL) │ │ ├── footer.xml # Footer customization (OPTIONAL) │ │ └── templates.xml # Base layout templates │ └── snippets/ │ └── custom_snippets.xml # Custom snippet definitions ├── static/ │ └── src/ │ ├── scss/ │ │ ├── primary_variables.scss │ │ ├── bootstrap_overridden.scss │ │ └── theme.scss │ ├── js/ │ │ ├── theme.js │ │ └── snippets_options.js │ └── img/ └── README.md -
生成
__manifest__.py{ 'name': 'Theme <Name>', 'version': '<odoo_version>.1.0.0', 'category': 'Website/Theme', 'author': 'TaqaTechno', 'website': 'https://www.taqatechno.com/', 'support': '[email protected]', 'license': 'LGPL-3', 'depends': ['website'], 'data': [ 'security/ir.model.access.csv', 'views/layout/templates.xml', 'views/layout/header.xml', 'views/layout/footer.xml', 'views/snippets/custom_snippets.xml', 'data/menu.xml', 'data/pages/home_page.xml', 'data/pages/aboutus_page.xml', 'data/pages/contactus_page.xml', ], 'assets': { 'web._assets_primary_variables': [ ('prepend', 'theme_<name>/static/src/scss/primary_variables.scss'), ], 'web._assets_frontend_helpers': [ 'theme_<name>/static/src/scss/bootstrap_overridden.scss', ], 'web.assets_frontend': [ 'theme_<name>/static/src/scss/theme.scss', 'theme_<name>/static/src/js/theme.js', ], 'website.assets_wysiwyg': [ 'theme_<name>/static/src/js/snippets_options.js', ], }, 'installable': True, 'auto_install': False, 'application': False, } -
生成
primary_variables.scss(完整 v5.0)// =================================================================== // Theme: <Name> // Generated by TAQAT Techno /create-theme command v5.0 // =================================================================== // // ⚠️ IMPORTANT: This file is PREPENDED before Odoo core variables! // DO NOT use map-merge() with core variables - they don't exist yet! // =================================================================== // === Font Configuration (STANDALONE - no map-merge!) === $o-theme-font-configs: ( 'Inter': ( 'family': ('Inter', sans-serif), 'url': 'Inter:300,300i,400,400i,500,500i,600,600i,700,700i', ), ); // === Website Values Palette (MASTER CONFIGURATION) === // NO bootstrap_overridden.scss needed - configure everything here! $o-website-values-palettes: ( ( // Reference existing palette (avoids map-merge issues) 'color-palettes-name': 'default-1', // === Typography === 'font': 'Inter', 'headings-font': 'Inter', 'navbar-font': 'Inter', 'buttons-font': 'Inter', // === Header (NO custom header.xml needed!) === // Options: 'default' | 'hamburger' | 'vertical' | 'sidebar' 'header-template': 'default', 'header-links-style': 'default', 'logo-height': 3rem, 'fixed-logo-height': 2rem, // === Buttons === 'btn-padding-y': 0.45rem, 'btn-padding-x': 1.35rem, 'btn-border-radius': 0.25rem, // === Inputs === 'input-padding-y': 0.45rem, 'input-border-radius': 0.25rem, // === Footer (NO custom footer.xml needed!) === // Options: 'default' | 'centered' | 'minimalist' | 'links' | 'descriptive' 'footer-template': 'default', 'footer-scrolltop': true, // === Links & Layout === 'link-underline': 'hover', 'layout': 'full', ) ); -
生成初始模板
<?xml version="1.0" encoding="utf-8"?> <odoo> <template id="layout" inherit_id="website.layout"> <xpath expr="//head" position="inside"> <meta name="theme-name" content="<Name>"/> </xpath> </template> </odoo>
命令:创建片段(适配版本)
触发条件:用户要求“创建片段”或“添加自定义片段”
工作流:
-
检测 Odoo 版本(确定注册方式)
-
适用于 Odoo 17 及更早版本:
<template id="s_<snippet_name>" name="<Snippet Name>"> <section class="s_<snippet_name>" data-name="<Snippet Name>"> <div class="container"> </div> </section> </template> <template id="s_<snippet_name>_insert" inherit_id="website.snippets"> <xpath expr="//div[@id='snippet_effect']//t[@t-snippet][last()]" position="after"> <t t-snippet="theme_<name>.s_<snippet_name>" string="<Snippet Name>" t-thumbnail="/theme_<name>/static/img/snippets/<snippet_name>.svg"/> </xpath> </template> -
适用于 Odoo 18/19:
<template id="s_<snippet_name>" name="<Snippet Name>"> <section class="s_<snippet_name>" data-name="<Snippet Name>"> <div class="container"> </div> </section> </template> <template id="snippet_group_custom" inherit_id="website.snippets"> <xpath expr="//div[@id='snippet_groups']" position="inside"> <t snippet-group="custom" t-snippet="website.s_snippet_group" string="Custom Snippets"/> </xpath> </template> <template id="s_<snippet_name>_insert" inherit_id="website.snippets"> <xpath expr="//div[@id='snippet_structure']/*[1]" position="before"> <t t-snippet="theme_<name>.s_<snippet_name>" string="<Snippet Name>" group="custom" t-thumbnail="/theme_<name>/static/img/snippets/<snippet_name>.svg"/> </xpath> </template> -
生成片段选项(如有需要)
<template id="s_<snippet_name>_options" inherit_id="website.snippet_options"> <xpath expr="." position="inside"> <div data-selector=".s_<snippet_name>"> <we-select string="Layout"> <we-button data-select-class="">Default</we-button> <we-button data-select-class="s_<snippet_name>_alt">Alternate</we-button> </we-select> <we-row string="Items"> <we-button data-add-item="true" data-no-preview="true" class="o_we_bg_brand_primary">Add Item</we-button> </we-row> <we-colorpicker string="Background Color" data-css-property="background-color"/> </div> </xpath> </template> -
生成选项 JavaScript(如需动态行为)
/** @odoo-module **/ import options from "@web_editor/js/editor/snippets.options"; options.registry.<SnippetName> = options.Class.extend({ /** * Add item to snippet */ addItem: function(previewMode, widgetValue, params) { let $lastItem = this.$target.find('.snippet-item').last(); if ($lastItem.length) { $lastItem.clone().appendTo(this.$target.find('.snippet-container')); } }, });在清单资产中包含:
'assets': { 'website.assets_wysiwyg': [ 'theme_<name>/static/src/js/snippets_options.js', ], }
Figma 集成工作流
命令:将 Figma 设计转换为 Odoo 主题
触发条件:用户提供 Figma URL,或要求“将 Figma 转换为 Odoo”“导入 Figma 设计”
先决条件:必须能够使用 Figma MCP
工作流:
-
使用 Figma MCP 和特定版本的提示词
Convert this Figma design to HTML for Odoo <version> website theme with these requirements: - Use Bootstrap v<bootstrap_version> classes (not Tailwind) - Apply proper Odoo theme structure with sections and containers - Include responsive classes (col-sm-*, col-md-*, col-lg-*) - Use semantic HTML5 elements (header, nav, main, section, article) - Add data attributes for Odoo website builder compatibility - Map colors to CSS custom properties for theme variables - Include accessibility attributes (aria-labels, alt text) -
提取调色板
Extract the color palette from this Figma design and map to CSS custom properties using the o-color-1 through o-color-5 naming convention for Odoo themes. -
将 HTML 转换为 QWeb 模板
- 用
<template>标签包裹 - 添加
t-name属性 - 添加供 Odoo 编辑器使用的数据属性
- 转换为基于区块的结构
- 用
-
根据 Figma 颜色生成 SCSS
python scripts/figma_converter.py <figma_url> <output_dir> <odoo_version> -
根据 Figma 组件创建片段
- 使用转换后的 HTML 作为片段内容
- 添加正确的 Odoo 结构
- 根据版本(17 与 18/19)注册片段
- 如果需要交互功能,则生成选项
Figma MCP 提示词参考
基础 HTML + Bootstrap:
Generate this Figma selection as HTML with Bootstrap v<version> classes instead of React and Tailwind. Use Odoo-compatible structure with proper semantic HTML5 elements.
颜色提取:
Extract colors as CSS custom properties for Odoo theme
排版提取:
Convert this Figma text styling to Bootstrap v<version> typography classes and CSS custom properties. Map font families to Google Fonts configuration for Odoo theme integration.
布局提取:
Convert this Figma layout to Bootstrap v<version> grid system and utility classes. Use proper container, row, col-* structure with responsive breakpoints.
Chrome DevTools 集成工作流
命令:从在线网站提取样式
触发条件:用户提供网站 URL,或要求“从网站提取”“复制网站样式”
前提条件:必须可用 Chrome DevTools MCP
工作流:
-
连接到 DevTools MCP
- 在 Chrome DevTools MCP 中打开 URL
- 选择要提取的元素
-
提取计算样式
python scripts/devtools_extractor.py <url> <output_dir> -
转换为 Odoo SCSS
- 将 CSS 属性映射为 SCSS 变量
- 将颜色转换为 Odoo 主题变量(o-color-*)
- 在适用时使用 Bootstrap 变量
-
映射到 Bootstrap 工具类
- 找出等效的 Bootstrap 类
- 尽可能用工具类替换自定义 CSS
- 生成基于类的实现方案建议
-
将 DOM 转换为 QWeb
- 提取 HTML 结构
- 添加 Odoo 专属属性
- 使用正确的模板结构进行封装
- 添加 Bootstrap 网格结构
-
生成完整的主题代码
- 包含提取样式的 SCSS 文件
- 包含结构的 QWeb 模板
- Bootstrap 类建议
- 片段注册 XML
JavaScript 开发
Public Widget 模式(publicWidget——主题必需)
重要:对于网站主题,始终使用 publicWidget 框架,不要使用 Owl 或原生 JavaScript。
适用场景:网站交互、主题功能、表单处理、动画
包含 editableMode 的完整结构:
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
publicWidget.registry.MyWidget = publicWidget.Widget.extend({
selector: '.my-selector',
disabledInEditableMode: false, // Allow in website builder
events: {
'click .button': '_onClick',
'change input': '_onChange',
'submit form': '_onSubmit',
},
/**
* Lifecycle: Called when widget starts
* CRITICAL: Check editableMode for website builder compatibility
*/
start: function () {
// IMPORTANT: Only run animations/interactions in read mode
if (!this.editableMode) {
this._initializeAnimation();
this._bindExternalEvents();
}
return this._super.apply(this, arguments);
},
/**
* Initialize animations (only in read mode)
*/
_initializeAnimation: function () {
// Animation code that should NOT run in edit mode
this.$el.addClass('animated');
},
/**
* Bind events outside the widget element
*/
_bindExternalEvents: function () {
$(window).on('scroll.myWidget', this._onScroll.bind(this));
$(window).on('resize.myWidget', this._onResize.bind(this));
},
/**
* Click handler
*/
_onClick: function (ev) {
ev.preventDefault();
// Don't run in edit mode
if (this.editableMode) return;
// Handler logic
},
/**
* Scroll handler
*/
_onScroll: function () {
// Throttle scroll events for performance
},
/**
* CRITICAL: Clean up event listeners to prevent memory leaks
*/
destroy: function () {
$(window).off('.myWidget'); // Remove namespaced events
this._super.apply(this, arguments);
},
});
export default publicWidget.registry.MyWidget;
publicWidget 要点:
- 始终检查
this.editableMode,再运行动画或交互 - 使用
disabledInEditableMode: false,让 Widget 在网站构建器中正常工作 - 始终在
destroy()方法中清理事件监听器 - 网站主题绝不要使用 Owl 或原生 JavaScript——只能使用 publicWidget
- 使用带命名空间的事件(
.myWidget),以便轻松清理
在清单中引入:
'assets': {
'web.assets_frontend': [
'module_name/static/src/js/my_widget.js',
],
}
Owl 组件模式(现代、响应式)
适用场景:复杂的交互式 UI、响应式状态管理、组件组合
Odoo 17(Owl v1):
/** @odoo-module **/
import { Component, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
class MyComponent extends Component {
setup() {
this.state = useState({
items: [],
loading: false,
});
}
async willStart() {
await this.loadData();
}
async loadData() {
this.state.loading = true;
// Fetch data via RPC
const data = await this.rpc('/my/route');
this.state.items = data.items;
this.state.loading = false;
}
onItemClick(item) {
console.log('Clicked:', item);
}
}
MyComponent.template = "module_name.MyComponentTemplate";
registry.category("public_components").add("MyComponent", MyComponent);
export default MyComponent;
Odoo 18/19(Owl v2——注意差异):
/** @odoo-module **/
import { Component, useState } from "@odoo/owl";
class MyComponent extends Component {
static template = "module_name.MyComponentTemplate";
static props = {
// Define props with validation
title: { type: String, optional: true },
items: { type: Array },
};
setup() {
this.state = useState({
selectedId: null,
});
}
// Owl v2 uses static template property
}
export default MyComponent;
XML 模板:
<template id="MyComponentTemplate" name="My Component">
<div class="my-component">
<h3 t-if="props.title"><t t-esc="props.title"/></h3>
<ul>
<li t-foreach="props.items" t-as="item" t-key="item.id">
<t t-esc="item.name"/>
</li>
</ul>
</div>
</template>
翻译(_t)最佳实践
关键规则:对于 JavaScript 数组/常量中的静态标签,应在定义时使用 _t(),而不要通过运行时包装方法处理。Odoo 会自动翻译 XML 模板中的静态字符串。
原因?
- Odoo 的翻译系统会自动提取 XML 模板(
.xml文件)中的静态字符串 - 在模板中对静态字符串使用
_t()会重复创建翻译条目,并带来不必要的开销 - 必须在 JavaScript 中定义时用
_t()包装字符串,这样 Odoo 的 PO 提取器才能找到它们 - 运行时包装方法(如
translateLabel(key))不起作用——提取器永远找不到这些字符串
正确——在定义时用 _t() 包装静态标签:
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
// ✅ CORRECT: Wrap each string with _t() at definition time
const MONTHS = [
{value: 1, short: _t("Jan"), full: _t("January")},
{value: 2, short: _t("Feb"), full: _t("February")},
{value: 3, short: _t("Mar"), full: _t("March")},
{value: 4, short: _t("Apr"), full: _t("April")},
{value: 5, short: _t("May"), full: _t("May")},
{value: 6, short: _t("Jun"), full: _t("June")},
{value: 7, short: _t("Jul"), full: _t("July")},
{value: 8, short: _t("Aug"), full: _t("August")},
{value: 9, short: _t("Sep"), full: _t("September")},
{value: 10, short: _t("Oct"), full: _t("October")},
{value: 11, short: _t("Nov"), full: _t("November")},
{value: 12, short: _t("Dec"), full: _t("December")},
];
// ✅ CORRECT: Status labels wrapped at definition
const STATUS_LABELS = {
draft: _t("Draft"),
pending: _t("Pending"),
approved: _t("Approved"),
rejected: _t("Rejected"),
};
export class DatePicker extends Component {
setup() {
this.months = MONTHS; // Already translated at definition
}
get displayLabel() {
const month = this.months.find(m => m.value === this.selectedMonth);
// No translateLabel() needed - values are already translated
return `${month?.short || ""} ${this.selectedYear}`;
}
}
错误——运行时包装方法不起作用:
// ❌ WRONG: Strings without _t() at definition will NOT be translated
const MONTHS = [
{value: 1, label: "Jan", full: "January"}, // NOT FOUND by PO extractor!
{value: 2, label: "Feb", full: "February"},
];
// ❌ WRONG: This pattern does NOT work for translation
translateLabel(key) {
return _t(key); // key is a variable, not a literal - PO extractor can't find it!
}
正确——模板使用预先翻译的值:
<t t-foreach="getAvailableMonths()" t-as="month" t-key="month.value">
<a href="#" t-on-click.prevent="() => this.selectMonth(month.value)">
<t t-esc="month.full"/>
</a>
</t>
正确——静态 XML 字符串无需包装:
<span>No data available</span>
<a href="#">Live</a>
<button type="submit">Submit</button>
何时在定义时使用 _t():
- ✅ JavaScript 数组中的静态标签(月名称、星期名称、状态标签)
- ✅ JavaScript 对象/常量中的静态标签
- ✅ 定义为常量的错误消息
- ✅ 将从 JavaScript 显示给用户的任何字符串字面量
何时不使用 _t():
- ❌ XML 模板中的静态文本(会自动翻译)
- ❌ 运行时包装方法,如
translateLabel(key)(不起作用!) - ❌ 在运行时传递给
_t()的动态变量 - ❌
.xml文件中的任何硬编码字符串
请记住:如果你在 JavaScript 中定义的字符串将显示给用户,应在定义处使用 _t() 包装,而不是在使用处。
SCSS 和 Bootstrap 自定义
主要变量覆盖
文件:static/src/scss/primary_variables.scss
用途:覆盖 Odoo 内置的样式变量(颜色、字体、间距)
引入位置:web._assets_primary_variables 资源包
结构:
// ============================================================
// Theme Primary Variables
// Override Odoo defaults with !default to allow further overriding
// ============================================================
// Color Palette
$o-color-1: #3498db !default; // Primary
$o-color-2: #2ecc71 !default; // Success
$o-color-3: #e74c3c !default; // Danger
$o-color-4: #f39c12 !default; // Warning
$o-color-5: #9b59b6 !default; // Info
// Website Values Palette Configuration
$o-website-values-palettes: (
(
// Color Configuration
'color-palettes-name': 'my-theme',
// Typography
'font': 'Inter',
'headings-font': 'Inter',
'navbar-font': 'Inter',
'buttons-font': 'Inter',
'headings-line-height': 1.2,
// Header & Navigation
'header-template': 'default', // or 'centered', 'boxed'
'header-font-size': 1rem,
'logo-height': 3rem,
'fixed-logo-height': 2rem,
'hamburger-position': 'right', // Mobile menu
// Buttons
'btn-padding-y': 0.45rem,
'btn-padding-x': 1.35rem,
'btn-padding-y-sm': 0.3rem,
'btn-padding-x-sm': 0.9rem,
'btn-padding-y-lg': 0.6rem,
'btn-padding-x-lg': 1.8rem,
'btn-border-radius': 0.25rem,
'btn-border-width': 1px,
'btn-font-size': 1rem,
// Input Fields
'input-padding-y': 0.45rem,
'input-border-radius': 0.25rem,
// Colors (map to palette)
'color-1': $o-color-1,
'color-2': $o-color-2,
'color-3': $o-color-3,
'color-4': $o-color-4,
'color-5': $o-color-5,
)
);
// Google Fonts Configuration (STANDALONE - no map-merge!)
$o-theme-font-configs: (
'Inter': (
'family': ('Inter', sans-serif),
'url': 'Inter:wght@300;400;500;600;700',
),
);
⚠️ 主题中绝不要使用以下模式:
// ❌ WRONG - Will cause "Undefined variable" errors
$o-color-palettes: map-merge($o-color-palettes, (...));
$o-theme-color-palettes: map-merge($o-theme-color-palettes, (...));
$o-theme-font-configs: map-merge($o-theme-font-configs, (...));
Bootstrap 覆盖项
文件:static/src/scss/bootstrap_overridden.scss
用途:覆盖 Odoo 未开放的 Bootstrap 变量
包含于:web._assets_frontend_helpers 资源包
结构:
// ============================================================
// Bootstrap Variable Overrides
// Only override Bootstrap vars not available in Odoo variables
// ============================================================
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
// Spacing
$spacer: 1rem !default;
// Grid breakpoints
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px,
xxl: 1400px
) !default;
// Container max widths
$container-max-widths: (
sm: 540px,
md: 720px,
lg: 960px,
xl: 1140px,
xxl: 1320px
) !default;
// Typography
$font-size-base: 1rem !default;
$line-height-base: 1.5 !default;
// Border radius
$border-radius: 0.25rem !default;
$border-radius-sm: 0.125rem !default;
$border-radius-lg: 0.5rem !default;
// Shadows
$box-shadow-sm: 0 .125rem .25rem rgba(0, 0, 0, .075) !default;
$box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15) !default;
$box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, .175) !default;
特定版本注意事项
Odoo 17 特性
-
Owl v1
- 从
@odoo/owl使用useState - 使用旧版导入路径
- 将组件模板作为单独的属性
- 从
-
片段结构
- 无需分组,直接插入
- 使用 XPath 定位
//div[@id='snippet_effect']
-
公共 Widget
- 从
@web/legacy/js/public/public_widget导入 - 可完全使用 jQuery
- 从
Odoo 18/19 特性
-
Owl v2
- 使用静态模板属性
- 必须进行 Props 验证
- 使用新的响应式钩子
- 相比 v1 有破坏性变更
-
片段分组
- 必须包含
snippet-group属性 - 按类别组织
- XPath 定位
//div[@id='snippet_structure']
- 必须包含
-
Bootstrap 5.1.3
- 均使用相同的 Bootstrap 版本
- 工具类保持一致
-
网站构建器增强功能
- 可在编辑器中浏览主题
- 可通过 UI 上传自定义字体
- 增强的片段选项
Bootstrap 4 到 5 的迁移(Odoo 14/15 → 16+)
迁移主题时:
-
类名替换:
python scripts/bootstrap_mapper.py <old_classes>常见替换:
ml-*→ms-*(margin-left → margin-start)mr-*→me-*(margin-right → margin-end)pl-*→ps-*(padding-left → padding-start)pr-*→pe-*(padding-right → padding-end)text-left→text-starttext-right→text-endfloat-left→float-startfloat-right→float-endform-group→mb-3custom-select→form-selectclose→btn-closebadge-*→bg-*(例如,badge-primary→bg-primary)font-weight-bold→fw-boldsr-only→visually-hiddenno-gutters→g-0
-
已移除的类(请寻找替代方案):
form-inline- 使用网格/弹性布局工具类jumbotron- 使用工具类重新创建media- 使用d-flex和弹性布局工具类
-
移除 jQuery:
- Bootstrap 5 使用原生 JavaScript
- 可能需要调整 Widget 初始化方式
前端性能最佳实践
-
资源打包
- 始终使用 Odoo 的资源包
- 不要单独链接外部文件
- 利用压缩功能(在生产环境中已启用)
-
延迟加载
- 在图片上使用
loading="lazy" - 利用 Odoo 的
/website/image路由自动调整图片大小 - 延迟加载视频(Odoo 18+ 内置支持)
- 在图片上使用
-
优化片段
- 批量处理服务端调用(所有数据使用一次 RPC)
- 使用 QWeb 高效渲染
- 尽量减少 DOM 操作
-
CSS 优化
- 使用 Bootstrap 工具类,而非自定义 CSS
- 保持 HTML 结构精简
- 从资源包中移除未使用的 CSS/JS
-
缓存
- 使用带有
store=True的计算字段 - 缓存开销较大的计算结果
- 在服务端执行繁重的工作
- 使用带有
-
SEO 注意事项
- 在服务端渲染关键内容
- 使用 Owl 增强功能,而非呈现主要内容
- 确保页面加载速度快
安全与访问控制
模型级访问控制
始终创建 security/ir.model.access.csv:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_model_user,model.user,model_model_name,base.group_user,1,1,1,0
access_model_public,model.public,model_model_name,,1,0,0,0
记录级规则
创建 security/rules.xml 以实现精细控制:
<record id="model_rule" model="ir.rule">
<field name="name">Model: User own records</field>
<field name="model_id" ref="model_model_name"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
控制器安全
from odoo import http
class MyController(http.Controller):
@http.route('/public/route', auth='public') # No login required
def public_route(self):
pass
@http.route('/user/route', auth='user') # Login required
def user_route(self):
pass
安全地公开数据
# Use sudo() carefully
records = request.env['model'].sudo().search([('published', '=', True)])
# Check access rights
if not request.env['model'].check_access_rights('read', raise_exception=False):
return request.redirect('/access-denied')
常见工作流
根据 Figma 创建完整主题
- 用户提供 Figma URL
- 检测目标 Odoo 版本
- 使用 Figma MCP 提取设计
- 生成主题模块结构
- 将颜色转换为 SCSS 变量
- 将组件转换为片段
- 创建片段选项
- 在 Odoo 网站构建器中测试
扩展现有主题
- 检测当前主题和版本
- 分析现有结构
- 创建继承模块
- 覆盖指定模板
- 添加自定义片段
- 扩展 SCSS 变量
跨版本迁移主题
- 检测源版本和目标版本
- 转换 Bootstrap 类(如果是 4→5)
- 更新片段结构(如果是 17→18/19)
- 更新 JavaScript(如果是 Owl v1→v2)
- 测试所有功能
- 生成迁移报告
测试与验证
主题安装测试
# Install theme
python -m odoo -c conf/<config>.conf -d <db> -i theme_<name> --stop-after-init
# Check for errors in log
grep ERROR odoo.log
片段注册测试
- 安装主题
- 前往 Website → Edit
- 检查片段面板
- 验证片段是否出现在正确的类别中
- 测试拖放
- 测试片段选项
浏览器测试
- 在多个浏览器中测试(Chrome、Firefox、Safari、Edge)
- 测试响应式断点(sm、md、lg、xl、xxl)
- 验证可访问性(ARIA 标签、键盘导航)
- 检查性能(Lighthouse、Page Speed)
故障排除
⚠️ SCSS 编译错误(严重)
问题:“Undefined variable: $o-color-palettes”或“$o-theme-font-configs”
**原因:**在主题 SCSS 中使用 map-merge() 合并 Odoo 核心变量。主题文件会在核心变量定义之前加载。
解决方案:
- 移除所有与核心变量一起使用的
map-merge()调用 - 将
$o-theme-font-configs定义为独立变量(不进行合并) - 使用
'color-palettes-name': 'default-1'引用现有调色板 - 请参阅上文“严重:SCSS 加载顺序”一节
问题:CSS 显示 0 条规则/样式未加载
**原因:**由于变量未定义,SCSS 编译静默失败。
诊断:
// In browser console
document.styleSheets[0].cssRules.length // Should be > 0
// If 0, SCSS compilation failed silently
解决方案:
- 检查浏览器控制台是否出现“Style compilation failed”错误
- 修复未定义变量错误(见上文)
- 清除资源缓存:
# Via Odoo shell
>>> self.env['ir.attachment'].search([('url', 'like', '/web/assets/')]).unlink()
>>> self.env.cr.commit()
问题:Google Fonts URL 格式错误
**原因:**使用 ir.asset 记录添加外部字体 URL 会导致 URL 重复。
**解决方案:**在 SCSS 中使用 $o-theme-font-configs,而不是 ir.asset XML 记录。
片段未显示
原因:
- XPath 不正确(版本不匹配)
- 缺少 group 属性(Odoo 18/19)
- 未找到模板
- XML 语法错误
解决方案:
- 检查 Odoo 版本并使用正确的插入方法
- 验证模板 ID 是否存在
- 检查 XML 语法
- 查看日志中的错误
样式未生效
原因:
- SCSS 未包含在正确的资源包中
- 变量覆盖时未使用
!default - CSS 优先级问题
- 资源未编译
- SCSS 编译静默失败(检查控制台!)
解决方案:
- 在清单中验证资源包
- 清除浏览器缓存
- 检查资源重新生成情况:
odoo-bin --update theme_<name> - 使用浏览器 DevTools 检查计算后的样式
- 先检查控制台中的 SCSS 错误!
JavaScript 无法运行
原因:
- 对应 Odoo 版本的导入路径不正确
- 未注册组件
- jQuery 冲突(Bootstrap 5)
- Owl 版本不匹配
解决方案:
- 检查对应 Odoo 版本的导入路径
- 验证组件/部件注册情况
- 检查控制台中的错误
- 确保使用了正确的 Owl 版本
页眉/页脚模板中的 XPath 错误
原因://header//nav 等复杂 XPath 表达式可能与 Odoo 的实际 HTML 结构不匹配。
**解决方案:**使用简单的 XPath 表达式:
<xpath expr="//header" position="attributes">
<xpath expr="//header//nav" position="attributes">
专家提示
-
始终先检测版本
- 不要假定 Odoo 版本
- 生成任何代码前都要先检查
-
优先使用 Bootstrap 实用工具类
- 自定义 CSS 越少,维护越容易
- 性能更好
- 与编辑器兼容
-
使用服务器端渲染优化 SEO
- 不要依赖客户端 JavaScript 提供内容
- 仅使用 Owl 增强功能
-
在编辑模式下测试
- 确保网站构建器能够处理你的代码
- 彻底测试片段选项
-
遵循 Odoo 模式
- 研究核心主题(
odoo/addons/theme_*) - 遵循现有惯例
- 使用相同的类前缀(
s_、o_)
- 研究核心主题(
-
文档
- 为复杂的 SCSS 添加注释
- 记录片段选项
- 为主题提供 README
渐进式 Web 应用(PWA)实现
命令:为 Odoo 网站设置 PWA
触发条件:用户要求“制作 PWA”“添加离线支持”或“启用推送通知”
工作流:
-
生成 Service Worker
// static/src/service-worker.js const CACHE_NAME = 'odoo-pwa-v1'; const urlsToCache = ['/', '/web/static/lib/owl/owl.js']; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(urlsToCache)) ); }); self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => response || fetch(event.request)) ); }); -
创建 Web App Manifest
{ "name": "Odoo PWA", "short_name": "OdooPWA", "start_url": "/", "display": "standalone", "background_color": "#875A7B", "theme_color": "#875A7B", "icons": [...] } -
注册 Service Worker
/** @odoo-module **/ import publicWidget from "@web/legacy/js/public/public_widget"; publicWidget.registry.ServiceWorkerRegistration = publicWidget.Widget.extend({ selector: 'body', start: function() { if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js'); } return this._super(...arguments); } });
现代 JavaScript 与 TypeScript
命令:设置 TypeScript
触发条件:用户要求“使用 TypeScript”“添加类型安全”或“配置 TypeScript”
工作流:
-
生成 tsconfig.json
{ "compilerOptions": { "target": "ES2020", "module": "ES2020", "lib": ["ES2020", "DOM"], "strict": true, "paths": { "@web/*": ["./addons/web/static/src/*"], "@owl/*": ["./addons/web/static/lib/owl/*"] } } } -
创建类型化 Owl 组件
/** @odoo-module **/ import { Component, useState } from "@odoo/owl"; interface ProductProps { id: number; name: string; price: number; } export class ProductComponent extends Component<ProductProps> { static template = xml`...`; setup() { this.state = useState({ loading: false }); } }
ES2020+ 模式
使用现代 JavaScript 功能:
// Optional chaining
const userName = this.props.user?.profile?.name;
// Nullish coalescing
const displayName = userName ?? 'Guest User';
// Dynamic imports
const module = await import('./HeavyFeatureComponent');
// Private class fields
class SecureWidget {
#apiKey = null;
#generateKey() {
return crypto.randomUUID();
}
}
测试基础设施
命令:设置测试
触发条件:用户要求“添加测试”“设置 Jest”或“配置 Cypress”
工作流:
-
Jest 配置
// jest.config.js module.exports = { testEnvironment: 'jsdom', moduleNameMapper: { '^@web/(.*)$': '<rootDir>/addons/web/static/src/$1', }, collectCoverageFrom: ['static/src/**/*.{js,jsx}'] }; -
Cypress E2E 测试
// cypress/e2e/ecommerce.cy.js describe('E-commerce Flow', () => { it('should complete purchase', () => { cy.visit('/shop'); cy.get('[data-product-id="1"]').click(); cy.get('#add_to_cart').click(); cy.get('.btn-primary').contains('Checkout').click(); }); }); -
视觉回归测试
// backstop.config.js module.exports = { scenarios: [{ label: 'Homepage', url: 'http://localhost:8069', misMatchThreshold: 0.1 }] };
性能优化
命令:优化 Core Web Vitals
触发条件:用户要求“提升性能”“优化 LCP/FID/CLS”或“加快网站速度”
工作流:
-
优化最大内容绘制(LCP)
// Preload critical resources const link = document.createElement('link'); link.rel = 'preload'; link.as = 'image'; link.href = '/web/image/website/1/logo'; document.head.appendChild(link); -
降低首次输入延迟(FID)
// Break up long tasks function optimizeLongTask(items) { const chunks = []; for (let i = 0; i < items.length; i += 100) { chunks.push(items.slice(i, i + 100)); } chunks.forEach(chunk => { requestIdleCallback(() => processChunk(chunk)); }); } -
尽量减少累积布局偏移(CLS)
/* Reserve space for dynamic content */ .product-image-container { aspect-ratio: 1/1; contain: layout; }
无障碍访问(符合 WCAG)
命令:实现无障碍访问
触发条件:用户要求“添加 ARIA”“提升无障碍访问能力”或“符合 WCAG”
工作流:
-
实现 ARIA
<form role="form" aria-label="Contact Form"> <input type="email" aria-labelledby="email-label" aria-describedby="email-error" aria-required="true" aria-invalid="false"/> <span id="email-error" role="alert" aria-live="polite"></span> </form> -
键盘导航
class KeyboardNavigationManager { handleTab(event) { if (event.shiftKey) { // Backward navigation } else { // Forward navigation } } trapFocus() { // Trap focus within modal } } -
屏幕阅读器支持
class ScreenReaderAnnouncer { announce(message, priority = 'polite') { this.liveRegion.setAttribute('aria-live', priority); this.liveRegion.textContent = message; } }
实时功能
命令:添加 WebSocket 支持
触发条件:用户要求“添加实时功能”“实现 WebSocket”或“实时更新”
工作流:
-
WebSocket 管理器
/** @odoo-module **/ export class WebSocketManager { constructor(url) { this.socket = new WebSocket(url); this.setupEventHandlers(); } setupEventHandlers() { this.socket.onmessage = (event) => { const data = JSON.parse(event.data); this.handleMessage(data); }; } send(data) { if (this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(data)); } } } -
服务器发送事件(SSE)
class SSEManager { constructor(url) { this.eventSource = new EventSource(url); this.eventSource.onmessage = (event) => { this.handleMessage(event.data); }; } }
Web 组件
命令:创建 Web 组件
触发条件:用户要求“创建 Web 组件”或“自定义元素”
工作流:
/** @odoo-module **/
export class OdooSearchInput extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
static get observedAttributes() {
return ['placeholder', 'value'];
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host { display: block; }
input { width: 100%; padding: 0.5rem; }
</style>
<input type="text" placeholder="${this.getAttribute('placeholder')}"/>
`;
}
}
customElements.define('odoo-search-input', OdooSearchInput);
Odoo 高级功能
导览管理器
命令:创建引导式导览
/** @odoo-module **/
import tour from 'web_tour.tour';
tour.register('product_tour', {
url: '/shop',
}, [{
content: 'Click on any product',
trigger: '.oe_product:first',
position: 'bottom',
}]);
自定义字段 Widget
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Component } from "@odoo/owl";
export class ColorPickerField extends Component {
static template = xml`
<input type="color" t-att-value="props.value" t-on-change="onChange"/>
`;
onChange(event) {
this.props.update(event.target.value);
}
}
registry.category("fields").add("color_picker", ColorPickerField);
系统托盘组件
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Component, useState } from "@odoo/owl";
export class NotificationBell extends Component {
static template = xml`...`;
setup() {
this.state = useState({ notifications: [] });
}
}
registry.category("systray").add("NotificationBell", {
Component: NotificationBell,
});
现代构建工具
Vite 配置
命令:设置 Vite 构建
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
base: '/web/static/',
build: {
rollupOptions: {
input: {
main: 'static/src/main.js',
website: 'static/src/website.js'
}
}
},
server: {
proxy: {
'/web': 'http://localhost:8069'
}
}
});
CI/CD 流水线
# .github/workflows/odoo-frontend.yml
name: Odoo Frontend CI/CD
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run tests
run: |
npm run test:unit
npm run test:e2e
- name: Build assets
run: npm run build
资源
- Odoo 文档:https://www.odoo.com/documentation/
- Bootstrap 5 文档:https://getbootstrap.com/docs/5.1/
- Owl 框架:https://github.com/odoo/owl
- TypeScript:https://www.typescriptlang.org/docs/
- Jest:https://jestjs.io/docs/
- Cypress:https://docs.cypress.io/
- Web 组件:https://developer.mozilla.org/en-US/docs/Web/Web_Components
- 社区论坛:https://www.odoo.com/forum/
主题功能激活系统(重要)
概述:theme.utils 模型
创建主题时,必须包含一个实现 theme.utils 模式的 models/theme_xxx.py 文件。这样,主题便可以:
- 启用/禁用页眉模板
- 启用/禁用页脚模板
- 在主题安装时激活可选功能
- 配置主题的默认外观
必需的文件结构
theme_yourtheme/
├── __init__.py # Must import models
├── __manifest__.py
├── models/
│ ├── __init__.py # Must import theme_yourtheme
│ └── theme_yourtheme.py # theme.utils implementation (REQUIRED!)
├── data/
├── views/
└── static/
models/init.py
from . import theme_yourtheme
models/theme_yourtheme.py(必需模式)
from odoo import models
class ThemeYourTheme(models.AbstractModel):
_inherit = 'theme.utils'
def _theme_yourtheme_post_copy(self, mod):
"""
Called automatically when theme is installed on a website.
Use enable_view() and disable_view() to configure templates.
IMPORTANT: Method name MUST be _theme_{technical_name}_post_copy
where technical_name matches folder name (underscores, not hyphens).
"""
# Enable desired header template (mutual exclusivity enforced)
self.enable_view('website.template_header_sales_two')
# Enable desired footer template (mutual exclusivity enforced)
self.enable_view('website.template_footer_contact')
# Enable optional features
self.enable_view('website.option_header_brand_logo')
# Disable unwanted defaults
self.disable_view('website.header_visibility_standard')
self.enable_view('website.header_visibility_fixed')
可用的关键方法
| 方法 | 用途 | 示例 |
|---|---|---|
enable_view(xml_id) | 激活模板 | self.enable_view('website.template_header_hamburger') |
disable_view(xml_id) | 停用模板 | self.disable_view('website.template_header_default') |
enable_asset(xml_id) | 激活资源包 | self.enable_asset('website.theme_custom_assets') |
disable_asset(xml_id) | 停用资源包 | self.disable_asset('website.theme_old_assets') |
互斥规则
页眉:同一时间只能启用一个主页眉模板。启用某个页眉时,enable_view() 会由 Odoo 自动处理其他页眉的停用。
页脚:同一时间只能启用一个主页脚模板。其他页脚也会以同样的方式自动停用。
页眉选项:对齐方式、可见性效果和组件可以与当前启用的页眉组合使用。
完整示例:现代商务主题
from odoo import models
class ThemeModernBusiness(models.AbstractModel):
_inherit = 'theme.utils'
def _theme_modern_business_post_copy(self, mod):
"""Configure Modern Business theme defaults."""
# === HEADER CONFIGURATION ===
# Use hamburger header with centered mobile
self.enable_view('website.template_header_hamburger')
self.enable_view('website.template_header_hamburger_mobile_align_center')
# Fixed header that disappears on scroll
self.disable_view('website.header_visibility_standard')
self.enable_view('website.header_visibility_disappears')
# Show logo, not brand name
self.enable_view('website.option_header_brand_logo')
self.disable_view('website.option_header_brand_name')
# === FOOTER CONFIGURATION ===
# Use contact footer with call-to-action style
self.enable_view('website.template_footer_contact')
# Enable scroll-to-top button
# (This is configured in $o-website-values-palettes, not via views)
# === OPTIONAL FEATURES ===
# Enable search in header
self.enable_view('website.header_search_box')
# Enable social links
self.enable_view('website.header_social_links')
动态页面完整参考
本节介绍 Odoo 中用于动态页面的所有可用模板。可在以下情况下参考:
- 对照 Figma 设计,找到最接近的 Odoo 模板
- 通过
_theme_xxx_post_copy()配置主题默认值 - 了解 Odoo 开箱即用的功能
📋 页眉模板(11 个主模板 + 变体)
所有页眉都继承自 website.layout。每个网站只能启用一个主页眉。
主页眉模板
| XML ID | 名称 | 描述 | 适用场景 |
|---|---|---|---|
website.template_header_default | 默认 | 标准水平导航栏 | 大多数网站 |
website.template_header_hamburger | 汉堡菜单 | 折叠式汉堡菜单 | 极简设计 |
website.template_header_stretch | 延展式 | 全宽延展式导航栏 | 宽屏布局 |
website.template_header_vertical | 垂直式 | 垂直侧边栏导航 | 类似应用的站点 |
website.template_header_search | 搜索式 | 带醒目搜索框的页眉 | 电子商务、目录站点 |
website.template_header_sales_one | 销售 1 | 面向电子商务的页眉 | 在线商店 |
website.template_header_sales_two | 销售 2 | 带分类的电子商务页眉 | 大型商品目录 |
website.template_header_sales_three | 销售 3 | 电子商务替代样式 | 以产品为中心的站点 |
website.template_header_sales_four | 销售 4 | 电子商务变体 | 时尚、零售 |
website.template_header_sidebar | 侧边栏 | 完整的侧边栏布局 | 仪表板站点 |
website.template_header_boxed | 方框式 | 圆角方框容器 | 现代品牌 |
页眉对齐方式变体(适用于每种页眉)
每种页眉都支持对齐方式变体。以默认页眉为例:
website.template_header_default- 左对齐(默认)website.template_header_default_align_center- 居中对齐website.template_header_default_align_right- 右对齐
移动端变体(添加 _mobile_align_center 或 _mobile_align_right 后缀):
website.template_header_hamburger_mobile_align_centerwebsite.template_header_hamburger_mobile_align_right
页眉可见性效果(互斥)
| XML ID | 效果 | 描述 |
|---|---|---|
website.header_visibility_standard | 标准 | 始终可见,随页面滚动 |
website.header_visibility_fixed | 固定 | 滚动时固定在顶部 |
website.header_visibility_disappears | 消失 | 向下滚动时隐藏,向上滚动时显示 |
website.header_visibility_fade_out | 淡出 | 滚动时淡出 |
页眉组件(可组合使用)
| XML ID | 组件 | 默认状态 |
|---|---|---|
website.option_header_brand_logo | 显示徽标图片 | 启用 |
website.option_header_brand_name | 显示文字“My Website” | 未启用 |
website.header_call_to_action | CTA 按钮(联系我们) | 启用 |
website.header_search_box | 搜索栏 | 启用 |
website.header_social_links | 社交媒体图标 | 未启用 |
website.header_text_element | 自定义文本元素 | 启用 |
website.header_language_selector | 语言下拉菜单 | 启用 |
页眉视觉参考
┌─────────────────────────────────────────────────────────────────┐
│ DEFAULT HEADER │
│ ┌────────┬────────────────────────────────┬───────────────────┐ │
│ │ LOGO │ Home About Services Blog │ Search CTA BTN │ │
│ └────────┴────────────────────────────────┴───────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ HAMBURGER HEADER │
│ ┌────────┬───────────────────────────────┬────────────────────┐ │
│ │ ☰ MENU │ LOGO │ Search CTA │ │
│ └────────┴───────────────────────────────┴────────────────────┘ │
│ ↓ (expanded menu) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Home │ About │ Services │ Blog │ Contact │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ SALES TWO HEADER (E-commerce) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Top bar: Phone | Email | Currency | Language │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ LOGO [ Search Bar ] Account Cart 🛒 │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ Category 1 │ Category 2 │ Category 3 │ All Products │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ VERTICAL/SIDEBAR HEADER │
│ ┌──────────────┬──────────────────────────────────────────────┐│
│ │ │ ││
│ │ LOGO │ ││
│ │ │ ││
│ │ ─────────── │ PAGE CONTENT ││
│ │ │ ││
│ │ Home │ ││
│ │ About │ ││
│ │ Services │ ││
│ │ Blog │ ││
│ │ Contact │ ││
│ │ │ ││
│ │ ─────────── │ ││
│ │ [Social] │ ││
│ └──────────────┴──────────────────────────────────────────────┘│
└────────────────────────────────────────────────────────────────┘
📋 页脚模板(9 个选项)
所有页脚均继承自 website.layout。每个网站只能启用一个主页脚。
| XML ID | 名称 | 描述 | 适用场景 |
|---|---|---|---|
website.footer_custom | 默认 | 可自定义的默认页脚 | 通用 |
website.template_footer_descriptive | 详细介绍 | 详细的公司介绍 | 企业网站 |
website.template_footer_centered | 居中 | 居中对齐的简约设计 | 着陆页 |
website.template_footer_links | 链接 | 多列链接 | 大型网站 |
website.template_footer_minimalist | 极简 | 极简页脚 | 简洁设计 |
website.template_footer_contact | 联系方式 | 侧重联系信息 | 服务型企业 |
website.template_footer_call_to_action | 行动号召 | 侧重新闻简报和行动号召 | 营销网站 |
website.template_footer_headline | 标题 | 大号标题文本 | 品牌宣言 |
website.template_footer_slideout | 滑出式 | 滚动时滑出 | 现代/潮流风格 |
页脚选项
| XML ID | 选项 | 描述 |
|---|---|---|
website.footer_no_copyright | 移除版权信息 | 隐藏版权行 |
website.footer_language_selector_inline | 内联语言选择器 | 语言选择器样式 |
页脚视觉参考
┌─────────────────────────────────────────────────────────────────┐
│ DEFAULT FOOTER │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ LOGO │ │
│ │ Company description text here... │ │
│ │ │ │
│ │ Links Services Contact │ │
│ │ · About · Web Dev 123 Street │ │
│ │ · Blog · Design City, Country │ │
│ │ · Careers · Support +1 234 567 890 │ │
│ │ │ │
│ │ [Social Icons] │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ © 2024 Company Name. All rights reserved. │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ MINIMALIST FOOTER │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ © 2024 Company · Privacy · Terms │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CONTACT FOOTER │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ┌──────────────┬───────────────┬────────────────────────┐ │ │
│ │ │ 📍 Address │ 📞 Phone │ ✉️ Email │ │ │
│ │ │ 123 Street │ +1 234 567 │ [email protected] │ │ │
│ │ │ City │ │ │ │ │
│ │ └──────────────┴───────────────┴────────────────────────┘ │ │
│ │ [Social Icons] │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CALL-TO-ACTION FOOTER │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Subscribe to our newsletter │ │
│ │ ┌─────────────────────────────────┬─────────────────────┐ │ │
│ │ │ Enter your email... │ [Subscribe Now] │ │ │
│ │ └─────────────────────────────────┴─────────────────────┘ │ │
│ │ │ │
│ │ © 2024 Company · [Social Icons] │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
📋 商店页面模板(website_sale 模块)
商店布局选项
| XML ID | 功能 | 默认值 | 描述 |
|---|---|---|---|
website_sale.products_design_card | 卡片设计 | 未启用 | 卡片式商品项 |
website_sale.products_design_thumbs | 缩略图 | 未启用 | 缩略图布局 |
website_sale.products_design_grid | 网格设计 | 未启用 | 网格布局 |
website_sale.products_thumb_4_3 | 4:3 比例 | 未启用 | 4:3 图片宽高比 |
website_sale.products_thumb_4_5 | 4:5 比例 | 未启用 | 4:5 图片宽高比 |
website_sale.products_thumb_2_3 | 2:3 比例 | 未启用 | 2:3 图片宽高比 |
website_sale.products_thumb_cover | 封面填充 | 已启用 | 图片填满容器 |
商店分类与筛选器
| XML ID | 功能 | 默认值 | 描述 |
|---|---|---|---|
website_sale.products_categories | 左侧边栏分类 | 未启用 | 在左侧边栏显示分类 |
website_sale.products_categories_top | 顶部分类 | 已启用 | 在顶部导航中显示分类 |
website_sale.products_attributes | 属性筛选器 | 已启用 | 商品属性筛选器 |
website_sale.filter_products_price | 价格筛选器 | 未启用 | 价格范围滑块 |
website_sale.filter_products_tags | 标签筛选器 | 已启用 | 按商品标签筛选 |
website_sale.search | 搜索框 | 已启用 | 搜索商品 |
website_sale.sort | 排序下拉菜单 | 已启用 | 对商品排序 |
website_sale.add_grid_or_list_option | 网格/列表切换 | 已启用 | 视图切换按钮 |
商品项选项
| XML ID | 功能 | 默认值 | 描述 |
|---|---|---|---|
website_sale.products_description | 描述 | 未启用 | 在商品列表中显示描述 |
website_sale.products_add_to_cart | 加入购物车 | 未启用 | 在商品列表中显示加入购物车按钮 |
商店视觉参考
┌─────────────────────────────────────────────────────────────────┐
│ SHOP PAGE (Categories Top - Default) │
├─────────────────────────────────────────────────────────────────┤
│ All │ Category 1 │ Category 2 │ Category 3 │
├─────────────────────────────────────────────────────────────────┤
│ [Search...] [Sort: Featured ▼] [☷ Grid] [☰ List] │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ [IMG] │ │ [IMG] │ │ [IMG] │ │ [IMG] │ │
│ │ │ │ │ │ │ │ │ │
│ │ Product 1│ │ Product 2│ │ Product 3│ │ Product 4│ │
│ │ $99.00 │ │ $149.00 │ │ $79.00 │ │ $199.00 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ [IMG] │ │ [IMG] │ │ [IMG] │ │ [IMG] │ │
│ │ ... │ │ ... │ │ ... │ │ ... │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ [1] [2] [3] ... [Next →] │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ SHOP PAGE (Categories Left Sidebar) │
├────────────────┬────────────────────────────────────────────────┤
│ CATEGORIES │ [Search...] [Sort ▼] [☷] [☰] │
│ ───────────── ├────────────────────────────────────────────────┤
│ □ All │ ┌────────┐ ┌────────┐ ┌────────┐ │
│ ▸ Category 1 │ │ [IMG] │ │ [IMG] │ │ [IMG] │ │
│ · Sub 1.1 │ │ Prod 1 │ │ Prod 2 │ │ Prod 3 │ │
│ · Sub 1.2 │ │ $99 │ │ $149 │ │ $79 │ │
│ ▸ Category 2 │ └────────┘ └────────┘ └────────┘ │
│ ▸ Category 3 │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │
│ PRICE │ │ ... │ │ ... │ │ ... │ │
│ ───────────── │ └────────┘ └────────┘ └────────┘ │
│ $0 ─●────── $500│ │
│ │ │
│ TAGS │ │
│ ───────────── │ │
│ [New] [Sale] │ │
└────────────────┴────────────────────────────────────────────────┘
📋 产品详情页模板
| XML ID | 功能 | 默认状态 | 描述 |
|---|---|---|---|
website_sale.product_tags | 产品标签 | 启用 | 显示产品标签 |
website_sale.product_comment | 评价 | 未启用 | 讨论/评分版块 |
website_sale.product_custom_text | 条款区块 | 启用 | 条款与条件 |
website_sale.product_share_buttons | 分享按钮 | 启用 | 社交媒体分享按钮 |
website_sale.product_quantity | 数量选择器 | 启用 | 数量输入框 |
website_sale.product_buy_now | 立即购买 | 未启用 | 快速购买按钮 |
website_sale.product_variants | 变体列表 | 未启用 | 变体列表视图 |
website_sale.alternative_products | 替代产品 | 启用 | 替代产品轮播图 |
website_sale.carousel_product_indicators_bottom | 轮播图底部指示器 | 启用 | 底部的图片指示器 |
website_sale.carousel_product_indicators_left | 轮播图左侧指示器 | 未启用 | 左侧的图片指示器 |
产品页面视觉参考
┌─────────────────────────────────────────────────────────────────┐
│ PRODUCT DETAIL PAGE │
├─────────────────────────────────────────────────────────────────┤
│ Home > Category > Product Name │
├───────────────────────────────┬─────────────────────────────────┤
│ │ │
│ ┌─────────────────────────┐ │ Product Name │
│ │ │ │ ★★★★☆ (24 reviews) │
│ │ [MAIN IMAGE] │ │ │
│ │ │ │ $199.00 │
│ │ │ │ ̶$̶2̶4̶9̶.̶0̶0̶ -20% │
│ └─────────────────────────┘ │ │
│ [○] [○] [●] [○] │ Color: [Blue ▼] │
│ │ Size: [M ▼] │
│ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │thumb1│ │thumb2│ │thumb3│ │ Qty: [- 1 +] │
│ └──────┘ └──────┘ └──────┘ │ │
│ │ [Add to Cart] [Buy Now] │
│ │ │
│ │ [♡ Wishlist] [🔗 Share] │
│ │ │
│ │ Tags: [New] [Bestseller] │
├───────────────────────────────┴─────────────────────────────────┤
│ DESCRIPTION │ SPECIFICATIONS │ REVIEWS (24) │
├─────────────────────────────────────────────────────────────────┤
│ Full product description text goes here... │
│ │
├─────────────────────────────────────────────────────────────────┤
│ ALTERNATIVE PRODUCTS │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Alt 1 │ │ Alt 2 │ │ Alt 3 │ │ Alt 4 │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
└─────────────────────────────────────────────────────────────────┘
📋 博客页面模板(website_blog 模块)
博客列表选项
| XML ID | 功能 | 默认状态 | 描述 |
|---|---|---|---|
website_blog.opt_blog_cover_post | 最新文章横幅 | 启用 | 将最新文章显示为横幅 |
website_blog.opt_blog_cover_post_fullwidth_design | 全宽横幅 | 启用 | 横幅横跨整个宽度 |
website_blog.opt_blog_list_view | 列表视图 | 未启用 | 以列表而非网格显示文章 |
website_blog.opt_blog_cards_design | 卡片设计 | 未启用 | Bootstrap 卡片组件 |
website_blog.opt_blog_readable | 易读模式 | 启用 | 使用更大、更易读的文本 |
website_blog.opt_blog_sidebar_show | 侧边栏 | 未启用 | 显示博客侧边栏 |
博客侧边栏组件(启用时)
| XML ID | 功能 | 默认状态 | 描述 |
|---|---|---|---|
website_blog.opt_sidebar_blog_index_archives | 归档 | 启用 | 按日期归档 |
website_blog.opt_sidebar_blog_index_follow_us | 关注我们 | 启用 | 社交媒体链接 |
website_blog.opt_sidebar_blog_index_tags | 标签 | 启用 | 标签云 |
博客文章循环显示
| XML ID | 功能 | 默认状态 | 描述 |
|---|---|---|---|
website_blog.opt_posts_loop_show_cover | 封面图片 | 启用 | 特色图片 |
website_blog.opt_posts_loop_show_author | 作者 | 启用 | 作者姓名/头像 |
website_blog.opt_posts_loop_show_stats | 统计信息 | 未启用 | 评论数/浏览量 |
website_blog.opt_posts_loop_show_teaser | 摘要 | 启用 | 预览文本和标签 |
博客文章详情选项
| XML ID | 功能 | 默认状态 | 描述 |
|---|---|---|---|
website_blog.opt_blog_post_readable | 易读模式 | 启用 | 使用更大、更易读的文本 |
website_blog.opt_blog_post_sidebar | 侧边栏 | 未启用 | 显示文章侧边栏 |
website_blog.opt_blog_post_regular_cover | 常规封面 | 未启用 | 标题显示在封面上方 |
website_blog.opt_blog_post_breadcrumb | 面包屑导航 | 启用 | 导航面包屑 |
website_blog.opt_blog_post_read_next | 阅读下一篇 | 启用 | 下一篇文章横幅 |
website_blog.opt_blog_post_comment | 评论 | 未启用 | 启用评论 |
website_blog.opt_blog_post_select_to_tweet | 选中文字发推 | 未启用 | 高亮文字后发推 |
博客视觉参考
┌─────────────────────────────────────────────────────────────────┐
│ BLOG LISTING (Grid View - Default) │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ [FEATURED POST BANNER IMAGE] ││
│ │ ││
│ │ Latest Article Title ││
│ │ Short teaser text for the featured post... ││
│ │ [Read More →] ││
│ └─────────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────────┤
│ All Posts │ Category 1 │ Category 2 │ [Search...] │
├─────────────────────────────────────────────────────────────────┤
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ [IMAGE] │ │ [IMAGE] │ │ [IMAGE] │ │
│ │ │ │ │ │ │ │
│ │ Post Title 1 │ │ Post Title 2 │ │ Post Title 3 │ │
│ │ By Author │ │ By Author │ │ By Author │ │
│ │ Jan 15, 2024 │ │ Jan 10, 2024 │ │ Jan 5, 2024 │ │
│ │ │ │ │ │ │ │
│ │ Teaser text... │ │ Teaser text... │ │ Teaser text... │ │
│ │ [Tag1] [Tag2] │ │ [Tag1] │ │ [Tag2] [Tag3] │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ BLOG POST DETAIL (Fullwidth Cover - Default) │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ [FULL WIDTH COVER IMAGE] ││
│ │ ││
│ │ Article Title Goes Here ││
│ │ By Author Name · January 15, 2024 ││
│ └─────────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────────┤
│ │
│ Full article content with readable typography. │
│ │
│ Multiple paragraphs of content... │
│ │
│ [Share: Facebook | Twitter | LinkedIn] │
│ │
│ Tags: [Technology] [Tutorial] [Odoo] │
│ │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ READ NEXT: Next Article Title ││
│ │ [Banner Image] ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
📋 购物车与结账模板
| XML ID | 功能 | 默认状态 | 描述 |
|---|---|---|---|
website_sale.suggested_products_list | 推荐产品 | 启用 | 购物车中的配件产品 |
website_sale.reduction_code | 促销码 | 启用 | 优惠券代码输入框 |
website_sale.address_b2b | B2B 字段 | 未启用 | 商业地址字段 |
website_sale.accept_terms_and_conditions | 条款与条件复选框 | 未启用 | 要求接受条款与条件 |
设计工作流:从 Figma 到 Odoo
方法
将 Figma 设计转换为 Odoo 主题时,请遵循以下工作流:
┌─────────────────────────────────────────────────────────────────┐
│ DESIGN WORKFLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. ANALYZE DESIGN │
│ ↓ │
│ Extract: Colors, Fonts, Layout, Components │
│ │
│ 2. COMPARE TO ODOO TEMPLATES │
│ ↓ │
│ Match: Header style, Footer style, Page layouts │
│ │
│ 3. CHOOSE CLOSEST TEMPLATE │
│ ↓ │
│ Select: Best matching header + footer + features │
│ │
│ 4. CONFIGURE VIA VARIABLES │
│ ↓ │
│ Set: $o-website-values-palettes configuration │
│ │
│ 5. ENHANCE WITH CUSTOM CSS │
│ ↓ │
│ Add: Only what templates can't provide │
│ │
│ 6. CREATE CUSTOM SNIPPETS (if needed) │
│ ↓ │
│ Build: Components not available in Odoo │
│ │
└─────────────────────────────────────────────────────────────────┘
第 1 步:分析 Figma 设计
提取以下关键元素:
| 元素 | 查找内容 | 对应项 |
|---|---|---|
| 主色 | 品牌主色(按钮、链接、CTA) | o-color-1 |
| 次要颜色 | 点缀色、次要按钮 | o-color-2 |
| 浅色背景 | 区块背景、卡片 | o-color-3 |
| 白色/基础色 | 主要内容背景 | o-color-4 |
| 深色/文本 | 标题、页脚、深色区块 | o-color-5 |
| 字体系列 | 正文、标题 | $o-theme-font-configs |
| 页眉样式 | 导航布局 | 'header-template' |
| 页脚样式 | 页脚布局 | 'footer-template' |
第 2 步:匹配页眉样式
| 如果设计包含…… | 使用模板 |
|---|---|
| 标准水平导航 | template_header_default |
| 隐藏菜单(汉堡图标) | template_header_hamburger |
| 全宽展开式导航 | template_header_stretch |
| 垂直侧边栏导航 | template_header_vertical |
| 突出的搜索栏 | template_header_search |
| 带有分类的电商布局 | template_header_sales_two |
| 顶栏 + 主导航 | template_header_sales_one |
| 带内容的侧边栏 | template_header_sidebar |
| 圆角/带边框容器 | template_header_boxed |
第 3 步:匹配页脚样式
| 如果设计包含…… | 使用模板 |
|---|---|
| 带 Logo 的多栏布局 | footer_custom(默认) |
| 详细的公司信息 | template_footer_descriptive |
| 居中简约布局 | template_footer_centered |
| 仅包含多列链接 | template_footer_links |
| 极简版权信息 | template_footer_minimalist |
| 突出联系信息 | template_footer_contact |
| 突出新闻简报/CTA | template_footer_call_to_action |
| 大号标题文字 | template_footer_headline |
| 现代滑出效果 | template_footer_slideout |
第 4 步:生成主题配置
根据分析结果创建配置:
// primary_variables.scss - Generated from Figma Analysis
$o-theme-font-configs: (
'Poppins': (
'family': ('Poppins', sans-serif),
'url': 'Poppins:300,400,500,600,700',
),
);
$o-website-values-palettes: (
(
'color-palettes-name': 'default-1',
// Typography (from Figma)
'font': 'Poppins',
'headings-font': 'Poppins',
// Header (matched to closest template)
'header-template': 'hamburger', // Figma shows hamburger menu
'header-links-style': 'pills', // Rounded pill-style links
'logo-height': 48px,
// Footer (matched to closest template)
'footer-template': 'contact', // Figma shows contact-focused footer
'footer-scrolltop': true,
// Buttons (from Figma measurements)
'btn-padding-y': 0.75rem,
'btn-padding-x': 1.5rem,
'btn-border-radius': 8px,
// Layout
'link-underline': 'never',
)
);
第 5 步:创建 theme.utils 实现
# models/theme_yourtheme.py
from odoo import models
class ThemeYourTheme(models.AbstractModel):
_inherit = 'theme.utils'
def _theme_yourtheme_post_copy(self, mod):
# Enable matched header template
self.enable_view('website.template_header_hamburger')
self.enable_view('website.template_header_hamburger_mobile_align_center')
# Enable matched footer template
self.enable_view('website.template_footer_contact')
# Enable desired header components
self.enable_view('website.option_header_brand_logo')
self.enable_view('website.header_search_box')
# Configure visibility effect
self.disable_view('website.header_visibility_standard')
self.enable_view('website.header_visibility_fixed')
决策流程图:选择页眉
START: Analyze header design
│
├─ Is navigation hidden by default?
│ │
│ ├─ YES → template_header_hamburger
│ │
│ └─ NO → Is there a vertical sidebar?
│ │
│ ├─ YES → Is it full-page sidebar?
│ │ │
│ │ ├─ YES → template_header_sidebar
│ │ └─ NO → template_header_vertical
│ │
│ └─ NO → Is it e-commerce focused?
│ │
│ ├─ YES → Does it have category mega-menu?
│ │ │
│ │ ├─ YES → template_header_sales_two
│ │ └─ NO → template_header_sales_one
│ │
│ └─ NO → Is search prominent?
│ │
│ ├─ YES → template_header_search
│ │
│ └─ NO → Is it boxed/contained?
│ │
│ ├─ YES → template_header_boxed
│ │
│ └─ NO → Is it full-width?
│ │
│ ├─ YES → template_header_stretch
│ └─ NO → template_header_default
决策流程图:选择页脚
START: Analyze footer design
│
├─ Is it minimal (just copyright)?
│ │
│ ├─ YES → template_footer_minimalist
│ │
│ └─ NO → Is it center-aligned?
│ │
│ ├─ YES → template_footer_centered
│ │
│ └─ NO → Is there a newsletter/CTA?
│ │
│ ├─ YES → template_footer_call_to_action
│ │
│ └─ NO → Is it contact info focused?
│ │
│ ├─ YES → template_footer_contact
│ │
│ └─ NO → Is it primarily links?
│ │
│ ├─ YES → template_footer_links
│ │
│ └─ NO → Has detailed description?
│ │
│ ├─ YES → template_footer_descriptive
│ │
│ └─ NO → footer_custom (default)
网站片段完整参考
本节介绍 Odoo 中所有可用的片段、它们的工作方式以及如何创建自定义片段。构建网站页面、创建自定义片段或了解网站构建器时,请参考本节。
📚 片段架构概述
片段是 Odoo 网站页面的可复用构建模块,可用于拖放式页面构建。
┌─────────────────────────────────────────────────────────────────┐
│ ODOO SNIPPET SYSTEM │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ STATIC SNIPPETS │ │ DYNAMIC SNIPPETS │ │
│ ├──────────────────┤ ├──────────────────┤ │
│ │ • Pre-built HTML │ │ • Data-driven │ │
│ │ • 81+ templates │ │ • ir.filters │ │
│ │ • Drag & drop │ │ • Display temps │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │
│ ┌───────────────────────────┴────────────────────────────────┐ │
│ │ SNIPPET OPTIONS SYSTEM │ │
│ ├────────────────────────────────────────────────────────────┤ │
│ │ we-select | we-button | we-colorpicker | we-input | etc. │ │
│ │ data-selector | data-select-class | data-js | data-css │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
📋 静态片段清单(81+ 个模板)
Odoo 提供 81+ 个静态片段模板,分为 6 个主要类别:
| 类别 | 数量 | 用途 |
|---|---|---|
| 结构 | 14 | 页面区块、布局 |
| 图库/媒体 | 3 | 图片库、轮播图 |
| 功能 | 11 | 功能展示 |
| 动态内容 | 15 | 地图、表单、嵌入内容 |
| 内部内容 | 16 | 卡片、引用、文本块 |
| 超级菜单 | 9 | 导航菜单 |
结构片段(14)
| 片段 ID | 名称 | 描述 | 最适用于 |
|---|---|---|---|
s_banner | 横幅 | 带背景的主视觉区域 | 落地页 |
s_cover | 封面 | 全宽封面图片 | 页眉 |
s_text_image | 文字 - 图片 | 两栏布局:左侧文字,右侧图片 | 功能介绍 |
s_image_text | 图片 - 文字 | 两栏布局:左侧图片,右侧文字 | 功能介绍 |
s_title | 标题 | 带副标题的章节标题 | 章节标题 |
s_text_block | 文字块 | 多栏文字内容 | 内容 |
s_numbers | 数字 | 统计数据/计数器展示 | 成就 |
s_picture | 图片 | 单张大图 | 视觉焦点 |
s_three_columns | 栏 | 三栏布局 | 内容网格 |
s_big_boxes | 大型方框 | 大型功能方框 | 服务 |
s_features | 功能 | 带图标的功能网格 | 优势 |
s_masonry_block | 瀑布流区块 | 瀑布流样式网格 | 图库 |
s_image_gallery | 图片库 | 多图图库 | 作品集 |
s_images_wall | 图片墙 | 墙面样式展示 | 媒体 |
功能片段(11)
| 片段 ID | 名称 | 描述 | 最适用于 |
|---|---|---|---|
s_comparisons | 对比 | 并排对比 | 产品对比 |
s_company_team | 公司团队 | 团队成员卡片 | 关于页面 |
s_call_to_action | 行动号召 | 带按钮的 CTA 区块 | 转化 |
s_references | 客户案例 | 客户徽标/参考资料 | 社会认同 |
s_accordion | 折叠面板 | 可折叠内容 | 常见问题 |
s_features_grid | 功能网格 | 功能图标网格 | 优势 |
s_table_of_content | 目录 | 滚动定位导航 | 长页面 |
s_pricelist | 价格表 | 定价表 | 产品 |
s_product_list | 产品列表 | 产品展示 | 电子商务 |
s_faq_collapse | 常见问题 | 常见问题解答 | 支持 |
s_tabs | 标签页 | 标签式内容 | 内容组织 |
动态内容片段(15)
| 片段 ID | 名称 | 描述 | 最适用于 |
|---|---|---|---|
s_google_map | Google 地图 | 嵌入 Google 地图 | 联系页面 |
s_map | 地图 | Leaflet/OpenStreetMap | 地点 |
s_embed_code | 嵌入代码 | 自定义 HTML/嵌入内容 | 第三方内容 |
s_website_form | 网站表单 | 联系表单/自定义表单 | 潜在客户收集 |
s_searchbar | 搜索栏 | 搜索功能 | 导航 |
s_social_media | 社交媒体 | 社交链接 | 互动 |
s_share | 分享 | 分享按钮 | 内容分享 |
s_dynamic_snippet | 动态片段 | 基础动态内容 | 数据展示 |
s_dynamic_snippet_carousel | 动态轮播 | 动态轮播 | 特色内容 |
s_chart | 图表 | 数据可视化 | 报告 |
s_countdown | 倒计时 | 计时器/倒计时 | 活动 |
s_popup | 弹出窗口 | 模态弹窗 | 促销 |
s_newsletter_block | 新闻简报 | 邮件订阅 | 营销 |
s_newsletter_popup | 新闻简报弹窗 | 弹窗订阅 | 潜在客户收集 |
s_newsletter_subscribe_form | 新闻简报表单 | 行内订阅 | 页脚 |
内部内容片段(16)
| 片段 ID | 名称 | 描述 | 最适用于 |
|---|---|---|---|
s_hr | 水平线 | 分隔线 | 分隔章节 |
s_alert | 警示框 | 警示/通知框 | 通知 |
s_card | 卡片 | 内容卡片 | 项目 |
s_three_cards | 三卡片 | 三卡片布局 | 服务 |
s_four_cards | 四卡片 | 四卡片布局 | 功能 |
s_timeline | 时间线 | 时间线展示 | 历史 |
s_process_steps | 流程步骤 | 分步指南 | 教程 |
s_quotes_carousel | 引言轮播 | 用户评价轮播 | 社会认同 |
s_quotes_grid | 引言网格 | 用户评价网格 | 评论 |
s_rating | 评分 | 星级评分展示 | 评论 |
s_progress_bar | 进度条 | 进度指示器 | 统计数据 |
s_blockquote | 引用块 | 带样式的引言 | 强调 |
s_badge | 徽章 | 徽章/标签 | 标签 |
s_button | 按钮 | 带样式的按钮 | 行动号召 |
s_separator | 分隔符 | 章节分隔符 | 视觉分隔 |
s_text_highlight | 文本高亮 | 高亮文本框 | 强调 |
超级菜单片段(9)
| 片段 ID | 名称 | 描述 |
|---|---|---|
s_mega_menu_multi_menus | 多菜单 | 多个菜单栏 |
s_mega_menu_menu_image_menu | 图片菜单 | 带图片的三个区域 |
s_mega_menu_little_icons | 小图标 | 基于图标的菜单 |
s_mega_menu_images_subtitles | 图片与副标题 | 带副标题的图片 |
s_mega_menu_cards | 卡片 | 卡片式菜单 |
s_mega_menu_big_icons | 大图标 | 大图标菜单 |
s_mega_menu_thumbnails | 缩略图 | 缩略图库菜单 |
s_mega_menu_odoo_menu | Odoo 菜单 | Odoo 风格菜单 |
s_mega_menu_no_extra_info | 无额外信息 | 简洁菜单 |
📋 动态片段系统
动态片段从数据库获取数据,并使用模板进行渲染。
架构
┌─────────────────────────────────────────────────────────────────┐
│ DYNAMIC SNIPPET FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ DATA SOURCE │ ← ir.filters / ir.actions.server │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ DYNAMIC FILTER │ ← website.snippet.filter model │
│ │ (Data Fetcher) │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ DISPLAY TEMPLATE │ ← dynamic_filter_template_* │
│ │ (Renderer) │ │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
商品动态片段(website_sale)
主片段:s_dynamic_snippet_products
展示模板(15 个变体):
| 模板键 | 名称 | 样式 |
|---|---|---|
dynamic_filter_template_product_product_add_to_cart | 加入购物车 | 带购买按钮 |
dynamic_filter_template_product_product_banner | 横幅 | 大型横幅格式 |
dynamic_filter_template_product_product_borderless_1/2/3 | 无边框 | 简洁无边框 |
dynamic_filter_template_product_product_card_style | 卡片样式 | 基于卡片的样式 |
dynamic_filter_template_product_product_centered | 居中 | 居中对齐 |
dynamic_filter_template_product_product_horizontal_card | 横向卡片 | 横向布局 |
dynamic_filter_template_product_product_mini | 迷你 | 紧凑展示 |
dynamic_filter_template_product_product_minimalist_1/2/3/4 | 极简 | 极简设计 |
dynamic_filter_template_product_product_picture | 图片 | 以图片为主 |
动态筛选器(6 个数据源):
| 筛选器 ID | 名称 | 逻辑 |
|---|---|---|
dynamic_filter_newest_products | 最新商品 | 按 create_date 降序排序 |
dynamic_filter_recently_sold_products | 最近售出的商品 | 最近有销量的商品 |
dynamic_filter_recently_viewed_products | 最近浏览的商品 | 用户的浏览历史 |
dynamic_filter_accessories | 配件 | 相关配件 |
dynamic_filter_products_sold_with | 搭配购买 | 经常一起购买的商品 |
dynamic_filter_alternative_products | 替代商品 | 类似商品 |
博客动态片段(website_blog)
主片段:s_blog_posts
展示模板(4 种布局):
| 模板键 | 名称 |
|---|---|
dynamic_filter_template_blog_post_big_picture | 大图 |
dynamic_filter_template_blog_post_card | 卡片 |
dynamic_filter_template_blog_post_horizontal | 横向 |
dynamic_filter_template_blog_post_vertical | 纵向 |
筛选器:dynamic_filter_latest_posts、dynamic_filter_most_read_posts
Event 动态片段(website_event)
主要片段:s_events
显示模板:dynamic_filter_template_event_event_card、dynamic_filter_template_event_event_list
筛选器:dynamic_filter_upcoming_events
📋 片段选项系统
选项系统允许用户在网站编辑器中自定义片段。
We-* 元素参考
| 元素 | 用途 | 关键属性 |
|---|---|---|
we-select | 下拉选择 | data-name、data-dependencies |
we-button | 切换按钮 | data-select-class、data-toggle-class |
we-colorpicker | 颜色选择器 | data-css-property、data-color-prefix |
we-input | 文本/数字输入 | data-attribute-name、data-unit |
we-range | 滑块 | data-min、data-max、data-step |
we-checkbox | 复选框切换 | data-select-class |
we-button-group | 按钮组 | 用于对齐等 |
we-row | 选项行 | 对相关输入进行分组 |
we-select-pager | 分页选择 | 用于大型选项集 |
数据属性参考
| 属性 | 用途 | 示例 |
|---|---|---|
data-selector | 目标 CSS 选择器 | data-selector=".s_banner" |
data-select-class | 切换 CSS 类 | data-select-class="shadow-lg" |
data-select-style | 启用样式编辑 | data-select-style="true" |
data-css-property | 要修改的 CSS 属性 | data-css-property="padding" |
data-js | JavaScript 处理程序类 | data-js="BackgroundImage" |
data-dependencies | 根据其他选项显示 | data-dependencies="image_opt" |
data-exclude | 对特定元素隐藏 | data-exclude=".no-shadow" |
data-no-preview | 禁用实时预览 | data-no-preview="true" |
data-attribute-name | 要设置的 HTML 属性 | data-attribute-name="data-speed" |
内置 JavaScript 处理程序
| 处理程序 | 用途 |
|---|---|
BackgroundImage | 背景图片管理 |
BackgroundPosition | 图片定位 |
BackgroundToggler | 切换背景类型 |
ColoredLevelBackground | 调色板背景 |
BackgroundShape | 装饰形状 |
ImageTools | 图片编辑 |
ReplaceMedia | 替换媒体 |
FontawesomeTools | 图标管理 |
Carousel | 轮播控件 |
DynamicSnippet | 动态内容选项 |
完整选项模板示例
<template id="s_my_snippet_options" inherit_id="website.snippet_options">
<xpath expr="." position="inside">
<div data-js="MySnippetHandler"
data-selector=".s_my_snippet"
data-drop-in=".oe_structure"
data-drop-near="section">
<we-select string="Layout" data-name="layout_opt">
<we-button data-select-class="layout-grid">Grid</we-button>
<we-button data-select-class="layout-list">List</we-button>
</we-select>
<we-colorpicker string="Background"
data-name="bg_color_opt"
data-select-style="true"
data-css-property="background-color"/>
<we-range string="Spacing"
data-name="spacing_opt"
data-select-style="true"
data-css-property="gap"
data-min="0"
data-max="100"
data-step="5"
data-unit="px"/>
<we-select string="Shadow Size"
data-name="shadow_size_opt"
data-dependencies="shadow_opt">
<we-button data-select-class="shadow-sm">Small</we-button>
<we-button data-select-class="shadow-lg">Large</we-button>
</we-select>
</div>
</xpath>
</template>
📋 创建自定义片段(版本适配)
Odoo 14-17:简单注册
<template id="s_my_snippet" name="My Snippet">
<section class="s_my_snippet pt48 pb48 o_cc o_cc1">
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2>My Custom Snippet</h2>
<p>Content goes here...</p>
</div>
</div>
</div>
</section>
</template>
<template id="s_my_snippet_insert" inherit_id="website.snippets">
<xpath expr="//div[@id='snippet_structure']//t[@t-snippet][last()]" position="after">
<t t-snippet="my_module.s_my_snippet"
t-thumbnail="/my_module/static/src/img/snippets/s_my_snippet.svg">
<keywords>my, custom, snippet</keywords>
</t>
</xpath>
</template>
Odoo 18-19:使用片段组
<template id="snippet_group_custom" inherit_id="website.snippets">
<xpath expr="//div[@id='snippet_groups']" position="inside">
<t snippet-group="custom"
t-snippet="website.s_snippet_group"
string="Custom Snippets"/>
</xpath>
</template>
<template id="s_my_snippet_insert" inherit_id="website.snippets">
<xpath expr="//div[@id='snippet_structure']/*[1]" position="before">
<t t-snippet="my_module.s_my_snippet"
string="My Snippet"
group="custom"
t-thumbnail="/my_module/static/src/img/snippets/s_my_snippet.svg"/>
</xpath>
</template>
片段面板类别
| 面板 ID | 类别 | XPath 目标 |
|---|---|---|
snippet_mega_menu | 超级菜单 | //div[@id='snippet_mega_menu'] |
snippet_structure | 结构 | //div[@id='snippet_structure'] |
snippet_media | 图库 | //div[@id='snippet_media'] |
snippet_feature | 功能 | //div[@id='snippet_feature'] |
snippet_dynamic | 动态内容 | //div[@id='snippet_dynamic'] |
snippet_inner_content | 内部内容 | //div[@id='snippet_inner_content'] |
📋 片段 JavaScript 集成
片段的 publicWidget 模式
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
publicWidget.registry.MySnippet = publicWidget.Widget.extend({
selector: '.s_my_snippet',
disabledInEditableMode: false, // Allow in website builder
events: {
'click .load-more': '_onLoadMore',
},
start: function () {
// CRITICAL: Check editableMode for builder compatibility
if (!this.editableMode) {
this._initializeAnimations();
}
return this._super(...arguments);
},
_initializeAnimations: function () {
// Animation code that should NOT run in edit mode
this.$el.addClass('animated');
},
_onLoadMore: function (ev) {
ev.preventDefault();
if (this.editableMode) return; // Don't run in edit mode
// Handler logic
},
destroy: function () {
// CRITICAL: Clean up event listeners
$(window).off('.mySnippet');
this._super(...arguments);
},
});
export default publicWidget.registry.MySnippet;
片段选项 JavaScript 处理程序
/** @odoo-module **/
import options from "@web_editor/js/editor/snippets.options";
options.registry.MySnippetOption = options.Class.extend({
/**
* Handle data attribute changes
*/
selectDataAttribute: function(previewMode, widgetValue, params) {
this._super(...arguments);
if (params.attributeName === 'layout') {
this._applyLayout(widgetValue);
}
},
_applyLayout: function(layout) {
this.$target.removeClass('layout-grid layout-list');
this.$target.addClass(`layout-${layout}`);
},
/**
* Compute option visibility
*/
_computeWidgetVisibility: function(methodName, params) {
if (methodName === 'showAdvanced') {
return this.$target.hasClass('advanced-mode');
}
return this._super(...arguments);
},
});
📋 片段的版本差异
| 功能 | Odoo 14-15 | Odoo 16-17 | Odoo 18-19 |
|---|---|---|---|
| 片段数量 | ~40 | 81+ | 155+ |
| 注册方式 | 简单 XPath | 简单 XPath | 必须使用分组 |
| 资源系统 | 模板继承 | ir.asset 模型 | ir.asset 模型 |
| JavaScript | odoo.define() | ES6 模块 | 基于插件 |
| Bootstrap | 4.x | 5.1.3 | 5.1.3 |
| 动态片段 | 基础 | 完整 | 完整 + 分类 |
| 编辑器 | 旧版 | 旧版 | 插件架构 |
Odoo 19 插件模式(新增)
import { Plugin } from "@html_editor/plugin";
import { registry } from "@web/core/registry";
export class MySnippetPlugin extends Plugin {
static id = "mySnippet";
static dependencies = ["builderOptions", "builderActions"];
resources = {
builder_options: [{
template: "my_module.MySnippetOption",
selector: "section",
applyTo: ".s_my_snippet",
}],
builder_actions: {
MyAction: { /* action definition */ },
},
};
}
registry.category("website-plugins").add("mySnippet", MySnippetPlugin);
📋 创建动态片段
步骤 1:定义筛选器
<record id="dynamic_filter_my_items" model="website.snippet.filter">
<field name="name">My Items</field>
<field name="model_name">my.model</field>
<field name="limit">12</field>
<field name="filter_id" ref="ir_filter_my_items"/>
</record>
<record id="ir_filter_my_items" model="ir.filters">
<field name="name">Latest Items</field>
<field name="model_id">my.model</field>
<field name="domain">[('is_published', '=', True)]</field>
<field name="sort">create_date desc</field>
</record>
步骤 2:创建显示模板
<template id="dynamic_filter_template_my_model_card" name="My Model Card">
<t t-foreach="records" t-as="record">
<div class="col-lg-4 mb-4">
<div class="card h-100 shadow-sm">
<img t-att-src="record.image_url" class="card-img-top"/>
<div class="card-body">
<h5 class="card-title" t-esc="record.name"/>
<p class="card-text" t-esc="record.description"/>
</div>
</div>
</div>
</t>
</template>
第 3 步:创建动态片段
<template id="s_dynamic_my_items" name="Dynamic My Items">
<section class="s_dynamic_snippet s_dynamic_my_items pt48 pb48"
data-snippet="s_dynamic_my_items"
data-name="Dynamic My Items"
data-filter-id="my_module.dynamic_filter_my_items"
data-template-key="my_module.dynamic_filter_template_my_model_card"
data-number-of-elements="6"
data-number-of-elements-small-devices="2">
<div class="container">
<div class="row">
<h2 class="col-12 text-center mb-4">Featured Items</h2>
</div>
<div class="dynamic_snippet_template row"/>
</div>
</section>
</template>
更新后的 /create-theme 命令(v6.0)
/create-theme 命令现在会自动生成 models/theme_xxx.py 文件,并激活模板。
创建的内容(v6.0)
theme_<name>/
├── __init__.py # Imports models
├── __manifest__.py # Updated with models/ import
├── models/
│ ├── __init__.py # Imports theme_<name>
│ └── theme_<name>.py # theme.utils implementation (NEW!)
├── security/
│ └── ir.model.access.csv
├── data/
│ ├── assets.xml
│ ├── menu.xml
│ └── pages/
│ ├── home_page.xml
│ ├── aboutus_page.xml
│ └── contactus_page.xml
├── views/
│ ├── layout/
│ │ └── templates.xml
│ └── snippets/
│ └── custom_snippets.xml
└── static/src/
├── scss/
│ ├── primary_variables.scss
│ └── theme.scss
├── js/
│ └── theme.js
└── img/
生成的 theme_.py
from odoo import models
class Theme<Name>(models.AbstractModel):
_inherit = 'theme.utils'
def _theme_<name>_post_copy(self, mod):
"""
Configure theme defaults when installed on a website.
This method is called automatically when the theme is applied.
Use enable_view() and disable_view() to configure templates.
"""
# === HEADER CONFIGURATION ===
# Enable desired header template (only one can be active)
self.enable_view('website.template_header_<detected>')
# Configure header visibility
self.disable_view('website.header_visibility_standard')
self.enable_view('website.header_visibility_fixed')
# Enable header components
self.enable_view('website.option_header_brand_logo')
# === FOOTER CONFIGURATION ===
# Enable desired footer template (only one can be active)
self.enable_view('website.template_footer_<detected>')
更新日志
-
v7.0.0:网站片段完整参考(重大更新)
- 新增:完整的静态片段清单(81+ 个模板)
- 结构片段(s_banner、s_cover、s_parallax 等)
- 功能片段(s_features、s_comparisons、s_image_text 等)
- 动态内容片段(s_dynamic_snippet_*、products、blog、events)
- 内部内容片段(s_hr、s_badge、s_card、s_blockquote 等)
- 超级菜单片段(s_mega_menu_*、multi_menus、odoo_menu)
- 新增:动态片段系统文档
- website.snippet.filter 模型架构
- 15 个产品展示模板(dynamic_filter_template_*)
- 6 个内置产品片段过滤器
- 博客和活动的动态片段模式
- 新增:片段选项系统参考
- 完整的 we-* 元素(we-select、we-button、we-colorpicker、we-input、we-range、we-checkbox)
- 数据属性参考(data-selector、data-select-class、data-js、data-css-property、data-dependencies)
- 内置 JavaScript 处理器(BackgroundImage、Carousel、ImageTools 等)
- 完整的片段选项 XML 示例
- 新增:版本感知型自定义片段创建
- Odoo 14-17 简单的 t-snippet 注册
- Odoo 18-19 片段组模式
- XPath 插入技术
- 新增:Odoo 19 插件模式文档
- 基于 @html_editor/plugin 的架构
- 130+ 个插件文件参考
- SnippetOption 类模式
- 新增:创建动态片段指南
- 定义过滤器(website.snippet.filter)
- 创建展示模板(dynamic_filter_template_*)
- 使用 data-filter-id 创建动态片段
- 新增:完整的静态片段清单(81+ 个模板)
-
v6.0.0:主题功能激活系统与动态页面参考(重大更新)
- 新增:主题功能激活系统
- 完整的
theme.utils模型文档 _theme_{module}_post_copy()模式要求enable_view()和disable_view()方法参考- 页眉/页脚互斥规则
- 完整的可运行示例
- 完整的
- 新增:完整的动态页面参考
- 11 个页眉模板,附带 XML ID 和可视化示意图
- 9 个页脚模板,附带 XML ID 和可视化示意图
- 所有商店页面模板(类别、筛选器、布局)
- 所有产品详情页模板
- 所有博客模板(列表、详情、侧边栏)
- 购物车和结账模板
- 新增:设计工作流方法
- Figma 分析提取指南
- 模板匹配决策流程图
- 根据设计生成配置
- 分步工作流文档
- 更新:/create-theme 命令 v6.0
- 现在会自动生成
models/theme_xxx.py - 根据配置激活模板
- 包含所有必需文件的完整模块结构
- 现在会自动生成
- 新增:主题功能激活系统
-
v5.1.0:全面增强变量参考(重大更新)
- 完整的 $o-theme-font-configs 参考:
- 'family':CSS font-family 列表(元组格式)
- 'url':仅限 Google Fonts 参数(不是完整 URL)
- 'properties':按上下文设置 CSS 覆盖项(base、headings、navbar、buttons)
- 字体别名文档:'base'、'headings'、'h2'-'h6'、'navbar'、'buttons'
- 阿拉伯语/RTL 字体支持示例
- 完整的 $o-color-palettes 参考:
- 5 种核心颜色的语义含义(o-color-1 至 o-color-5)
- 颜色组合(o_cc1 - o_cc5)预设和用法
- 颜色组合覆盖语法(
'o-cc{n}-{property}') - 组件分配(菜单、页脚、版权信息)
- 使用颜色类的 HTML 示例
- 完整的 $o-website-values-palettes 参考(115+ 个键):
- 字体排版和字体(13 个键):字体系列配置
- 字体大小(13 个键):基础和标题字号
- 行高和边距(33 个键):间距配置
- 按钮(17 个键):内边距、圆角、样式选项
- 输入框(12 个键):表单字段样式
- 页眉(13 个键):模板、链接样式、徽标、汉堡菜单
- 页脚(3 个键):模板、效果、回到顶部
- 链接(1 个键):下划线行为
- 布局(3 个键):全宽/固定宽度、背景
- 颜色和渐变(5 个键):调色板选择、渐变
- Google Fonts(2 个键):附加字体
- 恢复完整主题结构:header.xml、footer.xml、片段(可选,但包含在层级结构中)
- 删除冗余文档章节:清理重复内容
- 完整的 $o-theme-font-configs 参考:
-
v5.0.0:基于全面的变量系统分析进行重大增强
- 无需自定义 XML,即可通过变量控制页眉/页脚模板
- 通过
'header-template'和'footer-template'变量进行配置 - 通过
$o-website-values-palettes控制 Bootstrap
-
v4.0.0:根据实际主题开发问题进行关键修复
- ⚠️ SCSS 加载顺序文档:说明主题 SCSS 在核心变量之前加载
- 移除 map-merge() 模式:修复使用
map-merge()与核心变量的示例(这会导致“Undefined variable”错误) - 字体加载修复:将
$o-theme-font-configs用作独立配置,而不是通过ir.asset记录加载 - XPath 简化:使用简单表达式(//header),而不是复杂表达式(//header//nav)
- 增强故障排查:添加 SCSS 调试、资产缓存清理和静默编译失败相关内容
- 修正所有模板:现在
/create-theme命令生成的代码可正常运行,不会出现 map-merge 问题
-
v3.1.0:添加
/create-theme命令,根据 40+ 个实际实现生成完整主题- 完整的
$o-website-values-palettes配置 - 语义颜色系统(
o-color-1至o-color-5) - 单独页面文件模式(最佳实践)
- 处理
editableMode的 publicWidget 模式 - 特定版本的片段注册(14-19)
- 修复脚本输出的 Windows 控制台编码问题
- 完整的
-
v3.0.0:增强主题系统,加入 $o-website-values-palettes 参考和镜像模型架构
-
v2.0.0:新增 PWA 支持、TypeScript、测试框架、性能优化、无障碍功能、实时功能和现代构建工具
-
v1.0.0:首次发布,完整支持 Odoo 14-19,并集成 Figma/DevTools MCP