104 lines
5.5 KiB
Vue
104 lines
5.5 KiB
Vue
<template>
|
|
<div class="page">
|
|
<div class="cards">
|
|
<div class="card"><b>{{ stats.subscribed ?? '-' }}</b><span>可通知用户</span><i>已订阅授权 {{ stats.totalUsers ?? 0 }} 用户中</i></div>
|
|
<div class="card"><b>{{ stats.sentToday ?? '-' }}</b><span>今日已发送</span><i>累计 {{ stats.totalSent ?? 0 }} 条</i></div>
|
|
<div class="card"><b>{{ stats.templateConfigured ? '已配置' : '未配置' }}</b><span>消息模板</span><i>{{ stats.templateId ? stats.templateId.slice(0, 14) + '…' : '设置 WX_NOTIFY_TEMPLATE_ID' }}</i></div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h3>发送通知</h3>
|
|
<el-form label-width="90px" style="max-width: 560px">
|
|
<el-form-item label="场景">
|
|
<el-radio-group v-model="scene">
|
|
<el-radio value="release">版本发布</el-radio>
|
|
<el-radio value="manual">运营通知</el-radio>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
<el-form-item label="标题"><el-input v-model="title" placeholder="通知标题(20字内)" maxlength="20" show-word-limit /></el-form-item>
|
|
<el-form-item label="内容"><el-input v-model="thing1" type="textarea" :rows="2" placeholder="主要内容(20字内,模板字段限制)" maxlength="20" show-word-limit /></el-form-item>
|
|
<el-form-item label="补充"><el-input v-model="thing2" placeholder="补充说明(20字内,选填)" maxlength="20" show-word-limit /></el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" :loading="sending" :disabled="!stats.subscribed" @click="send">发送给 {{ stats.subscribed || 0 }} 位订阅用户</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
<el-alert v-if="!stats.templateConfigured" type="warning" :closable="false"
|
|
title="未配置模板:微信公众平台→功能→订阅消息→选「新版上线提醒」类模板→复制模板ID→服务器 compose 加 WX_NOTIFY_TEMPLATE_ID 环境变量" />
|
|
<el-alert v-else type="info" :closable="false"
|
|
title="订阅消息为一次性授权:每次用户登录小程序会弹一次授权,授权一次可发一条。发完需等用户下次登录再授权。" />
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h3>发送记录</h3>
|
|
<el-table :data="logs" size="small" stripe v-loading="loadingLogs">
|
|
<el-table-column label="时间" width="160"><template #default="{ row }">{{ fmt(row.createdAt) }}</template></el-table-column>
|
|
<el-table-column label="用户" width="120"><template #default="{ row }">{{ row.userId?.nickName || row.openid?.slice(0, 8) || '—' }}</template></el-table-column>
|
|
<el-table-column prop="title" label="标题" min-width="140" show-overflow-tooltip />
|
|
<el-table-column label="场景" width="90">
|
|
<template #default="{ row }">{{ row.scene === 'release' ? '版本发布' : '运营通知' }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="结果" width="80">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.status === 'sent' ? 'success' : 'danger'" size="small">{{ row.status === 'sent' ? '成功' : '失败' }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="errMsg" label="失败原因" min-width="140" show-overflow-tooltip />
|
|
</el-table>
|
|
<el-pagination background layout="total, prev, pager, next" :total="logsTotal" :page-size="20" v-model:current-page="logsPage" @current-change="loadLogs" style="margin-top: 12px" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { apiGet, apiPost } from '../api'
|
|
|
|
const stats = ref({})
|
|
const scene = ref('release')
|
|
const title = ref('')
|
|
const thing1 = ref('')
|
|
const thing2 = ref('')
|
|
const sending = ref(false)
|
|
const logs = ref([])
|
|
const logsTotal = ref(0)
|
|
const logsPage = ref(1)
|
|
const loadingLogs = ref(false)
|
|
|
|
function fmt(t) { return t ? new Date(t).toLocaleString('zh-CN', { hour12: false }) : '—' }
|
|
|
|
async function loadStats() {
|
|
try { stats.value = await apiGet('/api/admin/notify/stats') || {} } catch (e) { stats.value = {} }
|
|
}
|
|
async function loadLogs() {
|
|
loadingLogs.value = true
|
|
try {
|
|
const r = await apiGet(`/api/admin/notify/logs?page=${logsPage.value}&limit=20`)
|
|
logs.value = r.items || []
|
|
logsTotal.value = r.total || 0
|
|
} finally { loadingLogs.value = false }
|
|
}
|
|
async function send() {
|
|
if (!title.value.trim() || !thing1.value.trim()) return ElMessage.warning('请填写标题和内容')
|
|
sending.value = true
|
|
try {
|
|
const r = await apiPost('/api/admin/notify/send', { title: title.value, thing1: thing1.value, thing2: thing2.value, scene: scene.value })
|
|
ElMessage.success(r.message || '发送完成')
|
|
loadStats(); loadLogs()
|
|
title.value = ''; thing1.value = ''; thing2.value = ''
|
|
} catch (e) { ElMessage.error(e.message || '发送失败') } finally { sending.value = false }
|
|
}
|
|
onMounted(() => { loadStats(); loadLogs() })
|
|
</script>
|
|
|
|
<style scoped>
|
|
.page { display: flex; flex-direction: column; gap: 14px; }
|
|
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; }
|
|
.card { background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 1px 4px rgba(0,0,0,.06); }
|
|
.card b { font-size: 22px; color: #1f2333; }
|
|
.card span { font-size: 13px; color: #6b7080; display: block; }
|
|
.card i { font-size: 11px; color: #a6abbb; font-style: normal; }
|
|
.panel { background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 1px 4px rgba(0,0,0,.06); }
|
|
.panel h3 { margin: 0 0 14px; font-size: 14px; }
|
|
</style>
|