前言

在solitude主题有新的扩展,就是关于页面,而关于页面有打赏名单。

但是每次都需要重新构建,这就很麻烦,所以不如让它读取文件从而直接显示。

教程

首先就是在themes\solitude\layout\includes\widgets\page\about\award-dynamic.pug文件里修改代码,将如下代码放到其中

- award = site.data.about.award

- var sum = 0
if site.data.about.award && site.data.about.award.enable
    .author-content
        .author-content-item.single.reward
            .author-content-item-tips= _p('award.thanks')
            span.author-content-item-title= _p('award.title')
            .author-content-item-description
                != award.description
            .reward-list-all#reward-list-container
                .reward-loading
                    | 正在加载赞助数据...
                if theme.post.award.enable
                    .post-reward
                        .post-reward(onclick="AddRewardMask()")
                        .reward-button(title=_p('award.tipping'))
                            i.solitude.fas.fa-heart
                            = _p('award.tipping')
                        .reward-main
                            ul.reward-all
                                span.reward-title= theme.post.award.title
                                ul.reward-group
                                    - var rewards = theme.post.award.list
                                    each reward in rewards
                                        li.reward-item
                                            a(href=url_for(reward.url))
                                                img.post-qr-code-img(alt=reward.name, src=reward.qcode, style="border-color:" + reward.color)
                                            .post-qr-code-desc= reward.name
            .reward-list-tips#reward-tips
                p= award.tips.replace('{sum}', '0.00')

    script.
        // 获取赞助数据并渲染
        async function loadRewardData() {
            const container = document.getElementById('reward-list-container');
            const tipsContainer = document.getElementById('reward-tips');
            const awardTips = !{JSON.stringify(award.tips)};
            
            try {
                const response = await fetch('https://你绑定的域名/all');
                const data = await response.json();
                
                if (data && data.length > 0) {
                    // 按时间排序(最新的在前)
                    const sortedData = data.sort((a, b) => new Date(b.time) - new Date(a.time));
                    
                    // 计算总金额
                    let totalSum = 0;
                    
                    // 清空容器并重新渲染
                    const rewardListHtml = sortedData.map(reward => {
                        const amount = parseFloat(reward.amount || 0);
                        totalSum += amount;
                        
                        // 根据支付方式设置图标
                        let icon = 'fab fa-alipay';
                        if (reward.method === '微信') {
                            icon = 'fab fa-weixin';
                        } else if (reward.method === '支付宝') {
                            icon = 'fab fa-alipay';
                        }
                        
                                                 return `
                             <div class="reward-list-item">
                                 <div class="reward-list-item-name">${reward.person || '匿名'}</div>
                                 <div class="reward-list-bottom-group">
                                     <div class="reward-list-item-money" style="background-color: ${reward.color || '#09bb07'}">
                                         <i class="solitude ${icon}"></i>
                                         ¥ ${amount}
                                     </div>
                                     <time class="datetime reward-list-item-time" datetime="${reward.time}"></time>
                                 </div>
                             </div>
                         `;
                    }).join('');
                    
                    // 保存现有的打赏按钮
                    const existingPostReward = container.querySelector('.post-reward');
                    
                    // 先清空容器内容,但保留打赏按钮
                    const loadingDiv = container.querySelector('.reward-loading');
                    if (loadingDiv) {
                        loadingDiv.remove();
                    }
                    
                    // 清空所有赞助列表项(避免重复)
                    const existingItems = container.querySelectorAll('.reward-list-item');
                    existingItems.forEach(item => item.remove());
                    
                    // 添加赞助列表项到容器中(在打赏按钮之前)
                    const tempDiv = document.createElement('div');
                    tempDiv.innerHTML = rewardListHtml;
                    while (tempDiv.firstChild) {
                        if (existingPostReward) {
                            container.insertBefore(tempDiv.firstChild, existingPostReward);
                        } else {
                            container.appendChild(tempDiv.firstChild);
                        }
                    }
                    
                    // 更新总金额显示
                    tipsContainer.innerHTML = `<p>${awardTips.replace('{sum}', totalSum.toFixed(2))}</p>`;
                } else {
                    container.innerHTML = '<div class="reward-loading">暂无赞助数据</div>';
                }
            } catch (error) {
                console.error('加载赞助数据失败:', error);
                container.innerHTML = '<div class="reward-loading">加载赞助数据失败,请稍后重试</div>';
            }
        }
        
        // 页面加载完成后获取数据
        if (typeof window !== 'undefined') {
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', loadRewardData);
            } else {
                loadRewardData();
            }
            
            // 避免重复绑定PJAX事件监听器
            if (!window.rewardDataPjaxListenerAdded) {
                document.addEventListener('pjax:complete', loadRewardData);
                window.rewardDataPjaxListenerAdded = true;
            }
        } 

