329 lines
7.7 KiB
JavaScript
329 lines
7.7 KiB
JavaScript
/**
|
||
* 移动端交互优化工具
|
||
*/
|
||
|
||
// 检测移动设备
|
||
export const isMobileDevice = () => {
|
||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||
}
|
||
|
||
// 检测触摸设备
|
||
export const isTouchDevice = () => {
|
||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0
|
||
}
|
||
|
||
// 检测设备方向
|
||
export const getOrientation = () => {
|
||
return window.innerWidth > window.innerHeight ? 'landscape' : 'portrait'
|
||
}
|
||
|
||
// 监听方向变化
|
||
export const onOrientationChange = (callback) => {
|
||
let orientation = getOrientation()
|
||
|
||
const handleResize = () => {
|
||
const newOrientation = getOrientation()
|
||
if (newOrientation !== orientation) {
|
||
orientation = newOrientation
|
||
callback(newOrientation)
|
||
}
|
||
}
|
||
|
||
window.addEventListener('resize', handleResize)
|
||
|
||
return () => {
|
||
window.removeEventListener('resize', handleResize)
|
||
}
|
||
}
|
||
|
||
// 防止iOS缩放
|
||
export const preventIOSZoom = () => {
|
||
let lastTouchEnd = 0
|
||
|
||
const preventZoom = (e) => {
|
||
const now = Date.now()
|
||
if (now - lastTouchEnd <= 300) {
|
||
e.preventDefault()
|
||
}
|
||
lastTouchEnd = now
|
||
}
|
||
|
||
document.addEventListener('touchend', preventZoom, { passive: false })
|
||
|
||
return () => {
|
||
document.removeEventListener('touchend', preventZoom)
|
||
}
|
||
}
|
||
|
||
// 添加触摸反馈
|
||
export const addTouchFeedback = (element, options = {}) => {
|
||
const {
|
||
className = 'touch-feedback',
|
||
duration = 150,
|
||
scale = 0.98
|
||
} = options
|
||
|
||
if (!element) return
|
||
|
||
const style = document.createElement('style')
|
||
style.textContent = `
|
||
.${className} {
|
||
transform: scale(${scale});
|
||
transition: transform ${duration}ms ease;
|
||
}
|
||
`
|
||
document.head.appendChild(style)
|
||
|
||
const addFeedback = () => {
|
||
element.classList.add(className)
|
||
setTimeout(() => {
|
||
element.classList.remove(className)
|
||
}, duration)
|
||
}
|
||
|
||
element.addEventListener('touchstart', addFeedback, { passive: true })
|
||
|
||
return () => {
|
||
element.removeEventListener('touchstart', addFeedback)
|
||
if (style.parentNode) {
|
||
style.parentNode.removeChild(style)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 滚动到顶部
|
||
export const scrollToTop = (smooth = true) => {
|
||
window.scrollTo({
|
||
top: 0,
|
||
behavior: smooth ? 'smooth' : 'auto'
|
||
})
|
||
}
|
||
|
||
// 滚动到元素
|
||
export const scrollToElement = (element, options = {}) => {
|
||
const {
|
||
behavior = 'smooth',
|
||
block = 'start',
|
||
inline = 'nearest',
|
||
offset = 0
|
||
} = options
|
||
|
||
if (!element) return
|
||
|
||
const elementTop = element.offsetTop + offset
|
||
|
||
window.scrollTo({
|
||
top: elementTop,
|
||
behavior
|
||
})
|
||
}
|
||
|
||
// 获取安全区域
|
||
export const getSafeArea = () => {
|
||
const style = getComputedStyle(document.documentElement)
|
||
|
||
return {
|
||
top: parseInt(style.getPropertyValue('env(safe-area-inset-top)')) || 0,
|
||
right: parseInt(style.getPropertyValue('env(safe-area-inset-right)')) || 0,
|
||
bottom: parseInt(style.getPropertyValue('env(safe-area-inset-bottom)')) || 0,
|
||
left: parseInt(style.getPropertyValue('env(safe-area-inset-left)')) || 0
|
||
}
|
||
}
|
||
|
||
// 处理键盘弹起
|
||
export const handleVirtualKeyboard = () => {
|
||
let initialViewportHeight = window.innerHeight
|
||
|
||
const handleResize = () => {
|
||
const currentHeight = window.innerHeight
|
||
const heightDifference = initialViewportHeight - currentHeight
|
||
|
||
// 键盘弹起时的处理(高度差超过150px认为是键盘弹起)
|
||
if (heightDifference > 150) {
|
||
document.body.classList.add('keyboard-open')
|
||
document.body.style.height = `${currentHeight}px`
|
||
} else {
|
||
document.body.classList.remove('keyboard-open')
|
||
document.body.style.height = ''
|
||
}
|
||
}
|
||
|
||
window.addEventListener('resize', handleResize)
|
||
|
||
return () => {
|
||
window.removeEventListener('resize', handleResize)
|
||
document.body.classList.remove('keyboard-open')
|
||
document.body.style.height = ''
|
||
}
|
||
}
|
||
|
||
// 防抖函数
|
||
export const debounce = (func, wait) => {
|
||
let timeout
|
||
return function executedFunction(...args) {
|
||
const later = () => {
|
||
clearTimeout(timeout)
|
||
func(...args)
|
||
}
|
||
clearTimeout(timeout)
|
||
timeout = setTimeout(later, wait)
|
||
}
|
||
}
|
||
|
||
// 节流函数
|
||
export const throttle = (func, limit) => {
|
||
let inThrottle
|
||
return function(...args) {
|
||
if (!inThrottle) {
|
||
func.apply(this, args)
|
||
inThrottle = true
|
||
setTimeout(() => inThrottle = false, limit)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 复制到剪贴板
|
||
export const copyToClipboard = async (text) => {
|
||
try {
|
||
if (navigator.clipboard && window.isSecureContext) {
|
||
await navigator.clipboard.writeText(text)
|
||
return true
|
||
} else {
|
||
// 降级方案
|
||
const textArea = document.createElement('textarea')
|
||
textArea.value = text
|
||
textArea.style.position = 'fixed'
|
||
textArea.style.left = '-999999px'
|
||
textArea.style.top = '-999999px'
|
||
document.body.appendChild(textArea)
|
||
textArea.focus()
|
||
textArea.select()
|
||
|
||
try {
|
||
document.execCommand('copy')
|
||
return true
|
||
} catch (err) {
|
||
console.error('复制失败:', err)
|
||
return false
|
||
} finally {
|
||
textArea.remove()
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('复制失败:', err)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 震动反馈
|
||
export const vibrate = (pattern = 50) => {
|
||
if ('vibrate' in navigator) {
|
||
navigator.vibrate(pattern)
|
||
}
|
||
}
|
||
|
||
// 全屏显示
|
||
export const requestFullscreen = (element = document.documentElement) => {
|
||
if (element.requestFullscreen) {
|
||
return element.requestFullscreen()
|
||
} else if (element.webkitRequestFullscreen) {
|
||
return element.webkitRequestFullscreen()
|
||
} else if (element.msRequestFullscreen) {
|
||
return element.msRequestFullscreen()
|
||
}
|
||
}
|
||
|
||
// 退出全屏
|
||
export const exitFullscreen = () => {
|
||
if (document.exitFullscreen) {
|
||
return document.exitFullscreen()
|
||
} else if (document.webkitExitFullscreen) {
|
||
return document.webkitExitFullscreen()
|
||
} else if (document.msExitFullscreen) {
|
||
return document.msExitFullscreen()
|
||
}
|
||
}
|
||
|
||
// 检测网络状态
|
||
export const getNetworkStatus = () => {
|
||
if ('connection' in navigator) {
|
||
const connection = navigator.connection
|
||
return {
|
||
online: navigator.onLine,
|
||
effectiveType: connection.effectiveType,
|
||
downlink: connection.downlink,
|
||
rtt: connection.rtt,
|
||
saveData: connection.saveData
|
||
}
|
||
}
|
||
|
||
return {
|
||
online: navigator.onLine
|
||
}
|
||
}
|
||
|
||
// 监听网络变化
|
||
export const onNetworkChange = (callback) => {
|
||
const handleOnline = () => callback({ online: true })
|
||
const handleOffline = () => callback({ online: false })
|
||
|
||
window.addEventListener('online', handleOnline)
|
||
window.addEventListener('offline', handleOffline)
|
||
|
||
return () => {
|
||
window.removeEventListener('online', handleOnline)
|
||
window.removeEventListener('offline', handleOffline)
|
||
}
|
||
}
|
||
|
||
// PWA安装提示
|
||
export const handlePWAInstall = () => {
|
||
let deferredPrompt
|
||
|
||
const beforeInstallPrompt = (e) => {
|
||
e.preventDefault()
|
||
deferredPrompt = e
|
||
}
|
||
|
||
const showInstallPrompt = async () => {
|
||
if (deferredPrompt) {
|
||
deferredPrompt.prompt()
|
||
const { outcome } = await deferredPrompt.userChoice
|
||
deferredPrompt = null
|
||
return outcome === 'accepted'
|
||
}
|
||
return false
|
||
}
|
||
|
||
window.addEventListener('beforeinstallprompt', beforeInstallPrompt)
|
||
|
||
return {
|
||
showInstallPrompt,
|
||
cleanup: () => {
|
||
window.removeEventListener('beforeinstallprompt', beforeInstallPrompt)
|
||
}
|
||
}
|
||
}
|
||
|
||
export default {
|
||
isMobileDevice,
|
||
isTouchDevice,
|
||
getOrientation,
|
||
onOrientationChange,
|
||
preventIOSZoom,
|
||
addTouchFeedback,
|
||
scrollToTop,
|
||
scrollToElement,
|
||
getSafeArea,
|
||
handleVirtualKeyboard,
|
||
debounce,
|
||
throttle,
|
||
copyToClipboard,
|
||
vibrate,
|
||
requestFullscreen,
|
||
exitFullscreen,
|
||
getNetworkStatus,
|
||
onNetworkChange,
|
||
handlePWAInstall
|
||
}
|