Cloudflare 版专属表白生成器网站,全套部署教程和源码

admin7小时前 16

专属表白生成器

定制专属告白,留存专属爱意

管理员后台入口


                                                                            表白网站管理后台

管理员登录

部署教程

# 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 后台绑定,把网站改成你自己的域名


本文链接:https://sweetly.cn/thread-126.htm
转载请注明:7小时前 于 一起微笑的博客 发表
上传的附件:
推荐阅读
最新回复 (1)
  • admin7小时前
    引用2
    演示地址:https://520.025.me/
返回