Files
abot/plugins/stats_dashboard/templates/robot_management.html
2025-03-26 14:36:21 +08:00

581 lines
27 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">
<el-col :span="24">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>群机器人管理</span>
<el-button
type="primary"
size="small"
style="float: right; margin-left: 10px;"
@click="showAddGroupDialog">
添加群组
</el-button>
<el-input
placeholder="搜索群组..."
v-model="searchQuery"
style="width: 200px; float: right"
clearable>
</el-input>
</div>
<!-- 群组列表 -->
<el-table
:data="filteredGroups"
style="width: 100%"
border
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="群组信息">
<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 label="机器人状态" width="120">
<template slot-scope="scope">
<el-tag
:type="scope.row.robot_status === 'enabled' ? 'success' : 'danger'">
{% raw %}{{ scope.row.robot_status === 'enabled' ? '已启用' : '已关闭' }}{% endraw %}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280">
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
@click="viewGroupPermissions(scope.row)">
查看权限
</el-button>
<el-button
size="mini"
:type="scope.row.robot_status === 'enabled' ? 'danger' : 'success'"
@click="toggleRobotStatus(scope.row)">
{% raw %}{{ scope.row.robot_status === 'enabled' ? '关闭' : '启用' }}{% endraw %}
</el-button>
<el-button
size="mini"
type="info"
@click="viewMessageTrend(scope.row)">
消息趋势
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 批量操作按钮 -->
<div style="margin-top: 20px" v-if="selectedGroups.length > 0">
<el-alert
title="批量操作"
type="info"
:closable="false">
<span>已选择 {% raw %}{{ selectedGroups.length }}{% endraw %} 个群组</span>
</el-alert>
<div style="margin-top: 10px">
<el-button type="success" size="small" @click="batchEnableRobot">批量启用</el-button>
<el-button type="danger" size="small" @click="batchDisableRobot">批量关闭</el-button>
<el-button type="warning" size="small" @click="batchRemoveGroups">批量移除</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 群组权限管理对话框 -->
<el-dialog
:title="currentGroupName + ' 功能权限管理'"
:visible.sync="permissionDialogVisible"
width="70%">
<el-table :data="permissions" style="width: 100%" border>
<el-table-column prop="feature_id" label="功能ID" width="80"></el-table-column>
<el-table-column prop="feature_description" label="功能描述"></el-table-column>
<el-table-column label="状态" width="100">
<template slot-scope="scope">
<el-tag
:type="scope.row.status === 'enabled' ? 'success' : 'danger'">
{% raw %}{{ scope.row.status === 'enabled' ? '已启用' : '已关闭' }}{% endraw %}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template slot-scope="scope">
<el-switch
v-model="scope.row.statusBool"
active-color="#13ce66"
inactive-color="#ff4949"
@change="togglePermission(scope.row)">
</el-switch>
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button @click="enableAllPermissions" type="success">一键启用全部</el-button>
<el-button @click="disableAllPermissions" type="danger">一键关闭全部</el-button>
<el-button @click="permissionDialogVisible = false">关闭</el-button>
</div>
</el-dialog>
<!-- 添加群组对话框 -->
<el-dialog
title="添加群组"
:visible.sync="addGroupDialogVisible"
width="30%">
<el-form :model="addGroupForm" :rules="addGroupRules" ref="addGroupForm">
<el-form-item label="群组ID" prop="groupId">
<el-input v-model="addGroupForm.groupId" placeholder="请输入群组ID"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addGroupDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitAddGroup">确定</el-button>
</span>
</el-dialog>
<!-- 群组消息趋势图对话框 -->
<el-dialog
:title="currentGroupName + ' 消息趋势'"
:visible.sync="trendDialogVisible"
width="70%"
@opened="onTrendDialogOpened"
destroy-on-close>
<div v-if="trendLoading" style="text-align: center; padding: 20px;">
<i class="el-icon-loading"></i>
<p>加载中...</p>
</div>
<div v-else>
<div class="chart-container">
<h3>消息数量趋势</h3>
<!-- 添加 v-if 确保 trendData 存在 -->
<div v-if="trendData && trendData.dates && trendData.dates.length > 0">
<table class="el-table" style="width: 100%;">
<thead>
<tr>
<th style="padding: 12px 0; text-align: center; background-color: #f5f7fa; color: #606266; font-weight: bold; border-bottom: 1px solid #ebeef5;">日期</th>
<th style="padding: 12px 0; text-align: center; background-color: #f5f7fa; color: #606266; font-weight: bold; border-bottom: 1px solid #ebeef5;">消息数量</th>
</tr>
</thead>
<tbody>
<tr v-for="(date, index) in trendData.dates" :key="index">
<td style="padding: 12px 0; text-align: center; border-bottom: 1px solid #ebeef5;">{{ date }}</td>
<td style="padding: 12px 0; text-align: center; border-bottom: 1px solid #ebeef5;">
{{ trendData.counts[index] }}
<span style="color: #909399; font-size: 12px;">({{ getPercentage(index) }}%)</span>
<div style="width: 100%; height: 6px; background-color: #f0f0f0; margin-top: 5px; border-radius: 3px;">
<div style="height: 100%; background-color: #409EFF; border-radius: 3px;" :style="{ width: getPercentage(index) + '%' }"></div>
</div>
</td>
</tr>
<tr>
<td style="padding: 12px 0; text-align: center; border-bottom: 1px solid #ebeef5; font-weight: bold;">总计</td>
<td style="padding: 12px 0; text-align: center; border-bottom: 1px solid #ebeef5; font-weight: bold;">{{ totalMessages }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else style="text-align: center; padding: 20px;">暂无数据</div>
<!-- 统计摘要 - 同样添加 v-if 确保 trendData 存在 -->
<div v-if="trendData && trendData.dates && trendData.dates.length > 0" style="margin-top: 20px; padding: 15px; background-color: #f5f7fa; border-radius: 4px;">
<h4 style="margin: 0 0 10px 0;">统计摘要</h4>
<div style="display: flex; flex-wrap: wrap; gap: 15px;">
<div style="flex: 1; min-width: 150px;">
<div style="font-size: 12px; color: #909399;">总消息数</div>
<div style="font-size: 18px; font-weight: bold; color: #303133;">{{ totalMessages }}</div>
</div>
<div style="flex: 1; min-width: 150px;">
<div style="font-size: 12px; color: #909399;">日均消息数</div>
<div style="font-size: 18px; font-weight: bold; color: #303133;">{{ avgMessages }}</div>
</div>
<div style="flex: 1; min-width: 150px;">
<div style="font-size: 12px; color: #909399;">最高消息日</div>
<div style="font-size: 18px; font-weight: bold; color: #303133;">{{ maxDay }} ({{ maxMessages }})</div>
</div>
<div style="flex: 1; min-width: 150px;">
<div style="font-size: 12px; color: #909399;">最低消息日</div>
<div style="font-size: 18px; font-weight: bold; color: #303133;">{{ minDay }} ({{ minMessages }})</div>
</div>
</div>
</div>
</div>
<div style="margin-top: 20px; text-align: center;">
<el-radio-group v-model="trendDays" @change="loadMessageTrend">
<el-radio-button :label="7">最近7天</el-radio-button>
<el-radio-button :label="14">最近14天</el-radio-button>
<el-radio-button :label="30">最近30天</el-radio-button>
</el-radio-group>
</div>
</div>
</el-dialog>
</div>
{% endblock %}
{% block scripts %}
<script>
new Vue({
el: '#app',
mixins: [baseApp],
data() {
return {
groups: [],
permissions: [],
currentGroupId: null,
currentGroupName: '',
searchQuery: '',
selectedGroups: [],
permissionDialogVisible: false,
// 添加群组相关数据
addGroupDialogVisible: false,
addGroupForm: {
groupId: ''
},
addGroupRules: {
groupId: [
{ required: true, message: '请输入群组ID', trigger: 'blur' },
{ pattern: /^\S+$/, message: '群组ID不能包含空格', trigger: 'blur' }
]
},
// 趋势图相关数据
trendDialogVisible: false,
trendLoading: false,
trendDays: 7,
trendData: {
dates: [],
counts: []
},
charts: {} // 保留charts对象以防其他地方还在使用
}
},
computed: {
filteredGroups() {
if (!this.searchQuery) return this.groups;
const query = this.searchQuery.toLowerCase();
return this.groups.filter(group =>
(group.group_id && group.group_id.toLowerCase().includes(query)) ||
(group.group_name && group.group_name.toLowerCase().includes(query))
);
},
// 计算属性用于趋势数据
totalMessages() {
if (!this.trendData || !this.trendData.counts) return 0;
return this.trendData.counts.reduce((sum, count) => sum + parseInt(count || 0), 0);
},
avgMessages() {
if (!this.trendData || !this.trendData.dates || this.trendData.dates.length === 0) return 0;
return (this.totalMessages / this.trendData.dates.length).toFixed(2);
},
maxMessages() {
if (!this.trendData || !this.trendData.counts || this.trendData.counts.length === 0) return 0;
return Math.max(...this.trendData.counts.map(c => parseInt(c || 0)));
},
minMessages() {
if (!this.trendData || !this.trendData.counts || this.trendData.counts.length === 0) return 0;
return Math.min(...this.trendData.counts.map(c => parseInt(c || 0)));
},
maxDay() {
if (!this.trendData || !this.trendData.counts || !this.trendData.dates) return '';
const maxIndex = this.trendData.counts.indexOf(this.maxMessages.toString());
return maxIndex >= 0 ? this.trendData.dates[maxIndex] : '';
},
minDay() {
if (!this.trendData || !this.trendData.counts || !this.trendData.dates) return '';
const minIndex = this.trendData.counts.indexOf(this.minMessages.toString());
return minIndex >= 0 ? this.trendData.dates[minIndex] : '';
}
},
mounted() {
this.currentView = '6'; // 设置当前菜单项
this.loadGroups();
},
methods: {
loadGroups() {
axios.get('/api/robot/groups')
.then(response => {
if (response.data.success) {
this.groups = response.data.data || [];
} else {
this.$message.error('加载群组失败');
}
})
.catch(error => {
console.error('加载群组失败:', error);
this.$message.error('加载群组失败: ' + error.message);
});
},
handleSelectionChange(selection) {
this.selectedGroups = selection.map(item => item.group_id);
},
viewGroupPermissions(group) {
this.currentGroupId = group.group_id;
this.currentGroupName = group.group_name || group.group_id;
axios.get(`/api/robot/group/${group.group_id}/permissions`)
.then(response => {
if (response.data.success) {
// 添加布尔值属性用于switch组件
const permissionsData = response.data.data || [];
this.permissions = permissionsData.map(p => ({
...p,
statusBool: p.status === 'enabled'
}));
this.permissionDialogVisible = true;
} else {
this.$message.error('加载权限失败');
}
})
.catch(error => {
console.error('加载权限失败:', error);
this.$message.error('加载权限失败: ' + error.message);
});
},
togglePermission(permission) {
const newStatus = permission.statusBool ? 'enabled' : 'disabled';
axios.post(`/api/robot/group/${this.currentGroupId}/permission`, {
feature_id: permission.feature_id,
status: newStatus
})
.then(response => {
if (response.data.success) {
permission.status = newStatus;
this.$message.success('更新权限成功');
} else {
// 恢复原状态
permission.statusBool = !permission.statusBool;
this.$message.error('更新权限失败');
}
})
.catch(error => {
// 恢复原状态
permission.statusBool = !permission.statusBool;
console.error('更新权限失败:', error);
this.$message.error('更新权限失败: ' + error.message);
});
},
enableAllPermissions() {
this.updateAllPermissions('enabled');
},
disableAllPermissions() {
this.updateAllPermissions('disabled');
},
updateAllPermissions(status) {
axios.post(`/api/robot/group/${this.currentGroupId}/permissions`, {
status: status
})
.then(response => {
if (response.data.success) {
// 更新本地数据
this.permissions.forEach(p => {
p.status = status;
p.statusBool = status === 'enabled';
});
this.$message.success('批量更新权限成功');
} else {
this.$message.error('批量更新权限失败');
}
})
.catch(error => {
console.error('批量更新权限失败:', error);
this.$message.error('批量更新权限失败: ' + error.message);
});
},
toggleRobotStatus(group) {
const newStatus = group.robot_status === 'enabled' ? 'disabled' : 'enabled';
axios.post(`/api/robot/group/${group.group_id}/status`, {
status: newStatus
})
.then(response => {
if (response.data.success) {
group.robot_status = newStatus;
this.$message.success('更新机器人状态成功');
} else {
this.$message.error('更新机器人状态失败');
}
})
.catch(error => {
console.error('更新机器人状态失败:', error);
this.$message.error('更新机器人状态失败: ' + error.message);
});
},
showAddGroupDialog() {
this.addGroupForm.groupId = '';
this.addGroupDialogVisible = true;
},
submitAddGroup() {
this.$refs.addGroupForm.validate(valid => {
if (valid) {
axios.post('/api/robot/group', {
group_id: this.addGroupForm.groupId
})
.then(response => {
if (response.data.success) {
this.addGroupDialogVisible = false;
this.$message.success('添加群组成功');
// 重新加载群组列表
this.loadGroups();
} else {
this.$message.error('添加群组失败');
}
})
.catch(error => {
console.error('添加群组失败:', error);
this.$message.error('添加群组失败: ' + error.message);
});
}
});
},
batchEnableRobot() {
this.batchUpdateRobotStatus('enabled');
},
batchDisableRobot() {
this.batchUpdateRobotStatus('disabled');
},
batchUpdateRobotStatus(status) {
if (this.selectedGroups.length === 0) {
this.$message.warning('请先选择群组');
return;
}
axios.post('/api/robot/batch_operation', {
operation: 'update_status',
group_ids: this.selectedGroups,
status: status
})
.then(response => {
if (response.data.success) {
// 更新本地数据
this.groups.forEach(g => {
if (this.selectedGroups.includes(g.group_id)) {
g.robot_status = status;
}
});
this.$message.success('批量更新状态成功');
} else {
this.$message.error('批量更新状态失败');
}
})
.catch(error => {
console.error('批量更新状态失败:', error);
this.$message.error('批量更新状态失败: ' + error.message);
});
},
batchRemoveGroups() {
if (this.selectedGroups.length === 0) {
this.$message.warning('请先选择群组');
return;
}
this.$confirm(`确定要批量移除 ${this.selectedGroups.length} 个群组吗? 此操作将清除这些群组的所有设置。`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'danger'
}).then(() => {
axios.post('/api/robot/batch_operation', {
operation: 'remove_groups',
group_ids: this.selectedGroups
})
.then(response => {
if (response.data.success) {
// 从列表中移除这些群组
this.groups = this.groups.filter(g => !this.selectedGroups.includes(g.group_id));
this.selectedGroups = [];
this.$message.success('批量移除成功');
} else {
this.$message.error('批量移除失败');
}
})
.catch(error => {
console.error('批量移除失败:', error);
this.$message.error('批量移除失败: ' + error.message);
});
}).catch(() => {});
},
// 查看消息趋势
viewMessageTrend(group) {
this.currentGroupId = group.group_id;
this.currentGroupName = group.group_name || group.group_id;
this.trendDialogVisible = true;
},
// 对话框打开后的回调
onTrendDialogOpened() {
console.log('对话框已打开');
this.$nextTick(() => {
this.loadMessageTrend();
});
},
loadMessageTrend() {
this.trendLoading = true;
axios.get(`/api/robot/group/${this.currentGroupId}/message_trend?days=${this.trendDays}`)
.then(response => {
if (response.data.success) {
// 直接更新数据让Vue的响应式系统处理渲染
this.trendData = response.data.data || { dates: [], counts: [] };
this.trendLoading = false;
} else {
this.$message.error('加载消息趋势失败');
// 设置默认值
this.trendData = { dates: [], counts: [] };
this.trendLoading = false;
}
})
.catch(error => {
console.error('加载消息趋势失败:', error);
this.$message.error('加载消息趋势失败: ' + error.message);
// 设置默认值
this.trendData = { dates: [], counts: [] };
this.trendLoading = false;
});
},
// 计算百分比的方法
getPercentage(index) {
if (!this.trendData || !this.trendData.counts || !Array.isArray(this.trendData.counts)) return 0;
if (index < 0 || index >= this.trendData.counts.length) return 0;
const count = parseInt(this.trendData.counts[index] || 0);
return this.totalMessages > 0 ?
((count / this.totalMessages) * 100).toFixed(2) : 0;
},
// 确保在打开对话框时重置 trendData
viewMessageTrend(group) {
this.currentGroupId = group.group_id;
this.currentGroupName = group.group_name || group.group_id;
// 重置 trendData 为默认值
this.trendData = { dates: [], counts: [] };
this.trendDialogVisible = true;
}
// ... 其他方法保持不变 ...
}
});
</script>
{% endblock %}
{% block styles %}
<style>
.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;
}
</style>
{% endblock %}