feat(评论系统): 新增评论审核功能及界面优化

实现新评论需审核后显示的功能,包括后台设置开关、API支持、前端状态提示
优化管理后台界面样式和分页组件
添加成功消息提示组件及样式
更新相关文档说明
This commit is contained in:
anghunk
2026-01-20 22:02:46 +08:00
parent 2456adcfbb
commit 6a9d5c1167
13 changed files with 161 additions and 27 deletions

File diff suppressed because one or more lines are too long

View File

@@ -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);
} }

View File

@@ -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;

View File

@@ -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");

View File

@@ -41,14 +41,23 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
} }
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}`;
@@ -77,11 +86,10 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
} }
} }
// 验证成功,清除失败记录
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");

View File

@@ -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: '保存成功' });

View File

@@ -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' },
], ],

View File

@@ -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 | 否 | 是否开启新评论先审核再显示(不传则保持不变) |
### 成功响应 ### 成功响应

View File

@@ -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 表示新评论默认为待审核) |
### 错误响应 ### 错误响应

View File

@@ -1,4 +1,4 @@
# 管理后台 # 后台设置
管理后台用于审核评论、删除评论和管理评论设置。 管理后台用于审核评论、删除评论和管理评论设置。
@@ -26,6 +26,10 @@
设置后前台使用管理员邮箱评论需输入此密钥。如果没有经过验证,无法使用管理员邮箱进行评论,进一步避免邮箱被滥用。 设置后前台使用管理员邮箱评论需输入此密钥。如果没有经过验证,无法使用管理员邮箱进行评论,进一步避免邮箱被滥用。
### 新评论是否审核后再显示
开启后新评论默认状态为待审核,需管理员审核后才会显示在评论列表中。关闭为直接显示。
## 使用官方管理后台 ## 使用官方管理后台
使用官方提供的管理后台最新版本https://cwd-comments.zishu.me。 使用官方提供的管理后台最新版本https://cwd-comments.zishu.me。

View File

@@ -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');

View File

@@ -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,
}; };
} }

View File

@@ -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;