From 815eff9f9a3f2a4f8588645662361ed406388e9b Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:36:12 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=84=E8=AE=BA=E9=98=B6=E6=AE=B5=E4=B8=80?= =?UTF-8?q?=EF=BC=88=E5=89=8D=E7=AB=AF=EF=BC=89=EF=BC=9A=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=E5=8C=BA=20+=20=E7=99=BB=E5=BD=95=E5=BC=95=E5=AF=BC=E5=8D=A1?= =?UTF-8?q?=20+=20dev=20mock=EF=BC=8C=E7=BB=8F=E5=85=B8=E5=A4=B4=E5=83=8F?= =?UTF-8?q?=E7=95=99=E7=99=BD=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评论区(按已批准的设计文档,阶段一=前端;后端与多用户为阶段二/三) - classic / vivid 共用同一份 DOM:CommentSection + CommentItem(回复一层、 自引用渲染,回复/编辑框模板只写一份)+ CommentMarkdown(唯一 v-html 出口, 必过 sanitizeHtml)。比计划里的两套组件少一半文件,也避免两边结构漂移 - 数据层:api.js 追加 readerApi(me/providers/logout/comments/thread/create/ edit/remove);src/reader.js 读者身份 reactive 单例(仿 site.js, 挂 one:unauthorized 清 user);composables/useComments 按文章实例化 (刻意不做单例,换文章必须重置),乐观插入按当前排序落位 - 交互:排序(默认/最新/最早)、发表、回复(一层 + 加载更多回复)、 10 分钟内编辑、软删、分页(显示更多 N 条)、骨架屏、空态、错误态、审核中徽章 - 登录引导卡:三种 provider(GitHub/Google/Telegram);redirect 类整页跳转, widget 类(Telegram)点击时内联官方脚本(无跳转、无回调,官方按钮落位) - 样式:styles.css 追加 .cm-* 基础段(经典 token,vivid 靠别名继承同一份规则); ui/yohaku/comments.css 只写 vivid 差异(全前缀、特指度盖过基础版), 自己的窄屏/触屏规则写在文件内,不塞 mobile.css - main.ts:comments.css 插在 pages 之后 mobile 之前(层叠契约不变); dev mock 用 ?mock 门控动态 import,await 装完再挂载,否则首屏请求会打到真接口 修三处真问题 - vivid 的 a{color:inherit} 重置(特指度 0,1,2)把作者链接名拉回 body 色, 暗色下暗底暗字 —— 用 a.cm-name-a 压回 - classic 文章页整页掉进 236px 左列:PostView import 了 LeftNav/RightRail 却没渲染,补上(与 HomeView 三栏一致),文章页回到 640px - classic 信息流头像贴死列边框:.row-feed 的负 margin 正好抵光左右内边距, 改 padding 14/16 -> 14/30(30 = 16 出血 + 14 留白,hover 底色与分隔线仍铺满整列) mock 假数据 - 8 位作者(三种登录方式、不同色相头像、两个走首字母兜底)、 默认一屏 8 条各测一个排版点(代码块/引用/列表/已删除/审核中/已编辑/站长徽章/ 加载更多回复),长列表态 26 条测分页;右下角浮层切 9 个状态(?mock&state=), ?ui=classic 可强制 classic 壳(平时渲染不出来,没法做探针) 验证(headless Chromium + CDP 真实时间截图;virtual-time 对含动态 import 的页面 截图时机不稳,会截出全白,已换 CDP 方案) - 头部计数 13px/tabular-nums、.seg 3 选 1;头像 50% 圆、36/24px; 气泡圆角 3,20,20,20(缺角);回复缩进 36px 且无二层嵌套;360 宽无横向溢出 - 暗色:气泡/输入框/名字全部适配(量的时候被 a 的 150ms 颜色 transition 骗过, t0 与 t400 两次测量值不同才看清,稳态正确) - 产物:CSS 顺序 styles→pages→comments→mobile;49 条评论规则全带 html[data-ui=vivid] 前缀;grep __COMMENT_MOCK__ 为空、无 mock chunk —— 生产零 mock --- frontend/src/api.js | 25 ++ frontend/src/components/PostCard.vue | 4 +- .../src/components/comments/CommentItem.vue | 178 ++++++++++++++ .../components/comments/CommentMarkdown.vue | 21 ++ .../components/comments/CommentSection.vue | 179 ++++++++++++++ frontend/src/components/comments/brands.js | 12 + frontend/src/composables/useComments.js | 179 ++++++++++++++ frontend/src/main.ts | 12 +- frontend/src/mocks/comments.js | 228 ++++++++++++++++++ frontend/src/mocks/fixtures.js | 181 ++++++++++++++ frontend/src/reader.js | 53 ++++ frontend/src/styles.css | 151 ++++++++++++ frontend/src/ui/yohaku/YohakuArticle.vue | 4 + frontend/src/ui/yohaku/comments.css | 118 +++++++++ frontend/src/views/PostView.vue | 9 +- 15 files changed, 1351 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/comments/CommentItem.vue create mode 100644 frontend/src/components/comments/CommentMarkdown.vue create mode 100644 frontend/src/components/comments/CommentSection.vue create mode 100644 frontend/src/components/comments/brands.js create mode 100644 frontend/src/composables/useComments.js create mode 100644 frontend/src/mocks/comments.js create mode 100644 frontend/src/mocks/fixtures.js create mode 100644 frontend/src/reader.js create mode 100644 frontend/src/ui/yohaku/comments.css diff --git a/frontend/src/api.js b/frontend/src/api.js index 011dbda..581c0dc 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -85,6 +85,31 @@ export const adminApi = { saveSettings: (body) => request('/api/admin/settings', { method: 'PUT', body }) } +// 读者(评论区)身份。会话是服务端 httpOnly cookie(one_reader), +// 前端只拿 /api/auth/me 的结果做展示,不存 token。 +// 注意 me 在匿名时返回 200 {user:null} 而不是 401 —— request() 对每个 401 +// 都会派发 one:unauthorized,匿名访客每次开页都会刷一遍事件。 +export const readerApi = { + me: () => request('/api/auth/me'), + providers: () => request('/api/auth/providers'), + logout: () => request('/api/auth/logout', { method: 'POST' }), + // Telegram 是 Login Widget:官方脚本回调里直接带着签名字段,没有 code 可换 + telegram: (payload) => request('/api/auth/telegram', { method: 'POST', body: payload }), + + comments: (postId, { sort = 'default', page = 1, size = 10 } = {}) => + request('/api/comments?' + new URLSearchParams({ post_id: postId, sort, page, size })), + thread: (rootId, cursor = '', size = 10) => + request(`/api/comments/${rootId}/thread?` + new URLSearchParams({ cursor, size })), + create: (postId, bodyMd, parentId = 0) => + request('/api/comments', { + method: 'POST', + body: { post_id: postId, parent_id: parentId, body_md: bodyMd } + }), + edit: (id, bodyMd) => + request(`/api/comments/${id}`, { method: 'PUT', body: { body_md: bodyMd } }), + remove: (id) => request(`/api/comments/${id}`, { method: 'DELETE' }) +} + // 后台登录态:cookie 由服务端下发(httpOnly),这里只存一份展示用的用户名 const USER_KEY = 'one.admin.user' diff --git a/frontend/src/components/PostCard.vue b/frontend/src/components/PostCard.vue index bc349ea..82d7111 100644 --- a/frontend/src/components/PostCard.vue +++ b/frontend/src/components/PostCard.vue @@ -71,7 +71,9 @@ const initial = computed(() => (site.author_name || 'O').trim().slice(0, 1).toUp display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 12px; - padding: 14px 16px; + /* 左右 30 = 16(出血到列边)+ 14(内容离边框的呼吸位)。 + 之前是 16/16,负 margin 正好把内边距抵光,头像贴死在边框上。 */ + padding: 14px 30px; margin: 0 -16px; border-bottom: 1px solid var(--line-soft); transition: background 0.15s ease; diff --git a/frontend/src/components/comments/CommentItem.vue b/frontend/src/components/comments/CommentItem.vue new file mode 100644 index 0000000..a7baabf --- /dev/null +++ b/frontend/src/components/comments/CommentItem.vue @@ -0,0 +1,178 @@ + + +