Files
ljwx-health/DATA_FLOW_SIMULATOR.html
brunoandClaude Sonnet 4.5 ce06f0a916
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
feat: 前端OpenAPI代码生成与文档标准化 v2.0.3
主要变更:

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]>
2026-01-04 22:25:35 +08:00

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>