Elements Plus 跨设备自适应显示问题综合解决方案

文章目录

第一部分:问题深度剖析与诊断方法

1.1 Elements Plus 响应式设计失效问题全面分析

1.1.1 问题现象具体描述

在实际开发中,Elements Plus 组件库在 PC 端和 iPad 等平板设备上常出现以下自适应失效问题:

  1. 布局断裂与溢出问题:在 iPad 竖屏模式(768px 宽度)下,多列布局仍保持 PC 端的多列显示,导致内容挤压、重叠或水平滚动条出现
  2. 字体与间距比例失调:文本字号、行高、组件间距未随屏幕尺寸等比缩放,在 iPad 上显得过大或过小
  3. 交互元素尺寸不适配:按钮、输入框等交互元素在触摸屏设备上尺寸过小,难以精准点击
  4. 导航与菜单组件显示异常:侧边栏、导航菜单在有限宽度下未自动折叠或调整布局
  5. 表格数据展示混乱:数据表格列宽固定,在小屏幕设备上出现内容截断或布局错乱
  6. 模态框与弹出层定位错误:对话框、提示框等弹出组件未根据视口尺寸重新定位和调整大小
  7. 图片与媒体内容未自适应:图片保持固定尺寸,未根据容器宽度等比缩放

1.1.2 根本原因分析

  1. 断点系统不匹配:Elements Plus 默认响应式断点与 iPad 实际屏幕尺寸不匹配

    • 默认 sm 断点为 640px,而 iPad 竖屏宽度为 768px
    • iPad Pro(1024px)处于 mdlg 断点之间,导致样式应用不准确
  2. 视口与像素密度差异:iPad 等设备具有不同的像素密度(Retina 屏为 2x),CSS 像素与物理像素不对等

  3. 触摸与鼠标交互差异 :PC 端为指针设备(鼠标),iPad 为触摸设备,:hover 状态在触摸设备上表现异常

  4. REM 基准值计算差异:根元素字体大小计算方式在不同浏览器和设备上不一致

  5. CSS 媒体查询覆盖不全:部分组件的响应式样式可能被全局样式或自定义样式覆盖

1.2 诊断与测试方法

1.2.1 响应式断点检测工具

javascript 复制代码
// 响应式断点检测组件
import { ref, onMounted, onUnmounted } from 'vue'

export function useBreakpointDetection() {
  const breakpoint = ref('')
  const screenWidth = ref(0)
  const deviceType = ref('')
  const pixelRatio = ref(1)
  
  // Elements Plus 默认断点
  const BREAKPOINTS = {
    'xs': 0,
    'sm': 640,
    'md': 768,
    'lg': 1024,
    'xl': 1280,
    '2xl': 1536
  }
  
  // 设备类型检测
  const detectDeviceType = (width) => {
    if (width < 640) return 'mobile'
    if (width >= 640 && width < 768) return 'small-tablet'
    if (width >= 768 && width < 1024) return 'tablet' // iPad 竖屏
    if (width >= 1024 && width < 1280) return 'tablet-landscape' // iPad 横屏
    if (width >= 1280 && width < 1536) return 'desktop'
    return 'large-desktop'
  }
  
  // 检测当前断点
  const detectBreakpoint = (width) => {
    if (width < BREAKPOINTS.sm) return 'xs'
    if (width >= BREAKPOINTS.sm && width < BREAKPOINTS.md) return 'sm'
    if (width >= BREAKPOINTS.md && width < BREAKPOINTS.lg) return 'md'
    if (width >= BREAKPOINTS.lg && width < BREAKPOINTS.xl) return 'lg'
    if (width >= BREAKPOINTS.xl && width < BREAKPOINTS['2xl']) return 'xl'
    return '2xl'
  }
  
  const updateBreakpoint = () => {
    const width = window.innerWidth
    screenWidth.value = width
    breakpoint.value = detectBreakpoint(width)
    deviceType.value = detectDeviceType(width)
    pixelRatio.value = window.devicePixelRatio || 1
    
    // 调试信息输出
    console.log(`当前断点: ${breakpoint.value}`)
    console.log(`屏幕宽度: ${width}px`)
    console.log(`设备类型: ${deviceType.value}`)
    console.log(`像素密度: ${pixelRatio.value}`)
    console.log(`当前视口: ${document.documentElement.clientWidth}px`)
  }
  
  onMounted(() => {
    updateBreakpoint()
    window.addEventListener('resize', updateBreakpoint)
    
    // 检测触摸设备
    const isTouchDevice = 'ontouchstart' in window || 
      navigator.maxTouchPoints > 0 ||
      navigator.msMaxTouchPoints > 0
    
    console.log(`触摸设备: ${isTouchDevice}`)
    
    // 检测 Safari on iPad
    const isSafariOniPad = /iPad|iPhone|iPod/.test(navigator.userAgent) && 
      !window.MSStream && 
      'ontouchend' in document
    
    console.log(`iPad Safari: ${isSafariOniPad}`)
  })
  
  onUnmounted(() => {
    window.removeEventListener('resize', updateBreakpoint)
  })
  
  return {
    breakpoint,
    screenWidth,
    deviceType,
    pixelRatio
  }
}

// 在 Vue 组件中使用
export default {
  setup() {
    const { breakpoint, screenWidth, deviceType } = useBreakpointDetection()
    
    return {
      breakpoint,
      screenWidth,
      deviceType
    }
  }
}

1.2.2 CSS 覆盖检测脚本

javascript 复制代码
// CSS 样式检测工具
export function analyzeElementStyles(elementSelector) {
  const element = document.querySelector(elementSelector)
  if (!element) return null
  
  const computedStyles = window.getComputedStyle(element)
  const elementStyles = {}
  
  // 获取关键样式属性
  const importantProperties = [
    'width', 'height', 'max-width', 'min-width',
    'padding', 'margin', 'display', 'flex-direction',
    'grid-template-columns', 'font-size', 'line-height'
  ]
  
  importantProperties.forEach(prop => {
    elementStyles[prop] = computedStyles.getPropertyValue(prop)
  })
  
  // 检测媒体查询覆盖
  const mediaQueryOverrides = detectMediaQueryOverrides(elementSelector)
  
  // 检测内联样式
  const inlineStyles = element.getAttribute('style') || ''
  
  // 检测 !important 规则
  const importantRules = detectImportantRules(elementSelector)
  
  return {
    element: elementSelector,
    computedStyles: elementStyles,
    mediaQueryOverrides,
    inlineStyles,
    importantRules,
    classList: Array.from(element.classList)
  }
}

function detectMediaQueryOverrides(elementSelector) {
  const overrides = []
  const sheets = document.styleSheets
  
  for (let i = 0; i < sheets.length; i++) {
    try {
      const rules = sheets[i].cssRules || sheets[i].rules
      for (let j = 0; j < rules.length; j++) {
        const rule = rules[j]
        if (rule.media && rule.cssRules) {
          for (let k = 0; k < rule.cssRules.length; k++) {
            const mediaRule = rule.cssRules[k]
            if (mediaRule.selectorText && 
                mediaRule.selectorText.includes(elementSelector)) {
              overrides.push({
                mediaQuery: rule.media.mediaText,
                cssText: mediaRule.cssText
              })
            }
          }
        }
      }
    } catch (e) {
      // 跨域样式表可能无法访问
      console.warn('无法访问样式表:', e)
    }
  }
  
  return overrides
}

1.2.3 响应式测试工具集

html 复制代码
<!-- 响应式测试工具组件 -->
<template>
  <div class="responsive-debug-tool">
    <div class="debug-info" :style="infoStyle">
      <div>宽度: {{ screenWidth }}px</div>
      <div>断点: {{ breakpoint }}</div>
      <div>设备: {{ deviceType }}</div>
      <div>像素比: {{ pixelRatio }}</div>
      <div>视口: {{ viewportWidth }}px</div>
    </div>
    
    <div class="breakpoint-indicators">
      <div 
        v-for="bp in breakpoints" 
        :key="bp.name"
        :class="['indicator', { active: screenWidth >= bp.min }]"
        :style="{ left: `${(bp.min / 1920) * 100}%` }"
      >
        <span class="label">{{ bp.name }} ({{ bp.min }}px)</span>
      </div>
    </div>
    
    <button @click="toggleGrid" class="grid-toggle">
      {{ showGrid ? '隐藏' : '显示' }}栅格
    </button>
    
    <div v-if="showGrid" class="responsive-grid">
      <div class="grid-column" v-for="n in 12" :key="n"></div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted, onUnmounted } from 'vue'

export default {
  setup() {
    const screenWidth = ref(window.innerWidth)
    const breakpoint = ref('')
    const deviceType = ref('')
    const pixelRatio = ref(window.devicePixelRatio || 1)
    const viewportWidth = ref(document.documentElement.clientWidth)
    const showGrid = ref(false)
    
    const breakpoints = [
      { name: 'xs', min: 0 },
      { name: 'sm', min: 640 },
      { name: 'md', min: 768 },
      { name: 'lg', min: 1024 },
      { name: 'xl', min: 1280 },
      { name: '2xl', min: 1536 }
    ]
    
    const updateViewportInfo = () => {
      screenWidth.value = window.innerWidth
      viewportWidth.value = document.documentElement.clientWidth
      pixelRatio.value = window.devicePixelRatio || 1
      
      // 检测断点
      for (let i = breakpoints.length - 1; i >= 0; i--) {
        if (screenWidth.value >= breakpoints[i].min) {
          breakpoint.value = breakpoints[i].name
          break
        }
      }
      
      // 检测设备类型
      if (screenWidth.value < 768) {
        deviceType.value = '移动设备'
      } else if (screenWidth.value < 1024) {
        deviceType.value = '平板(竖屏)'
      } else if (screenWidth.value < 1280) {
        deviceType.value = '平板(横屏)'
      } else {
        deviceType.value = '桌面设备'
      }
    }
    
    const toggleGrid = () => {
      showGrid.value = !showGrid.value
    }
    
    onMounted(() => {
      updateViewportInfo()
      window.addEventListener('resize', updateViewportInfo)
      window.addEventListener('orientationchange', updateViewportInfo)
    })
    
    onUnmounted(() => {
      window.removeEventListener('resize', updateViewportInfo)
      window.removeEventListener('orientationchange', updateViewportInfo)
    })
    
    const infoStyle = {
      position: 'fixed',
      top: '10px',
      right: '10px',
      background: 'rgba(0,0,0,0.8)',
      color: '#fff',
      padding: '10px',
      borderRadius: '4px',
      fontSize: '12px',
      zIndex: 9999
    }
    
    return {
      screenWidth,
      breakpoint,
      deviceType,
      pixelRatio,
      viewportWidth,
      showGrid,
      breakpoints,
      toggleGrid,
      infoStyle
    }
  }
}
</script>

<style scoped>
.responsive-debug-tool {
  font-family: monospace;
}

