457 lines
9.2 KiB
Vue
457 lines
9.2 KiB
Vue
<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>
|