80 KiB
80 KiB
BigScreen 大屏模板优化实施指南
概述
本文档基于对 ljwx-bigscreen 系统中 main.html 和 personal.html 两个核心大屏模板的深入分析,结合现有 API 接口分析文档和重构建议,提供完整的优化实施方案。在不改变当前 UI 的前提下,全面提升模板的响应速度和可维护性。
当前状况分析
模板文件规模
- main.html: 10,003 行,约 368KB
- personal.html: 9,792 行,主要个人健康大屏
识别的主要问题
1. 代码结构问题
- 巨型单文件: 两个文件都超过 9000 行,难以维护
- 重复代码: 大量相似的样式定义和 JavaScript 函数
- 内联样式: 大量 CSS 直接写在 HTML 文件中
- 混杂职责: UI、业务逻辑、样式混合在一起
2. 性能瓶颈
- 阻塞式加载: 所有资源顺序加载,影响首屏渲染
- 无缓存策略: 重复请求相同的 API 接口
- DOM 操作频繁: 大量直接 DOM 操作影响性能
- 资源冗余: 未使用的 CSS 和 JavaScript 代码
3. 维护困难
- 查找困难: 在万行文件中定位问题极其困难
- 调试复杂: 错误定位和修复成本高
- 扩展限制: 添加新功能需要修改核心文件
- 团队协作: 多人同时修改容易产生冲突
优化实施方案
阶段一:文件结构重构(第 1-2 周)
1.1 模块化拆分策略
主要目录结构设计
ljwx-bigscreen/bigscreen/bigScreen/
├── templates/
│ ├── layouts/ # 布局模板
│ │ ├── base.html # 基础布局
│ │ ├── dashboard_layout.html # 大屏布局
│ │ └── personal_layout.html # 个人页面布局
│ ├── components/ # 组件模板
│ │ ├── charts/ # 图表组件
│ │ │ ├── health_score_chart.html
│ │ │ ├── trend_chart.html
│ │ │ └── radar_chart.html
│ │ ├── panels/ # 面板组件
│ │ │ ├── device_info_panel.html
│ │ │ ├── message_panel.html
│ │ │ └── alert_panel.html
│ │ └── widgets/ # 小组件
│ │ ├── loading_spinner.html
│ │ ├── status_indicator.html
│ │ └── metric_card.html
│ ├── pages/ # 页面模板
│ │ ├── main_optimized.html # 优化后的主页面
│ │ └── personal_optimized.html # 优化后的个人页面
│ └── partials/ # 页面片段
│ ├── header.html
│ ├── sidebar.html
│ └── footer.html
├── static/
│ ├── css/ # 样式文件
│ │ ├── base/ # 基础样式
│ │ │ ├── reset.css # 重置样式
│ │ │ ├── variables.css # CSS 变量
│ │ │ └── animations.css # 动画样式
│ │ ├── components/ # 组件样式
│ │ │ ├── charts.css
│ │ │ ├── panels.css
│ │ │ └── widgets.css
│ │ ├── pages/ # 页面样式
│ │ │ ├── main.css
│ │ │ └── personal.css
│ │ └── themes/ # 主题样式
│ │ ├── default.css
│ │ ├── dark.css
│ │ └── high_contrast.css
│ └── js/ # JavaScript 文件
│ ├── core/ # 核心库
│ │ ├── api-client.js # API 客户端
│ │ ├── cache-manager.js # 缓存管理
│ │ └── event-bus.js # 事件总线
│ ├── components/ # 组件脚本
│ │ ├── health-chart.js
│ │ ├── device-panel.js
│ │ └── message-panel.js
│ ├── pages/ # 页面脚本
│ │ ├── main-page.js
│ │ └── personal-page.js
│ └── utils/ # 工具函数
│ ├── formatters.js
│ ├── validators.js
│ └── helpers.js
1.2 基础布局模板设计
base.html - 基础模板
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}智能健康数据分析平台{% endblock %}</title>
<!-- 基础样式 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/base/reset.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/base/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/base/animations.css') }}">
<!-- 页面特定样式 -->
{% block stylesheets %}{% endblock %}
<!-- 预加载关键资源 -->
<link rel="preload" href="{{ url_for('static', filename='js/core/api-client.js') }}" as="script">
<link rel="preload" href="{{ url_for('static', filename='fonts/num.otf') }}" as="font" type="font/otf" crossorigin>
</head>
<body class="{% block body_class %}{% endblock %}">
<!-- 全局加载指示器 -->
<div id="global-loading" class="loading-overlay">
<div class="loading-spinner"></div>
<div class="loading-text">数据加载中...</div>
</div>
<!-- 主要内容区域 -->
<main id="main-content" class="main-content">
{% block content %}{% endblock %}
</main>
<!-- 全局错误提示 -->
<div id="error-toast" class="error-toast hidden">
<div class="error-content">
<span class="error-icon">⚠️</span>
<span class="error-message"></span>
<button class="error-close">×</button>
</div>
</div>
<!-- 核心脚本 -->
<script src="{{ url_for('static', filename='js/core/api-client.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/cache-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/event-bus.js') }}"></script>
<!-- 页面特定脚本 -->
{% block scripts %}{% endblock %}
<!-- 全局初始化脚本 -->
<script>
// 全局错误处理
window.addEventListener('error', function(e) {
console.error('Global error:', e.error);
showErrorToast('系统错误:' + e.message);
});
// 全局配置
window.APP_CONFIG = {
customerId: {{ customer_id|default('null') }},
apiBaseUrl: '/api/v1',
refreshInterval: {{ refresh_interval|default(5000) }},
debug: {{ 'true' if debug else 'false' }}
};
</script>
</body>
</html>
1.3 组件化架构实现
图表组件示例 (health-score-chart.js)
/**
* 健康评分图表组件
*/
class HealthScoreChart {
constructor(container, options = {}) {
this.container = container;
this.options = {
width: 400,
height: 300,
theme: 'default',
autoRefresh: true,
refreshInterval: 5000,
...options
};
this.chart = null;
this.data = null;
this.refreshTimer = null;
this.init();
}
init() {
this.createChart();
this.bindEvents();
if (this.options.autoRefresh) {
this.startAutoRefresh();
}
}
createChart() {
// 使用 ECharts 创建图表
this.chart = echarts.init(this.container);
const defaultOption = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}%'
},
series: [{
type: 'gauge',
min: 0,
max: 100,
radius: '80%',
axisLine: {
lineStyle: {
width: 8,
color: [
[0.2, '#ff4757'],
[0.4, '#ffa726'],
[0.6, '#66bb6a'],
[0.8, '#42a5f5'],
[1, '#00ff9d']
]
}
},
pointer: {
itemStyle: {
color: '#00e4ff'
}
},
axisTick: {
distance: -8,
length: 4,
lineStyle: {
color: '#fff',
width: 1
}
},
splitLine: {
distance: -15,
length: 8,
lineStyle: {
color: '#fff',
width: 2
}
},
axisLabel: {
color: '#fff',
distance: 20,
fontSize: 10
},
detail: {
valueAnimation: true,
formatter: '{value}%',
color: '#00e4ff',
fontSize: 20,
fontWeight: 'bold',
offsetCenter: [0, '70%']
},
data: [{
value: 0,
name: '健康评分'
}]
}]
};
this.chart.setOption(defaultOption);
this.handleResize();
}
updateData(newData) {
if (!this.chart || !newData) return;
this.data = newData;
const option = {
series: [{
data: [{
value: newData.score,
name: newData.title || '健康评分'
}]
}]
};
this.chart.setOption(option, false, true);
this.emitEvent('data-updated', newData);
}
async loadData(params = {}) {
try {
this.showLoading();
const cacheKey = `health-score-${JSON.stringify(params)}`;
let data = CacheManager.get(cacheKey);
if (!data) {
data = await ApiClient.get('/health/score/comprehensive', params);
CacheManager.set(cacheKey, data, 60000); // 缓存1分钟
}
this.updateData(data);
this.hideLoading();
} catch (error) {
this.hideLoading();
this.showError('健康评分数据加载失败');
console.error('Health score load error:', error);
}
}
startAutoRefresh() {
this.stopAutoRefresh();
this.refreshTimer = setInterval(() => {
if (this.isVisible()) {
this.refresh();
}
}, this.options.refreshInterval);
}
stopAutoRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
}
refresh() {
return this.loadData(this.lastParams);
}
showLoading() {
this.container.classList.add('loading');
}
hideLoading() {
this.container.classList.remove('loading');
}
showError(message) {
console.error('Chart error:', message);
this.emitEvent('error', { message });
}
isVisible() {
const rect = this.container.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
handleResize() {
if (this.chart) {
this.chart.resize();
}
}
bindEvents() {
// 窗口大小调整
window.addEventListener('resize', this.handleResize.bind(this));
// 页面可见性变化
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.stopAutoRefresh();
} else if (this.options.autoRefresh) {
this.startAutoRefresh();
}
});
}
emitEvent(eventName, data) {
EventBus.emit(`chart:${eventName}`, {
component: this,
data
});
}
destroy() {
this.stopAutoRefresh();
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
window.removeEventListener('resize', this.handleResize.bind(this));
}
}
阶段二:性能优化实施(第 3-4 周)
2.1 API 客户端优化
统一 API 客户端 (api-client.js)
/**
* 统一 API 客户端 - 支持批处理、缓存、重试
*/
class ApiClient {
constructor() {
this.baseURL = window.APP_CONFIG?.apiBaseUrl || '/api/v1';
this.timeout = 10000;
this.retryConfig = {
maxRetries: 3,
retryDelay: [1000, 2000, 4000],
retryConditions: [
error => error.status >= 500,
error => error.code === 'NETWORK_ERROR',
error => error.code === 'TIMEOUT_ERROR'
]
};
this.batchQueue = new Map();
this.batchTimeout = 100;
this.pendingRequests = new Map();
}
/**
* 通用请求方法
*/
async request(endpoint, options = {}) {
const requestKey = this.generateRequestKey(endpoint, options);
// 防止重复请求
if (this.pendingRequests.has(requestKey)) {
return this.pendingRequests.get(requestKey);
}
const requestPromise = this._executeRequest(endpoint, options);
this.pendingRequests.set(requestKey, requestPromise);
try {
const result = await requestPromise;
return result;
} finally {
this.pendingRequests.delete(requestKey);
}
}
async _executeRequest(endpoint, options = {}) {
const fullURL = this.buildURL(endpoint, options.params);
let lastError;
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(fullURL, {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
'X-Request-ID': this.generateRequestId(),
'X-Customer-ID': window.APP_CONFIG?.customerId || '',
...options.headers
},
body: options.body ? JSON.stringify(options.body) : undefined,
signal: controller.signal,
...options.fetchOptions
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new ApiError(response.status, await response.json());
}
const data = await response.json();
this.logRequest(endpoint, options, data, attempt);
return data;
} catch (error) {
lastError = this.normalizeError(error);
if (attempt < this.retryConfig.maxRetries && this.shouldRetry(lastError)) {
await this.delay(this.retryConfig.retryDelay[attempt]);
console.warn(`API请求重试 ${attempt + 1}/${this.retryConfig.maxRetries}:`, endpoint);
continue;
}
break;
}
}
this.logError(endpoint, options, lastError);
throw lastError;
}
/**
* GET 请求
*/
async get(endpoint, params = {}) {
return this.request(endpoint, { params });
}
/**
* POST 请求
*/
async post(endpoint, data = {}) {
return this.request(endpoint, {
method: 'POST',
body: data
});
}
/**
* PUT 请求
*/
async put(endpoint, data = {}) {
return this.request(endpoint, {
method: 'PUT',
body: data
});
}
/**
* DELETE 请求
*/
async delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
/**
* 批量请求
*/
async batch(requests) {
const promises = requests.map(req => {
return this.request(req.endpoint, req.options);
});
const results = await Promise.allSettled(promises);
return results.map((result, index) => ({
request: requests[index],
success: result.status === 'fulfilled',
data: result.status === 'fulfilled' ? result.value : null,
error: result.status === 'rejected' ? result.reason : null
}));
}
/**
* 并行请求
*/
async parallel(requests) {
const batchResults = await this.batch(requests);
const results = {};
batchResults.forEach((result, index) => {
const key = requests[index].key || `request_${index}`;
results[key] = result;
});
return results;
}
/**
* 缓存请求
*/
async cached(endpoint, params = {}, cacheTime = 60000) {
const cacheKey = this.generateCacheKey(endpoint, params);
let data = CacheManager.get(cacheKey);
if (!data) {
data = await this.get(endpoint, params);
CacheManager.set(cacheKey, data, cacheTime);
}
return data;
}
// 辅助方法
buildURL(endpoint, params = {}) {
let url = endpoint.startsWith('http') ? endpoint : `${this.baseURL}${endpoint}`;
if (Object.keys(params).length > 0) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
searchParams.append(key, value);
}
});
url += '?' + searchParams.toString();
}
return url;
}
generateRequestId() {
return 'req_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
generateRequestKey(endpoint, options) {
return `${endpoint}_${JSON.stringify(options)}`;
}
generateCacheKey(endpoint, params) {
return `api_${endpoint}_${JSON.stringify(params)}`;
}
shouldRetry(error) {
return this.retryConfig.retryConditions.some(condition => condition(error));
}
normalizeError(error) {
if (error.name === 'AbortError') {
return new ApiError(408, { message: '请求超时' });
}
if (error instanceof ApiError) {
return error;
}
return new ApiError(500, { message: error.message || '网络错误' });
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
logRequest(endpoint, options, data, attempts) {
if (window.APP_CONFIG?.debug) {
console.log(`✓ API Success [${attempts ? attempts + 1 : 1}次]:`, {
endpoint,
options,
dataSize: JSON.stringify(data).length,
timestamp: new Date().toISOString()
});
}
}
logError(endpoint, options, error) {
console.error('✗ API Error:', {
endpoint,
options,
error: error.message,
status: error.status,
timestamp: new Date().toISOString()
});
}
}
/**
* API 错误类
*/
class ApiError extends Error {
constructor(status, data) {
super(data.message || 'API请求失败');
this.name = 'ApiError';
this.status = status;
this.code = data.code || 'API_ERROR';
this.details = data.errors || [];
}
}
// 全局实例
window.ApiClient = new ApiClient();
2.2 缓存管理系统
智能缓存管理 (cache-manager.js)
/**
* 多层缓存管理系统
*/
class CacheManager {
constructor() {
this.memoryCache = new Map();
this.storageCache = this.getStorageEngine();
this.maxMemorySize = 50; // 最大内存缓存条目数
this.defaultTTL = 60000; // 默认TTL: 1分钟
this.cacheStats = {
hits: 0,
misses: 0,
sets: 0,
deletes: 0
};
this.init();
}
init() {
// 清理过期缓存
this.startCleanupTimer();
// 监听存储变化
this.bindStorageEvents();
// 页面关闭时清理
window.addEventListener('beforeunload', () => {
this.cleanup();
});
}
/**
* 获取缓存
*/
get(key) {
// 先检查内存缓存
const memoryItem = this.memoryCache.get(key);
if (memoryItem && !this.isExpired(memoryItem)) {
this.cacheStats.hits++;
return memoryItem.data;
}
// 再检查存储缓存
try {
const storageData = this.storageCache.getItem(this.getStorageKey(key));
if (storageData) {
const item = JSON.parse(storageData);
if (!this.isExpired(item)) {
// 提升到内存缓存
this.setMemoryCache(key, item.data, item.expires);
this.cacheStats.hits++;
return item.data;
}
}
} catch (error) {
console.warn('Storage cache error:', error);
}
this.cacheStats.misses++;
return null;
}
/**
* 设置缓存
*/
set(key, data, ttl = this.defaultTTL) {
const expires = Date.now() + ttl;
const item = { data, expires, timestamp: Date.now() };
// 设置内存缓存
this.setMemoryCache(key, data, expires);
// 设置存储缓存
try {
this.storageCache.setItem(
this.getStorageKey(key),
JSON.stringify(item)
);
} catch (error) {
console.warn('Storage cache set error:', error);
// 存储失败时清理一些旧缓存
this.cleanupStorage();
}
this.cacheStats.sets++;
}
/**
* 删除缓存
*/
delete(key) {
this.memoryCache.delete(key);
try {
this.storageCache.removeItem(this.getStorageKey(key));
} catch (error) {
console.warn('Storage cache delete error:', error);
}
this.cacheStats.deletes++;
}
/**
* 清空所有缓存
*/
clear() {
this.memoryCache.clear();
try {
const keys = Object.keys(this.storageCache);
keys.forEach(key => {
if (key.startsWith('cache_')) {
this.storageCache.removeItem(key);
}
});
} catch (error) {
console.warn('Storage cache clear error:', error);
}
}
/**
* 批量设置
*/
setMultiple(items, ttl = this.defaultTTL) {
Object.entries(items).forEach(([key, data]) => {
this.set(key, data, ttl);
});
}
/**
* 批量获取
*/
getMultiple(keys) {
const results = {};
keys.forEach(key => {
results[key] = this.get(key);
});
return results;
}
/**
* 检查键是否存在
*/
has(key) {
return this.get(key) !== null;
}
/**
* 获取缓存统计
*/
getStats() {
const hitRate = this.cacheStats.hits / (this.cacheStats.hits + this.cacheStats.misses) * 100;
return {
...this.cacheStats,
hitRate: isNaN(hitRate) ? 0 : Math.round(hitRate * 100) / 100,
memorySize: this.memoryCache.size,
timestamp: Date.now()
};
}
// 私有方法
setMemoryCache(key, data, expires) {
// 检查内存限制
if (this.memoryCache.size >= this.maxMemorySize) {
this.evictOldest();
}
this.memoryCache.set(key, {
data,
expires,
timestamp: Date.now()
});
}
evictOldest() {
let oldestKey = null;
let oldestTime = Date.now();
for (const [key, item] of this.memoryCache) {
if (item.timestamp < oldestTime) {
oldestTime = item.timestamp;
oldestKey = key;
}
}
if (oldestKey) {
this.memoryCache.delete(oldestKey);
}
}
isExpired(item) {
return Date.now() > item.expires;
}
getStorageKey(key) {
return `cache_${key}`;
}
getStorageEngine() {
try {
// 测试 localStorage 可用性
const testKey = 'cache_test';
localStorage.setItem(testKey, 'test');
localStorage.removeItem(testKey);
return localStorage;
} catch (error) {
// 回退到内存存储
console.warn('localStorage not available, using memory storage');
return new Map();
}
}
startCleanupTimer() {
// 每5分钟清理一次过期缓存
setInterval(() => {
this.cleanupExpired();
}, 5 * 60 * 1000);
}
cleanupExpired() {
// 清理内存缓存
for (const [key, item] of this.memoryCache) {
if (this.isExpired(item)) {
this.memoryCache.delete(key);
}
}
// 清理存储缓存
this.cleanupStorage();
}
cleanupStorage() {
if (this.storageCache instanceof Map) return;
try {
const keys = Object.keys(this.storageCache);
keys.forEach(key => {
if (key.startsWith('cache_')) {
try {
const data = JSON.parse(this.storageCache.getItem(key));
if (this.isExpired(data)) {
this.storageCache.removeItem(key);
}
} catch (error) {
// 数据损坏,删除
this.storageCache.removeItem(key);
}
}
});
} catch (error) {
console.warn('Storage cleanup error:', error);
}
}
bindStorageEvents() {
if (this.storageCache instanceof Map) return;
window.addEventListener('storage', (e) => {
if (e.key && e.key.startsWith('cache_')) {
const cacheKey = e.key.substring(6); // 移除 'cache_' 前缀
this.memoryCache.delete(cacheKey);
}
});
}
cleanup() {
this.cleanupExpired();
}
}
// 全局实例
window.CacheManager = new CacheManager();
阶段三:组件系统建设(第 5-6 周)
3.1 主页面重构示例
main_optimized.html
{% extends "layouts/dashboard_layout.html" %}
{% block title %}智能健康数据分析平台 - 主页{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/pages/main.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/charts.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/panels.css') }}">
{% endblock %}
{% block body_class %}main-dashboard{% endblock %}
{% block content %}
<div class="dashboard-container" id="dashboard-container">
<!-- 顶部状态栏 -->
<header class="dashboard-header">
{% include 'components/widgets/status_indicator.html' %}
{% include 'components/widgets/refresh_controls.html' %}
</header>
<!-- 主要内容区域 -->
<div class="dashboard-content">
<!-- 左侧面板 -->
<aside class="left-panel">
{% include 'components/panels/summary_panel.html' %}
{% include 'components/panels/alert_panel.html' %}
</aside>
<!-- 中央图表区 -->
<section class="center-panel">
<div class="chart-grid">
{% include 'components/charts/health_score_chart.html' %}
{% include 'components/charts/trend_chart.html' %}
{% include 'components/charts/distribution_chart.html' %}
</div>
</section>
<!-- 右侧面板 -->
<aside class="right-panel">
{% include 'components/panels/device_info_panel.html' %}
{% include 'components/panels/message_panel.html' %}
</aside>
</div>
<!-- 底部工具栏 -->
<footer class="dashboard-footer">
{% include 'components/widgets/pagination_controls.html' %}
{% include 'components/widgets/export_controls.html' %}
</footer>
</div>
<!-- 模态对话框 -->
<div id="modal-container"></div>
{% endblock %}
{% block scripts %}
<!-- ECharts 库 -->
<script src="{{ url_for('static', filename='js/lib/echarts-5.4.0.min.js') }}"></script>
<!-- 页面组件 -->
<script src="{{ url_for('static', filename='js/components/health-chart.js') }}"></script>
<script src="{{ url_for('static', filename='js/components/device-panel.js') }}"></script>
<script src="{{ url_for('static', filename='js/components/message-panel.js') }}"></script>
<script src="{{ url_for('static', filename='js/components/alert-panel.js') }}"></script>
<!-- 页面主脚本 -->
<script src="{{ url_for('static', filename='js/pages/main-page.js') }}"></script>
<script>
// 页面初始化
document.addEventListener('DOMContentLoaded', function() {
const mainPage = new MainPage({
container: '#dashboard-container',
refreshInterval: 5000,
autoRefresh: true,
customerId: {{ customer_id|default('null') }}
});
mainPage.init();
// 全局引用,便于调试
window.mainPage = mainPage;
});
</script>
{% endblock %}
3.2 页面控制器实现
main-page.js - 主页控制器
/**
* 主页面控制器
*/
class MainPage {
constructor(options = {}) {
this.options = {
container: '#dashboard-container',
refreshInterval: 5000,
autoRefresh: true,
customerId: null,
...options
};
this.container = document.querySelector(this.options.container);
this.components = new Map();
this.isInitialized = false;
this.refreshTimer = null;
this.lastRefreshTime = null;
}
async init() {
if (this.isInitialized) return;
try {
this.showLoading();
// 初始化组件
await this.initComponents();
// 绑定事件
this.bindEvents();
// 加载初始数据
await this.loadInitialData();
// 启动自动刷新
if (this.options.autoRefresh) {
this.startAutoRefresh();
}
this.isInitialized = true;
this.hideLoading();
console.log('主页面初始化完成');
} catch (error) {
this.hideLoading();
this.showError('页面初始化失败: ' + error.message);
console.error('Main page init error:', error);
}
}
async initComponents() {
const componentConfigs = [
{
name: 'healthChart',
class: HealthScoreChart,
container: '#health-score-chart',
options: {
autoRefresh: false // 由页面控制刷新
}
},
{
name: 'trendChart',
class: TrendChart,
container: '#trend-chart',
options: {
timeRange: '24h'
}
},
{
name: 'devicePanel',
class: DevicePanel,
container: '#device-panel',
options: {}
},
{
name: 'messagePanel',
class: MessagePanel,
container: '#message-panel',
options: {
maxMessages: 10
}
},
{
name: 'alertPanel',
class: AlertPanel,
container: '#alert-panel',
options: {
maxAlerts: 5
}
}
];
// 并行初始化组件
const initPromises = componentConfigs.map(async (config) => {
try {
const container = document.querySelector(config.container);
if (!container) {
console.warn(`组件容器不存在: ${config.container}`);
return;
}
const component = new config.class(container, config.options);
await component.init?.();
this.components.set(config.name, component);
console.log(`组件初始化完成: ${config.name}`);
} catch (error) {
console.error(`组件初始化失败 ${config.name}:`, error);
}
});
await Promise.all(initPromises);
}
async loadInitialData() {
const loadTasks = [];
// 准备所有数据加载任务
if (this.components.has('healthChart')) {
loadTasks.push({
name: 'healthScore',
task: () => this.loadHealthScore()
});
}
if (this.components.has('trendChart')) {
loadTasks.push({
name: 'trendData',
task: () => this.loadTrendData()
});
}
if (this.components.has('devicePanel')) {
loadTasks.push({
name: 'deviceInfo',
task: () => this.loadDeviceInfo()
});
}
if (this.components.has('messagePanel')) {
loadTasks.push({
name: 'messages',
task: () => this.loadMessages()
});
}
if (this.components.has('alertPanel')) {
loadTasks.push({
name: 'alerts',
task: () => this.loadAlerts()
});
}
// 并行执行所有加载任务
const results = await Promise.allSettled(
loadTasks.map(task => task.task())
);
// 记录加载结果
results.forEach((result, index) => {
const taskName = loadTasks[index].name;
if (result.status === 'fulfilled') {
console.log(`数据加载成功: ${taskName}`);
} else {
console.error(`数据加载失败 ${taskName}:`, result.reason);
}
});
this.lastRefreshTime = Date.now();
}
async loadHealthScore() {
const params = {
orgId: this.options.customerId,
days: 7,
includeFactors: true
};
const data = await ApiClient.cached('/health/score/comprehensive', params, 60000);
const healthChart = this.components.get('healthChart');
if (healthChart) {
healthChart.updateData(data);
}
return data;
}
async loadTrendData() {
const params = {
orgId: this.options.customerId,
timeRange: '24h',
metrics: ['health_score', 'device_count', 'alert_count']
};
const data = await ApiClient.cached('/health/trends/analysis', params, 300000); // 缓存5分钟
const trendChart = this.components.get('trendChart');
if (trendChart) {
trendChart.updateData(data);
}
return data;
}
async loadDeviceInfo() {
const params = {
orgId: this.options.customerId,
includeOffline: true
};
const data = await ApiClient.cached('/organization/devices/summary', params, 30000); // 缓存30秒
const devicePanel = this.components.get('devicePanel');
if (devicePanel) {
devicePanel.updateData(data);
}
return data;
}
async loadMessages() {
const params = {
orgId: this.options.customerId,
limit: 10,
type: 'SYSTEM,ALERT'
};
const data = await ApiClient.get('/messages/recent', params);
const messagePanel = this.components.get('messagePanel');
if (messagePanel) {
messagePanel.updateData(data);
}
return data;
}
async loadAlerts() {
const params = {
orgId: this.options.customerId,
status: 'ACTIVE',
limit: 5
};
const data = await ApiClient.get('/alerts/active', params);
const alertPanel = this.components.get('alertPanel');
if (alertPanel) {
alertPanel.updateData(data);
}
return data;
}
bindEvents() {
// 刷新按钮
const refreshBtn = document.getElementById('refresh-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.refresh());
}
// 自动刷新切换
const autoRefreshToggle = document.getElementById('auto-refresh-toggle');
if (autoRefreshToggle) {
autoRefreshToggle.addEventListener('change', (e) => {
if (e.target.checked) {
this.startAutoRefresh();
} else {
this.stopAutoRefresh();
}
});
}
// 页面可见性变化
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.stopAutoRefresh();
} else if (this.options.autoRefresh) {
this.startAutoRefresh();
}
});
// 组件事件
EventBus.on('component:error', (data) => {
console.error('组件错误:', data);
this.showError(`组件错误: ${data.message}`);
});
EventBus.on('alert:new', (data) => {
this.handleNewAlert(data);
});
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 'r':
e.preventDefault();
this.refresh();
break;
}
}
});
}
startAutoRefresh() {
this.stopAutoRefresh();
this.refreshTimer = setInterval(() => {
if (this.isPageVisible() && this.isInitialized) {
this.refresh();
}
}, this.options.refreshInterval);
console.log(`自动刷新启动,间隔: ${this.options.refreshInterval}ms`);
}
stopAutoRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
console.log('自动刷新停止');
}
}
async refresh() {
if (!this.isInitialized) return;
try {
console.log('开始刷新页面数据...');
const startTime = Date.now();
this.showRefreshing();
// 清理相关缓存
this.clearRelevantCache();
// 重新加载数据
await this.loadInitialData();
const duration = Date.now() - startTime;
console.log(`页面刷新完成,耗时: ${duration}ms`);
this.hideRefreshing();
this.showSuccess('数据刷新成功');
} catch (error) {
this.hideRefreshing();
this.showError('数据刷新失败: ' + error.message);
console.error('Refresh error:', error);
}
}
clearRelevantCache() {
const keysToDelete = [
'health-score',
'trend-data',
'device-info',
'messages',
'alerts'
];
keysToDelete.forEach(key => {
// 清理相关的缓存项
for (const cacheKey of CacheManager.memoryCache.keys()) {
if (cacheKey.includes(key)) {
CacheManager.delete(cacheKey);
}
}
});
}
handleNewAlert(alertData) {
// 播放提示音
this.playNotificationSound();
// 更新告警面板
const alertPanel = this.components.get('alertPanel');
if (alertPanel) {
alertPanel.addNewAlert(alertData);
}
// 显示通知
this.showNotification('新告警', alertData.message, 'warning');
}
isPageVisible() {
return !document.hidden;
}
showLoading() {
document.getElementById('global-loading')?.classList.remove('hidden');
}
hideLoading() {
document.getElementById('global-loading')?.classList.add('hidden');
}
showRefreshing() {
const refreshBtn = document.getElementById('refresh-btn');
if (refreshBtn) {
refreshBtn.classList.add('refreshing');
refreshBtn.disabled = true;
}
}
hideRefreshing() {
const refreshBtn = document.getElementById('refresh-btn');
if (refreshBtn) {
refreshBtn.classList.remove('refreshing');
refreshBtn.disabled = false;
}
}
showError(message) {
this.showToast(message, 'error');
}
showSuccess(message) {
this.showToast(message, 'success');
}
showToast(message, type = 'info') {
// 简单的 toast 实现
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
}, 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
}, 3000);
}
showNotification(title, message, type = 'info') {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(title, {
body: message,
icon: '/static/images/icon.png'
});
}
}
playNotificationSound() {
try {
const audio = new Audio('/static/sounds/notification.wav');
audio.volume = 0.3;
audio.play().catch(e => {
console.warn('无法播放提示音:', e);
});
} catch (error) {
console.warn('提示音播放失败:', error);
}
}
destroy() {
this.stopAutoRefresh();
// 销毁所有组件
this.components.forEach((component, name) => {
try {
if (component.destroy) {
component.destroy();
}
} catch (error) {
console.error(`组件销毁失败 ${name}:`, error);
}
});
this.components.clear();
this.isInitialized = false;
console.log('主页面已销毁');
}
}
阶段四:样式系统优化(第 7-8 周)
4.1 CSS 架构重构
变量系统 (variables.css)
/**
* CSS 变量系统 - 主题和设计令牌
*/
:root {
/* ============= 颜色系统 ============= */
/* 主色调 */
--primary-color: #00ff9d;
--primary-light: #33ffb5;
--primary-dark: #00cc7d;
--primary-rgb: 0, 255, 157;
/* 次要色调 */
--secondary-color: #00e4ff;
--secondary-light: #33e9ff;
--secondary-dark: #00b7cc;
--secondary-rgb: 0, 228, 255;
/* 强调色 */
--accent-color: #ff6b6b;
--accent-light: #ff8a8a;
--accent-dark: #ff4757;
--accent-rgb: 255, 107, 107;
/* 警告色系 */
--warning-color: #ffa726;
--warning-light: #ffb74d;
--warning-dark: #f57c00;
/* 成功色系 */
--success-color: #66bb6a;
--success-light: #81c784;
--success-dark: #4caf50;
/* 错误色系 */
--error-color: #ef5350;
--error-light: #f48fb1;
--error-dark: #c62828;
/* 信息色系 */
--info-color: #42a5f5;
--info-light: #64b5f6;
--info-dark: #1976d2;
/* ============= 背景色系统 ============= */
/* 主背景 */
--bg-primary: #0a1525;
--bg-primary-rgb: 10, 21, 37;
/* 次要背景 */
--bg-secondary: rgba(13, 20, 33, 0.95);
--bg-secondary-rgb: 13, 20, 33;
/* 卡片背景 */
--bg-card: rgba(6, 18, 25, 0.9);
--bg-card-hover: rgba(0, 21, 41, 0.95);
/* 面板背景 */
--bg-panel: linear-gradient(145deg, rgba(13, 20, 33, 0.95), rgba(6, 18, 25, 0.95));
/* 覆盖层背景 */
--bg-overlay: rgba(0, 0, 0, 0.8);
--bg-modal: rgba(0, 0, 0, 0.9);
/* ============= 文字色系统 ============= */
/* 主要文字 */
--text-primary: #ffffff;
--text-primary-rgb: 255, 255, 255;
/* 次要文字 */
--text-secondary: rgba(255, 255, 255, 0.8);
--text-secondary-rgb: 255, 255, 255;
/* 禁用文字 */
--text-disabled: rgba(255, 255, 255, 0.4);
/* 链接文字 */
--text-link: var(--secondary-color);
--text-link-hover: var(--secondary-light);
/* ============= 边框色系统 ============= */
/* 主边框 */
--border-primary: rgba(0, 255, 157, 0.3);
--border-secondary: rgba(0, 228, 255, 0.2);
/* 输入框边框 */
--border-input: rgba(255, 255, 255, 0.2);
--border-input-focus: var(--primary-color);
--border-input-error: var(--error-color);
/* 分割线 */
--border-divider: rgba(255, 255, 255, 0.1);
/* ============= 阴影系统 ============= */
/* 基础阴影 */
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.2);
--shadow-xl: 0 16px 32px rgba(0, 0, 0, 0.25);
/* 彩色阴影 */
--shadow-primary: 0 4px 15px rgba(var(--primary-rgb), 0.3);
--shadow-secondary: 0 4px 15px rgba(var(--secondary-rgb), 0.3);
--shadow-accent: 0 4px 15px rgba(var(--accent-rgb), 0.3);
/* 发光效果 */
--glow-primary: 0 0 20px rgba(var(--primary-rgb), 0.5);
--glow-secondary: 0 0 20px rgba(var(--secondary-rgb), 0.5);
--glow-accent: 0 0 20px rgba(var(--accent-rgb), 0.5);
/* ============= 尺寸系统 ============= */
/* 间距 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
--spacing-3xl: 64px;
/* 圆角 */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-full: 50%;
/* 边框宽度 */
--border-width: 1px;
--border-width-thick: 2px;
/* ============= 字体系统 ============= */
/* 字体族 */
--font-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
--font-mono: 'Courier New', Monaco, Consolas, monospace;
--font-number: 'NumFont', var(--font-mono);
/* 字体大小 */
--font-xs: 10px;
--font-sm: 12px;
--font-md: 14px;
--font-lg: 16px;
--font-xl: 18px;
--font-2xl: 24px;
--font-3xl: 32px;
--font-4xl: 48px;
/* 字体重量 */
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* 行高 */
--line-height-tight: 1.2;
--line-height-normal: 1.5;
--line-height-relaxed: 1.7;
/* ============= 层级系统 ============= */
--z-dropdown: 1000;
--z-sticky: 1010;
--z-fixed: 1020;
--z-modal-backdrop: 1030;
--z-modal: 1040;
--z-popover: 1050;
--z-tooltip: 1060;
--z-toast: 1070;
/* ============= 动画系统 ============= */
/* 过渡时长 */
--transition-fast: 150ms;
--transition-normal: 300ms;
--transition-slow: 500ms;
/* 缓动函数 */
--ease-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
/* ============= 断点系统 ============= */
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;
/* ============= 大屏特定变量 ============= */
/* 大屏布局 */
--dashboard-sidebar-width: 280px;
--dashboard-header-height: 80px;
--dashboard-footer-height: 60px;
/* 图表尺寸 */
--chart-height-sm: 200px;
--chart-height-md: 300px;
--chart-height-lg: 400px;
/* 面板尺寸 */
--panel-width-sm: 300px;
--panel-width-md: 400px;
--panel-width-lg: 500px;
/* 卡片尺寸 */
--card-min-height: 120px;
--card-max-width: 400px;
}
/* ============= 主题变体 ============= */
/* 深蓝主题 */
[data-theme="deep-blue"] {
--primary-color: #1890ff;
--primary-light: #40a9ff;
--primary-dark: #0c7cd5;
--primary-rgb: 24, 144, 255;
--bg-primary: #001529;
--bg-primary-rgb: 0, 21, 41;
--border-primary: rgba(24, 144, 255, 0.3);
--shadow-primary: 0 4px 15px rgba(24, 144, 255, 0.3);
--glow-primary: 0 0 20px rgba(24, 144, 255, 0.5);
}
/* 紫罗兰主题 */
[data-theme="purple"] {
--primary-color: #722ed1;
--primary-light: #9254de;
--primary-dark: #531dab;
--primary-rgb: 114, 46, 209;
--bg-primary: #1a0d2e;
--bg-primary-rgb: 26, 13, 46;
--border-primary: rgba(114, 46, 209, 0.3);
--shadow-primary: 0 4px 15px rgba(114, 46, 209, 0.3);
--glow-primary: 0 0 20px rgba(114, 46, 209, 0.5);
}
/* 绿色生态主题 */
[data-theme="eco-green"] {
--primary-color: #52c41a;
--primary-light: #73d13d;
--primary-dark: #389e0d;
--primary-rgb: 82, 196, 26;
--bg-primary: #162312;
--bg-primary-rgb: 22, 35, 18;
--border-primary: rgba(82, 196, 26, 0.3);
--shadow-primary: 0 4px 15px rgba(82, 196, 26, 0.3);
--glow-primary: 0 0 20px rgba(82, 196, 26, 0.5);
}
/* ============= 响应式变量 ============= */
@media (max-width: 768px) {
:root {
/* 移动端调整 */
--dashboard-sidebar-width: 100%;
--panel-width-md: 100%;
--font-sm: 11px;
--font-md: 13px;
--spacing-md: 12px;
--spacing-lg: 20px;
}
}
@media (min-width: 1440px) {
:root {
/* 大屏调整 */
--font-md: 15px;
--font-lg: 17px;
--spacing-lg: 28px;
--spacing-xl: 36px;
}
}
/* ============= 高对比度主题 ============= */
@media (prefers-contrast: high) {
:root {
--text-primary: #ffffff;
--text-secondary: #e0e0e0;
--border-primary: rgba(255, 255, 255, 0.5);
--border-secondary: rgba(255, 255, 255, 0.3);
}
}
/* ============= 减少动画主题 ============= */
@media (prefers-reduced-motion: reduce) {
:root {
--transition-fast: 0ms;
--transition-normal: 0ms;
--transition-slow: 0ms;
}
}
4.2 组件样式系统
图表组件样式 (charts.css)
/**
* 图表组件样式
*/
/* ============= 基础图表容器 ============= */
.chart-container {
position: relative;
background: var(--bg-card);
border: var(--border-width) solid var(--border-secondary);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
backdrop-filter: blur(10px);
transition: all var(--transition-normal) var(--ease-out);
overflow: hidden;
}
.chart-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg,
var(--primary-color),
var(--secondary-color),
var(--primary-color)
);
opacity: 0.6;
}
.chart-container:hover {
border-color: var(--border-primary);
box-shadow: var(--shadow-primary);
transform: translateY(-2px);
}
/* ============= 图表标题 ============= */
.chart-title {
color: var(--text-primary);
font-size: var(--font-lg);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-md);
display: flex;
align-items: center;
justify-content: space-between;
}
.chart-title-text {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.chart-title-icon {
width: 20px;
height: 20px;
color: var(--primary-color);
filter: drop-shadow(0 0 4px rgba(var(--primary-rgb), 0.5));
}
.chart-subtitle {
color: var(--text-secondary);
font-size: var(--font-sm);
font-weight: var(--font-weight-normal);
margin-top: var(--spacing-xs);
}
/* ============= 图表工具栏 ============= */
.chart-toolbar {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.chart-toolbar-btn {
width: 32px;
height: 32px;
border: var(--border-width) solid var(--border-input);
border-radius: var(--radius-md);
background: rgba(var(--bg-primary-rgb), 0.5);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-fast) var(--ease-out);
}
.chart-toolbar-btn:hover {
background: rgba(var(--primary-rgb), 0.1);
border-color: var(--primary-color);
color: var(--primary-color);
}
.chart-toolbar-btn:active {
transform: scale(0.95);
}
.chart-toolbar-btn.active {
background: rgba(var(--primary-rgb), 0.2);
border-color: var(--primary-color);
color: var(--primary-color);
box-shadow: inset 0 2px 4px rgba(var(--primary-rgb), 0.2);
}
/* ============= 图表内容区域 ============= */
.chart-content {
position: relative;
width: 100%;
min-height: var(--chart-height-md);
}
.chart-canvas {
width: 100% !important;
height: 100% !important;
}
/* ============= 图表加载状态 ============= */
.chart-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(var(--bg-primary-rgb), 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-md);
backdrop-filter: blur(4px);
z-index: 10;
opacity: 0;
visibility: hidden;
transition: all var(--transition-normal) var(--ease-out);
}
.chart-container.loading .chart-loading {
opacity: 1;
visibility: visible;
}
.chart-loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(var(--primary-rgb), 0.2);
border-top: 3px solid var(--primary-color);
border-radius: var(--radius-full);
animation: chart-spin 1s linear infinite;
}
.chart-loading-text {
color: var(--text-secondary);
font-size: var(--font-sm);
font-weight: var(--font-weight-medium);
}
@keyframes chart-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* ============= 图表错误状态 ============= */
.chart-error {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(var(--bg-primary-rgb), 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-md);
backdrop-filter: blur(4px);
z-index: 10;
opacity: 0;
visibility: hidden;
transition: all var(--transition-normal) var(--ease-out);
}
.chart-container.error .chart-error {
opacity: 1;
visibility: visible;
}
.chart-error-icon {
width: 48px;
height: 48px;
color: var(--error-color);
filter: drop-shadow(0 0 8px rgba(239, 83, 80, 0.3));
}
.chart-error-text {
color: var(--text-primary);
font-size: var(--font-md);
font-weight: var(--font-weight-medium);
text-align: center;
}
.chart-error-detail {
color: var(--text-secondary);
font-size: var(--font-sm);
text-align: center;
max-width: 80%;
}
.chart-error-retry {
margin-top: var(--spacing-md);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--error-color);
color: var(--text-primary);
border: none;
border-radius: var(--radius-md);
font-size: var(--font-sm);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: all var(--transition-fast) var(--ease-out);
}
.chart-error-retry:hover {
background: var(--error-light);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
/* ============= 健康评分图表特定样式 ============= */
.health-score-chart {
--chart-primary: var(--primary-color);
--chart-secondary: var(--secondary-color);
}
.health-score-display {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: var(--spacing-lg) 0;
}
.health-score-number {
color: var(--primary-color);
font-size: var(--font-4xl);
font-weight: var(--font-weight-bold);
font-family: var(--font-number);
text-shadow: 0 0 20px rgba(var(--primary-rgb), 0.5);
animation: health-score-pulse 2s ease-in-out infinite alternate;
}
.health-score-label {
color: var(--text-secondary);
font-size: var(--font-sm);
font-weight: var(--font-weight-medium);
margin-top: var(--spacing-sm);
letter-spacing: 1px;
}
.health-score-status {
margin-top: var(--spacing-sm);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-md);
font-size: var(--font-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.health-score-status.excellent {
background: rgba(102, 187, 106, 0.2);
color: var(--success-color);
border: 1px solid rgba(102, 187, 106, 0.3);
}
.health-score-status.good {
background: rgba(66, 165, 245, 0.2);
color: var(--info-color);
border: 1px solid rgba(66, 165, 245, 0.3);
}
.health-score-status.average {
background: rgba(255, 167, 38, 0.2);
color: var(--warning-color);
border: 1px solid rgba(255, 167, 38, 0.3);
}
.health-score-status.poor {
background: rgba(255, 107, 107, 0.2);
color: var(--accent-color);
border: 1px solid rgba(255, 107, 107, 0.3);
}
@keyframes health-score-pulse {
0% {
transform: scale(1);
text-shadow: 0 0 20px rgba(var(--primary-rgb), 0.5);
}
100% {
transform: scale(1.05);
text-shadow: 0 0 30px rgba(var(--primary-rgb), 0.8);
}
}
/* ============= 趋势图表特定样式 ============= */
.trend-chart {
--chart-line-color: var(--secondary-color);
--chart-area-color: rgba(var(--secondary-rgb), 0.1);
}
.trend-stats {
display: flex;
justify-content: space-around;
margin-top: var(--spacing-md);
padding: var(--spacing-md);
background: rgba(var(--bg-secondary-rgb), 0.5);
border-radius: var(--radius-md);
border: 1px solid var(--border-divider);
}
.trend-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
}
.trend-stat-value {
color: var(--text-primary);
font-size: var(--font-xl);
font-weight: var(--font-weight-bold);
font-family: var(--font-number);
}
.trend-stat-label {
color: var(--text-secondary);
font-size: var(--font-xs);
font-weight: var(--font-weight-medium);
}
.trend-stat-change {
font-size: var(--font-xs);
font-weight: var(--font-weight-semibold);
display: flex;
align-items: center;
gap: 2px;
}
.trend-stat-change.positive {
color: var(--success-color);
}
.trend-stat-change.negative {
color: var(--error-color);
}
.trend-stat-change.neutral {
color: var(--text-secondary);
}
/* ============= 雷达图特定样式 ============= */
.radar-chart {
--chart-radar-color: var(--primary-color);
--chart-radar-fill: rgba(var(--primary-rgb), 0.1);
}
.radar-legend {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
justify-content: center;
}
.radar-legend-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
background: rgba(var(--bg-secondary-rgb), 0.5);
border-radius: var(--radius-md);
border: 1px solid var(--border-divider);
}
.radar-legend-color {
width: 12px;
height: 12px;
border-radius: var(--radius-sm);
}
.radar-legend-text {
color: var(--text-secondary);
font-size: var(--font-xs);
font-weight: var(--font-weight-medium);
}
/* ============= 响应式设计 ============= */
@media (max-width: 768px) {
.chart-container {
padding: var(--spacing-md);
}
.chart-title {
font-size: var(--font-md);
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-sm);
}
.chart-toolbar {
order: -1;
align-self: flex-end;
}
.chart-content {
min-height: var(--chart-height-sm);
}
.trend-stats {
flex-direction: column;
gap: var(--spacing-sm);
}
.radar-legend {
flex-direction: column;
align-items: center;
}
}
@media (max-width: 480px) {
.chart-container {
padding: var(--spacing-sm);
}
.health-score-number {
font-size: var(--font-3xl);
}
.chart-loading-spinner {
width: 32px;
height: 32px;
}
.chart-error-icon {
width: 40px;
height: 40px;
}
}
/* ============= 打印样式 ============= */
@media print {
.chart-container {
background: white !important;
color: black !important;
border: 1px solid #ccc !important;
box-shadow: none !important;
}
.chart-toolbar {
display: none !important;
}
.chart-loading,
.chart-error {
display: none !important;
}
}
阶段五:测试与部署(第 9-10 周)
5.1 性能测试方案
前端性能测试脚本
/**
* 前端性能测试套件
*/
class PerformanceTestSuite {
constructor() {
this.tests = [];
this.results = [];
this.observer = null;
}
// 页面加载性能测试
async testPageLoad() {
const test = {
name: '页面加载性能',
startTime: performance.now()
};
// 监听页面加载完成
await new Promise(resolve => {
if (document.readyState === 'complete') {
resolve();
} else {
window.addEventListener('load', resolve);
}
});
const loadTime = performance.now() - test.startTime;
const navigation = performance.getEntriesByType('navigation')[0];
return {
...test,
endTime: performance.now(),
metrics: {
totalLoadTime: Math.round(loadTime),
domContentLoaded: Math.round(navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart),
firstPaint: this.getFirstPaint(),
firstContentfulPaint: this.getFirstContentfulPaint(),
largestContentfulPaint: await this.getLargestContentfulPaint()
}
};
}
// API 请求性能测试
async testApiPerformance() {
const apiTests = [
{ name: '健康评分API', endpoint: '/api/v1/health/score/comprehensive', params: { days: 7 } },
{ name: '设备信息API', endpoint: '/api/v1/organization/devices/summary', params: {} },
{ name: '消息列表API', endpoint: '/api/v1/messages/recent', params: { limit: 10 } },
{ name: '告警列表API', endpoint: '/api/v1/alerts/active', params: { limit: 5 } }
];
const results = [];
for (const apiTest of apiTests) {
const startTime = performance.now();
try {
await ApiClient.get(apiTest.endpoint, apiTest.params);
const endTime = performance.now();
results.push({
name: apiTest.name,
success: true,
responseTime: Math.round(endTime - startTime),
endpoint: apiTest.endpoint
});
} catch (error) {
results.push({
name: apiTest.name,
success: false,
error: error.message,
endpoint: apiTest.endpoint
});
}
}
return {
name: 'API请求性能测试',
results,
averageResponseTime: results
.filter(r => r.success)
.reduce((sum, r) => sum + r.responseTime, 0) / results.filter(r => r.success).length
};
}
// 内存使用测试
testMemoryUsage() {
if (!performance.memory) {
return { name: '内存使用测试', error: '浏览器不支持内存监控' };
}
return {
name: '内存使用测试',
metrics: {
usedJSHeapSize: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024), // MB
totalJSHeapSize: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024), // MB
jsHeapSizeLimit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024), // MB
memoryUsagePercent: Math.round((performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100)
}
};
}
// 渲染性能测试
async testRenderingPerformance() {
const frameData = [];
let startTime = performance.now();
let frameCount = 0;
// 监控60帧
const monitorFrames = () => {
return new Promise(resolve => {
const collectFrame = () => {
const currentTime = performance.now();
frameData.push(currentTime);
frameCount++;
if (frameCount < 60) {
requestAnimationFrame(collectFrame);
} else {
resolve();
}
};
requestAnimationFrame(collectFrame);
});
};
await monitorFrames();
// 计算帧率
const totalTime = frameData[frameData.length - 1] - frameData[0];
const averageFPS = Math.round((frameCount - 1) * 1000 / totalTime);
// 计算帧时间变化
const frameTimes = [];
for (let i = 1; i < frameData.length; i++) {
frameTimes.push(frameData[i] - frameData[i - 1]);
}
const avgFrameTime = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;
const maxFrameTime = Math.max(...frameTimes);
return {
name: '渲染性能测试',
metrics: {
averageFPS,
averageFrameTime: Math.round(avgFrameTime),
maxFrameTime: Math.round(maxFrameTime),
frameCount,
totalTime: Math.round(totalTime)
}
};
}
// 缓存性能测试
async testCachePerformance() {
const testKey = 'performance_test';
const testData = { timestamp: Date.now(), data: 'test data' };
// 测试缓存写入
const writeStartTime = performance.now();
CacheManager.set(testKey, testData, 60000);
const writeTime = performance.now() - writeStartTime;
// 测试缓存读取
const readStartTime = performance.now();
const cachedData = CacheManager.get(testKey);
const readTime = performance.now() - readStartTime;
// 清理测试数据
CacheManager.delete(testKey);
const cacheStats = CacheManager.getStats();
return {
name: '缓存性能测试',
metrics: {
writeTime: Math.round(writeTime * 100) / 100, // 精确到小数点后2位
readTime: Math.round(readTime * 100) / 100,
cacheHitRate: cacheStats.hitRate,
memorySize: cacheStats.memorySize,
success: cachedData && cachedData.timestamp === testData.timestamp
}
};
}
// 运行所有测试
async runAllTests() {
console.log('🚀 开始性能测试...');
this.results = [];
try {
// 页面加载测试
const pageLoadTest = await this.testPageLoad();
this.results.push(pageLoadTest);
console.log('✅ 页面加载测试完成');
// API性能测试
const apiTest = await this.testApiPerformance();
this.results.push(apiTest);
console.log('✅ API性能测试完成');
// 内存使用测试
const memoryTest = this.testMemoryUsage();
this.results.push(memoryTest);
console.log('✅ 内存使用测试完成');
// 渲染性能测试
const renderTest = await this.testRenderingPerformance();
this.results.push(renderTest);
console.log('✅ 渲染性能测试完成');
// 缓存性能测试
const cacheTest = await this.testCachePerformance();
this.results.push(cacheTest);
console.log('✅ 缓存性能测试完成');
// 生成报告
this.generateReport();
} catch (error) {
console.error('❌ 性能测试失败:', error);
}
}
// 生成测试报告
generateReport() {
const report = {
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
results: this.results,
summary: this.generateSummary()
};
console.log('📊 性能测试报告:', report);
// 显示报告到页面
this.displayReport(report);
return report;
}
// 生成测试摘要
generateSummary() {
const pageLoadResult = this.results.find(r => r.name === '页面加载性能');
const apiResult = this.results.find(r => r.name === 'API请求性能测试');
const memoryResult = this.results.find(r => r.name === '内存使用测试');
const renderResult = this.results.find(r => r.name === '渲染性能测试');
const cacheResult = this.results.find(r => r.name === '缓存性能测试');
return {
pageLoadTime: pageLoadResult?.metrics?.totalLoadTime || 0,
averageApiResponseTime: apiResult?.averageResponseTime || 0,
memoryUsage: memoryResult?.metrics?.memoryUsagePercent || 0,
averageFPS: renderResult?.metrics?.averageFPS || 0,
cacheHitRate: cacheResult?.metrics?.cacheHitRate || 0,
overallScore: this.calculateOverallScore()
};
}
// 计算总体评分
calculateOverallScore() {
let score = 100;
const pageLoadResult = this.results.find(r => r.name === '页面加载性能');
if (pageLoadResult?.metrics?.totalLoadTime > 3000) score -= 20;
else if (pageLoadResult?.metrics?.totalLoadTime > 2000) score -= 10;
const apiResult = this.results.find(r => r.name === 'API请求性能测试');
if (apiResult?.averageResponseTime > 1000) score -= 20;
else if (apiResult?.averageResponseTime > 500) score -= 10;
const memoryResult = this.results.find(r => r.name === '内存使用测试');
if (memoryResult?.metrics?.memoryUsagePercent > 80) score -= 15;
else if (memoryResult?.metrics?.memoryUsagePercent > 60) score -= 8;
const renderResult = this.results.find(r => r.name === '渲染性能测试');
if (renderResult?.metrics?.averageFPS < 30) score -= 20;
else if (renderResult?.metrics?.averageFPS < 50) score -= 10;
const cacheResult = this.results.find(r => r.name === '缓存性能测试');
if (cacheResult?.metrics?.cacheHitRate < 50) score -= 15;
else if (cacheResult?.metrics?.cacheHitRate < 70) score -= 8;
return Math.max(0, score);
}
// 显示测试报告
displayReport(report) {
// 创建报告界面
const reportModal = document.createElement('div');
reportModal.className = 'performance-report-modal';
reportModal.innerHTML = `
<div class="report-overlay">
<div class="report-container">
<div class="report-header">
<h2>性能测试报告</h2>
<button class="report-close">×</button>
</div>
<div class="report-content">
${this.renderReportContent(report)}
</div>
<div class="report-footer">
<button class="btn-export">导出报告</button>
<button class="btn-close">关闭</button>
</div>
</div>
</div>
`;
document.body.appendChild(reportModal);
// 绑定事件
reportModal.querySelector('.report-close').onclick = () => document.body.removeChild(reportModal);
reportModal.querySelector('.btn-close').onclick = () => document.body.removeChild(reportModal);
reportModal.querySelector('.btn-export').onclick = () => this.exportReport(report);
}
renderReportContent(report) {
const summary = report.summary;
return `
<div class="report-summary">
<div class="summary-score">
<div class="score-circle">
<span class="score-number">${summary.overallScore}</span>
</div>
<div class="score-label">综合评分</div>
</div>
<div class="summary-metrics">
<div class="metric">
<div class="metric-value">${summary.pageLoadTime}ms</div>
<div class="metric-label">页面加载时间</div>
</div>
<div class="metric">
<div class="metric-value">${Math.round(summary.averageApiResponseTime)}ms</div>
<div class="metric-label">平均API响应</div>
</div>
<div class="metric">
<div class="metric-value">${summary.memoryUsage}%</div>
<div class="metric-label">内存使用率</div>
</div>
<div class="metric">
<div class="metric-value">${summary.averageFPS}</div>
<div class="metric-label">平均帧率</div>
</div>
</div>
</div>
<div class="report-details">
${report.results.map(result => this.renderTestResult(result)).join('')}
</div>
`;
}
renderTestResult(result) {
return `
<div class="test-result">
<h3>${result.name}</h3>
<div class="test-metrics">
${result.metrics ? Object.entries(result.metrics).map(([key, value]) =>
`<div class="test-metric">
<span class="metric-key">${key}:</span>
<span class="metric-value">${value}</span>
</div>`
).join('') : ''}
</div>
</div>
`;
}
// 导出报告
exportReport(report) {
const dataStr = JSON.stringify(report, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `performance-report-${Date.now()}.json`;
link.click();
URL.revokeObjectURL(url);
}
// 辅助方法
getFirstPaint() {
const paintEntries = performance.getEntriesByType('paint');
const firstPaint = paintEntries.find(entry => entry.name === 'first-paint');
return firstPaint ? Math.round(firstPaint.startTime) : null;
}
getFirstContentfulPaint() {
const paintEntries = performance.getEntriesByType('paint');
const firstContentfulPaint = paintEntries.find(entry => entry.name === 'first-contentful-paint');
return firstContentfulPaint ? Math.round(firstContentfulPaint.startTime) : null;
}
async getLargestContentfulPaint() {
return new Promise(resolve => {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver(list => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
resolve(lastEntry ? Math.round(lastEntry.startTime) : null);
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
// 超时处理
setTimeout(() => {
observer.disconnect();
resolve(null);
}, 5000);
} else {
resolve(null);
}
});
}
}
// 全局实例
window.PerformanceTestSuite = new PerformanceTestSuite();
预期效果与评估指标
性能提升指标
- 首屏加载时间: 从当前 5-8 秒优化到 2-3 秒
- API 响应时间: 平均响应时间减少 40-60%
- 内存使用: 页面内存占用减少 30-50%
- 渲染性能: 页面流畅度提升,帧率稳定在 50+ FPS
维护性改进
- 代码行数减少: 单文件行数从 9000+ 减少到 300-500 行
- 组件复用性: 组件复用率提升到 80%
- 开发效率: 新功能开发时间减少 50%
- 错误定位: 问题定位时间减少 70%
用户体验提升
- 加载体验: 提供清晰的加载状态指示
- 交互响应: 所有操作响应时间小于 200ms
- 错误处理: 友好的错误提示和恢复机制
- 离线支持: 基础功能支持离线访问
实施建议与注意事项
实施顺序
- 第 1-2 周: 建立基础架构和模块化结构
- 第 3-4 周: 重构核心组件和 API 客户端
- 第 5-6 周: 完成组件系统和页面重构
- 第 7-8 周: 样式系统优化和主题系统
- 第 9-10 周: 性能测试、调优和部署
风险控制
- 渐进式迁移: 新旧系统并行运行,逐步切换
- 功能对等: 确保优化后功能完全对等
- 性能监控: 实时监控性能指标变化
- 回滚方案: 准备完整的回滚机制
团队协作
- 代码审查: 所有重构代码必须经过审查
- 文档更新: 同步更新开发和用户文档
- 培训支持: 为团队提供新架构培训
- 沟通协调: 建立定期沟通机制
文档版本: v1.0
制定时间: 2025-09-09
预计实施周期: 10 周
预期性能提升: 50-70%
维护效率提升: 60-80%
此优化方案将显著提升 BigScreen 大屏系统的性能和可维护性,为用户提供更好的使用体验,为开发团队提供更高效的开发环境。