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

@@ -37,22 +37,22 @@ public class AdminPostController {
}
@PostMapping("/{id}/pin")
public PostSummaryDto pin(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.pinPost(id));
public PostSummaryDto pin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.pinPost(id, auth.getName()));
}
@PostMapping("/{id}/unpin")
public PostSummaryDto unpin(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.unpinPost(id));
public PostSummaryDto unpin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.unpinPost(id, auth.getName()));
}
@PostMapping("/{id}/rss-exclude")
public PostSummaryDto excludeFromRss(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.excludeFromRss(id));
public PostSummaryDto excludeFromRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.excludeFromRss(id, auth.getName()));
}
@PostMapping("/{id}/rss-include")
public PostSummaryDto includeInRss(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.includeInRss(id));
public PostSummaryDto includeInRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.includeInRss(id, auth.getName()));
}
}

View File

@@ -0,0 +1,25 @@
package com.openisle.controller;
import com.openisle.dto.PostChangeLogDto;
import com.openisle.mapper.PostChangeLogMapper;
import com.openisle.service.PostChangeLogService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
public class PostChangeLogController {
private final PostChangeLogService changeLogService;
private final PostChangeLogMapper mapper;
@GetMapping("/{id}/change-logs")
public List<PostChangeLogDto> listLogs(@PathVariable Long id) {
return changeLogService.listLogs(id).stream()
.map(mapper::toDto)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,30 @@
package com.openisle.dto;
import com.openisle.model.PostChangeType;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
public class PostChangeLogDto {
private Long id;
private String username;
private PostChangeType type;
private LocalDateTime time;
private String oldTitle;
private String newTitle;
private String oldContent;
private String newContent;
private String oldCategory;
private String newCategory;
private String oldTags;
private String newTags;
private Boolean oldClosed;
private Boolean newClosed;
private LocalDateTime oldPinnedAt;
private LocalDateTime newPinnedAt;
private Boolean oldFeatured;
private Boolean newFeatured;
}

View File

@@ -0,0 +1,39 @@
package com.openisle.mapper;
import com.openisle.dto.PostChangeLogDto;
import com.openisle.model.*;
import org.springframework.stereotype.Component;
@Component
public class PostChangeLogMapper {
public PostChangeLogDto toDto(PostChangeLog log) {
PostChangeLogDto dto = new PostChangeLogDto();
dto.setId(log.getId());
dto.setUsername(log.getUser().getUsername());
dto.setType(log.getType());
dto.setTime(log.getCreatedAt());
if (log instanceof PostTitleChangeLog t) {
dto.setOldTitle(t.getOldTitle());
dto.setNewTitle(t.getNewTitle());
} else if (log instanceof PostContentChangeLog c) {
dto.setOldContent(c.getOldContent());
dto.setNewContent(c.getNewContent());
} else if (log instanceof PostCategoryChangeLog cat) {
dto.setOldCategory(cat.getOldCategory());
dto.setNewCategory(cat.getNewCategory());
} else if (log instanceof PostTagChangeLog tag) {
dto.setOldTags(tag.getOldTags());
dto.setNewTags(tag.getNewTags());
} else if (log instanceof PostClosedChangeLog cl) {
dto.setOldClosed(cl.isOldClosed());
dto.setNewClosed(cl.isNewClosed());
} else if (log instanceof PostPinnedChangeLog p) {
dto.setOldPinnedAt(p.getOldPinnedAt());
dto.setNewPinnedAt(p.getNewPinnedAt());
} else if (log instanceof PostFeaturedChangeLog f) {
dto.setOldFeatured(f.isOldFeatured());
dto.setNewFeatured(f.isNewFeatured());
}
return dto;
}
}

View File

@@ -0,0 +1,17 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_category_change_logs")
public class PostCategoryChangeLog extends PostChangeLog {
private String oldCategory;
private String newCategory;
}

View File

@@ -0,0 +1,37 @@
package com.openisle.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_change_logs")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class PostChangeLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "post_id")
private Post post;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id")
private User user;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PostChangeType type;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.model;
public enum PostChangeType {
CONTENT,
TITLE,
CATEGORY,
TAG,
CLOSED,
PINNED,
FEATURED
}

View File

@@ -0,0 +1,17 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_closed_change_logs")
public class PostClosedChangeLog extends PostChangeLog {
private boolean oldClosed;
private boolean newClosed;
}

View File

@@ -0,0 +1,21 @@
package com.openisle.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_content_change_logs")
public class PostContentChangeLog extends PostChangeLog {
@Column(name = "old_content", columnDefinition = "LONGTEXT")
private String oldContent;
@Column(name = "new_content", columnDefinition = "LONGTEXT")
private String newContent;
}

View File

@@ -0,0 +1,17 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_featured_change_logs")
public class PostFeaturedChangeLog extends PostChangeLog {
private boolean oldFeatured;
private boolean newFeatured;
}

View File

@@ -0,0 +1,19 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_pinned_change_logs")
public class PostPinnedChangeLog extends PostChangeLog {
private LocalDateTime oldPinnedAt;
private LocalDateTime newPinnedAt;
}

View File

@@ -0,0 +1,21 @@
package com.openisle.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_tag_change_logs")
public class PostTagChangeLog extends PostChangeLog {
@Column(name = "old_tags")
private String oldTags;
@Column(name = "new_tags")
private String newTags;
}

View File

@@ -0,0 +1,17 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_title_change_logs")
public class PostTitleChangeLog extends PostChangeLog {
private String oldTitle;
private String newTitle;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.repository;
import com.openisle.model.Post;
import com.openisle.model.PostChangeLog;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PostChangeLogRepository extends JpaRepository<PostChangeLog, Long> {
List<PostChangeLog> findByPostOrderByCreatedAtAsc(Post post);
}

View File

@@ -0,0 +1,94 @@
package com.openisle.service;
import com.openisle.model.*;
import com.openisle.repository.PostChangeLogRepository;
import com.openisle.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class PostChangeLogService {
private final PostChangeLogRepository logRepository;
private final PostRepository postRepository;
public void recordContentChange(Post post, User user, String oldContent, String newContent) {
PostContentChangeLog log = new PostContentChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.CONTENT);
log.setOldContent(oldContent);
log.setNewContent(newContent);
logRepository.save(log);
}
public void recordTitleChange(Post post, User user, String oldTitle, String newTitle) {
PostTitleChangeLog log = new PostTitleChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.TITLE);
log.setOldTitle(oldTitle);
log.setNewTitle(newTitle);
logRepository.save(log);
}
public void recordCategoryChange(Post post, User user, String oldCategory, String newCategory) {
PostCategoryChangeLog log = new PostCategoryChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.CATEGORY);
log.setOldCategory(oldCategory);
log.setNewCategory(newCategory);
logRepository.save(log);
}
public void recordTagChange(Post post, User user, Set<Tag> oldTags, Set<Tag> newTags) {
PostTagChangeLog log = new PostTagChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.TAG);
log.setOldTags(oldTags.stream().map(Tag::getName).collect(Collectors.joining(",")));
log.setNewTags(newTags.stream().map(Tag::getName).collect(Collectors.joining(",")));
logRepository.save(log);
}
public void recordClosedChange(Post post, User user, boolean oldClosed, boolean newClosed) {
PostClosedChangeLog log = new PostClosedChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.CLOSED);
log.setOldClosed(oldClosed);
log.setNewClosed(newClosed);
logRepository.save(log);
}
public void recordPinnedChange(Post post, User user, java.time.LocalDateTime oldPinnedAt, java.time.LocalDateTime newPinnedAt) {
PostPinnedChangeLog log = new PostPinnedChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.PINNED);
log.setOldPinnedAt(oldPinnedAt);
log.setNewPinnedAt(newPinnedAt);
logRepository.save(log);
}
public void recordFeaturedChange(Post post, User user, boolean oldFeatured, boolean newFeatured) {
PostFeaturedChangeLog log = new PostFeaturedChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.FEATURED);
log.setOldFeatured(oldFeatured);
log.setNewFeatured(newFeatured);
logRepository.save(log);
}
public List<PostChangeLog> listLogs(Long postId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
return logRepository.findByPostOrderByCreatedAtAsc(post);
}
}

