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

View File

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

View File

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

View File

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

View File

@@ -34,12 +34,16 @@ npm install
```bash
npx wrangler login
```
* **创建数据库和数据库表**,如果遇到提示,请按回车继续
```bash
npx wrangler d1 create CWD_DB
npx wrangler d1 execute CWD_DB --remote --file=./schemas/comment.sql
```
运行完成后可以确认一下 `wrangler.jsonc` 中是否有如下配置
```jsonc
"d1_databases": [
{
@@ -49,13 +53,17 @@ npm install
}
]
```
如果`binding`字段不是`CWD_DB`,请修改为`CWD_DB`
* **创建 KV 存储**,如果遇到提示,按回车继续
```bash
npx wrangler kv namespace create CWD_AUTH_KV
```
运行完成后可以确认一下 `wrangler.jsonc` 中是否有如下配置
```jsonc
"kv_namespaces": [
{
@@ -64,12 +72,14 @@ npm install
}
]
```
* **部署上线**
```bash
npm run deploy
```
没有异常报错后可以进入Cloudflare Workers 面板查看是否部署成功,若显示存在一个名称为 `cwd-backend-worker` 的项目即推送成功。
没有异常报错后,可以进入 Cloudflare Workers 面板查看是否部署成功,若显示存在一个名称为 `cwd-comments-api` 的项目即推送成功。
#### 3. 配置环境变量
@@ -79,30 +89,36 @@ npm install
#### 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 路由中预先配置) |
| `EMAIL_ADDRESS` | 管理员接收通知邮件的默认邮箱(可在后台设置中覆盖) |
| `ADMIN_NAME` | 管理员登录名称,默认为 admin |
| `ADMIN_PASSWORD` | 管理员登录密码,默认密码为 password |
| 变量名 | 描述 |
| ---------------- | ---------------------------------------------------------------- |
| `ADMIN_NAME` | 管理员登录名称 |
| `ADMIN_PASSWORD` | 管理员登录密码 |
| `CF_FROM_EMAIL` | 作为发件人显示的邮箱地址(需在 Cloudflare Email 路由中预先配置) |
| `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` 文件来设置
```bash
cp .dev.vars.example .dev.vars
# 编辑 .dev.vars 文件
npm run dev
```jsonc
{
...
"send_email": [
{
"name": "SEND_EMAIL",
"destination_address": "hi@zishu.me"
}
],
...
}
```

View File

@@ -16,8 +16,8 @@
<script>
const comments = new CWDComments({
el: '#comments', // 容器 id
apiBaseUrl: 'https://your-api.example.com', // 你部署的api地址
postSlug: 'my-post', // 当前页面路径,可使用博客程序支持的 url 模板路径
apiBaseUrl: 'https://your-api.example.com', // 你部署的 api 地址
postSlug: 'https://example.com/my-post', // 当前页面路径,可使用博客程序支持的 url 模板路径,或者直接使用 window.location.origin
});
comments.mount();
</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 全球边缘网络
- 🔒 **安全可靠**内置管理员认证、CORS 保护
- 📧 **邮件通知**:支持 Resend 邮件服务
- 🎨 **易于集成**:提供完整的 REST API
**开发中:**
- [ ]📧 **邮件通知**:基于 Cloudflare Workers 发送邮件
## 前置要求
- Node.js 16+
@@ -22,13 +27,18 @@ CWD 评论系统是一个基于 Cloudflare Workers 的轻量级评论解决方
```bash
# 克隆项目
git clone https://github.com/anghunk/cwd-comments
cd cwd-comments
# 安装依赖
# API 项目
cd cwd-comments-api
# 部署请查看文档
# 前端项目
cd cwd-comments-web
npm install
```
## 配置
- [后端配置](./backend-config.md)
- [前端配置](./frontend-config.md)
- [后端配置](https://cwd-comments-docs.zishu.me/guide/backend-config.html)
- [前端配置](https://cwd-comments-docs.zishu.me/guide/frontend-config.html)