Trae Assistant
Enhance functionality and prepare for HF Spaces
c5c8950
<!DOCTYPE html>
<html lang="zh-CN" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能预测性维护仿真系统 | Predictive Maintenance Sim</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
gray: {
900: '#111827',
800: '#1f2937',
700: '#374151',
},
blue: {
600: '#2563eb',
500: '#3b82f6',
}
}
}
}
}
</script>
<!-- Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- ECharts -->
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
[v-cloak] { display: none; }
.glass-panel {
background: rgba(31, 41, 55, 0.7);
backdrop-filter: blur(10px);
border: 1px solid rgba(75, 85, 99, 0.4);
}
.sensor-value {
font-family: 'Courier New', monospace;
font-weight: bold;
}
</style>
</head>
<body class="bg-gray-900 text-gray-100 min-h-screen font-sans">
<div id="app" v-cloak class="p-6 max-w-[1600px] mx-auto">
<!-- Header -->
<header class="mb-8 flex justify-between items-center border-b border-gray-700 pb-4">
<div>
<h1 class="text-3xl font-bold text-blue-500">
<i class="fa-solid fa-industry mr-2"></i>智能预测性维护仿真系统
</h1>
<p class="text-gray-400 mt-1">Industrial IoT Predictive Maintenance Dashboard (IIoT)</p>
</div>
<div class="flex gap-4">
<button @click="exportData" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg border border-gray-600 transition flex items-center gap-2">
<i class="fa-solid fa-download"></i> 导出日志
</button>
<div class="px-4 py-2 bg-gray-800 rounded-lg border border-gray-700">
<span class="text-gray-400">System Time:</span>
<span class="text-green-400 font-mono ml-2">[[ timestamp ]]</span>
</div>
</div>
</header>
<!-- KPI Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="glass-panel p-6 rounded-xl relative overflow-hidden">
<div class="absolute right-4 top-4 text-blue-500/20 text-6xl"><i class="fa-solid fa-heart-pulse"></i></div>
<h3 class="text-gray-400 text-sm uppercase tracking-wider">车间平均健康度</h3>
<div class="text-3xl font-bold mt-2" :class="avgHealth > 80 ? 'text-green-400' : 'text-yellow-400'">
[[ avgHealth ]]%
</div>
</div>
<div class="glass-panel p-6 rounded-xl relative overflow-hidden">
<div class="absolute right-4 top-4 text-red-500/20 text-6xl"><i class="fa-solid fa-triangle-exclamation"></i></div>
<h3 class="text-gray-400 text-sm uppercase tracking-wider">风险警报</h3>
<div class="text-3xl font-bold mt-2 text-white">
[[ riskLevel ]]
</div>
</div>
<div class="glass-panel p-6 rounded-xl relative overflow-hidden">
<div class="absolute right-4 top-4 text-green-500/20 text-6xl"><i class="fa-solid fa-money-bill-wave"></i></div>
<h3 class="text-gray-400 text-sm uppercase tracking-wider">累计维护成本</h3>
<div class="text-3xl font-bold mt-2 text-green-400">
$[[ totalCost ]]
</div>
</div>
<div class="glass-panel p-6 rounded-xl relative overflow-hidden">
<div class="absolute right-4 top-4 text-purple-500/20 text-6xl"><i class="fa-solid fa-clock"></i></div>
<h3 class="text-gray-400 text-sm uppercase tracking-wider">总运行小时数</h3>
<div class="text-3xl font-bold mt-2 text-purple-400">
[[ totalRunHours ]] h
</div>
</div>
</div>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Equipment List & Controls -->
<div class="lg:col-span-2 space-y-6">
<!-- Fleet Table -->
<div class="glass-panel rounded-xl overflow-hidden">
<div class="p-4 bg-gray-800/50 border-b border-gray-700 flex justify-between items-center">
<h2 class="text-xl font-semibold"><i class="fa-solid fa-list-ul mr-2 text-blue-500"></i>设备状态监控</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead class="bg-gray-800 text-gray-400 text-sm">
<tr>
<th class="p-4">ID / 名称</th>
<th class="p-4">类型</th>
<th class="p-4">状态</th>
<th class="p-4 text-right">健康度</th>
<th class="p-4 text-right">温度 (°C)</th>
<th class="p-4 text-right">振动 (mm/s)</th>
<th class="p-4 text-right">RUL预测 (h)</th>
<th class="p-4 text-center">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700">
<tr v-for="eq in fleet" :key="eq.id"
class="hover:bg-gray-800/50 transition cursor-pointer"
:class="{'bg-blue-900/20': selectedEqId === eq.id}"
@click="selectEquipment(eq)">
<td class="p-4">
<div class="font-bold text-white">[[ eq.id ]]</div>
<div class="text-xs text-gray-500">[[ eq.name ]]</div>
</td>
<td class="p-4 text-gray-300">[[ eq.type ]]</td>
<td class="p-4">
<span class="px-2 py-1 rounded text-xs font-bold"
:class="{
'bg-green-900 text-green-400': eq.status === 'Running',
'bg-yellow-900 text-yellow-400': eq.status === 'Warning',
'bg-red-900 text-red-400': eq.status === 'Critical',
'bg-gray-700 text-gray-300': eq.status === 'Stopped'
}">
[[ eq.status ]]
</span>
</td>
<td class="p-4 text-right">
<div class="flex items-center justify-end gap-2">
<div class="w-16 bg-gray-700 h-2 rounded-full overflow-hidden">
<div class="h-full transition-all duration-500"
:class="eq.health < 50 ? 'bg-red-500' : (eq.health < 80 ? 'bg-yellow-500' : 'bg-green-500')"
:style="{width: eq.health + '%'}"></div>
</div>
<span class="font-mono text-sm">[[ eq.health ]]%</span>
</div>
</td>
<td class="p-4 text-right font-mono" :class="{'text-red-400': eq.sensors.temp > 70}">
[[ eq.sensors.temp ]]
</td>
<td class="p-4 text-right font-mono" :class="{'text-red-400': eq.sensors.vibration > 1.5}">
[[ eq.sensors.vibration ]]
</td>
<td class="p-4 text-right font-mono text-blue-300">
[[ eq.rul ]]
</td>
<td class="p-4 text-center">
<button @click.stop="performMaintenance(eq.id)"
class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded transition"
:disabled="eq.health > 95"
:class="{'opacity-50 cursor-not-allowed': eq.health > 95}">
维护
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Live Chart -->
<div class="glass-panel rounded-xl p-4 h-[400px]">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-200">
<i class="fa-solid fa-chart-line mr-2 text-blue-500"></i>
实时传感器遥测 ([[ selectedEq ? selectedEq.name : 'All' ]])
</h3>
<div class="flex gap-2 text-xs">
<span class="flex items-center gap-1"><div class="w-3 h-3 bg-red-500 rounded-full"></div> 温度</span>
<span class="flex items-center gap-1"><div class="w-3 h-3 bg-yellow-400 rounded-full"></div> 振动</span>
</div>
</div>
<div id="sensorChart" class="w-full h-[320px]"></div>
</div>
</div>
<!-- Side Panel: Analytics & Details -->
<div class="space-y-6">
<!-- RUL Gauge -->
<div class="glass-panel rounded-xl p-6 flex flex-col items-center justify-center">
<h3 class="text-gray-400 text-sm mb-4">剩余寿命预测 (RUL)</h3>
<div id="rulGauge" class="w-full h-[250px]"></div>
<p class="text-center text-xs text-gray-500 mt-2">Based on exponential decay model</p>
</div>
<!-- System Tools: Upload -->
<div class="glass-panel rounded-xl p-4">
<h3 class="text-lg font-semibold mb-3 border-b border-gray-700 pb-2">
<i class="fa-solid fa-screwdriver-wrench mr-2 text-blue-500"></i>系统工具
</h3>
<div class="space-y-3">
<div>
<label class="block text-xs text-gray-400 mb-1">固件/配置上传 (支持大文件)</label>
<div class="flex gap-2">
<input type="file" ref="fileInput" class="block w-full text-xs text-gray-400
file:mr-2 file:py-2 file:px-4
file:rounded-full file:border-0
file:text-xs file:font-semibold
file:bg-blue-900/30 file:text-blue-400
hover:file:bg-blue-900/50 cursor-pointer"
@change="handleFileSelect"
>
<button @click="uploadFile" :disabled="!selectedFile || uploadStatus === 'uploading'"
class="px-3 py-1 bg-blue-600 hover:bg-blue-500 text-white rounded transition disabled:opacity-50 text-xs whitespace-nowrap">
<i class="fa-solid fa-upload" :class="{'fa-spin': uploadStatus === 'uploading'}"></i>
</button>
</div>
<div v-if="uploadStatus" class="mt-2 text-xs" :class="{
'text-yellow-400': uploadStatus === 'uploading',
'text-green-400': uploadStatus === 'success',
'text-red-400': uploadStatus === 'error'
}">
[[ uploadMessage ]]
</div>
</div>
</div>
</div>
<!-- Event Log -->
<div class="glass-panel rounded-xl p-4 flex-1 h-[400px] overflow-hidden flex flex-col">
<h3 class="text-lg font-semibold mb-3 border-b border-gray-700 pb-2">
<i class="fa-solid fa-clipboard-list mr-2 text-blue-500"></i>系统日志
</h3>
<div class="overflow-y-auto space-y-2 pr-2 text-sm flex-1">
<div v-for="(log, i) in systemLogs" :key="i" class="p-2 rounded bg-gray-800/50 border border-gray-700 flex justify-between">
<span :class="log.type === 'alert' ? 'text-red-400' : 'text-gray-300'">
<i :class="log.icon"></i> [[ log.message ]]
</span>
<span class="text-gray-600 text-xs">[[ log.time ]]</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted, onUnmounted } = Vue;
createApp({
delimiters: ['[[', ']]'],
setup() {
const fleet = ref([]);
const timestamp = ref('--:--:--');
const riskLevel = ref('Low');
const totalCost = ref(0);
const selectedEqId = ref(null);
const systemLogs = ref([]);
// Upload logic
const fileInput = ref(null);
const selectedFile = ref(null);
const uploadStatus = ref(''); // '', 'uploading', 'success', 'error'
const uploadMessage = ref('');
const handleFileSelect = (event) => {
selectedFile.value = event.target.files[0];
uploadStatus.value = '';
uploadMessage.value = '';
};
const uploadFile = async () => {
if (!selectedFile.value) return;
const formData = new FormData();
formData.append('file', selectedFile.value);
uploadStatus.value = 'uploading';
uploadMessage.value = 'Uploading...';
try {
const res = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok && data.success) {
uploadStatus.value = 'success';
uploadMessage.value = data.message;
addLog(`文件上传成功: ${selectedFile.value.name}`, 'success');
selectedFile.value = null;
if (fileInput.value) fileInput.value.value = '';
} else {
throw new Error(data.message || 'Upload failed');
}
} catch (e) {
uploadStatus.value = 'error';
uploadMessage.value = `Error: ${e.message}`;
addLog(`文件上传失败: ${selectedFile.value ? selectedFile.value.name : ''}`, 'alert');
}
};
// Charts
let chartInstance = null;
let gaugeInstance = null;
const chartData = {
time: [],
temp: [],
vib: []
};
const selectedEq = computed(() => {
return fleet.value.find(e => e.id === selectedEqId.value) || fleet.value[0];
});
const avgHealth = computed(() => {
if (fleet.value.length === 0) return 0;
const sum = fleet.value.reduce((acc, curr) => acc + curr.health, 0);
return Math.round(sum / fleet.value.length);
});
const totalRunHours = computed(() => {
return fleet.value.reduce((acc, curr) => acc + curr.run_hours, 0).toFixed(1);
});
const addLog = (msg, type='info') => {
const icons = {
'info': 'fa-solid fa-info-circle',
'alert': 'fa-solid fa-triangle-exclamation',
'success': 'fa-solid fa-check-circle'
};
systemLogs.value.unshift({
message: msg,
type: type,
icon: icons[type],
time: new Date().toLocaleTimeString()
});
if (systemLogs.value.length > 20) systemLogs.value.pop();
};
const initCharts = () => {
chartInstance = echarts.init(document.getElementById('sensorChart'));
gaugeInstance = echarts.init(document.getElementById('rulGauge'));
const option = {
backgroundColor: 'transparent',
grid: { left: 40, right: 20, top: 20, bottom: 20 },
xAxis: {
type: 'category',
data: [],
axisLine: { lineStyle: { color: '#4B5563' } }
},
yAxis: [
{ type: 'value', name: 'Temp', min: 0, max: 100, axisLine: { lineStyle: { color: '#EF4444' } }, splitLine: { show: false } },
{ type: 'value', name: 'Vib', min: 0, max: 5, axisLine: { lineStyle: { color: '#FBBF24' } }, splitLine: { lineStyle: { color: '#374151', type: 'dashed' } } }
],
series: [
{ name: 'Temp', type: 'line', data: [], smooth: true, showSymbol: false, itemStyle: { color: '#EF4444' } },
{ name: 'Vib', type: 'line', yAxisIndex: 1, data: [], smooth: true, showSymbol: false, itemStyle: { color: '#FBBF24' } }
]
};
chartInstance.setOption(option);
const gaugeOption = {
series: [{
type: 'gauge',
startAngle: 180, endAngle: 0,
min: 0, max: 2000,
splitNumber: 5,
itemStyle: { color: '#3B82F6' },
progress: { show: true, width: 18 },
pointer: { show: false },
axisLine: { lineStyle: { width: 18 } },
axisTick: { show: false },
splitLine: { length: 15, lineStyle: { width: 2, color: '#999' } },
axisLabel: { distance: 25, color: '#999', fontSize: 10 },
detail: { valueAnimation: true, fontSize: 30, offsetCenter: [0, '30%'], color: 'auto', formatter: '{value} h' },
data: [{ value: 1000 }]
}]
};
gaugeInstance.setOption(gaugeOption);
};
const updateData = async () => {
try {
const res = await fetch('/api/status');
const data = await res.json();
// Smart merge to prevent UI flicker
if (fleet.value.length === 0) {
fleet.value = data.fleet;
selectedEqId.value = data.fleet[0].id;
} else {
data.fleet.forEach((newItem, idx) => {
Object.assign(fleet.value[idx], newItem);
});
}
timestamp.value = data.timestamp;
totalCost.value = data.metrics.total_maintenance_cost;
riskLevel.value = data.metrics.fleet_risk_level;
// Check for alerts
fleet.value.forEach(eq => {
if (eq.status === 'Critical' && Math.random() < 0.1) {
addLog(`${eq.name} 状态危急! 建议立即停机维护`, 'alert');
}
});
// Update Charts
if (selectedEq.value) {
const now = new Date().toLocaleTimeString();
chartData.time.push(now);
chartData.temp.push(selectedEq.value.sensors.temp);
chartData.vib.push(selectedEq.value.sensors.vibration);
if (chartData.time.length > 20) {
chartData.time.shift();
chartData.temp.shift();
chartData.vib.shift();
}
chartInstance.setOption({
xAxis: { data: chartData.time },
series: [
{ data: chartData.temp },
{ data: chartData.vib }
]
});
gaugeInstance.setOption({
series: [{ data: [{ value: selectedEq.value.rul }] }]
});
}
} catch (e) {
console.error("Fetch error", e);
}
};
const selectEquipment = (eq) => {
selectedEqId.value = eq.id;
// Reset chart data for visual cleanliness
chartData.time = [];
chartData.temp = [];
chartData.vib = [];
};
const performMaintenance = async (id) => {
try {
const res = await fetch('/api/maintain', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id})
});
const data = await res.json();
if (data.success) {
addLog(`维护完成: ${id}`, 'success');
updateData(); // Immediate refresh
}
} catch (e) {
addLog('维护请求失败', 'alert');
}
};
const exportData = () => {
window.open('/api/export', '_blank');
};
onMounted(() => {
initCharts();
updateData();
setInterval(updateData, 1000); // 1s polling for real-time feel
window.addEventListener('resize', () => {
chartInstance && chartInstance.resize();
gaugeInstance && gaugeInstance.resize();
});
});
return {
fleet, timestamp, riskLevel, totalCost, avgHealth, totalRunHours,
selectedEqId, selectedEq, systemLogs,
selectEquipment, performMaintenance, exportData,
fileInput, selectedFile, uploadStatus, uploadMessage, handleFileSelect, uploadFile
};
}
}).mount('#app');
</script>
</body>
</html>