Files
vibe_coding/.cursor/rules/references/018-responsive-deep.md
2026-03-05 21:27:11 +08:00

10 KiB
Raw Blame History

018-responsive.mdc (Deep Reference)

该文件为原始详细规范归档,供 Tier 3 按需读取。


📱 Responsive Design & Multi-Device Standards

断点体系(与 Tailwind 统一)

⚠️ 双前端区分:本文件中的 Element Plus 移动端适配内容仅适用于管理端 (Case-Database-Frontend-admin/)。 用户端 (Case-Database-Frontend-user/) 使用 Headless UI + Tailwind CSS禁止引入 Element Plus

断点 Tailwind 前缀 最小宽度 目标设备
默认 (无前缀) 0px 手机竖屏 (< 640px)
sm sm: 640px 手机横屏 / 小平板
md md: 768px 平板竖屏 (iPad)
lg lg: 1024px 平板横屏 / 小屏笔记本
xl xl: 1280px 桌面
2xl 2xl: 1536px 大屏桌面 / 4K

原则移动优先Mobile-First— 先写手机样式,用断点向上覆盖。

<!-- ✅ 移动优先:默认手机,逐步增强 -->
<div class="p-4 md:p-6 xl:p-8 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
  <!-- 手机 1列 → 平板 2列 → 桌面 3列 -->
</div>

<!-- ❌ 桌面优先禁用max-* 限制往下适配 -->
<div class="grid grid-cols-3 max-md:grid-cols-1">...</div>

布局组件响应式模式

侧边栏布局(后台管理系统标准模式)

<!-- src/layouts/AppLayout.vue -->
<template>
  <div class="flex h-screen overflow-hidden bg-gray-50 dark:bg-gray-900">
    <!-- 遮罩层移动端打开侧边栏时显示 -->
    <Transition name="fade">
      <div
        v-if="isMobile && sidebarOpen"
        class="fixed inset-0 z-20 bg-black/50"
        @click="sidebarOpen = false"
      />
    </Transition>

    <!-- 侧边栏桌面固定移动端抽屉式 -->
    <aside
      :class="[
        'fixed lg:relative z-30 h-full transition-transform duration-300',
        'w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700',
        isMobile && !sidebarOpen ? '-translate-x-full' : 'translate-x-0',
      ]"
    >
      <ArtSidebarMenu />
    </aside>

    <!-- 主内容区 -->
    <div class="flex-1 flex flex-col min-w-0 overflow-hidden">
      <!-- 顶部导航 -->
      <header class="h-14 md:h-16 flex items-center px-4 md:px-6 border-b bg-white dark:bg-gray-800">
        <!-- 移动端汉堡菜单按钮 -->
        <button
          class="lg:hidden mr-3 p-2 rounded-lg text-gray-500 hover:bg-gray-100"
          aria-label="打开菜单"
          @click="sidebarOpen = !sidebarOpen"
        >
          <el-icon :size="20"><Menu /></el-icon>
        </button>
        <ArtHeader />
      </header>

      <!-- 页面内容 -->
      <main class="flex-1 overflow-auto">
        <div class="p-4 md:p-6 xl:p-8 max-w-screen-2xl mx-auto">
          <router-view />
        </div>
      </main>
    </div>
  </div>
</template>

<script setup>
const { isMobile } = useDevice()
const sidebarOpen = ref(false)

// 路由变化时关闭移动端侧边栏
const route = useRoute()
watch(() => route.path, () => {
  if (isMobile.value) sidebarOpen.value = false
})
</script>

useDevice Composable

// src/composables/useDevice.ts
export function useDevice() {
  const width = ref(window.innerWidth)

  const handleResize = useDebounceFn(() => {
    width.value = window.innerWidth
  }, 100)

  onMounted(() => window.addEventListener('resize', handleResize))
  onUnmounted(() => window.removeEventListener('resize', handleResize))

  return {
    width: readonly(width),
    isMobile: computed(() => width.value < 768),
    isTablet: computed(() => width.value >= 768 && width.value < 1024),
    isDesktop: computed(() => width.value >= 1024),
    isTouch: computed(() => 'ontouchstart' in window),
  }
}

Element Plus 移动端适配(仅管理端)

组件尺寸策略

<script setup>
const { isMobile } = useDevice()
const elSize = computed(() => (isMobile.value ? 'small' : 'default'))
</script>

<template>
  <!--  根据设备自适应 size -->
  <el-form :size="elSize">
    <el-input :size="elSize" />
    <el-button :size="elSize" type="primary">提交</el-button>
  </el-form>

  <!--  表格移动端简化列 -->
  <el-table :data="tableData">
    <el-table-column prop="name" label="名称" min-width="120" />
    <el-table-column v-if="!isMobile" prop="createdAt" label="创建时间" width="160" />
    <el-table-column v-if="!isMobile" prop="status" label="状态" width="100" />
    <!-- 移动端合并展示 -->
    <el-table-column v-if="isMobile" label="详情" min-width="200">
      <template #default="{ row }">
        <div class="text-sm">{{ row.status }} · {{ row.createdAt }}</div>
      </template>
    </el-table-column>
  </el-table>
</template>

对话框适配

<!--  移动端全屏对话框 -->
<el-dialog
  v-model="dialogVisible"
  :fullscreen="isMobile"
  :width="isMobile ? '100%' : '600px'"
  :class="{ 'rounded-t-2xl': isMobile }"
>

分页适配

<!--  移动端简化分页 -->
<el-pagination
  v-model:current-page="currentPage"
  v-model:page-size="pageSize"
  :layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
  :pager-count="isMobile ? 3 : 7"
  :page-sizes="isMobile ? [10, 20] : [10, 20, 50, 100]"
  :total="total"
