diff --git a/blog-admin/.gitignore b/blog-admin/.gitignore index a0c49463..df347610 100644 --- a/blog-admin/.gitignore +++ b/blog-admin/.gitignore @@ -2,6 +2,7 @@ node_modules/ .wrangler/ .selftest-build/ +.mail-build/ *.log # 机密(本地开发用,不提交) diff --git a/blog-admin/public/admin/admin.js b/blog-admin/public/admin/admin.js index 73f7e38b..0e58569b 100644 --- a/blog-admin/public/admin/admin.js +++ b/blog-admin/public/admin/admin.js @@ -346,6 +346,12 @@ function renderTabs() { html += ''; } + // 国内直连入口:Cloudflare 晚高峰丢包,写文章卡的时候一键切到国内机后台 + // (那边独立登录,点过去会由 Worker 先签一张「免登录票」换会话 Cookie) + html += '' + + '' + + ''; $('#tabs').innerHTML = html; } @@ -1846,6 +1852,19 @@ document.addEventListener('click', async (e) => { return; } + if (act === 'cn-line') { + // 一键跳国内机后台:先向 Worker 要一张「免登录票」(Worker 用共享令牌 + // 签名,国内机验签后直接发会话 Cookie),失败就退回普通地址手动登录。 + toast('正在打开国内线路…'); + let url = 'https://writeapi.usj.cc/admin/'; + try { + const d = await api('/editor/handoff'); + if (d && d.url) url = d.url; + } catch (e) { /* 拿不到票就直开,让用户手动登录 */ } + window.open(url, '_blank', 'noopener'); + return; + } + // --- 文章编辑 if (act === 'po-reload') { el.dataset.busy = '1'; try { await loadPosts(); } finally { delete el.dataset.busy; } return; } if (act === 'po-prev') { state.po.page = Math.max(1, state.po.page - 1); await loadPosts(); return; } diff --git a/blog-admin/src/index.ts b/blog-admin/src/index.ts index 03926002..0ea846af 100644 --- a/blog-admin/src/index.ts +++ b/blog-admin/src/index.ts @@ -45,9 +45,13 @@ router.post('/editor/upload', E.upload); router.get('/editor/git/status', E.gitStatus); router.post('/editor/git/publish', E.publish); router.post('/editor/git/sync', E.sync); +router.get('/editor/handoff', E.handoff); // ---------------------------------------------------------------- 评论 +// ★ /comments/lookup 必须注册在 /comments/:id 之前 —— 路由按注册顺序匹配, +// 两段路径下 `:id` 会把 "lookup" 吃掉。 router.get('/comments', C.listComments); +router.get('/comments/lookup', C.lookupCommenter); router.post('/comments', C.createComment); router.get('/comments/:id', C.getComment); router.put('/comments/:id', C.updateComment); diff --git a/blog-admin/src/lib/mail.ts b/blog-admin/src/lib/mail.ts index f69b2c3c..2df21b57 100644 --- a/blog-admin/src/lib/mail.ts +++ b/blog-admin/src/lib/mail.ts @@ -107,115 +107,169 @@ export async function mailQuotaOk(env: Env, limit = 90): Promise { } // ============================================================================ -// 模板 —— 与 artalk.yml 里 mail_tpl / notify_tpl 指向的两个文件一致 -// 改模板只需要动下面两段字符串 +// 模板 —— 微信聊天式:灰底 #EDEDED、自己的评论绿色气泡(#95EC69)靠右、 +// 对方的白色气泡靠左、圆角方头像、时间居中 —— 用大家天天在看的聊天直觉 +// 来传达「谁说了什么」,一眼就懂,不需要解释。 // -// 风格:聊天对话式(自己的评论靠右绿色气泡,对方靠左白色气泡), -// 配色清新(浅薄荷底 + 白卡 + 鲜绿主色 #2fa872)。 +// 以站点的老模板(notice.html / reply.html)为底改:头像(weavatar,邮箱 +// md5,明文不出现在邮件里)、点气泡跳转评论页这些老设计都保留。 +// +// 相比老模板的两处实质修复: +// 1. 表情包尺寸内联到位:老模板靠 head 里 `.comment_img img{width:100px}` +// 控制大小,而不少邮件客户端会把 head 的 `; + +/** 头像:40px 圆角方(微信样式),weavatar 按 Gravatar 规则用邮箱 md5 取图 */ +const AVATAR = (prefix: string, nick: string) => + `${nick}`; + +/** + * 气泡:bg 为 #95ec69(自己)/ #ffffff(对方)。 + * 气泡本身是一个嵌套 (宽度随内容收缩、上限 360px),比 inline-block + * 在邮件客户端里稳得多 —— 图片的 max-width 百分比也能正确解析。 + * align="right" 时气泡贴右(微信里自己说的话)。 + */ +const BUBBLE = (bg: string, color: string, inner: string, align?: 'right') => + `
+ +
${inner}
`; + +/** 回复提醒:自己的评论(绿泡靠右)+ 对方的新回复(白泡靠左),微信式上下两条 */ const REPLY_TPL = ` + +${HEAD_STYLE} - -
+ +
-
- {{site_name_first}} - {{site_name}} +
你在《{{page_title}}》的评论有回复
+
{{parent_comment.datetime}}
+ +
+ + + + + +
+
{{parent_comment.nick}}
+ ${BUBBLE('#95ec69', '#111111', `${BODY_OPEN}{{parent_comment.content}}${BODY_CLOSE}`, 'right')} +
${AVATAR('parent_comment', '{{parent_comment.nick}}')}
-
-
-
《{{page_title}}》 · 对话
+
{{comment.datetime}}
-
-
{{parent_comment.name_first}}
-
-
{{parent_comment.nick}} · {{parent_comment.datetime}}
-
{{parent_comment.content}}
-
-
- -
-
{{comment.name_first}}
-
-
{{comment.nick}} · {{comment.datetime}}
-
{{comment.content}}
-
-
-
- - - 查看并回复 - +
+ + + + + +
${AVATAR('comment', '{{comment.nick}}')} +
{{comment.nick}}
+ ${BUBBLE('#ffffff', '#111111', `${BODY_OPEN}{{comment.content}}${BODY_CLOSE}`)} +
-
- 此邮件来自 {{site_name}},请勿直接回复
- 如果你不想再收到这类通知,可以在评论时关闭"接收邮件通知" + + +
+ + +
+ 查看并回复 +
+
+ +
+ 此邮件来自{{site_name}},请勿直接回复
+ 不想再收到这类提醒,可以在评论时取消勾选「接收邮件通知」
`; -/** 管理员通知(原 notice.html) */ +/** 管理员通知:一条新评论(白泡靠左),微信式 */ const NOTICE_TPL = ` + +${HEAD_STYLE} - -
+ +
-
- {{site_name_first}} - {{site_name}} +
《{{page_title}}》有了新的评论
+
{{comment.datetime}}
+ +
+ + + + + +
${AVATAR('comment', '{{comment.nick}}')} +
{{comment.nick}}
+ ${BUBBLE('#ffffff', '#111111', `${BODY_OPEN}{{comment.content}}${BODY_CLOSE}`)} +
-
-
-
《{{page_title}}》 · 新留言
+ + +
+ + +
+ 前往查看 +
+
-
-
{{comment.name_first}}
-
-
{{comment.nick}} · {{comment.datetime}}
-
{{comment.content}}
-
-
-
- - - 前往查看 - -
- -
- 此邮件来自 {{site_name}},请勿直接回复 +
+ 此邮件来自{{site_name}}博客,请勿直接回复
`; -/** 主题沿用 artalk.yml 里的 mail_subject */ -export const REPLY_SUBJECT = '{{site_name}} · 新的评论回复'; -export const ADMIN_SUBJECT = '{{site_name}} · 新的评论待审'; +/** + * 邮件主题。 + * 回复:沿用老配置「新的评论回复」。 + * 管理员:原来是「新的评论待审」——像系统告警,改成一句人话。 + */ +export const REPLY_SUBJECT = '{{site_name}} · 您的评论收到了新回复'; +export const ADMIN_SUBJECT = '{{site_name}} · 您有一个新的评论'; // ------------------------------------------------------------------ 渲染 @@ -226,18 +280,27 @@ const esc = (v: unknown): string => /** * 评论正文 → 邮件里可用的 HTML。 - * 先走 Markdown(保留正文里的原生 HTML),再把表情图加上内联宽度 —— - * 原模板靠