mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-22 06:50:53 +08:00
Compare commits
21 Commits
codex/refa
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f152906f2 | ||
|
|
ef71d0b3d4 | ||
|
|
6f80d139ba | ||
|
|
7454931fa5 | ||
|
|
0852664a82 | ||
|
|
5814fb673a | ||
|
|
4ee4266e3d | ||
|
|
6a27fbe1d7 | ||
|
|
38ff04c358 | ||
|
|
fc27200ac1 | ||
|
|
b1998be425 | ||
|
|
72adc5b232 | ||
|
|
d24e67de5d | ||
|
|
eefefac236 | ||
|
|
2f339fdbdb | ||
|
|
3808becc8b | ||
|
|
18db4d7317 | ||
|
|
52cbb71945 | ||
|
|
39c34a9048 | ||
|
|
4baabf2224 | ||
|
|
8023183bc6 |
@@ -44,7 +44,7 @@ public class PostController {
|
|||||||
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
|
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
|
||||||
req.getPrizeCount(), req.getPointCost(),
|
req.getPrizeCount(), req.getPointCost(),
|
||||||
req.getStartTime(), req.getEndTime(),
|
req.getStartTime(), req.getEndTime(),
|
||||||
req.getOptions());
|
req.getOptions(), req.getMultiple());
|
||||||
draftService.deleteDraft(auth.getName());
|
draftService.deleteDraft(auth.getName());
|
||||||
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
||||||
dto.setReward(levelService.awardForPost(auth.getName()));
|
dto.setReward(levelService.awardForPost(auth.getName()));
|
||||||
@@ -94,7 +94,7 @@ public class PostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/poll/vote")
|
@PostMapping("/{id}/poll/vote")
|
||||||
public ResponseEntity<Void> vote(@PathVariable Long id, @RequestParam("option") int option, Authentication auth) {
|
public ResponseEntity<Void> vote(@PathVariable Long id, @RequestParam("option") List<Integer> option, Authentication auth) {
|
||||||
postService.votePoll(id, auth.getName(), option);
|
postService.votePoll(id, auth.getName(), option);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ public class PollDto {
|
|||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
private List<AuthorDto> participants;
|
private List<AuthorDto> participants;
|
||||||
private Map<Integer, List<AuthorDto>> optionParticipants;
|
private Map<Integer, List<AuthorDto>> optionParticipants;
|
||||||
|
private boolean multiple;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,6 @@ public class PostRequest {
|
|||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
// fields for poll posts
|
// fields for poll posts
|
||||||
private List<String> options;
|
private List<String> options;
|
||||||
|
private Boolean multiple;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ public class PostMapper {
|
|||||||
.collect(Collectors.groupingBy(PollVote::getOptionIndex,
|
.collect(Collectors.groupingBy(PollVote::getOptionIndex,
|
||||||
Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList())));
|
Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList())));
|
||||||
p.setOptionParticipants(optionParticipants);
|
p.setOptionParticipants(optionParticipants);
|
||||||
|
p.setMultiple(Boolean.TRUE.equals(pp.getMultiple()));
|
||||||
dto.setPoll(p);
|
dto.setPoll(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.SQLDelete;
|
||||||
|
import org.hibernate.annotations.Where;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ import java.time.LocalDateTime;
|
|||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@Table(name = "comments")
|
@Table(name = "comments")
|
||||||
|
@SQLDelete(sql = "UPDATE comments SET deleted_at = CURRENT_TIMESTAMP(6) WHERE id = ?")
|
||||||
|
@Where(clause = "deleted_at IS NULL")
|
||||||
public class Comment {
|
public class Comment {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@@ -41,4 +45,7 @@ public class Comment {
|
|||||||
@Column
|
@Column
|
||||||
private LocalDateTime pinnedAt;
|
private LocalDateTime pinnedAt;
|
||||||
|
|
||||||
|
@Column(name = "deleted_at")
|
||||||
|
private LocalDateTime deletedAt;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import jakarta.persistence.*;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import org.hibernate.annotations.SQLDelete;
|
||||||
|
import org.hibernate.annotations.Where;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ import java.time.LocalDateTime;
|
|||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@Table(name = "point_histories")
|
@Table(name = "point_histories")
|
||||||
|
@SQLDelete(sql = "UPDATE point_histories SET deleted_at = CURRENT_TIMESTAMP(6) WHERE id = ?")
|
||||||
|
@Where(clause = "deleted_at IS NULL")
|
||||||
public class PointHistory {
|
public class PointHistory {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@@ -46,4 +50,7 @@ public class PointHistory {
|
|||||||
|
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "deleted_at")
|
||||||
|
private LocalDateTime deletedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ public class PollPost extends Post {
|
|||||||
inverseJoinColumns = @JoinColumn(name = "user_id"))
|
inverseJoinColumns = @JoinColumn(name = "user_id"))
|
||||||
private Set<User> participants = new HashSet<>();
|
private Set<User> participants = new HashSet<>();
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private Boolean multiple = false;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "poll_votes", uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "user_id"}))
|
@Table(name = "poll_votes", uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "user_id", "option_index"}))
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.openisle.repository;
|
|||||||
|
|
||||||
import com.openisle.model.PointHistory;
|
import com.openisle.model.PointHistory;
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
|
import com.openisle.model.Comment;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -12,4 +13,6 @@ public interface PointHistoryRepository extends JpaRepository<PointHistory, Long
|
|||||||
long countByUser(User user);
|
long countByUser(User user);
|
||||||
|
|
||||||
List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt);
|
List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt);
|
||||||
|
|
||||||
|
List<PointHistory> findByComment(Comment comment);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.openisle.repository.UserRepository;
|
|||||||
import com.openisle.repository.ReactionRepository;
|
import com.openisle.repository.ReactionRepository;
|
||||||
import com.openisle.repository.CommentSubscriptionRepository;
|
import com.openisle.repository.CommentSubscriptionRepository;
|
||||||
import com.openisle.repository.NotificationRepository;
|
import com.openisle.repository.NotificationRepository;
|
||||||
|
import com.openisle.repository.PointHistoryRepository;
|
||||||
import com.openisle.service.NotificationService;
|
import com.openisle.service.NotificationService;
|
||||||
import com.openisle.service.SubscriptionService;
|
import com.openisle.service.SubscriptionService;
|
||||||
import com.openisle.model.Role;
|
import com.openisle.model.Role;
|
||||||
@@ -37,6 +38,7 @@ public class CommentService {
|
|||||||
private final ReactionRepository reactionRepository;
|
private final ReactionRepository reactionRepository;
|
||||||
private final CommentSubscriptionRepository commentSubscriptionRepository;
|
private final CommentSubscriptionRepository commentSubscriptionRepository;
|
||||||
private final NotificationRepository notificationRepository;
|
private final NotificationRepository notificationRepository;
|
||||||
|
private final PointHistoryRepository pointHistoryRepository;
|
||||||
private final ImageUploader imageUploader;
|
private final ImageUploader imageUploader;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -235,10 +237,14 @@ public class CommentService {
|
|||||||
for (Comment c : replies) {
|
for (Comment c : replies) {
|
||||||
deleteCommentCascade(c);
|
deleteCommentCascade(c);
|
||||||
}
|
}
|
||||||
|
// 逻辑删除相关的积分历史记录
|
||||||
|
pointHistoryRepository.findByComment(comment).forEach(pointHistoryRepository::delete);
|
||||||
|
// 删除其他相关数据
|
||||||
reactionRepository.findByComment(comment).forEach(reactionRepository::delete);
|
reactionRepository.findByComment(comment).forEach(reactionRepository::delete);
|
||||||
commentSubscriptionRepository.findByComment(comment).forEach(commentSubscriptionRepository::delete);
|
commentSubscriptionRepository.findByComment(comment).forEach(commentSubscriptionRepository::delete);
|
||||||
notificationRepository.deleteAll(notificationRepository.findByComment(comment));
|
notificationRepository.deleteAll(notificationRepository.findByComment(comment));
|
||||||
imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent()));
|
imageUploader.removeReferences(imageUploader.extractUrls(comment.getContent()));
|
||||||
|
// 逻辑删除评论
|
||||||
commentRepository.delete(comment);
|
commentRepository.delete(comment);
|
||||||
log.debug("deleteCommentCascade removed comment {}", comment.getId());
|
log.debug("deleteCommentCascade removed comment {}", comment.getId());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,8 @@ public class PostService {
|
|||||||
Integer pointCost,
|
Integer pointCost,
|
||||||
LocalDateTime startTime,
|
LocalDateTime startTime,
|
||||||
LocalDateTime endTime,
|
LocalDateTime endTime,
|
||||||
java.util.List<String> options) {
|
java.util.List<String> options,
|
||||||
|
Boolean multiple) {
|
||||||
long recent = postRepository.countByAuthorAfter(username,
|
long recent = postRepository.countByAuthorAfter(username,
|
||||||
java.time.LocalDateTime.now().minusMinutes(5));
|
java.time.LocalDateTime.now().minusMinutes(5));
|
||||||
if (recent >= 1) {
|
if (recent >= 1) {
|
||||||
@@ -227,6 +228,7 @@ public class PostService {
|
|||||||
PollPost pp = new PollPost();
|
PollPost pp = new PollPost();
|
||||||
pp.setOptions(options);
|
pp.setOptions(options);
|
||||||
pp.setEndTime(endTime);
|
pp.setEndTime(endTime);
|
||||||
|
pp.setMultiple(multiple != null && multiple);
|
||||||
post = pp;
|
post = pp;
|
||||||
} else {
|
} else {
|
||||||
post = new Post();
|
post = new Post();
|
||||||
@@ -302,7 +304,7 @@ public class PostService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public PollPost votePoll(Long postId, String username, int optionIndex) {
|
public PollPost votePoll(Long postId, String username, java.util.List<Integer> optionIndices) {
|
||||||
PollPost post = pollPostRepository.findById(postId)
|
PollPost post = pollPostRepository.findById(postId)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||||
if (post.getEndTime() != null && post.getEndTime().isBefore(LocalDateTime.now())) {
|
if (post.getEndTime() != null && post.getEndTime().isBefore(LocalDateTime.now())) {
|
||||||
@@ -313,16 +315,24 @@ public class PostService {
|
|||||||
if (post.getParticipants().contains(user)) {
|
if (post.getParticipants().contains(user)) {
|
||||||
throw new IllegalArgumentException("User already voted");
|
throw new IllegalArgumentException("User already voted");
|
||||||
}
|
}
|
||||||
if (optionIndex < 0 || optionIndex >= post.getOptions().size()) {
|
if (optionIndices == null || optionIndices.isEmpty()) {
|
||||||
throw new IllegalArgumentException("Invalid option");
|
throw new IllegalArgumentException("No options selected");
|
||||||
|
}
|
||||||
|
java.util.Set<Integer> unique = new java.util.HashSet<>(optionIndices);
|
||||||
|
for (int optionIndex : unique) {
|
||||||
|
if (optionIndex < 0 || optionIndex >= post.getOptions().size()) {
|
||||||
|
throw new IllegalArgumentException("Invalid option");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
post.getParticipants().add(user);
|
post.getParticipants().add(user);
|
||||||
post.getVotes().merge(optionIndex, 1, Integer::sum);
|
for (int optionIndex : unique) {
|
||||||
PollVote vote = new PollVote();
|
post.getVotes().merge(optionIndex, 1, Integer::sum);
|
||||||
vote.setPost(post);
|
PollVote vote = new PollVote();
|
||||||
vote.setUser(user);
|
vote.setPost(post);
|
||||||
vote.setOptionIndex(optionIndex);
|
vote.setUser(user);
|
||||||
pollVoteRepository.save(vote);
|
vote.setOptionIndex(optionIndex);
|
||||||
|
pollVoteRepository.save(vote);
|
||||||
|
}
|
||||||
PollPost saved = pollPostRepository.save(post);
|
PollPost saved = pollPostRepository.save(post);
|
||||||
if (post.getAuthor() != null && !post.getAuthor().getId().equals(user.getId())) {
|
if (post.getAuthor() != null && !post.getAuthor().getId().equals(user.getId())) {
|
||||||
notificationService.createNotification(post.getAuthor(), NotificationType.POLL_VOTE, post, null, null, user, null, null);
|
notificationService.createNotification(post.getAuthor(), NotificationType.POLL_VOTE, post, null, null, user, null, null);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Add logical delete support for comments and point_histories tables
|
||||||
|
|
||||||
|
-- Add deleted_at column to comments table
|
||||||
|
ALTER TABLE comments ADD COLUMN deleted_at DATETIME(6) NULL;
|
||||||
|
|
||||||
|
-- Add deleted_at column to point_histories table
|
||||||
|
ALTER TABLE point_histories ADD COLUMN deleted_at DATETIME(6) NULL;
|
||||||
|
|
||||||
|
-- Add index for better performance on logical delete queries
|
||||||
|
CREATE INDEX idx_comments_deleted_at ON comments(deleted_at);
|
||||||
|
CREATE INDEX idx_point_histories_deleted_at ON point_histories(deleted_at);
|
||||||
@@ -76,7 +76,7 @@ class PostControllerTest {
|
|||||||
post.setTags(Set.of(tag));
|
post.setTags(Set.of(tag));
|
||||||
|
|
||||||
when(postService.createPost(eq("alice"), eq(1L), eq("t"), eq("c"), eq(List.of(1L)),
|
when(postService.createPost(eq("alice"), eq(1L), eq("t"), eq("c"), eq(List.of(1L)),
|
||||||
isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull())).thenReturn(post);
|
isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull())).thenReturn(post);
|
||||||
when(postService.viewPost(eq(1L), any())).thenReturn(post);
|
when(postService.viewPost(eq(1L), any())).thenReturn(post);
|
||||||
when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of());
|
when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of());
|
||||||
when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of());
|
when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of());
|
||||||
@@ -187,7 +187,7 @@ class PostControllerTest {
|
|||||||
.andExpect(status().isBadRequest());
|
.andExpect(status().isBadRequest());
|
||||||
|
|
||||||
verify(postService, never()).createPost(any(), any(), any(), any(), any(),
|
verify(postService, never()).createPost(any(), any(), any(), any(), any(),
|
||||||
any(), any(), any(), any(), any(), any(), any(), any());
|
any(), any(), any(), any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.openisle.repository.UserRepository;
|
|||||||
import com.openisle.repository.ReactionRepository;
|
import com.openisle.repository.ReactionRepository;
|
||||||
import com.openisle.repository.CommentSubscriptionRepository;
|
import com.openisle.repository.CommentSubscriptionRepository;
|
||||||
import com.openisle.repository.NotificationRepository;
|
import com.openisle.repository.NotificationRepository;
|
||||||
|
import com.openisle.repository.PointHistoryRepository;
|
||||||
import com.openisle.exception.RateLimitException;
|
import com.openisle.exception.RateLimitException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -24,10 +25,11 @@ class CommentServiceTest {
|
|||||||
ReactionRepository reactionRepo = mock(ReactionRepository.class);
|
ReactionRepository reactionRepo = mock(ReactionRepository.class);
|
||||||
CommentSubscriptionRepository subRepo = mock(CommentSubscriptionRepository.class);
|
CommentSubscriptionRepository subRepo = mock(CommentSubscriptionRepository.class);
|
||||||
NotificationRepository nRepo = mock(NotificationRepository.class);
|
NotificationRepository nRepo = mock(NotificationRepository.class);
|
||||||
|
PointHistoryRepository pointHistoryRepo = mock(PointHistoryRepository.class);
|
||||||
ImageUploader imageUploader = mock(ImageUploader.class);
|
ImageUploader imageUploader = mock(ImageUploader.class);
|
||||||
|
|
||||||
CommentService service = new CommentService(commentRepo, postRepo, userRepo,
|
CommentService service = new CommentService(commentRepo, postRepo, userRepo,
|
||||||
notifService, subService, reactionRepo, subRepo, nRepo, imageUploader);
|
notifService, subService, reactionRepo, subRepo, nRepo, pointHistoryRepo, imageUploader);
|
||||||
|
|
||||||
when(commentRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(3L);
|
when(commentRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(3L);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ class PostServiceTest {
|
|||||||
CategoryRepository catRepo = mock(CategoryRepository.class);
|
CategoryRepository catRepo = mock(CategoryRepository.class);
|
||||||
TagRepository tagRepo = mock(TagRepository.class);
|
TagRepository tagRepo = mock(TagRepository.class);
|
||||||
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
||||||
|
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
|
||||||
|
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
|
||||||
NotificationService notifService = mock(NotificationService.class);
|
NotificationService notifService = mock(NotificationService.class);
|
||||||
SubscriptionService subService = mock(SubscriptionService.class);
|
SubscriptionService subService = mock(SubscriptionService.class);
|
||||||
CommentService commentService = mock(CommentService.class);
|
CommentService commentService = mock(CommentService.class);
|
||||||
@@ -37,7 +39,7 @@ class PostServiceTest {
|
|||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
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, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
@@ -69,6 +71,8 @@ class PostServiceTest {
|
|||||||
CategoryRepository catRepo = mock(CategoryRepository.class);
|
CategoryRepository catRepo = mock(CategoryRepository.class);
|
||||||
TagRepository tagRepo = mock(TagRepository.class);
|
TagRepository tagRepo = mock(TagRepository.class);
|
||||||
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
||||||
|
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
|
||||||
|
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
|
||||||
NotificationService notifService = mock(NotificationService.class);
|
NotificationService notifService = mock(NotificationService.class);
|
||||||
SubscriptionService subService = mock(SubscriptionService.class);
|
SubscriptionService subService = mock(SubscriptionService.class);
|
||||||
CommentService commentService = mock(CommentService.class);
|
CommentService commentService = mock(CommentService.class);
|
||||||
@@ -84,7 +88,7 @@ class PostServiceTest {
|
|||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
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, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
@@ -122,6 +126,8 @@ class PostServiceTest {
|
|||||||
CategoryRepository catRepo = mock(CategoryRepository.class);
|
CategoryRepository catRepo = mock(CategoryRepository.class);
|
||||||
TagRepository tagRepo = mock(TagRepository.class);
|
TagRepository tagRepo = mock(TagRepository.class);
|
||||||
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
||||||
|
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
|
||||||
|
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
|
||||||
NotificationService notifService = mock(NotificationService.class);
|
NotificationService notifService = mock(NotificationService.class);
|
||||||
SubscriptionService subService = mock(SubscriptionService.class);
|
SubscriptionService subService = mock(SubscriptionService.class);
|
||||||
CommentService commentService = mock(CommentService.class);
|
CommentService commentService = mock(CommentService.class);
|
||||||
@@ -137,7 +143,7 @@ class PostServiceTest {
|
|||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
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, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
@@ -146,7 +152,7 @@ class PostServiceTest {
|
|||||||
|
|
||||||
assertThrows(RateLimitException.class,
|
assertThrows(RateLimitException.class,
|
||||||
() -> service.createPost("alice", 1L, "t", "c", List.of(1L),
|
() -> service.createPost("alice", 1L, "t", "c", List.of(1L),
|
||||||
null, null, null, null, null, null, null, null));
|
null, null, null, null, null, null, null, null, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -156,6 +162,8 @@ class PostServiceTest {
|
|||||||
CategoryRepository catRepo = mock(CategoryRepository.class);
|
CategoryRepository catRepo = mock(CategoryRepository.class);
|
||||||
TagRepository tagRepo = mock(TagRepository.class);
|
TagRepository tagRepo = mock(TagRepository.class);
|
||||||
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
|
||||||
|
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
|
||||||
|
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
|
||||||
NotificationService notifService = mock(NotificationService.class);
|
NotificationService notifService = mock(NotificationService.class);
|
||||||
SubscriptionService subService = mock(SubscriptionService.class);
|
SubscriptionService subService = mock(SubscriptionService.class);
|
||||||
CommentService commentService = mock(CommentService.class);
|
CommentService commentService = mock(CommentService.class);
|
||||||
@@ -171,7 +179,7 @@ class PostServiceTest {
|
|||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
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, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ body {
|
|||||||
|
|
||||||
.vditor-toolbar--pin {
|
.vditor-toolbar--pin {
|
||||||
top: calc(var(--header-height) + 1px) !important;
|
top: calc(var(--header-height) + 1px) !important;
|
||||||
z-index: 2000;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vditor-panel {
|
.vditor-panel {
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
<flat-pickr v-model="data.endTime" :config="dateConfig" class="time-picker" />
|
<flat-pickr v-model="data.endTime" :config="dateConfig" class="time-picker" />
|
||||||
</client-only>
|
</client-only>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="poll-multiple-row">
|
||||||
|
<span class="poll-row-title">多选</span>
|
||||||
|
<BaseSwitch v-model="data.multiple" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -25,6 +29,7 @@
|
|||||||
import 'flatpickr/dist/flatpickr.css'
|
import 'flatpickr/dist/flatpickr.css'
|
||||||
import FlatPickr from 'vue-flatpickr-component'
|
import FlatPickr from 'vue-flatpickr-component'
|
||||||
import BaseInput from '~/components/BaseInput.vue'
|
import BaseInput from '~/components/BaseInput.vue'
|
||||||
|
import BaseSwitch from '~/components/BaseSwitch.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@@ -80,6 +85,11 @@ const removeOption = (idx) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.poll-multiple-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
.time-picker {
|
.time-picker {
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="post-prize-container">
|
<div class="post-prize-container" v-if="lottery">
|
||||||
<div class="prize-content">
|
<div class="prize-content">
|
||||||
<div class="prize-info">
|
<div class="prize-info">
|
||||||
<div class="prize-info-left">
|
<div class="prize-info-left">
|
||||||
@@ -79,30 +79,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { toast } from '~/main'
|
|
||||||
import { getToken, authState } from '~/utils/auth'
|
import { getToken, authState } from '~/utils/auth'
|
||||||
|
import { toast } from '~/main'
|
||||||
|
import { useRuntimeConfig } from '#imports'
|
||||||
import { useIsMobile } from '~/utils/screen'
|
import { useIsMobile } from '~/utils/screen'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
lottery: {
|
lottery: { type: Object, required: true },
|
||||||
type: Object,
|
postId: { type: [String, Number], required: true },
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
postId: {
|
|
||||||
type: [String, Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['refresh'])
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
|
||||||
|
|
||||||
const loggedIn = computed(() => authState.loggedIn)
|
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
const loggedIn = computed(() => authState.loggedIn)
|
||||||
const lotteryParticipants = computed(() => props.lottery?.participants || [])
|
const lotteryParticipants = computed(() => props.lottery?.participants || [])
|
||||||
const lotteryWinners = computed(() => props.lottery?.winners || [])
|
const lotteryWinners = computed(() => props.lottery?.winners || [])
|
||||||
const lotteryEnded = computed(() => {
|
const lotteryEnded = computed(() => {
|
||||||
@@ -115,8 +105,7 @@ const hasJoined = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const countdown = ref('00:00:00')
|
const countdown = ref('00:00:00')
|
||||||
let countdownTimer = null
|
let timer = null
|
||||||
|
|
||||||
const updateCountdown = () => {
|
const updateCountdown = () => {
|
||||||
if (!props.lottery || !props.lottery.endTime) {
|
if (!props.lottery || !props.lottery.endTime) {
|
||||||
countdown.value = '00:00:00'
|
countdown.value = '00:00:00'
|
||||||
@@ -125,9 +114,9 @@ const updateCountdown = () => {
|
|||||||
const diff = new Date(props.lottery.endTime).getTime() - Date.now()
|
const diff = new Date(props.lottery.endTime).getTime() - Date.now()
|
||||||
if (diff <= 0) {
|
if (diff <= 0) {
|
||||||
countdown.value = '00:00:00'
|
countdown.value = '00:00:00'
|
||||||
if (countdownTimer) {
|
if (timer) {
|
||||||
clearInterval(countdownTimer)
|
clearInterval(timer)
|
||||||
countdownTimer = null
|
timer = null
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -136,30 +125,28 @@ const updateCountdown = () => {
|
|||||||
const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0')
|
const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0')
|
||||||
countdown.value = `${h}:${m}:${s}`
|
countdown.value = `${h}:${m}:${s}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const startCountdown = () => {
|
const startCountdown = () => {
|
||||||
if (!import.meta.client) return
|
|
||||||
if (countdownTimer) clearInterval(countdownTimer)
|
|
||||||
updateCountdown()
|
updateCountdown()
|
||||||
countdownTimer = setInterval(updateCountdown, 1000)
|
if (timer) clearInterval(timer)
|
||||||
|
timer = setInterval(updateCountdown, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.lottery?.endTime,
|
() => props.lottery?.endTime,
|
||||||
() => {
|
() => {
|
||||||
if (props.lottery && props.lottery.endTime) {
|
if (props.lottery && props.lottery.endTime) startCountdown()
|
||||||
startCountdown()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
|
||||||
)
|
)
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.lottery && props.lottery.endTime) startCountdown()
|
||||||
|
})
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (countdownTimer) clearInterval(countdownTimer)
|
if (timer) clearInterval(timer)
|
||||||
})
|
})
|
||||||
|
|
||||||
const gotoUser = (id) => navigateTo(`/users/${id}`, { replace: true })
|
const gotoUser = (id) => navigateTo(`/users/${id}`, { replace: true })
|
||||||
|
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
const joinLottery = async () => {
|
const joinLottery = async () => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -313,4 +300,11 @@ const joinLottery = async () => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.join-prize-button,
|
||||||
|
.join-prize-button-disabled {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="post-poll-container">
|
<div class="post-poll-container" v-if="poll">
|
||||||
<div class="poll-top-container">
|
<div class="poll-top-container">
|
||||||
<div class="poll-options-container">
|
<div class="poll-options-container">
|
||||||
<div v-if="showPollResult || pollEnded || hasVoted">
|
<div v-if="showPollResult || pollEnded || hasVoted">
|
||||||
@@ -29,15 +29,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div class="poll-title-section">
|
||||||
v-for="(opt, idx) in poll.options"
|
<div class="poll-option-title" v-if="poll.multiple">多选</div>
|
||||||
:key="idx"
|
<div class="poll-option-title" v-else>单选</div>
|
||||||
class="poll-option"
|
|
||||||
@click="voteOption(idx)"
|
<div class="poll-left-time">
|
||||||
>
|
<div class="poll-left-time-title">离结束还有</div>
|
||||||
<input type="radio" :checked="false" name="poll-option" class="poll-option-input" />
|
<div class="poll-left-time-value">{{ countdown }}</div>
|
||||||
<span class="poll-option-text">{{ opt }}</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="poll.multiple">
|
||||||
|
<div
|
||||||
|
v-for="(opt, idx) in poll.options"
|
||||||
|
:key="idx"
|
||||||
|
class="poll-option"
|
||||||
|
@click="toggleOption(idx)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="selectedOptions.includes(idx)"
|
||||||
|
class="poll-option-input"
|
||||||
|
/>
|
||||||
|
<span class="poll-option-text">{{ opt }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="multi-selection-container">
|
||||||
|
<div class="join-poll-button" @click="submitMultiPoll">
|
||||||
|
<i class="fas fa-check"></i> 确认投票
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="(opt, idx) in poll.options"
|
||||||
|
:key="idx"
|
||||||
|
class="poll-option"
|
||||||
|
@click="selectOption(idx)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:checked="selectedOption === idx"
|
||||||
|
name="poll-option"
|
||||||
|
class="poll-option-input"
|
||||||
|
/>
|
||||||
|
<span class="poll-option-text">{{ opt }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="single-selection-container">
|
||||||
|
<div class="join-poll-button" @click="submitSinglePoll">
|
||||||
|
<i class="fas fa-check"></i> 确认投票
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="poll-info">
|
<div class="poll-info">
|
||||||
@@ -60,36 +103,29 @@
|
|||||||
>
|
>
|
||||||
<i class="fas fa-chart-bar"></i> 结果
|
<i class="fas fa-chart-bar"></i> 结果
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="pollEnded" class="poll-option-hint">
|
||||||
<div class="poll-left-time">
|
<i class="fas fa-stopwatch"></i> 投票已结束
|
||||||
<div class="poll-left-time-title">离结束还有</div>
|
</div>
|
||||||
<div class="poll-left-time-value">{{ countdown }}</div>
|
<div v-else class="poll-option-hint">
|
||||||
|
<i class="fas fa-stopwatch"></i> 您已投票,等待结束查看结果
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { toast } from '~/main'
|
|
||||||
import { getToken, authState } from '~/utils/auth'
|
import { getToken, authState } from '~/utils/auth'
|
||||||
|
import { toast } from '~/main'
|
||||||
|
import { useRuntimeConfig } from '#imports'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
poll: {
|
poll: { type: Object, required: true },
|
||||||
type: Object,
|
postId: { type: [String, Number], required: true },
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
postId: {
|
|
||||||
type: [String, Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['refresh'])
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const loggedIn = computed(() => authState.loggedIn)
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
|
||||||
|
|
||||||
const showPollResult = ref(false)
|
const showPollResult = ref(false)
|
||||||
|
|
||||||
const pollParticipants = computed(() => props.poll?.participants || [])
|
const pollParticipants = computed(() => props.poll?.participants || [])
|
||||||
@@ -109,17 +145,15 @@ const pollEnded = computed(() => {
|
|||||||
return new Date(props.poll.endTime).getTime() <= Date.now()
|
return new Date(props.poll.endTime).getTime() <= Date.now()
|
||||||
})
|
})
|
||||||
const hasVoted = computed(() => {
|
const hasVoted = computed(() => {
|
||||||
if (!authState.loggedIn) return false
|
if (!loggedIn.value) return false
|
||||||
return pollParticipants.value.some((p) => p.id === Number(authState.userId))
|
return pollParticipants.value.some((p) => p.id === Number(authState.userId))
|
||||||
})
|
})
|
||||||
|
|
||||||
watch([hasVoted, pollEnded], ([voted, ended]) => {
|
watch([hasVoted, pollEnded], ([voted, ended]) => {
|
||||||
if (voted || ended) showPollResult.value = true
|
if (voted || ended) showPollResult.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
const countdown = ref('00:00:00')
|
const countdown = ref('00:00:00')
|
||||||
let countdownTimer = null
|
let timer = null
|
||||||
|
|
||||||
const updateCountdown = () => {
|
const updateCountdown = () => {
|
||||||
if (!props.poll || !props.poll.endTime) {
|
if (!props.poll || !props.poll.endTime) {
|
||||||
countdown.value = '00:00:00'
|
countdown.value = '00:00:00'
|
||||||
@@ -128,9 +162,9 @@ const updateCountdown = () => {
|
|||||||
const diff = new Date(props.poll.endTime).getTime() - Date.now()
|
const diff = new Date(props.poll.endTime).getTime() - Date.now()
|
||||||
if (diff <= 0) {
|
if (diff <= 0) {
|
||||||
countdown.value = '00:00:00'
|
countdown.value = '00:00:00'
|
||||||
if (countdownTimer) {
|
if (timer) {
|
||||||
clearInterval(countdownTimer)
|
clearInterval(timer)
|
||||||
countdownTimer = null
|
timer = null
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -139,30 +173,28 @@ const updateCountdown = () => {
|
|||||||
const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0')
|
const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0')
|
||||||
countdown.value = `${h}:${m}:${s}`
|
countdown.value = `${h}:${m}:${s}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const startCountdown = () => {
|
const startCountdown = () => {
|
||||||
if (!import.meta.client) return
|
|
||||||
if (countdownTimer) clearInterval(countdownTimer)
|
|
||||||
updateCountdown()
|
updateCountdown()
|
||||||
countdownTimer = setInterval(updateCountdown, 1000)
|
if (timer) clearInterval(timer)
|
||||||
|
timer = setInterval(updateCountdown, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.poll?.endTime,
|
() => props.poll?.endTime,
|
||||||
() => {
|
() => {
|
||||||
if (props.poll && props.poll.endTime) {
|
if (props.poll && props.poll.endTime) startCountdown()
|
||||||
startCountdown()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
|
||||||
)
|
)
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.poll && props.poll.endTime) startCountdown()
|
||||||
|
})
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (countdownTimer) clearInterval(countdownTimer)
|
if (timer) clearInterval(timer)
|
||||||
})
|
})
|
||||||
|
|
||||||
const gotoUser = (id) => navigateTo(`/users/${id}`, { replace: true })
|
const gotoUser = (id) => navigateTo(`/users/${id}`, { replace: true })
|
||||||
|
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
const voteOption = async (idx) => {
|
const voteOption = async (idx) => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -182,9 +214,65 @@ const voteOption = async (idx) => {
|
|||||||
toast.error(data.error || '操作失败')
|
toast.error(data.error || '操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedOption = ref(null)
|
||||||
|
const selectOption = (idx) => {
|
||||||
|
selectedOption.value = idx
|
||||||
|
}
|
||||||
|
const submitSinglePoll = async () => {
|
||||||
|
if (selectedOption.value === null) {
|
||||||
|
toast.error('请选择一个选项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await voteOption(selectedOption.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedOptions = ref([])
|
||||||
|
const toggleOption = (idx) => {
|
||||||
|
const i = selectedOptions.value.indexOf(idx)
|
||||||
|
if (i >= 0) {
|
||||||
|
selectedOptions.value.splice(i, 1)
|
||||||
|
} else {
|
||||||
|
selectedOptions.value.push(idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const submitMultiPoll = async () => {
|
||||||
|
const token = getToken()
|
||||||
|
if (!token) {
|
||||||
|
toast.error('请先登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!selectedOptions.value.length) {
|
||||||
|
toast.error('请选择至少一个选项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const params = selectedOptions.value.map((o) => `option=${o}`).join('&')
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/posts/${props.postId}/poll/vote?${params}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success('投票成功')
|
||||||
|
emit('refresh')
|
||||||
|
showPollResult.value = true
|
||||||
|
} else {
|
||||||
|
toast.error(data.error || '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.post-poll-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
background-color: var(--lottery-background-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.poll-option-button {
|
.poll-option-button {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
@@ -206,6 +294,7 @@ const voteOption = async (idx) => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 4;
|
flex: 4;
|
||||||
|
border-right: 1px solid var(--normal-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-info {
|
.poll-info {
|
||||||
@@ -267,12 +356,14 @@ const voteOption = async (idx) => {
|
|||||||
.poll-left-time {
|
.poll-left-time {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-left-time-title {
|
.poll-left-time-title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
margin-right: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-left-time-value {
|
.poll-left-time-value {
|
||||||
@@ -281,21 +372,6 @@ const voteOption = async (idx) => {
|
|||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-poll-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
background-color: var(--lottery-background-color);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-question {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option-progress {
|
.poll-option-progress {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: rgb(187, 187, 187);
|
background-color: rgb(187, 187, 187);
|
||||||
@@ -321,11 +397,51 @@ const voteOption = async (idx) => {
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-vote-button {
|
.multi-selection-container,
|
||||||
margin-top: 5px;
|
.single-selection-container {
|
||||||
color: var(--primary-color);
|
margin-top: 30px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-selection-title,
|
||||||
|
.single-selection-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-title-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-left-time {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-poll-button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: fit-content;
|
}
|
||||||
|
|
||||||
|
.join-poll-button:hover {
|
||||||
|
background-color: var(--primary-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-participants {
|
.poll-participants {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ const lottery = reactive({
|
|||||||
const poll = reactive({
|
const poll = reactive({
|
||||||
options: ['', ''],
|
options: ['', ''],
|
||||||
endTime: null,
|
endTime: null,
|
||||||
|
multiple: false,
|
||||||
})
|
})
|
||||||
const startTime = ref(null)
|
const startTime = ref(null)
|
||||||
const isWaitingPosting = ref(false)
|
const isWaitingPosting = ref(false)
|
||||||
@@ -121,6 +122,7 @@ const clearPost = async () => {
|
|||||||
startTime.value = null
|
startTime.value = null
|
||||||
poll.options = ['', '']
|
poll.options = ['', '']
|
||||||
poll.endTime = null
|
poll.endTime = null
|
||||||
|
poll.multiple = false
|
||||||
|
|
||||||
// 删除草稿
|
// 删除草稿
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
@@ -318,6 +320,7 @@ const submitPost = async () => {
|
|||||||
prizeCount: postType.value === 'LOTTERY' ? lottery.prizeCount : undefined,
|
prizeCount: postType.value === 'LOTTERY' ? lottery.prizeCount : undefined,
|
||||||
prizeDescription: postType.value === 'LOTTERY' ? lottery.prizeDescription : undefined,
|
prizeDescription: postType.value === 'LOTTERY' ? lottery.prizeDescription : undefined,
|
||||||
options: postType.value === 'POLL' ? poll.options : undefined,
|
options: postType.value === 'POLL' ? poll.options : undefined,
|
||||||
|
multiple: postType.value === 'POLL' ? poll.multiple : undefined,
|
||||||
startTime:
|
startTime:
|
||||||
postType.value === 'LOTTERY' ? new Date(startTime.value).toISOString() : undefined,
|
postType.value === 'LOTTERY' ? new Date(startTime.value).toISOString() : undefined,
|
||||||
pointCost: postType.value === 'LOTTERY' ? lottery.pointCost : undefined,
|
pointCost: postType.value === 'LOTTERY' ? lottery.pointCost : undefined,
|
||||||
|
|||||||
@@ -1032,7 +1032,120 @@ onMounted(async () => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-menu-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-info-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0px;
|
||||||
|
border-bottom: 1px solid var(--normal-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-container {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-item {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-item-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-medal {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 4px;
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-time {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content-text {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-footer-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-viewer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-viewer-item-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-viewer-item {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.make-reaction-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-link:hover {
|
||||||
|
background-color: #e2e2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-editor-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.post-page-main-container {
|
.post-page-main-container {
|
||||||
@@ -1084,8 +1197,5 @@ onMounted(async () => {
|
|||||||
.loading-container {
|
.loading-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user