refactor: dashboard.js 模块化拆分 从7766行拆为13个独立模块

核心 dashboard.js 缩减至266行,只保留:
- TSPDashboard class 定义(constructor、init、bindEvents、switchTab)
- 分页组件、缓存、状态管理、自动刷新、WebSocket、i18n

功能模块拆分到 modules/ 目录:
- chat.js (207行)  智能对话
- agent.js (424行)  Agent管理
- alerts.js (348行)  预警管理
- knowledge.js (699行)  知识库管理
- workorders.js (约500行)  工单管理
- conversations.js (671行)  对话历史
- monitoring.js (455行)  Token/AI监控
- system.js (1388行)  系统优化+设置+数据分析图表
- tenants.js (165行)  租户管理
- utils.js (573行)  工具函数
- dashboard-home.js (286行)  仪表板首页
- feishu-sync.js (698行)  飞书同步管理器

拆分方式:Object.assign(TSPDashboard.prototype, {...})
this 引用完全不会断,所有模块方法互相调用正常
This commit is contained in:
2026-04-02 15:10:23 +08:00
parent d691007c86
commit 58b3c615ef
14 changed files with 6550 additions and 7565 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,424 @@
// Agent管理模块
Object.assign(TSPDashboard.prototype, {
async toggleAgentMode(enabled) {
try {
const response = await fetch('/api/agent/toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ enabled })
});
const data = await response.json();
if (data.success) {
this.isAgentMode = enabled;
this.showNotification(`Agent模式已${enabled ? '启用' : '禁用'}`, 'success');
this.loadAgentData();
} else {
this.showNotification('切换Agent模式失败', 'error');
}
} catch (error) {
console.error('切换Agent模式失败:', error);
this.showNotification('切换Agent模式失败', 'error');
}
},
async loadAgentData() {
try {
const [statusResp, toolsResp] = await Promise.all([
fetch('/api/agent/status'),
fetch('/api/agent/tools/stats')
]);
const data = await statusResp.json();
const toolsData = await toolsResp.json();
if (data.success) {
// 更新 ReactAgent 状态
const react = data.react_agent || {};
const stateEl = document.getElementById('agent-current-state');
stateEl.textContent = react.status || (data.is_active ? 'active' : 'inactive');
stateEl.className = `badge ${data.is_active ? 'bg-success' : 'bg-secondary'}`;
document.getElementById('agent-available-tools').textContent = react.tool_count || 0;
document.getElementById('agent-max-rounds').textContent = react.max_tool_rounds || 5;
document.getElementById('agent-history-count').textContent = react.history_count || 0;
// 工具列表 — 使用 ReactAgent 的工具定义
const tools = (toolsData.success ? toolsData.tools : []) || [];
this.updateToolsList(tools);
// 执行历史
const history = (toolsData.success ? toolsData.recent_history : []) || [];
this.updateAgentExecutionHistory(history);
}
} catch (error) {
console.error('加载Agent数据失败:', error);
}
},
updateToolsList(tools) {
const toolsList = document.getElementById('tools-list');
if (!tools || tools.length === 0) {
toolsList.innerHTML = '<div class="empty-state"><i class="fas fa-tools"></i><p>暂无工具</p></div>';
return;
}
const toolsHtml = tools.map(tool => {
// ReactAgent 工具定义格式: { name, description, parameters }
const params = tool.parameters || {};
const paramList = Object.entries(params).map(([k, v]) =>
`<code>${k}</code><span class="text-muted small">(${v.required ? '必填' : '可选'})</span>`
).join(', ');
return `
<div class="border-bottom pb-2 mb-2">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>${tool.name}</strong>
<div class="text-muted small">${tool.description || ''}</div>
${paramList ? `<div class="small mt-1">参数: ${paramList}</div>` : ''}
</div>
<button class="btn btn-sm btn-outline-primary" data-tool="${tool.name}" title="执行工具">
<i class="fas fa-play"></i>
</button>
</div>
</div>
`;
}).join('');
toolsList.innerHTML = toolsHtml;
// 绑定执行事件
toolsList.querySelectorAll('button[data-tool]').forEach(btn => {
btn.addEventListener('click', async () => {
const tool = btn.getAttribute('data-tool');
let params = {};
try {
const input = prompt('请输入执行参数(JSON)', '{}');
if (input === null) return;
params = JSON.parse(input);
} catch (e) {
this.showNotification('参数格式错误应为JSON', 'warning');
return;
}
try {
const resp = await fetch('/api/agent/tools/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tool, parameters: params })
});
const res = await resp.json();
if (res.success) {
this.showNotification(`工具 ${tool} 执行成功`, 'success');
await this.loadAgentData();
} else {
this.showNotification(res.error || `工具 ${tool} 执行失败`, 'error');
}
} catch (err) {
this.showNotification('执行工具失败: ' + err.message, 'error');
}
});
});
},
updateExecutionHistory(history) {
const historyContainer = document.getElementById('agent-execution-history');
if (history.length === 0) {
historyContainer.innerHTML = '<div class="empty-state"><i class="fas fa-history"></i><p>暂无执行历史</p></div>';
return;
}
const historyHtml = history.slice(-5).map(item => `
<div class="border-bottom pb-2 mb-2">
<div class="d-flex justify-content-between">
<strong>${item.type || '未知任务'}</strong>
<small class="text-muted">${new Date(item.timestamp).toLocaleString()}</small>
</div>
<div class="text-muted small">${item.description || '无描述'}</div>
<div class="mt-1">
<span class="badge ${item.success ? 'bg-success' : 'bg-danger'}">
${item.success ? '成功' : '失败'}
</span>
</div>
</div>
`).join('');
historyContainer.innerHTML = historyHtml;
},
async startAgentMonitoring() {
try {
const response = await fetch('/api/agent/monitoring/start', { method: 'POST' });
const data = await response.json();
if (data.success) {
this.showNotification('Agent监控已启动', 'success');
} else {
this.showNotification('启动Agent监控失败', 'error');
}
} catch (error) {
console.error('启动Agent监控失败:', error);
this.showNotification('启动Agent监控失败', 'error');
}
},
async stopAgentMonitoring() {
try {
const response = await fetch('/api/agent/monitoring/stop', { method: 'POST' });
const data = await response.json();
if (data.success) {
this.showNotification('Agent监控已停止', 'success');
} else {
this.showNotification('停止Agent监控失败', 'error');
}
} catch (error) {
console.error('停止Agent监控失败:', error);
this.showNotification('停止Agent监控失败', 'error');
}
},
async proactiveMonitoring() {
try {
const response = await fetch('/api/agent/proactive-monitoring', { method: 'POST' });
const data = await response.json();
if (data.success) {
this.showNotification(`主动监控完成,发现 ${data.proactive_actions?.length || 0} 个行动机会`, 'info');
} else {
this.showNotification('主动监控失败', 'error');
}
} catch (error) {
console.error('主动监控失败:', error);
this.showNotification('主动监控失败', 'error');
}
},
async intelligentAnalysis() {
try {
const response = await fetch('/api/agent/intelligent-analysis', { method: 'POST' });
const data = await response.json();
if (data.success) {
this.showNotification('智能分析完成', 'success');
// 更新分析图表
this.updateAnalyticsChart(data.analysis);
} else {
this.showNotification('智能分析失败', 'error');
}
} catch (error) {
console.error('智能分析失败:', error);
this.showNotification('智能分析失败', 'error');
}
},
updateAnalyticsChart(analysis) {
if (!this.charts.analytics || !analysis) return;
// 更新分析图表数据
const labels = analysis.trends?.dates || [];
const satisfactionData = analysis.trends?.satisfaction || [];
const resolutionTimeData = analysis.trends?.resolution_time || [];
this.charts.analytics.data.labels = labels;
this.charts.analytics.data.datasets[0].data = satisfactionData;
this.charts.analytics.data.datasets[1].data = resolutionTimeData;
this.charts.analytics.update();
},
async sendAgentMessage() {
const messageInput = document.getElementById('agent-message-input');
const message = messageInput.value.trim();
if (!message) {
this.showNotification('请输入消息', 'warning');
return;
}
try {
// 显示发送状态
const sendBtn = document.getElementById('send-agent-message');
const originalText = sendBtn.innerHTML;
sendBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>发送中...';
sendBtn.disabled = true;
const response = await fetch('/api/agent/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
context: {
user_id: 'admin',
session_id: `agent_session_${Date.now()}`
}
})
});
const data = await response.json();
if (data.success) {
this.showNotification('Agent响应成功', 'success');
// 清空输入框
messageInput.value = '';
// 刷新执行历史
await this.loadAgentExecutionHistory();
} else {
this.showNotification('Agent响应失败: ' + (data.error || '未知错误'), 'error');
}
} catch (error) {
console.error('发送Agent消息失败:', error);
this.showNotification('发送Agent消息失败: ' + error.message, 'error');
} finally {
// 恢复按钮状态
const sendBtn = document.getElementById('send-agent-message');
sendBtn.innerHTML = '<i class="fas fa-paper-plane me-1"></i>发送';
sendBtn.disabled = false;
}
},
// 清空Agent对话
clearAgentChat() {
document.getElementById('agent-message-input').value = '';
this.showNotification('对话已清空', 'info');
},
// 加载Agent执行历史
async loadAgentExecutionHistory() {
try {
const response = await fetch('/api/agent/action-history?limit=10');
const data = await response.json();
if (data.success) {
this.updateExecutionHistory(data.history);
}
} catch (error) {
console.error('加载Agent执行历史失败:', error);
}
},
// 触发示例动作 (from lines 6856+)
async triggerSampleAction() {
try {
const response = await fetch('/api/agent/trigger-sample', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
this.showNotification('示例动作执行成功', 'success');
await this.loadAgentExecutionHistory();
} else {
this.showNotification('示例动作执行失败: ' + (data.error || '未知错误'), 'error');
}
} catch (error) {
console.error('触发示例动作失败:', error);
this.showNotification('触发示例动作失败: ' + error.message, 'error');
}
},
// 清空Agent历史 (from lines 6871+)
async clearAgentHistory() {
if (!confirm('确定要清空Agent执行历史吗')) {
return;
}
try {
const response = await fetch('/api/agent/clear-history', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
this.showNotification('Agent历史已清空', 'success');
await this.loadAgentExecutionHistory();
} else {
this.showNotification('清空Agent历史失败: ' + (data.error || '未知错误'), 'error');
}
} catch (error) {
console.error('清空Agent历史失败:', error);
this.showNotification('清空Agent历史失败: ' + error.message, 'error');
}
},
updateAgentExecutionHistory(history) {
const container = document.getElementById('agent-execution-history');
if (!history || history.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-history"></i>
<p>暂无执行历史</p>
</div>
`;
return;
}
const historyHtml = history.map(record => {
const startTime = new Date(record.start_time * 1000).toLocaleString();
const endTime = new Date(record.end_time * 1000).toLocaleString();
const duration = Math.round((record.end_time - record.start_time) * 100) / 100;
const priorityColor = {
5: 'danger',
4: 'warning',
3: 'info',
2: 'secondary',
1: 'light'
}[record.priority] || 'secondary';
const confidenceColor = record.confidence >= 0.8 ? 'success' :
record.confidence >= 0.5 ? 'warning' : 'danger';
return `
<div class="card mb-2">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">${record.description}</h6>
<div class="d-flex gap-3 mb-2">
<span class="badge bg-${priorityColor}">优先级 ${record.priority}</span>
<span class="badge bg-${confidenceColor}">置信度 ${(record.confidence * 100).toFixed(0)}%</span>
<span class="badge bg-${record.success ? 'success' : 'danger'}">${record.success ? '成功' : '失败'}</span>
</div>
<small class="text-muted">
<i class="fas fa-clock me-1"></i>开始: ${startTime} |
<i class="fas fa-stopwatch me-1"></i>耗时: ${duration}
</small>
</div>
<div class="ms-3">
<span class="badge bg-primary">${record.action_type}</span>
</div>
</div>
${record.result && record.result.message ? `
<div class="mt-2">
<small class="text-muted">结果: ${record.result.message}</small>
</div>
` : ''}
</div>
</div>
`;
}).join('');
container.innerHTML = historyHtml;
},
async refreshAgentHistory() {
try {
const response = await fetch('/api/agent/action-history?limit=20');
const data = await response.json();
if (data.success) {
this.updateAgentExecutionHistory(data.history);
this.showNotification(`已加载 ${data.count} 条执行历史`, 'success');
} else {
throw new Error(data.error || '获取执行历史失败');
}
} catch (error) {
console.error('刷新Agent历史失败:', error);
this.showNotification('刷新Agent历史失败: ' + error.message, 'error');
}
}
});

View File

@@ -0,0 +1,348 @@
// 预警管理模块
Object.assign(TSPDashboard.prototype, {
async loadAlerts(page = 1, forceRefresh = false) {
const cacheKey = `alerts_page_${page}`;
if (!forceRefresh && this.cache.has(cacheKey)) {
const cachedData = this.cache.get(cacheKey);
this.updateAlertsDisplay(cachedData.alerts);
this.updateAlertsPagination(cachedData);
this.updateAlertStatistics(cachedData.alerts); // 添加统计更新
return;
}
try {
const pageSize = this.getPageSize('alerts-pagination');
const response = await fetch(`/api/alerts?page=${page}&per_page=${pageSize}`);
const data = await response.json();
this.cache.set(cacheKey, data);
this.updateAlertsDisplay(data.alerts);
this.updateAlertsPagination(data);
this.updateAlertStatistics(data.alerts); // 添加统计更新
} catch (error) {
console.error('加载预警失败:', error);
this.showNotification('加载预警失败', 'error');
}
},
updateAlertsDisplay(alerts) {
const container = document.getElementById('alerts-container');
if (alerts.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-check-circle"></i>
<h5>暂无活跃预警</h5>
<p>系统运行正常,没有需要处理的预警</p>
</div>
`;
return;
}
// 添加预警列表的批量操作头部
const headerHtml = `
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<input type="checkbox" id="select-all-alerts" class="form-check-input me-2" onchange="dashboard.toggleSelectAllAlerts()">
<label for="select-all-alerts" class="form-check-label">全选</label>
</div>
<div class="btn-group">
<button class="btn btn-sm btn-danger" id="batch-delete-alerts" onclick="dashboard.batchDeleteAlerts()" disabled>
<i class="fas fa-trash me-1"></i>批量删除
</button>
</div>
</div>
`;
const alertsHtml = alerts.map(alert => `
<div class="alert-item ${alert.level}">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex align-items-start">
<input type="checkbox" class="form-check-input me-2 alert-checkbox" value="${alert.id}" onchange="dashboard.updateBatchDeleteAlertsButton()">
<div class="flex-grow-1">
<div class="d-flex align-items-center mb-2">
<span class="badge bg-${this.getAlertColor(alert.level)} me-2">${this.getLevelText(alert.level)}</span>
<span class="fw-bold">${alert.rule_name || '未知规则'}</span>
<span class="ms-auto text-muted small">${this.formatTime(alert.created_at)}</span>
</div>
<div class="alert-message mb-2">${alert.message}</div>
<div class="alert-meta text-muted small">
类型: ${this.getTypeText(alert.alert_type)} |
级别: ${this.getLevelText(alert.level)}
</div>
</div>
</div>
<div class="ms-3">
<button class="btn btn-sm btn-outline-success" onclick="dashboard.resolveAlert(${alert.id})" title="解决预警">
<i class="fas fa-check"></i>
<span class="d-none d-md-inline ms-1">解决</span>
</button>
</div>
</div>
</div>
`).join('');
container.innerHTML = headerHtml + alertsHtml;
},
updateAlertsPagination(data) {
this.createPaginationComponent(data, 'alerts-pagination', 'loadAlerts', '条预警');
},
updateAlertStatistics(alerts) {
// 如果传入的是分页数据,需要重新获取全部数据来计算统计
if (alerts && alerts.length > 0) {
// 检查是否是分页数据通常分页数据少于50条
const pageSize = this.getPageSize('alerts-pagination');
if (alerts.length <= pageSize) {
// 可能是分页数据,需要获取全部数据
this.updateAlertStatisticsFromAPI();
return;
}
}
// 使用传入的数据计算统计
const stats = (alerts || []).reduce((acc, alert) => {
acc[alert.level] = (acc[alert.level] || 0) + 1;
acc.total = (acc.total || 0) + 1;
return acc;
}, {});
document.getElementById('critical-alerts').textContent = stats.critical || 0;
document.getElementById('warning-alerts').textContent = stats.warning || 0;
document.getElementById('info-alerts').textContent = stats.info || 0;
document.getElementById('total-alerts-count').textContent = stats.total || 0;
},
// 从API获取全部预警数据来计算统计
async updateAlertStatisticsFromAPI() {
try {
// 获取全部预警数据(不分页)
const response = await fetch('/api/alerts?per_page=1000'); // 获取大量数据
const data = await response.json();
if (data.alerts) {
const stats = data.alerts.reduce((acc, alert) => {
acc[alert.level] = (acc[alert.level] || 0) + 1;
acc.total = (acc.total || 0) + 1;
return acc;
}, {});
document.getElementById('critical-alerts').textContent = stats.critical || 0;
document.getElementById('warning-alerts').textContent = stats.warning || 0;
document.getElementById('info-alerts').textContent = stats.info || 0;
document.getElementById('total-alerts-count').textContent = stats.total || 0;
// 更新缓存
this.cache.set('alerts_stats', {
data: data.alerts,
timestamp: Date.now()
});
}
} catch (error) {
console.error('获取全部预警统计失败:', error);
}
},
// 预警批量删除功能
toggleSelectAllAlerts() {
const selectAllCheckbox = document.getElementById('select-all-alerts');
const alertCheckboxes = document.querySelectorAll('.alert-checkbox');
alertCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
this.updateBatchDeleteAlertsButton();
},
updateBatchDeleteAlertsButton() {
const selectedCheckboxes = document.querySelectorAll('.alert-checkbox:checked');
const batchDeleteBtn = document.getElementById('batch-delete-alerts');
if (batchDeleteBtn) {
batchDeleteBtn.disabled = selectedCheckboxes.length === 0;
batchDeleteBtn.textContent = selectedCheckboxes.length > 0
? `批量删除 (${selectedCheckboxes.length})`
: '批量删除';
}
},
async batchDeleteAlerts() {
const selectedCheckboxes = document.querySelectorAll('.alert-checkbox:checked');
const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
if (selectedIds.length === 0) {
this.showNotification('请选择要删除的预警', 'warning');
return;
}
if (!confirm(`确定要删除选中的 ${selectedIds.length} 个预警吗?此操作不可撤销。`)) {
return;
}
try {
const response = await fetch('/api/batch-delete/alerts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: selectedIds })
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
// 清除所有相关缓存
this.cache.delete('alerts');
this.cache.delete('alerts_stats');
// 立即更新统计数字,避免跳动
await this.updateStatsAfterDelete(selectedIds.length);
// 重新加载预警列表
await this.loadAlerts();
// 重置批量删除按钮状态
this.updateBatchDeleteAlertsButton();
} else {
this.showNotification(data.error || '批量删除失败', 'error');
}
} catch (error) {
console.error('批量删除预警失败:', error);
this.showNotification('批量删除预警失败', 'error');
}
},
// 删除后更新统计数字(平滑更新)
async updateStatsAfterDelete(deletedCount) {
try {
// 直接调用API获取最新统计不依赖页面显示数据
await this.updateAlertStatisticsFromAPI();
// 更新最后更新时间,避免智能更新机制干扰
this.lastUpdateTimes.alerts = Date.now();
} catch (error) {
console.error('更新删除后统计失败:', error);
// 如果计算失败,直接刷新
await this.refreshAlertStats();
}
},
// 获取当前显示的预警数据
getCurrentDisplayedAlerts() {
const alertElements = document.querySelectorAll('.alert-item');
const alerts = [];
alertElements.forEach(element => {
const level = element.querySelector('.alert-level')?.textContent?.trim();
if (level) {
alerts.push({ level: level.toLowerCase() });
}
});
return alerts;
},
// 计算预警统计
calculateAlertStats(alerts) {
const stats = { critical: 0, warning: 0, info: 0, total: 0 };
alerts.forEach(alert => {
const level = alert.level.toLowerCase();
if (stats.hasOwnProperty(level)) {
stats[level]++;
}
stats.total++;
});
return stats;
},
// 平滑更新预警统计数字
smoothUpdateAlertStats(newStats) {
// 获取当前显示的数字
const currentCritical = parseInt(document.getElementById('critical-alerts')?.textContent || '0');
const currentWarning = parseInt(document.getElementById('warning-alerts')?.textContent || '0');
const currentInfo = parseInt(document.getElementById('info-alerts')?.textContent || '0');
const currentTotal = parseInt(document.getElementById('total-alerts-count')?.textContent || '0');
// 平滑过渡到新数字
this.animateNumberChange('critical-alerts', currentCritical, newStats.critical);
this.animateNumberChange('warning-alerts', currentWarning, newStats.warning);
this.animateNumberChange('info-alerts', currentInfo, newStats.info);
this.animateNumberChange('total-alerts-count', currentTotal, newStats.total);
},
// 数字变化动画
animateNumberChange(elementId, from, to) {
const element = document.getElementById(elementId);
if (!element) return;
const duration = 300; // 300ms动画
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// 使用缓动函数
const easeOut = 1 - Math.pow(1 - progress, 3);
const currentValue = Math.round(from + (to - from) * easeOut);
element.textContent = currentValue;
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
},
// 解决预警后更新统计数字
async updateStatsAfterResolve(alertId) {
try {
// 直接调用API获取最新统计不依赖页面显示数据
await this.updateAlertStatisticsFromAPI();
// 更新最后更新时间,避免智能更新机制干扰
this.lastUpdateTimes.alerts = Date.now();
} catch (error) {
console.error('更新解决后统计失败:', error);
// 如果计算失败,直接刷新
await this.refreshAlertStats();
}
},
async resolveAlert(alertId) {
try {
const response = await fetch(`/api/alerts/${alertId}/resolve`, { method: 'POST' });
const data = await response.json();
if (data.success) {
this.showNotification('预警已解决', 'success');
// 清除缓存
this.cache.delete('alerts');
this.cache.delete('alerts_stats');
// 立即更新统计数字
await this.updateStatsAfterResolve(alertId);
// 重新加载预警列表
this.loadAlerts();
} else {
this.showNotification('解决预警失败', 'error');
}
} catch (error) {
console.error('解决预警失败:', error);
this.showNotification('解决预警失败', 'error');
}
}
});

