优化移动端界面,新增退单管理功能,添加相关API接口,更新权限设置,调整布局以支持响应式设计,提升用户体验。
This commit is contained in:
328
src/utils/mobile.js
Normal file
328
src/utils/mobile.js
Normal file
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* 移动端交互优化工具
|
||||
*/
|
||||
|
||||
// 检测移动设备
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user