842 lines
27 KiB
JavaScript
842 lines
27 KiB
JavaScript
/**
|
||
* 神东煤炭综采工作面数据管理与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 = '<p class="text-muted">暂无工作面数据</p>';
|
||
return;
|
||
}
|
||
|
||
let html = `
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>工作面名称</th>
|
||
<th>工作面代码</th>
|
||
<th>采集次数</th>
|
||
<th>监测点数</th>
|
||
<th>最近采集时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
unitStats.forEach(unit => {
|
||
html += `
|
||
<tr onclick="switchToMonitor('${unit.unit_code}')" style="cursor:pointer;">
|
||
<td>${unit.unit_name || '-'}</td>
|
||
<td><code>${unit.unit_code}</code></td>
|
||
<td>${unit.collect_count || 0}</td>
|
||
<td>${unit.tag_count || 0}</td>
|
||
<td>${unit.last_time || '暂无'}</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += '</tbody></table>';
|
||
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 = '<option value="">请选择工作面</option>';
|
||
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 = '<p class="text-muted">暂无工作面数据</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = units.map(unit => `
|
||
<div class="unit-item" onclick="switchToMonitor('${unit.unit_code}')">
|
||
<div>
|
||
<div class="unit-name">${unit.unit_name}</div>
|
||
<span class="unit-code">${unit.unit_code}</span>
|
||
</div>
|
||
<div class="unit-status">
|
||
<span class="status-dot ${unit.has_data ? '' : 'empty'}"></span>
|
||
<span>${unit.has_data ? '有数据' : '暂无数据'}</span>
|
||
</div>
|
||
</div>
|
||
`).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 = '<p>暂无采集记录</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = results.map(item => `
|
||
<div class="collect-status-item">
|
||
<span>${item.unit_name}</span>
|
||
<span>
|
||
${item.stats ? `
|
||
采集${item.stats.today_count}次 · ${item.stats.last_time || '未知'}
|
||
` : '暂无数据'}
|
||
</span>
|
||
</div>
|
||
`).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 = '<option value="">默认模型</option>';
|
||
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 = '<p>请选择工作面查看实时数据</p>';
|
||
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 = '<p>暂无数据</p>';
|
||
return;
|
||
}
|
||
|
||
// 按设备分类
|
||
const categories = categorizeData(data);
|
||
|
||
let html = '';
|
||
for (const [cat, items] of Object.entries(categories)) {
|
||
html += `
|
||
<h3 style="margin: 16px 0 8px; color: var(--gray-700);">${cat}</h3>
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>参数名称</th>
|
||
<th>数值</th>
|
||
<th>单位</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
items.forEach(item => {
|
||
const tag = item.tag_short_name || item.tag_name || '';
|
||
// 优先使用后端返回的中文名称,否则显示英文标签
|
||
const displayName = item.chinese_name && item.chinese_name !== tag
|
||
? `${item.chinese_name} <small style="color: #999;">(${tag})</small>`
|
||
: 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 ?
|
||
'<span class="badge badge-success">正常</span>' :
|
||
'<span class="badge badge-warning">异常</span>';
|
||
|
||
html += `
|
||
<tr>
|
||
<td>${displayName}</td>
|
||
<td class="${valueClass}">${value}</td>
|
||
<td>${unit}</td>
|
||
<td>${statusBadge}</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += '</tbody></table>';
|
||
}
|
||
|
||
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 = '<p><span class="loading"></span> AI正在分析中,请稍候...</p>';
|
||
|
||
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 = `<p class="text-muted">分析失败: ${error.message}</p>`;
|
||
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 = '<p><span class="loading"></span> AI正在分析趋势,请稍候...</p>';
|
||
|
||
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 = `<p class="text-muted">分析失败: ${error.message}</p>`;
|
||
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 = `
|
||
<div class="result-meta" style="margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--gray-300);">
|
||
<span class="badge badge-success">分析成功</span>
|
||
${data.model_name ? `<span class="badge badge-info" style="margin-left: 8px;">${data.model_name}</span>` : ''}
|
||
</div>
|
||
<div class="result-content">${formatMarkdown(data.content)}</div>
|
||
`;
|
||
} else {
|
||
resultEl.innerHTML = `
|
||
<div class="result-meta">
|
||
<span class="badge badge-danger">分析失败</span>
|
||
</div>
|
||
<p style="margin-top: 12px;">${data.content || '未知错误'}</p>
|
||
`;
|
||
if (summaryCard) summaryCard.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function formatMarkdown(text) {
|
||
// 简单Markdown格式化
|
||
let html = text
|
||
// 标题
|
||
.replace(/^#### (.+)$/gm, '<h4>$1</h4>')
|
||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||
// 加粗
|
||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||
// 斜体
|
||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||
// 无序列表
|
||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||
// 分割线
|
||
.replace(/^---$/gm, '<hr>')
|
||
// 换行
|
||
.replace(/\n/g, '<br>');
|
||
|
||
// 包裹连续的li
|
||
html = html.replace(/(<li>.*?<\/li>(?:<br>)?)+/g, function(match) {
|
||
return '<ul>' + match.replace(/<br>/g, '') + '</ul>';
|
||
});
|
||
|
||
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 = '<p><span class="loading"></span> 正在生成报告...</p>';
|
||
|
||
try {
|
||
const data = await apiRequest('/report/word', {
|
||
method: 'POST',
|
||
body: {
|
||
unit_code: unitCode,
|
||
type: reportType
|
||
}
|
||
});
|
||
|
||
const fileInfo = data.data;
|
||
|
||
previewEl.innerHTML = `
|
||
<div style="text-align: center; padding: 40px;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">📄</div>
|
||
<h3>报告生成成功</h3>
|
||
<p style="color: var(--gray-700); margin: 12px 0;">文件名: ${fileInfo.filename}</p>
|
||
<a href="/api/report/download/${fileInfo.filename}" class="btn btn-primary" download>
|
||
下载报告
|
||
</a>
|
||
</div>
|
||
`;
|
||
|
||
showToast('报告生成成功');
|
||
|
||
} catch (error) {
|
||
previewEl.innerHTML = `<p class="text-muted">报告生成失败: ${error.message}</p>`;
|
||
showToast('报告生成失败', 'error');
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '生成报告';
|
||
}
|
||
} |