View File

@@ -19,6 +19,7 @@ import com.openisle.repository.CategoryRepository;
import com.openisle.repository.TagRepository;
import com.openisle.service.SubscriptionService;
import com.openisle.service.CommentService;
import com.openisle.service.PostChangeLogService;
import com.openisle.repository.CommentRepository;
import com.openisle.repository.ReactionRepository;
import com.openisle.repository.PostSubscriptionRepository;
@@ -74,6 +75,7 @@ public class PostService {
private final EmailSender emailSender;
private final ApplicationContext applicationContext;
private final PointService pointService;
private final PostChangeLogService postChangeLogService;
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
@Value("${app.website-url:https://www.open-isle.com}")
private String websiteUrl;
@@ -99,6 +101,7 @@ public class PostService {
EmailSender emailSender,
ApplicationContext applicationContext,
PointService pointService,
PostChangeLogService postChangeLogService,
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
this.postRepository = postRepository;
this.userRepository = userRepository;
@@ -120,6 +123,7 @@ public class PostService {
this.emailSender = emailSender;
this.applicationContext = applicationContext;
this.pointService = pointService;
this.postChangeLogService = postChangeLogService;
this.publishMode = publishMode;
}
@@ -159,19 +163,28 @@ public class PostService {
return postRepository.findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus.PUBLISHED, pageable);
}
public Post excludeFromRss(Long id) {
public Post excludeFromRss(Long id, String username) {
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
boolean oldFeatured = !Boolean.TRUE.equals(post.getRssExcluded());
post.setRssExcluded(true);
return postRepository.save(post);
Post saved = postRepository.save(post);
postChangeLogService.recordFeaturedChange(saved, user, oldFeatured, false);
return saved;
}
public Post includeInRss(Long id) {
public Post includeInRss(Long id, String username) {
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
boolean oldFeatured = !Boolean.TRUE.equals(post.getRssExcluded());
post.setRssExcluded(false);
post = postRepository.save(post);
notificationService.createNotification(post.getAuthor(), NotificationType.POST_FEATURED, post, null, null, null, null, null);
pointService.awardForFeatured(post.getAuthor().getUsername(), post.getId());
return post;
Post saved = postRepository.save(post);
postChangeLogService.recordFeaturedChange(saved, user, oldFeatured, true);
notificationService.createNotification(saved.getAuthor(), NotificationType.POST_FEATURED, saved, null, null, null, null, null);
pointService.awardForFeatured(saved.getAuthor().getUsername(), saved.getId());
return saved;
}
public Post createPost(String username,
@@ -638,18 +651,28 @@ public class PostService {
return post;
}
public Post pinPost(Long id) {
public Post pinPost(Long id, String username) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
java.time.LocalDateTime oldPinned = post.getPinnedAt();
post.setPinnedAt(java.time.LocalDateTime.now());
return postRepository.save(post);
Post saved = postRepository.save(post);
postChangeLogService.recordPinnedChange(saved, user, oldPinned, saved.getPinnedAt());
return saved;
}
public Post unpinPost(Long id) {
public Post unpinPost(Long id, String username) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
java.time.LocalDateTime oldPinned = post.getPinnedAt();
post.setPinnedAt(null);
return postRepository.save(post);
Post saved = postRepository.save(post);
postChangeLogService.recordPinnedChange(saved, user, oldPinned, null);
return saved;
}
public Post closePost(Long id, String username) {
@@ -660,8 +683,11 @@ public class PostService {
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
throw new IllegalArgumentException("Unauthorized");
}
boolean oldClosed = post.isClosed();
post.setClosed(true);
return postRepository.save(post);
Post saved = postRepository.save(post);
postChangeLogService.recordClosedChange(saved, user, oldClosed, true);
return saved;
}
public Post reopenPost(Long id, String username) {
@@ -672,8 +698,11 @@ public class PostService {
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
throw new IllegalArgumentException("Unauthorized");
}
boolean oldClosed = post.isClosed();
post.setClosed(false);
return postRepository.save(post);
Post saved = postRepository.save(post);
postChangeLogService.recordClosedChange(saved, user, oldClosed, false);
return saved;
}
@org.springframework.transaction.annotation.Transactional
@@ -702,14 +731,30 @@ public class PostService {
if (tags.isEmpty()) {
throw new IllegalArgumentException("Tag not found");
}
post.setTitle(title);
String oldTitle = post.getTitle();
String oldContent = post.getContent();
Category oldCategory = post.getCategory();
java.util.Set<com.openisle.model.Tag> oldTags = new java.util.HashSet<>(post.getTags());
post.setTitle(title);
post.setContent(content);
post.setCategory(category);
post.setTags(new java.util.HashSet<>(tags));
Post updated = postRepository.save(post);
imageUploader.adjustReferences(oldContent, content);
notificationService.notifyMentions(content, user, updated, null);
if (!java.util.Objects.equals(oldTitle, title)) {
postChangeLogService.recordTitleChange(updated, user, oldTitle, title);
}
if (!java.util.Objects.equals(oldContent, content)) {
postChangeLogService.recordContentChange(updated, user, oldContent, content);
}
if (!java.util.Objects.equals(oldCategory.getId(), category.getId())) {
postChangeLogService.recordCategoryChange(updated, user, oldCategory.getName(), category.getName());
}
java.util.Set<com.openisle.model.Tag> newTags = new java.util.HashSet<>(tags);
if (!oldTags.equals(newTags)) {
postChangeLogService.recordTagChange(updated, user, oldTags, newTags);
}
return updated;
}

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)