diff --git a/admin/dashboard/templates/contacts_management.html b/admin/dashboard/templates/contacts_management.html
index 8c4b8a1..a3f226d 100644
--- a/admin/dashboard/templates/contacts_management.html
+++ b/admin/dashboard/templates/contacts_management.html
@@ -405,11 +405,19 @@
群功能权限
- 一键启用
- 一键关闭
+
+ {% raw %}{{ groupPermissionsCollapsed ? '展开查看' : '收起' }}{% endraw %}
+
+
+ 一键启用
+ 一键关闭
+
-
+
+ 群功能权限默认折叠,点击“展开查看”后再进行配置。
+
+
@@ -434,9 +442,17 @@
-
+
+ 进群欢迎配置默认折叠,点击“展开查看”后再编辑当前群的欢迎文案。
+
+
@@ -835,7 +851,11 @@
managedGroupMap: {},
groupPermissions: [],
groupPermissionsLoading: false,
+ // 群权限区域默认折叠,避免详情弹窗一打开就被大量开关占满视线。
+ groupPermissionsCollapsed: true,
groupWelcomeConfigLoading: false,
+ // 欢迎配置表单默认折叠,需要用户主动展开后再查看或编辑。
+ groupWelcomeCollapsed: true,
groupWelcomeConfig: {
welcome_text_enabled: true,
welcome_text_template: '👏欢迎 {nickname} 加入群聊!🎉',
@@ -990,6 +1010,9 @@
handleCurrentChange(page) { this.currentPage = page; },
viewGroupDetails(group) {
this.currentGroup = { ...group };
+ // 切换群时恢复默认折叠态,保持“手动展开再看”的交互习惯。
+ this.groupPermissionsCollapsed = true;
+ this.groupWelcomeCollapsed = true;
this.groupDetailDialogVisible = true;
// 进入群详情时先加载群资料,保证群主/公告/管理员信息第一时间可见。
this.loadGroupProfile(group.wxid);
@@ -1249,7 +1272,19 @@
if (response.data.success) {
const members = response.data.data.members;
this.memberContextEnabled = !!response.data.data.member_context_enabled;
- this.groupMembersList = members.map(item => ({ wxid: item.wxid, name: item.nick_name, display_name: item.display_name, status: item.status, latest_active_time: item.latest_active_time, small_head_img_url: item.small_head_img_url, activity_level: item.activity_level, response_style_hint: item.response_style_hint, summary_text: item.summary_text, last_profiled_at: item.last_profiled_at }));
+ // 后端已按最后发言时间倒序返回,这里保持字段展开写法,方便后续继续扩展成员画像字段。
+ this.groupMembersList = members.map(item => ({
+ wxid: item.wxid,
+ name: item.nick_name,
+ display_name: item.display_name,
+ status: item.status,
+ latest_active_time: item.latest_active_time,
+ small_head_img_url: item.small_head_img_url,
+ activity_level: item.activity_level,
+ response_style_hint: item.response_style_hint,
+ summary_text: item.summary_text,
+ last_profiled_at: item.last_profiled_at
+ }));
} else { this.$message.error('获取群成员失败'); }
}).catch(error => { console.error('加载群成员数据失败:', error); this.$message.error('加载群成员数据失败'); }).finally(() => { this.groupMembersLoading = false; });
},
@@ -1416,9 +1451,37 @@
if (!lastMessage) return '暂无消息';
const sender = lastMessage.sender || '未知成员';
const time = lastMessage.timestamp || '';
- const content = lastMessage.content || '[非文本消息]';
+ const content = this.getCompactLastMessageContent(lastMessage);
return `${sender} · ${time} · ${content}`;
},
+ getCompactLastMessageContent(lastMessage) {
+ // 群详情顶部只需要紧凑预览,不需要把 XML、链接卡片原文或媒体原始内容完整展开。
+ const messageType = String((lastMessage && lastMessage.message_type) || '').trim();
+ const rawContent = String((lastMessage && lastMessage.content) || '').trim();
+ const compactMap = {
+ '3': '[图片]',
+ '43': '[视频]',
+ '62': '[视频]',
+ '47': '[表情]',
+ '1048625': '[表情]',
+ '1090519089': '[表情]',
+ '49': '[链接]'
+ };
+ if (compactMap[messageType]) {
+ return compactMap[messageType];
+ }
+ if (rawContent.startsWith(']+>/.test(rawContent)) {
+ return '[XML消息]';
+ }
+ if (!rawContent) {
+ return '[非文本消息]';
+ }
+ const singleLineContent = rawContent.replace(/\s+/g, ' ').trim();
+ return singleLineContent.length > 60 ? `${singleLineContent.slice(0, 60)}...` : singleLineContent;
+ },
openChatDialog(user) {
this.currentChatUser = user;
this.chatType = user && user.wxid && user.wxid.endsWith('@chatroom') ? 'group' : 'personal';
@@ -1715,6 +1778,11 @@
.group-permission-section { margin-top: 20px; }
.welcome-config-card { margin-top: 14px; }
.group-members-section { margin-top: 20px; }
+ .detail-card-header-actions { display: flex; align-items: center; gap: 10px; }
+ .collapsed-placeholder {
+ padding: 14px 16px; border-radius: 12px; background: #f8fafc; color: #64748b;
+ border: 1px dashed rgba(148, 163, 184, 0.45); font-size: 13px;
+ }
.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;
diff --git a/db/contacts_db.py b/db/contacts_db.py
index b7245f7..24d78dc 100644
--- a/db/contacts_db.py
+++ b/db/contacts_db.py
@@ -732,12 +732,23 @@ class ContactsDBOperator(BaseDBOperator):
return []
# 新增获取群成员列表接口
def get_chatroom_small_member_list(self, chatroom_id: str) -> List[dict]:
- """获取群成员列表"""
+ """获取群成员列表。
+
+ 说明:
+ 1. 群详情页的成员列表只展示当前仍在群内的成员,已退群成员不再返回;
+ 2. 成员需要按最后发言时间倒序展示,方便后台优先看到最近活跃的人;
+ 3. 从未发言的成员排在后面,避免把“无活跃记录”的成员顶到前面。
+ """
try:
sql = """
SELECT wxid, nick_name, display_name, status, latest_active_time, small_head_img_url
FROM t_chatroom_member
WHERE chatroom_id = %s
+ AND status = 1
+ ORDER BY
+ CASE WHEN latest_active_time IS NULL THEN 1 ELSE 0 END ASC,
+ latest_active_time DESC,
+ COALESCE(NULLIF(display_name, ''), nick_name, wxid) ASC
"""
results = self.execute_query(sql, (chatroom_id,))
@@ -916,6 +927,7 @@ class ContactsDBOperator(BaseDBOperator):
, CASE WHEN latest_active_time IS NULL THEN 1 ELSE 0 END AS never_spoken
FROM t_chatroom_member
WHERE chatroom_id = %s
+ AND status = 1
AND (latest_active_time IS NULL OR latest_active_time <= DATE_SUB(NOW(), INTERVAL %s DAY))
ORDER BY inactivity_days DESC
LIMIT %s
@@ -931,12 +943,18 @@ class ContactsDBOperator(BaseDBOperator):
return []
def get_group_member_summary(self, chatroom_id: str, inactive_days: int = 30) -> Dict[str, Any]:
- """获取群成员概览摘要"""
+ """获取群成员概览摘要。
+
+ 说明:
+ 1. 群运营看板里的成员统计口径应当只基于“当前仍在群内”的成员;
+ 2. 已退群成员如果继续参与统计,会导致僵尸成员数、活跃覆盖率等指标失真;
+ 3. 因此这里统一过滤 status = 1,确保详情页数字与成员列表保持一致。
+ """
try:
sql = """
SELECT
COUNT(*) AS total_members,
- SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS in_group_members,
+ COUNT(*) AS in_group_members,
SUM(CASE WHEN is_owner = 1 THEN 1 ELSE 0 END) AS owner_count,
SUM(CASE WHEN is_admin = 1 THEN 1 ELSE 0 END) AS admin_count,
SUM(CASE WHEN latest_active_time IS NOT NULL THEN 1 ELSE 0 END) AS spoken_members,
@@ -964,6 +982,7 @@ class ContactsDBOperator(BaseDBOperator):
) AS inactive_members
FROM t_chatroom_member
WHERE chatroom_id = %s
+ AND status = 1
"""
result = self.execute_query(sql, (inactive_days, chatroom_id), fetch_one=True) or {}
return {