|
|
|
@ -1,6 +1,6 @@ |
|
|
|
<script setup lang="ts"> |
|
|
|
import { ref, computed, watch } from 'vue' |
|
|
|
import { useRoute } from 'vue-router' |
|
|
|
import { useRoute, useRouter } from 'vue-router' |
|
|
|
import { articleApi } from '@/api/client' |
|
|
|
import { renderMarkdown } from '@/utils/markdown' |
|
|
|
import { setPageMeta } from '@/utils/seo' |
|
|
|
@ -10,6 +10,7 @@ import ArticleMetaBar from '@/components/ArticleMetaBar.vue' |
|
|
|
import type { ArticleDetail, ResponseArticleDetail } from '@/generated/api' |
|
|
|
|
|
|
|
const route = useRoute() |
|
|
|
const router = useRouter() |
|
|
|
const slug = computed(() => String(route.params.slug ?? '')) |
|
|
|
|
|
|
|
const { data: resp, loading, error, execute } = useApi<ResponseArticleDetail>( |
|
|
|
@ -18,11 +19,39 @@ const { data: resp, loading, error, execute } = useApi<ResponseArticleDetail>( |
|
|
|
|
|
|
|
const article = ref<ArticleDetail | null>(null) |
|
|
|
|
|
|
|
const starred = ref(false) |
|
|
|
const starCount = ref(0) |
|
|
|
const starBusy = ref(false) |
|
|
|
|
|
|
|
const renderedContent = computed(() => { |
|
|
|
if (!article.value?.content) return '' |
|
|
|
return renderMarkdown(article.value.content) |
|
|
|
}) |
|
|
|
|
|
|
|
async function toggleStar() { |
|
|
|
if (!localStorage.getItem('token')) { |
|
|
|
router.push('/login') |
|
|
|
return |
|
|
|
} |
|
|
|
if (!article.value || starBusy.value) return |
|
|
|
starBusy.value = true |
|
|
|
const was = starred.value |
|
|
|
starred.value = !was |
|
|
|
starCount.value += was ? -1 : 1 |
|
|
|
try { |
|
|
|
if (was) { |
|
|
|
await articleApi.apiArticlesSlugStarDelete(article.value.slug) |
|
|
|
} else { |
|
|
|
await articleApi.apiArticlesSlugStarPost(article.value.slug) |
|
|
|
} |
|
|
|
} catch { |
|
|
|
starred.value = was |
|
|
|
starCount.value += was ? 1 : -1 |
|
|
|
} finally { |
|
|
|
starBusy.value = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function applyMeta(data: ArticleDetail) { |
|
|
|
setPageMeta({ |
|
|
|
title: data.title, |
|
|
|
@ -38,6 +67,8 @@ async function load() { |
|
|
|
article.value = resp.value?.data ?? null |
|
|
|
if (article.value) { |
|
|
|
applyMeta(article.value) |
|
|
|
starred.value = article.value.hasStarred ?? false |
|
|
|
starCount.value = article.value.starCount |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -59,6 +90,12 @@ watch(slug, load, { immediate: true }) |
|
|
|
:view-count="article.viewCount" |
|
|
|
:content="article.content" |
|
|
|
/> |
|
|
|
<button class="star-btn" :class="{ starred }" @click="toggleStar"> |
|
|
|
<svg class="star-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18"> |
|
|
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/> |
|
|
|
</svg> |
|
|
|
<span class="star-count">{{ starCount }}</span> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
<div class="detail-tags"> |
|
|
|
<span v-for="tag in article.tagNames" :key="tag" class="tag">{{ tag }}</span> |
|
|
|
@ -124,4 +161,36 @@ watch(slug, load, { immediate: true }) |
|
|
|
.error-text { |
|
|
|
color: var(--ac-danger); |
|
|
|
} |
|
|
|
|
|
|
|
.star-btn { |
|
|
|
display: inline-flex; |
|
|
|
align-items: center; |
|
|
|
gap: 4px; |
|
|
|
border: 1px solid var(--ac-border); |
|
|
|
border-radius: var(--song-radius-m); |
|
|
|
background: var(--ac-bg-card); |
|
|
|
color: var(--ac-text-disabled); |
|
|
|
padding: 2px 10px; |
|
|
|
cursor: pointer; |
|
|
|
font-size: 13px; |
|
|
|
transition: color var(--song-dur-1) var(--song-ease), border-color var(--song-dur-1) var(--song-ease); |
|
|
|
} |
|
|
|
|
|
|
|
.star-btn:hover { |
|
|
|
border-color: var(--ac-primary); |
|
|
|
color: var(--ac-primary-ink); |
|
|
|
} |
|
|
|
|
|
|
|
.star-btn.starred { |
|
|
|
color: var(--ac-primary); |
|
|
|
border-color: var(--ac-primary); |
|
|
|
} |
|
|
|
|
|
|
|
.star-icon { |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.star-count { |
|
|
|
font-variant-numeric: tabular-nums; |
|
|
|
} |
|
|
|
</style> |