mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-16 12:01:00 +08:00
Merge pull request #674 from nagisa77/codex/add-points-history-system-with-ui
feat: add point history
This commit is contained in:
@@ -53,7 +53,7 @@ public class AuthController {
|
||||
try {
|
||||
User user = userService.registerWithInvite(
|
||||
req.getUsername(), req.getEmail(), req.getPassword());
|
||||
inviteService.consume(req.getInviteToken());
|
||||
inviteService.consume(req.getInviteToken(), user.getUsername());
|
||||
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"token", jwtService.generateToken(user.getUsername()),
|
||||
@@ -154,7 +154,7 @@ public class AuthController {
|
||||
if (resultOpt.isPresent()) {
|
||||
AuthResult result = resultOpt.get();
|
||||
if (viaInvite && result.isNewUser()) {
|
||||
inviteService.consume(req.getInviteToken());
|
||||
inviteService.consume(req.getInviteToken(), user.getUsername());
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||
"reason_code", "INVITE_APPROVED"
|
||||
@@ -229,7 +229,7 @@ public class AuthController {
|
||||
if (resultOpt.isPresent()) {
|
||||
AuthResult result = resultOpt.get();
|
||||
if (viaInvite && result.isNewUser()) {
|
||||
inviteService.consume(req.getInviteToken());
|
||||
inviteService.consume(req.getInviteToken(), user.getUsername());
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||
"reason_code", "INVITE_APPROVED"
|
||||
@@ -276,7 +276,7 @@ public class AuthController {
|
||||
if (resultOpt.isPresent()) {
|
||||
AuthResult result = resultOpt.get();
|
||||
if (viaInvite && result.isNewUser()) {
|
||||
inviteService.consume(req.getInviteToken());
|
||||
inviteService.consume(req.getInviteToken(), user.getUsername());
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||
"reason_code", "INVITE_APPROVED"
|
||||
@@ -323,7 +323,7 @@ public class AuthController {
|
||||
if (resultOpt.isPresent()) {
|
||||
AuthResult result = resultOpt.get();
|
||||
if (viaInvite && result.isNewUser()) {
|
||||
inviteService.consume(req.getInviteToken());
|
||||
inviteService.consume(req.getInviteToken(), user.getUsername());
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
||||
"reason_code", "INVITE_APPROVED"
|
||||
|
||||
@@ -47,7 +47,7 @@ public class CommentController {
|
||||
Comment comment = commentService.addComment(auth.getName(), postId, req.getContent());
|
||||
CommentDto dto = commentMapper.toDto(comment);
|
||||
dto.setReward(levelService.awardForComment(auth.getName()));
|
||||
dto.setPointReward(pointService.awardForComment(auth.getName(),postId));
|
||||
dto.setPointReward(pointService.awardForComment(auth.getName(), postId, comment.getId()));
|
||||
log.debug("createComment succeeded for comment {}", comment.getId());
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.dto.PointHistoryDto;
|
||||
import com.openisle.mapper.PointHistoryMapper;
|
||||
import com.openisle.service.PointService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/point-histories")
|
||||
@RequiredArgsConstructor
|
||||
public class PointHistoryController {
|
||||
private final PointService pointService;
|
||||
private final PointHistoryMapper pointHistoryMapper;
|
||||
|
||||
@GetMapping
|
||||
public List<PointHistoryDto> list(Authentication auth) {
|
||||
return pointService.listHistory(auth.getName()).stream()
|
||||
.map(pointHistoryMapper::toDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public class PostController {
|
||||
draftService.deleteDraft(auth.getName());
|
||||
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
||||
dto.setReward(levelService.awardForPost(auth.getName()));
|
||||
dto.setPointReward(pointService.awardForPost(auth.getName()));
|
||||
dto.setPointReward(pointService.awardForPost(auth.getName(), post.getId()));
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
|
||||
23
backend/src/main/java/com/openisle/dto/PointHistoryDto.java
Normal file
23
backend/src/main/java/com/openisle/dto/PointHistoryDto.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.openisle.dto;
|
||||
|
||||
import com.openisle.model.PointHistoryType;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class PointHistoryDto {
|
||||
private Long id;
|
||||
private PointHistoryType type;
|
||||
private int amount;
|
||||
private int balance;
|
||||
private Long postId;
|
||||
private String postTitle;
|
||||
private Long commentId;
|
||||
private String commentContent;
|
||||
private Long fromUserId;
|
||||
private String fromUserName;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.openisle.mapper;
|
||||
|
||||
import com.openisle.dto.PointHistoryDto;
|
||||
import com.openisle.model.PointHistory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PointHistoryMapper {
|
||||
public PointHistoryDto toDto(PointHistory history) {
|
||||
PointHistoryDto dto = new PointHistoryDto();
|
||||
dto.setId(history.getId());
|
||||
dto.setType(history.getType());
|
||||
dto.setAmount(history.getAmount());
|
||||
dto.setBalance(history.getBalance());
|
||||
dto.setCreatedAt(history.getCreatedAt());
|
||||
if (history.getPost() != null) {
|
||||
dto.setPostId(history.getPost().getId());
|
||||
dto.setPostTitle(history.getPost().getTitle());
|
||||
}
|
||||
if (history.getComment() != null) {
|
||||
dto.setCommentId(history.getComment().getId());
|
||||
dto.setCommentContent(history.getComment().getContent());
|
||||
if (history.getComment().getPost() != null && dto.getPostId() == null) {
|
||||
dto.setPostId(history.getComment().getPost().getId());
|
||||
dto.setPostTitle(history.getComment().getPost().getTitle());
|
||||
}
|
||||
}
|
||||
if (history.getFromUser() != null) {
|
||||
dto.setFromUserId(history.getFromUser().getId());
|
||||
dto.setFromUserName(history.getFromUser().getUsername());
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
49
backend/src/main/java/com/openisle/model/PointHistory.java
Normal file
49
backend/src/main/java/com/openisle/model/PointHistory.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** Point change history for a user. */
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Table(name = "point_histories")
|
||||
public class PointHistory {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private PointHistoryType type;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int amount;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int balance;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "post_id")
|
||||
private Post post;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "comment_id")
|
||||
private Comment comment;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "from_user_id")
|
||||
private User fromUser;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.openisle.model;
|
||||
|
||||
public enum PointHistoryType {
|
||||
POST,
|
||||
COMMENT,
|
||||
POST_LIKED,
|
||||
COMMENT_LIKED,
|
||||
INVITE,
|
||||
SYSTEM_ONLINE
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.PointHistory;
|
||||
import com.openisle.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PointHistoryRepository extends JpaRepository<PointHistory, Long> {
|
||||
List<PointHistory> findByUserOrderByIdDesc(User user);
|
||||
long countByUser(User user);
|
||||
}
|
||||
@@ -45,10 +45,10 @@ public class InviteService {
|
||||
return invite != null && invite.getUsageCount() < 3;
|
||||
}
|
||||
|
||||
public void consume(String token) {
|
||||
public void consume(String token, String newUserName) {
|
||||
InviteToken invite = inviteTokenRepository.findById(token).orElseThrow();
|
||||
invite.setUsageCount(invite.getUsageCount() + 1);
|
||||
inviteTokenRepository.save(invite);
|
||||
pointService.awardForInvite(invite.getInviter().getUsername());
|
||||
pointService.awardForInvite(invite.getInviter().getUsername(), newUserName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.model.PointLog;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.model.*;
|
||||
import com.openisle.repository.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -16,19 +15,22 @@ public class PointService {
|
||||
private final PointLogRepository pointLogRepository;
|
||||
private final PostRepository postRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
private final PointHistoryRepository pointHistoryRepository;
|
||||
|
||||
public int awardForPost(String userName) {
|
||||
public int awardForPost(String userName, Long postId) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
PointLog log = getTodayLog(user);
|
||||
if (log.getPostCount() > 1) return 0;
|
||||
log.setPostCount(log.getPostCount() + 1);
|
||||
pointLogRepository.save(log);
|
||||
return addPoint(user, 30);
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return addPoint(user, 30, PointHistoryType.POST, post, null, null);
|
||||
}
|
||||
|
||||
public int awardForInvite(String userName) {
|
||||
public int awardForInvite(String userName, String inviteeName) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
return addPoint(user, 500);
|
||||
User invitee = userRepository.findByUsername(inviteeName).orElseThrow();
|
||||
return addPoint(user, 500, PointHistoryType.INVITE, null, null, invitee);
|
||||
}
|
||||
|
||||
private PointLog getTodayLog(User user) {
|
||||
@@ -45,20 +47,38 @@ public class PointService {
|
||||
});
|
||||
}
|
||||
|
||||
private int addPoint(User user, int amount) {
|
||||
private int addPoint(User user, int amount, PointHistoryType type,
|
||||
Post post, Comment comment, User fromUser) {
|
||||
user.setPoint(user.getPoint() + amount);
|
||||
userRepository.save(user);
|
||||
recordHistory(user, type, amount, post, comment, fromUser);
|
||||
return amount;
|
||||
}
|
||||
|
||||
private void recordHistory(User user, PointHistoryType type, int amount,
|
||||
Post post, Comment comment, User fromUser) {
|
||||
PointHistory history = new PointHistory();
|
||||
history.setUser(user);
|
||||
history.setType(type);
|
||||
history.setAmount(amount);
|
||||
history.setBalance(user.getPoint());
|
||||
history.setPost(post);
|
||||
history.setComment(comment);
|
||||
history.setFromUser(fromUser);
|
||||
history.setCreatedAt(java.time.LocalDateTime.now());
|
||||
pointHistoryRepository.save(history);
|
||||
}
|
||||
|
||||
// 同时为评论者和发帖人增加积分,返回值为评论者增加的积分数
|
||||
// 注意需要考虑发帖和回复是同一人的场景
|
||||
public int awardForComment(String commenterName, Long postId) {
|
||||
public int awardForComment(String commenterName, Long postId, Long commentId) {
|
||||
// 标记评论者是否已达到积分奖励上限
|
||||
boolean isTheRewardCapped = false;
|
||||
|
||||
// 根据帖子id找到发帖人
|
||||
User poster = postRepository.findById(postId).orElseThrow().getAuthor();
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
User poster = post.getAuthor();
|
||||
Comment comment = commentRepository.findById(commentId).orElseThrow();
|
||||
|
||||
// 获取评论者的加分日志
|
||||
User commenter = userRepository.findByUsername(commenterName).orElseThrow();
|
||||
@@ -74,15 +94,15 @@ public class PointService {
|
||||
} else {
|
||||
log.setCommentCount(log.getCommentCount() + 1);
|
||||
pointLogRepository.save(log);
|
||||
return addPoint(commenter, 10);
|
||||
return addPoint(commenter, 10, PointHistoryType.COMMENT, post, comment, null);
|
||||
}
|
||||
} else {
|
||||
addPoint(poster, 10);
|
||||
addPoint(poster, 10, PointHistoryType.COMMENT, post, comment, commenter);
|
||||
// 如果发帖人与评论者不是同一个,则根据是否达到积分上限来判断评论者加分情况
|
||||
if (isTheRewardCapped) {
|
||||
return 0;
|
||||
} else {
|
||||
return addPoint(commenter, 10);
|
||||
return addPoint(commenter, 10, PointHistoryType.COMMENT, post, comment, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +121,8 @@ public class PointService {
|
||||
}
|
||||
|
||||
// 如果不是同一个,则为发帖人加分
|
||||
return addPoint(poster, 10);
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return addPoint(poster, 10, PointHistoryType.POST_LIKED, post, null, reactioner);
|
||||
}
|
||||
|
||||
// 考虑点赞者和评论者是同一个的情况
|
||||
@@ -118,7 +139,17 @@ public class PointService {
|
||||
}
|
||||
|
||||
// 如果不是同一个,则为发帖人加分
|
||||
return addPoint(commenter, 10);
|
||||
Comment comment = commentRepository.findById(commentId).orElseThrow();
|
||||
Post post = comment.getPost();
|
||||
return addPoint(commenter, 10, PointHistoryType.COMMENT_LIKED, post, comment, reactioner);
|
||||
}
|
||||
|
||||
public java.util.List<PointHistory> listHistory(String userName) {
|
||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
||||
if (pointHistoryRepository.countByUser(user) == 0) {
|
||||
recordHistory(user, PointHistoryType.SYSTEM_ONLINE, 0, null, null, null);
|
||||
}
|
||||
return pointHistoryRepository.findByUserOrderByIdDesc(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,62 +1,151 @@
|
||||
<template>
|
||||
<div class="point-mall-page">
|
||||
<section class="rules">
|
||||
<div class="section-title">🎉 积分规则</div>
|
||||
<div class="section-content">
|
||||
<div class="section-item" v-for="(rule, idx) in pointRules" :key="idx">{{ rule }}</div>
|
||||
<div class="point-tabs">
|
||||
<div
|
||||
:class="['point-tab-item', { selected: selectedTab === 'mall' }]"
|
||||
@click="selectedTab = 'mall'"
|
||||
>
|
||||
积分兑换
|
||||
</div>
|
||||
<div
|
||||
:class="['point-tab-item', { selected: selectedTab === 'history' }]"
|
||||
@click="selectedTab = 'history'"
|
||||
>
|
||||
积分历史
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="loading-points-container" v-if="isLoading">
|
||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||
</div>
|
||||
|
||||
<div class="point-info">
|
||||
<p v-if="authState.loggedIn && point !== null">
|
||||
<span><i class="fas fa-coins coin-icon"></i></span>我的积分:<span class="point-value">{{
|
||||
point
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<template v-if="selectedTab === 'mall'">
|
||||
<section class="rules">
|
||||
<div class="section-title">🎉 积分规则</div>
|
||||
<div class="section-content">
|
||||
<div class="section-item" v-for="(rule, idx) in pointRules" :key="idx">{{ rule }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="goods">
|
||||
<div class="goods-item" v-for="(good, idx) in goods" :key="idx">
|
||||
<img class="goods-item-image" :src="good.image" alt="good.name" />
|
||||
<div class="goods-item-name">{{ good.name }}</div>
|
||||
<div class="goods-item-cost">
|
||||
<i class="fas fa-coins"></i>
|
||||
{{ good.cost }} 积分
|
||||
</div>
|
||||
<div
|
||||
class="goods-item-button"
|
||||
:class="{ disabled: !authState.loggedIn || point === null || point < good.cost }"
|
||||
@click="openRedeem(good)"
|
||||
>
|
||||
兑换
|
||||
</div>
|
||||
<div class="loading-points-container" v-if="isLoading">
|
||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||
</div>
|
||||
</section>
|
||||
<RedeemPopup
|
||||
:visible="dialogVisible"
|
||||
v-model="contact"
|
||||
:loading="loading"
|
||||
@close="closeRedeem"
|
||||
@submit="submitRedeem"
|
||||
/>
|
||||
|
||||
<div class="point-info">
|
||||
<p v-if="authState.loggedIn && point !== null">
|
||||
<span><i class="fas fa-coins coin-icon"></i></span>我的积分:<span class="point-value">{{
|
||||
point
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section class="goods">
|
||||
<div class="goods-item" v-for="(good, idx) in goods" :key="idx">
|
||||
<img class="goods-item-image" :src="good.image" alt="good.name" />
|
||||
<div class="goods-item-name">{{ good.name }}</div>
|
||||
<div class="goods-item-cost">
|
||||
<i class="fas fa-coins"></i>
|
||||
{{ good.cost }} 积分
|
||||
</div>
|
||||
<div
|
||||
class="goods-item-button"
|
||||
:class="{ disabled: !authState.loggedIn || point === null || point < good.cost }"
|
||||
@click="openRedeem(good)"
|
||||
>
|
||||
兑换
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<RedeemPopup
|
||||
:visible="dialogVisible"
|
||||
v-model="contact"
|
||||
:loading="loading"
|
||||
@close="closeRedeem"
|
||||
@submit="submitRedeem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="loading-points-container" v-if="historyLoading">
|
||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||
</div>
|
||||
<BasePlaceholder v-else-if="histories.length === 0" text="暂无积分记录" icon="fas fa-inbox" />
|
||||
<div class="timeline-container" v-else>
|
||||
<BaseTimeline :items="histories">
|
||||
<template #item="{ item }">
|
||||
<div class="history-content">
|
||||
<template v-if="item.type === 'POST'">
|
||||
发送帖子
|
||||
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||
item.postTitle
|
||||
}}</NuxtLink>
|
||||
,获得{{ item.amount }}积分
|
||||
</template>
|
||||
<template v-else-if="item.type === 'COMMENT' && item.commentId && !item.fromUserId">
|
||||
发送评论
|
||||
<NuxtLink
|
||||
:to="`/posts/${item.postId}#comment-${item.commentId}`"
|
||||
class="timeline-link"
|
||||
>{{ stripMarkdownLength(item.commentContent, 100) }}</NuxtLink
|
||||
>
|
||||
,获得{{ item.amount }}积分
|
||||
</template>
|
||||
<template v-else-if="item.type === 'POST_LIKED' && item.fromUserId">
|
||||
帖子
|
||||
<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 === 'COMMENT_LIKED' && item.fromUserId">
|
||||
评论
|
||||
<NuxtLink
|
||||
:to="`/posts/${item.postId}#comment-${item.commentId}`"
|
||||
class="timeline-link"
|
||||
>{{ stripMarkdownLength(item.commentContent, 100) }}</NuxtLink
|
||||
>
|
||||
被
|
||||
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||
item.fromUserName
|
||||
}}</NuxtLink>
|
||||
按赞,获得{{ item.amount }}积分
|
||||
</template>
|
||||
<template v-else-if="item.type === 'INVITE' && item.fromUserId">
|
||||
邀请了好友
|
||||
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||
item.fromUserName
|
||||
}}</NuxtLink
|
||||
>,加入社区,获得 {{ item.amount }} 积分
|
||||
</template>
|
||||
<template v-else-if="item.type === 'SYSTEM_ONLINE'">
|
||||
积分历史系统上线,你目前的积分是 {{ item.balance }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</BaseTimeline>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { authState, fetchCurrentUser, getToken } from '~/utils/auth'
|
||||
import { toast } from '~/main'
|
||||
import RedeemPopup from '~/components/RedeemPopup.vue'
|
||||
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||
import BasePlaceholder from '~/components/BasePlaceholder.vue'
|
||||
import { stripMarkdownLength } from '~/utils/markdown'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const API_BASE_URL = config.public.apiBaseUrl
|
||||
|
||||
const selectedTab = ref('mall')
|
||||
const point = ref(null)
|
||||
const isLoading = ref(false)
|
||||
const histories = ref([])
|
||||
const historyLoading = ref(false)
|
||||
const historyLoaded = ref(false)
|
||||
|
||||
const pointRules = [
|
||||
'发帖:每天前两次,每次 30 积分',
|
||||
@@ -82,6 +171,12 @@ onMounted(async () => {
|
||||
isLoading.value = false
|
||||
})
|
||||
|
||||
watch(selectedTab, (val) => {
|
||||
if (val === 'history' && !historyLoaded.value) {
|
||||
loadHistory()
|
||||
}
|
||||
})
|
||||
|
||||
const loadGoods = async () => {
|
||||
const res = await fetch(`${API_BASE_URL}/api/point-goods`)
|
||||
if (res.ok) {
|
||||
@@ -89,6 +184,23 @@ const loadGoods = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadHistory = async () => {
|
||||
if (!authState.loggedIn) {
|
||||
historyLoaded.value = true
|
||||
return
|
||||
}
|
||||
historyLoading.value = true
|
||||
const token = getToken()
|
||||
const res = await fetch(`${API_BASE_URL}/api/point-histories`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (res.ok) {
|
||||
histories.value = await res.json()
|
||||
}
|
||||
historyLoading.value = false
|
||||
historyLoaded.value = true
|
||||
}
|
||||
|
||||
const openRedeem = (good) => {
|
||||
if (!authState.loggedIn || point.value === null || point.value < good.cost) {
|
||||
toast.error('积分不足')
|
||||
@@ -135,6 +247,29 @@ const submitRedeem = async () => {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.point-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--normal-border-color);
|
||||
}
|
||||
|
||||
.point-tab-item {
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.point-tab-item.selected {
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.timeline-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.timeline-link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.loading-points-container {
|
||||
margin-top: 100px;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user