View File

@@ -0,0 +1,207 @@
// 智能对话模块
Object.assign(TSPDashboard.prototype, {
async startChat() {
try {
const userId = document.getElementById('user-id').value;
const workOrderId = document.getElementById('work-order-id').value;
const tenantId = document.getElementById('chat-tenant-id')?.value || 'default';
const response = await fetch('/api/chat/session', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: userId,
work_order_id: workOrderId ? parseInt(workOrderId) : null,
tenant_id: tenantId
})
});
const data = await response.json();
if (data.success) {
this.sessionId = data.session_id;
document.getElementById('start-chat').disabled = true;
document.getElementById('end-chat').disabled = false;
document.getElementById('message-input').disabled = false;
document.getElementById('send-button').disabled = false;
document.getElementById('session-info').textContent = `会话ID: ${this.sessionId}`;
document.getElementById('connection-status').className = 'badge bg-success';
document.getElementById('connection-status').innerHTML = '<i class="fas fa-circle me-1"></i>已连接';
this.showNotification('对话已开始', 'success');
} else {
this.showNotification('开始对话失败', 'error');
}
} catch (error) {
console.error('开始对话失败:', error);
this.showNotification('开始对话失败', 'error');
}
},
async endChat() {
try {
if (!this.sessionId) return;
const response = await fetch(`/api/chat/session/${this.sessionId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
this.sessionId = null;
document.getElementById('start-chat').disabled = false;
document.getElementById('end-chat').disabled = true;
document.getElementById('message-input').disabled = true;
document.getElementById('send-button').disabled = true;
document.getElementById('session-info').textContent = '未开始对话';
document.getElementById('connection-status').className = 'badge bg-secondary';
document.getElementById('connection-status').innerHTML = '<i class="fas fa-circle me-1"></i>未连接';
this.showNotification('对话已结束', 'info');
} else {
this.showNotification('结束对话失败', 'error');
}
} catch (error) {
console.error('结束对话失败:', error);
this.showNotification('结束对话失败', 'error');
}
},
async sendMessage() {
const messageInput = document.getElementById('message-input');
const message = messageInput.value.trim();
if (!message || !this.sessionId) return;
// 显示用户消息
this.addMessage('user', message);
messageInput.value = '';
// 显示占位提示:小奇正在查询中
const typingId = this.showTypingIndicator();
// 发送消息到服务器
try {
const response = await fetch('/api/chat/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_id: this.sessionId,
message: message
})
});
const data = await response.json();
if (data.success) {
this.updateTypingIndicator(typingId, data.response, data.knowledge_used);
} else {
this.updateTypingIndicator(typingId, '抱歉,处理您的消息时出现了错误。', null, true);
}
} catch (error) {
console.error('发送消息失败:', error);
this.updateTypingIndicator(typingId, '网络连接错误,请稍后重试。', null, true);
}
},
showTypingIndicator() {
const messagesContainer = document.getElementById('chat-messages');
const id = `typing-${Date.now()}`;
const messageDiv = document.createElement('div');
messageDiv.className = 'message assistant';
messageDiv.id = id;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.innerHTML = '<i class="fas fa-robot"></i>';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = `
<div>小奇正在查询中,请稍后…</div>
<div class="message-time">${new Date().toLocaleTimeString()}</div>
`;
messageDiv.appendChild(avatar);
messageDiv.appendChild(contentDiv);
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return id;
},
updateTypingIndicator(typingId, content, knowledgeUsed = null, isError = false) {
const node = document.getElementById(typingId);
if (!node) {
// 回退:若占位不存在则直接追加
this.addMessage('assistant', content, knowledgeUsed, isError);
return;
}
const contentDiv = node.querySelector('.message-content');
if (contentDiv) {
contentDiv.innerHTML = `
<div>${content}</div>
<div class="message-time">${new Date().toLocaleTimeString()}</div>
`;
if (knowledgeUsed && knowledgeUsed.length > 0) {
const knowledgeDiv = document.createElement('div');
knowledgeDiv.className = 'knowledge-info';
knowledgeDiv.innerHTML = `
<i class="fas fa-lightbulb me-1"></i>
使用了知识库: ${knowledgeUsed.map(k => k.question || k.source || '实时数据').join(', ')}
`;
contentDiv.appendChild(knowledgeDiv);
}
if (isError) {
contentDiv.style.borderLeft = '4px solid #dc3545';
} else {
contentDiv.style.borderLeft = '';
}
}
const messagesContainer = document.getElementById('chat-messages');
messagesContainer.scrollTop = messagesContainer.scrollHeight;
},
addMessage(role, content, knowledgeUsed = null, isError = false) {
const messagesContainer = document.getElementById('chat-messages');
// 移除欢迎消息
const welcomeMsg = messagesContainer.querySelector('.text-center');
if (welcomeMsg) {
welcomeMsg.remove();
}
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.innerHTML = role === 'user' ? '<i class="fas fa-user"></i>' : '<i class="fas fa-robot"></i>';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = `
<div>${content}</div>
<div class="message-time">${new Date().toLocaleTimeString()}</div>
`;
if (knowledgeUsed && knowledgeUsed.length > 0) {
const knowledgeDiv = document.createElement('div');
knowledgeDiv.className = 'knowledge-info';
knowledgeDiv.innerHTML = `
<i class="fas fa-lightbulb me-1"></i>
使用了知识库: ${knowledgeUsed.map(k => k.question).join(', ')}
`;
contentDiv.appendChild(knowledgeDiv);
}
if (isError) {
contentDiv.style.borderLeft = '4px solid #dc3545';
}
messageDiv.appendChild(avatar);
messageDiv.appendChild(contentDiv);
messagesContainer.appendChild(messageDiv);
// 滚动到底部
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
});

View File

@@ -0,0 +1,671 @@
// 对话历史模块
Object.assign(TSPDashboard.prototype, {
async loadConversationTenantList() {
this.conversationCurrentTenantId = null;
this.renderConversationBreadcrumb(null);
// 加载全局统计
this.loadConversationStats(null);
// 显示租户列表容器,隐藏详情容器
const tenantListEl = document.getElementById('conversation-tenant-list');
const tenantDetailEl = document.getElementById('conversation-tenant-detail');
if (tenantListEl) tenantListEl.style.display = '';
if (tenantDetailEl) tenantDetailEl.style.display = 'none';
// 显示加载中 spinner
if (tenantListEl) {
tenantListEl.innerHTML = '<div class="col-12 text-center py-4"><i class="fas fa-spinner fa-spin fa-2x"></i><p class="mt-2 text-muted">加载中...</p></div>';
}
try {
const response = await fetch('/api/conversations/tenants');
const tenants = await response.json();
if (!Array.isArray(tenants) || tenants.length === 0) {
if (tenantListEl) {
tenantListEl.innerHTML = '<div class="col-12 text-center py-4"><i class="fas fa-comments fa-2x text-muted"></i><p class="mt-2 text-muted">暂无对话会话数据</p></div>';
}
return;
}
const cardsHtml = tenants.map(t => {
const lastActive = t.last_active_time ? new Date(t.last_active_time).toLocaleString() : '无';
return `
<div class="col-md-4 col-sm-6 mb-3">
<div class="card h-100 shadow-sm" style="cursor:pointer" onclick="dashboard.loadConversationTenantDetail('${t.tenant_id}')">
<div class="card-body">
<h6 class="card-title"><i class="fas fa-building me-2"></i>${t.tenant_id}</h6>
<div class="d-flex justify-content-between mt-3">
<div>
<small class="text-muted">会话总数</small>
<h5 class="mb-0">${t.session_count}</h5>
</div>
<div>
<small class="text-muted">消息总数</small>
<h5 class="mb-0">${t.message_count}</h5>
</div>
<div>
<small class="text-muted">活跃会话</small>
<h5 class="mb-0 text-success">${t.active_session_count}</h5>
</div>
</div>
<div class="mt-3">
<small class="text-muted"><i class="fas fa-clock me-1"></i>最近活跃: ${lastActive}</small>
</div>
</div>
</div>
</div>
`;
}).join('');
if (tenantListEl) {
tenantListEl.innerHTML = cardsHtml;
}
} catch (error) {
console.error('加载对话租户列表失败:', error);
if (tenantListEl) {
tenantListEl.innerHTML = '<div class="col-12 text-center py-4 text-danger"><i class="fas fa-exclamation-triangle"></i> 加载失败</div>';
}
this.showNotification('加载对话租户列表失败', 'error');
}
},
// Task 7.1: 加载对话历史租户详情视图
async loadConversationTenantDetail(tenantId, page = 1) {
this.conversationCurrentTenantId = tenantId;
this.paginationConfig.currentConversationPage = page;
// 隐藏租户列表,显示详情容器
const tenantListEl = document.getElementById('conversation-tenant-list');
const tenantDetailEl = document.getElementById('conversation-tenant-detail');
if (tenantListEl) tenantListEl.style.display = 'none';
if (tenantDetailEl) tenantDetailEl.style.display = '';
// 渲染面包屑(如果 renderConversationBreadcrumb 已实现)
if (typeof this.renderConversationBreadcrumb === 'function') {
this.renderConversationBreadcrumb(tenantId);
}
// 加载租户级统计(如果 loadConversationStats 已实现)
if (typeof this.loadConversationStats === 'function') {
this.loadConversationStats(tenantId);
}
// 显示加载中 spinner
const sessionListEl = document.getElementById('conversation-session-list');
if (sessionListEl) {
sessionListEl.innerHTML = '<div class="text-center py-4"><i class="fas fa-spinner fa-spin fa-2x"></i><p class="mt-2 text-muted">加载中...</p></div>';
}
try {
const perPage = this.getPageSize('conversation-session-pagination');
const statusFilter = document.getElementById('conversation-status-filter')?.value || '';
const dateFilter = document.getElementById('conversation-detail-date-filter')?.value || '';
let url = `/api/conversations/sessions?tenant_id=${encodeURIComponent(tenantId)}&page=${page}&per_page=${perPage}`;
if (statusFilter) url += `&status=${encodeURIComponent(statusFilter)}`;
if (dateFilter) url += `&date_filter=${encodeURIComponent(dateFilter)}`;
const response = await fetch(url);
const data = await response.json();
if (data.sessions) {
this.renderConversationSessionTable(data.sessions, tenantId);
this.updateConversationTenantPagination(data);
} else {
if (sessionListEl) {
sessionListEl.innerHTML = '<div class="text-center py-4"><i class="fas fa-comments fa-2x text-muted"></i><p class="mt-2 text-muted">暂无会话数据</p></div>';
}
}
} catch (error) {
console.error('加载租户会话列表失败:', error);
if (sessionListEl) {
sessionListEl.innerHTML = '<div class="text-center py-4 text-danger"><i class="fas fa-exclamation-triangle"></i> 加载失败</div>';
}
this.showNotification('加载租户会话列表失败', 'error');
}
},
// Task 7.1: 渲染会话表格
renderConversationSessionTable(sessions, tenantId) {
const sessionListEl = document.getElementById('conversation-session-list');
if (!sessionListEl) return;
if (!sessions || sessions.length === 0) {
sessionListEl.innerHTML = '<div class="text-center py-4"><i class="fas fa-comments fa-2x text-muted"></i><p class="mt-2 text-muted">暂无会话数据</p></div>';
return;
}
const statusBadge = (status) => {
if (status === 'active') return '<span class="badge bg-success">活跃</span>';
if (status === 'ended') return '<span class="badge bg-secondary">已结束</span>';
return `<span class="badge bg-info">${status || '未知'}</span>`;
};
const sourceBadge = (source) => {
if (source === 'feishu') return '<span class="badge bg-purple">飞书</span>';
if (source === 'websocket') return '<span class="badge bg-blue">WebSocket</span>';
if (source === 'api') return '<span class="badge bg-indigo">API</span>';
return `<span class="badge bg-secondary">${source || '未知'}</span>`;
};
const formatTime = (isoStr) => {
if (!isoStr) return '-';
return new Date(isoStr).toLocaleString();
};
const rowsHtml = sessions.map(s => `
<tr style="cursor:pointer" onclick="dashboard.viewSessionMessages('${s.session_id}', '${(s.title || '').replace(/'/g, "\\'")}')">
<td>${s.title || s.session_id || '-'}</td>
<td class="text-center">${s.message_count || 0}</td>
<td class="text-center">${statusBadge(s.status)}</td>
<td class="text-center">${sourceBadge(s.source)}</td>
<td>${formatTime(s.created_at)}</td>
<td>${formatTime(s.updated_at)}</td>
<td class="text-center" onclick="event.stopPropagation()">
<button class="btn btn-outline-danger btn-sm" onclick="dashboard.deleteConversationSession('${s.session_id}')">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`).join('');
sessionListEl.innerHTML = `
<div class="table-responsive">
<table class="table table-hover table-striped mb-0">
<thead>
<tr>
<th>会话标题</th>
<th class="text-center">消息数</th>
<th class="text-center">状态</th>
<th class="text-center">来源</th>
<th>创建时间</th>
<th>更新时间</th>
<th class="text-center">操作</th>
</tr>
</thead>
<tbody>
${rowsHtml}
</tbody>
</table>
</div>
`;
},
// Task 7.1: 查看会话消息详情
async viewSessionMessages(sessionId, sessionTitle) {
try {
const response = await fetch(`/api/conversations/sessions/${encodeURIComponent(sessionId)}`);
const data = await response.json();
if (data.success && data.messages) {
// 更新面包屑到第三层(如果已实现)
if (typeof this.renderConversationBreadcrumb === 'function') {
this.renderConversationBreadcrumb(this.conversationCurrentTenantId, sessionTitle || sessionId);
}
this.showSessionMessagesModal(data.session, data.messages);
} else {
throw new Error(data.error || '获取会话消息失败');
}
} catch (error) {
console.error('获取会话消息失败:', error);
this.showNotification('获取会话消息失败: ' + error.message, 'error');
}
},
// Task 7.1: 显示会话消息模态框
showSessionMessagesModal(session, messages) {
const messagesHtml = messages.map(msg => `
<div class="mb-3 p-3 border rounded">
<div class="mb-2">
<strong class="text-primary"><i class="fas fa-user me-1"></i>用户:</strong>
<div class="mt-1">${msg.user_message || ''}</div>
</div>
<div>
<strong class="text-success"><i class="fas fa-robot me-1"></i>助手:</strong>
<div class="mt-1">${msg.assistant_response || ''}</div>
</div>
<div class="mt-2 text-muted">
<small>
${msg.timestamp ? new Date(msg.timestamp).toLocaleString() : ''}
${msg.response_time ? ` | 响应: ${msg.response_time}s` : ''}
${msg.confidence_score ? ` | 置信度: ${Math.round(msg.confidence_score * 100)}%` : ''}
</small>
</div>
</div>
`).join('');
const modalHtml = `
<div class="modal fade" id="sessionMessagesModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-comments me-2"></i>${session?.title || session?.session_id || '会话详情'}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3 text-muted">
<small>
会话ID: ${session?.session_id || '-'} |
消息数: ${session?.message_count || 0} |
状态: ${session?.status || '-'} |
来源: ${session?.source || '-'}
</small>
</div>
${messages.length > 0 ? messagesHtml : '<div class="text-center text-muted py-3">暂无消息记录</div>'}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
`;
// 移除已存在的模态框
const existingModal = document.getElementById('sessionMessagesModal');
if (existingModal) existingModal.remove();
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('sessionMessagesModal'));
modal.show();
// 模态框关闭后清理
document.getElementById('sessionMessagesModal').addEventListener('hidden.bs.modal', function () {
this.remove();
});
},
// Task 7.1: 对话租户详情分页
updateConversationTenantPagination(data) {
this.createPaginationComponent(data, 'conversation-session-pagination', 'loadConversationTenantDetailPage', '条会话');
},
// Task 7.2: 面包屑导航
renderConversationBreadcrumb(tenantId, sessionTitle) {
const breadcrumbEl = document.getElementById('conversation-breadcrumb');
if (!breadcrumbEl) return;
if (!tenantId) {
breadcrumbEl.innerHTML = '';
return;
}
if (!sessionTitle) {
// 租户详情视图: "对话历史 > {tenant_id}"
breadcrumbEl.innerHTML = `
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="#" onclick="dashboard.loadConversationTenantList(); return false;"><i class="fas fa-history me-1"></i>对话历史</a></li>
<li class="breadcrumb-item active" aria-current="page">${tenantId}</li>
</ol>
</nav>
`;
} else {
// 消息详情视图: "对话历史 > {tenant_id} > {session_title}"
breadcrumbEl.innerHTML = `
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="#" onclick="dashboard.loadConversationTenantList(); return false;"><i class="fas fa-history me-1"></i>对话历史</a></li>
<li class="breadcrumb-item"><a href="#" onclick="dashboard.loadConversationTenantDetail('${tenantId}'); return false;">${tenantId}</a></li>
<li class="breadcrumb-item active" aria-current="page">${sessionTitle}</li>
</ol>
</nav>
`;
}
this.conversationCurrentTenantId = tenantId;
},
// Task 7.1: 应用对话筛选条件
applyConversationFilters() {
if (this.conversationCurrentTenantId) {
this.loadConversationTenantDetail(this.conversationCurrentTenantId, 1);
}
},
// Task 7.3: 删除会话并处理空租户自动返回
async deleteConversationSession(sessionId) {
if (!confirm('确定要删除这个会话及其所有消息吗?')) return;
try {
const response = await fetch(`/api/conversations/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
this.showNotification('会话已删除', 'success');
// 刷新当前租户详情视图并检查是否还有剩余会话
if (this.conversationCurrentTenantId) {
const tenantId = this.conversationCurrentTenantId;
const perPage = this.getPageSize('conversation-session-pagination');
const checkUrl = `/api/conversations/sessions?tenant_id=${encodeURIComponent(tenantId)}&page=1&per_page=${perPage}`;
const checkResp = await fetch(checkUrl);
const checkData = await checkResp.json();
if (!checkData.sessions || checkData.sessions.length === 0 || checkData.total === 0) {
// 该租户下已无会话,自动返回租户列表视图
this.loadConversationTenantList();
} else {
// 仍有会话,刷新当前详情视图(调整页码避免越界)
const maxPage = checkData.total_pages || 1;
const currentPage = Math.min(this.paginationConfig.currentConversationPage, maxPage);
await this.loadConversationTenantDetail(tenantId, currentPage);
}
}
} else {
throw new Error(data.error || '删除失败');
}
} catch (error) {
console.error('删除会话失败:', error);
this.showNotification('删除会话失败: ' + error.message, 'error');
}
},
// 对话历史管理
async loadConversationHistory(page = 1) {
try {
const pageSize = this.getPageSize('conversations-pagination');
const response = await fetch(`/api/conversations?page=${page}&per_page=${pageSize}`);
const data = await response.json();
if (data.conversations) {
this.renderConversationList(data.conversations || []);
this.updateConversationPagination(data);
this.updateConversationStats(data.stats || {});
} else {
throw new Error(data.error || '加载对话历史失败');
}
} catch (error) {
console.error('加载对话历史失败:', error);
this.showNotification('加载对话历史失败: ' + error.message, 'error');
}
},
renderConversationList(conversations) {
const container = document.getElementById('conversation-list');
if (!conversations || conversations.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-comments"></i>
<p>暂无对话记录</p>
</div>
`;
return;
}
const html = conversations.map(conv => {
// 识别来源和类型
let sourceBadge = '';
const method = conv.invocation_method || '';
const userId = conv.user_id || '';
// 判断是否来自飞书 (根据调用方式或ID格式)
if (method.includes('feishu') || userId.startsWith('ou_')) {
sourceBadge = '<span class="badge bg-purple me-1">飞书</span>';
} else {
sourceBadge = '<span class="badge bg-blue me-1">Web</span>';
}
// 判断群聊/私聊
let typeBadge = '';
if (method.includes('group')) {
typeBadge = '<span class="badge bg-azure me-1">群聊</span>';
} else if (method.includes('p2p')) {
typeBadge = '<span class="badge bg-indigo me-1">私聊</span>';
}
return `
<div class="card mb-3 conversation-item" data-conversation-id="${conv.id}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<h6 class="mb-1" style="font-family: var(--font-family-primary); font-weight: 600;">
${sourceBadge}
${typeBadge}
用户: ${userId || '匿名'}
</h6>
<small class="text-muted">${new Date(conv.timestamp).toLocaleString()}</small>
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="dashboard.viewConversation(${conv.id})">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-outline-danger" onclick="dashboard.deleteConversation(${conv.id})">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="conversation-preview">
<p class="mb-1"><strong>用户:</strong> ${conv.user_message?.substring(0, 100)}${conv.user_message?.length > 100 ? '...' : ''}</p>
<p class="mb-0"><strong>助手:</strong> ${conv.assistant_response?.substring(0, 100)}${conv.assistant_response?.length > 100 ? '...' : ''}</p>
</div>
<div class="mt-2">
<span class="badge bg-secondary">响应时间: ${conv.response_time || 0}ms</span>
${conv.work_order_id ? `<span class="badge bg-info">工单: ${conv.work_order_id}</span>` : ''}
</div>
</div>
</div>
`;
}).join('');
container.innerHTML = html;
},
updateConversationPagination(data) {
this.createPaginationComponent(data, 'conversations-pagination', 'loadConversationHistory', '条对话');
},
updateConversationStats(stats) {
document.getElementById('conversation-total').textContent = stats.total || 0;
document.getElementById('conversation-today').textContent = stats.today || 0;
document.getElementById('conversation-avg-response').textContent = `${stats.avg_response_time || 0}ms`;
document.getElementById('conversation-active-users').textContent = stats.active_users || 0;
},
// Task 8.3: 加载对话统计(支持租户级别)
async loadConversationStats(tenantId) {
try {
let url = '/api/conversations/analytics';
if (tenantId) {
url += `?tenant_id=${encodeURIComponent(tenantId)}`;
}
const response = await fetch(url);
const data = await response.json();
const analytics = data.analytics || {};
const convStats = analytics.conversations || {};
const totalEl = document.getElementById('conversation-total');
const todayEl = document.getElementById('conversation-today');
const avgResponseEl = document.getElementById('conversation-avg-response');
const activeUsersEl = document.getElementById('conversation-active-users');
if (totalEl) totalEl.textContent = convStats.total || 0;
if (todayEl) todayEl.textContent = convStats.today || 0;
if (avgResponseEl) avgResponseEl.textContent = `${Math.round(convStats.avg_response_time || 0)}ms`;
if (activeUsersEl) activeUsersEl.textContent = convStats.active_users || 0;
} catch (error) {
console.error('加载对话统计失败:', error);
}
},
async refreshConversationHistory() {
// 先尝试触发一次合并迁移(幂等,重复调用也安全)
try {
await fetch('/api/conversations/migrate-merge', { method: 'POST' });
} catch (e) { /* 忽略迁移失败 */ }
// 根据当前视图状态刷新:租户详情视图或租户列表视图
if (this.conversationCurrentTenantId) {
await this.loadConversationTenantDetail(this.conversationCurrentTenantId, this.paginationConfig.currentConversationPage);
} else {
await this.loadConversationTenantList();
}
this.showNotification('对话历史已刷新', 'success');
},
async clearAllConversations() {
if (!confirm('确定要清空所有对话历史吗?此操作不可恢复!')) {
return;
}
try {
const response = await fetch('/api/conversations/clear', { method: 'DELETE' });
const data = await response.json();
if (data.success) {
this.showNotification('对话历史已清空', 'success');
await this.loadConversationHistory();
} else {
throw new Error(data.error || '清空对话历史失败');
}
} catch (error) {
console.error('清空对话历史失败:', error);
this.showNotification('清空对话历史失败: ' + error.message, 'error');
}
},
async deleteConversation(conversationId) {
if (!confirm('确定要删除这条对话记录吗?')) {
return;
}
try {
const response = await fetch(`/api/conversations/${conversationId}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
this.showNotification('对话记录已删除', 'success');
await this.loadConversationHistory();
} else {
throw new Error(data.error || '删除对话记录失败');
}
} catch (error) {
console.error('删除对话记录失败:', error);
this.showNotification('删除对话记录失败: ' + error.message, 'error');
}
},
async viewConversation(conversationId) {
try {
const response = await fetch(`/api/conversations/${conversationId}`);
const data = await response.json();
if (data.success) {
data.user_id = data.user_id || '匿名';
this.showConversationModal(data);
} else {
throw new Error(data.error || '获取对话详情失败');
}
} catch (error) {
console.error('获取对话详情失败:', error);
this.showNotification('获取对话详情失败: ' + error.message, 'error');
}
},
showConversationModal(conversation) {
const modalHtml = `
<div class="modal fade" id="conversationModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">对话详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<strong>用户:</strong> ${conversation.user_id || '匿名'}<br>
<strong>时间:</strong> ${new Date(conversation.timestamp).toLocaleString()}<br>
<strong>响应时间:</strong> ${conversation.response_time || 0}ms
</div>
<div class="mb-3">
<h6>用户消息:</h6>
<div class="border p-3 rounded">${conversation.user_message || ''}</div>
</div>
<div class="mb-3">
<h6>助手回复:</h6>
<div class="border p-3 rounded">${conversation.assistant_response || ''}</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
`;
// 移除已存在的模态框
const existingModal = document.getElementById('conversationModal');
if (existingModal) {
existingModal.remove();
}
// 添加新模态框
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('conversationModal'));
modal.show();
},
async filterConversations() {
const search = document.getElementById('conversation-search').value.trim();
const userFilter = document.getElementById('conversation-user-filter')?.value || '';
const dateFilter = document.getElementById('conversation-date-filter')?.value || '';
// Task 8.1: 在 Tenant_Detail_View 中搜索时自动附加 tenant_id 参数
if (this.conversationCurrentTenantId) {
if (!search) {
// 清空搜索时恢复当前租户的完整分页列表
this.loadConversationTenantDetail(this.conversationCurrentTenantId);
return;
}
try {
const perPage = this.getPageSize('conversation-session-pagination');
let url = `/api/conversations/sessions?search=${encodeURIComponent(search)}&tenant_id=${encodeURIComponent(this.conversationCurrentTenantId)}&page=1&per_page=${perPage}`;
if (dateFilter) url += `&date_filter=${encodeURIComponent(dateFilter)}`;
const response = await fetch(url);
const data = await response.json();
if (data.sessions) {
this.renderConversationSessionTable(data.sessions, this.conversationCurrentTenantId);
this.updateConversationTenantPagination(data);
} else {
const sessionListEl = document.getElementById('conversation-session-list');
if (sessionListEl) {
sessionListEl.innerHTML = '<div class="text-center py-4"><i class="fas fa-search fa-2x text-muted"></i><p class="mt-2 text-muted">未找到匹配的会话</p></div>';
}
const paginationEl = document.getElementById('conversation-session-pagination');
if (paginationEl) paginationEl.innerHTML = '';
}
} catch (error) {
console.error('搜索租户会话失败:', error);
this.showNotification('搜索会话失败: ' + error.message, 'error');
}
return;
}
// 非租户详情视图:保持原有行为
try {
const params = new URLSearchParams();
if (search) params.append('search', search);
if (userFilter) params.append('user_id', userFilter);
if (dateFilter) params.append('date_filter', dateFilter);
const response = await fetch(`/api/conversations?${params.toString()}`);
const data = await response.json();
if (data.success) {
this.renderConversationList(data.conversations || []);
this.renderConversationPagination(data.pagination || {});
} else {
throw new Error(data.error || '筛选对话失败');
}
} catch (error) {
console.error('筛选对话失败:', error);
this.showNotification('筛选对话失败: ' + error.message, 'error');
}
}
});