.breakpoint-indicators {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: linear-gradient(90deg, #ff6b6b, #ffd93d, #6bcf7f, #4d96ff);
  z-index: 9998;
}

.indicator {
  position: absolute;
  top: -10px;
  transform: translateX(-50%);
}

.indicator .label {
  position: absolute;
  top: 10px;
  white-space: nowrap;
  font-size: 10px;
  color: #666;
  display: none;
}

.indicator.active .label {
  display: block;
}

.indicator::after {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 2px;
  height: 10px;
  background: #333;
}

.grid-toggle {
  position: fixed;
  bottom: 20px;
  right: 20px;
  padding: 8px 16px;
  background: #4d96ff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  z-index: 9999;
}

.responsive-grid {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 9997;
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 16px;
  padding: 0 16px;
  box-sizing: border-box;
}

.grid-column {
  background: rgba(77, 150, 255, 0.1);
  border: 1px dashed rgba(77, 150, 255, 0.3);
}
</style>

第二部分:核心解决方案与实现原理

2.1 方案一:自定义响应式断点系统

2.1.1 原理深度解析

Elements Plus 使用 Tailwind CSS 的断点系统作为响应式设计基础,默认断点配置为:

  • sm: 640px
  • md: 768px
  • lg: 1024px
  • xl: 1280px
  • 2xl: 1536px

问题所在 :iPad 竖屏宽度为 768px,正好落在 md 断点的起始位置。而许多组件在 md 断点的样式并未针对平板设备进行优化,导致显示异常。

解决方案原理:通过扩展和调整断点系统,为 iPad 等设备创建更精细的响应式控制:

  1. 在 768px-1024px 之间添加平板专用断点
  2. 优化现有断点的样式覆盖
  3. 确保断点过渡平滑自然

2.1.2 实现步骤详解

步骤一:创建自定义断点配置文件

javascript 复制代码
// src/styles/breakpoints.js
export const customBreakpoints = {
  // 移动设备断点
  'xs': { min: '0px', max: '479px' },
  'sm': { min: '480px', max: '639px' },
  
  // 小屏平板断点(7-8寸平板)
  'md': { min: '640px', max: '767px' },
  
  // 标准平板断点(iPad 竖屏)
  'lg': { min: '768px', max: '1023px' },
  
  // 大屏平板/小屏桌面(iPad 横屏)
  'xl': { min: '1024px', max: '1279px' },
  
  // 桌面设备
  '2xl': { min: '1280px', max: '1535px' },
  
  // 大屏桌面
  '3xl': { min: '1536px', max: null }
}

// 生成媒体查询工具函数
export const createMediaQuery = (breakpoint, direction = 'up') => {
  const bp = customBreakpoints[breakpoint]
  if (!bp) return ''
  
  if (direction === 'up') {
    return `@media (min-width: ${bp.min})`
  } else if (direction === 'down') {
    if (bp.max) {
      return `@media (max-width: ${bp.max})`
    }
    return ''
  } else if (direction === 'only') {
    if (bp.max) {
      return `@media (min-width: ${bp.min}) and (max-width: ${bp.max})`
    }
    return `@media (min-width: ${bp.min})`
  }
}

// 设备类型检测
export const deviceType = () => {
  const width = window.innerWidth
  if (width <= 479) return 'mobile'
  if (width <= 767) return 'small-tablet'
  if (width <= 1023) return 'tablet' // iPad 竖屏
  if (width <= 1279) return 'tablet-landscape' // iPad 横屏
  if (width <= 1535) return 'desktop'
  return 'large-desktop'
}

步骤二:覆盖 Elements Plus 断点配置

javascript 复制代码
// vue.config.js 或 vite.config.js
module.exports = {
  css: {
    loaderOptions: {
      sass: {
        // 注入自定义断点变量
        additionalData: `
          $--sm: 480px;
          $--md: 640px;
          $--lg: 768px;
          $--xl: 1024px;
          $--2xl: 1280px;
          
          // 响应式工具类
          @mixin respond-to($breakpoint) {
            @if $breakpoint == 'mobile' {
              @media (max-width: 479px) { @content; }
            }
            @else if $breakpoint == 'small-tablet' {
              @media (min-width: 480px) and (max-width: 767px) { @content; }
            }
            @else if $breakpoint == 'tablet' {
              @media (min-width: 768px) and (max-width: 1023px) { @content; }
            }
            @else if $breakpoint == 'tablet-landscape' {
              @media (min-width: 1024px) and (max-width: 1279px) { @content; }
            }
            @else if $breakpoint == 'desktop' {
              @media (min-width: 1280px) and (max-width: 1535px) { @content; }
            }
            @else if $breakpoint == 'large-desktop' {
              @media (min-width: 1536px) { @content; }
            }
          }
        `
      }
    }
  }
}

步骤三:创建响应式混合指令

javascript 复制代码
// src/directives/responsive.js
import { createApp } from 'vue'

const responsiveDirective = {
  mounted(el, binding) {
    const { value } = binding
    const classPrefix = value?.prefix || 'responsive'
    const breakpoints = value?.breakpoints || ['sm', 'md', 'lg', 'xl']
    
    // 创建响应式类监听器
    const updateResponsiveClasses = () => {
      const width = window.innerWidth
      
      // 移除旧的响应式类
      breakpoints.forEach(bp => {
        el.classList.remove(`${classPrefix}-${bp}`)
        el.classList.remove(`${classPrefix}-${bp}-up`)
        el.classList.remove(`${classPrefix}-${bp}-down`)
      })
      
      // 添加新的响应式类
      const breakpointMap = {
        'sm': 480,
        'md': 640,
        'lg': 768,
        'xl': 1024,
        '2xl': 1280
      }
      
      // 检测当前适用的断点
      for (const [bp, minWidth] of Object.entries(breakpointMap).reverse()) {
        if (width >= minWidth) {
          el.classList.add(`${classPrefix}-${bp}`)
          el.classList.add(`${classPrefix}-${bp}-up`)
          break
        }
      }
      
      // 添加向下兼容的类
      for (const [bp, minWidth] of Object.entries(breakpointMap)) {
        if (width < minWidth) {
          el.classList.add(`${classPrefix}-${bp}-down`)
          break
        }
      }
    }
    
    // 初始更新
    updateResponsiveClasses()
    
    // 添加 resize 监听
    const handleResize = () => {
      requestAnimationFrame(updateResponsiveClasses)
    }
    
    window.addEventListener('resize', handleResize)
    
    // 存储清理函数
    el._responsiveCleanup = () => {
      window.removeEventListener('resize', handleResize)
    }
  },
  
  unmounted(el) {
    if (el._responsiveCleanup) {
      el._responsiveCleanup()
    }
  }
}

// 注册指令
export default {
  install(app) {
    app.directive('responsive', responsiveDirective)
  }
}

// 在 main.js 中使用
import responsiveDirective from './directives/responsive'
createApp(App)
  .use(responsiveDirective)
  .mount('#app')

步骤四:创建响应式工具类

scss 复制代码
// src/styles/responsive-utilities.scss
// 响应式显示/隐藏工具类
.responsive-hidden {
  &-xs { @media (max-width: 479px) { display: none !important; } }
  &-sm { @media (min-width: 480px) and (max-width: 639px) { display: none !important; } }
  &-md { @media (min-width: 640px) and (max-width: 767px) { display: none !important; } }
  &-lg { @media (min-width: 768px) and (max-width: 1023px) { display: none !important; } }
  &-xl { @media (min-width: 1024px) and (max-width: 1279px) { display: none !important; } }
  &-2xl { @media (min-width: 1280px) { display: none !important; } }
  
  // 向上隐藏
  &-sm-up { @media (min-width: 480px) { display: none !important; } }
  &-md-up { @media (min-width: 640px) { display: none !important; } }
  &-lg-up { @media (min-width: 768px) { display: none !important; } }
  
  // 向下隐藏
  &-md-down { @media (max-width: 767px) { display: none !important; } }
  &-lg-down { @media (max-width: 1023px) { display: none !important; } }
  &-xl-down { @media (max-width: 1279px) { display: none !important; } }
}

// 响应式间距工具
$spacing-scale: 0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10;

@each $size in $spacing-scale {
  $index: index($spacing-scale, $size) - 1;
  
  // 移动端优先的间距
  .p-#{$index} { padding: #{$size}rem; }
  .m-#{$index} { margin: #{$size}rem; }
  
  // 响应式间距覆盖
  @media (min-width: 768px) {
    .lg\:p-#{$index} { padding: #{$size * 1.2}rem !important; }
    .lg\:m-#{$index} { margin: #{$size * 1.2}rem !important; }
  }
  
  @media (min-width: 1024px) {
    .xl\:p-#{$index} { padding: #{$size * 1.5}rem !important; }
    .xl\:m-#{$index} { margin: #{$size * 1.5}rem !important; }
  }
}

// 响应式字体大小
$font-scale: 0.75, 0.875, 1, 1.125, 1.25, 1.5, 1.875, 2.25, 3, 3.75, 4.5;

@for $i from 1 through length($font-scale) {
  $size: nth($font-scale, $i);
  
  .text-#{$i} { font-size: #{$size}rem; }
  
  // 平板设备字体调整
  @media (min-width: 768px) and (max-width: 1023px) {
    .lg\:text-#{$i} { font-size: #{$size * 0.95}rem !important; }
  }
  
  // 桌面设备字体调整
  @media (min-width: 1024px) {
    .xl\:text-#{$i} { font-size: #{$size * 1.1}rem !important; }
  }
}

// 响应式 Flex/Grid 布局
.responsive-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  
  @media (min-width: 640px) {
    grid-template-columns: repeat(2, 1fr);
  }
  
  @media (min-width: 768px) {
    grid-template-columns: repeat(3, 1fr);
    gap: 1.25rem;
  }
  
  @media (min-width: 1024px) {
    grid-template-columns: repeat(4, 1fr);
    gap: 1.5rem;
  }
  
  @media (min-width: 1280px) {
    grid-template-columns: repeat(6, 1fr);
    gap: 2rem;
  }
}

// 触摸设备优化
.touch-device {
  @media (hover: none) and (pointer: coarse) {
    // 增大触摸目标
    button, 
    .el-button,
    [role="button"] {
      min-height: 44px !important;
      min-width: 44px !important;
      padding: 12px 20px !important;
    }
    
    // 输入框优化
    input,
    .el-input__inner,
    textarea {
      font-size: 16px !important; // 防止 iOS 缩放
      min-height: 44px !important;
    }
    
    // 移除 hover 效果
    *:hover {
      background-color: inherit !important;
      color: inherit !important;
    }
  }
}

步骤五:创建响应式布局组件

vue 复制代码
<!-- src/components/layout/ResponsiveContainer.vue -->
<template>
  <div 
    :class="[
      'responsive-container',
      {
        'responsive-container--fluid': fluid,
        'responsive-container--full-height': fullHeight,
        [`responsive-container--${breakpoint}`]: true
      }
    ]"
    :style="containerStyle"
  >
    <slot />
  </div>
</template>

<script>
import { ref, computed, onMounted, onUnmounted } from 'vue'

export default {
  name: 'ResponsiveContainer',
  
  props: {
    fluid: {
      type: Boolean,
      default: false
    },
    fullHeight: {
      type: Boolean,
      default: false
    },
    maxWidth: {
      type: String,
      default: null
    },
    gutter: {
      type: [String, Number],
      default: '16px'
    },
    adaptivePadding: {
      type: Boolean,
      default: true
    }
  },
  
  setup(props) {
    const breakpoint = ref('')
    const screenWidth = ref(0)
    
    // 响应式断点检测
    const detectBreakpoint = (width) => {
      if (width < 480) return 'xs'
      if (width < 640) return 'sm'
      if (width < 768) return 'md'
      if (width < 1024) return 'lg'
      if (width < 1280) return 'xl'
      return '2xl'
    }
    
    const updateBreakpoint = () => {
      screenWidth.value = window.innerWidth
      breakpoint.value = detectBreakpoint(screenWidth.value)
    }
    
    onMounted(() => {
      updateBreakpoint()
      window.addEventListener('resize', updateBreakpoint)
    })
    
    onUnmounted(() => {
      window.removeEventListener('resize', updateBreakpoint)
    })
    
    // 计算容器样式
    const containerStyle = computed(() => {
      const styles = {}
      
      // 设置最大宽度
      if (!props.fluid) {
        const maxWidths = {
          'xs': '100%',
          'sm': '540px',
          'md': '720px',
          'lg': '960px',
          'xl': '1140px',
          '2xl': '1320px'
        }
        
        styles.maxWidth = props.maxWidth || maxWidths[breakpoint.value] || '1320px'
        styles.marginLeft = 'auto'
        styles.marginRight = 'auto'
      }
      
      // 设置内边距
      if (props.adaptivePadding) {
        const paddings = {
          'xs': '12px',
          'sm': '16px',
          'md': '20px',
          'lg': '24px',
          'xl': '32px',
          '2xl': '40px'
        }
        
        const padding = paddings[breakpoint.value] || '24px'
        styles.paddingLeft = padding
        styles.paddingRight = padding
      } else if (props.gutter) {
        const gutter = typeof props.gutter === 'number' ? `${props.gutter}px` : props.gutter
        styles.paddingLeft = gutter
        styles.paddingRight = gutter
      }
      
      // 设置高度
      if (props.fullHeight) {
        styles.minHeight = '100vh'
      }
      
      return styles
    })
    
    // 提供响应式上下文
    provide('responsiveContext', {
      breakpoint,
      screenWidth,
      isMobile: computed(() => ['xs', 'sm'].includes(breakpoint.value)),
      isTablet: computed(() => ['md', 'lg'].includes(breakpoint.value)),
      isDesktop: computed(() => ['xl', '2xl'].includes(breakpoint.value))
    })
    
    return {
      breakpoint,
      containerStyle
    }
  }
}
</script>

<style scoped>
.responsive-container {
  width: 100%;
  box-sizing: border-box;
  transition: all 0.3s ease;
}

