表白网站管理后台
管理员登录
部署教程
# Cloudflare 版表白网站
**Cloudflare Pages \+ D1 数据库** 架构,**全免费、全球加速、无需服务器**,一键部署即可使用。
## 改造说明
- ✅ 完全兼容原项目所有功能:表白创建、链接分享、回应、后台管理
- ✅ Cloudflare 原生 D1 数据库(兼容 SQL 语法,全球边缘存储)
- ✅ 无服务器架构,永久在线不关机
- ✅ 自动兼容原动态路由 `/abc123`,前端代码零修改
- ✅ 免费版 Cloudflare 即可完美运行,无任何费用
---
## 一、成品项目文件结构
```Plain Text
confession-site/
├── functions/
│ └── [[path]].js # 核心路由+API+数据库逻辑(Cloudflare Functions)
├── index.html # 前台表白页(与原项目完全一致)
├── admin.html # 管理员后台(与原项目完全一致)
├── wrangler.toml # Cloudflare 配置文件
└── package.json # 部署工具依赖
```
---
## 二、完整源码
### 1\. 核心函数文件 `functions/\[\[path\]\]\.js`
// 自定义管理员密码!部署前务必修改此处!
const ADMIN_PASSWORD = "123456";
// CORS 跨域头
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
// 生成6位唯一表白ID
function createRandomId() {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let id = "";
for(let i=0;i<6;i++){
id += chars[Math.floor(Math.random() * chars.length)];
}
return id;
}
// 初始化数据表
async function initTable(db) {
await db.prepare(`CREATE TABLE IF NOT EXISTS confession (
id TEXT PRIMARY KEY,
fromName TEXT NOT NULL,
toName TEXT NOT NULL,
content TEXT NOT NULL,
status TEXT DEFAULT 'pending',
reply TEXT DEFAULT '',
createTime INTEGER,
replyTime INTEGER
)`).run();
}
export async function onRequest(context) {
const { request, next, env } = context;
const url = new URL(request.url);
const path = url.pathname;
const db = env.DB;
await initTable(db);
// --------------------------
// 1. API 处理
// --------------------------
if (path.startsWith('/api/')) {
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
// 创建表白
if (path === '/api/create' && request.method === 'POST') {
const {from, to, content} = await request.json();
if(!from || !to || !content){
return new Response(JSON.stringify({error:"参数不全"}), { headers: corsHeaders });
}
async function insertData(){
const id = createRandomId();
const exist = await db.prepare("SELECT id FROM confession WHERE id = ?").bind(id).first();
if(exist) return insertData();
const createTime = Date.now();
await db.prepare(`INSERT INTO confession (id,fromName,toName,content,status,reply,createTime)
VALUES (?,?,?,?,?,?,?)`).bind(id,from,to,content,"pending","",createTime).run();
return id;
}
const id = await insertData();
return new Response(JSON.stringify({id}), { headers: corsHeaders });
}
// 查询单个表白
if (path === '/api/get' && request.method === 'GET') {
const id = url.searchParams.get('id');
const row = await db.prepare("SELECT * FROM confession WHERE id = ?").bind(id).first();
if(!row) {
return new Response(JSON.stringify({error:"表白不存在"}), { headers: corsHeaders });
}
return new Response(JSON.stringify(row), { headers: corsHeaders });
}
// 回应表白
if (path === '/api/reply' && request.method === 'POST') {
const {id, status, msg} = await request.json();
const replyTime = Date.now();
await db.prepare(`UPDATE confession SET status=?, reply=?, replyTime=? WHERE id=?`)
.bind(status, msg, replyTime, id).run();
return new Response(JSON.stringify({success:true}), { headers: corsHeaders });
}
// 管理员后台
if (path === '/api/admin' && request.method === 'POST') {
const {pwd, delId} = await request.json();
if(pwd !== ADMIN_PASSWORD) {
return new Response(JSON.stringify({error:"管理员密码错误"}), { headers: corsHeaders });
}
if(delId){
await db.prepare("DELETE FROM confession WHERE id = ?").bind(delId).run();
return new Response(JSON.stringify({success:true}), { headers: corsHeaders });
}
const list = await db.prepare("SELECT * FROM confession ORDER BY createTime DESC").all();
return new Response(JSON.stringify({list: list.results}), { headers: corsHeaders });
}
} catch (err) {
return new Response(JSON.stringify({error:"服务器异常"}), { status:500, headers: corsHeaders });
}
}
// --------------------------
// 2. 动态路由 /abc123 → 直接走静态文件(不手动 fetch)
// --------------------------
const purePath = path.slice(1);
if (purePath.length === 6 && !purePath.includes('.')) {
// 直接放行,让 Pages 自动返回 index.html,不再手动 fetch
return next();
}
// --------------------------
// 3. 其他静态资源
// --------------------------
return next();
}
```
### 2\. 前台页面 `index\.html`
> 与原项目完全一致,无需修改,直接使用
>
>
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
专属表白页面
<style>
/* 内置完整样式,彻底移除外网CDN,纯离线运行 */
:root{--love:#ff6378;}
*{margin:0;padding:0;box-sizing:border-box;}
body{background:linear-gradient(to bottom right,#fef2f2,#ffffff);min-height:100vh;font-family:system-ui,-apple-system,sans-serif;}
.text-love{color:var(--love);}
.bg-love{background-color:var(--love);}
.bg-love\/10{background-color:rgba(255,99,120,0.1);}
.border-love\/20{border-color:rgba(255,99,120,0.2);}
.hover\:bg-love\/90:hover{background-color:rgba(255,99,120,0.9);}
.focus\:ring-love\/50:focus{box-shadow:0 0 0 2px rgba(255,99,120,0.5);outline:none;}
.container{width:100%;margin:0 auto;padding:0 1rem;}
.max-w-lg{max-width:32rem;}
.max-w-2xl{max-width:42rem;}
.px-4{padding-left:1rem;padding-right:1rem;}
.py-10{padding-top:2.5rem;padding-bottom:2.5rem;}
.py-3{padding-top:0.75rem;padding-bottom:0.75rem;}
.p-6{padding:1.5rem;}
.p-8{padding:2rem;}
.p-5{padding:1.25rem;}
.p-4{padding:1rem;}
.mt-2{margin-top:0.5rem;}
.mt-3{margin-top:0.75rem;}
.mt-4{margin-top:1rem;}
.mt-6{margin-top:1.5rem;}
.mt-8{margin-top:2rem;}
.mb-2{margin-bottom:0.5rem;}
.mb-3{margin-bottom:0.75rem;}
.mb-4{margin-bottom:1rem;}
.mb-6{margin-bottom:1.5rem;}
.mb-8{margin-bottom:2rem;}
.mx-auto{margin-left:auto;margin-right:auto;}
.text-center{text-align:center;}
.text-left{text-align:left;}
.text-sm{font-size:0.875rem;}
.text-xs{font-size:0.75rem;}
.text-lg{font-size:1.125rem;}
.text-xl{font-size:1.25rem;}
.text-2xl{font-size:1.5rem;}
.text-3xl{font-size:1.875rem;}
.text-5xl{font-size:3rem;}
.font-medium{font-weight:500;}
.font-bold{font-weight:700;}
.text-gray-400{color:#9ca3af;}
.text-gray-500{color:#6b7280;}
.text-gray-600{color:#4b5563;}
.text-gray-700{color:#374151;}
.text-gray-800{color:#1f2937;}
.text-green-500{color:#22c55e;}
.text-green-600{color:#16a34a;}
.text-red-500{color:#ef4444;}
.bg-white{background:#fff;}
.bg-gray-50{background:#f9fafb;}
.bg-gray-100{background:#f3f4f6;}
.bg-green-50{background:#f0fdf4;}
.rounded-xl{border-radius:0.75rem;}
.rounded-2xl{border-radius:1rem;}
.shadow{box-shadow:0 1px 3px #0000001a;}
.shadow-lg{box-shadow:0 10px 15px -3px #0000001a;}
.border{border-width:1px;border-style:solid;}
.border-gray-200{border-color:#e5e7eb;}
.border-green-200{border-color:#bbf7d0;}
.grid{display:grid;}
.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr));}
.gap-4{gap:1rem;}
.hidden{display:none;}
.block{display:block;}
.inline-block{display:inline-block;}
.w-full{width:100%;}
.w-14{width:3.5rem;}
.w-16{width:4rem;}
.h-14{height:3.5rem;}
.h-16{height:4rem;}
.underline{text-decoration:underline;}
.break-all{word-break:break-all;}
.leading-relaxed{line-height:1.625;}
.space-y-4 > * + *{margin-top:1rem;}
.hover\:bg-gray-200:hover{background:#e5e7eb;}
.hover\:bg-rose-600:hover{background:#e11d48;}
.hover\:text-love:hover{color:var(--love);}
.hover\:text-red-700:hover{color:#b91c1c;}
.fixed{position:fixed;}
.inset-0{top:0;right:0;bottom:0;left:0;}
.pointer-events-none{pointer-events:none;}
/* 浪漫动画样式 */
.heart{position:fixed;top:-30px;color:#ff6378;font-size:18px;animation:heartFall linear forwards;z-index:-1;opacity:0.8}
@keyframes heartFall{to{transform:translateY(100vh) rotate(720deg);opacity:0}}
.fade-in{animation:fadeIn 0.6s ease-out}
@keyframes fadeIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
.pulse{animation:pulse 2s infinite}
@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.03)}}
</style>
</head>
<body>
<div id="hearts" class="fixed inset-0 pointer-events-none"></div>
<div class="container mx-auto px-4 py-10 max-w-lg fade-in">
<div id="create-box">
<svg class="w-16 h-16 text-love mx-auto pulse" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
<h1 class="text-3xl font-bold text-gray-800 mb-3 mt-4">专属表白生成器</h1>
<p class="text-gray-500 mb-8">定制专属告白,留存专属爱意</p>
<form id="createForm" class="bg-white rounded-2xl shadow-lg p-6 text-left space-y-4">
<div>
<label class="block text-gray-700 mb-1 text-sm">你的名字(表白人)</label>
<input type="text" id="fromName" required class="w-full px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-love/50" placeholder="请输入你的昵称">
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">对方名字(被表白人)</label>
<input type="text" id="toName" required class="w-full px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-love/50" placeholder="请输入对方昵称">
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">表白文案</label>
<textarea id="content" required rows="4" class="w-full px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-love/50" placeholder="写下专属告白文案..."></textarea>
</div>
<button type="submit" class="w-full bg-love text-white py-3 rounded-xl font-medium hover:bg-love/90 transition-all pulse">
生成专属表白链接
</button>
</form>
<div id="successTip" class="hidden mt-6 bg-green-50 border border-green-200 rounded-xl p-4 text-left">
<p class="text-green-700 font-medium mb-2">✅ 表白链接生成成功!</p>
<a id="linkUrl" target="_blank" class="text-love break-all underline text-sm"></a>
<p class="text-gray-400 text-xs mt-2">发送给对方,即可接收你的告白</p>
</div>
<a href="/admin.html" class="inline-block mt-8 text-sm text-gray-400 hover:text-love">管理员后台入口</a>
</div>
<div id="detail-box"></div>
</div>
<script>
// 爱心飘落动画
function createHeart(){
const container = document.getElementById('hearts');
const heart = document.createElement('div');
heart.className = 'heart';
heart.innerText = ['❤️','💕','💗'][Math.floor(Math.random()*3)];
heart.style.left = Math.random()*100 + 'vw';
heart.style.fontSize = (Math.random()*12+14)+'px';
heart.style.animationDuration = (Math.random()*3+4)+'s';
container.appendChild(heart);
setTimeout(()=>heart.remove(),7000)
}
setInterval(createHeart,400);
// 全局DOM变量
let createBox, detailBox;
// 页面完全加载完毕后再执行所有逻辑【彻底修复异步竞态BUG】
window.onload = function(){
createBox = document.getElementById('create-box');
detailBox = document.getElementById('detail-box');
const path = window.location.pathname.slice(1);
// 判断是否为表白详情页
if(path && path !== 'admin.html'){
createBox.classList.add('hidden');
loadDetail(path);
}
}
// 生成表白
document.getElementById('createForm').onsubmit = async(e)=>{
e.preventDefault();
const from = document.getElementById('fromName').value.trim();
const to = document.getElementById('toName').value.trim();
const content = document.getElementById('content').value.trim();
if(!from||!to||!content) return alert('请填写完整信息');
const res = await fetch('/api/create',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({from,to,content})
});
const data = await res.json();
if(data.id){
const link = `${location.origin}/${data.id}`;
document.getElementById('linkUrl').href = link;
document.getElementById('linkUrl').innerText = link;
document.getElementById('successTip').classList.remove('hidden');
}else{
alert(data.error||'生成失败,请重试')
}
}
// 加载表白详情【终极修复undefined,三重容错兜底】
async function loadDetail(id){
detailBox.innerHTML = '<p class="text-center text-gray-500 py-20">加载中...</p>';
detailBox.classList.remove('hidden');
try {
const res = await fetch(`/api/get?id=${id}`);
const data = await res.json();
// 数据不存在
if(!data || data.error){
detailBox.innerHTML = `<div class="text-center py-20"><p class="text-gray-500 mt-4 text-lg">表白链接不存在或已失效</p></div>`;
return;
}
// 【三重兜底!百分百杜绝undefined】
// 无论接口返回什么,都不会显示undefined,空值自动替换为匿名
const fromName = (data?.fromName ?? '匿名');
const toName = (data?.toName ?? '匿名');
const content = (data?.content ?? '暂无表白文案');
const reply = (data?.reply ?? '');
// 未回应
if(data.status === 'pending'){
detailBox.innerHTML = `
<div class="text-center fade-in">
<svg class="w-14 h-14 text-love mx-auto pulse mb-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
<h2 class="text-2xl font-bold text-gray-800 mb-2">${fromName} 向你表白啦</h2>
<div class="bg-white shadow-lg rounded-2xl p-6 my-6 text-left">
<p class="text-gray-700 leading-relaxed">${content}</p>
</div>
<div class="grid grid-cols-2 gap-4">
<button id="refuseBtn" class="py-3 bg-gray-100 rounded-xl text-gray-600 hover:bg-gray-200 transition-all">拒绝</button>
<button id="agreeBtn" class="py-3 bg-love text-white rounded-xl hover:bg-love/90 transition-all pulse">我愿意</button>
</div>
</div>`;
document.getElementById('agreeBtn').onclick = async()=>{
const msg = prompt('写下你的专属回应:','我也喜欢你!');
if(msg === null) return;
await replyAct(id,'agree',msg);
}
document.getElementById('refuseBtn').onclick = async()=>{
if(!confirm('确定拒绝本次告白?')) return;
await replyAct(id,'refuse','');
}
}
// 表白成功
else if(data.status === 'agree'){
detailBox.innerHTML = `
<div class="text-center fade-in">
<div class="bg-love/10 border border-love/20 rounded-2xl p-8 mb-6">
<svg class="w-16 h-16 text-love mx-auto mb-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
<h2 class="text-2xl font-bold text-love mb-2">🎉 表白成功 · 爱意永久留存</h2>
<p>${fromName} ❤️ ${toName}</p>
</div>
<div class="bg-white shadow rounded-xl p-4 mb-4 text-left">
<p class="text-xs text-gray-400">表白文案</p>
<p class="text-gray-700 mt-1">${content}</p>
</div>
<div class="bg-green-50 border border-green-200 rounded-xl p-4 text-left">
<p class="text-xs text-green-500">对方永久祝福语</p>
<p class="text-gray-800 font-medium mt-1">${reply}</p>
</div>
</div>`;
}
// 表白失败
else{
detailBox.innerHTML = `
<div class="text-center py-10 fade-in">
<p class="text-5xl mb-4">💔</p>
<h2 class="text-xl text-gray-500">本次表白已被拒绝</h2>
</div>`;
}
} catch (err) {
// 异常兜底,防止接口报错导致页面出错
detailBox.innerHTML = `<div class="text-center py-20"><p class="text-gray-500 mt-4 text-lg">加载失败,请刷新重试</p></div>`;
}
}
// 提交回应
async function replyAct(id,status,msg){
try {
const res = await fetch('/api/reply',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({id,status,msg})
});
const data = await res.json();
if(data.success) location.reload();
else alert('操作失败,请重试')
} catch (err) {
alert('网络异常,请重试')
}
}
</script>
</body>
</html>
```
### 3\. 管理员后台 `admin\.html`
> 与原项目完全一致,无需修改
>
>
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
表白网站管理后台
<style>
:root{--love:#ff6378;}
*{margin:0;padding:0;box-sizing:border-box;font-family:system-ui,-apple-system,sans-serif;}
body{background:#f9fafb;min-height:100vh;}
.container{width:100%;margin:0 auto;padding:0 1rem;}
.max-w-2xl{max-width:42rem;}
.px-4{padding-left:1rem;padding-right:1rem;}
.py-10{padding-top:2.5rem;padding-bottom:2.5rem;}
.p-8{padding:2rem;}
.p-5{padding:1.25rem;}
.mt-4{margin-top:1rem;}
.mt-3{margin-top:0.75rem;}
.mb-2{margin-bottom:0.5rem;}
.mb-6{margin-bottom:1.5rem;}
.mx-auto{margin-left:auto;margin-right:auto;}
.text-center{text-align:center;}
.text-sm{font-size:0.875rem;}
.text-xs{font-size:0.75rem;}
.text-xl{font-size:1.25rem;}
.text-2xl{font-size:1.5rem;}
.font-medium{font-weight:500;}
.font-bold{font-weight:700;}
.text-gray-400{color:#9ca3af;}
.text-gray-500{color:#6b7280;}
.text-gray-600{color:#4b5563;}
.text-gray-800{color:#1f2937;}
.text-green-500{color:#22c55e;}
.text-green-600{color:#16a34a;}
.text-red-500{color:#ef4444;}
.hover\:text-red-700:hover{color:#b91c1c;}
.bg-white{background:#fff;}
.bg-rose-500{background:#f43f5e;}
.hover\:bg-rose-600:hover{background:#e11d48;}
.rounded-xl{border-radius:0.75rem;}
.rounded-2xl{border-radius:1rem;}
.shadow-lg{box-shadow:0 10px 15px -3px #0000001a;}
.shadow{box-shadow:0 1px 3px #0000001a;}
.border{border-width:1px;border-style:solid;border-color:#e5e7eb;}
.focus\:ring-rose-400:focus{box-shadow:0 0 0 2px #fb7185;outline:none;}
.w-full{width:100%;}
.text-white{color:#fff;}
.hidden{display:none;}
.space-y-4 > * + *{margin-top:1rem;}
.transition-all{transition:all 0.2s ease;}
</style>
</head>
<body>
<div class="container mx-auto px-4 py-10 max-w-2xl">
<div id="loginBox" class="bg-white rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-bold text-center text-gray-800 mb-6">管理员登录</h2>
<input type="password" id="pwd" placeholder="请输入管理员密码" class="w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-rose-400">
<button id="loginBtn" class="w-full mt-4 bg-rose-500 text-white py-3 rounded-xl hover:bg-rose-600 transition-all">登录后台</button>
</div>
<div id="adminBox">
<h2 class="text-2xl font-bold text-gray-800 mb-6">表白数据管理中心</h2>
<div id="listBox"></div>
</div>
</div>
<script>
const loginBox = document.getElementById('loginBox');
const adminBox = document.getElementById('adminBox');
const listBox = document.getElementById('listBox');
let adminPwd = "";
document.getElementById('loginBtn').onclick = async()=>{
const pwd = document.getElementById('pwd').value;
adminPwd = pwd;
if(!pwd) return alert('请输入密码');
const res = await fetch('/api/admin',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({pwd})
});
const data = await res.json();
if(data.error) return alert(data.error);
loginBox.classList.add('hidden');
adminBox.classList.remove('hidden');
renderList(data.list);
}
function renderList(list){
if(list.length === 0){
listBox.innerHTML = '<p class="text-center text-gray-400 py-10">暂无表白数据</p>';
return;
}
listBox.innerHTML = '';
list.forEach(item=>{
const statusText = item.status==='pending'?'未回应':item.status==='agree'?'✅ 表白成功':'💔 表白失败';
const statusColor = item.status==='pending'?'text-gray-500':item.status==='agree'?'text-green-500':'text-red-500';
listBox.innerHTML += `
<div class="bg-white rounded-xl shadow p-5">
<div class="flex justify-between items-center mb-2">
<p>${item.fromName} → ${item.toName}</p>
<span class="text-sm ${statusColor}">${statusText}</span>
</div>
<p class="text-sm text-gray-600 mb-2">${item.content}</p>
${item.reply ? `<p class="text-xs text-green-600">对方回应:${item.reply}</p>` : ''}
<p class="text-xs text-gray-400 mt-2">链接ID:${item.id}</p>
<button data-id="${item.id}" class="del-btn mt-3 text-xs text-red-500 hover:text-red-700"> 删除本条记录</button>
</div>`
})
document.querySelectorAll('.del-btn').forEach(btn=>{
btn.onclick = async()=>{
if(!confirm('确定删除该表白记录?')) return;
const id = btn.dataset.id;
const res = await fetch('/api/admin',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({pwd:adminPwd,delId:id})
});
const data = await res.json();
if(data.success){
alert('删除成功');
location.reload();
}
}
})
}
</script>
</body>
</html>
```
### 4\. Cloudflare 配置文件 `wrangler\.toml`
```toml
name = "confession-site"
compatibility_date = "2024-05-01"
pages_build_output_dir = "."
# D1 数据库绑定,创建完数据库后会自动填充ID
[[d1_databases]]
binding = "DB"
database_name = "confession-db"
database_id = "" # 执行创建数据库命令后,这里会自动填充
```
### 5\. 依赖配置 `package\.json`
```json
{
"name": "confession-site",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "wrangler pages dev .",
"deploy": "wrangler pages deploy ."
},
"devDependencies": {
"wrangler": "^3.50.0"
}
}
```
---
## 三、一键部署教程(新手傻瓜式)
### 前置准备
1. 注册一个 [Cloudflare 账号](https://dash.cloudflare.com/)(免费)
2. 电脑安装 Node\.js(官网下载默认安装即可,[https://nodejs\.org/](https://nodejs.org/))
---
### 部署步骤
#### 1\. 下载项目文件
把上面的所有文件,按照项目结构,在本地新建一个文件夹 `confession\-site`,把所有文件放进去。
#### 2\. 修改管理员密码
打开 `functions/\[\[path\]\]\.js`,找到第一行的 `ADMIN\_PASSWORD`,把默认的 `123456` 改成你自己的密码!
```javascript
const ADMIN_PASSWORD = "你的新密码"; // 务必修改!
```
#### 3\. 安装依赖
打开项目文件夹,在地址栏输入 `cmd` 回车打开命令行,执行:
```bash
npm install
```
#### 4\. 登录 Cloudflare
命令行执行,会自动打开浏览器登录你的 Cloudflare 账号:
```bash
npx wrangler login
```
#### 5\. 创建 D1 数据库
命令行执行,创建专属的数据库:
```bash
npx wrangler d1 create confession-db
```
执行完之后,它会输出类似下面的内容:
```Plain Text
✅ Successfully created DB 'confession-db'
[[d1_databases]]
binding = "DB"
database_name = "confession-db"
database_id = "xxxx-xxxx-xxxx-xxxx-xxxx"
```
把这一段内容,**替换掉你项目里 ****`wrangler\.toml`**** 最后那几行**,也就是把自动生成的 database\_id 填进去。
#### 6\. 部署到 Cloudflare
最后执行部署命令,等待完成即可:
```bash
npm run deploy
```
部署完成后,它会给你一个类似 `https://confession\-site\.xxx\.pages\.dev` 的地址,这就是你的网站地址了!
---
## 四、使用说明
- 前台地址:`https://你的域名/` (创建表白、生成链接)
- 后台地址:`https://你的域名/admin\.html` (用你设置的管理员密码登录)
- 表白链接:生成的 `https://你的域名/abc123`,直接发给对方即可
---
## 五、注意事项
1. **免费额度**:Cloudflare 免费版完全够用,D1 免费有 5GB 存储、10 万次读写 / 天,Pages 免费有 10 万次函数调用 / 天,足够小网站使用
2. **自动建表**:第一次访问网站的时候,会自动创建数据库表,不用手动执行 SQL
3. **永久在线**:部署完成后,网站就永久在线了,不用开电脑,不用服务器
4. **自定义域名**:如果你有自己的域名,可以在 Cloudflare Pages 后台绑定,把网站改成你自己的域名转载请注明:7小时前 于 一起微笑的博客 发表
