Browse Source
- Introduced defaultCardType field in categories schema and updated migrations. - Removed slug validation from category creation and update schemas. - Updated category service to handle defaultCardType in create and update operations. - Adjusted API endpoints to reflect changes in category schema.aaaaa
17 changed files with 2876 additions and 81 deletions
@ -0,0 +1,207 @@ |
|||
<script setup lang="ts"> |
|||
import { computed } from 'vue' |
|||
import { marked } from 'marked' |
|||
|
|||
const props = defineProps<{ |
|||
title: string |
|||
description: string |
|||
content: string |
|||
tags: string[] |
|||
aspectRatio: number |
|||
categoryName?: string |
|||
}>() |
|||
|
|||
const contentHtml = computed(() => { |
|||
if (!props.content) return '' |
|||
return marked(props.content, { breaks: true }) as string |
|||
}) |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="card"> |
|||
<span v-if="categoryName" class="card-cat-chip">{{ categoryName }}</span> |
|||
|
|||
<!-- 上部分:正文内容预览 --> |
|||
<div class="card-preview"> |
|||
<div class="preview-text" v-html="contentHtml" /> |
|||
<div class="content-fade" /> |
|||
</div> |
|||
|
|||
<!-- 下部分:标签 / 标题 / 描述 --> |
|||
<div class="card-footer"> |
|||
<div v-if="tags && tags.length" class="tags"> |
|||
<span v-for="tag in tags" :key="tag" class="tag">{{ tag }}</span> |
|||
</div> |
|||
<h3>{{ title }}</h3> |
|||
<p v-if="description" class="desc">{{ description }}</p> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.card-cat-chip { |
|||
position: absolute; |
|||
top: 8px; |
|||
left: 8px; |
|||
z-index: 2; |
|||
font-size: 10px; |
|||
font-weight: 600; |
|||
padding: 2px 8px; |
|||
border-radius: 9999px; |
|||
background: rgba(204, 120, 92, 0.12); |
|||
color: var(--color-primary); |
|||
letter-spacing: 0.3px; |
|||
} |
|||
|
|||
.card { |
|||
position: relative; |
|||
border-radius: 12px; |
|||
background: var(--color-surface-card); |
|||
overflow: hidden; |
|||
border: 1px solid var(--color-hairline-soft); |
|||
display: flex; |
|||
flex-direction: column; |
|||
transition: transform 0.4s var(--ease-out-expo), box-shadow 0.4s var(--ease-out-expo); |
|||
} |
|||
|
|||
.card:hover { |
|||
transform: translateY(-2px) scale(1.005); |
|||
box-shadow: 0 8px 24px rgba(20, 20, 19, 0.08); |
|||
} |
|||
|
|||
/* ── 上部分:正文预览 ── */ |
|||
|
|||
.card-preview { |
|||
position: relative; |
|||
padding: 20px 20px 16px; |
|||
} |
|||
|
|||
.preview-text { |
|||
font-size: 13px; |
|||
font-weight: 400; |
|||
line-height: 1.7; |
|||
color: var(--color-body); |
|||
max-height: 96px; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
/* markdown 元素降级样式(预览态,不让标题/代码过于抢眼) */ |
|||
.preview-text :deep(h1), |
|||
.preview-text :deep(h2), |
|||
.preview-text :deep(h3) { |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
line-height: 1.5; |
|||
color: var(--color-ink); |
|||
margin: 0 0 4px; |
|||
} |
|||
|
|||
.preview-text :deep(p) { |
|||
margin: 0 0 4px; |
|||
} |
|||
|
|||
.preview-text :deep(ul), |
|||
.preview-text :deep(ol) { |
|||
margin: 0 0 4px; |
|||
padding-left: 18px; |
|||
} |
|||
|
|||
.preview-text :deep(li) { |
|||
margin-bottom: 2px; |
|||
} |
|||
|
|||
.preview-text :deep(code) { |
|||
font-size: 12px; |
|||
background: var(--color-surface-cream-strong); |
|||
padding: 1px 5px; |
|||
border-radius: 4px; |
|||
} |
|||
|
|||
.preview-text :deep(pre) { |
|||
font-size: 12px; |
|||
background: var(--color-surface-cream-strong); |
|||
padding: 8px 12px; |
|||
border-radius: 6px; |
|||
margin: 0 0 6px; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.preview-text :deep(blockquote) { |
|||
margin: 0 0 4px; |
|||
padding-left: 12px; |
|||
border-left: 2px solid var(--color-hairline); |
|||
color: var(--color-muted); |
|||
} |
|||
|
|||
.preview-text :deep(a) { |
|||
color: var(--color-primary); |
|||
pointer-events: none; |
|||
} |
|||
|
|||
.preview-text :deep(img) { |
|||
display: none; |
|||
} |
|||
|
|||
.content-fade { |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
height: 52px; |
|||
background: linear-gradient( |
|||
to bottom, |
|||
transparent 0%, |
|||
var(--color-surface-card) 100% |
|||
); |
|||
pointer-events: none; |
|||
} |
|||
|
|||
/* ── 下部分:标签 / 标题 / 描述 ── */ |
|||
|
|||
.card-footer { |
|||
padding: 0 20px 20px; |
|||
} |
|||
|
|||
.tags { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 6px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.tag { |
|||
font-size: 11px; |
|||
font-weight: 500; |
|||
color: var(--color-primary); |
|||
background: rgba(204, 120, 92, 0.08); |
|||
padding: 3px 10px; |
|||
border-radius: 9999px; |
|||
line-height: 1.3; |
|||
} |
|||
|
|||
.card-footer h3 { |
|||
font-family: var(--font-display); |
|||
font-size: 15.5px; |
|||
font-weight: 400; |
|||
line-height: 1.3; |
|||
color: var(--color-ink); |
|||
margin: 0 0 6px; |
|||
letter-spacing: -0.01em; |
|||
overflow: hidden; |
|||
display: -webkit-box; |
|||
-webkit-line-clamp: 2; |
|||
-webkit-box-orient: vertical; |
|||
} |
|||
|
|||
.desc { |
|||
font-size: 12px; |
|||
font-weight: 400; |
|||
line-height: 1.55; |
|||
color: var(--color-muted); |
|||
margin: 0; |
|||
overflow: hidden; |
|||
display: -webkit-box; |
|||
-webkit-line-clamp: 2; |
|||
-webkit-box-orient: vertical; |
|||
} |
|||
</style> |
|||
Binary file not shown.
@ -0,0 +1 @@ |
|||
ALTER TABLE `categories` ADD `default_card_type` text; |
|||
@ -0,0 +1,2 @@ |
|||
DROP INDEX `idx_category_slug`;--> statement-breakpoint |
|||
ALTER TABLE `categories` DROP COLUMN `slug`; |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue