普通视图

发现新文章,点击刷新页面。
昨天以前首页

从零实现一个前端监控系统:性能、错误与用户行为全方位监控

2026年4月19日 17:03

从零实现一个前端监控系统:性能、错误与用户行为全方位监控

深入探索前端监控 SDK 的实现原理,从性能指标采集、错误捕获到用户行为追踪,手把手教你打造一个企业级的前端监控方案。

前言

在现代 Web 应用中,前端监控是保障产品质量和用户体验的重要基石。一个完善的前端监控系统应该具备以下能力:

  • 性能监控:采集页面加载性能、接口请求耗时等关键指标
  • 错误监控:捕获 JS 错误、资源加载失败、Promise 异常等问题
  • 行为监控:追踪用户点击、页面跳转、PV/UV 等行为数据
  • 数据上报:高效、可靠地将数据发送到服务端

本文将基于 webEyeSDK 项目,详细讲解如何从零实现一个前端监控 SDK。

架构设计

整体架构

webEyeSDK
├── src/
│   ├── webEyeSDK.js        # SDK 入口文件
│   ├── config.js            # 配置管理
│   ├── report.js            # 数据上报
│   ├── cache.js             # 数据缓存
│   ├── utils.js             # 工具函数
│   ├── performance/         # 性能监控
│   │   ├── index.js
│   │   ├── observeLCP.js
│   │   ├── observerFCP.js
│   │   ├── observerLoad.js
│   │   ├── observerPaint.js
│   │   ├── observerEntries.js
│   │   ├── fetch.js
│   │   └── xhr.js
│   ├── error/               # 错误监控
│   │   └── index.js
│   └── behavior/            # 行为监控
│       ├── index.js
│       ├── pv.js
│       ├── onClick.js
│       └── pageChange.js

模块职责

模块 职责
Performance 采集页面性能指标(FCP、LCP、Load 等)
Error 捕获 JS 错误、资源错误、Promise 错误
Behavior 追踪用户行为(点击、页面跳转、PV)
Report 数据上报(支持 sendBeacon、XHR、Image)
Cache 数据缓存与批量上报

核心功能实现

一、性能监控

性能监控是前端监控的核心模块,主要通过 Performance APIPerformanceObserver 来采集关键指标。

1. FCP(首次内容绘制)

FCP(First Contentful Paint)测量页面首次渲染任何文本、图像等内容的时间。

import { lazyReportBatch } from '../report';

export default function observerFCP() {
    const entryHandler = (list) => {
        for (const entry of list.getEntries()) {
            if (entry.name === 'first-contentful-paint') {
                observer.disconnect();
                const json = entry.toJSON();
                const reportData = {
                    ...json,
                    type: 'performance',
                    subType: entry.name,
                    pageUrl: window.location.href,
                };
                lazyReportBatch(reportData);
            }
        }
    };

    // 统计和计算 FCP 的时间
    const observer = new PerformanceObserver(entryHandler);
    // buffered: true 确保观察到所有 paint 事件
    observer.observe({ type: 'paint', buffered: true });
}

核心要点

  • 使用 PerformanceObserver 监听 paint 类型的性能条目
  • buffered: true 确保能观察到页面加载过程中已发生的性能事件
  • 找到 first-contentful-paint 后立即断开监听,避免重复上报
2. LCP(最大内容绘制)

LCP(Largest Contentful Paint)测量视口内最大内容元素渲染的时间,是 Core Web Vitals 的重要指标。

import { lazyReportBatch } from '../report';

export default function observerLCP() {
    const entryHandler = (list) => {
        if (observer) {
            observer.disconnect();
        }
        for (const entry of list.getEntries()) {
            const json = entry.toJSON();
            const reportData = {
                ...json,
                type: 'performance',
                subType: entry.name,
                pageUrl: window.location.href,
            };
            lazyReportBatch(reportData);
        }
    };

    // 统计和计算 LCP 的时间
    const observer = new PerformanceObserver(entryHandler);
    observer.observe({ type: 'largest-contentful-paint', buffered: true });
}

LCP 特点

  • LCP 可能会多次触发(如最大内容元素改变),需要持续监听
  • 通常在用户交互或页面加载完成后才上报最终值
  • Google 建议 LCP 应在 2.5 秒以内
3. XHR/Fetch 请求监控

通过重写 XMLHttpRequest 原型方法,实现接口请求的性能监控。

import { lazyReportBatch } from '../report';

export const originalProto = XMLHttpRequest.prototype;
export const originalSend = originalProto.send;
export const originalOpen = originalProto.open;

function overwriteOpenAndSend() {
    originalProto.open = function newOpen(...args) {
        this.url = args[1];
        this.method = args[0];
        originalOpen.apply(this, args);
    };

    originalProto.send = function newSend(...args) {
        this.startTime = Date.now();
        const onLoaded = () => {
            this.endTime = Date.now();
            this.duration = this.endTime - this.startTime;

            const { url, method, startTime, endTime, duration, status } = this;
            const reportData = {
                status,
                duration,
                startTime,
                endTime,
                url,
                method: method.toUpperCase(),
                type: 'performance',
                success: status >= 200 && status < 300,
                subType: 'xhr'
            };

            lazyReportBatch(reportData);
            this.removeEventListener('loadend', onLoaded, true);
        };

        this.addEventListener('loadend', onLoaded, true);
        originalSend.apply(this, args);
    };
}

export default function xhr() {
    overwriteOpenAndSend();
}

实现原理

  • 重写 XMLHttpRequest.prototype.open 方法,记录请求 URL 和方法
  • 重写 XMLHttpRequest.prototype.send 方法,记录请求开始时间
  • 监听 loadend 事件,计算请求耗时并上报
4. 性能监控入口
import fetch from "./fetch";
import observerEntries from "./observerEntries";
import observerLCP from "./observeLCP";
import observerFCP from "./observerFCP";
import observerLoad from "./observerLoad";
import observerPaint from "./observerPaint";
import xhr from "./xhr";

export default function performance() {
    fetch();
    observerEntries();
    observerLCP();
    observerFCP();
    observerLoad();
    observerPaint();
    xhr();
}

二、错误监控

错误监控帮助开发者及时发现和定位线上问题,是保障应用稳定性的关键。

1. JS 运行时错误

通过 window.onerror 捕获 JavaScript 运行时错误。

window.onerror = function (msg, url, lineNo, columnNo, error) {
    const reportData = {
        type: 'error',
        subType: 'js',
        msg,
        url,
        lineNo,
        columnNo,
        stack: error.stack,
        pageUrl: window.location.href,
        startTime: performance.now(),
    };
    lazyReportBatch(reportData);
};

参数说明

  • msg:错误消息
  • url:发生错误的脚本 URL
  • lineNo:错误行号
  • columnNo:错误列号
  • error:Error 对象,包含堆栈信息
2. 资源加载错误

资源加载错误(如图片、CSS、JS 文件加载失败)需要通过事件捕获来监听。

window.addEventListener(
    'error',
    function (e) {
        const target = e.target;
        if (target.src || target.href) {
            const url = target.src || target.href;
            const reportData = {
                type: 'error',
                subType: 'resource',
                url,
                html: target.outerHTML,
                pageUrl: window.location.href,
                paths: e.path,
            };
            lazyReportBatch(reportData);
        }
    },
    true  // 使用捕获阶段
);

关键点

  • 必须在捕获阶段监听(第三个参数为 true),因为资源加载错误不会冒泡
  • 通过 e.target.srce.target.href 判断是否为资源错误
3. Promise 错误

Promise 中未被捕获的错误需要监听 unhandledrejection 事件。

window.addEventListener(
    'unhandledrejection',
    function (e) {
        const reportData = {
            type: 'error',
            subType: 'promise',
            reason: e.reason?.stack,
            pageUrl: window.location.href,
            startTime: e.timeStamp,
        };
        lazyReportBatch(reportData);
    },
    true
);
4. 框架错误捕获

Vue 错误捕获

export function install(Vue, options) {
    if (__webEyeSDK__.vue) return;
    __webEyeSDK__.vue = true;
    setConfig(options);

    const handler = Vue.config.errorHandler;
    Vue.config.errorHandler = function (err, vm, info) {
        const reportData = {
            info,
            error: err.stack,
            subType: 'vue',
            type: 'error',
            startTime: window.performance.now(),
            pageURL: window.location.href,
        };
        lazyReportBatch(reportData);

        if (handler) {
            handler.call(this, err, vm, info);
        }
    };
}

React 错误捕获

export function errorBoundary(err, info) {
    if (__webEyeSDK__.react) return;
    __webEyeSDK__.react = true;

    const reportData = {
        error: err?.stack,
        info,
        subType: 'react',
        type: 'error',
        startTime: window.performance.now(),
        pageURL: window.location.href,
    };
    lazyReportBatch(reportData);
}

使用方式

// Vue 项目
import webEyeSDK from './webEyeSDK';
Vue.use(webEyeSDK, { appId: 'xxx' });

// React 项目
import webEyeSDK from './webEyeSDK';
class ErrorBoundary extends React.Component {
    componentDidCatch(error, info) {
        webEyeSDK.errorBoundary(error, info);
    }
    render() {
        return this.props.children;
    }
}
5. 错误监控入口
import { lazyReportBatch } from '../report';

export default function error() {
    // 捕获资源加载失败的错误
    window.addEventListener('error', function (e) {
        const target = e.target;
        if (target.src || target.href) {
            const url = target.src || target.href;
            const reportData = {
                type: 'error',
                subType: 'resource',
                url,
                html: target.outerHTML,
                pageUrl: window.location.href,
                paths: e.path,
            };
            lazyReportBatch(reportData);
        }
    }, true);

    // 捕获 JS 错误
    window.onerror = function (msg, url, lineNo, columnNo, error) {
        const reportData = {
            type: 'error',
            subType: 'js',
            msg,
            url,
            lineNo,
            columnNo,
            stack: error.stack,
            pageUrl: window.location.href,
            startTime: performance.now(),
        };
        lazyReportBatch(reportData);
    };

    // 捕获 Promise 错误
    window.addEventListener('unhandledrejection', function (e) {
        const reportData = {
            type: 'error',
            subType: 'promise',
            reason: e.reason?.stack,
            pageUrl: window.location.href,
            startTime: e.timeStamp,
        };
        lazyReportBatch(reportData);
    }, true);
}

三、行为监控

用户行为监控帮助我们理解用户如何使用应用,为产品优化提供数据支持。

1. PV(页面浏览量)
import { lazyReportBatch } from '../report';
import { generateUniqueId } from '../utils';

export default function pv() {
    const reportData = {
        type: 'behavior',
        subType: 'pv',
        startTime: performance.now(),
        pageUrl: window.location.href,
        referrer: document.referrer,
        uuid: generateUniqueId(),
    };
    lazyReportBatch(reportData);
}

PV 数据包含

  • 当前页面 URL
  • 来源页面(document.referrer
  • 访问时间
  • 唯一标识(用于关联用户行为链路)
2. 点击行为
import { lazyReportBatch } from '../report';

export default function onClick() {
    ['mousedown', 'touchstart'].forEach((eventType) => {
        window.addEventListener(eventType, (e) => {
            const target = e.target;
            if (target.tagName) {
                const reportData = {
                    type: 'behavior',
                    subType: 'click',
                    target: target.tagName,
                    startTime: e.timeStamp,
                    innerHtml: target.innerHTML,
                    outerHtml: target.outerHTML,
                    width: target.offsetWidth,
                    height: target.offsetHeight,
                    eventType,
                    path: e.path,
                };
                lazyReportBatch(reportData);
            }
        });
    });
}

点击数据用途

  • 分析用户交互热点
  • 绘制热力图
  • 检测异常点击行为
3. 页面跳转
import { lazyReportBatch } from '../report';
import { generateUniqueId } from '../utils';

export default function pageChange() {
    let oldUrl = '';

    // Hash 路由
    window.addEventListener('hashchange', function (event) {
        const newUrl = event.newURL;
        const reportData = {
            from: oldUrl,
            to: newUrl,
            type: 'behavior',
            subType: 'hashchange',
            startTime: performance.now(),
            uuid: generateUniqueId(),
        };
        lazyReportBatch(reportData);
        oldUrl = newUrl;
    }, true);

    let from = '';
    // History 路由
    window.addEventListener('popstate', function (event) {
        const to = window.location.href;
        const reportData = {
            from: from,
            to: to,
            type: 'behavior',
            subType: 'popstate',
            startTime: performance.now(),
            uuid: generateUniqueId(),
        };
        lazyReportBatch(reportData);
        from = to;
    }, true);
}

路由监听

  • 支持 Hash 路由(hashchange 事件)
  • 支持 History 路由(popstate 事件)
  • 记录跳转前后 URL,用于分析用户路径
4. 行为监控入口
import onClick from './onClick';
import pageChange from './pageChange';
import pv from './pv';

export default function behavior() {
    onClick();
    pageChange();
    pv();
}

四、数据上报

数据上报是前端监控的最后一步,需要保证数据可靠、高效地发送到服务端。

1. 上报策略
const config = {
    url: '',
    projectName: 'eyesdk',
    appId: '123456',
    userId: '123456',
    isImageUpload: false,
    batchSize: 5,
};

配置项说明

  • url:上报接口地址
  • appId:应用唯一标识
  • userId:用户标识
  • isImageUpload:是否使用图片方式上报
  • batchSize:批量上报阈值
2. 批量上报
import { addCache, getCache, clearCache } from './cache';

export function lazyReportBatch(data) {
    addCache(data);
    const dataCache = getCache();

    if (dataCache.length && dataCache.length > config.batchSize) {
        report(dataCache);
        clearCache();
    }
}

批量上报优势

  • 减少网络请求次数
  • 降低服务端压力
  • 提升性能
3. 多种上报方式
export function report(data) {
    if (!config.url) {
        console.error('请设置上传 url 地址');
    }

    const reportData = JSON.stringify({
        id: generateUniqueId(),
        data,
    });

    // 使用图片方式上报
    if (config.isImageUpload) {
        imgRequest(reportData);
    } else {
        // 优先使用 sendBeacon
        if (window.navigator.sendBeacon) {
            return beaconRequest(reportData);
        } else {
            xhrRequest(reportData);
        }
    }
}

上报方式对比

方式 优点 缺点 适用场景
sendBeacon 异步、不阻塞页面卸载、可靠 浏览器兼容性 页面关闭时上报
Image 简单、跨域友好 数据大小限制 简单数据上报
XHR 兼容性好、支持 POST 可能被阻塞 常规上报
4. sendBeacon 上报
export function beaconRequest(data) {
    if (window.requestIdleCallback) {
        window.requestIdleCallback(
            () => {
                window.navigator.sendBeacon(config.url, data);
            },
            { timeout: 3000 }
        );
    } else {
        setTimeout(() => {
            window.navigator.sendBeacon(config.url, data);
        });
    }
}

sendBeacon 特点

  • 浏览器在页面卸载时也能可靠发送
  • 异步执行,不阻塞页面关闭
  • 适合用于页面关闭前的数据上报
5. XHR 上报
export function xhrRequest(data) {
    if (window.requestIdleCallback) {
        window.requestIdleCallback(
            () => {
                const xhr = new XMLHttpRequest();
                originalOpen.call(xhr, 'post', config.url);
                originalSend.call(xhr, JSON.stringify(data));
            },
            { timeout: 3000 }
        );
    } else {
        setTimeout(() => {
            const xhr = new XMLHttpRequest();
            originalOpen.call(xhr, 'post', url);
            originalSend.call(xhr, JSON.stringify(data));
        });
    }
}

requestIdleCallback

  • 在浏览器空闲时执行上报
  • 避免阻塞关键渲染任务
  • 设置 timeout: 3000 确保最迟 3 秒后执行
6. 图片上报
export function imgRequest(data) {
    const img = new Image();
    img.src = `${config.url}?data=${encodeURIComponent(JSON.stringify(data))}`;
}

Image 上报优势

  • 实现简单
  • 天然支持跨域
  • 无需担心阻塞

五、数据缓存

import { deepCopy } from './utils.js';

const cache = [];

export function getCache() {
    return deepCopy(cache);
}

export function addCache(data) {
    cache.push(data);
}

export function clearCache() {
    cache.length = 0;
}

缓存机制

  • 使用数组缓存待上报数据
  • 达到阈值后批量上报
  • 上报后清空缓存

六、工具函数

// 深拷贝
export function deepCopy(target) {
    if (typeof target === 'object') {
        const result = Array.isArray(target) ? [] : {};
        for (const key in target) {
            if (typeof target[key] == 'object') {
                result[key] = deepCopy(target[key]);
            } else {
                result[key] = target[key];
            }
        }
        return result;
    }
    return target;
}

// 生成唯一 ID
export function generateUniqueId() {
    return 'id-' + Date.now() + '-' + Math.random().toString(36).substring(2, 9);
}

七、SDK 入口

import performance from './performance/index';
import error from './error/index';
import behavior from './behavior/index';
import { setConfig } from './config';
import { lazyReportBatch } from './report';

window.__webEyeSDK__ = {
    version: '0.0.1',
};

// 针对 Vue 项目的错误捕获
export function install(Vue, options) {
    if (__webEyeSDK__.vue) return;
    __webEyeSDK__.vue = true;
    setConfig(options);
    const handler = Vue.config.errorHandler;
    Vue.config.errorHandler = function (err, vm, info) {
        const reportData = {
            info,
            error: err.stack,
            subType: 'vue',
            type: 'error',
            startTime: window.performance.now(),
            pageURL: window.location.href,
        };
        lazyReportBatch(reportData);
        if (handler) {
            handler.call(this, err, vm, info);
        }
    };
}

// 针对 React 项目的错误捕获
export function errorBoundary(err, info) {
    if (__webEyeSDK__.react) return;
    __webEyeSDK__.react = true;
    const reportData = {
        error: err?.stack,
        info,
        subType: 'react',
        type: 'error',
        startTime: window.performance.now(),
        pageURL: window.location.href,
    };
    lazyReportBatch(reportData);
}

export function init(options) {
    setConfig(options);
    performance();
    error();
    behavior();
}

export default {
    install,
    errorBoundary,
    performance,
    error,
    behavior,
    init,
}

使用指南

安装

import webEyeSDK from './webEyeSDK';

// 初始化
webEyeSDK.init({
    url: 'http://your-server.com/report',
    appId: 'your-app-id',
    userId: 'user-123',
    batchSize: 10,
});

Vue 项目集成

import Vue from 'vue';
import webEyeSDK from './webEyeSDK';

Vue.use(webEyeSDK, {
    url: 'http://your-server.com/report',
    appId: 'your-app-id',
});

React 项目集成

import React from 'react';
import webEyeSDK from './webEyeSDK';

class ErrorBoundary extends React.Component {
    componentDidCatch(error, info) {
        webEyeSDK.errorBoundary(error, info);
    }

    render() {
        return this.props.children;
    }
}

// 初始化
webEyeSDK.init({
    url: 'http://your-server.com/report',
    appId: 'your-app-id',
});

核心特性总结

功能模块 监控内容 实现方式
性能监控 FCP、LCP、Load、XHR/Fetch PerformanceObserver、重写原型
错误监控 JS 错误、资源错误、Promise 错误 window.onerror、事件监听
行为监控 PV、点击、页面跳转 事件监听
数据上报 批量上报、多种方式 sendBeacon、XHR、Image

性能优化建议

  1. 使用 requestIdleCallback:在浏览器空闲时执行数据上报,避免阻塞关键渲染
  2. 批量上报:减少网络请求次数,降低服务端压力
  3. sendBeacon:页面关闭时使用 sendBeacon 保证数据可靠性
  4. 数据压缩:上报前压缩数据,减少传输体积
  5. 采样上报:对高频事件(如点击)进行采样,减少数据量

扩展方向

  1. SourceMap 解析:还原压缩后的错误堆栈
  2. 录屏回放:使用 rrweb 记录用户操作
  3. 白屏检测:检测页面白屏问题
  4. 性能评分:基于 Core Web Vitals 计算性能评分
  5. 告警系统:实时告警通知

参考资料

总结

本文从零实现了一个前端监控 SDK,涵盖了性能监控、错误监控、行为监控三大核心模块。通过 Performance API、事件监听、原型重写等技术,实现了全方位的前端监控能力。

掌握前端监控的实现原理,不仅能帮助你在工作中构建完善的监控系统,还能加深对浏览器性能、错误处理等底层机制的理解。


如果觉得本文有帮助,欢迎点赞收藏,有问题欢迎在评论区讨论!

大屏开发必读:Scale/VW/Rem/流式/断点/混合方案全解析(附完整demo)

2026年4月18日 16:35

大屏适配终极指南:6 种主流方案实战对比

数据可视化大屏开发中,屏幕适配是最令人头疼的问题之一。本文通过 6 个完整 Demo,深度对比 Scale、VW/VH、Rem、流式布局、响应式断点、混合方案这 6 种主流方案的优缺点与适用场景。

前言

在开发数据可视化大屏时,你是否遇到过这些问题:

  • 设计稿是 1920×1080,实际大屏是 3840×2160,怎么适配?
  • 大屏比例不是 16:9,出现黑边或拉伸怎么办?
  • 小屏幕上字体太小看不清,大屏幕上内容太空旷
  • 图表、地图、视频等组件在不同尺寸下表现不一致

本文将通过 6 个完整的可交互 Demo,带你逐一攻克这些难题。

方案一:Scale 等比例缩放

核心原理

通过 CSS transform: scale() 将整个页面按屏幕比例缩放,是最简单直观的方案。

const DESIGN_WIDTH = 1920;
const DESIGN_HEIGHT = 1080;

function setScale() {
  const scaleX = window.innerWidth / DESIGN_WIDTH;
  const scaleY = window.innerHeight / DESIGN_HEIGHT;
  const scale = Math.min(scaleX, scaleY);

  screen.style.transform = `scale(${scale})`;

  // 居中显示
  const left = (window.innerWidth - DESIGN_WIDTH * scale) / 2;
  const top = (window.innerHeight - DESIGN_HEIGHT * scale) / 2;
  screen.style.left = left + 'px';
  screen.style.top = top + 'px';
}

优缺点分析

优点:

  • 完美还原设计稿比例
  • 实现简单,几行代码搞定
  • 字体、图表自动缩放,无需额外处理
  • 兼容性好,不需要关注浏览器兼容性

缺点:

  • 屏幕比例不符时出现黑边(如 4:3 屏幕)
  • 小屏幕上字体会缩得很小,可能看不清
  • 无法利用多余空间,浪费屏幕资源

适用场景

数据可视化大屏、监控中心大屏等需要精确还原设计稿的场景。

方案二:VW/VH 视口单位

核心原理

使用 CSS viewport 单位(vw/vh)代替 px,让元素根据视口尺寸自适应。

.container {
  width: 50vw;      /* 视口宽度的 50% */
  height: 30vh;     /* 视口高度的 30% */
  font-size: 2vw;   /* 字体随视口宽度变化 */
  padding: 2vh 3vw; /* 内边距也自适应 */
}

优缺点分析

优点:

  • 纯 CSS 方案,无 JS 依赖
  • 充分利用全部屏幕空间,无黑边
  • 无缩放导致的模糊问题

缺点:

  • 计算公式复杂,需要手动换算
  • 宽高难以协调,容易出现变形
  • 单位换算易出错,维护成本高

适用场景

内容型页面、需要充分利用屏幕空间的场景。

方案三:Rem 动态计算

核心原理

根据视口宽度动态计算根字体大小,所有尺寸使用 rem 单位。

function setRem() {
  const designWidth = 1920;
  const rem = (window.innerWidth / designWidth) * 100;
  document.documentElement.style.fontSize = rem + 'px';
}

// CSS 中使用 rem
.card {
  width: 4rem;      /* 设计稿 400px */
  height: 2rem;     /* 设计稿 200px */
  font-size: 0.24rem; /* 设计稿 24px */
}

优缺点分析

优点:

  • 计算相对直观(设计稿 px / 100 = rem)
  • 兼容性最好,支持 IE9+
  • 无缩放模糊问题

缺点:

  • 依赖 JS 初始化,页面可能闪烁
  • 高度处理困难,只能基于宽度适配
  • 需要手动或使用工具转换单位

适用场景

移动端 H5 页面、PC 端后台系统。

方案四:流式/弹性布局

核心原理

使用百分比、Flexbox、Grid 等 CSS 原生能力实现响应式布局。

.dashboard {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
}

.card {
  display: flex;
  flex-direction: column;
  padding: 2%;
}

.chart {
  flex: 1;
  min-height: 200px;
}

优缺点分析

优点:

  • 纯 CSS 方案,无任何依赖
  • 自然响应式,适配各种屏幕
  • 内容自适应,信息密度可变

缺点:

  • 设计稿还原度低,无法精确控制
  • 图表比例容易失控
  • 空间利用率低,不够美观

适用场景

B 端后台系统、管理平台等以内容为主的页面。

方案五:响应式断点

核心原理

使用 @media 查询针对不同屏幕尺寸写多套样式规则。

/* 1920×1080 大屏 */
@media (min-width: 1920px) {
  .container { width: 1800px; font-size: 24px; }
  .grid { grid-template-columns: repeat(4, 1fr); }
}

/* 3840×2160 4K 屏 */
@media (min-width: 3840px) {
  .container { width: 3600px; font-size: 48px; }
  .grid { grid-template-columns: repeat(6, 1fr); }
}

/* 1366×768 小屏 */
@media (max-width: 1366px) {
  .container { width: 1300px; font-size: 18px; }
  .grid { grid-template-columns: repeat(3, 1fr); }
}

优缺点分析

优点:

  • 精确控制各个分辨率的显示效果
  • 字体可读性可控
  • 适合有固定规格的大屏群

缺点:

  • 代码量爆炸,维护极其困难
  • 无法覆盖所有可能的分辨率
  • 新增规格需要大量修改

适用场景

有固定规格的大屏群、展厅多屏展示系统。

方案六:混合方案(推荐)

核心原理

结合 Scale + Rem 的优点,既保证设计稿比例,又确保字体在小屏可读。

const DESIGN_WIDTH = 1920;
const DESIGN_HEIGHT = 1080;
const MIN_SCALE = 0.6; // 最小缩放限制

function adapt() {
  const winW = window.innerWidth;
  const winH = window.innerHeight;

  // Scale 计算 - 保证整体比例
  const scaleX = winW / DESIGN_WIDTH;
  const scaleY = winH / DESIGN_HEIGHT;
  const scale = Math.max(Math.min(scaleX, scaleY), MIN_SCALE);

  // 应用 scale
  screen.style.transform = `scale(${scale})`;

  // Rem 计算 - 根据缩放比例调整根字体
  // 当 scale < 1 时,增加根字体补偿
  const baseRem = 100;
  const fontScale = Math.max(scale, MIN_SCALE);
  const rem = baseRem * fontScale;
  document.documentElement.style.fontSize = rem + 'px';
}

优缺点分析

优点:

  • 等比例缩放保证布局一致性
  • 字体最小值保护,防止过小不可读
  • 大屏清晰、小屏可读
  • 兼顾视觉和体验

缺点:

  • 需要 JS 支持
  • 计算逻辑稍复杂

适用场景

通用推荐方案,适合绝大多数大屏开发场景。

方案对比一览表

方案 实现难度 设计稿还原度 响应式表现 小屏可读性 维护成本 推荐场景
Scale 简单 极高 一般 数据可视化大屏
VW/VH 中等 中等 中等 中等 内容型页面
Rem 中等 一般 中等 中等 移动端 H5
流式布局 简单 极好 B 端后台系统
断点方案 复杂 中-高 极高 固定规格大屏群
混合方案 中等 中等 通用推荐

场景推荐速查

🖥️ 数据可视化大屏 / 监控中心

需要精确还原设计稿,图表比例严格保持,像素级对齐。

推荐: Scale 等比例缩放(接受黑边)或 混合方案

示例: 企业展厅大屏、运营监控看板

📊 B 端后台 / 管理系统

内容为主,需要充分利用屏幕空间,信息密度要高。

推荐: 流式布局 或 VW/VH 方案

示例: CRM 系统、数据管理平台

🌐 多端适配 / 响应式网站

需要覆盖手机、平板、电脑、大屏等多种设备。

推荐: 响应式断点 + 流式布局

示例: 企业官网、数据门户

完整 Demo 代码

以下是 6 种大屏适配方案的完整可运行代码,保存为 HTML 文件后可直接在浏览器中打开体验。

Demo 1: Scale 等比例缩放

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>方案1: Scale 等比例缩放 - 大屏适配方案对比</title>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }

        /* 信息面板 - 不参与缩放 */
        .info-panel {
            position: fixed;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.85);
            color: #fff;
            padding: 20px;
            border-radius: 8px;
            z-index: 9999;
            width: 320px;
            font-size: 14px;
        }

        .info-panel h3 {
            color: #4fc3f7;
            margin-bottom: 12px;
            font-size: 16px;
        }

        .info-panel .pros-cons {
            margin-bottom: 15px;
        }

        .info-panel .pros {
            color: #81c784;
        }

        .info-panel .cons {
            color: #e57373;
        }

        .info-panel .current-scale {
            background: #ff9800;
            color: #000;
            padding: 8px 12px;
            border-radius: 4px;
            font-weight: bold;
            margin-top: 10px;
        }

        /* 大屏容器 - 按 1920*1080 设计 */
        .screen-container {
            width: 1920px;
            height: 1080px;
            transform-origin: 0 0;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            position: relative;
            overflow: hidden;
        }

        /* 大屏内容样式 */
        .header {
            height: 100px;
            background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 48px;
            color: #fff;
            text-shadow: 0 0 20px rgba(79, 195, 247, 0.5);
            letter-spacing: 8px;
        }

        .main-content {
            display: flex;
            padding: 30px;
            gap: 20px;
            height: calc(100% - 100px);
        }

        .sidebar {
            width: 400px;
            background: rgba(15, 52, 96, 0.3);
            border-radius: 16px;
            padding: 20px;
            border: 1px solid rgba(79, 195, 247, 0.2);
        }

        .chart-area {
            flex: 1;
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 20px;
        }

        .card {
            background: rgba(15, 52, 96, 0.3);
            border-radius: 16px;
            padding: 20px;
            border: 1px solid rgba(79, 195, 247, 0.2);
            display: flex;
            flex-direction: column;
        }

        .card-title {
            color: #4fc3f7;
            font-size: 24px;
            margin-bottom: 15px;
        }

        .card-value {
            color: #fff;
            font-size: 56px;
            font-weight: bold;
        }

        .mini-chart {
            flex: 1;
            margin-top: 15px;
            border-radius: 8px;
            min-height: 180px;
        }

        .notice {
            position: absolute;
            bottom: 20px;
            left: 20px;
            right: 20px;
            background: rgba(255, 152, 0, 0.2);
            border: 1px solid #ff9800;
            color: #ff9800;
            padding: 15px 20px;
            border-radius: 8px;
            font-size: 18px;
        }

        .grid-line {
            position: absolute;
            top: 50%;
            left: 0;
            right: 0;
            height: 1px;
            border-top: 2px dashed rgba(79, 195, 247, 0.1);
        }

        .grid-line::before {
            content: '设计稿中心线 (960px)';
            position: absolute;
            left: 50%;
            transform: translateX(-50%);
            color: rgba(79, 195, 247, 0.3);
            font-size: 14px;
        }
    </style>