最后就是去cloudflare添加works代码

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const path = url.pathname;
    
    // 检查KV是否配置
    if (!env.TIP_STORAGE) {
      return new Response(showErrorPage(
        "系统配置错误", 
        "KV存储未配置,请在Worker设置中绑定KV命名空间到'TIP_STORAGE'变量"
      ), {
        status: 500,
        headers: { 'Content-Type': 'text/html; charset=utf-8' }
      });
    }
    
    try {
      switch (path) {
        case '/':
          // 判断是否是操作后的重定向请求
          const isAfterOperation = request.headers.get('Referer')?.includes('/add-tip') || 
                                 request.headers.get('Referer')?.includes('/delete-tip');
          return await handleHome(request, env, isAfterOperation);
        case '/login':
          return await handleLogin(request, env);
        case '/add-tip':
          return await handleAddTip(request, env);
        case '/delete-tip':
          return await handleDeleteTip(request, env);
        case '/all':
          return await handleAllTips(request, env);
        default:
          return new Response(showErrorPage("页面未找到", "请求的路径不存在"), {
            status: 404,
            headers: { 'Content-Type': 'text/html; charset=utf-8' }
          });
      }
    } catch (err) {
      return new Response(showErrorPage("操作失败", err.message), {
        status: 500,
        headers: { 'Content-Type': 'text/html; charset=utf-8' }
      });
    }
  }
};

// 验证是否已登录
async function isAuthenticated(request) {
  const cookies = request.headers.get('Cookie') || '';
  return cookies.includes('authenticated=true');
}

// 处理首页请求
async function handleHome(request, env, forceRefresh = false) {
  const authenticated = await isAuthenticated(request);
  
  if (!authenticated) {
    return new Response(loginPageHtml(), {
      headers: { 'Content-Type': 'text/html; charset=utf-8' }
    });
  }
  
  let tips;
  if (forceRefresh) {
    // 操作后强制刷新策略
    tips = await getLatestTips(env);
  } else {
    // 普通请求使用常规策略
    tips = await getTips(env);
  }
  
  return new Response(tipManagementPageHtml(tips), {
    headers: { 
      'Content-Type': 'text/html; charset=utf-8',
      'Cache-Control': 'no-store, no-cache, must-revalidate',
      'Pragma': 'no-cache',
      'Expires': '0'
    }
  });
}

// 处理登录请求
async function handleLogin(request, env) {
  // 处理登出
  const url = new URL(request.url);
  if (url.searchParams.get('logout')) {
    return new Response('', {
      status: 302,
      headers: {
        'Location': '/',
        'Set-Cookie': 'authenticated=true; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0',
        'Content-Type': 'text/html; charset=utf-8'
      }
    });
  }
  
  if (request.method !== 'POST') {
    return new Response(showErrorPage("方法不允许", "请使用POST方法登录"), {
      status: 405,
      headers: { 'Content-Type': 'text/html; charset=utf-8' }
    });
  }
  
  const formData = await request.formData();
  const password = formData.get('password');
  const correctPassword = env.TIP_PASSWORD || 'defaultpassword';
  
  if (password === correctPassword) {
    return new Response('', {
      status: 302,
      headers: {
        'Location': '/',
        'Set-Cookie': 'authenticated=true; HttpOnly; Secure; SameSite=Strict; Path=/',
        'Content-Type': 'text/html; charset=utf-8'
      }
    });
  } else {
    return new Response(loginPageHtml('密码错误,请重试'), {
      headers: { 'Content-Type': 'text/html; charset=utf-8' }
    });
  }
}

