ai_v/templates/points.html
24024 0da71bc439 ```
feat(admin): 添加积分发放管理和邀请奖励功能

- 新增积分发放相关模型 PointsGrant 和 InviteReward
- 实现管理员积分发放接口(单个、批量、全员发放)
- 添加积分发放记录查询和统计功能
- 集成邀请奖励机制,在用户充值时自动发放邀请奖励
- 在用户注册流程中集成邀请码功能
- 扩展用户信息返回积分和创建时间字段
- 添加前端邀请码处理和邀请统计功能
```
2026-01-23 21:46:08 +08:00

574 lines
30 KiB
HTML

{% extends "base.html" %}
{% block title %}积分与邀请管理 - AI 视界{% endblock %}
{% block content %}
<div class="w-full h-full overflow-hidden flex flex-col p-6 lg:p-10 bg-slate-50/50">
<div class="max-w-7xl w-full mx-auto flex flex-col h-full space-y-6">
<!-- Header -->
<div class="flex items-center justify-between shrink-0">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 bg-indigo-600 text-white rounded-2xl flex items-center justify-center shadow-lg shadow-indigo-200">
<i data-lucide="gift" class="w-7 h-7"></i>
</div>
<div>
<h1 class="text-3xl font-black text-slate-900 tracking-tight">积分与邀请管理</h1>
<p class="text-slate-400 text-sm font-medium italic">Points & Growth Ecosystem</p>
</div>
</div>
<!-- Tabs Switcher -->
<div
class="bg-white/80 backdrop-blur-md p-1.5 rounded-2xl flex font-black text-[12px] uppercase tracking-wider shadow-sm border border-slate-100/50">
<button onclick="switchTab('grant')" id="tab-grant"
class="px-6 py-2.5 rounded-xl transition-all duration-300">发放积分</button>
<button onclick="switchTab('history')" id="tab-history"
class="px-6 py-2.5 rounded-xl transition-all duration-300">发放记录</button>
<button onclick="switchTab('invite')" id="tab-invite"
class="px-6 py-2.5 rounded-xl transition-all duration-300">邀请统计</button>
</div>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 shrink-0">
<div class="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-xl shadow-slate-200/40">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-blue-50 text-blue-600 rounded-2xl flex items-center justify-center">
<i data-lucide="users" class="w-6 h-6"></i>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">总邀请用户</p>
<h3 id="stat-total-invited" class="text-2xl font-black text-slate-900">0</h3>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-xl shadow-slate-200/40">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-amber-50 text-amber-600 rounded-2xl flex items-center justify-center">
<i data-lucide="zap" class="w-6 h-6"></i>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">累计奖励积分</p>
<h3 id="stat-total-rewards" class="text-2xl font-black text-slate-900">0</h3>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-xl shadow-slate-200/40">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-indigo-50 text-indigo-600 rounded-2xl flex items-center justify-center">
<i data-lucide="send" class="w-6 h-6"></i>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">手动发放总计</p>
<h3 id="stat-total-grants" class="text-2xl font-black text-slate-900">0</h3>
</div>
</div>
</div>
<div class="bg-indigo-600 p-6 rounded-[2rem] shadow-xl shadow-indigo-200/50 text-white">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center">
<i data-lucide="crown" class="w-6 h-6"></i>
</div>
<div>
<p class="text-[10px] font-black text-indigo-200 uppercase tracking-widest">邀请达人榜首</p>
<h3 id="stat-top-inviter" class="text-lg font-black truncate w-32">加载中...</h3>
</div>
</div>
</div>
</div>
<!-- Main Content (Grant Tab) -->
<div id="view-grant"
class="flex-1 flex flex-col min-h-0 space-y-4 animate-in fade-in slide-in-from-bottom-4 duration-500">
<!-- Action Bar -->
<div
class="flex items-center justify-between gap-4 bg-white/50 backdrop-blur-md p-4 rounded-3xl border border-white/50 shadow-sm">
<div class="relative w-96">
<i data-lucide="search" class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"></i>
<input type="text" id="userSearchInput" onkeyup="if(event.key === 'Enter') loadUserList()"
placeholder="搜索手机号或用户ID..."
class="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-2xl text-sm font-bold outline-none focus:border-indigo-500 transition-all">
</div>
<div class="flex items-center gap-3">
<button onclick="openGrantModal('selected')"
class="px-5 py-2.5 bg-indigo-600 text-white rounded-2xl text-xs font-black hover:bg-indigo-700 transition-all shadow-lg shadow-indigo-100 flex items-center gap-2">
<i data-lucide="check-square" class="w-4 h-4"></i> 发放选中用户
</button>
<button onclick="openGrantModal('all')"
class="px-5 py-2.5 bg-slate-900 text-white rounded-2xl text-xs font-black hover:bg-slate-800 transition-all shadow-lg shadow-slate-200 flex items-center gap-2">
<i data-lucide="globe" class="w-4 h-4"></i> 全员发放 (Global)
</button>
</div>
</div>
<!-- User Data Table -->
<div
class="bg-white rounded-[2.5rem] shadow-2xl shadow-slate-200/50 border border-slate-100 flex-1 flex flex-col overflow-hidden">
<div class="flex-1 overflow-auto custom-scrollbar">
<table class="w-full text-left border-collapse">
<thead class="sticky top-0 bg-white z-10">
<tr>
<th class="px-8 py-5 border-b border-slate-50">
<input type="checkbox" id="selectAllUsers" onchange="toggleSelectAll(this)"
class="w-4 h-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500 transition-all">
</th>
<th
class="px-4 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
用户 ID</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
手机号</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
当前积分</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
用户角色</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50 text-right">
注册时间</th>
</tr>
</thead>
<tbody id="userTableBody" class="divide-y divide-slate-50">
<!-- Dynamic Content -->
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="p-6 border-t border-slate-50 flex items-center justify-between bg-slate-50/30">
<span class="text-xs font-bold text-slate-400" id="userPageInfo">加载中...</span>
<div class="flex items-center gap-2">
<button onclick="changeUserPage(-1)"
class="p-2.5 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 disabled:opacity-50 transition-all shadow-sm">
<i data-lucide="chevron-left" class="w-4 h-4 text-slate-600"></i>
</button>
<button onclick="changeUserPage(1)"
class="p-2.5 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 disabled:opacity-50 transition-all shadow-sm">
<i data-lucide="chevron-right" class="w-4 h-4 text-slate-600"></i>
</button>
</div>
</div>
</div>
</div>
<!-- History Tab (Hidden by Default) -->
<div id="view-history"
class="hidden flex-1 flex flex-col min-h-0 animate-in fade-in slide-in-from-bottom-4 duration-500">
<!-- Same History Table Structure as before, but full width -->
<div
class="bg-white rounded-[2.5rem] shadow-2xl shadow-slate-200/50 border border-slate-100 flex-1 flex flex-col overflow-hidden">
<div class="p-6 border-b border-slate-50 bg-slate-50/30 flex justify-between items-center">
<div class="relative w-96">
<i data-lucide="search"
class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"></i>
<input type="text" id="historySearch" onkeyup="if(event.key === 'Enter') loadGrantHistory()"
placeholder="搜索手机号或操作备注..."
class="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold outline-none focus:border-indigo-500 transition-all">
</div>
</div>
<div class="flex-1 overflow-auto custom-scrollbar">
<table class="w-full text-left border-collapse">
<thead class="sticky top-0 bg-white z-10">
<tr>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
接收用户</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
积分数</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
发放原因</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
操作人</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50 text-right">
时间</th>
</tr>
</thead>
<tbody id="grantHistoryTable" class="divide-y divide-slate-50">
<!-- Dynamic Content -->
</tbody>
</table>
</div>
<div class="p-6 border-t border-slate-50 flex items-center justify-between bg-slate-50/30">
<span class="text-xs font-bold text-slate-400" id="grantPageInfo">共 0 条记录</span>
<div class="flex items-center gap-2">
<button onclick="changeGrantPage(-1)"
class="p-2.5 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 disabled:opacity-50 transition-all shadow-sm"><i
data-lucide="chevron-left" class="w-4 h-4"></i></button>
<button onclick="changeGrantPage(1)"
class="p-2.5 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 disabled:opacity-50 transition-all shadow-sm"><i
data-lucide="chevron-right" class="w-4 h-4"></i></button>
</div>
</div>
</div>
</div>
<!-- Invite Tab (Hidden by Default) -->
<div id="view-invite"
class="hidden flex-1 flex flex-col min-h-0 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div
class="bg-white rounded-[2.5rem] shadow-2xl shadow-slate-200/50 border border-slate-100 flex-1 flex flex-col overflow-hidden">
<div class="p-6 border-b border-slate-50 bg-slate-50/30 flex justify-between items-center">
<h3 class="font-black text-slate-800 flex items-center gap-2">
<i data-lucide="list-tree" class="w-5 h-5 text-indigo-500"></i>
邀请奖励明细
</h3>
<div class="relative w-72">
<i data-lucide="search"
class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"></i>
<input type="text" id="inviteSearch" onkeyup="if(event.key === 'Enter') loadInviteRewards()"
placeholder="搜索邀请双方..."
class="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold outline-none focus:border-indigo-500 transition-all">
</div>
</div>
<div class="flex-1 overflow-auto custom-scrollbar">
<table class="w-full text-left border-collapse">
<thead class="sticky top-0 bg-white z-10">
<tr>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
邀请人</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
被邀请人</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
奖励积分 (10%)</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
充值轮次</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50 text-right">
结算时间</th>
</tr>
</thead>
<tbody id="inviteRewardsTable" class="divide-y divide-slate-50">
<!-- Dynamic Content -->
</tbody>
</table>
</div>
<div class="p-6 border-t border-slate-50 flex items-center justify-between bg-slate-50/30">
<span class="text-xs font-bold text-slate-400" id="invitePageInfo">共 0 条数据</span>
<div class="flex items-center gap-2">
<button onclick="changeInvitePage(-1)"
class="p-2.5 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 disabled:opacity-50 transition-all shadow-sm"><i
data-lucide="chevron-left" class="w-4 h-4"></i></button>
<button onclick="changeInvitePage(1)"
class="p-2.5 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 disabled:opacity-50 transition-all shadow-sm"><i
data-lucide="chevron-right" class="w-4 h-4"></i></button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Grant Modal -->
<div id="grantModal"
class="hidden fixed inset-0 z-50 flex items-center justify-center p-6 backdrop-blur-md bg-slate-900/20 animate-in fade-in duration-300">
<div
class="bg-white w-full max-w-md rounded-[3rem] shadow-2xl border border-slate-100 p-10 space-y-8 animate-in zoom-in duration-300">
<div class="text-center space-y-2">
<div
class="w-20 h-20 bg-indigo-50 text-indigo-600 rounded-[2.5rem] flex items-center justify-center mx-auto mb-4">
<i data-lucide="plus-circle" id="modalIcon" class="w-10 h-10"></i>
</div>
<h2 id="modalTitle" class="text-2xl font-black text-slate-900 uppercase">确定发放积分</h2>
<p id="modalSub" class="text-slate-400 text-sm font-medium">请确认积分数量与发放理由</p>
</div>
<div class="space-y-6">
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">发放额度
(Points)</label>
<div class="relative">
<i data-lucide="zap" class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-amber-500"></i>
<input type="number" id="modalPoints" placeholder="请输入正整数"
class="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-lg font-black outline-none focus:border-indigo-500 transition-all">
</div>
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">发放原因 / 备注</label>
<textarea id="modalReason" rows="3" placeholder="例如:运营活动奖励..."
class="w-full p-4 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-medium outline-none focus:border-indigo-500 transition-all resize-none"></textarea>
</div>
</div>
<div class="flex items-center gap-4">
<button onclick="closeGrantModal()"
class="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-black text-sm hover:bg-slate-200 transition-all">取消</button>
<button id="btnModalConfirm" onclick="confirmGrant()"
class="flex-1 py-4 bg-indigo-600 text-white rounded-2xl font-black text-sm hover:bg-indigo-700 transition-all shadow-xl shadow-indigo-100">确认发放</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// --- State Management ---
let currentTab = 'grant';
let userPage = 1;
let grantPage = 1;
let invitePage = 1;
let currentGrantTarget = 'selected'; // 'selected' or 'all'
// --- Tab Logic ---
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('[id^="tab-"]').forEach(btn => {
if (btn.id === `tab-${tab}`) {
btn.classList.add('bg-white', 'text-slate-900', 'shadow-sm', 'scale-105');
btn.classList.remove('text-slate-400');
} else {
btn.classList.remove('bg-white', 'text-slate-900', 'shadow-sm', 'scale-105');
btn.classList.add('text-slate-400');
}
});
document.getElementById('view-grant').classList.toggle('hidden', tab !== 'grant');
document.getElementById('view-history').classList.toggle('hidden', tab !== 'history');
document.getElementById('view-invite').classList.toggle('hidden', tab !== 'invite');
if (tab === 'grant') loadUserList();
if (tab === 'history') loadGrantHistory();
if (tab === 'invite') loadInviteRewards();
}
// --- Overall Stats ---
async function loadStats() {
try {
const r = await fetch('/api/admin/invite/stats');
const d = await r.json();
document.getElementById('stat-total-invited').innerText = d.total_invited;
document.getElementById('stat-total-rewards').innerText = d.total_rewards;
document.getElementById('stat-total-grants').innerText = d.total_grants;
if (d.top_inviters && d.top_inviters.length > 0) {
document.getElementById('stat-top-inviter').innerText = d.top_inviters[0].phone;
} else {
document.getElementById('stat-top-inviter').innerText = '尚无排名';
}
} catch (e) {
console.error('加载统计失败', e);
}
}
// --- User Management (Grant Tab) ---
async function loadUserList() {
const q = document.getElementById('userSearchInput').value;
const r = await fetch(`/api/admin/users?page=${userPage}&q=${q}&per_page=15`);
const d = await r.json();
const tbody = document.getElementById('userTableBody');
document.getElementById('userPageInfo').innerText = `${d.current_page} / ${d.pages} 页 (共 ${d.total} 人)`;
tbody.innerHTML = d.users.map(u => `
<tr class="hover:bg-slate-50/80 transition-all duration-300">
<td class="px-8 py-5">
<input type="checkbox" name="userSelect" value="${u.id}" class="user-checkbox w-4 h-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500 transition-all">
</td>
<td class="px-4 py-5 font-mono text-xs text-slate-400">#${u.id}</td>
<td class="px-8 py-5 font-black text-slate-800">${u.phone}</td>
<td class="px-8 py-5">
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-black bg-indigo-50 text-indigo-600">
<i data-lucide="zap" class="w-3 h-3 text-amber-500"></i>
${u.points}
</span>
</td>
<td class="px-8 py-5">
<span class="text-xs font-bold text-slate-500">${u.role || '普通用户'}</span>
</td>
<td class="px-8 py-5 text-right text-[10px] font-black text-slate-300 uppercase italic">
${u.created_at}
</td>
</tr>
`).join('');
lucide.createIcons();
document.getElementById('selectAllUsers').checked = false;
}
function toggleSelectAll(master) {
const checkboxes = document.getElementsByName('userSelect');
for (let cb of checkboxes) {
cb.checked = master.checked;
}
}
function changeUserPage(delta) {
userPage += delta;
if (userPage < 1) userPage = 1;
loadUserList();
}
// --- Modal Logic ---
function openGrantModal(target) {
currentGrantTarget = target;
const modal = document.getElementById('grantModal');
const title = document.getElementById('modalTitle');
const sub = document.getElementById('modalSub');
if (target === 'all') {
title.innerText = "全员普惠发放";
sub.innerText = "注意:此动作将影响系统内所有注册用户";
} else {
const selectedCount = Array.from(document.getElementsByName('userSelect')).filter(c => c.checked).length;
if (selectedCount === 0) return showToast('请先在列表中勾选目标用户', 'warning');
title.innerText = `批量发放 (${selectedCount} 人)`;
sub.innerText = "仅对选中的用户进行积分补偿或奖励";
}
modal.classList.remove('hidden');
}
function closeGrantModal() {
document.getElementById('grantModal').classList.add('hidden');
document.getElementById('modalPoints').value = '';
document.getElementById('modalReason').value = '';
}
async function confirmGrant() {
const points = document.getElementById('modalPoints').value;
const reason = document.getElementById('modalReason').value;
if (!points || points <= 0) return showToast('请输入有效的积分数', 'warning');
let url = '';
let body = { points, reason };
if (currentGrantTarget === 'all') {
url = '/api/admin/points/global_grant';
} else {
url = '/api/admin/points/batch_grant_ids';
const selectedIds = Array.from(document.getElementsByName('userSelect'))
.filter(c => c.checked)
.map(c => parseInt(c.value));
body.user_ids = selectedIds;
}
const btn = document.getElementById('btnModalConfirm');
btn.disabled = true;
btn.innerText = '执行中...';
try {
const r = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const d = await r.json();
if (d.message) {
showToast(`${d.message}`, 'success');
closeGrantModal();
loadUserList();
loadStats();
} else {
showToast(d.error, 'error');
}
} catch (e) {
showToast('请求异常,发送失败', 'error');
} finally {
btn.disabled = false;
btn.innerText = '确认发放';
}
}
// --- Grant History ---
async function loadGrantHistory() {
const q = document.getElementById('historySearch').value;
const r = await fetch(`/api/admin/points/grants?page=${grantPage}&q=${q}`);
const d = await r.json();
const tbody = document.getElementById('grantHistoryTable');
document.getElementById('grantPageInfo').innerText = `${d.current_page} / ${d.pages} 页 (共 ${d.total} 条记录)`;
tbody.innerHTML = d.grants.map(g => `
<tr class="hover:bg-slate-50 transition-colors border-b border-slate-50 last:border-0 text-sm">
<td class="px-8 py-5">
<div class="flex flex-col">
<span class="font-black text-slate-800 text-sm">${g.user_phone}</span>
<span class="text-[9px] text-slate-400 font-mono italic">UID: ${g.user_id}</span>
</div>
</td>
<td class="px-8 py-5">
<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-black bg-emerald-50 text-emerald-600">
+${g.points}
</span>
</td>
<td class="px-8 py-5 font-bold text-slate-500">${g.reason || '-'}</td>
<td class="px-8 py-5">
<span class="text-xs font-black text-slate-700 underline decoration-indigo-200">${g.admin_phone}</span>
</td>
<td class="px-8 py-5 text-right text-[10px] font-black text-slate-300 uppercase">
${g.created_at}
</td>
</tr>
`).join('');
lucide.createIcons();
}
function changeGrantPage(delta) {
grantPage += delta;
if (grantPage < 1) grantPage = 1;
loadGrantHistory();
}
// --- Invite Rewards ---
async function loadInviteRewards() {
const q = document.getElementById('inviteSearch').value;
const r = await fetch(`/api/admin/invite/rewards?page=${invitePage}&q=${q}`);
const d = await r.json();
const tbody = document.getElementById('inviteRewardsTable');
document.getElementById('invitePageInfo').innerText = `${d.current_page} / ${d.pages} 页 (共 ${d.total} 条数据)`;
tbody.innerHTML = d.rewards.map(r => `
<tr class="hover:bg-slate-50 transition-colors border-b border-slate-50 last:border-0 text-sm">
<td class="px-8 py-5">
<div class="flex items-center gap-2">
<div class="w-8 h-8 bg-indigo-100 text-indigo-600 rounded-lg flex items-center justify-center font-black text-[9px]">师</div>
<span class="font-black text-slate-800">${r.inviter_phone}</span>
</div>
</td>
<td class="px-8 py-5">
<div class="flex flex-col">
<span class="font-bold text-slate-700">${r.invitee_phone}</span>
<span class="text-[9px] text-slate-400">UID: ${r.invitee_id}</span>
</div>
</td>
<td class="px-8 py-5">
<span class="text-indigo-600 font-black">+${r.reward_points} 积分</span>
</td>
<td class="px-8 py-5">
<span class="px-2 py-0.5 bg-slate-100 text-slate-500 text-[10px] font-black rounded uppercase">充值 #${r.recharge_count}</span>
</td>
<td class="px-8 py-5 text-right text-[10px] font-black text-slate-300 uppercase">
${r.created_at}
</td>
</tr>
`).join('');
lucide.createIcons();
}
function changeInvitePage(delta) {
invitePage += delta;
if (invitePage < 1) invitePage = 1;
loadInviteRewards();
}
// --- Init ---
document.addEventListener('DOMContentLoaded', () => {
switchTab('grant');
loadStats();
});
</script>
{% endblock %}