.responsive-container--fluid {
  max-width: 100% !important;
}

.responsive-container--full-height {
  display: flex;
  flex-direction: column;
}

/* 平板设备优化 */
@media (min-width: 768px) and (max-width: 1023px) {
  .responsive-container--lg {
    --container-padding: 24px;
    --grid-gap: 20px;
  }
}

/* 平板横屏优化 */
@media (min-width: 1024px) and (max-width: 1279px) {
  .responsive-container--xl {
    --container-padding: 32px;
    --grid-gap: 24px;
  }
}
</style>

2.2 方案二:触摸设备专属优化方案

2.2.1 触摸设备特性分析

  1. 交互差异:触摸设备无 hover 状态,需要提供视觉反馈
  2. 精度差异:手指触摸精度低于鼠标指针,需要更大的点击目标
  3. 手势支持:支持滑动、捏合等手势操作
  4. 虚拟键盘:输入时虚拟键盘弹出,影响布局

2.2.2 触摸优化实现方案

vue 复制代码
<!-- src/components/touch/TouchOptimizer.vue -->
<template>
  <div :class="touchClasses">
    <slot />
  </div>
</template>

<script>
import { ref, onMounted, computed } from 'vue'

export default {
  name: 'TouchOptimizer',
  
  props: {
    enabled: {
      type: Boolean,
      default: true
    },
    minTouchSize: {
      type: Number,
      default: 44
    },
    optimizeInputs: {
      type: Boolean,
      default: true
    },
    disableHover: {
      type: Boolean,
      default: true
    }
  },
  
  setup(props) {
    const isTouchDevice = ref(false)
    const isIOS = ref(false)
    const isIPad = ref(false)
    
    // 检测设备类型
    const detectDevice = () => {
      // 检测触摸设备
      isTouchDevice.value = 'ontouchstart' in window || 
        navigator.maxTouchPoints > 0 ||
        navigator.msMaxTouchPoints > 0
      
      // 检测 iOS 设备
      const ua = navigator.userAgent
      isIOS.value = /iPad|iPhone|iPod/.test(ua) && !window.MSStream
      
      // 检测 iPad
      isIPad.value = /iPad/.test(ua) || 
        (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
      
      console.log('设备检测:', {
        isTouchDevice: isTouchDevice.value,
        isIOS: isIOS.value,
        isIPad: isIPad.value,
        userAgent: ua
      })
    }
    
    // 优化触摸交互
    const optimizeTouchInteraction = () => {
      if (!props.enabled || !isTouchDevice.value) return
      
      // 动态添加触摸优化样式
      const styleId = 'touch-optimization-styles'
      if (document.getElementById(styleId)) return
      
      const style = document.createElement('style')
      style.id = styleId
      
      const minSize = props.minTouchSize
      
      style.textContent = `
        /* 触摸设备优化样式 */
        @media (hover: none) and (pointer: coarse) {
          /* 增大点击目标 */
          button, 
          .el-button,
          [role="button"],
          .clickable {
            min-height: ${minSize}px !important;
            min-width: ${minSize}px !important;
            padding: 12px 20px !important;
          }
          
          /* 优化输入框 */
          input,
          .el-input__inner,
          textarea,
          select {
            font-size: 16px !important;
            min-height: ${minSize}px !important;
            padding: 12px !important;
          }
          
          /* 优化表单元素 */
          .el-radio,
          .el-checkbox {
            min-height: ${minSize}px !important;
          }
          
          .el-radio__inner,
          .el-checkbox__inner {
            transform: scale(1.2);
            transform-origin: left center;
          }
          
          /* 优化表格交互 */
          .el-table__row {
            min-height: ${minSize}px !important;
          }
          
          /* 禁用 hover 效果 */
          ${props.disableHover ? `
            *:hover {
              background-color: inherit !important;
              color: inherit !important;
              transform: none !important;
            }
            
            /* 用 active 状态替代 hover */
            button:active,
            .el-button:active,
            [role="button"]:active {
              opacity: 0.8;
              transform: scale(0.98);
            }
          ` : ''}
          
          /* iOS 特定优化 */
          ${isIOS.value ? `
            /* 防止 iOS 缩放 */
            input[type="text"],
            input[type="search"],
            input[type="tel"],
            input[type="url"],
            input[type="email"] {
              font-size: 16px !important;
            }
            
            /* 优化 Safari 渲染 */
            * {
              -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
            }
            
            /* 平滑滚动 */
            .scrollable {
              -webkit-overflow-scrolling: touch;
            }
          ` : ''}
          
          /* iPad 特定优化 */
          ${isIPad.value ? `
            /* 优化 iPad 上的鼠标指针 */
            @media (pointer: fine) and (hover: hover) {
              /* iPad 连接鼠标时 */
              .el-button:hover {
                background-color: var(--el-button-hover-bg-color) !important;
              }
            }
            
            /* 优化 iPad 分屏模式 */
            @media (max-width: 1024px) {
              .responsive-container {
                padding-left: env(safe-area-inset-left) !important;
                padding-right: env(safe-area-inset-right) !important;
              }
            }
          ` : ''}
        }
        
        /* 触摸反馈动画 */
        .touch-feedback {
          position: relative;
          overflow: hidden;
        }
        
        .touch-feedback::after {
          content: '';
          position: absolute;
          top: 50%;
          left: 50%;
          width: 100%;
          height: 100%;
          background: rgba(255, 255, 255, 0.3);
          border-radius: 50%;
          transform: translate(-50%, -50%) scale(0);
          opacity: 0;
          transition: transform 0.3s, opacity 0.3s;
          pointer-events: none;
        }
        
        .touch-feedback:active::after {
          transform: translate(-50%, -50%) scale(1);
          opacity: 1;
          transition: 0s;
        }
      `
      
      document.head.appendChild(style)
    }
    
    // 优化虚拟键盘处理
    const optimizeVirtualKeyboard = () => {
      if (!props.optimizeInputs) return
      
      // 监听输入框聚焦,滚动到可视区域
      const handleFocus = (event) => {
        if (!isTouchDevice.value) return
        
        const target = event.target
        if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
          setTimeout(() => {
            target.scrollIntoView({
              behavior: 'smooth',
              block: 'center'
            })
          }, 300)
        }
      }
      
      // 监听视口变化(虚拟键盘弹出)
      const handleResize = () => {
        if (!isTouchDevice.value) return
        
        const viewportHeight = window.innerHeight
        const documentHeight = document.documentElement.clientHeight
        
        // 检测虚拟键盘是否弹出
        if (viewportHeight < documentHeight * 0.7) {
          document.body.classList.add('virtual-keyboard-open')
        } else {
          document.body.classList.remove('virtual-keyboard-open')
        }
      }
      
      document.addEventListener('focusin', handleFocus)
      window.addEventListener('resize', handleResize)
      
      return () => {
        document.removeEventListener('focusin', handleFocus)
        window.removeEventListener('resize', handleResize)
      }
    }
    
    // 添加手势支持
    const addGestureSupport = () => {
      if (!isTouchDevice.value) return
      
      // 滑动手势支持
      const touchStart = { x: 0, y: 0 }
      
      const handleTouchStart = (event) => {
        const touch = event.touches[0]
        touchStart.x = touch.clientX
        touchStart.y = touch.clientY
      }
      
      const handleTouchEnd = (event) => {
        const touch = event.changedTouches[0]
        const deltaX = touch.clientX - touchStart.x
        const deltaY = touch.clientY - touchStart.y
        const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
        
        // 检测滑动手势
        if (distance > 50) {
          const eventDetail = {
            direction: Math.abs(deltaX) > Math.abs(deltaY) 
              ? (deltaX > 0 ? 'right' : 'left')
              : (deltaY > 0 ? 'down' : 'up'),
            distance: distance,
            startX: touchStart.x,
            startY: touchStart.y,
            endX: touch.clientX,
            endY: touch.clientY
          }
          
          // 触发自定义手势事件
          const gestureEvent = new CustomEvent('touch-gesture', {
            detail: eventDetail,
            bubbles: true
          })
          
          event.target.dispatchEvent(gestureEvent)
        }
      }
      
      document.addEventListener('touchstart', handleTouchStart, { passive: true })
      document.addEventListener('touchend', handleTouchEnd, { passive: true })
      
      return () => {
        document.removeEventListener('touchstart', handleTouchStart)
        document.removeEventListener('touchend', handleTouchEnd)
      }
    }
    
    onMounted(() => {
      detectDevice()
      optimizeTouchInteraction()
      
      const cleanupKeyboard = optimizeVirtualKeyboard()
      const cleanupGesture = addGestureSupport()
      
      // 清理函数
      return () => {
        cleanupKeyboard?.()
        cleanupGesture?.()
      }
    })
    
    const touchClasses = computed(() => ({
      'touch-optimized': props.enabled && isTouchDevice.value,
      'ios-device': isIOS.value,
      'ipad-device': isIPad.value,
      'touch-feedback': props.enabled
    }))
    
    return {
      isTouchDevice,
      isIOS,
      isIPad,
      touchClasses
    }
  }
}
</script>

<style scoped>
.touch-optimized {
  --touch-target-size: 44px;
  --touch-padding: 12px;
}

/* 虚拟键盘打开时的优化 */
.virtual-keyboard-open .fixed-bottom {
  bottom: var(--keyboard-height, 300px);
  transition: bottom 0.3s ease;
}

/* 防止内容被键盘遮挡 */
@media (max-height: 500px) {
  .virtual-keyboard-open .scrollable-content {
    max-height: calc(100vh - var(--keyboard-height, 300px));
    overflow-y: auto;
  }
}
</style>

2.2.3 触摸友好的 Elements Plus 组件包装器

vue 复制代码
<!-- src/components/touch/TouchButton.vue -->
<template>
  <el-button
    ref="buttonRef"
    :class="[
      'touch-button',
      {
        'touch-button--large': large,
        'touch-button--block': block,
        'touch-button--loading': loading,
        'touch-button--disabled': disabled || loading
      }
    ]"
    :size="computedSize"
    :type="type"
    :disabled="disabled || loading"
    :loading="loading"
    @click="handleClick"
    @touchstart="handleTouchStart"
    @touchend="handleTouchEnd"
  >
    <slot />
    <span v-if="showTouchHint && isTouchDevice" class="touch-button__hint">
      <i class="el-icon-touch" />
    </span>
  </el-button>
</template>

<script>
import { ref, computed, onMounted } from 'vue'
import { ElButton } from 'element-plus'

