Compare commits

...

9 Commits

Author SHA1 Message Date
tim
a9d8181940 fix: timeline ui 重构 2025-09-19 16:21:19 +08:00
Tim
4cc108094d Merge pull request #1009 from nagisa77/codex/integrate-timelinetagitem-and-refactor-components
feat: extract timeline tag item component
2025-09-19 13:50:15 +08:00
Tim
bfa57cce44 feat: extract timeline tag item component 2025-09-19 13:44:37 +08:00
tim
8ebdcd94f5 fix: timeline 继承标签介绍 2025-09-19 11:30:58 +08:00
tim
9991210db2 fix: 部分ui修改 2025-09-19 11:21:27 +08:00
Tim
1c59815afa Merge pull request #1007 from nagisa77/codex/refactor-user-posts-display-components-aopsvr
Enhance user timeline post metadata and grouping
2025-09-19 00:32:17 +08:00
tim
bc767a6ac9 Revert "Enhance user timeline grouping and post metadata"
This reverts commit b6c2471bc3.
2025-09-19 00:31:24 +08:00
Tim
1c1915285d Merge pull request #1006 from nagisa77/codex/refactor-user-posts-display-components
Enhance user timeline grouping and post metadata
2025-09-19 00:22:58 +08:00
Tim
b6c2471bc3 Enhance user timeline grouping and post metadata 2025-09-19 00:22:34 +08:00
6 changed files with 200 additions and 78 deletions

View File

