mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-03-01 01:21:04 +08:00
Enhance user timeline post metadata and grouping
This commit is contained in:
185
frontend_nuxt/components/TimelineCommentGroup.vue
Normal file
185
frontend_nuxt/components/TimelineCommentGroup.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="timeline-container">
|
||||
<div class="timeline-header">
|
||||
<div class="timeline-title">{{ headerText }}</div>
|
||||
<div class="timeline-date">{{ formattedDate }}</div>
|
||||
</div>
|
||||
<div class="comment-content" v-if="entries.length > 0">
|
||||
<div class="comment-content-item" v-for="entry in entries" :key="entry.comment.id">
|
||||
<div class="comment-content-item-main">
|
||||
<comment-one class="comment-content-item-icon" />
|
||||
<div class="comment-content-item-text">
|
||||
<span class="comment-content-item-prefix">
|
||||
在
|
||||
<NuxtLink :to="`/posts/${entry.comment.post.id}`" class="timeline-link">
|
||||
{{ entry.comment.post.title }}
|
||||
</NuxtLink>
|
||||
<template v-if="entry.comment.parentComment">
|
||||
下对
|
||||
<NuxtLink
|
||||
:to="`/posts/${entry.comment.post.id}#comment-${entry.comment.parentComment.id}`"
|
||||
class="timeline-link"
|
||||
>
|
||||
{{ parentSnippet(entry) }}
|
||||
</NuxtLink>
|
||||
回复了
|
||||
</template>
|
||||
<template v-else> 下评论了 </template>
|
||||
</span>
|
||||
<NuxtLink
|
||||
:to="`/posts/${entry.comment.post.id}#comment-${entry.comment.id}`"
|
||||
class="timeline-comment-link"
|
||||
>
|
||||
{{ stripContent(entry.comment.content) }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline-date">{{ formatEntryDate(entry.createdAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { stripMarkdownLength } from '~/utils/markdown'
|
||||
import TimeManager from '~/utils/time'
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
})
|
||||
|
||||
const entries = computed(() => {
|
||||
if (Array.isArray(props.item.entries)) {
|
||||
return props.item.entries
|
||||
}
|
||||
if (props.item.comment) {
|
||||
return [
|
||||
{
|
||||
type: props.item.type,
|
||||
comment: props.item.comment,
|
||||
createdAt: props.item.createdAt,
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const formattedDate = computed(() => TimeManager.format(props.item.createdAt))
|
||||
|
||||
const hasReplies = computed(() => entries.value.some((entry) => !!entry.comment.parentComment))
|
||||
const hasComments = computed(() => entries.value.some((entry) => !entry.comment.parentComment))
|
||||
|
||||
const headerText = computed(() => {
|
||||
const count = entries.value.length
|
||||
if (count === 0) return ''
|
||||
if (hasComments.value && hasReplies.value) {
|
||||
return `发布了${count}条评论/回复`
|
||||
}
|
||||
if (hasReplies.value) {
|
||||
return `发布了${count}条回复`
|
||||
}
|
||||
return `发布了${count}条评论`
|
||||
})
|
||||
|
||||
const formatEntryDate = (date) => TimeManager.format(date)
|
||||
const stripContent = (content) => stripMarkdownLength(content ?? '', 200)
|
||||
const parentSnippet = (entry) =>
|
||||
stripMarkdownLength(entry.comment.parentComment?.content ?? '', 200)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.timeline-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--timeline-card-background, transparent);
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
font-size: 12px;
|
||||
color: var(--timeline-date-color, #888);
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.comment-content-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--comment-item-border, rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
|
||||
.comment-content-item:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.comment-content-item-main {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.comment-content-item-icon {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.comment-content-item-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.comment-content-item-prefix {
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.timeline-comment-link {
|
||||
font-size: 14px;
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.timeline-comment-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.timeline-link {
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.timeline-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comment-content-item-prefix,
|
||||
.timeline-comment-link {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
149
frontend_nuxt/components/TimelinePostItem.vue
Normal file
149
frontend_nuxt/components/TimelinePostItem.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="timeline-container">
|
||||
<div class="timeline-header">
|
||||
<div class="timeline-title">发布了文章</div>
|
||||
<div class="timeline-date">{{ formattedDate }}</div>
|
||||
</div>
|
||||
<div class="article-container">
|
||||
<NuxtLink :to="postLink" class="timeline-article-link">
|
||||
{{ item.post?.title }}
|
||||
</NuxtLink>
|
||||
<div class="timeline-snippet">
|
||||
{{ strippedSnippet }}
|
||||
</div>
|
||||
<div class="article-meta" v-if="hasMeta">
|
||||
<ArticleCategory v-if="item.post?.category" :category="item.post.category" />
|
||||
<div class="article-tags" v-if="(item.post?.tags?.length ?? 0) > 0">
|
||||
<span class="article-tag" v-for="tag in item.post?.tags" :key="tag.id || tag.name">
|
||||
#{{ tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="article-comment-count" v-if="item.post?.commentCount !== undefined">
|
||||
<comment-one class="article-comment-count-icon" />
|
||||
<span>{{ item.post?.commentCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||
import { stripMarkdown } from '~/utils/markdown'
|
||||
import TimeManager from '~/utils/time'
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
})
|
||||
|
||||
const postLink = computed(() => {
|
||||
const id = props.item.post?.id
|
||||
return id ? `/posts/${id}` : '#'
|
||||
})
|
||||
|
||||
const formattedDate = computed(() => TimeManager.format(props.item.createdAt))
|
||||
const strippedSnippet = computed(() => stripMarkdown(props.item.post?.snippet ?? ''))
|
||||
const hasMeta = computed(() => {
|
||||
const tags = props.item.post?.tags ?? []
|
||||
const hasTags = Array.isArray(tags) && tags.length > 0
|
||||
const hasCategory = !!props.item.post?.category
|
||||
const hasCommentCount =
|
||||
props.item.post?.commentCount !== undefined && props.item.post?.commentCount !== null
|
||||
return hasTags || hasCategory || hasCommentCount
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.timeline-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--timeline-card-background, transparent);
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
font-size: 12px;
|
||||
color: var(--timeline-date-color, #888);
|
||||
}
|
||||
|
||||
.article-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.timeline-article-link {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.timeline-article-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.timeline-snippet {
|
||||
color: var(--timeline-snippet-color, #666);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.article-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.article-tag {
|
||||
background-color: var(--article-info-background-color);
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.article-comment-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.article-comment-count-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.timeline-article-link {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.timeline-snippet {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user