417 lines
11 KiB
JavaScript
417 lines
11 KiB
JavaScript
/**
|
||
* 测试页面基础框架 - 通用函数
|
||
* 提供API调用、图表渲染、UI交互等公共功能
|
||
*/
|
||
|
||
// ==================== 配置 ====================
|
||
const API_BASE_URL = 'http://localhost:8000/api/v1';
|
||
|
||
// ==================== API 调用函数 ====================
|
||
|
||
/**
|
||
* 发送 API 请求
|
||
* @param {string} endpoint - 接口端点
|
||
* @param {object} data - 请求数据
|
||
* @param {string} method - HTTP 方法
|
||
* @returns {Promise<object>} 响应数据
|
||
*/
|
||
async function apiRequest(endpoint, data, method = 'POST') {
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||
method: method,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: method === 'POST' ? JSON.stringify(data) : null,
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
return result;
|
||
} else {
|
||
throw new Error(result.message || '请求失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('API 请求错误:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示加载状态
|
||
* @param {string} elementId - 元素ID
|
||
*/
|
||
function showLoading(elementId) {
|
||
const element = document.getElementById(elementId);
|
||
if (element) {
|
||
element.innerHTML = `
|
||
<div class="loading-container">
|
||
<div class="spinner"></div>
|
||
<p>加载中...</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 隐藏加载状态
|
||
* @param {string} elementId - 元素ID
|
||
* @param {string} content - 新内容
|
||
*/
|
||
function hideLoading(elementId, content = '') {
|
||
const element = document.getElementById(elementId);
|
||
if (element) {
|
||
element.innerHTML = content;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示错误信息
|
||
* @param {string} elementId - 元素ID
|
||
* @param {string} message - 错误消息
|
||
*/
|
||
function showError(elementId, message) {
|
||
const element = document.getElementById(elementId);
|
||
if (element) {
|
||
element.innerHTML = `
|
||
<div class="error-container">
|
||
<div class="error-icon">⚠️</div>
|
||
<div class="error-message">${message}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示成功消息
|
||
* @param {string} elementId - 元素ID
|
||
* @param {string} message - 成功消息
|
||
*/
|
||
function showSuccess(elementId, message) {
|
||
const element = document.getElementById(elementId);
|
||
if (element) {
|
||
element.innerHTML = `
|
||
<div class="success-container">
|
||
<div class="success-icon">✅</div>
|
||
<div class="success-message">${message}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// ==================== 图表渲染函数 ====================
|
||
|
||
/**
|
||
* 渲染柱状图(使用纯CSS/HTML)
|
||
* @param {string} containerId - 容器ID
|
||
* @param {Array} data - 数据数组 [{label, value, color}]
|
||
* @param {string} title - 图表标题
|
||
*/
|
||
function renderBarChart(containerId, data, title = '') {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
|
||
const maxValue = Math.max(...data.map(d => d.value));
|
||
|
||
let html = `
|
||
<div class="chart-container">
|
||
${title ? `<h3 class="chart-title">${title}</h3>` : ''}
|
||
<div class="bar-chart">
|
||
`;
|
||
|
||
data.forEach((item, index) => {
|
||
const percentage = (item.value / maxValue) * 100;
|
||
const color = item.color || getBarColor(index);
|
||
html += `
|
||
<div class="bar-item">
|
||
<div class="bar-label">${item.label}</div>
|
||
<div class="bar-track">
|
||
<div class="bar-fill" style="width: ${percentage}%; background-color: ${color};"></div>
|
||
</div>
|
||
<div class="bar-value">${item.value}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
/**
|
||
* 渲染饼图(使用CSS)
|
||
* @param {string} containerId - 容器ID
|
||
* @param {Array} data - 数据数组 [{label, value, color}]
|
||
* @param {string} title - 图表标题
|
||
*/
|
||
function renderPieChart(containerId, data, title = '') {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
|
||
const total = data.reduce((sum, item) => sum + item.value, 0);
|
||
|
||
let html = `
|
||
<div class="chart-container">
|
||
${title ? `<h3 class="chart-title">${title}</h3>` : ''}
|
||
<div class="pie-chart-wrapper">
|
||
<div class="pie-chart">
|
||
`;
|
||
|
||
let currentAngle = 0;
|
||
data.forEach((item, index) => {
|
||
const percentage = (item.value / total) * 100;
|
||
const angle = (item.value / total) * 360;
|
||
const color = item.color || getBarColor(index);
|
||
|
||
html += `
|
||
<div class="pie-segment" style="
|
||
--angle: ${currentAngle}deg;
|
||
--size: ${angle}deg;
|
||
background: ${color};
|
||
"></div>
|
||
`;
|
||
|
||
currentAngle += angle;
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
<div class="pie-legend">
|
||
`;
|
||
|
||
data.forEach((item, index) => {
|
||
const percentage = ((item.value / total) * 100).toFixed(1);
|
||
const color = item.color || getBarColor(index);
|
||
html += `
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background-color: ${color};"></div>
|
||
<div class="legend-label">${item.label}: ${item.value} (${percentage}%)</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
/**
|
||
* 渲染卡片列表
|
||
* @param {string} containerId - 容器ID
|
||
* @param {Array} items - 卡片项目数组
|
||
* @param {string} title - 标题
|
||
*/
|
||
function renderCardList(containerId, items, title = '') {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
|
||
let html = `
|
||
<div class="card-list-container">
|
||
${title ? `<h3 class="section-title">${title}</h3>` : ''}
|
||
<div class="card-list">
|
||
`;
|
||
|
||
items.forEach((item, index) => {
|
||
html += `
|
||
<div class="card-item">
|
||
<div class="card-header">
|
||
<div class="card-title">${item.title || item.name || item.id || `项目 ${index + 1}`}</div>
|
||
${item.badge ? `<div class="card-badge ${item.badgeClass || 'badge-info'}">${item.badge}</div>` : ''}
|
||
</div>
|
||
<div class="card-content">${item.content || item.description || ''}</div>
|
||
${item.details ? `
|
||
<div class="card-details">
|
||
${renderKeyValueList(item.details)}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
/**
|
||
* 渲染键值列表
|
||
* @param {object} data - 数据对象
|
||
* @returns {string} HTML字符串
|
||
*/
|
||
function renderKeyValueList(data) {
|
||
if (!data || typeof data !== 'object') return '';
|
||
|
||
let html = '<div class="kv-list">';
|
||
for (const [key, value] of Object.entries(data)) {
|
||
html += `
|
||
<div class="kv-item">
|
||
<span class="kv-key">${key}:</span>
|
||
<span class="kv-value">${value}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
/**
|
||
* 渲染表格数据
|
||
* @param {string} containerId - 容器ID
|
||
* @param {Array} columns - 列定义 [{key, label, width}]
|
||
* @param {Array} data - 数据数组
|
||
* @param {string} title - 表格标题
|
||
*/
|
||
function renderTable(containerId, columns, data, title = '') {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
|
||
let html = `
|
||
<div class="table-container">
|
||
${title ? `<h3 class="section-title">${title}</h3>` : ''}
|
||
<div class="table-wrapper">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
`;
|
||
|
||
columns.forEach(col => {
|
||
html += `<th style="width: ${col.width || 'auto'}">${col.label}</th>`;
|
||
});
|
||
|
||
html += `
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
data.forEach(row => {
|
||
html += '<tr>';
|
||
columns.forEach(col => {
|
||
const value = row[col.key];
|
||
html += `<td>${formatCellValue(value)}</td>`;
|
||
});
|
||
html += '</tr>';
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// ==================== 辅助函数 ====================
|
||
|
||
/**
|
||
* 获取柱状图颜色
|
||
* @param {number} index - 索引
|
||
* @returns {string} 颜色值
|
||
*/
|
||
function getBarColor(index) {
|
||
const colors = [
|
||
'#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b',
|
||
'#858796', '#5a5c69', '#6610f2', '#e83e8c', '#fd7e14'
|
||
];
|
||
return colors[index % colors.length];
|
||
}
|
||
|
||
/**
|
||
* 格式化单元格值
|
||
* @param {any} value - 值
|
||
* @returns {string} 格式化后的字符串
|
||
*/
|
||
function formatCellValue(value) {
|
||
if (value === null || value === undefined) return '-';
|
||
if (Array.isArray(value)) return value.join(', ');
|
||
if (typeof value === 'object') return JSON.stringify(value);
|
||
return String(value);
|
||
}
|
||
|
||
/**
|
||
* 格式化数字
|
||
* @param {number} num - 数字
|
||
* @param {number} decimals - 小数位数
|
||
* @returns {string} 格式化后的字符串
|
||
*/
|
||
function formatNumber(num, decimals = 2) {
|
||
if (num === null || num === undefined) return '-';
|
||
return num.toLocaleString('zh-CN', {
|
||
minimumFractionDigits: decimals,
|
||
maximumFractionDigits: decimals
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 格式化时间
|
||
* @param {number} seconds - 秒数
|
||
* @returns {string} 格式化后的时间字符串
|
||
*/
|
||
function formatTime(seconds) {
|
||
if (seconds < 1) {
|
||
return `${(seconds * 1000).toFixed(0)}ms`;
|
||
} else if (seconds < 60) {
|
||
return `${seconds.toFixed(2)}秒`;
|
||
} else {
|
||
const minutes = Math.floor(seconds / 60);
|
||
const secs = (seconds % 60).toFixed(0);
|
||
return `${minutes}分${secs}秒`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 延迟函数
|
||
* @param {number} ms - 毫秒数
|
||
* @returns {Promise}
|
||
*/
|
||
function delay(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
|
||
/**
|
||
* 复制文本到剪贴板
|
||
* @param {string} text - 文本
|
||
*/
|
||
function copyToClipboard(text) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
showToast('已复制到剪贴板');
|
||
}).catch(err => {
|
||
console.error('复制失败:', err);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 显示提示消息
|
||
* @param {string} message - 消息内容
|
||
*/
|
||
function showToast(message) {
|
||
const toast = document.createElement('div');
|
||
toast.className = 'toast';
|
||
toast.textContent = message;
|
||
document.body.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.classList.add('show');
|
||
}, 10);
|
||
|
||
setTimeout(() => {
|
||
toast.classList.remove('show');
|
||
setTimeout(() => {
|
||
document.body.removeChild(toast);
|
||
}, 300);
|
||
}, 2000);
|
||
}
|