Files
abot/admin/dashboard/templates/index.html
2025-11-25 17:01:11 +08:00

790 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}首页概览 - 机器人管理后台{% endblock %}
{% block content %}
<!-- 首页概览 -->
<div>
<!-- 用户信息和统计数据 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="6">
<el-card shadow="hover">
<div v-if="currentUser.success" class="user-info-container">
<el-row :gutter="10">
<el-col :span="6">
<div class="user-avatar-column">
<div class="user-avatar-container">
<img :src="currentUser.data.avatar" alt="用户头像" style="width: 82px; height: 82px;" />
</div>
</div>
</el-col>
<el-col :span="18">
<div class="user-info-column">
<div class="user-info-item">
<span class="user-info-label">昵称:</span>
<span class="user-info-value">{% raw %}{{ currentUser.data.nickname || '未知用户' }}{% endraw %}</span>
</div>
<div class="user-info-item">
<span class="user-info-label">微信ID:</span>
<span class="user-info-value">{% raw %}{{ currentUser.data.wx_id }}{% endraw %}</span>
</div>
<div class="user-info-item" v-if="currentUser.data.mobile">
<span class="user-info-label">手机号:</span>
<span class="user-info-value">{% raw %}{{ currentUser.data.mobile }}{% endraw %}</span>
</div>
<div class="user-info-item" v-if="currentUser.data.signature">
<span class="user-info-label">签名:</span>
<span class="user-info-value">{% raw %}{{ currentUser.data.signature }}{% endraw %}</span>
</div>
</div>
</el-col>
</el-row>
</div>
<div v-else class="user-info-empty">
<i class="el-icon-warning"></i>
<div>{% raw %}{{ currentUser.message || '未获取到用户信息' }}{% endraw %}</div>
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stats-card">
<div slot="header">
<span>总调用次数</span>
</div>
<div style="font-size: 24px; text-align: center;">
{% raw %}{{ totalCalls }}{% endraw %}
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stats-card">
<div slot="header">
<span>成功率</span>
</div>
<div style="font-size: 24px; text-align: center;">
{% raw %}{{ successRate.toFixed(2) }}{% endraw %}%
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stats-card">
<div slot="header">
<span>活跃用户数</span>
</div>
<div style="font-size: 24px; text-align: center;">
{% raw %}{{ activeUsers }}{% endraw %}
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stats-card">
<div slot="header">
<span>活跃群组数</span>
</div>
<div style="font-size: 24px; text-align: center;">
{% raw %}{{ activeGroups }}{% endraw %}
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stats-card">
<div slot="header">
<span>平均响应时间</span>
</div>
<div style="font-size: 24px; text-align: center;">
{% raw %}{{ avgResponseTime.toFixed(2) }}{% endraw %} ms
</div>
</el-card>
</el-col>
<el-col :span="3">
<el-card shadow="hover" class="stats-card">
<div slot="header">
<span>系统运行时间</span>
</div>
<div style="font-size: 24px; text-align: center;">
{% raw %}{{ formattedUptime }}{% endraw %}
</div>
</el-card>
</el-col>
</el-row>
<!-- 添加热门用户、群组和插件 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="8">
<el-card shadow="hover">
<div slot="header">
<h3>热门用户</h3>
</div>
<el-table :data="topUsers" style="width: 100%">
<!-- 修改将用户ID改为用户信息使用固定像素宽度 -->
<el-table-column label="用户信息" min-width="180">
<template slot-scope="scope">
{% raw %}{{ scope.row.user_name || scope.row.user_id }} ({{ scope.row.user_id }}){% endraw %}
</template>
</el-table-column>
<el-table-column prop="total_calls" label="调用次数" width="80" align="center"></el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<div slot="header">
<h3>热门群组</h3>
</div>
<el-table :data="topGroups" style="width: 100%">
<!-- 修改将群组ID改为群组信息使用固定像素宽度 -->
<el-table-column label="群组信息" min-width="180">
<template slot-scope="scope">
{% raw %}{{ scope.row.group_name || scope.row.group_id }} ({{ scope.row.group_id }}){% endraw %}
</template>
</el-table-column>
<el-table-column prop="total_calls" label="调用次数" width="80" align="center"></el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<div slot="header">
<h3>热门插件</h3>
</div>
<el-table :data="topPlugins" style="width: 100%">
<el-table-column prop="plugin_name" label="插件名称" min-width="180"></el-table-column>
<el-table-column prop="total_calls" label="调用次数" width="80" align="center"></el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="12">
<el-card shadow="hover">
<div slot="header">
<h3>插件使用排行</h3>
</div>
<div class="chart-container">
<canvas id="pluginChart"></canvas>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<div slot="header">
<h3>成功率分析</h3>
</div>
<div class="chart-container">
<canvas id="successRateChart"></canvas>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<el-card shadow="hover">
<div slot="header">
<h3>使用趋势</h3>
</div>
<div class="chart-container" style="height: 300px;">
<canvas id="trendChart" width="800" height="150"></canvas>
</div>
</el-card>
</el-col>
</el-row>
<!-- 添加一个新行用于显示按小时聊天趋势图 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<el-card shadow="hover" v-loading="hourlyMessageTrendLoading">
<div slot="header" style="display: flex; justify-content: space-between; align-items: center;">
<h3>按小时聊天趋势</h3>
<div style="display: flex; align-items: center;">
<el-select v-model="selectedGroupForHourlyTrend" placeholder="选择群组" style="width: 200px; margin-right: 10px;" @change="loadHourlyMessageTrend">
<el-option v-for="group in groups" :key="group.group_id" :label="group.group_name" :value="group.group_id"></el-option>
</el-select>
<el-select v-model="hourlyTrendDays" placeholder="选择时间范围" style="width: 120px;" @change="loadHourlyMessageTrend">
<el-option :value="1" label="最近1天"></el-option>
<el-option :value="2" label="最近2天"></el-option>
<el-option :value="3" label="最近3天"></el-option>
</el-select>
</div>
</div>
<div class="chart-container" style="height: 300px;">
<canvas id="hourlyMessageTrendChart" width="800" height="150"></canvas>
</div>
</el-card>
</el-col>
</el-row>
</div>
{% endblock %}
{% block scripts %}
<script>
new Vue({
el: '#app',
mixins: [baseApp],
data() {
return {
totalCalls: 0,
successRate: 0,
activeUsers: 0,
activeGroups: 0,
avgResponseTime: 0, // 确保这里初始化为数字
topUsers: [],
topGroups: [],
topPlugins: [],
pluginStats: [],
charts: {}, // 添加charts对象
// 添加当前用户信息对象
currentUser: {
success: false,
message: '加载中...',
data: {}
},
// 添加系统信息相关数据
systemInfo: {
os: '加载中...',
os_version: '',
python_version: '加载中...',
cpu_usage: 0,
memory_usage: 0,
disk_usage: 0,
uptime: 0,
timestamp: '-'
},
groups: [],
selectedGroupForHourlyTrend: '',
hourlyTrendDays: 1,
hourlyMessageTrendData: {
hours: [],
counts: []
},
hourlyMessageTrendLoading: true,
charts: {
pluginChart: null,
successRateChart: null,
trendChart: null,
cpuChart: null,
memoryChart: null,
diskChart: null,
hourlyMessageTrendChart: null // 添加这一行
}
}
},
computed: {
// 计算属性:格式化的运行时间
formattedUptime() {
const seconds = this.systemInfo.uptime;
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
let result = '';
if (days > 0) result += days + 'D';
if (hours > 0 || days > 0) result += hours + 'H';
result += minutes + 'M';
return result;
}
},
mounted() {
this.currentView = '1';
this.loadData();
// 加载系统信息
this.loadSystemInfo();
// 加载当前用户信息
this.loadCurrentUserInfo();
this.loadGroups(); // 添加这一行
// 设置定时刷新系统信息每30秒
this.systemInfoTimer = setInterval(this.loadSystemInfo, 30000);
},
beforeDestroy() {
// 组件销毁前清除定时器
if (this.systemInfoTimer) {
clearInterval(this.systemInfoTimer);
}
},
methods: {
loadData() {
const days = parseInt(this.timeRange);
this.loadDashboardSummary(days);
this.loadPluginStats(days);
this.loadPluginTrend(days);
},
// 添加加载系统信息的方法
loadSystemInfo() {
axios.get('/api/system_info')
.then(response => {
if (response.data.success) {
this.systemInfo = response.data.data;
this.$nextTick(() => {
this.renderSystemCharts();
});
}
})
.catch(error => {
console.error('加载系统信息出错:', error);
});
},
// 渲染系统状态饼图
renderSystemCharts() {
// 渲染CPU使用率饼图
this.renderPieChart('cpuChart', this.systemInfo.cpu_usage, 'CPU使用率');
// 渲染内存使用率饼图
this.renderPieChart('memoryChart', this.systemInfo.memory_usage, '内存使用率');
// 渲染磁盘使用率饼图
this.renderPieChart('diskChart', this.systemInfo.disk_usage, '磁盘使用率');
},
// 通用饼图渲染方法
renderPieChart(chartId, usageValue, label) {
const ctx = document.getElementById(chartId).getContext('2d');
// 销毁旧图表
if (this.charts[chartId]) {
this.charts[chartId].destroy();
}
// 设置颜色
let color = 'rgba(75, 192, 192, 1)'; // 绿色 (低负载)
if (usageValue > 70) {
color = 'rgba(255, 99, 132, 1)'; // 红色 (高负载)
} else if (usageValue > 50) {
color = 'rgba(255, 205, 86, 1)'; // 黄色 (中等负载)
}
// 创建新图表
this.charts[chartId] = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['已使用', '可用'],
datasets: [{
data: [usageValue, 100 - usageValue],
backgroundColor: [
color,
'rgba(220, 220, 220, 0.6)'
],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
cutout: '70%',
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return context.label + ': ' + context.raw + '%';
}
}
}
}
}
});
},
// 添加获取当前用户信息的方法
loadCurrentUserInfo() {
axios.get('/api/current_user_info')
.then(response => {
this.currentUser = response.data;
})
.catch(error => {
console.error('加载当前用户信息出错:', error);
this.currentUser = {
success: false,
message: '获取用户信息失败',
data: {}
};
});
},
// 添加获取进度条状态的方法
getProgressStatus(value) {
if (value > 70) {
return 'exception'; // 红色 (高负载)
} else if (value > 50) {
return 'warning'; // 黄色 (中等负载)
} else {
return 'success'; // 绿色 (低负载)
}
},
loadDashboardSummary(days) {
axios.get(`/api/dashboard_summary?days=${days}`)
.then(response => {
if (response.data.success) {
const data = response.data.data;
this.totalCalls = parseInt(data.total_calls) || 0;
this.successRate = parseFloat(data.success_rate) || 0;
this.activeUsers = data.active_users || 0;
this.activeGroups = data.active_groups || 0;
this.avgResponseTime = parseFloat(data.avg_response_time) || 0;
this.topUsers = data.top_users || [];
this.topGroups = data.top_groups || [];
this.topPlugins = data.top_plugins || [];
}
})
.catch(error => {
console.error('加载仪表盘摘要数据出错:', error);
this.$message.error('加载仪表盘摘要数据出错');
});
},
loadPluginStats(days) {
axios.get(`/api/plugin_stats?days=${days}`)
.then(response => {
if (response.data.success) {
this.pluginStats = response.data.data || [];
this.$nextTick(() => {
this.renderPluginChart();
this.renderSuccessRateChart();
});
}
})
.catch(error => {
console.error('加载插件统计数据出错:', error);
this.$message.error('加载插件统计数据出错');
});
},
loadPluginTrend(days) {
axios.get(`/api/plugin_trend?days=${days}`)
.then(response => {
if (response.data.success) {
const trendData = response.data.data || [];
this.$nextTick(() => {
this.renderTrendChart(trendData);
});
}
})
.catch(error => {
console.error('加载插件趋势数据出错:', error);
this.$message.error('加载插件趋势数据出错');
});
},
renderPluginChart() {
const ctx = document.getElementById('pluginChart').getContext('2d');
// 销毁旧图表
if (this.charts.pluginChart) {
this.charts.pluginChart.destroy();
}
// 准备数据
const sortedData = [...this.pluginStats].sort((a, b) => b.total_calls - a.total_calls).slice(0, 10);
// 修改这里:将插件名称和指令组合作为标签
const labels = sortedData.map(item => `${item.plugin_name}(${item.command})`);
const data = sortedData.map(item => item.total_calls);
// 创建新图表
this.charts.pluginChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: '调用次数',
data: data,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
}
}
});
},
renderSuccessRateChart() {
const ctx = document.getElementById('successRateChart').getContext('2d');
// 销毁旧图表
if (this.charts.successRateChart) {
this.charts.successRateChart.destroy();
}
// 准备数据
const sortedData = [...this.pluginStats]
.filter(item => item.total_calls > 0)
.sort((a, b) => (b.success_calls / b.total_calls) - (a.success_calls / a.total_calls))
.slice(0, 10);
// 修改这里:将插件名称和指令组合作为标签
const labels = sortedData.map(item => `${item.plugin_name}(${item.command})`);
const data = sortedData.map(item => (item.success_calls / item.total_calls) * 100);
// 创建新图表
this.charts.successRateChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: '成功率(%)',
data: data,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
},
renderTrendChart(trendData) {
const ctx = document.getElementById('trendChart').getContext('2d');
// 销毁旧图表
if (this.charts && this.charts.trendChart) {
this.charts.trendChart.destroy();
}
// 确保charts对象存在
if (!this.charts) {
this.charts = {};
}
// 准备数据 - 修改字段名匹配和数据类型转换
const labels = trendData.map(item => item.date); // 使用date而不是stat_date
const totalData = trendData.map(item => parseInt(item.total_calls) || 0); // 确保转换为数字
const successData = trendData.map(item => parseInt(item.success_calls) || 0);
const errorData = trendData.map(item => parseInt(item.failed_calls) || 0); // 使用failed_calls而不是error_calls
console.log('处理后的趋势数据:', { labels, totalData, successData, errorData });
// 创建新图表
this.charts.trendChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: '总调用',
data: totalData,
fill: false,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
tension: 0.1
},
{
label: '成功调用',
data: successData,
fill: false,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
tension: 0.1
},
{
label: '失败调用',
data: errorData,
fill: false,
backgroundColor: 'rgba(255, 99, 132, 0.6)',
borderColor: 'rgba(255, 99, 132, 1)',
tension: 0.1
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true
}
}
}
});
},
loadGroups() {
axios.get('/api/groups')
.then(response => {
if (response.data && response.data.groups) {
this.groups = response.data.groups;
if (this.groups.length > 0) {
this.selectedGroupForHourlyTrend = this.groups[0].group_id;
this.loadHourlyMessageTrend();
}
}
})
.catch(error => {
console.error('加载群组列表失败:', error);
this.$message.error('加载群组列表失败');
});
},
loadHourlyMessageTrend() {
if (!this.selectedGroupForHourlyTrend) return;
this.hourlyMessageTrendLoading = true;
axios.get('/api/hourly_message_trend', {
params: {
group_id: this.selectedGroupForHourlyTrend,
days: this.hourlyTrendDays
}
})
.then(response => {
if (response.data && response.data.success) {
this.hourlyMessageTrendData = response.data.data;
this.$nextTick(() => {
this.renderHourlyMessageTrendChart();
});
} else {
this.$message.error(response.data.error || '加载按小时聊天趋势数据失败');
}
this.hourlyMessageTrendLoading = false;
})
.catch(error => {
console.error('加载按小时聊天趋势数据失败:', error);
this.$message.error('加载按小时聊天趋势数据失败');
this.hourlyMessageTrendLoading = false;
});
},
renderHourlyMessageTrendChart() {
const ctx = document.getElementById('hourlyMessageTrendChart').getContext('2d');
// 销毁旧图表
if (this.charts.hourlyMessageTrendChart) {
this.charts.hourlyMessageTrendChart.destroy();
}
// 创建新图表
this.charts.hourlyMessageTrendChart = new Chart(ctx, {
type: 'line',
data: {
labels: this.hourlyMessageTrendData.hours,
datasets: [{
label: '消息数量',
data: this.hourlyMessageTrendData.counts,
fill: false,
backgroundColor: 'rgba(255, 99, 132, 0.6)',
borderColor: 'rgba(255, 99, 132, 1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
},
x: {
ticks: {
maxRotation: 45,
minRotation: 45
}
}
},
plugins: {
title: {
display: true,
text: `${this.hourlyMessageTrendData.group_name || '未知群组'} 的聊天趋势`
}
}
}
});
}
}
});
</script>
{% endblock %}
{% block styles %}
<style>
/* 基础卡片样式 */
.stats-card {
margin-bottom: 15px;
height: 120px;
}
.chart-container {
margin-bottom: 20px;
padding: 10px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.chart-container h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 16px;
color: #606266;
}
/* 用户信息卡片样式 */
.user-info-container {
padding: 5px 0;
}
.user-avatar-column {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.user-avatar-container {
width: 30px !important;
height: 30px !important;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.user-avatar-container img {
width: 15px !important;
height: 15px !important;
object-fit: contain;
}
.user-info-column {
padding-left: 5px;
}
.user-info-item {
display: flex;
align-items: center;
font-size: 12px;
line-height: 1.4;
margin-bottom: 4px;
}
.user-info-label {
color: #606266;
width: 45px;
flex-shrink: 0;
font-weight: bold;
}
.user-info-value {
color: #303133;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
}
.user-info-empty {
text-align: center;
color: #909399;
padding: 10px 0;
}
</style>
{% endblock %}