Vue 移动端适配:rem/vw单位与flexible.js
【摘要】 一、引言1.1 移动端适配的重要性在移动互联网时代,移动设备已成为主要的互联网接入方式。据统计,2024年全球移动互联网用户已超过50亿,移动端流量占比超过60%。面对碎片化的屏幕尺寸和多样化的设备分辨率,移动端适配成为前端开发的核心挑战。1.2 移动端适配的技术演进class MobileAdaptationEvolution { static getDevelopmentStage...
一、引言
1.1 移动端适配的重要性
1.2 移动端适配的技术演进
class MobileAdaptationEvolution {
static getDevelopmentStages() {
return {
'1.0 时代': '固定宽度布局,简单媒体查询',
'2.0 时代': '流式布局,百分比单位',
'3.0 时代': '响应式设计,弹性布局',
'4.0 时代': 'REM/VW单位,Viewport适配',
'5.0 时代': '容器查询,智能适配'
};
}
static getTechnologyComparison() {
return {
'REM 适配': {
'兼容性': '⭐⭐⭐⭐⭐',
'精确度': '⭐⭐⭐⭐',
'开发效率': '⭐⭐⭐⭐',
'维护成本': '⭐⭐⭐',
'性能表现': '⭐⭐⭐⭐'
},
'VW 适配': {
'兼容性': '⭐⭐⭐⭐',
'精确度': '⭐⭐⭐⭐⭐',
'开发效率': '⭐⭐⭐⭐⭐',
'维护成本': '⭐⭐',
'性能表现': '⭐⭐⭐⭐⭐'
},
'Flexible.js': {
'兼容性': '⭐⭐⭐⭐⭐',
'精确度': '⭐⭐⭐⭐',
'开发效率': '⭐⭐⭐⭐',
'维护成本': '⭐⭐⭐',
'性能表现': '⭐⭐⭐⭐'
}
};
}
}
1.3 市场数据支持
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
二、技术背景
2.1 核心概念解析
2.1.1 REM单位原理
// REM(Root EM)基于根元素字体大小的相对单位
class REMUnit {
constructor() {
this.baseFontSize = 16; // 默认根字体大小(px)
}
// 计算REM值
calculateREM(pixelValue) {
return pixelValue / this.baseFontSize;
}
// 设置根字体大小
setRootFontSize(newSize) {
document.documentElement.style.fontSize = newSize + 'px';
this.baseFontSize = newSize;
}
// 响应式调整
responsiveAdjustment(designWidth = 750) {
const clientWidth = document.documentElement.clientWidth;
const scale = clientWidth / designWidth;
this.setRootFontSize(scale * 100); // 1rem = 100px(设计稿基准)
}
}
2.1.2 VW单位原理
// VW(Viewport Width)基于视口宽度的相对单位
class VWUnit {
// 计算VW值
calculateVW(pixelValue, designWidth = 750) {
return (pixelValue / designWidth) * 100;
}
// VW到PX的转换
vwToPx(vwValue, viewportWidth) {
return (vwValue / 100) * viewportWidth;
}
// 处理VW的兼容性
handleCompatibility() {
// 检测浏览器支持情况
const isSupported = CSS.supports('width', '1vw');
if (!isSupported) {
this.applyFallback();
}
}
applyFallback() {
// 使用JavaScript polyfill
window.addEventListener('resize', this.vwPolyfill);
}
}
2.1.3 Flexible.js工作机制
// Flexible.js 核心实现原理
class Flexible {
constructor() {
this.dpr = window.devicePixelRatio || 1;
this.docEl = document.documentElement;
this.metaEl = document.querySelector('meta[name="viewport"]');
this.init();
}
init() {
this.setViewport();
this.setBodyFontSize();
this.setRemUnit();
this.bindEvents();
}
// 设置Viewport
setViewport() {
if (this.metaEl) {
this.metaEl.setAttribute('content',
`width=device-width, initial-scale=${1/this.dpr}, maximum-scale=${1/this.dpr}, minimum-scale=${1/this.dpr}, user-scalable=no`);
} else {
const meta = document.createElement('meta');
meta.setAttribute('name', 'viewport');
meta.setAttribute('content',
`width=device-width, initial-scale=${1/this.dpr}, maximum-scale=${1/this.dpr}, minimum-scale=${1/this.dpr}, user-scalable=no`);
document.head.appendChild(meta);
}
}
// 设置REM基准
setRemUnit() {
const width = this.docEl.clientWidth;
const rem = width / 10; // 将屏幕分成10份
this.docEl.style.fontSize = rem + 'px';
}
// 事件绑定
bindEvents() {
window.addEventListener('resize', () => {
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => this.setRemUnit(), 300);
});
window.addEventListener('pageshow', (e) => {
if (e.persisted) this.setRemUnit();
});
}
// 设置Body字体大小(防止继承影响)
setBodyFontSize() {
if (document.body) {
document.body.style.fontSize = (12 * this.dpr) + 'px';
} else {
document.addEventListener('DOMContentLoaded', this.setBodyFontSize);
}
}
}
2.2 技术对比分析
2.2.1 单位特性对比表
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.2.2 应用场景分析
class AdaptationScenario {
static getBestPractices() {
return {
'电商网站': {
'推荐方案': 'REM + Flexible.js',
'理由': '需要精确控制元素尺寸,兼容老设备',
'实现复杂度': '中等'
},
'管理系统': {
'推荐方案': 'VW + 媒体查询',
'理由': '现代浏览器支持好,开发效率高',
'实现复杂度': '低'
},
'移动H5': {
'推荐方案': 'REM + VW 混合',
'理由': '兼顾兼容性和开发体验',
'实现复杂度': '高'
},
'跨端应用': {
'推荐方案': '响应式 + 容器查询',
'理由': '需要适应多种容器环境',
'实现复杂度': '高'
}
};
}
static getPerformanceMetrics() {
return {
'REM方案': {
'加载时间': '100-200ms',
'渲染性能': '优秀',
'内存占用': '低',
'CPU使用率': '低'
},
'VW方案': {
'加载时间': '50-100ms',
'渲染性能': '优秀',
'内存占用': '极低',
'CPU使用率': '极低'
},
'Flexible.js': {
'加载时间': '150-250ms',
'渲染性能': '良好',
'内存占用': '中等',
'CPU使用率': '中等'
}
};
}
}
三、环境准备与配置
3.1 项目初始化配置
3.1.1 Vue项目创建与配置
# 创建Vue项目
vue create mobile-adaptation-demo
# 安装必要依赖
npm install postcss-px-to-viewport postcss-rem-flexible amfe-flexible
npm install lib-flexible -S
3.1.2 项目结构规划
src/
├── assets/
│ ├── styles/
│ │ ├── adaption/ # 适配相关样式
│ │ │ ├── rem.scss # REM适配配置
│ │ │ ├── vw.scss # VW适配配置
│ │ │ └── mixins.scss # 适配混合器
│ │ └── base/
│ │ ├── reset.scss # 样式重置
│ │ └── variables.scss # 样式变量
├── utils/
│ ├── adaption.js # 适配工具函数
│ └── flexible.js # Flexible.js封装
└── views/
├── rem-demo.vue # REM适配示例
├── vw-demo.vue # VW适配示例
└── flexible-demo.vue # Flexible.js示例
3.2 三种适配方案的详细配置
3.2.1 REM适配方案配置
// postcss.config.js - REM配置
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 16, // 基准字体大小
unitPrecision: 5, // 转换精度
propList: ['*'], // 需要转换的属性
selectorBlackList: [], // 忽略的选择器
replace: true,
mediaQuery: false,
minPixelValue: 2, // 最小转换值
exclude: /node_modules/i
}
}
}
// src/assets/styles/adaption/_rem.scss
// REM适配核心样式
:root {
// 基准字体大小设置
font-size: 16px;
@media screen and (min-width: 375px) {
font-size: calc(16px + 2 * (100vw - 375px) / 39); // 375px-414px适配
}
@media screen and (min-width: 414px) {
font-size: 18px; // 414px基准
}
@media screen and (min-width: 768px) {
font-size: 20px; // 平板设备
}
}
// REM工具类
.rem-container {
width: 100%;
max-width: 20rem; // 320px
margin: 0 auto;
padding: 0 1rem; // 16px
}
.rem-button {
height: 2.5rem; // 40px
padding: 0 1.5rem; // 24px
font-size: 0.875rem; // 14px
border-radius: 0.25rem; // 4px
}
// REM响应式混合器
@mixin rem-responsive($property, $values...) {
#{$property}: $values;
// 针对不同屏幕的REM适配
@media screen and (max-width: 320px) {
#{$property}: $values * 0.9;
}
@media screen and (min-width: 414px) and (max-width: 768px) {
#{$property}: $values * 1.1;
}
}
3.2.2 VW适配方案配置
// postcss.config.js - VW配置
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375, // 设计稿宽度
viewportHeight: 667, // 设计稿高度
unitPrecision: 3, // 转换精度
viewportUnit: 'vw', // 转换单位
selectorBlackList: ['.ignore', '.hairlines'], // 忽略类名
minPixelValue: 1, // 最小转换值
mediaQuery: false, // 媒体查询中的px是否转换
exclude: /(\/|\\)(node_modules)(\/|\\)/
}
}
}
// src/assets/styles/adaption/_vw.scss
// VW适配核心样式
:root {
// VW基准设置
--vw-base: 3.75vw; // 基于375px设计稿
}
// VW容器类
.vw-container {
width: 100vw;
max-width: 100vw;
margin: 0 auto;
}
// VW工具函数
@function vw($px) {
@return ($px / 3.75) * 1vw;
}
@function vh($px) {
@return ($px / 6.67) * 1vh;
}
// VW组件样式
.vw-button {
width: vw(345);
height: vw(44);
padding: vw(12) vw(16);
font-size: vw(16);
border-radius: vw(8);
}
// 安全区域适配
.safe-area {
padding-left: constant(safe-area-inset-left);
padding-right: constant(safe-area-inset-right);
padding-bottom: constant(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
3.2.3 Flexible.js方案配置
// src/utils/flexible.js
(function flexible(window, document) {
const docEl = document.documentElement;
const dpr = window.devicePixelRatio || 1;
// 调整body字体大小
function setBodyFontSize() {
if (document.body) {
document.body.style.fontSize = (12 * dpr) + 'px';
} else {
document.addEventListener('DOMContentLoaded', setBodyFontSize);
}
}
setBodyFontSize();
// 设置1rem = viewWidth / 10
function setRemUnit() {
const rem = docEl.clientWidth / 10;
docEl.style.fontSize = rem + 'px';
}
setRemUnit();
// 监听页面重置大小
window.addEventListener('resize', setRemUnit);
window.addEventListener('pageshow', function(e) {
if (e.persisted) {
setRemUnit();
}
});
// 检测0.5px支持
if (dpr >= 2) {
const fakeBody = document.createElement('body');
const testElement = document.createElement('div');
testElement.style.border = '.5px solid transparent';
fakeBody.appendChild(testElement);
docEl.appendChild(fakeBody);
if (testElement.offsetHeight === 1) {
docEl.classList.add('hairlines');
}
docEl.removeChild(fakeBody);
}
}(window, document));
// src/assets/styles/adaption/_flexible.scss
// Flexible.js适配样式
[data-dpr="1"] .dpr-text {
font-size: 12px;
}
[data-dpr="2"] .dpr-text {
font-size: 24px;
}
[data-dpr="3"] .dpr-text {
font-size: 36px;
}
// 1px边框解决方案
.hairlines {
.border-1px {
position: relative;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 1px;
background: #e5e5e5;
transform: scaleY(0.5);
transform-origin: 0 0;
}
}
}
// Flexible.js工具类
.flexible-container {
width: 10rem; // 满屏宽度
margin: 0 auto;
}
.px2rem(@name, @px) {
@{name}: @px / 75 * 1rem;
}
四、详细代码实现
4.1 REM适配完整示例
4.1.1 REM配置工具类
// src/utils/rem-adaption.js
export class REMAdaption {
constructor(designWidth = 750, maxWidth = 768) {
this.designWidth = designWidth;
this.maxWidth = maxWidth;
this.init();
}
init() {
this.setRemUnit();
this.bindEvents();
}
// 设置REM基准
setRemUnit() {
const docEl = document.documentElement;
const clientWidth = Math.min(docEl.clientWidth, this.maxWidth);
const rem = (clientWidth / this.designWidth) * 100;
docEl.style.fontSize = rem + 'px';
// 设置实际字体大小(防止过大)
const actualSize = parseFloat(getComputedStyle(docEl).fontSize);
if (actualSize !== rem) {
docEl.style.fontSize = (rem * rem / actualSize) + 'px';
}
}
// 事件绑定
bindEvents() {
let timer = null;
window.addEventListener('resize', () => {
clearTimeout(timer);
timer = setTimeout(() => this.setRemUnit(), 300);
});
window.addEventListener('pageshow', (e) => {
if (e.persisted) this.setRemUnit();
});
}
// PX转REM工具函数
static px2rem(px, designWidth = 750) {
return (px / designWidth) * 100 + 'rem';
}
// REM转PX工具函数
static rem2px(rem, designWidth = 750) {
return (rem / 100) * designWidth + 'px';
}
}
// 初始化REM适配
export const initREM = (designWidth = 750) => {
return new REMAdaption(designWidth);
};
4.1.2 REM适配Vue组件
<!-- src/views/RemDemo.vue -->
<template>
<div class="rem-demo-container">
<!-- 头部导航 -->
<header class="rem-header">
<div class="rem-nav">
<h1 class="rem-title">REM适配演示</h1>
<nav class="rem-nav-menu">
<a v-for="item in navItems"
:key="item.id"
:class="['rem-nav-item', { active: activeNav === item.id }]"
@click="activeNav = item.id">
{{ item.text }}
</a>
</nav>
</div>
</header>
<!-- 主要内容区域 -->
<main class="rem-main">
<!-- 响应式网格布局 -->
<div class="rem-grid">
<div v-for="card in cardList"
:key="card.id"
class="rem-card"
:style="{ backgroundColor: card.color }">
<div class="rem-card-header">
<h3>{{ card.title }}</h3>
<span class="rem-badge">{{ card.badge }}</span>
</div>
<div class="rem-card-content">
<p>{{ card.content }}</p>
</div>
<div class="rem-card-footer">
<button class="rem-button primary">查看详情</button>
<button class="rem-button secondary">收藏</button>
</div>
</div>
</div>
<!-- 响应式表格 -->
<div class="rem-table-container">
<table class="rem-table">
<thead>
<tr>
<th v-for="col in tableColumns" :key="col.key">{{ col.title }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in tableData" :key="row.id">
<td v-for="col in tableColumns" :key="col.key">{{ row[col.key] }}</td>
</tr>
</tbody>
</table>
</div>
</main>
<!-- 底部信息 -->
<footer class="rem-footer">
<p>© 2024 REM适配演示. 所有尺寸均使用REM单位</p>
</footer>
</div>
</template>
<script>
import { initREM } from '@/utils/rem-adaption';
export default {
name: 'RemDemo',
data() {
return {
activeNav: 'home',
navItems: [
{ id: 'home', text: '首页' },
{ id: 'products', text: '产品' },
{ id: 'about', text: '关于' },
{ id: 'contact', text: '联系' }
],
cardList: [
{
id: 1,
title: '响应式卡片1',
content: '这是一个使用REM单位适配的卡片组件',
badge: 'New',
color: '#f0f8ff'
},
{
id: 2,
title: '响应式卡片2',
content: '在不同设备上保持一致的视觉效果',
badge: 'Hot',
color: '#fff0f5'
},
{
id: 3,
title: '响应式卡片3',
content: '基于根字体大小的相对单位计算',
badge: '推荐',
color: '#f5f5f5'
}
],
tableColumns: [
{ key: 'name', title: '名称' },
{ key: 'size', title: '尺寸(REM)' },
{ key: 'actual', title: '实际像素' }
],
tableData: [
{ id: 1, name: '按钮高度', size: '2.5rem', actual: '40px' },
{ id: 2, name: '卡片内边距', size: '1rem', actual: '16px' },
{ id: 3, name: '字体大小', size: '0.875rem', actual: '14px' }
]
};
},
mounted() {
// 初始化REM适配
initREM(750);
// 监听窗口变化
this.handleResize();
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize);
},
methods: {
handleResize() {
// 可以在这里添加特定的响应式逻辑
console.log('窗口尺寸变化,当前REM基准:', getComputedStyle(document.documentElement).fontSize);
}
}
};
</script>
<style lang="scss" scoped>
// 引入REM适配样式
@import '@/assets/styles/adaption/rem.scss';
.rem-demo-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.rem-header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
.rem-nav {
@include rem-responsive(padding, 0 1rem);
@include rem-responsive(height, 3rem);
display: flex;
justify-content: space-between;
align-items: center;
max-width: 20rem;
margin: 0 auto;
.rem-title {
@include rem-responsive(font-size, 1.125rem);
font-weight: 600;
color: #333;
margin: 0;
}
.rem-nav-menu {
display: flex;
gap: 1.5rem;
.rem-nav-item {
@include rem-responsive(padding, 0.5rem 1rem);
@include rem-responsive(font-size, 0.875rem);
color: #666;
text-decoration: none;
border-radius: 0.5rem;
transition: all 0.3s ease;
cursor: pointer;
&:hover, &.active {
background: #667eea;
color: white;
}
}
}
}
}
.rem-main {
@include rem-responsive(padding, 2rem 1rem);
max-width: 20rem;
margin: 0 auto;
}
.rem-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
gap: 1rem;
margin-bottom: 2rem;
.rem-card {
@include rem-responsive(padding, 1.5rem);
border-radius: 0.75rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
.rem-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
h3 {
@include rem-responsive(font-size, 1rem);
margin: 0;
color: #333;
}
.rem-badge {
@include rem-responsive(padding, 0.25rem 0.5rem);
@include rem-responsive(font-size, 0.75rem);
background: #ff4757;
color: white;
border-radius: 1rem;
}
}
.rem-card-content {
@include rem-responsive(margin-bottom, 1rem);
p {
@include rem-responsive(font-size, 0.875rem);
color: #666;
line-height: 1.5;
margin: 0;
}
}
.rem-card-footer {
display: flex;
gap: 0.5rem;
.rem-button {
@include rem-responsive(padding, 0.5rem 1rem);
@include rem-responsive(font-size, 0.875rem);
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.3s ease;
&.primary {
background: #667eea;
color: white;
&:hover {
background: #5a6fd8;
}
}
&.secondary {
background: transparent;
color: #667eea;
border: 1px solid #667eea;
&:hover {
background: #667eea;
color: white;
}
}
}
}
}
}
.rem-table-container {
overflow-x: auto;
background: white;
border-radius: 0.75rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
.rem-table {
width: 100%;
border-collapse: collapse;
th, td {
@include rem-responsive(padding, 1rem);
text-align: left;
border-bottom: 1px solid #e5e5e5;
}
th {
@include rem-responsive(font-size, 0.875rem);
font-weight: 600;
color: #333;
background: #f8f9fa;
}
td {
@include rem-responsive(font-size, 0.875rem);
color: #666;
}
tr:last-child td {
border-bottom: none;
}
}
}
.rem-footer {
@include rem-responsive(padding, 1.5rem);
text-align: center;
background: rgba(255, 255, 255, 0.95);
border-top: 1px solid rgba(255, 255, 255, 0.2);
p {
@include rem-responsive(font-size, 0.875rem);
color: #666;
margin: 0;
}
}
// 响应式媒体查询
@media (max-width: 768px) {
.rem-grid {
grid-template-columns: 1fr;
}
.rem-nav-menu {
gap: 0.5rem !important;
.rem-nav-item {
padding: 0.25rem 0.5rem !important;
}
}
}
@media (max-width: 480px) {
.rem-nav {
flex-direction: column;
height: auto !important;
padding: 1rem !important;
.rem-nav-menu {
margin-top: 1rem;
flex-wrap: wrap;
justify-content: center;
}
}
}
</style>
4.2 VW适配完整示例
4.2.1 VW配置工具类
// src/utils/vw-adaption.js
export class VWAdaption {
constructor(designWidth = 375, designHeight = 667) {
this.designWidth = designWidth;
this.designHeight = designHeight;
this.init();
}
init() {
this.setViewportMeta();
this.handleOrientationChange();
}
// 设置Viewport Meta
setViewportMeta() {
let viewport = document.querySelector('meta[name="viewport"]');
if (!viewport) {
viewport = document.createElement('meta');
viewport.name = 'viewport';
document.head.appendChild(viewport);
}
viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
}
// 处理横竖屏切换
handleOrientationChange() {
window.addEventListener('orientationchange', () => {
setTimeout(() => {
this.fixViewportScale();
}, 300);
});
}
// 修复视口缩放问题
fixViewportScale() {
const viewport = document.querySelector('meta[name="viewport"]');
if (viewport) {
viewport.content = 'width=device-width, initial-scale=1.0';
}
}
// PX转VW工具函数
static px2vw(px, designWidth = 375) {
return (px / designWidth) * 100 + 'vw';
}
// PX转VH工具函数
static px2vh(px, designHeight = 667) {
return (px / designHeight) * 100 + 'vh';
}
// VW转PX工具函数
static vw2px(vw, viewportWidth) {
return (parseFloat(vw) / 100) * viewportWidth + 'px';
}
// 安全区域适配
static safeArea(property, position = 'bottom') {
return `
${property}: constant(safe-area-inset-${position});
${property}: env(safe-area-inset-${position});
`;
}
}
// CSS-in-JS风格的VW适配函数
export const createVWStyle = (styles, designWidth = 375) => {
const convertedStyles = {};
Object.keys(styles).forEach(key => {
const value = styles[key];
if (typeof value === 'number' && key !== 'zIndex' && key !== 'opacity') {
// 转换数值为VW
convertedStyles[key] = (value / designWidth) * 100 + 'vw';
} else if (typeof value === 'string' && /^\d+px$/.test(value)) {
// 转换px字符串为VW
const pxValue = parseFloat(value);
convertedStyles[key] = (pxValue / designWidth) * 100 + 'vw';
} else {
convertedStyles[key] = value;
}
});
return convertedStyles;
};
4.2.2 VW适配Vue组件
<!-- src/views/VwDemo.vue -->
<template>
<div class="vw-demo-container safe-area">
<!-- 沉浸式头部 -->
<header class="vw-header">
<div class="vw-header-content">
<button class="vw-back-button" @click="handleBack">
<span class="vw-icon">←</span>
</button>
<h1 class="vw-title">VW适配演示</h1>
<button class="vw-menu-button" @click="showMenu = !showMenu">
<span class="vw-icon">⋮</span>
</button>
</div>
<!-- 下拉菜单 -->
<transition name="vw-menu-slide">
<div v-show="showMenu" class="vw-dropdown-menu">
<div v-for="item in menuItems"
:key="item.id"
class="vw-menu-item"
@click="handleMenuClick(item)">
<span class="vw-menu-icon">{{ item.icon }}</span>
<span class="vw-menu-text">{{ item.text }}</span>
</div>
</div>
</transition>
</header>
<!-- 可滚动内容区域 -->
<main class="vw-main">
<!-- 轮播图 -->
<section class="vw-carousel">
<div class="vw-carousel-track"
:style="{ transform: `translateX(-${currentSlide * 100}%)` }">
<div v-for="(slide, index) in carouselSlides"
:key="index"
class="vw-carousel-slide"
:style="{ backgroundImage: `url(${slide.image})` }">
<div class="vw-slide-content">
<h3>{{ slide.title }}</h3>
<p>{{ slide.description }}</p>
</div>
</div>
</div>
<!-- 指示器 -->
<div class="vw-carousel-indicators">
<span v-for="(_, index) in carouselSlides"
:key="index"
:class="['vw-indicator', { active: index === currentSlide }]"
@click="currentSlide = index">
</span>
</div>
</section>
<!-- 功能卡片网格 -->
<section class="vw-feature-grid">
<h2 class="vw-section-title">功能特性</h2>
<div class="vw-grid">
<div v-for="feature in features"
:key="feature.id"
class="vw-feature-card"
@click="handleFeatureClick(feature)">
<div class="vw-feature-icon" :style="{ backgroundColor: feature.color }">
{{ feature.icon }}
</div>
<h4>{{ feature.title }}</h4>
<p>{{ feature.description }}</p>
</div>
</div>
</section>
<!-- 数据统计 -->
<section class="vw-stats-section">
<h2 class="vw-section-title">性能统计</h2>
<div class="vw-stats-grid">
<div v-for="stat in stats"
:key="stat.id"
class="vw-stat-item">
<div class="vw-stat-value">{{ stat.value }}</div>
<div class="vw-stat-label">{{ stat.label }}</div>
</div>
</div>
</section>
</main>
<!-- 底部导航 -->
<footer class="vw-footer safe-area">
<nav class="vw-bottom-nav">
<div v-for="nav in bottomNav"
:key="nav.id"
:class="['vw-nav-item', { active: activeNav === nav.id }]"
@click="activeNav = nav.id">
<span class="vw-nav-icon">{{ nav.icon }}</span>
<span class="vw-nav-text">{{ nav.text }}</span>
</div>
</nav>
</footer>
</div>
</template>
<script>
import { VWAdaption, createVWStyle } from '@/utils/vw-adaption';
export default {
name: 'VwDemo',
data() {
return {
showMenu: false,
currentSlide: 0,
activeNav: 'home',
carouselSlides: [
{
image: 'https://picsum.photos/375/200?random=1',
title: 'VW单位适配',
description: '基于视口宽度的响应式设计'
},
{
image: 'https://picsum.photos/375/200?random=2',
title: '完美比例',
description: '在任何设备上保持设计比例'
},
{
image: 'https://picsum.photos/375/200?random=3',
title: '高性能',
description: '原生CSS单位,渲染性能优秀'
}
],
menuItems: [
{ id: 'settings', icon: '⚙️', text: '设置' },
{ id: 'profile', icon: '👤', text: '个人资料' },
{ id: 'help', icon: '❓', text: '帮助中心' },
{ id: 'feedback', icon: '💬', text: '意见反馈' }
],
features: [
{
id: 1,
icon: '📱',
title: '响应式设计',
description: '完美适配各种移动设备',
color: '#ff6b6b'
},
{
id: 2,
title: '高性能渲染',
description: '基于VW单位的优化渲染',
icon: '⚡',
color: '#4ecdc4'
},
{
id: 3,
icon: '🎨',
title: '设计一致性',
description: '保持设计稿原始比例',
color: '#45b7d1'
},
{
id: 4,
icon: '🔧',
title: '易于维护',
description: '简洁的适配方案',
color: '#96ceb4'
}
],
stats: [
{ id: 1, value: '100%', label: '适配精度' },
{ id: 2, value: '60FPS', label: '渲染帧率' },
{ id: 3, value: '<100ms', label: '加载时间' },
{ id: 4, value: '99.9%', label: '兼容性' }
],
bottomNav: [
{ id: 'home', icon: '🏠', text: '首页' },
{ id: 'explore', icon: '🔍', text: '发现' },
{ id: 'cart', icon: '🛒', text: '购物车' },
{ id: 'profile', icon: '👤', text: '我的' }
]
};
},
mounted() {
// 初始化VW适配
new VWAdaption(375, 667);
// 自动轮播
this.startAutoPlay();
// 监听横竖屏切换
this.handleOrientation();
},
beforeDestroy() {
this.stopAutoPlay();
},
methods: {
startAutoPlay() {
this.autoPlayTimer = setInterval(() => {
this.currentSlide = (this.currentSlide + 1) % this.carouselSlides.length;
}, 3000);
},
stopAutoPlay() {
if (this.autoPlayTimer) {
clearInterval(this.autoPlayTimer);
}
},
handleOrientation() {
window.addEventListener('orientationchange', () => {
// 横竖屏切换时重新计算布局
setTimeout(() => {
this.$forceUpdate();
}, 300);
});
},
handleBack() {
this.$router.back();
},
handleMenuClick(item) {
this.showMenu = false;
console.log('菜单点击:', item);
},
handleFeatureClick(feature) {
console.log('功能点击:', feature);
}
}
};
</script>
<style lang="scss" scoped>
// 引入VW适配样式
@import '@/assets/styles/adaption/vw.scss';
.vw-demo-container {
min-height: 100vh;
background: #f5f5f5;
display: flex;
flex-direction: column;
}
.vw-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
position: relative;
.vw-header-content {
@include vw-responsive(padding, 12px 16px);
display: flex;
align-items: center;
justify-content: space-between;
.vw-back-button, .vw-menu-button {
@include vw-responsive(width, 44px);
@include vw-responsive(height, 44px);
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 50%;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.3);
}
.vw-icon {
@include vw-responsive(font-size, 18px);
}
}
.vw-title {
@include vw-responsive(font-size, 18px);
font-weight: 600;
margin: 0;
}
}
.vw-dropdown-menu {
background: white;
border-radius: 0 0 8px 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
overflow: hidden;
.vw-menu-item {
@include vw-responsive(padding, 12px 16px);
display: flex;
align-items: center;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background: #f5f5f5;
}
.vw-menu-icon {
@include vw-responsive(margin-right, 12px);
@include vw-responsive(font-size, 18px);
}
.vw-menu-text {
@include vw-responsive(font-size, 14px);
color: #333;
}
}
}
}
.vw-main {
flex: 1;
overflow-y: auto;
}
.vw-carousel {
position: relative;
@include vw-responsive(height, 200px);
overflow: hidden;
border-radius: 0 0 16px 16px;
.vw-carousel-track {
display: flex;
transition: transform 0.5s ease;
height: 100%;
.vw-carousel-slide {
flex-shrink: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
}
.vw-slide-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
@include vw-responsive(padding, 20px 16px);
color: white;
z-index: 1;
h3 {
@include vw-responsive(font-size, 18px);
@include vw-responsive(margin-bottom, 8px);
font-weight: 600;
}
p {
@include vw-responsive(font-size, 14px);
margin: 0;
opacity: 0.9;
}
}
}
}
.vw-carousel-indicators {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
.vw-indicator {
@include vw-responsive(width, 8px);
@include vw-responsive(height, 8px);
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
&.active {
background: white;
@include vw-responsive(width, 20px);
border-radius: 4px;
}
}
}
}
.vw-feature-grid {
@include vw-responsive(padding, 20px 16px);
.vw-section-title {
@include vw-responsive(font-size, 20px);
@include vw-responsive(margin-bottom, 16px);
font-weight: 600;
color: #333;
}
.vw-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
.vw-feature-card {
background: white;
@include vw-responsive(padding, 16px);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
cursor: pointer;
transition: transform 0.3s ease;
&:hover {
transform: translateY(-2px);
}
.vw-feature-icon {
@include vw-responsive(width, 48px);
@include vw-responsive(height, 48px);
@include vw-responsive(border-radius, 12px);
@include vw-responsive(margin, 0 auto 12px);
@include vw-responsive(font-size, 24px);
display: flex;
align-items: center;
justify-content: center;
}
h4 {
@include vw-responsive(font-size, 16px);
@include vw-responsive(margin-bottom, 8px);
font-weight: 600;
color: #333;
}
p {
@include vw-responsive(font-size, 12px);
color: #666;
margin: 0;
line-height: 1.4;
}
}
}
}
.vw-stats-section {
background: white;
@include vw-responsive(padding, 20px 16px);
@include vw-responsive(margin-top, 12px);
.vw-section-title {
@include vw-responsive(font-size, 20px);
@include vw-responsive(margin-bottom, 16px);
font-weight: 600;
color: #333;
}
.vw-stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
.vw-stat-item {
text-align: center;
@include vw-responsive(padding, 16px);
background: #f8f9fa;
border-radius: 8px;
.vw-stat-value {
@include vw-responsive(font-size, 24px);
font-weight: 700;
color: #667eea;
@include vw-responsive(margin-bottom, 4px);
}
.vw-stat-label {
@include vw-responsive(font-size, 12px);
color: #666;
}
}
}
}
.vw-footer {
background: white;
border-top: 1px solid #e5e5e5;
.vw-bottom-nav {
display: flex;
justify-content: space-around;
@include vw-responsive(padding, 8px 0);
.vw-nav-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
@include vw-responsive(padding, 8px);
border-radius: 8px;
transition: all 0.3s ease;
&.active {
color: #667eea;
.vw-nav-icon {
background: #667eea;
color: white;
}
}
.vw-nav-icon {
@include vw-responsive(width, 24px);
@include vw-responsive(height, 24px);
@include vw-responsive(border-radius, 6px);
display: flex;
align-items: center;
justify-content: center;
@include vw-responsive(margin-bottom, 4px);
@include vw-responsive(font-size, 12px);
transition: all 0.3s ease;
}
.vw-nav-text {
@include vw-responsive(font-size, 10px);
}
}
}
}
// 动画效果
.vw-menu-slide-enter-active,
.vw-menu-slide-leave-active {
transition: all 0.3s ease;
}
.vw-menu-slide-enter-from,
.vw-menu-slide-leave-to {
opacity: 0;
transform: translateY(-10px);
}
// 响应式调整
@media (max-width: 320px) {
.vw-grid {
grid-template-columns: 1fr !important;
}
.vw-stats-grid {
grid-template-columns: 1fr !important;
}
}
@media (min-width: 768px) {
.vw-grid {
grid-template-columns: repeat(4, 1fr) !important;
}
.vw-stats-grid {
grid-template-columns: repeat(4, 1fr) !important;
}
}
// 横屏适配
@media (orientation: landscape) and (max-height: 500px) {
.vw-carousel {
height: 150px !important;
}
.vw-feature-grid,
.vw-stats-section {
padding: 12px 16px !important;
}
}
// 暗色模式支持
@media (prefers-color-scheme: dark) {
.vw-demo-container {
background: #1a1a1a;
}
.vw-feature-card,
.vw-stats-section {
background: #2d2d2d;
h4, .vw-section-title {
color: #fff;
}
p {
color: #ccc;
}
}
.vw-footer {
background: #2d2d2d;
border-top-color: #404040;
}
}
// VW响应式混合器
@mixin vw-responsive($property, $values...) {
@each $value in $values {
@if type-of($value) == number {
#{$property}: ($value / 3.75) * 1vw;
} @else {
#{$property}: $value;
}
}
}
</style>
4.3 Flexible.js完整示例
4.3.1 Flexible.js增强配置
// src/utils/enhanced-flexible.js
(function(win, lib) {
const doc = win.document;
const docEl = doc.documentElement;
let metaEl = doc.querySelector('meta[name="viewport"]');
const flexibleEl = doc.querySelector('meta[name="flexible"]');
let dpr = 0;
let scale = 0;
let tid;
const flexible = lib.flexible || (lib.flexible = {});
// 设备像素比检测
if (!dpr && !scale) {
const isAndroid = win.navigator.appVersion.match(/android/gi);
const isIPhone = win.navigator.appVersion.match(/iphone/gi);
const devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS设备
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备
dpr = 1;
}
scale = 1 / dpr;
}
// 设置viewport
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content',
`initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}, user-scalable=no`);
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
const wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}
// 设置data-dpr属性
function setDpr() {
docEl.setAttribute('data-dpr', dpr);
}
// 设置rem基准
function refreshRem() {
const width = docEl.getBoundingClientRect().width;
// 限制最大宽度
const maxWidth = flexible.maxWidth || 540;
const clientWidth = Math.min(width, maxWidth);
// 计算rem(分成10份)
const rem = clientWidth / 10;
docEl.style.fontSize = rem + 'px';
// 触发rem变化事件
win.dispatchEvent(new CustomEvent('remChange', {
detail: { rem: rem, dpr: dpr }
}));
}
// 事件绑定
win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
});
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
});
// 横竖屏切换处理
win.addEventListener('orientationchange', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
});
// 初始化
if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px';
setDpr();
refreshRem();
} else {
doc.addEventListener('DOMContentLoaded', function() {
doc.body.style.fontSize = 12 * dpr + 'px';
setDpr();
refreshRem();
});
}
// 暴露API
flexible.dpr = dpr;
flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
let val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val = parseFloat(d) * this.rem;
}
return val;
};
flexible.px2rem = function(d) {
let val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val = parseFloat(d) / this.rem;
}
return val;
};
})(window, window['lib'] || (window['lib'] = {}));
// Vue插件封装
export const FlexiblePlugin = {
install(Vue, options = {}) {
const { designWidth = 750, maxWidth = 540 } = options;
// 设置配置
if (window.lib && window.lib.flexible) {
window.lib.flexible.designWidth = designWidth;
window.lib.flexible.maxWidth = maxWidth;
}
// 提供全局方法
Vue.prototype.$flexible = {
px2rem(px) {
return window.lib.flexible.px2rem(px + 'px');
},
rem2px(rem) {
return window.lib.flexible.rem2px(rem + 'rem');
},
getRem() {
return parseFloat(getComputedStyle(document.documentElement).fontSize);
},
getDpr() {
return window.lib.flexible.dpr;
}
};
// 自定义指令:rem适配
Vue.directive('rem', {
bind(el, binding) {
const value = binding.value;
if (typeof value === 'object') {
Object.keys(value).forEach(prop => {
if (typeof value[prop] === 'number') {
el.style[prop] = Vue.prototype.$flexible.px2rem(value[prop]) + 'rem';
}
});
}
}
});
}
};
4.3.2 Flexible.js Vue组件示例
<!-- src/views/FlexibleDemo.vue -->
<template>
<div class="flexible-demo-container" :data-dpr="dpr">
<!-- 设备信息展示 -->
<div class="device-info">
<div class="info-item">
<span class="info-label">设备像素比:</span>
<span class="info-value">{{ dpr }}</span>
</div>
<div class="info-item">
<span class="info-label">当前REM基准:</span>
<span class="info-value">{{ currentRem }}px</span>
</div>
<div class="info-item">
<span class="info-label">屏幕宽度:</span>
<span class="info-value">{{ screenWidth }}px</span>
</div>
</div>
<!-- 适配演示区域 -->
<div class="demo-section">
<h2 class="section-title">Flexible.js 适配演示</h2>
<!-- 1px边框演示 -->
<div class="border-demo">
<h3>1px边框解决方案</h3>
<div class="border-items">
<div class="border-item hairline">普通边框</div>
<div class="border-item hairline-top">上边框</div>
<div class="border-item hairline-bottom">下边框</div>
<div class="border-item hairline-left">左边框</div>
<div class="border-item hairline-right">右边框</div>
</div>
</div>
<!-- 字体大小适配 -->
<div class="font-demo">
<h3>多DPR字体适配</h3>
<div class="font-items">
<div v-for="size in fontSizes"
:key="size"
class="font-item"
:style="{ fontSize: size + 'px' }">
{{ size }}px字体 - DPR: {{ dpr }}
</div>
</div>
</div>
<!-- 布局适配 -->
<div class="layout-demo">
<h3>弹性布局适配</h3>
<div class="layout-grid">
<div v-for="n in 6"
:key="n"
class="grid-item"
:style="getGridStyle(n)">
项目 {{ n }}
</div>
</div>
</div>
<!-- 图片适配 -->
<div class="image-demo">
<h3>多倍图适配</h3>
<div class="image-container">
<img v-for="img in images"
:key="img.id"
:src="getImageSrc(img)"
:alt="img.alt"
class="adaptive-image"
:style="getImageStyle(img)">
</div>
</div>
</div>
<!-- 交互测试 -->
<div class="interaction-test">
<h3>交互元素测试</h3>
<div class="interaction-items">
<button class="flexible-button primary" @click="handleButtonClick">
主要按钮
</button>
<button class="flexible-button secondary" @click="handleButtonClick">
次要按钮
</button>
<button class="flexible-button ghost" @click="handleButtonClick">
幽灵按钮
</button>
</div>
<div class="input-group">
<input v-model="inputValue"
class="flexible-input"
placeholder="测试输入框适配">
<button class="flexible-button" @click="handleSubmit">提交</button>
</div>
</div>
</div>
</template>
<script>
import { FlexiblePlugin } from '@/utils/enhanced-flexible';
export default {
name: 'FlexibleDemo',
// 注册Flexible插件
beforeCreate() {
if (!this.$flexible) {
Vue.use(FlexiblePlugin, {
designWidth: 750,
maxWidth: 540
});
}
},
data() {
return {
dpr: 1,
currentRem: 0,
screenWidth: 0,
inputValue: '',
fontSizes: [12, 14, 16, 18, 20, 24],
images: [
{ id: 1, width: 200, height: 150, alt: '示例图片1' },
{ id: 2, width: 180, height: 120, alt: '示例图片2' },
{ id: 3, width: 220, height: 160, alt: '示例图片3' }
]
};
},
mounted() {
this.initFlexible();
this.bindEvents();
},
beforeDestroy() {
this.unbindEvents();
},
methods: {
initFlexible() {
// 获取当前设备信息
this.dpr = this.$flexible.getDpr();
this.currentRem = this.$flexible.getRem();
this.screenWidth = window.screen.width;
console.log('Flexible.js 初始化完成:', {
dpr: this.dpr,
rem: this.currentRem,
screenWidth: this.screenWidth
});
},
bindEvents() {
// 监听rem变化
window.addEventListener('remChange', this.handleRemChange);
window.addEventListener('resize', this.handleResize);
},
unbindEvents() {
window.removeEventListener('remChange', this.handleRemChange);
window.removeEventListener('resize', this.handleResize);
},
handleRemChange(event) {
this.currentRem = event.detail.rem;
this.dpr = event.detail.dpr;
},
handleResize() {
this.screenWidth = window.innerWidth;
},
getGridStyle(index) {
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3'];
return {
backgroundColor: colors[(index - 1) % colors.length],
height: this.$flexible.px2rem(80) + 'rem'
};
},
getImageSrc(img) {
// 根据DPR加载不同倍数的图片
const multiplier = this.dpr >= 3 ? 3 : this.dpr >= 2 ? 2 : 1;
return `https://picsum.photos/${img.width * multiplier}/${img.height * multiplier}?random=${img.id}`;
},
getImageStyle(img) {
return {
width: this.$flexible.px2rem(img.width) + 'rem',
height: this.$flexible.px2rem(img.height) + 'rem'
};
},
handleButtonClick() {
this.$message.success('按钮点击成功!当前DPR: ' + this.dpr);
},
handleSubmit() {
if (this.inputValue) {
this.$message.info(`输入内容: ${this.inputValue}`);
this.inputValue = '';
}
}
}
};
</script>
<style lang="scss" scoped>
// 引入Flexible.js适配样式
@import '@/assets/styles/adaption/flexible.scss';
.flexible-demo-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 0.4rem; // 20px in 750 design
}
.device-info {
background: rgba(255, 255, 255, 0.95);
border-radius: 0.2rem; // 10px
padding: 0.3rem; // 15px
margin-bottom: 0.4rem; // 20px
display: flex;
justify-content: space-around;
flex-wrap: wrap;
.info-item {
display: flex;
flex-direction: column;
align-items: center;
margin: 0.1rem; // 5px
.info-label {
font-size: 0.24rem; // 12px
color: #666;
margin-bottom: 0.1rem; // 5px
}
.info-value {
font-size: 0.32rem; // 16px
font-weight: 600;
color: #333;
}
}
}
.demo-section {
background: white;
border-radius: 0.2rem; // 10px
padding: 0.3rem; // 15px
margin-bottom: 0.4rem; // 20px
.section-title {
font-size: 0.36rem; // 18px
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 0.3rem; // 15px
}
}
.border-demo {
margin-bottom: 0.4rem; // 20px
h3 {
font-size: 0.28rem; // 14px
color: #666;
margin-bottom: 0.2rem; // 10px
}
.border-items {
display: flex;
flex-wrap: wrap;
gap: 0.2rem; // 10px
.border-item {
padding: 0.2rem; // 10px
text-align: center;
border-radius: 0.1rem; // 5px
font-size: 0.24rem; // 12px
// 1px边框解决方案
&.hairline {
border: 1px solid #e5e5e5;
}
&.hairline-top {
border-top: 1px solid #e5e5e5;
}
&.hairline-bottom {
border-bottom: 1px solid #e5e5e5;
}
&.hairline-left {
border-left: 1px solid #e5e5e5;
}
&.hairline-right {
border-right: 1px solid #e5e5e5;
}
}
}
}
.font-demo {
margin-bottom: 0.4rem; // 20px
h3 {
font-size: 0.28rem; // 14px
color: #666;
margin-bottom: 0.2rem; // 10px
}
.font-items {
display: flex;
flex-direction: column;
gap: 0.2rem; // 10px
.font-item {
padding: 0.2rem; // 10px
background: #f8f9fa;
border-radius: 0.1rem; // 5px
text-align: center;
}
}
}
.layout-demo {
margin-bottom: 0.4rem; // 20px
h3 {
font-size: 0.28rem; // 14px
color: #666;
margin-bottom: 0.2rem; // 10px
}
.layout-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.2rem; // 10px
.grid-item {
padding: 0.3rem; // 15px
color: white;
text-align: center;
border-radius: 0.1rem; // 5px
font-size: 0.24rem; // 12px
display: flex;
align-items: center;
justify-content: center;
}
}
}
.image-demo {
margin-bottom: 0.4rem; // 20px
h3 {
font-size: 0.28rem; // 14px
color: #666;
margin-bottom: 0.2rem; // 10px
}
.image-container {
display: flex;
gap: 0.2rem; // 10px
overflow-x: auto;
.adaptive-image {
border-radius: 0.1rem; // 5px
object-fit: cover;
flex-shrink: 0;
}
}
}
.interaction-test {
background: white;
border-radius: 0.2rem; // 10px
padding: 0.3rem; // 15px
h3 {
font-size: 0.28rem; // 14px
color: #666;
margin-bottom: 0.2rem; // 10px
}
.interaction-items {
display: flex;
gap: 0.2rem; // 10px
margin-bottom: 0.3rem; // 15px
flex-wrap: wrap;
.flexible-button {
padding: 0.2rem 0.4rem; // 10px 20px
border: none;
border-radius: 0.1rem; // 5px
font-size: 0.28rem; // 14px
cursor: pointer;
transition: all 0.3s ease;
&.primary {
background: #667eea;
color: white;
&:hover {
background: #5a6fd8;
}
}
&.secondary {
background: #e9ecef;
color: #333;
&:hover {
background: #dee2e6;
}
}
&.ghost {
background: transparent;
color: #667eea;
border: 1px solid #667eea;
&:hover {
background: #667eea;
color: white;
}
}
}
}
.input-group {
display: flex;
gap: 0.2rem; // 10px
.flexible-input {
flex: 1;
padding: 0.2rem 0.3rem; // 10px 15px
border: 1px solid #e5e5e5;
border-radius: 0.1rem; // 5px
font-size: 0.28rem; // 14px
&:focus {
outline: none;
border-color: #667eea;
}
}
}
}
// DPR特定样式
[data-dpr="1"] {
.font-item {
font-weight: normal;
}
.flexible-button {
font-weight: normal;
}
}
[data-dpr="2"] {
.font-item {
font-weight: 500;
}
.flexible-button {
font-weight: 500;
}
.hairline {
border-width: 0.5px;
}
}
[data-dpr="3"] {
.font-item {
font-weight: 600;
}
.flexible-button {
font-weight: 600;
}
.hairline {
border-width: 0.333px;
}
}
// 响应式调整
@media (max-width: 768px) {
.layout-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
.interaction-items {
flex-direction: column;
.flexible-button {
width: 100%;
}
}
}
@media (max-width: 480px) {
.layout-grid {
grid-template-columns: 1fr !important;
}
.device-info {
flex-direction: column;
align-items: center;
.info-item {
margin: 0.1rem 0;
}
}
}
// 横屏适配
@media (orientation: landscape) and (max-height: 500px) {
.flexible-demo-container {
padding: 0.2rem;
}
.demo-section {
padding: 0.2rem;
}
}
// 暗色模式支持
@media (prefers-color-scheme: dark) {
.demo-section,
.interaction-test {
background
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)