// 处理 /all 路由 - 返回所有打赏信息的 JSON
async function handleAllTips(request, env) {
  // 移除登录验证,允许公开访问
  const tips = await getLatestTips(env);
  
  // 简化 JSON 显示,只包含需要的字段
  const simplifiedTips = tips.map(tip => ({
    person: tip.person,
    amount: tip.amount,
    time: tip.time,
    method: tip.method,
    color: tip.color
  }));
  
  return new Response(JSON.stringify(simplifiedTips, null, 2), {
    headers: { 
      'Content-Type': 'application/json; charset=utf-8',
      'Cache-Control': 'no-store, no-cache, must-revalidate',
      'Pragma': 'no-cache',
      'Expires': '0'
    }
  });
}

// 获取当前时间(上海时区)作为默认值,格式化为 datetime-local 输入框需要的格式
function getCurrentShanghaiTime() {
  const now = new Date();
  const shanghaiTime = new Date(now.toLocaleString("en-US", {timeZone: "Asia/Shanghai"}));
  
  // 格式化为 YYYY-MM-DDTHH:mm 格式,用于 datetime-local 输入框
  const year = shanghaiTime.getFullYear();
  const month = String(shanghaiTime.getMonth() + 1).padStart(2, '0');
  const day = String(shanghaiTime.getDate()).padStart(2, '0');
  const hours = String(shanghaiTime.getHours()).padStart(2, '0');
  const minutes = String(shanghaiTime.getMinutes()).padStart(2, '0');
  
  return `${year}-${month}-${day}T${hours}:${minutes}`;
}

// 将 datetime-local 格式转换为显示格式
function formatTimeForDisplay(datetimeLocal) {
  if (!datetimeLocal) return '';
  
  // 将 YYYY-MM-DDTHH:mm 转换为 YYYY-MM-DD HH:mm:ss
  const [date, time] = datetimeLocal.split('T');
  return `${date} ${time}:00`;
}

// 将显示格式转换为 datetime-local 格式
function formatTimeForInput(displayTime) {
  if (!displayTime) return '';
  
  // 将 YYYY-MM-DD HH:mm:ss 转换为 YYYY-MM-DDTHH:mm
  return displayTime.replace(' ', 'T').replace(':00', '');
}

// 常规获取打赏记录
async function getTips(env) {
  const keys = await env.TIP_STORAGE.list({ limit: 1000, cacheTtl: 60 });
  const tips = [];
  
  for (const key of keys.keys) {
    const tip = await env.TIP_STORAGE.get(key.name, { type: 'json', cacheTtl: 60 });
    if (tip) {
      tips.push({ id: key.name, ...tip });
    }
  }
  
  return tips.sort((a, b) => b.createdAt - a.createdAt);
}

// 强制获取最新数据的专用方法
async function getLatestTips(env) {
  // 1. 先清除可能的本地缓存
  // 2. 获取最新的键列表
  const keys = await env.TIP_STORAGE.list({ limit: 1000, cacheTtl: 60 });
  
  // 3. 等待KV完成同步
  await new Promise(resolve => setTimeout(resolve, 250));
  
  const tips = [];
  // 4. 逐个获取并验证最新数据
  for (const key of keys.keys) {
    let tip = null;
    // 最多尝试3次确保获取最新数据
    for (let i = 0; i < 3; i++) {
      tip = await env.TIP_STORAGE.get(key.name, { type: 'json', cacheTtl: 60 });
      if (tip) break;
      if (i < 2) await new Promise(resolve => setTimeout(resolve, 150 * (i + 1)));
    }
    if (tip) {
      tips.push({ id: key.name, ...tip });
    }
  }
  
  return tips.sort((a, b) => b.createdAt - a.createdAt);
}

// 处理添加打赏记录
async function handleAddTip(request, env) {
  const authenticated = await isAuthenticated(request);
  if (!authenticated) {
    return new Response(showErrorPage("未授权", "请先登录"), {
      status: 401,
      headers: { 'Content-Type': 'text/html; charset=utf-8' }
    });
  }
  
  if (request.method !== 'POST') {
    return new Response(showErrorPage("方法不允许", "请使用POST方法添加记录"), {
      status: 405,
      headers: { 'Content-Type': 'text/html; charset=utf-8' }
    });
  }
  
  const formData = await request.formData();
  const id = crypto.randomUUID();
  
  const tip = {
    amount: formData.get('amount'),
    person: formData.get('person'),
    method: formData.get('method'),
    color: formData.get('color'),
    time: formatTimeForDisplay(formData.get('time')) || '',
    createdAt: Date.now()
  };
  
  // 保存数据并等待确认
  await env.TIP_STORAGE.put(id, JSON.stringify(tip));
  
  // 关键:等待足够时间确保KV已完成写入
  await new Promise(resolve => setTimeout(resolve, 400));
  
  // 直接重定向到首页,不添加任何参数
  return new Response('', {
    status: 302,
    headers: { 
      'Location': '/',
      'Cache-Control': 'no-store',
      'Pragma': 'no-cache'
    }
  });
}

