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