feat: 运营报告生成功能

- 后端新增 /admin/stats/report 接口,生成14天运营数据报告
- 报告内容:垃圾信息变化趋势、高频风险词Top10、误判率趋势
- 前端运营看板增加"生成报告"按钮,展示完整报告
- 支持复制报告文本到剪贴板

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
刘正航
2026-04-22 00:07:07 +08:00
parent 6d62120443
commit 385ebe25e7
4 changed files with 337 additions and 1 deletions

View File

@@ -486,6 +486,84 @@ button.btn::after {
color: #f2f7ff;
}
/* 运营报告样式 */
.report-modal {
margin-top: 20rpx;
padding: 24rpx;
border-radius: 24rpx;
background: linear-gradient(145deg, rgba(24, 47, 78, 0.95), rgba(16, 31, 53, 0.98));
border: 1rpx solid rgba(139, 177, 223, 0.35);
position: relative;
}
.report-header {
text-align: center;
margin-bottom: 16rpx;
}
.report-title {
font-size: 32rpx;
font-weight: 700;
color: #eff6ff;
}
.report-period {
font-size: 22rpx;
color: var(--sub);
margin-top: 6rpx;
}
.report-close {
position: absolute;
top: 16rpx;
right: 16rpx;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: rgba(255, 91, 111, 0.2);
color: #ffdce1;
font-size: 28rpx;
text-align: center;
line-height: 40rpx;
}
.report-section {
margin-top: 16rpx;
padding: 16rpx;
border-radius: 16rpx;
background: rgba(17, 35, 58, 0.6);
}
.report-section-title {
font-size: 26rpx;
font-weight: 600;
color: #dceeff;
margin-bottom: 12rpx;
}
.report-kpi {
padding: 12rpx;
border-radius: 12rpx;
background: rgba(19, 40, 66, 0.7);
text-align: center;
}
.report-kpi-value {
font-size: 28rpx;
font-weight: 700;
color: #f2f7ff;
}
.report-kpi-label {
font-size: 20rpx;
color: var(--sub);
margin-top: 4rpx;
}
.report-trend-item {
margin-top: 8rpx;
}
.chip-group {
display: flex;
flex-wrap: wrap;

View File

@@ -7,7 +7,9 @@ Page({
kpis: [],
bars: [],
sourceDist: [],
topKeywords: []
topKeywords: [],
report: null,
reportLoading: false
},
formatPercent(value, digits = 2) {
@@ -67,5 +69,74 @@ Page({
this.setData({ loading: false })
if (fromPullDown) wx.stopPullDownRefresh()
}
},
async generateReport() {
this.setData({ reportLoading: true })
try {
const report = await request({ url: '/admin/stats/report' })
// 处理趋势数据,计算进度条宽度
const spamTrend = (report.spam_trend || []).map((item) => {
const maxBlocked = Math.max(...(report.spam_trend || []).map((r) => r.blocked || 0), 1)
return {
...item,
blocked_percent: `${Math.max(4, Math.round((item.blocked || 0) / maxBlocked * 100))}%`
}
})
const misjudgeTrend = (report.misjudge_trend || []).map((item) => ({
...item,
rate_percent: `${Math.round((item.misjudge_rate || 0) * 100)}%`
}))
this.setData({
report: {
...report,
spam_trend: spamTrend,
misjudge_trend: misjudgeTrend
}
})
wx.showToast({ title: '报告已生成', icon: 'success' })
} finally {
this.setData({ reportLoading: false })
}
},
closeReport() {
this.setData({ report: null })
},
copyReportText() {
const report = this.data.report
if (!report) return
const summary = report.summary || {}
const lines = [
`【垃圾信息运营报告】`,
`报告周期:${report.period}`,
`生成时间:${(report.report_date || '').replace('T', ' ').slice(0, 19)}`,
'',
`【汇总统计】`,
`总发布量:${summary.total_posts || 0}`,
`拦截量:${summary.total_blocked || 0}`,
`正常发布:${summary.total_published || 0}`,
`拦截率:${this.formatPercent(summary.blocked_ratio, 2)}`,
`复核总数:${summary.total_reviews || 0}`,
`误判放行:${summary.total_approved || 0}`,
`平均误判率:${summary.avg_misjudge_rate_text || '0%'}`,
'',
`【高频风险词 Top10】`,
(report.top_keywords || []).slice(0, 10).map((k) => `${k.token}(${k.count}次)`).join('、'),
'',
`【近7日趋势】`,
(report.spam_trend || []).slice(-7).map((t) => `${t.label}: 拦截${t.blocked}条,发布${t.published}`).join('\n')
]
wx.setClipboardData({
data: lines.join('\n'),
success: () => wx.showToast({ title: '报告已复制', icon: 'success' })
})
}
})

View File

@@ -54,4 +54,83 @@
<text class="tag" wx:for="{{topKeywords}}" wx:key="token">{{item.token}} × {{item.count}}</text>
</view>
</view>
<!-- 生成报告按钮 -->
<view class="card fade-up fade-up-delay-3">
<button class="btn btn-accent" loading="{{reportLoading}}" bindtap="generateReport">生成运营报告</button>
</view>
<!-- 报告展示区域 -->
<view class="report-modal" wx:if="{{report}}">
<view class="report-header">
<view class="report-title">垃圾信息运营报告</view>
<view class="report-period">{{report.period}}</view>
</view>
<view class="report-close" bindtap="closeReport">×</view>
<view class="report-section">
<view class="report-section-title">汇总统计</view>
<view class="grid-3">
<view class="report-kpi">
<view class="report-kpi-value">{{report.summary.total_posts}}</view>
<view class="report-kpi-label">总发布量</view>
</view>
<view class="report-kpi">
<view class="report-kpi-value">{{report.summary.total_blocked}}</view>
<view class="report-kpi-label">拦截量</view>
</view>
<view class="report-kpi">
<view class="report-kpi-value">{{report.summary.total_published}}</view>
<view class="report-kpi-label">正常发布</view>
</view>
</view>
<view class="row">
<text class="label">拦截率</text>
<text class="value">{{report.summary.blocked_ratio * 100}}%</text>
</view>
<view class="row">
<text class="label">平均误判率</text>
<text class="value">{{report.summary.avg_misjudge_rate_text}}</text>
</view>
</view>
<view class="report-section">
<view class="report-section-title">垃圾信息数量变化近14天</view>
<view class="report-trend-item" wx:for="{{report.spam_trend}}" wx:key="date">
<view class="row">
<text class="label">{{item.label}}</text>
<text class="value">拦截 {{item.blocked}} / 发布 {{item.published}}</text>
</view>
<view class="progress-track">
<view class="progress-fill" style="width: {{item.blocked_percent}};"></view>
</view>
</view>
</view>
<view class="report-section">
<view class="report-section-title">高频风险词 Top10</view>
<view class="chip-group">
<text class="tag tag-danger" wx:for="{{report.topKeywords}}" wx:for-item="kw" wx:if="{{index < 10}}" wx:key="token">{{kw.token}} × {{kw.count}}</text>
</view>
</view>
<view class="report-section">
<view class="report-section-title">误判率趋势近14天</view>
<view class="report-trend-item" wx:for="{{report.misjudge_trend}}" wx:key="date">
<view class="row">
<text class="label">{{item.label}}</text>
<text class="value">{{item.misjudge_rate_text}}</text>
</view>
<view class="progress-track">
<view class="progress-fill-safe" style="width: {{item.rate_percent}};"></view>
</view>
</view>
</view>
<view class="btn-row">
<button class="btn btn-primary" bindtap="copyReportText">复制报告文本</button>
<button class="btn btn-ghost" bindtap="closeReport">关闭</button>
</view>
</view>
</view>