Files
java-template/docs/bigscreen-template-optimization-implementation-guide.md
T
2025-09-09 16:30:41 +08:00

2811 lines
80 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 - 基础模板
```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)
```javascript
/**
* 健康评分图表组件
*/
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)
```javascript
/**
* 统一 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)
```javascript
/**
* 多层缓存管理系统
*/
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
```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 - 主页控制器
```javascript
/**
* 主页面控制器
*/
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
/**
* 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)
```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 性能测试方案
##### 前端性能测试脚本
```javascript
/**
* 前端性能测试套件
*/
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">&times;</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();
```
## 预期效果与评估指标
### 性能提升指标
1. **首屏加载时间**: 从当前 5-8 秒优化到 2-3 秒
2. **API 响应时间**: 平均响应时间减少 40-60%
3. **内存使用**: 页面内存占用减少 30-50%
4. **渲染性能**: 页面流畅度提升,帧率稳定在 50+ FPS
### 维护性改进
1. **代码行数减少**: 单文件行数从 9000+ 减少到 300-500 行
2. **组件复用性**: 组件复用率提升到 80%
3. **开发效率**: 新功能开发时间减少 50%
4. **错误定位**: 问题定位时间减少 70%
### 用户体验提升
1. **加载体验**: 提供清晰的加载状态指示
2. **交互响应**: 所有操作响应时间小于 200ms
3. **错误处理**: 友好的错误提示和恢复机制
4. **离线支持**: 基础功能支持离线访问
## 实施建议与注意事项
### 实施顺序
1. **第 1-2 周**: 建立基础架构和模块化结构
2. **第 3-4 周**: 重构核心组件和 API 客户端
3. **第 5-6 周**: 完成组件系统和页面重构
4. **第 7-8 周**: 样式系统优化和主题系统
5. **第 9-10 周**: 性能测试、调优和部署
### 风险控制
1. **渐进式迁移**: 新旧系统并行运行,逐步切换
2. **功能对等**: 确保优化后功能完全对等
3. **性能监控**: 实时监控性能指标变化
4. **回滚方案**: 准备完整的回滚机制
### 团队协作
1. **代码审查**: 所有重构代码必须经过审查
2. **文档更新**: 同步更新开发和用户文档
3. **培训支持**: 为团队提供新架构培训
4. **沟通协调**: 建立定期沟通机制
---
**文档版本**: v1.0
**制定时间**: 2025-09-09
**预计实施周期**: 10 周
**预期性能提升**: 50-70%
**维护效率提升**: 60-80%
此优化方案将显著提升 BigScreen 大屏系统的性能和可维护性,为用户提供更好的使用体验,为开发团队提供更高效的开发环境。