// 处理删除打赏记录
async function handleDeleteTip(request, env) {
  const authenticated = await isAuthenticated(request);
  if (!authenticated) {
    return new Response(showErrorPage("未授权", "请先登录"), {
      status: 401,
      headers: { 'Content-Type': 'text/html; charset=utf-8' }
    });
  }
  
  const url = new URL(request.url);
  const id = url.searchParams.get('id');
  
  if (!id) {
    return new Response(showErrorPage("参数错误", "缺少记录ID"), {
      status: 400,
      headers: { 'Content-Type': 'text/html; charset=utf-8' }
    });
  }
  
  // 执行删除操作
  await env.TIP_STORAGE.delete(id);
  
  // 关键:等待足够时间确保KV已完成删除
  await new Promise(resolve => setTimeout(resolve, 400));
  
  // 直接重定向到首页,不添加任何参数
  return new Response('', {
    status: 302,
    headers: { 
      'Location': '/',
      'Cache-Control': 'no-store',
      'Pragma': 'no-cache'
    }
  });
}

// 错误页面
function showErrorPage(title, message) {
  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}</title>
      <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        .error { color: #d32f2f; margin: 20px 0; }
        .back { margin-top: 20px; }
        a { color: #1976d2; text-decoration: none; }
        a:hover { text-decoration: underline; }
      </style>
    </head>
    <body>
      <h1>${title}</h1>
      <div class="error">${message}</div>
      <div class="back"><a href="/">返回首页</a></div>
    </body>
    </html>
  `;
}

// 登录页面HTML
function loginPageHtml(errorMessage = '') {
  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>
        body { font-family: Arial, sans-serif; background: #f5f5f5; margin: 0; padding: 20px; }
        .login-container { max-width: 400px; margin: 100px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { text-align: center; color: #333; margin-bottom: 30px; }
        .form-group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 5px; color: #555; }
        input[type="password"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
        button { width: 100%; padding: 12px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
        button:hover { background: #1565c0; }
        .error { color: #d32f2f; margin-bottom: 20px; text-align: center; }
      </style>
    </head>
    <body>
      <div class="login-container">
        <h1>打赏管理系统</h1>
        ${errorMessage ? `<div class="error">${errorMessage}</div>` : ''}
        <form method="POST" action="/login">
          <div class="form-group">
            <label for="password">密码:</label>
            <input type="password" id="password" name="password" required>
          </div>
          <button type="submit">登录</button>
        </form>
      </div>
    </body>
    </html>
  `;
}

// 打赏管理页面HTML
function tipManagementPageHtml(tips) {
  const currentTime = getCurrentShanghaiTime();
  
  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>
        body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
        .container { max-width: 1200px; margin: 0 auto; }
        .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
        h1 { color: #333; margin: 0; }
        .logout { color: #d32f2f; text-decoration: none; }
        .logout:hover { text-decoration: underline; }
        .form-section { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
        .form-group { display: flex; flex-direction: column; }
        label { margin-bottom: 5px; color: #555; font-weight: bold; }
        input, select { padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
        button { padding: 12px 24px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
        button:hover { background: #1565c0; }
        .tips-section { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        .tips-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
        .api-link { color: #1976d2; text-decoration: none; }
        .api-link:hover { text-decoration: underline; }
        .tips-table { width: 100%; border-collapse: collapse; }
        .tips-table th, .tips-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
        .tips-table th { background: #f8f9fa; font-weight: bold; }
        .delete-btn { color: #d32f2f; text-decoration: none; }
        .delete-btn:hover { text-decoration: underline; }
        .amount { font-weight: bold; }
        .person { color: #1976d2; }
        .method { color: #388e3c; }
        .time { color: #666; font-size: 12px; }
        .empty { text-align: center; color: #666; padding: 40px; }
      </style>
    </head>
    <body>
      <div class="container">
        <div class="header">
          <h1>打赏管理系统</h1>
          <a href="/login?logout=true" class="logout">退出登录</a>
        </div>
        
        <div class="form-section">
          <h2>添加打赏记录</h2>
          <form method="POST" action="/add-tip">
            <div class="form-grid">
              <div class="form-group">
                <label for="amount">金额:</label>
                <input type="number" id="amount" name="amount" step="0.01" required>
              </div>
              <div class="form-group">
                <label for="person">打赏人:</label>
                <input type="text" id="person" name="person" required>
              </div>
                             <div class="form-group">
                 <label for="method">支付方式:</label>
                 <select id="method" name="method" required>
                   <option value="">请选择</option>
                   <option value="微信">微信</option>
                   <option value="支付宝">支付宝</option>
                 </select>
               </div>
               <div class="form-group">
                 <label for="color">颜色:</label>
                 <div style="display: flex; align-items: center; gap: 10px;">
                   <input type="color" id="color" name="color" value="#1677FF" style="width: 50px; height: 50px; padding: 0; border: none; border-radius: 4px; cursor: pointer;">
                   <input type="text" id="colorHex" placeholder="#1677FF" style="width: 80px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace; font-size: 14px;">
                 </div>
               </div>
              <div class="form-group">
                <label for="time">时间:</label>
                <input type="datetime-local" id="time" name="time" value="${currentTime}" required>
              </div>
            </div>
                         <button type="submit" style="margin-top: 15px;">添加记录</button>
           </form>
           
           <script>
             // 颜色选择器和十六进制输入框双向同步
             const colorPicker = document.getElementById('color');
             const colorHex = document.getElementById('colorHex');
             const methodSelect = document.getElementById('method');
             
             // 支付方式改变时自动设置对应颜色
             methodSelect.addEventListener('change', function(e) {
               if (e.target.value === '支付宝') {
                 colorPicker.value = '#1677FF';
                 colorHex.value = '#1677FF';
               } else if (e.target.value === '微信') {
                 colorPicker.value = '#09bb07';
                 colorHex.value = '#09bb07';
               }
             });
             
             // 颜色选择器改变时更新十六进制输入框
             colorPicker.addEventListener('input', function(e) {
               colorHex.value = e.target.value;
             });
             
             // 十六进制输入框改变时更新颜色选择器
             colorHex.addEventListener('input', function(e) {
               const value = e.target.value;
               if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
                 colorPicker.value = value;
               }
             });
             
             // 十六进制输入框失去焦点时验证格式
             colorHex.addEventListener('blur', function(e) {
               const value = e.target.value;
               if (!/^#[0-9A-Fa-f]{6}$/.test(value) && value !== '') {
                 e.target.value = colorPicker.value;
               }
             });
             
             // 初始化十六进制输入框的值
             colorHex.value = colorPicker.value;
           </script>
         </div>
        
        <div class="tips-section">
          <div class="tips-header">
            <h2>打赏记录 (${tips.length})</h2>
            <a href="/all" class="api-link" target="_blank">查看 JSON 数据</a>
          </div>
          ${tips.length === 0 ? 
            '<div class="empty">暂无打赏记录</div>' : 
            `<table class="tips-table">
              <thead>
                <tr>
                  <th>金额</th>
                  <th>打赏人</th>
                  <th>支付方式</th>
                  <th>时间</th>
                  <th>操作</th>
                </tr>
              </thead>
              <tbody>
                ${tips.map(tip => `
                  <tr>
                    <td class="amount" style="color: ${tip.color}">¥${tip.amount}</td>
                    <td class="person">${tip.person}</td>
                    <td class="method">${tip.method}</td>
                                         <td class="time">${tip.time || formatTimeForDisplay(getCurrentShanghaiTime())}</td>
                    <td><a href="/delete-tip?id=${tip.id}" class="delete-btn" onclick="return confirm('确定要删除这条记录吗?')">删除</a></td>
                  </tr>
                `).join('')}
              </tbody>
            </table>`
          }
        </div>
      </div>
    </body>
    </html>
  `;
}
    

弄好以后绑定KV空间为TIP_STORAGE,环境变量设置TIP_PASSWORD,这就是你的登录密码。

记得将award-dynamic.pug文件中的你绑定的域名要改成你自己的绑定了cloudflare works的域名。

最后一键三连就能看到效果了。