Merge branch 'main' into codex/post

This commit is contained in:
Tim
2025-07-09 18:29:21 +08:00
committed by GitHub
11 changed files with 346 additions and 108 deletions

View File

@@ -9,7 +9,13 @@
<div class="topic-container">
<div class="topic-item-container">
<div v-for="topic in topics" :key="topic" class="topic-item" :class="{ selected: topic === selectedTopic }">
<div
v-for="topic in topics"
:key="topic"
class="topic-item"
:class="{ selected: topic === selectedTopic }"
@click="selectedTopic = topic"
>
{{ topic }}
</div>
<CategorySelect v-model="selectedCategory" />
@@ -18,60 +24,71 @@
</div>
<div class="article-container">
<div class="header-container">
<div class="header-item main-item">
<div class="header-item-text">话题</div>
</div>
<div class="header-item avatars">
<div class="header-item-text">参与人员</div>
</div>
<div class="header-item">
<div class="header-item-text">回复</div>
</div>
<div class="header-item">
<div class="header-item-text">浏览</div>
</div>
<div class="header-item">
<div class="header-item-text">活动</div>
</div>
</div>
<div v-if="isLoadingPosts && articles.length === 0" class="loading-container">
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
</div>
<div v-else-if="articles.length === 0">
<div class="no-posts-container">
<div class="no-posts-text">暂时没有帖子 :( 点击发帖发送第一篇相关帖子吧!</div>
</div>
</div>
<div class="article-item" v-for="article in articles" :key="article.id">
<div class="article-main-container">
<router-link class="article-item-title main-item" :to="`/posts/${article.id}`">
{{ article.title }}
</router-link>
<div class="article-item-description main-item">{{ sanitizeDescription(article.description) }}</div>
<div class="article-info-container main-item">
<ArticleTags :tags="[article.category]" />
<ArticleTags :tags="article.tags" />
<template v-if="selectedTopic === '最新'">
<div class="header-container">
<div class="header-item main-item">
<div class="header-item-text">话题</div>
</div>
<div class="header-item avatars">
<div class="header-item-text">参与人员</div>
</div>
<div class="header-item">
<div class="header-item-text">回复</div>
</div>
<div class="header-item">
<div class="header-item-text">浏览</div>
</div>
<div class="header-item">
<div class="header-item-text">活动</div>
</div>
</div>
<div class="article-member-avatars-container">
<div class="article-member-avatar-item" v-for="(avatar, idx) in article.members" :key="idx">
<img class="article-member-avatar-item-img" :src="avatar" alt="avatar">
<div v-if="isLoadingPosts && articles.length === 0" class="loading-container">
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
</div>
<div v-else-if="articles.length === 0">
<div class="no-posts-container">
<div class="no-posts-text">暂时没有帖子 :( 点击发帖发送第一篇相关帖子吧!</div>
</div>
</div>
<div class="article-comments">
{{ article.comments }}
</div>
<div class="article-views">
{{ article.views }}
</div>
<div class="article-time">
{{ article.time }}
<div class="article-item" v-for="article in articles" :key="article.id">
<div class="article-main-container">
<router-link class="article-item-title main-item" :to="`/posts/${article.id}`">
{{ article.title }}
</router-link>
<div class="article-item-description main-item">{{ sanitizeDescription(article.description) }}</div>
<div class="article-info-container main-item">
<ArticleTags :tags="[article.category]" />
<ArticleTags :tags="article.tags" />
</div>
</div>
<div class="article-member-avatars-container">
<div class="article-member-avatar-item" v-for="(avatar, idx) in article.members" :key="idx">
<img class="article-member-avatar-item-img" :src="avatar" alt="avatar">
</div>
</div>
<div class="article-comments">
{{ article.comments }}
</div>
<div class="article-views">
{{ article.views }}
</div>
<div class="article-time">
{{ article.time }}
</div>
</div>
</template>
<div v-else-if="selectedTopic === '排行榜'" class="placeholder-container">
排行榜功能开发中敬请期待
</div>
<div v-else-if="selectedTopic === '热门'" class="placeholder-container">
热门帖子功能开发中敬请期待
</div>
<div v-else class="placeholder-container">
分类浏览功能开发中敬请期待
</div>
<div v-if="isLoadingPosts && articles.length > 0" class="loading-container bottom-loading">
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
@@ -350,4 +367,13 @@ export default {
height: 100%;
object-fit: cover;
}
.placeholder-container {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
font-size: 16px;
opacity: 0.7;
}
</style>

View File

@@ -13,18 +13,19 @@
<BaseTimeline :items="notifications">
<template #item="{ item }">
<div class="notif-content">
<div class="notif-content" :class="{ read: item.read }">
<span v-if="!item.read" class="unread-dot"></span>
<span class="notif-type">
<template v-if="item.type === 'COMMENT_REPLY' && item.parentComment">
<div class="notif-content-container">
<span class="notif-user">{{ item.comment.author.username }} </span> 对我的评论
<span>
<router-link class="notif-content-text"
<router-link class="notif-content-text" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.parentComment.id}`">
{{ sanitizeDescription(item.parentComment.content) }}
</router-link>
</span> 回复了 <span>
<router-link class="notif-content-text" :to="`/posts/${item.post.id}#comment-${item.comment.id}`">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}#comment-${item.comment.id}`">
{{ sanitizeDescription(item.comment.content) }}
</router-link>
</span>
@@ -34,11 +35,11 @@
<div class="notif-content-container">
<span class="notif-user">{{ item.comment.author.username }} </span> 对我的文章
<span>
<router-link class="notif-content-text" :to="`/posts/${item.post.id}`">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
{{ sanitizeDescription(item.post.title) }}
</router-link>
</span> 回复了 <span>
<router-link class="notif-content-text" :to="`/posts/${item.post.id}#comment-${item.comment.id}`">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}#comment-${item.comment.id}`">
{{ sanitizeDescription(item.comment.content) }}
</router-link>
</span>
@@ -63,6 +64,7 @@ import { useRouter } from 'vue-router'
import { API_BASE_URL } from '../main'
import BaseTimeline from '../components/BaseTimeline.vue'
import { getToken } from '../utils/auth'
import { markNotificationsRead } from '../utils/notification'
import { toast } from '../main'
import { stripMarkdown } from '../utils/markdown'
import { hatch } from 'ldrs'
@@ -76,6 +78,15 @@ export default {
const notifications = ref([])
const isLoadingMessage = ref(false)
const markRead = async id => {
if (!id) return
const ok = await markNotificationsRead([id])
if (ok) {
const n = notifications.value.find(n => n.id === id)
if (n) n.read = true
}
}
const iconMap = {
POST_VIEWED: 'fas fa-eye',
COMMENT_REPLY: 'fas fa-reply',
@@ -114,7 +125,10 @@ export default {
notifications.value.push({
...n,
src: n.comment.author.avatar,
iconClick: () => router.push(`/users/${n.comment.author.id}`)
iconClick: () => {
markRead(n.id)
router.push(`/users/${n.comment.author.id}`)
}
})
} else {
notifications.value.push({
@@ -149,7 +163,7 @@ export default {
onMounted(fetchNotifications)
return { notifications, formatType, sanitizeDescription, isLoadingMessage }
return { notifications, formatType, sanitizeDescription, isLoadingMessage, markRead }
}
}
</script>
@@ -187,6 +201,21 @@ export default {
display: flex;
flex-direction: column;
margin-bottom: 30px;
position: relative;
}
.notif-content.read {
opacity: 0.7;
}
.unread-dot {
position: absolute;
left: -10px;
top: 4px;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #ff4d4f;
}
.notif-type {

View File

@@ -27,31 +27,11 @@
<div class="info-content-text" v-html="renderMarkdown(postContent)"></div>
<div class="article-footer-container">
<div class="reactions-container">
<div class="reactions-viewer">
<div class="reactions-viewer-item-container">
<div class="reactions-viewer-item">
🤣
</div>
<div class="reactions-viewer-item">
</div>
<div class="reactions-viewer-item">
👏
</div>
</div>
<div class="reactions-count">1882</div>
<ReactionsGroup v-model="postReactions" content-type="post" :content-id="postId">
<div class="make-reaction-item copy-link" @click="copyPostLink">
<i class="fas fa-link"></i>
</div>
<div class="make-reaction-container">
<div class="make-reaction-item like-reaction">
<i class="far fa-heart"></i>
</div>
<div class="make-reaction-item copy-link" @click="copyPostLink">
<i class="fas fa-link"></i>
</div>
</div>
</div>
</ReactionsGroup>
</div>
</div>
</div>
@@ -98,6 +78,7 @@ import CommentItem from '../components/CommentItem.vue'
import CommentEditor from '../components/CommentEditor.vue'
import BaseTimeline from '../components/BaseTimeline.vue'
import ArticleTags from '../components/ArticleTags.vue'
import ReactionsGroup from '../components/ReactionsGroup.vue'
import { renderMarkdown } from '../utils/markdown'
import { API_BASE_URL, toast } from '../main'
import { getToken } from '../utils/auth'
@@ -107,7 +88,7 @@ hatch.register()
export default {
name: 'PostPageView',
components: { CommentItem, CommentEditor, BaseTimeline, ArticleTags },
components: { CommentItem, CommentEditor, BaseTimeline, ArticleTags, ReactionsGroup },
setup() {
const route = useRoute()
const postId = route.params.id
@@ -118,6 +99,7 @@ export default {
const postContent = ref('')
const category = ref('')
const tags = ref([])
const postReactions = ref([])
const comments = ref([])
const isWaitingFetchingPost = ref(false);
const isWaitingPostingComment = ref(false);
@@ -150,6 +132,7 @@ export default {
time: new Date(c.createdAt).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' }),
avatar: c.author.avatar,
text: c.content,
reactions: c.reactions || [],
reply: (c.replies || []).map(mapComment),
openReplies: false,
src: c.author.avatar,
@@ -196,6 +179,7 @@ export default {
title.value = data.title
category.value = data.category
tags.value = data.tags || []
postReactions.value = data.reactions || []
comments.value = (data.comments || []).map(mapComment)
postTime.value = new Date(data.createdAt).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' })
await nextTick()
@@ -323,6 +307,8 @@ export default {
mainContainer,
currentIndex,
totalPosts,
postReactions,
postId,
postComment,
onSliderInput,
onScroll: updateCurrentIndex,