</head>
<body>
    <!-- 信息面板 -->
    <div class="info-panel">
        <h3>方案1: Scale 等比例缩放</h3>
        <div class="pros-cons">
            <div class="pros">✓ 优点:</div>
            • 完美还原设计稿比例<br>
            • 实现简单直观<br>
            • 字体/图表自动缩放<br><br>
            <div class="cons">✗ 缺点:</div>
            • 屏幕比例不符时出现黑边<br>
            • 字体过小可能看不清<br>
            • 无法利用多余空间
        </div>
        <div class="current-scale" id="scaleInfo">
            缩放比例: 1.0<br>
            窗口尺寸: 1920×1080
        </div>
    </div>

    <!-- 大屏容器 -->
    <div class="screen-container" id="screen">
        <div class="header">SCALE 方案演示 - 1920×1080 设计稿</div>

        <div class="main-content">
            <div class="sidebar">
                <div class="card-title">左侧信息面板</div>
                <p style="color: rgba(255,255,255,0.7); font-size: 18px; line-height: 1.8;">
                    这是基于 1920×1080 设计稿开发的页面。<br><br>
                    修改浏览器窗口大小,观察整个页面如何等比例缩放。注意两侧的空白区域(当屏幕比例不是 16:9 时)。
                </p>
            </div>

            <div class="chart-area">
                <div class="card">
                    <div class="card-title">实时用户数</div>
                    <div class="card-value" id="value1">128,456</div>
                    <div class="mini-chart" id="chart1"></div>
                </div>
                <div class="card">
                    <div class="card-title">交易金额</div>
                    <div class="card-value" id="value2">¥2.3M</div>
                    <div class="mini-chart" id="chart2"></div>
                </div>
                <div class="card">
                    <div class="card-title">系统负载</div>
                    <div class="card-value" id="value3">68%</div>
                    <div class="mini-chart" id="chart3"></div>
                </div>
                <div class="card">
                    <div class="card-title">响应时间</div>
                    <div class="card-value" id="value4">23ms</div>
                    <div class="mini-chart" id="chart4"></div>
                </div>
            </div>
        </div>

        <div class="grid-line"></div>

        <div class="notice">
            💡 提示:调整浏览器窗口为 4:3 比例或手机尺寸,观察两侧的黑边/留白。这是 Scale 方案的典型特征。
        </div>
    </div>

    <script>
        const screen = document.getElementById('screen');
        const scaleInfo = document.getElementById('scaleInfo');

        // 设计稿尺寸
        const DESIGN_WIDTH = 1920;
        const DESIGN_HEIGHT = 1080;

        function setScale() {
            const winW = window.innerWidth;
            const winH = window.innerHeight;

            // 计算宽高缩放比例,取较小值保持完整显示
            const scaleX = winW / DESIGN_WIDTH;
            const scaleY = winH / DESIGN_HEIGHT;
            const scale = Math.min(scaleX, scaleY);

            // 应用缩放
            screen.style.transform = `scale(${scale})`;

            // 可选:居中显示
            const left = (winW - DESIGN_WIDTH * scale) / 2;
            const top = (winH - DESIGN_HEIGHT * scale) / 2;
            screen.style.position = 'absolute';
            screen.style.left = left + 'px';
            screen.style.top = top + 'px';

            // 更新信息
            scaleInfo.innerHTML = `
                缩放比例: ${scale.toFixed(3)}<br>
                窗口尺寸: ${winW}×${winH}<br>
                设计稿: 1920×1080<br>
                空白区域: ${Math.round((winW - DESIGN_WIDTH * scale))}×${Math.round((winH - DESIGN_HEIGHT * scale))}
            `;
        }

        setScale();

        // ===== ECharts 图表配置 =====
        const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
        chart1.setOption({
            backgroundColor: 'transparent',
            grid: { top: 30, right: 20, bottom: 25, left: 50 },
            xAxis: {
                type: 'category',
                data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
                axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 12 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
                axisLabel: { color: '#fff' }
            },
            series: [{
                type: 'bar',
                data: [32000, 28000, 85000, 120000, 98000, 128456],
                itemStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: '#4fc3f7' },
                        { offset: 1, color: '#2196f3' }
                    ]),
                    borderRadius: [4, 4, 0, 0]
                }
            }]
        });

        const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
        chart2.setOption({
            backgroundColor: 'transparent',
            grid: { top: 30, right: 20, bottom: 25, left: 50 },
            xAxis: {
                type: 'category',
                data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
                axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 12 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
                axisLabel: { color: '#fff', formatter: '¥{value}万' }
            },
            series: [{
                type: 'line',
                data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
                smooth: true,
                lineStyle: { color: '#e91e63', width: 3 },
                itemStyle: { color: '#e91e63' },
                areaStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
                        { offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
                    ])
                }
            }]
        });

        const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
        chart3.setOption({
            backgroundColor: 'transparent',
            series: [{
                type: 'pie',
                radius: ['40%', '70%'],
                center: ['50%', '55%'],
                data: [
                    { value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
                    { value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
                    { value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
                    { value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
                ],
                label: { color: '#fff', fontSize: 11 }
            }]
        });

        const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
        chart4.setOption({
            backgroundColor: 'transparent',
            series: [{
                type: 'gauge',
                radius: '80%',
                center: ['50%', '55%'],
                min: 0,
                max: 100,
                splitNumber: 10,
                axisLine: {
                    lineStyle: {
                        width: 10,
                        color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
                    }
                },
                pointer: { itemStyle: { color: '#4fc3f7' } },
                detail: {
                    formatter: '{value}ms',
                    color: '#4fc3f7',
                    fontSize: 20,
                    offsetCenter: [0, '70%']
                },
                data: [{ value: 23 }]
            }]
        });

        // 图表引用数组用于resize
        const charts = [chart1, chart2, chart3, chart4];

        // 防抖处理 resize
        let timer;
        window.addEventListener('resize', () => {
            clearTimeout(timer);
            timer = setTimeout(() => {
                setScale();
                charts.forEach(chart => chart.resize());
            }, 100);
        });

        // 模拟数据更新
        setInterval(() => {
            const newValue = Math.floor(Math.random() * 20) + 15;
            chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
            document.getElementById('value4').textContent = newValue + 'ms';
        }, 3000);
    </script>
</body>
</html>

使用说明:将以上代码保存为 1-scale-demo.html,直接在浏览器中打开即可体验。调整窗口大小观察等比例缩放效果,注意两侧的黑边。

Demo 2: VW/VH 视口单位方案

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>方案2: VW/VH 方案 - 大屏适配方案对比</title>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            min-height: 100vh;
            overflow-x: hidden;
        }

        /* 信息面板 */
        .info-panel {
            position: fixed;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.85);
            color: #fff;
            padding: 20px;
            border-radius: 8px;
            z-index: 9999;
            width: 320px;
            font-size: 14px;
        }

        .info-panel h3 {
            color: #81c784;
            margin-bottom: 12px;
            font-size: 16px;
        }

        .info-panel .pros-cons {
            margin-bottom: 15px;
        }

        .info-panel .pros {
            color: #81c784;
        }

        .info-panel .cons {
            color: #e57373;
        }

        .info-panel .formula {
            background: #333;
            padding: 10px;
            border-radius: 4px;
            font-family: 'Courier New', monospace;
            font-size: 12px;
            margin-top: 10px;
            color: #ff9800;
        }

        /* VW/VH 布局 - 直接使用视口单位 */
        .header {
            /* 设计稿 100px / 1080px * 100vh */
            height: 9.259vh;
            background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            /* 设计稿 48px / 1080px * 100vh */
            font-size: 4.444vh;
            color: #fff;
            text-shadow: 0 0 2vh rgba(79, 195, 247, 0.5);
            letter-spacing: 0.7vw;
        }

        .main-content {
            display: flex;
            /* 设计稿 30px / 1080px * 100vh */
            padding: 2.778vh 1.562vw;
            /* 设计稿 20px / 1080px * 100vh */
            gap: 1.852vh 1.042vw;
            /* 总高度 100vh - header 高度 */
            height: 90.741vh;
        }

        .sidebar {
            /* 设计稿 400px / 1920px * 100vw */
            width: 20.833vw;
            background: rgba(15, 52, 96, 0.3);
            /* 设计稿 16px / 1080px * 100vh */
            border-radius: 1.481vh;
            /* 设计稿 20px / 1080px * 100vh */
            padding: 1.852vh;
            border: 0.093vh solid rgba(79, 195, 247, 0.2);
        }

        .chart-area {
            flex: 1;
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 1.852vh 1.042vw;
        }

        .card {
            background: rgba(15, 52, 96, 0.3);
            border-radius: 1.481vh;
            padding: 1.852vh;
            border: 0.093vh solid rgba(79, 195, 247, 0.2);
            display: flex;
            flex-direction: column;
        }

        .card-title {
            color: #4fc3f7;
            /* 设计稿 24px / 1080px * 100vh */
            font-size: 2.222vh;
            margin-bottom: 1.389vh;
        }

        .card-value {
            color: #fff;
            /* 设计稿 56px / 1080px * 100vh */
            font-size: 5.185vh;
            font-weight: bold;
        }

        .mini-chart {
            flex: 1;
            min-height: 13.889vh;
            margin-top: 1.5vh;
            border-radius: 0.741vh;
        }

        /* 代码展示区域 */
        .code-panel {
            margin-top: 2vh;
            background: rgba(0, 0, 0, 0.5);
            padding: 1.5vh;
            border-radius: 1vh;
            font-family: 'Courier New', monospace;
            font-size: 1.3vh;
            color: #a5d6a7;
            overflow: hidden;
        }

        .notice {
            position: fixed;
            bottom: 2vh;
            left: 2vw;
            right: 22vw;
            background: rgba(244, 67, 54, 0.2);
            border: 1px solid #f44336;
            color: #f44336;
            padding: 1.5vh 2vw;
            border-radius: 0.8vh;
            font-size: 1.6vh;
        }

        /* 问题演示:文字溢出 */
        .overflow-demo {
            background: rgba(244, 67, 54, 0.1);
            border: 1px dashed #f44336;
            padding: 1vh;
            margin-top: 1vh;
            font-size: 1.5vh;
        }

        /* 使用 CSS 变量简化计算 */
        :root {
            --vh: 1vh;
            --vw: 1vw;
        }

        /* 但这不是完美的解决方案 */
        .sidebar p {
            color: rgba(255,255,255,0.7);
            font-size: 1.667vh;
            line-height: 1.8;
        }
    </style>
</head>
<body>
    <!-- 信息面板 -->
    <div class="info-panel">
        <h3>方案2: VW/VH 方案</h3>
        <div class="pros-cons">
            <div class="pros">✓ 优点:</div>
            • 无 JS 依赖<br>
            • 利用全部视口空间<br>
            • 无黑边/留白<br><br>
            <div class="cons">✗ 缺点:</div>
            • 计算公式复杂<br>
            • 单位换算容易出错<br>
            • 字体可能过大/过小<br>
            • 宽高比例难以协调
        </div>
        <div class="formula">
            计算公式:<br>
            100px / 1920px * 100vw<br>
            = 5.208vw
        </div>
    </div>

    <!-- 页面内容 -->
    <div class="header">VW/VH 方案演示 - 满屏无黑边</div>

    <div class="main-content">
        <div class="sidebar">
            <div class="card-title">左侧信息面板</div>
            <p>
                此方案使用 vw/vh 单位代替 px。<br><br>
                虽然页面始终铺满屏幕,但计算复杂。注意右侧卡牌区域在小屏幕上文字可能显得过大。
            </p>

            <div class="code-panel">
                /* 实际开发中的混乱 */<br>
                width: 20.833vw;<br>
                height: 13.889vh;<br>
                font-size: 4.444vh;<br>
                padding: 1.852vh 1.562vw;<br>
                /* 这些数字是怎么来的? */
            </div>

            <div class="overflow-demo">
                <strong>⚠️ 问题演示:</strong><br>
                当屏幕很宽但很矮时,文字按 vh 计算变得极小,而容器按 vw 计算保持宽大,导致内容稀疏。
            </div>
        </div>

        <div class="chart-area">
            <div class="card">
                <div class="card-title">实时用户数</div>
                <div class="card-value">128,456</div>
                <div class="mini-chart" id="chart1"></div>
            </div>
            <div class="card">
                <div class="card-title">交易金额</div>
                <div class="card-value">¥2.3M</div>
                <div class="mini-chart" id="chart2"></div>
            </div>
            <div class="card">
                <div class="card-title">系统负载</div>
                <div class="card-value">68%</div>
                <div class="mini-chart" id="chart3"></div>
            </div>
            <div class="card">
                <div class="card-title">响应时间</div>
                <div class="card-value">23ms</div>
                <div class="mini-chart" id="chart4"></div>
            </div>
        </div>
    </div>

    <div class="notice">
        ⚠️ 缺点演示:调整浏览器窗口为超宽矮屏(如 2560×600),观察字体与容器比例的失调。对比 Scale 方案在这个场景的表现。
    </div>

    <script>
        // ===== ECharts 图表配置 =====
        // 图表1: 柱状图 - 实时用户数
        const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
        const option1 = {
            backgroundColor: 'transparent',
            grid: { top: 30, right: 15, bottom: 25, left: 45 },
            xAxis: {
                type: 'category',
                data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
                axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            series: [{
                type: 'bar',
                data: [32000, 28000, 85000, 120000, 98000, 128456],
                itemStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: '#4fc3f7' },
                        { offset: 1, color: '#2196f3' }
                    ]),
                    borderRadius: [4, 4, 0, 0]
                },
                animationDuration: 1500
            }]
        };
        chart1.setOption(option1);

        // 图表2: 折线图 - 交易金额趋势
        const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
        const option2 = {
            backgroundColor: 'transparent',
            grid: { top: 30, right: 15, bottom: 25, left: 50 },
            xAxis: {
                type: 'category',
                data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
                axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
                axisLabel: { color: '#fff', fontSize: 10, formatter: '¥{value}' }
            },
            series: [{
                type: 'line',
                data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
                smooth: true,
                symbol: 'circle',
                symbolSize: 6,
                lineStyle: { color: '#e91e63', width: 3 },
                itemStyle: { color: '#e91e63' },
                areaStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
                        { offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
                    ])
                },
                animationDuration: 1500
            }]
        };
        chart2.setOption(option2);

        // 图表3: 饼图 - 系统负载分布
        const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
        const option3 = {
            backgroundColor: 'transparent',
            series: [{
                type: 'pie',
                radius: ['35%', '65%'],
                center: ['50%', '55%'],
                data: [
                    { value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
                    { value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
                    { value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
                    { value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
                ],
                label: { color: '#fff', fontSize: 10 },
                labelLine: { lineStyle: { color: 'rgba(255,255,255,0.5)' } },
                animationDuration: 1500
            }]
        };
        chart3.setOption(option3);

        // 图表4: 仪表盘 - 响应时间
        const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
        const option4 = {
            backgroundColor: 'transparent',
            series: [{
                type: 'gauge',
                radius: '75%',
                center: ['50%', '55%'],
                min: 0,
                max: 100,
                splitNumber: 10,
                axisLine: {
                    lineStyle: {
                        width: 8,
                        color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
                    }
                },
                pointer: { itemStyle: { color: '#4fc3f7' }, width: 4 },
                axisTick: { distance: -8, length: 4, lineStyle: { color: '#fff' } },
                splitLine: { distance: -8, length: 10, lineStyle: { color: '#fff' } },
                axisLabel: { color: '#fff', distance: -20, fontSize: 9 },
                detail: {
                    valueAnimation: true,
                    formatter: '{value}ms',
                    color: '#4fc3f7',
                    fontSize: 16,
                    offsetCenter: [0, '65%']
                },
                data: [{ value: 23 }],
                animationDuration: 2000
            }]
        };
        chart4.setOption(option4);

        // 响应式调整
        const charts = [chart1, chart2, chart3, chart4];
        window.addEventListener('resize', () => {
            charts.forEach(chart => chart.resize());
        });

        // 模拟数据更新
        setInterval(() => {
            const newValue = Math.floor(Math.random() * 20) + 15;
            chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
        }, 3000);
    </script>
</body>
</html>

Demo 3: Rem 动态计算方案

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>方案3: Rem 方案 - 大屏适配方案对比</title>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
    <script>
        // Rem 计算逻辑
        (function() {
            const designWidth = 1920;

            function setRem() {
                const winWidth = window.innerWidth;
                // 以设计稿宽度为基准,100rem = 设计稿宽度
                const rem = winWidth / designWidth * 100;
                document.documentElement.style.fontSize = rem + 'px';
            }

            setRem();

            let timer;
            window.addEventListener('resize', function() {
                clearTimeout(timer);
                timer = setTimeout(setRem, 100);
            });
        })();
    </script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            min-height: 100vh;
            overflow-x: hidden;
        }

        /* 信息面板 */
        .info-panel {
            position: fixed;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.85);
            color: #fff;
            padding: 0.2rem;
            border-radius: 0.08rem;
            z-index: 9999;
            width: 3.3rem;
            font-size: 0.073rem;
        }

        .info-panel h3 {
            color: #4fc3f7;
            margin-bottom: 0.062rem;
            font-size: 0.083rem;
        }

        .info-panel .pros-cons {
            margin-bottom: 0.078rem;
        }

        .info-panel .pros {
            color: #81c784;
        }

        .info-panel .cons {
            color: #e57373;
        }

        .info-panel .current-rem {
            background: #4caf50;
            color: #000;
            padding: 0.05rem;
            border-radius: 0.04rem;
            font-weight: bold;
            margin-top: 0.05rem;
        }

        /* Rem 布局 - 1rem = 设计稿中 100px (在 1920px 宽度下) */
        .header {
            /* 设计稿 100px = 1rem */
            height: 1rem;
            background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            /* 设计稿 48px = 0.48rem */
            font-size: 0.48rem;
            color: #fff;
            text-shadow: 0 0 0.1rem rgba(79, 195, 247, 0.5);
            letter-spacing: 0.08rem;
        }

        .main-content {
            display: flex;
            /* 设计稿 30px = 0.3rem */
            padding: 0.3rem;
            /* 设计稿 20px = 0.2rem */
            gap: 0.2rem;
            /* 总高度 100vh - header */
            min-height: calc(100vh - 1rem);
        }

        .sidebar {
            /* 设计稿 400px = 4rem */
            width: 4rem;
            background: rgba(15, 52, 96, 0.3);
            /* 设计稿 16px = 0.16rem */
            border-radius: 0.16rem;
            padding: 0.2rem;
            border: 0.01rem solid rgba(79, 195, 247, 0.2);
        }

        .sidebar p {
            color: rgba(255,255,255,0.7);
            font-size: 0.18rem;
            line-height: 1.8;
        }

        .chart-area {
            flex: 1;
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 0.2rem;
        }

        .card {
            background: rgba(15, 52, 96, 0.3);
            border-radius: 0.16rem;
            padding: 0.2rem;
            border: 0.01rem solid rgba(79, 195, 247, 0.2);
            display: flex;
            flex-direction: column;
        }

        .card-title {
            color: #4fc3f7;
            font-size: 0.22rem;
            margin-bottom: 0.12rem;
        }

        .card-value {
            color: #fff;
            font-size: 0.56rem;
            font-weight: bold;
        }

        .mini-chart {
            flex: 1;
            min-height: 1.5rem;
            margin-top: 0.15rem;
            border-radius: 0.08rem;
        }

        /* 代码展示 */
        .code-panel {
            margin-top: 0.25rem;
            background: rgba(0, 0, 0, 0.5);
            padding: 0.15rem;
            border-radius: 0.1rem;
            font-family: 'Courier New', monospace;
            font-size: 0.13rem;
            color: #a5d6a7;
        }

        /* 闪烁问题演示 */
        .flash-demo {
            margin-top: 0.2rem;
            padding: 0.15rem;
            background: rgba(255, 152, 0, 0.1);
            border: 1px dashed #ff9800;
            color: #ff9800;
            font-size: 0.15rem;
        }

        .fouc-warning {
            background: rgba(244, 67, 54, 0.2);
            border: 1px solid #f44336;
            color: #f44336;
            padding: 0.15rem;
            margin-top: 0.2rem;
            border-radius: 0.08rem;
            font-size: 0.15rem;
        }

        .notice {
            position: fixed;
            bottom: 0.2rem;
            left: 0.3rem;
            right: 3.6rem;
            background: rgba(255, 152, 0, 0.2);
            border: 1px solid #ff9800;
            color: #ff9800;
            padding: 0.15rem 0.2rem;
            border-radius: 0.08rem;
            font-size: 0.16rem;
        }

        /* FOUC 模拟 - 页面加载时的闪烁 */
        .no-js-fallback {
            display: none;
        }
    </style>
</head>
<body>
    <!-- 信息面板 -->
    <div class="info-panel">
        <h3>方案3: Rem 方案</h3>
        <div class="pros-cons">
            <div class="pros">✓ 优点:</div>
            • 计算相对直观 (设计稿/100)<br>
            • 兼容性好 (支持 IE)<br>
            • 宽高等比缩放<br><br>
            <div class="cons">✗ 缺点:</div>
            • 依赖 JS 设置根字体<br>
            • 页面加载可能闪烁<br>
            • 高度仍需要特殊处理<br>
            • 设计稿转换工作量大
        </div>
        <div class="current-rem" id="remInfo">
            根字体: 100px<br>
            (1920px 宽度下)
        </div>
    </div>

    <!-- 页面内容 -->
    <div class="header">REM 方案演示 - JS 动态计算</div>

    <div class="main-content">
        <div class="sidebar">
            <div class="card-title">左侧信息面板</div>
            <p>
                1rem = 设计稿的 100px<br><br>
                在 1920px 宽度的屏幕上,根字体大小为 100px,便于计算转换。
            </p>

            <div class="code-panel">
                // JS 计算根字体 (head 中)<br>
                const rem = winWidth / 1920 * 100;<br>
                html.style.fontSize = rem + 'px';<br><br>
                // CSS 使用<br>
                width: 4rem;  /* = 400px */
            </div>

            <div class="flash-demo">
                <strong>⚠️ 闪烁问题 (FOUC):</strong><br>
                如果 JS 在 head 末尾执行,页面会先按默认 16px 渲染,然后跳动到计算值。
            </div>

            <div class="fouc-warning">
                💡 解决方案:将 rem 计算脚本放在 &lt;head&gt; 最前面,或使用内联 style。
            </div>
        </div>

        <div class="chart-area">
            <div class="card">
                <div class="card-title">实时用户数</div>
                <div class="card-value">128,456</div>
                <div class="mini-chart" id="chart1"></div>
            </div>
            <div class="card">
                <div class="card-title">交易金额</div>
                <div class="card-value">¥2.3M</div>
                <div class="mini-chart" id="chart2"></div>
            </div>
            <div class="card">
                <div class="card-title">系统负载</div>
                <div class="card-value">68%</div>
                <div class="mini-chart" id="chart3"></div>
            </div>
            <div class="card">
                <div class="card-title">响应时间</div>
                <div class="card-value">23ms</div>
                <div class="mini-chart" id="chart4"></div>
            </div>
        </div>
    </div>

    <div class="notice">
        💡 修改窗口大小观察变化。注意:此方案主要处理宽度适配,高度方向元素可能会超出屏幕(尝试将窗口压得很矮)。
    </div>

    <script>
        // 更新 rem 信息
        function updateRemInfo() {
            const rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
            document.getElementById('remInfo').innerHTML = `
                根字体: ${rem.toFixed(2)}px<br>
                窗口宽度: ${window.innerWidth}px<br>
                1rem = 设计稿的 100px
            `;
        }

        updateRemInfo();

        let timer;
        window.addEventListener('resize', function() {
            clearTimeout(timer);
            timer = setTimeout(updateRemInfo, 100);
        });

        // ===== ECharts 图表配置 =====
        // 图表1: 柱状图 - 实时用户数
        const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
        const option1 = {
            backgroundColor: 'transparent',
            grid: { top: 30, right: 15, bottom: 25, left: 45 },
            xAxis: {
                type: 'category',
                data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
                axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            series: [{
                type: 'bar',
                data: [32000, 28000, 85000, 120000, 98000, 128456],
                itemStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: '#4fc3f7' },
                        { offset: 1, color: '#2196f3' }
                    ]),
                    borderRadius: [4, 4, 0, 0]
                },
                animationDuration: 1500
            }]
        };
        chart1.setOption(option1);

        // 图表2: 折线图 - 交易金额趋势
        const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
        const option2 = {
            backgroundColor: 'transparent',
            grid: { top: 30, right: 15, bottom: 25, left: 50 },
            xAxis: {
                type: 'category',
                data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
                axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
                axisLabel: { color: '#fff', fontSize: 10, formatter: '¥{value}' }
            },
            series: [{
                type: 'line',
                data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
                smooth: true,
                symbol: 'circle',
                symbolSize: 6,
                lineStyle: { color: '#e91e63', width: 3 },
                itemStyle: { color: '#e91e63' },
                areaStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
                        { offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
                    ])
                },
                animationDuration: 1500
            }]
        };
        chart2.setOption(option2);

        // 图表3: 饼图 - 系统负载分布
        const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
        const option3 = {
            backgroundColor: 'transparent',
            series: [{
                type: 'pie',
                radius: ['35%', '65%'],
                center: ['50%', '55%'],
                data: [
                    { value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
                    { value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
                    { value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
                    { value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
                ],
                label: { color: '#fff', fontSize: 10 },
                labelLine: { lineStyle: { color: 'rgba(255,255,255,0.5)' } },
                animationDuration: 1500
            }]
        };
        chart3.setOption(option3);

        // 图表4: 仪表盘 - 响应时间
        const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
        const option4 = {
            backgroundColor: 'transparent',
            series: [{
                type: 'gauge',
                radius: '75%',
                center: ['50%', '55%'],
                min: 0,
                max: 100,
                splitNumber: 10,
                axisLine: {
                    lineStyle: {
                        width: 8,
                        color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
                    }
                },
                pointer: { itemStyle: { color: '#4fc3f7' }, width: 4 },
                axisTick: { distance: -8, length: 4, lineStyle: { color: '#fff' } },
                splitLine: { distance: -8, length: 10, lineStyle: { color: '#fff' } },
                axisLabel: { color: '#fff', distance: -20, fontSize: 9 },
                detail: {
                    valueAnimation: true,
                    formatter: '{value}ms',
                    color: '#4fc3f7',
                    fontSize: 16,
                    offsetCenter: [0, '65%']
                },
                data: [{ value: 23 }],
                animationDuration: 2000
            }]
        };
        chart4.setOption(option4);

        // 响应式调整
        const charts = [chart1, chart2, chart3, chart4];
        window.addEventListener('resize', () => {
            charts.forEach(chart => chart.resize());
        });

        // 模拟数据更新
        setInterval(() => {
            const newValue = Math.floor(Math.random() * 20) + 15;
            chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
        }, 3000);
    </script>
</body>
</html>

Demo 4: 流式/弹性布局方案

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>方案4: 流式布局 - 大屏适配方案对比</title>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            min-height: 100vh;
            overflow-x: hidden;
        }

        /* 信息面板 - 使用 px 保持固定大小参考 */
        .info-panel {
            position: fixed;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.85);
            color: #fff;
            padding: 20px;
            border-radius: 8px;
            z-index: 9999;
            width: 320px;
            font-size: 14px;
        }

        .info-panel h3 {
            color: #ff9800;
            margin-bottom: 12px;
            font-size: 16px;
        }

        .info-panel .pros-cons {
            margin-bottom: 15px;
        }

        .info-panel .pros {
            color: #81c784;
        }

        .info-panel .cons {
            color: #e57373;
        }

        /* 流式布局 - 使用 % fr auto 等弹性单位 */
        .header {
            height: 10%; /* 百分比高度 */
            min-height: 60px;
            max-height: 120px;
            background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: clamp(24px, 4vw, 48px); /* 流体字体 */
            color: #fff;
            text-shadow: 0 0 20px rgba(79, 195, 247, 0.5);
            letter-spacing: 0.5vw;
            padding: 0 5%;
        }

        .main-content {
            display: flex;
            padding: 3%;
            gap: 2%;
            min-height: calc(90% - 60px);
        }

        .sidebar {
            width: 25%; /* 百分比宽度 */
            min-width: 200px;
            max-width: 400px;
            background: rgba(15, 52, 96, 0.3);
            border-radius: 16px;
            padding: 20px;
            border: 1px solid rgba(79, 195, 247, 0.2);
        }

        .sidebar p {
            color: rgba(255,255,255,0.7);
            font-size: clamp(14px, 1.5vw, 18px);
            line-height: 1.8;
        }

        /* CSS Grid 布局 */
        .chart-area {
            flex: 1;
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 20px;
        }

        .card {
            background: rgba(15, 52, 96, 0.3);
            border-radius: 16px;
            padding: 20px;
            border: 1px solid rgba(79, 195, 247, 0.2);
            display: flex;
            flex-direction: column;
            min-height: 150px;
        }

        .card-title {
            color: #4fc3f7;
            font-size: clamp(16px, 2vw, 24px);
            margin-bottom: 10px;
        }

        .card-value {
            color: #fff;
            font-size: clamp(28px, 4vw, 56px);
            font-weight: bold;
            white-space: nowrap;
        }

        .mini-chart {
            flex: 1;
            min-height: 100px;
            margin-top: 10px;
            border-radius: 8px;
        }

        /* 问题展示:数据大屏布局崩坏 */
        .problem-demo {
            margin-top: 20px;
            padding: 15px;
            background: rgba(244, 67, 54, 0.1);
            border: 1px dashed #f44336;
            border-radius: 8px;
        }

        .problem-demo h4 {
            color: #f44336;
            margin-bottom: 10px;
        }

        .problem-demo p {
            color: #f44336;
            font-size: 14px;
        }

        /* 视觉偏差对比: */
        .comparison-box {
            display: flex;
            gap: 20px;
            margin-top: 15px;
        }

        .fixed-ratio {
            width: 100px;
            height: 60px;
            background: #4fc3f7;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #000;
            font-size: 12px;
        }

        .fluid-shape {
            width: 20%;
            padding-bottom: 12%;
            background: #ff9800;
            position: relative;
        }

        .fluid-shape span {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: #000;
            font-size: 12px;
            white-space: nowrap;
        }

        .notice {
            position: fixed;
            bottom: 20px;
            left: 20px;
            right: 360px;
            background: rgba(244, 67, 54, 0.2);
            border: 1px solid #f44336;
            color: #f44336;
            padding: 15px 20px;
            border-radius: 8px;
            font-size: 14px;
        }

        .label-tag {
            display: inline-block;
            background: #4caf50;
            color: #fff;
            padding: 2px 8px;
            border-radius: 4px;
            font-size: 12px;
            margin-right: 5px;
        }

        .label-tag.warning {
            background: #ff9800;
        }
    </style>
</head>
<body>
    <!-- 信息面板 -->
    <div class="info-panel">
        <h3>方案4: 流式/弹性布局</h3>
        <div class="pros-cons">
            <div class="pros">✓ 优点:</div>
            • 纯 CSS,无依赖<br>
            • 自然响应式<br>
            • 内容自适应<br><br>
            <div class="cons">✗ 缺点:</div><strong>大屏空间利用率低</strong><br>
            • 图表比例难以控制<br>
            • 无法精确还原设计稿<br>
            • 文字/图形可能变形
        </div>
        <div style="margin-top: 10px; font-size: 12px; color: #999;">
            适合:后台管理系统<br>
            不适合:数据可视化大屏
        </div>
    </div>

    <!-- 页面内容 -->
    <div class="header">流式布局演示 - 自然伸缩</div>

    <div class="main-content">
        <div class="sidebar">
            <div style="color: #ff9800; font-size: 18px; margin-bottom: 15px;">左侧信息面板</div>
            <p>
                <span class="label-tag">%</span> 百分比宽度<br>
                <span class="label-tag">fr</span> Grid 弹性分配<br>
                <span class="label-tag warning">clamp</span> 流体字体<br><br>

                这个方案使用 CSS 的固有响应式能力。但请注意右侧卡片在宽屏上的变化——它们会无限拉宽!
            </p>

            <div class="problem-demo">
                <h4>⚠️ 大屏场景的问题</h4>
                <p>
                    <strong>问题1:</strong> 宽屏下卡片过度拉伸<br>
                    <strong>问题2:</strong> 图表比例失控<br>
                    <strong>问题3:</strong> 无法精确对齐设计稿像素
                </p>

                <div class="comparison-box">
                    <div class="fixed-ratio">固定比例</div>
                    <div class="fluid-shape"><span>20%宽度</span></div>
                </div>
                <p style="margin-top: 10px; font-size: 12px;">
                    调整窗口宽度,观察流体元素的宽高比例变化。
                </p>
            </div>
        </div>

        <div class="chart-area">
            <div class="card">
                <div class="card-title">实时用户数</div>
                <div class="card-value">128,456</div>
                <div class="mini-chart" id="chart1"></div>
            </div>
            <div class="card">
                <div class="card-title">交易金额</div>
                <div class="card-value">¥2.3M</div>
                <div class="mini-chart" id="chart2"></div>
            </div>
            <div class="card">
                <div class="card-title">系统负载</div>
                <div class="card-value">68%</div>
                <div class="mini-chart" id="chart3"></div>
            </div>
            <div class="card">
                <div class="card-title">响应时间</div>
                <div class="card-value">23ms</div>
                <div class="mini-chart" id="chart4"></div>
            </div>
        </div>
    </div>

    <div class="notice">
        ❌ 调整窗口到超宽屏(≥2560px),观察右侧卡片的变形:宽高比例完全失控,图表变成"矮胖"形状。对比 Scale 方案在这个场景的表现。
    </div>

    <script>
        // ===== ECharts 图表配置 =====
        // 图表1: 柱状图 - 实时用户数
        const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
        const option1 = {
            backgroundColor: 'transparent',
            grid: { top: 30, right: 15, bottom: 25, left: 45 },
            xAxis: {
                type: 'category',
                data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
                axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            series: [{
                type: 'bar',
                data: [32000, 28000, 85000, 120000, 98000, 128456],
                itemStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: '#4fc3f7' },
                        { offset: 1, color: '#2196f3' }
                    ]),
                    borderRadius: [4, 4, 0, 0]
                },
                animationDuration: 1500
            }]
        };
        chart1.setOption(option1);

        // 图表2: 折线图 - 交易金额趋势
        const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
        const option2 = {
            backgroundColor: 'transparent',
            grid: { top: 30, right: 15, bottom: 25, left: 50 },
            xAxis: {
                type: 'category',
                data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
                axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
                axisLabel: { color: '#fff', fontSize: 10, formatter: '¥{value}' }
            },
            series: [{
                type: 'line',
                data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
                smooth: true,
                symbol: 'circle',
                symbolSize: 6,
                lineStyle: { color: '#e91e63', width: 3 },
                itemStyle: { color: '#e91e63' },
                areaStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
                        { offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
                    ])
                },
                animationDuration: 1500
            }]
        };
        chart2.setOption(option2);

        // 图表3: 饼图 - 系统负载分布
        const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
        const option3 = {
            backgroundColor: 'transparent',
            series: [{
                type: 'pie',
                radius: ['35%', '65%'],
                center: ['50%', '55%'],
                data: [
                    { value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
                    { value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
                    { value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
                    { value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
                ],
                label: { color: '#fff', fontSize: 10 },
                labelLine: { lineStyle: { color: 'rgba(255,255,255,0.5)' } },
                animationDuration: 1500
            }]
        };
        chart3.setOption(option3);

        // 图表4: 仪表盘 - 响应时间
        const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
        const option4 = {
            backgroundColor: 'transparent',
            series: [{
                type: 'gauge',
                radius: '75%',
                center: ['50%', '55%'],
                min: 0,
                max: 100,
                splitNumber: 10,
                axisLine: {
                    lineStyle: {
                        width: 8,
                        color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
                    }
                },
                pointer: { itemStyle: { color: '#4fc3f7' }, width: 4 },
                axisTick: { distance: -8, length: 4, lineStyle: { color: '#fff' } },
                splitLine: { distance: -8, length: 10, lineStyle: { color: '#fff' } },
                axisLabel: { color: '#fff', distance: -20, fontSize: 9 },
                detail: {
                    valueAnimation: true,
                    formatter: '{value}ms',
                    color: '#4fc3f7',
                    fontSize: 16,
                    offsetCenter: [0, '65%']
                },
                data: [{ value: 23 }],
                animationDuration: 2000
            }]
        };
        chart4.setOption(option4);

        // 响应式调整
        const charts = [chart1, chart2, chart3, chart4];
        window.addEventListener('resize', () => {
            charts.forEach(chart => chart.resize());
        });

        // 模拟数据更新
        setInterval(() => {
            const newValue = Math.floor(Math.random() * 20) + 15;
            chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
        }, 3000);
    </script>
</body>
</html>

Demo 5: 响应式断点方案

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>方案5: 响应式断点 - 大屏适配方案对比</title>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            min-height: 100vh;
            overflow-x: hidden;
        }

        /* 信息面板 */
        .info-panel {
            position: fixed;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.85);
            color: #fff;
            padding: 20px;
            border-radius: 8px;
            z-index: 9999;
            width: 320px;
            font-size: 14px;
            max-height: 90vh;
            overflow-y: auto;
        }

        .info-panel h3 {
            color: #e91e63;
            margin-bottom: 12px;
            font-size: 16px;
        }

        .info-panel .pros-cons {
            margin-bottom: 15px;
        }

        .info-panel .pros {
            color: #81c784;
        }

        .info-panel .cons {
            color: #e57373;
        }

        .current-breakpoint {
            background: #e91e63;
            color: #fff;
            padding: 10px;
            border-radius: 4px;
            font-weight: bold;
            margin-top: 10px;
            font-size: 16px;
        }

        .breakpoint-list {
            margin-top: 10px;
            font-size: 12px;
        }

        .breakpoint-list div {
            padding: 4px 0;
        }

        .breakpoint-list .active {
            color: #e91e63;
            font-weight: bold;
        }

        /* 断点样式定义 */

        /* 默认/移动端: < 768px */
        .header {
            height: 50px;
            background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 16px;
            color: #fff;
            text-shadow: 0 0 10px rgba(79, 195, 247, 0.5);
            letter-spacing: 2px;
        }

        .main-content {
            display: flex;
            flex-direction: column;
            padding: 10px;
            gap: 15px;
        }

        .sidebar {
            width: 100%;
            background: rgba(15, 52, 96, 0.3);
            border-radius: 8px;
            padding: 15px;
            border: 1px solid rgba(79, 195, 247, 0.2);
        }

        .sidebar p {
            color: rgba(255,255,255,0.7);
            font-size: 14px;
            line-height: 1.6;
        }

        .chart-area {
            display: grid;
            grid-template-columns: 1fr;
            gap: 15px;
        }

        .card {
            background: rgba(15, 52, 96, 0.3);
            border-radius: 8px;
            padding: 15px;
            border: 1px solid rgba(79, 195, 247, 0.2);
        }

        .card-title {
            color: #4fc3f7;
            font-size: 16px;
            margin-bottom: 8px;
        }

        .card-value {
            color: #fff;
            font-size: 28px;
            font-weight: bold;
        }

        .mini-chart {
            height: 100px;
            margin-top: 10px;
            border-radius: 4px;
        }

        /* 平板: 768px - 1200px */
        @media (min-width: 768px) {
            .header {
                height: 70px;
                font-size: 24px;
                letter-spacing: 4px;
            }

            .main-content {
                flex-direction: row;
                padding: 20px;
            }

            .sidebar {
                width: 280px;
                padding: 20px;
            }

            .chart-area {
                grid-template-columns: repeat(2, 1fr);
                flex: 1;
            }

            .card-title {
                font-size: 18px;
            }

            .card-value {
                font-size: 32px;
            }

            .mini-chart {
                height: 100px;
            }
        }

        /* 代码体积问题展示 */
        @media (min-width: 1200px) {
            .header {
                height: 80px;
                font-size: 32px;
                letter-spacing: 6px;
            }

            .main-content {
                padding: 25px;
                gap: 20px;
            }

            .sidebar {
                width: 350px;
            }

            .card-title {
                font-size: 20px;
                margin-bottom: 10px;
            }

            .card-value {
                font-size: 40px;
            }

            .mini-chart {
                height: 120px;
                margin-top: 15px;
            }
        }

        /* 大屏幕: 1920px - 2560px */
        @media (min-width: 1920px) {
            .header {
                height: 100px;
                font-size: 40px;
                letter-spacing: 8px;
            }

            .main-content {
                padding: 30px;
            }

            .sidebar {
                width: 400px;
            }

            .sidebar p {
                font-size: 16px;
            }

            .card-value {
                font-size: 48px;
            }

            .mini-chart {
                height: 150px;
            }
        }

        /* 超大屏幕: > 2560px */
        @media (min-width: 2560px) {
            .header {
                height: 120px;
                font-size: 48px;
            }

            .chart-area {
                grid-template-columns: repeat(4, 1fr);
            }

            .card-value {
                font-size: 56px;
            }
        }

        /* 代码体积问题展示 */
        .code-stats {
            margin-top: 15px;
            padding: 10px;
            background: rgba(244, 67, 54, 0.1);
            border: 1px dashed #f44336;
            border-radius: 4px;
        }

        .code-stats h4 {
            color: #f44336;
            margin-bottom: 5px;
        }

        .code-stats .stat {
            display: flex;
            justify-content: space-between;
            font-size: 12px;
            padding: 3px 0;
        }

        .notice {
            position: fixed;
            bottom: 20px;
            left: 20px;
            right: 360px;
            background: rgba(233, 30, 99, 0.2);
            border: 1px solid #e91e63;
            color: #e91e63;
            padding: 15px 20px;
            border-radius: 8px;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <!-- 信息面板 -->
    <div class="info-panel">
        <h3>方案5: 响应式断点</h3>
        <div class="pros-cons">
            <div class="pros">✓ 优点:</div>
            • 精确控制各分辨率<br>
            • 字体可读性可控<br>
            • 适合固定规格大屏<br><br>
            <div class="cons">✗ 缺点:</div><strong>代码量爆炸</strong><br>
            • 维护困难<br>
            • 无法覆盖所有尺寸<br>
            • CSS 文件体积大
        </div>

        <div class="current-breakpoint" id="currentBP">
            当前断点: 默认/移动端
        </div>

        <div class="breakpoint-list">
            <div data-bp="0">&lt; 768px: 移动端</div>
            <div data-bp="1">768px - 1200px: 平板</div>
            <div data-bp="2">1200px - 1920px: 桌面</div>
            <div data-bp="3">1920px - 2560px: 大屏</div>
            <div data-bp="4">&gt; 2560px: 超大屏</div>
        </div>

        <div class="code-stats">
            <h4>⚠️ 维护成本</h4>
            <div class="stat">
                <span>每个组件</span>
                <span>×5 套样式</span>
            </div>
            <div class="stat">
                <span>10个组件</span>
                <span>= 50套样式</span>
            </div>
            <div class="stat">
                <span>新增分辨率</span>
                <span>全文件修改</span>
            </div>
        </div>
    </div>

    <!-- 页面内容 -->
    <div class="header">响应式断点演示 - @media 查询</div>

    <div class="main-content">
        <div class="sidebar">
            <div style="color: #e91e63; font-size: 20px; margin-bottom: 15px;">左侧信息面板</div>
            <p>
                调整窗口宽度,观察布局在不同断点的变化。<br><br>

                <strong>可能遇到的问题:</strong><br>
                • 1366px 和 1440px 怎么办?<br>
                • 3840px(4K)应该如何显示?<br>
                • 修改一个组件要改 5 处?
            </p>
        </div>

        <div class="chart-area">
            <div class="card">
                <div class="card-title">实时用户数</div>
                <div class="card-value">128,456</div>
                <div class="mini-chart" id="chart1"></div>
            </div>
            <div class="card">
                <div class="card-title">交易金额</div>
                <div class="card-value">¥2.3M</div>
                <div class="mini-chart" id="chart2"></div>
            </div>
            <div class="card">
                <div class="card-title">系统负载</div>
                <div class="card-value">68%</div>
                <div class="mini-chart" id="chart3"></div>
            </div>
            <div class="card">
                <div class="card-title">响应时间</div>
                <div class="card-value">23ms</div>
                <div class="mini-chart" id="chart4"></div>
            </div>
        </div>
    </div>

    <div class="notice">
        🔧 拖动窗口宽度观察布局变化。注意:即使 "桌面" 断点(1920px)也不是精确还原设计稿,只是 "看起来差不多"。
    </div>

    <script>
        // 检测当前断点
        const breakpoints = [
            { name: '默认/移动端', max: 768 },
            { name: '平板', max: 1200 },
            { name: '桌面', max: 1920 },
            { name: '大屏', max: 2560 },
            { name: '超大屏', max: Infinity }
        ];

        function detectBreakpoint() {
            const width = window.innerWidth;
            let activeIndex = 0;

            for (let i = 0; i < breakpoints.length; i++) {
                if (width < breakpoints[i].max) {
                    activeIndex = i;
                    break;
                }
                activeIndex = i;
            }

            document.getElementById('currentBP').textContent =
                `当前断点: ${breakpoints[activeIndex].name} (${width}px)`;

            // 高亮断点列表
            document.querySelectorAll('.breakpoint-list div').forEach((div, index) => {
                div.classList.toggle('active', index === activeIndex);
            });
        }

        detectBreakpoint();

        let timer;
        window.addEventListener('resize', () => {
            clearTimeout(timer);
            timer = setTimeout(detectBreakpoint, 100);
        });

        // ===== ECharts 图表配置 =====
        // 图表1: 柱状图 - 实时用户数
        const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
        const option1 = {
            backgroundColor: 'transparent',
            grid: { top: 25, right: 10, bottom: 20, left: 40 },
            xAxis: {
                type: 'category',
                data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
                axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 9 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
                axisLabel: { color: '#fff', fontSize: 9 }
            },
            series: [{
                type: 'bar',
                data: [32000, 28000, 85000, 120000, 98000, 128456],
                itemStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: '#4fc3f7' },
                        { offset: 1, color: '#2196f3' }
                    ]),
                    borderRadius: [4, 4, 0, 0]
                },
                animationDuration: 1500
            }]
        };
        chart1.setOption(option1);

        // 图表2: 折线图 - 交易金额趋势
        const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
        const option2 = {
            backgroundColor: 'transparent',
            grid: { top: 25, right: 10, bottom: 20, left: 45 },
            xAxis: {
                type: 'category',
                data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
                axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 9 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
                axisLabel: { color: '#fff', fontSize: 9, formatter: '¥{value}' }
            },
            series: [{
                type: 'line',
                data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
                smooth: true,
                symbol: 'circle',
                symbolSize: 5,
                lineStyle: { color: '#e91e63', width: 2 },
                itemStyle: { color: '#e91e63' },
                areaStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
                        { offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
                    ])
                },
                animationDuration: 1500
            }]
        };
        chart2.setOption(option2);

        // 图表3: 饼图 - 系统负载分布
        const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
        const option3 = {
            backgroundColor: 'transparent',
            series: [{
                type: 'pie',
                radius: ['30%', '60%'],
                center: ['50%', '55%'],
                data: [
                    { value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
                    { value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
                    { value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
                    { value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
                ],
                label: { color: '#fff', fontSize: 9 },
                labelLine: { lineStyle: { color: 'rgba(255,255,255,0.5)' } },
                animationDuration: 1500
            }]
        };
        chart3.setOption(option3);

        // 图表4: 仪表盘 - 响应时间
        const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
        const option4 = {
            backgroundColor: 'transparent',
            series: [{
                type: 'gauge',
                radius: '70%',
                center: ['50%', '55%'],
                min: 0,
                max: 100,
                splitNumber: 10,
                axisLine: {
                    lineStyle: {
                        width: 6,
                        color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
                    }
                },
                pointer: { itemStyle: { color: '#4fc3f7' }, width: 3 },
                axisTick: { distance: -6, length: 3, lineStyle: { color: '#fff' } },
                splitLine: { distance: -6, length: 8, lineStyle: { color: '#fff' } },
                axisLabel: { color: '#fff', distance: -15, fontSize: 8 },
                detail: {
                    valueAnimation: true,
                    formatter: '{value}ms',
                    color: '#4fc3f7',
                    fontSize: 12,
                    offsetCenter: [0, '60%']
                },
                data: [{ value: 23 }],
                animationDuration: 2000
            }]
        };
        chart4.setOption(option4);

        // 响应式调整
        const charts = [chart1, chart2, chart3, chart4];
        window.addEventListener('resize', () => {
            charts.forEach(chart => chart.resize());
        });

        // 模拟数据更新
        setInterval(() => {
            const newValue = Math.floor(Math.random() * 20) + 15;
            chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
        }, 3000);
    </script>
</body>
</html>

Demo 6: 混合方案(推荐)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>方案6: 混合方案(推荐) - 大屏适配方案对比</title>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
    <script>
        // 混合方案:Scale + Rem
        (function() {
            const designWidth = 1920;
            const designHeight = 1080;
            const minScale = 0.6; // 最小缩放限制,防止过小

            function adapt() {
                const winW = window.innerWidth;
                const winH = window.innerHeight;

                // Scale 计算
                const scaleX = winW / designWidth;
                const scaleY = winH / designHeight;
                const scale = Math.max(Math.min(scaleX, scaleY), minScale);

                // 应用 scale
                const screen = document.getElementById('screen');
                if (screen) {
                    screen.style.transform = `scale(${scale})`;
                }

                // Rem 计算 - 根据缩放比例调整根字体
                // 当 scale < 1 时,增加根字体补偿
                const baseRem = 100;
                const fontScale = Math.max(scale, minScale);
                const rem = baseRem * fontScale;

                document.documentElement.style.fontSize = rem + 'px';

                // 更新信息面板
                updateInfo(scale, rem, winW, winH);
            }

            function updateInfo(scale, rem, winW, winH) {
                const info = document.getElementById('mixedInfo');
                if (info) {
                    info.innerHTML = `
                        Scale: ${scale.toFixed(3)}<br>
                        Rem: ${rem.toFixed(1)}px<br>
                        窗口: ${winW}×${winH}<br>
                        最小限制: ${minScale}
                    `;
                }
            }

            // 页面加载前执行
            adapt();

            // 防抖 resize
            let timer;
            window.addEventListener('resize', () => {
                clearTimeout(timer);
                timer = setTimeout(adapt, 100);
            });

            // 暴露全局供切换模式使用
            window.adaptMode = 'mixed'; // mixed, scale-only, rem-only

            window.setAdaptMode = function(mode) {
                window.adaptMode = mode;

                const screen = document.getElementById('screen');
                const designWidth = 1920;
                const designHeight = 1080;
                const winW = window.innerWidth;
                const winH = window.innerHeight;

                if (mode === 'scale-only') {
                    const scale = Math.min(winW / designWidth, winH / designHeight);
                    screen.style.transform = `scale(${scale})`;
                    document.documentElement.style.fontSize = '100px';
                } else if (mode === 'rem-only') {
                    screen.style.transform = 'none';
                    const rem = winW / designWidth * 100;
                    document.documentElement.style.fontSize = rem + 'px';
                } else {
                    adapt();
                }
            };

            // 初始化覆盖
            window.setAdaptMode('mixed');
        })();
    </script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            overflow: hidden;
            background: #0a0a1a;
        }

        /* 信息面板 - 固定不缩放 */
        .info-panel {
            position: fixed;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.9);
            color: #fff;
            padding: 20px;
            border-radius: 8px;
            z-index: 9999;
            width: 340px;
            font-size: 14px;
            border: 1px solid #4caf50;
        }

        .info-panel h3 {
            color: #4caf50;
            margin-bottom: 12px;
            font-size: 16px;
        }

        .info-panel .recommend {
            background: #4caf50;
            color: #000;
            padding: 5px 10px;
            border-radius: 4px;
            font-size: 12px;
            font-weight: bold;
            display: inline-block;
            margin-bottom: 10px;
        }

        .info-panel .pros-cons {
            margin-bottom: 15px;
        }

        .info-panel .pros {
            color: #81c784;
        }

        .info-panel .cons {
            color: #fff176;
        }

        .mode-switcher {
            margin-top: 15px;
            padding-top: 15px;
            border-top: 1px solid #333;
        }

        .mode-switcher h4 {
            color: #4fc3f7;
            margin-bottom: 10px;
        }

        .mode-btn {
            display: block;
            width: 100%;
            padding: 8px 12px;
            margin-bottom: 6px;
            background: #333;
            color: #fff;
            border: 1px solid #555;
            border-radius: 4px;
            cursor: pointer;
            text-align: left;
            font-size: 12px;
        }

        .mode-btn:hover {
            background: #444;
        }

        .mode-btn.active {
            background: #4caf50;
            border-color: #4caf50;
            color: #000;
        }

        .info-value {
            background: #2196f3;
            color: #fff;
            padding: 10px;
            border-radius: 4px;
            margin-top: 10px;
            font-family: 'Courier New', monospace;
            font-size: 12px;
        }

        /* 大屏容器 */
        .screen-container {
            width: 1920px;
            height: 1080px;
            transform-origin: 0 0;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            position: absolute;
            overflow: hidden;
            top: 0;
            left: 0;
        }

        /* 居中显示 */
        .centered {
            transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease;
        }

        /* 大屏内容样式 - 使用 rem */
        .header {
            height: 1rem;
            background: linear-gradient(90deg, #0f3460 0%, #533483 50%, #0f3460 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 0.5rem;
            color: #fff;
            text-shadow: 0 0 0.1rem rgba(79, 195, 247, 0.5);
            letter-spacing: 0.08rem;
        }

        .main-content {
            display: flex;
            padding: 0.3rem;
            gap: 0.2rem;
            height: calc(100% - 1rem);
        }

        .sidebar {
            width: 4rem;
            background: rgba(15, 52, 96, 0.3);
            border-radius: 0.16rem;
            padding: 0.2rem;
            border: 0.01rem solid rgba(79, 195, 247, 0.2);
        }

        .sidebar p {
            color: rgba(255,255,255,0.7);
            font-size: 0.18rem;
            line-height: 1.8;
        }

        .chart-area {
            flex: 1;
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 0.2rem;
        }

        .card {
            background: rgba(15, 52, 96, 0.3);
            border-radius: 0.16rem;
            padding: 0.2rem;
            border: 0.01rem solid rgba(79, 195, 247, 0.2);
            display: flex;
            flex-direction: column;
        }

        .card-title {
            color: #4fc3f7;
            font-size: 0.22rem;
            margin-bottom: 0.08rem;
        }

        .card-value {
            color: #fff;
            font-size: 0.56rem;
            font-weight: bold;
        }

        .mini-chart {
            flex: 1;
            min-height: 1.5rem;
            margin-top: 0.15rem;
            border-radius: 0.08rem;
        }

        /* 特性说明卡片 */
        .feature-card {
            margin-top: 0.15rem;
            padding: 0.15rem;
            background: rgba(76, 175, 80, 0.1);
            border: 1px solid #4caf50;
            border-radius: 0.1rem;
        }

        .feature-card h4 {
            color: #4caf50;
            font-size: 0.18rem;
            margin-bottom: 0.05rem;
        }

        .feature-card p {
            font-size: 0.14rem;
            color: rgba(255,255,255,0.8);
        }

        /* 对比指示器 */
        .compare-indicator {
            position: fixed;
            bottom: 20px;
            left: 20px;
            background: rgba(0, 0, 0, 0.9);
            padding: 15px 20px;
            border-radius: 8px;
            color: #fff;
            border-left: 4px solid #4caf50;
        }

        .compare-indicator h4 {
            color: #4caf50;
            margin-bottom: 5px;
        }
    </style>
</head>
<body>
    <!-- 信息面板 -->
    <div class="info-panel">
        <div class="recommend">⭐ 生产环境推荐</div>
        <h3>方案6: 混合方案</h3>
        <div class="pros-cons">
            <div class="pros">✓ 结合 Scale + Rem 优点</div>
            • 等比例缩放保证布局<br>
            • 字体最小值防止过小<br>
            • 大屏清晰、小屏可读<br><br>
            <div class="cons">△ 注意:</div>
            • 需要 JS 支持<br>
            • 计算逻辑稍复杂
        </div>

        <div class="mode-switcher">
            <h4>模式切换对比:</h4>
            <button class="mode-btn active" onclick="switchMode('mixed', this)">
                🌟 混合方案 (推荐)
            </button>
            <button class="mode-btn" onclick="switchMode('scale-only', this)">
                📐 纯 Scale (字体过小)
            </button>
            <button class="mode-btn" onclick="switchMode('rem-only', this)">
                🔤 纯 Rem (可能变形)
            </button>
        </div>

        <div class="info-value" id="mixedInfo">
            Scale: 1.0<br>
            Rem: 100px<br>
            窗口: 1920×1080
        </div>
    </div>

    <!-- 对比指示器 -->
    <div class="compare-indicator">
        <h4>💡 对比技巧</h4>
        <p>1. 切换到"纯 Scale",缩小窗口,观察字体变小</p>
        <p>2. 切换回"混合方案",字体有最小值限制</p>
        <p>3. 调整到4K屏,观察布局比例保持与设计稿一致</p>
    </div>

    <!-- 大屏容器 -->
    <div class="screen-container centered" id="screen">
        <div class="header">混合方案演示 - Scale + Rem 双重保障</div>

        <div class="main-content">
            <div class="sidebar">
                <div style="color: #4caf50; font-size: 0.24rem; margin-bottom: 0.15rem;">混合方案说明</div>
                <p>
                    此方案结合 Scale 的视觉一致性 和 Rem 的灵活性。<br><br>

                    <strong>核心算法:</strong><br>
                    1. 计算 screen 的 scale 比例<br>
                    2. 根字体 = baseFont * max(scale, minLimit)<br>
                    3. 所有尺寸使用 rem 单位<br><br>

                    这样既保持设计稿比例,又确保文字可读。
                </p>

                <div class="feature-card">
                    <h4>🎯 最小字体保护</h4>
                    <p>当屏幕缩小时,字体不会无限缩小,保证基本可读性。</p>
                </div>

                <div class="feature-card">
                    <h4>📐 严格比例保持</h4>
                    <p>图表、卡片的宽高比例严格遵循设计稿,无变形。</p>
                </div>
            </div>

            <div class="chart-area">
                <div class="card">
                    <div class="card-title">实时用户数</div>
                    <div class="card-value">128,456</div>
                    <div class="mini-chart" id="chart1"></div>
                </div>
                <div class="card">
                    <div class="card-title">交易金额</div>
                    <div class="card-value">¥2.3M</div>
                    <div class="mini-chart" id="chart2"></div>
                </div>
                <div class="card">
                    <div class="card-title">系统负载</div>
                    <div class="card-value">68%</div>
                    <div class="mini-chart" id="chart3"></div>
                </div>
                <div class="card">
                    <div class="card-title">响应时间</div>
                    <div class="card-value">23ms</div>
                    <div class="mini-chart" id="chart4"></div>
                </div>
            </div>
        </div>
    </div>

    <script>
        function switchMode(mode, btn) {
            window.setAdaptMode(mode);

            // 更新按钮状态
            document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');

            // 更新位置信息
            updatePosition();
        }

        function updatePosition() {
            const screen = document.getElementById('screen');
            const winW = window.innerWidth;
            const winH = window.innerHeight;
            const designW = 1920;
            const designH = 1080;

            // 获取当前 scale
            const transform = getComputedStyle(screen).transform;
            let scale = 1;
            if (transform && transform !== 'none') {
                const matrix = transform.match(/matrix\(([^)]+)\)/);
                if (matrix) {
                    const values = matrix[1].split(',').map(parseFloat);
                    scale = values[0];
                }
            }

            // 居中计算
            const left = (winW - designW * scale) / 2;
            const top = (winH - designH * scale) / 2;

            screen.style.left = Math.max(0, left) + 'px';
            screen.style.top = Math.max(0, top) + 'px';
        }

        // 初始化位置
        window.addEventListener('load', updatePosition);
        window.addEventListener('resize', updatePosition);

        // ===== ECharts 图表配置 =====
        // 图表1: 柱状图 - 实时用户数
        const chart1 = echarts.init(document.getElementById('chart1'), 'dark');
        const option1 = {
            backgroundColor: 'transparent',
            grid: { top: 30, right: 15, bottom: 25, left: 45 },
            xAxis: {
                type: 'category',
                data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
                axisLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(79, 195, 247, 0.1)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            series: [{
                type: 'bar',
                data: [32000, 28000, 85000, 120000, 98000, 128456],
                itemStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: '#4fc3f7' },
                        { offset: 1, color: '#2196f3' }
                    ]),
                    borderRadius: [4, 4, 0, 0]
                },
                animationDuration: 1500
            }]
        };
        chart1.setOption(option1);

        // 图表2: 折线图 - 交易金额趋势
        const chart2 = echarts.init(document.getElementById('chart2'), 'dark');
        const option2 = {
            backgroundColor: 'transparent',
            grid: { top: 30, right: 15, bottom: 25, left: 50 },
            xAxis: {
                type: 'category',
                data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
                axisLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.5)' } },
                axisLabel: { color: '#fff', fontSize: 10 }
            },
            yAxis: {
                type: 'value',
                splitLine: { lineStyle: { color: 'rgba(83, 52, 131, 0.1)' } },
                axisLabel: { color: '#fff', fontSize: 10, formatter: '¥{value}' }
            },
            series: [{
                type: 'line',
                data: [1.2, 1.5, 1.8, 2.1, 1.9, 2.5, 2.3],
                smooth: true,
                symbol: 'circle',
                symbolSize: 6,
                lineStyle: { color: '#e91e63', width: 3 },
                itemStyle: { color: '#e91e63' },
                areaStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: 'rgba(233, 30, 99, 0.4)' },
                        { offset: 1, color: 'rgba(233, 30, 99, 0.05)' }
                    ])
                },
                animationDuration: 1500
            }]
        };
        chart2.setOption(option2);

        // 图表3: 饼图 - 系统负载分布
        const chart3 = echarts.init(document.getElementById('chart3'), 'dark');
        const option3 = {
            backgroundColor: 'transparent',
            series: [{
                type: 'pie',
                radius: ['35%', '65%'],
                center: ['50%', '55%'],
                data: [
                    { value: 35, name: 'CPU', itemStyle: { color: '#4caf50' } },
                    { value: 28, name: '内存', itemStyle: { color: '#2196f3' } },
                    { value: 20, name: '磁盘', itemStyle: { color: '#ff9800' } },
                    { value: 17, name: '网络', itemStyle: { color: '#9c27b0' } }
                ],
                label: { color: '#fff', fontSize: 10 },
                labelLine: { lineStyle: { color: 'rgba(255,255,255,0.5)' } },
                animationDuration: 1500
            }]
        };
        chart3.setOption(option3);

        // 图表4: 仪表盘 - 响应时间
        const chart4 = echarts.init(document.getElementById('chart4'), 'dark');
        const option4 = {
            backgroundColor: 'transparent',
            series: [{
                type: 'gauge',
                radius: '75%',
                center: ['50%', '55%'],
                min: 0,
                max: 100,
                splitNumber: 10,
                axisLine: {
                    lineStyle: {
                        width: 8,
                        color: [[0.3, '#4caf50'], [0.7, '#2196f3'], [1, '#f44336']]
                    }
                },
                pointer: { itemStyle: { color: '#4fc3f7' }, width: 4 },
                axisTick: { distance: -8, length: 4, lineStyle: { color: '#fff' } },
                splitLine: { distance: -8, length: 10, lineStyle: { color: '#fff' } },
                axisLabel: { color: '#fff', distance: -20, fontSize: 9 },
                detail: {
                    valueAnimation: true,
                    formatter: '{value}ms',
                    color: '#4fc3f7',
                    fontSize: 16,
                    offsetCenter: [0, '65%']
                },
                data: [{ value: 23 }],
                animationDuration: 2000
            }]
        };
        chart4.setOption(option4);

        // 响应式调整
        const charts = [chart1, chart2, chart3, chart4];
        window.addEventListener('resize', () => {
            charts.forEach(chart => chart.resize());
        });

        // 模拟数据更新
        setInterval(() => {
            const newValue = Math.floor(Math.random() * 20) + 15;
            chart4.setOption({ series: [{ data: [{ value: newValue }] }] });
        }, 3000);
    </script>
