CI/CD Pipeline / security-scan (push) Successful in 53s
CI/CD Pipeline / test-build-boot (push) Successful in 3m9s
CI/CD Pipeline / build-pr (ljwx-admin) (push) Has been skipped
CI/CD Pipeline / build-pr (ljwx-bigscreen) (push) Has been skipped
CI/CD Pipeline / build-pr (ljwx-boot) (push) Has been skipped
CI/CD Pipeline / test-admin (push) Successful in 6m42s
CI/CD Pipeline / test-bigscreen (push) Successful in 52m7s
CI/CD Pipeline / build-push (ljwx-bigscreen) (push) Successful in 7s
CI/CD Pipeline / build-push (ljwx-boot) (push) Successful in 38s
CI/CD Pipeline / build-push (ljwx-admin) (push) Failing after 1m56s
CI/CD Pipeline / update-deploy (push) Has been skipped
主要变更: 1. OpenAPI代码生成 - 从403个API端点自动生成TypeScript类型(29,138行) - 集成openapi-fetch类型安全客户端,100%类型覆盖 - 新增api:generate和api:check脚本到package.json - 创建GitHub Actions自动同步检查流程 2. 文档标准化管理 - 重构docs/目录为8分类结构(getting-started, architecture, deployment等) - 精简根目录文档从12个减至5个核心文档(-58%) - 归档172个历史文档到archive/2025/ - 创建自动化合规检查脚本scripts/check-doc-location.sh - 新增文档管理规范docs/07-compliance/documentation-standards.md 3. 构建修复 - 修复数据流Dashboard图标导入错误(@vicons/antd → @iconify/vue) - 移除错误的icon包装函数,改用Icon组件直接引用 4. 文档更新 - README.md: 新增v2.0.3功能介绍和技术亮点 - CHANGELOG.md: 详细记录OpenAPI实施和文档重构 - CLAUDE.md: 强制文档管理规范,防止未来违规 技术细节: - openapi-typescript ^7.10.1, openapi-fetch ^0.15.0 - 生成的types.ts文件: 798KB, 29,138行 - 文档分类: 8个主目录 + 归档目录 - 自动化检查: 根目录仅允许5个核心文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
460 lines
14 KiB
HTML
460 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>数据流监控模拟器 - LJWX Health</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
|
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
color: #333;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
color: white;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2.5em;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.header p {
|
|
font-size: 1.1em;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 1.3em;
|
|
font-weight: 600;
|
|
margin-bottom: 16px;
|
|
color: #667eea;
|
|
}
|
|
|
|
.settings-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.form-group label {
|
|
font-weight: 500;
|
|
margin-bottom: 8px;
|
|
color: #666;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group select {
|
|
padding: 10px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
transition: border-color 0.3s;
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group select:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.button-group {
|
|
display: flex;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
flex: 1;
|
|
min-width: 150px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #5568d3;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
}
|
|
|
|
.btn-success {
|
|
background: #48bb78;
|
|
color: white;
|
|
}
|
|
|
|
.btn-success:hover {
|
|
background: #38a169;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(72, 187, 120, 0.4);
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #f56565;
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: #e53e3e;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(245, 101, 101, 0.4);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.log-container {
|
|
background: #1a202c;
|
|
color: #68d391;
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 13px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.log-entry {
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.log-entry.success {
|
|
color: #68d391;
|
|
}
|
|
|
|
.log-entry.error {
|
|
color: #fc8181;
|
|
}
|
|
|
|
.log-entry.info {
|
|
color: #63b3ed;
|
|
}
|
|
|
|
.log-entry.warning {
|
|
color: #f6ad55;
|
|
}
|
|
|
|
.stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 16px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-card .label {
|
|
font-size: 0.9em;
|
|
opacity: 0.9;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stat-card .value {
|
|
font-size: 2em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.5;
|
|
}
|
|
}
|
|
|
|
.running {
|
|
animation: pulse 2s infinite;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🏥 数据流监控模拟器</h1>
|
|
<p>LJWX Health - 健康数据流实时测试工具</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">⚙️ 配置参数</div>
|
|
<div class="settings-grid">
|
|
<div class="form-group">
|
|
<label for="apiUrl">后端API地址</label>
|
|
<input type="text" id="apiUrl" value="http://localhost:9998">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="interval">上传间隔 (毫秒)</label>
|
|
<input type="number" id="interval" value="3000" min="100" max="60000">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="batchSize">每批数据量</label>
|
|
<input type="number" id="batchSize" value="5" min="1" max="100">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="deviceCount">模拟设备数</label>
|
|
<input type="number" id="deviceCount" value="3" min="1" max="10">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="button-group">
|
|
<button class="btn btn-primary" onclick="startSimulation()" id="startBtn">
|
|
🚀 开始模拟
|
|
</button>
|
|
<button class="btn btn-danger" onclick="stopSimulation()" id="stopBtn" disabled>
|
|
⏹️ 停止模拟
|
|
</button>
|
|
<button class="btn btn-success" onclick="sendSingleBatch()">
|
|
📤 发送单次数据
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">📊 实时统计</div>
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<div class="label">总发送次数</div>
|
|
<div class="value" id="totalBatches">0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">总数据量</div>
|
|
<div class="value" id="totalRecords">0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">成功</div>
|
|
<div class="value" id="successCount">0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">失败</div>
|
|
<div class="value" id="errorCount">0</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">📋 运行日志</div>
|
|
<div class="log-container" id="logContainer">
|
|
<div class="log-entry info">💡 提示: 请确保后端服务(ljwx-boot)已启动在 http://localhost:9998</div>
|
|
<div class="log-entry info">💡 提示: 请确保前端页面(ljwx-admin)已打开在 http://192.168.1.83:3333/monitor/data-flow</div>
|
|
<div class="log-entry info">💡 提示: 点击"开始模拟"按钮开始生成测试数据</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let simulationInterval = null;
|
|
let stats = {
|
|
totalBatches: 0,
|
|
totalRecords: 0,
|
|
successCount: 0,
|
|
errorCount: 0
|
|
};
|
|
|
|
// 生成随机健康数据
|
|
function generateHealthData(deviceIndex) {
|
|
const now = new Date();
|
|
const formatDateTime = (date) => {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
};
|
|
|
|
return {
|
|
deviceSn: `WATCH-SIMULATOR-${String(deviceIndex).padStart(3, '0')}`,
|
|
userId: 100 + deviceIndex,
|
|
orgId: 1,
|
|
customerId: 8,
|
|
heart_rate: 60 + Math.floor(Math.random() * 40),
|
|
blood_oxygen: 95 + Math.floor(Math.random() * 5),
|
|
temperature: 36.0 + Math.random() * 1.5,
|
|
pressure_high: 110 + Math.floor(Math.random() * 30),
|
|
pressure_low: 70 + Math.floor(Math.random() * 20),
|
|
stress: Math.floor(Math.random() * 100),
|
|
step: Math.floor(Math.random() * 10000),
|
|
distance: Math.random() * 10,
|
|
calorie: Math.random() * 500,
|
|
latitude: 39.9 + Math.random() * 0.1,
|
|
longitude: 116.4 + Math.random() * 0.1,
|
|
altitude: 50 + Math.random() * 100,
|
|
timestamp: formatDateTime(now)
|
|
};
|
|
}
|
|
|
|
// 发送单批数据
|
|
async function sendSingleBatch() {
|
|
const apiUrl = document.getElementById('apiUrl').value;
|
|
const batchSize = parseInt(document.getElementById('batchSize').value);
|
|
const deviceCount = parseInt(document.getElementById('deviceCount').value);
|
|
|
|
const batch = [];
|
|
for (let i = 0; i < batchSize; i++) {
|
|
const deviceIndex = (i % deviceCount) + 1;
|
|
batch.push(generateHealthData(deviceIndex));
|
|
}
|
|
|
|
logInfo(`📤 发送批次数据: ${batchSize}条`);
|
|
|
|
try {
|
|
const response = await fetch(`${apiUrl}/batch/upload_health_data`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(batch)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success || result.code === 200) {
|
|
stats.totalBatches++;
|
|
stats.totalRecords += batchSize;
|
|
stats.successCount++;
|
|
updateStats();
|
|
logSuccess(`✅ 上传成功: ${JSON.stringify(result.data || result.result)}`);
|
|
} else {
|
|
stats.errorCount++;
|
|
updateStats();
|
|
logError(`❌ 上传失败: ${result.message || result.msg}`);
|
|
}
|
|
} catch (error) {
|
|
stats.errorCount++;
|
|
updateStats();
|
|
logError(`❌ 网络错误: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// 开始模拟
|
|
function startSimulation() {
|
|
const interval = parseInt(document.getElementById('interval').value);
|
|
|
|
if (simulationInterval) {
|
|
logWarning('⚠️ 模拟已在运行中');
|
|
return;
|
|
}
|
|
|
|
logInfo(`🚀 开始模拟,间隔: ${interval}ms`);
|
|
document.getElementById('startBtn').disabled = true;
|
|
document.getElementById('stopBtn').disabled = false;
|
|
document.getElementById('startBtn').classList.add('running');
|
|
|
|
// 立即发送一次
|
|
sendSingleBatch();
|
|
|
|
// 设置定时发送
|
|
simulationInterval = setInterval(sendSingleBatch, interval);
|
|
}
|
|
|
|
// 停止模拟
|
|
function stopSimulation() {
|
|
if (simulationInterval) {
|
|
clearInterval(simulationInterval);
|
|
simulationInterval = null;
|
|
logInfo('⏹️ 模拟已停止');
|
|
document.getElementById('startBtn').disabled = false;
|
|
document.getElementById('stopBtn').disabled = true;
|
|
document.getElementById('startBtn').classList.remove('running');
|
|
}
|
|
}
|
|
|
|
// 更新统计信息
|
|
function updateStats() {
|
|
document.getElementById('totalBatches').textContent = stats.totalBatches;
|
|
document.getElementById('totalRecords').textContent = stats.totalRecords;
|
|
document.getElementById('successCount').textContent = stats.successCount;
|
|
document.getElementById('errorCount').textContent = stats.errorCount;
|
|
}
|
|
|
|
// 日志函数
|
|
function addLog(message, type = 'info') {
|
|
const logContainer = document.getElementById('logContainer');
|
|
const entry = document.createElement('div');
|
|
entry.className = `log-entry ${type}`;
|
|
const time = new Date().toLocaleTimeString('zh-CN');
|
|
entry.textContent = `[${time}] ${message}`;
|
|
logContainer.appendChild(entry);
|
|
logContainer.scrollTop = logContainer.scrollHeight;
|
|
|
|
// 限制日志数量
|
|
if (logContainer.children.length > 100) {
|
|
logContainer.removeChild(logContainer.firstChild);
|
|
}
|
|
}
|
|
|
|
function logInfo(message) {
|
|
addLog(message, 'info');
|
|
}
|
|
|
|
function logSuccess(message) {
|
|
addLog(message, 'success');
|
|
}
|
|
|
|
function logError(message) {
|
|
addLog(message, 'error');
|
|
}
|
|
|
|
function logWarning(message) {
|
|
addLog(message, 'warning');
|
|
}
|
|
|
|
// 页面关闭时停止模拟
|
|
window.addEventListener('beforeunload', () => {
|
|
stopSimulation();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|