Add post change log tracking

This commit is contained in:
Tim
2025-09-08 11:27:35 +08:00
parent 7cc32c36b1
commit 039d482517
18 changed files with 543 additions and 30 deletions

View File

@@ -0,0 +1,44 @@
<template>
<div :id="`change-log-${log.id}`" class="change-log-container">
<div class="change-log-text">
<span class="change-log-user">{{ log.username }}</span>
<span v-if="log.type === 'CONTENT'">变更了文章内容</span>
<span v-else-if="log.type === 'TITLE'">变更了文章标题</span>
<span v-else-if="log.type === 'CATEGORY'">变更了文章分类</span>
<span v-else-if="log.type === 'TAG'">变更了文章标签</span>
<span v-else-if="log.type === 'CLOSED'">
<template v-if="log.newClosed">关闭了文章</template>
<template v-else>重新打开了文章</template>
</span>
<span v-else-if="log.type === 'PINNED'">
<template v-if="log.newPinnedAt">置顶了文章</template>
<template v-else>取消置顶了文章</template>
</span>
<span v-else-if="log.type === 'FEATURED'">
<template v-if="log.newFeatured">将文章设为精选</template>
<template v-else>取消精选文章</template>
</span>
</div>
<div class="change-log-time">{{ log.time }}</div>
</div>
</template>
<script setup>
const props = defineProps({ log: Object })
</script>
<style scoped>
.change-log-container {
display: flex;
flex-direction: column;
padding: 4px 0;
}
.change-log-user {
font-weight: bold;
margin-right: 4px;
}
.change-log-time {
font-size: 12px;
opacity: 0.6;
}
</style>

View File

