mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-04-21 03:17:28 +08:00
feat: extract timeline tag item component
This commit is contained in:
126
frontend_nuxt/components/TimelineTagItem.vue
Normal file
126
frontend_nuxt/components/TimelineTagItem.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<div class="timeline-tag-item">
|
||||||
|
<template v-if="mode === 'timeline'">
|
||||||
|
<div class="tags-container">
|
||||||
|
<div class="tags-container-item">
|
||||||
|
<div class="timeline-tag-title">{{ title }}</div>
|
||||||
|
<ArticleTags v-if="tag" :tags="[tag]" />
|
||||||
|
</div>
|
||||||
|
<div v-if="timelineDate" class="timeline-date">{{ timelineDate }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasDescription" class="timeline-snippet">
|
||||||
|
{{ tag?.description }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="timeline-link" :class="{ clickable: isClickable }" @click="handleTagClick">
|
||||||
|
{{ tag?.name }}<span v-if="tag?.count"> x{{ tag.count }}</span>
|
||||||
|
</span>
|
||||||
|
<div v-if="hasDescription" class="timeline-snippet">
|
||||||
|
{{ tag?.description }}
|
||||||
|
</div>
|
||||||
|
<div v-if="summaryDate" class="timeline-date">{{ summaryDate }}</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import TimeManager from '~/utils/time'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: { type: Object, required: true },
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'timeline',
|
||||||
|
validator: (value) => ['timeline', 'summary'].includes(value),
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '创建了标签',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['tag-click'])
|
||||||
|
|
||||||
|
const tag = computed(() => props.item?.tag ?? null)
|
||||||
|
const hasDescription = computed(() => {
|
||||||
|
const description = tag.value?.description
|
||||||
|
return !!description
|
||||||
|
})
|
||||||
|
|
||||||
|
const timelineDate = computed(() => {
|
||||||
|
const date = props.item?.createdAt ?? tag.value?.createdAt
|
||||||
|
return date ? TimeManager.format(date) : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const summaryDate = computed(() => {
|
||||||
|
const date = tag.value?.createdAt ?? props.item?.createdAt
|
||||||
|
return date ? TimeManager.format(date) : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const isClickable = computed(() => props.mode === 'summary' && !!tag.value)
|
||||||
|
|
||||||
|
const handleTagClick = () => {
|
||||||
|
if (!isClickable.value) return
|
||||||
|
emit('tag-click', tag.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.timeline-tag-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
padding-top: 5px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-tag-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: gray;
|
||||||
|
margin-top: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-snippet {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-link {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
word-break: break-word;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-link.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-link.clickable:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -143,15 +143,7 @@
|
|||||||
<div class="summary-content" v-if="hotPosts.length > 0">
|
<div class="summary-content" v-if="hotPosts.length > 0">
|
||||||
<BaseTimeline :items="hotPosts">
|
<BaseTimeline :items="hotPosts">
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<NuxtLink :to="`/posts/${item.post.id}`" class="timeline-link">
|
<TimelinePostItem :item="item" />
|
||||||
{{ item.post.title }}
|
|
||||||
</NuxtLink>
|
|
||||||
<div class="timeline-snippet">
|
|
||||||
{{ stripMarkdown(item.post.snippet) }}
|
|
||||||
</div>
|
|
||||||
<div class="timeline-date">
|
|
||||||
{{ formatDate(item.post.createdAt) }}
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</BaseTimeline>
|
</BaseTimeline>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,15 +156,7 @@
|
|||||||
<div class="summary-content" v-if="hotTags.length > 0">
|
<div class="summary-content" v-if="hotTags.length > 0">
|
||||||
<BaseTimeline :items="hotTags">
|
<BaseTimeline :items="hotTags">
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<span class="timeline-link" @click="gotoTag(item.tag)">
|
<TimelineTagItem :item="item" mode="summary" @tag-click="gotoTag" />
|
||||||
{{ item.tag.name }}<span v-if="item.tag.count"> x{{ item.tag.count }}</span>
|
|
||||||
</span>
|
|
||||||
<div class="timeline-snippet" v-if="item.tag.description">
|
|
||||||
{{ item.tag.description }}
|
|
||||||
</div>
|
|
||||||
<div class="timeline-date">
|
|
||||||
{{ formatDate(item.tag.createdAt) }}
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</BaseTimeline>
|
</BaseTimeline>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,16 +206,7 @@
|
|||||||
<TimelineCommentGroup :item="item" />
|
<TimelineCommentGroup :item="item" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="item.type === 'tag'">
|
<template v-else-if="item.type === 'tag'">
|
||||||
<div class="tags-container">
|
<TimelineTagItem :item="item" />
|
||||||
<div class="tags-container-item">
|
|
||||||
<div class="timeline-tag-title">创建了标签</div>
|
|
||||||
<ArticleTags :tags="[item.tag]" />
|
|
||||||
</div>
|
|
||||||
<div class="timeline-date">{{ formatDate(item.createdAt) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-snippet" v-if="item.tag.description">
|
|
||||||
{{ item.tag.description }}
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</BaseTimeline>
|
</BaseTimeline>
|
||||||
@@ -297,6 +272,7 @@ import BaseTabs from '~/components/BaseTabs.vue'
|
|||||||
import LevelProgress from '~/components/LevelProgress.vue'
|
import LevelProgress from '~/components/LevelProgress.vue'
|
||||||
import TimelineCommentGroup from '~/components/TimelineCommentGroup.vue'
|
import TimelineCommentGroup from '~/components/TimelineCommentGroup.vue'
|
||||||
import TimelinePostItem from '~/components/TimelinePostItem.vue'
|
import TimelinePostItem from '~/components/TimelinePostItem.vue'
|
||||||
|
import TimelineTagItem from '~/components/TimelineTagItem.vue'
|
||||||
import UserList from '~/components/UserList.vue'
|
import UserList from '~/components/UserList.vue'
|
||||||
import { toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { authState, getToken } from '~/utils/auth'
|
import { authState, getToken } from '~/utils/auth'
|
||||||
@@ -386,7 +362,12 @@ const fetchSummary = async () => {
|
|||||||
const postsRes = await fetch(`${API_BASE_URL}/api/users/${username}/hot-posts`)
|
const postsRes = await fetch(`${API_BASE_URL}/api/users/${username}/hot-posts`)
|
||||||
if (postsRes.ok) {
|
if (postsRes.ok) {
|
||||||
const data = await postsRes.json()
|
const data = await postsRes.json()
|
||||||
hotPosts.value = data.map((p) => ({ icon: 'file-text', post: p }))
|
hotPosts.value = data.map((p) => ({
|
||||||
|
icon: 'file-text',
|
||||||
|
type: 'post',
|
||||||
|
post: p,
|
||||||
|
createdAt: p.createdAt,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const repliesRes = await fetch(`${API_BASE_URL}/api/users/${username}/hot-replies`)
|
const repliesRes = await fetch(`${API_BASE_URL}/api/users/${username}/hot-replies`)
|
||||||
@@ -398,7 +379,12 @@ const fetchSummary = async () => {
|
|||||||
const tagsRes = await fetch(`${API_BASE_URL}/api/users/${username}/hot-tags`)
|
const tagsRes = await fetch(`${API_BASE_URL}/api/users/${username}/hot-tags`)
|
||||||
if (tagsRes.ok) {
|
if (tagsRes.ok) {
|
||||||
const data = await tagsRes.json()
|
const data = await tagsRes.json()
|
||||||
hotTags.value = data.map((t) => ({ icon: 'tag-one', tag: t }))
|
hotTags.value = data.map((t) => ({
|
||||||
|
icon: 'tag-one',
|
||||||
|
type: 'tag',
|
||||||
|
tag: t,
|
||||||
|
createdAt: t.createdAt,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user