增强群总结弹窗查看能力
- 后端群总结接口补充完整 summary_text 字段\n- 群运营详情页新增最近群总结全文弹窗\n- 保留原有摘要卡片展示并补充类型与生成时间
This commit is contained in:
@@ -97,8 +97,9 @@ def _load_recent_group_summaries(server, group_id: str, limit: int = 3) -> list:
|
|||||||
|
|
||||||
设计说明:
|
设计说明:
|
||||||
1. 群画像适合给“结论”,但运营者往往还想看到最近几期总结到底说了什么;
|
1. 群画像适合给“结论”,但运营者往往还想看到最近几期总结到底说了什么;
|
||||||
2. 这里不把整段长文原样吐给前端,而是裁成可扫读的短摘要,避免弹窗过重;
|
2. 默认卡片仍使用短摘要,避免详情页被大段文本挤占;
|
||||||
3. 若没有总结记录,返回空列表,由前端优雅降级。
|
3. 同时补充完整总结正文,供前端弹窗按需展开查看;
|
||||||
|
4. 若没有总结记录,返回空列表,由前端优雅降级。
|
||||||
"""
|
"""
|
||||||
summary_db = getattr(server, "message_summary_db", None)
|
summary_db = getattr(server, "message_summary_db", None)
|
||||||
if summary_db is None:
|
if summary_db is None:
|
||||||
@@ -124,6 +125,9 @@ def _load_recent_group_summaries(server, group_id: str, limit: int = 3) -> list:
|
|||||||
"summary_type": str(row.get("summary_type") or "").strip(),
|
"summary_type": str(row.get("summary_type") or "").strip(),
|
||||||
"source_message_count": int(row.get("source_message_count") or 0),
|
"source_message_count": int(row.get("source_message_count") or 0),
|
||||||
"last_generated_at": str(row.get("last_generated_at") or "").strip(),
|
"last_generated_at": str(row.get("last_generated_at") or "").strip(),
|
||||||
|
# 详情弹窗需要查看完整总结正文;这里保留原文,不做截断。
|
||||||
|
"summary_text": raw_summary,
|
||||||
|
# 卡片区域继续展示压缩后的摘要,避免详情首屏信息密度过高。
|
||||||
"summary_excerpt": normalized_summary[:180] + ("..." if len(normalized_summary) > 180 else ""),
|
"summary_excerpt": normalized_summary[:180] + ("..." if len(normalized_summary) > 180 else ""),
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -349,10 +349,24 @@
|
|||||||
<div class="ops-summary-timeline" v-if="(groupInsight.recent_summaries || []).length">
|
<div class="ops-summary-timeline" v-if="(groupInsight.recent_summaries || []).length">
|
||||||
<div v-for="item in groupInsight.recent_summaries" :key="`${item.summary_type}-${item.period_key}`" class="ops-summary-item">
|
<div v-for="item in groupInsight.recent_summaries" :key="`${item.summary_type}-${item.period_key}`" class="ops-summary-item">
|
||||||
<div class="ops-summary-head">
|
<div class="ops-summary-head">
|
||||||
<span class="ops-summary-period">{% raw %}{{ item.period_key || '-' }}{% endraw %}</span>
|
<div class="ops-summary-head-main">
|
||||||
<span class="ops-summary-meta">{% raw %}{{ item.source_message_count || 0 }}{% endraw %} 条消息</span>
|
<span class="ops-summary-period">{% raw %}{{ item.period_key || '-' }}{% endraw %}</span>
|
||||||
|
<span class="ops-summary-type">{% raw %}{{ formatSummaryTypeLabel(item.summary_type) }}{% endraw %}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ops-summary-head-side">
|
||||||
|
<span class="ops-summary-meta">{% raw %}{{ item.source_message_count || 0 }}{% endraw %} 条消息</span>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:disabled="!item.summary_text"
|
||||||
|
@click="openSummaryDetailDialog(item)">
|
||||||
|
查看全文
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ops-summary-excerpt">{% raw %}{{ item.summary_excerpt || '暂无摘要内容' }}{% endraw %}</div>
|
<div class="ops-summary-excerpt">{% raw %}{{ item.summary_excerpt || '暂无摘要内容' }}{% endraw %}</div>
|
||||||
|
<div class="ops-summary-time">{% raw %}{{ item.last_generated_at || '暂无生成时间' }}{% endraw %}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-inline">当前还没有可展示的群总结记录</div>
|
<div v-else class="empty-inline">当前还没有可展示的群总结记录</div>
|
||||||
@@ -850,6 +864,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="群总结详情" :visible.sync="summaryDetailDialogVisible" width="56%">
|
||||||
|
<div class="summary-detail-dialog" v-if="currentSummaryRecord">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="总结周期">{% raw %}{{ currentSummaryRecord.period_key || '-' }}{% endraw %}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="总结类型">{% raw %}{{ formatSummaryTypeLabel(currentSummaryRecord.summary_type) }}{% endraw %}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="消息样本">{% raw %}{{ currentSummaryRecord.source_message_count || 0 }}{% endraw %} 条</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="生成时间">{% raw %}{{ currentSummaryRecord.last_generated_at || '-' }}{% endraw %}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<div class="summary-detail-body">
|
||||||
|
<div class="summary-detail-title">完整总结</div>
|
||||||
|
<div class="summary-detail-text">{% raw %}{{ currentSummaryRecord.summary_text || '暂无可展示的总结正文' }}{% endraw %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog title="公众号详情" :visible.sync="officialDetailDialogVisible" width="50%">
|
<el-dialog title="公众号详情" :visible.sync="officialDetailDialogVisible" width="50%">
|
||||||
<div class="detail-avatar-wrap">
|
<div class="detail-avatar-wrap">
|
||||||
<el-avatar size="large" :src="getHeadImage(currentOfficial.wxid)" @error="() => true" class="detail-avatar">
|
<el-avatar size="large" :src="getHeadImage(currentOfficial.wxid)" @error="() => true" class="detail-avatar">
|
||||||
@@ -1053,6 +1082,9 @@
|
|||||||
groupAnnouncementSyncing: false,
|
groupAnnouncementSyncing: false,
|
||||||
groupInsight: null,
|
groupInsight: null,
|
||||||
groupInsightLoading: false,
|
groupInsightLoading: false,
|
||||||
|
// 群总结正文采用弹窗承载,避免详情区被长文本撑满。
|
||||||
|
summaryDetailDialogVisible: false,
|
||||||
|
currentSummaryRecord: null,
|
||||||
groupMembersList: [], groupMembersCurrentPage: 1, groupMembersPageSize: 10, groupMemberSearchQuery: '', groupMembersLoading: false,
|
groupMembersList: [], groupMembersCurrentPage: 1, groupMembersPageSize: 10, groupMemberSearchQuery: '', groupMembersLoading: false,
|
||||||
memberContextDialogVisible: false, memberContextLoading: false, memberContext: null, currentContextMember: {},
|
memberContextDialogVisible: false, memberContextLoading: false, memberContext: null, currentContextMember: {},
|
||||||
memberContextEnabled: false,
|
memberContextEnabled: false,
|
||||||
@@ -1217,6 +1249,9 @@
|
|||||||
// 切换群时恢复默认折叠态,保持“手动展开再看”的交互习惯。
|
// 切换群时恢复默认折叠态,保持“手动展开再看”的交互习惯。
|
||||||
this.groupPermissionsCollapsed = true;
|
this.groupPermissionsCollapsed = true;
|
||||||
this.groupWelcomeCollapsed = true;
|
this.groupWelcomeCollapsed = true;
|
||||||
|
// 群总结详情挂在当前群上下文里,因此切群时要清空旧弹窗与旧正文。
|
||||||
|
this.summaryDetailDialogVisible = false;
|
||||||
|
this.currentSummaryRecord = null;
|
||||||
this.groupDetailDialogVisible = true;
|
this.groupDetailDialogVisible = true;
|
||||||
// 进入群详情时先加载群资料,保证群主/公告/管理员信息第一时间可见。
|
// 进入群详情时先加载群资料,保证群主/公告/管理员信息第一时间可见。
|
||||||
this.loadGroupProfile(group.wxid);
|
this.loadGroupProfile(group.wxid);
|
||||||
@@ -1225,6 +1260,27 @@
|
|||||||
this.loadGroupInsights(group.wxid);
|
this.loadGroupInsights(group.wxid);
|
||||||
this.loadGroupWelcomeConfig(group.wxid);
|
this.loadGroupWelcomeConfig(group.wxid);
|
||||||
},
|
},
|
||||||
|
formatSummaryTypeLabel(summaryType) {
|
||||||
|
const normalizedType = String(summaryType || '').trim().toLowerCase();
|
||||||
|
// 这里把后端枚举名映射成更像运营视角的文案,避免把内部字段直接暴露给页面。
|
||||||
|
if (!normalizedType) return '常规总结';
|
||||||
|
const labelMap = {
|
||||||
|
daily: '日总结',
|
||||||
|
weekly: '周总结',
|
||||||
|
monthly: '月总结',
|
||||||
|
manual: '手动总结'
|
||||||
|
};
|
||||||
|
return labelMap[normalizedType] || summaryType;
|
||||||
|
},
|
||||||
|
openSummaryDetailDialog(summaryItem) {
|
||||||
|
if (!summaryItem || !summaryItem.summary_text) {
|
||||||
|
this.$message.info('当前总结还没有完整内容可查看');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 只读弹窗复制一份当前记录,避免后续列表刷新时把弹窗内容一并抖掉。
|
||||||
|
this.currentSummaryRecord = { ...summaryItem };
|
||||||
|
this.summaryDetailDialogVisible = true;
|
||||||
|
},
|
||||||
viewUserDetails(user) { this.currentUser = user; this.userDetailDialogVisible = true; },
|
viewUserDetails(user) { this.currentUser = user; this.userDetailDialogVisible = true; },
|
||||||
viewOfficialDetails(official) { this.currentOfficial = official; this.officialDetailDialogVisible = true; },
|
viewOfficialDetails(official) { this.currentOfficial = official; this.officialDetailDialogVisible = true; },
|
||||||
viewPublicDetails(publicFriend) { this.currentPublic = publicFriend; this.publicDetailDialogVisible = true; },
|
viewPublicDetails(publicFriend) { this.currentPublic = publicFriend; this.publicDetailDialogVisible = true; },
|
||||||
@@ -2106,11 +2162,33 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
.ops-summary-head-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.ops-summary-head-side {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.ops-summary-period {
|
.ops-summary-period {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
.ops-summary-type {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(37,99,235,0.08);
|
||||||
|
color: #1d4ed8;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
.ops-summary-meta {
|
.ops-summary-meta {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
@@ -2120,6 +2198,33 @@
|
|||||||
color: #475569;
|
color: #475569;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
.ops-summary-time {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.summary-detail-body {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.summary-detail-title {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
.summary-detail-text {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(148,163,184,0.14);
|
||||||
|
background: rgba(248,250,252,0.92);
|
||||||
|
color: #334155;
|
||||||
|
line-height: 1.8;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
max-height: 420px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
.feature-chip-list { display: flex; gap: 8px; flex-wrap: wrap; min-height: 60px; align-items: flex-start; }
|
.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; }
|
.empty-inline, .detail-inline-note { font-size: 12px; color: #64748b; }
|
||||||
.group-announcement-wrap { display: flex; gap: 12px; align-items: flex-start; justify-content: space-between; }
|
.group-announcement-wrap { display: flex; gap: 12px; align-items: flex-start; justify-content: space-between; }
|
||||||
|
|||||||
Reference in New Issue
Block a user