View File

@@ -0,0 +1,286 @@
// Dashboard Home Module - 仪表板首页相关方法
Object.assign(TSPDashboard.prototype, {
async loadDashboardData() {
try {
const [sessionsResponse, alertsResponse, workordersResponse, knowledgeResponse] = await Promise.all([
fetch('/api/chat/sessions'),
fetch('/api/alerts?per_page=1000'),
fetch('/api/workorders'),
fetch('/api/knowledge/stats')
]);
const sessions = await sessionsResponse.json();
const alerts = await alertsResponse.json();
const workorders = await workordersResponse.json();
const knowledge = await knowledgeResponse.json();
document.getElementById('total-sessions').textContent = sessions.sessions?.length || 0;
document.getElementById('total-alerts').textContent = alerts.alerts?.length || 0;
document.getElementById('total-workorders').textContent = workorders.workorders?.filter(w => w.status === 'open').length || 0;
document.getElementById('knowledge-count').textContent = knowledge.total_entries || 0;
if (alerts.alerts) {
this.updateAlertStatistics(alerts.alerts);
}
document.getElementById('knowledge-total').textContent = knowledge.total_entries || 0;
document.getElementById('knowledge-active').textContent = knowledge.active_entries || 0;
const confidencePercent = Math.round((knowledge.average_confidence || 0) * 100);
document.getElementById('knowledge-confidence-bar').style.width = `${confidencePercent}%`;
document.getElementById('knowledge-confidence-bar').setAttribute('aria-valuenow', confidencePercent);
document.getElementById('knowledge-confidence-bar').textContent = `${confidencePercent}%`;
await this.updatePerformanceChart(sessions, alerts, workorders);
await this.updateSystemHealth();
await this.loadAnalytics();
} catch (error) {
console.error('加载仪表板数据失败:', error);
}
},
initCharts() {
const performanceCtx = document.getElementById('performanceChart');
if (performanceCtx) {
this.charts.performance = new Chart(performanceCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '工单数量',
data: [],
borderColor: '#007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
tension: 0.4
}, {
label: '预警数量',
data: [],
borderColor: '#dc3545',
backgroundColor: 'rgba(220, 53, 69, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
const analyticsCtx = document.getElementById('analyticsChart');
if (analyticsCtx) {
this.charts.analytics = new Chart(analyticsCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '满意度',
data: [],
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.4
}, {
label: '解决时间(小时)',
data: [],
borderColor: '#ffc107',
backgroundColor: 'rgba(255, 193, 7, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
const categoryCtx = document.getElementById('categoryChart');
if (categoryCtx) {
this.charts.category = new Chart(categoryCtx, {
type: 'doughnut',
data: {
labels: [],
datasets: [{
data: [],
backgroundColor: [
'#007bff',
'#28a745',
'#ffc107',
'#dc3545',
'#17a2b8'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
}
},
async updatePerformanceChart(sessions, alerts, workorders) {
if (!this.charts.performance) return;
try {
const response = await fetch('/api/analytics?days=7&dimension=performance');
const analyticsData = await response.json();
if (analyticsData.trend && analyticsData.trend.length > 0) {
const labels = analyticsData.trend.map(item => {
const date = new Date(item.date);
return `${date.getMonth() + 1}/${date.getDate()}`;
});
const workorderData = analyticsData.trend.map(item => item.workorders || 0);
const alertData = analyticsData.trend.map(item => item.alerts || 0);
this.charts.performance.data.labels = labels;
this.charts.performance.data.datasets[0].data = workorderData;
this.charts.performance.data.datasets[1].data = alertData;
this.charts.performance.update();
} else {
this.charts.performance.data.labels = ['暂无数据'];
this.charts.performance.data.datasets[0].data = [0];
this.charts.performance.data.datasets[1].data = [0];
this.charts.performance.update();
}
} catch (error) {
console.error('获取性能趋势数据失败:', error);
this.charts.performance.data.labels = ['数据加载失败'];
this.charts.performance.data.datasets[0].data = [0];
this.charts.performance.data.datasets[1].data = [0];
this.charts.performance.update();
}
},
async updateSystemHealth() {
try {
const response = await fetch('/api/settings');
const settings = await response.json();
const healthScore = Math.max(0, 100 - (settings.memory_usage_percent || 0) - (settings.cpu_usage_percent || 0));
const healthProgress = document.getElementById('health-progress');
const healthDot = document.getElementById('system-health-dot');
const healthText = document.getElementById('system-health-text');
if (healthProgress) {
healthProgress.style.width = `${healthScore}%`;
healthProgress.setAttribute('aria-valuenow', healthScore);
}
if (healthDot) {
healthDot.className = 'health-dot';
if (healthScore >= 80) healthDot.classList.add('excellent');
else if (healthScore >= 60) healthDot.classList.add('good');
else if (healthScore >= 40) healthDot.classList.add('fair');
else if (healthScore >= 20) healthDot.classList.add('poor');
else healthDot.classList.add('critical');
}
if (healthText) {
const statusText = healthScore >= 80 ? '优秀' :
healthScore >= 60 ? '良好' :
healthScore >= 40 ? '一般' :
healthScore >= 20 ? '较差' : '严重';
healthText.textContent = `${statusText} (${healthScore}%)`;
}
const memoryProgress = document.getElementById('memory-progress');
if (memoryProgress && settings.memory_usage_percent !== undefined) {
memoryProgress.style.width = `${settings.memory_usage_percent}%`;
memoryProgress.setAttribute('aria-valuenow', settings.memory_usage_percent);
}
const cpuProgress = document.getElementById('cpu-progress');
if (cpuProgress && settings.cpu_usage_percent !== undefined) {
cpuProgress.style.width = `${settings.cpu_usage_percent}%`;
cpuProgress.setAttribute('aria-valuenow', settings.cpu_usage_percent);
}
} catch (error) {
console.error('更新系统健康状态失败:', error);
}
},
async loadHealth() {
try {
const response = await fetch('/api/health');
const data = await response.json();
this.updateHealthDisplay(data);
} catch (error) {
console.error('加载健康状态失败:', error);
}
},
updateHealthDisplay(health) {
const healthScore = health.health_score || 0;
const healthStatus = health.status || 'unknown';
const healthDot = document.getElementById('health-dot');
const healthStatusText = document.getElementById('health-status');
const systemHealthDot = document.getElementById('system-health-dot');
const systemHealthText = document.getElementById('system-health-text');
const healthProgress = document.getElementById('health-progress');
if (healthDot) {
healthDot.className = `health-dot ${healthStatus}`;
}
if (healthStatusText) {
healthStatusText.textContent = this.getHealthStatusText(healthStatus);
}
if (systemHealthDot) {
systemHealthDot.className = `health-dot ${healthStatus}`;
}
if (systemHealthText) {
systemHealthText.textContent = this.getHealthStatusText(healthStatus);
}
if (healthProgress) {
healthProgress.style.width = `${healthScore * 100}%`;
healthProgress.className = `progress-bar ${this.getHealthColor(healthScore)}`;
}
const memoryUsage = health.memory_usage || 0;
const memoryProgress = document.getElementById('memory-progress');
if (memoryProgress) {
memoryProgress.style.width = `${memoryUsage}%`;
}
const cpuUsage = health.cpu_usage || 0;
const cpuProgress = document.getElementById('cpu-progress');
if (cpuProgress) {
cpuProgress.style.width = `${cpuUsage}%`;
}
},
getHealthStatusText(status) {
const statusMap = {
'excellent': '优秀',
'good': '良好',
'fair': '一般',
'poor': '较差',
'critical': '严重',
'unknown': '未知'
};
return statusMap[status] || status;
},
getHealthColor(score) {
if (score >= 0.8) return 'bg-success';
if (score >= 0.6) return 'bg-info';
if (score >= 0.4) return 'bg-warning';
return 'bg-danger';
}
});

View File

@@ -0,0 +1,698 @@
class FeishuSyncManager {
constructor() {
this.loadConfig();
this.refreshStatus();
}
async loadConfig() {
try {
const response = await fetch('/api/feishu-sync/config');
const data = await response.json();
if (data.success) {
const config = data.config;
document.getElementById('appId').value = config.feishu.app_id || '';
document.getElementById('appSecret').value = '';
document.getElementById('appToken').value = config.feishu.app_token || '';
document.getElementById('tableId').value = config.feishu.table_id || '';
// 显示配置状态
const statusBadge = config.feishu.status === 'active' ?
'<span class="badge bg-success">已配置</span>' :
'<span class="badge bg-warning">未配置</span>';
// 可以在这里添加状态显示
}
} catch (error) {
console.error('加载配置失败:', error);
}
}
async saveConfig() {
const config = {
app_id: document.getElementById('appId').value,
app_secret: document.getElementById('appSecret').value,
app_token: document.getElementById('appToken').value,
table_id: document.getElementById('tableId').value
};
if (!config.app_id || !config.app_secret || !config.app_token || !config.table_id) {
this.showNotification('请填写完整的配置信息', 'error');
return;
}
try {
const response = await fetch('/api/feishu-sync/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
const data = await response.json();
if (data.success) {
this.showNotification('配置保存成功', 'success');
} else {
this.showNotification('配置保存失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('配置保存失败: ' + error.message, 'error');
}
}
async testConnection() {
try {
this.showNotification('正在测试连接...', 'info');
const response = await fetch('/api/feishu-sync/test-connection');
const data = await response.json();
if (data.success) {
this.showNotification('飞书连接正常', 'success');
} else {
this.showNotification('连接失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('连接测试失败: ' + error.message, 'error');
}
}
async syncFromFeishu() {
try {
const limit = document.getElementById('syncLimit').value;
this.showNotification('开始从飞书同步数据...', 'info');
this.showProgress(true);
const response = await fetch('/api/feishu-sync/sync-from-feishu', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
generate_ai_suggestions: false,
limit: parseInt(limit)
})
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
this.addSyncLog(data.message);
this.refreshStatus();
} else {
this.showNotification('同步失败: ' + data.error, 'error');
this.addSyncLog('同步失败: ' + data.error);
}
} catch (error) {
this.showNotification('同步失败: ' + error.message, 'error');
this.addSyncLog('同步失败: ' + error.message);
} finally {
this.showProgress(false);
}
}
async syncWithAI() {
try {
const limit = document.getElementById('syncLimit').value;
this.showNotification('开始同步数据并生成AI建议...', 'info');
this.showProgress(true);
const response = await fetch('/api/feishu-sync/sync-from-feishu', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
generate_ai_suggestions: true,
limit: parseInt(limit)
})
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
this.addSyncLog(data.message);
this.refreshStatus();
} else {
this.showNotification('同步失败: ' + data.error, 'error');
this.addSyncLog('同步失败: ' + data.error);
}
} catch (error) {
this.showNotification('同步失败: ' + error.message, 'error');
this.addSyncLog('同步失败: ' + error.message);
} finally {
this.showProgress(false);
}
}
// 打开字段映射管理页面
openFieldMapping() {
const section = document.getElementById('fieldMappingSection');
if (section.style.display === 'none') {
section.style.display = 'block';
// 自动加载映射状态
this.loadMappingStatus();
} else {
section.style.display = 'none';
}
}
async previewFeishuData() {
try {
this.showNotification('正在获取飞书数据预览...', 'info');
const response = await fetch('/api/feishu-sync/preview-feishu-data');
const data = await response.json();
if (data.success) {
this.displayPreviewData(data.preview_data);
this.showNotification(`获取到 ${data.total_count} 条预览数据`, 'success');
} else {
this.showNotification('获取预览数据失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('获取预览数据失败: ' + error.message, 'error');
}
}
displayPreviewData(data) {
const tbody = document.querySelector('#previewTable tbody');
tbody.innerHTML = '';
data.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${item.record_id}</td>
<td>${item.fields['TR Number'] || '-'}</td>
<td>${item.fields['TR Description'] || '-'}</td>
<td>${item.fields['Type of problem'] || '-'}</td>
<td>${item.fields['Source'] || '-'}</td>
<td>${item.fields['TR (Priority/Status)'] || '-'}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="feishuSync.createWorkorder('${item.record_id}')">
<i class="fas fa-plus"></i> 创建工单
</button>
</td>
`;
tbody.appendChild(row);
});
document.getElementById('previewSection').style.display = 'block';
}
async refreshStatus() {
try {
const response = await fetch('/api/feishu-sync/status');
const data = await response.json();
if (data.success) {
const status = data.status;
document.getElementById('totalLocalWorkorders').textContent = status.total_local_workorders || 0;
document.getElementById('syncedWorkorders').textContent = status.synced_workorders || 0;
document.getElementById('unsyncedWorkorders').textContent = status.unsynced_workorders || 0;
}
} catch (error) {
console.error('刷新状态失败:', error);
}
}
showProgress(show) {
const progress = document.getElementById('syncProgress');
if (show) {
progress.style.display = 'block';
const bar = progress.querySelector('.progress-bar');
bar.style.width = '100%';
} else {
setTimeout(() => {
progress.style.display = 'none';
const bar = progress.querySelector('.progress-bar');
bar.style.width = '0%';
}, 1000);
}
}
addSyncLog(message) {
const log = document.getElementById('syncLog');
const timestamp = new Date().toLocaleString();
const logEntry = document.createElement('div');
logEntry.innerHTML = `<small class="text-muted">[${timestamp}]</small> ${message}`;
if (log.querySelector('.text-muted')) {
log.innerHTML = '';
}
log.appendChild(logEntry);
log.scrollTop = log.scrollHeight;
}
async exportConfig() {
try {
const response = await fetch('/api/feishu-sync/config/export');
const data = await response.json();
if (data.success) {
// 创建下载链接
const blob = new Blob([data.config], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `feishu_config_${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showNotification('配置导出成功', 'success');
} else {
this.showNotification('配置导出失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('配置导出失败: ' + error.message, 'error');
}
}
showImportModal() {
const modal = new bootstrap.Modal(document.getElementById('importConfigModal'));
modal.show();
}
async importConfig() {
try {
const configJson = document.getElementById('configJson').value.trim();
if (!configJson) {
this.showNotification('请输入配置JSON数据', 'warning');
return;
}
const response = await fetch('/api/feishu-sync/config/import', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ config: configJson })
});
const data = await response.json();
if (data.success) {
this.showNotification('配置导入成功', 'success');
this.loadConfig();
this.refreshStatus();
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('importConfigModal'));
modal.hide();
document.getElementById('configJson').value = '';
} else {
this.showNotification('配置导入失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('配置导入失败: ' + error.message, 'error');
}
}
async resetConfig() {
if (confirm('确定要重置所有配置吗?此操作不可撤销!')) {
try {
const response = await fetch('/api/feishu-sync/config/reset', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
this.showNotification('配置重置成功', 'success');
this.loadConfig();
this.refreshStatus();
} else {
this.showNotification('配置重置失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('配置重置失败: ' + error.message, 'error');
}
}
}
async createWorkorder(recordId) {
if (confirm(`确定要从飞书记录 ${recordId} 创建工单吗?`)) {
try {
this.showNotification('正在创建工单...', 'info');
const response = await fetch('/api/feishu-sync/create-workorder', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
record_id: recordId
})
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
// 刷新工单列表(如果用户在工单页面)
if (typeof window.refreshWorkOrders === 'function') {
window.refreshWorkOrders();
}
} else {
this.showNotification('创建工单失败: ' + data.message, 'error');
}
} catch (error) {
this.showNotification('创建工单失败: ' + error.message, 'error');
}
}
}
// 字段映射管理方法
async discoverFields() {
try {
this.showNotification('正在发现字段...', 'info');
const response = await fetch('/api/feishu-sync/field-mapping/discover', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ limit: 5 })
});
const data = await response.json();
if (data.success) {
this.displayDiscoveryResults(data.discovery_report);
this.showNotification('字段发现完成', 'success');
} else {
this.showNotification('字段发现失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('字段发现失败: ' + error.message, 'error');
}
}
displayDiscoveryResults(report) {
const container = document.getElementById('fieldMappingContent');
let html = '';
// 已映射字段
if (report.mapped_fields && Object.keys(report.mapped_fields).length > 0) {
html += '<div class="mb-3"><h6 class="text-success"><i class="fas fa-check-circle"></i> 已映射字段</h6>';
for (const [feishuField, localField] of Object.entries(report.mapped_fields)) {
html += `<div class="alert alert-success py-2">
<strong>${feishuField}</strong> → <span class="badge bg-success">${localField}</span>
</div>`;
}
html += '</div>';
}
// 未映射字段和建议
if (report.unmapped_fields && report.unmapped_fields.length > 0) {
html += '<div class="mb-3"><h6 class="text-warning"><i class="fas fa-exclamation-triangle"></i> 未映射字段</h6>';
for (const field of report.unmapped_fields) {
html += `<div class="alert alert-warning py-2">
<strong>${field}</strong>`;
const suggestions = report.suggested_mappings[field] || [];
if (suggestions.length > 0) {
html += '<div class="mt-2"><small class="text-muted">建议映射:</small>';
suggestions.slice(0, 2).forEach(suggestion => {
html += `<div class="mt-1">
<span class="badge bg-${suggestion.confidence === 'high' ? 'success' : 'warning'}">${suggestion.local_field}</span>
<small class="text-muted">(${suggestion.reason})</small>
<button class="btn btn-sm btn-outline-primary ms-2" onclick="feishuSync.applySuggestion('${field}', '${suggestion.local_field}')">应用</button>
</div>`;
});
html += '</div>';
}
html += '</div>';
}
html += '</div>';
}
container.innerHTML = html;
}
async applySuggestion(feishuField, localField) {
if (confirm(`确定要将 "${feishuField}" 映射到 "${localField}" 吗?`)) {
try {
const response = await fetch('/api/feishu-sync/field-mapping/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
feishu_field: feishuField,
local_field: localField,
priority: 3
})
});
const data = await response.json();
if (data.success) {
this.showNotification('映射添加成功!', 'success');
this.discoverFields(); // 重新发现字段
} else {
this.showNotification('添加映射失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('请求失败: ' + error.message, 'error');
}
}
}
async loadMappingStatus() {
try {
const response = await fetch('/api/feishu-sync/field-mapping/status');
const data = await response.json();
if (data.success) {
this.displayMappingStatus(data.status);
} else {
this.showNotification('获取映射状态失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('请求失败: ' + error.message, 'error');
}
}
displayMappingStatus(status) {
const container = document.getElementById('fieldMappingContent');
let html = '';
html += `<div class="row mb-3">
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-primary">${status.total_mappings}</h5>
<p class="card-text">直接映射</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-info">${status.total_aliases}</h5>
<p class="card-text">别名映射</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-warning">${status.total_patterns}</h5>
<p class="card-text">模式匹配</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title ${status.auto_mapping_enabled ? 'text-success' : 'text-danger'}">
${status.auto_mapping_enabled ? '启用' : '禁用'}
</h5>
<p class="card-text">自动映射</p>
</div>
</div>
</div>
</div>`;
// 显示当前映射
if (status.field_mapping && Object.keys(status.field_mapping).length > 0) {
html += '<h6>当前字段映射:</h6><div class="row">';
for (const [feishuField, localField] of Object.entries(status.field_mapping)) {
html += `<div class="col-md-6 mb-2">
<div class="alert alert-info py-2">
<strong>${feishuField}</strong> → <span class="badge bg-primary">${localField}</span>
<button class="btn btn-sm btn-outline-danger float-end" onclick="feishuSync.removeMapping('${feishuField}')">删除</button>
</div>
</div>`;
}
html += '</div>';
}
container.innerHTML = html;
}
async removeMapping(feishuField) {
if (confirm(`确定要删除映射 "${feishuField}" 吗?`)) {
try {
const response = await fetch('/api/feishu-sync/field-mapping/remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
feishu_field: feishuField
})
});
const data = await response.json();
if (data.success) {
this.showNotification('映射删除成功!', 'success');
this.loadMappingStatus(); // 刷新状态
} else {
this.showNotification('删除映射失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('请求失败: ' + error.message, 'error');
}
}
}
showAddMappingModal() {
// 简单的添加映射功能
const feishuField = prompt('请输入飞书字段名:');
if (!feishuField) return;
const localField = prompt('请输入本地字段名 (如: order_id, description, category):');
if (!localField) return;
this.addFieldMapping(feishuField, localField);
}
async addFieldMapping(feishuField, localField) {
try {
const response = await fetch('/api/feishu-sync/field-mapping/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
feishu_field: feishuField,
local_field: localField,
priority: 3
})
});
const data = await response.json();
if (data.success) {
this.showNotification('映射添加成功!', 'success');
this.loadMappingStatus(); // 刷新状态
} else {
this.showNotification('添加映射失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('请求失败: ' + error.message, 'error');
}
}
async checkPermissions() {
try {
this.showNotification('正在检查飞书权限...', 'info');
const response = await fetch('/api/feishu-sync/check-permissions');
const data = await response.json();
if (data.success) {
this.displayPermissionCheck(data.permission_check, data.summary);
this.showNotification('权限检查完成', 'success');
} else {
this.showNotification('权限检查失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('权限检查失败: ' + error.message, 'error');
}
}
displayPermissionCheck(permissionCheck, summary) {
const container = document.getElementById('fieldMappingContent');
let html = '<div class="card"><div class="card-header"><h6 class="card-title mb-0"><i class="fas fa-shield-alt me-2"></i>飞书权限检查结果</h6></div><div class="card-body">';
// 整体状态
const statusClass = permissionCheck.success ? 'success' : 'danger';
const statusIcon = permissionCheck.success ? 'check-circle' : 'exclamation-triangle';
html += `<div class="alert alert-${statusClass}">
<i class="fas fa-${statusIcon}"></i>
整体状态: ${permissionCheck.success ? '正常' : '异常'}
</div>`;
// 检查项目
html += '<h6>检查项目:</h6>';
for (const [checkName, checkResult] of Object.entries(permissionCheck.checks)) {
const statusClass = checkResult.status === 'success' ? 'success' :
checkResult.status === 'warning' ? 'warning' : 'danger';
const statusIcon = checkResult.status === 'success' ? 'check-circle' :
checkResult.status === 'warning' ? 'exclamation-triangle' : 'times-circle';
html += `<div class="alert alert-${statusClass} py-2">
<i class="fas fa-${statusIcon}"></i>
<strong>${checkName}</strong>: ${checkResult.message}
</div>`;
}
// 修复建议
if (permissionCheck.recommendations && permissionCheck.recommendations.length > 0) {
html += '<h6>修复建议:</h6><ul class="list-group mb-3">';
permissionCheck.recommendations.forEach(rec => {
html += `<li class="list-group-item">${rec}</li>`;
});
html += '</ul>';
}
// 错误信息
if (permissionCheck.errors && permissionCheck.errors.length > 0) {
html += '<h6>错误信息:</h6><ul class="list-group">';
permissionCheck.errors.forEach(error => {
html += `<li class="list-group-item list-group-item-danger">${error}</li>`;
});
html += '</ul>';
}
html += '</div></div>';
container.innerHTML = html;
// 显示字段映射管理区域
const section = document.getElementById('fieldMappingSection');
section.style.display = 'block';
}
showNotification(message, type = 'info') {
const container = document.getElementById('notificationContainer');
const alert = document.createElement('div');
alert.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show`;
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
container.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
}

View File

@@ -0,0 +1,699 @@
// 知识库管理模块
Object.assign(TSPDashboard.prototype, {
async loadKnowledge(page = 1) {
// 委托给租户列表视图
this.loadKnowledgeTenantList();
},
// Task 6.1: 加载租户列表视图
async loadKnowledgeTenantList() {
this.knowledgeCurrentTenantId = null;
this.renderKnowledgeBreadcrumb(null);
// 显示租户列表容器,隐藏详情容器
const tenantListEl = document.getElementById('knowledge-tenant-list');
const tenantDetailEl = document.getElementById('knowledge-tenant-detail');
const searchBar = document.getElementById('knowledge-search-bar');
const addBtn = document.getElementById('knowledge-add-btn');
const uploadBtn = document.getElementById('knowledge-upload-btn');
tenantListEl.style.display = '';
tenantDetailEl.style.display = 'none';
if (searchBar) searchBar.style.display = 'none';
if (addBtn) addBtn.style.display = 'none';
if (uploadBtn) uploadBtn.style.display = 'none';
// 显示加载中
tenantListEl.innerHTML = '<div class="col-12 text-center py-4"><i class="fas fa-spinner fa-spin fa-2x"></i><p class="mt-2 text-muted">加载中...</p></div>';
// 加载全局统计
this.loadKnowledgeStats(null);
try {
const response = await fetch('/api/knowledge/tenants');
const tenants = await response.json();
if (!Array.isArray(tenants) || tenants.length === 0) {
tenantListEl.innerHTML = '<div class="col-12 text-center py-4"><i class="fas fa-database fa-2x text-muted"></i><p class="mt-2 text-muted">暂无知识库数据</p></div>';
return;
}
const cardsHtml = tenants.map(t => `
<div class="col-md-4 col-sm-6 mb-3">
<div class="card h-100 shadow-sm" style="cursor:pointer" onclick="dashboard.openTenantDetail('${t.tenant_id}')">
<div class="card-body">
<h6 class="card-title"><i class="fas fa-building me-2"></i>${t.tenant_id}</h6>
<div class="d-flex justify-content-between mt-3">
<div>
<small class="text-muted">知识条目</small>
<h5 class="mb-0">${t.entry_count}</h5>
</div>
<div>
<small class="text-muted">已验证</small>
<h5 class="mb-0 text-success">${t.verified_count}</h5>
</div>
</div>
<div class="progress mt-3" style="height: 6px;">
<div class="progress-bar bg-success" style="width: ${t.entry_count > 0 ? Math.round(t.verified_count / t.entry_count * 100) : 0}%"></div>
</div>
<small class="text-muted">${t.entry_count > 0 ? Math.round(t.verified_count / t.entry_count * 100) : 0}% 已验证</small>
</div>
</div>
</div>
`).join('');
tenantListEl.innerHTML = cardsHtml;
} catch (error) {
console.error('加载租户列表失败:', error);
tenantListEl.innerHTML = '<div class="col-12 text-center py-4 text-danger"><i class="fas fa-exclamation-triangle"></i> 加载失败</div>';
this.showNotification('加载租户列表失败', 'error');
}
},
// Task 6.2: 刷新按钮
refreshKnowledge() {
if (this.knowledgeCurrentTenantId) {
this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, this.paginationConfig.currentKnowledgePage);
} else {
this.loadKnowledgeTenantList();
}
},
// Task 7.1: 加载租户详情视图
async loadKnowledgeTenantDetail(tenantId, page = 1) {
this.knowledgeCurrentTenantId = tenantId;
this.paginationConfig.currentKnowledgePage = page;
this.renderKnowledgeBreadcrumb(tenantId);
// 隐藏租户列表,显示详情容器
const tenantListEl = document.getElementById('knowledge-tenant-list');
const tenantDetailEl = document.getElementById('knowledge-tenant-detail');
const searchBar = document.getElementById('knowledge-search-bar');
const addBtn = document.getElementById('knowledge-add-btn');
const uploadBtn = document.getElementById('knowledge-upload-btn');
tenantListEl.style.display = 'none';
tenantDetailEl.style.display = '';
if (searchBar) searchBar.style.display = '';
if (addBtn) addBtn.style.display = '';
if (uploadBtn) uploadBtn.style.display = '';
// 清空搜索框
const searchInput = document.getElementById('knowledge-search');
if (searchInput) searchInput.value = '';
// 加载租户级统计
this.loadKnowledgeStats(tenantId);
const listEl = document.getElementById('knowledge-list');
listEl.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-spin"></i></div>';
try {
const pageSize = this.getPageSize('knowledge-pagination');
const categoryFilter = document.getElementById('knowledge-category-filter')?.value || '';
const verifiedFilter = document.getElementById('knowledge-verified-filter')?.value || '';
let url = `/api/knowledge?tenant_id=${encodeURIComponent(tenantId)}&page=${page}&per_page=${pageSize}`;
if (categoryFilter) url += `&category=${encodeURIComponent(categoryFilter)}`;
if (verifiedFilter) url += `&verified=${encodeURIComponent(verifiedFilter)}`;
const response = await fetch(url);
const data = await response.json();
if (data.knowledge) {
this.updateKnowledgeDisplay(data.knowledge);
this.updateKnowledgeTenantPagination(data);
} else {
this.updateKnowledgeDisplay(data);
}
} catch (error) {
console.error('加载租户详情失败:', error);
listEl.innerHTML = '<div class="text-center py-4 text-danger"><i class="fas fa-exclamation-triangle"></i> 加载失败</div>';
this.showNotification('加载租户详情失败', 'error');
}
},
// Task 7.1: 填充分类筛选下拉框(从统计数据获取完整分类列表)
populateCategoryFilter(categories) {
const select = document.getElementById('knowledge-category-filter');
if (!select) return;
const currentValue = select.value;
const sorted = categories.slice().sort();
const existingOptions = Array.from(select.options).slice(1).map(o => o.value);
if (JSON.stringify(sorted) !== JSON.stringify(existingOptions)) {
select.innerHTML = '<option value="">全部分类</option>' +
sorted.map(c => `<option value="${c}"${c === currentValue ? ' selected' : ''}>${c}</option>`).join('');
}
},
// Task 7.1: 从租户卡片进入详情(重置筛选条件)
openTenantDetail(tenantId) {
const categoryFilterEl = document.getElementById('knowledge-category-filter');
const verifiedFilterEl = document.getElementById('knowledge-verified-filter');
if (categoryFilterEl) categoryFilterEl.value = '';
if (verifiedFilterEl) verifiedFilterEl.value = '';
this.loadKnowledgeTenantDetail(tenantId, 1);
},
// Task 7.1: 应用筛选条件
applyKnowledgeFilters() {
if (this.knowledgeCurrentTenantId) {
this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, 1);
}
},
// Task 7.1: 租户详情分页
updateKnowledgeTenantPagination(data) {
this.createPaginationComponent(data, 'knowledge-pagination', 'loadKnowledgeTenantDetailPage', '条知识');
},
// Task 7.2: 面包屑导航
renderKnowledgeBreadcrumb(tenantId) {
const breadcrumbEl = document.getElementById('knowledge-breadcrumb');
if (!breadcrumbEl) return;
if (!tenantId) {
breadcrumbEl.innerHTML = '<h5 class="mb-0"><i class="fas fa-database me-2"></i>知识库</h5>';
} else {
breadcrumbEl.innerHTML = `
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="#" onclick="dashboard.loadKnowledgeTenantList(); return false;"><i class="fas fa-database me-1"></i>知识库</a></li>
<li class="breadcrumb-item active" aria-current="page">${tenantId}</li>
</ol>
</nav>
`;
}
},
// Task 7.3 / 8.2: 刷新当前知识库视图的辅助方法
async refreshKnowledgeCurrentView() {
if (this.knowledgeCurrentTenantId) {
await this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, this.paginationConfig.currentKnowledgePage);
} else {
await this.loadKnowledgeTenantList();
}
},
// Task 8.2: 加载知识库统计(支持租户级别)
async loadKnowledgeStats(tenantId) {
try {
let url = '/api/knowledge/stats';
if (tenantId) {
url += `?tenant_id=${encodeURIComponent(tenantId)}`;
}
const response = await fetch(url);
const knowledge = await response.json();
const totalEl = document.getElementById('knowledge-total');
const activeEl = document.getElementById('knowledge-active');
const confidenceBarEl = document.getElementById('knowledge-confidence-bar');
if (totalEl) totalEl.textContent = knowledge.total_entries || 0;
if (activeEl) activeEl.textContent = knowledge.active_entries || 0;
const confidencePercent = Math.round((knowledge.average_confidence || 0) * 100);
if (confidenceBarEl) {
confidenceBarEl.style.width = `${confidencePercent}%`;
confidenceBarEl.setAttribute('aria-valuenow', confidencePercent);
confidenceBarEl.textContent = `${confidencePercent}%`;
}
// Task 7.1: 在租户详情视图中,用统计数据填充分类筛选下拉框
if (tenantId && knowledge.category_distribution) {
this.populateCategoryFilter(Object.keys(knowledge.category_distribution));
}
} catch (error) {
console.error('加载知识库统计失败:', error);
}
},
updateKnowledgeDisplay(knowledge) {
const container = document.getElementById('knowledge-list');
if (knowledge.length === 0) {
container.innerHTML = '<div class="empty-state"><i class="fas fa-database"></i><p>暂无知识条目</p></div>';
return;
}
// 添加知识库列表的批量操作头部
const headerHtml = `
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<input type="checkbox" id="select-all-knowledge" class="form-check-input me-2" onchange="dashboard.toggleSelectAllKnowledge()">
<label for="select-all-knowledge" class="form-check-label">全选</label>
</div>
<div class="btn-group">
<button class="btn btn-sm btn-success" id="batch-verify-knowledge" onclick="dashboard.batchVerifyKnowledge()" disabled>
<i class="fas fa-check-circle me-1"></i>批量验证
</button>
<button class="btn btn-sm btn-warning" id="batch-unverify-knowledge" onclick="dashboard.batchUnverifyKnowledge()" disabled>
<i class="fas fa-times-circle me-1"></i>批量取消验证
</button>
<button class="btn btn-sm btn-danger" id="batch-delete-knowledge" onclick="dashboard.batchDeleteKnowledge()" disabled>
<i class="fas fa-trash me-1"></i>批量删除
</button>
</div>
</div>
`;
const knowledgeHtml = knowledge.map(item => `
<div class="knowledge-item">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex align-items-start">
<input type="checkbox" class="form-check-input me-2 knowledge-checkbox" value="${item.id}" onchange="dashboard.updateBatchDeleteKnowledgeButton()">
<div class="flex-grow-1">
<h6 class="mb-1">${item.question}</h6>
<p class="text-muted mb-2">${item.answer}</p>
<div class="d-flex gap-3">
<small class="text-muted">分类: ${item.category}</small>
<small class="text-muted">置信度: ${Math.round(item.confidence_score * 100)}%</small>
<small class="text-muted">使用次数: ${item.usage_count || 0}</small>
<span class="badge ${item.is_verified ? 'bg-success' : 'bg-warning'}">
${item.is_verified ? '已验证' : '未验证'}
</span>
</div>
</div>
</div>
<div class="ms-3">
<div class="btn-group" role="group">
${item.is_verified ?
`<button class="btn btn-sm btn-outline-warning" onclick="dashboard.unverifyKnowledge(${item.id})" title="取消验证">
<i class="fas fa-times-circle"></i>
<span class="d-none d-md-inline ms-1">取消验证</span>
</button>` :
`<button class="btn btn-sm btn-outline-success" onclick="dashboard.verifyKnowledge(${item.id})" title="验证">
<i class="fas fa-check-circle"></i>
<span class="d-none d-md-inline ms-1">验证</span>
</button>`
}
<button class="btn btn-sm btn-outline-danger" onclick="dashboard.deleteKnowledge(${item.id})" title="删除">
<i class="fas fa-trash"></i>
<span class="d-none d-md-inline ms-1">删除</span>
</button>
</div>
</div>
</div>
</div>
`).join('');
container.innerHTML = headerHtml + knowledgeHtml;
},
updateKnowledgePagination(data) {
this.createPaginationComponent(data, 'knowledge-pagination', 'loadKnowledge', '条知识');
},
async searchKnowledge() {
const query = document.getElementById('knowledge-search').value.trim();
if (!query) {
// Task 8.1: 清空搜索时恢复当前租户的分页列表
if (this.knowledgeCurrentTenantId) {
this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId);
} else {
this.loadKnowledge();
}
return;
}
try {
// Task 8.1: 搜索时附加 tenant_id 参数
let url = `/api/knowledge/search?q=${encodeURIComponent(query)}`;
if (this.knowledgeCurrentTenantId) {
url += `&tenant_id=${encodeURIComponent(this.knowledgeCurrentTenantId)}`;
}
const response = await fetch(url);
const results = await response.json();
this.updateKnowledgeDisplay(results);
// 搜索结果不显示分页
const paginationEl = document.getElementById('knowledge-pagination');
if (paginationEl) paginationEl.innerHTML = '';
} catch (error) {
console.error('搜索知识库失败:', error);
}
},
async addKnowledge() {
const question = document.getElementById('knowledge-question').value.trim();
const answer = document.getElementById('knowledge-answer').value.trim();
const category = document.getElementById('knowledge-category').value;
const confidence = parseFloat(document.getElementById('knowledge-confidence').value);
if (!question || !answer) {
this.showNotification('请填写完整信息', 'warning');
return;
}
try {
// Task 7.3: 添加知识条目时自动设置 tenant_id
const body = {
question,
answer,
category,
confidence_score: confidence
};
if (this.knowledgeCurrentTenantId) {
body.tenant_id = this.knowledgeCurrentTenantId;
}
const response = await fetch('/api/knowledge', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
const data = await response.json();
if (data.success) {
this.showNotification('知识添加成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('addKnowledgeModal')).hide();
document.getElementById('knowledge-form').reset();
// Task 7.3: 刷新当前视图
if (this.knowledgeCurrentTenantId) {
this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, this.paginationConfig.currentKnowledgePage);
} else {
this.loadKnowledgeTenantList();
}
} else {
this.showNotification('添加知识失败', 'error');
}
} catch (error) {
console.error('添加知识失败:', error);
this.showNotification('添加知识失败', 'error');
}
},
async uploadFile() {
const fileInput = document.getElementById('file-input');
const processMethod = document.getElementById('process-method').value;
const category = document.getElementById('file-category').value;
const confidence = parseFloat(document.getElementById('file-confidence').value);
const manualQuestion = document.getElementById('manual-question').value.trim();
if (!fileInput.files[0]) {
this.showNotification('请选择文件', 'warning');
return;
}
if (processMethod === 'manual' && !manualQuestion) {
this.showNotification('请指定问题', 'warning');
return;
}
// 显示进度条
const progressDiv = document.getElementById('upload-progress');
const progressBar = progressDiv.querySelector('.progress-bar');
const statusText = document.getElementById('upload-status');
progressDiv.style.display = 'block';
progressBar.style.width = '0%';
statusText.textContent = '正在上传文件...';
try {
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('process_method', processMethod);
formData.append('category', category);
formData.append('confidence_score', confidence);
if (manualQuestion) {
formData.append('manual_question', manualQuestion);
}
// 模拟进度更新
let progress = 0;
const progressInterval = setInterval(() => {
progress += Math.random() * 20;
if (progress > 90) progress = 90;
progressBar.style.width = progress + '%';
if (progress < 30) {
statusText.textContent = '正在上传文件...';
} else if (progress < 60) {
statusText.textContent = '正在解析文件内容...';
} else if (progress < 90) {
statusText.textContent = '正在生成知识库...';
}
}, 500);
const response = await fetch('/api/knowledge/upload', {
method: 'POST',
body: formData
});
clearInterval(progressInterval);
progressBar.style.width = '100%';
statusText.textContent = '处理完成!';
const data = await response.json();
setTimeout(() => {
progressDiv.style.display = 'none';
if (data.success) {
this.showNotification(`文件处理成功,生成了 ${data.knowledge_count || 0} 条知识`, 'success');
bootstrap.Modal.getInstance(document.getElementById('uploadFileModal')).hide();
document.getElementById('file-upload-form').reset();
this.refreshKnowledgeCurrentView();
} else {
this.showNotification(data.error || '文件处理失败', 'error');
}
}, 1000);
} catch (error) {
console.error('文件上传失败:', error);
progressDiv.style.display = 'none';
this.showNotification('文件上传失败', 'error');
}
},
async verifyKnowledge(knowledgeId) {
try {
const response = await fetch(`/api/knowledge/verify/${knowledgeId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
verified_by: 'admin'
})
});
const data = await response.json();
if (data.success) {
this.showNotification('知识库验证成功', 'success');
this.refreshKnowledgeCurrentView();
} else {
this.showNotification('知识库验证失败', 'error');
}
} catch (error) {
console.error('验证知识库失败:', error);
this.showNotification('验证知识库失败', 'error');
}
},
async unverifyKnowledge(knowledgeId) {
try {
const response = await fetch(`/api/knowledge/unverify/${knowledgeId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
this.showNotification('取消验证成功', 'success');
this.refreshKnowledgeCurrentView();
} else {
this.showNotification('取消验证失败', 'error');
}
} catch (error) {
console.error('取消验证失败:', error);
this.showNotification('取消验证失败', 'error');
}
},
async deleteKnowledge(knowledgeId) {
if (!confirm('确定要删除这条知识库条目吗?')) {
return;
}
try {
const response = await fetch(`/api/knowledge/delete/${knowledgeId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
this.showNotification('知识库条目已删除', 'success');
// Task 7.3: 删除后检查是否所有条目已删除,若是则返回租户列表
if (this.knowledgeCurrentTenantId) {
const checkResp = await fetch(`/api/knowledge?tenant_id=${encodeURIComponent(this.knowledgeCurrentTenantId)}&page=1&per_page=1`);
const checkData = await checkResp.json();
if (checkData.total === 0) {
this.showNotification('该租户下已无知识条目,返回租户列表', 'info');
this.loadKnowledgeTenantList();
return;
}
}
this.refreshKnowledgeCurrentView();
} else {
this.showNotification('知识库删除失败', 'error');
}
} catch (error) {
console.error('删除知识库失败:', error);
this.showNotification('删除知识库失败', 'error');
}
},
// 知识库批量删除功能
toggleSelectAllKnowledge() {
const selectAllCheckbox = document.getElementById('select-all-knowledge');
const knowledgeCheckboxes = document.querySelectorAll('.knowledge-checkbox');
knowledgeCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
this.updateBatchDeleteKnowledgeButton();
},
updateBatchDeleteKnowledgeButton() {
const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
const batchDeleteBtn = document.getElementById('batch-delete-knowledge');
const batchVerifyBtn = document.getElementById('batch-verify-knowledge');
const batchUnverifyBtn = document.getElementById('batch-unverify-knowledge');
const hasSelection = selectedCheckboxes.length > 0;
if (batchDeleteBtn) {
batchDeleteBtn.disabled = !hasSelection;
batchDeleteBtn.innerHTML = hasSelection
? `<i class="fas fa-trash me-1"></i>批量删除 (${selectedCheckboxes.length})`
: '<i class="fas fa-trash me-1"></i>批量删除';
}
if (batchVerifyBtn) {
batchVerifyBtn.disabled = !hasSelection;
}
if (batchUnverifyBtn) {
batchUnverifyBtn.disabled = !hasSelection;
}
},
async batchDeleteKnowledge() {
const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
if (selectedIds.length === 0) {
this.showNotification('请选择要删除的知识库条目', 'warning');
return;
}
if (!confirm(`确定要删除选中的 ${selectedIds.length} 个知识库条目吗?此操作不可撤销。`)) {
return;
}
try {
const response = await fetch('/api/batch-delete/knowledge', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: selectedIds })
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
// 清除缓存并强制刷新
this.cache.delete('knowledge');
// Task 7.3: 删除后检查是否所有条目已删除,若是则返回租户列表
if (this.knowledgeCurrentTenantId) {
const checkResp = await fetch(`/api/knowledge?tenant_id=${encodeURIComponent(this.knowledgeCurrentTenantId)}&page=1&per_page=1`);
const checkData = await checkResp.json();
if (checkData.total === 0) {
this.showNotification('该租户下已无知识条目,返回租户列表', 'info');
this.loadKnowledgeTenantList();
return;
}
}
await this.refreshKnowledgeCurrentView();
// 重置批量操作按钮状态
this.updateBatchDeleteKnowledgeButton();
} else {
this.showNotification(data.error || '批量删除失败', 'error');
}
} catch (error) {
console.error('批量删除知识库条目失败:', error);
this.showNotification('批量删除知识库条目失败', 'error');
}
},
// Task 7.3: 批量验证知识条目
async batchVerifyKnowledge() {
const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
if (selectedIds.length === 0) {
this.showNotification('请选择要验证的知识库条目', 'warning');
return;
}
try {
const response = await fetch('/api/knowledge/batch_verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: selectedIds })
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message || '批量验证成功', 'success');
await this.refreshKnowledgeCurrentView();
this.updateBatchDeleteKnowledgeButton();
} else {
this.showNotification(data.error || '批量验证失败', 'error');
}
} catch (error) {
console.error('批量验证知识库条目失败:', error);
this.showNotification('批量验证知识库条目失败', 'error');
}
},
// Task 7.3: 批量取消验证知识条目
async batchUnverifyKnowledge() {
const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
if (selectedIds.length === 0) {
this.showNotification('请选择要取消验证的知识库条目', 'warning');
return;
}
try {
const response = await fetch('/api/knowledge/batch_unverify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: selectedIds })
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message || '批量取消验证成功', 'success');
await this.refreshKnowledgeCurrentView();
this.updateBatchDeleteKnowledgeButton();
} else {
this.showNotification(data.error || '批量取消验证失败', 'error');
}
} catch (error) {
console.error('批量取消验证知识库条目失败:', error);
this.showNotification('批量取消验证知识库条目失败', 'error');
}
}
});

View File

@@ -0,0 +1,455 @@
// 监控模块Token监控 + AI监控
Object.assign(TSPDashboard.prototype, {
async loadTokenMonitor() {
try {
const response = await fetch('/api/token-monitor/stats');
const data = await response.json();
if (data.success) {
this.updateTokenStats(data);
this.loadTokenChart();
this.loadTokenRecords();
} else {
throw new Error(data.error || '加载Token监控数据失败');
}
} catch (error) {
console.error('加载Token监控数据失败:', error);
this.showNotification('加载Token监控数据失败: ' + error.message, 'error');
}
},
updateTokenStats(stats) {
document.getElementById('token-today').textContent = stats.today_tokens || 0;
document.getElementById('token-month').textContent = stats.month_tokens || 0;
document.getElementById('token-cost').textContent = `¥${stats.total_cost || 0}`;
document.getElementById('token-budget').textContent = `¥${stats.budget_limit || 1000}`;
},
async loadTokenChart() {
try {
const response = await fetch('/api/token-monitor/chart');
const data = await response.json();
if (data.success) {
this.renderTokenChart(data);
}
} catch (error) {
console.error('加载Token图表失败:', error);
}
},
renderTokenChart(data) {
const ctx = document.getElementById('tokenChart').getContext('2d');
if (this.charts.tokenChart) {
this.charts.tokenChart.destroy();
}
this.charts.tokenChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.labels || [],
datasets: [{
label: 'Token消耗',
data: data.tokens || [],
borderColor: '#007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
tension: 0.4
}, {
label: '成本',
data: data.costs || [],
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.4,
yAxisID: 'y1'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: 'Token数量'
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: '成本 (元)'
},
grid: {
drawOnChartArea: false,
},
}
}
}
});
},
async loadTokenRecords() {
try {
const response = await fetch('/api/token-monitor/records');
const data = await response.json();
if (data.success) {
this.renderTokenRecords(data.records || []);
}
} catch (error) {
console.error('加载Token记录失败:', error);
}
},
renderTokenRecords(records) {
const tbody = document.getElementById('token-records');
if (!records || records.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center text-muted">暂无记录</td>
</tr>
`;
return;
}
const html = records.map(record => `
<tr>
<td>${new Date(record.timestamp).toLocaleString()}</td>
<td>${record.user_id || '匿名'}</td>
<td>${record.model || 'qwen-turbo'}</td>
<td>${record.input_tokens || 0}</td>
<td>${record.output_tokens || 0}</td>
<td>${record.total_tokens || 0}</td>
<td>¥${record.cost || 0}</td>
<td>${record.response_time || 0}ms</td>
</tr>
`).join('');
tbody.innerHTML = html;
},
async saveTokenSettings() {
const dailyThreshold = document.getElementById('daily-threshold').value;
const monthlyBudget = document.getElementById('monthly-budget').value;
const enableAlerts = document.getElementById('enable-alerts').checked;
try {
const response = await fetch('/api/token-monitor/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
daily_threshold: parseInt(dailyThreshold),
monthly_budget: parseFloat(monthlyBudget),
enable_alerts: enableAlerts
})
});
const data = await response.json();
if (data.success) {
this.showNotification('Token设置已保存', 'success');
} else {
throw new Error(data.error || '保存Token设置失败');
}
} catch (error) {
console.error('保存Token设置失败:', error);
this.showNotification('保存Token设置失败: ' + error.message, 'error');
}
},
async updateTokenChart(period) {
// 更新按钮状态
document.querySelectorAll('#tokenChart').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
try {
const response = await fetch(`/api/token-monitor/chart?period=${period}`);
const data = await response.json();
if (data.success) {
this.renderTokenChart(data);
}
} catch (error) {
console.error('更新Token图表失败:', error);
}
},
async exportTokenData() {
try {
const response = await fetch('/api/token-monitor/export');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'token_usage_data.xlsx';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
this.showNotification('Token数据导出成功', 'success');
} catch (error) {
console.error('导出Token数据失败:', error);
this.showNotification('导出Token数据失败: ' + error.message, 'error');
}
},
async refreshTokenData() {
await this.loadTokenMonitor();
this.showNotification('Token数据已刷新', 'success');
},
// AI监控
async loadAIMonitor() {
try {
const response = await fetch('/api/ai-monitor/stats');
const data = await response.json();
if (data.success) {
this.updateAIStats(data);
this.loadModelComparisonChart();
this.loadErrorDistributionChart();
this.loadErrorLog();
} else {
throw new Error(data.error || '加载AI监控数据失败');
}
} catch (error) {
console.error('加载AI监控数据失败:', error);
this.showNotification('加载AI监控数据失败: ' + error.message, 'error');
}
},
updateAIStats(stats) {
document.getElementById('ai-success-rate').textContent = `${stats.success_rate || 0}%`;
document.getElementById('ai-response-time').textContent = `${stats.avg_response_time || 0}ms`;
document.getElementById('ai-error-rate').textContent = `${stats.error_rate || 0}%`;
document.getElementById('ai-total-calls').textContent = stats.total_calls || 0;
},
async loadModelComparisonChart() {
try {
const response = await fetch('/api/ai-monitor/model-comparison');
const data = await response.json();
if (data.success) {
this.renderModelComparisonChart(data);
}
} catch (error) {
console.error('加载模型对比图表失败:', error);
}
},
renderModelComparisonChart(data) {
const ctx = document.getElementById('modelComparisonChart').getContext('2d');
if (this.charts.modelComparisonChart) {
this.charts.modelComparisonChart.destroy();
}
this.charts.modelComparisonChart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.models || [],
datasets: [{
label: '成功率 (%)',
data: data.success_rates || [],
backgroundColor: 'rgba(40, 167, 69, 0.8)'
}, {
label: '平均响应时间 (ms)',
data: data.response_times || [],
backgroundColor: 'rgba(255, 193, 7, 0.8)',
yAxisID: 'y1'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: '成功率 (%)'
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: '响应时间 (ms)'
},
grid: {
drawOnChartArea: false,
},
}
}
}
});
},
async loadErrorDistributionChart() {
try {
const response = await fetch('/api/ai-monitor/error-distribution');
const data = await response.json();
if (data.success) {
this.renderErrorDistributionChart(data);
}
} catch (error) {
console.error('加载错误分布图表失败:', error);
}
},
renderErrorDistributionChart(data) {
const ctx = document.getElementById('errorDistributionChart').getContext('2d');
if (this.charts.errorDistributionChart) {
this.charts.errorDistributionChart.destroy();
}
this.charts.errorDistributionChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: data.error_types || [],
datasets: [{
data: data.counts || [],
backgroundColor: [
'#dc3545',
'#fd7e14',
'#ffc107',
'#17a2b8',
'#6c757d'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
},
async loadErrorLog() {
try {
const response = await fetch('/api/ai-monitor/error-log');
const data = await response.json();
if (data.success) {
this.renderErrorLog(data.errors || []);
}
} catch (error) {
console.error('加载错误日志失败:', error);
}
},
renderErrorLog(errors) {
const tbody = document.getElementById('error-log');
if (!errors || errors.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-muted">暂无错误记录</td>
</tr>
`;
return;
}
const html = errors.map(error => `
<tr>
<td>${new Date(error.timestamp).toLocaleString()}</td>
<td><span class="badge bg-danger">${error.error_type || '未知'}</span></td>
<td>${error.error_message || ''}</td>
<td>${error.model || 'qwen-turbo'}</td>
<td>${error.user_id || '匿名'}</td>
<td>
<button class="btn btn-outline-primary btn-sm" onclick="dashboard.viewErrorDetail(${error.id})">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
`).join('');
tbody.innerHTML = html;
},
async refreshErrorLog() {
await this.loadErrorLog();
this.showNotification('错误日志已刷新', 'success');
},
async clearErrorLog() {
if (!confirm('确定要清空错误日志吗?')) {
return;
}
try {
const response = await fetch('/api/ai-monitor/error-log', { method: 'DELETE' });
const data = await response.json();
if (data.success) {
this.showNotification('错误日志已清空', 'success');
await this.loadErrorLog();
} else {
throw new Error(data.error || '清空错误日志失败');
}
} catch (error) {
console.error('清空错误日志失败:', error);
this.showNotification('清空错误日志失败: ' + error.message, 'error');
}
},
async viewErrorDetail(convId) {
const modal = new bootstrap.Modal(document.getElementById('errorDetailModal'));
const body = document.getElementById('errorDetailBody');
body.innerHTML = '<div class="text-center text-muted"><i class="fas fa-spinner fa-spin me-2"></i>加载中...</div>';
modal.show();
try {
const response = await fetch(`/api/ai-monitor/error-log/${convId}`);
const data = await response.json();
if (data.success) {
const d = data.detail;
body.innerHTML = `
<table class="table table-bordered mb-0">
<tr><th style="width:120px">记录ID</th><td>${d.id}</td></tr>
<tr><th>时间</th><td>${d.timestamp ? new Date(d.timestamp).toLocaleString() : '-'}</td></tr>
<tr><th>分类</th><td>${d.category || '-'}</td></tr>
<tr><th>来源</th><td>${d.source || '-'}</td></tr>
<tr><th>置信度</th><td>${d.confidence_score != null ? d.confidence_score : '-'}</td></tr>
<tr><th>响应时间</th><td>${d.response_time != null ? d.response_time + ' ms' : '-'}</td></tr>
<tr><th>用户消息</th><td><pre class="mb-0" style="white-space:pre-wrap">${this.escapeHtml(d.user_message || '-')}</pre></td></tr>
<tr><th>助手回复</th><td><pre class="mb-0" style="white-space:pre-wrap">${this.escapeHtml(d.assistant_response || '-')}</pre></td></tr>
</table>
`;
} else {
body.innerHTML = `<div class="alert alert-danger">${data.error || '加载失败'}</div>`;
}
} catch (error) {
body.innerHTML = `<div class="alert alert-danger">请求失败: ${error.message}</div>`;
}
},
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
// 租户管理模块
Object.assign(TSPDashboard.prototype, {
async loadTenantList() {
const container = document.getElementById('tenant-list');
if (!container) return;
container.innerHTML = '<div class="text-center py-4"><i class="fas fa-spinner fa-spin fa-2x"></i></div>';
try {
const response = await fetch('/api/tenants');
const tenants = await response.json();
if (!Array.isArray(tenants) || tenants.length === 0) {
container.innerHTML = '<div class="text-center py-4 text-muted">暂无租户,请点击"新建租户"创建</div>';
return;
}
container.innerHTML = tenants.map(t => {
const feishuCfg = t.config?.feishu || {};
const groupCount = (feishuCfg.chat_groups || []).length;
const hasFeishu = feishuCfg.app_id || groupCount > 0;
return `
<div class="card mb-2">
<div class="card-body d-flex justify-content-between align-items-center py-2">
<div>
<strong>${t.name}</strong>
<span class="text-muted ms-2">(${t.tenant_id})</span>
${t.description ? `<br><small class="text-muted">${t.description}</small>` : ''}
${!t.is_active ? '<span class="badge bg-secondary ms-2">已禁用</span>' : ''}
${hasFeishu ? `<span class="badge bg-info ms-2"><i class="fas fa-robot me-1"></i>飞书${groupCount > 0 ? ` (${groupCount}群)` : ''}</span>` : ''}
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="dashboard.showEditTenantModal('${t.tenant_id}', '${(t.name || '').replace(/'/g, "\\'")}', '${(t.description || '').replace(/'/g, "\\'")}')">
<i class="fas fa-edit"></i>
</button>
${t.tenant_id !== 'default' ? `
<button class="btn btn-outline-danger" onclick="dashboard.deleteTenant('${t.tenant_id}')">
<i class="fas fa-trash"></i>
</button>` : ''}
</div>
</div>
</div>
`}).join('');
} catch (error) {
console.error('加载租户列表失败:', error);
container.innerHTML = '<div class="text-center py-4 text-danger">加载失败</div>';
}
},
showCreateTenantModal() {
document.getElementById('tenantModalTitle').textContent = '新建租户';
document.getElementById('tenant-edit-id').value = '';
document.getElementById('tenant-id-input').value = '';
document.getElementById('tenant-id-input').disabled = false;
document.getElementById('tenant-id-group').style.display = '';
document.getElementById('tenant-name-input').value = '';
document.getElementById('tenant-desc-input').value = '';
document.getElementById('tenant-feishu-appid').value = '';
document.getElementById('tenant-feishu-appsecret').value = '';
document.getElementById('tenant-feishu-chatgroups').value = '';
new bootstrap.Modal(document.getElementById('tenantModal')).show();
},
async showEditTenantModal(tenantId, name, description) {
document.getElementById('tenantModalTitle').textContent = '编辑租户';
document.getElementById('tenant-edit-id').value = tenantId;
document.getElementById('tenant-id-input').value = tenantId;
document.getElementById('tenant-id-input').disabled = true;
document.getElementById('tenant-name-input').value = name;
document.getElementById('tenant-desc-input').value = description;
// 加载租户的飞书配置
try {
const resp = await fetch('/api/tenants');
const tenants = await resp.json();
const tenant = tenants.find(t => t.tenant_id === tenantId);
const feishuCfg = tenant?.config?.feishu || {};
document.getElementById('tenant-feishu-appid').value = feishuCfg.app_id || '';
document.getElementById('tenant-feishu-appsecret').value = feishuCfg.app_secret || '';
document.getElementById('tenant-feishu-chatgroups').value = (feishuCfg.chat_groups || []).join('\n');
} catch (e) {
document.getElementById('tenant-feishu-appid').value = '';
document.getElementById('tenant-feishu-appsecret').value = '';
document.getElementById('tenant-feishu-chatgroups').value = '';
}
new bootstrap.Modal(document.getElementById('tenantModal')).show();
},
async saveTenant() {
const editId = document.getElementById('tenant-edit-id').value;
const tenantId = document.getElementById('tenant-id-input').value.trim();
const name = document.getElementById('tenant-name-input').value.trim();
const description = document.getElementById('tenant-desc-input').value.trim();
// 飞书配置
const feishuAppId = document.getElementById('tenant-feishu-appid').value.trim();
const feishuAppSecret = document.getElementById('tenant-feishu-appsecret').value.trim();
const chatGroupsText = document.getElementById('tenant-feishu-chatgroups').value.trim();
const chatGroups = chatGroupsText ? chatGroupsText.split('\n').map(s => s.trim()).filter(Boolean) : [];
const config = {};
if (feishuAppId || feishuAppSecret || chatGroups.length > 0) {
config.feishu = {};
if (feishuAppId) config.feishu.app_id = feishuAppId;
if (feishuAppSecret) config.feishu.app_secret = feishuAppSecret;
if (chatGroups.length > 0) config.feishu.chat_groups = chatGroups;
}
if (!name) {
this.showNotification('租户名称不能为空', 'error');
return;
}
try {
let response;
if (editId) {
response = await fetch(`/api/tenants/${encodeURIComponent(editId)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description, config })
});
} else {
if (!tenantId) {
this.showNotification('租户标识不能为空', 'error');
return;
}
response = await fetch('/api/tenants', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tenant_id: tenantId, name, description, config })
});
}
const data = await response.json();
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('tenantModal')).hide();
this.showNotification(editId ? '租户已更新' : '租户已创建', 'success');
this.loadTenantList();
this.populateTenantSelectors();
} else {
this.showNotification(data.error || '操作失败', 'error');
}
} catch (error) {
console.error('保存租户失败:', error);
this.showNotification('保存租户失败: ' + error.message, 'error');
}
},
async deleteTenant(tenantId) {
if (!confirm(`确定要删除租户 "${tenantId}" 吗?该操作不会删除关联数据。`)) return;
try {
const response = await fetch(`/api/tenants/${encodeURIComponent(tenantId)}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
this.showNotification('租户已删除', 'success');
this.loadTenantList();
} else {
this.showNotification(data.error || '删除失败', 'error');
}
} catch (error) {
console.error('删除租户失败:', error);
this.showNotification('删除租户失败: ' + error.message, 'error');
}
}
});

View File

@@ -0,0 +1,573 @@
// 工具函数模块
Object.assign(TSPDashboard.prototype, {
getLevelText(level) {
const levelMap = {
'critical': '严重',
'error': '错误',
'warning': '警告',
'info': '信息'
};
return levelMap[level] || level;
},
getTypeText(type) {
const typeMap = {
'performance': '性能',
'quality': '质量',
'volume': '量级',
'system': '系统',
'business': '业务'
};
return typeMap[type] || type;
},
getAlertColor(level) {
const colorMap = {
'critical': 'danger',
'error': 'danger',
'warning': 'warning',
'info': 'info'
};
return colorMap[level] || 'secondary';
},
getPriorityText(priority) {
const priorityMap = {
'low': '低',
'medium': '中',
'high': '高',
'urgent': '紧急'
};
return priorityMap[priority] || priority;
},
getSeverityText(severity) {
const severityMap = {
'low': '低',
'medium': '中',
'high': '高',
'critical': '严重'
};
return severityMap[severity] || severity;
},
getSatisfactionText(level) {
const satisfactionMap = {
'very_satisfied': '非常满意',
'satisfied': '满意',
'neutral': '一般',
'dissatisfied': '不满意'
};
return satisfactionMap[level] || level;
},
getPerformanceTrendText(trend) {
const trendMap = {
'up': '上升',
'down': '下降',
'stable': '稳定'
};
return trendMap[trend] || trend;
},
getPriorityColor(priority) {
const colorMap = {
'low': 'secondary',
'medium': 'primary',
'high': 'warning',
'urgent': 'danger'
};
return colorMap[priority] || 'secondary';
},
getStatusText(status) {
const statusMap = {
'open': '待处理',
'in_progress': '处理中',
'resolved': '已解决',
'closed': '已关闭'
};
return statusMap[status] || status;
},
// 统计数字预览功能
async showStatPreview(type, status) {
try {
let title = '';
let data = [];
let apiUrl = '';
switch (type) {
case 'workorder':
title = this.getWorkorderPreviewTitle(status);
apiUrl = status === 'all' ? '/api/workorders' : `/api/workorders/by-status/${status}`;
break;
case 'alert':
title = this.getAlertPreviewTitle(status);
apiUrl = `/api/alerts/by-level/${status}`;
break;
case 'knowledge':
title = this.getKnowledgePreviewTitle(status);
apiUrl = `/api/knowledge/by-status/${status}`;
break;
default:
return;
}
// 显示加载状态
this.showLoadingModal(title);
const response = await fetch(apiUrl);
const result = await response.json();
// 处理不同的API响应结构
if (result.success !== false) {
if (result.success === true) {
// 新API结构: {success: true, data: {workorders: [...]}}
data = result.data[type + 's'] || result.data.knowledge || [];
} else if (result.workorders) {
// 旧API结构: {workorders: [...], page: 1, ...}
data = result.workorders || [];
} else if (result.alerts) {
// 预警API结构
data = result.alerts || [];
} else if (result.knowledge) {
// 知识库API结构
data = result.knowledge || [];
} else {
data = [];
}
this.showPreviewModal(title, type, data);
} else {
const errorMsg = result.error || result.message || '未知错误';
this.showNotification('获取数据失败: ' + errorMsg, 'error');
}
} catch (error) {
console.error('预览失败:', error);
this.showNotification('预览失败: ' + error.message, 'error');
}
},
getWorkorderPreviewTitle(status) {
const titles = {
'all': '所有工单',
'open': '待处理工单',
'in_progress': '处理中工单',
'resolved': '已解决工单',
'closed': '已关闭工单'
};
return titles[status] || '工单列表';
},
getAlertPreviewTitle(level) {
const titles = {
'critical': '严重预警',
'warning': '警告预警',
'info': '信息预警'
};
return titles[level] || '预警列表';
},
getKnowledgePreviewTitle(status) {
const titles = {
'verified': '已验证知识',
'unverified': '未验证知识'
};
return titles[status] || '知识库条目';
},
showLoadingModal(title) {
const modalHtml = `
<div class="modal fade" id="statPreviewModal" tabindex="-1" data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
</div>
<div class="modal-body text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2">正在加载数据...</p>
</div>
</div>
</div>
</div>
`;
// 移除已存在的模态框和遮罩
this.removeExistingModal();
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modalElement = document.getElementById('statPreviewModal');
const modal = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true
});
// 添加事件监听器确保正确清理
modalElement.addEventListener('hidden.bs.modal', () => {
this.cleanupModal();
});
modal.show();
},
showPreviewModal(title, type, data) {
let contentHtml = '';
if (data.length === 0) {
contentHtml = `
<div class="text-center py-4">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<h5 class="text-muted">暂无数据</h5>
<p class="text-muted">当前条件下没有找到相关记录</p>
</div>
`;
} else {
switch (type) {
case 'workorder':
contentHtml = this.generateWorkorderPreviewHtml(data);
break;
case 'alert':
contentHtml = this.generateAlertPreviewHtml(data);
break;
case 'knowledge':
contentHtml = this.generateKnowledgePreviewHtml(data);
break;
}
}
const modalHtml = `
<div class="modal fade" id="statPreviewModal" tabindex="-1" data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-eye me-2"></i>${title}
<span class="badge bg-primary ms-2">${data.length} 条记录</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
</div>
<div class="modal-body">
${contentHtml}
</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.goToFullView('${type}', '${status}')">查看完整列表</button>
</div>
</div>
</div>
</div>
`;
// 移除已存在的模态框和遮罩
this.removeExistingModal();
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modalElement = document.getElementById('statPreviewModal');
const modal = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true
});
// 添加事件监听器确保正确清理
modalElement.addEventListener('hidden.bs.modal', () => {
this.cleanupModal();
});
modal.show();
},
generateWorkorderPreviewHtml(workorders) {
return `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>工单ID</th>
<th>标题</th>
<th>状态</th>
<th>优先级</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${workorders.map(wo => `
<tr>
<td>${wo.order_id || wo.id}</td>
<td>
<div class="text-truncate" style="max-width: 200px;" title="${wo.title}">
${wo.title}
</div>
</td>
<td>
<span class="badge bg-${this.getStatusColor(wo.status)}">
${this.getStatusText(wo.status)}
</span>
</td>
<td>
<span class="badge bg-${this.getPriorityColor(wo.priority)}">
${this.getPriorityText(wo.priority)}
</span>
</td>
<td>${new Date(wo.created_at).toLocaleString()}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="dashboard.viewWorkOrderDetails(${wo.id})">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
},
generateAlertPreviewHtml(alerts) {
return `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>预警ID</th>
<th>消息</th>
<th>级别</th>
<th>类型</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${alerts.map(alert => `
<tr>
<td>${alert.id}</td>
<td>
<div class="text-truncate" style="max-width: 300px;" title="${alert.message}">
${alert.message}
</div>
</td>
<td>
<span class="badge bg-${this.getAlertLevelColor(alert.level)}">
${this.getLevelText(alert.level)}
</span>
</td>
<td>${this.getTypeText(alert.alert_type)}</td>
<td>${new Date(alert.created_at).toLocaleString()}</td>
<td>
<button class="btn btn-sm btn-outline-success" onclick="dashboard.resolveAlert(${alert.id})">
<i class="fas fa-check"></i>
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
},
generateKnowledgePreviewHtml(knowledge) {
return `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>分类</th>
<th>验证状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${knowledge.map(item => `
<tr>
<td>${item.id}</td>
<td>
<div class="text-truncate" style="max-width: 250px;" title="${item.title}">
${item.title}
</div>
</td>
<td>${item.category || '未分类'}</td>
<td>
<span class="badge bg-${item.is_verified ? 'success' : 'warning'}">
${item.is_verified ? '已验证' : '未验证'}
</span>
</td>
<td>${new Date(item.created_at).toLocaleString()}</td>
<td>
<div class="btn-group">
${item.is_verified ?
`<button class="btn btn-sm btn-outline-warning" onclick="dashboard.unverifyKnowledge(${item.id})" title="取消验证">
<i class="fas fa-times-circle"></i>
</button>` :
`<button class="btn btn-sm btn-outline-success" onclick="dashboard.verifyKnowledge(${item.id})" title="验证">
<i class="fas fa-check-circle"></i>
</button>`
}
<button class="btn btn-sm btn-outline-danger" onclick="dashboard.deleteKnowledge(${item.id})" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
},
getAlertLevelColor(level) {
const colorMap = {
'critical': 'danger',
'warning': 'warning',
'info': 'info'
};
return colorMap[level] || 'secondary';
},
goToFullView(type, status) {
// 关闭预览模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('statPreviewModal'));
if (modal) {
modal.hide();
}
// 切换到对应的标签页
switch (type) {
case 'workorder':
this.switchTab('workorders');
// 设置筛选器
if (status !== 'all') {
setTimeout(() => {
const filter = document.getElementById('workorder-status-filter');
if (filter) {
filter.value = status;
this.loadWorkOrders();
}
}, 100);
}
break;
case 'alert':
this.switchTab('alerts');
// 设置筛选器
setTimeout(() => {
const filter = document.getElementById('alert-filter');
if (filter) {
filter.value = status;
this.updateAlertsDisplay();
}
}, 100);
break;
case 'knowledge':
this.switchTab('knowledge');
break;
}
},
// 模态框清理方法
removeExistingModal() {
const existingModal = document.getElementById('statPreviewModal');
if (existingModal) {
// 获取模态框实例并销毁
const modalInstance = bootstrap.Modal.getInstance(existingModal);
if (modalInstance) {
modalInstance.dispose();
}
existingModal.remove();
}
// 清理可能残留的遮罩
const backdrops = document.querySelectorAll('.modal-backdrop');
backdrops.forEach(backdrop => backdrop.remove());
// 恢复body的滚动
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
},
cleanupModal() {
// 延迟清理,确保动画完成
setTimeout(() => {
this.removeExistingModal();
}, 300);
},
getStatusColor(status) {
const colorMap = {
'open': 'warning',
'in_progress': 'info',
'resolved': 'success',
'closed': 'secondary'
};
return colorMap[status] || 'secondary';
},
formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) { // 1分钟内
return '刚刚';
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`;
} else if (diff < 86400000) { // 1天内
return `${Math.floor(diff / 3600000)}小时前`;
} else {
return date.toLocaleDateString();
}
},
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show`;
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
},
getSimilarityExplanation(percent) {
if (percent >= 95) {
return "语义高度相似AI建议与人工描述基本一致建议自动审批";
} else if (percent >= 90) {
return "语义较为相似AI建议与人工描述大体一致建议人工审核";
} else if (percent >= 80) {
return "语义部分相似AI建议与人工描述有一定差异需要人工判断";
} else if (percent >= 60) {
return "语义相似度较低AI建议与人工描述差异较大建议使用人工描述";
} else {
return "语义差异很大AI建议与人工描述差异很大优先使用人工描述";
}
},
getSimilarityMessage(percent, approved, useHumanResolution = false) {
if (useHumanResolution) {
return `人工描述已保存!语义相似度: ${percent}%AI准确率低于90%,将使用人工描述入库`;
} else if (approved) {
return `人工描述已保存!语义相似度: ${percent}%,已自动审批入库`;
} else if (percent >= 90) {
return `人工描述已保存!语义相似度: ${percent}%,建议人工审核后审批`;
} else if (percent >= 80) {
return `人工描述已保存!语义相似度: ${percent}%,需要人工判断是否审批`;
} else {
return `人工描述已保存!语义相似度: ${percent}%,建议使用人工描述入库`;
}
}
});

