视觉:评论区从「带边框的聊天气泡盒」改成与去卡片后文章页一致的纸面排版 - .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 通过
188 lines
8.3 KiB
JavaScript
188 lines
8.3 KiB
JavaScript
// 评论 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
|
||
}
|