388 lines
10 KiB
Markdown
388 lines
10 KiB
Markdown
# 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)— 先写手机样式,用断点向上覆盖。
|
||
|
||
```html
|
||
<!-- ✅ 移动优先:默认手机,逐步增强 -->
|
||
<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>
|
||
```
|
||
|
||
---
|
||
|
||
## 布局组件响应式模式
|
||
|
||
### 侧边栏布局(后台管理系统标准模式)
|
||
|
||
```vue
|
||
<!-- 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
|
||
|
||
```typescript
|
||
// 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 移动端适配(仅管理端)
|
||
|
||
### 组件尺寸策略
|
||
|
||
```vue
|
||
<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>
|
||
```
|
||
|
||
### 对话框适配
|
||
|
||
```vue
|
||
<!-- ✅ 移动端全屏对话框 -->
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:fullscreen="isMobile"
|
||
:width="isMobile ? '100%' : '600px'"
|
||
:class="{ 'rounded-t-2xl': isMobile }"
|
||
>
|
||
```
|
||
|
||
### 分页适配
|
||
|
||
```vue
|
||
<!-- ✅ 移动端简化分页 -->
|
||
<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"
|
||
/>
|
||
```
|
||
|
||
### 表单布局
|
||
|
||
```vue
|
||
<!-- ✅ 移动端垂直布局,桌面水平布局 -->
|
||
<el-form
|
||
:label-position="isMobile ? 'top' : 'right'"
|
||
:label-width="isMobile ? 'auto' : '100px'"
|
||
>
|
||
```
|
||
|
||
---
|
||
|
||
## 触摸与手势优化
|
||
|
||
### 最小触摸目标尺寸
|
||
|
||
```scss
|
||
// 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;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 滑动手势(移动端列表操作)
|
||
|
||
```typescript
|
||
// 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 }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 图片与媒体响应式
|
||
|
||
```vue
|
||
<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>
|
||
```
|
||
|
||
---
|
||
|
||
## 字体与文字排版
|
||
|
||
```scss
|
||
// 流式字体大小:随屏幕宽度线性变化
|
||
: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;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 安全区域(刘海屏 / 全面屏)
|
||
|
||
```css
|
||
/* 底部安全区域(iOS 全面屏 Home Bar)*/
|
||
.bottom-nav {
|
||
padding-bottom: env(safe-area-inset-bottom, 16px);
|
||
}
|
||
|
||
/* 顶部安全区域(刘海屏)*/
|
||
.top-bar {
|
||
padding-top: env(safe-area-inset-top, 0px);
|
||
}
|
||
```
|
||
|
||
```typescript
|
||
// 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)
|
||
|
||
```typescript
|
||
// 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)模式下的布局需专门测试
|