diff --git a/quick_menu_access/__init__.py b/quick_menu_access/__init__.py new file mode 100644 index 000000000..f7209b171 --- /dev/null +++ b/quick_menu_access/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers diff --git a/quick_menu_access/__manifest__.py b/quick_menu_access/__manifest__.py new file mode 100644 index 000000000..f4d0e9d62 --- /dev/null +++ b/quick_menu_access/__manifest__.py @@ -0,0 +1,29 @@ +{ + 'name': '快捷菜单访问', + 'version': '17.0.1.0.0', + 'category': 'Tools', + 'summary': '快速访问常用菜单', + 'description': """ + 为用户提供快速访问常用菜单的悬浮按钮功能。 + 支持自定义配置每个用户的快捷菜单。 + """, + 'author': 'js', + 'website': 'https://www.jsmeiyouwangzhan.com', + 'depends': ['base', 'web'], + 'data': [ + 'security/ir.model.access.csv', + 'security/ir_rule.xml', + 'views/quick_menu_config_views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'quick_menu_access/static/src/components/floating_menu/floating_menu.js', + 'quick_menu_access/static/src/components/floating_menu/floating_menu.xml', + 'quick_menu_access/static/src/components/floating_menu/floating_menu.scss', + ], + }, + 'application': True, + 'installable': True, + 'auto_install': False, + 'license': 'LGPL-3', +} \ No newline at end of file diff --git a/quick_menu_access/controllers/__init__.py b/quick_menu_access/controllers/__init__.py new file mode 100644 index 000000000..576a1f4f1 --- /dev/null +++ b/quick_menu_access/controllers/__init__.py @@ -0,0 +1 @@ +from . import main \ No newline at end of file diff --git a/quick_menu_access/controllers/main.py b/quick_menu_access/controllers/main.py new file mode 100644 index 000000000..10805ca0a --- /dev/null +++ b/quick_menu_access/controllers/main.py @@ -0,0 +1,41 @@ +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 \ No newline at end of file diff --git a/quick_menu_access/models/__init__.py b/quick_menu_access/models/__init__.py new file mode 100644 index 000000000..c9d9023fe --- /dev/null +++ b/quick_menu_access/models/__init__.py @@ -0,0 +1 @@ +from . import quick_menu_config \ No newline at end of file diff --git a/quick_menu_access/models/quick_menu_config.py b/quick_menu_access/models/quick_menu_config.py new file mode 100644 index 000000000..73e4fb9f6 --- /dev/null +++ b/quick_menu_access/models/quick_menu_config.py @@ -0,0 +1,57 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class QuickMenuConfig(models.Model): + _name = 'quick.menu.config' + _description = '快捷菜单配置' + _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个快捷菜单!')) diff --git a/quick_menu_access/security/ir.model.access.csv b/quick_menu_access/security/ir.model.access.csv new file mode 100644 index 000000000..d10e8ba4c --- /dev/null +++ b/quick_menu_access/security/ir.model.access.csv @@ -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 \ No newline at end of file diff --git a/quick_menu_access/security/ir_rule.xml b/quick_menu_access/security/ir_rule.xml new file mode 100644 index 000000000..f62c9695e --- /dev/null +++ b/quick_menu_access/security/ir_rule.xml @@ -0,0 +1,16 @@ + + + + + + 用户只能查看和管理自己的快捷菜单 + + [('user_id', '=', user.id)] + + + + + + + + \ No newline at end of file diff --git a/quick_menu_access/static/src/components/floating_menu/floating_menu.js b/quick_menu_access/static/src/components/floating_menu/floating_menu.js new file mode 100644 index 000000000..e1e2ecb38 --- /dev/null +++ b/quick_menu_access/static/src/components/floating_menu/floating_menu.js @@ -0,0 +1,316 @@ +/** @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"; + +export class FloatingMenu extends Component { + 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: [] // 菜单项将从服务器获取 + }); + + // 存储超时ID以便清理 + this.hoverTimeout = null; + this.leaveTimeout = null; + + this.action = useService("action"); + this.rpc = useService("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(); + await browser.setTimeout(() => this.setupObserver(), 2000); + // 绑定鼠标移动和抬起事件到文档以实现更流畅的拖动 + document.addEventListener('mousemove', this.onMouseMove); + document.addEventListener('mouseup', this.onMouseUp); + document.addEventListener('click', this.onDocumentClick); + }); + + 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 positions = this.calculateMenuPositions(menus.length); + 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);' + })); + } catch (error) { + console.error('加载菜单项失败:', error); + } + } + + calculateMenuPositions(count) { + const positions = []; + const radius = 90; // 增加半径,使菜单项之间的距离变大 + 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() { + const websitePreviewElement = document.querySelector('.o_website_preview'); + const isWebsitePreview = !!websitePreviewElement; + if (this.state.isVisible !== !isWebsitePreview) { + this.state.isVisible = !isWebsitePreview; + 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(); + } + }, 200); // 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; + } + + _onDocumentClick(event) { + if (this.state.isOpen && this.root.el) { + // 检查点击是否在菜单容器外部 + if (!this.root.el.contains(event.target)) { + this.closeMenu(); + } + } + } +} + +FloatingMenu.template = "quick_menu_access.FloatingMenu"; + +registry.category("main_components").add("FloatingMenu", { + Component: FloatingMenu, +}); \ No newline at end of file diff --git a/quick_menu_access/static/src/components/floating_menu/floating_menu.scss b/quick_menu_access/static/src/components/floating_menu/floating_menu.scss new file mode 100644 index 000000000..4ca84363d --- /dev/null +++ b/quick_menu_access/static/src/components/floating_menu/floating_menu.scss @@ -0,0 +1,330 @@ +// 带有单独定位圆形项目的浮动菜单的SCSS样式 + +$menu-container-size: 250px; // 增加容器大小,适应更大的菜单分布 +$center-button-size: 50px; +$menu-item-size: 45px; // 每个圆形菜单项的直径 +$ring-size: 180px; // 增加圆环大小 + +// 新的配色方案 +$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%; + width: $ring-size; + height: $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; +} \ No newline at end of file diff --git a/quick_menu_access/static/src/components/floating_menu/floating_menu.xml b/quick_menu_access/static/src/components/floating_menu/floating_menu.xml new file mode 100644 index 000000000..d5fcffa79 --- /dev/null +++ b/quick_menu_access/static/src/components/floating_menu/floating_menu.xml @@ -0,0 +1,40 @@ + + + +
+
+ + + + + + + +
+
+
+
\ No newline at end of file diff --git a/quick_menu_access/views/quick_menu_config_views.xml b/quick_menu_access/views/quick_menu_config_views.xml new file mode 100644 index 000000000..867b1fc9c --- /dev/null +++ b/quick_menu_access/views/quick_menu_config_views.xml @@ -0,0 +1,92 @@ + + + + + quick.menu.config.tree + quick.menu.config + + + + + + + + + + + + + + + + quick.menu.config.form + quick.menu.config + +
+ +
+
+ + + + + + + + + + + + + +
+
+
+
+ + + + quick.menu.config.search + quick.menu.config + + + + + + + + + + + + + + + + + + + + 快捷菜单配置 + quick.menu.config + tree,form + {'search_default_active': 1} + +

+ 创建第一个快捷菜单配置 +

+

+ 您可以为不同的用户配置快捷菜单,方便快速访问常用功能。 +

+
+
+ + + +
\ No newline at end of file