feat(评论系统): 新增评论审核功能及界面优化
实现新评论需审核后显示的功能,包括后台设置开关、API支持、前端状态提示 优化管理后台界面样式和分页组件 添加成功消息提示组件及样式 更新相关文档说明
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -42,6 +42,7 @@ export type CommentSettingsResponse = {
|
|||||||
allowedDomains?: string[];
|
allowedDomains?: string[];
|
||||||
adminKey?: string | null;
|
adminKey?: string | null;
|
||||||
adminKeySet?: boolean;
|
adminKeySet?: boolean;
|
||||||
|
requireReview?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EmailNotifySettingsResponse = {
|
export type EmailNotifySettingsResponse = {
|
||||||
@@ -135,6 +136,7 @@ export function saveCommentSettings(data: {
|
|||||||
adminEnabled?: boolean;
|
adminEnabled?: boolean;
|
||||||
allowedDomains?: string[];
|
allowedDomains?: string[];
|
||||||
adminKey?: string;
|
adminKey?: string;
|
||||||
|
requireReview?: boolean;
|
||||||
}): Promise<{ message: string }> {
|
}): Promise<{ message: string }> {
|
||||||
return put<{ message: string }>('/admin/settings/comments', data);
|
return put<{ message: string }>('/admin/settings/comments', data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,14 +369,14 @@ onMounted(() => {
|
|||||||
|
|
||||||
.table-row {
|
.table-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid #eaeae0;
|
/* border-bottom: 1px solid #eaeae0; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-row:last-child {
|
.table-row:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-row:hover {
|
.table-row:hover .table-cell{
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,6 +387,7 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
border-bottom: 1px solid #eaeae0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-cell-id {
|
.table-cell-id {
|
||||||
@@ -499,7 +500,7 @@ onMounted(() => {
|
|||||||
.cell-avatar {
|
.cell-avatar {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 50%;
|
border-radius: 5px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,17 +569,22 @@ onMounted(() => {
|
|||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-button {
|
.pagination-button {
|
||||||
|
height: 28px;
|
||||||
|
min-width: 28px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #d0d7de;
|
border: 1px solid #d0d7de;
|
||||||
background-color: #f6f8fa;
|
background-color: #f6f8fa;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-button:disabled {
|
.pagination-button:disabled {
|
||||||
@@ -608,7 +614,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
.pagination-input {
|
.pagination-input {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 24px;
|
height: 28px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -27,6 +27,13 @@
|
|||||||
<span class="slider" />
|
<span class="slider" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label class="form-label">新评论是否审核后再显示</label>
|
||||||
|
<label class="switch">
|
||||||
|
<input v-model="requireReview" type="checkbox" />
|
||||||
|
<span class="slider" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label class="form-label">头像前缀(默认:https://gravatar.com/avatar)</label>
|
<label class="form-label">头像前缀(默认:https://gravatar.com/avatar)</label>
|
||||||
<input v-model="avatarPrefix" class="form-input" type="text" />
|
<input v-model="avatarPrefix" class="form-input" type="text" />
|
||||||
@@ -300,6 +307,7 @@ const commentAdminEnabled = ref(false);
|
|||||||
const allowedDomains = ref("");
|
const allowedDomains = ref("");
|
||||||
const commentAdminKey = ref("");
|
const commentAdminKey = ref("");
|
||||||
const adminKeySet = ref(false);
|
const adminKeySet = ref(false);
|
||||||
|
const requireReview = ref(false);
|
||||||
const savingEmail = ref(false);
|
const savingEmail = ref(false);
|
||||||
const testingEmail = ref(false);
|
const testingEmail = ref(false);
|
||||||
const savingComment = ref(false);
|
const savingComment = ref(false);
|
||||||
@@ -359,6 +367,7 @@ async function load() {
|
|||||||
: "";
|
: "";
|
||||||
commentAdminKey.value = commentRes.adminKey || "";
|
commentAdminKey.value = commentRes.adminKey || "";
|
||||||
adminKeySet.value = !!commentRes.adminKeySet;
|
adminKeySet.value = !!commentRes.adminKeySet;
|
||||||
|
requireReview.value = !!commentRes.requireReview;
|
||||||
emailGlobalEnabled.value = !!emailNotifyRes.globalEnabled;
|
emailGlobalEnabled.value = !!emailNotifyRes.globalEnabled;
|
||||||
|
|
||||||
if (emailNotifyRes.templates) {
|
if (emailNotifyRes.templates) {
|
||||||
@@ -488,6 +497,7 @@ async function saveComment() {
|
|||||||
.map((d) => d.trim())
|
.map((d) => d.trim())
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
adminKey: commentAdminKey.value || undefined,
|
adminKey: commentAdminKey.value || undefined,
|
||||||
|
requireReview: requireReview.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
showToast(res.message || "保存成功", "success");
|
showToast(res.message || "保存成功", "success");
|
||||||
|
|||||||
@@ -40,15 +40,24 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
return c.json({ message: '邮箱格式不正确' }, 400);
|
return c.json({ message: '邮箱格式不正确' }, 400);
|
||||||
}
|
}
|
||||||
const ua = c.req.header('user-agent') || "";
|
const ua = c.req.header('user-agent') || "";
|
||||||
|
|
||||||
// 1. 获取 IP (Worker 获取 IP 的标准方式)
|
|
||||||
const ip = c.req.header('cf-connecting-ip') || "127.0.0.1";
|
const ip = c.req.header('cf-connecting-ip') || "127.0.0.1";
|
||||||
|
|
||||||
// 1.5 管理员身份验证
|
const adminEmail = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
|
||||||
const adminEmail = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?').bind('comment_admin_email').first<string>('value');
|
.bind('comment_admin_email')
|
||||||
|
.first<string>('value');
|
||||||
|
|
||||||
|
const requireReviewRaw = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
|
||||||
|
.bind('comment_require_review')
|
||||||
|
.first<string>('value');
|
||||||
|
const requireReview = requireReviewRaw === '1';
|
||||||
|
|
||||||
|
let isAdminComment = false;
|
||||||
|
|
||||||
if (adminEmail && email === adminEmail) {
|
if (adminEmail && email === adminEmail) {
|
||||||
const adminKey = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?').bind('comment_admin_key_hash').first<string>('value');
|
const adminKey = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
|
||||||
|
.bind('comment_admin_key_hash')
|
||||||
|
.first<string>('value');
|
||||||
|
|
||||||
if (adminKey) {
|
if (adminKey) {
|
||||||
const lockKey = `admin_lock:${ip}`;
|
const lockKey = `admin_lock:${ip}`;
|
||||||
@@ -76,12 +85,11 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
return c.json({ message: "密钥错误" }, 401);
|
return c.json({ message: "密钥错误" }, 401);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证成功,清除失败记录
|
|
||||||
await c.env.CWD_AUTH_KV.delete(`admin_fail:${ip}`);
|
await c.env.CWD_AUTH_KV.delete(`admin_fail:${ip}`);
|
||||||
|
isAdminComment = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 检查评论频率控制 (对应 canPostComment)
|
// 2. 检查评论频率控制 (对应 canPostComment)
|
||||||
// 这里建议使用 D1 查最近一条评论的时间,或者直接放行(如果使用了 Cloudflare WAF)
|
// 这里建议使用 D1 查最近一条评论的时间,或者直接放行(如果使用了 Cloudflare WAF)
|
||||||
const lastComment = await c.env.CWD_DB.prepare(
|
const lastComment = await c.env.CWD_DB.prepare(
|
||||||
@@ -123,7 +131,8 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
const uaParser = new UAParser(ua);
|
const uaParser = new UAParser(ua);
|
||||||
const uaResult = uaParser.getResult();
|
const uaResult = uaParser.getResult();
|
||||||
|
|
||||||
// 4. 写入 D1 数据库
|
const defaultStatus = requireReview && !isAdminComment ? "pending" : "approved";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { success } = await c.env.CWD_DB.prepare(`
|
const { success } = await c.env.CWD_DB.prepare(`
|
||||||
INSERT INTO Comment (
|
INSERT INTO Comment (
|
||||||
@@ -145,7 +154,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
contentText,
|
contentText,
|
||||||
contentHtml,
|
contentHtml,
|
||||||
parentId || null,
|
parentId || null,
|
||||||
"approved" // 或者从环境变量读取默认状态
|
defaultStatus
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
if (!success) throw new Error("Database insert failed");
|
if (!success) throw new Error("Database insert failed");
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const COMMENT_AVATAR_PREFIX_KEY = 'comment_avatar_prefix';
|
|||||||
const COMMENT_ADMIN_ENABLED_KEY = 'comment_admin_enabled';
|
const COMMENT_ADMIN_ENABLED_KEY = 'comment_admin_enabled';
|
||||||
const COMMENT_ALLOWED_DOMAINS_KEY = 'comment_allowed_domains';
|
const COMMENT_ALLOWED_DOMAINS_KEY = 'comment_allowed_domains';
|
||||||
const COMMENT_ADMIN_KEY_HASH_KEY = 'comment_admin_key_hash';
|
const COMMENT_ADMIN_KEY_HASH_KEY = 'comment_admin_key_hash';
|
||||||
|
const COMMENT_REQUIRE_REVIEW_KEY = 'comment_require_review';
|
||||||
|
|
||||||
|
|
||||||
async function loadCommentSettings(env: Bindings) {
|
async function loadCommentSettings(env: Bindings) {
|
||||||
@@ -43,10 +44,11 @@ async function loadCommentSettings(env: Bindings) {
|
|||||||
COMMENT_AVATAR_PREFIX_KEY,
|
COMMENT_AVATAR_PREFIX_KEY,
|
||||||
COMMENT_ADMIN_ENABLED_KEY,
|
COMMENT_ADMIN_ENABLED_KEY,
|
||||||
COMMENT_ALLOWED_DOMAINS_KEY,
|
COMMENT_ALLOWED_DOMAINS_KEY,
|
||||||
COMMENT_ADMIN_KEY_HASH_KEY
|
COMMENT_ADMIN_KEY_HASH_KEY,
|
||||||
|
COMMENT_REQUIRE_REVIEW_KEY
|
||||||
];
|
];
|
||||||
const { results } = await env.CWD_DB.prepare(
|
const { results } = await env.CWD_DB.prepare(
|
||||||
'SELECT key, value FROM Settings WHERE key IN (?, ?, ?, ?, ?, ?)'
|
'SELECT key, value FROM Settings WHERE key IN (?, ?, ?, ?, ?, ?, ?)'
|
||||||
)
|
)
|
||||||
.bind(...keys)
|
.bind(...keys)
|
||||||
.all<{ key: string; value: string }>();
|
.all<{ key: string; value: string }>();
|
||||||
@@ -61,6 +63,9 @@ async function loadCommentSettings(env: Bindings) {
|
|||||||
const enabledRaw = map.get(COMMENT_ADMIN_ENABLED_KEY) ?? null;
|
const enabledRaw = map.get(COMMENT_ADMIN_ENABLED_KEY) ?? null;
|
||||||
const adminEnabled = enabledRaw === '1';
|
const adminEnabled = enabledRaw === '1';
|
||||||
|
|
||||||
|
const requireReviewRaw = map.get(COMMENT_REQUIRE_REVIEW_KEY) ?? null;
|
||||||
|
const requireReview = requireReviewRaw === '1';
|
||||||
|
|
||||||
// 解析允许的域名列表
|
// 解析允许的域名列表
|
||||||
const allowedDomainsRaw = map.get(COMMENT_ALLOWED_DOMAINS_KEY) ?? '';
|
const allowedDomainsRaw = map.get(COMMENT_ALLOWED_DOMAINS_KEY) ?? '';
|
||||||
const allowedDomains = allowedDomainsRaw
|
const allowedDomains = allowedDomainsRaw
|
||||||
@@ -73,6 +78,7 @@ async function loadCommentSettings(env: Bindings) {
|
|||||||
avatarPrefix: map.get(COMMENT_AVATAR_PREFIX_KEY) ?? null,
|
avatarPrefix: map.get(COMMENT_AVATAR_PREFIX_KEY) ?? null,
|
||||||
adminEnabled,
|
adminEnabled,
|
||||||
allowedDomains,
|
allowedDomains,
|
||||||
|
requireReview,
|
||||||
adminKey: map.get(COMMENT_ADMIN_KEY_HASH_KEY) ?? null,
|
adminKey: map.get(COMMENT_ADMIN_KEY_HASH_KEY) ?? null,
|
||||||
adminKeySet: !!map.get(COMMENT_ADMIN_KEY_HASH_KEY)
|
adminKeySet: !!map.get(COMMENT_ADMIN_KEY_HASH_KEY)
|
||||||
};
|
};
|
||||||
@@ -87,6 +93,7 @@ async function saveCommentSettings(
|
|||||||
adminEnabled?: boolean;
|
adminEnabled?: boolean;
|
||||||
allowedDomains?: string[];
|
allowedDomains?: string[];
|
||||||
adminKey?: string;
|
adminKey?: string;
|
||||||
|
requireReview?: boolean;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
await env.CWD_DB.prepare(
|
await env.CWD_DB.prepare(
|
||||||
@@ -118,6 +125,15 @@ async function saveCommentSettings(
|
|||||||
{
|
{
|
||||||
key: COMMENT_ADMIN_KEY_HASH_KEY,
|
key: COMMENT_ADMIN_KEY_HASH_KEY,
|
||||||
value: adminKeyValue
|
value: adminKeyValue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COMMENT_REQUIRE_REVIEW_KEY,
|
||||||
|
value:
|
||||||
|
typeof settings.requireReview === 'boolean'
|
||||||
|
? settings.requireReview
|
||||||
|
? '1'
|
||||||
|
: '0'
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -240,6 +256,7 @@ app.put('/admin/settings/comments', async (c) => {
|
|||||||
const rawAdminEnabled = body.adminEnabled;
|
const rawAdminEnabled = body.adminEnabled;
|
||||||
const rawAllowedDomains = Array.isArray(body.allowedDomains) ? body.allowedDomains : [];
|
const rawAllowedDomains = Array.isArray(body.allowedDomains) ? body.allowedDomains : [];
|
||||||
const rawAdminKey = typeof body.adminKey === 'string' ? body.adminKey : undefined;
|
const rawAdminKey = typeof body.adminKey === 'string' ? body.adminKey : undefined;
|
||||||
|
const rawRequireReview = body.requireReview;
|
||||||
|
|
||||||
const adminEmail = rawAdminEmail.trim();
|
const adminEmail = rawAdminEmail.trim();
|
||||||
const adminBadge = rawAdminBadge.trim();
|
const adminBadge = rawAdminBadge.trim();
|
||||||
@@ -252,6 +269,10 @@ app.put('/admin/settings/comments', async (c) => {
|
|||||||
.map((d: any) => (typeof d === 'string' ? d.trim() : ''))
|
.map((d: any) => (typeof d === 'string' ? d.trim() : ''))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const adminKey = rawAdminKey; // Can be undefined or empty string
|
const adminKey = rawAdminKey; // Can be undefined or empty string
|
||||||
|
const requireReview =
|
||||||
|
typeof rawRequireReview === 'boolean'
|
||||||
|
? rawRequireReview
|
||||||
|
: rawRequireReview === '1' || rawRequireReview === 1;
|
||||||
|
|
||||||
if (adminEmail && !isValidEmail(adminEmail)) {
|
if (adminEmail && !isValidEmail(adminEmail)) {
|
||||||
return c.json({ message: '邮箱格式不正确' }, 400);
|
return c.json({ message: '邮箱格式不正确' }, 400);
|
||||||
@@ -263,7 +284,8 @@ app.put('/admin/settings/comments', async (c) => {
|
|||||||
avatarPrefix,
|
avatarPrefix,
|
||||||
adminEnabled,
|
adminEnabled,
|
||||||
allowedDomains,
|
allowedDomains,
|
||||||
adminKey
|
adminKey,
|
||||||
|
requireReview
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json({ message: '保存成功' });
|
return c.json({ message: '保存成功' });
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default [
|
|||||||
{
|
{
|
||||||
text: '功能',
|
text: '功能',
|
||||||
items: [
|
items: [
|
||||||
{ text: '管理后台', link: '/function/admin-panel' },
|
{ text: '后台设置', link: '/function/admin-panel' },
|
||||||
{ text: '通知邮箱', link: '/function/email-reminder' },
|
{ text: '通知邮箱', link: '/function/email-reminder' },
|
||||||
{ text: '数据迁移', link: '/function/data-migration' },
|
{ text: '数据迁移', link: '/function/data-migration' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -450,7 +450,8 @@ Token 通过登录接口获取,有效期为 24 小时。
|
|||||||
"adminEnabled": true,
|
"adminEnabled": true,
|
||||||
"allowedDomains": [],
|
"allowedDomains": [],
|
||||||
"adminKey": "your-admin-key",
|
"adminKey": "your-admin-key",
|
||||||
"adminKeySet": true
|
"adminKeySet": true,
|
||||||
|
"requireReview": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -465,6 +466,7 @@ Token 通过登录接口获取,有效期为 24 小时。
|
|||||||
| `allowedDomains` | Array\<string\> | 允许调用组件的域名列表,留空则不限制 |
|
| `allowedDomains` | Array\<string\> | 允许调用组件的域名列表,留空则不限制 |
|
||||||
| `adminKey` | string\|null | 管理员评论密钥(明文),仅通过管理后台接口返回 |
|
| `adminKey` | string\|null | 管理员评论密钥(明文),仅通过管理后台接口返回 |
|
||||||
| `adminKeySet` | boolean | 是否已经设置过管理员评论密钥 |
|
| `adminKeySet` | boolean | 是否已经设置过管理员评论密钥 |
|
||||||
|
| `requireReview` | boolean | 是否开启新评论先审核再显示(true 表示新评论默认为待审核) |
|
||||||
|
|
||||||
### 错误响应
|
### 错误响应
|
||||||
|
|
||||||
@@ -499,7 +501,8 @@ Token 通过登录接口获取,有效期为 24 小时。
|
|||||||
"avatarPrefix": "https://cravatar.cn/avatar",
|
"avatarPrefix": "https://cravatar.cn/avatar",
|
||||||
"adminEnabled": true,
|
"adminEnabled": true,
|
||||||
"allowedDomains": [],
|
"allowedDomains": [],
|
||||||
"adminKey": "your-admin-key"
|
"adminKey": "your-admin-key",
|
||||||
|
"requireReview": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -513,6 +516,7 @@ Token 通过登录接口获取,有效期为 24 小时。
|
|||||||
| `adminEnabled` | boolean | 否 | 是否启用博主标识相关展示 |
|
| `adminEnabled` | boolean | 否 | 是否启用博主标识相关展示 |
|
||||||
| `allowedDomains` | Array | 否 | 允许前端调用组件的域名列表 |
|
| `allowedDomains` | Array | 否 | 允许前端调用组件的域名列表 |
|
||||||
| `adminKey` | string | 否 | 管理员评论密钥,留空则表示清除密钥;设置后前台管理员评论需输入密钥 |
|
| `adminKey` | string | 否 | 管理员评论密钥,留空则表示清除密钥;设置后前台管理员评论需输入密钥 |
|
||||||
|
| `requireReview` | boolean | 否 | 是否开启新评论先审核再显示(不传则保持不变) |
|
||||||
|
|
||||||
### 成功响应
|
### 成功响应
|
||||||
|
|
||||||
|
|||||||
@@ -238,7 +238,8 @@
|
|||||||
"adminBadge": "博主",
|
"adminBadge": "博主",
|
||||||
"avatarPrefix": "https://gravatar.com/avatar",
|
"avatarPrefix": "https://gravatar.com/avatar",
|
||||||
"adminEnabled": true,
|
"adminEnabled": true,
|
||||||
"allowedDomains": []
|
"allowedDomains": [],
|
||||||
|
"requireReview": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -251,6 +252,7 @@
|
|||||||
| `avatarPrefix` | string | 头像地址前缀,如 Gravatar 或 Cravatar 镜像地址 |
|
| `avatarPrefix` | string | 头像地址前缀,如 Gravatar 或 Cravatar 镜像地址 |
|
||||||
| `adminEnabled` | boolean | 是否启用博主标识相关展示(关闭时不显示徽标,但仍可作为管理员邮箱) |
|
| `adminEnabled` | boolean | 是否启用博主标识相关展示(关闭时不显示徽标,但仍可作为管理员邮箱) |
|
||||||
| `allowedDomains` | Array\<string\> | 允许调用组件的域名列表,留空则不限制 |
|
| `allowedDomains` | Array\<string\> | 允许调用组件的域名列表,留空则不限制 |
|
||||||
|
| `requireReview` | boolean | 是否开启新评论先审核再显示(true 表示新评论默认为待审核) |
|
||||||
|
|
||||||
### 错误响应
|
### 错误响应
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 管理后台
|
# 后台设置
|
||||||
|
|
||||||
管理后台用于审核评论、删除评论和管理评论设置。
|
管理后台用于审核评论、删除评论和管理评论设置。
|
||||||
|
|
||||||
@@ -26,6 +26,10 @@
|
|||||||
|
|
||||||
设置后前台使用管理员邮箱评论需输入此密钥。如果没有经过验证,无法使用管理员邮箱进行评论,进一步避免邮箱被滥用。
|
设置后前台使用管理员邮箱评论需输入此密钥。如果没有经过验证,无法使用管理员邮箱进行评论,进一步避免邮箱被滥用。
|
||||||
|
|
||||||
|
### 新评论是否审核后再显示
|
||||||
|
|
||||||
|
开启后新评论默认状态为待审核,需管理员审核后才会显示在评论列表中。关闭为直接显示。
|
||||||
|
|
||||||
## 使用官方管理后台
|
## 使用官方管理后台
|
||||||
|
|
||||||
使用官方提供的管理后台(最新版本)https://cwd-comments.zishu.me。
|
使用官方提供的管理后台(最新版本)https://cwd-comments.zishu.me。
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ export class CWDComments {
|
|||||||
if (serverConfig.adminEnabled && serverConfig.adminBadge) {
|
if (serverConfig.adminEnabled && serverConfig.adminBadge) {
|
||||||
this.config.adminBadge = serverConfig.adminBadge;
|
this.config.adminBadge = serverConfig.adminBadge;
|
||||||
}
|
}
|
||||||
|
if (typeof serverConfig.requireReview === 'boolean') {
|
||||||
|
this.config.requireReview = serverConfig.requireReview;
|
||||||
|
}
|
||||||
|
|
||||||
const api = createApiClient(this.config);
|
const api = createApiClient(this.config);
|
||||||
this.api = api;
|
this.api = api;
|
||||||
@@ -256,6 +259,29 @@ export class CWDComments {
|
|||||||
existingError.remove();
|
existingError.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingSuccess = this.mountPoint.querySelector('.cwd-success-inline');
|
||||||
|
if (state.successMessage) {
|
||||||
|
if (!existingSuccess) {
|
||||||
|
const successEl = document.createElement('div');
|
||||||
|
successEl.className = 'cwd-success-inline';
|
||||||
|
successEl.innerHTML = `
|
||||||
|
<span>${state.successMessage}</span>
|
||||||
|
<button type="button" class="cwd-error-close" data-action="clear-success">✕</button>
|
||||||
|
`;
|
||||||
|
successEl.querySelector('[data-action="clear-success"]').addEventListener('click', () => {
|
||||||
|
this.store.clearSuccess();
|
||||||
|
});
|
||||||
|
this.mountPoint.insertBefore(successEl, this.mountPoint.firstChild);
|
||||||
|
} else {
|
||||||
|
const span = existingSuccess.querySelector('span');
|
||||||
|
if (span) {
|
||||||
|
span.textContent = state.successMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (existingSuccess) {
|
||||||
|
existingSuccess.remove();
|
||||||
|
}
|
||||||
|
|
||||||
// 创建头部统计
|
// 创建头部统计
|
||||||
let header = this.mountPoint.querySelector('.cwd-comments-header');
|
let header = this.mountPoint.querySelector('.cwd-comments-header');
|
||||||
if (!header) {
|
if (!header) {
|
||||||
@@ -352,6 +378,29 @@ export class CWDComments {
|
|||||||
existingError.remove();
|
existingError.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingSuccess = this.mountPoint?.querySelector('.cwd-success-inline');
|
||||||
|
if (state.successMessage) {
|
||||||
|
if (!existingSuccess) {
|
||||||
|
const successEl = document.createElement('div');
|
||||||
|
successEl.className = 'cwd-success-inline';
|
||||||
|
successEl.innerHTML = `
|
||||||
|
<span>${state.successMessage}</span>
|
||||||
|
<button type="button" class="cwd-error-close" data-action="clear-success">✕</button>
|
||||||
|
`;
|
||||||
|
successEl.querySelector('[data-action="clear-success"]').addEventListener('click', () => {
|
||||||
|
this.store.clearSuccess();
|
||||||
|
});
|
||||||
|
this.mountPoint?.insertBefore(successEl, this.mountPoint.firstChild);
|
||||||
|
} else {
|
||||||
|
const span = existingSuccess.querySelector('span');
|
||||||
|
if (span) {
|
||||||
|
span.textContent = state.successMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (existingSuccess) {
|
||||||
|
existingSuccess.remove();
|
||||||
|
}
|
||||||
|
|
||||||
// 更新头部统计
|
// 更新头部统计
|
||||||
const header = this.mountPoint?.querySelector('.cwd-comments-header');
|
const header = this.mountPoint?.querySelector('.cwd-comments-header');
|
||||||
const countEl = header?.querySelector('.cwd-comments-count-number');
|
const countEl = header?.querySelector('.cwd-comments-count-number');
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export function createCommentStore(config, fetchComments, submitComment) {
|
|||||||
comments: [],
|
comments: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
|
successMessage: '',
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
pagination: {
|
pagination: {
|
||||||
@@ -186,6 +187,7 @@ export function createCommentStore(config, fetchComments, submitComment) {
|
|||||||
formErrors: {},
|
formErrors: {},
|
||||||
submitting: true,
|
submitting: true,
|
||||||
error: null,
|
error: null,
|
||||||
|
successMessage: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -197,10 +199,14 @@ export function createCommentStore(config, fetchComments, submitComment) {
|
|||||||
adminToken: auth.getToken() // Add token if exists
|
adminToken: auth.getToken() // Add token if exists
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清空评论内容
|
const successMessage = config.requireReview
|
||||||
|
? '已提交评论,待管理员审核后显示'
|
||||||
|
: '评论已提交';
|
||||||
|
|
||||||
store.setState({
|
store.setState({
|
||||||
form: { ...form, content: '' },
|
form: { ...form, content: '' },
|
||||||
submitting: false,
|
submitting: false,
|
||||||
|
successMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 重新加载评论
|
// 重新加载评论
|
||||||
@@ -210,6 +216,7 @@ export function createCommentStore(config, fetchComments, submitComment) {
|
|||||||
store.setState({
|
store.setState({
|
||||||
error: e instanceof Error ? e.message : '提交评论失败',
|
error: e instanceof Error ? e.message : '提交评论失败',
|
||||||
submitting: false,
|
submitting: false,
|
||||||
|
successMessage: '',
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -335,6 +342,12 @@ export function createCommentStore(config, fetchComments, submitComment) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearSuccess() {
|
||||||
|
store.setState({
|
||||||
|
successMessage: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换页码
|
* 切换页码
|
||||||
* @param {number} page - 页码
|
* @param {number} page - 页码
|
||||||
@@ -366,6 +379,7 @@ export function createCommentStore(config, fetchComments, submitComment) {
|
|||||||
updateReplyContent,
|
updateReplyContent,
|
||||||
clearReplyError,
|
clearReplyError,
|
||||||
clearError,
|
clearError,
|
||||||
|
clearSuccess,
|
||||||
goToPage,
|
goToPage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,7 +298,6 @@
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 1px solid var(--cwd-border, #d0d7de);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--cwd-bg-avatar, #f6f8fa)
|
background: var(--cwd-bg-avatar, #f6f8fa)
|
||||||
}
|
}
|
||||||
@@ -660,6 +659,19 @@
|
|||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cwd-success-inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1a7f37;
|
||||||
|
background: #dafbe1;
|
||||||
|
border: 1px solid #54ae65;
|
||||||
|
border-radius: var(--cwd-radius, 6px);
|
||||||
|
}
|
||||||
|
|
||||||
/* ========== 其他 ========== */
|
/* ========== 其他 ========== */
|
||||||
.cwd-load-more {
|
.cwd-load-more {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -824,4 +836,4 @@
|
|||||||
|
|
||||||
.cwd-btn-text:hover {
|
.cwd-btn-text:hover {
|
||||||
color: var(--cwd-primary);
|
color: var(--cwd-primary);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user