// 评论 mock 的假数据(仅 dev,见 mocks/comments.js)。 // 字段要和后端 model.Comment 对齐 —— 真接口落地后,这里的差别就是前端要改的地方。 // 内容按真实博客评论的口吻写的:提问 / 挑错 / 补充 / 串门,别改成「好文,学习了」那种。 // 头像:内联 SVG(离线可用),每个作者一个色相,顺便把 那条分支走通 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 || '

写点什么。

' 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: '

三栏那个侧边目录会跟着滚动高亮,这个细节做得好。IntersectionObserver 还是算 scroll 位置?

' + '

另外时间线里「更早 / 上一篇」这组标签,是文章自己的字段还是按位置算的?

' }) c1.replies = [ reply(c1, { author: ME, ago: 5 * HOUR, html: '

算 scroll 位置,没用 observer —— 目录只有几条,没必要。

' }), reply(c1, { author: OWNER, ago: 4 * HOUR + 20 * MIN, html: '

后面会换成 observer,等条目多了再说。

' }), reply(c1, { author: USERS.yuki, ago: 3 * HOUR, html: '

标签是按位置算的:找到当前篇,前后各两篇贴上「上一篇 / 下一篇 / 更早 / 更新」。

' }), reply(c1, { author: USERS.mason, ago: 2 * HOUR, html: '

学到了。

' }), reply(c1, { author: USERS.noli, ago: 80 * MIN, html: '

同问 observer,楼上已经答了,收了。

' }) ] c1.reply_count = 8 // 只展开 5 条,剩 3 条走「加载更多回复」(thread 接口) const c2 = mk({ author: USERS.yuki, ago: 5 * HOUR, html: '

挑个刺:.proj-grid 的轨道下限要是裸 300px,320px 的屏会溢出吧?

' + '
grid-template-columns: repeat(auto-fill, minmax(min(300px,100%), 1fr));
' + '

这样写就稳了。

' }) const c3 = mk({ author: OWNER, ago: 2 * DAY, html: '

补一句:留白的尺度是 28 / 48 / 60,不要随手改成整十数,视觉节奏会散。

' + '

间距是设计的一部分,不是留白的一部分。

' }) const c4 = mk({ author: USERS.awei, ago: 40 * HOUR, html: '

手机上那个 ☰ 菜单挺好用的。要是能把 主题切换 也放进去就完美了 —— 现在得退回顶栏才切得动。

' + '' }) const c5 = mk({ author: USERS.noli, ago: 26 * HOUR, is_deleted: true, html: '' }) const c6 = mk({ author: ME, ago: 20 * HOUR, state: 'pending', html: '

这条在等审核(comment_review=1 时会走这个状态),只有我自己看得见。

' }) const c7 = mk({ author: USERS.tg, ago: 14 * HOUR, html: '

手机上那个 ☰ 菜单挺好用的。Telegram 登录什么时候开?

' }) const c8 = mk({ author: USERS.mason, ago: 8 * HOUR, edited_at: new Date(Date.now() - 6 * HOUR).toISOString(), html: '

已编辑过的评论会带一个小标记。顺手贴个我抄走的 color-mix 用法:

' + '

color-mix(in srgb, var(--ink) 6%, transparent)

' }) return [c8, c7, c6, c2, c4, c5, c3, c1] } // ---- 长列表:把 baseItems 铺开再加一批,专门测「显示更多」分页 ---- export function longItems() { const out = baseItems() const lines = [ ['收藏了,回头照着做一遍。', USERS.lin], ['暗色下那套 --v-* 是怎么对齐的?手调的还是工具生成的?', USERS.mason], ['正文行宽 728 是算出来的还是试出来的?', USERS.awei], ['首页那个「做过的东西」排版舒服。', USERS.yuki], ['

提个需求:评论能不能支持 表情回应?不用点赞那么复杂,几个 emoji 就够。

', USERS.awei], ['RSS 里能带上评论数吗?', USERS.noli], ['

照着把 mobile.css 的思路搬回我们项目了,一处收口确实比到处写媒体查询干净。

', USERS.mason], ['字体是思源宋体?正文里中英混排的字距很舒服。', USERS.lin], ['试了下 320px 宽,没横向滚动,赞。', USERS.yuki], ['

后台那个命令面板(⌘K)是现成的还是自己实现的?

', USERS.awei], ['归档页按年份分组比无限流舒服。', USERS.noli], ['希望以后能匿名评论 —— 不想为了一条评论去连 GitHub。', USERS.tg], ['

时间线那条渐变竖线:

background: linear-gradient(180deg, var(--accent), transparent);

stolen, thanks.

', USERS.yuki], ['文章里的首字下沉在暗色下会不会太跳?', USERS.mason], ['关注了。', USERS.lin], ['

标签页那组 写 / 设 / 篇 / 辑 的措辞是故意的吧,挺妙。

', USERS.awei], ['页脚那句引言换过几次了?', USERS.tg], ['

RSS 阅读器里看到的排版比网页还干净,罕见。

', USERS.yuki] ] lines.forEach(([html, author], i) => { out.push(mk({ author, ago: 2 * DAY + (i + 1) * 7 * HOUR, html })) }) return out }