Files
ONE/frontend/src/mocks/fixtures.js
T
Sakurasan 815eff9f9a 评论阶段一(前端):评论区 + 登录引导卡 + dev mock,经典头像留白修复
评论区(按已批准的设计文档,阶段一=前端;后端与多用户为阶段二/三)
- 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
2026-09-26 18:36:12 +08:00

182 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 评论 mock 的假数据(仅 dev,见 mocks/comments.js)。
// 字段要和后端 model.Comment 对齐 —— 真接口落地后,这里的差别就是前端要改的地方。
// 内容按真实博客评论的口吻写的:提问 / 挑错 / 补充 / 串门,别改成「好文,学习了」那种。
// 头像:内联 SVG(离线可用),每个作者一个色相,顺便把 <img> 那条分支走通
const AV =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' fill='%23__COLOR__'/%3E%3Ccircle cx='32' cy='25' r='11' fill='%23fff' opacity='.92'/%3E%3Cpath d='M12 64c0-12 9-20 20-20s20 8 20 20z' fill='%23fff' opacity='.92'/%3E%3C/svg%3E"
const avatar = (hex) => AV.replace('__COLOR__', hex)
export const ME = {
id: 3,
name: 'hegu',
handle: 'cnhegu',
avatar_url: avatar('69a6cc'),
url: '',
provider: 'github',
role: 'reader',
is_owner: false
}
export const OWNER = {
id: 1,
name: 'ONE',
handle: 'one-blog',
avatar_url: '',
url: 'https://example.com',
provider: 'github',
role: 'admin',
is_owner: true
}
export const USERS = {
me: ME,
owner: OWNER,
lin: { id: 9, name: '林间', handle: 'linjian-dev', avatar_url: avatar('8a7f6b'), url: '', provider: 'google', role: 'reader', is_owner: false },
yuki: { id: 12, name: 'Yuki', handle: 'yuki0x0', avatar_url: avatar('6b8f9e'), url: 'https://yuki.example.dev', provider: 'github', role: 'member', is_owner: false },
awei: { id: 21, name: '阿伟', handle: '', avatar_url: '', url: '', provider: 'telegram', role: 'reader', is_owner: false },
mason: { id: 30, name: 'Mason', handle: 'masoncodes', avatar_url: avatar('7d6f9c'), url: '', provider: 'github', role: 'member', is_owner: false },
noli: { id: 44, name: 'noli', handle: '', avatar_url: '', url: '', provider: 'google', role: 'reader', is_owner: false },
tg: { id: 11, name: 'TG 用户', handle: '', avatar_url: '', url: '', provider: 'telegram', role: 'reader', is_owner: false }
}
// 旧名字留个别名,mock 主文件里还在用
export const GUEST = USERS.lin
export const TGUSER = USERS.tg
export const PROVIDERS = [
{ id: 'github', label: 'GitHub', kind: 'redirect' },
{ id: 'google', label: 'Google', kind: 'redirect' },
{ id: 'telegram', label: 'Telegram', kind: 'widget', login: 'example_bot' }
]
const MIN = 60e3
const HOUR = 60 * MIN
const DAY = 24 * HOUR
let seq = 100
export function mk({ author = AUTHORS.lin, html, ago = HOUR, state = 'published', ...rest }) {
const text = html || '<p>写点什么。</p>'
return {
id: ++seq,
parent_id: 0,
root_id: 0,
body_html: text,
body_md: text.replace(/<[^>]+>/g, ''),
state,
created_at: new Date(Date.now() - ago).toISOString(),
edited_at: '',
is_deleted: false,
reply_count: 0,
replies: [],
author,
...rest
}
}
export function reply(parent, opts = {}) {
const r = mk(opts)
r.parent_id = parent.id
r.root_id = parent.id
return r
}
// ---- 一屏的量:覆盖各种状态和排版(徽章 / 缺角气泡 / 代码 / 引用 / 列表 / 删除)----
export function baseItems() {
// 主楼:最长的讨论串(5 条回复、reply_count 同值,测「加载更多回复」的按钮态)
const c1 = mk({
author: USERS.lin,
ago: 6 * HOUR,
html:
'<p>三栏那个侧边目录会跟着滚动高亮,这个细节做得好。<code>IntersectionObserver</code> 还是算 scroll 位置?</p>' +
'<p>另外时间线里「更早 / 上一篇」这组标签,是文章自己的字段还是按位置算的?</p>'
})
c1.replies = [
reply(c1, { author: ME, ago: 5 * HOUR, html: '<p>算 scroll 位置,没用 observer —— 目录只有几条,没必要。</p>' }),
reply(c1, { author: OWNER, ago: 4 * HOUR + 20 * MIN, html: '<p>后面会换成 observer,等条目多了再说。</p>' }),
reply(c1, { author: USERS.yuki, ago: 3 * HOUR, html: '<p>标签是按位置算的:找到当前篇,前后各两篇贴上「上一篇 / 下一篇 / 更早 / 更新」。</p>' }),
reply(c1, { author: USERS.mason, ago: 2 * HOUR, html: '<p>学到了。</p>' }),
reply(c1, { author: USERS.noli, ago: 80 * MIN, html: '<p>同问 observer,楼上已经答了,收了。</p>' })
]
c1.reply_count = 8 // 只展开 5 条,剩 3 条走「加载更多回复」(thread 接口)
const c2 = mk({
author: USERS.yuki,
ago: 5 * HOUR,
html:
'<p>挑个刺:<code>.proj-grid</code> 的轨道下限要是裸 <code>300px</code>,320px 的屏会溢出吧?</p>' +
'<pre><code>grid-template-columns: repeat(auto-fill, minmax(min(300px,100%), 1fr));</code></pre>' +
'<p>这样写就稳了。</p>'
})
const c3 = mk({
author: OWNER,
ago: 2 * DAY,
html:
'<p>补一句:留白的尺度是 <strong>28 / 48 / 60</strong>,不要随手改成整十数,视觉节奏会散。</p>' +
'<blockquote><p>间距是设计的一部分,不是留白的一部分。</p></blockquote>'
})
const c4 = mk({
author: USERS.awei,
ago: 40 * HOUR,
html:
'<p>手机上那个 ☰ 菜单挺好用的。要是能把 <strong>主题切换</strong> 也放进去就完美了 —— 现在得退回顶栏才切得动。</p>' +
'<ul><li>时间线 / 归档 / 标签 / 作品 / 关于</li><li>底部:主题</li></ul>'
})
const c5 = mk({ author: USERS.noli, ago: 26 * HOUR, is_deleted: true, html: '' })
const c6 = mk({
author: ME,
ago: 20 * HOUR,
state: 'pending',
html: '<p>这条在等审核(<code>comment_review=1</code> 时会走这个状态),只有我自己看得见。</p>'
})
const c7 = mk({
author: USERS.tg,
ago: 14 * HOUR,
html: '<p>手机上那个 ☰ 菜单挺好用的。Telegram 登录什么时候开?</p>'
})
const c8 = mk({
author: USERS.mason,
ago: 8 * HOUR,
edited_at: new Date(Date.now() - 6 * HOUR).toISOString(),
html:
'<p>已编辑过的评论会带一个小标记。顺手贴个我抄走的 <code>color-mix</code> 用法:</p>' +
'<blockquote><p>color-mix(in srgb, var(--ink) 6%, transparent)</p></blockquote>'
})
return [c8, c7, c6, c2, c4, c5, c3, c1]
}
// ---- 长列表:把 baseItems 铺开再加一批,专门测「显示更多」分页 ----
export function longItems() {
const out = baseItems()
const lines = [
['收藏了,回头照着做一遍。', USERS.lin],
['暗色下那套 <code>--v-*</code> 是怎么对齐的?手调的还是工具生成的?', USERS.mason],
['正文行宽 728 是算出来的还是试出来的?', USERS.awei],
['首页那个「做过的东西」排版舒服。', USERS.yuki],
['<p>提个需求:评论能不能支持 <em>表情回应</em>?不用点赞那么复杂,几个 emoji 就够。</p>', USERS.awei],
['RSS 里能带上评论数吗?', USERS.noli],
['<p>照着把 <code>mobile.css</code> 的思路搬回我们项目了,一处收口确实比到处写媒体查询干净。</p>', USERS.mason],
['字体是思源宋体?正文里中英混排的字距很舒服。', USERS.lin],
['试了下 320px 宽,没横向滚动,赞。', USERS.yuki],
['<p>后台那个命令面板(<code>⌘K</code>)是现成的还是自己实现的?</p>', USERS.awei],
['归档页按年份分组比无限流舒服。', USERS.noli],
['希望以后能匿名评论 —— 不想为了一条评论去连 GitHub。', USERS.tg],
['<p>时间线那条渐变竖线:</p><pre><code>background: linear-gradient(180deg, var(--accent), transparent);</code></pre><p>stolen, thanks.</p>', USERS.yuki],
['文章里的首字下沉在暗色下会不会太跳?', USERS.mason],
['关注了。', USERS.lin],
['<p>标签页那组 <strong>写 / 设 / 篇 / 辑</strong> 的措辞是故意的吧,挺妙。</p>', USERS.awei],
['页脚那句引言换过几次了?', USERS.tg],
['<p>RSS 阅读器里看到的排版比网页还干净,罕见。</p>', USERS.yuki]
]
lines.forEach(([html, author], i) => {
out.push(mk({ author, ago: 2 * DAY + (i + 1) * 7 * HOUR, html }))
})
return out
}