@@ -95,7 +95,7 @@ export default {
} }
.timeline-item:last-child::before { .timeline-item:last-child::before {
display: none; bottom: 0px;
} }
.timeline-content { .timeline-content {

View File

@@ -9,23 +9,6 @@
<div class="comment-content-item-main"> <div class="comment-content-item-main">
<comment-one class="comment-content-item-icon" /> <comment-one class="comment-content-item-icon" />
<div class="comment-content-item-text"> <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 <NuxtLink
:to="`/posts/${entry.comment.post.id}#comment-${entry.comment.id}`" :to="`/posts/${entry.comment.post.id}#comment-${entry.comment.id}`"
class="timeline-comment-link" class="timeline-comment-link"
@@ -65,7 +48,7 @@ const entries = computed(() => {
return [] return []
}) })
const formattedDate = computed(() => TimeManager.format(props.item.createdAt)) const formattedDate = computed(() => TimeManager.formatWithDay(props.item.createdAt))
const hasReplies = computed(() => entries.value.some((entry) => !!entry.comment.parentComment)) const hasReplies = computed(() => entries.value.some((entry) => !!entry.comment.parentComment))
const hasComments = computed(() => entries.value.some((entry) => !entry.comment.parentComment)) const hasComments = computed(() => entries.value.some((entry) => !entry.comment.parentComment))
@@ -93,9 +76,8 @@ const parentSnippet = (entry) =>
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
padding: 10px 12px; padding-top: 5px;
border-radius: 10px; padding-bottom: 20px;
background: var(--timeline-card-background, transparent);
} }
.timeline-header { .timeline-header {
@@ -112,20 +94,20 @@ const parentSnippet = (entry) =>
.timeline-date { .timeline-date {
font-size: 12px; font-size: 12px;
color: var(--timeline-date-color, #888); color: var(--timeline-date-color, #888);
white-space: nowrap;
} }
.comment-content { .comment-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 3px;
} }
.comment-content-item { .comment-content-item {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
justify-content: space-between;
gap: 6px; gap: 6px;
padding-bottom: 6px;
border-bottom: 1px solid var(--comment-item-border, rgba(0, 0, 0, 0.05));
} }
.comment-content-item:last-child { .comment-content-item:last-child {
@@ -160,11 +142,11 @@ const parentSnippet = (entry) =>
.timeline-comment-link { .timeline-comment-link {
font-size: 14px; font-size: 14px;
color: var(--link-color); color: var(--link-color);
text-decoration: none; text-decoration: underline;
} }
.timeline-comment-link:hover { .timeline-comment-link:hover {
text-decoration: underline; color: var(--primary-color);
} }
.timeline-link { .timeline-link {

View File

@@ -13,11 +13,7 @@
</div> </div>
<div class="article-meta" v-if="hasMeta"> <div class="article-meta" v-if="hasMeta">
<ArticleCategory v-if="item.post?.category" :category="item.post.category" /> <ArticleCategory v-if="item.post?.category" :category="item.post.category" />
<div class="article-tags" v-if="(item.post?.tags?.length ?? 0) > 0"> <ArticleTags :tags="item.post?.tags" />
<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"> <div class="article-comment-count" v-if="item.post?.commentCount !== undefined">
<comment-one class="article-comment-count-icon" /> <comment-one class="article-comment-count-icon" />
<span>{{ item.post?.commentCount }}</span> <span>{{ item.post?.commentCount }}</span>
@@ -29,7 +25,6 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import ArticleCategory from '~/components/ArticleCategory.vue'
import { stripMarkdown } from '~/utils/markdown' import { stripMarkdown } from '~/utils/markdown'
import TimeManager from '~/utils/time' import TimeManager from '~/utils/time'
@@ -58,8 +53,8 @@ const hasMeta = computed(() => {
.timeline-container { .timeline-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-top: 5px;
gap: 12px; gap: 12px;
padding: 10px 12px;
border-radius: 10px; border-radius: 10px;
background: var(--timeline-card-background, transparent); background: var(--timeline-card-background, transparent);
} }
@@ -83,6 +78,9 @@ const hasMeta = computed(() => {
.article-container { .article-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid var(--normal-border-color);
border-radius: 10px;
padding: 10px;
gap: 6px; gap: 6px;
} }

View File

@@ -0,0 +1,111 @@
<template>
<div class="timeline-tag-item">
<div class="tags-container">
<div class="tags-container-item">
<div class="timeline-tag-title">创建了标签</div>
<ArticleTags v-if="tag" :tags="[tag]" />
<span class="timeline-tag-count" v-if="tag?.count"> x{{ tag.count }}</span>
</div>
<div v-if="timelineDate" class="timeline-date">{{ timelineDate }}</div>
</div>
<div v-if="hasDescription" class="timeline-snippet">
{{ tag?.description }}
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import TimeManager from '~/utils/time'
const props = defineProps({
item: { type: Object, required: true },
})
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-tag-count {
font-size: 12px;
}
.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>

View File

@@ -112,19 +112,18 @@
{{ item.comment.post.title }} {{ item.comment.post.title }}
</NuxtLink> </NuxtLink>
<template v-if="item.comment.parentComment"> <template v-if="item.comment.parentComment">
下对
<NuxtLink <NuxtLink
:to="`/posts/${item.comment.post.id}#comment-${item.comment.parentComment.id}`" :to="`/posts/${item.comment.post.id}#comment-${item.comment.parentComment.id}`"
class="timeline-link" class="timeline-comment-link"
> >
{{ stripMarkdownLength(item.comment.parentComment.content, 200) }} {{ stripMarkdownLength(item.comment.parentComment.content, 200) }}
</NuxtLink> </NuxtLink>
回复了 <next class="reply-icon" /> 回复了
</template> </template>
<template v-else> 下评论了 </template> <template v-else> 下评论了 </template>
<NuxtLink <NuxtLink
:to="`/posts/${item.comment.post.id}#comment-${item.comment.id}`" :to="`/posts/${item.comment.post.id}#comment-${item.comment.id}`"
class="timeline-link" class="timeline-comment-link"
> >
{{ stripMarkdownLength(item.comment.content, 200) }} {{ stripMarkdownLength(item.comment.content, 200) }}
</NuxtLink> </NuxtLink>
@@ -143,15 +142,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 +155,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" />
{{ 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>
@@ -212,13 +195,6 @@
<div class="timeline-list"> <div class="timeline-list">
<BaseTimeline :items="filteredTimelineItems"> <BaseTimeline :items="filteredTimelineItems">
<template #item="{ item }"> <template #item="{ item }">
<!-- <template v-if="item.type === 'post'">
发布了文章
<NuxtLink :to="`/posts/${item.post.id}`" class="timeline-link">
{{ item.post.title }}
</NuxtLink>
<div class="timeline-date">{{ formatDate(item.createdAt) }}</div>
</template> -->
<template v-if="item.type === 'post'"> <template v-if="item.type === 'post'">
<TimelinePostItem :item="item" /> <TimelinePostItem :item="item" />
</template> </template>
@@ -229,14 +205,7 @@
<TimelineCommentGroup :item="item" /> <TimelineCommentGroup :item="item" />
</template> </template>
<template v-else-if="item.type === 'tag'"> <template v-else-if="item.type === 'tag'">
创建了标签 <TimelineTagItem :item="item" />
<span class="timeline-link" @click="gotoTag(item.tag)">
{{ 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.createdAt) }}</div>
</template> </template>
</template> </template>
</BaseTimeline> </BaseTimeline>
@@ -302,6 +271,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'
@@ -391,7 +361,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`)
@@ -403,7 +378,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,
}))
} }
} }
@@ -689,6 +669,11 @@ watch(selectedTab, async (val) => {
color: #666; color: #666;
} }
.reply-icon {
color: var(--primary-color);
margin-left: 5px;
}
.profile-page-header-user-info-buttons { .profile-page-header-user-info-buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -941,8 +926,8 @@ watch(selectedTab, async (val) => {
.timeline-link { .timeline-link {
font-weight: bold; font-weight: bold;
color: var(--primary-color);
text-decoration: none; text-decoration: none;
color: var(--text-color);
word-break: break-word; word-break: break-word;
} }
@@ -979,9 +964,25 @@ watch(selectedTab, async (val) => {
justify-content: space-between; justify-content: space-between;
} }
.timeline-title { .tags-container {
font-size: 18px; display: flex;
font-weight: bold; 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;
} }
.comment-content { .comment-content {
@@ -1017,6 +1018,7 @@ watch(selectedTab, async (val) => {
color: var(--text-color); color: var(--text-color);
word-break: break-word; word-break: break-word;
text-decoration: underline; text-decoration: underline;
margin-left: 5px;
} }
.timeline-comment-link:hover { .timeline-comment-link:hover {

View File

@@ -40,4 +40,33 @@ export default class TimeManager {
return `${date.getFullYear()}.${month}.${day} ${timePart}` return `${date.getFullYear()}.${month}.${day} ${timePart}`
} }
// 仅显示日期(不含时间)
static formatWithDay(input) {
const date = new Date(input)
if (Number.isNaN(date.getTime())) return ''
const now = new Date()
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const startOfDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
const diffDays = Math.floor((startOfToday - startOfDate) / 86400000)
if (diffDays === 0) return '今天'
if (diffDays === 1) return '昨天'
if (diffDays === 2) return '前天'
const month = date.getMonth() + 1
const day = date.getDate()
if (date.getFullYear() === now.getFullYear()) {
return `${month}.${day}`
}
if (date.getFullYear() === now.getFullYear() - 1) {
return `去年 ${month}.${day}`
}
return `${date.getFullYear()}.${month}.${day}`
}
} }