Files
ONE/frontend/src/mocks/fixtures.js
T
Sakurasan 75737034d9 评论:去气泡改纸面排版 + 回复 @提及 + 双主题手机 12px 贴边
视觉:评论区从「带边框的聊天气泡盒」改成与去卡片后文章页一致的纸面排版
- .cm-bubble 类名保留(DOM 不动)盒子拆掉:正文直接落在纸面上,层级靠
  头像 / 名字行 / 留白撑起,条与条之间不画线不画盒;删除态靠变灰变斜认
- 输入区去卡片盒,只留 textarea 底下一条「书写线」,聚焦变强调色
  (vivid 再加同色光晕,与 .search 聚焦同一语言);名字 / 计数 / 发送
  排在线下方,像在纸上落款
- 操作钮 ✎ ✕ ↩ 圆图标 → 一行文字钮(回复/编辑/删除):桌面 hover 浮现、
  位置预留不跳版;触屏常显并加大可点面积;删除悬停给红;补 focus-visible 环
  (文字钮无边框,键盘可达性自己画)
- 回复串:空白缩进改左侧发丝线(与左栏时间线同一语言);26px 小头像不再
  戴登录方式小标(戴不下);登录引导卡去框,一行引导 + 一排登录圆钮
- 暗色代码块修复(.cm-md pre 与 .prose pre 同一 bug):背景
  color-mix(92% ink, accent) 在暗色下混出浅底浅字,暗端覆盖为
  color-mix(86% paper, accent) + 细边框

@提及
- 回复另一条回复时正文前带强调色 @对方名字,扁平列表里才看得出在跟谁
  说话;回主楼不标。名字由 parent_id 在已加载回复列表里解析,父回复
  未加载就不标 —— 后端阶段二契约不变,纯前端即可工作
- 浮动贴正文首行行首(与首字下沉同一技巧),字号行高与 .cm-md 成对同步
  (基础 / 手机断点 / vivid 三处,改字号要一起改);正文以代码块、列表
  开头时退成独立一行,避免被块级背景盖住
- 编辑态去掉「正在改定此评:」,交给 placeholder;mock 加 reply-to-reply
  用例(Mason 回 ONE)

手机 12px 贴边(两套主题正文与评论共用同一条左缘)
- vivid:.article-layout 手机断点 1fr → minmax(0,1fr)。裸 1fr 的自动最小
  尺寸是 min-content,正文里不可断行的长 token 把整列顶到 592px、整页
  横向溢出(实测 375 视口 bodyScrollWidth 612);页边距 20 → 12;
  .prose 加 overflow-wrap:break-word
- classic:.shell 手机 20 → 12(首页信息流同步收边),PostView .wrap
  ≤640 侧边距归零 —— 那 4px 正是正文比评论多出的一截
- 评论手机断点(styles.css 基础段,两套 UI 共用):头像 36→28、回复
  缩进收紧、排序 / 登录 / 操作钮 44px 触控目标、输入 16px 防 iOS
  聚焦缩放;.cm-md 加 overflow-wrap:break-word(用户内容长链接必须断行)

杂项
- vite dev 加 --host:手机连局域网可直接评审
- 实测(浏览器逐状态截图过目):vivid/classic × 亮/暗 × 桌面/390,
  默认 / 未登录 / 空 / 审核中 / 已删除 / 多级回复 / 回复框 / 编辑框
  共 13 张;375 视口 bodyScrollWidth = 375 无溢出;pnpm build 通过
2026-09-26 22:34:36 +08:00

188 lines
8.3 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>'
})
const r1 = reply(c1, { author: ME, ago: 5 * HOUR, html: '<p>算 scroll 位置,没用 observer —— 目录只有几条,没必要。</p>' })
const r2 = reply(c1, { author: OWNER, ago: 4 * HOUR + 20 * MIN, html: '<p>后面会换成 observer,等条目多了再说。</p>' })
// 回复「回复」:parent 指向另一条回复、root 仍指主楼,前端靠 @提及 认亲
const r2b = reply(r2, { author: USERS.mason, ago: 4 * HOUR, html: '<p>行,那我等 observer。顺手问下高亮会做吗?</p>' })
r2b.root_id = c1.id
c1.replies = [
r1,
r2,
r2b,
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
}