/>

表单布局

<!--  移动端垂直布局桌面水平布局 -->
<el-form
  :label-position="isMobile ? 'top' : 'right'"
  :label-width="isMobile ? 'auto' : '100px'"
>

触摸与手势优化

最小触摸目标尺寸

// src/assets/styles/touch.scss
// 根据 WCAG 2.5.5,触摸目标最小 44×44px
.touch-target {
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
}

@media (hover: none) and (pointer: coarse) {
  // 触屏设备:增大可点击区域
  .el-button {
    min-height: 44px;
    padding-left: 16px;
    padding-right: 16px;
  }

  .el-input__wrapper {
    min-height: 44px;
  }
}

滑动手势(移动端列表操作)

// src/composables/useSwipeAction.ts
export function useSwipeAction(onDelete: () => void, onEdit: () => void) {
  const startX = ref(0)
  const offsetX = ref(0)
  const THRESHOLD = 80

  function onTouchStart(e: TouchEvent) {
    startX.value = e.touches[0].clientX
  }

  function onTouchMove(e: TouchEvent) {
    const diff = e.touches[0].clientX - startX.value
    offsetX.value = Math.max(-160, Math.min(0, diff)) // 最多滑动 160px
  }

  function onTouchEnd() {
    if (offsetX.value < -THRESHOLD) {
      // 滑过阈值:显示操作按钮
    } else {
      offsetX.value = 0 // 弹回
    }
  }

  return { offsetX, onTouchStart, onTouchMove, onTouchEnd }
}

图片与媒体响应式

<template>
  <!--  响应式图片不同分辨率加载不同尺寸 -->
  <picture>
    <source media="(min-width: 1280px)" srcset="/img/banner-xl.webp" />
    <source media="(min-width: 768px)" srcset="/img/banner-md.webp" />
    <img src="/img/banner-sm.webp" alt="Banner" class="w-full h-auto object-cover" loading="lazy" />
  </picture>

  <!--  使用 Intersection Observer 懒加载 -->
  <img
    v-lazy="imageUrl"
    alt="产品图片"
    class="w-full aspect-square object-cover rounded-lg"
    :class="{ 'animate-pulse bg-gray-200': !isLoaded }"
  />
</template>

字体与文字排版

// 流式字体大小:随屏幕宽度线性变化
:root {
  --font-base: clamp(14px, 1vw + 12px, 16px);
  --font-heading: clamp(20px, 3vw + 10px, 36px);
}

body { font-size: var(--font-base); }
h1 { font-size: var(--font-heading); }

// 行高:移动端更宽松(小屏阅读舒适度)
p {
  line-height: 1.6;

  @media (max-width: 767px) {
    line-height: 1.8;
  }
}

安全区域(刘海屏 / 全面屏)

/* 底部安全区域iOS 全面屏 Home Bar*/
.bottom-nav {
  padding-bottom: env(safe-area-inset-bottom, 16px);
}

/* 顶部安全区域(刘海屏)*/
.top-bar {
  padding-top: env(safe-area-inset-top, 0px);
}
// tailwind.config.ts — 添加安全区域工具类
theme: {
  extend: {
    padding: {
      'safe-bottom': 'env(safe-area-inset-bottom, 16px)',
      'safe-top': 'env(safe-area-inset-top, 0px)',
    },
  },
},

响应式测试矩阵

设备 分辨率 断点 关键功能检查
iPhone SE (3代) 375×667 xs 侧边栏抽屉、表单垂直布局
iPhone 14 Pro 393×852 xs 安全区域、全面屏底部导航
iPhone 14 Pro Max 430×932 sm 横屏布局、键盘遮挡处理
iPad (10代) 820×1180 md 平板双栏、对话框宽度
iPad Pro 12.9" 1024×1366 lg 分页组件、表格完整列
MacBook Air 13" 1280×800 xl 完整侧边栏、桌面表格
4K 显示器 1920×1080 2xl 最大宽度限制、留白

测试脚本Playwright

// tests/responsive.spec.ts
import { test, expect } from '@playwright/test'

const viewports = [
  { name: 'mobile', width: 375, height: 812 },
  { name: 'tablet', width: 768, height: 1024 },
  { name: 'desktop', width: 1280, height: 800 },
]

for (const vp of viewports) {
  test(`dashboard layout on ${vp.name}`, async ({ page }) => {
    await page.setViewportSize({ width: vp.width, height: vp.height })
    await page.goto('/dashboard')

    if (vp.name === 'mobile') {
      // 移动端:侧边栏默认隐藏
      await expect(page.locator('aside')).toHaveCSS('transform', 'matrix(1, 0, 0, 1, -256, 0)')
      // 汉堡按钮可见
      await expect(page.locator('[aria-label="打开菜单"]')).toBeVisible()
    } else {
      // 平板/桌面:侧边栏默认显示
      await expect(page.locator('aside')).toBeVisible()
    }
  })
}

规则

  • 所有新页面必须在 375px / 768px / 1280px 三个断点下验证
  • 禁止使用 px 设置字体大小(用 rem 或 Tailwind 文本类)
  • 触摸目标最小 44×44px
  • 禁止使用 :hover 作为唯一交互反馈(移动端无 hover
  • 管理端:所有 el-dialog 必须处理移动端 fullscreen 属性(用户端使用 Headless UI Dialog
  • 图片必须设置 loading="lazy"aspect-ratio(防止布局抖动 CLS
  • 横屏landscape模式下的布局需专门测试