574 lines
30 KiB
HTML
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 %}
|