|
@@ -0,0 +1,418 @@
|
|
|
+/**
|
|
|
+ * 统一的缩放管理器
|
|
|
+ * 集成检测和策略应用功能
|
|
|
+ */
|
|
|
+
|
|
|
+export const SCALE_STRATEGIES = {
|
|
|
+ NONE: 'none', // 不处理缩放
|
|
|
+ CSS_VARIABLES: 'css', // 使用CSS变量调整
|
|
|
+ VIEWPORT: 'viewport', // 使用viewport调整
|
|
|
+ HYBRID: 'hybrid' // 混合策略
|
|
|
+}
|
|
|
+
|
|
|
+export class ScaleManager {
|
|
|
+ constructor() {
|
|
|
+ this.callbacks = []
|
|
|
+ this.currentScale = 1
|
|
|
+ this.currentStrategy = SCALE_STRATEGIES.CSS_VARIABLES
|
|
|
+ this.isEnabled = true
|
|
|
+ this.isInitialized = false
|
|
|
+ this.init()
|
|
|
+ }
|
|
|
+
|
|
|
+ init() {
|
|
|
+ if (this.isInitialized) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.selectBestStrategy()
|
|
|
+ this.detectScale()
|
|
|
+ this.setupListeners()
|
|
|
+ this.isInitialized = true
|
|
|
+
|
|
|
+ console.log('ScaleManager initialized')
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检测当前系统缩放比例
|
|
|
+ */
|
|
|
+ detectScale() {
|
|
|
+ const devicePixelRatio = window.devicePixelRatio || 1
|
|
|
+
|
|
|
+ // 使用多种方法检测缩放
|
|
|
+ const methods = {
|
|
|
+ devicePixelRatio: devicePixelRatio,
|
|
|
+ screenRatio: window.screen.width / window.screen.availWidth,
|
|
|
+ visualViewport: window.visualViewport ? window.visualViewport.scale : 1
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建测试元素进行精确检测
|
|
|
+ const testElement = document.createElement('div')
|
|
|
+ testElement.style.cssText = `
|
|
|
+ position: absolute;
|
|
|
+ top: -1000px;
|
|
|
+ left: -1000px;
|
|
|
+ width: 100px;
|
|
|
+ height: 100px;
|
|
|
+ visibility: hidden;
|
|
|
+ pointer-events: none;
|
|
|
+ `
|
|
|
+ document.body.appendChild(testElement)
|
|
|
+
|
|
|
+ const rect = testElement.getBoundingClientRect()
|
|
|
+ document.body.removeChild(testElement)
|
|
|
+
|
|
|
+ // 常见的系统缩放比例
|
|
|
+ const commonScales = [1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 3]
|
|
|
+
|
|
|
+ // 使用设备像素比作为主要检测方法
|
|
|
+ let detectedScale = devicePixelRatio
|
|
|
+
|
|
|
+ // 如果设备像素比不是标准值,尝试找到最接近的标准值
|
|
|
+ if (!commonScales.includes(devicePixelRatio)) {
|
|
|
+ detectedScale = commonScales.reduce((prev, curr) => {
|
|
|
+ return Math.abs(curr - devicePixelRatio) < Math.abs(prev - devicePixelRatio) ? curr : prev
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证检测结果的合理性
|
|
|
+ if (detectedScale < 1 || detectedScale > 3) {
|
|
|
+ detectedScale = 1
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果检测到的缩放与之前不同,触发回调和策略应用
|
|
|
+ if (Math.abs(detectedScale - this.currentScale) > 0.01) {
|
|
|
+ this.currentScale = detectedScale
|
|
|
+ const scaleInfo = this.getScaleInfo()
|
|
|
+ this.applyStrategy(scaleInfo)
|
|
|
+ this.notifyCallbacks(detectedScale)
|
|
|
+ }
|
|
|
+
|
|
|
+ return detectedScale
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据环境选择最佳策略
|
|
|
+ */
|
|
|
+ selectBestStrategy() {
|
|
|
+ const userAgent = navigator.userAgent.toLowerCase()
|
|
|
+ const isChrome = userAgent.includes('chrome')
|
|
|
+ const isEdge = userAgent.includes('edge')
|
|
|
+ const isFirefox = userAgent.includes('firefox')
|
|
|
+ const isSafari = userAgent.includes('safari') && !isChrome
|
|
|
+
|
|
|
+ if (isChrome || isEdge) {
|
|
|
+ this.currentStrategy = SCALE_STRATEGIES.CSS_VARIABLES
|
|
|
+ } else if (isFirefox) {
|
|
|
+ this.currentStrategy = SCALE_STRATEGIES.HYBRID
|
|
|
+ } else if (isSafari) {
|
|
|
+ this.currentStrategy = SCALE_STRATEGIES.VIEWPORT
|
|
|
+ } else {
|
|
|
+ this.currentStrategy = SCALE_STRATEGIES.CSS_VARIABLES
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.currentStrategy
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 应用缩放策略
|
|
|
+ */
|
|
|
+ applyStrategy(scaleInfo) {
|
|
|
+ if (!this.isEnabled || !scaleInfo.shouldApply) {
|
|
|
+ this.resetStrategy()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (this.currentStrategy) {
|
|
|
+ case SCALE_STRATEGIES.CSS_VARIABLES:
|
|
|
+ this.applyCSSVariableStrategy(scaleInfo)
|
|
|
+ break
|
|
|
+ case SCALE_STRATEGIES.VIEWPORT:
|
|
|
+ this.applyViewportStrategy(scaleInfo)
|
|
|
+ break
|
|
|
+ case SCALE_STRATEGIES.HYBRID:
|
|
|
+ this.applyHybridStrategy(scaleInfo)
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ this.applyCSSVariableStrategy(scaleInfo)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * CSS变量策略
|
|
|
+ */
|
|
|
+ applyCSSVariableStrategy(scaleInfo) {
|
|
|
+ // 确保DOM已准备好
|
|
|
+ if (!document.body) {
|
|
|
+ setTimeout(() => this.applyCSSVariableStrategy(scaleInfo), 50)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ document.body.setAttribute('data-scale', scaleInfo.percentage)
|
|
|
+ document.body.classList.add('system-scaled')
|
|
|
+
|
|
|
+ // 设置CSS变量
|
|
|
+ const root = document.documentElement
|
|
|
+ root.style.setProperty('--detected-scale', scaleInfo.detected)
|
|
|
+ root.style.setProperty('--counter-scale', scaleInfo.counter)
|
|
|
+ root.style.setProperty('--scale-percentage', `${scaleInfo.percentage}%`)
|
|
|
+
|
|
|
+ console.log(`应用CSS变量策略: ${scaleInfo.percentage}%`)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Viewport策略
|
|
|
+ */
|
|
|
+ applyViewportStrategy(scaleInfo) {
|
|
|
+ // 确保DOM已准备好
|
|
|
+ if (!document.head || !document.body) {
|
|
|
+ setTimeout(() => this.applyViewportStrategy(scaleInfo), 50)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ let viewport = document.querySelector('meta[name="viewport"]')
|
|
|
+ if (!viewport) {
|
|
|
+ viewport = document.createElement('meta')
|
|
|
+ viewport.name = 'viewport'
|
|
|
+ document.head.appendChild(viewport)
|
|
|
+ }
|
|
|
+
|
|
|
+ const scale = scaleInfo.counter
|
|
|
+ viewport.content = `width=device-width, initial-scale=${scale}, maximum-scale=${scale}, user-scalable=no`
|
|
|
+
|
|
|
+ document.body.setAttribute('data-scale', scaleInfo.percentage)
|
|
|
+ document.body.classList.add('viewport-scaled')
|
|
|
+
|
|
|
+ console.log(`应用Viewport策略: scale=${scale}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 混合策略
|
|
|
+ */
|
|
|
+ applyHybridStrategy(scaleInfo) {
|
|
|
+ this.applyCSSVariableStrategy(scaleInfo)
|
|
|
+
|
|
|
+ // 确保DOM已准备好
|
|
|
+ if (!document.body) {
|
|
|
+ setTimeout(() => this.applyHybridStrategy(scaleInfo), 50)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对于极端缩放比例,添加轻微的transform调整
|
|
|
+ if (scaleInfo.detected >= 2) {
|
|
|
+ // 延迟查找layout元素,确保组件已挂载
|
|
|
+ setTimeout(() => {
|
|
|
+ const layout = document.querySelector('.layout')
|
|
|
+ if (layout) {
|
|
|
+ const adjustScale = 0.95
|
|
|
+ layout.style.transform = `scale(${adjustScale})`
|
|
|
+ layout.style.transformOrigin = 'top center'
|
|
|
+ }
|
|
|
+ }, 100)
|
|
|
+ }
|
|
|
+
|
|
|
+ document.body.classList.add('hybrid-scaled')
|
|
|
+ console.log(`应用混合策略: ${scaleInfo.percentage}%`)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 重置所有策略
|
|
|
+ */
|
|
|
+ resetStrategy() {
|
|
|
+ document.body.classList.remove('system-scaled', 'viewport-scaled', 'hybrid-scaled')
|
|
|
+ document.body.removeAttribute('data-scale')
|
|
|
+
|
|
|
+ const root = document.documentElement
|
|
|
+ root.style.removeProperty('--detected-scale')
|
|
|
+ root.style.removeProperty('--counter-scale')
|
|
|
+ root.style.removeProperty('--scale-percentage')
|
|
|
+
|
|
|
+ let viewport = document.querySelector('meta[name="viewport"]')
|
|
|
+ if (viewport) {
|
|
|
+ viewport.content = 'width=device-width, initial-scale=1.0'
|
|
|
+ }
|
|
|
+
|
|
|
+ const layout = document.querySelector('.layout')
|
|
|
+ if (layout) {
|
|
|
+ layout.style.transform = ''
|
|
|
+ layout.style.transformOrigin = ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置监听器
|
|
|
+ */
|
|
|
+ setupListeners() {
|
|
|
+ // 监听窗口大小变化
|
|
|
+ window.addEventListener('resize', () => {
|
|
|
+ setTimeout(() => this.detectScale(), 100)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 监听设备像素比变化
|
|
|
+ if (window.matchMedia) {
|
|
|
+ const mediaQueries = [
|
|
|
+ '(-webkit-device-pixel-ratio: 1)',
|
|
|
+ '(-webkit-device-pixel-ratio: 1.25)',
|
|
|
+ '(-webkit-device-pixel-ratio: 1.5)',
|
|
|
+ '(-webkit-device-pixel-ratio: 1.75)',
|
|
|
+ '(-webkit-device-pixel-ratio: 2)',
|
|
|
+ '(-webkit-device-pixel-ratio: 2.25)',
|
|
|
+ '(-webkit-device-pixel-ratio: 2.5)',
|
|
|
+ '(-webkit-device-pixel-ratio: 3)'
|
|
|
+ ]
|
|
|
+
|
|
|
+ mediaQueries.forEach(query => {
|
|
|
+ const mq = window.matchMedia(query)
|
|
|
+ mq.addListener(() => {
|
|
|
+ setTimeout(() => this.detectScale(), 100)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加缩放变化回调
|
|
|
+ */
|
|
|
+ onScaleChange(callback) {
|
|
|
+ this.callbacks.push(callback)
|
|
|
+ // 立即调用一次回调
|
|
|
+ callback(this.currentScale)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 移除缩放变化回调
|
|
|
+ */
|
|
|
+ removeScaleChangeListener(callback) {
|
|
|
+ const index = this.callbacks.indexOf(callback)
|
|
|
+ if (index > -1) {
|
|
|
+ this.callbacks.splice(index, 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通知所有回调
|
|
|
+ */
|
|
|
+ notifyCallbacks(scale) {
|
|
|
+ this.callbacks.forEach(callback => {
|
|
|
+ try {
|
|
|
+ callback(scale)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Scale change callback error:', error)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前缩放比例
|
|
|
+ */
|
|
|
+ getCurrentScale() {
|
|
|
+ return this.currentScale
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断是否需要应用反向缩放
|
|
|
+ */
|
|
|
+ shouldApplyCounterScale() {
|
|
|
+ return this.currentScale > 1
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取反向缩放比例
|
|
|
+ */
|
|
|
+ getCounterScale() {
|
|
|
+ return this.shouldApplyCounterScale() ? 1 / this.currentScale : 1
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取缩放信息
|
|
|
+ */
|
|
|
+ getScaleInfo() {
|
|
|
+ return {
|
|
|
+ detected: this.currentScale,
|
|
|
+ counter: this.getCounterScale(),
|
|
|
+ shouldApply: this.shouldApplyCounterScale(),
|
|
|
+ percentage: Math.round(this.currentScale * 100)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 启用/禁用缩放处理
|
|
|
+ */
|
|
|
+ setEnabled(enabled) {
|
|
|
+ this.isEnabled = enabled
|
|
|
+ if (!enabled) {
|
|
|
+ this.resetStrategy()
|
|
|
+ } else {
|
|
|
+ const scaleInfo = this.getScaleInfo()
|
|
|
+ this.applyStrategy(scaleInfo)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置策略
|
|
|
+ */
|
|
|
+ setStrategy(strategy) {
|
|
|
+ if (Object.values(SCALE_STRATEGIES).includes(strategy)) {
|
|
|
+ this.currentStrategy = strategy
|
|
|
+ const scaleInfo = this.getScaleInfo()
|
|
|
+ this.applyStrategy(scaleInfo)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前策略
|
|
|
+ */
|
|
|
+ getCurrentStrategy() {
|
|
|
+ return this.currentStrategy
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 重新应用当前策略(用于页面跳转后恢复状态)
|
|
|
+ */
|
|
|
+ reapplyCurrentStrategy() {
|
|
|
+ if (this.isEnabled && this.shouldApplyCounterScale()) {
|
|
|
+ const scaleInfo = this.getScaleInfo()
|
|
|
+ this.applyStrategy(scaleInfo)
|
|
|
+ console.log('重新应用缩放策略')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 强制刷新缩放状态
|
|
|
+ */
|
|
|
+ refresh() {
|
|
|
+ this.detectScale()
|
|
|
+ this.reapplyCurrentStrategy()
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查并恢复缩放状态(用于页面跳转后)
|
|
|
+ */
|
|
|
+ checkAndRestore() {
|
|
|
+ // 检查DOM状态是否正确
|
|
|
+ const scaleInfo = this.getScaleInfo()
|
|
|
+
|
|
|
+ if (scaleInfo.shouldApply) {
|
|
|
+ const hasDataScale = document.body.hasAttribute('data-scale')
|
|
|
+ const hasScaledClass = document.body.classList.contains('system-scaled') ||
|
|
|
+ document.body.classList.contains('viewport-scaled') ||
|
|
|
+ document.body.classList.contains('hybrid-scaled')
|
|
|
+
|
|
|
+ // 如果DOM状态不正确,重新应用策略
|
|
|
+ if (!hasDataScale || !hasScaledClass) {
|
|
|
+ console.log('检测到缩放状态丢失,正在恢复...')
|
|
|
+ this.applyStrategy(scaleInfo)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 创建全局实例
|
|
|
+export const scaleManager = new ScaleManager()
|
|
|
+
|
|
|
+// 便捷函数
|
|
|
+export const onScaleChange = (callback) => scaleManager.onScaleChange(callback)
|
|
|
+export const getCurrentScale = () => scaleManager.getCurrentScale()
|
|
|
+export const getScaleInfo = () => scaleManager.getScaleInfo()
|
|
|
+export const setScaleStrategy = (strategy) => scaleManager.setStrategy(strategy)
|
|
|
+export const setScaleEnabled = (enabled) => scaleManager.setEnabled(enabled)
|