410 lines
23 KiB
JavaScript
410 lines
23 KiB
JavaScript
/** @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);
|
|
}
|
|
});
|
|
}
|
|
}
|