Compare commits

...

21 Commits

Author SHA1 Message Date
Tim 17929dd95d test: add PostChangeLogService to PostService tests 2025-09-08 15:42:08 +08:00
Tim f478b55538 Merge pull request #924 from nagisa77/codex/add-article-metadata-change-logging
Track post metadata changes and display in timeline
2025-09-08 15:35:44 +08:00
Tim c58c14f9b7 feat: 设置system的icon+role 2025-09-08 15:35:09 +08:00
Tim 990d7cfbf9 fix: 投票结果UI 2025-09-08 15:32:57 +08:00
Tim eb860a74af Merge pull request #934 from nagisa77/codex/add-system-user-for-vote-and-lottery-results
Create system user for internal logging
2025-09-08 15:21:30 +08:00
Tim b3d050b42e Add system user and log attribution 2025-09-08 15:19:17 +08:00
Tim db678a95c6 Merge pull request #933 from nagisa77/codex/call-recordlotteryresult-and-recordvoteresult
feat: log poll and lottery results
2025-09-08 15:00:30 +08:00
Tim 6d66cb48dc feat: log poll and lottery results 2025-09-08 15:00:15 +08:00
Tim 1fe2994743 fix: 适配分类/tags ui 2025-09-08 14:56:44 +08:00
Tim 126b10ce45 Merge pull request #932 from nagisa77/codex/update-changelog-to-return-dto-format-rnzqgd
Expose category and tag changes as DTOs
2025-09-08 14:46:09 +08:00
Tim 3b1843b6dd Return category and tag change logs as DTOs 2025-09-08 14:45:47 +08:00
Tim 6a5d00f086 Revert "Return structured category and tag data in change logs"
This reverts commit fe167aa0b9.
2025-09-08 14:44:08 +08:00
Tim 06368a6cf1 Merge pull request #931 from nagisa77/codex/add-dark-mode-support-for-diff2html
feat: enable dark mode for diff2html
2025-09-08 14:29:01 +08:00
Tim c38e4bc44c feat: enable dark mode for diff2html 2025-09-08 14:28:42 +08:00
Tim e9f25d3b1a Merge pull request #930 from nagisa77/codex/update-changelog-to-return-dto-format
Return structured category and tag data in change logs
2025-09-08 14:27:36 +08:00
Tim fe167aa0b9 Return structured category and tag data in change logs 2025-09-08 14:27:18 +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
15 changed files with 292 additions and 32 deletions
@@ -0,0 +1,36 @@
package com.openisle.config;
import com.openisle.model.Role;
import com.openisle.model.User;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* Ensure a dedicated "system" user exists for internal operations.
*/
@Component
@RequiredArgsConstructor
public class SystemUserInitializer implements CommandLineRunner {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public void run(String... args) {
userRepository.findByUsername("system").orElseGet(() -> {
User system = new User();
system.setUsername("system");
system.setEmail("system@openisle.local");
// todo(tim): raw password 采用环境变量
system.setPassword(passwordEncoder.encode("system"));
system.setRole(Role.USER);
system.setVerified(true);
system.setApproved(true);
system.setAvatar("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png");
return userRepository.save(system);
});
}
}
@@ -5,22 +5,24 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
@Getter @Getter
@Setter @Setter
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;
private String newTitle; private String newTitle;
private String oldContent; private String oldContent;
private String newContent; private String newContent;
private String oldCategory; private CategoryDto oldCategory;
private String newCategory; private CategoryDto newCategory;
private String oldTags; private List<TagDto> oldTags;
private String newTags; private List<TagDto> newTags;
private Boolean oldClosed; private Boolean oldClosed;
private Boolean newClosed; private Boolean newClosed;
private LocalDateTime oldPinnedAt; private LocalDateTime oldPinnedAt;
@@ -1,15 +1,35 @@
package com.openisle.mapper; package com.openisle.mapper;
import com.openisle.dto.CategoryDto;
import com.openisle.dto.PostChangeLogDto; import com.openisle.dto.PostChangeLogDto;
import com.openisle.dto.TagDto;
import com.openisle.model.*; import com.openisle.model.*;
import com.openisle.repository.CategoryRepository;
import com.openisle.repository.TagRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Component @Component
@RequiredArgsConstructor
public class PostChangeLogMapper { public class PostChangeLogMapper {
private final CategoryRepository categoryRepository;
private final TagRepository tagRepository;
private final CategoryMapper categoryMapper;
private final TagMapper tagMapper;
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());
if (log.getUser() != null) {
dto.setUsername(log.getUser().getUsername()); 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) {
@@ -19,11 +39,11 @@ public class PostChangeLogMapper {
dto.setOldContent(c.getOldContent()); dto.setOldContent(c.getOldContent());
dto.setNewContent(c.getNewContent()); dto.setNewContent(c.getNewContent());
} else if (log instanceof PostCategoryChangeLog cat) { } else if (log instanceof PostCategoryChangeLog cat) {
dto.setOldCategory(cat.getOldCategory()); dto.setOldCategory(mapCategory(cat.getOldCategory()));
dto.setNewCategory(cat.getNewCategory()); dto.setNewCategory(mapCategory(cat.getNewCategory()));
} else if (log instanceof PostTagChangeLog tag) { } else if (log instanceof PostTagChangeLog tag) {
dto.setOldTags(tag.getOldTags()); dto.setOldTags(mapTags(tag.getOldTags()));
dto.setNewTags(tag.getNewTags()); dto.setNewTags(mapTags(tag.getNewTags()));
} else if (log instanceof PostClosedChangeLog cl) { } else if (log instanceof PostClosedChangeLog cl) {
dto.setOldClosed(cl.isOldClosed()); dto.setOldClosed(cl.isOldClosed());
dto.setNewClosed(cl.isNewClosed()); dto.setNewClosed(cl.isNewClosed());
@@ -36,4 +56,37 @@ public class PostChangeLogMapper {
} }
return dto; return dto;
} }
private CategoryDto mapCategory(String name) {
if (name == null) {
return null;
}
return categoryRepository.findByName(name)
.map(categoryMapper::toDto)
.orElseGet(() -> {
CategoryDto dto = new CategoryDto();
dto.setName(name);
return dto;
});
}
private List<TagDto> mapTags(String tags) {
if (tags == null || tags.isBlank()) {
return Collections.emptyList();
}
return Arrays.stream(tags.split(","))
.map(String::trim)
.map(this::mapTag)
.collect(Collectors.toList());
}
private TagDto mapTag(String name) {
return tagRepository.findByName(name)
.map(tagMapper::toDto)
.orElseGet(() -> {
TagDto dto = new TagDto();
dto.setName(name);
return dto;
});
}
} }
@@ -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;
@@ -7,5 +7,7 @@ public enum PostChangeType {
TAG, TAG,
CLOSED, CLOSED,
PINNED, PINNED,
FEATURED FEATURED,
VOTE_RESULT,
LOTTERY_RESULT
} }
@@ -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 {
}
@@ -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 {
}
@@ -4,7 +4,10 @@ import com.openisle.model.Category;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface CategoryRepository extends JpaRepository<Category, Long> { public interface CategoryRepository extends JpaRepository<Category, Long> {
List<Category> findByNameContainingIgnoreCase(String keyword); List<Category> findByNameContainingIgnoreCase(String keyword);
Optional<Category> findByName(String name);
} }
@@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface TagRepository extends JpaRepository<Tag, Long> { public interface TagRepository extends JpaRepository<Tag, Long> {
List<Tag> findByNameContainingIgnoreCase(String keyword); List<Tag> findByNameContainingIgnoreCase(String keyword);
@@ -15,4 +16,6 @@ public interface TagRepository extends JpaRepository<Tag, Long> {
List<Tag> findByCreatorOrderByCreatedAtDesc(User creator, Pageable pageable); List<Tag> findByCreatorOrderByCreatedAtDesc(User creator, Pageable pageable);
List<Tag> findByCreator(User creator); List<Tag> findByCreator(User creator);
Optional<Tag> findByName(String name);
} }
@@ -3,6 +3,7 @@ package com.openisle.service;
import com.openisle.model.*; import com.openisle.model.*;
import com.openisle.repository.PostChangeLogRepository; import com.openisle.repository.PostChangeLogRepository;
import com.openisle.repository.PostRepository; import com.openisle.repository.PostRepository;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -15,6 +16,12 @@ import java.util.stream.Collectors;
public class PostChangeLogService { public class PostChangeLogService {
private final PostChangeLogRepository logRepository; private final PostChangeLogRepository logRepository;
private final PostRepository postRepository; private final PostRepository postRepository;
private final UserRepository userRepository;
private User getSystemUser() {
return userRepository.findByUsername("system")
.orElseThrow(() -> new IllegalStateException("System user not found"));
}
public void recordContentChange(Post post, User user, String oldContent, String newContent) { public void recordContentChange(Post post, User user, String oldContent, String newContent) {
PostContentChangeLog log = new PostContentChangeLog(); PostContentChangeLog log = new PostContentChangeLog();
@@ -86,6 +93,22 @@ public class PostChangeLogService {
logRepository.save(log); logRepository.save(log);
} }
public void recordVoteResult(Post post) {
PostVoteResultChangeLog log = new PostVoteResultChangeLog();
log.setPost(post);
log.setUser(getSystemUser());
log.setType(PostChangeType.VOTE_RESULT);
logRepository.save(log);
}
public void recordLotteryResult(Post post) {
PostLotteryResultChangeLog log = new PostLotteryResultChangeLog();
log.setPost(post);
log.setUser(getSystemUser());
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"));
@@ -368,6 +368,7 @@ public class PostService {
for (User participant : pp.getParticipants()) { for (User participant : pp.getParticipants()) {
notificationService.createNotification(participant, NotificationType.POLL_RESULT_PARTICIPANT, pp, null, null, null, null, null); notificationService.createNotification(participant, NotificationType.POLL_RESULT_PARTICIPANT, pp, null, null, null, null, null);
} }
postChangeLogService.recordVoteResult(pp);
}); });
} }
@@ -402,6 +403,7 @@ public class PostService {
notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null); notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null);
notificationService.sendCustomPush(lp.getAuthor(), "抽奖已开奖", String.format("%s/posts/%d", websiteUrl, lp.getId())); notificationService.sendCustomPush(lp.getAuthor(), "抽奖已开奖", String.format("%s/posts/%d", websiteUrl, lp.getId()));
} }
postChangeLogService.recordLotteryResult(lp);
}); });
} }
@@ -37,11 +37,12 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT); imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post(); Post post = new Post();
@@ -86,11 +87,12 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT); imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post(); Post post = new Post();
@@ -141,11 +143,12 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT); imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L); when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
@@ -177,11 +180,12 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT); imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
User author = new User(); User author = new User();
+101 -16
View File
@@ -1,28 +1,53 @@
<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"
alt="avatar"
@click="() => navigateTo(`/users/${log.username}`)"
/>
<span v-if="log.username" class="change-log-user">{{ log.username }}</span>
<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 change-log-category">
<div class="change-log-category-text">变更了文章分类, </div>
<ArticleCategory :category="log.oldCategory" />
<div class="change-log-category-text">修改为</div>
<ArticleCategory :category="log.newCategory" />
</span> </span>
<span v-else-if="log.type === 'TITLE'">变更了文章标题</span> <span v-else-if="log.type === 'TAG'" class="change-log-content change-log-category">
<span v-else-if="log.type === 'CATEGORY'">变更了文章分类</span> <div class="change-log-category-text">变更了文章标签, </div>
<span v-else-if="log.type === 'TAG'">变更了文章标签</span> <ArticleTags :tags="log.oldTags" />
<span v-else-if="log.type === 'CLOSED'"> <div class="change-log-category-text">修改为</div>
<ArticleTags :tags="log.newTags" />
</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 +55,49 @@
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'
import ArticleCategory from '~/components/ArticleCategory.vue'
import ArticleTags from '~/components/ArticleTags.vue'
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 +107,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;
@@ -62,4 +140,11 @@ const diffHtml = computed(() => {
.content-diff { .content-diff {
margin-top: 8px; margin-top: 8px;
} }
.change-log-category {
display: flex;
flex-direction: row;
gap: 4px;
align-items: center;
}
</style> </style>
+14 -1
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),
}) })
+2
View File
@@ -76,6 +76,7 @@ import {
DoubleDown, DoubleDown,
Open, Open,
Dislike, Dislike,
CheckOne,
} from '@icon-park/vue-next' } from '@icon-park/vue-next'
export default defineNuxtPlugin((nuxtApp) => { export default defineNuxtPlugin((nuxtApp) => {
@@ -155,4 +156,5 @@ export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('DoubleDown', DoubleDown) nuxtApp.vueApp.component('DoubleDown', DoubleDown)
nuxtApp.vueApp.component('OpenIcon', Open) nuxtApp.vueApp.component('OpenIcon', Open)
nuxtApp.vueApp.component('Dislike', Dislike) nuxtApp.vueApp.component('Dislike', Dislike)
nuxtApp.vueApp.component('CheckOne', CheckOne)
}) })