v0.3.20: 每日汇报系统 — 每天8点自动收集前一日数据并推送汇总报告
新增:
- src/services/daily-report.service.ts (核心服务: 数据收集/报告生成/格式化/调度器)
- API: GET/PUT daily-report/config, GET daily-report/preview, POST daily-report/test, GET daily-report/last-run
- 前端: 侧边栏"📊 每日汇报"菜单 + SystemConfig.vue 配置面板(时间/内容开关/预览/测试发送)
- main.ts: 每60秒检查调度, 08:00-08:04 窗口内运行
报告内容: 搜索统计/转存统计(成功率)/各网盘容量和活跃状态/用户数
This commit is contained in:
@@ -38,6 +38,7 @@
|
||||
<el-menu-item index="sys-strategy">⚡ 性能配置</el-menu-item>
|
||||
<el-menu-item index="sys-password">🔑 修改密码</el-menu-item>
|
||||
<el-menu-item index="sys-notify">📬 消息推送</el-menu-item>
|
||||
<el-menu-item index="sys-daily-report">📊 每日汇报</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item index="save-records">
|
||||
@@ -96,6 +97,7 @@ const pageTitles: Record<string, string> = {
|
||||
'sys-strategy': '性能配置',
|
||||
'sys-password': '修改管理员密码',
|
||||
'sys-notify': '消息推送',
|
||||
'sys-daily-report': '每日汇报',
|
||||
'save-records': '转存日志',
|
||||
}
|
||||
|
||||
|
||||
@@ -612,6 +612,51 @@
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
|
||||
<!-- 📊 每日汇报 -->
|
||||
<el-card id="section-sys-daily-report" v-show="!activeSection || activeSection === 'sys-daily-report'">
|
||||
<template #header>
|
||||
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||
<span>📊 每日汇报</span>
|
||||
<div>
|
||||
<el-button size="small" :loading="dailyReportPreviewing" @click="handleDailyReportPreview">📋 预览</el-button>
|
||||
<el-button size="small" type="primary" :loading="dailyReportSending" @click="handleDailyReportSendTest">▶ 发送测试</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="140px" label-position="left">
|
||||
<el-form-item label="启用每日汇报">
|
||||
<el-switch v-model="dailyReportForm.enabled" active-text="每天8点自动发送" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发送时间">
|
||||
<el-time-picker
|
||||
v-model="dailyReportForm.time"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
placeholder="选择时间"
|
||||
:disabled="!dailyReportForm.enabled"
|
||||
/>
|
||||
<div class="form-tip">默认每天 08:00 发送前一天的汇总报告</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="报告内容">
|
||||
<div style="display:flex; flex-wrap:wrap; gap:16px;">
|
||||
<el-switch v-model="dailyReportForm.includeSearch" active-text="搜索统计" :disabled="!dailyReportForm.enabled" />
|
||||
<el-switch v-model="dailyReportForm.includeSaves" active-text="转存统计" :disabled="!dailyReportForm.enabled" />
|
||||
<el-switch v-model="dailyReportForm.includeStorage" active-text="网盘容量" :disabled="!dailyReportForm.enabled" />
|
||||
<el-switch v-model="dailyReportForm.includeUsers" active-text="用户数" :disabled="!dailyReportForm.enabled" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="上次发送">
|
||||
<span>{{ dailyReportLastRun || '从未发送' }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- Preview modal -->
|
||||
<el-dialog v-model="dailyReportPreviewVisible" title="📊 每日汇报预览" width="600px">
|
||||
<div style="white-space: pre-wrap; font-family: monospace; background: var(--el-fill-color-light); padding: 16px; border-radius: 8px; max-height: 500px; overflow-y: auto;">{{ dailyReportPreview }}</div>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
|
||||
<!-- 🔄 系统维护 --> <el-card id="section-sys-maintenance" v-show="!activeSection || activeSection === 'sys-maintenance'"> <template #header> <span>🔄 系统维护</span> </template> <el-form label-width="180px" label-position="left"> <el-form-item label="自动更新镜像"> <el-switch v-model="autoUpdateEnabled" active-text="启用" inactive-text="禁用" /> <div class="form-tip">启用后 CloudSearch 将自动检测并更新到最新镜像版本</div> <div class="form-tip" style="color: var(--(--el-color-warning,#e6a23c));"> 当前需手动在服务器执行:docker-compose -f /opt/CloudSearch/docker-compose.yml pull && docker-compose -f /opt/CloudSearch/docker-compose.yml up -d </div> </el-form-item> </el-form> </el-card>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
@@ -694,6 +739,92 @@ const autoUpdateEnabled = computed({
|
||||
})
|
||||
|
||||
// ======================== Push User Notifications ========================
|
||||
|
||||
// ======================== Daily Report ========================
|
||||
const dailyReportForm = reactive({
|
||||
enabled: true,
|
||||
time: '08:00',
|
||||
includeSearch: true,
|
||||
includeSaves: true,
|
||||
includeStorage: true,
|
||||
includeUsers: true,
|
||||
})
|
||||
const dailyReportPreviewing = ref(false)
|
||||
const dailyReportSending = ref(false)
|
||||
const dailyReportPreview = ref('')
|
||||
const dailyReportPreviewVisible = ref(false)
|
||||
const dailyReportLastRun = ref('')
|
||||
|
||||
async function loadDailyReportConfig() {
|
||||
try {
|
||||
const res = await fetch('/api/admin/daily-report/config', {
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` }
|
||||
})
|
||||
if (res.ok) {
|
||||
const cfg = await res.json()
|
||||
Object.assign(dailyReportForm, cfg)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
async function loadDailyReportLastRun() {
|
||||
try {
|
||||
const res = await fetch('/api/admin/daily-report/last-run', {
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` }
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (data.date) {
|
||||
dailyReportLastRun.value = `${data.date} ${new Date(data.sentAt).toLocaleTimeString('zh-CN')}`
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
async function saveDailyReportConfig() {
|
||||
try {
|
||||
await fetch('/api/admin/daily-report/config', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${localStorage.getItem("admin_token")}`,
|
||||
},
|
||||
body: JSON.stringify({ ...dailyReportForm }),
|
||||
})
|
||||
} catch {}
|
||||
}
|
||||
async function handleDailyReportPreview() {
|
||||
dailyReportPreviewing.value = true
|
||||
try {
|
||||
const res = await fetch('/api/admin/daily-report/preview', {
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` }
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
dailyReportPreview.value = data.content
|
||||
dailyReportPreviewVisible.value = true
|
||||
}
|
||||
} finally {
|
||||
dailyReportPreviewing.value = false
|
||||
}
|
||||
}
|
||||
async function handleDailyReportSendTest() {
|
||||
dailyReportSending.value = true
|
||||
try {
|
||||
const res = await fetch('/api/admin/daily-report/test', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("admin_token")}` },
|
||||
})
|
||||
if (res.ok) {
|
||||
ElMessage.success('测试报告已发送到全局通知通道')
|
||||
} else {
|
||||
ElMessage.error('发送失败')
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('发送失败')
|
||||
} finally {
|
||||
dailyReportSending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const pushUsers = ref<any[]>([])
|
||||
const notifyProviders = ref<Record<string, any>>({})
|
||||
// const pushUserDialogVisible = ref(false) // removed - using inline form
|
||||
@@ -1005,6 +1136,8 @@ onMounted(async () => {
|
||||
initGlobalNotifyForm()
|
||||
await loadGlobalNotifyConfig()
|
||||
loadPushUsers()
|
||||
loadDailyReportConfig()
|
||||
loadDailyReportLastRun()
|
||||
loadPushUserAccountOptions()
|
||||
})
|
||||
|
||||
@@ -1320,6 +1453,7 @@ async function handleSave() {
|
||||
key: cfg.key,
|
||||
value: String(configs[cfg.key] ?? cfg.value),
|
||||
}))
|
||||
await saveDailyReportConfig()
|
||||
// Add global_notify_config as JSON entry
|
||||
entries.push({
|
||||
key: 'global_notify_config',
|
||||
|
||||
Reference in New Issue
Block a user