@@ -122,9 +122,10 @@
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
</div>
<div v-else class="comments-container">
<BaseTimeline :items="comments">
<BaseTimeline :items="timelineItems">
<template #item="{ item }">
<CommentItem
v-if="item.kind === 'comment'"
:key="item.id"
:comment="item"
:level="0"
@@ -133,6 +134,7 @@
:post-closed="closed"
@deleted="onCommentDeleted"
/>
<PostChangeLogItem v-else :key="item.id" :log="item" />
</template>
</BaseTimeline>
</div>
@@ -182,6 +184,7 @@ import { useRoute } from 'vue-router'
import CommentItem from '~/components/CommentItem.vue'
import CommentEditor from '~/components/CommentEditor.vue'
import BaseTimeline from '~/components/BaseTimeline.vue'
import PostChangeLogItem from '~/components/PostChangeLogItem.vue'
import ArticleTags from '~/components/ArticleTags.vue'
import ArticleCategory from '~/components/ArticleCategory.vue'
import ReactionsGroup from '~/components/ReactionsGroup.vue'
@@ -212,6 +215,7 @@ const category = ref('')
const tags = ref([])
const postReactions = ref([])
const comments = ref([])
const changeLogs = ref([])
const status = ref('PUBLISHED')
const closed = ref(false)
const pinnedAt = ref(null)
@@ -225,6 +229,11 @@ const subscribed = ref(false)
const commentSort = ref('NEWEST')
const isFetchingComments = ref(false)
const isMobile = useIsMobile()
const timelineItems = computed(() => {
const cs = comments.value.map((c) => ({ ...c, kind: 'comment' }))
const ls = changeLogs.value.map((l) => ({ ...l, kind: 'log' }))
return [...cs, ...ls].sort((a, b) => new Date(a.time) - new Date(b.time))
})
const headerHeight = import.meta.client
? parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 0
@@ -290,8 +299,13 @@ const gatherPostItems = () => {
const main = mainContainer.value.querySelector('.info-content-container')
if (main) items.push({ el: main, top: getTop(main) })
for (const c of comments.value) {
const el = document.getElementById('comment-' + c.id)
for (const c of timelineItems.value) {
let el
if (c.kind === 'comment') {
el = document.getElementById('comment-' + c.id)
} else {
el = document.getElementById('change-log-' + c.id)
}
if (el) {
items.push({ el, top: getTop(el) })
}
@@ -329,6 +343,16 @@ const mapComment = (
parentUserClick: parentUserId ? () => navigateTo(`/users/${parentUserId}`) : null,
})
const mapChangeLog = (l) => ({
id: l.id,
username: l.username,
type: l.type,
time: TimeManager.format(l.time),
newClosed: l.newClosed,
newPinnedAt: l.newPinnedAt,
newFeatured: l.newFeatured,
})
const getTop = (el) => {
return el.getBoundingClientRect().top + window.scrollY
}
@@ -422,19 +446,21 @@ watchEffect(() => {
// router.replace('/404')
// }
const totalPosts = computed(() => comments.value.length + 1)
const totalPosts = computed(() => timelineItems.value.length + 1)
const lastReplyTime = computed(() =>
comments.value.length ? comments.value[comments.value.length - 1].time : postTime.value,
timelineItems.value.length
? timelineItems.value[timelineItems.value.length - 1].time
: postTime.value,
)
const firstReplyTime = computed(() =>
comments.value.length ? comments.value[0].time : postTime.value,
timelineItems.value.length ? timelineItems.value[0].time : postTime.value,
)
const scrollerTopTime = computed(() =>
commentSort.value === 'OLDEST' ? postTime.value : firstReplyTime.value,
)
watch(
() => comments.value.length,
() => timelineItems.value.length,
async () => {
await nextTick()
gatherPostItems()
@@ -546,6 +572,7 @@ const approvePost = async () => {
status.value = 'PUBLISHED'
toast.success('已通过审核')
await refreshPost()
await fetchChangeLogs()
} else {
toast.error('操作失败')
}
@@ -561,6 +588,7 @@ const pinPost = async () => {
if (res.ok) {
toast.success('已置顶')
await refreshPost()
await fetchChangeLogs()
} else {
toast.error('操作失败')
}
@@ -576,6 +604,7 @@ const unpinPost = async () => {
if (res.ok) {
toast.success('已取消置顶')
await refreshPost()
await fetchChangeLogs()
} else {
toast.error('操作失败')
}
@@ -591,6 +620,7 @@ const excludeRss = async () => {
if (res.ok) {
rssExcluded.value = true
toast.success('已标记为rss不推荐')
await fetchChangeLogs()
} else {
toast.error('操作失败')
}
@@ -606,6 +636,7 @@ const includeRss = async () => {
if (res.ok) {
rssExcluded.value = false
toast.success('已标记为rss推荐')
await fetchChangeLogs()
} else {
toast.error('操作失败')
}
@@ -622,6 +653,7 @@ const closePost = async () => {
closed.value = true
toast.success('已关闭')
await refreshPost()
await fetchChangeLogs()
} else {
toast.error('操作失败')
}
@@ -638,6 +670,7 @@ const reopenPost = async () => {
closed.value = false
toast.success('已重新打开')
await refreshPost()
await fetchChangeLogs()
} else {
toast.error('操作失败')
}
@@ -682,6 +715,7 @@ const rejectPost = async () => {
status.value = 'REJECTED'
toast.success('已驳回')
await refreshPost()
await fetchChangeLogs()
} else {
toast.error('操作失败')
}
@@ -740,6 +774,20 @@ const fetchComments = async () => {
}
}
const fetchChangeLogs = async () => {
try {
const res = await fetch(`${API_BASE_URL}/api/posts/${postId}/change-logs`)
if (res.ok) {
const data = await res.json()
changeLogs.value = data.map(mapChangeLog)
await nextTick()
gatherPostItems()
}
} catch (e) {
console.debug('Fetch change logs error', e)
}
}
watch(commentSort, fetchComments)
const jumpToHashComment = async () => {
@@ -763,7 +811,7 @@ const gotoProfile = () => {
const initPage = async () => {
scrollTo(0, 0)
await fetchComments()
await Promise.all([fetchComments(), fetchChangeLogs()])
const hash = location.hash
const id = hash.startsWith('#comment-') ? hash.substring('#comment-'.length) : null
if (id) expandCommentPath(id)