feat: 优化数据分析页面,添加Excel工单导入功能
- 优化数据分析页面,添加可定制的图表功能 - 支持多种图表类型:折线图、柱状图、饼图、环形图、雷达图、极坐标图 - 添加图表定制功能:时间范围选择、数据维度选择 - 实现Excel工单导入功能,支持详情.xlsx文件 - 添加工单编辑功能,包括前端UI和后端API - 修复WebSocket连接错误,处理invalid Connection header问题 - 简化预警管理参数,改为卡片式选择 - 实现Agent主动调用,无需人工干预 - 改进知识库导入,结合累计工单内容与大模型输出
This commit is contained in:
@@ -1198,9 +1198,14 @@ class TSPDashboard {
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="dashboard.updateWorkOrder(${workorder.id})">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-outline-info" onclick="dashboard.viewWorkOrderDetails(${workorder.id})" title="查看详情">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="dashboard.updateWorkOrder(${workorder.id})" title="编辑">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1262,17 +1267,748 @@ class TSPDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
async viewWorkOrderDetails(workorderId) {
|
||||
try {
|
||||
const response = await fetch(`/api/workorders/${workorderId}`);
|
||||
const workorder = await response.json();
|
||||
|
||||
if (workorder.error) {
|
||||
this.showNotification('获取工单详情失败', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.showWorkOrderDetailsModal(workorder);
|
||||
} catch (error) {
|
||||
console.error('获取工单详情失败:', error);
|
||||
this.showNotification('获取工单详情失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showWorkOrderDetailsModal(workorder) {
|
||||
// 创建模态框HTML
|
||||
const modalHtml = `
|
||||
<div class="modal fade" id="workOrderDetailsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">工单详情 - ${workorder.order_id || workorder.id}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<h6>基本信息</h6>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>工单号:</strong></td>
|
||||
<td>${workorder.order_id || workorder.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>标题:</strong></td>
|
||||
<td>${workorder.title}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>分类:</strong></td>
|
||||
<td>${workorder.category}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>优先级:</strong></td>
|
||||
<td><span class="badge bg-${this.getPriorityColor(workorder.priority)}">${this.getPriorityText(workorder.priority)}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>状态:</strong></td>
|
||||
<td><span class="badge bg-${this.getStatusColor(workorder.status)}">${this.getStatusText(workorder.status)}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>创建时间:</strong></td>
|
||||
<td>${new Date(workorder.created_at).toLocaleString()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>更新时间:</strong></td>
|
||||
<td>${new Date(workorder.updated_at).toLocaleString()}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>问题描述</h6>
|
||||
<div class="border p-3 rounded">
|
||||
${workorder.description}
|
||||
</div>
|
||||
${workorder.resolution ? `
|
||||
<h6 class="mt-3">解决方案</h6>
|
||||
<div class="border p-3 rounded bg-light">
|
||||
${workorder.resolution}
|
||||
</div>
|
||||
` : ''}
|
||||
${workorder.satisfaction_score ? `
|
||||
<h6 class="mt-3">满意度评分</h6>
|
||||
<div class="border p-3 rounded">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: ${workorder.satisfaction_score * 100}%"></div>
|
||||
</div>
|
||||
<small class="text-muted">${workorder.satisfaction_score}/5.0</small>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${workorder.conversations && workorder.conversations.length > 0 ? `
|
||||
<h6>对话记录</h6>
|
||||
<div class="conversation-history" style="max-height: 300px; overflow-y: auto;">
|
||||
${workorder.conversations.map(conv => `
|
||||
<div class="border-bottom pb-2 mb-2">
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-muted">${new Date(conv.timestamp).toLocaleString()}</small>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<strong>用户:</strong> ${conv.user_message}
|
||||
</div>
|
||||
<div>
|
||||
<strong>助手:</strong> ${conv.assistant_response}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" onclick="dashboard.updateWorkOrder(${workorder.id})">编辑工单</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 移除已存在的模态框
|
||||
const existingModal = document.getElementById('workOrderDetailsModal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// 添加新的模态框到页面
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
|
||||
// 显示模态框
|
||||
const modal = new bootstrap.Modal(document.getElementById('workOrderDetailsModal'));
|
||||
modal.show();
|
||||
|
||||
// 模态框关闭时移除DOM元素
|
||||
document.getElementById('workOrderDetailsModal').addEventListener('hidden.bs.modal', function() {
|
||||
this.remove();
|
||||
});
|
||||
}
|
||||
|
||||
async updateWorkOrder(workorderId) {
|
||||
try {
|
||||
// 获取工单详情
|
||||
const response = await fetch(`/api/workorders/${workorderId}`);
|
||||
const workorder = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.showEditWorkOrderModal(workorder);
|
||||
} else {
|
||||
throw new Error(workorder.error || '获取工单详情失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取工单详情失败:', error);
|
||||
this.showNotification('获取工单详情失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showEditWorkOrderModal(workorder) {
|
||||
// 创建编辑工单模态框
|
||||
const modalHtml = `
|
||||
<div class="modal fade" id="editWorkOrderModal" tabindex="-1" aria-labelledby="editWorkOrderModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editWorkOrderModalLabel">编辑工单 #${workorder.id}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editWorkOrderForm">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label for="editTitle" class="form-label">标题 *</label>
|
||||
<input type="text" class="form-control" id="editTitle" value="${workorder.title}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="editPriority" class="form-label">优先级</label>
|
||||
<select class="form-select" id="editPriority">
|
||||
<option value="low" ${workorder.priority === 'low' ? 'selected' : ''}>低</option>
|
||||
<option value="medium" ${workorder.priority === 'medium' ? 'selected' : ''}>中</option>
|
||||
<option value="high" ${workorder.priority === 'high' ? 'selected' : ''}>高</option>
|
||||
<option value="urgent" ${workorder.priority === 'urgent' ? 'selected' : ''}>紧急</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="editCategory" class="form-label">分类</label>
|
||||
<select class="form-select" id="editCategory">
|
||||
<option value="技术问题" ${workorder.category === '技术问题' ? 'selected' : ''}>技术问题</option>
|
||||
<option value="业务问题" ${workorder.category === '业务问题' ? 'selected' : ''}>业务问题</option>
|
||||
<option value="系统故障" ${workorder.category === '系统故障' ? 'selected' : ''}>系统故障</option>
|
||||
<option value="功能需求" ${workorder.category === '功能需求' ? 'selected' : ''}>功能需求</option>
|
||||
<option value="其他" ${workorder.category === '其他' ? 'selected' : ''}>其他</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="editStatus" class="form-label">状态</label>
|
||||
<select class="form-select" id="editStatus">
|
||||
<option value="open" ${workorder.status === 'open' ? 'selected' : ''}>待处理</option>
|
||||
<option value="in_progress" ${workorder.status === 'in_progress' ? 'selected' : ''}>处理中</option>
|
||||
<option value="resolved" ${workorder.status === 'resolved' ? 'selected' : ''}>已解决</option>
|
||||
<option value="closed" ${workorder.status === 'closed' ? 'selected' : ''}>已关闭</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="editDescription" class="form-label">描述 *</label>
|
||||
<textarea class="form-control" id="editDescription" rows="4" required>${workorder.description}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="editResolution" class="form-label">解决方案</label>
|
||||
<textarea class="form-control" id="editResolution" rows="3" placeholder="请输入解决方案...">${workorder.resolution || ''}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="editSatisfactionScore" class="form-label">满意度评分 (1-5)</label>
|
||||
<input type="number" class="form-control" id="editSatisfactionScore" min="1" max="5" value="${workorder.satisfaction_score || ''}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="dashboard.saveWorkOrder(${workorder.id})">保存修改</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 移除已存在的模态框
|
||||
const existingModal = document.getElementById('editWorkOrderModal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// 添加新模态框到页面
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
|
||||
// 显示模态框
|
||||
const modal = new bootstrap.Modal(document.getElementById('editWorkOrderModal'));
|
||||
modal.show();
|
||||
|
||||
// 模态框关闭时清理
|
||||
document.getElementById('editWorkOrderModal').addEventListener('hidden.bs.modal', function() {
|
||||
this.remove();
|
||||
});
|
||||
}
|
||||
|
||||
async saveWorkOrder(workorderId) {
|
||||
try {
|
||||
// 获取表单数据
|
||||
const formData = {
|
||||
title: document.getElementById('editTitle').value,
|
||||
description: document.getElementById('editDescription').value,
|
||||
category: document.getElementById('editCategory').value,
|
||||
priority: document.getElementById('editPriority').value,
|
||||
status: document.getElementById('editStatus').value,
|
||||
resolution: document.getElementById('editResolution').value,
|
||||
satisfaction_score: parseInt(document.getElementById('editSatisfactionScore').value) || null
|
||||
};
|
||||
|
||||
// 验证必填字段
|
||||
if (!formData.title.trim() || !formData.description.trim()) {
|
||||
this.showNotification('标题和描述不能为空', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送更新请求
|
||||
const response = await fetch(`/api/workorders/${workorderId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.showNotification('工单更新成功', 'success');
|
||||
// 关闭模态框
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('editWorkOrderModal'));
|
||||
modal.hide();
|
||||
// 刷新工单列表
|
||||
this.loadWorkOrders();
|
||||
} else {
|
||||
throw new Error(result.error || '更新工单失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新工单失败:', error);
|
||||
this.showNotification('更新工单失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 工单导入功能
|
||||
showImportModal() {
|
||||
// 显示导入模态框
|
||||
const modal = new bootstrap.Modal(document.getElementById('importWorkOrderModal'));
|
||||
modal.show();
|
||||
|
||||
// 重置表单
|
||||
document.getElementById('excel-file-input').value = '';
|
||||
document.getElementById('import-progress').classList.add('d-none');
|
||||
document.getElementById('import-result').classList.add('d-none');
|
||||
}
|
||||
|
||||
async downloadTemplate() {
|
||||
try {
|
||||
const response = await fetch('/api/workorders/import/template');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// 创建下载链接
|
||||
const link = document.createElement('a');
|
||||
link.href = result.template_url;
|
||||
link.download = '工单导入模板.xlsx';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.showNotification('模板下载成功', 'success');
|
||||
} else {
|
||||
throw new Error(result.error || '下载模板失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载模板失败:', error);
|
||||
this.showNotification('下载模板失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async importWorkOrders() {
|
||||
const fileInput = document.getElementById('excel-file-input');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
this.showNotification('请选择要导入的Excel文件', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
if (!file.name.match(/\.(xlsx|xls)$/)) {
|
||||
this.showNotification('只支持Excel文件(.xlsx, .xls)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if (file.size > 16 * 1024 * 1024) {
|
||||
this.showNotification('文件大小不能超过16MB', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示进度条
|
||||
document.getElementById('import-progress').classList.remove('d-none');
|
||||
document.getElementById('import-result').classList.add('d-none');
|
||||
|
||||
try {
|
||||
// 创建FormData
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// 发送导入请求
|
||||
const response = await fetch('/api/workorders/import', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
// 显示成功消息
|
||||
document.getElementById('import-progress').classList.add('d-none');
|
||||
document.getElementById('import-result').classList.remove('d-none');
|
||||
document.getElementById('import-success-message').textContent =
|
||||
`成功导入 ${result.imported_count} 个工单`;
|
||||
|
||||
this.showNotification(result.message, 'success');
|
||||
|
||||
// 刷新工单列表
|
||||
this.loadWorkOrders();
|
||||
|
||||
// 3秒后关闭模态框
|
||||
setTimeout(() => {
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('importWorkOrderModal'));
|
||||
modal.hide();
|
||||
}, 3000);
|
||||
|
||||
} else {
|
||||
throw new Error(result.error || '导入工单失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('导入工单失败:', error);
|
||||
document.getElementById('import-progress').classList.add('d-none');
|
||||
this.showNotification('导入工单失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 数据分析
|
||||
async loadAnalytics() {
|
||||
try {
|
||||
const response = await fetch('/api/analytics');
|
||||
const analytics = await response.json();
|
||||
this.updateAnalyticsDisplay(analytics);
|
||||
this.initializeCharts();
|
||||
} catch (error) {
|
||||
console.error('加载分析数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
initializeCharts() {
|
||||
this.charts = {};
|
||||
this.updateCharts();
|
||||
}
|
||||
|
||||
// 更新所有图表
|
||||
async updateCharts() {
|
||||
try {
|
||||
const timeRange = document.getElementById('timeRange').value;
|
||||
const chartType = document.getElementById('chartType').value;
|
||||
const dataDimension = document.getElementById('dataDimension').value;
|
||||
|
||||
// 获取数据
|
||||
const response = await fetch(`/api/analytics?timeRange=${timeRange}&dimension=${dataDimension}`);
|
||||
const data = await response.json();
|
||||
|
||||
// 更新统计卡片
|
||||
this.updateStatisticsCards(data);
|
||||
|
||||
// 更新图表
|
||||
this.updateMainChart(data, chartType);
|
||||
this.updateDistributionChart(data);
|
||||
this.updateTrendChart(data);
|
||||
this.updatePriorityChart(data);
|
||||
|
||||
// 更新分析报告
|
||||
this.updateAnalyticsReport(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('更新图表失败:', error);
|
||||
this.showNotification('更新图表失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计卡片
|
||||
updateStatisticsCards(data) {
|
||||
const total = data.workorders?.total || 0;
|
||||
const open = data.workorders?.open || 0;
|
||||
const resolved = data.workorders?.resolved || 0;
|
||||
const avgSatisfaction = data.satisfaction?.average || 0;
|
||||
|
||||
document.getElementById('totalWorkorders').textContent = total;
|
||||
document.getElementById('openWorkorders').textContent = open;
|
||||
document.getElementById('resolvedWorkorders').textContent = resolved;
|
||||
document.getElementById('avgSatisfaction').textContent = avgSatisfaction.toFixed(1);
|
||||
|
||||
// 更新进度条
|
||||
if (total > 0) {
|
||||
document.getElementById('openProgress').style.width = `${(open / total) * 100}%`;
|
||||
document.getElementById('resolvedProgress').style.width = `${(resolved / total) * 100}%`;
|
||||
document.getElementById('satisfactionProgress').style.width = `${(avgSatisfaction / 5) * 100}%`;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新主图表
|
||||
updateMainChart(data, chartType) {
|
||||
const ctx = document.getElementById('mainChart').getContext('2d');
|
||||
|
||||
// 销毁现有图表
|
||||
if (this.charts.mainChart) {
|
||||
this.charts.mainChart.destroy();
|
||||
}
|
||||
|
||||
const chartData = this.prepareChartData(data, chartType);
|
||||
|
||||
this.charts.mainChart = new Chart(ctx, {
|
||||
type: chartType,
|
||||
data: chartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '数据分析趋势'
|
||||
},
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
}
|
||||
},
|
||||
scales: chartType === 'pie' || chartType === 'doughnut' ? {} : {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: '时间'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: '数量'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新分布图表
|
||||
updateDistributionChart(data) {
|
||||
const ctx = document.getElementById('distributionChart').getContext('2d');
|
||||
|
||||
if (this.charts.distributionChart) {
|
||||
this.charts.distributionChart.destroy();
|
||||
}
|
||||
|
||||
const categories = data.workorders?.by_category || {};
|
||||
const labels = Object.keys(categories);
|
||||
const values = Object.values(categories);
|
||||
|
||||
this.charts.distributionChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: values,
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56',
|
||||
'#4BC0C0',
|
||||
'#9966FF',
|
||||
'#FF9F40'
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '工单分类分布'
|
||||
},
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新趋势图表
|
||||
updateTrendChart(data) {
|
||||
const ctx = document.getElementById('trendChart').getContext('2d');
|
||||
|
||||
if (this.charts.trendChart) {
|
||||
this.charts.trendChart.destroy();
|
||||
}
|
||||
|
||||
const trendData = data.trend || [];
|
||||
const labels = trendData.map(item => item.date);
|
||||
const workorders = trendData.map(item => item.workorders);
|
||||
const alerts = trendData.map(item => item.alerts);
|
||||
|
||||
this.charts.trendChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: '工单数量',
|
||||
data: workorders,
|
||||
borderColor: '#36A2EB',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||||
tension: 0.4
|
||||
}, {
|
||||
label: '预警数量',
|
||||
data: alerts,
|
||||
borderColor: '#FF6384',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '时间趋势分析'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: '日期'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: '数量'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新优先级图表
|
||||
updatePriorityChart(data) {
|
||||
const ctx = document.getElementById('priorityChart').getContext('2d');
|
||||
|
||||
if (this.charts.priorityChart) {
|
||||
this.charts.priorityChart.destroy();
|
||||
}
|
||||
|
||||
const priorities = data.workorders?.by_priority || {};
|
||||
const labels = Object.keys(priorities).map(p => this.getPriorityText(p));
|
||||
const values = Object.values(priorities);
|
||||
|
||||
this.charts.priorityChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: '工单数量',
|
||||
data: values,
|
||||
backgroundColor: [
|
||||
'#28a745', // 低 - 绿色
|
||||
'#ffc107', // 中 - 黄色
|
||||
'#fd7e14', // 高 - 橙色
|
||||
'#dc3545' // 紧急 - 红色
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '优先级分布'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 准备图表数据
|
||||
prepareChartData(data, chartType) {
|
||||
const trendData = data.trend || [];
|
||||
const labels = trendData.map(item => item.date);
|
||||
const workorders = trendData.map(item => item.workorders);
|
||||
|
||||
if (chartType === 'pie' || chartType === 'doughnut') {
|
||||
const categories = data.workorders?.by_category || {};
|
||||
return {
|
||||
labels: Object.keys(categories),
|
||||
datasets: [{
|
||||
data: Object.values(categories),
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56',
|
||||
'#4BC0C0',
|
||||
'#9966FF',
|
||||
'#FF9F40'
|
||||
]
|
||||
}]
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: '工单数量',
|
||||
data: workorders,
|
||||
borderColor: '#36A2EB',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||||
tension: chartType === 'line' ? 0.4 : 0
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 导出图表
|
||||
exportChart(chartId) {
|
||||
if (this.charts[chartId]) {
|
||||
const link = document.createElement('a');
|
||||
link.download = `${chartId}_chart.png`;
|
||||
link.href = this.charts[chartId].toBase64Image();
|
||||
link.click();
|
||||
}
|
||||
}
|
||||
|
||||
// 全屏图表
|
||||
fullscreenChart(chartId) {
|
||||
// 这里可以实现全屏显示功能
|
||||
this.showNotification('全屏功能开发中', 'info');
|
||||
}
|
||||
|
||||
// 导出报告
|
||||
async exportReport() {
|
||||
try {
|
||||
const response = await fetch('/api/analytics/export');
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'analytics_report.xlsx';
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('导出报告失败:', error);
|
||||
this.showNotification('导出报告失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 打印报告
|
||||
printReport() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
updateAnalyticsDisplay(analytics) {
|
||||
// 更新分析报告
|
||||
const reportContainer = document.getElementById('analytics-report');
|
||||
|
||||
Reference in New Issue
Block a user