评论:去气泡改纸面排版 + 回复 @提及 + 双主题手机 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 通过
This commit is contained in:
Sakurasan
2026-09-26 22:34:36 +08:00
parent f29d8e053a
commit 75737034d9
8 changed files with 238 additions and 141 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"packageManager": "pnpm@10.34.4",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview"
},
@@ -8,11 +8,16 @@ import { relativeDate } from '../../utils'
// 单条评论。回复在同一个组件里自引用渲染(depth 0 → 1),
// 这样「回复框 / 编辑框」的模板只写一遍,两层不会各写一份再漂移。
// 只渲染一层:depth >= 1 时不再显示它自己的 replies 与「加载更多」。
//
// 回复之间的相互回复在扁平列表里靠 @提及 认亲:depth 0 渲染每条回复时
// 解析出它回的是谁(parent_id 指向主楼 → 不标;指向同串里另一条回复 →
// 把那条的作者名传下去)。父回复没被加载进来时解析不到,就不标。
const props = defineProps({
c: { type: Object, required: true },
api: { type: Object, required: true },
depth: { type: Number, default: 0 },
maxLen: { type: Number, default: 500 }
maxLen: { type: Number, default: 500 },
replyTo: { type: String, default: '' }
})
const s = props.api.s
@@ -77,6 +82,19 @@ async function onRemove() {
const moreReplies = computed(() =>
props.depth === 0 ? Math.max(0, (props.c.reply_count || 0) - ((props.c.replies || []).length)) : 0
)
// 这条回复要 @ 谁:parent 指向主楼(props.c)不用标;指向串里另一条
// 已加载的回复才标。正文以块级元素开头(代码块/列表)时 @ 没法浮在
// 首行行首,模板里会退成独立一行。
const atInline = computed(
() => !!props.replyTo && /^<p[\s>]/i.test((props.c.body_html || '').trim())
)
function replyNameOf(r) {
if (!r.parent_id || r.parent_id === props.c.id) return ''
const t = (props.c.replies || []).find((x) => x.id === r.parent_id)
return t ? (t.author && t.author.name) || '' : ''
}
</script>
<template>
@@ -112,10 +130,10 @@ const moreReplies = computed(() =>
<span v-if="c.edited_at" class="cm-edited-tip">已编辑</span>
</div>
<!-- 编辑态:把气泡换成输入框 -->
<!-- 编辑态:把正文换成输入框 -->
<div v-if="editing" class="cm-field cm-field-inline">
<p class="cm-field-tip">正在改定此评:</p>
<textarea v-model="editDraft" class="cm-input" rows="3" :maxlength="maxLen" aria-label="编辑评论" />
<textarea v-model="editDraft" class="cm-input" rows="3" :maxlength="maxLen"
placeholder="编辑这条评论…" aria-label="编辑评论" />
<div class="cm-bar">
<span class="cm-bar-l" />
<span class="cm-bar-r">
@@ -129,12 +147,17 @@ const moreReplies = computed(() =>
<div v-else class="cm-bubble">
<span v-if="c.is_deleted" class="cm-gone">该评论已删除</span>
<CommentMarkdown v-else :html="c.body_html" />
<template v-else>
<!-- 回复回复时带上 @对方,扁平列表里才看得出在跟谁说话 -->
<span v-if="replyTo && atInline" class="cm-at">@{{ replyTo }}</span>
<p v-else-if="replyTo" class="cm-at cm-at-block">@{{ replyTo }}</p>
<CommentMarkdown :html="c.body_html" />
</template>
<div v-if="!c.is_deleted" class="cm-acts">
<button v-if="canEdit" type="button" class="cm-act" aria-label="编辑" title="编辑" @click="startEdit">✎</button>
<button v-if="canDelete" type="button" class="cm-act" aria-label="删除" title="删除" @click="onRemove">✕</button>
<button v-if="reader.user" type="button" class="cm-act" aria-label="回复" title="回复" @click="startReply">↩</button>
<button v-if="reader.user" type="button" class="cm-act" @click="startReply">回复</button>
<button v-if="canEdit" type="button" class="cm-act" @click="startEdit">编辑</button>
<button v-if="canDelete" type="button" class="cm-act is-danger" @click="onRemove">删除</button>
</div>
</div>
@@ -168,6 +191,7 @@ const moreReplies = computed(() =>
:api="api"
:depth="depth + 1"
:max-len="maxLen"
:reply-to="replyNameOf(r)"
/>
</ul>
<button v-if="moreReplies > 0" type="button" class="cm-more-r" @click="api.loadMoreReplies(c)">
+8 -2
View File
@@ -90,9 +90,15 @@ export function baseItems() {
'<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 = [
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>' }),
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>' })
+137 -76
View File
@@ -129,6 +129,11 @@ h1, h2, h3, h4 {font-family: var(--serif);
.layout {grid-template-columns: minmax(0, 1fr);}
.layout > .rail-left {display: none;}
}
@media (max-width: 640px) {
/* 手机页边距收到 12px:与评论区(.cm-sec 手机断点)和余白主题的文章列对齐,
正文和评论共用同一条左缘(文章 .wrap 的侧边距在 PostView.vue 里归零) */
.shell {padding: 0 12px;}
}
.eyebrow {font-size: 12px;
letter-spacing: 0.16em;
text-transform: uppercase;
@@ -1097,108 +1102,120 @@ a,
评论区
--------------------------------------------------------------------------
classic 和 vivid 共用同一份 DOM(components/comments/)。
视觉语言与去卡片后的文章页一致:评论正文直接落在纸面上,
不再套带边框的气泡盒 —— 层级靠头像 / 名字行 / 留白撑起来。
这里只写「两边都要」的基础,用经典那套 token —— vivid 把那批名字
(--paper/--card/--ink/--accent/--line/--paper-sunken…)都别名到了 --v-*,
所以同一份规则在两套 UI 下都成立。
vivid 的差异(缺角气泡、渐变底、头像 ring、触屏尺寸)在
vivid 的差异(头像配色、代码块、弹簧手感、触屏尺寸)在
ui/yohaku/comments.css,那边特指度更高,会盖住这里。
========================================================================== */
/* 头部只放计数 + 排序。刻意不写「评论」二字标题:参考站也是这样,
靠计数和排序控件就能认出这是评论区,多一行标题只是噪音。 */
/* 头部只放计数 + 排序。刻意不写「评论」二字标题:靠计数和排序控件
就能认出这是评论区,多一行标题只是噪音。 */
.cm-sec {margin-top: 28px;padding: 22px 26px 0;border-top: 1px solid var(--line);}
.cm-head {display: flex;align-items: center;justify-content: space-between;
gap: 12px;flex-wrap: wrap;margin-bottom: 20px;}
gap: 12px;flex-wrap: wrap;margin-bottom: 24px;}
.cm-count {font-size: 13px;font-weight: 600;color: var(--muted);
font-variant-numeric: tabular-nums;}
/* classic 下是一排文字钮;vivid 里这个元素还带 .seg,会被 pill 化 */
.cm-sort {display: inline-flex;align-items: center;gap: 2px;}
.cm-sort button {font-size: 13px;font-weight: 600;color: var(--muted);
padding: 5px 10px;border: 0;border-radius: 999px;background: none;cursor: pointer;}
padding: 5px 10px;border: 0;border-radius: 999px;background: none;cursor: pointer;
transition: color .2s,background-color .2s;}
.cm-sort button.on {color: var(--accent);background: var(--accent-soft);}
/* 未登录:登录引导卡 */
.cm-gate {padding: 22px;border: 1px solid var(--line);border-radius: 12px;
background: var(--card);text-align: center;}
.cm-gate-skel {height: 104px;background: var(--paper-sunken);border-style: dashed;}
.cm-gate-t {margin: 0 0 14px;font-size: 13px;color: var(--muted);}
/* 未登录:不画卡片,一行引导 + 一排登录方式,直接落在纸面上 */
.cm-gate {padding: 26px 0 4px;text-align: center;}
.cm-gate-skel {height: 96px;background: var(--paper-sunken);border-radius: 12px;}
.cm-gate-t {margin: 0 0 16px;font-size: 13px;color: var(--muted);}
.cm-gate-hint {margin: 0;font-size: 13px;color: var(--muted);}
.cm-providers {display: flex;justify-content: center;gap: 12px;
.cm-providers {display: flex;justify-content: center;gap: 14px;
margin: 0;padding: 0;list-style: none;}
.cm-provider {display: grid;place-items: center;width: 40px;height: 40px;
border: 1px solid var(--line);border-radius: 50%;background: var(--card);color: var(--ink);
.cm-provider {display: grid;place-items: center;width: 42px;height: 42px;
border: 1px solid var(--line);border-radius: 50%;background: none;color: var(--muted);
cursor: pointer;transition: border-color .2s,color .2s;}
.cm-provider:hover {border-color: var(--accent);color: var(--accent);}
.cm-brand {width: 18px;height: 18px;fill: currentColor;}
.cm-brand-t {font-size: 15px;font-weight: 700;}
.cm-tg {margin-top: 12px;}
.cm-tg {margin-top: 14px;}
/* 输入区 */
/* 输入区:没有盒子,只有 textarea 底下的一条「书写线」,聚焦时变强调色。
名字 / 计数 / 发送排在线下方,像在纸上落款。 */
.cm-compose {display: flex;gap: 12px;align-items: flex-start;}
.cm-av-me {width: 36px;height: 36px;}
.cm-field {flex: 1;min-width: 0;overflow: hidden;
border: 1px solid var(--line);border-radius: 12px;background: var(--card);}
.cm-field:focus-within {border-color: var(--accent);}
.cm-field-inline {margin-top: 10px;}
.cm-field-tip {margin: 0;padding: 10px 12px 0;font-size: 13px;color: var(--muted);}
.cm-input {display: block;width: 100%;min-height: 72px;padding: 10px 12px;
border: 0;background: none;resize: vertical;
font: inherit;font-size: 14px;line-height: 1.7;color: var(--ink);}
.cm-field {flex: 1;min-width: 0;}
.cm-field-inline {margin-top: 12px;}
.cm-input {display: block;width: 100%;min-height: 68px;padding: 4px 0 10px;
border: 0;border-bottom: 1px solid var(--line);background: none;resize: vertical;
font: inherit;font-size: 14.5px;line-height: 1.75;color: var(--ink);
transition: border-color .2s;}
.cm-input:focus {outline: none;}
.cm-field:focus-within .cm-input {border-bottom-color: var(--accent);}
.cm-bar {display: flex;align-items: center;justify-content: space-between;gap: 10px;
padding: 6px 10px;border-top: 1px solid var(--line-soft);
font-size: 13px;color: var(--muted);}
.cm-bar-l {min-width: 0;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}
.cm-bar-r {display: flex;flex: none;align-items: center;gap: 10px;}
.cm-len {font-variant-numeric: tabular-nums;}
padding: 8px 0 0;font-size: 12.5px;color: var(--muted);}
.cm-bar-l {display: flex;align-items: center;gap: 8px;min-width: 0;
overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}
.cm-bar-l b {font-weight: 600;color: var(--ink-soft);}
.cm-bar-r {display: flex;flex: none;align-items: center;gap: 12px;}
.cm-len {font-size: 12px;font-variant-numeric: tabular-nums;}
.cm-len.over {color: #a64953;}
.cm-link {padding: 0;border: 0;background: none;color: var(--muted);
font-size: 13px;cursor: pointer;}
.cm-link {padding: 4px 2px;border: 0;background: none;color: var(--muted);
font-size: 12.5px;cursor: pointer;transition: color .2s;}
.cm-link:hover {color: var(--accent);}
.cm-send {padding: 5px 14px;border: 0;border-radius: 999px;cursor: pointer;
background: var(--accent);color: #fff;font-size: 13px;font-weight: 600;}
.cm-send:disabled {opacity: .45;cursor: default;}
.cm-send {padding: 6px 18px;border: 0;border-radius: 999px;cursor: pointer;
background: var(--accent);color: #fff;font-size: 13px;font-weight: 600;
transition: opacity .2s;}
.cm-send:disabled {opacity: .4;cursor: default;}
.cm-err {margin: 14px 0 0;font-size: 13px;color: #a64953;}
/* 列表 */
.cm-list {display: flex;flex-direction: column;gap: 18px;
margin: 22px 0 0;padding: 0;list-style: none;}
/* 列表:条与条之间靠留白分隔,不画线不画盒 */
.cm-list {display: flex;flex-direction: column;gap: 28px;
margin: 26px 0 0;padding: 0;list-style: none;}
.cm-item {display: flex;gap: 12px;align-items: flex-start;}
.cm-av {position: relative;display: grid;place-items: center;flex: none;
width: 36px;height: 36px;border-radius: 50%;
background: var(--paper-sunken);color: var(--muted);
font-size: 14px;font-weight: 700;
box-shadow: 0 0 0 2px var(--paper);}
font-size: 14px;font-weight: 700;}
.cm-av img {display: block;width: 100%;height: 100%;border-radius: 50%;object-fit: cover;}
.cm-av.is-owner {box-shadow: 0 0 0 2px var(--accent-soft);}
/* 头像角上的登录方式小标 */
.cm-pv {position: absolute;right: -2px;bottom: -2px;
display: grid;place-items: center;width: 14px;height: 14px;border-radius: 50%;
/* 头像角上的登录方式小标:回复的 26px 头像戴不下,只在顶层显示 */
.cm-pv {position: absolute;right: -3px;bottom: -3px;
display: grid;place-items: center;width: 15px;height: 15px;border-radius: 50%;
background: var(--card);color: var(--muted);font-style: normal;
box-shadow: 0 0 0 1.5px var(--line);}
.cm-pv svg {width: 8px;height: 8px;fill: currentColor;}
.cm-replies .cm-pv {display: none;}
.cm-main {flex: 1;min-width: 0;}
.cm-name {display: flex;align-items: center;gap: 8px;flex-wrap: wrap;margin-bottom: 6px;}
.cm-name-a {font-size: 13.5px;font-weight: 600;color: var(--ink);}
/* 名字行:名字、徽标、时间共用一条基线 */
.cm-name {display: flex;align-items: baseline;gap: 8px;flex-wrap: wrap;
row-gap: 2px;margin-bottom: 6px;}
.cm-name-a {font-size: 14px;font-weight: 600;color: var(--ink);}
a.cm-name-a:hover {color: var(--accent);}
.cm-owner {padding: 1px 6px;border-radius: 999px;background: var(--accent-soft);
.cm-owner {padding: 1px 7px;border-radius: 999px;background: var(--accent-soft);
color: var(--accent);font-size: 11px;font-weight: 600;}
.cm-time {font-size: 11.5px;color: var(--muted);font-variant-numeric: tabular-nums;}
.cm-pending {padding: 1px 6px;border-radius: 999px;background: var(--paper-sunken);
.cm-time {font-size: 12px;color: var(--faint);font-variant-numeric: tabular-nums;}
.cm-pending {padding: 1px 7px;border-radius: 999px;background: var(--paper-sunken);
color: var(--muted);font-size: 11px;font-weight: 600;}
.cm-edited-tip {font-size: 11px;color: var(--muted);opacity: .75;}
.cm-edited-tip {font-size: 11.5px;color: var(--faint);}
/* 气泡:左上角不收圆(缺口朝头像),是这套评论的形状特征 */
.cm-bubble {position: relative;display: inline-block;
max-width: calc(100% - 3rem);padding: 8px 12px;
border: 1px solid var(--line);border-radius: 12px;border-top-left-radius: 3px;
background: var(--card);color: var(--ink);}
.cm-item.is-reply .cm-bubble {border-top-left-radius: 12px;}
.cm-item.is-deleted .cm-bubble {background: none;border-style: dashed;}
.cm-gone {font-size: 13px;color: var(--muted);font-style: italic;}
/* 正文:类名保留 .cm-bubble(DOM 不动),盒子拆掉 —— 和文章正文同一种「纸」。
删除态不给盒子,靠变灰变斜认。 */
.cm-bubble {display: block;padding: 0;border: 0;background: none;color: var(--ink);}
.cm-item.is-deleted .cm-bubble {color: var(--muted);}
.cm-gone {font-size: 13.5px;color: var(--muted);font-style: italic;}
/* 正文(走 CommentMarkdown,后端已渲染 + 前端 DOMPurify 再洗一遍) */
.cm-md {font-size: 14px;line-height: 1.65;}
/* 正文(走 CommentMarkdown,后端已渲染 + 前端 DOMPurify 再洗一遍)。
@提及 和 .cm-md 共用字号行高:float 贴首行时两者行盒必须一样高,
三处成对存在(基础 / 手机断点 / vivid),改字号要一起改。 */
.cm-md, .cm-at {font-size: 14.5px;line-height: 1.75;}
.cm-md {
/* 评论是用户生成内容,长链接/长代码串必须在行宽内断行 */
overflow-wrap: break-word;}
/* 回复回复时的 @对方:浮在正文首行行首(和首字下沉同一个技巧),
不占一整行;正文以代码块/列表开头时由 .cm-at-block 退成独立一行 */
.cm-at {float: left;margin: 0 .45em 0 0;color: var(--accent);font-weight: 600;}
.cm-at-block {float: none;margin: 0 0 4px;}
.cm-md > :first-child {margin-top: 0;}
.cm-md > :last-child {margin-bottom: 0;}
.cm-md p {margin: 0 0 .6em;}
@@ -1209,37 +1226,81 @@ a.cm-name-a:hover {color: var(--accent);}
background: var(--paper-sunken);}
.cm-md pre code {padding: 0;background: none;}
.cm-md ul, .cm-md ol {margin: .5em 0;padding-left: 1.4em;}
.cm-md blockquote {margin: .6em 0;padding-left: 10px;border-left: 2px solid var(--line);
.cm-md blockquote {margin: .6em 0;padding-left: 12px;border-left: 2px solid var(--line);
color: var(--muted);}
.cm-md img {max-width: 100%;border-radius: 6px;}
/* 编辑 / 删除 / 回复:hover 或键盘聚焦才出现,触屏直接常显 */
.cm-acts {position: absolute;right: 0;bottom: -10px;display: flex;gap: 4px;
/* 操作:一行安静的文字钮(回复 / 编辑 / 删除)。桌面 hover 或键盘聚焦
才浮现,位置预留、不跳版;触屏常显、加高可点面积。删除悬停给红。 */
.cm-acts {display: flex;gap: 2px;margin-top: 2px;
opacity: 0;transition: opacity .2s;}
.cm-item:hover .cm-acts, .cm-item:focus-within .cm-acts {opacity: 1;}
@media (hover: none) {
.cm-acts {opacity: 1;}
}
.cm-act {display: grid;place-items: center;width: 24px;height: 24px;
border: 1px solid var(--line);border-radius: 50%;background: var(--card);
color: var(--muted);font-size: 12px;cursor: pointer;}
.cm-act:hover {border-color: var(--accent);color: var(--accent);}
.cm-act {padding: 5px 8px;border: 0;background: none;color: var(--muted);
font-size: 12.5px;font-weight: 600;cursor: pointer;transition: color .2s;}
.cm-act:first-child {margin-left: -8px;}
.cm-act:hover {color: var(--accent);}
.cm-act.is-danger:hover {color: #a64953;}
/* 键盘可达:文字钮没有边框,聚焦环自己画(鼠标点击不触发 focus-visible) */
.cm-act:focus-visible, .cm-link:focus-visible, .cm-more-r:focus-visible,
.cm-sort button:focus-visible, .cm-provider:focus-visible {
outline: 2px solid var(--accent-line);outline-offset: 2px;border-radius: 6px;}
.cm-send:focus-visible {outline: 2px solid var(--accent-line);outline-offset: 2px;}
/* 回复只有一层,缩进 36px */
.cm-replies {display: flex;flex-direction: column;gap: 14px;
margin: 14px 0 0;padding: 0 0 0 36px;list-style: none;}
.cm-replies .cm-av {width: 28px;height: 28px;font-size: 12px;}
.cm-more-r {margin: 10px 0 0 36px;padding: 4px 12px;border: 1px solid var(--line);
border-radius: 999px;background: none;color: var(--muted);font-size: 12px;cursor: pointer;}
.cm-more-r:hover {border-color: var(--accent);color: var(--accent);}
/* 回复:只有一层。左侧一条发丝线把回复串起来(和左栏时间线同一语言),
线从第一条回复头顶到末条脚下,比空白缩进更有「同属一条」的指向。 */
.cm-replies {display: flex;flex-direction: column;gap: 18px;
margin: 14px 0 0;padding: 0 0 2px 18px;list-style: none;
border-left: 1px solid var(--line);}
.cm-replies .cm-av {width: 26px;height: 26px;font-size: 11.5px;}
.cm-more-r {margin: 12px 0 0;padding: 5px 0;border: 0;background: none;
color: var(--muted);font-size: 12.5px;font-weight: 600;cursor: pointer;
transition: color .2s;}
.cm-more-r:hover {color: var(--accent);}
/* 分页 / 空态 */
.cm-more {display: flex;justify-content: center;margin-top: 22px;}
.cm-empty {margin-top: 22px;}
.cm-more {display: flex;justify-content: center;margin-top: 26px;}
.cm-empty {margin-top: 26px;}
/* 骨架 */
/* 骨架:和真实列表同一副骨架(头像 + 两行),落位一致才不闪 */
.cm-skel-row {display: flex;gap: 12px;align-items: flex-start;}
.cm-skel-av {flex: none;width: 36px;height: 36px;border-radius: 50%;background: var(--paper-sunken);}
.cm-skel-lines {display: flex;flex: 1;flex-direction: column;gap: 8px;}
.cm-skel-lines {display: flex;flex: 1;flex-direction: column;gap: 8px;padding-top: 3px;}
.cm-skel-lines i {display: block;height: 12px;border-radius: 6px;background: var(--paper-sunken);}
.cm-skel-lines i:last-child {width: 55%;}
/* --------------------------------------------------------------------------
手机(≤640):评论是纯阅读 + 触控场景 —— 缩头像、收紧缩进、
可点目标加高、输入 16px 防 iOS 聚焦缩放。
页边距与 .article-layout 的手机页边距(12px,见 mobile.css)对齐,
正文和评论共用同一条左缘,头像离屏幕边 12px。
-------------------------------------------------------------------------- */
@media (max-width: 640px) {
.cm-sec {padding: 20px 0 0;}
.cm-head {margin-bottom: 18px;}
.cm-list {gap: 22px;margin-top: 20px;}
.cm-item {gap: 10px;}
.cm-av, .cm-av-me {width: 28px;height: 28px;font-size: 12px;}
.cm-name-a {font-size: 13.5px;}
.cm-md, .cm-at {font-size: 14px;line-height: 1.7;}
.cm-replies {padding-left: 14px;gap: 14px;}
.cm-replies .cm-av {width: 24px;height: 24px;font-size: 10.5px;}
.cm-act {padding: 8px 10px;}
.cm-act:first-child {margin-left: -10px;}
.cm-sort button {min-height: 44px;}
.cm-provider {width: 44px;height: 44px;}
.cm-compose {gap: 10px;}
.cm-input {font-size: 16px;}
.cm-send {padding: 9px 18px;}
.cm-link {padding: 8px 2px;}
}
/* 触屏(可能是平板/桌面触屏,不限于窄屏):可点目标给足 */
@media (pointer: coarse) {
.cm-provider {width: 44px;height: 44px;}
.cm-act {padding: 8px 10px;}
.cm-link {padding: 8px 2px;}
.cm-more-r {padding: 10px 0;}
.cm-input {font-size: 16px;}
}
+11 -2
View File
@@ -3,7 +3,10 @@ html[data-ui='vivid'] .article-layout {max-width:1100px;margin:0 auto;padding:24
grid-template-columns:140px minmax(0,1fr) 180px;
gap:24px;align-items:start;}
@media (max-width:960px) {
html[data-ui='vivid'] .article-layout {grid-template-columns:1fr;gap:0}
/* 1fr 的自动最小尺寸是 min-content:正文里出现不可断行的长 token
(长内联代码/链接)时轨道会被顶到 592px+,整页横向溢出。
minmax(0,1fr) 和 classic 的 .layout 同一写法,把最小尺寸钉死为 0。 */
html[data-ui='vivid'] .article-layout {grid-template-columns:minmax(0,1fr);gap:0}
html[data-ui='vivid'] .side-l, html[data-ui='vivid'] .side-r {display:none}
html[data-ui='vivid'] .progress-pct {display:none}
}
@@ -61,7 +64,9 @@ html[data-ui='vivid'] .paper > .dek {margin:0 0 32px;font-size:17px;line-height:
font-family:var(--serif);}
html[data-ui='vivid'] .prose {font-size:16.5px;line-height:1.95;color:var(--text);max-width:var(--measure);
/* 中英混排自动补一点字距(Chrome 120+ 的 text-autospace)。不支持就退回现在的样子。 */
text-autospace:normal;}
text-autospace:normal;
/* 长 token(URL/内联代码)超出行宽时在任意字符处断行,别把列撑破 */
overflow-wrap:break-word;}
/* 段距上下各 1.5em(参考 .v10dui0 .rich-paragraph),首末段把外边距吃掉,
否则标题/分隔线上方会多出一段空白 */
html[data-ui='vivid'] .prose p {margin:1.5em 0}
@@ -93,6 +98,10 @@ html[data-ui='vivid'] .prose pre {background:color-mix(in srgb,var(--ink) 92%,va
color:#e8e8f0;padding:18px 20px;border-radius:12px;
overflow-x:auto;font-size:13.5px;line-height:1.7;
box-shadow:var(--shadow-lg);}
/* 暗色下「92% ink」混出的是浅底,浅底配浅字没法读;暗端换深底微混强调色 */
html[data-ui='vivid'][data-theme='vivid-dark'] .prose pre {
background:color-mix(in srgb,var(--paper) 86%,var(--accent));
border:1px solid var(--line);box-shadow:none;}
html[data-ui='vivid'] .prose pre code {background:none;padding:0;color:inherit;font-size:inherit}
html[data-ui='vivid'] .prose blockquote {margin:1.8em 0;padding:16px 20px;
background:var(--accent-soft);
+36 -49
View File
@@ -2,8 +2,9 @@
评论区:vivid(余白)的差异部分
--------------------------------------------------------------------------
基础样式在 styles.css 的 .cm-*(classic 和 vivid 共用同一份 DOM)。
这里只写 vivid 和 classic 长得不一样的地方 —— 全部带 html[data-ui='vivid']
前缀,特指度比基础版高,天然盖住它。
基础版已经是「去气泡」的编辑式排版:正文落在纸面上、回复串在一条
发丝线上、操作是一行文字钮。这里只写 vivid 和 classic 长得不一样的地方
—— 全部带 html[data-ui='vivid'] 前缀,特指度比基础版高,天然盖住它。
自己的移动端覆盖也写在下面(不要挪进 mobile.css:那是「靠最后导入赢」,
语义上不该承担某个具体组件的窄屏规则)。
========================================================================== */
@@ -15,28 +16,22 @@ html[data-ui='vivid'] .cm-sec {margin-top: 8px;padding: 24px 24px 0;border-top:
html[data-ui='vivid'] .cm-count {font-size: 13px;color: var(--muted);letter-spacing: .01em;}
/* .cm-sort 在 vivid 里同时带 .seg 类,pill 外观由 home.css 的 .seg 规则给 */
/* 未登录:一张安静的纸片,用和输入框同一套「场地」观感 */
html[data-ui='vivid'] .cm-gate {padding: 26px 22px;border-radius: var(--radius-lg);
border-color: var(--line);background: var(--card);box-shadow: var(--shadow);}
html[data-ui='vivid'] .cm-gate-skel {box-shadow: none;border-radius: var(--radius);}
html[data-ui='vivid'] .cm-provider {width: 40px;height: 40px;border-color: var(--line);
background: var(--card);color: var(--soft);
/* 未登录:基础版已经不画卡片,这里只给登录钮弹簧手感 */
html[data-ui='vivid'] .cm-provider {border-color: var(--line);color: var(--soft);
transition: border-color .3s var(--spring),color .3s var(--spring),transform .5s var(--spring);}
@media (hover: hover) and (pointer: fine) {
html[data-ui='vivid'] .cm-provider:hover {border-color: var(--accent);color: var(--accent);
transform: translateY(-2px);}
}
/* 输入框:vivid 的 .search / .seg 是同一套「无边框 + 聚焦才亮」的思路 */
html[data-ui='vivid'] .cm-field {border-radius: var(--radius);background: var(--card);
box-shadow: var(--shadow);
transition: border-color .3s var(--ease),box-shadow .3s var(--ease);}
html[data-ui='vivid'] .cm-field:focus-within {border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);}
/* 输入区:基础版的「书写线」在 vivid 下聚焦时除了变强调色,再给一点同色光,
和 .search 聚焦时的 ring 是同一种「亮起来」的语言 */
html[data-ui='vivid'] .cm-input {font-size: 14.5px;line-height: 1.8;}
html[data-ui='vivid'] .cm-bar {border-top-color: var(--line);}
html[data-ui='vivid'] .cm-field:focus-within .cm-input {border-bottom-color: var(--accent);
box-shadow: 0 10px 24px -18px var(--accent-glow);}
html[data-ui='vivid'] .cm-bar-l b {color: var(--soft);}
html[data-ui='vivid'] .cm-send {background: var(--accent);color: #fff;
transition: transform .5s var(--spring),box-shadow .3s;}
transition: transform .5s var(--spring),box-shadow .3s,opacity .2s;}
@media (hover: hover) and (pointer: fine) {
html[data-ui='vivid'] .cm-send:not(:disabled):hover {transform: scale(1.04);
box-shadow: 0 4px 12px var(--accent-glow);}
@@ -44,13 +39,12 @@ html[data-ui='vivid'] .cm-send {background: var(--accent);color: #fff;
html[data-ui='vivid'] .cm-send:active {transform: scale(.97);}
/* 列表 */
html[data-ui='vivid'] .cm-list {gap: 20px;}
html[data-ui='vivid'] .cm-av {width: 36px;height: 36px;background: var(--accent-soft);color: var(--accent);
box-shadow: 0 0 0 2px var(--paper);}
html[data-ui='vivid'] .cm-list {gap: 24px;}
html[data-ui='vivid'] .cm-av {width: 36px;height: 36px;background: var(--accent-soft);color: var(--accent);}
/* 站长的头像 ring 用强调色,一眼能认出来 */
html[data-ui='vivid'] .cm-av.is-owner {box-shadow: 0 0 0 2px var(--paper),0 0 0 3.5px var(--accent-soft);}
html[data-ui='vivid'] .cm-pv {box-shadow: 0 0 0 1.5px var(--paper);color: var(--muted);}
html[data-ui='vivid'] .cm-name-a {font-size: 13.5px;color: var(--text);letter-spacing: .01em;}
html[data-ui='vivid'] .cm-name-a {font-size: 14px;color: var(--text);letter-spacing: .01em;}
/* tokens.css 的 a 重置(html[data-ui=vivid] a,特指度 0,1,2)会把链接名拉回
inherit —— 继承到的是 body 的颜色,暗色下正好是深底深字。得在这里压回去。 */
html[data-ui='vivid'] a.cm-name-a {color: var(--text);}
@@ -61,13 +55,10 @@ html[data-ui='vivid'] .cm-owner {background: var(--accent-soft);color: var(--acc
html[data-ui='vivid'] .cm-time {letter-spacing: .02em;}
html[data-ui='vivid'] .cm-pending {background: color-mix(in srgb,var(--ink) 6%,transparent);}
/* 气泡:vivid 里用「场地」那套(细边 + 顶上一点渐变),缺角保留 */
html[data-ui='vivid'] .cm-bubble {padding: 9px 14px;border-radius: var(--radius-lg);
border-top-left-radius: 3px;border-color: var(--line);background: var(--card);
color: var(--text);
background-image: linear-gradient(180deg,var(--accent-soft) 0,transparent 26%);}
html[data-ui='vivid'] .cm-item.is-reply .cm-bubble {border-top-left-radius: var(--radius-lg);}
html[data-ui='vivid'] .cm-md {font-size: 14.5px;line-height: 1.75;}
/* 正文落在纸面上;vivid 只接管 Markdown 内的元素配色 */
html[data-ui='vivid'] .cm-bubble {color: var(--text);}
/* cm-at 的字号行高跟 .cm-md 成对改(见 styles.css 里那条注释) */
html[data-ui='vivid'] .cm-md, html[data-ui='vivid'] .cm-at {font-size: 14.5px;line-height: 1.8;}
html[data-ui='vivid'] .cm-md a {text-decoration: underline;
text-decoration-color: color-mix(in srgb,var(--accent) 40%,transparent);
text-underline-offset: 2px;}
@@ -78,41 +69,37 @@ html[data-ui='vivid'] .cm-md code {background: var(--accent-soft);color: var(--a
html[data-ui='vivid'] .cm-md pre {background: color-mix(in srgb,var(--ink) 92%,var(--accent));
color: #e8e8f0;box-shadow: var(--shadow-lg);}
html[data-ui='vivid'] .cm-md pre code {background: none;color: inherit;}
/* 暗色下上面的「92% ink」混出的是浅底,配上浅字就没法读了;
暗端改成深底微混强调色,字色不变 */
html[data-ui='vivid'][data-theme='vivid-dark'] .cm-md pre {
background: color-mix(in srgb,var(--paper) 86%,var(--accent));
border: 1px solid var(--line);box-shadow: none;}
html[data-ui='vivid'] .cm-md blockquote {border-left-color: var(--accent);
font-family: var(--serif);font-style: italic;}
/* 操作钮 */
html[data-ui='vivid'] .cm-act {border-color: var(--line);background: var(--card);color: var(--muted);
transition: border-color .3s var(--spring),color .3s var(--spring),transform .5s var(--spring);}
/* 操作钮:文字钮继承基础版,vivid 给弹簧手感;删除悬停同样给红 */
@media (hover: hover) and (pointer: fine) {
html[data-ui='vivid'] .cm-act:hover {border-color: var(--accent);color: var(--accent);
transform: translateY(-1px);}
html[data-ui='vivid'] .cm-act {transition: color .3s var(--spring);}
html[data-ui='vivid'] .cm-act:hover {color: var(--accent);}
}
/* 回复 */
html[data-ui='vivid'] .cm-more-r {border-color: var(--line);color: var(--muted);}
/* 回复线与「加载更多回复」 */
html[data-ui='vivid'] .cm-replies {border-left-color: var(--line);}
html[data-ui='vivid'] .cm-more-r {color: var(--muted);}
html[data-ui='vivid'] .cm-empty .chip {font-size: 13px;padding: 9px 22px;}
/* --------------------------------------------------------------------------
手机:头像 24px(桌面 36px),触屏的可点目标 44px、输入框 16px(防聚焦缩放)
手机:基础版已经把头像/缩进/可点目标收过一轮,这里只补 vivid 自己的
页边距对齐(.cm-sec 桌面是 24px,手机跟 .article-layout 的 20px 走)
-------------------------------------------------------------------------- */
@media (max-width:640px) {
html[data-ui='vivid'] .cm-sec {margin-top: 4px;padding: 20px 20px 0;}
html[data-ui='vivid'] .cm-sec {margin-top: 4px;padding: 20px 0 0;}
html[data-ui='vivid'] .cm-list {gap: 20px;}
html[data-ui='vivid'] .cm-av,
html[data-ui='vivid'] .cm-av-me {width: 24px;height: 24px;font-size: 11px;}
html[data-ui='vivid'] .cm-replies {padding-left: 20px;}
html[data-ui='vivid'] .cm-more-r {margin-left: 20px;}
html[data-ui='vivid'] .cm-bubble {max-width: calc(100% - 1rem);}
html[data-ui='vivid'] .cm-provider {width: 44px;height: 44px;}
html[data-ui='vivid'] .cm-act {width: 32px;height: 32px;font-size: 13px;}
html[data-ui='vivid'] .cm-sort button {min-height: 44px;}
html[data-ui='vivid'] .cm-input {font-size: 16px;}
html[data-ui='vivid'] .cm-av-me {width: 28px;height: 28px;font-size: 12px;}
html[data-ui='vivid'] .cm-replies .cm-av {width: 24px;height: 24px;font-size: 10.5px;}
}
/* 触屏(可能是平板/桌面触屏,不限于窄屏):可点目标给足 */
/* 触屏(可能是平板/桌面触屏,不限于窄屏) */
@media (pointer: coarse) {
html[data-ui='vivid'] .cm-provider {width: 44px;height: 44px;}
html[data-ui='vivid'] .cm-act {width: 32px;height: 32px;}
html[data-ui='vivid'] .cm-more-r {padding: 10px 16px;}
html[data-ui='vivid'] .cm-link {padding: 8px 2px;}
html[data-ui='vivid'] .cm-input {font-size: 16px;}
}
+5 -3
View File
@@ -14,10 +14,12 @@
/* 左右 20px 对齐:hero 的标题和下面卡片列表的左边缘要在同一条竖线上 */
html[data-ui='vivid'] .hero {padding:48px 20px 32px}
html[data-ui='vivid'] .wrap {padding:8px 20px 36px}
/* 阅读页在手机上只收回留白:.article-layout 走 20px 页边距,.paper 的内边距归零
/* 阅读页在手机上只收回留白:.article-layout 走 12px 页边距,.paper 的内边距归零
(桌面那 24px 是为了给 776 的中列凑出 --measure 的 728,手机上只会浪费宽度)。
卡片样式本身已在 article.css 里去掉了,手机桌面一致。 */
html[data-ui='vivid'] .article-layout {padding:20px 20px 40px}
12px 是站主定的「不影响美观的前提下尽量贴边」,评论区(styles.css 的 .cm-sec
手机断点)与此对齐,正文和评论共用同一条左缘。卡片样式本身已在 article.css
里去掉了,手机桌面一致。 */
html[data-ui='vivid'] .article-layout {padding:20px 12px 40px}
html[data-ui='vivid'] .paper {padding:0}
html[data-ui='vivid'] .about-card {padding:32px 22px}
/* showcase 的 .showcase 是 max-width + 自己的 padding(不带 .wrap 类),
+8
View File
@@ -236,4 +236,12 @@ const initial = computed(() => (site.author_name || 'O').trim().slice(0, 1).toUp
font-size: 27px;
}
}
@media (max-width: 640px) {
/* 整站手机页边距是 12px(styles.css 的 .shell),这里把侧边距归零,
正文才和评论区落在同一条 12px 左缘上 */
.wrap {
padding: 18px 0 40px;
}
}
</style>