优化移动端界面,新增退单管理功能,添加相关API接口,更新权限设置,调整布局以支持响应式设计,提升用户体验。

This commit is contained in:
zyh
2025-08-27 21:20:05 +08:00
parent 4060a1c6e9
commit 7f4c2ca831
15 changed files with 3595 additions and 198 deletions

328
src/utils/mobile.js Normal file
View 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
}