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>
|