mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-05-08 03:37:28 +08:00
Merge pull request #725 from nagisa77/feature/daily_bugfix_0826
0826 daily bugfix
This commit is contained in:
@@ -41,7 +41,8 @@ public class PostController {
|
|||||||
Post post = postService.createPost(auth.getName(), req.getCategoryId(),
|
Post post = postService.createPost(auth.getName(), req.getCategoryId(),
|
||||||
req.getTitle(), req.getContent(), req.getTagIds(),
|
req.getTitle(), req.getContent(), req.getTagIds(),
|
||||||
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
|
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
|
||||||
req.getPrizeCount(), req.getStartTime(), req.getEndTime());
|
req.getPrizeCount(), req.getPointCost(),
|
||||||
|
req.getStartTime(), req.getEndTime());
|
||||||
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()));
|
||||||
|
|||||||
@@ -105,6 +105,17 @@ public class UserController {
|
|||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{identifier}/subscribed-posts")
|
||||||
|
public java.util.List<PostMetaDto> subscribedPosts(@PathVariable("identifier") String identifier,
|
||||||
|
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||||
|
int l = limit != null ? limit : defaultPostsLimit;
|
||||||
|
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||||
|
return subscriptionService.getSubscribedPosts(user.getUsername()).stream()
|
||||||
|
.limit(l)
|
||||||
|
.map(userMapper::toMetaDto)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{identifier}/replies")
|
@GetMapping("/{identifier}/replies")
|
||||||
public java.util.List<CommentInfoDto> userReplies(@PathVariable("identifier") String identifier,
|
public java.util.List<CommentInfoDto> userReplies(@PathVariable("identifier") String identifier,
|
||||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public class LotteryDto {
|
|||||||
private String prizeDescription;
|
private String prizeDescription;
|
||||||
private String prizeIcon;
|
private String prizeIcon;
|
||||||
private int prizeCount;
|
private int prizeCount;
|
||||||
|
private int pointCost;
|
||||||
private LocalDateTime startTime;
|
private LocalDateTime startTime;
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
private List<AuthorDto> participants;
|
private List<AuthorDto> participants;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public class PostRequest {
|
|||||||
private String prizeDescription;
|
private String prizeDescription;
|
||||||
private String prizeIcon;
|
private String prizeIcon;
|
||||||
private Integer prizeCount;
|
private Integer prizeCount;
|
||||||
|
private Integer pointCost;
|
||||||
private LocalDateTime startTime;
|
private LocalDateTime startTime;
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public class PostMapper {
|
|||||||
l.setPrizeDescription(lp.getPrizeDescription());
|
l.setPrizeDescription(lp.getPrizeDescription());
|
||||||
l.setPrizeIcon(lp.getPrizeIcon());
|
l.setPrizeIcon(lp.getPrizeIcon());
|
||||||
l.setPrizeCount(lp.getPrizeCount());
|
l.setPrizeCount(lp.getPrizeCount());
|
||||||
|
l.setPointCost(lp.getPointCost());
|
||||||
l.setStartTime(lp.getStartTime());
|
l.setStartTime(lp.getStartTime());
|
||||||
l.setEndTime(lp.getEndTime());
|
l.setEndTime(lp.getEndTime());
|
||||||
l.setParticipants(lp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
|
l.setParticipants(lp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ public class LotteryPost extends Post {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private int prizeCount;
|
private int prizeCount;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private int pointCost;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private LocalDateTime startTime;
|
private LocalDateTime startTime;
|
||||||
|
|
||||||
|
|||||||
@@ -8,5 +8,7 @@ public enum PointHistoryType {
|
|||||||
INVITE,
|
INVITE,
|
||||||
FEATURE,
|
FEATURE,
|
||||||
SYSTEM_ONLINE,
|
SYSTEM_ONLINE,
|
||||||
REDEEM
|
REDEEM,
|
||||||
|
LOTTERY_JOIN,
|
||||||
|
LOTTERY_REWARD
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.openisle.service;
|
|||||||
|
|
||||||
import com.openisle.model.*;
|
import com.openisle.model.*;
|
||||||
import com.openisle.repository.*;
|
import com.openisle.repository.*;
|
||||||
|
import com.openisle.exception.FieldException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -39,6 +40,17 @@ public class PointService {
|
|||||||
return addPoint(user, 500, PointHistoryType.FEATURE, post, null, null);
|
return addPoint(user, 500, PointHistoryType.FEATURE, post, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void processLotteryJoin(User participant, LotteryPost post) {
|
||||||
|
int cost = post.getPointCost();
|
||||||
|
if (cost > 0) {
|
||||||
|
if (participant.getPoint() < cost) {
|
||||||
|
throw new FieldException("point", "积分不足");
|
||||||
|
}
|
||||||
|
addPoint(participant, -cost, PointHistoryType.LOTTERY_JOIN, post, null, post.getAuthor());
|
||||||
|
addPoint(post.getAuthor(), cost, PointHistoryType.LOTTERY_REWARD, post, null, participant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private PointLog getTodayLog(User user) {
|
private PointLog getTodayLog(User user) {
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
return pointLogRepository.findByUserAndLogDate(user, today)
|
return pointLogRepository.findByUserAndLogDate(user, today)
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ public class PostService {
|
|||||||
String prizeDescription,
|
String prizeDescription,
|
||||||
String prizeIcon,
|
String prizeIcon,
|
||||||
Integer prizeCount,
|
Integer prizeCount,
|
||||||
|
Integer pointCost,
|
||||||
LocalDateTime startTime,
|
LocalDateTime startTime,
|
||||||
LocalDateTime endTime) {
|
LocalDateTime endTime) {
|
||||||
long recent = postRepository.countByAuthorAfter(username,
|
long recent = postRepository.countByAuthorAfter(username,
|
||||||
@@ -188,10 +189,14 @@ public class PostService {
|
|||||||
PostType actualType = type != null ? type : PostType.NORMAL;
|
PostType actualType = type != null ? type : PostType.NORMAL;
|
||||||
Post post;
|
Post post;
|
||||||
if (actualType == PostType.LOTTERY) {
|
if (actualType == PostType.LOTTERY) {
|
||||||
|
if (pointCost != null && (pointCost < 0 || pointCost > 100)) {
|
||||||
|
throw new IllegalArgumentException("pointCost must be between 0 and 100");
|
||||||
|
}
|
||||||
LotteryPost lp = new LotteryPost();
|
LotteryPost lp = new LotteryPost();
|
||||||
lp.setPrizeDescription(prizeDescription);
|
lp.setPrizeDescription(prizeDescription);
|
||||||
lp.setPrizeIcon(prizeIcon);
|
lp.setPrizeIcon(prizeIcon);
|
||||||
lp.setPrizeCount(prizeCount != null ? prizeCount : 0);
|
lp.setPrizeCount(prizeCount != null ? prizeCount : 0);
|
||||||
|
lp.setPointCost(pointCost != null ? pointCost : 0);
|
||||||
lp.setStartTime(startTime);
|
lp.setStartTime(startTime);
|
||||||
lp.setEndTime(endTime);
|
lp.setEndTime(endTime);
|
||||||
post = lp;
|
post = lp;
|
||||||
@@ -250,8 +255,10 @@ public class PostService {
|
|||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
post.getParticipants().add(user);
|
if (post.getParticipants().add(user)) {
|
||||||
lotteryPostRepository.save(post);
|
pointService.processLotteryJoin(user, post);
|
||||||
|
lotteryPostRepository.save(post);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ public class SubscriptionService {
|
|||||||
return commentSubRepo.findByComment(c).stream().map(CommentSubscription::getUser).toList();
|
return commentSubRepo.findByComment(c).stream().map(CommentSubscription::getUser).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Post> getSubscribedPosts(String username) {
|
||||||
|
User user = userRepo.findByUsername(username).orElseThrow();
|
||||||
|
return postSubRepo.findByUser(user).stream().map(PostSubscription::getPost).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public long countSubscribers(String username) {
|
public long countSubscribers(String username) {
|
||||||
User user = userRepo.findByUsername(username).orElseThrow();
|
User user = userRepo.findByUsername(username).orElseThrow();
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE lottery_posts ADD COLUMN point_cost INT NOT NULL DEFAULT 0;
|
||||||
@@ -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())).thenReturn(post);
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -136,6 +136,30 @@ class UserControllerTest {
|
|||||||
.andExpect(jsonPath("$[0].title").value("hello"));
|
.andExpect(jsonPath("$[0].title").value("hello"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listSubscribedPosts() throws Exception {
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername("bob");
|
||||||
|
com.openisle.model.Category cat = new com.openisle.model.Category();
|
||||||
|
cat.setName("tech");
|
||||||
|
com.openisle.model.Post post = new com.openisle.model.Post();
|
||||||
|
post.setId(6L);
|
||||||
|
post.setTitle("fav");
|
||||||
|
post.setCreatedAt(java.time.LocalDateTime.now());
|
||||||
|
post.setCategory(cat);
|
||||||
|
post.setAuthor(user);
|
||||||
|
Mockito.when(userService.findByIdentifier("bob")).thenReturn(Optional.of(user));
|
||||||
|
Mockito.when(subscriptionService.getSubscribedPosts("bob")).thenReturn(java.util.List.of(post));
|
||||||
|
PostMetaDto meta = new PostMetaDto();
|
||||||
|
meta.setId(6L);
|
||||||
|
meta.setTitle("fav");
|
||||||
|
Mockito.when(userMapper.toMetaDto(post)).thenReturn(meta);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/users/bob/subscribed-posts"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$[0].title").value("fav"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void listUserReplies() throws Exception {
|
void listUserReplies() throws Exception {
|
||||||
User user = new User();
|
User user = new User();
|
||||||
|
|||||||
@@ -146,7 +146,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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -9,14 +9,12 @@
|
|||||||
title="收起至 100px"
|
title="收起至 100px"
|
||||||
@click="collapseToMini"
|
@click="collapseToMini"
|
||||||
></i>
|
></i>
|
||||||
<!-- 回弹:60vh -->
|
|
||||||
<i
|
<i
|
||||||
class="fas fa-chevron-up"
|
class="fas fa-chevron-up"
|
||||||
v-if="floatHeight !== DEFAULT_HEIGHT"
|
v-if="floatHeight !== DEFAULT_HEIGHT"
|
||||||
title="回弹至 60vh"
|
title="回弹至 60vh"
|
||||||
@click="reboundToDefault"
|
@click="reboundToDefault"
|
||||||
></i>
|
></i>
|
||||||
<!-- 全屏打开(原有逻辑) -->
|
|
||||||
<i class="fas fa-expand" title="在页面中打开" @click="expand"></i>
|
<i class="fas fa-expand" title="在页面中打开" @click="expand"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +59,6 @@ function injectBaseTag() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当浮窗重新出现时,恢复默认高度
|
|
||||||
watch(
|
watch(
|
||||||
() => floatRoute.value,
|
() => floatRoute.value,
|
||||||
(v) => {
|
(v) => {
|
||||||
@@ -76,7 +73,6 @@ watch(
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
/* 高度由内联样式绑定控制:60vh / 100px */
|
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
border: 1px solid var(--normal-border-color);
|
border: 1px solid var(--normal-border-color);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
@mouseenter="cancelHide"
|
@mouseenter="cancelHide"
|
||||||
@mouseleave="scheduleHide"
|
@mouseleave="scheduleHide"
|
||||||
>
|
>
|
||||||
<template v-if="reactions.length < 4">
|
<template v-if="counts.length < 4">
|
||||||
<div
|
<div
|
||||||
v-for="r in displayedReactions"
|
v-for="r in displayedReactions"
|
||||||
:key="r.type"
|
:key="r.type"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<i class="fas fa-compress" @click="minimize" title="最小化"></i>
|
<i class="fas fa-compress" @click="minimize" title="最小化"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div :class="['tab', { active: activeTab === 'messages' }]" @click="activeTab = 'messages'">
|
<div :class="['tab', { active: activeTab === 'messages' }]" @click="switchToMessage">
|
||||||
站内信
|
站内信
|
||||||
</div>
|
</div>
|
||||||
<div :class="['tab', { active: activeTab === 'channels' }]" @click="switchToChannels">
|
<div :class="['tab', { active: activeTab === 'channels' }]" @click="switchToChannels">
|
||||||
@@ -147,7 +147,7 @@ const { fetchChannelUnread: refreshChannelUnread, setFromList: setChannelUnreadF
|
|||||||
useChannelsUnreadCount()
|
useChannelsUnreadCount()
|
||||||
let subscription = null
|
let subscription = null
|
||||||
|
|
||||||
const activeTab = ref('messages')
|
const activeTab = ref('channels')
|
||||||
const channels = ref([])
|
const channels = ref([])
|
||||||
const loadingChannels = ref(false)
|
const loadingChannels = ref(false)
|
||||||
const isFloatMode = computed(() => route.query.float === '1')
|
const isFloatMode = computed(() => route.query.float === '1')
|
||||||
@@ -159,6 +159,7 @@ async function fetchConversations() {
|
|||||||
toast.error('请先登录')
|
toast.error('请先登录')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/messages/conversations`, {
|
const response = await fetch(`${API_BASE_URL}/api/messages/conversations`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -215,11 +216,14 @@ async function fetchChannels() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchToMessage() {
|
||||||
|
activeTab.value = 'messages'
|
||||||
|
fetchConversations()
|
||||||
|
}
|
||||||
|
|
||||||
function switchToChannels() {
|
function switchToChannels() {
|
||||||
activeTab.value = 'channels'
|
activeTab.value = 'channels'
|
||||||
if (channels.value.length === 0) {
|
fetchChannels()
|
||||||
fetchChannels()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goToChannel(id) {
|
async function goToChannel(id) {
|
||||||
@@ -244,12 +248,15 @@ async function goToChannel(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
loading.value = true
|
|
||||||
currentUser.value = await fetchCurrentUser()
|
currentUser.value = await fetchCurrentUser()
|
||||||
|
|
||||||
if (currentUser.value) {
|
if (currentUser.value) {
|
||||||
await fetchConversations()
|
if (activeTab.value === 'messages') {
|
||||||
refreshGlobalUnreadCount() // Refresh global count when entering the list
|
await fetchConversations()
|
||||||
|
} else {
|
||||||
|
await fetchChannels()
|
||||||
|
}
|
||||||
|
refreshGlobalUnreadCount()
|
||||||
refreshChannelUnread()
|
refreshChannelUnread()
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (token && !isConnected.value) {
|
if (token && !isConnected.value) {
|
||||||
|
|||||||
@@ -66,6 +66,18 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="prize-point-row">
|
||||||
|
<span class="prize-row-title">参与所需积分</span>
|
||||||
|
<div class="prize-count-input">
|
||||||
|
<input
|
||||||
|
class="prize-count-input-field"
|
||||||
|
type="number"
|
||||||
|
v-model.number="pointCost"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="prize-time-row">
|
<div class="prize-time-row">
|
||||||
<span class="prize-row-title">抽奖结束时间</span>
|
<span class="prize-row-title">抽奖结束时间</span>
|
||||||
<client-only>
|
<client-only>
|
||||||
@@ -105,6 +117,7 @@ const showPrizeCropper = ref(false)
|
|||||||
const prizeName = ref('')
|
const prizeName = ref('')
|
||||||
const prizeCount = ref(1)
|
const prizeCount = ref(1)
|
||||||
const prizeDescription = ref('')
|
const prizeDescription = ref('')
|
||||||
|
const pointCost = ref(0)
|
||||||
const endTime = ref(null)
|
const endTime = ref(null)
|
||||||
const startTime = ref(null)
|
const startTime = ref(null)
|
||||||
const dateConfig = { enableTime: true, time_24hr: true, dateFormat: 'Y-m-d H:i' }
|
const dateConfig = { enableTime: true, time_24hr: true, dateFormat: 'Y-m-d H:i' }
|
||||||
@@ -133,6 +146,11 @@ watch(prizeCount, (val) => {
|
|||||||
if (!val || val < 1) prizeCount.value = 1
|
if (!val || val < 1) prizeCount.value = 1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(pointCost, (val) => {
|
||||||
|
if (val === undefined || val === null || val < 0) pointCost.value = 0
|
||||||
|
if (val > 100) pointCost.value = 100
|
||||||
|
})
|
||||||
|
|
||||||
const loadDraft = async () => {
|
const loadDraft = async () => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) return
|
if (!token) return
|
||||||
@@ -168,6 +186,7 @@ const clearPost = async () => {
|
|||||||
showPrizeCropper.value = false
|
showPrizeCropper.value = false
|
||||||
prizeDescription.value = ''
|
prizeDescription.value = ''
|
||||||
prizeCount.value = 1
|
prizeCount.value = 1
|
||||||
|
pointCost.value = 0
|
||||||
endTime.value = null
|
endTime.value = null
|
||||||
startTime.value = null
|
startTime.value = null
|
||||||
|
|
||||||
@@ -315,6 +334,10 @@ const submitPost = async () => {
|
|||||||
toast.error('请选择抽奖结束时间')
|
toast.error('请选择抽奖结束时间')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (pointCost.value < 0 || pointCost.value > 100) {
|
||||||
|
toast.error('参与积分需在0到100之间')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
@@ -354,6 +377,7 @@ const submitPost = async () => {
|
|||||||
prizeDescription: postType.value === 'LOTTERY' ? prizeDescription.value : undefined,
|
prizeDescription: postType.value === 'LOTTERY' ? prizeDescription.value : 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' ? pointCost.value : undefined,
|
||||||
// 将时间转换为 UTC+8.5 时区 todo: 需要优化
|
// 将时间转换为 UTC+8.5 时区 todo: 需要优化
|
||||||
endTime:
|
endTime:
|
||||||
postType.value === 'LOTTERY'
|
postType.value === 'LOTTERY'
|
||||||
@@ -498,6 +522,8 @@ const submitPost = async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
|
margin-bottom: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prize-row-title {
|
.prize-row-title {
|
||||||
|
|||||||
@@ -146,6 +146,24 @@
|
|||||||
<template v-else-if="item.type === 'REDEEM'">
|
<template v-else-if="item.type === 'REDEEM'">
|
||||||
兑换商品,消耗 {{ -item.amount }} 积分
|
兑换商品,消耗 {{ -item.amount }} 积分
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="item.type === 'LOTTERY_JOIN'">
|
||||||
|
参与抽奖帖
|
||||||
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
|
item.postTitle
|
||||||
|
}}</NuxtLink>
|
||||||
|
,消耗 {{ -item.amount }} 积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'LOTTERY_REWARD'">
|
||||||
|
你的抽奖帖
|
||||||
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
|
item.postTitle
|
||||||
|
}}</NuxtLink>
|
||||||
|
被
|
||||||
|
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||||
|
item.fromUserName
|
||||||
|
}}</NuxtLink>
|
||||||
|
参与,获得 {{ item.amount }} 积分
|
||||||
|
</template>
|
||||||
<template v-else-if="item.type === 'SYSTEM_ONLINE'"> 积分历史系统上线 </template>
|
<template v-else-if="item.type === 'SYSTEM_ONLINE'"> 积分历史系统上线 </template>
|
||||||
<i class="fas fa-coins"></i> 你目前的积分是 {{ item.balance }}
|
<i class="fas fa-coins"></i> 你目前的积分是 {{ item.balance }}
|
||||||
</div>
|
</div>
|
||||||
@@ -201,6 +219,8 @@ const iconMap = {
|
|||||||
SYSTEM_ONLINE: 'fas fa-clock',
|
SYSTEM_ONLINE: 'fas fa-clock',
|
||||||
REDEEM: 'fas fa-gift',
|
REDEEM: 'fas fa-gift',
|
||||||
FEATURE: 'fas fa-star',
|
FEATURE: 'fas fa-star',
|
||||||
|
LOTTERY_JOIN: 'fas fa-ticket-alt',
|
||||||
|
LOTTERY_REWARD: 'fas fa-ticket-alt',
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -119,7 +119,9 @@
|
|||||||
class="join-prize-button"
|
class="join-prize-button"
|
||||||
@click="joinLottery"
|
@click="joinLottery"
|
||||||
>
|
>
|
||||||
<div class="join-prize-button-text">参与抽奖</div>
|
<div class="join-prize-button-text">
|
||||||
|
参与抽奖 <i class="fas fa-coins"></i> {{ lottery.pointCost }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
||||||
<div class="join-prize-button-text">已参与</div>
|
<div class="join-prize-button-text">已参与</div>
|
||||||
@@ -134,7 +136,9 @@
|
|||||||
class="join-prize-button"
|
class="join-prize-button"
|
||||||
@click="joinLottery"
|
@click="joinLottery"
|
||||||
>
|
>
|
||||||
<div class="join-prize-button-text">参与抽奖</div>
|
<div class="join-prize-button-text">
|
||||||
|
参与抽奖 <i class="fas fa-coins"></i> {{ lottery.pointCost }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
||||||
<div class="join-prize-button-text">已参与</div>
|
<div class="join-prize-button-text">已参与</div>
|
||||||
@@ -810,11 +814,12 @@ const joinLottery = async () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success('已参与抽奖')
|
toast.success('已参与抽奖')
|
||||||
await refreshPost()
|
await refreshPost()
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error(data.error || '操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -894,7 +899,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.post-page-container {
|
.post-page-container {
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -94,6 +94,13 @@
|
|||||||
<i class="fas fa-user-plus"></i>
|
<i class="fas fa-user-plus"></i>
|
||||||
<div class="profile-tabs-item-label">关注</div>
|
<div class="profile-tabs-item-label">关注</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
:class="['profile-tabs-item', { selected: selectedTab === 'favorites' }]"
|
||||||
|
@click="selectedTab = 'favorites'"
|
||||||
|
>
|
||||||
|
<i class="fas fa-bookmark"></i>
|
||||||
|
<div class="profile-tabs-item-label">收藏</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="['profile-tabs-item', { selected: selectedTab === 'achievements' }]"
|
:class="['profile-tabs-item', { selected: selectedTab === 'achievements' }]"
|
||||||
@click="selectedTab = 'achievements'"
|
@click="selectedTab = 'achievements'"
|
||||||
@@ -318,6 +325,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="selectedTab === 'favorites'" class="favorites-container">
|
||||||
|
<div v-if="favoritePosts.length > 0">
|
||||||
|
<BaseTimeline :items="favoritePosts">
|
||||||
|
<template #item="{ item }">
|
||||||
|
<NuxtLink :to="`/posts/${item.post.id}`" class="timeline-link">
|
||||||
|
{{ item.post.title }}
|
||||||
|
</NuxtLink>
|
||||||
|
<div class="timeline-snippet">
|
||||||
|
{{ stripMarkdown(item.post.snippet) }}
|
||||||
|
</div>
|
||||||
|
<div class="timeline-date">{{ formatDate(item.post.createdAt) }}</div>
|
||||||
|
</template>
|
||||||
|
</BaseTimeline>
|
||||||
|
</div>
|
||||||
|
<div v-else class="summary-empty">暂无收藏文章</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="selectedTab === 'achievements'" class="achievements-container">
|
<div v-else-if="selectedTab === 'achievements'" class="achievements-container">
|
||||||
<AchievementList :medals="medals" :can-select="isMine" />
|
<AchievementList :medals="medals" :can-select="isMine" />
|
||||||
</div>
|
</div>
|
||||||
@@ -352,6 +376,7 @@ const user = ref({})
|
|||||||
const hotPosts = ref([])
|
const hotPosts = ref([])
|
||||||
const hotReplies = ref([])
|
const hotReplies = ref([])
|
||||||
const hotTags = ref([])
|
const hotTags = ref([])
|
||||||
|
const favoritePosts = ref([])
|
||||||
const timelineItems = ref([])
|
const timelineItems = ref([])
|
||||||
const timelineFilter = ref('all')
|
const timelineFilter = ref('all')
|
||||||
const filteredTimelineItems = computed(() => {
|
const filteredTimelineItems = computed(() => {
|
||||||
@@ -369,7 +394,7 @@ const subscribed = ref(false)
|
|||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const tabLoading = ref(false)
|
const tabLoading = ref(false)
|
||||||
const selectedTab = ref(
|
const selectedTab = ref(
|
||||||
['summary', 'timeline', 'following', 'achievements'].includes(route.query.tab)
|
['summary', 'timeline', 'following', 'favorites', 'achievements'].includes(route.query.tab)
|
||||||
? route.query.tab
|
? route.query.tab
|
||||||
: 'summary',
|
: 'summary',
|
||||||
)
|
)
|
||||||
@@ -472,6 +497,16 @@ const fetchFollowUsers = async () => {
|
|||||||
followings.value = followingRes.ok ? await followingRes.json() : []
|
followings.value = followingRes.ok ? await followingRes.json() : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchFavorites = async () => {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/users/${username}/subscribed-posts`)
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
favoritePosts.value = data.map((p) => ({ icon: 'fas fa-bookmark', post: p }))
|
||||||
|
} else {
|
||||||
|
favoritePosts.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadSummary = async () => {
|
const loadSummary = async () => {
|
||||||
tabLoading.value = true
|
tabLoading.value = true
|
||||||
await fetchSummary()
|
await fetchSummary()
|
||||||
@@ -490,6 +525,12 @@ const loadFollow = async () => {
|
|||||||
tabLoading.value = false
|
tabLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadFavorites = async () => {
|
||||||
|
tabLoading.value = true
|
||||||
|
await fetchFavorites()
|
||||||
|
tabLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const fetchAchievements = async () => {
|
const fetchAchievements = async () => {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/medals?userId=${user.value.id}`)
|
const res = await fetch(`${API_BASE_URL}/api/medals?userId=${user.value.id}`)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -578,6 +619,8 @@ const init = async () => {
|
|||||||
await loadTimeline()
|
await loadTimeline()
|
||||||
} else if (selectedTab.value === 'following') {
|
} else if (selectedTab.value === 'following') {
|
||||||
await loadFollow()
|
await loadFollow()
|
||||||
|
} else if (selectedTab.value === 'favorites') {
|
||||||
|
await loadFavorites()
|
||||||
} else if (selectedTab.value === 'achievements') {
|
} else if (selectedTab.value === 'achievements') {
|
||||||
await loadAchievements()
|
await loadAchievements()
|
||||||
}
|
}
|
||||||
@@ -596,6 +639,8 @@ watch(selectedTab, async (val) => {
|
|||||||
await loadTimeline()
|
await loadTimeline()
|
||||||
} else if (val === 'following' && followers.value.length === 0 && followings.value.length === 0) {
|
} else if (val === 'following' && followers.value.length === 0 && followings.value.length === 0) {
|
||||||
await loadFollow()
|
await loadFollow()
|
||||||
|
} else if (val === 'favorites' && favoritePosts.value.length === 0) {
|
||||||
|
await loadFavorites()
|
||||||
} else if (val === 'achievements' && medals.value.length === 0) {
|
} else if (val === 'achievements' && medals.value.length === 0) {
|
||||||
await loadAchievements()
|
await loadAchievements()
|
||||||
}
|
}
|
||||||
@@ -900,6 +945,10 @@ watch(selectedTab, async (val) => {
|
|||||||
.follow-container {
|
.follow-container {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.favorites-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.follow-tabs {
|
.follow-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ const md = new MarkdownIt({
|
|||||||
|
|
||||||
md.use(mentionPlugin)
|
md.use(mentionPlugin)
|
||||||
md.use(tiebaEmojiPlugin)
|
md.use(tiebaEmojiPlugin)
|
||||||
md.use(linkPlugin) // 添加链接插件
|
md.use(linkPlugin)
|
||||||
|
|
||||||
export function renderMarkdown(text) {
|
export function renderMarkdown(text) {
|
||||||
return md.render(text || '')
|
return md.render(text || '')
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function createVditor(editorId, options = {}) {
|
|||||||
const { placeholder = '', preview = {}, input, after } = options
|
const { placeholder = '', preview = {}, input, after } = options
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
||||||
|
|
||||||
const fetchMentions = async (value) => {
|
const fetchMentions = async (value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -80,7 +81,7 @@ export function createVditor(editorId, options = {}) {
|
|||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
vditorPostCitation(API_BASE_URL),
|
vditorPostCitation(API_BASE_URL, WEBSITE_BASE_URL),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
cdn: 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/vditor',
|
cdn: 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/vditor',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ async function searchPost(apiBaseUrl, keyword) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (apiBaseUrl) => {
|
export default (apiBaseUrl, websiteBaseUrl) => {
|
||||||
return {
|
return {
|
||||||
key: '#',
|
key: '#',
|
||||||
hint: async (keyword) => {
|
hint: async (keyword) => {
|
||||||
@@ -22,8 +22,8 @@ export default (apiBaseUrl) => {
|
|||||||
let value = ''
|
let value = ''
|
||||||
return (
|
return (
|
||||||
body.map((item) => ({
|
body.map((item) => ({
|
||||||
value: `[${item.title}](/posts/${item.id})`,
|
value: `[🔗${item.title}](${websiteBaseUrl}/posts/${item.id})`,
|
||||||
html: `<div>${item.title}</div>`,
|
html: `<div><i class="fas fa-link"></i> ${item.title}</div>`,
|
||||||
})) ?? []
|
})) ?? []
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user