diff --git a/k_ry_mobile_form/README.md b/k_ry_mobile_form/README.md new file mode 100644 index 000000000..3cb50c564 --- /dev/null +++ b/k_ry_mobile_form/README.md @@ -0,0 +1,54 @@ +# k_ry_mobile_form - 科瑞移动端表单优化模块 + +## 功能说明 + +此模块为 Odoo 17 的 form 视图添加移动端优化功能: +- 在手机浏览器中,form 视图 header 的工具按钮栏会悬浮在屏幕底部 +- 提供类似正常 APP 的底部按钮栏体验 +- 桌面端不受影响,保持原有显示方式 + +## 依赖 + +- `base` +- `web` + +## 安装 + +1. 将模块放置在 Odoo 的 addons 目录下 +2. 更新应用列表 +3. 安装 `k_ry_mobile_form` 模块 + +## 技术实现 + +### 文件结构 + +``` +k_ry_mobile_form/ +├── __init__.py +├── __manifest__.py +├── README.md +└── static/ + └── src/ + ├── js/ + │ └── status_bar_buttons.js # 覆盖 StatusBarButtons 组件模板 + ├── xml/ + │ └── status_bar_buttons.xml # 新的模板定义 + └── scss/ + └── status_bar_buttons.scss # 移动端底部悬浮栏样式 +``` + +### 实现原理 + +1. **JavaScript 覆盖**:使用 `patch` 方法覆盖 `StatusBarButtons` 组件的模板 +2. **模板替换**:提供新的模板,根据 `env.isSmall` 判断显示方式 +3. **样式隔离**:使用媒体查询确保移动端和桌面端样式互不影响 + +## 使用说明 + +安装模块后,无需额外配置。在手机浏览器中打开任何包含 header 的 form 视图时,工具按钮栏会自动显示在底部。 + +## 注意事项 + +- 此模块只影响 form 视图的 header 按钮栏 +- 桌面端完全不受影响 +- 支持 iOS 设备的底部安全区域(safe area) diff --git a/k_ry_mobile_form/static/src/js/status_bar_buttons.js b/k_ry_mobile_form/static/src/js/status_bar_buttons.js index 6fa0ea98e..57f149006 100644 --- a/k_ry_mobile_form/static/src/js/status_bar_buttons.js +++ b/k_ry_mobile_form/static/src/js/status_bar_buttons.js @@ -2,8 +2,381 @@ import { StatusBarButtons } from "@web/views/form/status_bar_buttons/status_bar_buttons"; import { patch } from "@web/core/utils/patch"; +import { useEffect } from "@odoo/owl"; -// 覆盖 StatusBarButtons 组件的模板 +// 检测是否为移动设备(包括iOS Safari) +const isMobileDevice = () => { + if (typeof window === 'undefined') return false; + + // 检测触摸设备 + const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + + // 检测用户代理(iOS设备) + const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; + + // 检测屏幕宽度 + const isSmallScreen = window.innerWidth <= 767; + + // 检测是否为移动设备(综合考虑) + return hasTouch && (isIOS || isSmallScreen); +}; + +// 覆盖 StatusBarButtons 组件,添加移动端检测逻辑 patch(StatusBarButtons, { template: "k_ry_mobile_form.StatusBarButtons", + setup() { + // 如果 env.isSmall 判断不准确,强制显示移动端按钮 + if (typeof window !== 'undefined') { + useEffect(() => { + const checkAndFixMobileButtons = () => { + try { + // 如果是移动设备但 env.isSmall 为 false,强制显示移动端按钮 + if (isMobileDevice()) { + const mobileContainers = document.querySelectorAll('.o_statusbar_buttons_mobile_fixed'); + if (mobileContainers.length === 0) return; + + mobileContainers.forEach(container => { + try { + // 使用内联样式强制显示,优先级最高 + container.style.setProperty('display', 'block', 'important'); + container.style.setProperty('visibility', 'visible', 'important'); + container.style.setProperty('opacity', '1', 'important'); + container.style.setProperty('position', 'fixed', 'important'); + container.style.setProperty('bottom', '0', 'important'); + container.style.setProperty('left', '0', 'important'); + container.style.setProperty('right', '0', 'important'); + container.style.setProperty('z-index', '99999', 'important'); + // 强制设置背景色,防止被主题覆盖 + container.style.setProperty('background-color', '#ffffff', 'important'); + container.style.setProperty('background', '#ffffff', 'important'); + + // 同时修复容器内的按钮 + const buttons = container.querySelectorAll('button, a.btn, .btn, a[role="button"], .o_statusbar_buttons > * > button, .o_statusbar_buttons > * > a'); + buttons.forEach(button => { + try { + // 强制设置按钮可见性 + button.style.setProperty('visibility', 'visible', 'important'); + button.style.setProperty('opacity', '1', 'important'); + button.style.setProperty('display', 'inline-block', 'important'); + button.style.setProperty('font-size', '14px', 'important'); + button.style.setProperty('font-weight', '500', 'important'); + // 防止文字被设置为透明 + button.style.setProperty('text-indent', '0', 'important'); + + // 检查按钮颜色 + const computedStyle = getComputedStyle(button); + const bgColor = computedStyle.backgroundColor; + const textColor = computedStyle.color; + + // 如果背景色是透明或白色,且文字颜色也是白色或透明,强制设置颜色 + const isTransparent = bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)'; + const isWhiteBg = bgColor === 'rgb(255, 255, 255)' || bgColor === '#ffffff' || bgColor === '#fff'; + const isWhiteText = textColor === 'rgb(255, 255, 255)' || textColor === '#ffffff' || textColor === '#fff'; + const isTransparentText = textColor === 'transparent' || textColor === 'rgba(0, 0, 0, 0)'; + + // 如果按钮不可见,强制设置颜色 + if (isTransparent || (isWhiteBg && (isWhiteText || isTransparentText)) || textColor === 'rgba(0, 0, 0, 0)') { + // 根据按钮类型设置颜色 + const hasPrimary = button.classList.contains('btn-primary') || button.classList.contains('o_statusbar_status'); + const hasSecondary = button.classList.contains('btn-secondary'); + const hasSuccess = button.classList.contains('btn-success'); + const hasDanger = button.classList.contains('btn-danger'); + const hasWarning = button.classList.contains('btn-warning'); + const hasInfo = button.classList.contains('btn-info'); + + if (hasPrimary || hasSuccess) { + button.style.setProperty('background-color', '#007bff', 'important'); + button.style.setProperty('color', '#ffffff', 'important'); + } else if (hasSecondary) { + button.style.setProperty('background-color', '#6c757d', 'important'); + button.style.setProperty('color', '#ffffff', 'important'); + } else if (hasDanger) { + button.style.setProperty('background-color', '#dc3545', 'important'); + button.style.setProperty('color', '#ffffff', 'important'); + } else if (hasWarning) { + button.style.setProperty('background-color', '#ffc107', 'important'); + button.style.setProperty('color', '#212529', 'important'); + } else if (hasInfo) { + button.style.setProperty('background-color', '#17a2b8', 'important'); + button.style.setProperty('color', '#ffffff', 'important'); + } else { + // 默认蓝色按钮 + button.style.setProperty('background-color', '#007bff', 'important'); + button.style.setProperty('color', '#ffffff', 'important'); + } + } + } catch (e) { + console.warn('Error fixing button in setup:', e); + } + }); + } catch (e) { + console.warn('Error fixing container in setup:', e); + } + }); + + // 隐藏桌面端按钮 + try { + const desktopButtons = document.querySelectorAll('.o_form_statusbar > .o_statusbar_buttons:not(.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons)'); + desktopButtons.forEach(btn => { + btn.style.setProperty('display', 'none', 'important'); + }); + } catch (e) { + console.warn('Error hiding desktop buttons in setup:', e); + } + } + } catch (e) { + console.warn('Error in checkAndFixMobileButtons:', e); + } + }; + + // 延迟执行,确保DOM已渲染 + setTimeout(checkAndFixMobileButtons, 100); + + // 监听窗口大小变化 + const handleResize = () => { + setTimeout(checkAndFixMobileButtons, 100); + }; + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }); + } + }, }); + +// iOS Safari修复:确保移动端按钮在iOS上可点击和可见 +// 使用MutationObserver监听DOM变化,确保新添加的按钮也能正常工作 +if (typeof window !== 'undefined' && typeof document !== 'undefined') { + let fixTimeout = null; + let observer = null; + let checkInterval = null; + + // 防抖函数,避免频繁执行 + const debounce = (func, wait) => { + return function executedFunction(...args) { + const later = () => { + clearTimeout(fixTimeout); + func(...args); + }; + clearTimeout(fixTimeout); + fixTimeout = setTimeout(later, wait); + }; + }; + + const fixIOSButtons = () => { + try { + // 确保移动端按钮容器可见 + const containers = document.querySelectorAll('.o_statusbar_buttons_mobile_fixed'); + if (containers.length === 0) return; + + containers.forEach(container => { + try { + // 如果是移动设备,确保容器可见 + if (isMobileDevice()) { + // 使用 setProperty 的 important 标志强制设置样式 + container.style.setProperty('display', 'block', 'important'); + container.style.setProperty('visibility', 'visible', 'important'); + container.style.setProperty('opacity', '1', 'important'); + container.style.setProperty('position', 'fixed', 'important'); + container.style.setProperty('bottom', '0', 'important'); + container.style.setProperty('left', '0', 'important'); + container.style.setProperty('right', '0', 'important'); + container.style.setProperty('z-index', '99999', 'important'); + // 强制设置背景色,防止被主题覆盖 + container.style.setProperty('background-color', '#ffffff', 'important'); + container.style.setProperty('background', '#ffffff', 'important'); + } + + // 修复按钮的交互性和可见性 + const buttons = container.querySelectorAll('button, a.btn, .btn, a[role="button"], .o_statusbar_buttons > * > button, .o_statusbar_buttons > * > a'); + buttons.forEach(button => { + try { + // 使用 setProperty 强制设置样式 + button.style.setProperty('pointer-events', 'auto', 'important'); + button.style.setProperty('cursor', 'pointer', 'important'); + button.style.setProperty('touch-action', 'manipulation', 'important'); + button.style.setProperty('visibility', 'visible', 'important'); + button.style.setProperty('opacity', '1', 'important'); + button.style.setProperty('display', 'inline-block', 'important'); + // 确保按钮有最小高度和字体大小 + button.style.setProperty('min-height', '44px', 'important'); + button.style.setProperty('font-size', '14px', 'important'); + button.style.setProperty('font-weight', '500', 'important'); + // 防止文字被设置为透明 + button.style.setProperty('text-indent', '0', 'important'); + + // 检查按钮颜色 + const computedStyle = getComputedStyle(button); + const bgColor = computedStyle.backgroundColor; + const textColor = computedStyle.color; + + // 如果背景色是透明或白色,且文字颜色也是白色或透明,强制设置颜色 + const isTransparent = bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)'; + const isWhiteBg = bgColor === 'rgb(255, 255, 255)' || bgColor === '#ffffff' || bgColor === '#fff'; + const isWhiteText = textColor === 'rgb(255, 255, 255)' || textColor === '#ffffff' || textColor === '#fff'; + const isTransparentText = textColor === 'transparent' || textColor === 'rgba(0, 0, 0, 0)'; + + // 如果按钮不可见,强制设置颜色 + if (isTransparent || (isWhiteBg && (isWhiteText || isTransparentText)) || textColor === 'rgba(0, 0, 0, 0)') { + // 根据按钮类型设置颜色 + const hasPrimary = button.classList.contains('btn-primary') || button.classList.contains('o_statusbar_status'); + const hasSecondary = button.classList.contains('btn-secondary'); + const hasSuccess = button.classList.contains('btn-success'); + const hasDanger = button.classList.contains('btn-danger'); + const hasWarning = button.classList.contains('btn-warning'); + const hasInfo = button.classList.contains('btn-info'); + + if (hasPrimary || hasSuccess) { + button.style.setProperty('background-color', '#007bff', 'important'); + button.style.setProperty('color', '#ffffff', 'important'); + } else if (hasSecondary) { + button.style.setProperty('background-color', '#6c757d', 'important'); + button.style.setProperty('color', '#ffffff', 'important'); + } else if (hasDanger) { + button.style.setProperty('background-color', '#dc3545', 'important'); + button.style.setProperty('color', '#ffffff', 'important'); + } else if (hasWarning) { + button.style.setProperty('background-color', '#ffc107', 'important'); + button.style.setProperty('color', '#212529', 'important'); + } else if (hasInfo) { + button.style.setProperty('background-color', '#17a2b8', 'important'); + button.style.setProperty('color', '#ffffff', 'important'); + } else { + // 默认蓝色按钮 + button.style.setProperty('background-color', '#007bff', 'important'); + button.style.setProperty('color', '#ffffff', 'important'); + } + } + } catch (e) { + console.warn('Error fixing button:', e); + } + }); + } catch (e) { + console.warn('Error fixing container:', e); + } + }); + + // 在移动设备上,确保原始状态栏中的按钮被隐藏 + if (isMobileDevice()) { + try { + const desktopButtons = document.querySelectorAll('.o_form_statusbar > .o_statusbar_buttons:not(.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons)'); + desktopButtons.forEach(btn => { + btn.style.setProperty('display', 'none', 'important'); + }); + } catch (e) { + console.warn('Error hiding desktop buttons:', e); + } + } + } catch (e) { + console.warn('Error in fixIOSButtons:', e); + } + }; + + // 防抖版本的修复函数 + const debouncedFixIOSButtons = debounce(fixIOSButtons, 300); + + // 初始化函数:确保DOM准备好后再设置observer + const initIOSButtonFix = () => { + try { + // 先执行一次修复 + fixIOSButtons(); + + // 确保document.body存在后再创建observer + if (document.body && !observer) { + // 监听DOM变化,确保动态添加的按钮也能正常工作 + observer = new MutationObserver((mutations) => { + // 只处理相关的变化 + const hasRelevantChange = mutations.some(mutation => { + if (mutation.type === 'childList') { + return Array.from(mutation.addedNodes).some(node => + node.nodeType === 1 && ( + node.classList?.contains('o_statusbar_buttons_mobile_fixed') || + node.querySelector?.('.o_statusbar_buttons_mobile_fixed') + ) + ); + } + if (mutation.type === 'attributes') { + return mutation.target.classList?.contains('o_statusbar_buttons_mobile_fixed') || + mutation.target.closest?.('.o_statusbar_buttons_mobile_fixed'); + } + return false; + }); + + if (hasRelevantChange) { + debouncedFixIOSButtons(); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['style', 'class'] + }); + } + } catch (e) { + console.warn('Error initializing button fix:', e); + } + }; + + // 页面加载后执行 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initIOSButtonFix, { once: true }); + } else { + // 使用setTimeout确保DOM完全准备好,不阻塞页面加载 + setTimeout(initIOSButtonFix, 100); + } + + // 监听窗口大小变化和方向变化(iOS Safari 横竖屏切换) + window.addEventListener('resize', () => { + debouncedFixIOSButtons(); + }, { passive: true }); + + window.addEventListener('orientationchange', () => { + setTimeout(debouncedFixIOSButtons, 200); + }, { passive: true }); + + // 定期检查按钮是否可见(防止被其他样式覆盖) + if (isMobileDevice()) { + // 延迟启动定时器,避免影响页面加载 + setTimeout(() => { + checkInterval = setInterval(() => { + try { + const containers = document.querySelectorAll('.o_statusbar_buttons_mobile_fixed'); + if (containers.length > 0) { + containers.forEach(container => { + try { + const computedStyle = getComputedStyle(container); + // 如果按钮被隐藏,强制显示 + if (computedStyle.display === 'none' || + computedStyle.visibility === 'hidden' || + parseFloat(computedStyle.opacity) < 0.1) { + fixIOSButtons(); + } + } catch (e) { + console.warn('Error checking container:', e); + } + }); + } + } catch (e) { + console.warn('Error in interval check:', e); + } + }, 2000); // 改为每2秒检查一次,减少频率 + }, 2000); + + // 页面卸载时清除定时器和observer + window.addEventListener('beforeunload', () => { + if (checkInterval) { + clearInterval(checkInterval); + } + if (observer) { + observer.disconnect(); + } + if (fixTimeout) { + clearTimeout(fixTimeout); + } + }); + } +} diff --git a/k_ry_mobile_form/static/src/scss/status_bar_buttons.scss b/k_ry_mobile_form/static/src/scss/status_bar_buttons.scss index d07f8d598..74a2c302f 100644 --- a/k_ry_mobile_form/static/src/scss/status_bar_buttons.scss +++ b/k_ry_mobile_form/static/src/scss/status_bar_buttons.scss @@ -1,41 +1,171 @@ // Mobile: 底部悬浮栏样式 -.o_statusbar_buttons_mobile_fixed { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 1050; // 确保在其他元素之上,但低于模态框 - background-color: $o-view-background-color; - border-top: 1px solid $border-color; - box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); - padding: map-get($spacers, 2) map-get($spacers, 3); +// 使用高优先级选择器,防止被其他主题样式覆盖 +.o_statusbar_buttons_mobile_fixed, +body .o_statusbar_buttons_mobile_fixed, +html body .o_statusbar_buttons_mobile_fixed { + position: fixed !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + z-index: 99999 !important; // 比主题的9999更高,确保在最上层 + // 强制设置背景色,防止被主题覆盖 + background-color: #ffffff !important; + background: #ffffff !important; + border-top: 1px solid $border-color !important; + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1) !important; + padding: map-get($spacers, 2) map-get($spacers, 3) !important; // 为底部悬浮栏预留空间,避免内容被遮挡(支持 iOS safe area) - padding-bottom: calc(#{map-get($spacers, 2)} + env(safe-area-inset-bottom, 0px)); + padding-bottom: calc(#{map-get($spacers, 2)} + env(safe-area-inset-bottom, 0px)) !important; + // iOS Safari 修复:确保元素可交互 + -webkit-transform: translateZ(0) !important; + transform: translateZ(0) !important; + // iOS Safari 修复:防止滚动时fixed定位失效 + -webkit-backface-visibility: hidden !important; + backface-visibility: hidden !important; + // 强制显示属性 + display: block !important; + visibility: visible !important; + opacity: 1 !important; + // 确保元素不会被其他样式隐藏 + overflow: visible !important; + clip: auto !important; + clip-path: none !important; +} .o_statusbar_buttons { - max-width: 100%; - width: 100%; + max-width: 100% !important; + width: 100% !important; + // iOS Safari 修复:确保容器可交互 + pointer-events: auto !important; + -webkit-transform: translateZ(0) !important; + transform: translateZ(0) !important; + // 强制显示 + display: flex !important; + visibility: visible !important; + opacity: 1 !important; > * { - flex: 1; - min-width: 0; + flex: 1 !important; + min-width: 0 !important; + // iOS Safari 修复:确保子元素可交互 + pointer-events: auto !important; + // 强制显示 + display: block !important; + visibility: visible !important; + opacity: 1 !important; - > button { - width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + > button, + > a, + > .btn { + width: 100% !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; // 确保按钮有足够的点击区域 - min-height: 44px; + min-height: 44px !important; + // iOS Safari 修复:确保按钮可点击 + cursor: pointer !important; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1) !important; + touch-action: manipulation !important; + // iOS Safari 修复:防止文本选择影响点击 + -webkit-user-select: none !important; + user-select: none !important; + -webkit-touch-callout: none !important; + // iOS Safari 修复:确保按钮在层级之上 + position: relative !important; + z-index: 1 !important; + // iOS Safari 修复:确保pointer-events正确 + pointer-events: auto !important; + // iOS Safari 修复:防止transform影响点击 + -webkit-transform: translateZ(0) !important; + transform: translateZ(0) !important; + // 强制显示按钮 + display: inline-block !important; + visibility: visible !important; + opacity: 1 !important; + // 确保按钮背景和文字可见,防止被主题覆盖 + background-color: #007bff !important; + background: #007bff !important; + color: #ffffff !important; + // 确保字体大小可见 + font-size: 14px !important; + font-weight: 500 !important; + // 确保有边框和内边距 + border: 1px solid #0056b3 !important; + border-radius: 4px !important; + padding: 8px 16px !important; + text-align: center !important; + // 防止文字被设置为透明 + text-indent: 0 !important; + } + + // 特殊处理不同类型的按钮,确保都有可见的样式 + > button.btn-primary, + > a.btn-primary, + > .btn-primary { + background-color: #007bff !important; + background: #007bff !important; + color: #ffffff !important; + border-color: #0056b3 !important; + } + + > button.btn-secondary, + > a.btn-secondary, + > .btn-secondary { + background-color: #6c757d !important; + background: #6c757d !important; + color: #ffffff !important; + border-color: #545b62 !important; + } + + > button.btn-success, + > a.btn-success, + > .btn-success { + background-color: #28a745 !important; + background: #28a745 !important; + color: #ffffff !important; + border-color: #1e7e34 !important; + } + + > button.btn-danger, + > a.btn-danger, + > .btn-danger { + background-color: #dc3545 !important; + background: #dc3545 !important; + color: #ffffff !important; + border-color: #c82333 !important; + } + + > button.btn-warning, + > a.btn-warning, + > .btn-warning { + background-color: #ffc107 !important; + background: #ffc107 !important; + color: #212529 !important; + border-color: #e0a800 !important; + } + + > button.btn-info, + > a.btn-info, + > .btn-info { + background-color: #17a2b8 !important; + background: #17a2b8 !important; + color: #ffffff !important; + border-color: #117a8b !important; } } } -} -// 桌面端隐藏移动端悬浮栏 + +// 桌面端隐藏移动端悬浮栏(但保留触摸设备的显示) @include media-breakpoint-up(md) { - .o_statusbar_buttons_mobile_fixed { - display: none !important; + .o_statusbar_buttons_mobile_fixed, + body .o_statusbar_buttons_mobile_fixed, + html body .o_statusbar_buttons_mobile_fixed { + // 只在非触摸设备上隐藏(桌面浏览器) + @media (hover: hover) and (pointer: fine) { + display: none !important; + } } } @@ -44,7 +174,7 @@ .o_form_statusbar { // 隐藏状态栏中的按钮容器,但保留字段等其他内容 > .o_statusbar_buttons:not(.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons) { - display: none; + display: none !important; } } @@ -53,3 +183,59 @@ padding-bottom: 80px; // 为底部悬浮栏预留空间 } } + +// iOS Safari 特殊处理:确保在 iOS 设备上按钮可见 +// 使用高优先级选择器,防止被其他主题样式覆盖 +@media (hover: none) and (pointer: coarse) { + .o_statusbar_buttons_mobile_fixed, + body .o_statusbar_buttons_mobile_fixed, + html body .o_statusbar_buttons_mobile_fixed { + display: block !important; + visibility: visible !important; + opacity: 1 !important; + position: fixed !important; + bottom: 0 !important; + z-index: 99999 !important; + } + + .o_form_statusbar > .o_statusbar_buttons:not(.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons), + body .o_form_statusbar > .o_statusbar_buttons:not(.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons) { + display: none !important; + } +} + +// 通用移动设备强制显示规则(防止被其他主题覆盖) +@media (max-width: 767px) { + .o_statusbar_buttons_mobile_fixed, + body .o_statusbar_buttons_mobile_fixed, + html body .o_statusbar_buttons_mobile_fixed { + display: block !important; + visibility: visible !important; + opacity: 1 !important; + position: fixed !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + z-index: 99999 !important; + // 强制背景色,防止被主题覆盖 + background-color: #ffffff !important; + background: #ffffff !important; + } + + // 确保按钮容器内的所有按钮都可见 + .o_statusbar_buttons_mobile_fixed .o_statusbar_buttons, + body .o_statusbar_buttons_mobile_fixed .o_statusbar_buttons, + html body .o_statusbar_buttons_mobile_fixed .o_statusbar_buttons { + button, a, .btn { + // 防止文字被设置为透明 + color: #ffffff !important; + text-indent: 0 !important; + // 确保有背景色 + background-color: #007bff !important; + background: #007bff !important; + // 确保字体可见 + font-size: 14px !important; + font-weight: 500 !important; + } + } +} diff --git a/k_ry_mobile_form/static/src/xml/status_bar_buttons.xml b/k_ry_mobile_form/static/src/xml/status_bar_buttons.xml index f27a576d5..8dc0df7f1 100644 --- a/k_ry_mobile_form/static/src/xml/status_bar_buttons.xml +++ b/k_ry_mobile_form/static/src/xml/status_bar_buttons.xml @@ -4,24 +4,20 @@ - - -
-
- - - -
-
-
- - -
+ +
+
- +
+ +
+ + + +
diff --git a/room/models/room_booking.py b/room/models/room_booking.py index e0185e2c3..9288d7252 100644 --- a/room/models/room_booking.py +++ b/room/models/room_booking.py @@ -92,7 +92,7 @@ class RoomBooking(models.Model): def _onchange_meeting_type_id(self): if self.meeting_type_id: self.room_users_ids = self.meeting_type_id.users_ids - self.host = self.meeting_type_id.host_user_id if self.meeting_type_id.host_user_id else False + self.host = self.meeting_type_id.host_user_id.id if self.meeting_type_id.host_user_id else False else: self.room_users_ids = [(5, 0, 0)] self.host = False diff --git a/soong_workflow/models/soong_workflow_mixin.py b/soong_workflow/models/soong_workflow_mixin.py index 84b9c566d..743981bd0 100644 --- a/soong_workflow/models/soong_workflow_mixin.py +++ b/soong_workflow/models/soong_workflow_mixin.py @@ -559,6 +559,22 @@ class SoongWorkflowMixin(models.AbstractModel): comment_note_id = btn_rec.node_id.comment_id.id else: comment_note_id = False + # 加签人员过滤 + if self.log_ids: + log_node_ids = self.log_ids.mapped('node_id.id') + log_nodes = self.env['soong.workflow.node'].browse(log_node_ids) + all_node_employees = log_nodes.mapped('employee_ids') + else: + current_node = node_list[state_list.index(state)] + all_node_employees = current_node.employee_ids + + log_done_by_users = self.log_ids.mapped('done_by') + log2_done_by_users = self.log_ids2.mapped('done_by') + all_done_by_users = log_done_by_users | log2_done_by_users + done_by_employees = self.env['hr.employee'].search([ + ('user_id', 'in', all_done_by_users.ids) + ]) + all_filter_employees = all_node_employees | done_by_employees return { "name": _("评论"), "type": "ir.actions.act_window", @@ -572,6 +588,7 @@ class SoongWorkflowMixin(models.AbstractModel): 'default_comment_note_id': comment_note_id, "default_code": self.code, "default_res_model": self._name, + "default_filter_personnel_ids": [(6, 0, all_filter_employees.ids)], "default_node_btn_id": btn_rec.id, "default_is_supervising": is_supervising, "default_trans_link_id": self.env['trans.link'].search(trans_domain, limit=1).id if btn_rec.name == '转交' else False, diff --git a/soong_workflow/models/soong_workflow_node.py b/soong_workflow/models/soong_workflow_node.py index 6bb3310e7..8a0915b46 100644 --- a/soong_workflow/models/soong_workflow_node.py +++ b/soong_workflow/models/soong_workflow_node.py @@ -86,15 +86,57 @@ class SoongWorkflowNode(models.Model): is_carbon_copy = fields.Boolean(string="抄送/传阅") is_archive = fields.Boolean(string="归档") is_apostille = fields.Boolean(string="加签", default=False) - employee_ids = fields.Many2many('hr.employee', string='抄送/传阅人员') + department1_ids = fields.Many2many( + "hr.department", + string="抄送部门", + relation="soong_workflow_node_department_rel", + column1="node_id", + column2="department_id" + ) + user_role_ids = fields.Many2many("soong.user.role", string="抄送角色", ) + users_role_group_ids = fields.Many2many('soong.user.role.group', string='抄送角色组') + employee_ids = fields.Many2many( + 'hr.employee', + string='抄送/传阅人员', + relation="soong_workflow_node_employee_total_rel", + column1="node_id", + column2="employee_total_id" + ) is_end = fields.Boolean(string="结束", default=False) is_confirm = fields.Boolean(string="确认", default=False) - department_id = fields.Many2many('hr.department', string='部门转入范围') + department_id = fields.Many2many( + 'hr.department', + string='部门转入范围', + relation="soong_workflow_node_transfer_department_rel", + column1="node_id", + column2="transfer_department_id" + ) comment_id = fields.Many2one('soong.note', string="固定意见") approval_concatenation = fields.Selection([('chuan', '串申'), ('bing', '并申')], string="审批形式", default="bing", help='并:任何人均给意见,最后一个完成的人转交到下一节点.' '串:都会收到消息,但任意一个人转交后,即到下一节点,其他人将不再显示,相当于收回.') + @api.onchange('department1_ids', 'user_role_ids', 'users_role_group_ids', 'employee1_ids') + def _onchange_sync_employee_ids(self): + for record in self: + final_employees = self.env['hr.employee'] + role_group_roles = record.users_role_group_ids.mapped('user_role_ids') + all_target_roles = record.user_role_ids | role_group_roles + if all_target_roles: + role_employees = self.env['hr.employee'].search([ + '|', + ('user_role_id', 'in', all_target_roles.ids), + ('user_role_ids', 'in', all_target_roles.ids) + ]) + final_employees |= role_employees + + if record.department1_ids: + dept_employees = self.env['hr.employee'].search([ + ('department_id', 'in', record.department1_ids.ids) + ]) + final_employees |= dept_employees + record.employee_ids = final_employees if final_employees else [(5, 0, 0)] + @api.onchange("is_apostille") def onchange_is_apostille(self): for record in self: diff --git a/soong_workflow/models/soong_workflow_record.py b/soong_workflow/models/soong_workflow_record.py index 900f18379..d526bcf44 100644 --- a/soong_workflow/models/soong_workflow_record.py +++ b/soong_workflow/models/soong_workflow_record.py @@ -543,19 +543,20 @@ class SoongWorkflowRecord(models.Model): def reminders_record(self): record_id = self.env[self.res_model].search([('id', '=', self.res_id)]) menu_id = self.env['ir.ui.menu'].search([('name', '=', '我的流程')]) + target_url = f'https://oa.thtzjt.com/web#aciton=4299&model=soong.workflow.record&view_type=list&cids=1-2-3-4&menu_id={menu_id.id}' for rec in record_id.log_ids: if rec.approval_state in ['0', '1']: - url=f'https://oa.thtzjt.com/web#aciton=4299&model=soong.workflow.record&view_type=list&cids=1-2-3-4&menu_id={menu_id.id}' + url = self.sudo().env["wecom.apps"].generate_oauth_login_url(target_url, rec.done_by.id) description = "标题: {}\n".format(record_id.name) + self.env.user.name + ' ' + str(fields.Datetime.now() + timedelta(hours=8)) - self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', user_id=rec.done_by.employee_id.wecom_userid, + self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', user_id=rec.done_by.employee_ids.wecom_userid, title=record_id._description + '催办传阅/抄送,请尽快处理', description=description, url=url, btntxt="查看详情") for rec in record_id.log_ids2: if rec.approval_state == '0': - url=f'https://oa.thtzjt.com/web#aciton=4299&model=soong.workflow.record&view_type=list&cids=1-2-3-4&menu_id={menu_id.id}' + url = self.sudo().env["wecom.apps"].generate_oauth_login_url(target_url, rec.done_by.id) description = "标题: {}\n".format(record_id.name) + self.env.user.name + ' ' + str(fields.Datetime.now() + timedelta(hours=8)) - self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', user_id=rec.done_by.employee_id.wecom_userid, + self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', user_id=rec.done_by.employee_ids.wecom_userid, title=record_id._description + '催办传阅/抄送,请尽快查看', description=description, url=url, btntxt="查看详情") diff --git a/soong_workflow/views/soong_workflow_node_views.xml b/soong_workflow/views/soong_workflow_node_views.xml index a61f21220..02263bf35 100644 --- a/soong_workflow/views/soong_workflow_node_views.xml +++ b/soong_workflow/views/soong_workflow_node_views.xml @@ -37,6 +37,12 @@ + + + diff --git a/soong_workflow/views/soong_workflow_record.xml b/soong_workflow/views/soong_workflow_record.xml index 2f82940bd..bb8276e97 100644 --- a/soong_workflow/views/soong_workflow_record.xml +++ b/soong_workflow/views/soong_workflow_record.xml @@ -85,7 +85,7 @@ soong.workflow.record
- + @@ -58,7 +59,7 @@ - + @@ -93,8 +94,8 @@ - - + + @@ -109,7 +110,7 @@ - +