export default {
  name: 'TouchButton',
  
  components: {
    ElButton
  },
  
  props: {
    size: {
      type: String,
      default: 'default',
      validator: (value) => ['mini', 'small', 'default', 'large'].includes(value)
    },
    type: {
      type: String,
      default: 'default',
      validator: (value) => 
        ['default', 'primary', 'success', 'warning', 'danger', 'info', 'text'].includes(value)
    },
    disabled: {
      type: Boolean,
      default: false
    },
    loading: {
      type: Boolean,
      default: false
    },
    large: {
      type: Boolean,
      default: false
    },
    block: {
      type: Boolean,
      default: false
    },
    showTouchHint: {
      type: Boolean,
      default: false
    },
    rippleEffect: {
      type: Boolean,
      default: true
    }
  },
  
  setup(props, { emit }) {
    const buttonRef = ref(null)
    const isTouchDevice = ref(false)
    const isPressed = ref(false)
    
    // 检测触摸设备
    onMounted(() => {
      isTouchDevice.value = 'ontouchstart' in window || 
        navigator.maxTouchPoints > 0
    
  // 添加触摸优化样式
      if (isTouchDevice.value) {
        const style = document.createElement('style')
        style.textContent = `
          .touch-button {
            position: relative;
            overflow: hidden;
            user-select: none;
            -webkit-tap-highlight-color: transparent;
          }
          
          .touch-button--large {
            min-height: 48px !important;
            min-width: 48px !important;
            padding: 14px 24px !important;
            font-size: 16px !important;
          }
          
          .touch-button--block {
            width: 100% !important;
            display: block !important;
          }
          
          .touch-button__ripple {
            position: absolute;
            border-radius: 50%;
            background: rgba(255, 255, 255, 0.6);
            transform: scale(0);
            animation: ripple 0.6s linear;
            pointer-events: none;
          }
          
          @keyframes ripple {
            to {
              transform: scale(4);
              opacity: 0;
            }
          }
          
          .touch-button__hint {
            margin-left: 8px;
            opacity: 0.6;
            font-size: 0.9em;
          }
          
          .touch-button:active {
            transform: scale(0.98);
            transition: transform 0.1s;
          }
          
          .touch-button--loading {
            opacity: 0.7;
            cursor: wait !important;
          }
          
          .touch-button--disabled {
            opacity: 0.5;
            cursor: not-allowed !important;
          }
        `
        document.head.appendChild(style)
      }
    })
    
    // 计算按钮尺寸
    const computedSize = computed(() => {
      if (isTouchDevice.value && props.large) {
        return 'large'
      }
      return props.size
    })
    
    // 涟漪效果
    const createRipple = (event) => {
      if (!props.rippleEffect || !buttonRef.value) return
      
      const button = buttonRef.value.$el
      const circle = document.createElement('span')
      const diameter = Math.max(button.clientWidth, button.clientHeight)
      const radius = diameter / 2
      
      circle.style.width = circle.style.height = `${diameter}px`
      circle.style.left = `${event.clientX - button.getBoundingClientRect().left - radius}px`
      circle.style.top = `${event.clientY - button.getBoundingClientRect().top - radius}px`
      circle.classList.add('touch-button__ripple')
      
      const ripple = button.getElementsByClassName('touch-button__ripple')[0]
      
      if (ripple) {
        ripple.remove()
      }
      
      button.appendChild(circle)
    }
    
    // 事件处理
    const handleClick = (event) => {
      if (!isTouchDevice.value) {
        createRipple(event)
      }
      emit('click', event)
    }
    
    const handleTouchStart = (event) => {
      if (!isTouchDevice.value) return
      
      isPressed.value = true
      createRipple(event.touches[0])
      
      // 添加按下状态类
      if (buttonRef.value?.$el) {
        buttonRef.value.$el.classList.add('touch-button--pressed')
      }
    }
    
    const handleTouchEnd = () => {
      if (!isTouchDevice.value) return
      
      isPressed.value = false
      
      // 移除按下状态类
      if (buttonRef.value?.$el) {
        buttonRef.value.$el.classList.remove('touch-button--pressed')
      }
    }
    
    return {
      buttonRef,
      isTouchDevice,
      isPressed,
      computedSize,
      handleClick,
      handleTouchStart,
      handleTouchEnd
    }
  }
}
</script>

第三部分:组件级自适应优化

3.1 表格组件响应式优化

3.1.1 问题分析

Elements Plus 表格在 iPad 等中等屏幕设备上常出现以下问题:

  • 列过多导致水平滚动
  • 列宽固定,内容被截断
  • 操作按钮过小,难以点击
  • 缺少移动端友好视图

3.1.2 响应式表格组件实现

vue 复制代码
<!-- src/components/table/ResponsiveTable.vue -->
<template>
  <div 
    :class="[
      'responsive-table-container',
      {
        'responsive-table-container--striped': striped,
        'responsive-table-container--bordered': bordered,
        'responsive-table-container--hover': hover,
        'responsive-table-container--small': size === 'small',
        'responsive-table-container--large': size === 'large',
        'responsive-table-container--card': cardView,
        [`responsive-table-container--${breakpoint}`]: true
      }
    ]"
    :style="containerStyle"
  >
    <!-- 工具栏 -->
    <div v-if="showToolbar" class="responsive-table__toolbar">
      <div class="responsive-table__toolbar-left">
        <slot name="toolbar-left" />
        
        <!-- 视图切换 -->
        <div v-if="allowViewToggle" class="view-toggle">
          <el-button-group>
            <el-button
              :type="viewMode === 'table' ? 'primary' : 'default'"
              size="small"
              @click="viewMode = 'table'"
              :title="isMobile ? '列表视图' : '表格视图'"
            >
              <i class="el-icon-s-grid" />
              <span v-if="!isMobile">表格</span>
            </el-button>
            <el-button
              :type="viewMode === 'card' ? 'primary' : 'default'"
              size="small"
              @click="viewMode = 'card'"
              :title="isMobile ? '卡片视图' : '卡片视图'"
            >
              <i class="el-icon-s-data" />
              <span v-if="!isMobile">卡片</span>
            </el-button>
          </el-button-group>
        </div>
      </div>
      
      <div class="responsive-table__toolbar-right">
        <!-- 列显示控制 -->
        <el-dropdown 
          v-if="showColumnControl" 
          trigger="click"
          :disabled="viewMode === 'card'"
        >
          <el-button size="small">
            <i class="el-icon-setting" />
            <span v-if="!isMobile">列设置</span>
          </el-button>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item 
                v-for="column in columns" 
                :key="column.prop"
                :disabled="column.fixed"
              >
                <el-checkbox
                  v-model="column.visible"
                  @change="handleColumnVisibilityChange(column)"
                >
                  {{ column.label }}
                </el-checkbox>
              </el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
        
        <slot name="toolbar-right" />
      </div>
    </div>
    
    <!-- 表格视图 -->
    <div 
      v-if="viewMode === 'table'"
      :class="[
        'responsive-table-wrapper',
        { 'responsive-table-wrapper--scrollable': scrollable }
      ]"
      :style="wrapperStyle"
    >
      <el-table
        ref="tableRef"
        :data="data"
        :height="height"
        :max-height="maxHeight"
        :stripe="striped"
        :border="bordered"
        :size="computedSize"
        :row-class-name="rowClassName"
        :cell-class-name="cellClassName"
        :header-cell-class-name="headerCellClassName"
        :show-header="showHeader"
        :highlight-current-row="highlightCurrentRow"
        @row-click="handleRowClick"
        @selection-change="handleSelectionChange"
      >
        <!-- 选择列 -->
        <el-table-column
          v-if="selectable"
          type="selection"
          :width="isMobile ? 40 : 55"
          :fixed="isMobile ? 'left' : false"
          align="center"
        />
        
        <!-- 序号列 -->
        <el-table-column
          v-if="showIndex"
          type="index"
          :label="indexLabel"
          :width="isMobile ? 50 : 60"
          :fixed="isMobile ? 'left' : false"
          align="center"
        />
        
        <!-- 动态列 -->
        <template v-for="column in visibleColumns" :key="column.prop">
          <el-table-column
            :prop="column.prop"
            :label="column.label"
            :width="getColumnWidth(column)"
            :min-width="getColumnMinWidth(column)"
            :fixed="getColumnFixed(column)"
            :align="column.align || 'left'"
            :sortable="column.sortable"
            :formatter="column.formatter"
            :show-overflow-tooltip="column.showTooltip !== false"
          >
            <!-- 自定义列内容 -->
            <template v-if="column.slot" #default="scope">
              <slot :name="column.slot" :row="scope.row" />
            </template>
          </el-table-column>
        </template>
        
        <!-- 操作列 -->
        <el-table-column
          v-if="showActions"
          :label="actionLabel"
          :width="getActionsWidth"
          :fixed="isMobile ? 'right' : false"
          align="center"
        >
          <template #default="scope">
            <div class="responsive-table__actions">
              <slot name="actions" :row="scope.row" />
              
              <!-- 默认操作按钮 -->
              <template v-if="defaultActions">
                <el-button
                  v-if="defaultActions.view"
                  size="small"
                  type="text"
                  @click.stop="handleView(scope.row)"
                  :title="defaultActions.view.title || '查看'"
                >
                  <i class="el-icon-view" />
                  <span v-if="!isMobile">{{ defaultActions.view.text || '查看' }}</span>
                </el-button>
                
                <el-button
                  v-if="defaultActions.edit"
                  size="small"
                  type="text"
                  @click.stop="handleEdit(scope.row)"
                  :title="defaultActions.edit.title || '编辑'"
                >
                  <i class="el-icon-edit" />
                  <span v-if="!isMobile">{{ defaultActions.edit.text || '编辑' }}</span>
                </el-button>
                
                <el-button
                  v-if="defaultActions.delete"
                  size="small"
                  type="text"
                  @click.stop="handleDelete(scope.row)"
                  :title="defaultActions.delete.title || '删除'"
                >
                  <i class="el-icon-delete" />
                  <span v-if="!isMobile">{{ defaultActions.delete.text || '删除' }}</span>
                </el-button>
              </template>
            </div>
          </template>
        </el-table-column>
        
        <!-- 空状态 -->
        <template #empty>
          <div class="responsive-table__empty">
            <slot name="empty">
              <i class="el-icon-document" />
              <p>{{ emptyText || '暂无数据' }}</p>
            </slot>
          </div>
        </template>
      </el-table>
      
      <!-- 移动端浮动操作按钮 -->
      <div v-if="isMobile && showActions" class="mobile-floating-actions">
        <el-button
          v-for="action in mobileActions"
          :key="action.name"
          type="primary"
          :icon="action.icon"
          circle
          @click="handleMobileAction(action)"
        />
      </div>
    </div>
    
    <!-- 卡片视图 -->
    <div v-else-if="viewMode === 'card'" class="responsive-card-view">
      <div class="card-list">
        <div
          v-for="(item, index) in data"
          :key="item.id || index"
          :class="[
            'data-card',
            {
              'data-card--selected': selectedRows.includes(item),
              'data-card--striped': striped && index % 2 === 0
            }
          ]"
          @click="handleCardClick(item)"
        >
          <!-- 卡片头部 -->
          <div class="card-header">
            <div class="card-header-left">
              <el-checkbox
                v-if="selectable"
                :model-value="selectedRows.includes(item)"
                @click.stop="toggleRowSelection(item)"
              />
              
              <div class="card-title">
                <slot name="card-title" :row="item">
                  {{ getCardTitle(item) }}
                </slot>
              </div>
            </div>
            
            <div class="card-header-right">
              <slot name="card-header-actions" :row="item" />
            </div>
          </div>
          
          <!-- 卡片内容 -->
          <div class="card-content">
            <template v-for="column in visibleColumns" :key="column.prop">
              <div v-if="shouldShowInCard(column)" class="card-field">
                <span class="card-field__label">{{ column.label }}:</span>
                <span class="card-field__value">
                  <slot v-if="column.slot" :name="column.slot" :row="item" />
                  <template v-else>
                    {{ column.formatter ? column.formatter(item) : item[column.prop] }}
                  </template>
                </span>
              </div>
            </template>
          </div>
          
          <!-- 卡片操作 -->
          <div v-if="showActions" class="card-actions">
            <slot name="card-actions" :row="item">
              <el-button-group>
                <el-button
                  v-if="defaultActions?.view"
                  size="small"
                  @click.stop="handleView(item)"
                >
                  查看
                </el-button>
                <el-button
                  v-if="defaultActions?.edit"
                  size="small"
                  type="primary"
                  @click.stop="handleEdit(item)"
                >
                  编辑
                </el-button>
                <el-button
                  v-if="defaultActions?.delete"
                  size="small"
                  type="danger"
                  @click.stop="handleDelete(item)"
                >
                  删除
                </el-button>
              </el-button-group>
            </slot>
          </div>
        </div>
      </div>
    </div>
    
    <!-- 分页器 -->
    <div v-if="showPagination" class="responsive-table__pagination">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :total="total"
        :page-sizes="pageSizes"
        :layout="paginationLayout"
        :small="isMobile"
        :background="true"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
  </div>
</template>

<script>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { 
  ElTable, 
  ElTableColumn, 
  ElButton, 
  ElButtonGroup,
  ElCheckbox,
  ElDropdown,
  ElDropdownMenu,
  ElDropdownItem,
  ElPagination
} from 'element-plus'