</body>
</html>
/* 使用 rem 单位编写样式 */
.header {
    height: 1rem;           /* 设计稿 100px */
    font-size: 0.5rem;      /* 设计稿 50px */
    letter-spacing: 0.08rem;
}

.sidebar {
    width: 4rem;            /* 设计稿 400px */
    padding: 0.2rem;        /* 设计稿 20px */
}

.card-title {
    font-size: 0.22rem;     /* 设计稿 22px */
}

.card-value {
    font-size: 0.56rem;     /* 设计稿 56px */
}

完整 Demo 文件列表:

  • demo1 - Scale 等比例缩放(上方已提供完整代码)
  • demo2 - VW/VH 视口单位方案
  • demo3 - Rem 动态计算方案
  • demo4 - 流式/弹性布局方案
  • demo5 - 响应式断点方案
  • demo6 - 混合方案(推荐)

以上所有demo均可在本地环境中直接运行,建议按顺序体验对比各方案的表现差异

核心代码示例

Scale 方案核心代码

<!DOCTYPE html>
<html>
<head>
  <style>
    .screen-container {
      width: 1920px;
      height: 1080px;
      transform-origin: 0 0;
      background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
    }
  </style>
</head>
<body>
  <div class="screen-container" id="screen">
    <!-- 大屏内容 -->
  </div>

  <script>
    const DESIGN_WIDTH = 1920;
    const DESIGN_HEIGHT = 1080;

    function setScale() {
      const scaleX = window.innerWidth / DESIGN_WIDTH;
      const scaleY = window.innerHeight / DESIGN_HEIGHT;
      const scale = Math.min(scaleX, scaleY);

      const screen = document.getElementById('screen');
      screen.style.transform = `scale(${scale})`;

      // 居中显示
      const left = (window.innerWidth - DESIGN_WIDTH * scale) / 2;
      const top = (window.innerHeight - DESIGN_HEIGHT * scale) / 2;
      screen.style.left = left + 'px';
      screen.style.top = top + 'px';
    }

    setScale();
    window.addEventListener('resize', setScale);
  </script>
