fix: 更新文档链接和说明,调整评论管理界面样式
This commit is contained in:
@@ -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+
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
...
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
# 快速开始
|
> [!WARNING]
|
||||||
|
> 目前仍处于 Beta 测试阶段,欢迎反馈测试结果。
|
||||||
|
|
||||||
## 简介
|
# cwd-comments
|
||||||
|
|
||||||
CWD 评论系统是一个基于 Cloudflare Workers 的轻量级评论解决方案,使用 Hono 框架构建,数据存储使用 Cloudflare D1(SQLite)和 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user