群成员列表
@@ -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; }
+ }
{% endblock %}