export default {
  name: 'ResponsiveTable',
  
  components: {
    ElTable,
    ElTableColumn,
    ElButton,
    ElButtonGroup,
    ElCheckbox,
    ElDropdown,
    ElDropdownMenu,
    ElDropdownItem,
    ElPagination
  },
  
  props: {
    // 数据相关
    data: {
      type: Array,
      default: () => []
    },
    columns: {
      type: Array,
      default: () => [],
      required: true
    },
    
    // 显示配置
    striped: {
      type: Boolean,
      default: false
    },
    bordered: {
      type: Boolean,
      default: false
    },
    hover: {
      type: Boolean,
      default: true
    },
    size: {
      type: String,
      default: 'default',
      validator: (value) => ['mini', 'small', 'default', 'large'].includes(value)
    },
    height: [String, Number],
    maxHeight: [String, Number],
    scrollable: {
      type: Boolean,
      default: false
    },
    showHeader: {
      type: Boolean,
      default: true
    },
    highlightCurrentRow: {
      type: Boolean,
      default: false
    },
    
    // 功能配置
    selectable: {
      type: Boolean,
      default: false
    },
    showIndex: {
      type: Boolean,
      default: false
    },
    indexLabel: {
      type: String,
      default: '序号'
    },
    showActions: {
      type: Boolean,
      default: false
    },
    actionLabel: {
      type: String,
      default: '操作'
    },
    defaultActions: {
      type: Object,
      default: null
    },
    
    // 工具栏配置
    showToolbar: {
      type: Boolean,
      default: true
    },
    allowViewToggle: {
      type: Boolean,
      default: true
    },
    showColumnControl: {
      type: Boolean,
      default: true
    },
    
    // 卡片视图配置
    cardView: {
      type: Boolean,
      default: false
    },
    cardTitleField: {
      type: String,
      default: 'title'
    },
    cardFields: {
      type: Array,
      default: () => []
    },
    
    // 分页配置
    showPagination: {
      type: Boolean,
      default: false
    },
    total: {
      type: Number,
      default: 0
    },
    pageSizes: {
      type: Array,
      default: () => [10, 20, 50, 100]
    },
    paginationLayout: {
      type: String,
      default: 'total, sizes, prev, pager, next, jumper'
    },
    
    // 空状态
    emptyText: {
      type: String,
      default: ''
    },
    
    // 响应式配置
    mobileBreakpoint: {
      type: Number,
      default: 768
    },
    tabletBreakpoint: {
      type: Number,
      default: 1024
    },
    autoSwitchView: {
      type: Boolean,
      default: true
    }
  },
  
  emits: [
    'row-click',
    'selection-change',
    'view',
    'edit',
    'delete',
    'size-change',
    'current-change',
    'mobile-action'
  ],
  
  setup(props, { emit }) {
    // 响应式状态
    const tableRef = ref(null)
    const breakpoint = ref('desktop')
    const viewMode = ref('table')
    const selectedRows = ref([])
    const columnVisibility = ref({})
    const currentPage = ref(1)
    const pageSize = ref(10)
    
    // 响应式断点检测
    const updateBreakpoint = () => {
      const width = window.innerWidth
      
      if (width < props.mobileBreakpoint) {
        breakpoint.value = 'mobile'
        if (props.autoSwitchView) {
          viewMode.value = 'card'
        }
      } else if (width < props.tabletBreakpoint) {
        breakpoint.value = 'tablet'
      } else {
        breakpoint.value = 'desktop'
        if (props.autoSwitchView) {
          viewMode.value = 'table'
        }
      }
    }
    
    // 设备类型计算属性
    const isMobile = computed(() => breakpoint.value === 'mobile')
    const isTablet = computed(() => breakpoint.value === 'tablet')
    const isDesktop = computed(() => breakpoint.value === 'desktop')
    
    // 列可见性管理
    const visibleColumns = computed(() => {
      return props.columns.filter(column => {
        const isVisible = columnVisibility.value[column.prop] !== false
        const shouldShow = column.hidden !== true && isVisible
        
        // 移动端隐藏部分列
        if (isMobile.value && column.hideOnMobile) {
          return false
        }
        
        // 平板设备隐藏部分列
        if (isTablet.value && column.hideOnTablet) {
          return false
        }
        
        return shouldShow
      })
    })
    
    // 初始化列可见性
    const initColumnVisibility = () => {
      props.columns.forEach(column => {
        if (column.visible !== undefined) {
          columnVisibility.value[column.prop] = column.visible
        } else {
          columnVisibility.value[column.prop] = true
        }
      })
    }
    
    // 计算列宽
    const getColumnWidth = (column) => {
      // 移动端优化
      if (isMobile.value) {
        if (column.mobileWidth) return column.mobileWidth
        if (column.width) return Math.min(column.width, 120)
        return null
      }
      
      // 平板优化
      if (isTablet.value) {
        if (column.tabletWidth) return column.tabletWidth
        return column.width
      }
      
      return column.width
    }
    
    const getColumnMinWidth = (column) => {
      if (isMobile.value) {
        return column.mobileMinWidth || 80
      }
      if (isTablet.value) {
        return column.tabletMinWidth || 100
      }
      return column.minWidth || 120
    }
    
    const getColumnFixed = (column) => {
      if (isMobile.value && column.fixedOnMobile !== undefined) {
        return column.fixedOnMobile
      }
      return column.fixed
    }
    
    // 操作列宽度计算
    const getActionsWidth = computed(() => {
      if (isMobile.value) {
        const actionCount = props.defaultActions 
          ? Object.keys(props.defaultActions).length 
          : 0
        return Math.max(actionCount * 40, 80)
      }
      return 180
    })
    
    // 移动端操作按钮
    const mobileActions = computed(() => {
      if (!props.defaultActions) return []
      
      const actions = []
      if (props.defaultActions.view) {
        actions.push({
          name: 'view',
          icon: 'el-icon-view',
          handler: props.defaultActions.view.handler
        })
      }
      if (props.defaultActions.edit) {
        actions.push({
          name: 'edit',
          icon: 'el-icon-edit',
          handler: props.defaultActions.edit.handler
        })
      }
      if (props.defaultActions.delete) {
        actions.push({
          name: 'delete',
          icon: 'el-icon-delete',
          handler: props.defaultActions.delete.handler
        })
      }
      return actions
    })
    
    // 表格尺寸计算
    const computedSize = computed(() => {
      if (isMobile.value) return 'small'
      if (isTablet.value) return 'default'
      return props.size
    })
    
    // 容器样式
    const containerStyle = computed(() => {
      const styles = {}
      
      if (isMobile.value) {
        styles.fontSize = '14px'
      }
      
      return styles
    })
    
    // 包装器样式
    const wrapperStyle = computed(() => {
      if (!props.scrollable) return {}
      
      return {
        overflowX: 'auto',
        WebkitOverflowScrolling: 'touch'
      }
    })
    
    // 卡片视图相关
    const getCardTitle = (row) => {
      if (props.cardTitleField && row[props.cardTitleField]) {
        return row[props.cardTitleField]
      }
      return `项目 ${row.id || row._id}`
    }
    
    const shouldShowInCard = (column) => {
      if (props.cardFields.length > 0) {
        return props.cardFields.includes(column.prop)
      }
      
      // 默认显示前3列
      const index = props.columns.findIndex(col => col.prop === column.prop)
      return index < 3
    }
    
    // 事件处理
    const handleColumnVisibilityChange = (column) => {
      columnVisibility.value[column.prop] = column.visible
      emit('column-visibility-change', column)
    }
    
    const handleRowClick = (row, column, event) => {
      emit('row-click', row, column, event)
    }
    
    const handleCardClick = (row) => {
      emit('row-click', row, null, null)
    }
    
    const handleSelectionChange = (selection) => {
      selectedRows.value = selection
      emit('selection-change', selection)
    }
    
    const toggleRowSelection = (row) => {
      const index = selectedRows.value.findIndex(
        selected => selected === row || selected.id === row.id
      )
      
      if (index > -1) {
        selectedRows.value.splice(index, 1)
      } else {
        selectedRows.value.push(row)
      }
      
      emit('selection-change', selectedRows.value)
    }
    
    const handleView = (row) => {
      emit('view', row)
    }
    
    const handleEdit = (row) => {
      emit('edit', row)
    }
    
    const handleDelete = (row) => {
      emit('delete', row)
    }
    
    const handleMobileAction = (action) => {
      emit('mobile-action', action.name)
      if (action.handler) {
        action.handler()
      }
    }
    
    const handleSizeChange = (size) => {
      pageSize.value = size
      emit('size-change', size)
    }
    
    const handleCurrentChange = (page) => {
      currentPage.value = page
      emit('current-change', page)
    }
    
    // 行类名
    const rowClassName = ({ row, rowIndex }) => {
      const classes = []
      
      if (props.striped && rowIndex % 2 === 0) {
        classes.push('striped-row')
      }
      
      if (selectedRows.value.includes(row)) {
        classes.push('selected-row')
      }
      
      return classes.join(' ')
    }
    
    // 单元格类名
    const cellClassName = ({ row, column, rowIndex, columnIndex }) => {
      const classes = []
      
      if (isMobile.value && column.property === 'selection') {
        classes.push('mobile-selection-cell')
      }
      
      return classes.join(' ')
    }
    
    // 表头单元格类名
    const headerCellClassName = ({ row, column, rowIndex, columnIndex }) => {
      if (isMobile.value) {
        return 'mobile-header-cell'
      }
      return ''
    }
    
    // 生命周期
    onMounted(() => {
      updateBreakpoint()
      initColumnVisibility()
      
      window.addEventListener('resize', updateBreakpoint)
      
      // 添加触摸优化
      if ('ontouchstart' in window) {
        document.body.classList.add('touch-device')
      }
    })
    
    onUnmounted(() => {
      window.removeEventListener('resize', updateBreakpoint)
    })
    
    // 监听数据变化
    watch(() => props.data, () => {
      if (tableRef.value) {
        tableRef.value.doLayout()
      }
    }, { deep: true })
    
    // 监听断点变化
    watch(breakpoint, (newVal) => {
      console.log(`表格断点变化: ${newVal}`)
    })
    
    // 暴露方法
    const clearSelection = () => {
      if (tableRef.value) {
        tableRef.value.clearSelection()
      }
      selectedRows.value = []
    }
    
    const toggleRowSelectionByIndex = (index) => {
      if (tableRef.value) {
        tableRef.value.toggleRowSelection(props.data[index])
      }
    }
    
    return {
      // refs
      tableRef,
      
      // 状态
      breakpoint,
      viewMode,
      selectedRows,
      columnVisibility,
      currentPage,
      pageSize,
      
      // 计算属性
      isMobile,
      isTablet,
      isDesktop,
      visibleColumns,
      getActionsWidth,
      mobileActions,
      computedSize,
      containerStyle,
      wrapperStyle,
      
      // 方法
      getColumnWidth,
      getColumnMinWidth,
      getColumnFixed,
      getCardTitle,
      shouldShowInCard,
      handleColumnVisibilityChange,
      handleRowClick,
      handleCardClick,
      handleSelectionChange,
      toggleRowSelection,
      handleView,
      handleEdit,
      handleDelete,
      handleMobileAction,
      handleSizeChange,
      handleCurrentChange,
      rowClassName,
      cellClassName,
      headerCellClassName,
      clearSelection,
      toggleRowSelectionByIndex
    }
  }
}
</script>

<style scoped>
.responsive-table-container {
  position: relative;
  background: var(--el-bg-color);
  border-radius: var(--el-border-radius-base);
  transition: all 0.3s ease;
}

/* 工具栏样式 */
.responsive-table__toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  border-bottom: 1px solid var(--el-border-color-light);
  flex-wrap: wrap;
  gap: 12px;
}

.responsive-table__toolbar-left,
.responsive-table__toolbar-right {
  display: flex;
  align-items: center;
  gap: 12px;
}

.view-toggle {
  display: flex;
  align-items: center;
}

/* 表格包装器 */
.responsive-table-wrapper {
  position: relative;
  overflow: hidden;
}

.responsive-table-wrapper--scrollable {
  overflow-x: auto;
}

/* 移动端优化 */
@media (max-width: 767px) {
  .responsive-table-container--mobile {
    border-radius: 0;
    margin: 0 -16px;
    width: calc(100% + 32px);
  }
  
  .responsive-table__toolbar {
    flex-direction: column;
    align-items: stretch;
    padding: 12px 16px;
  }
  
  .responsive-table__toolbar-left,
  .responsive-table__toolbar-right {
    width: 100%;
    justify-content: space-between;
  }
  
  .el-table {
    font-size: 14px;
  }
  
  .el-table__body-wrapper {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  
  .el-table th,
  .el-table td {
    padding: 8px 4px;
  }
  
  .mobile-header-cell {
    font-size: 12px;
    font-weight: 600;
    white-space: nowrap;
  }
  
  .mobile-selection-cell {
    padding: 8px 4px !important;
  }
  
  /* 移动端浮动操作按钮 */
  .mobile-floating-actions {
    position: fixed;
    bottom: 80px;
    right: 20px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    z-index: 1000;
  }
  
  .mobile-floating-actions .el-button {
    width: 56px;
    height: 56px;
    font-size: 20px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  }
}

/* 平板优化 */
@media (min-width: 768px) and (max-width: 1023px) {
  .responsive-table-container--tablet {
    margin: 0;
  }
  
  .el-table {
    font-size: 14px;
  }
  
  .el-table th,
  .el-table td {
    padding: 10px 8px;
  }
  
  /* 隐藏不重要的列 */
  .responsive-table-container--tablet :deep(.hidden-on-tablet) {
    display: none;
  }
}

/* 卡片视图样式 */
.responsive-card-view {
  padding: 16px;
}

.card-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.data-card {
  background: var(--el-bg-color);
  border: 1px solid var(--el-border-color-light);
  border-radius: var(--el-border-radius-base);
  padding: 16px;
  transition: all 0.3s ease;
  cursor: pointer;
}

.data-card:hover {
  border-color: var(--el-color-primary);
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}

.data-card--selected {
  border-color: var(--el-color-primary);
  background: rgba(var(--el-color-primary-rgb), 0.05);
}

.data-card--striped {
  background: var(--el-fill-color-light);
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--el-border-color-lighter);
}

