- 多轨道时间线: 视频/图片/音频/文字, 拖拽/修剪/分割/复制/删除 - 12款滤镜特效 + 4种转场 + 4种文字动画 - 6款程序化原创配乐(Web Audio 实时合成, 零版权风险) - Canvas 合成 + MediaRecorder 实时导出 WebM(VP9+Opus) - 账号体系 + 每日导出额度 + 企业/AI短剧付费升级 + 管理后台 - 项目自动保存/缩略图, 移动端自适应 - 个人免费每天30次导出, 企业¥299/月 不限量, AI短剧¥499/月 不限量
103 lines
5.6 KiB
HTML
103 lines
5.6 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>FreeCut · 管理后台</title>
|
|
<link rel="stylesheet" href="/css/style.css">
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚙️</text></svg>">
|
|
<style>
|
|
table { width: 100%; border-collapse: collapse; background: var(--bg2); border-radius: 10px; overflow: hidden; }
|
|
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--line); font-size: 13px; }
|
|
th { background: var(--bg3); color: var(--muted); font-weight: 600; }
|
|
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px,1fr)); gap: 14px; margin: 20px 0; }
|
|
.stat { background: var(--bg2); border: 1px solid var(--line); border-radius: 12px; padding: 16px; }
|
|
.stat .n { font-size: 28px; font-weight: 800; background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
|
.stat .l { color: var(--muted); font-size: 13px; }
|
|
select, button { margin-right: 6px; }
|
|
.wrap { max-width: 1100px; margin: 0 auto; padding: 20px 16px 60px; }
|
|
.bar-mini { height: 10px; background: var(--bg3); border-radius: 5px; overflow: hidden; display: inline-block; width: 120px; vertical-align: middle; }
|
|
.bar-mini i { display: block; height: 100%; background: var(--grad); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="land-nav">
|
|
<span class="logo">⚙️ FreeCut 管理后台</span>
|
|
<span class="spacer"></span>
|
|
<a class="btn small ghost" href="/">← 返回首页</a>
|
|
</nav>
|
|
<div class="wrap">
|
|
<div class="stats" id="stats"></div>
|
|
|
|
<h3 style="margin:24px 0 10px">📋 升级申请</h3>
|
|
<div id="req-box"></div>
|
|
|
|
<h3 style="margin:24px 0 10px">👥 用户管理</h3>
|
|
<table>
|
|
<thead><tr><th>ID</th><th>用户名</th><th>昵称</th><th>等级</th><th>付费到期</th><th>今日导出</th><th>操作</th></tr></thead>
|
|
<tbody id="user-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
<script src="/js/common.js"></script>
|
|
<script>
|
|
const $ = s => document.querySelector(s);
|
|
async function load() {
|
|
try {
|
|
const s = await FC.api('/api/admin/stats');
|
|
$('#stats').innerHTML = `
|
|
<div class="stat"><div class="n">${s.users}</div><div class="l">注册用户</div></div>
|
|
<div class="stat"><div class="n">${s.projects}</div><div class="l">作品数</div></div>
|
|
<div class="stat"><div class="n">${s.exports}</div><div class="l">累计导出</div></div>
|
|
<div class="stat"><div class="n">近7天导出</div><div class="l">${s.days.map(d => `${d.date.slice(5)} <span class="bar-mini"><i style="width:${Math.max(4, Math.min(100, d.count * 12))}%"></i></span> ${d.count}`).join(' · ')}</div></div>`;
|
|
} catch (e) { FC.toast(e.message, 'err'); }
|
|
// 申请
|
|
try {
|
|
const r = await FC.api('/api/admin/requests');
|
|
const list = r.requests;
|
|
$('#req-box').innerHTML = !list.length ? '<p class="muted">暂无申请</p>' : `
|
|
<table><thead><tr><th>ID</th><th>用户</th><th>申请版本</th><th>联系方式</th><th>状态</th><th>时间</th><th>操作</th></tr></thead><tbody>
|
|
${list.map(x => `<tr>
|
|
<td>${x.id}</td><td>${FC.esc(x.username || x.user_id)}</td>
|
|
<td>${x.target_tier === 'business' ? '🏢 企业版' : '🎬 AI短剧版'}</td>
|
|
<td>${FC.esc(x.contact || '-')}</td>
|
|
<td>${x.status === 'pending' ? '<span class="tag" style="background:rgba(243,156,18,.15);color:var(--warn)">待审核</span>' : x.status === 'approved' ? '<span class="tag free">已开通</span>' : '<span class="tag">已拒绝</span>'}</td>
|
|
<td>${new Date(x.created_at * 1000).toLocaleString()}</td>
|
|
<td>${x.status === 'pending' ? `<button class="btn small" onclick="handleReq(${x.id},'approve')">✅ 开通(付费1年)</button> <button class="btn small danger" onclick="handleReq(${x.id},'reject')">✕ 拒绝</button>` : '-'}</td>
|
|
</tr>`).join('')}</tbody></table>`;
|
|
} catch (e) {}
|
|
// 用户
|
|
try {
|
|
const r = await FC.api('/api/admin/users');
|
|
$('#user-tbody').innerHTML = r.users.map(u => `<tr>
|
|
<td>${u.id}</td><td>${FC.esc(u.username)}</td><td>${FC.esc(u.nickname)}</td>
|
|
<td><select onchange="setTier(${u.id},this.value)">
|
|
${Object.entries({free:'个人免费',pro:'个人专业',business:'企业',ai_short:'AI短剧'}).map(([k,v]) => `<option value="${k}" ${u.tier===k?'selected':''}>${v}</option>`).join('')}
|
|
</select></td>
|
|
<td>${u.paid_expire ? new Date(u.paid_expire*1000).toLocaleDateString() : '—'}</td>
|
|
<td>${u.today_exports}</td>
|
|
<td><button class="btn small ghost" onclick="setPaid(${u.id},${u.paid_expire?'0':now()+31536000},this)">${u.paid_expire?'取消付费':'开通付费'}</button></td>
|
|
</tr>`).join('');
|
|
} catch (e) { FC.toast(e.message, 'err'); }
|
|
}
|
|
window.handleReq = async (id, action) => {
|
|
try { await FC.api('/api/admin/request', { method: 'POST', body: { id, action } }); FC.toast('已处理', 'ok'); load(); }
|
|
catch (e) { FC.toast(e.message, 'err'); }
|
|
};
|
|
window.setTier = async (id, tier) => {
|
|
try { await FC.api('/api/admin/user', { method: 'POST', body: { id, tier } }); FC.toast('已更新等级', 'ok'); }
|
|
catch (e) { FC.toast(e.message, 'err'); }
|
|
};
|
|
window.setPaid = async (id, expire, btn) => {
|
|
try {
|
|
const r = await FC.api('/api/admin/users');
|
|
const u = r.users.find(x => x.id === id);
|
|
await FC.api('/api/admin/user', { method: 'POST', body: { id, tier: u.tier, paid_expire: expire } });
|
|
FC.toast('已更新付费状态', 'ok'); load();
|
|
} catch (e) { FC.toast(e.message, 'err'); }
|
|
};
|
|
window.now = () => Math.floor(Date.now()/1000);
|
|
document.addEventListener('DOMContentLoaded', load);
|
|
</script>
|
|
</body>
|
|
</html>
|