CF上搭建订阅管理系统
// 订阅管理系统 - Cloudflare Workers 主入口);export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const path = url.pathname;// 处理 CORS
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
});
}// 静态页面路由
if (path === '/' || path === '/index.html') {
return new Response(await getLoginPage(), {
headers: { 'Content-Type': 'text/html' }
});
}if (path === '/admin888/' || path === '/admin888/index.html') {
return new Response(await getAdminPage(), {
headers: { 'Content-Type': 'text/html' }
});
}// 备份文件路由
if (path.startsWith('/backup/')) {
return new Response('Backup endpoint', { status: 200 });
}// API 路由
if (path.startsWith('/api/')) {
return handleApi(request, env, path);
}// 默认返回 404
return new Response('Not Found', { status: 404 });
}
};// 获取登录页面
async function getLoginPage() {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>订阅管理 - 登录</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
width: 100%;
max-width: 400px;
}
.login-title {
text-align: center;
margin-bottom: 30px;
color: #333;
font-size: 24px;
}
.form-group {
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.form-group label {
min-width: 80px;
color: #555;
font-weight: 500;
display: flex;
align-items: center;
gap: 5px;
}
.form-group input {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.captcha-group {
display: flex;
gap: 10px;
align-items: center;
flex: 1;
min-width: 0;
}
.captcha-group input {
flex: 1;
min-width: 0;
}
.captcha-canvas {
border-radius: 5px;
cursor: pointer;
border: 1px solid #ddd;
max-width: 120px;
height: 40px;
flex-shrink: 0;
}
.login-btn {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s;
}
.login-btn:hover {
transform: translateY(-2px);
}
.error-msg {
color: #e74c3c;
text-align: center;
margin-top: 15px;
display: none;
}
</style>
</head>
<body>
<div class="login-container">
<h1 class="login-title">🔐 订阅管理系统</h1>
<form id="loginForm">
<div class="form-group">
<label for="username">👤 帐号</label>
<input type="text" id="username" name="username" required placeholder="请输入帐号">
</div>
<div class="form-group">
<label for="password">🔑 密码</label>
<input type="password" id="password" name="password" required placeholder="请输入密码">
</div>
<div class="form-group">
<label for="captchaInput">🛡️ 验证码</label>
<div class="captcha-group">
<input type="text" id="captchaInput" required placeholder="验证码.." maxlength="5">
<canvas id="captchaCanvas" class="captcha-canvas" width="120" height="40" title="点击刷新验证码"></canvas>
</div>
</div>
<button type="submit" class="login-btn">登录</button>
<p class="error-msg" id="errorMsg">帐号或密码错误</p>
</form>
</div>
<script>
// 验证码相关
let currentCaptcha = '';// 生成随机颜色
function randomColor() {
return 'rgb(' + Math.floor(Math.random() 256) + ',' + Math.floor(Math.random() 256) + ',' + Math.floor(Math.random() * 256) + ')';
}// 生成验证码
function generateCaptcha() {
const canvas = document.getElementById('captchaCanvas');
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;// 清空画布
ctx.clearRect(0, 0, width, height);// 生成随机背景色
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, width, height);// 绘制干扰点
for (let i = 0; i < 50; i++) {
ctx.fillStyle = randomColor();
ctx.beginPath();
ctx.arc(Math.random() width, Math.random() height, 2, 0, 2 * Math.PI);
ctx.fill();
}// 绘制干扰线
for (let i = 0; i < 5; i++) {
ctx.strokeStyle = randomColor();
ctx.lineWidth = 1 + Math.random() * 2;
ctx.beginPath();
ctx.moveTo(Math.random() width, Math.random() height);
ctx.lineTo(Math.random() width, Math.random() height);
ctx.stroke();
}// 生成 5 位数字验证码
const chars = '0123456789';
currentCaptcha = '';
for (let i = 0; i < 5; i++) {
currentCaptcha += chars.charAt(Math.floor(Math.random() * chars.length));
}// 绘制验证码文字
ctx.font = 'bold 24px Arial';
ctx.textBaseline = 'middle';
for (let i = 0; i < 5; i++) {
ctx.fillStyle = randomColor();
ctx.save();
const x = 15 + i * 20;
const y = 20 + (Math.random() - 0.5) * 10;
const angle = (Math.random() - 0.5) * 0.4;
ctx.translate(x, y);
ctx.rotate(angle);
ctx.fillText(currentCaptcha[i], 0, 0);
ctx.restore();
}
}document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const captchaInput = document.getElementById('captchaInput').value;// 验证验证码
if (captchaInput !== currentCaptcha) {
document.getElementById('errorMsg').textContent = '验证码错误';
document.getElementById('errorMsg').style.display = 'block';
generateCaptcha();
document.getElementById('captchaInput').value = '';
return;
}try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();if (data.success) {
localStorage.setItem('authToken', data.token);
window.location.href = '/admin888/';
} else {
if (data.remainingAttempts !== undefined) {
document.getElementById('errorMsg').textContent = '帐号或密码错误,还剩 ' + data.remainingAttempts + ' 次尝试机会';
} else {
document.getElementById('errorMsg').textContent = '帐号或密码错误';
}
document.getElementById('errorMsg').style.display = 'block';
generateCaptcha();
document.getElementById('captchaInput').value = '';
}
} catch (err) {
document.getElementById('errorMsg').textContent = '登录失败,请重试';
document.getElementById('errorMsg').style.display = 'block';
generateCaptcha();
document.getElementById('captchaInput').value = '';
}
});// 点击验证码刷新
document.getElementById('captchaCanvas').addEventListener('click', generateCaptcha);// 页面加载时生成验证码
generateCaptcha();
</script>
</body>
</html>`;
}// 获取管理页面
async function getAdminPage() {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>订阅管理 - 后台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f6fa;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 { font-size: 20px; }
.logout-btn {
background: rgba(255,255,255,0.2);
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.nav-tabs {
display: flex;
background: white;
border-radius: 8px;
margin-bottom: 20px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.nav-tab {
padding: 15px 25px;
cursor: pointer;
border: none;
background: white;
font-size: 14px;
transition: all 0.3s;
}
.nav-tab.active {
background: #667eea;
color: white;
}
.nav-tab:hover:not(.active) {
background: #f0f0f0;
}
.content-area {
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.section-title {
font-size: 18px;
margin-bottom: 20px;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
}
.add-btn {
background: #27ae60;
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.add-btn:hover { background: #219a52; }
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #555;
}
tr:hover { background: #f8f9fa; }
.action-btn {
padding: 5px 10px;
margin-right: 5px;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
.edit-btn { background: #3498db; color: white; }
.delete-btn { background: #e74c3c; color: white; }
.restore-btn { background: #f39c12; color: white; }
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
}
.modal-content {
background: white;
width: 90%;
max-width: 500px;
margin: 100px auto;
border-radius: 8px;
padding: 25px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
.form-group input, .form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
.form-group input:focus, .form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.submit-btn {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.status-ok { color: #27ae60; }
.status-warning { color: #f39c12; }
.status-danger { color: #e74c3c; }
.category-select {
margin-bottom: 20px;
}
.category-select select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
.copyable {
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.copyable:hover {
color: #667eea;
text-decoration: underline;
}
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
color: white;
padding: 12px 24px;
border-radius: 5px;
z-index: 2000;
display: none;
font-size: 14px;
}
.backup-info {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
font-size: 14px;
color: #555;
}
.backup-btn {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
}
.backup-btn:hover { background: #5568d3; }
</style>
</head>
<body>
<div class="header">
<h1>📋 订阅管理系统</h1>
<button class="logout-btn" onclick="logout()">退出登录</button>
</div><div class="container">
<div class="nav-tabs">
<button class="nav-tab active" onclick="showSection('products')">商品管理</button>
<button class="nav-tab" onclick="showSection('categories')">分类管理</button>
<button class="nav-tab" onclick="showSection('backup')">数据备份</button>
</div><div id="productsSection" class="content-area">
<div class="section-title">
<span>商品列表</span>
<div>
<button class="action-btn delete-btn" onclick="batchDeleteProducts()" style="margin-right: 10px;">🗑️ 批量删除</button>
<button class="add-btn" onclick="openProductModal()">+ 添加商品</button>
</div>
</div>
<div class="category-select">
<label>选择分类:</label>
<select id="categoryFilter" onchange="loadProducts()">
<option value="">全部分类</option>
</select>
</div>
<table>
<thead>
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
<th>序号</th>
<th>商品名称</th>
<th>分类</th>
<th>注册时间</th>
<th>续费时间</th>
<th>剩余天数</th>
<th>托管地址</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody id="productTable"></tbody>
</table>
</div><div id="categoriesSection" class="content-area" style="display:none;">
<div class="section-title">
<span>分类列表</span>
<button class="add-btn" onclick="openCategoryModal()">+ 添加分类</button>
</div>
<table>
<thead>
<tr>
<th>序号</th>
<th>分类名称</th>
<th>帐号</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody id="categoryTable"></tbody>
</table>
</div><div id="backupSection" class="content-area" style="display:none;">
<div class="section-title">
<span>备份管理</span>
<button class="backup-btn" onclick="createBackup()">📦 创建备份</button>
</div>
<div class="backup-info">
💡 备份说明:系统最多保留最近 5 个备份,创建新备份时会自动删除最旧的备份。恢复操作将覆盖当前所有数据,请谨慎操作。
</div>
<table>
<thead>
<tr>
<th>序号</th>
<th>备份时间</th>
<th>分类数量</th>
<th>商品数量</th>
<th>操作</th>
</tr>
</thead>
<tbody id="backupTable"></tbody>
</table>
</div>
</div><!-- 分类模态框 -->
<div id="categoryModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="categoryModalTitle">添加分类</h3>
<button class="close-btn" onclick="closeCategoryModal()">×</button>
</div>
<form id="categoryForm">
<input type="hidden" id="categoryId">
<div class="form-group">
<label>分类名称</label>
<input type="text" id="categoryName" required>
</div>
<div class="form-group">
<label>帐号</label>
<input type="text" id="categoryAccount">
</div>
<div class="form-group">
<label>密码</label>
<input type="text" id="categoryPassword">
</div>
<div class="form-group">
<label>备注</label>
<textarea id="categoryRemark" rows="3"></textarea>
</div>
<button type="submit" class="submit-btn">保存</button>
</form>
</div>
</div><!-- 商品模态框 -->
<div id="productModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="productModalTitle">添加商品</h3>
<button class="close-btn" onclick="closeProductModal()">×</button>
</div>
<form id="productForm">
<input type="hidden" id="productId">
<div class="form-group">
<label>商品名称</label>
<input type="text" id="productName" required>
</div>
<div class="form-group">
<label>所属分类</label>
<select id="productCategory" required></select>
</div>
<div class="form-group">
<label>注册时间</label>
<input type="date" id="registerDate" required>
</div>
<div class="form-group">
<label>续费时间</label>
<div style="display: flex; gap: 10px; align-items: center; width: 100%;">
<input type="date" id="renewDate" style="flex: 1; min-width: 0;">
<label style="white-space: nowrap; display: flex; align-items: center; gap: 5px;"><input type="checkbox" id="renewDateLongterm" onchange="toggleRenewDate()"> 长期</label>
</div>
</div>
<div class="form-group">
<label>托管地址</label>
<input type="text" id="hostUrl">
</div>
<div class="form-group">
<label>备注</label>
<textarea id="productRemark" rows="3"></textarea>
</div>
<button type="submit" class="submit-btn">保存</button>
</form>
</div>
</div><script>
// 认证检查
function checkAuth() {
const token = localStorage.getItem('authToken');
if (!token) {
window.location.href = '/';
}
}function logout() {
localStorage.removeItem('authToken');
window.location.href = '/';
}// 页面切换
function showSection(section) {
document.querySelectorAll('.nav-tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.content-area').forEach(area => area.style.display = 'none');if (section === 'products') {
document.querySelector('.nav-tab:nth-child(1)').classList.add('active');
document.getElementById('productsSection').style.display = 'block';
loadCategoriesForSelect();
loadProducts();
} else if (section === 'categories') {
document.querySelector('.nav-tab:nth-child(2)').classList.add('active');
document.getElementById('categoriesSection').style.display = 'block';
loadCategories();
} else if (section === 'backup') {
document.querySelector('.nav-tab:nth-child(3)').classList.add('active');
document.getElementById('backupSection').style.display = 'block';
loadBackups();
}
}// 加载分类
async function loadCategories() {
const response = await fetch('/api/categories', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
});
const data = await response.json();
// 按 sort 字段排序
data.sort((a, b) => (a.sort || 0) - (b.sort || 0));
const tbody = document.getElementById('categoryTable');
tbody.innerHTML = '';data.forEach((cat, index) => {
const tr = document.createElement('tr');
tr.innerHTML = \`
<td>
\${index + 1}
<button class="action-btn" onclick="moveCategory('\${cat.id}', 'up')" style="margin-left:5px;">↑</button>
<button class="action-btn" onclick="moveCategory('\${cat.id}', 'down')">↓</button>
</td>
<td class="copyable" onclick="copyToClipboard('\${cat.name.replace(/'/g, "\\'")}')">\${cat.name}</td>
<td class="copyable" onclick="copyToClipboard('\${(cat.account || '-').replace(/'/g, "\\'")}')">\${cat.account || '-'}</td>
<td class="copyable" onclick="copyToClipboard('\${(cat.remark || '-').replace(/'/g, "\\'")}')">\${cat.remark || '-'}</td>
<td>
<button class="action-btn edit-btn" onclick="editCategory('\${cat.id}')">编辑</button>
<button class="action-btn delete-btn" onclick="deleteCategory('\${cat.id}')">删除</button>
</td>
\`;
tbody.appendChild(tr);
});
}// 加载分类到选择框
async function loadCategoriesForSelect() {
const response = await fetch('/api/categories', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
});
const data = await response.json();
const select = document.getElementById('productCategory');
const filter = document.getElementById('categoryFilter');select.innerHTML = '<option value="">请选择分类</option>';
filter.innerHTML = '<option value="">全部分类</option>';data.forEach(cat => {
select.innerHTML += \<option value="\${cat.id}">\${cat.name}</option>\;
filter.innerHTML += \<option value="\${cat.id}">\${cat.name}</option>\;
});
}// 加载商品
async function loadProducts() {
const categoryId = document.getElementById('categoryFilter').value;
const url = categoryId ? \/api/products?categoryId=\${categoryId}\: '/api/products';const response = await fetch(url, {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
});
const data = await response.json();// 按剩余天数排序:剩余天数少的在前,长期在后
data.sort((a, b) => {
const daysA = calculateDays(a.renewDate);
const daysB = calculateDays(b.renewDate);// 长期排在最后
if (daysA.isLongterm && daysB.isLongterm) return 0;
if (daysA.isLongterm) return 1;
if (daysB.isLongterm) return -1;// 剩余天数少的在前
return daysA.days - daysB.days;
});const tbody = document.getElementById('productTable');
tbody.innerHTML = '';data.forEach((prod, index) => {
const daysInfo = calculateDays(prod.renewDate);
const statusClass = daysInfo.isLongterm ? 'status-ok' : (daysInfo.days >= 180 ? 'status-ok' : (daysInfo.days >= 30 ? 'status-warning' : 'status-danger'));const tr = document.createElement('tr');
tr.innerHTML = \`
<td><input type="checkbox" class="product-checkbox" value="\${prod.id}"></td>
<td>\${index + 1}</td>
<td class="copyable" onclick="copyToClipboard('\${prod.name.replace(/'/g, "\\'")}')">\${prod.name}</td>
<td>\${prod.categoryName || '-'}</td>
<td>\${prod.registerDate}</td>
<td>\${prod.renewDate}</td>
<td class="\${statusClass}">\${daysInfo.display}</td>
<td>\${prod.hostUrl || '-'}</td>
<td>\${prod.remark || '-'}</td>
<td>
<button class="action-btn edit-btn" onclick="editProduct('\${prod.id}')">编辑</button>
<button class="action-btn delete-btn" onclick="deleteProduct('\${prod.id}')">删除</button>
</td>
\`;
tbody.appendChild(tr);
});
}// 计算剩余天数
function calculateDays(renewDate) {
if (!renewDate || renewDate === 'longterm') {
return { isLongterm: true, days: 9999, display: '长期' };
}
const renew = new Date(renewDate);
const now = new Date();
const diff = renew - now;
const days = Math.ceil(diff / (1000 60 60 * 24));
return { isLongterm: false, days: days, display: days + '天' };
}// 切换长期选项
function toggleRenewDate() {
const isLongterm = document.getElementById('renewDateLongterm').checked;
document.getElementById('renewDate').disabled = isLongterm;
if (isLongterm) {
document.getElementById('renewDate').value = '';
}
}// 分类模态框
function openCategoryModal() {
document.getElementById('categoryModalTitle').textContent = '添加分类';
document.getElementById('categoryForm').reset();
document.getElementById('categoryId').value = '';
document.getElementById('categoryModal').style.display = 'block';
}function closeCategoryModal() {
document.getElementById('categoryModal').style.display = 'none';
}async function editCategory(id) {
const response = await fetch(\/api/categories/\${id}\, {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
});
const data = await response.json();document.getElementById('categoryModalTitle').textContent = '编辑分类';
document.getElementById('categoryId').value = data.id;
document.getElementById('categoryName').value = data.name;
document.getElementById('categoryAccount').value = data.account || '';
document.getElementById('categoryPassword').value = data.password || '';
document.getElementById('categoryRemark').value = data.remark || '';
document.getElementById('categoryModal').style.display = 'block';
}async function deleteCategory(id) {
if (!confirm('确定要删除这个分类吗?')) return;await fetch(\
/api/categories/\${id}\, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
});
loadCategories();
}document.getElementById('categoryForm').addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('categoryId').value;
const data = {
name: document.getElementById('categoryName').value,
account: document.getElementById('categoryAccount').value,
password: document.getElementById('categoryPassword').value,
remark: document.getElementById('categoryRemark').value
};const url = id ? \
/api/categories/\${id}\: '/api/categories';
const method = id ? 'PUT' : 'POST';await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('authToken')
},
body: JSON.stringify(data)
});closeCategoryModal();
loadCategories();
});// 商品模态框
function openProductModal() {
document.getElementById('productModalTitle').textContent = '添加商品';
document.getElementById('productForm').reset();
document.getElementById('productId').value = '';
document.getElementById('productModal').style.display = 'block';
}function closeProductModal() {
document.getElementById('productModal').style.display = 'none';
}async function editProduct(id) {
const response = await fetch(\/api/products/\${id}\, {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
});
const data = await response.json();document.getElementById('productModalTitle').textContent = '编辑商品';
document.getElementById('productId').value = data.id;
document.getElementById('productName').value = data.name;
document.getElementById('productCategory').value = data.categoryId;
document.getElementById('registerDate').value = data.registerDate;
if (data.renewDate === 'longterm') {
document.getElementById('renewDateLongterm').checked = true;
document.getElementById('renewDate').value = '';
document.getElementById('renewDate').disabled = true;
} else {
document.getElementById('renewDateLongterm').checked = false;
document.getElementById('renewDate').value = data.renewDate;
document.getElementById('renewDate').disabled = false;
}
document.getElementById('hostUrl').value = data.hostUrl || '';
document.getElementById('productRemark').value = data.remark || '';
document.getElementById('productModal').style.display = 'block';
}async function deleteProduct(id) {
if (!confirm('确定要删除这个商品吗?')) return;await fetch(\
/api/products/\${id}\, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
});
loadProducts();
}document.getElementById('productForm').addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('productId').value;
const data = {
name: document.getElementById('productName').value,
categoryId: document.getElementById('productCategory').value,
registerDate: document.getElementById('registerDate').value,
renewDate: document.getElementById('renewDateLongterm').checked ? 'longterm' : document.getElementById('renewDate').value,
hostUrl: document.getElementById('hostUrl').value,
remark: document.getElementById('productRemark').value
};const url = id ? \
/api/products/\${id}\: '/api/products';
const method = id ? 'PUT' : 'POST';await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('authToken')
},
body: JSON.stringify(data)
});closeProductModal();
loadProducts();
});// 初始化
checkAuth();
loadCategoriesForSelect();
loadProducts();// 全选/取消全选
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.product-checkbox');
checkboxes.forEach(cb => {
cb.checked = selectAll.checked;
});
}// 批量删除商品
async function batchDeleteProducts() {
const checkboxes = document.querySelectorAll('.product-checkbox:checked');
if (checkboxes.length === 0) {
showToast('请先选择要删除的商品');
return;
}if (!confirm(\
确定要删除选中的 \${checkboxes.length} 个商品吗?\)) return;const ids = Array.from(checkboxes).map(cb => cb.value);
try {
const response = await fetch('/api/products/batch-delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('authToken')
},
body: JSON.stringify({ ids })
});
const data = await response.json();if (data.success) {
showToast(\成功删除 \${ids.length} 个商品\);
document.getElementById('selectAll').checked = false;
loadProducts();
} else {
showToast('删除失败:' + (data.message || '未知错误'));
}
} catch (err) {
showToast('删除失败,请重试');
}
}// 复制到剪贴板
function copyToClipboard(text) {
if (!text || text === '-') return;navigator.clipboard.writeText(text).then(() => {
showToast('已复制:' + text);
}).catch(err => {
// 降级方案
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showToast('已复制:' + text);
});
}// 显示提示
function showToast(message) {
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
toast.className = 'toast';
document.body.appendChild(toast);
}
toast.textContent = message;
toast.style.display = 'block';setTimeout(() => {
toast.style.display = 'none';
}, 2000);
}// 移动分类顺序
async function moveCategory(id, direction) {
try {
const response = await fetch('/api/categories/move', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('authToken')
},
body: JSON.stringify({ id, direction })
});
const data = await response.json();
if (data.success) {
loadCategories();
} else {
showToast('移动失败:' + (data.message || '未知错误'));
}
} catch (err) {
showToast('移动失败,请重试');
}
}// 备份相关功能
async function loadBackups() {
const response = await fetch('/api/backups', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
});
const data = await response.json();
const tbody = document.getElementById('backupTable');
tbody.innerHTML = '';data.forEach((backup, index) => {
const tr = document.createElement('tr');
const backupTime = new Date(backup.createdAt).toLocaleString('zh-CN');
tr.innerHTML = \`
<td>\${index + 1}</td>
<td>\${backupTime}</td>
<td>\${backup.categoriesCount || 0}</td>
<td>\${backup.productsCount || 0}</td>
<td>
<button class="action-btn restore-btn" onclick="restoreBackup('\${backup.id}')">恢复</button>
<button class="action-btn delete-btn" onclick="deleteBackup('\${backup.id}')">删除</button>
</td>
\`;
tbody.appendChild(tr);
});
}async function createBackup() {
if (!confirm('确定要创建当前数据的备份吗?')) return;try {
const response = await fetch('/api/backups', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('authToken')
}
});
const data = await response.json();if (data.success) {
showToast('备份创建成功');
loadBackups();
} else {
showToast('备份失败:' + (data.message || '未知错误'));
}
} catch (err) {
showToast('备份失败,请重试');
}
}async function restoreBackup(id) {
if (!confirm('⚠️ 警告:恢复操作将覆盖当前所有数据!确定要继续吗?')) return;
if (!confirm('再次确认:此操作不可逆,确定要恢复此备份吗?')) return;try {
const response = await fetch(\/api/backups/\${id}/restore\, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('authToken')
}
});
const data = await response.json();if (data.success) {
showToast('恢复成功');
loadCategories();
loadProducts();
loadBackups();
} else {
showToast('恢复失败:' + (data.message || '未知错误'));
}
} catch (err) {
showToast('恢复失败,请重试');
}
}async function deleteBackup(id) {
if (!confirm('确定要删除这个备份吗?')) return;try {
await fetch(\/api/backups/\${id}\, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('authToken') }
});
showToast('备份已删除');
loadBackups();
} catch (err) {
showToast('删除失败,请重试');
}
}
</script>
</body>
</html>`;
}// 处理 API 请求
async function handleApi(request, env, path) {
const headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
};// 获取客户端 IP
const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';try {
// 登录
if (path === '/api/login' && request.method === 'POST') {
// 检查 IP 是否被限制
const rateLimitCheck = await checkRateLimit(env, clientIP);
if (!rateLimitCheck.allowed) {
const remainingTime = Math.ceil((rateLimitCheck.unlockedAt - Date.now()) / (1000 60 60));
return new Response(JSON.stringify({
success: false,
message:登录次数过多,请在${remainingTime}小时后重试
}), { headers });
}const { username, password } = await request.json();
if (username === env.ADMINUSERNAME && password === env.ADMINPASSWORD) {
// 登录成功,清除该 IP 的失败记录
await env.SUBKV.delete(loginattempt_${clientIP}
const token = btoa(JSON.stringify({ username, time: Date.now() }));
return new Response(JSON.stringify({ success: true, token }), { headers });
}// 登录失败,记录尝试次数并获取剩余次数
const attemptResult = await recordLoginAttempt(env, clientIP);
return new Response(JSON.stringify({
success: false,
message: '帐号或密码错误',
remainingAttempts: attemptResult.remainingAttempts
}), { headers });
}// 验证 token
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return new Response(JSON.stringify({ error: '未授权' }), { status: 401, headers });
}// 分类管理
if (path === '/api/categories' && request.method === 'GET') {
const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];
return new Response(JSON.stringify(categories), { headers });
}if (path === '/api/categories' && request.method === 'POST') {
const data = await request.json();
const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];// 获取最大 sort 值
const maxSort = categories.length > 0 ? Math.max(...categories.map(c => c.sort || 0)) : 0;const newCategory = {
id: Date.now().toString(),
name: data.name,
account: data.account,
password: data.password,
remark: data.remark,
sort: maxSort + 1,
createdAt: new Date().toISOString()
};categories.push(newCategory);
await env.SUB_KV.put('categories', JSON.stringify(categories));return new Response(JSON.stringify(newCategory), { headers });
}if (path.match(/^\/api\/categories\/[^\/]+$/) && request.method === 'GET') {
const id = path.split('/').pop();
const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];
const category = categories.find(c => c.id === id);if (!category) {
return new Response(JSON.stringify({ error: '未找到' }), { status: 404, headers });
}return new Response(JSON.stringify(category), { headers });
}if (path.match(/^\/api\/categories\/[^\/]+$/) && request.method === 'PUT') {
const id = path.split('/').pop();
const data = await request.json();
const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];const index = categories.findIndex(c => c.id === id);
if (index === -1) {
return new Response(JSON.stringify({ error: '未找到' }), { status: 404, headers });
}categories[index] = { ...categories[index], ...data, updatedAt: new Date().toISOString() };
await env.SUB_KV.put('categories', JSON.stringify(categories));return new Response(JSON.stringify(categories[index]), { headers });
}if (path.match(/^\/api\/categories\/[^\/]+$/) && request.method === 'DELETE') {
const id = path.split('/').pop();
const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];const filtered = categories.filter(c => c.id !== id);
await env.SUB_KV.put('categories', JSON.stringify(filtered));// 删除该分类下的所有商品
const products = await env.SUB_KV.get('products', { type: 'json' }) || [];
const filteredProducts = products.filter(p => p.categoryId !== id);
await env.SUB_KV.put('products', JSON.stringify(filteredProducts));return new Response(JSON.stringify({ success: true }), { headers });
}// 移动分类顺序
if (path === '/api/categories/move' && request.method === 'POST') {
const { id, direction } = await request.json();
let categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];// 确保所有分类都有 sort 字段,初始化缺失的字段
categories = categories.map((c, index) => ({
...c,
sort: c.sort !== undefined ? c.sort : index + 1
}));// 按 sort 排序
categories.sort((a, b) => (a.sort || 0) - (b.sort || 0));const index = categories.findIndex(c => c.id === id);
if (index === -1) {
return new Response(JSON.stringify({ success: false, message: '分类不存在' }), { headers });
}// 上移或下移
const newIndex = direction === 'up' ? index - 1 : index + 1;
if (newIndex < 0 || newIndex >= categories.length) {
return new Response(JSON.stringify({ success: false, message: '无法移动' }), { headers });
}// 交换 sort 值
const temp = categories[index].sort;
categories[index].sort = categories[newIndex].sort;
categories[newIndex].sort = temp;// 保存更新后的分类数据
await env.SUB_KV.put('categories', JSON.stringify(categories));return new Response(JSON.stringify({ success: true }), { headers });
}// 商品管理
if (path === '/api/products' && request.method === 'GET') {
const url = new URL(request.url);
const categoryId = url.searchParams.get('categoryId');let products = await env.SUB_KV.get('products', { type: 'json' }) || [];
const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];// 添加分类名称
products = products.map(p => {
const category = categories.find(c => c.id === p.categoryId);
return { ...p, categoryName: category ? category.name : '' };
});if (categoryId) {
products = products.filter(p => p.categoryId === categoryId);
}return new Response(JSON.stringify(products), { headers });
}if (path === '/api/products' && request.method === 'POST') {
const data = await request.json();
const products = await env.SUB_KV.get('products', { type: 'json' }) || [];const newProduct = {
id: Date.now().toString(),
name: data.name,
categoryId: data.categoryId,
registerDate: data.registerDate,
renewDate: data.renewDate,
hostUrl: data.hostUrl,
remark: data.remark,
createdAt: new Date().toISOString()
};products.push(newProduct);
await env.SUB_KV.put('products', JSON.stringify(products));return new Response(JSON.stringify(newProduct), { headers });
}if (path.match(/^\/api\/products\/[^\/]+$/) && request.method === 'GET') {
const id = path.split('/').pop();
const products = await env.SUB_KV.get('products', { type: 'json' }) || [];
const product = products.find(p => p.id === id);if (!product) {
return new Response(JSON.stringify({ error: '未找到' }), { status: 404, headers });
}return new Response(JSON.stringify(product), { headers });
}if (path.match(/^\/api\/products\/[^\/]+$/) && request.method === 'PUT') {
const id = path.split('/').pop();
const data = await request.json();
const products = await env.SUB_KV.get('products', { type: 'json' }) || [];const index = products.findIndex(p => p.id === id);
if (index === -1) {
return new Response(JSON.stringify({ error: '未找到' }), { status: 404, headers });
}products[index] = { ...products[index], ...data, updatedAt: new Date().toISOString() };
await env.SUB_KV.put('products', JSON.stringify(products));return new Response(JSON.stringify(products[index]), { headers });
}if (path.match(/^\/api\/products\/[^\/]+$/) && request.method === 'DELETE') {
const id = path.split('/').pop();
const products = await env.SUB_KV.get('products', { type: 'json' }) || [];const filtered = products.filter(p => p.id !== id);
await env.SUB_KV.put('products', JSON.stringify(filtered));return new Response(JSON.stringify({ success: true }), { headers });
}if (path === '/api/products/batch-delete' && request.method === 'POST') {
const { ids } = await request.json();
const products = await env.SUB_KV.get('products', { type: 'json' }) || [];const filtered = products.filter(p => !ids.includes(p.id));
await env.SUB_KV.put('products', JSON.stringify(filtered));return new Response(JSON.stringify({ success: true }), { headers });
}// 备份管理
if (path === '/api/backups' && request.method === 'GET') {
const backups = await env.SUB_KV.get('backups', { type: 'json' }) || [];
return new Response(JSON.stringify(backups), { headers });
}if (path === '/api/backups' && request.method === 'POST') {
const categories = await env.SUB_KV.get('categories', { type: 'json' }) || [];
const products = await env.SUB_KV.get('products', { type: 'json' }) || [];
const backups = await env.SUB_KV.get('backups', { type: 'json' }) || [];const backupId = Date.now().toString();
const newBackup = {
id: backupId,
createdAt: new Date().toISOString(),
categories: categories,
products: products,
categoriesCount: categories.length,
productsCount: products.length
};// 保存备份数据
await env.SUBKV.put(backup${backupId}, JSON.stringify(newBackup));// 添加备份记录
backups.push({
id: backupId,
createdAt: newBackup.createdAt,
categoriesCount: categories.length,
productsCount: products.length
});// 保留最多 5 个备份,删除最旧的
while (backups.length > 5) {
const oldest = backups.shift();
await env.SUBKV.delete(backup${oldest.id});
}await env.SUB_KV.put('backups', JSON.stringify(backups));
return new Response(JSON.stringify({ success: true, backupId }), { headers });
}if (path.match(/^\/api\/backups\/[^\/]+\/restore$/) && request.method === 'POST') {
const id = path.split('/')[3];
const backupData = await env.SUBKV.get(backup${id}, { type: 'json' });if (!backupData) {
return new Response(JSON.stringify({ success: false, message: '备份不存在' }), { headers });
}// 恢复数据
await env.SUB_KV.put('categories', JSON.stringify(backupData.categories || []));
await env.SUB_KV.put('products', JSON.stringify(backupData.products || []));return new Response(JSON.stringify({ success: true }), { headers });
}if (path.match(/^\/api\/backups\/[^\/]+$/) && request.method === 'DELETE') {
const id = path.split('/').pop();
const backups = await env.SUB_KV.get('backups', { type: 'json' }) || [];const filtered = backups.filter(b => b.id !== id);
await env.SUB_KV.put('backups', JSON.stringify(filtered));
await env.SUBKV.delete(backup${id});return new Response(JSON.stringify({ success: true }), { headers });
}return new Response(JSON.stringify({ error: '未知接口' }), { status: 404, headers });
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), { status: 500, headers });
}
}// 检查 IP 速率限制
async function checkRateLimit(env, ip) {
const key =loginattempt${ip};
const data = await env.SUB_KV.get(key, { type: 'json' });if (!data) {
return { allowed: true };
}// 检查是否处于限制期
if (data.locked && data.unlockedAt) {
if (Date.now() < data.unlockedAt) {
return {
allowed: false,
unlockedAt: data.unlockedAt
};
} else {
// 限制期已过,清除记录
await env.SUB_KV.delete(key);
return { allowed: true };
}
}return { allowed: true };
}// 记录登录尝试
async function recordLoginAttempt(env, ip) {
const key =loginattempt${ip};
const data = await env.SUB_KV.get(key, { type: 'json' }) || { attempts: 0, locked: false };data.attempts = (data.attempts || 0) + 1;
// 计算剩余尝试次数
const remainingAttempts = Math.max(0, 10 - data.attempts);// 达到 10 次失败,锁定 24 小时
if (data.attempts >= 10) {
data.locked = true;
data.unlockedAt = Date.now() + (24 60 60 * 1000); // 24 小时后解锁
}// 设置过期时间(24 小时)
await env.SUB_KV.put(key, JSON.stringify(data), { expirationTtl: 24 60 60 });return {
attempts: data.attempts,
remainingAttempts: remainingAttempts,
locked: data.locked
};
}