.card-header-left {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 1;
  min-width: 0;
}

.card-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--el-text-color-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-content {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 16px;
}

.card-field {
  display: flex;
  font-size: 14px;
  line-height: 1.5;
}

.card-field__label {
  color: var(--el-text-color-secondary);
  min-width: 80px;
  flex-shrink: 0;
}

.card-field__value {
  color: var(--el-text-color-primary);
  flex: 1;
  min-width: 0;
  word-break: break-word;
}

.card-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding-top: 12px;
  border-top: 1px solid var(--el-border-color-lighter);
}

/* 空状态样式 */
.responsive-table__empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 60px 20px;
  color: var(--el-text-color-secondary);
  text-align: center;
}

.responsive-table__empty i {
  font-size: 48px;
  margin-bottom: 16px;
  opacity: 0.3;
}

.responsive-table__empty p {
  margin: 0;
  font-size: 14px;
}

/* 分页器样式 */
.responsive-table__pagination {
  display: flex;
  justify-content: center;
  padding: 20px 16px;
  border-top: 1px solid var(--el-border-color-light);
}

/* 触摸设备优化 */
.touch-device .data-card {
  min-height: 44px;
}

.touch-device .el-button {
  min-height: 44px;
  min-width: 44px;
}

/* 操作按钮容器 */
.responsive-table__actions {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
}

@media (max-width: 767px) {
  .responsive-table__actions {
    justify-content: center;
  }
  
  .responsive-table__actions .el-button {
    padding: 4px 8px;
    min-width: auto;
  }
  
  .responsive-table__actions .el-button span {
    display: none;
  }
}

/* 行样式增强 */
:deep(.striped-row) {
  background-color: var(--el-fill-color-light) !important;
}

:deep(.selected-row) {
  background-color: rgba(var(--el-color-primary-rgb), 0.1) !important;
}

:deep(.selected-row:hover > td) {
  background-color: rgba(var(--el-color-primary-rgb), 0.15) !important;
}

/* 平滑滚动优化 */
.responsive-table-wrapper {
  scrollbar-width: thin;
  scrollbar-color: var(--el-border-color) transparent;
}

.responsive-table-wrapper::-webkit-scrollbar {
  height: 8px;
  width: 8px;
}

.responsive-table-wrapper::-webkit-scrollbar-track {
  background: transparent;
}

.responsive-table-wrapper::-webkit-scrollbar-thumb {
  background-color: var(--el-border-color);
  border-radius: 4px;
}

.responsive-table-wrapper::-webkit-scrollbar-thumb:hover {
  background-color: var(--el-border-color-dark);
}
</style>

3.1.3 使用示例与配置

vue 复制代码
<!-- 使用响应式表格组件的示例 -->
<template>
  <ResponsiveTable
    :data="tableData"
    :columns="columns"
    :total="total"
    :show-pagination="true"
    :selectable="true"
    :show-actions="true"
    :default-actions="defaultActions"
    @selection-change="handleSelectionChange"
    @view="handleView"
    @edit="handleEdit"
    @delete="handleDelete"
    @size-change="handlePageSizeChange"
    @current-change="handlePageChange"
  >
    <!-- 自定义状态列 -->
    <template #status="{ row }">
      <el-tag :type="getStatusType(row.status)">
        {{ row.status }}
      </el-tag>
    </template>
    
    <!-- 自定义操作 -->
    <template #actions="{ row }">
      <el-button
        size="small"
        type="primary"
        @click="handleCustomAction(row)"
      >
        自定义操作
      </el-button>
    </template>
    
    <!-- 工具栏左侧插槽 -->
    <template #toolbar-left>
      <el-button type="primary" @click="handleAdd">
        新增
      </el-button>
    </template>
    
    <!-- 空状态插槽 -->
    <template #empty>
      <div class="custom-empty">
        <i class="el-icon-data-analysis" />
        <p>暂无数据,点击上方按钮添加</p>
      </div>
    </template>
  </ResponsiveTable>
</template>

<script>
import { ref, onMounted } from 'vue'
import ResponsiveTable from '@/components/table/ResponsiveTable.vue'

export default {
  components: {
    ResponsiveTable
  },
  
  setup() {
    // 表格数据
    const tableData = ref([])
    const total = ref(0)
    
    // 列配置
    const columns = ref([
      {
        prop: 'id',
        label: 'ID',
        width: 80,
        fixed: 'left',
        hideOnMobile: true // 移动端隐藏ID列
      },
      {
        prop: 'name',
        label: '名称',
        minWidth: 120,
        fixedOnMobile: 'left' // 移动端固定
      },
      {
        prop: 'category',
        label: '分类',
        width: 100,
        hideOnTablet: true // 平板隐藏
      },
      {
        prop: 'price',
        label: '价格',
        width: 100,
        align: 'right',
        formatter: (row) => `¥${row.price}`
      },
      {
        prop: 'stock',
        label: '库存',
        width: 80,
        align: 'center'
      },
      {
        prop: 'status',
        label: '状态',
        width: 100,
        slot: 'status', // 使用自定义插槽
        hideOnMobile: true
      },
      {
        prop: 'createdAt',
        label: '创建时间',
        width: 150,
        hideOnMobile: true,
        hideOnTablet: true
      }
    ])
    
    // 默认操作配置
    const defaultActions = ref({
      view: {
        text: '查看',
        title: '查看详情',
        handler: (row) => handleView(row)
      },
      edit: {
        text: '编辑',
        title: '编辑项目',
        handler: (row) => handleEdit(row)
      },
      delete: {
        text: '删除',
        title: '删除项目',
        handler: (row) => handleDelete(row)
      }
    })
    
    // 获取表格数据
    const fetchTableData = async (page = 1, size = 10) => {
      try {
        // 模拟API调用
        const response = await mockApi.fetchData(page, size)
        tableData.value = response.data
        total.value = response.total
      } catch (error) {
        console.error('获取数据失败:', error)
      }
    }
    
    // 事件处理
    const handleSelectionChange = (selection) => {
      console.log('选中行:', selection)
    }
    
    const handleView = (row) => {
      console.log('查看:', row)
    }
    
    const handleEdit = (row) => {
      console.log('编辑:', row)
    }
    
    const handleDelete = (row) => {
      console.log('删除:', row)
    }
    
    const handleAdd = () => {
      console.log('新增项目')
    }
    
    const handleCustomAction = (row) => {
      console.log('自定义操作:', row)
    }
    
    const handlePageSizeChange = (size) => {
      console.log('每页大小:', size)
      fetchTableData(1, size)
    }
    
    const handlePageChange = (page) => {
      console.log('当前页:', page)
      fetchTableData(page)
    }
    
    // 状态类型映射
    const getStatusType = (status) => {
      const typeMap = {
        'active': 'success',
        'inactive': 'info',
        'pending': 'warning',
        'rejected': 'danger'
      }
      return typeMap[status] || 'info'
    }
    
    onMounted(() => {
      fetchTableData()
    })
    
    return {
      tableData,
      total,
      columns,
      defaultActions,
      handleSelectionChange,
      handleView,
      handleEdit,
      handleDelete,
      handleAdd,
      handleCustomAction,
      handlePageSizeChange,
      handlePageChange,
      getStatusType
    }
  }
}
</script>

3.2 表单组件响应式优化

3.2.1 表单组件自适应实现