View File

@@ -0,0 +1,458 @@
// 工单管理模块
Object.assign(TSPDashboard.prototype, {
async generateAISuggestion(workorderId) {
const button = document.querySelector(`button[onclick="dashboard.generateAISuggestion(${workorderId})"]`);
const textarea = document.getElementById(`aiSuggestion_${workorderId}`);
try {
if (button) { button.classList.add('btn-loading'); button.disabled = true; }
if (textarea) { textarea.classList.add('ai-loading'); textarea.value = '正在生成AI建议请稍候...'; }
const resp = await fetch(`/api/workorders/${workorderId}/ai-suggestion`, { method: 'POST' });
const data = await resp.json();
if (data.success) {
if (textarea) {
textarea.value = data.suggestion || '';
textarea.classList.remove('ai-loading');
textarea.classList.add('success-animation');
setTimeout(() => textarea.classList.remove('success-animation'), 600);
}
this.showNotification('AI建议已生成', 'success');
} else { throw new Error(data.error || '生成失败'); }
} catch (e) {
console.error('生成AI建议失败:', e);
if (textarea) { textarea.value = 'AI建议生成失败请重试'; textarea.classList.remove('ai-loading'); }
this.showNotification('生成AI建议失败: ' + e.message, 'error');
} finally {
if (button) { button.classList.remove('btn-loading'); button.disabled = false; }
}
},
async saveHumanResolution(workorderId) {
try {
const text = document.getElementById(`humanResolution_${workorderId}`).value.trim();
if (!text) { this.showNotification('请输入人工描述', 'warning'); return; }
const resp = await fetch(`/api/workorders/${workorderId}/human-resolution`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ human_resolution: text })
});
const data = await resp.json();
if (data.success) {
const simEl = document.getElementById(`aiSim_${workorderId}`);
const apprEl = document.getElementById(`aiApproved_${workorderId}`);
const approveBtn = document.getElementById(`approveBtn_${workorderId}`);
const percent = Math.round((data.similarity || 0) * 100);
if (simEl) {
simEl.innerHTML = `<i class="fas fa-percentage"></i>语义相似度: ${percent}%`;
simEl.className = percent >= 90 ? 'similarity-badge high' : percent >= 80 ? 'similarity-badge medium' : 'similarity-badge low';
simEl.title = this.getSimilarityExplanation(percent);
}
if (apprEl) {
if (data.use_human_resolution) { apprEl.textContent = '将使用人工描述入库'; apprEl.className = 'status-badge human-resolution'; }
else if (data.approved) { apprEl.textContent = '已自动审批'; apprEl.className = 'status-badge approved'; }
else { apprEl.textContent = '未审批'; apprEl.className = 'status-badge pending'; }
}
if (approveBtn) {
const canApprove = data.approved || data.use_human_resolution;
approveBtn.disabled = !canApprove;
if (data.use_human_resolution) { approveBtn.textContent = '使用人工描述入库'; approveBtn.className = 'approve-btn'; }
else if (data.approved) { approveBtn.textContent = '已自动审批'; approveBtn.className = 'approve-btn approved'; }
else { approveBtn.textContent = '审批入库'; approveBtn.className = 'approve-btn'; }
}
const message = this.getSimilarityMessage(percent, data.approved, data.use_human_resolution);
this.showNotification(message, data.approved ? 'success' : data.use_human_resolution ? 'warning' : 'info');
} else { throw new Error(data.error || '保存失败'); }
} catch (e) {
console.error('保存人工描述失败:', e);
this.showNotification('保存人工描述失败: ' + e.message, 'error');
}
},
async approveToKnowledge(workorderId) {
try {
const resp = await fetch(`/api/workorders/${workorderId}/approve-to-knowledge`, { method: 'POST' });
const data = await resp.json();
if (data.success) {
const contentType = data.used_content === 'human_resolution' ? '人工描述' : 'AI建议';
const confidence = Math.round((data.confidence_score || 0) * 100);
this.showNotification(`已入库为知识条目!使用${contentType},置信度: ${confidence}%`, 'success');
} else { throw new Error(data.error || '入库失败'); }
} catch (e) {
console.error('入库失败:', e);
this.showNotification('入库失败: ' + e.message, 'error');
}
},
async loadWorkOrders(page = 1, forceRefresh = false) {
this.paginationConfig.currentWorkOrderPage = page;
const cacheKey = `workorders_page_${page}`;
if (!forceRefresh && this.cache.has(cacheKey)) {
const cachedData = this.cache.get(cacheKey);
this.updateWorkOrdersDisplay(cachedData.workorders);
this.updateWorkOrdersPagination(cachedData);
return;
}
try {
const statusFilter = document.getElementById('workorder-status-filter')?.value || 'all';
const priorityFilter = document.getElementById('workorder-priority-filter')?.value || 'all';
const tenantFilter = document.getElementById('workorder-tenant-filter')?.value || 'all';
let url = '/api/workorders';
const params = new URLSearchParams();
params.append('page', page);
params.append('per_page', this.getPageSize('workorders-pagination').toString());
if (statusFilter !== 'all') params.append('status', statusFilter);
if (priorityFilter !== 'all') params.append('priority', priorityFilter);
if (tenantFilter !== 'all') params.append('tenant_id', tenantFilter);
if (forceRefresh) params.append('_t', Date.now().toString());
if (params.toString()) url += '?' + params.toString();
const response = await fetch(url, {
cache: forceRefresh ? 'no-cache' : 'default',
headers: forceRefresh ? { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' } : {}
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
const data = await response.json();
this.updateWorkOrdersDisplay(data.workorders);
this.updateWorkOrdersPagination(data);
this.cache.set(cacheKey, data);
} catch (error) {
console.error('加载工单失败:', error);
this.showNotification('加载工单失败: ' + error.message, 'error');
}
},
updateWorkOrdersDisplay(workorders) {
const container = document.getElementById('workorders-list');
if (workorders.length === 0) {
container.innerHTML = '<div class="empty-state"><i class="fas fa-tasks"></i><p>暂无工单</p></div>';
return;
}
const headerHtml = `<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<input type="checkbox" id="select-all-workorders" class="form-check-input me-2" onchange="dashboard.toggleSelectAllWorkorders()">
<label for="select-all-workorders" class="form-check-label">全选</label>
</div>
<div class="btn-group">
<button class="btn btn-sm btn-danger" id="batch-delete-workorders" onclick="dashboard.batchDeleteWorkorders()" disabled>
<i class="fas fa-trash me-1"></i>批量删除
</button>
</div>
</div>`;
const workordersHtml = workorders.map(wo => `
<div class="work-order-item">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex align-items-start">
<input type="checkbox" class="form-check-input me-2 workorder-checkbox" value="${wo.id}" onchange="dashboard.updateBatchDeleteButton()">
<div class="flex-grow-1">
<h6 class="mb-1">${wo.title}</h6>
<p class="text-muted mb-2">${wo.description ? wo.description.substring(0, 100) + (wo.description.length > 100 ? '...' : '') : '无问题描述'}</p>
<div class="d-flex gap-3">
<span class="badge bg-${this.getPriorityColor(wo.priority)}">${this.getPriorityText(wo.priority)}</span>
<span class="badge bg-${this.getStatusColor(wo.status)}">${this.getStatusText(wo.status)}</span>
<small class="text-muted">分类: ${wo.category}</small>
<small class="text-muted">创建时间: ${new Date(wo.created_at).toLocaleString()}</small>
</div>
</div>
</div>
<div class="ms-3">
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-info" onclick="dashboard.viewWorkOrderDetails(${wo.id})" title="查看详情"><i class="fas fa-eye"></i><span class="d-none d-md-inline ms-1">查看</span></button>
<button class="btn btn-sm btn-outline-primary" onclick="dashboard.updateWorkOrder(${wo.id})" title="编辑"><i class="fas fa-edit"></i><span class="d-none d-md-inline ms-1">编辑</span></button>
<button class="btn btn-sm btn-outline-danger" onclick="dashboard.deleteWorkOrder(${wo.id})" title="删除"><i class="fas fa-trash"></i><span class="d-none d-md-inline ms-1">删除</span></button>
</div>
</div>
</div>
</div>
`).join('');
container.innerHTML = headerHtml + workordersHtml;
},
updateWorkOrdersPagination(data) {
this.createPaginationComponent(data, 'workorders-pagination', 'loadWorkOrders', '个工单');
},
updateWorkOrderStatistics(workorders) {
const stats = workorders.reduce((acc, wo) => { acc.total = (acc.total || 0) + 1; acc[wo.status] = (acc[wo.status] || 0) + 1; return acc; }, {});
const statusMapping = {
'open': ['open', '待处理', '新建', 'new'],
'in_progress': ['in_progress', '处理中', '进行中', 'progress', 'processing'],
'resolved': ['resolved', '已解决', '已完成'],
'closed': ['closed', '已关闭', '关闭']
};
const mapped_counts = {'open': 0, 'in_progress': 0, 'resolved': 0, 'closed': 0};
for (const [status, count] of Object.entries(stats)) {
if (status === 'total') continue;
const status_lower = String(status).toLowerCase();
for (const [mapped_status, possible_values] of Object.entries(statusMapping)) {
if (possible_values.some(v => v.toLowerCase() === status_lower)) { mapped_counts[mapped_status] += count; break; }
}
}
document.getElementById('workorders-total').textContent = stats.total || 0;
document.getElementById('workorders-open').textContent = mapped_counts['open'];
document.getElementById('workorders-progress').textContent = mapped_counts['in_progress'];
document.getElementById('workorders-resolved').textContent = mapped_counts['resolved'];
},
toggleSelectAllWorkorders() {
const selectAllCheckbox = document.getElementById('select-all-workorders');
document.querySelectorAll('.workorder-checkbox').forEach(cb => cb.checked = selectAllCheckbox.checked);
this.updateBatchDeleteButton();
},
updateBatchDeleteButton() {
const selected = document.querySelectorAll('.workorder-checkbox:checked');
const btn = document.getElementById('batch-delete-workorders');
if (btn) { btn.disabled = selected.length === 0; btn.textContent = selected.length > 0 ? `批量删除 (${selected.length})` : '批量删除'; }
},
async batchDeleteWorkorders() {
const selectedIds = Array.from(document.querySelectorAll('.workorder-checkbox:checked')).map(cb => parseInt(cb.value));
if (selectedIds.length === 0) { this.showNotification('请选择要删除的工单', 'warning'); return; }
if (!confirm(`确定要删除选中的 ${selectedIds.length} 个工单吗?此操作不可撤销。`)) return;
try {
const response = await fetch('/api/batch-delete/workorders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: selectedIds }) });
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
this.cache.delete('workorders');
await this.loadWorkOrders(true);
await this.loadAnalytics();
this.updateBatchDeleteButton();
} else { this.showNotification(data.error || '批量删除失败', 'error'); }
} catch (error) { console.error('批量删除工单失败:', error); this.showNotification('批量删除工单失败', 'error'); }
},
async createWorkOrder() {
const title = document.getElementById('wo-title').value.trim();
const description = document.getElementById('wo-description').value.trim();
const category = document.getElementById('wo-category').value;
const priority = document.getElementById('wo-priority').value;
if (!title || !description) { this.showNotification('请填写完整信息', 'warning'); return; }
try {
const response = await fetch('/api/workorders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, description, category, priority }) });
const data = await response.json();
if (data.success) {
this.showNotification('工单创建成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('createWorkOrderModal')).hide();
document.getElementById('work-order-form').reset();
await this.loadWorkOrders();
await this.loadAnalytics();
} else { this.showNotification('创建工单失败: ' + (data.error || '未知错误'), 'error'); }
} catch (error) { console.error('创建工单失败:', error); this.showNotification('创建工单失败', 'error'); }
},
showCreateWorkOrderModal() {
const modal = new bootstrap.Modal(document.getElementById('createWorkOrderModal'));
modal.show();
},
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) {
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 class="ai-suggestion-section">
<div class="ai-suggestion-header">
<h6 class="ai-suggestion-title"><i class="fas fa-robot"></i>AI建议与人工描述</h6>
<button class="generate-ai-btn" onclick="dashboard.generateAISuggestion(${workorder.id})"><i class="fas fa-magic me-1"></i>生成AI建议</button>
</div>
<div class="ai-suggestion-content">
<label class="form-label fw-bold text-primary mb-2"><i class="fas fa-brain me-1"></i>AI建议</label>
<textarea id="aiSuggestion_${workorder.id}" class="form-control" rows="4" placeholder="点击上方按钮生成AI建议..." readonly></textarea>
</div>
<div class="human-resolution-content">
<label class="form-label fw-bold text-warning mb-2"><i class="fas fa-user-edit me-1"></i>人工描述</label>
<textarea id="humanResolution_${workorder.id}" class="form-control" rows="3" placeholder="请填写人工处理描述..."></textarea>
</div>
<div class="similarity-indicator">
<button class="save-human-btn" onclick="dashboard.saveHumanResolution(${workorder.id})"><i class="fas fa-save me-1"></i>保存人工描述并评估</button>
<span id="aiSim_${workorder.id}" class="similarity-badge bg-secondary"><i class="fas fa-percentage"></i>相似度: --</span>
<span id="aiApproved_${workorder.id}" class="status-badge pending">未审批</span>
</div>
<div class="action-buttons">
<button id="approveBtn_${workorder.id}" class="approve-btn" onclick="dashboard.approveToKnowledge(${workorder.id})" disabled><i class="fas fa-check me-1"></i>审批入库</button>
</div>
</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();
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 (workorder.id) { this.showEditWorkOrderModal(workorder); }
else { throw new Error(workorder.error || '获取工单详情失败'); }
} catch (error) { console.error('获取工单详情失败:', error); this.showNotification('获取工单详情失败: ' + error.message, 'error'); }
},
async deleteWorkOrder(workorderId) {
if (!confirm('确定要删除这个工单吗?此操作不可撤销。')) return;
try {
const response = await fetch(`/api/workorders/${workorderId}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
this.showNotification('工单删除成功', 'success');
await this.loadWorkOrders(this.paginationConfig.currentWorkOrderPage, true);
await this.loadAnalytics();
} else { this.showNotification('删除工单失败: ' + (data.error || '未知错误'), 'error'); }
} catch (error) { console.error('删除工单失败:', error); this.showNotification('删除工单失败: ' + error.message, 'error'); }
},
showEditWorkOrderModal(workorder) {
const modalHtml = `
<div class="modal fade" id="editWorkOrderModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content">
<div class="modal-header"><h5 class="modal-title">编辑工单 #${workorder.id}</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body"><form id="editWorkOrderForm">
<div class="row">
<div class="col-md-8"><div class="mb-3"><label 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 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 class="form-label">分类</label><select class="form-select" id="editCategory">
<option value="技术问题" ${workorder.category === '技术问题' ? 'selected' : ''}>技术问题</option>
<option value="APP功能" ${workorder.category === 'APP功能' ? 'selected' : ''}>APP功能</option>
<option value="远程控制" ${workorder.category === '远程控制' ? 'selected' : ''}>远程控制</option>
<option value="车辆绑定" ${workorder.category === '车辆绑定' ? 'selected' : ''}>车辆绑定</option>
<option value="系统故障" ${workorder.category === '系统故障' ? 'selected' : ''}>系统故障</option>
<option value="OTA升级" ${workorder.category === 'OTA升级' ? 'selected' : ''}>OTA升级</option>
<option value="其他" ${workorder.category === '其他' ? 'selected' : ''}>其他</option>
</select></div></div>
<div class="col-md-6"><div class="mb-3"><label 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 class="form-label">问题详细描述 *</label><textarea class="form-control" id="editDescription" rows="4" required>${workorder.description}</textarea></div>
<div class="mb-3"><label 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 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);
new bootstrap.Modal(document.getElementById('editWorkOrderModal')).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 (result.success) {
this.showNotification('工单更新成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('editWorkOrderModal')).hide();
await this.loadWorkOrders();
await this.loadAnalytics();
} 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 resp = await fetch('/api/workorders/import/template/file');
if (!resp.ok) throw new Error('下载接口返回错误');
const blob = await resp.blob();
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = blobUrl; a.download = '工单导入模板.xlsx';
document.body.appendChild(a); a.click(); document.body.removeChild(a);
window.URL.revokeObjectURL(blobUrl);
this.showNotification('模板下载成功', 'success');
} 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 {
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 (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();
setTimeout(() => { bootstrap.Modal.getInstance(document.getElementById('importWorkOrderModal')).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');
}
}
});

View File

@@ -2629,6 +2629,19 @@
<script src="{{ url_for('static', filename='js/app-new.js') }}"></script> <script src="{{ url_for('static', filename='js/app-new.js') }}"></script>
<!-- 原有dashboard.js保持兼容 --> <!-- 原有dashboard.js保持兼容 -->
<script src="{{ url_for('static', filename='js/dashboard.js') }}?v=1.0.9"></script> <script src="{{ url_for('static', filename='js/dashboard.js') }}?v=2.0.0"></script>
<!-- 功能模块 -->
<script src="{{ url_for('static', filename='js/modules/utils.js') }}?v=2.0.0"></script>
<script src="{{ url_for('static', filename='js/modules/dashboard-home.js') }}?v=2.0.0"></script>
<script src="{{ url_for('static', filename='js/modules/chat.js') }}?v=2.0.0"></script>
<script src="{{ url_for('static', filename='js/modules/agent.js') }}?v=2.0.0"></script>
<script src="{{ url_for('static', filename='js/modules/alerts.js') }}?v=2.0.0"></script>
<script src="{{ url_for('static', filename='js/modules/knowledge.js') }}?v=2.0.0"></script>
<script src="{{ url_for('static', filename='js/modules/workorders.js') }}?v=2.0.0"></script>
<script src="{{ url_for('static', filename='js/modules/conversations.js') }}?v=2.0.0"></script>
<script src="{{ url_for('static', filename='js/modules/monitoring.js') }}?v=2.0.0"></script>
<script src="{{ url_for('static', filename='js/modules/system.js') }}?v=2.0.0"></script>
<script src="{{ url_for('static', filename='js/modules/tenants.js') }}?v=2.0.0"></script>
<script src="{{ url_for('static', filename='js/modules/feishu-sync.js') }}?v=2.0.0"></script>
</body> </body>
</html> </html>