457 lines
9.2 KiB
Vue
Raw Normal View History

2025-12-04 10:04:21 +08:00
<template>
<article class="mk-card" ref="cardRef" :data-transition-slug="slug">
<!-- 封面 -->
<div class="mk-cover" :data-hero-cover="slug">
<img :src="cover" :alt="title || 'cover'" class="mk-img" loading="lazy" />
<!-- 悬浮层 CSS 控制显示 -->
<div class="mk-overlay">
<div class="mk-btns">
<a
v-if="visitHref"
:href="visitHref"
class="mk-btn"
target="_blank"
rel="noopener"
>
</a>
<NuxtLink
v-if="detailHref"
:to="detailHref"
class="mk-btn mk-btn--ghost"
@click="handleDetailClick"
>
详情
</NuxtLink>
</div>
</div>
</div>
<!-- 标题 -->
<h3 class="mk-title" :title="title" :data-hero-title="slug">
{{ title }}
</h3>
<!-- 标签 -->
<div class="mk-tags" v-if="tags?.length">
<span
v-for="t in tags"
:key="t"
class="mk-tag"
:class="{ 'mk-tag--rainbow': isRecTag(t) }"
>
{{ t }}
</span>
</div>
<!-- 时间 -->
<div class="mk-time" v-if="createdAt">
发布{{ fdate(createdAt) }}
</div>
<!-- 底部信息 -->
<footer class="mk-meta">
<div class="mk-owner">
<img v-if="ownerAvatar" :src="ownerAvatar" class="mk-avatar" alt="" />
<span class="mk-owner-name" :title="ownerName">{{ ownerName }}</span>
</div>
<div class="mk-stats">
<span class="mk-stat">
<!-- 眼睛 -->
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M12 5c5.5 0 9.6 4.1 10.7 6.4a1.9 1.9 0 0 1 0 1.3C21.6 15.9 17.5 20 12 20S2.4 15.9 1.3 12.7a1.9 1.9 0 0 1 0-1.3C2.4 9.1 6.5 5 12 5Zm0 3.2a4.8 4.8 0 1 0 0 9.6 4.8 4.8 0 0 0 0-9.6Zm0 2.4a2.4 2.4 0 1 1 0 4.8 2.4 2.4 0 0 1 0-4.8Z"
/>
</svg>
{{ nfmt(views) }}
</span>
<button
type="button"
class="mk-stat mk-like"
:class="{ 'mk-like--active': favorited }"
@click.stop="emit('toggle-like')"
:aria-pressed="favorited ? 'true' : 'false'"
>
<!-- 心形 -->
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M12 21s-6.7-4.5-9.4-7.2A6 6 0 1 1 12 6a6 6 0 1 1 9.4 7.8C18.7 16.5 12 21 12 21Z"
/>
</svg>
{{ nfmt(likes) }}
</button>
</div>
</footer>
</article>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useSharedTransition } from '@/composables/useSharedTransition'
const emit = defineEmits<{
(e: 'toggle-like'): void
(e: 'view'): void
}>()
const props = withDefaults(
defineProps<{
slug?: string
cover: string
title: string
tags?: string[]
ownerName?: string
ownerAvatar?: string
views?: number
likes?: number
favorited?: boolean
/** 外链访问按钮 */
visitHref?: string
/** 站内详情页路由NuxtLink */
detailHref?: string | Record<string, any>
/** 创建时间 */
createdAt?: string
}>(),
{
slug: '',
tags: () => [],
ownerName: '秒哒官方',
ownerAvatar: '',
views: 0,
likes: 0,
favorited: false,
createdAt: '',
}
)
const cardRef = ref<HTMLElement | null>(null)
const transition = useSharedTransition()
onMounted(() => {
if (cardRef.value && props.slug) {
// 标记卡片位置用于转场动画
transition.markSource(props.slug, cardRef.value)
}
})
onBeforeUnmount(() => {
if (cardRef.value && props.slug) {
// 更新卡片位置(用户可能滚动了页面)
transition.markSource(props.slug, cardRef.value)
}
})
function handleDetailClick() {
// 在导航前标记当前卡片位置
if (cardRef.value && props.slug) {
transition.markSource(props.slug, cardRef.value)
}
emit('view')
}
/** 简单的 1200 -> 1.2K 格式化 */
function nfmt(n?: number) {
const v = n ?? 0
if (v >= 1_000_000)
return (v / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M'
if (v >= 1_000) return (v / 1_000).toFixed(1).replace(/\.0$/, '') + 'K'
return String(v)
}
/** 是否为"推荐类"标签(炫彩) */
function isRecTag(tag?: string) {
if (!tag) return false
const t = String(tag).trim().toLowerCase()
// 兼容:推荐 / 精选 / 热门 / 置顶 / rec包含这些词也算
const keys = ['推荐', '精选', '热门', '置顶', 'rec']
return keys.some(k => t === k.toLowerCase() || t.includes(k.toLowerCase()))
}
function fdate(v?: string) {
if (!v) return ''
try {
const d = new Date(v)
if (Number.isNaN(d.getTime())) return ''
return d.toLocaleDateString()
} catch {
return ''
}
}
</script>
<style scoped>
/* 容器 */
.mk-card {
--radius: 16px;
--shadow: 0 6px 20px rgba(16, 24, 40, 0.08);
--shadow-hover: 0 12px 32px rgba(16, 24, 40, 0.14);
/* 与首页保持一致的三原色渐变(紫→靛蓝→蓝→青) */
--rainbow: linear-gradient(
90deg,
#7c3aed 0%,
#6366f1 33%,
#60a5fa 66%,
#22d3ee 100%
);
background: #fff;
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
transition: box-shadow 0.25s ease, transform 0.25s ease;
}
.mk-card:hover,
.mk-card:focus-within {
box-shadow: var(--shadow-hover);
transform: translateY(-2px);
}
/* 封面与悬浮层 */
.mk-cover {
position: relative;
aspect-ratio: 16 / 9;
overflow: hidden;
border-radius: calc(var(--radius) - 2px);
margin: 12px 12px 10px;
}
.mk-img {
width: 100%;
height: 100%;
object-fit: cover;
transform: scale(1.001);
transition: transform 0.35s ease;
border-radius: inherit;
}
.mk-card:hover .mk-img,
.mk-card:focus-within .mk-img {
transform: scale(1.03);
}
/* 渐变遮罩 + 按钮 */
.mk-overlay {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 16px;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 30%,
rgba(0, 0, 0, 0.45) 100%
);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.mk-card:hover .mk-overlay,
.mk-card:focus-within .mk-overlay {
opacity: 1;
pointer-events: auto;
}
.mk-btns {
display: flex;
gap: 14px;
transform: translateY(8px);
transition: transform 0.25s ease 0.02s;
}
.mk-card:hover .mk-overlay .mk-btns,
.mk-card:focus-within .mk-overlay .mk-btns {
transform: translateY(0);
}
.mk-btn {
min-width: 140px;
height: 44px;
padding: 0 18px;
border-radius: 12px;
border: 1px solid #111827;
background: #111827;
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
letter-spacing: 0.5px;
text-decoration: none;
transition: all 0.2s ease;
}
.mk-btn:hover {
filter: brightness(1.07);
transform: translateY(-1px);
}
.mk-btn--ghost {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.65);
color: #fff;
}
/* 标题(单行省略) */
.mk-title {
margin: 0 16px;
font-size: 18px;
line-height: 1.5;
font-weight: 700;
color: #111827;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 标签 */
.mk-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
padding: 8px 16px 0;
}
.mk-tag {
font-size: 12px;
color: #111827;
background: #f3f4f6;
border: 1px solid #e5e7eb;
padding: 2px 8px;
border-radius: 10px;
}
/* 炫彩"推荐"标签 */
.mk-tag--rainbow {
color: #fff;
border: none;
background-image: var(--rainbow);
background-size: 220% 220%;
animation: mkRainbowShift 6s ease infinite;
box-shadow: 0 6px 14px rgba(99, 102, 241, 0.22);
}
.mk-tag--rainbow:hover {
filter: brightness(1.05);
}
/* 降低动画偏好时,禁用流动,仅保留渐变底色 */
@media (prefers-reduced-motion: reduce) {
.mk-tag--rainbow {
animation: none;
background-size: 100% 100%;
}
}
@keyframes mkRainbowShift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* 时间 */
.mk-time {
margin: 4px 16px 0;
color: #6b7280;
font-size: 12px;
}
/* 底部信息 */
.mk-meta {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px 14px;
}
.mk-owner {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.mk-avatar {
width: 22px;
height: 22px;
border-radius: 50%;
object-fit: cover;
background: #eef2ff;
}
.mk-owner-name {
font-size: 14px;
color: #6b7280;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 统计 */
.mk-stats {
display: inline-flex;
gap: 16px;
color: #6b7280;
}
.mk-stat {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
}
.mk-stat svg {
width: 18px;
height: 18px;
fill: currentColor;
}
.mk-like {
background: transparent;
border: none;
color: inherit;
padding: 0;
cursor: pointer;
transition: color 0.2s ease;
}
.mk-like svg {
transition: transform 0.15s ease;
}
.mk-like:hover svg {
transform: scale(1.08);
}
.mk-like--active {
color: #e11d48;
}
.mk-like--active svg {
fill: currentColor;
}
/* 小屏优化 */
@media (max-width: 640px) {
.mk-btn {
min-width: 110px;
height: 40px;
}
.mk-title {
font-size: 16px;
}
}
</style>