vue 复制代码
<!-- src/components/form/ResponsiveForm.vue -->
<template>
  <div 
    :class="[
      'responsive-form',
      {
        'responsive-form--inline': layout === 'inline',
        'responsive-form--vertical': layout === 'vertical',
        'responsive-form--horizontal': layout === 'horizontal',
        'responsive-form--disabled': disabled,
        'responsive-form--loading': loading,
        [`responsive-form--${breakpoint}`]: true,
        [`responsive-form--label-${labelPosition}`]: labelPosition
      }
    ]"
    :style="formStyle"
  >
    <!-- 表单标题 -->
    <div v-if="title || $slots.title" class="responsive-form__header">
      <slot name="title">
        <h3 class="responsive-form__title">{{ title }}</h3>
      </slot>
    </div>
    
    <!-- 表单内容 -->
    <div class="responsive-form__content">
      <slot />
      
      <!-- 自动生成的表单项 -->
      <template v-if="fields && fields.length > 0">
        <div
          v-for="(field, index) in visibleFields"
          :key="field.name || index"
          :class="[
            'form-field',
            `form-field--${field.type || 'input'}`,
            {
              'form-field--required': field.required,
              'form-field--hidden': field.hidden,
              'form-field--full-width': field.span === 24 || getFieldSpan(field) === 24
            }
          ]"
          :style="getFieldStyle(field)"
        >
          <!-- 标签 -->
          <label
            v-if="field.label && showLabel"
            :for="field.name"
            :class="[
              'form-field__label',
              {
                'form-field__label--required': field.required,
                'form-field__label--top': labelPosition === 'top',
                'form-field__label--left': labelPosition === 'left',
                'form-field__label--right': labelPosition === 'right'
              }
            ]"
            :style="labelStyle"
          >
            {{ field.label }}
            <span v-if="field.tooltip" class="form-field__tooltip">
              <el-tooltip :content="field.tooltip" placement="top">
                <i class="el-icon-question" />
              </el-tooltip>
            </span>
          </label>
          
          <!-- 表单项内容 -->
          <div class="form-field__control">
            <!-- 文本输入框 -->
            <el-input
              v-if="field.type === 'input'"
              v-model="formData[field.name]"
              :placeholder="field.placeholder || `请输入${field.label}`"
              :clearable="field.clearable !== false"
              :disabled="field.disabled || disabled"
              :readonly="field.readonly"
              :maxlength="field.maxlength"
              :show-word-limit="field.showWordLimit"
              :type="field.inputType || 'text'"
              :size="fieldSize"
              @change="handleFieldChange(field)"
              @blur="handleFieldBlur(field)"
            >
              <!-- 前置内容 -->
              <template v-if="field.prepend" #prepend>
                <span v-if="typeof field.prepend === 'string'">{{ field.prepend }}</span>
                <component v-else :is="field.prepend" />
              </template>
              
              <!-- 后置内容 -->
              <template v-if="field.append" #append>
                <span v-if="typeof field.append === 'string'">{{ field.append }}</span>
                <component v-else :is="field.append" />
              </template>
              
              <!-- 前缀图标 -->
              <template v-if="field.prefixIcon" #prefix>
                <i :class="field.prefixIcon" />
              </template>
              
              <!-- 后缀图标 -->
              <template v-if="field.suffixIcon" #suffix>
                <i :class="field.suffixIcon" />
              </template>
            </el-input>
            
            <!-- 数字输入框 -->
            <el-input-number
              v-else-if="field.type === 'number'"
              v-model="formData[field.name]"
              :min="field.min"
              :max="field.max"
              :step="field.step"
              :precision="field.precision"
              :controls="field.controls !== false"
              :controls-position="field.controlsPosition"
              :disabled="field.disabled || disabled"
              :placeholder="field.placeholder"
              :size="fieldSize"
              @change="handleFieldChange(field)"
            />
            
            <!-- 选择器 -->
            <el-select
              v-else-if="field.type === 'select'"
              v-model="formData[field.name]"
              :multiple="field.multiple"
              :filterable="field.filterable"
              :clearable="field.clearable !== false"
              :disabled="field.disabled || disabled"
              :placeholder="field.placeholder || `请选择${field.label}`"
              :size="fieldSize"
              @change="handleFieldChange(field)"
            >
              <el-option
                v-for="option in getOptions(field)"
                :key="option.value"
                :label="option.label"
                :value="option.value"
                :disabled="option.disabled"
              />
            </el-select>
            
            <!-- 日期选择器 -->
            <el-date-picker
              v-else-if="field.type === 'date'"
              v-model="formData[field.name]"
              :type="field.dateType || 'date'"
              :format="field.format"
              :value-format="field.valueFormat"
              :clearable="field.clearable !== false"
              :disabled="field.disabled || disabled"
              :placeholder="field.placeholder"
              :size="fieldSize"
              @change="handleFieldChange(field)"
            />
            
            <!-- 时间选择器 -->
            <el-time-picker
              v-else-if="field.type === 'time'"
              v-model="formData[field.name]"
              :format="field.format"
              :value-format="field.valueFormat"
              :clearable="field.clearable !== false"
              :disabled="field.disabled || disabled"
              :placeholder="field.placeholder"
              :size="fieldSize"
              @change="handleFieldChange(field)"
            />
            
            <!-- 开关 -->
            <el-switch
              v-else-if="field.type === 'switch'"
              v-model="formData[field.name]"
              :active-text="field.activeText"
              :inactive-text="field.inactiveText"
              :active-value="field.activeValue !== undefined ? field.activeValue : true"
              :inactive-value="field.inactiveValue !== undefined ? field.inactiveValue : false"
              :disabled="field.disabled || disabled"
              :size="fieldSize"
              @change="handleFieldChange(field)"
            />
            
            <!-- 单选框组 -->
            <el-radio-group
              v-else-if="field.type === 'radio'"
              v-model="formData[field.name]"
              :disabled="field.disabled || disabled"
              :size="fieldSize"
              @change="handleFieldChange(field)"
            >
              <el-radio
                v-for="option in getOptions(field)"
                :key="option.value"
                :label="option.value"
                :disabled="option.disabled"
              >
                {{ option.label }}
              </el-radio>
            </el-radio-group>
            
            <!-- 复选框组 -->
            <el-checkbox-group
              v-else-if="field.type === 'checkbox'"
              v-model="formData[field.name]"
              :disabled="field.disabled || disabled"
              :size="fieldSize"
              @change="handleFieldChange(field)"
            >
              <el-checkbox
                v-for="option in getOptions(field)"
                :key="option.value"
                :label="option.value"
                :disabled="option.disabled"
              >
                {{ option.label }}
              </el-checkbox>
            </el-checkbox-group>
            
            <!-- 滑块 -->
            <el-slider
              v-else-if="field.type === 'slider'"
              v-model="formData[field.name]"
              :min="field.min"
              :max="field.max"
              :step="field.step"
              :show-stops="field.showStops"
              :show-input="field.showInput"
              :disabled="field.disabled || disabled"
              :size="fieldSize"
              @change="handleFieldChange(field)"
            />
            
            <!-- 评分 -->
            <el-rate
              v-else-if="field.type === 'rate'"
              v-model="formData[field.name]"
              :max="field.max || 5"
              :allow-half="field.allowHalf"
              :show-text="field.showText"
              :disabled="field.disabled || disabled"
              :size="fieldSize"
              @change="handleFieldChange(field)"
            />
            
            <!-- 颜色选择器 -->
            <el-color-picker
              v-else-if="field.type === 'color'"
              v-model="formData[field.name]"
              :show-alpha="field.showAlpha"
              :predefine="field.predefine"
              :disabled="field.disabled || disabled"
              :size="fieldSize"
              @change="handleFieldChange(field)"
            />
            
            <!-- 上传组件 -->
            <el-upload
              v-else-if="field.type === 'upload'"
              v-model:file-list="formData[field.name]"
              :action="field.action"
              :multiple="field.multiple"
              :limit="field.limit"
              :accept="field.accept"
              :list-type="field.listType || 'text'"
              :disabled="field.disabled || disabled"
              :size="fieldSize"
              @change="handleFieldChange(field)"
            >
              <el-button :size="fieldSize" :disabled="field.disabled || disabled">
                <i class="el-icon-upload" />
                点击上传
              </el-button>
            </el-upload>
            
            <!-- 自定义组件 -->
            <component
              v-else-if="field.component"
              :is="field.component"
              v-model="formData[field.name]"
              v-bind="field.props"
              :size="fieldSize"
              :disabled="field.disabled || disabled"
              @change="handleFieldChange(field)"
            />
            
            <!-- 错误提示 -->
            <div
              v-if="fieldErrors[field.name]"
              class="form-field__error"
            >
              {{ fieldErrors[field.name] }}
            </div>
            
            <!-- 帮助文本 -->
            <div
              v-if="field.help"
              class="form-field__help"
            >
              {{ field.help }}
            </div>
          </div>
        </div>
      </template>
    </div>
    
    <!-- 表单操作 -->
    <div v-if="showActions" class="responsive-form__actions">
      <slot name="actions">
        <el-button
          type="primary"
          :loading="loading"
          :disabled="disabled"
          @click="handleSubmit"
        >
          {{ submitText }}
        </el-button>
        
        <el-button
          v-if="showReset"
          @click="handleReset"
          :disabled="loading || disabled"
        >
          {{ resetText }}
        </el-button>
        
        <el-button
          v-if="showCancel"
          @click="handleCancel"
          :disabled="loading"
        >
          {{ cancelText }}
        </el-button>
      </slot>
    </div>
  </div>
</template>

<script>
import { ref, computed, watch, provide, onMounted, onUnmounted } from 'vue'
import {
  ElInput,
  ElInputNumber,
  ElSelect,
  ElOption,
  ElDatePicker,
  ElTimePicker,
  ElSwitch,
  ElRadioGroup,
  ElRadio,
  ElCheckboxGroup,
  ElCheckbox,
  ElSlider,
  ElRate,
  ElColorPicker,
  ElUpload,
  ElButton,
  ElTooltip
} from 'element-plus'

export default {
  name: 'ResponsiveForm',
  
  components: {
    ElInput,
    ElInputNumber,
    ElSelect,
    ElOption,
    ElDatePicker,
    ElTimePicker,
    ElSwitch,
    ElRadioGroup,
    ElRadio,
    ElCheckboxGroup,
    ElCheckbox,
    ElSlider,
    ElRate,
    ElColorPicker,
    ElUpload,
    ElButton,
    ElTooltip
  },
  
  props: {
    // 表单配置
    modelValue: {
      type: Object,
      default: () => ({})
    },
    fields: {
      type: Array,
      default: () => []
    },
    rules: {
      type: Object,
      default: () => ({})
    },
    
    // 布局配置
    layout: {
      type: String,
      default: 'horizontal',
      validator: (value) => ['horizontal', 'vertical', 'inline'].includes(value)
    },
    labelPosition: {
      type: String,
      default: 'right',
      validator: (value) => ['left', 'right', 'top'].includes(value)
    },
    labelWidth: {
      type: [String, Number],
      default: 'auto'
    },
    showLabel: {
      type: Boolean,
      default: true
    },
    inline: {
      type: Boolean,
      default: false
    },
    span: {
      type: Number,
      default: 24
    },
    gutter: {
      type: Number,
      default: 16
    },
    
    // 显示配置
    title: {
      type: String,
      default: ''
    },
    showActions: {
      type: Boolean,
      default: true
    },
    showReset: {
      type: Boolean,
      default: true
    },
    showCancel: {
      type: Boolean,
      default: false
    },
    submitText: {
      type: String,
      default: '提交'
    },
    resetText: {
      type: String,
      default: '重置'
    },
    cancelText: {
      type: String,
      default: '取消'
    },
    
    // 状态控制
    disabled: {
      type: Boolean,
      default: false
    },
    loading: {
      type: Boolean,
      default: false
    },
    readonly: {
      type: Boolean,
      default: false
    },
    
    // 响应式配置
    responsive: {
      type: Boolean,
      default: true
    },
    mobileBreakpoint: {
      type: Number,
      default: 768
    },
    tabletBreakpoint: {
      type: Number,
      default: 1024
    },
    autoAdjustLayout: {
      type: Boolean,
      default: true
    },
    
    // 验证配置
    validateOnChange: {
      type: Boolean,
      default: true
    },
    validateOnBlur: {
      type: Boolean,
      default: true
    },
    showValidationMessage: {
      type: Boolean,
      default: true
    }
  },
  
  emits: [
    'update:modelValue',
    'submit',
    'reset',
    'cancel',
    'change',
    'validate',
    'field-change',
    'field-blur'
  ],
  
  setup(props, { emit }) {
    // 响应式状态
    const breakpoint = ref('desktop')
    const formData = ref({ ...props.modelValue })
    const fieldErrors = ref({})
    const isSubmitting = ref(false)
    
    // 响应式断点检测
    const updateBreakpoint = () => {
      const width = window.innerWidth
      
      if (width < props.mobileBreakpoint) {
        breakpoint.value = 'mobile'
      } else if (width < props.tabletBreakpoint) {
        breakpoint.value = 'tablet'
      } else {
        breakpoint.value = 'desktop'
      }
    }
    
    // 设备类型计算属性
    const isMobile = computed(() => breakpoint.value === 'mobile')
    const isTablet = computed(() => breakpoint.value === 'tablet')
    const isDesktop = computed(() => breakpoint.value === 'desktop')
    
    // 计算可见字段
    const visibleFields = computed(() => {
      return props.fields.filter(field => {
        // 隐藏字段
        if (field.hidden) return false
        
        // 响应式隐藏
        if (isMobile.value && field.hideOnMobile) return false
        if (isTablet.value && field.hideOnTablet) return false
        
        // 条件显示
        if (field.showIf && typeof field.showIf === 'function') {
          return field.showIf(formData.value)
        }
        
        return true
      })
    })
    
    // 计算表单布局
    const computedLayout = computed(() => {
      if (!props.autoAdjustLayout) return props.layout
      
      if (isMobile.value) return 'vertical'
      if (isTablet.value) return 'vertical'
      return props.layout
    })
    
    const computedLabelPosition = computed(() => {
      if (!props.autoAdjustLayout) return props.labelPosition
      
      if (isMobile.value) return 'top'
      if (isTablet.value) return 'top'
      return props.labelPosition
    })
    
    // 计算字段尺寸
    const fieldSize = computed(() => {
      if (isMobile.value) return 'small'
      if (isTablet.value) return 'default'
      return 'default'
    })
    
    // 计算标签宽度
    const computedLabelWidth = computed(() => {
      if (props.labelWidth === 'auto') {
        if (isMobile.value) return '100%'
        if (isTablet.value) return '120px'
        return '150px'
      }
      return props.labelWidth
    })
    
    // 表单样式
    const formStyle = computed(() => {
      const styles = {}
      
      if (props.inline) {
        styles.display = 'flex'
        styles.flexWrap = 'wrap'
        styles.alignItems = 'flex-start'
        styles.gap = `${props.gutter}px`
      }
      
      return styles
    })
    
    // 标签样式
    const labelStyle = computed(() => {
      if (computedLabelPosition.value !== 'left') return {}
      
      return {
        width: typeof computedLabelWidth.value === 'number' 
          ? `${computedLabelWidth.value}px` 
          : computedLabelWidth.value,
        textAlign: computedLabelPosition.value === 'left' ? 'right' : 'left'
      }
    })
    
    // 计算字段跨度
    const getFieldSpan = (field) => {
      if (field.span) return field.span
      
      // 响应式跨度
      if (isMobile.value && field.mobileSpan) return field.mobileSpan
      if (isTablet.value && field.tabletSpan) return field.tabletSpan
      
      // 默认值
      if (isMobile.value) return 24 // 移动端单列
      if (isTablet.value) return field.tabletSpan || 12 // 平板端双列
      
      return field.span || props.span
    }
    
    // 获取字段样式
    const getFieldStyle = (field) => {
      const span = getFieldSpan(field)
      const isFullWidth = span === 24 || field.fullWidth
      
      const styles = {}
      
      if (props.inline) {
        styles.flex = `0 0 calc(${(span / 24) * 100}% - ${props.gutter}px)`
        styles.maxWidth = `calc(${(span / 24) * 100}% - ${props.gutter}px)`
      } else if (computedLayout.value === 'horizontal') {
        if (computedLabelPosition.value === 'left') {
          styles.display = 'flex'
          styles.alignItems = field.type === 'switch' ? 'center' : 'flex-start'
          styles.marginBottom = '20px'
        }
      }
      
      // 移动端优化
      if (isMobile.value) {
        styles.marginBottom = '16px'
      }
      
      return styles
    }
    
    // 获取选项列表
    const getOptions = (field) => {
      if (Array.isArray(field.options)) {
        return field.options
      }
      
      if (typeof field.options === 'function') {
        return field.options(formData.value)
      }
      
      return []
    }
    
    // 验证表单
    const validateForm = async () => {
      const errors = {}
      let isValid = true
      
      // 验证每个字段
      for (const field of visibleFields.value) {
        if (field.rules || props.rules[field.name]) {
          const rules = field.rules || props.rules[field.name]
          const value = formData.value[field.name]
          
          for (const rule of Array.isArray(rules) ? rules : [rules]) {
            // 必填验证
            if (rule.required && (value === undefined || value === null || value === '')) {
              errors[field.name] = rule.message || `${field.label}不能为空`
              isValid = false
              break
            }
            
            // 正则验证
            if (rule.pattern && !rule.pattern.test(value)) {
              errors[field.name] = rule.message || `${field.label}格式不正确`
              isValid = false
              break
            }
            
            // 自定义验证函数
            if (rule.validator && typeof rule.validator === 'function') {
              const result = rule.validator(value, formData.value)
              if (result === false || typeof result === 'string') {
                errors[field.name] = result || rule.message || `${field.label}验证失败`
                isValid = false
                break
              }
            }
          }
        }
      }
      
      fieldErrors.value = errors
      emit('validate', { isValid, errors })
      
      return { isValid, errors }
    }
    
    // 验证单个字段
    const validateField = (field) => {
      if (!props.validateOnChange) return
      
      const rules = field.rules || props.rules[field.name]
      if (!rules) return
      
      const value = formData.value[field.name]
      const fieldRules = Array.isArray(rules) ? rules : [rules]
      
      for (const rule of fieldRules) {
        // 必填验证
        if (rule.required && (value === undefined || value === null || value === '')) {
          fieldErrors.value[field.name] = rule.message || `${field.label}不能为空`
          return false
        }
        
        // 正则验证
        if (rule.pattern && !rule.pattern.test(value)) {
          fieldErrors.value[field.name] = rule.message || `${field.label}格式不正确`
          return false
        }
        
        // 自定义验证
        if (rule.validator && typeof rule.validator === 'function') {
          const result = rule.validator(value, formData.value)
          if (result === false || typeof result === 'string') {
            fieldErrors.value[field.name] = result || rule.message || `${field.label}验证失败`
            return false
          }
        }
      }
      
      // 验证通过,清除错误
      delete fieldErrors.value[field.name]
      return true
    }
    
    // 事件处理
    const handleFieldChange = (field) => {
      emit('field-change', { field, value: formData.value[field.name] })
      emit('change', formData.value)
      emit('update:modelValue', formData.value)
      
      // 触发验证
      if (props.validateOnChange) {
        validateField(field)
      }
    }
    
    const handleFieldBlur = (field) => {
      emit('field-blur', { field, value: formData.value[field.name] })
      
      // 触发验证
      if (props.validateOnBlur) {
        validateField(field)
      }
    }
    
    const handleSubmit = async () => {
      // 验证表单
      const { isValid } = await validateForm()
      
      if (!isValid) {
        return
      }
      
      isSubmitting.value = true
      
      try {
        await emit('submit', formData.value)
      } finally {
        isSubmitting.value = false
      }
    }
    
    const handleReset = () => {
      formData.value = { ...props.modelValue }
      fieldErrors.value = {}
      emit('reset', formData.value)
    }
    
    const handleCancel = () => {
      emit('cancel')
    }
    
    // 初始化表单数据
    const initFormData = () => {
      const initialData = { ...props.modelValue }
      
      // 为每个字段设置初始值
      props.fields.forEach(field => {
        if (field.name && initialData[field.name] === undefined) {
          initialData[field.name] = field.defaultValue !== undefined 
            ? field.defaultValue 
            : null
        }
      })
      
      formData.value = initialData
    }
    
    // 监听数据变化
    watch(() => props.modelValue, (newValue) => {
      formData.value = { ...newValue }
    }, { deep: true })
    
    // 提供表单上下文
    provide('formContext', {
      formData,
      fieldErrors,
      disabled: computed(() => props.disabled),
      readonly: computed(() => props.readonly),
      loading: computed(() => props.loading || isSubmitting.value),
      validateField,
      getFieldSpan
    })
    
    // 生命周期
    onMounted(() => {
      updateBreakpoint()
      initFormData()
      window.addEventListener('resize', updateBreakpoint)
    })
    
    onUnmounted(() => {
      window.removeEventListener('resize', updateBreakpoint)
    })
    
    return {
      // 状态
      breakpoint,
      formData,
      fieldErrors,
      
      // 计算属性
      isMobile,
      isTablet,
      isDesktop,
      visibleFields,
      computedLayout,
      computedLabelPosition,
      fieldSize,
      computedLabelWidth,
      formStyle,
      labelStyle,
      
      // 方法
      getFieldSpan,
      getFieldStyle,
      getOptions,
      handleFieldChange,
      handleFieldBlur,
      handleSubmit,
      handleReset,
      handleCancel
    }
  }
}
</script>