</body>
</html>

混合方案核心代码

// 混合方案:Scale + Rem
const DESIGN_WIDTH = 1920;
const DESIGN_HEIGHT = 1080;
const MIN_SCALE = 0.6; // 最小缩放限制,防止过小

function adapt() {
  const winW = window.innerWidth;
  const winH = window.innerHeight;

  // Scale 计算
  const scaleX = winW / DESIGN_WIDTH;
  const scaleY = winH / DESIGN_HEIGHT;
  const scale = Math.max(Math.min(scaleX, scaleY), MIN_SCALE);

  // 应用 scale
  const screen = document.getElementById('screen');
  screen.style.transform = `scale(${scale})`;

  // Rem 计算 - 根据缩放比例调整根字体
  // 当 scale < 1 时,增加根字体补偿
  const baseRem = 100;
  const fontScale = Math.max(scale, MIN_SCALE);
  const rem = baseRem * fontScale;

  document.documentElement.style.fontSize = rem + 'px';
}

adapt();
window.addEventListener('resize', adapt);

总结

大屏适配没有银弹,每种方案都有其适用场景:

  1. 简单大屏项目:使用 Scale 方案,快速实现
  2. 内容管理系统:使用流式布局,灵活适配
  3. 移动端优先:使用 Rem 方案,成熟稳定
  4. 多端统一:使用混合方案,兼顾体验

选择方案时需要综合考虑:

  • 设计稿还原要求
  • 目标设备规格
  • 开发维护成本
  • 团队技术栈

希望本文能帮助你在大屏开发中游刃有余!


如果觉得有帮助,欢迎点赞收藏,有问题欢迎在评论区讨论!

❌
❌