refactor: merge group permissions into contacts
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from flask import Blueprint, render_template, jsonify, request, current_app
|
||||
from flask import Blueprint, render_template, jsonify, request, current_app, redirect
|
||||
from .auth import login_required
|
||||
from loguru import logger
|
||||
from utils.robot_cmd.robot_command import GroupBotManager, Feature, PermissionStatus
|
||||
@@ -13,7 +13,7 @@ LOG = logger
|
||||
@robot_bp.route('/')
|
||||
@login_required
|
||||
def robot_management():
|
||||
return render_template('robot_management.html')
|
||||
return redirect('/contacts')
|
||||
|
||||
|
||||
# API路由
|
||||
|
||||
@@ -726,7 +726,6 @@
|
||||
defaultPath: '/groups',
|
||||
items: [
|
||||
{ label: '群组统计', path: '/groups' },
|
||||
{ label: '权限管理', path: '/robot' },
|
||||
{ label: '通讯录', path: '/contacts' },
|
||||
{ label: '虚拟群组', path: '/virtual_group' }
|
||||
]
|
||||
@@ -786,7 +785,7 @@
|
||||
},
|
||||
created() {
|
||||
const path = window.location.pathname;
|
||||
this.showTimeRangeSelector = ['/', '/plugins', '/users', '/groups', '/robot', '/errors'].includes(path);
|
||||
this.showTimeRangeSelector = ['/', '/plugins', '/users', '/groups', '/errors'].includes(path);
|
||||
},
|
||||
mounted() {
|
||||
document.querySelector('.app-container').classList.add('loaded');
|
||||
|
||||
@@ -75,6 +75,13 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="机器人状态" width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.robot_status === 'enabled' ? 'success' : 'info'">
|
||||
{% raw %}{{ scope.row.robot_status === 'enabled' ? '已启用' : '未启用' }}{% endraw %}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220">
|
||||
<template slot-scope="scope">
|
||||
<div class="action-row">
|
||||
@@ -176,12 +183,220 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog title="群组详情" :visible.sync="groupDetailDialogVisible" width="72%">
|
||||
<el-dialog title="群组详情" :visible.sync="groupDetailDialogVisible" width="84%" top="4vh">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="群ID">{% raw %}{{ currentGroup.wxid }}{% endraw %}</el-descriptions-item>
|
||||
<el-descriptions-item label="群名称">{% raw %}{{ currentGroup.name }}{% endraw %}</el-descriptions-item>
|
||||
<el-descriptions-item label="机器人状态">
|
||||
<el-tag :type="currentGroup.robot_status === 'enabled' ? 'success' : 'info'">
|
||||
{% raw %}{{ currentGroup.robot_status === 'enabled' ? '已启用' : '未启用' }}{% endraw %}
|
||||
</el-tag>
|
||||
<el-button
|
||||
size="mini"
|
||||
:type="currentGroup.robot_status === 'enabled' ? 'danger' : 'success'"
|
||||
class="inline-action-button"
|
||||
@click="toggleCurrentGroupRobotStatus">
|
||||
{% raw %}{{ currentGroup.robot_status === 'enabled' ? '关闭机器人' : '启用机器人' }}{% endraw %}
|
||||
</el-button>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="group-insight-section" v-loading="groupInsightLoading">
|
||||
<template v-if="groupInsight">
|
||||
<div class="detail-hero">
|
||||
<div>
|
||||
<div class="detail-health-label">群健康度</div>
|
||||
<div class="detail-health-value">{% raw %}{{ groupInsight.health_score }}{% endraw %}</div>
|
||||
<div class="detail-health-note">综合活跃、僵尸成员、消息量与插件调用生成</div>
|
||||
</div>
|
||||
<div class="detail-tags">
|
||||
<el-tag type="success" effect="plain">启用功能 {% raw %}{{ groupInsight.permissions.enabled_count }}{% endraw %}</el-tag>
|
||||
<el-tag type="info" effect="plain">近30天消息 {% raw %}{{ groupInsight.overview.message_count_30d }}{% endraw %}</el-tag>
|
||||
<el-tag type="warning" effect="plain">僵尸成员 {% raw %}{{ groupInsight.overview.zombie_member_count }}{% endraw %}</el-tag>
|
||||
<el-tag type="danger" effect="plain">插件调用 {% raw %}{{ groupInsight.overview.plugin_call_count_30d }}{% endraw %}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16" class="overview-grid detail-overview-grid">
|
||||
<el-col :span="6" v-for="item in groupInsightOverviewCards" :key="item.label">
|
||||
<el-card class="overview-card" shadow="never">
|
||||
<div class="overview-label">{% raw %}{{ item.label }}{% endraw %}</div>
|
||||
<div class="overview-value overview-value--detail">{% raw %}{{ item.value }}{% endraw %}</div>
|
||||
<div class="overview-note">{% raw %}{{ item.note }}{% endraw %}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-title">
|
||||
<h3>运营建议</h3>
|
||||
</div>
|
||||
<div class="suggestion-list">
|
||||
<el-alert
|
||||
v-for="(item, index) in groupInsight.operation_suggestions"
|
||||
:key="index"
|
||||
:title="item.title"
|
||||
:type="item.level"
|
||||
:description="item.desc"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="suggestion-item">
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16" class="detail-panels">
|
||||
<el-col :span="12">
|
||||
<el-card class="detail-card" shadow="never">
|
||||
<div slot="header" class="detail-card-header">
|
||||
<span>发言结构</span>
|
||||
<span class="detail-card-sub">近30天</span>
|
||||
</div>
|
||||
<el-table :data="groupInsight.message_summary.type_mix_30d" size="mini">
|
||||
<el-table-column prop="label" label="类型"></el-table-column>
|
||||
<el-table-column prop="count" label="数量" width="100"></el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card class="detail-card" shadow="never">
|
||||
<div slot="header" class="detail-card-header">
|
||||
<span>启用功能</span>
|
||||
<span class="detail-card-sub">当前权限</span>
|
||||
</div>
|
||||
<div class="feature-chip-list">
|
||||
<el-tag
|
||||
v-for="feature in groupInsight.permissions.enabled_features"
|
||||
:key="feature"
|
||||
size="small"
|
||||
type="success"
|
||||
effect="plain">
|
||||
{% raw %}{{ feature }}{% endraw %}
|
||||
</el-tag>
|
||||
<span v-if="!groupInsight.permissions.enabled_features.length" class="empty-inline">暂无启用功能</span>
|
||||
</div>
|
||||
<div class="detail-inline-note">
|
||||
最近一条消息:
|
||||
<span v-if="groupInsight.message_summary.last_message">
|
||||
{% raw %}{{ formatLastMessage(groupInsight.message_summary.last_message) }}{% endraw %}
|
||||
</span>
|
||||
<span v-else>暂无消息</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" class="detail-panels">
|
||||
<el-col :span="12">
|
||||
<el-card class="detail-card" shadow="never">
|
||||
<div slot="header" class="detail-card-header">
|
||||
<span>僵尸用户 / 潜水成员</span>
|
||||
<span class="detail-card-sub">30天未活跃优先</span>
|
||||
</div>
|
||||
<el-table :data="groupInsight.inactive_members" size="mini" empty-text="暂无明显沉默成员">
|
||||
<el-table-column prop="display_name" label="成员"></el-table-column>
|
||||
<el-table-column prop="inactivity_days" label="未活跃天数" width="110"></el-table-column>
|
||||
<el-table-column prop="latest_active_time" label="最后活跃时间" min-width="160"></el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card class="detail-card" shadow="never">
|
||||
<div slot="header" class="detail-card-header">
|
||||
<span>发言排行</span>
|
||||
<span class="detail-card-sub">近30天 Top 10</span>
|
||||
</div>
|
||||
<el-table :data="groupInsight.speaker_ranking" size="mini" empty-text="暂无发言数据">
|
||||
<el-table-column prop="display_name" label="成员"></el-table-column>
|
||||
<el-table-column prop="message_count" label="消息数" width="90"></el-table-column>
|
||||
<el-table-column prop="last_message_time" label="最后发言" min-width="160"></el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" class="detail-panels">
|
||||
<el-col :span="12">
|
||||
<el-card class="detail-card" shadow="never">
|
||||
<div slot="header" class="detail-card-header">
|
||||
<span>活跃高峰时段</span>
|
||||
<span class="detail-card-sub">近30天小时分布</span>
|
||||
</div>
|
||||
<div class="peak-hour-list">
|
||||
<div v-for="item in groupInsight.message_summary.peak_hours_30d" :key="item.label" class="peak-hour-item">
|
||||
<div class="peak-hour-rank">{% raw %}{{ item.label }}{% endraw %}</div>
|
||||
<div class="peak-hour-count">{% raw %}{{ item.message_count }}{% endraw %} 条</div>
|
||||
</div>
|
||||
<div v-if="!groupInsight.message_summary.peak_hours_30d.length" class="empty-inline">暂无足够数据</div>
|
||||
</div>
|
||||
<div class="chart-shell chart-shell--compact chart-shell--mini">
|
||||
<canvas id="contactsGroupHourlyChart" height="220"></canvas>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card class="detail-card" shadow="never">
|
||||
<div slot="header" class="detail-card-header">
|
||||
<span>插件调用情况</span>
|
||||
<span class="detail-card-sub">近30天</span>
|
||||
</div>
|
||||
<el-table :data="groupInsight.plugin_stats" size="mini" empty-text="暂无插件调用">
|
||||
<el-table-column prop="plugin_name" label="插件"></el-table-column>
|
||||
<el-table-column prop="command" label="命令"></el-table-column>
|
||||
<el-table-column prop="total_calls" label="调用" width="80"></el-table-column>
|
||||
<el-table-column prop="last_used_at" label="最近调用" min-width="160"></el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" class="detail-panels">
|
||||
<el-col :span="24">
|
||||
<el-card class="detail-card" shadow="never">
|
||||
<div slot="header" class="detail-card-header">
|
||||
<span>消息趋势</span>
|
||||
<span class="detail-card-sub">近14天</span>
|
||||
</div>
|
||||
<div class="chart-shell chart-shell--compact">
|
||||
<canvas id="contactsGroupTrendChart" height="240"></canvas>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="group-permission-section">
|
||||
<div class="section-title">
|
||||
<h3>群功能权限</h3>
|
||||
<div class="section-actions">
|
||||
<el-button size="mini" type="success" @click="updateAllGroupPermissions('enabled')">一键启用</el-button>
|
||||
<el-button size="mini" type="danger" @click="updateAllGroupPermissions('disabled')">一键关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="groupPermissions" style="width: 100%" v-loading="groupPermissionsLoading" size="mini">
|
||||
<el-table-column prop="feature_id" label="功能ID" width="90"></el-table-column>
|
||||
<el-table-column prop="feature_description" label="功能描述"></el-table-column>
|
||||
<el-table-column label="当前状态" width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.status === 'enabled' ? 'success' : 'info'">
|
||||
{% raw %}{{ scope.row.status === 'enabled' ? '已启用' : '已关闭' }}{% endraw %}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="切换" width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.statusBool"
|
||||
active-color="#10b981"
|
||||
inactive-color="#cbd5e1"
|
||||
@change="toggleGroupPermission(scope.row)">
|
||||
</el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="group-members-section">
|
||||
<div class="section-title">
|
||||
<h3>群成员列表</h3>
|
||||
@@ -447,6 +662,11 @@
|
||||
officialDetailDialogVisible: false,
|
||||
publicDetailDialogVisible: false,
|
||||
currentGroup: {}, currentUser: {}, currentOfficial: {}, currentPublic: {},
|
||||
managedGroupMap: {},
|
||||
groupPermissions: [],
|
||||
groupPermissionsLoading: false,
|
||||
groupInsight: null,
|
||||
groupInsightLoading: false,
|
||||
groupMembersList: [], groupMembersCurrentPage: 1, groupMembersPageSize: 10, groupMemberSearchQuery: '', groupMembersLoading: false,
|
||||
memberContextDialogVisible: false, memberContextLoading: false, memberContext: null, currentContextMember: {},
|
||||
memberContextEnabled: false,
|
||||
@@ -480,6 +700,20 @@
|
||||
const query = this.groupMemberSearchQuery.toLowerCase();
|
||||
const list = !query ? this.groupMembersList : this.groupMembersList.filter(member => member.wxid.toLowerCase().includes(query) || (member.name && member.name.toLowerCase().includes(query)));
|
||||
return list.slice((this.groupMembersCurrentPage - 1) * this.groupMembersPageSize, this.groupMembersCurrentPage * this.groupMembersPageSize);
|
||||
},
|
||||
groupInsightOverviewCards() {
|
||||
if (!this.groupInsight) return [];
|
||||
const overview = this.groupInsight.overview || {};
|
||||
return [
|
||||
{ label: '群成员数', value: overview.member_count || 0, note: '当前群成员规模' },
|
||||
{ label: '7天活跃成员', value: overview.active_member_count_7d || 0, note: '近7天至少发言一次' },
|
||||
{ label: '僵尸成员数', value: overview.zombie_member_count || 0, note: '30天未发言或从未发言' },
|
||||
{ label: '从未发言', value: overview.never_spoken_count || 0, note: '未记录到活跃痕迹' },
|
||||
{ label: '24h消息量', value: overview.message_count_24h || 0, note: '用于判断短期热度' },
|
||||
{ label: '30天消息量', value: overview.message_count_30d || 0, note: '用于判断持续热度' },
|
||||
{ label: '插件调用', value: overview.plugin_call_count_30d || 0, note: '近30天插件总触发次数' },
|
||||
{ label: '插件种类', value: overview.plugin_count_30d || 0, note: '本群真实使用到的插件数' }
|
||||
];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -489,7 +723,36 @@
|
||||
methods: {
|
||||
loadContactsData() {
|
||||
axios.get('/contacts/api/statistics').then(response => { if (response.data.success) this.statistics = response.data.data; }).catch(error => { console.error('加载联系人统计数据失败:', error); this.$message.error('加载联系人统计数据失败'); });
|
||||
axios.get('/contacts/api/groups').then(response => { if (response.data.success) { const groups = response.data.data.groups; this.groupsList = Object.entries(groups).map(([wxid, name]) => ({ wxid, name })); } }).catch(error => { console.error('加载群组数据失败:', error); this.$message.error('加载群组数据失败'); });
|
||||
Promise.all([
|
||||
axios.get('/robot/api/groups').catch(error => {
|
||||
console.error('加载群组管理状态失败:', error);
|
||||
return { data: { success: false, data: [] } };
|
||||
}),
|
||||
axios.get('/contacts/api/groups')
|
||||
]).then(([robotResponse, contactsResponse]) => {
|
||||
const managedList = robotResponse.data.success ? (robotResponse.data.data || []) : [];
|
||||
this.managedGroupMap = managedList.reduce((acc, item) => {
|
||||
acc[item.group_id] = item;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (contactsResponse.data.success) {
|
||||
const groups = contactsResponse.data.data.groups;
|
||||
this.groupsList = Object.entries(groups).map(([wxid, name]) => {
|
||||
const managed = this.managedGroupMap[wxid] || {};
|
||||
return {
|
||||
wxid,
|
||||
name,
|
||||
robot_status: managed.robot_status || 'disabled'
|
||||
};
|
||||
});
|
||||
} else {
|
||||
this.$message.error('加载群组数据失败');
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('加载群组数据失败:', error);
|
||||
this.$message.error('加载群组数据失败');
|
||||
});
|
||||
axios.get('/contacts/api/personal').then(response => { if (response.data.success) { const personal = response.data.data.personal; this.personalList = Object.entries(personal).map(([wxid, name]) => ({ wxid, name })); } }).catch(error => { console.error('加载个人联系人数据失败:', error); this.$message.error('加载个人联系人数据失败'); });
|
||||
axios.get('/contacts/api/official').then(response => { if (response.data.success) { const official = response.data.data.official; this.officialList = Object.entries(official).map(([wxid, name]) => ({ wxid, name })); } }).catch(error => { console.error('加载公众号数据失败:', error); this.$message.error('加载公众号数据失败'); });
|
||||
axios.get('/contacts/api/public').then(response => { if (response.data.success) { const publicFriends = response.data.data.public; this.publicList = Object.entries(publicFriends).map(([wxid, name]) => ({ wxid, name })); } }).catch(error => { console.error('加载公共好友数据失败:', error); this.$message.error('加载公共好友数据失败'); });
|
||||
@@ -504,10 +767,132 @@
|
||||
getHeadImage(wxid) { return this.headImages[wxid] || ''; },
|
||||
handleSizeChange(size) { this.pageSize = size; },
|
||||
handleCurrentChange(page) { this.currentPage = page; },
|
||||
viewGroupDetails(group) { this.currentGroup = group; this.groupDetailDialogVisible = true; this.loadGroupMembers(group.wxid); },
|
||||
viewGroupDetails(group) {
|
||||
this.currentGroup = { ...group };
|
||||
this.groupDetailDialogVisible = true;
|
||||
this.loadGroupMembers(group.wxid);
|
||||
this.loadGroupPermissions(group.wxid);
|
||||
this.loadGroupInsights(group.wxid);
|
||||
},
|
||||
viewUserDetails(user) { this.currentUser = user; this.userDetailDialogVisible = true; },
|
||||
viewOfficialDetails(official) { this.currentOfficial = official; this.officialDetailDialogVisible = true; },
|
||||
viewPublicDetails(publicFriend) { this.currentPublic = publicFriend; this.publicDetailDialogVisible = true; },
|
||||
loadGroupPermissions(groupId) {
|
||||
this.groupPermissionsLoading = true;
|
||||
this.groupPermissions = [];
|
||||
axios.get(`/robot/api/group/${groupId}/permissions`)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
const permissions = response.data.data || [];
|
||||
this.groupPermissions = permissions.map(item => ({
|
||||
...item,
|
||||
statusBool: item.status === 'enabled'
|
||||
}));
|
||||
} else {
|
||||
this.$message.error('加载群权限失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载群权限失败:', error);
|
||||
this.$message.error('加载群权限失败');
|
||||
})
|
||||
.finally(() => { this.groupPermissionsLoading = false; });
|
||||
},
|
||||
loadGroupInsights(groupId) {
|
||||
this.groupInsightLoading = true;
|
||||
this.groupInsight = null;
|
||||
axios.get(`/robot/api/group/${groupId}/detail`)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
this.groupInsight = response.data.data || null;
|
||||
this.$nextTick(() => {
|
||||
this.renderContactsGroupTrendChart();
|
||||
this.renderContactsGroupHourlyChart();
|
||||
});
|
||||
} else {
|
||||
this.$message.error('加载群诊断失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载群诊断失败:', error);
|
||||
this.$message.error('加载群诊断失败');
|
||||
})
|
||||
.finally(() => { this.groupInsightLoading = false; });
|
||||
},
|
||||
toggleGroupPermission(permission) {
|
||||
const newStatus = permission.statusBool ? 'enabled' : 'disabled';
|
||||
axios.post(`/robot/api/group/${this.currentGroup.wxid}/permissions`, {
|
||||
feature_id: permission.feature_id,
|
||||
status: newStatus
|
||||
}).then(response => {
|
||||
if (response.data.success) {
|
||||
permission.status = newStatus;
|
||||
if (permission.feature_name === 'ROBOT') {
|
||||
this.currentGroup.robot_status = newStatus;
|
||||
this.syncGroupRobotStatus(this.currentGroup.wxid, newStatus);
|
||||
}
|
||||
this.loadGroupInsights(this.currentGroup.wxid);
|
||||
this.$message.success('权限更新成功');
|
||||
} else {
|
||||
permission.statusBool = !permission.statusBool;
|
||||
this.$message.error('权限更新失败');
|
||||
}
|
||||
}).catch(error => {
|
||||
permission.statusBool = !permission.statusBool;
|
||||
console.error('权限更新失败:', error);
|
||||
this.$message.error('权限更新失败');
|
||||
});
|
||||
},
|
||||
updateAllGroupPermissions(status) {
|
||||
this.groupPermissionsLoading = true;
|
||||
const tasks = this.groupPermissions.map(item => axios.post(`/robot/api/group/${this.currentGroup.wxid}/permissions`, {
|
||||
feature_id: item.feature_id,
|
||||
status: status
|
||||
}));
|
||||
Promise.all(tasks).then(() => {
|
||||
this.groupPermissions.forEach(item => {
|
||||
item.status = status;
|
||||
item.statusBool = status === 'enabled';
|
||||
});
|
||||
const robotPermission = this.groupPermissions.find(item => item.feature_name === 'ROBOT');
|
||||
if (robotPermission) {
|
||||
this.currentGroup.robot_status = status;
|
||||
this.syncGroupRobotStatus(this.currentGroup.wxid, status);
|
||||
}
|
||||
this.loadGroupInsights(this.currentGroup.wxid);
|
||||
this.$message.success('群权限批量更新成功');
|
||||
}).catch(error => {
|
||||
console.error('群权限批量更新失败:', error);
|
||||
this.$message.error('群权限批量更新失败');
|
||||
}).finally(() => { this.groupPermissionsLoading = false; });
|
||||
},
|
||||
toggleCurrentGroupRobotStatus() {
|
||||
const newStatus = this.currentGroup.robot_status === 'enabled' ? 'disabled' : 'enabled';
|
||||
axios.post(`/robot/api/group/${this.currentGroup.wxid}/status`, {
|
||||
status: newStatus
|
||||
}).then(response => {
|
||||
if (response.data.success) {
|
||||
this.currentGroup.robot_status = newStatus;
|
||||
this.syncGroupRobotStatus(this.currentGroup.wxid, newStatus);
|
||||
this.loadGroupPermissions(this.currentGroup.wxid);
|
||||
this.loadGroupInsights(this.currentGroup.wxid);
|
||||
this.$message.success('机器人状态更新成功');
|
||||
} else {
|
||||
this.$message.error('机器人状态更新失败');
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('机器人状态更新失败:', error);
|
||||
this.$message.error('机器人状态更新失败');
|
||||
});
|
||||
},
|
||||
syncGroupRobotStatus(groupId, status) {
|
||||
this.groupsList = this.groupsList.map(item => item.wxid === groupId ? { ...item, robot_status: status } : item);
|
||||
if (this.managedGroupMap[groupId]) {
|
||||
this.managedGroupMap[groupId].robot_status = status;
|
||||
} else {
|
||||
this.managedGroupMap[groupId] = { group_id: groupId, robot_status: status };
|
||||
}
|
||||
},
|
||||
loadGroupMembers(roomid) {
|
||||
this.groupMembersLoading = true; this.groupMembersList = []; this.groupMembersCurrentPage = 1;
|
||||
axios.get(`/contacts/api/group_members/${roomid}`).then(response => {
|
||||
@@ -587,6 +972,103 @@
|
||||
this.$message.error('刷新成员交互摘要失败');
|
||||
}).finally(() => { this.memberContextLoading = false; });
|
||||
},
|
||||
renderContactsGroupTrendChart() {
|
||||
if (!this.groupInsight || !this.groupInsight.message_summary) return;
|
||||
const canvas = document.getElementById('contactsGroupTrendChart');
|
||||
if (!canvas) return;
|
||||
const trendData = this.groupInsight.message_summary.trend_14d || { dates: [], counts: [] };
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (this.charts && this.charts.contactsGroupTrendChart) {
|
||||
this.charts.contactsGroupTrendChart.destroy();
|
||||
}
|
||||
|
||||
if (!this.charts) {
|
||||
this.charts = {};
|
||||
}
|
||||
|
||||
this.charts.contactsGroupTrendChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: trendData.dates,
|
||||
datasets: [{
|
||||
label: '消息数量',
|
||||
data: trendData.counts,
|
||||
backgroundColor: 'rgba(249, 115, 22, 0.72)',
|
||||
borderRadius: 8
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: { beginAtZero: true, grid: { color: 'rgba(148,163,184,0.12)' } },
|
||||
x: { grid: { display: false } }
|
||||
},
|
||||
plugins: { legend: { display: false } }
|
||||
}
|
||||
});
|
||||
},
|
||||
renderContactsGroupHourlyChart() {
|
||||
if (!this.groupInsight || !this.groupInsight.message_summary) return;
|
||||
const canvas = document.getElementById('contactsGroupHourlyChart');
|
||||
if (!canvas) return;
|
||||
|
||||
const raw = this.groupInsight.message_summary.hourly_distribution_30d || [];
|
||||
const hourMap = {};
|
||||
raw.forEach(item => {
|
||||
hourMap[item.hour] = item.message_count;
|
||||
});
|
||||
|
||||
const labels = [];
|
||||
const counts = [];
|
||||
for (let i = 0; i < 24; i++) {
|
||||
labels.push(`${String(i).padStart(2, '0')}:00`);
|
||||
counts.push(hourMap[i] || 0);
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (this.charts && this.charts.contactsGroupHourlyChart) {
|
||||
this.charts.contactsGroupHourlyChart.destroy();
|
||||
}
|
||||
|
||||
if (!this.charts) {
|
||||
this.charts = {};
|
||||
}
|
||||
|
||||
this.charts.contactsGroupHourlyChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: '小时消息量',
|
||||
data: counts,
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(14, 165, 233, 0.12)',
|
||||
borderColor: 'rgba(14, 165, 233, 1)',
|
||||
tension: 0.32,
|
||||
borderWidth: 2,
|
||||
pointRadius: 1.5
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: { beginAtZero: true, grid: { color: 'rgba(148,163,184,0.12)' } },
|
||||
x: { grid: { display: false } }
|
||||
},
|
||||
plugins: { legend: { display: false } }
|
||||
}
|
||||
});
|
||||
},
|
||||
formatLastMessage(lastMessage) {
|
||||
if (!lastMessage) return '暂无消息';
|
||||
const sender = lastMessage.sender || '未知成员';
|
||||
const time = lastMessage.timestamp || '';
|
||||
const content = lastMessage.content || '[非文本消息]';
|
||||
return `${sender} · ${time} · ${content}`;
|
||||
},
|
||||
openChatDialog(user) { this.currentChatUser = user; this.chatDialogVisible = true; this.chatMessages = []; },
|
||||
async sendTextMessage() {
|
||||
if (!this.messageInput.trim()) return;
|
||||
@@ -652,16 +1134,61 @@
|
||||
.entity-subtitle { margin-top: 4px; font-size: 12px; color: #94a3b8; }
|
||||
.action-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.pagination-container { margin-top: 20px; text-align: right; }
|
||||
.group-insight-section { margin-top: 20px; }
|
||||
.group-permission-section { margin-top: 20px; }
|
||||
.group-members-section { margin-top: 20px; }
|
||||
.section-title {
|
||||
margin: 20px 0 15px 0; border-bottom: 1px solid rgba(148,163,184,0.12); padding-bottom: 10px;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.section-actions { display: flex; align-items: center; gap: 10px; }
|
||||
.inline-action-button { margin-left: 10px; }
|
||||
.section-title h3 { margin: 0; font-size: 18px; color: #0f172a; }
|
||||
.group-search { width: 220px; }
|
||||
.detail-avatar-wrap { text-align: center; margin-bottom: 20px; }
|
||||
.detail-avatar { width: 100px; height: 100px; }
|
||||
.detail-hero {
|
||||
display: flex; align-items: flex-start; justify-content: space-between; gap: 20px;
|
||||
padding: 18px 20px; margin-bottom: 16px; border-radius: 22px;
|
||||
background: linear-gradient(135deg, rgba(249,115,22,0.10), rgba(14,165,233,0.08), rgba(255,255,255,0.96));
|
||||
border: 1px solid rgba(148,163,184,0.14);
|
||||
}
|
||||
.detail-health-label { font-size: 13px; color: #9a3412; margin-bottom: 8px; }
|
||||
.detail-health-value { font-size: 38px; line-height: 1; font-weight: 700; color: #111827; margin-bottom: 10px; }
|
||||
.detail-health-note { font-size: 13px; color: #64748b; }
|
||||
.detail-tags { display: flex; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.detail-overview-grid { margin-bottom: 16px; }
|
||||
.overview-value--detail { font-size: 24px; }
|
||||
.detail-section { margin-bottom: 16px; }
|
||||
.diagnosis-grid {
|
||||
display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px;
|
||||
}
|
||||
.detail-panels .el-col { margin-bottom: 16px; }
|
||||
.detail-card { border-radius: 18px; }
|
||||
.detail-card-header {
|
||||
display: flex; align-items: center; justify-content: space-between; font-weight: 600; color: #0f172a;
|
||||
}
|
||||
.detail-card-sub { font-size: 12px; color: #94a3b8; font-weight: 500; }
|
||||
.feature-chip-list { display: flex; gap: 8px; flex-wrap: wrap; min-height: 60px; align-items: flex-start; }
|
||||
.empty-inline, .detail-inline-note { font-size: 12px; color: #64748b; }
|
||||
.detail-inline-note { margin-top: 12px; line-height: 1.6; }
|
||||
.suggestion-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.suggestion-item { border-radius: 14px; }
|
||||
.peak-hour-list { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
|
||||
.peak-hour-item {
|
||||
min-width: 132px; padding: 12px 14px; border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(14,165,233,0.08), rgba(255,255,255,0.96));
|
||||
border: 1px solid rgba(148,163,184,0.12);
|
||||
}
|
||||
.peak-hour-rank { font-size: 13px; color: #0f172a; font-weight: 600; margin-bottom: 6px; }
|
||||
.peak-hour-count { font-size: 12px; color: #64748b; }
|
||||
.chart-shell {
|
||||
padding: 16px; border-radius: 18px;
|
||||
background: linear-gradient(180deg, rgba(248,250,252,0.78), rgba(255,255,255,0.96));
|
||||
border: 1px solid rgba(148,163,184,0.12);
|
||||
}
|
||||
.chart-shell--compact { min-height: 260px; }
|
||||
.chart-shell--mini { min-height: 220px; }
|
||||
.member-context-toolbar {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 16px 0;
|
||||
}
|
||||
@@ -682,5 +1209,14 @@
|
||||
.message-time { font-size: 12px; color: #94a3b8; margin-top: 5px; }
|
||||
.input-area { padding: 20px 0 0; }
|
||||
.toolbar { margin-top: 10px; display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
@media (max-width: 1200px) {
|
||||
.diagnosis-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.detail-hero, .page-hero, .workspace-header, .section-title { flex-direction: column; align-items: stretch; }
|
||||
.page-hero-actions, .detail-tags { justify-content: flex-start; }
|
||||
.hero-search, .group-search { width: 100%; }
|
||||
.diagnosis-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user