799 lines
43 KiB
HTML
799 lines
43 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>宇之然 AI - 管理后台</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>tailwind.config={theme:{extend:{colors:{brand:{50:'#f0f7ff',100:'#e0effe',200:'#bae0fd',300:'#7cc8fb',400:'#36aaf5',500:'#0c8ee7',600:'#0070c4',700:'#015a9f',800:'#064c83',900:'#0b406d'}}}}}}</script>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen">
|
|
|
|
<div id="loginPage" class="min-h-screen flex items-center justify-center">
|
|
<div class="bg-white p-8 rounded-2xl shadow-sm border w-full max-w-sm">
|
|
<div class="text-center mb-8">
|
|
<h1 class="text-2xl font-bold text-brand-600">宇之然 AI</h1>
|
|
<p class="text-gray-500 text-sm mt-1">管理后台</p>
|
|
</div>
|
|
<div class="space-y-4">
|
|
<input id="username" type="text" placeholder="管理员账号" class="w-full px-4 py-2.5 border rounded-lg text-sm focus:outline-none focus:border-brand-400" autocomplete="username">
|
|
<input id="password" type="password" placeholder="密码" class="w-full px-4 py-2.5 border rounded-lg text-sm focus:outline-none focus:border-brand-400" autocomplete="current-password">
|
|
<button onclick="login()" class="w-full py-2.5 bg-brand-600 text-white text-sm font-medium rounded-lg hover:bg-brand-700">登录</button>
|
|
<p id="loginError" class="text-red-500 text-sm text-center hidden"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="adminApp" class="hidden">
|
|
<nav class="bg-white border-b px-6 py-3 flex items-center justify-between sticky top-0 z-50">
|
|
<div class="flex items-center gap-3">
|
|
<h1 class="text-lg font-bold text-brand-600">宇之然 AI 管理后台</h1>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<span id="adminName" class="text-sm text-gray-500"></span>
|
|
<button onclick="logout()" class="text-sm text-gray-400 hover:text-red-500">退出</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="flex">
|
|
<aside class="w-56 bg-white border-r min-h-[calc(100vh-4rem)] p-4 flex-shrink-0 overflow-y-auto">
|
|
<nav class="space-y-1">
|
|
<a href="#" onclick="showPage('dashboard')" class="nav-item block px-3 py-2 rounded-lg text-sm bg-brand-50 text-brand-600 font-medium" data-page="dashboard">📊 数据看板</a>
|
|
<a href="#" onclick="showPage('categories')" class="nav-item block px-3 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-50" data-page="categories">📂 分类管理</a>
|
|
<a href="#" onclick="showPage('users')" class="nav-item block px-3 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-50" data-page="users">👥 用户管理</a>
|
|
<a href="#" onclick="showPage('courses')" class="nav-item block px-3 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-50" data-page="courses">📚 课程管理</a>
|
|
<a href="#" onclick="showPage('contents')" class="nav-item block px-3 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-50" data-page="contents">📝 内容管理</a>
|
|
<a href="#" onclick="showPage('prompts')" class="nav-item block px-3 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-50" data-page="prompts">💡 提示词管理</a>
|
|
<a href="#" onclick="showPage('tools')" class="nav-item block px-3 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-50" data-page="tools">🔧 工具管理</a>
|
|
<a href="#" onclick="showPage('orders')" class="nav-item block px-3 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-50" data-page="orders">💰 订单管理</a>
|
|
</nav>
|
|
</aside>
|
|
|
|
<main class="flex-1 p-6 overflow-auto">
|
|
<div id="toast" class="fixed top-4 right-4 z-50 hidden"></div>
|
|
|
|
<div id="page-dashboard" class="page-content">
|
|
<h2 class="text-xl font-bold mb-6">数据看板</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8" id="statsGrid">
|
|
<div class="bg-white p-5 rounded-xl border"><p class="text-sm text-gray-400">用户总数</p><p class="text-2xl font-bold" id="stat-userCount">-</p></div>
|
|
<div class="bg-white p-5 rounded-xl border"><p class="text-sm text-gray-400">课程总数</p><p class="text-2xl font-bold" id="stat-courseCount">-</p></div>
|
|
<div class="bg-white p-5 rounded-xl border"><p class="text-sm text-gray-400">内容总数</p><p class="text-2xl font-bold" id="stat-contentCount">-</p></div>
|
|
<div class="bg-white p-5 rounded-xl border"><p class="text-sm text-gray-400">提示词数</p><p class="text-2xl font-bold" id="stat-promptCount">-</p></div>
|
|
<div class="bg-white p-5 rounded-xl border"><p class="text-sm text-gray-400">订单数</p><p class="text-2xl font-bold" id="stat-orderCount">-</p></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-categories" class="page-content hidden">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-bold">分类管理</h2>
|
|
<button onclick="showCategoryForm()" class="px-4 py-2 bg-brand-600 text-white text-sm rounded-lg hover:bg-brand-700">+ 新建分类</button>
|
|
</div>
|
|
<div id="categoryForm" class="hidden mb-6 bg-white p-6 rounded-xl border max-w-lg">
|
|
<h3 class="text-lg font-semibold mb-4" id="categoryFormTitle">新建分类</h3>
|
|
<div class="space-y-3">
|
|
<input id="caf-name" placeholder="分类名称" class="w-full px-3 py-2 border rounded-lg text-sm">
|
|
<input id="caf-slug" placeholder="标识 (slug)" class="w-full px-3 py-2 border rounded-lg text-sm">
|
|
<input id="caf-desc" placeholder="描述" class="w-full px-3 py-2 border rounded-lg text-sm">
|
|
<input id="caf-sort" type="number" placeholder="排序 (数字越小越靠前)" class="w-full px-3 py-2 border rounded-lg text-sm">
|
|
<div class="flex gap-2">
|
|
<button onclick="saveCategory()" class="px-4 py-2 bg-brand-600 text-white text-sm rounded-lg">保存</button>
|
|
<button onclick="cancelCategoryForm()" class="px-4 py-2 border text-sm rounded-lg">取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-xl border overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50 text-gray-500">
|
|
<tr><th class="text-left px-4 py-3 font-medium">ID</th><th class="text-left px-4 py-3 font-medium">名称</th><th class="text-left px-4 py-3 font-medium">标识</th><th class="text-left px-4 py-3 font-medium">排序</th><th class="text-left px-4 py-3 font-medium">关联内容</th><th class="text-left px-4 py-3 font-medium">操作</th></tr>
|
|
</thead>
|
|
<tbody id="categoriesTable" class="divide-y"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-users" class="page-content hidden">
|
|
<h2 class="text-xl font-bold mb-6">用户管理</h2>
|
|
<div class="bg-white rounded-xl border overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50 text-gray-500">
|
|
<tr><th class="text-left px-4 py-3 font-medium">ID</th><th class="text-left px-4 py-3 font-medium">昵称</th><th class="text-left px-4 py-3 font-medium">手机号</th><th class="text-left px-4 py-3 font-medium">邮箱</th><th class="text-left px-4 py-3 font-medium">会员</th><th class="text-left px-4 py-3 font-medium">注册时间</th></tr>
|
|
</thead>
|
|
<tbody id="usersTable" class="divide-y"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-courses" class="page-content hidden">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-bold">课程管理</h2>
|
|
<button onclick="showCourseForm()" class="px-4 py-2 bg-brand-600 text-white text-sm rounded-lg hover:bg-brand-700">+ 新建课程</button>
|
|
</div>
|
|
<div class="bg-white rounded-xl border overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50 text-gray-500">
|
|
<tr><th class="text-left px-4 py-3 font-medium">ID</th><th class="text-left px-4 py-3 font-medium">标题</th><th class="text-left px-4 py-3 font-medium">分类</th><th class="text-left px-4 py-3 font-medium">价格</th><th class="text-left px-4 py-3 font-medium">课时</th><th class="text-left px-4 py-3 font-medium">状态</th><th class="text-left px-4 py-3 font-medium">操作</th></tr>
|
|
</thead>
|
|
<tbody id="coursesTable" class="divide-y"></tbody>
|
|
</table>
|
|
</div>
|
|
<div id="courseForm" class="hidden mt-6 bg-white p-6 rounded-xl border max-w-4xl">
|
|
<h3 class="text-lg font-semibold mb-4" id="courseFormTitle">新建课程</h3>
|
|
<div class="space-y-3">
|
|
<input id="cf-title" placeholder="课程标题" class="w-full px-3 py-2 border rounded-lg text-sm">
|
|
<textarea id="cf-desc" placeholder="课程描述" class="w-full px-3 py-2 border rounded-lg text-sm" rows="2"></textarea>
|
|
<div class="flex gap-3">
|
|
<select id="cf-category" class="flex-1 px-3 py-2 border rounded-lg text-sm"></select>
|
|
<input id="cf-price" type="number" placeholder="价格(元)" class="w-32 px-3 py-2 border rounded-lg text-sm">
|
|
</div>
|
|
<div class="flex gap-3 items-center">
|
|
<label class="flex items-center gap-2 text-sm"><input type="checkbox" id="cf-isFree" checked> 免费</label>
|
|
<select id="cf-status" class="px-3 py-2 border rounded-lg text-sm">
|
|
<option value="DRAFT">草稿</option><option value="PUBLISHED">已发布</option>
|
|
</select>
|
|
<input id="cf-sort" type="number" placeholder="排序" class="w-20 px-3 py-2 border rounded-lg text-sm">
|
|
</div>
|
|
</div>
|
|
|
|
<div id="chaptersSection" class="mt-6 hidden">
|
|
<h4 class="text-md font-semibold text-gray-900 mb-3 flex items-center justify-between">
|
|
<span>章节管理</span>
|
|
<button onclick="addChapter()" class="text-xs px-3 py-1 bg-gray-100 rounded-lg hover:bg-gray-200">+ 添加章节</button>
|
|
</h4>
|
|
<div id="chaptersList" class="space-y-3"></div>
|
|
</div>
|
|
|
|
<div class="flex gap-2 mt-4">
|
|
<button onclick="saveCourse()" class="px-6 py-2 bg-brand-600 text-white text-sm rounded-lg hover:bg-brand-700">保存课程</button>
|
|
<button onclick="cancelCourseForm()" class="px-4 py-2 border text-sm rounded-lg">取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-contents" class="page-content hidden">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-bold">内容管理</h2>
|
|
<button onclick="showContentForm()" class="px-4 py-2 bg-brand-600 text-white text-sm rounded-lg hover:bg-brand-700">+ 新建内容</button>
|
|
</div>
|
|
<div class="bg-white rounded-xl border overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50 text-gray-500">
|
|
<tr><th class="text-left px-4 py-3 font-medium">ID</th><th class="text-left px-4 py-3 font-medium">标题</th><th class="text-left px-4 py-3 font-medium">类型</th><th class="text-left px-4 py-3 font-medium">浏览量</th><th class="text-left px-4 py-3 font-medium">状态</th><th class="text-left px-4 py-3 font-medium">操作</th></tr>
|
|
</thead>
|
|
<tbody id="contentsTable" class="divide-y"></tbody>
|
|
</table>
|
|
</div>
|
|
<div id="contentForm" class="hidden mt-6 bg-white p-6 rounded-xl border max-w-2xl">
|
|
<h3 class="text-lg font-semibold mb-4" id="contentFormTitle">新建内容</h3>
|
|
<div class="space-y-3">
|
|
<input id="cof-title" placeholder="内容标题" class="w-full px-3 py-2 border rounded-lg text-sm">
|
|
<textarea id="cof-summary" placeholder="摘要" class="w-full px-3 py-2 border rounded-lg text-sm" rows="2"></textarea>
|
|
<textarea id="cof-content" placeholder="内容 (支持 Markdown)" class="w-full px-3 py-2 border rounded-lg text-sm" rows="8"></textarea>
|
|
<div class="flex gap-3">
|
|
<select id="cof-type" class="px-3 py-2 border rounded-lg text-sm">
|
|
<option value="article">文章</option><option value="tutorial">教程</option><option value="news">资讯</option>
|
|
</select>
|
|
<select id="cof-status" class="px-3 py-2 border rounded-lg text-sm">
|
|
<option value="DRAFT">草稿</option><option value="PUBLISHED">已发布</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button onclick="saveContent()" class="px-4 py-2 bg-brand-600 text-white text-sm rounded-lg">保存</button>
|
|
<button onclick="cancelContentForm()" class="px-4 py-2 border text-sm rounded-lg">取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-prompts" class="page-content hidden">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-bold">提示词管理</h2>
|
|
<button onclick="showPromptForm()" class="px-4 py-2 bg-brand-600 text-white text-sm rounded-lg hover:bg-brand-700">+ 新建提示词</button>
|
|
</div>
|
|
<div class="bg-white rounded-xl border overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50 text-gray-500">
|
|
<tr><th class="text-left px-4 py-3 font-medium">ID</th><th class="text-left px-4 py-3 font-medium">标题</th><th class="text-left px-4 py-3 font-medium">标签</th><th class="text-left px-4 py-3 font-medium">模型</th><th class="text-left px-4 py-3 font-medium">状态</th><th class="text-left px-4 py-3 font-medium">操作</th></tr>
|
|
</thead>
|
|
<tbody id="promptsTable" class="divide-y"></tbody>
|
|
</table>
|
|
</div>
|
|
<div id="promptForm" class="hidden mt-6 bg-white p-6 rounded-xl border max-w-2xl">
|
|
<h3 class="text-lg font-semibold mb-4" id="promptFormTitle">新建提示词</h3>
|
|
<div class="space-y-3">
|
|
<input id="pf-title" placeholder="提示词标题" class="w-full px-3 py-2 border rounded-lg text-sm">
|
|
<textarea id="pf-content" placeholder="提示词内容" class="w-full px-3 py-2 border rounded-lg text-sm" rows="6"></textarea>
|
|
<input id="pf-tags" placeholder="标签(逗号分隔)" class="w-full px-3 py-2 border rounded-lg text-sm">
|
|
<div class="flex gap-3">
|
|
<input id="pf-model" placeholder="适用模型" class="flex-1 px-3 py-2 border rounded-lg text-sm">
|
|
<select id="pf-status" class="px-3 py-2 border rounded-lg text-sm">
|
|
<option value="DRAFT">草稿</option><option value="PUBLISHED">已发布</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button onclick="savePrompt()" class="px-4 py-2 bg-brand-600 text-white text-sm rounded-lg">保存</button>
|
|
<button onclick="cancelPromptForm()" class="px-4 py-2 border text-sm rounded-lg">取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-tools" class="page-content hidden">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-bold">工具管理</h2>
|
|
<button onclick="showToolForm()" class="px-4 py-2 bg-brand-600 text-white text-sm rounded-lg hover:bg-brand-700">+ 新建工具</button>
|
|
</div>
|
|
<div class="bg-white rounded-xl border overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50 text-gray-500">
|
|
<tr><th class="text-left px-4 py-3 font-medium">ID</th><th class="text-left px-4 py-3 font-medium">名称</th><th class="text-left px-4 py-3 font-medium">URL</th><th class="text-left px-4 py-3 font-medium">推荐</th><th class="text-left px-4 py-3 font-medium">状态</th><th class="text-left px-4 py-3 font-medium">操作</th></tr>
|
|
</thead>
|
|
<tbody id="toolsTable" class="divide-y"></tbody>
|
|
</table>
|
|
</div>
|
|
<div id="toolForm" class="hidden mt-6 bg-white p-6 rounded-xl border max-w-2xl">
|
|
<h3 class="text-lg font-semibold mb-4" id="toolFormTitle">新建工具</h3>
|
|
<div class="space-y-3">
|
|
<input id="tf-name" placeholder="工具名称" class="w-full px-3 py-2 border rounded-lg text-sm">
|
|
<input id="tf-url" placeholder="工具 URL" class="w-full px-3 py-2 border rounded-lg text-sm">
|
|
<textarea id="tf-desc" placeholder="描述" class="w-full px-3 py-2 border rounded-lg text-sm" rows="2"></textarea>
|
|
<input id="tf-tags" placeholder="标签(逗号分隔)" class="w-full px-3 py-2 border rounded-lg text-sm">
|
|
<div class="flex gap-3">
|
|
<label class="flex items-center gap-2 text-sm"><input type="checkbox" id="tf-featured"> 推荐</label>
|
|
<select id="tf-status" class="px-3 py-2 border rounded-lg text-sm">
|
|
<option value="DRAFT">草稿</option><option value="PUBLISHED">已发布</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button onclick="saveTool()" class="px-4 py-2 bg-brand-600 text-white text-sm rounded-lg">保存</button>
|
|
<button onclick="cancelToolForm()" class="px-4 py-2 border text-sm rounded-lg">取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-orders" class="page-content hidden">
|
|
<h2 class="text-xl font-bold mb-6">订单管理</h2>
|
|
<div class="bg-white rounded-xl border overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50 text-gray-500">
|
|
<tr><th class="text-left px-4 py-3 font-medium">订单号</th><th class="text-left px-4 py-3 font-medium">用户</th><th class="text-left px-4 py-3 font-medium">金额</th><th class="text-left px-4 py-3 font-medium">套餐</th><th class="text-left px-4 py-3 font-medium">状态</th><th class="text-left px-4 py-3 font-medium">支付渠道</th><th class="text-left px-4 py-3 font-medium">时间</th></tr>
|
|
</thead>
|
|
<tbody id="ordersTable" class="divide-y"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API = 'http://localhost:4000/api/v1';
|
|
let editingId = null;
|
|
let chapterCounter = 0;
|
|
let categoriesCache = [];
|
|
|
|
function $(id) { return document.getElementById(id); }
|
|
|
|
function toast(msg, type = 'success') {
|
|
const el = $('toast');
|
|
el.className = `fixed top-4 right-4 z-50 px-4 py-3 rounded-lg text-sm font-medium shadow-lg transition-all ${
|
|
type === 'success' ? 'bg-green-600 text-white' : 'bg-red-600 text-white'
|
|
}`;
|
|
el.textContent = msg;
|
|
el.classList.remove('hidden');
|
|
setTimeout(() => el.classList.add('hidden'), 3000);
|
|
}
|
|
|
|
function loading(btn, isLoading) {
|
|
if (isLoading) { btn.disabled = true; btn._text = btn.textContent; btn.textContent = '处理中...'; }
|
|
else { btn.disabled = false; btn.textContent = btn._text || btn.textContent; }
|
|
}
|
|
|
|
async function api(url, opts = {}) {
|
|
const token = JSON.parse(localStorage.getItem('admin_user') || '{}').token;
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
const res = await fetch(API + url, { headers, ...opts });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.message || '请求失败');
|
|
return data;
|
|
}
|
|
|
|
async function login() {
|
|
const username = $('username').value;
|
|
const password = $('password').value;
|
|
if (!username || !password) { $('loginError').classList.remove('hidden'); $('loginError').textContent = '请填写账号和密码'; return; }
|
|
try {
|
|
const data = await api('/admin/login', { method: 'POST', body: JSON.stringify({ username, password }) });
|
|
const tokenRes = await fetch(API + '/admin/dashboard', {
|
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('_jwt') }
|
|
});
|
|
const loginRes = await fetch(API + '/auth/login', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ account: username, password })
|
|
});
|
|
const loginData = await loginRes.json();
|
|
data.token = loginData.accessToken || '';
|
|
if (!data.token) {
|
|
const adminStr = btoa(JSON.stringify({ id: data.id, username: data.username }));
|
|
data.token = 'admin_' + adminStr;
|
|
}
|
|
localStorage.setItem('admin_user', JSON.stringify(data));
|
|
$('loginPage').classList.add('hidden');
|
|
$('adminApp').classList.remove('hidden');
|
|
$('adminName').textContent = data.username;
|
|
showPage('dashboard');
|
|
} catch (e) {
|
|
$('loginError').classList.remove('hidden');
|
|
$('loginError').textContent = e.message || '无法连接服务器';
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('admin_user');
|
|
$('adminApp').classList.add('hidden');
|
|
$('loginPage').classList.remove('hidden');
|
|
}
|
|
|
|
const user = JSON.parse(localStorage.getItem('admin_user') || '{}');
|
|
if (user.id) {
|
|
$('loginPage').classList.add('hidden');
|
|
$('adminApp').classList.remove('hidden');
|
|
$('adminName').textContent = user.username;
|
|
showPage('dashboard');
|
|
}
|
|
|
|
function showPage(page) {
|
|
document.querySelectorAll('.page-content').forEach(el => el.classList.add('hidden'));
|
|
$(`page-${page}`).classList.remove('hidden');
|
|
document.querySelectorAll('.nav-item').forEach(el => {
|
|
el.classList.remove('bg-brand-50', 'text-brand-600', 'font-medium');
|
|
});
|
|
const navItem = document.querySelector(`[data-page="${page}"]`);
|
|
if (navItem) navItem.classList.add('bg-brand-50', 'text-brand-600', 'font-medium');
|
|
const loaders = {
|
|
dashboard: loadDashboard, categories: loadCategories, users: loadUsers,
|
|
courses: loadCourses, contents: loadContents, prompts: loadPrompts,
|
|
tools: loadTools, orders: loadOrders,
|
|
};
|
|
if (loaders[page]) loaders[page]();
|
|
}
|
|
|
|
async function loadDashboard() {
|
|
try {
|
|
const data = await api('/admin/dashboard');
|
|
if (data.stats) Object.keys(data.stats).forEach(k => {
|
|
const el = $(`stat-${k}`);
|
|
if (el) el.textContent = data.stats[k].toLocaleString();
|
|
});
|
|
} catch (e) { toast('加载数据看板失败: ' + e.message, 'error'); }
|
|
}
|
|
|
|
async function loadCategories() {
|
|
try {
|
|
const data = await api('/categories');
|
|
$('categoriesTable').innerHTML = (Array.isArray(data) ? data : data.items || []).map(c =>
|
|
`<tr class="hover:bg-gray-50"><td class="px-4 py-3">${c.id}</td><td class="px-4 py-3 font-medium">${c.name}</td><td class="px-4 py-3 text-gray-400">${c.slug}</td><td class="px-4 py-3">${c.sortOrder}</td><td class="px-4 py-3 text-xs text-gray-400">课程 ${c._count?.courses||0} · 提示词 ${c._count?.prompts||0} · 工具 ${c._count?.tools||0}</td><td class="px-4 py-3"><button onclick="editCategory(${c.id})" class="text-brand-600 text-sm mr-2">编辑</button><button onclick="deleteCategory(${c.id})" class="text-red-500 text-sm">删除</button></td></tr>`
|
|
).join('') || '<tr><td colspan="6" class="px-4 py-8 text-center text-gray-400">暂无数据</td></tr>';
|
|
} catch (e) { toast('加载分类失败: ' + e.message, 'error'); }
|
|
}
|
|
|
|
function showCategoryForm() {
|
|
editingId = null;
|
|
$('categoryFormTitle').textContent = '新建分类';
|
|
['caf-name','caf-slug','caf-desc','caf-sort'].forEach(id => $(id).value = '');
|
|
$('categoryForm').classList.remove('hidden');
|
|
}
|
|
function cancelCategoryForm() { $('categoryForm').classList.add('hidden'); }
|
|
|
|
async function saveCategory() {
|
|
const body = {
|
|
name: $('caf-name').value, slug: $('caf-slug').value,
|
|
description: $('caf-desc').value, sortOrder: parseInt($('caf-sort').value) || 0,
|
|
};
|
|
if (!body.name || !body.slug) return toast('请填写名称和标识', 'error');
|
|
try {
|
|
const url = editingId ? `/categories/${editingId}` : '/categories';
|
|
await api(url, { method: editingId ? 'PUT' : 'POST', body: JSON.stringify(body) });
|
|
toast(editingId ? '分类已更新' : '分类已创建');
|
|
cancelCategoryForm(); loadCategories();
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function editCategory(id) {
|
|
try {
|
|
const data = await api(`/categories/${id}`);
|
|
editingId = id;
|
|
$('categoryFormTitle').textContent = '编辑分类';
|
|
$('caf-name').value = data.name;
|
|
$('caf-slug').value = data.slug;
|
|
$('caf-desc').value = data.description || '';
|
|
$('caf-sort').value = data.sortOrder || 0;
|
|
$('categoryForm').classList.remove('hidden');
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function deleteCategory(id) {
|
|
if (!confirm('确定删除此分类?关联内容不会被删除。')) return;
|
|
try {
|
|
await api(`/categories/${id}`, { method: 'DELETE' });
|
|
toast('分类已删除');
|
|
loadCategories();
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function loadUsers() {
|
|
try {
|
|
const data = await api('/users');
|
|
$('usersTable').innerHTML = (data.items || []).map(u =>
|
|
`<tr class="hover:bg-gray-50"><td class="px-4 py-3">${u.id}</td><td class="px-4 py-3">${u.nickname || '-'}</td><td class="px-4 py-3">${u.phone || '-'}</td><td class="px-4 py-3">${u.email || '-'}</td><td class="px-4 py-3">${u.memberPlan === 'FREE' ? '免费' : u.memberPlan}</td><td class="px-4 py-3 text-gray-400">${new Date(u.createdAt).toLocaleDateString()}</td></tr>`
|
|
).join('') || '<tr><td colspan="6" class="px-4 py-8 text-center text-gray-400">暂无数据</td></tr>';
|
|
} catch (e) { toast('加载用户失败: ' + e.message, 'error'); }
|
|
}
|
|
|
|
async function loadCourses() {
|
|
try {
|
|
const data = await api('/courses');
|
|
$('coursesTable').innerHTML = (data.items || []).map(c => {
|
|
const lessonCount = c.chapters?.reduce((s, ch) => s + (ch.lessons?.length || 0), 0) || 0;
|
|
return `<tr class="hover:bg-gray-50"><td class="px-4 py-3">${c.id}</td><td class="px-4 py-3 font-medium">${c.title}</td><td class="px-4 py-3">${c.category?.name || '-'}</td><td class="px-4 py-3">${c.isFree ? '免费' : '¥' + c.price}</td><td class="px-4 py-3 text-xs text-gray-400">${c.chapters?.length||0} 章 / ${lessonCount} 课</td><td class="px-4 py-3"><span class="text-xs px-2 py-0.5 rounded ${c.status === 'PUBLISHED' ? 'bg-green-50 text-green-600' : 'bg-gray-50 text-gray-400'}">${c.status === 'PUBLISHED' ? '已发布' : '草稿'}</span></td><td class="px-4 py-3"><button onclick="editCourse(${c.id})" class="text-brand-600 text-sm mr-2">编辑</button><button onclick="deleteCourse(${c.id})" class="text-red-500 text-sm">删除</button></td></tr>`;
|
|
}).join('') || '<tr><td colspan="7" class="px-4 py-8 text-center text-gray-400">暂无数据</td></tr>';
|
|
} catch (e) { toast('加载课程失败: ' + e.message, 'error'); }
|
|
}
|
|
|
|
function showCourseForm() {
|
|
editingId = null;
|
|
$('courseFormTitle').textContent = '新建课程';
|
|
$('cf-title').value = ''; $('cf-desc').value = '';
|
|
$('cf-price').value = '0'; $('cf-isFree').checked = true;
|
|
$('cf-status').value = 'DRAFT'; $('cf-sort').value = '0';
|
|
$('chaptersSection').classList.add('hidden');
|
|
$('chaptersList').innerHTML = '';
|
|
loadCategorySelect('cf-category');
|
|
$('courseForm').classList.remove('hidden');
|
|
}
|
|
function cancelCourseForm() {
|
|
$('courseForm').classList.add('hidden');
|
|
$('chaptersSection').classList.add('hidden');
|
|
}
|
|
|
|
async function loadCategorySelect(id) {
|
|
try {
|
|
const data = await api('/categories');
|
|
const cats = Array.isArray(data) ? data : data.items || [];
|
|
categoriesCache = cats;
|
|
$(id).innerHTML = '<option value="">无分类</option>' + cats.map(c =>
|
|
`<option value="${c.id}">${c.name}</option>`
|
|
).join('');
|
|
} catch (e) {
|
|
$(id).innerHTML = '<option value="">无分类</option>';
|
|
}
|
|
}
|
|
|
|
function addChapter(data) {
|
|
const idx = chapterCounter++;
|
|
const div = document.createElement('div');
|
|
div.className = 'chapter-block bg-gray-50 rounded-lg p-3 border';
|
|
div.dataset.idx = idx;
|
|
div.innerHTML = `
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<span class="text-xs text-gray-400 font-medium chapter-num">第 ${$('#chaptersList').children.length + 1} 章</span>
|
|
<input class="chapter-title flex-1 px-2 py-1 border rounded text-sm" placeholder="章节标题" value="${data?.title || ''}">
|
|
<input class="chapter-sort w-16 px-2 py-1 border rounded text-sm" type="number" placeholder="排序" value="${data?.sortOrder || 0}">
|
|
<button onclick="removeChapter(this)" class="text-red-400 hover:text-red-600 text-sm px-1">✕</button>
|
|
</div>
|
|
<div class="lessons-list space-y-1 ml-2"></div>
|
|
<button onclick="addLesson(this)" class="text-xs text-brand-600 hover:text-brand-700 mt-1 ml-2">+ 添加课时</button>
|
|
`;
|
|
if (data?.lessons) {
|
|
data.lessons.forEach(l => {
|
|
const lessonDiv = createLessonEl(l);
|
|
div.querySelector('.lessons-list').appendChild(lessonDiv);
|
|
});
|
|
}
|
|
$('chaptersList').appendChild(div);
|
|
updateChapterNumbers();
|
|
}
|
|
|
|
function createLessonEl(data) {
|
|
const div = document.createElement('div');
|
|
div.className = 'lesson-item flex items-center gap-2 py-1';
|
|
div.innerHTML = `
|
|
<span class="text-xs text-gray-400 w-6">${$('#chaptersList').querySelectorAll('.lesson-item').length + 1}.</span>
|
|
<input class="lesson-title flex-1 px-2 py-1 border rounded text-xs" placeholder="课时标题" value="${data?.title || ''}">
|
|
<input class="lesson-sort w-14 px-2 py-1 border rounded text-xs" type="number" placeholder="排序" value="${data?.sortOrder || 0}">
|
|
<select class="lesson-status px-2 py-1 border rounded text-xs">
|
|
<option value="PUBLISHED" ${data?.status === 'PUBLISHED' ? 'selected' : ''}>已发布</option>
|
|
<option value="DRAFT" ${data?.status === 'DRAFT' ? 'selected' : ''}>草稿</option>
|
|
</select>
|
|
<button onclick="this.parentElement.remove()" class="text-red-300 hover:text-red-500 text-xs px-1">✕</button>
|
|
`;
|
|
return div;
|
|
}
|
|
|
|
function addLesson(btn) {
|
|
const lessonsList = btn.previousElementSibling;
|
|
const el = createLessonEl(null);
|
|
lessonsList.appendChild(el);
|
|
updateLessonNumbers();
|
|
}
|
|
|
|
function removeChapter(btn) {
|
|
btn.closest('.chapter-block').remove();
|
|
updateChapterNumbers();
|
|
}
|
|
|
|
function updateChapterNumbers() {
|
|
$('chaptersList').querySelectorAll('.chapter-block').forEach((el, i) => {
|
|
el.querySelector('.chapter-num').textContent = `第 ${i + 1} 章`;
|
|
});
|
|
}
|
|
function updateLessonNumbers() {
|
|
$('chaptersList').querySelectorAll('.lesson-item').forEach((el, i) => {
|
|
el.querySelector('span').textContent = `${i + 1}.`;
|
|
});
|
|
}
|
|
|
|
function collectChaptersData() {
|
|
const chapters = [];
|
|
$('chaptersList').querySelectorAll('.chapter-block').forEach(block => {
|
|
const lessons = [];
|
|
block.querySelectorAll('.lesson-item').forEach(item => {
|
|
lessons.push({
|
|
title: item.querySelector('.lesson-title').value,
|
|
sortOrder: parseInt(item.querySelector('.lesson-sort').value) || 0,
|
|
status: item.querySelector('.lesson-status').value,
|
|
});
|
|
});
|
|
chapters.push({
|
|
title: block.querySelector('.chapter-title').value,
|
|
sortOrder: parseInt(block.querySelector('.chapter-sort').value) || 0,
|
|
lessons,
|
|
});
|
|
});
|
|
return chapters;
|
|
}
|
|
|
|
async function saveCourse() {
|
|
const title = $('cf-title').value;
|
|
if (!title) return toast('请输入课程标题', 'error');
|
|
const chapters = collectChaptersData();
|
|
const body = {
|
|
title,
|
|
description: $('cf-desc').value,
|
|
price: parseFloat($('cf-price').value) || 0,
|
|
isFree: $('cf-isFree').checked,
|
|
status: $('cf-status').value,
|
|
sortOrder: parseInt($('cf-sort').value) || 0,
|
|
chapters,
|
|
};
|
|
if ($('cf-category').value) body.categoryId = parseInt($('cf-category').value);
|
|
try {
|
|
if (editingId) {
|
|
await api(`/courses/${editingId}`, { method: 'PUT', body: JSON.stringify({ title: body.title, description: body.description, price: body.price, isFree: body.isFree, status: body.status, sortOrder: body.sortOrder, categoryId: body.categoryId }) });
|
|
if (chapters.length > 0) {
|
|
await api(`/admin/courses/${editingId}/chapters`, { method: 'PUT', body: JSON.stringify({ chapters }) });
|
|
}
|
|
toast('课程已更新');
|
|
} else {
|
|
const course = await api('/courses', { method: 'POST', body: JSON.stringify(body) });
|
|
if (chapters.length > 0 && course.id) {
|
|
await api(`/admin/courses/${course.id}/chapters`, { method: 'PUT', body: JSON.stringify({ chapters }) });
|
|
}
|
|
toast('课程已创建');
|
|
}
|
|
cancelCourseForm(); loadCourses();
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function editCourse(id) {
|
|
try {
|
|
const data = await api(`/courses/${id}`);
|
|
editingId = id;
|
|
$('courseFormTitle').textContent = '编辑课程';
|
|
$('cf-title').value = data.title;
|
|
$('cf-desc').value = data.description || '';
|
|
$('cf-price').value = data.price;
|
|
$('cf-isFree').checked = data.isFree;
|
|
$('cf-status').value = data.status;
|
|
$('cf-sort').value = data.sortOrder || 0;
|
|
await loadCategorySelect('cf-category');
|
|
if (data.categoryId) $('cf-category').value = data.categoryId;
|
|
$('chaptersList').innerHTML = '';
|
|
chapterCounter = 0;
|
|
if (data.chapters && data.chapters.length > 0) {
|
|
$('chaptersSection').classList.remove('hidden');
|
|
data.chapters.forEach(ch => addChapter(ch));
|
|
} else {
|
|
$('chaptersSection').classList.remove('hidden');
|
|
}
|
|
$('courseForm').classList.remove('hidden');
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function deleteCourse(id) {
|
|
if (!confirm('确定删除此课程?')) return;
|
|
try {
|
|
await api(`/courses/${id}`, { method: 'DELETE' });
|
|
toast('课程已删除');
|
|
loadCourses();
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function loadContents() {
|
|
try {
|
|
const data = await api('/contents');
|
|
$('contentsTable').innerHTML = (data.items || []).map(c =>
|
|
`<tr class="hover:bg-gray-50"><td class="px-4 py-3">${c.id}</td><td class="px-4 py-3">${c.title}</td><td class="px-4 py-3">${c.contentType}</td><td class="px-4 py-3">${c.viewCount}</td><td class="px-4 py-3"><span class="text-xs px-2 py-0.5 rounded ${c.status === 'PUBLISHED' ? 'bg-green-50 text-green-600' : 'bg-gray-50 text-gray-400'}">${c.status === 'PUBLISHED' ? '已发布' : '草稿'}</span></td><td class="px-4 py-3"><button onclick="editContent(${c.id})" class="text-brand-600 text-sm mr-2">编辑</button><button onclick="deleteContent(${c.id})" class="text-red-500 text-sm">删除</button></td></tr>`
|
|
).join('') || '<tr><td colspan="6" class="px-4 py-8 text-center text-gray-400">暂无数据</td></tr>';
|
|
} catch (e) { toast('加载内容失败: ' + e.message, 'error'); }
|
|
}
|
|
|
|
function showContentForm() {
|
|
editingId = null;
|
|
$('contentFormTitle').textContent = '新建内容';
|
|
['cof-title','cof-summary','cof-content'].forEach(id => $(id).value = '');
|
|
$('cof-type').value = 'article'; $('cof-status').value = 'DRAFT';
|
|
$('contentForm').classList.remove('hidden');
|
|
}
|
|
function cancelContentForm() { $('contentForm').classList.add('hidden'); }
|
|
|
|
async function saveContent() {
|
|
const body = { title: $('cof-title').value, summary: $('cof-summary').value, content: $('cof-content').value, contentType: $('cof-type').value, status: $('cof-status').value };
|
|
if (!body.title) return toast('请输入标题', 'error');
|
|
try {
|
|
await api(editingId ? `/contents/${editingId}` : '/contents', {
|
|
method: editingId ? 'PUT' : 'POST', body: JSON.stringify(body),
|
|
});
|
|
toast(editingId ? '内容已更新' : '内容已创建');
|
|
cancelContentForm(); loadContents();
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function editContent(id) {
|
|
try {
|
|
const data = await api(`/contents/${id}`);
|
|
editingId = id;
|
|
$('contentFormTitle').textContent = '编辑内容';
|
|
$('cof-title').value = data.title;
|
|
$('cof-summary').value = data.summary || '';
|
|
$('cof-content').value = data.content || '';
|
|
$('cof-type').value = data.contentType;
|
|
$('cof-status').value = data.status;
|
|
$('contentForm').classList.remove('hidden');
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function deleteContent(id) {
|
|
if (!confirm('确定删除?')) return;
|
|
try {
|
|
await api(`/contents/${id}`, { method: 'DELETE' });
|
|
toast('内容已删除');
|
|
loadContents();
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function loadPrompts() {
|
|
try {
|
|
const data = await api('/prompts');
|
|
$('promptsTable').innerHTML = (data.items || []).map(p =>
|
|
`<tr class="hover:bg-gray-50"><td class="px-4 py-3">${p.id}</td><td class="px-4 py-3">${p.title}</td><td class="px-4 py-3">${p.tags || '-'}</td><td class="px-4 py-3">${p.model || '-'}</td><td class="px-4 py-3"><span class="text-xs px-2 py-0.5 rounded ${p.status === 'PUBLISHED' ? 'bg-green-50 text-green-600' : 'bg-gray-50 text-gray-400'}">${p.status === 'PUBLISHED' ? '已发布' : '草稿'}</span></td><td class="px-4 py-3"><button onclick="editPrompt(${p.id})" class="text-brand-600 text-sm mr-2">编辑</button><button onclick="deletePrompt(${p.id})" class="text-red-500 text-sm">删除</button></td></tr>`
|
|
).join('') || '<tr><td colspan="6" class="px-4 py-8 text-center text-gray-400">暂无数据</td></tr>';
|
|
} catch (e) { toast('加载提示词失败: ' + e.message, 'error'); }
|
|
}
|
|
|
|
function showPromptForm() {
|
|
editingId = null;
|
|
$('promptFormTitle').textContent = '新建提示词';
|
|
['pf-title','pf-content','pf-tags','pf-model'].forEach(id => $(id).value = '');
|
|
$('pf-status').value = 'DRAFT';
|
|
$('promptForm').classList.remove('hidden');
|
|
}
|
|
function cancelPromptForm() { $('promptForm').classList.add('hidden'); }
|
|
|
|
async function savePrompt() {
|
|
const body = { title: $('pf-title').value, content: $('pf-content').value, tags: $('pf-tags').value, model: $('pf-model').value, status: $('pf-status').value };
|
|
if (!body.title || !body.content) return toast('请输入标题和内容', 'error');
|
|
try {
|
|
await api(editingId ? `/prompts/${editingId}` : '/prompts', {
|
|
method: editingId ? 'PUT' : 'POST', body: JSON.stringify(body),
|
|
});
|
|
toast(editingId ? '提示词已更新' : '提示词已创建');
|
|
cancelPromptForm(); loadPrompts();
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function editPrompt(id) {
|
|
try {
|
|
const data = await api(`/prompts/${id}`);
|
|
editingId = id;
|
|
$('promptFormTitle').textContent = '编辑提示词';
|
|
$('pf-title').value = data.title;
|
|
$('pf-content').value = data.content;
|
|
$('pf-tags').value = data.tags || '';
|
|
$('pf-model').value = data.model || '';
|
|
$('pf-status').value = data.status;
|
|
$('promptForm').classList.remove('hidden');
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function deletePrompt(id) {
|
|
if (!confirm('确定删除?')) return;
|
|
try {
|
|
await api(`/prompts/${id}`, { method: 'DELETE' });
|
|
toast('提示词已删除');
|
|
loadPrompts();
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function loadTools() {
|
|
try {
|
|
const data = await api('/tools');
|
|
$('toolsTable').innerHTML = (data.items || []).map(t =>
|
|
`<tr class="hover:bg-gray-50"><td class="px-4 py-3">${t.id}</td><td class="px-4 py-3 font-medium">${t.name}</td><td class="px-4 py-3"><a href="${t.url}" target="_blank" class="text-brand-600 text-xs">${(t.url||'').slice(0, 30)}...</a></td><td class="px-4 py-3">${t.isFeatured ? '⭐' : '-'}</td><td class="px-4 py-3"><span class="text-xs px-2 py-0.5 rounded ${t.status === 'PUBLISHED' ? 'bg-green-50 text-green-600' : 'bg-gray-50 text-gray-400'}">${t.status === 'PUBLISHED' ? '已发布' : '草稿'}</span></td><td class="px-4 py-3"><button onclick="editTool(${t.id})" class="text-brand-600 text-sm mr-2">编辑</button><button onclick="deleteTool(${t.id})" class="text-red-500 text-sm">删除</button></td></tr>`
|
|
).join('') || '<tr><td colspan="6" class="px-4 py-8 text-center text-gray-400">暂无数据</td></tr>';
|
|
} catch (e) { toast('加载工具失败: ' + e.message, 'error'); }
|
|
}
|
|
|
|
function showToolForm() {
|
|
editingId = null;
|
|
$('toolFormTitle').textContent = '新建工具';
|
|
['tf-name','tf-url','tf-desc','tf-tags'].forEach(id => $(id).value = '');
|
|
$('tf-featured').checked = false;
|
|
$('tf-status').value = 'DRAFT';
|
|
$('toolForm').classList.remove('hidden');
|
|
}
|
|
function cancelToolForm() { $('toolForm').classList.add('hidden'); }
|
|
|
|
async function saveTool() {
|
|
const body = { name: $('tf-name').value, url: $('tf-url').value, description: $('tf-desc').value, tags: $('tf-tags').value, isFeatured: $('tf-featured').checked, status: $('tf-status').value };
|
|
if (!body.name || !body.url) return toast('请输入名称和URL', 'error');
|
|
try {
|
|
await api(editingId ? `/tools/${editingId}` : '/tools', {
|
|
method: editingId ? 'PUT' : 'POST', body: JSON.stringify(body),
|
|
});
|
|
toast(editingId ? '工具已更新' : '工具已创建');
|
|
cancelToolForm(); loadTools();
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function editTool(id) {
|
|
try {
|
|
const data = await api(`/tools/${id}`);
|
|
editingId = id;
|
|
$('toolFormTitle').textContent = '编辑工具';
|
|
$('tf-name').value = data.name;
|
|
$('tf-url').value = data.url;
|
|
$('tf-desc').value = data.description || '';
|
|
$('tf-tags').value = data.tags || '';
|
|
$('tf-featured').checked = data.isFeatured;
|
|
$('tf-status').value = data.status;
|
|
$('toolForm').classList.remove('hidden');
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function deleteTool(id) {
|
|
if (!confirm('确定删除?')) return;
|
|
try {
|
|
await api(`/tools/${id}`, { method: 'DELETE' });
|
|
toast('工具已删除');
|
|
loadTools();
|
|
} catch (e) { toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function loadOrders() {
|
|
try {
|
|
const data = await api('/admin/orders');
|
|
$('ordersTable').innerHTML = (data.items || []).map(o =>
|
|
`<tr class="hover:bg-gray-50"><td class="px-4 py-3 font-mono text-xs">${o.orderNo}</td><td class="px-4 py-3">${o.user?.nickname || o.user?.phone || '用户#' + o.userId}</td><td class="px-4 py-3">¥${o.amount.toFixed(2)}</td><td class="px-4 py-3">${o.planType}</td><td class="px-4 py-3"><span class="text-xs px-2 py-0.5 rounded ${
|
|
o.status === 'PAID' ? 'bg-green-50 text-green-600' :
|
|
o.status === 'REFUNDED' ? 'bg-red-50 text-red-600' :
|
|
'bg-yellow-50 text-yellow-600'
|
|
}">${o.status === 'PAID' ? '已支付' : o.status === 'REFUNDED' ? '已退款' : o.status === 'CANCELLED' ? '已取消' : '待支付'}</span></td><td class="px-4 py-3 text-xs text-gray-400">${o.payChannel || '-'}</td><td class="px-4 py-3 text-xs text-gray-400">${new Date(o.createdAt).toLocaleString()}</td></tr>`
|
|
).join('') || '<tr><td colspan="7" class="px-4 py-8 text-center text-gray-400">暂无订单数据</td></tr>';
|
|
} catch (e) { toast('加载订单失败: ' + e.message, 'error'); }
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|