前言
在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的域名。
最后一键三连就能看到效果了。