feat(comments): 添加评论组件自动获取页面标题和URL功能
refactor(email): 简化评论回复邮件通知逻辑 fix(docs): 移除过时的EMAIL_ADDRESS配置说明 style(store): 修改loading初始状态为true
This commit is contained in:
3
cwd-comments-admin/.env.example
Normal file
3
cwd-comments-admin/.env.example
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
VITE_API_BASE_URL=https://api.example.com
|
||||||
|
VITE_ADMIN_NAME=anghunk@example.com
|
||||||
|
VITE_ADMIN_PASSWORD=123456
|
||||||
@@ -13,7 +13,8 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
if (!data || typeof data !== 'object') {
|
if (!data || typeof data !== 'object') {
|
||||||
return c.json({ message: '无效的请求体' }, 400);
|
return c.json({ message: '无效的请求体' }, 400);
|
||||||
}
|
}
|
||||||
const { post_slug, content: rawContent, author: rawAuthor, email, url, parent_id, post_title, post_url } = data;
|
const { post_slug, content: rawContent, author: rawAuthor, email, url, post_title, post_url } = data;
|
||||||
|
const parentId = (data as any).parent_id ?? (data as any).parentId ?? null;
|
||||||
if (!post_slug || typeof post_slug !== 'string') {
|
if (!post_slug || typeof post_slug !== 'string') {
|
||||||
return c.json({ message: 'post_slug 必填' }, 400);
|
return c.json({ message: 'post_slug 必填' }, 400);
|
||||||
}
|
}
|
||||||
@@ -64,7 +65,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
|
|
||||||
console.log('PostComment:request', {
|
console.log('PostComment:request', {
|
||||||
postSlug: post_slug,
|
postSlug: post_slug,
|
||||||
hasParent: !!parent_id,
|
hasParent: parentId !== null && parentId !== undefined,
|
||||||
author,
|
author,
|
||||||
email,
|
email,
|
||||||
ip,
|
ip,
|
||||||
@@ -95,7 +96,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
userAgent,
|
userAgent,
|
||||||
content,
|
content,
|
||||||
content,
|
content,
|
||||||
parent_id || null,
|
parentId || null,
|
||||||
"approved" // 或者从环境变量读取默认状态
|
"approved" // 或者从环境变量读取默认状态
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
@@ -103,70 +104,60 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
|
|
||||||
console.log('PostComment:inserted', {
|
console.log('PostComment:inserted', {
|
||||||
postSlug: post_slug,
|
postSlug: post_slug,
|
||||||
hasParent: !!parent_id,
|
hasParent: parentId !== null && parentId !== undefined,
|
||||||
ip
|
ip
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. 发送邮件通知 (后台异步执行,不阻塞用户响应)
|
// 5. 发送邮件通知 (后台异步执行,不阻塞用户响应)
|
||||||
if (c.env.SEND_EMAIL && c.env.CF_FROM_EMAIL) {
|
if (c.env.SEND_EMAIL && c.env.CF_FROM_EMAIL) {
|
||||||
console.log('PostComment:mailDispatch:start', {
|
console.log('PostComment:mailDispatch:start', {
|
||||||
hasParent: !!data.parent_id
|
hasParent: parentId !== null && parentId !== undefined
|
||||||
});
|
});
|
||||||
c.executionCtx.waitUntil((async () => {
|
c.executionCtx.waitUntil((async () => {
|
||||||
try {
|
try {
|
||||||
if (data.parent_id) {
|
if (parentId !== null && parentId !== undefined) {
|
||||||
|
let adminEmail: string | null = null;
|
||||||
|
try {
|
||||||
|
adminEmail = await getAdminNotifyEmail(c.env);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('PostComment:mailDispatch:userReply:getAdminEmailFailed', e);
|
||||||
|
}
|
||||||
|
const isAdminReply = !!adminEmail && email === adminEmail;
|
||||||
|
|
||||||
const parentComment = await c.env.CWD_DB.prepare(
|
const parentComment = await c.env.CWD_DB.prepare(
|
||||||
"SELECT author, email, content_text FROM Comment WHERE id = ?"
|
"SELECT author, email, content_text FROM Comment WHERE id = ?"
|
||||||
).bind(data.parent_id).first<{ author: string, email: string, content_text: string }>();
|
).bind(parentId).first<{ author: string, email: string, content_text: string }>();
|
||||||
|
|
||||||
if (parentComment && parentComment.email !== data.email) {
|
if (parentComment && parentComment.email && parentComment.email !== email) {
|
||||||
let adminEmail: string | null = null;
|
let canSendUserMail = true;
|
||||||
try {
|
if (!isAdminReply) {
|
||||||
adminEmail = await getAdminNotifyEmail(c.env);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('PostComment:mailDispatch:userReply:getAdminEmailFailed', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdminReply = !!adminEmail && email === adminEmail;
|
|
||||||
const isParentThirdParty = !!adminEmail && parentComment.email !== adminEmail;
|
|
||||||
|
|
||||||
if (isAdminReply && isParentThirdParty) {
|
|
||||||
const recentUserMail = await c.env.CWD_DB.prepare(
|
const recentUserMail = await c.env.CWD_DB.prepare(
|
||||||
"SELECT created_at FROM EmailLog WHERE recipient = ? AND type = 'user-reply' ORDER BY created_at DESC LIMIT 1"
|
"SELECT created_at FROM EmailLog WHERE recipient = ? AND type = 'user-reply' ORDER BY created_at DESC LIMIT 1"
|
||||||
).bind(parentComment.email).first<{ created_at: string }>();
|
).bind(parentComment.email).first<{ created_at: string }>();
|
||||||
const canSendUserMail = !recentUserMail || (Date.now() - new Date(recentUserMail.created_at).getTime() > 60 * 1000);
|
canSendUserMail =
|
||||||
|
!recentUserMail ||
|
||||||
if (canSendUserMail && isValidEmail(parentComment.email)) {
|
(Date.now() - new Date(recentUserMail.created_at).getTime() > 30 * 1000);
|
||||||
console.log('PostComment:mailDispatch:userReply:send', {
|
}
|
||||||
toEmail: parentComment.email,
|
|
||||||
toName: parentComment.author
|
if (canSendUserMail && isValidEmail(parentComment.email)) {
|
||||||
});
|
console.log('PostComment:mailDispatch:userReply:send', {
|
||||||
await sendCommentReplyNotification(c.env, {
|
toEmail: parentComment.email,
|
||||||
toEmail: parentComment.email,
|
toName: parentComment.author
|
||||||
toName: parentComment.author,
|
});
|
||||||
postTitle: data.post_title,
|
await sendCommentReplyNotification(c.env, {
|
||||||
parentComment: parentComment.content_text,
|
toEmail: parentComment.email,
|
||||||
replyAuthor: author,
|
toName: parentComment.author,
|
||||||
replyContent: content,
|
postTitle: data.post_title,
|
||||||
postUrl: data.post_url,
|
parentComment: parentComment.content_text,
|
||||||
});
|
replyAuthor: author,
|
||||||
await c.env.CWD_DB.prepare(
|
replyContent: content,
|
||||||
"INSERT INTO EmailLog (recipient, type, ip_address, created_at) VALUES (?, ?, ?, ?)"
|
postUrl: data.post_url,
|
||||||
).bind(parentComment.email, 'user-reply', ip, new Date().toISOString()).run();
|
});
|
||||||
console.log('PostComment:mailDispatch:userReply:logInserted', {
|
await c.env.CWD_DB.prepare(
|
||||||
toEmail: parentComment.email
|
"INSERT INTO EmailLog (recipient, type, ip_address, created_at) VALUES (?, ?, ?, ?)"
|
||||||
});
|
).bind(parentComment.email, 'user-reply', ip, new Date().toISOString()).run();
|
||||||
}
|
console.log('PostComment:mailDispatch:userReply:logInserted', {
|
||||||
if (!canSendUserMail) {
|
toEmail: parentComment.email
|
||||||
console.log('PostComment:mailDispatch:userReply:skippedByRateLimit', {
|
|
||||||
toEmail: parentComment.email
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('PostComment:mailDispatch:userReply:skipNonAdminReply', {
|
|
||||||
adminEmail,
|
|
||||||
currentEmail: email,
|
|
||||||
parentEmail: parentComment.email
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export async function sendCommentNotification(
|
|||||||
<div style="background-color:#f4f4f5;padding:24px 0;">
|
<div style="background-color:#f4f4f5;padding:24px 0;">
|
||||||
<div style="max-width:640px;margin:0 auto;background:#ffffff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;color:#111827;">
|
<div style="max-width:640px;margin:0 auto;background:#ffffff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;color:#111827;">
|
||||||
<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#0f766e,#059669);">
|
<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;background:linear-gradient(135deg,#0f766e,#059669);">
|
||||||
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#f9fafb;">新评论提醒 - ${postTitle}</h1>
|
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#f9fafb;">新评论提醒</h1>
|
||||||
<p style="margin:4px 0 0;font-size:12px;color:#d1fae5;">你的文章收到了新的评论</p>
|
<p style="margin:4px 0 0;font-size:12px;color:#d1fae5;">你的文章收到了新的评论</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:24px 28px;">
|
<div style="padding:24px 28px;">
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ npm install
|
|||||||
| `ADMIN_NAME` | 管理员登录名称 |
|
| `ADMIN_NAME` | 管理员登录名称 |
|
||||||
| `ADMIN_PASSWORD` | 管理员登录密码 |
|
| `ADMIN_PASSWORD` | 管理员登录密码 |
|
||||||
| `CF_FROM_EMAIL` | 作为发件人显示的邮箱地址(需在 Cloudflare Email 路由中预先配置)选填 |
|
| `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()` 直接发信。
|
||||||
|
|
||||||
@@ -115,12 +114,11 @@ npm install
|
|||||||
...
|
...
|
||||||
"send_email": [
|
"send_email": [
|
||||||
{
|
{
|
||||||
"name": "SEND_EMAIL",
|
"name": "SEND_EMAIL"
|
||||||
"destination_address": "xxx"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`destination_address` 和参数 `CF_FROM_EMAIL` 这里填写的邮箱是你绑定域名后,创建的 email 路由,两者需保持一致
|
参数 `CF_FROM_EMAIL` 这里填写的邮箱是你绑定域名后,创建的 email 路由,两者需保持一致
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ export class CWDComments {
|
|||||||
*/
|
*/
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.config = { ...config };
|
this.config = { ...config };
|
||||||
|
if (!this.config.postTitle && typeof document !== 'undefined') {
|
||||||
|
this.config.postTitle = document.title || this.config.postSlug;
|
||||||
|
}
|
||||||
|
if (!this.config.postUrl && typeof window !== 'undefined') {
|
||||||
|
this.config.postUrl = window.location.href;
|
||||||
|
}
|
||||||
this.hostElement = this._resolveElement(config.el);
|
this.hostElement = this._resolveElement(config.el);
|
||||||
this.shadowRoot = null;
|
this.shadowRoot = null;
|
||||||
this.mountPoint = null;
|
this.mountPoint = null;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export function createCommentStore(config, fetchComments, submitComment) {
|
|||||||
const store = new Store({
|
const store = new Store({
|
||||||
// 评论数据
|
// 评论数据
|
||||||
comments: [],
|
comments: [],
|
||||||
loading: false,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
|
|||||||
Reference in New Issue
Block a user