vivid 时间线加操作行(回复/赞/查看),长文文字边距对齐短文 8px

- 卡片/纸条本体保持整块链接,操作行是链接外的兄弟节点(<a> 里不嵌交互元素)
- 回复 → 文章页 #comments 锚点;查看 → 文章页;赞是本机赞
  (无登录体系,状态存 localStorage,后续要做全局计数再加表和端点)
- Phosphor 图标:对话气泡 / 心形 / 眼睛,按下有缩放反馈,liked 态 accent 高亮
- .card .body 横向边距 26px→8px,与纸条文字同一条竖线
This commit is contained in:
Sakurasan committed 2026-10-01 17:28:24 +08:00
1 parent 17b1143183
commit 00e85bf667
2 files changed
+99 -45

No files matched your search

+84 -44
View File
@@ -1,18 +1,44 @@
<script setup>
import { RouterLink } from 'vue-router'
import { reactive } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import { openLightbox } from '../../lightbox'
import { thumbURL, thumbifyHtml } from '../../utils'
import LinkCard from '../../components/LinkCard.vue'
import { PhChatCircle, PhHeart, PhEye } from '@phosphor-icons/vue'
// 余白列表。对应设计稿的 <div class="feed">:长文是卡片(封面 + 标题 + 摘要 + 元信息 + 标签),
// 短文是纸条(直接铺正文,元信息在正文后)。两种密度混在同一条时间线里,这是设计稿的主张。
// 元信息(类型/日期/时长/字数)从标题上方挪到了正文之后、和标签同一块:
// 卡片开头只留标题摘要,读完再看「什么时候、多长」。
//
// 卡片/纸条本体是整块可点的链接;操作行(回复 / 赞 / 查看)是链接外面的兄弟节点,
// 不能塞进 <a> 里——嵌套交互元素既不合 HTML 语义,点击冒泡也理不清。
//
// 赞是本机赞:站点没有登录体系,点赞状态存 localStorage(按设备计),
// 不上传服务端。以后要做全局计数,加一张 post_likes 表 + 一个 POST 端点即可替换。
const props = defineProps({
items: { type: Array, default: () => [] }
})
const router = useRouter()
const LIKES_KEY = 'one_feed_likes'
const likes = reactive(readLikes())
function readLikes() {
try { return JSON.parse(localStorage.getItem(LIKES_KEY) || '{}') } catch { return {} }
}
function keyOf(p) { return String(p.id || p.slug) }
function toggleLike(p) {
const k = keyOf(p)
if (likes[k]) delete likes[k]
else likes[k] = 1
try { localStorage.setItem(LIKES_KEY, JSON.stringify(likes)) } catch { /* 隐私模式等存不上就算了 */ }
}
function go(p, hash) {
router.push(`/post/${encodeURIComponent(p.slug)}${hash || ''}`)
}
const fmt = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })
const fmtDate = (iso) => fmt.format(new Date(iso)).replace(/\//g, '-')
const fmtDateTime = (iso) => {
@@ -34,56 +60,70 @@ const shortTitle = (p) => plain(p.content_html).slice(0, 40) + '…'
<template>
<div class="feed">
<template v-for="p in props.items" :key="p.id || p.slug">
<!-- 长文:卡片 -->
<RouterLink v-if="p.kind === 'long'" class="card" :to="`/post/${encodeURIComponent(p.slug)}`">
<div v-if="p.cover_url" class="cov">
<img :src="thumbURL(p.cover_url, 640)" alt="" loading="lazy">
<!-- 长文:卡片 + 操作行 -->
<div v-if="p.kind === 'long'" class="feed-item">
<RouterLink class="card" :to="`/post/${encodeURIComponent(p.slug)}`">
<div v-if="p.cover_url" class="cov">
<img :src="thumbURL(p.cover_url, 640)" alt="" loading="lazy">
</div>
<div class="body">
<h2>{{ p.title }}</h2>
<p class="sum">{{ p.summary }}</p>
<div class="meta">
<span class="badge long">LONG</span>
<time :datetime="p.published_at">{{ fmtDate(p.published_at) }}</time>
<span>· 约 {{ p.reading_minutes }} 分钟</span>
<span>· {{ nf.format(p.content_len || 0) }} 字</span>
</div>
<div class="tags">
<span v-for="t in p.tags || []" :key="t" class="tg">{{ t }}</span>
</div>
</div>
</RouterLink>
<div class="acts">
<button type="button" @click="go(p, '#comments')"><PhChatCircle class="ic" aria-hidden="true" />回复</button>
<button type="button" :class="{ liked: likes[keyOf(p)] }" @click="toggleLike(p)"><PhHeart class="ic" aria-hidden="true" />赞</button>
<button type="button" @click="go(p)"><PhEye class="ic" aria-hidden="true" />查看</button>
</div>
<div class="body">
<h2>{{ p.title }}</h2>
<p class="sum">{{ p.summary }}</p>
</div>
<!-- 短文:纸条 + 操作行。带图时桌面端左右分栏(.split,见 home.css) -->
<div v-else class="feed-item">
<RouterLink class="note" :class="{ split: p.images && p.images.length > 0 }" :to="`/post/${encodeURIComponent(p.slug)}`">
<div class="body" v-html="thumbifyHtml(p.content_html)" />
<!-- 短文配图:Twitter 式网格(快发盒上传);点图开预览(不跳详情) -->
<div
v-if="p.images && p.images.length"
class="imgs"
:class="'n' + Math.min(p.images.length, 4)"
>
<img
v-for="(u, i) in p.images.slice(0, 4)"
:key="i"
:src="thumbURL(u, p.images.length === 1 ? 720 : 480)"
alt=""
loading="lazy"
decoding="async"
@click.prevent.stop="openLightbox(p.images, i)"
/>
</div>
<!-- 外链预览卡片:正文里第一个链接,发布时后端已抓好 -->
<LinkCard v-if="p.link_card" :card="p.link_card" />
<div class="meta">
<span class="badge long">LONG</span>
<time :datetime="p.published_at">{{ fmtDate(p.published_at) }}</time>
<span>· 约 {{ p.reading_minutes }} 分钟</span>
<span>· {{ nf.format(p.content_len || 0) }} 字</span>
<span class="badge short">SHORT</span>
<time :datetime="p.published_at">{{ fmtDateTime(p.published_at) }}</time>
</div>
<div class="tags">
<span v-for="t in p.tags || []" :key="t" class="tg">{{ t }}</span>
</div>
</RouterLink>
<div class="acts note-acts">
<button type="button" @click="go(p, '#comments')"><PhChatCircle class="ic" aria-hidden="true" />回复</button>
<button type="button" :class="{ liked: likes[keyOf(p)] }" @click="toggleLike(p)"><PhHeart class="ic" aria-hidden="true" />赞</button>
<button type="button" @click="go(p)"><PhEye class="ic" aria-hidden="true" />查看</button>
</div>
</RouterLink>
<!-- 短文:纸条,正文直接铺开;带图时桌面端左右分栏(.split,见 home.css) -->
<RouterLink v-else class="note" :class="{ split: p.images && p.images.length > 0 }" :to="`/post/${encodeURIComponent(p.slug)}`">
<div class="body" v-html="thumbifyHtml(p.content_html)" />
<!-- 短文配图:Twitter 式网格(快发盒上传);点图开预览(不跳详情) -->
<div
v-if="p.images && p.images.length"
class="imgs"
:class="'n' + Math.min(p.images.length, 4)"
>
<img
v-for="(u, i) in p.images.slice(0, 4)"
:key="i"
:src="thumbURL(u, p.images.length === 1 ? 720 : 480)"
alt=""
loading="lazy"
decoding="async"
@click.prevent.stop="openLightbox(p.images, i)"
/>
</div>
<!-- 外链预览卡片:正文里第一个链接,发布时后端已抓好 -->
<LinkCard v-if="p.link_card" :card="p.link_card" />
<div class="meta">
<span class="badge short">SHORT</span>
<time :datetime="p.published_at">{{ fmtDateTime(p.published_at) }}</time>
</div>
<div class="tags">
<span v-for="t in p.tags || []" :key="t" class="tg">{{ t }}</span>
</div>
</RouterLink>
</div>
</template>
</div>
</template>
+15 -1
View File
@@ -85,7 +85,7 @@ html[data-ui='vivid'] .card .cov img {width:100%;height:100%;object-fit:cover;tr
@media (hover: hover) and (pointer: fine) {
html[data-ui='vivid'] .card:hover .cov img {transform:scale(1.04)}
}
html[data-ui='vivid'] .card .body {padding:22px 26px 24px}
html[data-ui='vivid'] .card .body {padding:22px 8px 24px}
/* 元信息挪到摘要之后(原来在标题上方):和标签一起收在卡片尾部 */
html[data-ui='vivid'] .card .meta {display:flex;gap:10px;align-items:center;flex-wrap:wrap;
font-size:12px;color:var(--muted);font-weight:600;margin-top:16px;}
@@ -184,6 +184,20 @@ html[data-ui='vivid'] .note .imgs.n3 img:first-child {grid-row:span 2;}
html[data-ui='vivid'] .note.split .tags {grid-column:1 / -1;margin-top:0;}
}
html[data-ui='vivid'] .note .tags {display:flex;gap:14px;flex-wrap:wrap;margin-top:8px}
/* 操作行:回复 / 赞 / 查看。挂在链接外面(兄弟节点),左边距 8px 与
纸条文字、卡片正文同一条竖线。赞目前是本机赞(localStorage),不传服务端。 */
html[data-ui='vivid'] .acts {display:flex;gap:2px;padding:0 8px;margin-top:4px}
html[data-ui='vivid'] .acts button {display:inline-flex;align-items:center;gap:6px;
padding:7px 10px;border:0;border-radius:999px;background:transparent;
font-family:inherit;font-size:12.5px;font-weight:600;color:var(--muted);
cursor:pointer;touch-action:manipulation;
transition:color .2s var(--ease),background-color .2s var(--ease),transform 120ms ease-out;}
html[data-ui='vivid'] .acts button .ic {width:17px;height:17px}
@media (hover: hover) and (pointer: fine) {
html[data-ui='vivid'] .acts button:hover {color:var(--accent);background:var(--accent-soft)}
}
html[data-ui='vivid'] .acts button:active {transform:scale(.94)}
html[data-ui='vivid'] .acts button.liked {color:var(--accent)}
html[data-ui='vivid'] .pager {display:flex;gap:8px;align-items:center;justify-content:center;
margin-top:36px;flex-wrap:wrap;}
html[data-ui='vivid'] .pager button {font-size:13px;font-weight:700;padding:9px 16px;border-radius:999px;