fix: 更新文档链接和说明,调整评论管理界面样式

This commit is contained in:
anghunk
2026-01-19 17:24:57 +08:00
parent 73bbe44fbc
commit ff6563527b
7 changed files with 323 additions and 260 deletions

View File

@@ -5,15 +5,17 @@
Cloudflare Worker 版本基于 Cloudflare Workers + D1 + KV 实现,无需服务器即可部署运行的评论组件。 Cloudflare Worker 版本基于 Cloudflare Workers + D1 + KV 实现,无需服务器即可部署运行的评论组件。
[文档地址](https://cwd-comments.zishu.me) [文档地址](https://cwd-comments-docs.zishu.me)
## 特性 ## 特性
- ⚡️ **极速响应**:基于 Cloudflare 全球边缘网络 - ⚡️ **极速响应**:基于 Cloudflare 全球边缘网络
- 🔒 **安全可靠**内置管理员认证、CORS 保护 - 🔒 **安全可靠**内置管理员认证、CORS 保护
- 📧 **邮件通知**:基于 Cloudflare Workers 发送邮件
- 🎨 **易于集成**:提供完整的 REST API - 🎨 **易于集成**:提供完整的 REST API
**开发中:**
- [ ]📧 **邮件通知**:基于 Cloudflare Workers 发送邮件
## 前置要求 ## 前置要求
- Node.js 16+ - Node.js 16+

View File

@@ -17,63 +17,63 @@
<div v-if="loading" class="page-hint">加载中...</div> <div v-if="loading" class="page-hint">加载中...</div>
<div v-else-if="error" class="page-error">{{ error }}</div> <div v-else-if="error" class="page-error">{{ error }}</div>
<div v-else> <div v-else>
<div class="comment-list"> <div class="comment-table">
<div <div class="table-header">
v-for="item in filteredComments" <div class="table-cell table-cell-id">ID</div>
:key="item.id" <div class="table-cell table-cell-author">作者</div>
class="comment-card" <div class="table-cell table-cell-content">内容</div>
> <div class="table-cell table-cell-path">路径</div>
<div class="comment-card-header"> <div class="table-cell table-cell-time">时间</div>
<div class="comment-author"> <div class="table-cell table-cell-status">状态</div>
<div class="comment-author-name"> <div class="table-cell table-cell-actions">操作</div>
{{ item.author }} </div>
</div> <div v-for="item in filteredComments" :key="item.id" class="table-row">
<div class="comment-author-email"> <div class="table-cell table-cell-id">
{{ item.email }} <span class="cell-id">#{{ item.id }}</span>
</div>
</div>
<div class="comment-meta">
<span class="comment-path">{{ item.postSlug }}</span>
<span class="comment-time">{{ formatDate(item.pubDate) }}</span>
</div>
</div> </div>
<div class="comment-content"> <div class="table-cell table-cell-author">
{{ item.contentText }} <div class="cell-author-name">{{ item.author }}</div>
<div class="cell-author-email">{{ item.email }}</div>
</div> </div>
<div class="comment-footer"> <div class="table-cell table-cell-content">
<div class="comment-info"> <div class="cell-content-text">{{ item.contentText }}</div>
<span class="comment-id">#{{ item.id }}</span> </div>
<span <div class="table-cell table-cell-path">
class="comment-status" <span class="cell-path" :title="item.postSlug">{{ item.postSlug }}</span>
:class="`comment-status-${item.status}`" </div>
> <div class="table-cell table-cell-time">
{{ formatStatus(item.status) }} <span class="cell-time">{{ formatDate(item.pubDate) }}</span>
</span> </div>
</div> <div class="table-cell table-cell-status">
<div class="comment-actions"> <span class="cell-status" :class="`cell-status-${item.status}`">
{{ formatStatus(item.status) }}
</span>
</div>
<div class="table-cell table-cell-actions">
<div class="table-actions">
<button <button
class="comment-action" class="table-action"
@click="changeStatus(item, 'approved')" @click="changeStatus(item, 'approved')"
:disabled="item.status === 'approved'" :disabled="item.status === 'approved'"
> >
通过 通过
</button> </button>
<button <button
class="comment-action" class="table-action"
@click="changeStatus(item, 'pending')" @click="changeStatus(item, 'pending')"
:disabled="item.status === 'pending'" :disabled="item.status === 'pending'"
> >
待审 待审
</button> </button>
<button <button
class="comment-action" class="table-action"
@click="changeStatus(item, 'rejected')" @click="changeStatus(item, 'rejected')"
:disabled="item.status === 'rejected'" :disabled="item.status === 'rejected'"
> >
拒绝 拒绝
</button> </button>
<button <button
class="comment-action comment-action-danger" class="table-action table-action-danger"
@click="removeComment(item)" @click="removeComment(item)"
> >
删除 删除
@@ -81,10 +81,7 @@
</div> </div>
</div> </div>
</div> </div>
<div <div v-if="filteredComments.length === 0" class="table-empty">
v-if="filteredComments.length === 0"
class="page-hint comment-empty"
>
暂无数据 暂无数据
</div> </div>
</div> </div>
@@ -220,7 +217,7 @@ onMounted(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 8px; margin: 0;
} }
.toolbar-left { .toolbar-left {
@@ -236,6 +233,9 @@ onMounted(() => {
.toolbar-select { .toolbar-select {
padding: 4px 8px; padding: 4px 8px;
font-size: 13px; font-size: 13px;
border: 1px solid #d0d7de;
border-radius: 4px;
background-color: #ffffff;
} }
.toolbar-button { .toolbar-button {
@@ -257,182 +257,192 @@ onMounted(() => {
color: #d1242f; color: #d1242f;
} }
.comment-list { .comment-table {
display: flex;
flex-direction: column;
gap: 12px;
}
.comment-card {
background-color: #ffffff; background-color: #ffffff;
border-radius: 6px;
border: 1px solid #d0d7de; border: 1px solid #d0d7de;
border-radius: 6px;
overflow: hidden;
}
.table-header {
display: flex;
background-color: #f6f8fa;
border-bottom: 1px solid #d0d7de;
}
.table-row {
display: flex;
border-bottom: 1px solid #eaeae0;
}
.table-row:last-child {
border-bottom: none;
}
.table-row:hover {
background-color: #f8f9fa;
}
.table-cell {
padding: 10px 12px; padding: 10px 12px;
box-shadow: 0 1px 0 rgba(27, 31, 36, 0.04); font-size: 13px;
}
.comment-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
margin-bottom: 8px;
}
.comment-author {
display: flex;
flex-direction: column;
gap: 2px;
}
.comment-author-name {
font-size: 14px;
font-weight: 600;
color: #24292f; color: #24292f;
display: flex;
align-items: center;
} }
.comment-author-email { .table-cell-id {
width: 50px;
flex-shrink: 0;
align-items: center;
white-space: nowrap;
}
.table-cell-author {
width: 180px;
flex-direction: column;
align-items: flex-start !important;
flex-shrink: 0;
}
.table-cell-content {
flex: 1;
min-width: 200px;
}
.table-cell-path {
width: 160px;
flex-shrink: 0;
}
.table-cell-time {
width: 150px;
flex-shrink: 0;
}
.table-cell-status {
width: 80px;
flex-shrink: 0;
justify-content: center;
align-items: center;
}
.table-cell-actions {
width: 240px;
flex-shrink: 0;
justify-content: flex-start;
align-items: center;
}
.table-header .table-cell {
font-weight: 500;
color: #57606a;
align-items: center;
}
.cell-id {
font-size: 12px; font-size: 12px;
color: #57606a; color: #57606a;
}
.cell-author-name {
font-size: 13px;
font-weight: 500;
margin-bottom: 2px;
}
.cell-author-email {
font-size: 11px;
color: #57606a;
word-break: break-all; word-break: break-all;
} }
.comment-meta { .cell-content-text {
display: flex; font-size: 13px;
flex-direction: column; line-height: 1.5;
align-items: flex-end; white-space: pre-wrap;
gap: 4px; word-break: break-word;
font-size: 12px; max-height: 60px;
color: #57606a; overflow-y: auto;
} }
.comment-path { .cell-path {
max-width: 220px; font-size: 12px;
color: #57606a;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.comment-time { .cell-time {
white-space: nowrap;
}
.comment-content {
font-size: 14px;
line-height: 1.6;
color: #24292f;
margin-bottom: 8px;
white-space: pre-wrap;
word-break: break-word;
}
.comment-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.comment-info {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px; font-size: 12px;
color: #57606a; color: #57606a;
} }
.comment-id { .cell-status {
padding: 2px 6px; padding: 3px 8px;
border-radius: 999px; border-radius: 999px;
background-color: #f6f8fa; font-size: 11px;
font-weight: 500;
} }
.comment-status { .cell-status-approved {
padding: 2px 8px;
border-radius: 999px;
background-color: #f6f8fa;
}
.comment-status-approved {
color: #1a7f37; color: #1a7f37;
background-color: #e7f5eb; background-color: #e7f5eb;
} }
.comment-status-pending { .cell-status-pending {
color: #9a6700; color: #9a6700;
background-color: #fff8c5; background-color: #fff8c5;
} }
.comment-status-rejected { .cell-status-rejected {
color: #d1242f; color: #d1242f;
background-color: #ffebe9; background-color: #ffebe9;
} }
.comment-actions { .table-actions {
display: flex; display: flex;
flex-wrap: wrap;
gap: 4px; gap: 4px;
flex-wrap: wrap;
} }
.comment-action { .table-action {
padding: 4px 8px; padding: 3px 8px;
border-radius: 4px; border-radius: 4px;
border: 1px solid #d0d7de; border: 1px solid #d0d7de;
background-color: #f6f8fa; background-color: #ffffff;
font-size: 12px; font-size: 11px;
cursor: pointer; cursor: pointer;
} }
.comment-action-danger { .table-action:hover {
background-color: #f6f8fa;
}
.table-action:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.table-action-danger {
border-color: #d1242f; border-color: #d1242f;
color: #d1242f; color: #d1242f;
} }
.comment-action:disabled { .table-action-danger:hover {
opacity: 0.5; background-color: #ffebe9;
cursor: not-allowed;
} }
.comment-empty { .table-empty {
margin-top: 8px; padding: 32px;
text-align: center; text-align: center;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
gap: 12px;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f0f0f0;
border-top-color: #0969da;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
font-size: 14px;
color: #57606a; color: #57606a;
} font-size: 13px;
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
} }
.pagination { .pagination {
margin-top: 8px; margin-top: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -447,7 +457,13 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
} }
.pagination-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pagination-info { .pagination-info {
font-size: 13px; font-size: 13px;
color: #57606a;
} }
</style> </style>

View File

@@ -1,130 +1,149 @@
<template> <template>
<div class="layout"> <div class="layout">
<header class="layout-header"> <header class="layout-header">
<div class="layout-title">CWD 评论后台</div> <div class="layout-title">CWD 评论后台</div>
<div class="layout-actions"> <div class="layout-actions">
<button class="layout-button" @click="goSettings">设置</button> <a
<button class="layout-button" @click="handleLogout">退出</button> class="layout-button"
</div> href="https://github.com/anghunk/cwd-comments"
</header> target="_blank"
<div class="layout-body"> >
<nav class="layout-sider"> Github
<ul class="menu"> </a>
<li class="menu-item" :class="{ active: isRouteActive('comments') }" @click="goComments">评论管理</li> <button class="layout-button" @click="goSettings">设置</button>
<li class="menu-item" :class="{ active: isRouteActive('settings') }" @click="goSettings">系统设置</li> <button class="layout-button" @click="handleLogout">退出</button>
</ul> </div>
</nav> </header>
<main class="layout-content"> <div class="layout-body">
<router-view /> <nav class="layout-sider">
</main> <ul class="menu">
</div> <li
</div> class="menu-item"
:class="{ active: isRouteActive('comments') }"
@click="goComments"
>
评论管理
</li>
<li
class="menu-item"
:class="{ active: isRouteActive('settings') }"
@click="goSettings"
>
系统设置
</li>
</ul>
</nav>
<main class="layout-content">
<router-view />
</main>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from "vue-router";
import { logoutAdmin } from '../api/admin'; import { logoutAdmin } from "../api/admin";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
function isRouteActive(name: string) { function isRouteActive(name: string) {
return route.name === name; return route.name === name;
} }
function goComments() { function goComments() {
router.push({ name: 'comments' }); router.push({ name: "comments" });
} }
function goSettings() { function goSettings() {
router.push({ name: 'settings' }); router.push({ name: "settings" });
} }
function handleLogout() { function handleLogout() {
logoutAdmin(); logoutAdmin();
router.push({ name: 'login' }); router.push({ name: "login" });
} }
</script> </script>
<style scoped> <style scoped>
.layout { .layout {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
} }
.layout-header { .layout-header {
height: 56px; height: 56px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 24px; padding: 0 24px;
background-color: #24292f; background-color: #24292f;
color: #ffffff; color: #ffffff;
} }
.layout-title { .layout-title {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
} }
.layout-actions { .layout-actions {
display: flex; display: flex;
gap: 8px; gap: 8px;
} }
.layout-button { .layout-button {
padding: 6px 10px; text-decoration: none;
border-radius: 4px; padding: 6px 10px;
border: 1px solid #57606a; border-radius: 4px;
background-color: #24292f; border: 1px solid #57606a;
color: #ffffff; background-color: #24292f;
cursor: pointer; color: #ffffff;
font-size: 13px; cursor: pointer;
font-size: 13px;
} }
.layout-button:hover { .layout-button:hover {
background-color: #32383f; background-color: #32383f;
} }
.layout-body { .layout-body {
display: flex; display: flex;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }
.layout-sider { .layout-sider {
width: 180px; width: 180px;
background-color: #f6f8fa; background-color: #f6f8fa;
border-right: 1px solid #d0d7de; border-right: 1px solid #d0d7de;
} }
.menu { .menu {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 12px 0; padding: 12px 0;
} }
.menu-item { .menu-item {
padding: 10px 16px; padding: 10px 16px;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
color: #24292f; color: #24292f;
} }
.menu-item:hover { .menu-item:hover {
background-color: #eaeef2; background-color: #eaeef2;
} }
.menu-item.active { .menu-item.active {
background-color: #d0ebff; background-color: #d0ebff;
font-weight: 600; font-weight: 600;
} }
.layout-content { .layout-content {
flex: 1; flex: 1;
padding: 16px 20px; padding: 16px 20px;
overflow: auto; overflow: auto;
} }
</style> </style>

View File

@@ -4,7 +4,7 @@
*/ */
{ {
"$schema": "node_modules/wrangler/config-schema.json", "$schema": "node_modules/wrangler/config-schema.json",
"name": "cwd-comments", "name": "cwd-comments-api",
"main": "src/index.ts", "main": "src/index.ts",
"compatibility_date": "2026-01-03", "compatibility_date": "2026-01-03",
"workers_dev": true, "workers_dev": true,

View File

@@ -34,12 +34,16 @@ npm install
```bash ```bash
npx wrangler login npx wrangler login
``` ```
* **创建数据库和数据库表**,如果遇到提示,请按回车继续 * **创建数据库和数据库表**,如果遇到提示,请按回车继续
```bash ```bash
npx wrangler d1 create CWD_DB npx wrangler d1 create CWD_DB
npx wrangler d1 execute CWD_DB --remote --file=./schemas/comment.sql npx wrangler d1 execute CWD_DB --remote --file=./schemas/comment.sql
``` ```
运行完成后可以确认一下 `wrangler.jsonc` 中是否有如下配置 运行完成后可以确认一下 `wrangler.jsonc` 中是否有如下配置
```jsonc ```jsonc
"d1_databases": [ "d1_databases": [
{ {
@@ -49,13 +53,17 @@ npm install
} }
] ]
``` ```
如果`binding`字段不是`CWD_DB`,请修改为`CWD_DB` 如果`binding`字段不是`CWD_DB`,请修改为`CWD_DB`
* **创建 KV 存储**,如果遇到提示,按回车继续 * **创建 KV 存储**,如果遇到提示,按回车继续
```bash ```bash
npx wrangler kv namespace create CWD_AUTH_KV npx wrangler kv namespace create CWD_AUTH_KV
``` ```
运行完成后可以确认一下 `wrangler.jsonc` 中是否有如下配置 运行完成后可以确认一下 `wrangler.jsonc` 中是否有如下配置
```jsonc ```jsonc
"kv_namespaces": [ "kv_namespaces": [
{ {
@@ -64,12 +72,14 @@ npm install
} }
] ]
``` ```
* **部署上线** * **部署上线**
```bash ```bash
npm run deploy npm run deploy
``` ```
没有异常报错后可以进入Cloudflare Workers 面板查看是否部署成功,若显示存在一个名称为 `cwd-backend-worker` 的项目即推送成功。 没有异常报错后,可以进入 Cloudflare Workers 面板查看是否部署成功,若显示存在一个名称为 `cwd-comments-api` 的项目即推送成功。
#### 3. 配置环境变量 #### 3. 配置环境变量
@@ -79,30 +89,36 @@ npm install
#### 4. 检测部署情况 #### 4. 检测部署情况
部署成功后回得到一个域名,即为后端的域名(格式一般为`https://cwd-backend-worker.xxx.workers.dev`。访问该域名,如果显示后端管理页面并可以正常登录则部署成功,将此域名填写到博客的配置文件中即可使用评论功能 部署成功后回得到一个域名,即为后端的域名(格式一般为`https://cwd-comments-api.xxx.workers.dev`。访问该域名,如果显示部署成功页面,说明 API 部署成功,可以到管理后台进行登录
当然也可以使用自定义域名,注意不要使用三级域名,即`*.*.example.com` 当然也可以使用自定义域名。
## 环境变量 ## 环境变量
所需环境变量如下表所示,请参考源码中 `.dev.vars.example` 文件 所需环境变量如下表所示
| 变量名 | 描述 | | 变量名 | 描述 |
| -------------------- | ------------------------------------------------------------------------------------ | | ---------------- | ---------------------------------------------------------------- |
| `CF_FROM_EMAIL` | 作为发件人显示的邮箱地址(需在 Cloudflare Email 路由中预先配置) | | `ADMIN_NAME` | 管理员登录名称 |
| `EMAIL_ADDRESS` | 管理员接收通知邮件的默认邮箱(可在后台设置中覆盖) | | `ADMIN_PASSWORD` | 管理员登录密码 |
| `ADMIN_NAME` | 管理员登录名称,默认为 admin | | `CF_FROM_EMAIL` | 作为发件人显示的邮箱地址(需在 Cloudflare Email 路由中预先配置) |
| `ADMIN_PASSWORD` | 管理员登录密码,默认密码为 password | | `EMAIL_ADDRESS` | 管理员接收通知邮件的默认邮箱(可在后台设置中覆盖) |
**注:** 需要在 Cloudflare 控制面板中为 Email 路由开启发送权限并配置发件人域和地址,并在 `wrangler.jsonc` 中为 Worker 添加 `send_email` 绑定,以便在代码中通过 `env.SEND_EMAIL.send()` 直接发信。 **注** 需要在 Cloudflare 控制面板中为 Email 路由开启发送权限并配置发件人域和地址,并在 `wrangler.jsonc` 中为 Worker 添加 `send_email` 绑定,以便在代码中通过 `env.SEND_EMAIL.send()` 直接发信。
## 发信设置
## 本地测试 手动在 `wrangler.jsonc` 中添加 `send_email` 绑定,以便在代码中通过 `env.SEND_EMAIL.send()` 直接发信。
如果需要本地测试,环境变量可以使用 `.dev.vars` 文件来设置 ```jsonc
{
```bash ...
cp .dev.vars.example .dev.vars "send_email": [
# 编辑 .dev.vars 文件 {
npm run dev "name": "SEND_EMAIL",
"destination_address": "hi@zishu.me"
}
],
...
}
``` ```

View File

@@ -16,8 +16,8 @@
<script> <script>
const comments = new CWDComments({ const comments = new CWDComments({
el: '#comments', // 容器 id el: '#comments', // 容器 id
apiBaseUrl: 'https://your-api.example.com', // 你部署的api地址 apiBaseUrl: 'https://your-api.example.com', // 你部署的 api 地址
postSlug: 'my-post', // 当前页面路径,可使用博客程序支持的 url 模板路径 postSlug: 'https://example.com/my-post', // 当前页面路径,可使用博客程序支持的 url 模板路径,或者直接使用 window.location.origin
}); });
comments.mount(); comments.mount();
</script> </script>

View File

@@ -1,16 +1,21 @@
# 快速开始 > [!WARNING]
> 目前仍处于 Beta 测试阶段,欢迎反馈测试结果。
## 简介 # cwd-comments
CWD 评论系统是一个基于 Cloudflare Workers 的轻量级评论解决方案,使用 Hono 框架构建,数据存储使用 Cloudflare D1SQLite和 KV Cloudflare Worker 版本基于 Cloudflare Workers + D1 + KV 实现,无需服务器即可部署运行的评论组件
[文档地址](https://cwd-comments-docs.zishu.me)
## 特性 ## 特性
- ⚡️ **极速响应**:基于 Cloudflare 全球边缘网络 - ⚡️ **极速响应**:基于 Cloudflare 全球边缘网络
- 🔒 **安全可靠**内置管理员认证、CORS 保护 - 🔒 **安全可靠**内置管理员认证、CORS 保护
- 📧 **邮件通知**:支持 Resend 邮件服务
- 🎨 **易于集成**:提供完整的 REST API - 🎨 **易于集成**:提供完整的 REST API
**开发中:**
- [ ]📧 **邮件通知**:基于 Cloudflare Workers 发送邮件
## 前置要求 ## 前置要求
- Node.js 16+ - Node.js 16+
@@ -22,13 +27,18 @@ CWD 评论系统是一个基于 Cloudflare Workers 的轻量级评论解决方
```bash ```bash
# 克隆项目 # 克隆项目
git clone https://github.com/anghunk/cwd-comments git clone https://github.com/anghunk/cwd-comments
cd cwd-comments
# 安装依赖 # API 项目
cd cwd-comments-api
# 部署请查看文档
# 前端项目
cd cwd-comments-web
npm install npm install
``` ```
## 配置 ## 配置
- [后端配置](./backend-config.md) - [后端配置](https://cwd-comments-docs.zishu.me/guide/backend-config.html)
- [前端配置](./frontend-config.md) - [前端配置](https://cwd-comments-docs.zishu.me/guide/frontend-config.html)