/**
* 神东煤炭综采工作面数据管理与AI分析系统
* 前端 JavaScript 逻辑
*/
// ==================== 全局变量 ====================
let units = [];
let aiModels = [];
let realtimeData = [];
// ==================== 初始化 ====================
document.addEventListener('DOMContentLoaded', function() {
initNavigation();
updateTime();
setInterval(updateTime, 1000);
loadUnits();
loadAIModels();
loadAllStats();
initForms();
initTimeRange();
});
// ==================== 导航 ====================
function initNavigation() {
const links = document.querySelectorAll('.navbar-menu a');
links.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const page = this.getAttribute('data-page');
switchPage(page);
});
});
}
function switchPage(pageName) {
// 更新菜单激活状态
document.querySelectorAll('.navbar-menu a').forEach(a => a.classList.remove('active'));
const activeLink = document.querySelector(`.navbar-menu a[data-page="${pageName}"]`);
if (activeLink) activeLink.classList.add('active');
// 切换页面
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
const page = document.getElementById(pageName);
if (page) page.classList.add('active');
}
// ==================== 时间更新 ====================
function updateTime() {
const now = new Date();
const timeStr = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
const el = document.getElementById('current-time');
if (el) el.textContent = timeStr;
}
// ==================== API 请求 ====================
async function apiRequest(endpoint, options = {}) {
try {
// AI分析请求设置更长的超时时间(10分钟)
const isAiRequest = endpoint.startsWith('/ai/');
const timeoutMs = isAiRequest ? (options.timeout || 600000) : (options.timeout || 30000);
// 创建带超时的AbortController
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(`/api${endpoint}`, {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...options.headers
},
body: options.body ? JSON.stringify(options.body) : undefined,
signal: controller.signal
});
clearTimeout(timeoutId);
const data = await response.json();
if (!response.ok || !data.success) {
// 优先使用 error 字段,其次使用 data.content(AI分析错误),最后使用默认提示
let errorMsg = data.error || '请求失败';
if (data.data && data.data.status === 'error' && data.data.content) {
errorMsg = data.data.content;
}
throw new Error(errorMsg);
}
return data;
} catch (error) {
console.error('API请求错误:', error);
if (error.name === 'AbortError') {
throw new Error(`请求超时(等待时间过长),请检查网络连接或稍后重试`);
}
throw error;
}
}
// ==================== Toast 提示 ====================
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// ==================== 加载总体统计 ====================
async function loadAllStats() {
try {
const data = await apiRequest('/stats/all');
const stats = data.data || {};
// 更新顶部统计卡片
const unitCountEl = document.getElementById('unit-count');
const tagCountEl = document.getElementById('tag-count');
const totalCollectsEl = document.getElementById('total-collects');
const todayCollectsEl = document.getElementById('today-collects');
const dataTotalEl = document.getElementById('data-total');
if (unitCountEl) unitCountEl.textContent = stats.unit_count || 0;
if (tagCountEl) tagCountEl.textContent = stats.tag_count || 0;
if (totalCollectsEl) totalCollectsEl.textContent = stats.total_collects || 0;
if (todayCollectsEl) todayCollectsEl.textContent = stats.today_collects || 0;
if (dataTotalEl) dataTotalEl.textContent = formatNumber(stats.data_total || 0);
// 渲染工作面统计表格
renderUnitStatsTable(stats.unit_stats || []);
} catch (error) {
console.error('加载总体统计失败:', error);
}
}
function formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
function renderUnitStatsTable(unitStats) {
const container = document.getElementById('unit-stats-table');
if (!container) return;
if (!unitStats || unitStats.length === 0) {
container.innerHTML = '
暂无工作面数据
';
return;
}
let html = `
工作面名称
工作面代码
采集次数
监测点数
最近采集时间
`;
unitStats.forEach(unit => {
html += `
${unit.unit_name || '-'}
${unit.unit_code}
${unit.collect_count || 0}
${unit.tag_count || 0}
${unit.last_time || '暂无'}
`;
});
html += '
';
container.innerHTML = html;
}
// ==================== 加载工作面列表 ====================
async function loadUnits() {
try {
const data = await apiRequest('/units');
units = data.data || [];
// 更新下拉框
updateUnitSelects();
// 更新列表
renderUnitList();
// 加载采集状态
loadCollectStatus();
// 加载总体统计
loadAllStats();
} catch (error) {
showToast('加载工作面列表失败: ' + error.message, 'error');
}
}
function updateUnitSelects() {
const selects = ['collect-unit', 'monitor-unit', 'fault-unit', 'trend-unit', 'report-unit'];
selects.forEach(id => {
const select = document.getElementById(id);
if (select) {
const current = select.value;
select.innerHTML = '请选择工作面 ';
units.forEach(unit => {
const option = document.createElement('option');
option.value = unit.unit_code;
option.textContent = `${unit.unit_name} (${unit.unit_code})`;
select.appendChild(option);
});
if (current) select.value = current;
}
});
}
function renderUnitList() {
const container = document.getElementById('unit-list');
if (!container) return;
if (units.length === 0) {
container.innerHTML = '暂无工作面数据
';
return;
}
container.innerHTML = units.map(unit => `
${unit.unit_name}
${unit.unit_code}
${unit.has_data ? '有数据' : '暂无数据'}
`).join('');
}
function switchToMonitor(unitCode) {
const monitorSelect = document.getElementById('monitor-unit');
if (monitorSelect) {
monitorSelect.value = unitCode;
switchPage('monitor');
loadRealtimeData();
}
}
// ==================== 加载采集状态 ====================
async function loadCollectStatus() {
const container = document.getElementById('collect-status');
if (!container) return;
try {
const promises = units.map(async (unit) => {
try {
const data = await apiRequest(`/stats/${unit.unit_code}`);
return {
...unit,
stats: data.data
};
} catch {
return { ...unit, stats: null };
}
});
const results = await Promise.all(promises);
if (results.length === 0) {
container.innerHTML = '暂无采集记录
';
return;
}
container.innerHTML = results.map(item => `
${item.unit_name}
${item.stats ? `
采集${item.stats.today_count}次 · ${item.stats.last_time || '未知'}
` : '暂无数据'}
`).join('');
} catch (error) {
console.error('加载采集状态失败:', error);
}
}
// ==================== 加载AI模型列表 ====================
async function loadAIModels() {
try {
const data = await apiRequest('/ai/models');
aiModels = data.data || [];
const selects = ['fault-model', 'trend-model'];
selects.forEach(id => {
const select = document.getElementById(id);
if (select) {
select.innerHTML = '默认模型 ';
aiModels.forEach(model => {
const option = document.createElement('option');
option.value = model.key;
option.textContent = `${model.name}${model.available ? '' : ' (未配置)'}`;
if (!model.available) option.disabled = true;
select.appendChild(option);
});
}
});
} catch (error) {
console.error('加载AI模型列表失败:', error);
}
}
// ==================== 表单初始化 ====================
function initForms() {
// 数据采集表单
const collectForm = document.getElementById('collect-form');
if (collectForm) {
collectForm.addEventListener('submit', async function(e) {
e.preventDefault();
await startCollect();
});
}
// 故障诊断表单
const faultForm = document.getElementById('fault-form');
if (faultForm) {
faultForm.addEventListener('submit', async function(e) {
e.preventDefault();
await startFaultAnalysis();
});
}
// 趋势分析表单
const trendForm = document.getElementById('trend-form');
if (trendForm) {
trendForm.addEventListener('submit', async function(e) {
e.preventDefault();
await startTrendAnalysis();
});
}
// 报告生成表单
const reportForm = document.getElementById('report-form');
if (reportForm) {
reportForm.addEventListener('submit', async function(e) {
e.preventDefault();
await generateReport();
});
}
}
// ==================== 时间范围初始化 ====================
function initTimeRange() {
const now = new Date();
const oneHourAgo = new Date(now.getTime() - 1 * 60 * 60 * 1000);
const formatDateTime = (date) => {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const h = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
return `${y}-${m}-${d}T${h}:${min}`;
};
const startEl = document.getElementById('trend-start');
const endEl = document.getElementById('trend-end');
if (startEl) startEl.value = formatDateTime(oneHourAgo);
if (endEl) endEl.value = formatDateTime(now);
}
// ==================== 数据采集 ====================
async function startCollect() {
const unitCode = document.getElementById('collect-unit').value;
const deviceType = document.getElementById('collect-device').value;
if (!unitCode) {
showToast('请选择工作面', 'warning');
return;
}
const btn = document.getElementById('collect-btn');
const btnText = btn.querySelector('.btn-text');
const btnLoading = btn.querySelector('.btn-loading');
btn.disabled = true;
btnText.style.display = 'none';
btnLoading.style.display = 'inline';
addLog('开始采集数据...', 'info');
try {
const body = { unit_code: unitCode };
if (deviceType) body.device_type = deviceType;
const data = await apiRequest('/collect', {
method: 'POST',
body: body
});
const result = data.data;
addLog(`采集完成: ${result.message || '成功'}`, 'success');
if (result.total_collected) {
addLog(`共采集 ${result.total_collected} 条数据`, 'success');
}
showToast('数据采集成功');
// 刷新状态
loadCollectStatus();
updateUnitDataStatus();
} catch (error) {
addLog(`采集失败: ${error.message}`, 'error');
showToast('数据采集失败: ' + error.message, 'error');
} finally {
btn.disabled = false;
btnText.style.display = 'inline';
btnLoading.style.display = 'none';
}
}
async function collectAll() {
const btn = document.getElementById('collect-all-btn');
btn.disabled = true;
btn.textContent = '采集中...';
addLog('开始全量采集...', 'info');
try {
const data = await apiRequest('/collect/all', {
method: 'POST'
});
addLog('全量采集完成', 'success');
showToast('全量采集完成');
loadCollectStatus();
updateUnitDataStatus();
} catch (error) {
addLog(`全量采集失败: ${error.message}`, 'error');
showToast('全量采集失败', 'error');
} finally {
btn.disabled = false;
btn.textContent = '全量采集';
}
}
function updateUnitDataStatus() {
// 重新加载units以更新状态
loadUnits();
}
// ==================== 日志 ====================
function addLog(message, type = 'info') {
const container = document.getElementById('collect-log');
if (!container) return;
const time = new Date().toLocaleTimeString('zh-CN');
const logEl = document.createElement('p');
logEl.className = `log-${type}`;
logEl.textContent = `[${time}] ${message}`;
container.appendChild(logEl);
container.scrollTop = container.scrollHeight;
}
// ==================== 实时数据 ====================
async function loadRealtimeData() {
const unitCode = document.getElementById('monitor-unit').value;
if (!unitCode) {
document.getElementById('realtime-data').innerHTML = '请选择工作面查看实时数据
';
return;
}
try {
const data = await apiRequest(`/realtime/${unitCode}`);
realtimeData = data.data || [];
// 更新时间
const timeEl = document.getElementById('monitor-time');
if (timeEl && data.snapshot_time) {
timeEl.textContent = `数据时间: ${data.snapshot_time}`;
}
// 更新统计
updateStats(realtimeData);
// 渲染表格
renderRealtimeTable(realtimeData);
} catch (error) {
showToast('加载实时数据失败: ' + error.message, 'error');
}
}
function updateStats(data) {
// 更新首页的监测点总数(仅当在首页时才有意义)
const tagCountEl = document.getElementById('tag-count');
if (tagCountEl) {
tagCountEl.textContent = data.length;
}
}
function renderRealtimeTable(data) {
const container = document.getElementById('realtime-data');
if (!data || data.length === 0) {
container.innerHTML = '暂无数据
';
return;
}
// 按设备分类
const categories = categorizeData(data);
let html = '';
for (const [cat, items] of Object.entries(categories)) {
html += `
${cat}
参数名称
数值
单位
状态
`;
items.forEach(item => {
const tag = item.tag_short_name || item.tag_name || '';
// 优先使用后端返回的中文名称,否则显示英文标签
const displayName = item.chinese_name && item.chinese_name !== tag
? `${item.chinese_name} (${tag}) `
: tag;
const value = item.tag_value != null ? item.tag_value :
(item.tag_value_bool != null ? (item.tag_value_bool ? 'ON' : 'OFF') : 'N/A');
const unit = item.tag_unit || '';
const quality = item.quality || 0;
const valueClass = quality === 0 ? 'value-normal' : 'value-warning';
const statusBadge = quality === 0 ?
'正常 ' :
'异常 ';
html += `
${displayName}
${value}
${unit}
${statusBadge}
`;
});
html += '
';
}
container.innerHTML = html;
}
function categorizeData(data) {
const categories = {};
data.forEach(item => {
const tag = item.tag_short_name || item.tag_name || '';
let cat = '其他';
if (tag.includes('Shearer') || tag.includes('shearer')) {
cat = '采煤机';
} else if (tag.includes('AFC') || tag.includes('LOA') || tag.includes('CRU')) {
cat = '三机';
} else if (tag.includes('Emulsion') || tag.includes('Spray')) {
cat = '泵站';
} else if (tag.includes('Loop')) {
cat = '开关';
}
if (!categories[cat]) categories[cat] = [];
categories[cat].push(item);
});
return categories;
}
function filterRealtimeData() {
const filter = document.getElementById('monitor-filter').value;
const search = document.getElementById('monitor-search').value.toLowerCase();
let filtered = realtimeData;
if (filter) {
filtered = filtered.filter(item => {
const tag = item.tag_short_name || item.tag_name || '';
const chinese = (item.chinese_name || '').toLowerCase();
return tag.includes(filter) || chinese.includes(filter);
});
}
if (search) {
filtered = filtered.filter(item => {
const tag = (item.tag_short_name || item.tag_name || '').toLowerCase();
const chinese = (item.chinese_name || '').toLowerCase();
return tag.includes(search) || chinese.includes(search);
});
}
renderRealtimeTable(filtered);
}
// ==================== AI分析 ====================
async function startFaultAnalysis() {
const unitCode = document.getElementById('fault-unit').value;
const modelKey = document.getElementById('fault-model').value;
if (!unitCode) {
showToast('请选择工作面', 'warning');
return;
}
const btn = document.getElementById('fault-btn');
btn.disabled = true;
btn.textContent = '分析中...';
const resultEl = document.getElementById('analysis-result');
resultEl.innerHTML = ' AI正在分析中,请稍候...
';
try {
const body = { unit_code: unitCode };
if (modelKey) body.model_key = modelKey;
const data = await apiRequest('/ai/fault', {
method: 'POST',
body: body
});
displayAnalysisResult(data.data);
showToast('故障诊断完成');
} catch (error) {
resultEl.innerHTML = `分析失败: ${error.message}
`;
showToast('故障诊断失败', 'error');
} finally {
btn.disabled = false;
btn.textContent = '开始诊断';
}
}
async function startTrendAnalysis() {
const unitCode = document.getElementById('trend-unit').value;
const startTime = document.getElementById('trend-start').value;
const endTime = document.getElementById('trend-end').value;
const modelKey = document.getElementById('trend-model').value;
if (!unitCode || !startTime || !endTime) {
showToast('请填写完整信息', 'warning');
return;
}
const btn = document.getElementById('trend-btn');
btn.disabled = true;
btn.textContent = '分析中...';
const resultEl = document.getElementById('analysis-result');
resultEl.innerHTML = ' AI正在分析趋势,请稍候...
';
try {
const body = {
unit_code: unitCode,
start_time: startTime,
end_time: endTime
};
if (modelKey) body.model_key = modelKey;
const data = await apiRequest('/ai/trend', {
method: 'POST',
body: body
});
displayAnalysisResult(data.data);
showToast('趋势分析完成');
} catch (error) {
resultEl.innerHTML = `分析失败: ${error.message}
`;
showToast('趋势分析失败', 'error');
} finally {
btn.disabled = false;
btn.textContent = '开始分析';
}
}
function displayAnalysisResult(data) {
const resultEl = document.getElementById('analysis-result');
const summaryCard = document.getElementById('data-summary-card');
const summaryContent = document.getElementById('data-summary-content');
const summaryDataCount = document.getElementById('summary-data-count');
// 显示数据摘要
if (data.data_summary) {
if (summaryCard) summaryCard.style.display = 'block';
if (summaryDataCount) {
const count = data.data_count || 0;
summaryDataCount.textContent = `共 ${count} 个监测点`;
}
if (summaryContent) {
summaryContent.innerHTML = formatMarkdown(data.data_summary);
}
} else {
if (summaryCard) summaryCard.style.display = 'none';
}
if (data.status === 'success') {
resultEl.innerHTML = `
分析成功
${data.model_name ? `${data.model_name} ` : ''}
${formatMarkdown(data.content)}
`;
} else {
resultEl.innerHTML = `
分析失败
${data.content || '未知错误'}
`;
if (summaryCard) summaryCard.style.display = 'none';
}
}
function formatMarkdown(text) {
// 简单Markdown格式化
let html = text
// 标题
.replace(/^#### (.+)$/gm, '$1 ')
.replace(/^### (.+)$/gm, '$1 ')
.replace(/^## (.+)$/gm, '$1 ')
.replace(/^# (.+)$/gm, '$1 ')
// 加粗
.replace(/\*\*(.+?)\*\*/g, '$1 ')
// 斜体
.replace(/\*(.+?)\*/g, '$1 ')
// 无序列表
.replace(/^- (.+)$/gm, '$1 ')
// 分割线
.replace(/^---$/gm, ' ')
// 换行
.replace(/\n/g, ' ');
// 包裹连续的li
html = html.replace(/(.*?<\/li>(?: )?)+/g, function(match) {
return '' + match.replace(/ /g, '') + ' ';
});
return html;
}
function copyResult() {
const resultEl = document.getElementById('analysis-result');
const text = resultEl.innerText;
navigator.clipboard.writeText(text).then(() => {
showToast('已复制到剪贴板');
}).catch(() => {
showToast('复制失败', 'error');
});
}
// ==================== 报告生成 ====================
async function generateReport() {
const unitCode = document.getElementById('report-unit').value;
const reportType = document.getElementById('report-type').value;
if (!unitCode) {
showToast('请选择工作面', 'warning');
return;
}
const btn = document.getElementById('report-btn');
btn.disabled = true;
btn.textContent = '生成中...';
const previewEl = document.getElementById('report-preview');
previewEl.innerHTML = ' 正在生成报告...
';
try {
const data = await apiRequest('/report/word', {
method: 'POST',
body: {
unit_code: unitCode,
type: reportType
}
});
const fileInfo = data.data;
previewEl.innerHTML = `
📄
报告生成成功
文件名: ${fileInfo.filename}
下载报告
`;
showToast('报告生成成功');
} catch (error) {
previewEl.innerHTML = `报告生成失败: ${error.message}
`;
showToast('报告生成失败', 'error');
} finally {
btn.disabled = false;
btn.textContent = '生成报告';
}
}