Compare commits

...

6 Commits

Author SHA1 Message Date
Tim
c38e4bc44c feat: enable dark mode for diff2html 2025-09-08 14:28:42 +08:00
Tim
f3421265d2 fix: 修改changelog UI 2025-09-08 14:02:47 +08:00
Tim
f4817cd6d1 Merge pull request #929 from nagisa77/codex/add-user-avatar-return-in-changelog
feat: expand post change log details
2025-09-08 13:54:51 +08:00
Tim
5ae0f9311c feat: add result change log entities 2025-09-08 13:54:35 +08:00
Tim
567452f570 feat: 标题/内容变化的ui 2025-09-08 13:46:22 +08:00
Tim
bb4e866bd0 Merge pull request #928 from nagisa77/codex/add-content-change-details-rendering
feat(frontend): render diff for content changes
2025-09-08 13:22:44 +08:00
9 changed files with 148 additions and 21 deletions

View File

@@ -11,6 +11,7 @@ import java.time.LocalDateTime;
public class PostChangeLogDto { public class PostChangeLogDto {
private Long id; private Long id;
private String username; private String username;
private String userAvatar;
private PostChangeType type; private PostChangeType type;
private LocalDateTime time; private LocalDateTime time;
private String oldTitle; private String oldTitle;

View File

@@ -9,7 +9,10 @@ public class PostChangeLogMapper {
public PostChangeLogDto toDto(PostChangeLog log) { public PostChangeLogDto toDto(PostChangeLog log) {
PostChangeLogDto dto = new PostChangeLogDto(); PostChangeLogDto dto = new PostChangeLogDto();
dto.setId(log.getId()); dto.setId(log.getId());
dto.setUsername(log.getUser().getUsername()); if (log.getUser() != null) {
dto.setUsername(log.getUser().getUsername());
dto.setUserAvatar(log.getUser().getAvatar());
}
dto.setType(log.getType()); dto.setType(log.getType());
dto.setTime(log.getCreatedAt()); dto.setTime(log.getCreatedAt());
if (log instanceof PostTitleChangeLog t) { if (log instanceof PostTitleChangeLog t) {

View File

@@ -23,7 +23,7 @@ public abstract class PostChangeLog {
@JoinColumn(name = "post_id") @JoinColumn(name = "post_id")
private Post post; private Post post;
@ManyToOne(fetch = FetchType.LAZY, optional = false) @ManyToOne(fetch = FetchType.LAZY, optional = true)
@JoinColumn(name = "user_id") @JoinColumn(name = "user_id")
private User user; private User user;

View File

@@ -7,5 +7,7 @@ public enum PostChangeType {
TAG, TAG,
CLOSED, CLOSED,
PINNED, PINNED,
FEATURED FEATURED,
VOTE_RESULT,
LOTTERY_RESULT
} }

View File

@@ -0,0 +1,16 @@
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_lottery_result_change_logs")
public class PostLotteryResultChangeLog extends PostChangeLog {
}

View File

@@ -0,0 +1,16 @@
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_vote_result_change_logs")
public class PostVoteResultChangeLog extends PostChangeLog {
}

View File

@@ -86,6 +86,20 @@ public class PostChangeLogService {
logRepository.save(log); logRepository.save(log);
} }
public void recordVoteResult(Post post) {
PostVoteResultChangeLog log = new PostVoteResultChangeLog();
log.setPost(post);
log.setType(PostChangeType.VOTE_RESULT);
logRepository.save(log);
}
public void recordLotteryResult(Post post) {
PostLotteryResultChangeLog log = new PostLotteryResultChangeLog();
log.setPost(post);
log.setType(PostChangeType.LOTTERY_RESULT);
logRepository.save(log);
}
public List<PostChangeLog> listLogs(Long postId) { public List<PostChangeLog> listLogs(Long postId) {
Post post = postRepository.findById(postId) Post post = postRepository.findById(postId)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));

View File

@@ -1,28 +1,39 @@
<template> <template>
<div :id="`change-log-${log.id}`" class="change-log-container"> <div :id="`change-log-${log.id}`" class="change-log-container">
<div class="change-log-text"> <div class="change-log-text">
<span class="change-log-user">{{ log.username }}</span> <BaseImage
<span v-if="log.type === 'CONTENT'"> v-if="log.userAvatar"
变更了文章内容 class="change-log-avatar"
<div class="content-diff" v-html="diffHtml"></div> :src="log.userAvatar"
</span> alt="avatar"
<span v-else-if="log.type === 'TITLE'">变更了文章标题</span> @click="() => navigateTo(`/users/${log.username}`)"
<span v-else-if="log.type === 'CATEGORY'">变更了文章分类</span> />
<span v-else-if="log.type === 'TAG'">变更了文章标签</span> <span v-if="log.username" class="change-log-user">{{ log.username }}</span>
<span v-else-if="log.type === 'CLOSED'"> <span v-if="log.type === 'CONTENT'" class="change-log-content">变更了文章内容</span>
<span v-else-if="log.type === 'TITLE'" class="change-log-content">变更了文章标题</span>
<span v-else-if="log.type === 'CATEGORY'" class="change-log-content">变更了文章分类</span>
<span v-else-if="log.type === 'TAG'" class="change-log-content">变更了文章标签</span>
<span v-else-if="log.type === 'CLOSED'" class="change-log-content">
<template v-if="log.newClosed">关闭了文章</template> <template v-if="log.newClosed">关闭了文章</template>
<template v-else>重新打开了文章</template> <template v-else>重新打开了文章</template>
</span> </span>
<span v-else-if="log.type === 'PINNED'"> <span v-else-if="log.type === 'PINNED'" class="change-log-content">
<template v-if="log.newPinnedAt">置顶了文章</template> <template v-if="log.newPinnedAt">置顶了文章</template>
<template v-else>取消置顶文章</template> <template v-else>取消置顶文章</template>
</span> </span>
<span v-else-if="log.type === 'FEATURED'"> <span v-else-if="log.type === 'FEATURED'" class="change-log-content">
<template v-if="log.newFeatured">将文章设为精选</template> <template v-if="log.newFeatured">将文章设为精选</template>
<template v-else>取消精选文章</template> <template v-else>取消精选文章</template>
</span> </span>
<span v-else-if="log.type === 'VOTE_RESULT'" class="change-log-content">投票已出结果</span>
<span v-else-if="log.type === 'LOTTERY_RESULT'" class="change-log-content">抽奖已开奖</span>
</div> </div>
<div class="change-log-time">{{ log.time }}</div> <div class="change-log-time">{{ log.time }}</div>
<div
v-if="log.type === 'CONTENT' || log.type === 'TITLE'"
class="content-diff"
v-html="diffHtml"
></div>
</div> </div>
</template> </template>
@@ -30,14 +41,47 @@
import { computed } from 'vue' import { computed } from 'vue'
import { html } from 'diff2html' import { html } from 'diff2html'
import { createTwoFilesPatch } from 'diff' import { createTwoFilesPatch } from 'diff'
import { useIsMobile } from '~/utils/screen'
import 'diff2html/bundles/css/diff2html.min.css' import 'diff2html/bundles/css/diff2html.min.css'
const props = defineProps({ log: Object }) import BaseImage from '~/components/BaseImage.vue'
import { navigateTo } from 'nuxt/app'
import { themeState } from '~/utils/theme'
const props = defineProps({
log: Object,
title: String,
})
const diffHtml = computed(() => { const diffHtml = computed(() => {
const isMobile = useIsMobile()
// Track theme changes
const isDark = import.meta.client && document.documentElement.dataset.theme === 'dark'
themeState.mode
const colorScheme = isDark ? 'dark' : 'light'
if (props.log.type === 'CONTENT') { if (props.log.type === 'CONTENT') {
const oldContent = props.log.oldContent ?? '' const oldContent = props.log.oldContent ?? ''
const newContent = props.log.newContent ?? '' const newContent = props.log.newContent ?? ''
const diff = createTwoFilesPatch('old', 'new', oldContent, newContent) const diff = createTwoFilesPatch(props.title, props.title, oldContent, newContent)
return html(diff, { inputFormat: 'diff', showFiles: false, matching: 'lines' }) return html(diff, {
inputFormat: 'diff',
showFiles: false,
matching: 'lines',
drawFileList: false,
outputFormat: isMobile.value ? 'line-by-line' : 'side-by-side',
colorScheme,
})
} else if (props.log.type === 'TITLE') {
const oldTitle = props.log.oldTitle ?? ''
const newTitle = props.log.newTitle ?? ''
const diff = createTwoFilesPatch(oldTitle, newTitle, '', '')
return html(diff, {
inputFormat: 'diff',
showFiles: false,
matching: 'lines',
drawFileList: false,
outputFormat: isMobile.value ? 'line-by-line' : 'side-by-side',
colorScheme,
})
} }
return '' return ''
}) })
@@ -47,13 +91,31 @@ const diffHtml = computed(() => {
.change-log-container { .change-log-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-bottom: 30px; /* padding-top: 5px; */
opacity: 0.7; /* padding-bottom: 30px; */
font-size: 14px;
}
.change-log-text {
display: flex;
align-items: center;
} }
.change-log-user { .change-log-user {
font-weight: bold; font-weight: bold;
margin-right: 4px; margin-right: 4px;
} }
.change-log-user,
.change-log-content {
opacity: 0.7;
}
.change-log-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 4px;
cursor: pointer;
}
.change-log-time { .change-log-time {
font-size: 12px; font-size: 12px;
opacity: 0.6; opacity: 0.6;

View File

@@ -134,7 +134,7 @@
:post-closed="closed" :post-closed="closed"
@deleted="onCommentDeleted" @deleted="onCommentDeleted"
/> />
<PostChangeLogItem v-else :log="item" /> <PostChangeLogItem v-else :log="item" :title="title" />
</template> </template>
</BaseTimeline> </BaseTimeline>
</div> </div>
@@ -363,6 +363,10 @@ const changeLogIcon = (l) => {
} else { } else {
return 'dislike' return 'dislike'
} }
} else if (l.type === 'VOTE_RESULT') {
return 'check-one'
} else if (l.type === 'LOTTERY_RESULT') {
return 'gift'
} else { } else {
return 'info' return 'info'
} }
@@ -371,12 +375,21 @@ const changeLogIcon = (l) => {
const mapChangeLog = (l) => ({ const mapChangeLog = (l) => ({
id: l.id, id: l.id,
username: l.username, username: l.username,
userAvatar: l.userAvatar,
type: l.type, type: l.type,
createdAt: l.time, createdAt: l.time,
time: TimeManager.format(l.time), time: TimeManager.format(l.time),
newClosed: l.newClosed, newClosed: l.newClosed,
newPinnedAt: l.newPinnedAt, newPinnedAt: l.newPinnedAt,
newFeatured: l.newFeatured, newFeatured: l.newFeatured,
oldContent: l.oldContent,
newContent: l.newContent,
oldTitle: l.oldTitle,
newTitle: l.newTitle,
oldCategory: l.oldCategory,
newCategory: l.newCategory,
oldTags: l.oldTags,
newTags: l.newTags,
icon: changeLogIcon(l), icon: changeLogIcon(l),
}) })