苹果按钮

This commit is contained in:
pengyuthon
2026-03-20 12:54:57 +08:00
parent a672f54ce5
commit f12d1f1562
7 changed files with 865 additions and 130 deletions
@@ -2,8 +2,408 @@
import { StatusBarButtons } from "@web/views/form/status_bar_buttons/status_bar_buttons";
import { patch } from "@web/core/utils/patch";
import { onMounted, onWillUnmount, useEffect, useRef } 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() {
const mobileBarRef = useRef("mobileBarRoot");
let restoreMobileBarPlacement = null;
// 将移动端底部栏挂到表单根节点,与 .o_form_view_container 同级(卸载前移回,便于 OWL 正确销毁)
onMounted(() => {
const mobile = mobileBarRef.el;
if (!mobile) {
return;
}
const formRoot = mobile.closest(".o_form_view");
if (!formRoot || mobile.parentNode === formRoot) {
return;
}
const origParent = mobile.parentNode;
const origNext = mobile.nextSibling;
formRoot.appendChild(mobile);
restoreMobileBarPlacement = () => {
if (origParent && mobile.parentNode === formRoot) {
origParent.insertBefore(mobile, origNext);
}
};
});
onWillUnmount(() => {
restoreMobileBarPlacement?.();
});
// 如果 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, 200);
// 监听窗口大小变化
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);
}
});
}
}
@@ -0,0 +1,9 @@
/** @odoo-module **/
import { StatusBarButtons } from "@web/views/form/status_bar_buttons/status_bar_buttons";
import { patch } from "@web/core/utils/patch";
// 覆盖 StatusBarButtons 组件的模板
patch(StatusBarButtons, {
template: "k_ry_mobile_form.StatusBarButtons",
});
@@ -1,44 +1,25 @@
// Mobile: 底部悬浮栏样式
.o_statusbar_buttons_mobile_fixed {
position: fixed !important; // iOS 必须使用 !important
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
z-index: 9999 !important; // 使用更高的 z-index
background-color: $o-view-background-color !important;
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);
// 为底部悬浮栏预留空间,避免内容被遮挡(支持 iOS safe area)
padding-bottom: calc(#{map-get($spacers, 2)} + env(safe-area-inset-bottom, 0px));
// iOS 优化:强制 GPU 加速和可见性
-webkit-transform: translateZ(0);
transform: translateZ(0);
-webkit-backface-visibility: visible;
backface-visibility: visible;
-webkit-perspective: 1000;
perspective: 1000;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
// 确保容器有明确的高度和背景
min-height: 60px;
background: $o-view-background-color;
.o_statusbar_buttons {
max-width: 100%;
width: 100%;
display: flex !important; // iOS 需要强制 flex 显示
flex-direction: row !important;
align-items: center !important;
justify-content: center !important;
> * {
flex: 1;
min-width: 0;
display: block !important; // 确保子元素显示
> button {
width: 100%;
white-space: nowrap;
@@ -46,22 +27,6 @@
text-overflow: ellipsis;
// 确保按钮有足够的点击区域
min-height: 44px;
// iOS 优化:确保按钮可见且可点击
opacity: 1 !important;
visibility: visible !important;
display: inline-block !important;
-webkit-appearance: none;
appearance: none;
// 确保按钮背景和边框可见
background-color: inherit;
position: relative;
z-index: 1;
// 强制文本显示
color: inherit;
font-size: inherit;
}
}
}
@@ -82,8 +47,8 @@
display: none;
}
}
// 为 form view 加底部 padding,避免内容被底部悬浮栏遮挡
// 为 form view 添加底部 padding,避免内容被底部悬浮栏遮挡
.o_form_view {
padding-bottom: 80px; // 为底部悬浮栏预留空间
}
@@ -91,14 +56,3 @@
position: relative !important;
}
}
// iOS 专用调试样式 - 如果还是看不到,启用这个来诊断
// .o_statusbar_buttons_mobile_fixed {
// background-color: rgba(255, 0, 0, 0.3) !important;
// border: 2px solid red !important;
// }
// .o_statusbar_buttons_mobile_fixed button {
// background-color: rgba(0, 255, 0, 0.3) !important;
// border: 1px solid green !important;
// color: black !important;
// }
@@ -1,58 +0,0 @@
// 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);
// 为底部悬浮栏预留空间,避免内容被遮挡(支持 iOS safe area)
padding-bottom: calc(#{map-get($spacers, 2)} + env(safe-area-inset-bottom, 0px));
.o_statusbar_buttons {
max-width: 100%;
width: 100%;
> * {
flex: 1;
min-width: 0;
> button {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
// 确保按钮有足够的点击区域
min-height: 44px;
}
}
}
}
// 桌面端隐藏移动端悬浮栏
@include media-breakpoint-up(md) {
.o_statusbar_buttons_mobile_fixed {
display: none !important;
}
}
// 移动端隐藏原始状态栏中的按钮(避免重复显示)
@include media-breakpoint-down(md) {
.o_form_statusbar {
// 隐藏状态栏中的按钮容器,但保留字段等其他内容
> .o_statusbar_buttons:not(.o_statusbar_buttons_mobile_fixed .o_statusbar_buttons) {
display: none;
}
}
// 为 form view 添加底部 padding,避免内容被底部悬浮栏遮挡
.o_form_view {
padding-bottom: 80px; // 为底部悬浮栏预留空间
}
.o_content .o_form_statusbar {
position: relative !important;
}
}
@@ -4,24 +4,21 @@
<!-- 覆盖 web.StatusBarButtons 模板,实现移动端底部悬浮栏 -->
<t t-name="k_ry_mobile_form.StatusBarButtons">
<t t-set="slots" t-value="visibleSlotNames" />
<t t-if="env.isSmall">
<!-- Mobile: 底部悬浮栏 (添加 iOS 优化类名) -->
<div class="o_statusbar_buttons_mobile_fixed ios-optimized">
<div class="o_statusbar_buttons d-flex align-items-center justify-content-center gap-2">
<t t-foreach="slots" t-as="slot" t-key="slot">
<t t-slot="{{ slot }}" />
</t>
</div>
</div>
</t>
<t t-else="">
<!-- Desktop: 正常显示 -->
<div class="o_statusbar_buttons d-flex align-items-center align-content-around flex-wrap gap-1">
<!-- 移动端:底部悬浮栏(始终渲染,通过CSS控制显示) -->
<!-- 挂载后由 JS 移到 .o_form_view 下,与 .o_form_view_container 同级,避免被中间层 overflow 裁剪 -->
<div class="o_statusbar_buttons_mobile_fixed" t-ref="mobileBarRoot" t-att-style="env.isSmall ? '' : 'display: none;'">
<div class="o_statusbar_buttons d-flex align-items-center justify-content-center gap-2">
<t t-foreach="slots" t-as="slot" t-key="slot">
<t t-slot="{{ slot }}" />
</t>
</div>
</t>
</div>
<!-- 桌面端:正常显示 -->
<div class="o_statusbar_buttons d-flex align-items-center align-content-around flex-wrap gap-1" t-att-style="env.isSmall ? 'display: none;' : ''">
<t t-foreach="slots" t-as="slot" t-key="slot">
<t t-slot="{{ slot }}" />
</t>
</div>
</t>
</templates>
+409
View File
@@ -0,0 +1,409 @@
/** @odoo-module **/
import { StatusBarButtons } from "@web/views/form/status_bar_buttons/status_bar_buttons";
import { patch } from "@web/core/utils/patch";
import { onMounted, onWillUnmount, useEffect, useRef } from "@odoo/owl";
// 检测是否为移动设备(包括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() {
const mobileBarRef = useRef("mobileBarRoot");
let restoreMobileBarPlacement = null;
// 将移动端底部栏挂到表单根节点,与 .o_form_view_container 同级(卸载前移回,便于 OWL 正确销毁)
onMounted(() => {
const mobile = mobileBarRef.el;
if (!mobile) {
return;
}
const formRoot = mobile.closest(".o_form_view");
if (!formRoot || mobile.parentNode === formRoot) {
return;
}
const origParent = mobile.parentNode;
const origNext = mobile.nextSibling;
formRoot.appendChild(mobile);
restoreMobileBarPlacement = () => {
if (origParent && mobile.parentNode === formRoot) {
origParent.insertBefore(mobile, origNext);
}
};
});
onWillUnmount(() => {
restoreMobileBarPlacement?.();
});
// 如果 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, 200);
// 监听窗口大小变化
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);
}
});
}
}
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!-- 覆盖 web.StatusBarButtons 模板,实现移动端底部悬浮栏 -->
<t t-name="k_ry_mobile_form.StatusBarButtons">
<t t-set="slots" t-value="visibleSlotNames" />
<!-- 移动端:底部悬浮栏(始终渲染,通过CSS控制显示) -->
<!-- 挂载后由 JS 移到 .o_form_view 下,与 .o_form_view_container 同级,避免被中间层 overflow 裁剪 -->
<div class="o_statusbar_buttons_mobile_fixed" t-ref="mobileBarRoot" t-att-style="env.isSmall ? '' : 'display: none;'">
<div class="o_statusbar_buttons d-flex align-items-center justify-content-center gap-2">
<t t-foreach="slots" t-as="slot" t-key="slot">
<t t-slot="{{ slot }}" />
</t>
</div>
</div>
<!-- 桌面端:正常显示 -->
<div class="o_statusbar_buttons d-flex align-items-center align-content-around flex-wrap gap-1" t-att-style="env.isSmall ? 'display: none;' : ''">
<t t-foreach="slots" t-as="slot" t-key="slot">
<t t-slot="{{ slot }}" />
</t>
</div>
</t>
</templates>