feat: 管理后台用户详情页增加订阅状态和测试消息发送

- 后端新增 /api/admin/users/:id 返回用户订阅列表
- 后端新增 POST /api/admin/users/:id/test-message 发送测试订阅消息
- 前端用户详情页展示订阅状态(只读,按类型分组)
- 前端增加发送版本更新测试消息功能
This commit is contained in:
Developer
2026-05-20 20:01:38 +08:00
parent 344b25a2d8
commit 1ca73922d8
2 changed files with 117 additions and 1 deletions
+78
View File
@@ -67,6 +67,9 @@ tr:hover td { background: #f8f9fa; }
.badge-vip { background: #e8f0fe; color: #1967d2; }
.btn-detail { padding: 4px 12px; background: var(--primary); color: #fff; border: none; border-radius: 4px; font-size: 12px; cursor: pointer; }
.btn-detail:hover { background: var(--primary-hover); }
.btn-send-msg { padding: 8px 16px; background: var(--primary); color: #fff; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; white-space: nowrap; }
.btn-send-msg:hover { background: var(--primary-hover); }
.btn-send-msg:disabled { opacity: .6; cursor: not-allowed; }
/* Pagination */
.pagination { padding: 16px 20px; display: flex; justify-content: center; align-items: center; gap: 12px; }
@@ -355,6 +358,7 @@ async function openModal(id) {
try {
const d = await api('/api/admin/users/' + id);
S.editingUser = d.data;
S.subscriptions = d.subscriptions || [];
renderModal(d.data);
} catch (e) {
showToast('加载用户详情失败', false);
@@ -404,7 +408,81 @@ function renderModal(u) {
<div class="field-row">
<div class="field"><label>平台限额</label><input type="number" id="edit-platformLimit" value="${u.platformLimit || 0}" min="0"></div>
<div class="field"><label>当前平台数</label><input type="number" id="edit-platformCount" value="${u.platformCount || 0}" min="0"></div>
</div>
<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">
<div class="field"><label>订阅通知状态(只读)</label><div id="subscribeStatus" class="ro">加载中...</div></div>
<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">
<div class="field"><label>发送测试消息</label>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input id="test-tplId" value="DaPPZVFsLsHzj6TJAFK5rUp8B4DW-9JOtJF00v-uLMQ" style="flex:1;min-width:180px" placeholder="模板ID">
<input id="test-str1" value="v2.0" style="width:100px" placeholder="character_string1">
<input id="test-t3" value="版本已更新" style="width:100px" placeholder="thing3">
<input id="test-t6" value="请查看更新内容" style="width:120px" placeholder="thing6">
<button class="btn-send-msg" onclick="sendTestMessage()">发送测试</button>
</div>
<div id="testResult" style="margin-top:8px"></div>
</div>`;
if (S.subscriptions) {
renderSubscriptions(S.subscriptions);
}
}
function renderSubscriptions(subs) {
const el = document.getElementById('subscribeStatus');
if (!subs || subs.length === 0) {
el.innerHTML = '<span style="color:var(--text-secondary)">该用户未订阅任何通知</span>';
return;
}
const typeMap = {};
subs.forEach(s => {
if (!typeMap[s.type]) typeMap[s.type] = [];
typeMap[s.type].push(s);
});
let html = '<div style="font-size:13px">';
for (const [type, items] of Object.entries(typeMap)) {
const latest = items[0];
const badgeClass = latest.status === 'active' ? 'badge-active' : (latest.status === 'used' ? 'badge-inactive' : 'badge-banned');
const label = latest.status === 'active' ? '已授权' : (latest.status === 'used' ? '已使用' : latest.status);
const typeName = type === 'version-update' ? '版本更新通知' : (type === 'expiry-reminder' ? '到期提醒通知' : type);
html += `<div style="margin-bottom:8px">
<span class="badge ${badgeClass}">${label}</span>
<span style="margin-left:8px">${typeName}</span>
<span style="margin-left:8px;color:var(--text-secondary)">模板: ${latest.templateId.substring(0,8)}...</span>
<span style="margin-left:8px;color:var(--text-secondary)">订阅: ${fmtDate(latest.subscribedAt)}</span>
</div>`;
}
html += '</div>';
el.innerHTML = html;
}
async function sendTestMessage() {
if (!S.editingUser) return;
const btn = document.querySelector('.btn-send-msg');
const resultEl = document.getElementById('testResult');
btn.disabled = true;
btn.textContent = '发送中...';
resultEl.innerHTML = '';
try {
const res = await api('/api/admin/users/' + S.editingUser._id + '/test-message', {
method: 'POST',
body: JSON.stringify({
templateId: document.getElementById('test-tplId').value.trim(),
character_string1: document.getElementById('test-str1').value.trim(),
thing3: document.getElementById('test-t3').value.trim(),
thing6: document.getElementById('test-t6').value.trim()
})
});
if (res.success) {
resultEl.innerHTML = `<span style="color:#137333">✅ 发送成功 (msgid: ${res.data.messageId})</span>`;
} else {
resultEl.innerHTML = `<span style="color:var(--danger)">❌ 发送失败: errcode=${res.data.errcode} ${res.data.errmsg || ''}</span>`;
}
} catch (e) {
resultEl.innerHTML = `<span style="color:var(--danger)">❌ 异常: ${e.message}</span>`;
} finally {
btn.disabled = false;
btn.textContent = '发送测试';
}
}
function toggleVipExpire() {