1
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import controllers
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "Quick Navigation",
|
||||
"summary": "快捷导航",
|
||||
"description": "快捷导航",
|
||||
"author": "Yuthen",
|
||||
"website": "#",
|
||||
"category": "Tools/Quick Navigation",
|
||||
"version": "18.0.0.1",
|
||||
"depends": ["base", "web"],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"security/ir_rule.xml",
|
||||
"views/quick_menu_config_views.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"quick_navigation/static/src/**/*",
|
||||
],
|
||||
},
|
||||
"installable": True,
|
||||
"application": False,
|
||||
"license": "LGPL-3",
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
from . import main
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class QuickMenuController(http.Controller):
|
||||
@http.route("/web/quick_menu/get_menus", type="json", auth="user")
|
||||
def get_quick_menus(self):
|
||||
"""获取当前用户的快捷菜单配置"""
|
||||
QuickMenuConfig = request.env["quick.menu.config"]
|
||||
menus = QuickMenuConfig.search(
|
||||
[("user_id", "=", request.env.user.id), ("active", "=", True)],
|
||||
order="sequence",
|
||||
)
|
||||
|
||||
result = []
|
||||
for menu in menus:
|
||||
# 检查菜单是否存在且用户有权限访问
|
||||
if (
|
||||
menu.menu_id
|
||||
and menu.menu_id.action
|
||||
and request.env.user.has_group("base.group_user")
|
||||
):
|
||||
action = menu.menu_id.action.sudo()
|
||||
action_type = action._name.split(".")[0]
|
||||
|
||||
# 获取action的完整信息
|
||||
action_data = action.read()[0]
|
||||
if action_type == "ir":
|
||||
action_data.update(
|
||||
{
|
||||
"type": "ir.actions.act_window",
|
||||
"target": "current",
|
||||
}
|
||||
)
|
||||
|
||||
result.append(
|
||||
{
|
||||
"id": menu.id,
|
||||
"name": menu.name or menu.menu_id.name,
|
||||
"icon": menu.icon,
|
||||
"color": menu.color or "#2ECC71", # 如果颜色为空,使用默认颜色
|
||||
"action": action_data,
|
||||
"sequence": menu.sequence,
|
||||
"menu_id": menu.menu_id.id,
|
||||
"show_title": menu.show_title,
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@http.route("/web/quick_menu/get_visibility", type="json", auth="user")
|
||||
def get_quick_menu_visibility(self):
|
||||
"""返回当前用户的快捷菜单显示状态"""
|
||||
user = request.env.user.sudo()
|
||||
return {"is_visible": bool(user.quick_menu_is_visible)}
|
||||
|
||||
@http.route("/web/quick_menu/set_visibility", type="json", auth="user")
|
||||
def set_quick_menu_visibility(self, is_visible):
|
||||
"""更新当前用户的快捷菜单显示状态"""
|
||||
user = request.env.user.sudo()
|
||||
normalized_value = is_visible
|
||||
if not isinstance(is_visible, bool):
|
||||
normalized_value = str(is_visible).lower() in ("1", "true", "t", "yes", "y")
|
||||
user.write({"quick_menu_is_visible": normalized_value})
|
||||
return {"is_visible": normalized_value}
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import quick_menu_config
|
||||
from . import res_users
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,92 @@
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class QuickMenuConfig(models.Model):
|
||||
_name = "quick.menu.config"
|
||||
_description = "Quick Menu Configuration"
|
||||
_order = "sequence, id"
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"user_menu_uniq",
|
||||
"unique(user_id, menu_id)",
|
||||
"同一用户不能重复配置相同的菜单!",
|
||||
)
|
||||
]
|
||||
|
||||
def _get_next_sequence(self):
|
||||
"""获取当前用户的下一个序号(从1开始递增)"""
|
||||
user_id = self.env.user.id
|
||||
max_sequence = (
|
||||
self.env["quick.menu.config"]
|
||||
.search(
|
||||
[("user_id", "=", user_id), ("active", "=", True)],
|
||||
order="sequence desc",
|
||||
limit=1,
|
||||
)
|
||||
.sequence
|
||||
or 0
|
||||
)
|
||||
return max_sequence + 1
|
||||
|
||||
name = fields.Char(string="名称", required=True)
|
||||
user_id = fields.Many2one(
|
||||
"res.users",
|
||||
string="用户",
|
||||
required=True,
|
||||
default=lambda self: self.env.user,
|
||||
ondelete="cascade",
|
||||
)
|
||||
menu_id = fields.Many2one(
|
||||
"ir.ui.menu",
|
||||
string="菜单",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
domain=[("action", "!=", False)],
|
||||
) # 只允许选择有action的菜单
|
||||
sequence = fields.Integer(
|
||||
string="序号", default=lambda self: self._get_next_sequence()
|
||||
)
|
||||
icon = fields.Selection(
|
||||
[
|
||||
("fa-star", "星星"),
|
||||
("fa-heart", "心形"),
|
||||
("fa-user", "用户"),
|
||||
("fa-cog", "设置"),
|
||||
("fa-home", "主页"),
|
||||
("fa-list", "列表"),
|
||||
("fa-calendar", "日历"),
|
||||
("fa-envelope", "信封"),
|
||||
("fa-bell", "铃铛"),
|
||||
("fa-file", "文件"),
|
||||
],
|
||||
string="图标",
|
||||
required=True,
|
||||
default="fa-star",
|
||||
)
|
||||
color = fields.Char(
|
||||
string="颜色",
|
||||
default="#2ECC71",
|
||||
help="菜单项的颜色,可以使用HEX颜色代码,例如 #FF0000 表示红色",
|
||||
)
|
||||
active = fields.Boolean(string="启用", default=True)
|
||||
show_title = fields.Boolean(
|
||||
string="默认显示标题",
|
||||
default=False,
|
||||
help="如果勾选,该菜单项的标题将在菜单展开时默认显示,无需鼠标悬停",
|
||||
)
|
||||
|
||||
@api.constrains("menu_id")
|
||||
def _check_menu_count(self):
|
||||
"""检查每个用户的快捷菜单数量不超过8个"""
|
||||
for record in self:
|
||||
menu_count = self.search_count(
|
||||
[
|
||||
("user_id", "=", record.user_id.id),
|
||||
("active", "=", True),
|
||||
("id", "!=", record.id),
|
||||
]
|
||||
)
|
||||
if menu_count >= 8:
|
||||
raise ValidationError(_("每个用户最多只能配置8个快捷菜单!"))
|
||||
@@ -0,0 +1,11 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = "res.users"
|
||||
|
||||
quick_menu_is_visible = fields.Boolean(
|
||||
string="显示快捷菜单",
|
||||
default=True,
|
||||
help="控制当前用户的快捷菜单是否显示在页面上。",
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_quick_menu_config,access.quick.menu.config,model_quick_menu_config,base.group_user,1,1,1,1
|
||||
|
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- 快捷菜单权限规则:用户只能查看和管理自己的快捷菜单 -->
|
||||
<record id="rule_quick_menu_config_user" model="ir.rule">
|
||||
<field name="name">用户只能查看和管理自己的快捷菜单</field>
|
||||
<field name="model_id" ref="model_quick_menu_config"/>
|
||||
<field name="domain_force">[('user_id', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762086593490" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7830" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M712.4 341.6l-173.4 393.4c-10.2 22-43.8 14.8-43.8-9.6v-188.2c0-0.6 0-1.2-0.2-1.6-0.2-0.6-0.2-1-0.4-1.6-0.2-0.6-0.4-1-0.8-1.4-0.4-0.4-0.6-0.8-1-1.2-0.4-0.4-0.8-0.8-1.2-1-0.4-0.4-1-0.6-1.4-0.8-0.6-0.2-1-0.4-1.6-0.4-0.6-0.2-1-0.2-1.6-0.2h-188.2c-24.2 0-31.6-33.4-9.6-43.6l393.4-173.6c2.2-1 4.4-1.6 6.6-1.8 2.2-0.2 4.6-0.2 6.8 0.2s4.4 1.2 6.4 2.2c2 1 3.8 2.4 5.6 4 1.6 1.6 3 3.4 4 5.6 1 2 1.8 4.2 2.2 6.4 0.4 2.2 0.4 4.6 0.2 6.8-0.4 2.2-1 4.4-2 6.4zM518.2 73.2C271.4 70 70 271.4 73.2 518.2c3.2 236.6 196 429.4 432.6 432.6 246.8 3.4 448.2-198 444.8-444.8-3.2-236.8-195.8-429.4-432.4-432.8z" p-id="7831" data-spm-anchor-id="a313x.search_index.0.i11.683d3a81IBy3Xt" class="" fill="#0085FE"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,409 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Component, useState, onMounted, onWillUnmount, useRef } from "@odoo/owl";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
export class FloatingMenu extends Component {
|
||||
static template = "quick_navigation.FloatingMenu";
|
||||
setup() {
|
||||
this.root = useRef("root_container");
|
||||
this.state = useState({
|
||||
isOpen: false,
|
||||
isVisible: true, // 用于在网站预览中隐藏
|
||||
isComponentVisible: true, // 用于用户关闭组件
|
||||
isDragging: false,
|
||||
// 初始位置,这些将通过拖动更新
|
||||
containerPosition: { right: 50, bottom: 50 },
|
||||
dragStartMouse: { x: 0, y: 0 },
|
||||
dragStartContainer: { right: 0, bottom: 0 },
|
||||
menuItems: [] // 菜单项将从服务器获取
|
||||
});
|
||||
|
||||
// 初始化全局状态管理
|
||||
if (!window.quickMenuGlobalState) {
|
||||
window.quickMenuGlobalState = useState({
|
||||
showSystrayIcon: false
|
||||
});
|
||||
}
|
||||
this.globalState = window.quickMenuGlobalState;
|
||||
|
||||
// 存储超时ID以便清理
|
||||
this.hoverTimeout = null;
|
||||
this.leaveTimeout = null;
|
||||
|
||||
this.action = useService("action");
|
||||
this.rpc = rpc;
|
||||
|
||||
this.onMouseMove = this._onMouseMove.bind(this);
|
||||
this.onMouseUp = this._onMouseUp.bind(this);
|
||||
this.onDocumentClick = this._onDocumentClick.bind(this);
|
||||
this.onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this.onMouseLeave = this._onMouseLeave.bind(this);
|
||||
|
||||
onMounted(async () => {
|
||||
await this.loadMenuItems();
|
||||
// 初始化基于后端持久化字段的可见性
|
||||
try {
|
||||
const visibility = await this.rpc('/web/quick_menu/get_visibility');
|
||||
const isVisible = !!(visibility && visibility.is_visible);
|
||||
this.state.isComponentVisible = isVisible;
|
||||
if (this.globalState) {
|
||||
this.globalState.showSystrayIcon = !isVisible;
|
||||
}
|
||||
} catch (err) {
|
||||
// 后端不可用时,保持默认显示
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('获取快捷菜单可见性失败,使用默认值', err);
|
||||
}
|
||||
await browser.setTimeout(() => this.setupObserver(), 2000);
|
||||
// 绑定鼠标移动和抬起事件到文档以实现更流畅的拖动
|
||||
document.addEventListener('mousemove', this.onMouseMove);
|
||||
document.addEventListener('mouseup', this.onMouseUp);
|
||||
document.addEventListener('click', this.onDocumentClick);
|
||||
|
||||
// 确保CSS变量在DOM完全加载后设置
|
||||
await browser.setTimeout(() => {
|
||||
if (this.state.menuItems.length > 0) {
|
||||
const dynamicRingSize = this.calculateDynamicRingSize(this.state.menuItems.length);
|
||||
this.setDynamicRingSize(dynamicRingSize);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 注册全局方法供SystrayMenu调用
|
||||
if (!window.quickMenuManager) {
|
||||
window.quickMenuManager = {
|
||||
showFloatingMenu: () => this.showFloatingMenu()
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
this.cleanupObserver();
|
||||
// 清除所有事件监听器和超时
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
document.removeEventListener('mouseup', this.onMouseUp);
|
||||
document.removeEventListener('click', this.onDocumentClick);
|
||||
|
||||
// 清除所有可能的超时
|
||||
if (this.hoverTimeout) {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
this.hoverTimeout = null;
|
||||
}
|
||||
if (this.leaveTimeout) {
|
||||
clearTimeout(this.leaveTimeout);
|
||||
this.leaveTimeout = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadMenuItems() {
|
||||
try {
|
||||
const menus = await this.rpc('/web/quick_menu/get_menus');
|
||||
// 根据菜单数量动态计算圆环大小
|
||||
const dynamicRingSize = this.calculateDynamicRingSize(menus.length);
|
||||
|
||||
// 计算每个菜单项的位置,使用动态半径
|
||||
const positions = this.calculateMenuPositions(menus.length, dynamicRingSize / 2);
|
||||
this.state.menuItems = menus.map((menu, index) => ({
|
||||
...menu,
|
||||
x: positions[index].x,
|
||||
y: positions[index].y,
|
||||
direction: positions[index].direction,
|
||||
icon: `fa ${menu.icon}`,
|
||||
initialStyle: 'transform: translate(0px, 0px);',
|
||||
currentStyle: 'transform: translate(0px, 0px);'
|
||||
}));
|
||||
|
||||
// 设置CSS变量用于动态圆环大小
|
||||
this.setDynamicRingSize(dynamicRingSize);
|
||||
} catch (error) {
|
||||
console.error('加载菜单项失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
calculateDynamicRingSize(menuCount) {
|
||||
// 根据菜单数量计算合适的圆环大小
|
||||
// 基础大小 + 根据菜单数量的额外大小,但最大不超过160px
|
||||
const baseSize = 100; // 基础圆环大小
|
||||
const sizePerItem = 8; // 每个菜单项增加的大小
|
||||
const calculatedSize = baseSize + (menuCount * sizePerItem);
|
||||
|
||||
// 限制最大为160px,最小为100px
|
||||
return Math.min(Math.max(calculatedSize, 100), 160);
|
||||
}
|
||||
|
||||
setDynamicRingSize(ringSize) {
|
||||
// 在根容器上设置CSS变量
|
||||
if (this.root.el) {
|
||||
this.root.el.style.setProperty('--dynamic-ring-size', `${ringSize}px`);
|
||||
}
|
||||
}
|
||||
|
||||
calculateMenuPositions(count, radius = 90) {
|
||||
const positions = [];
|
||||
const angleStep = (2 * Math.PI) / count; // 平均分布的角度步长
|
||||
const threshold = 0.3; // 判断是否在轴上的阈值
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = i * angleStep - Math.PI / 2; // 从正上方开始分布
|
||||
const x = Math.round(radius * Math.cos(angle));
|
||||
const y = Math.round(radius * Math.sin(angle));
|
||||
|
||||
// 根据坐标轴位置确定方向
|
||||
// 0-向右,1-向左,2-向上,3-向下
|
||||
let direction;
|
||||
|
||||
// 判断是否在y轴上
|
||||
if (Math.abs(Math.cos(angle)) < threshold) {
|
||||
// 接近y轴
|
||||
if (y > 0) {
|
||||
direction = 3; // y轴正半轴,向上展示
|
||||
} else {
|
||||
direction = 2; // y轴负半轴,向下展示
|
||||
}
|
||||
}
|
||||
// 判断是否在x轴上
|
||||
else if (Math.abs(Math.sin(angle)) < threshold) {
|
||||
// 接近x轴
|
||||
if (x > 0) {
|
||||
direction = 0; // x轴正半轴,向右展示
|
||||
} else {
|
||||
direction = 1; // x轴负半轴,向左展示
|
||||
}
|
||||
}
|
||||
// 不在轴上,按象限判断
|
||||
else {
|
||||
if (x > 0) {
|
||||
direction = 0; // 一四象限,向右展示
|
||||
} else {
|
||||
direction = 1; // 二三象限,向左展示
|
||||
}
|
||||
}
|
||||
|
||||
positions.push({
|
||||
x: x,
|
||||
y: y,
|
||||
direction: direction
|
||||
});
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
setupObserver() {
|
||||
this.checkWebsitePreview();
|
||||
this.observer = new MutationObserver(() => {
|
||||
browser.setTimeout(() => this.checkWebsitePreview(), 100);
|
||||
});
|
||||
this.observer.observe(document.body, { attributes: true, childList: true, subtree: true });
|
||||
}
|
||||
|
||||
cleanupObserver() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
}
|
||||
|
||||
checkWebsitePreview() {
|
||||
// 使用 Odoo 服务直接判断是否在网站模式
|
||||
// undefined = 后台模式,有值 = 网站模式
|
||||
debugger;
|
||||
const isWebsiteMode = !!this.env.services.website?.currentWebsite;
|
||||
|
||||
// 更新可见性状态(网站模式时隐藏浮动菜单)
|
||||
if (this.state.isVisible !== !isWebsiteMode) {
|
||||
this.state.isVisible = !isWebsiteMode;
|
||||
if (!this.state.isVisible && this.state.isOpen) {
|
||||
this.state.isOpen = false; // 当变为不可见时也关闭菜单
|
||||
// 当菜单隐藏时重置位置
|
||||
this.state.menuItems = this.state.menuItems.map(item => ({
|
||||
...item,
|
||||
currentStyle: item.initialStyle
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCenterMouseDown(event) {
|
||||
if (event.target.classList.contains('quick-menu-close-button')) {
|
||||
// 如果点击的是关闭按钮,不要开始拖动或切换
|
||||
return;
|
||||
}
|
||||
if (!this.state.isDragging) {
|
||||
this.state.isDragging = true;
|
||||
this.state.dragStartMouse = { x: event.clientX, y: event.clientY };
|
||||
// 获取当前容器位置(假设通过style.right和style.bottom设置)
|
||||
const rect = this.root.el.getBoundingClientRect();
|
||||
this.state.dragStartContainer = {
|
||||
right: window.innerWidth - rect.right,
|
||||
bottom: window.innerHeight - rect.bottom
|
||||
};
|
||||
event.preventDefault(); // 防止文本选择
|
||||
}
|
||||
}
|
||||
|
||||
_onMouseMove(event) {
|
||||
if (!this.state.isDragging) return;
|
||||
const deltaX = event.clientX - this.state.dragStartMouse.x;
|
||||
const deltaY = event.clientY - this.state.dragStartMouse.y;
|
||||
|
||||
// 新位置是初始位置加上偏移量。由于我们使用right/bottom,正的deltaX意味着更小的'right'值
|
||||
this.state.containerPosition = {
|
||||
right: Math.max(0, this.state.dragStartContainer.right - deltaX),
|
||||
bottom: Math.max(0, this.state.dragStartContainer.bottom - deltaY)
|
||||
};
|
||||
}
|
||||
|
||||
_onMouseUp(event) {
|
||||
if (this.state.isDragging) {
|
||||
this.state.isDragging = false;
|
||||
if (!this.state.isOpen && event.target.closest('.menu-center')) {
|
||||
// 如果是在中心按钮上结束拖动,且菜单处于关闭状态,切换菜单
|
||||
// 这样可以实现点击中心按钮切换菜单,而不是拖动
|
||||
this.openMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标进入菜单容器
|
||||
_onMouseEnter() {
|
||||
// 清除可能的离开超时
|
||||
if (this.leaveTimeout) {
|
||||
clearTimeout(this.leaveTimeout);
|
||||
this.leaveTimeout = null;
|
||||
}
|
||||
|
||||
if (!this.state.isDragging) {
|
||||
// 添加延迟,避免偶然经过时菜单就打开
|
||||
this.hoverTimeout = setTimeout(() => {
|
||||
if (!this.state.isOpen) {
|
||||
this.openMenu();
|
||||
}
|
||||
}, 300); // 200毫秒的悬停延迟
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标离开菜单容器
|
||||
_onMouseLeave() {
|
||||
// 清除可能的悬停超时
|
||||
if (this.hoverTimeout) {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
this.hoverTimeout = null;
|
||||
}
|
||||
|
||||
// 添加延迟关闭,避免用户意外移出又移入时菜单闪烁
|
||||
if (!this.state.isDragging && this.state.isOpen) {
|
||||
this.leaveTimeout = setTimeout(() => {
|
||||
this.closeMenu();
|
||||
}, 300); // 300毫秒的离开延迟
|
||||
}
|
||||
}
|
||||
|
||||
// 打开菜单
|
||||
openMenu() {
|
||||
this.state.isOpen = true;
|
||||
this.applyCustomPositions();
|
||||
}
|
||||
|
||||
// 关闭菜单
|
||||
closeMenu() {
|
||||
this.state.isOpen = false;
|
||||
this.state.menuItems = this.state.menuItems.map(item => ({
|
||||
...item,
|
||||
currentStyle: item.initialStyle
|
||||
}));
|
||||
}
|
||||
|
||||
// 在菜单打开后添加一个短暂的延迟,防止菜单项在动画期间响应hover
|
||||
openMenu() {
|
||||
this.state.isOpen = true;
|
||||
this.applyCustomPositions();
|
||||
|
||||
// 添加延迟处理hover事件
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.menu-item').forEach(item => {
|
||||
item.classList.add('can-hover');
|
||||
});
|
||||
}, 1000); // 500ms延迟,确保所有菜单项动画完成
|
||||
}
|
||||
|
||||
// 重命名以避免与非拖动时的中心按钮点击冲突
|
||||
toggleMenuDisplay(event) {
|
||||
// 只有在不拖动且不点击关闭按钮时才切换
|
||||
if (event && event.target.classList.contains('quick-menu-close-button')) return;
|
||||
|
||||
if (this.state.isOpen) {
|
||||
this.closeMenu();
|
||||
} else {
|
||||
this.openMenu();
|
||||
}
|
||||
}
|
||||
|
||||
applyCustomPositions() {
|
||||
this.state.menuItems = this.state.menuItems.map(item => {
|
||||
return { ...item, currentStyle: `transform: translate(${item.x}px, ${item.y}px);` };
|
||||
});
|
||||
}
|
||||
|
||||
onMenuClick(menuItem) {
|
||||
// 执行菜单动作
|
||||
if (menuItem.action) {
|
||||
this.action.doAction(menuItem.action);
|
||||
}
|
||||
|
||||
// 关闭菜单
|
||||
this.closeMenu();
|
||||
}
|
||||
|
||||
closeComponent(event) {
|
||||
event.stopPropagation(); // 防止点击事件冒泡到onCenterMouseDown或toggleMenuDisplay
|
||||
this.state.isComponentVisible = false;
|
||||
// 关闭时折叠菜单项
|
||||
if (this.state.isOpen) {
|
||||
this.closeMenu();
|
||||
}
|
||||
// 显示SystrayMenu图标
|
||||
if (this.globalState) {
|
||||
this.globalState.showSystrayIcon = true;
|
||||
}
|
||||
// 持久化为不可见
|
||||
try {
|
||||
this.rpc('/web/quick_menu/set_visibility', { is_visible: false });
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('更新快捷菜单可见性失败', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 重新显示快捷菜单的方法
|
||||
showFloatingMenu() {
|
||||
this.state.isComponentVisible = true;
|
||||
// 隐藏SystrayMenu图标
|
||||
if (this.globalState) {
|
||||
this.globalState.showSystrayIcon = false;
|
||||
}
|
||||
// 持久化为可见
|
||||
try {
|
||||
this.rpc('/web/quick_menu/set_visibility', { is_visible: true });
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('更新快捷菜单可见性失败', err);
|
||||
}
|
||||
}
|
||||
|
||||
_onDocumentClick(event) {
|
||||
if (this.state.isOpen && this.root.el) {
|
||||
// 检查点击是否在菜单容器外部
|
||||
if (!this.root.el.contains(event.target)) {
|
||||
this.closeMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("main_components").add("FloatingMenu", {
|
||||
Component: FloatingMenu,
|
||||
});
|
||||
@@ -0,0 +1,331 @@
|
||||
// 带有单独定位圆形项目的浮动菜单的SCSS样式
|
||||
|
||||
$menu-container-size: 250px; // 增加容器大小,适应更大的菜单分布
|
||||
$center-button-size: 50px;
|
||||
$menu-item-size: 45px; // 每个圆形菜单项的直径
|
||||
$ring-size: 160px; // 默认圆环大小,可被动态CSS变量覆盖
|
||||
|
||||
// 新的配色方案
|
||||
$main-color: #3498DB; // 主色调:靛蓝色
|
||||
$secondary-color: #2ECC71; // 次要色调:绿色(默认颜色)
|
||||
$icon-color: white;
|
||||
$item-bg-color: white;
|
||||
$item-border-color: $secondary-color;
|
||||
$text-color-on-item: $secondary-color;
|
||||
$text-color-on-hover-label: white;
|
||||
$close-color: #95A5A6; // 关闭按钮颜色
|
||||
$close-button-size: 20px;
|
||||
$ring-color: rgba(46, 204, 113, 0.1); // 圆环颜色,使用次要色调的透明版本
|
||||
$ring-border-color: rgba(46, 204, 113, 0.2); // 圆环边框颜色
|
||||
|
||||
.o_floating_menu_container {
|
||||
position: fixed;
|
||||
// 初始位置由JS状态设置
|
||||
// right: 50px;
|
||||
// bottom: 50px;
|
||||
z-index: 1000;
|
||||
cursor: default; // 容器的默认光标样式
|
||||
|
||||
// 在网站预览页面中隐藏
|
||||
.o_website_preview & {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.o_floating_menu {
|
||||
position: relative;
|
||||
// 宽度和高度由项目展开决定,或使用menu-container-size保持视觉一致性
|
||||
width: $menu-container-size;
|
||||
height: $menu-container-size;
|
||||
|
||||
// 添加圆环
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
// 使用动态CSS变量,根据菜单数量调整大小,最大160px
|
||||
width: var(--dynamic-ring-size, $ring-size);
|
||||
height: var(--dynamic-ring-size, $ring-size);
|
||||
transform: translate(-50%, -50%) scale(0.8);
|
||||
border-radius: 50%;
|
||||
background: $ring-color;
|
||||
border: 2px solid $ring-border-color;
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&.menu-open::before {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
&.dragging .menu-center {
|
||||
cursor: grabbing; // 拖动时改变光标
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
.menu-center {
|
||||
position: absolute;
|
||||
left: 50%; // 在.o_floating_menu容器中居中
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: $center-button-size;
|
||||
height: $center-button-size;
|
||||
background: $main-color;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: grab; // 可拖动时的默认光标(关闭状态)
|
||||
z-index: 10;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
user-select: none; // 防止在拖动尝试期间选中文本
|
||||
|
||||
&:hover {
|
||||
background: lighten($main-color, 10%);
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
i.fa-compass {
|
||||
color: $icon-color;
|
||||
font-size: 20px;
|
||||
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
.quick-menu-close-button {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
width: $close-button-size;
|
||||
height: $close-button-size;
|
||||
background-color: $close-color;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: scale(0.5);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 11; // 在菜单中心图标之上
|
||||
|
||||
&:hover {
|
||||
background-color: darken($close-color, 10%);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 仅在菜单未打开时在悬停时显示关闭按钮
|
||||
// 这个条件现在主要由XML中的t-if处理
|
||||
// 我们依赖XML中的t-if="!state.isOpen"
|
||||
&:hover .quick-menu-close-button {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-open .menu-center {
|
||||
cursor: pointer; // 菜单打开时重置为普通指针
|
||||
background: $secondary-color;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.2);
|
||||
|
||||
i.fa-compass {
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
position: absolute;
|
||||
// 从.o_floating_menu的中心开始,然后通过JS平移
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
transform: scale(0.6);
|
||||
// 修复z-index,确保菜单项始终显示在上层
|
||||
z-index: 15;
|
||||
|
||||
.menu-item-content { // 可见的圆形项目
|
||||
width: $menu-item-size;
|
||||
height: $menu-item-size;
|
||||
background: $item-bg-color;
|
||||
// 使用CSS变量替代固定颜色
|
||||
border: 2px solid var(--menu-item-color, $secondary-color);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
position: relative; // 用于名称标签定位
|
||||
// 居中圆圈本身,因为.menu-item只是一个定位点
|
||||
transform: translate(-50%, -50%);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
// 使用CSS变量替代固定颜色
|
||||
background: var(--menu-item-color, $secondary-color);
|
||||
border-color: var(--menu-item-color, $secondary-color);
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
|
||||
i {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menu-item-name {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
// 使用CSS变量替代固定颜色
|
||||
color: var(--menu-item-color, $secondary-color);
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
// 菜单项标题样式
|
||||
.menu-item-name {
|
||||
position: absolute;
|
||||
// 使用CSS变量替代固定颜色
|
||||
background: var(--menu-item-color, $secondary-color);
|
||||
color: $text-color-on-hover-label;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
z-index: 20;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
// 方向0 - 向右
|
||||
&.direction-0 {
|
||||
left: calc(100% + 10px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
&::before {
|
||||
top: 50%;
|
||||
left: -8px;
|
||||
transform: translateY(-50%);
|
||||
// 使用CSS变量替代固定颜色
|
||||
border-right-color: var(--menu-item-color, $secondary-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 方向1 - 向左
|
||||
&.direction-1 {
|
||||
right: calc(100% + 10px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
&::before {
|
||||
top: 50%;
|
||||
right: -8px;
|
||||
transform: translateY(-50%);
|
||||
// 使用CSS变量替代固定颜色
|
||||
border-left-color: var(--menu-item-color, $secondary-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 方向2 - 向上
|
||||
&.direction-2 {
|
||||
bottom: calc(100% + 10px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
&::before {
|
||||
bottom: -8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
// 使用CSS变量替代固定颜色
|
||||
border-top-color: var(--menu-item-color, $secondary-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 方向3 - 向下
|
||||
&.direction-3 {
|
||||
top: calc(100% + 10px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
&::before {
|
||||
top: -8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
// 使用CSS变量替代固定颜色
|
||||
border-bottom-color: var(--menu-item-color, $secondary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-open .menu-item {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
transform: scale(1);
|
||||
|
||||
// 为每个菜单项添加级联延迟显示效果
|
||||
@for $i from 1 through 10 {
|
||||
&:nth-child(#{$i + 1}) {
|
||||
transition-delay: #{$i * 0.05}s;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认显示带有show-title类的标题
|
||||
.menu-item-content .menu-item-name.show-title {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
// 根据方向设置正确的transform
|
||||
&.direction-0 {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&.direction-1 {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&.direction-2 {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&.direction-3 {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item:not(.can-hover):hover .menu-item-name {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="quick_navigation.FloatingMenu" owl="1">
|
||||
<div t-if="state.isComponentVisible and state.isVisible"
|
||||
class="o_floating_menu_container"
|
||||
t-attf-style="right: {{ state.containerPosition.right }}px; bottom: {{ state.containerPosition.bottom }}px;"
|
||||
t-ref="root_container"
|
||||
t-on-mouseenter="onMouseEnter"
|
||||
t-on-mouseleave="onMouseLeave">
|
||||
<div class="o_floating_menu" t-att-class="{ 'menu-open': state.isOpen, 'dragging': state.isDragging }">
|
||||
<!-- 中心菜单按钮 -->
|
||||
<div class="menu-center"
|
||||
t-on-mousedown="onCenterMouseDown"
|
||||
t-on-click="toggleMenuDisplay">
|
||||
<i class="fa fa-compass"/>
|
||||
<!-- 关闭小按钮 -->
|
||||
<div t-if="!state.isOpen" class="quick-menu-close-button" t-on-click.stop="closeComponent">
|
||||
<i class="fa fa-times"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 菜单项列表 -->
|
||||
<t t-foreach="state.menuItems" t-as="item" t-key="item.id">
|
||||
<div class="menu-item" t-att-style="item.currentStyle">
|
||||
<div class="menu-item-content"
|
||||
t-att-data-item-id="item.id"
|
||||
t-on-click="() => this.onMenuClick(item)"
|
||||
t-att-data-direction="item.direction"
|
||||
t-attf-style="--menu-item-color: {{ item.color }};">
|
||||
<!-- 图标和名称作为直接子元素 -->
|
||||
<i t-att-class="item.icon"/>
|
||||
<span t-att-class="{'menu-item-name': true, ['direction-' + item.direction]: true, 'show-title': item.show_title}"
|
||||
t-esc="item.name"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,33 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export class QuickMenuSystray extends Component {
|
||||
static template = "quick_navigation.QuickMenuSystray";
|
||||
setup() {
|
||||
// 获取全局状态
|
||||
if (!window.quickMenuGlobalState) {
|
||||
window.quickMenuGlobalState = useState({
|
||||
showSystrayIcon: false
|
||||
});
|
||||
}
|
||||
this.globalState = window.quickMenuGlobalState;
|
||||
}
|
||||
onClick() {
|
||||
// 点击图标时显示快捷菜单
|
||||
if (window.quickMenuManager && window.quickMenuManager.showFloatingMenu) {
|
||||
window.quickMenuManager.showFloatingMenu();
|
||||
}
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
return this.globalState.showSystrayIcon;
|
||||
}
|
||||
}
|
||||
|
||||
export const systrayItem = {
|
||||
Component: QuickMenuSystray,
|
||||
};
|
||||
|
||||
registry.category("systray").add("quick_menu_access.systray", systrayItem, { sequence: 50 });
|
||||
@@ -0,0 +1,19 @@
|
||||
.quick-menu-systray {
|
||||
cursor: pointer;
|
||||
padding: 0 0.75rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
i.fa-compass {
|
||||
color: #3498DB; // 使用与浮动菜单相同的主色调
|
||||
font-size: 16px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="quick_navigation.QuickMenuSystray" owl="1">
|
||||
<div t-if="isVisible"
|
||||
class="o_mail_systray_item quick-menu-systray"
|
||||
t-on-click="onClick"
|
||||
title="显示快捷菜单">
|
||||
<i class="fa fa-compass" role="img" aria-label="Quick Menu"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,34 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
function quickMenuConfigItem(env) {
|
||||
const actionService = env.services.action;
|
||||
|
||||
return {
|
||||
type: "item",
|
||||
id: "quick_menu_config",
|
||||
description: _t("快捷菜单配置"),
|
||||
sequence: 45, // 放在分隔符和设置之间
|
||||
|
||||
async callback() {
|
||||
try {
|
||||
await actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: _t("快捷菜单配置"),
|
||||
res_model: "quick.menu.config",
|
||||
view_mode: "list,form",
|
||||
views: [[false, "list"], [false, "form"]],
|
||||
context: { search_default_active: 1 },
|
||||
target: "current",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to load quick menu config action:", error);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 注册到用户菜单
|
||||
registry.category("user_menuitems").add("quick_menu_config", quickMenuConfigItem);
|
||||
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- 树状视图 -->
|
||||
<record id="view_quick_menu_config_list" model="ir.ui.view">
|
||||
<field name="name">quick.menu.config.list</field>
|
||||
<field name="model">quick.menu.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="menu_id"/>
|
||||
<field name="icon"/>
|
||||
<field name="color" widget="color"/>
|
||||
<field name="show_title" widget="boolean_toggle"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
<!-- 表单视图 -->
|
||||
<record id="view_quick_menu_config_form" model="ir.ui.view">
|
||||
<field name="name">quick.menu.config.form</field>
|
||||
<field name="model">quick.menu.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="输入配置名称..."/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="menu_id"/>
|
||||
<field name="sequence" readonly="1"/>
|
||||
<field name="user_id" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="icon"/>
|
||||
<field name="color" widget="color"/>
|
||||
<field name="show_title" widget="boolean_toggle"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<!-- 搜索视图 -->
|
||||
<record id="view_quick_menu_config_search" model="ir.ui.view">
|
||||
<field name="name">quick.menu.config.search</field>
|
||||
<field name="model">quick.menu.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="user_id"/>
|
||||
<field name="menu_id"/>
|
||||
<field name="color"/>
|
||||
<filter string="已启用" name="active" domain="[('active', '=', True)]"/>
|
||||
<filter string="已禁用" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<filter string="默认显示标题" name="show_title" domain="[('show_title', '=', True)]"/>
|
||||
<group expand="0" string="分组">
|
||||
<filter string="用户" name="group_by_user" context="{'group_by': 'user_id'}"/>
|
||||
<filter string="菜单" name="group_by_menu" context="{'group_by': 'menu_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<!-- 动作 -->
|
||||
<record id="action_quick_menu_config" model="ir.actions.act_window">
|
||||
<field name="name">快捷菜单配置</field>
|
||||
<field name="res_model">quick.menu.config</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="context">{'search_default_active': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">创建第一个快捷菜单配置</p>
|
||||
<p>您可以为不同的用户配置快捷菜单,方便快速访问常用功能。</p>
|
||||
</field>
|
||||
</record>
|
||||
<!-- 菜单 -->
|
||||
<!-- <menuitem id="menu_quick_menu_config"
|
||||
name="快捷菜单配置"
|
||||
action="action_quick_menu_config"
|
||||
parent="base.menu_administration"
|
||||
sequence="100"/> -->
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user