<style scoped>
.responsive-form {
  width: 100%;
  box-sizing: border-box;
}

/* 表单头部 */
.responsive-form__header {
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--el-border-color-light);
}

.responsive-form__title {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
  color: var(--el-text-color-primary);
}

/* 表单内容 */
.responsive-form__content {
  margin-bottom: 24px;
}

/* 表单项样式 */
.form-field {
  position: relative;
  margin-bottom: 20px;
}

.form-field--hidden {
  display: none !important;
}

.form-field--full-width {
  width: 100%;
}

/* 标签样式 */
.form-field__label {
  display: block;
  margin-bottom: 8px;
  font-size: 14px;
  font-weight: 500;
  color: var(--el-text-color-primary);
  line-height: 1.5;
}

.form-field__label--required::before {
  content: '*';
  color: var(--el-color-danger);
  margin-right: 4px;
}

.form-field__label--top {
  display: block;
}

.form-field__label--left {
  display: inline-block;
  margin-right: 12px;
  text-align: right;
  vertical-align: middle;
  line-height: 32px;
}

.form-field__label--right {
  display: inline-block;
  margin-left: 12px;
  text-align: left;
  vertical-align: middle;
  line-height: 32px;
}

.form-field__tooltip {
  margin-left: 4px;
  color: var(--el-text-color-secondary);
  cursor: help;
}

/* 表单控件容器 */
.form-field__control {
  flex: 1;
  min-width: 0;
}

/* 错误提示 */
.form-field__error {
  margin-top: 4px;
  font-size: 12px;
  color: var(--el-color-danger);
  line-height: 1.5;
}

/* 帮助文本 */
.form-field__help {
  margin-top: 4px;
  font-size: 12px;
  color: var(--el-text-color-secondary);
  line-height: 1.5;
}

/* 表单操作 */
.responsive-form__actions {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  padding-top: 20px;
  border-top: 1px solid var(--el-border-color-light);
}

/* 移动端优化 */
@media (max-width: 767px) {
  .responsive-form--mobile {
    padding: 16px;
  }
  
  .responsive-form__header {
    margin-bottom: 20px;
    padding-bottom: 12px;
  }
  
  .responsive-form__title {
    font-size: 16px;
  }
  
  .form-field {
    margin-bottom: 16px;
  }
  
  .form-field__label {
    font-size: 13px;
    margin-bottom: 6px;
  }
  
  .responsive-form__actions {
    flex-direction: column;
    gap: 8px;
  }
  
  .responsive-form__actions .el-button {
    width: 100%;
    margin: 0;
  }
  
  /* 移动端触摸优化 */
  .el-input,
  .el-select,
  .el-date-picker,
  .el-time-picker {
    --el-component-size: 44px;
  }
  
  .el-input__inner,
  .el-select__wrapper,
  .el-date-picker__editor {
    height: 44px !important;
    min-height: 44px !important;
    line-height: 44px !important;
  }
  
  .el-button {
    min-height: 44px !important;
    padding: 12px 20px !important;
  }
  
  .el-radio,
  .el-checkbox {
    min-height: 44px !important;
    line-height: 44px !important;
  }
  
  .el-radio__inner,
  .el-checkbox__inner {
    transform: scale(1.2);
  }
}

/* 平板优化 */
@media (min-width: 768px) and (max-width: 1023px) {
  .responsive-form--tablet {
    padding: 24px;
  }
  
  .responsive-form__title {
    font-size: 17px;
  }
  
  /* 平板端双列布局 */
  .responsive-form__content {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 20px;
  }
  
  .form-field {
    margin-bottom: 0;
  }
  
  .form-field--full-width {
    grid-column: 1 / -1;
  }
  
  /* 水平布局调整 */
  .responsive-form--horizontal .form-field {
    display: flex;
    align-items: center;
  }
  
  .responsive-form--horizontal .form-field__label {
    margin-bottom: 0;
    margin-right: 12px;
    width: 120px;
    text-align: right;
    flex-shrink: 0;
  }
  
  .responsive-form--horizontal .form-field__control {
    flex: 1;
  }
}

/* 桌面端优化 */
@media (min-width: 1024px) {
  .responsive-form--desktop {
    padding: 32px;
  }
  
  .responsive-form__title {
    font-size: 20px;
  }
  
  /* 桌面端多列布局 */
  .responsive-form__content {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 24px;
  }
  
  .form-field {
    margin-bottom: 0;
  }
  
  .form-field--full-width {
    grid-column: 1 / -1;
  }
  
  /* 水平布局优化 */
  .responsive-form--horizontal .form-field {
    display: flex;
    align-items: flex-start;
  }
  
  .responsive-form--horizontal .form-field__label {
    margin-bottom: 0;
    margin-right: 16px;
    width: 150px;
    text-align: right;
    flex-shrink: 0;
    line-height: 32px;
  }
  
  .responsive-form--horizontal .form-field__control {
    flex: 1;
  }
}

/* 行内表单优化 */
.responsive-form--inline {
  padding: 0;
  border: none;
}

.responsive-form--inline .form-field {
  margin-bottom: 0;
}

/* 垂直表单优化 */
.responsive-form--vertical .form-field {
  display: block;
}

.responsive-form--vertical .form-field__label {
  display: block;
  text-align: left;
  width: 100%;
}

/* 禁用状态 */
.responsive-form--disabled .form-field__control :deep(*) {
  opacity: 0.6;
  cursor: not-allowed;
}

/* 加载状态 */
.responsive-form--loading {
  position: relative;
}

.responsive-form--loading::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(255, 255, 255, 0.7);
  z-index: 10;
  pointer-events: none;
}

/* 触摸设备优化 */
@media (hover: none) and (pointer: coarse) {
  .responsive-form {
    --touch-target-size: 44px;
  }
  
  /* 增大触摸目标 */
  .el-button,
  [role="button"] {
    min-height: var(--touch-target-size) !important;
    min-width: var(--touch-target-size) !important;
  }
  
  /* 优化输入框 */
  input,
  textarea,
  select {
    font-size: 16px !important;
    min-height: var(--touch-target-size) !important;
  }
  
  /* 移除 hover 效果 */
  .el-button:hover {
    background-color: inherit !important;
    color: inherit !important;
  }
  
  /* 用 active 状态替代 */
  .el-button:active {
    opacity: 0.8;
    transform: scale(0.98);
  }
}

/* 平滑过渡 */
.responsive-form,
.form-field,
.form-field__label,
.form-field__control {
  transition: all 0.3s ease;
}
</style>
相关推荐
老歌老听老掉牙1 天前
从战场到商场:最优化算法如何用数学重塑世界?
python·算法·最优化
weixin_440730501 天前
java面向对象OPP-三大特性
java·开发语言·python
七夜zippoe1 天前
Python多进程编程实战:彻底突破GIL限制的完整指南
python·编程·多进程·process·gil
Amelia1111111 天前
day43
python
不加糖4351 天前
设计模式 -- 适配器 & 策略模式
python·设计模式
2401_841495641 天前
【机器学习】深度信念网络(DBN)
人工智能·python·深度学习·神经网络·机器学习·无监督预训练·有监督微调
0和1的舞者1 天前
字典与文件操作全解析
python·学习
Dxy12393102161 天前
报错:OSError: [WinError 1455] 页面文件太小,无法完成操作
python
好汉学技术1 天前
Scrapy框架数据存储完全指南(实操为主,易落地)
python