mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-12 10:00:58 +08:00
Compare commits
1 Commits
codex/fix-
...
codex/upda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
615832f112 |
9
.github/workflows/deploy-docs.yml
vendored
9
.github/workflows/deploy-docs.yml
vendored
@@ -1,11 +1,7 @@
|
|||||||
name: Deploy Documentation
|
name: Deploy Documentation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
push:
|
||||||
inputs:
|
|
||||||
build-id:
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -20,9 +16,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Log build
|
|
||||||
run: echo "Running documentation deployment from build ${{ inputs.build-id }}"
|
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
11
.github/workflows/deploy-staging.yml
vendored
11
.github/workflows/deploy-staging.yml
vendored
@@ -5,9 +5,6 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -24,11 +21,3 @@ jobs:
|
|||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.SSH_KEY }}
|
||||||
script: bash /opt/openisle/deploy-staging.sh
|
script: bash /opt/openisle/deploy-staging.sh
|
||||||
|
|
||||||
deploy-docs:
|
|
||||||
needs: build-and-deploy
|
|
||||||
if: ${{ success() }}
|
|
||||||
uses: ./.github/workflows/deploy-docs.yml
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
build-id: ${{ github.run_id }}
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ public class CachingConfig {
|
|||||||
public static final String CATEGORY_CACHE_NAME="openisle_categories";
|
public static final String CATEGORY_CACHE_NAME="openisle_categories";
|
||||||
// 在线人数缓存名
|
// 在线人数缓存名
|
||||||
public static final String ONLINE_CACHE_NAME="openisle_online";
|
public static final String ONLINE_CACHE_NAME="openisle_online";
|
||||||
// 注册验证码
|
|
||||||
public static final String VERIFY_CACHE_NAME="openisle_verify";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义Redis的序列化器
|
* 自定义Redis的序列化器
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.openisle.config;
|
|
||||||
|
|
||||||
import com.openisle.model.Role;
|
|
||||||
import com.openisle.model.User;
|
|
||||||
import com.openisle.repository.UserRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.boot.CommandLineRunner;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure a dedicated "system" user exists for internal operations.
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class SystemUserInitializer implements CommandLineRunner {
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run(String... args) {
|
|
||||||
userRepository.findByUsername("system").orElseGet(() -> {
|
|
||||||
User system = new User();
|
|
||||||
system.setUsername("system");
|
|
||||||
system.setEmail("system@openisle.local");
|
|
||||||
// todo(tim): raw password 采用环境变量
|
|
||||||
system.setPassword(passwordEncoder.encode("system"));
|
|
||||||
system.setRole(Role.USER);
|
|
||||||
system.setVerified(true);
|
|
||||||
system.setApproved(true);
|
|
||||||
system.setAvatar("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png");
|
|
||||||
return userRepository.save(system);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -37,22 +37,22 @@ public class AdminPostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/pin")
|
@PostMapping("/{id}/pin")
|
||||||
public PostSummaryDto pin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
public PostSummaryDto pin(@PathVariable Long id) {
|
||||||
return postMapper.toSummaryDto(postService.pinPost(id, auth.getName()));
|
return postMapper.toSummaryDto(postService.pinPost(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/unpin")
|
@PostMapping("/{id}/unpin")
|
||||||
public PostSummaryDto unpin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
public PostSummaryDto unpin(@PathVariable Long id) {
|
||||||
return postMapper.toSummaryDto(postService.unpinPost(id, auth.getName()));
|
return postMapper.toSummaryDto(postService.unpinPost(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/rss-exclude")
|
@PostMapping("/{id}/rss-exclude")
|
||||||
public PostSummaryDto excludeFromRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
public PostSummaryDto excludeFromRss(@PathVariable Long id) {
|
||||||
return postMapper.toSummaryDto(postService.excludeFromRss(id, auth.getName()));
|
return postMapper.toSummaryDto(postService.excludeFromRss(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/rss-include")
|
@PostMapping("/{id}/rss-include")
|
||||||
public PostSummaryDto includeInRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
|
public PostSummaryDto includeInRss(@PathVariable Long id) {
|
||||||
return postMapper.toSummaryDto(postService.includeInRss(id, auth.getName()));
|
return postMapper.toSummaryDto(postService.includeInRss(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
package com.openisle.controller;
|
package com.openisle.controller;
|
||||||
|
|
||||||
import com.openisle.config.CachingConfig;
|
|
||||||
import com.openisle.dto.*;
|
import com.openisle.dto.*;
|
||||||
import com.openisle.exception.FieldException;
|
import com.openisle.exception.FieldException;
|
||||||
import com.openisle.model.RegisterMode;
|
import com.openisle.model.RegisterMode;
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import com.openisle.service.*;
|
import com.openisle.service.*;
|
||||||
import com.openisle.util.VerifyType;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/auth")
|
@RequestMapping("/api/auth")
|
||||||
@@ -60,8 +56,7 @@ public class AuthController {
|
|||||||
User user = userService.registerWithInvite(
|
User user = userService.registerWithInvite(
|
||||||
req.getUsername(), req.getEmail(), req.getPassword());
|
req.getUsername(), req.getEmail(), req.getPassword());
|
||||||
inviteService.consume(req.getInviteToken(), user.getUsername());
|
inviteService.consume(req.getInviteToken(), user.getUsername());
|
||||||
// 发送确认邮件
|
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||||
userService.sendVerifyMail(user, VerifyType.REGISTER);
|
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(Map.of(
|
||||||
"token", jwtService.generateToken(user.getUsername()),
|
"token", jwtService.generateToken(user.getUsername()),
|
||||||
"reason_code", "INVITE_APPROVED"
|
"reason_code", "INVITE_APPROVED"
|
||||||
@@ -75,8 +70,7 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
User user = userService.register(
|
User user = userService.register(
|
||||||
req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode());
|
req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode());
|
||||||
// 发送确认邮件
|
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||||
userService.sendVerifyMail(user, VerifyType.REGISTER);
|
|
||||||
if (!user.isApproved()) {
|
if (!user.isApproved()) {
|
||||||
notificationService.createRegisterRequestNotifications(user, user.getRegisterReason());
|
notificationService.createRegisterRequestNotifications(user, user.getRegisterReason());
|
||||||
}
|
}
|
||||||
@@ -85,12 +79,13 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/verify")
|
@PostMapping("/verify")
|
||||||
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
|
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
|
||||||
Optional<User> userOpt = userService.findByUsername(req.getUsername());
|
boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
|
||||||
if (userOpt.isEmpty()) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
|
|
||||||
}
|
|
||||||
boolean ok = userService.verifyCode(userOpt.get(), req.getCode(), VerifyType.REGISTER);
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
Optional<User> userOpt = userService.findByUsername(req.getUsername());
|
||||||
|
if (userOpt.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
|
||||||
|
}
|
||||||
|
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
if (user.isApproved()) {
|
if (user.isApproved()) {
|
||||||
@@ -127,7 +122,7 @@ public class AuthController {
|
|||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
if (!user.isVerified()) {
|
if (!user.isVerified()) {
|
||||||
user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode());
|
user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode());
|
||||||
userService.sendVerifyMail(user, VerifyType.REGISTER);
|
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "User not verified",
|
"error", "User not verified",
|
||||||
"reason_code", "NOT_VERIFIED",
|
"reason_code", "NOT_VERIFIED",
|
||||||
@@ -422,17 +417,14 @@ public class AuthController {
|
|||||||
if (userOpt.isEmpty()) {
|
if (userOpt.isEmpty()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
|
return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
|
||||||
}
|
}
|
||||||
userService.sendVerifyMail(userOpt.get(), VerifyType.RESET_PASSWORD);
|
String code = userService.generatePasswordResetCode(req.getEmail());
|
||||||
|
emailService.sendEmail(req.getEmail(), "请填写验证码以重置密码", "您的验证码是" + code);
|
||||||
return ResponseEntity.ok(Map.of("message", "Verification code sent"));
|
return ResponseEntity.ok(Map.of("message", "Verification code sent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/forgot/verify")
|
@PostMapping("/forgot/verify")
|
||||||
public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) {
|
public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) {
|
||||||
Optional<User> userOpt = userService.findByEmail(req.getEmail());
|
boolean ok = userService.verifyPasswordResetCode(req.getEmail(), req.getCode());
|
||||||
if (userOpt.isEmpty()) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
|
|
||||||
}
|
|
||||||
boolean ok = userService.verifyCode(userOpt.get(), req.getCode(), VerifyType.RESET_PASSWORD);
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
String username = userService.findByEmail(req.getEmail()).get().getUsername();
|
String username = userService.findByEmail(req.getEmail()).get().getUsername();
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username)));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username)));
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.openisle.controller;
|
|
||||||
|
|
||||||
import com.openisle.dto.PostChangeLogDto;
|
|
||||||
import com.openisle.mapper.PostChangeLogMapper;
|
|
||||||
import com.openisle.service.PostChangeLogService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/posts")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class PostChangeLogController {
|
|
||||||
private final PostChangeLogService changeLogService;
|
|
||||||
private final PostChangeLogMapper mapper;
|
|
||||||
|
|
||||||
@GetMapping("/{id}/change-logs")
|
|
||||||
public List<PostChangeLogDto> listLogs(@PathVariable Long id) {
|
|
||||||
return changeLogService.listLogs(id).stream()
|
|
||||||
.map(mapper::toDto)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.openisle.dto;
|
|
||||||
|
|
||||||
import com.openisle.model.PostChangeType;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
public class PostChangeLogDto {
|
|
||||||
private Long id;
|
|
||||||
private String username;
|
|
||||||
private String userAvatar;
|
|
||||||
private PostChangeType type;
|
|
||||||
private LocalDateTime time;
|
|
||||||
private String oldTitle;
|
|
||||||
private String newTitle;
|
|
||||||
private String oldContent;
|
|
||||||
private String newContent;
|
|
||||||
private CategoryDto oldCategory;
|
|
||||||
private CategoryDto newCategory;
|
|
||||||
private List<TagDto> oldTags;
|
|
||||||
private List<TagDto> newTags;
|
|
||||||
private Boolean oldClosed;
|
|
||||||
private Boolean newClosed;
|
|
||||||
private LocalDateTime oldPinnedAt;
|
|
||||||
private LocalDateTime newPinnedAt;
|
|
||||||
private Boolean oldFeatured;
|
|
||||||
private Boolean newFeatured;
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package com.openisle.mapper;
|
|
||||||
|
|
||||||
import com.openisle.dto.CategoryDto;
|
|
||||||
import com.openisle.dto.PostChangeLogDto;
|
|
||||||
import com.openisle.dto.TagDto;
|
|
||||||
import com.openisle.model.*;
|
|
||||||
import com.openisle.repository.CategoryRepository;
|
|
||||||
import com.openisle.repository.TagRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class PostChangeLogMapper {
|
|
||||||
|
|
||||||
private final CategoryRepository categoryRepository;
|
|
||||||
private final TagRepository tagRepository;
|
|
||||||
private final CategoryMapper categoryMapper;
|
|
||||||
private final TagMapper tagMapper;
|
|
||||||
|
|
||||||
public PostChangeLogDto toDto(PostChangeLog log) {
|
|
||||||
PostChangeLogDto dto = new PostChangeLogDto();
|
|
||||||
dto.setId(log.getId());
|
|
||||||
if (log.getUser() != null) {
|
|
||||||
dto.setUsername(log.getUser().getUsername());
|
|
||||||
dto.setUserAvatar(log.getUser().getAvatar());
|
|
||||||
}
|
|
||||||
dto.setType(log.getType());
|
|
||||||
dto.setTime(log.getCreatedAt());
|
|
||||||
if (log instanceof PostTitleChangeLog t) {
|
|
||||||
dto.setOldTitle(t.getOldTitle());
|
|
||||||
dto.setNewTitle(t.getNewTitle());
|
|
||||||
} else if (log instanceof PostContentChangeLog c) {
|
|
||||||
dto.setOldContent(c.getOldContent());
|
|
||||||
dto.setNewContent(c.getNewContent());
|
|
||||||
} else if (log instanceof PostCategoryChangeLog cat) {
|
|
||||||
dto.setOldCategory(mapCategory(cat.getOldCategory()));
|
|
||||||
dto.setNewCategory(mapCategory(cat.getNewCategory()));
|
|
||||||
} else if (log instanceof PostTagChangeLog tag) {
|
|
||||||
dto.setOldTags(mapTags(tag.getOldTags()));
|
|
||||||
dto.setNewTags(mapTags(tag.getNewTags()));
|
|
||||||
} else if (log instanceof PostClosedChangeLog cl) {
|
|
||||||
dto.setOldClosed(cl.isOldClosed());
|
|
||||||
dto.setNewClosed(cl.isNewClosed());
|
|
||||||
} else if (log instanceof PostPinnedChangeLog p) {
|
|
||||||
dto.setOldPinnedAt(p.getOldPinnedAt());
|
|
||||||
dto.setNewPinnedAt(p.getNewPinnedAt());
|
|
||||||
} else if (log instanceof PostFeaturedChangeLog f) {
|
|
||||||
dto.setOldFeatured(f.isOldFeatured());
|
|
||||||
dto.setNewFeatured(f.isNewFeatured());
|
|
||||||
}
|
|
||||||
return dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CategoryDto mapCategory(String name) {
|
|
||||||
if (name == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return categoryRepository.findByName(name)
|
|
||||||
.map(categoryMapper::toDto)
|
|
||||||
.orElseGet(() -> {
|
|
||||||
CategoryDto dto = new CategoryDto();
|
|
||||||
dto.setName(name);
|
|
||||||
return dto;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<TagDto> mapTags(String tags) {
|
|
||||||
if (tags == null || tags.isBlank()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
return Arrays.stream(tags.split(","))
|
|
||||||
.map(String::trim)
|
|
||||||
.map(this::mapTag)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private TagDto mapTag(String name) {
|
|
||||||
return tagRepository.findByName(name)
|
|
||||||
.map(tagMapper::toDto)
|
|
||||||
.orElseGet(() -> {
|
|
||||||
TagDto dto = new TagDto();
|
|
||||||
dto.setName(name);
|
|
||||||
return dto;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Entity
|
|
||||||
@Table(name = "post_category_change_logs")
|
|
||||||
public class PostCategoryChangeLog extends PostChangeLog {
|
|
||||||
private String oldCategory;
|
|
||||||
private String newCategory;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Entity
|
|
||||||
@Table(name = "post_change_logs")
|
|
||||||
@Inheritance(strategy = InheritanceType.JOINED)
|
|
||||||
public abstract class PostChangeLog {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
|
||||||
@JoinColumn(name = "post_id")
|
|
||||||
private Post post;
|
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = true)
|
|
||||||
@JoinColumn(name = "user_id")
|
|
||||||
private User user;
|
|
||||||
|
|
||||||
@CreationTimestamp
|
|
||||||
@Column(nullable = false, updatable = false)
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
@Column(nullable = false)
|
|
||||||
private PostChangeType type;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
public enum PostChangeType {
|
|
||||||
CONTENT,
|
|
||||||
TITLE,
|
|
||||||
CATEGORY,
|
|
||||||
TAG,
|
|
||||||
CLOSED,
|
|
||||||
PINNED,
|
|
||||||
FEATURED,
|
|
||||||
VOTE_RESULT,
|
|
||||||
LOTTERY_RESULT
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Entity
|
|
||||||
@Table(name = "post_closed_change_logs")
|
|
||||||
public class PostClosedChangeLog extends PostChangeLog {
|
|
||||||
private boolean oldClosed;
|
|
||||||
private boolean newClosed;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Entity
|
|
||||||
@Table(name = "post_content_change_logs")
|
|
||||||
public class PostContentChangeLog extends PostChangeLog {
|
|
||||||
@Column(name = "old_content", columnDefinition = "LONGTEXT")
|
|
||||||
private String oldContent;
|
|
||||||
|
|
||||||
@Column(name = "new_content", columnDefinition = "LONGTEXT")
|
|
||||||
private String newContent;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Entity
|
|
||||||
@Table(name = "post_featured_change_logs")
|
|
||||||
public class PostFeaturedChangeLog extends PostChangeLog {
|
|
||||||
private boolean oldFeatured;
|
|
||||||
private boolean newFeatured;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Entity
|
|
||||||
@Table(name = "post_lottery_result_change_logs")
|
|
||||||
public class PostLotteryResultChangeLog extends PostChangeLog {
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Entity
|
|
||||||
@Table(name = "post_pinned_change_logs")
|
|
||||||
public class PostPinnedChangeLog extends PostChangeLog {
|
|
||||||
private LocalDateTime oldPinnedAt;
|
|
||||||
private LocalDateTime newPinnedAt;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Entity
|
|
||||||
@Table(name = "post_tag_change_logs")
|
|
||||||
public class PostTagChangeLog extends PostChangeLog {
|
|
||||||
@Column(name = "old_tags")
|
|
||||||
private String oldTags;
|
|
||||||
|
|
||||||
@Column(name = "new_tags")
|
|
||||||
private String newTags;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Entity
|
|
||||||
@Table(name = "post_title_change_logs")
|
|
||||||
public class PostTitleChangeLog extends PostChangeLog {
|
|
||||||
private String oldTitle;
|
|
||||||
private String newTitle;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Entity
|
|
||||||
@Table(name = "post_vote_result_change_logs")
|
|
||||||
public class PostVoteResultChangeLog extends PostChangeLog {
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -4,10 +4,7 @@ import com.openisle.model.Category;
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface CategoryRepository extends JpaRepository<Category, Long> {
|
public interface CategoryRepository extends JpaRepository<Category, Long> {
|
||||||
List<Category> findByNameContainingIgnoreCase(String keyword);
|
List<Category> findByNameContainingIgnoreCase(String keyword);
|
||||||
|
|
||||||
Optional<Category> findByName(String name);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.openisle.repository;
|
|
||||||
|
|
||||||
import com.openisle.model.Post;
|
|
||||||
import com.openisle.model.PostChangeLog;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface PostChangeLogRepository extends JpaRepository<PostChangeLog, Long> {
|
|
||||||
List<PostChangeLog> findByPostOrderByCreatedAtAsc(Post post);
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface TagRepository extends JpaRepository<Tag, Long> {
|
public interface TagRepository extends JpaRepository<Tag, Long> {
|
||||||
List<Tag> findByNameContainingIgnoreCase(String keyword);
|
List<Tag> findByNameContainingIgnoreCase(String keyword);
|
||||||
@@ -16,6 +15,4 @@ public interface TagRepository extends JpaRepository<Tag, Long> {
|
|||||||
|
|
||||||
List<Tag> findByCreatorOrderByCreatedAtDesc(User creator, Pageable pageable);
|
List<Tag> findByCreatorOrderByCreatedAtDesc(User creator, Pageable pageable);
|
||||||
List<Tag> findByCreator(User creator);
|
List<Tag> findByCreator(User creator);
|
||||||
|
|
||||||
Optional<Tag> findByName(String name);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
package com.openisle.service;
|
|
||||||
|
|
||||||
import com.openisle.model.*;
|
|
||||||
import com.openisle.repository.PostChangeLogRepository;
|
|
||||||
import com.openisle.repository.PostRepository;
|
|
||||||
import com.openisle.repository.UserRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class PostChangeLogService {
|
|
||||||
private final PostChangeLogRepository logRepository;
|
|
||||||
private final PostRepository postRepository;
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
|
|
||||||
private User getSystemUser() {
|
|
||||||
return userRepository.findByUsername("system")
|
|
||||||
.orElseThrow(() -> new IllegalStateException("System user not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordContentChange(Post post, User user, String oldContent, String newContent) {
|
|
||||||
PostContentChangeLog log = new PostContentChangeLog();
|
|
||||||
log.setPost(post);
|
|
||||||
log.setUser(user);
|
|
||||||
log.setType(PostChangeType.CONTENT);
|
|
||||||
log.setOldContent(oldContent);
|
|
||||||
log.setNewContent(newContent);
|
|
||||||
logRepository.save(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordTitleChange(Post post, User user, String oldTitle, String newTitle) {
|
|
||||||
PostTitleChangeLog log = new PostTitleChangeLog();
|
|
||||||
log.setPost(post);
|
|
||||||
log.setUser(user);
|
|
||||||
log.setType(PostChangeType.TITLE);
|
|
||||||
log.setOldTitle(oldTitle);
|
|
||||||
log.setNewTitle(newTitle);
|
|
||||||
logRepository.save(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordCategoryChange(Post post, User user, String oldCategory, String newCategory) {
|
|
||||||
PostCategoryChangeLog log = new PostCategoryChangeLog();
|
|
||||||
log.setPost(post);
|
|
||||||
log.setUser(user);
|
|
||||||
log.setType(PostChangeType.CATEGORY);
|
|
||||||
log.setOldCategory(oldCategory);
|
|
||||||
log.setNewCategory(newCategory);
|
|
||||||
logRepository.save(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordTagChange(Post post, User user, Set<Tag> oldTags, Set<Tag> newTags) {
|
|
||||||
PostTagChangeLog log = new PostTagChangeLog();
|
|
||||||
log.setPost(post);
|
|
||||||
log.setUser(user);
|
|
||||||
log.setType(PostChangeType.TAG);
|
|
||||||
log.setOldTags(oldTags.stream().map(Tag::getName).collect(Collectors.joining(",")));
|
|
||||||
log.setNewTags(newTags.stream().map(Tag::getName).collect(Collectors.joining(",")));
|
|
||||||
logRepository.save(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordClosedChange(Post post, User user, boolean oldClosed, boolean newClosed) {
|
|
||||||
PostClosedChangeLog log = new PostClosedChangeLog();
|
|
||||||
log.setPost(post);
|
|
||||||
log.setUser(user);
|
|
||||||
log.setType(PostChangeType.CLOSED);
|
|
||||||
log.setOldClosed(oldClosed);
|
|
||||||
log.setNewClosed(newClosed);
|
|
||||||
logRepository.save(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordPinnedChange(Post post, User user, java.time.LocalDateTime oldPinnedAt, java.time.LocalDateTime newPinnedAt) {
|
|
||||||
PostPinnedChangeLog log = new PostPinnedChangeLog();
|
|
||||||
log.setPost(post);
|
|
||||||
log.setUser(user);
|
|
||||||
log.setType(PostChangeType.PINNED);
|
|
||||||
log.setOldPinnedAt(oldPinnedAt);
|
|
||||||
log.setNewPinnedAt(newPinnedAt);
|
|
||||||
logRepository.save(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordFeaturedChange(Post post, User user, boolean oldFeatured, boolean newFeatured) {
|
|
||||||
PostFeaturedChangeLog log = new PostFeaturedChangeLog();
|
|
||||||
log.setPost(post);
|
|
||||||
log.setUser(user);
|
|
||||||
log.setType(PostChangeType.FEATURED);
|
|
||||||
log.setOldFeatured(oldFeatured);
|
|
||||||
log.setNewFeatured(newFeatured);
|
|
||||||
logRepository.save(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordVoteResult(Post post) {
|
|
||||||
PostVoteResultChangeLog log = new PostVoteResultChangeLog();
|
|
||||||
log.setPost(post);
|
|
||||||
log.setUser(getSystemUser());
|
|
||||||
log.setType(PostChangeType.VOTE_RESULT);
|
|
||||||
logRepository.save(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void recordLotteryResult(Post post) {
|
|
||||||
PostLotteryResultChangeLog log = new PostLotteryResultChangeLog();
|
|
||||||
log.setPost(post);
|
|
||||||
log.setUser(getSystemUser());
|
|
||||||
log.setType(PostChangeType.LOTTERY_RESULT);
|
|
||||||
logRepository.save(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<PostChangeLog> listLogs(Long postId) {
|
|
||||||
Post post = postRepository.findById(postId)
|
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
|
||||||
return logRepository.findByPostOrderByCreatedAtAsc(post);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,6 @@ import com.openisle.repository.CategoryRepository;
|
|||||||
import com.openisle.repository.TagRepository;
|
import com.openisle.repository.TagRepository;
|
||||||
import com.openisle.service.SubscriptionService;
|
import com.openisle.service.SubscriptionService;
|
||||||
import com.openisle.service.CommentService;
|
import com.openisle.service.CommentService;
|
||||||
import com.openisle.service.PostChangeLogService;
|
|
||||||
import com.openisle.repository.CommentRepository;
|
import com.openisle.repository.CommentRepository;
|
||||||
import com.openisle.repository.ReactionRepository;
|
import com.openisle.repository.ReactionRepository;
|
||||||
import com.openisle.repository.PostSubscriptionRepository;
|
import com.openisle.repository.PostSubscriptionRepository;
|
||||||
@@ -75,7 +74,6 @@ public class PostService {
|
|||||||
private final EmailSender emailSender;
|
private final EmailSender emailSender;
|
||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
private final PointService pointService;
|
private final PointService pointService;
|
||||||
private final PostChangeLogService postChangeLogService;
|
|
||||||
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
|
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
|
||||||
@Value("${app.website-url:https://www.open-isle.com}")
|
@Value("${app.website-url:https://www.open-isle.com}")
|
||||||
private String websiteUrl;
|
private String websiteUrl;
|
||||||
@@ -101,7 +99,6 @@ public class PostService {
|
|||||||
EmailSender emailSender,
|
EmailSender emailSender,
|
||||||
ApplicationContext applicationContext,
|
ApplicationContext applicationContext,
|
||||||
PointService pointService,
|
PointService pointService,
|
||||||
PostChangeLogService postChangeLogService,
|
|
||||||
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
|
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
|
||||||
this.postRepository = postRepository;
|
this.postRepository = postRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
@@ -123,7 +120,6 @@ public class PostService {
|
|||||||
this.emailSender = emailSender;
|
this.emailSender = emailSender;
|
||||||
this.applicationContext = applicationContext;
|
this.applicationContext = applicationContext;
|
||||||
this.pointService = pointService;
|
this.pointService = pointService;
|
||||||
this.postChangeLogService = postChangeLogService;
|
|
||||||
this.publishMode = publishMode;
|
this.publishMode = publishMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,28 +159,19 @@ public class PostService {
|
|||||||
return postRepository.findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus.PUBLISHED, pageable);
|
return postRepository.findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus.PUBLISHED, pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Post excludeFromRss(Long id, String username) {
|
public Post excludeFromRss(Long id) {
|
||||||
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||||
User user = userRepository.findByUsername(username)
|
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
||||||
boolean oldFeatured = !Boolean.TRUE.equals(post.getRssExcluded());
|
|
||||||
post.setRssExcluded(true);
|
post.setRssExcluded(true);
|
||||||
Post saved = postRepository.save(post);
|
return postRepository.save(post);
|
||||||
postChangeLogService.recordFeaturedChange(saved, user, oldFeatured, false);
|
|
||||||
return saved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Post includeInRss(Long id, String username) {
|
public Post includeInRss(Long id) {
|
||||||
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||||
User user = userRepository.findByUsername(username)
|
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
||||||
boolean oldFeatured = !Boolean.TRUE.equals(post.getRssExcluded());
|
|
||||||
post.setRssExcluded(false);
|
post.setRssExcluded(false);
|
||||||
Post saved = postRepository.save(post);
|
post = postRepository.save(post);
|
||||||
postChangeLogService.recordFeaturedChange(saved, user, oldFeatured, true);
|
notificationService.createNotification(post.getAuthor(), NotificationType.POST_FEATURED, post, null, null, null, null, null);
|
||||||
notificationService.createNotification(saved.getAuthor(), NotificationType.POST_FEATURED, saved, null, null, null, null, null);
|
pointService.awardForFeatured(post.getAuthor().getUsername(), post.getId());
|
||||||
pointService.awardForFeatured(saved.getAuthor().getUsername(), saved.getId());
|
return post;
|
||||||
return saved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Post createPost(String username,
|
public Post createPost(String username,
|
||||||
@@ -368,7 +355,6 @@ public class PostService {
|
|||||||
for (User participant : pp.getParticipants()) {
|
for (User participant : pp.getParticipants()) {
|
||||||
notificationService.createNotification(participant, NotificationType.POLL_RESULT_PARTICIPANT, pp, null, null, null, null, null);
|
notificationService.createNotification(participant, NotificationType.POLL_RESULT_PARTICIPANT, pp, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
postChangeLogService.recordVoteResult(pp);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +389,6 @@ public class PostService {
|
|||||||
notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null);
|
notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null);
|
||||||
notificationService.sendCustomPush(lp.getAuthor(), "抽奖已开奖", String.format("%s/posts/%d", websiteUrl, lp.getId()));
|
notificationService.sendCustomPush(lp.getAuthor(), "抽奖已开奖", String.format("%s/posts/%d", websiteUrl, lp.getId()));
|
||||||
}
|
}
|
||||||
postChangeLogService.recordLotteryResult(lp);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,28 +638,18 @@ public class PostService {
|
|||||||
return post;
|
return post;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Post pinPost(Long id, String username) {
|
public Post pinPost(Long id) {
|
||||||
Post post = postRepository.findById(id)
|
Post post = postRepository.findById(id)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||||
User user = userRepository.findByUsername(username)
|
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
||||||
java.time.LocalDateTime oldPinned = post.getPinnedAt();
|
|
||||||
post.setPinnedAt(java.time.LocalDateTime.now());
|
post.setPinnedAt(java.time.LocalDateTime.now());
|
||||||
Post saved = postRepository.save(post);
|
return postRepository.save(post);
|
||||||
postChangeLogService.recordPinnedChange(saved, user, oldPinned, saved.getPinnedAt());
|
|
||||||
return saved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Post unpinPost(Long id, String username) {
|
public Post unpinPost(Long id) {
|
||||||
Post post = postRepository.findById(id)
|
Post post = postRepository.findById(id)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||||
User user = userRepository.findByUsername(username)
|
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
|
||||||
java.time.LocalDateTime oldPinned = post.getPinnedAt();
|
|
||||||
post.setPinnedAt(null);
|
post.setPinnedAt(null);
|
||||||
Post saved = postRepository.save(post);
|
return postRepository.save(post);
|
||||||
postChangeLogService.recordPinnedChange(saved, user, oldPinned, null);
|
|
||||||
return saved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Post closePost(Long id, String username) {
|
public Post closePost(Long id, String username) {
|
||||||
@@ -685,11 +660,8 @@ public class PostService {
|
|||||||
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||||
throw new IllegalArgumentException("Unauthorized");
|
throw new IllegalArgumentException("Unauthorized");
|
||||||
}
|
}
|
||||||
boolean oldClosed = post.isClosed();
|
|
||||||
post.setClosed(true);
|
post.setClosed(true);
|
||||||
Post saved = postRepository.save(post);
|
return postRepository.save(post);
|
||||||
postChangeLogService.recordClosedChange(saved, user, oldClosed, true);
|
|
||||||
return saved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Post reopenPost(Long id, String username) {
|
public Post reopenPost(Long id, String username) {
|
||||||
@@ -700,11 +672,8 @@ public class PostService {
|
|||||||
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
|
||||||
throw new IllegalArgumentException("Unauthorized");
|
throw new IllegalArgumentException("Unauthorized");
|
||||||
}
|
}
|
||||||
boolean oldClosed = post.isClosed();
|
|
||||||
post.setClosed(false);
|
post.setClosed(false);
|
||||||
Post saved = postRepository.save(post);
|
return postRepository.save(post);
|
||||||
postChangeLogService.recordClosedChange(saved, user, oldClosed, false);
|
|
||||||
return saved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@org.springframework.transaction.annotation.Transactional
|
@org.springframework.transaction.annotation.Transactional
|
||||||
@@ -733,30 +702,14 @@ public class PostService {
|
|||||||
if (tags.isEmpty()) {
|
if (tags.isEmpty()) {
|
||||||
throw new IllegalArgumentException("Tag not found");
|
throw new IllegalArgumentException("Tag not found");
|
||||||
}
|
}
|
||||||
String oldTitle = post.getTitle();
|
|
||||||
String oldContent = post.getContent();
|
|
||||||
Category oldCategory = post.getCategory();
|
|
||||||
java.util.Set<com.openisle.model.Tag> oldTags = new java.util.HashSet<>(post.getTags());
|
|
||||||
post.setTitle(title);
|
post.setTitle(title);
|
||||||
|
String oldContent = post.getContent();
|
||||||
post.setContent(content);
|
post.setContent(content);
|
||||||
post.setCategory(category);
|
post.setCategory(category);
|
||||||
post.setTags(new java.util.HashSet<>(tags));
|
post.setTags(new java.util.HashSet<>(tags));
|
||||||
Post updated = postRepository.save(post);
|
Post updated = postRepository.save(post);
|
||||||
imageUploader.adjustReferences(oldContent, content);
|
imageUploader.adjustReferences(oldContent, content);
|
||||||
notificationService.notifyMentions(content, user, updated, null);
|
notificationService.notifyMentions(content, user, updated, null);
|
||||||
if (!java.util.Objects.equals(oldTitle, title)) {
|
|
||||||
postChangeLogService.recordTitleChange(updated, user, oldTitle, title);
|
|
||||||
}
|
|
||||||
if (!java.util.Objects.equals(oldContent, content)) {
|
|
||||||
postChangeLogService.recordContentChange(updated, user, oldContent, content);
|
|
||||||
}
|
|
||||||
if (!java.util.Objects.equals(oldCategory.getId(), category.getId())) {
|
|
||||||
postChangeLogService.recordCategoryChange(updated, user, oldCategory.getName(), category.getName());
|
|
||||||
}
|
|
||||||
java.util.Set<com.openisle.model.Tag> newTags = new java.util.HashSet<>(tags);
|
|
||||||
if (!oldTags.equals(newTags)) {
|
|
||||||
postChangeLogService.recordTagChange(updated, user, oldTags, newTags);
|
|
||||||
}
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.openisle.service;
|
package com.openisle.service;
|
||||||
|
|
||||||
import com.openisle.config.CachingConfig;
|
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import com.openisle.model.Role;
|
import com.openisle.model.Role;
|
||||||
import com.openisle.service.PasswordValidator;
|
import com.openisle.service.PasswordValidator;
|
||||||
@@ -8,18 +7,13 @@ import com.openisle.service.UsernameValidator;
|
|||||||
import com.openisle.service.AvatarGenerator;
|
import com.openisle.service.AvatarGenerator;
|
||||||
import com.openisle.exception.FieldException;
|
import com.openisle.exception.FieldException;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import com.openisle.util.VerifyType;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -31,10 +25,6 @@ public class UserService {
|
|||||||
private final ImageUploader imageUploader;
|
private final ImageUploader imageUploader;
|
||||||
private final AvatarGenerator avatarGenerator;
|
private final AvatarGenerator avatarGenerator;
|
||||||
|
|
||||||
private final RedisTemplate redisTemplate;
|
|
||||||
|
|
||||||
private final EmailSender emailService;
|
|
||||||
|
|
||||||
public User register(String username, String email, String password, String reason, com.openisle.model.RegisterMode mode) {
|
public User register(String username, String email, String password, String reason, com.openisle.model.RegisterMode mode) {
|
||||||
usernameValidator.validate(username);
|
usernameValidator.validate(username);
|
||||||
passwordValidator.validate(password);
|
passwordValidator.validate(password);
|
||||||
@@ -48,7 +38,7 @@ public class UserService {
|
|||||||
// 未验证 → 允许“重注册”:覆盖必要字段并重新发验证码
|
// 未验证 → 允许“重注册”:覆盖必要字段并重新发验证码
|
||||||
u.setEmail(email); // 若不允许改邮箱可去掉
|
u.setEmail(email); // 若不允许改邮箱可去掉
|
||||||
u.setPassword(passwordEncoder.encode(password));
|
u.setPassword(passwordEncoder.encode(password));
|
||||||
// u.setVerificationCode(genCode());
|
u.setVerificationCode(genCode());
|
||||||
u.setRegisterReason(reason);
|
u.setRegisterReason(reason);
|
||||||
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||||
return userRepository.save(u);
|
return userRepository.save(u);
|
||||||
@@ -64,7 +54,7 @@ public class UserService {
|
|||||||
// 未验证 → 允许“重注册”
|
// 未验证 → 允许“重注册”
|
||||||
u.setUsername(username); // 若不允许改用户名可去掉
|
u.setUsername(username); // 若不允许改用户名可去掉
|
||||||
u.setPassword(passwordEncoder.encode(password));
|
u.setPassword(passwordEncoder.encode(password));
|
||||||
// u.setVerificationCode(genCode());
|
u.setVerificationCode(genCode());
|
||||||
u.setRegisterReason(reason);
|
u.setRegisterReason(reason);
|
||||||
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||||
return userRepository.save(u);
|
return userRepository.save(u);
|
||||||
@@ -77,7 +67,7 @@ public class UserService {
|
|||||||
user.setPassword(passwordEncoder.encode(password));
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
user.setRole(Role.USER);
|
user.setRole(Role.USER);
|
||||||
user.setVerified(false);
|
user.setVerified(false);
|
||||||
// user.setVerificationCode(genCode());
|
user.setVerificationCode(genCode());
|
||||||
user.setAvatar(avatarGenerator.generate(username));
|
user.setAvatar(avatarGenerator.generate(username));
|
||||||
user.setRegisterReason(reason);
|
user.setRegisterReason(reason);
|
||||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||||
@@ -87,7 +77,7 @@ public class UserService {
|
|||||||
public User registerWithInvite(String username, String email, String password) {
|
public User registerWithInvite(String username, String email, String password) {
|
||||||
User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT);
|
User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT);
|
||||||
user.setVerified(true);
|
user.setVerified(true);
|
||||||
// user.setVerificationCode(genCode());
|
user.setVerificationCode(genCode());
|
||||||
return userRepository.save(user);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,58 +85,16 @@ public class UserService {
|
|||||||
return String.format("%06d", new Random().nextInt(1000000));
|
return String.format("%06d", new Random().nextInt(1000000));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public boolean verifyCode(String username, String code) {
|
||||||
* 将验证码存入缓存,并发送邮件
|
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||||
* @param user
|
if (userOpt.isPresent() && code.equals(userOpt.get().getVerificationCode())) {
|
||||||
*/
|
User user = userOpt.get();
|
||||||
public void sendVerifyMail(User user, VerifyType verifyType){
|
|
||||||
//缓存验证码
|
|
||||||
String code = genCode();
|
|
||||||
String key;
|
|
||||||
String subject;
|
|
||||||
String content = "您的验证码是:" + code;
|
|
||||||
// 注册类型
|
|
||||||
if(verifyType.equals(VerifyType.REGISTER)){
|
|
||||||
key = CachingConfig.VERIFY_CACHE_NAME + ":register:code:" + user.getUsername();
|
|
||||||
subject = "在网站填写验证码以验证(有效期为5分钟)";
|
|
||||||
}else {
|
|
||||||
// 重置密码
|
|
||||||
key = CachingConfig.VERIFY_CACHE_NAME + ":reset_password:code:" + user.getUsername();
|
|
||||||
subject = "请填写验证码以重置密码(有效期为5分钟)";
|
|
||||||
}
|
|
||||||
|
|
||||||
redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);// 五分钟后验证码过期
|
|
||||||
emailService.sendEmail(user.getEmail(), subject, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证code是否正确
|
|
||||||
* @param user
|
|
||||||
* @param code
|
|
||||||
* @param verifyType
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public boolean verifyCode(User user, String code, VerifyType verifyType) {
|
|
||||||
// 生成key
|
|
||||||
String key1 = VerifyType.REGISTER.equals(verifyType)?":register:code:":":reset_password:code:";
|
|
||||||
String key = CachingConfig.VERIFY_CACHE_NAME + key1 + user.getUsername();
|
|
||||||
// 这里不能使用getAndDelete,需要6.x版本
|
|
||||||
String cachedCode = (String)redisTemplate.opsForValue().get(key);
|
|
||||||
// 如果校验code过期或者不存在
|
|
||||||
// 或者校验code不一致
|
|
||||||
if(Objects.isNull(cachedCode)
|
|
||||||
|| !cachedCode.equals(code)){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 注册模式需要设置已经确认
|
|
||||||
if(VerifyType.REGISTER.equals(verifyType)){
|
|
||||||
user.setVerified(true);
|
user.setVerified(true);
|
||||||
|
user.setVerificationCode(null);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
// 走到这里说明验证成功删除验证码
|
return false;
|
||||||
redisTemplate.delete(key);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<User> authenticate(String username, String password) {
|
public Optional<User> authenticate(String username, String password) {
|
||||||
@@ -217,6 +165,26 @@ public class UserService {
|
|||||||
return userRepository.save(user);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String generatePasswordResetCode(String email) {
|
||||||
|
User user = userRepository.findByEmail(email)
|
||||||
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
|
String code = genCode();
|
||||||
|
user.setPasswordResetCode(code);
|
||||||
|
userRepository.save(user);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verifyPasswordResetCode(String email, String code) {
|
||||||
|
Optional<User> userOpt = userRepository.findByEmail(email);
|
||||||
|
if (userOpt.isPresent() && code.equals(userOpt.get().getPasswordResetCode())) {
|
||||||
|
User user = userOpt.get();
|
||||||
|
user.setPasswordResetCode(null);
|
||||||
|
userRepository.save(user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public User updatePassword(String username, String newPassword) {
|
public User updatePassword(String username, String newPassword) {
|
||||||
passwordValidator.validate(newPassword);
|
passwordValidator.validate(newPassword);
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.openisle.util;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证码类型
|
|
||||||
* @author smallclover
|
|
||||||
* @since 2025-09-08
|
|
||||||
*/
|
|
||||||
public enum VerifyType {
|
|
||||||
REGISTER(1),
|
|
||||||
RESET_PASSWORD(2);
|
|
||||||
private final int code;
|
|
||||||
|
|
||||||
VerifyType(int code) {
|
|
||||||
this.code = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -108,7 +108,6 @@ rabbitmq.sharding.enabled=true
|
|||||||
# see https://springdoc.org/#springdoc-openapi-core-properties
|
# see https://springdoc.org/#springdoc-openapi-core-properties
|
||||||
springdoc.api-docs.path=/api/v3/api-docs
|
springdoc.api-docs.path=/api/v3/api-docs
|
||||||
springdoc.api-docs.enabled=true
|
springdoc.api-docs.enabled=true
|
||||||
springdoc.api-docs.server-url=${WEBSITE_URL:https://www.open-isle.com}
|
|
||||||
springdoc.info.title=OpenIsle
|
springdoc.info.title=OpenIsle
|
||||||
springdoc.info.description=OpenIsle Open API Documentation
|
springdoc.info.description=OpenIsle Open API Documentation
|
||||||
springdoc.info.version=0.0.1
|
springdoc.info.version=0.0.1
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.openisle.model.User;
|
|||||||
import com.openisle.service.*;
|
import com.openisle.service.*;
|
||||||
import com.openisle.model.RegisterMode;
|
import com.openisle.model.RegisterMode;
|
||||||
import com.openisle.repository.UserRepository;
|
import com.openisle.repository.UserRepository;
|
||||||
import com.openisle.util.VerifyType;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -72,9 +71,7 @@ class AuthControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void verifyCodeEndpoint() throws Exception {
|
void verifyCodeEndpoint() throws Exception {
|
||||||
User user = new User();
|
Mockito.when(userService.verifyCode("u", "123")).thenReturn(true);
|
||||||
user.setUsername("u");
|
|
||||||
Mockito.when(userService.verifyCode(user, "123", VerifyType.REGISTER)).thenReturn(true);
|
|
||||||
Mockito.when(jwtService.generateReasonToken("u")).thenReturn("reason_token");
|
Mockito.when(jwtService.generateReasonToken("u")).thenReturn("reason_token");
|
||||||
|
|
||||||
mockMvc.perform(post("/api/auth/verify")
|
mockMvc.perform(post("/api/auth/verify")
|
||||||
|
|||||||
@@ -37,12 +37,11 @@ class PostServiceTest {
|
|||||||
EmailSender emailSender = mock(EmailSender.class);
|
EmailSender emailSender = mock(EmailSender.class);
|
||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.class);
|
||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
@@ -87,12 +86,11 @@ class PostServiceTest {
|
|||||||
EmailSender emailSender = mock(EmailSender.class);
|
EmailSender emailSender = mock(EmailSender.class);
|
||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.class);
|
||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
@@ -143,12 +141,11 @@ class PostServiceTest {
|
|||||||
EmailSender emailSender = mock(EmailSender.class);
|
EmailSender emailSender = mock(EmailSender.class);
|
||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.class);
|
||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
|
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
|
||||||
@@ -180,12 +177,11 @@ class PostServiceTest {
|
|||||||
EmailSender emailSender = mock(EmailSender.class);
|
EmailSender emailSender = mock(EmailSender.class);
|
||||||
ApplicationContext context = mock(ApplicationContext.class);
|
ApplicationContext context = mock(ApplicationContext.class);
|
||||||
PointService pointService = mock(PointService.class);
|
PointService pointService = mock(PointService.class);
|
||||||
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
|
|
||||||
|
|
||||||
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
|
||||||
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
|
||||||
reactionRepo, subRepo, notificationRepo, postReadService,
|
reactionRepo, subRepo, notificationRepo, postReadService,
|
||||||
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
|
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
|
||||||
when(context.getBean(PostService.class)).thenReturn(service);
|
when(context.getBean(PostService.class)).thenReturn(service);
|
||||||
|
|
||||||
User author = new User();
|
User author = new User();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
--background-color: white;
|
--background-color: white;
|
||||||
--background-color-blur: rgba(255, 255, 255, 0.57);
|
--background-color-blur: rgba(255, 255, 255, 0.57);
|
||||||
--menu-border-color: lightgray;
|
--menu-border-color: lightgray;
|
||||||
--normal-border-color: rgba(211, 211, 211, 0.63);
|
--normal-border-color: lightgray;
|
||||||
--menu-selected-background-color: rgba(88, 241, 255, 0.166);
|
--menu-selected-background-color: rgba(88, 241, 255, 0.166);
|
||||||
--normal-light-background-color: rgba(242, 242, 242, 0.884);
|
--normal-light-background-color: rgba(242, 242, 242, 0.884);
|
||||||
--menu-selected-background-color-hover: rgba(242, 242, 242, 0.884);
|
--menu-selected-background-color-hover: rgba(242, 242, 242, 0.884);
|
||||||
@@ -348,22 +348,6 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust diff2html layout on mobile */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.content-diff .d2h-wrapper,
|
|
||||||
.content-diff .d2h-code-line,
|
|
||||||
.content-diff .d2h-code-side-line,
|
|
||||||
.content-diff .d2h-code-line-ctn,
|
|
||||||
.content-diff .d2h-code-side-line-ctn,
|
|
||||||
.content-diff .d2h-file-header {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-diff .d2h-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transition API */
|
/* Transition API */
|
||||||
::view-transition-old(root),
|
::view-transition-old(root),
|
||||||
::view-transition-new(root) {
|
::view-transition-new(root) {
|
||||||
|
|||||||
33
frontend_nuxt/components/NewMessageContainer.vue
Normal file
33
frontend_nuxt/components/NewMessageContainer.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div class="new-message-container" :style="{ bottom: bottom + 'px' }" @click="$emit('click')">
|
||||||
|
{{ count }} 条新消息,点击查看
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
count: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.new-message-container {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div :id="`change-log-${log.id}`" class="change-log-container">
|
|
||||||
<div class="change-log-text">
|
|
||||||
<BaseImage
|
|
||||||
v-if="log.userAvatar"
|
|
||||||
class="change-log-avatar"
|
|
||||||
:src="log.userAvatar"
|
|
||||||
alt="avatar"
|
|
||||||
@click="() => navigateTo(`/users/${log.username}`)"
|
|
||||||
/>
|
|
||||||
<span v-if="log.username" class="change-log-user">{{ log.username }}</span>
|
|
||||||
<span v-if="log.type === 'CONTENT'" class="change-log-content">变更了文章内容</span>
|
|
||||||
<span v-else-if="log.type === 'TITLE'" class="change-log-content">变更了文章标题</span>
|
|
||||||
<template v-else-if="log.type === 'CATEGORY'">
|
|
||||||
<div class="change-log-category-text">变更了文章分类, 从</div>
|
|
||||||
<ArticleCategory :category="log.oldCategory" />
|
|
||||||
<div class="change-log-category-text">修改为</div>
|
|
||||||
<ArticleCategory :category="log.newCategory" />
|
|
||||||
</template>
|
|
||||||
<template v-else-if="log.type === 'TAG'">
|
|
||||||
<div class="change-log-category-text">变更了文章标签, 从</div>
|
|
||||||
<ArticleTags :tags="log.oldTags" />
|
|
||||||
<div class="change-log-category-text">修改为</div>
|
|
||||||
<ArticleTags :tags="log.newTags" />
|
|
||||||
</template>
|
|
||||||
<span v-else-if="log.type === 'CLOSED'" class="change-log-content">
|
|
||||||
<template v-if="log.newClosed">关闭了文章</template>
|
|
||||||
<template v-else>重新打开了文章</template>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="log.type === 'PINNED'" class="change-log-content">
|
|
||||||
<template v-if="log.newPinnedAt">置顶了文章</template>
|
|
||||||
<template v-else>取消置顶文章</template>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="log.type === 'FEATURED'" class="change-log-content">
|
|
||||||
<template v-if="log.newFeatured">将文章设为精选</template>
|
|
||||||
<template v-else>取消精选文章</template>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="log.type === 'VOTE_RESULT'" class="change-log-content"
|
|
||||||
>系统已计算投票结果</span
|
|
||||||
>
|
|
||||||
<span v-else-if="log.type === 'LOTTERY_RESULT'" class="change-log-content"
|
|
||||||
>系统已「精密计算」抽奖结果 (=゚ω゚)ノ</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="change-log-time">{{ log.time }}</div>
|
|
||||||
<div
|
|
||||||
v-if="log.type === 'CONTENT' || log.type === 'TITLE'"
|
|
||||||
class="content-diff"
|
|
||||||
v-html="diffHtml"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { html } from 'diff2html'
|
|
||||||
import { createTwoFilesPatch } from 'diff'
|
|
||||||
import { useIsMobile } from '~/utils/screen'
|
|
||||||
import 'diff2html/bundles/css/diff2html.min.css'
|
|
||||||
import BaseImage from '~/components/BaseImage.vue'
|
|
||||||
import { navigateTo } from 'nuxt/app'
|
|
||||||
import { themeState } from '~/utils/theme'
|
|
||||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
|
||||||
import ArticleTags from '~/components/ArticleTags.vue'
|
|
||||||
const props = defineProps({
|
|
||||||
log: Object,
|
|
||||||
title: String,
|
|
||||||
})
|
|
||||||
|
|
||||||
const diffHtml = computed(() => {
|
|
||||||
// Track theme changes
|
|
||||||
const isDark = import.meta.client && document.documentElement.dataset.theme === 'dark'
|
|
||||||
themeState.mode
|
|
||||||
const colorScheme = isDark ? 'dark' : 'light'
|
|
||||||
|
|
||||||
if (props.log.type === 'CONTENT') {
|
|
||||||
const oldContent = props.log.oldContent ?? ''
|
|
||||||
const newContent = props.log.newContent ?? ''
|
|
||||||
const diff = createTwoFilesPatch(props.title, props.title, oldContent, newContent)
|
|
||||||
return html(diff, {
|
|
||||||
inputFormat: 'diff',
|
|
||||||
showFiles: false,
|
|
||||||
matching: 'lines',
|
|
||||||
drawFileList: false,
|
|
||||||
colorScheme,
|
|
||||||
})
|
|
||||||
} else if (props.log.type === 'TITLE') {
|
|
||||||
const oldTitle = props.log.oldTitle ?? ''
|
|
||||||
const newTitle = props.log.newTitle ?? ''
|
|
||||||
const diff = createTwoFilesPatch(oldTitle, newTitle, '', '')
|
|
||||||
return html(diff, {
|
|
||||||
inputFormat: 'diff',
|
|
||||||
showFiles: false,
|
|
||||||
matching: 'lines',
|
|
||||||
drawFileList: false,
|
|
||||||
colorScheme,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.change-log-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
/* padding-top: 5px; */
|
|
||||||
/* padding-bottom: 30px; */
|
|
||||||
font-size: 14px;
|
|
||||||
border-bottom: 1px solid var(--normal-border-color);
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
.change-log-text {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.change-log-user {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-log-user,
|
|
||||||
.change-log-content {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-log-avatar {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.change-log-time {
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-diff {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-log-category {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
80
frontend_nuxt/package-lock.json
generated
80
frontend_nuxt/package-lock.json
generated
@@ -10,8 +10,6 @@
|
|||||||
"@nuxt/image": "^1.11.0",
|
"@nuxt/image": "^1.11.0",
|
||||||
"@stomp/stompjs": "^7.0.0",
|
"@stomp/stompjs": "^7.0.0",
|
||||||
"cropperjs": "^1.6.2",
|
"cropperjs": "^1.6.2",
|
||||||
"diff": "^8.0.2",
|
|
||||||
"diff2html": "^3.4.52",
|
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
@@ -7220,41 +7218,6 @@
|
|||||||
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
|
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/diff2html": {
|
|
||||||
"version": "3.4.52",
|
|
||||||
"resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.52.tgz",
|
|
||||||
"integrity": "sha512-qhMg8/I3sZ4zm/6R/Kh0xd6qG6Vm86w6M+C9W+DuH1V8ACz+1cgEC8/k0ucjv6AGqZWzHm/8G1gh7IlrUqCMhg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"diff": "^7.0.0",
|
|
||||||
"hogan.js": "3.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"highlight.js": "11.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/diff2html/node_modules/diff": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/diff2html/node_modules/highlight.js": {
|
|
||||||
"version": "11.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz",
|
|
||||||
"integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
@@ -8328,49 +8291,6 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hogan.js": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==",
|
|
||||||
"dependencies": {
|
|
||||||
"mkdirp": "0.3.0",
|
|
||||||
"nopt": "1.0.10"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"hulk": "bin/hulk"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hogan.js/node_modules/abbrev": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/hogan.js/node_modules/mkdirp": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
|
|
||||||
"integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==",
|
|
||||||
"deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
|
|
||||||
"license": "MIT/X11",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hogan.js/node_modules/nopt": {
|
|
||||||
"version": "1.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
|
|
||||||
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"abbrev": "1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"nopt": "bin/nopt.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hookable": {
|
"node_modules/hookable": {
|
||||||
"version": "5.5.3",
|
"version": "5.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
"@nuxt/image": "^1.11.0",
|
"@nuxt/image": "^1.11.0",
|
||||||
"@stomp/stompjs": "^7.0.0",
|
"@stomp/stompjs": "^7.0.0",
|
||||||
"cropperjs": "^1.6.2",
|
"cropperjs": "^1.6.2",
|
||||||
"diff": "^8.0.2",
|
|
||||||
"diff2html": "^3.4.52",
|
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
|||||||
@@ -36,11 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.replyTo" class="reply-preview info-content-text">
|
<div v-if="item.replyTo" class="reply-preview info-content-text">
|
||||||
<div class="reply-header">
|
<div class="reply-author">{{ item.replyTo.sender.username }}</div>
|
||||||
<next class="reply-icon" />
|
|
||||||
<BaseImage class="reply-avatar" :src="item.replyTo.sender.avatar" alt="avatar" />
|
|
||||||
<div class="reply-author">{{ item.replyTo.sender.username }}:</div>
|
|
||||||
</div>
|
|
||||||
<div class="reply-content" v-html="renderMarkdown(item.replyTo.content)"></div>
|
<div class="reply-content" v-html="renderMarkdown(item.replyTo.content)"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
@@ -66,22 +62,19 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message-input-area">
|
<NewMessageContainer
|
||||||
<div
|
v-if="showNewMessageContainer"
|
||||||
v-if="newMessagesCount > 0 && !isUserNearBottom"
|
:count="newMessagesCount"
|
||||||
class="new-message-container"
|
:bottom="inputAreaHeight + 20"
|
||||||
@click="handleScrollToBottom"
|
@click="handleNewMessagesClick"
|
||||||
>
|
/>
|
||||||
<double-down />
|
|
||||||
<div class="new-message-count">有{{ newMessagesCount }}条新消息</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="message-input-area" ref="messageInputAreaEl">
|
||||||
<div v-if="replyTo" class="active-reply">
|
<div v-if="replyTo" class="active-reply">
|
||||||
正在回复 {{ replyTo.sender.username }}:
|
正在回复 {{ replyTo.sender.username }}:
|
||||||
{{ stripMarkdownLength(replyTo.content, 50) }}
|
{{ stripMarkdownLength(replyTo.content, 50) }}
|
||||||
<close-icon class="close-reply" @click="replyTo = null" />
|
<close-icon class="close-reply" @click="replyTo = null" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MessageEditor :loading="sending" @submit="sendMessage" />
|
<MessageEditor :loading="sending" @submit="sendMessage" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,6 +103,7 @@ import { useChannelsUnreadCount } from '~/composables/useChannelsUnreadCount'
|
|||||||
import TimeManager from '~/utils/time'
|
import TimeManager from '~/utils/time'
|
||||||
import BaseTimeline from '~/components/BaseTimeline.vue'
|
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||||
import BasePlaceholder from '~/components/BasePlaceholder.vue'
|
import BasePlaceholder from '~/components/BasePlaceholder.vue'
|
||||||
|
import NewMessageContainer from '~/components/NewMessageContainer.vue'
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -126,6 +120,7 @@ const error = ref(null)
|
|||||||
const conversationId = route.params.id
|
const conversationId = route.params.id
|
||||||
const currentUser = ref(null)
|
const currentUser = ref(null)
|
||||||
const messagesListEl = ref(null)
|
const messagesListEl = ref(null)
|
||||||
|
const messageInputAreaEl = ref(null)
|
||||||
const currentPage = ref(0)
|
const currentPage = ref(0)
|
||||||
const totalPages = ref(0)
|
const totalPages = ref(0)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
@@ -135,6 +130,20 @@ const isFloatMode = computed(() => route.query.float !== undefined)
|
|||||||
const floatRoute = useState('messageFloatRoute')
|
const floatRoute = useState('messageFloatRoute')
|
||||||
const replyTo = ref(null)
|
const replyTo = ref(null)
|
||||||
const newMessagesCount = ref(0)
|
const newMessagesCount = ref(0)
|
||||||
|
const inputAreaHeight = ref(0)
|
||||||
|
const showNewMessageContainer = computed(
|
||||||
|
() => newMessagesCount.value > 0 && !isUserNearBottom.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
function updateInputAreaHeight() {
|
||||||
|
if (!messageInputAreaEl.value) return
|
||||||
|
inputAreaHeight.value = messageInputAreaEl.value.offsetHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNewMessagesClick() {
|
||||||
|
scrollToBottomSmooth()
|
||||||
|
newMessagesCount.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
const isUserNearBottom = ref(true)
|
const isUserNearBottom = ref(true)
|
||||||
function updateNearBottom() {
|
function updateNearBottom() {
|
||||||
@@ -142,9 +151,6 @@ function updateNearBottom() {
|
|||||||
if (!el) return
|
if (!el) return
|
||||||
const threshold = 40 // px
|
const threshold = 40 // px
|
||||||
isUserNearBottom.value = el.scrollHeight - el.scrollTop - el.clientHeight <= threshold
|
isUserNearBottom.value = el.scrollHeight - el.scrollTop - el.clientHeight <= threshold
|
||||||
if (isUserNearBottom.value) {
|
|
||||||
newMessagesCount.value = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasMoreMessages = computed(() => currentPage.value < totalPages.value - 1)
|
const hasMoreMessages = computed(() => currentPage.value < totalPages.value - 1)
|
||||||
@@ -188,11 +194,6 @@ function scrollToBottomInstant() {
|
|||||||
el.scrollTop = el.scrollHeight
|
el.scrollTop = el.scrollHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScrollToBottom() {
|
|
||||||
scrollToBottomSmooth()
|
|
||||||
newMessagesCount.value = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchMessages(page = 0) {
|
async function fetchMessages(page = 0) {
|
||||||
if (page === 0) {
|
if (page === 0) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -324,7 +325,6 @@ async function sendMessage(content, clearInput) {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
// 仅“发送消息成功后”才平滑滚动到底部
|
// 仅“发送消息成功后”才平滑滚动到底部
|
||||||
scrollToBottomSmooth()
|
scrollToBottomSmooth()
|
||||||
newMessagesCount.value = 0
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e.message)
|
toast.error(e.message)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -353,6 +353,10 @@ onMounted(async () => {
|
|||||||
messagesListEl.value.addEventListener('scroll', updateNearBottom, { passive: true })
|
messagesListEl.value.addEventListener('scroll', updateNearBottom, { passive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', updateInputAreaHeight)
|
||||||
|
await nextTick()
|
||||||
|
updateInputAreaHeight()
|
||||||
|
|
||||||
currentUser.value = await fetchCurrentUser()
|
currentUser.value = await fetchCurrentUser()
|
||||||
if (currentUser.value) {
|
if (currentUser.value) {
|
||||||
await fetchMessages(0)
|
await fetchMessages(0)
|
||||||
@@ -394,7 +398,6 @@ const subscribeToConversation = () => {
|
|||||||
|
|
||||||
await markConversationAsRead()
|
await markConversationAsRead()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
if (isUserNearBottom.value) {
|
if (isUserNearBottom.value) {
|
||||||
scrollToBottomSmooth()
|
scrollToBottomSmooth()
|
||||||
} else {
|
} else {
|
||||||
@@ -412,6 +415,14 @@ watch(isConnected, (newValue) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(isUserNearBottom, (val) => {
|
||||||
|
if (val) newMessagesCount.value = 0
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(replyTo, () => {
|
||||||
|
nextTick(updateInputAreaHeight)
|
||||||
|
})
|
||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
// 返回页面时:刷新数据与已读,并滚动到底部
|
// 返回页面时:刷新数据与已读,并滚动到底部
|
||||||
if (currentUser.value) {
|
if (currentUser.value) {
|
||||||
@@ -444,6 +455,7 @@ onUnmounted(() => {
|
|||||||
if (messagesListEl.value) {
|
if (messagesListEl.value) {
|
||||||
messagesListEl.value.removeEventListener('scroll', updateNearBottom)
|
messagesListEl.value.removeEventListener('scroll', updateNearBottom)
|
||||||
}
|
}
|
||||||
|
window.removeEventListener('resize', updateInputAreaHeight)
|
||||||
})
|
})
|
||||||
|
|
||||||
function minimize() {
|
function minimize() {
|
||||||
@@ -581,25 +593,6 @@ function goBack() {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-message-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--normal-border-color);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 3px 6px;
|
|
||||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
|
||||||
width: fit-content;
|
|
||||||
position: absolute;
|
|
||||||
bottom: calc(100% + 20px);
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 10;
|
|
||||||
background-color: var(--background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -630,6 +623,11 @@ function goBack() {
|
|||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-input-area {
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-container {
|
.loading-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -646,19 +644,6 @@ function goBack() {
|
|||||||
.message-input-area {
|
.message-input-area {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-icon {
|
|
||||||
color: var(--primary-color);
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-avatar {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-preview {
|
.reply-preview {
|
||||||
@@ -670,16 +655,9 @@ function goBack() {
|
|||||||
background-color: var(--normal-light-background-color);
|
background-color: var(--normal-light-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-author {
|
.reply-author {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
opacity: 0.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-btn {
|
.reply-btn {
|
||||||
|
|||||||
@@ -122,10 +122,9 @@
|
|||||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="comments-container">
|
<div v-else class="comments-container">
|
||||||
<BaseTimeline :items="timelineItems">
|
<BaseTimeline :items="comments">
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<CommentItem
|
<CommentItem
|
||||||
v-if="item.kind === 'comment'"
|
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:comment="item"
|
:comment="item"
|
||||||
:level="0"
|
:level="0"
|
||||||
@@ -134,7 +133,6 @@
|
|||||||
:post-closed="closed"
|
:post-closed="closed"
|
||||||
@deleted="onCommentDeleted"
|
@deleted="onCommentDeleted"
|
||||||
/>
|
/>
|
||||||
<PostChangeLogItem v-else :log="item" :title="title" />
|
|
||||||
</template>
|
</template>
|
||||||
</BaseTimeline>
|
</BaseTimeline>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,7 +182,6 @@ import { useRoute } from 'vue-router'
|
|||||||
import CommentItem from '~/components/CommentItem.vue'
|
import CommentItem from '~/components/CommentItem.vue'
|
||||||
import CommentEditor from '~/components/CommentEditor.vue'
|
import CommentEditor from '~/components/CommentEditor.vue'
|
||||||
import BaseTimeline from '~/components/BaseTimeline.vue'
|
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||||
import PostChangeLogItem from '~/components/PostChangeLogItem.vue'
|
|
||||||
import ArticleTags from '~/components/ArticleTags.vue'
|
import ArticleTags from '~/components/ArticleTags.vue'
|
||||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||||
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
||||||
@@ -215,7 +212,6 @@ const category = ref('')
|
|||||||
const tags = ref([])
|
const tags = ref([])
|
||||||
const postReactions = ref([])
|
const postReactions = ref([])
|
||||||
const comments = ref([])
|
const comments = ref([])
|
||||||
const changeLogs = ref([])
|
|
||||||
const status = ref('PUBLISHED')
|
const status = ref('PUBLISHED')
|
||||||
const closed = ref(false)
|
const closed = ref(false)
|
||||||
const pinnedAt = ref(null)
|
const pinnedAt = ref(null)
|
||||||
@@ -229,7 +225,6 @@ const subscribed = ref(false)
|
|||||||
const commentSort = ref('NEWEST')
|
const commentSort = ref('NEWEST')
|
||||||
const isFetchingComments = ref(false)
|
const isFetchingComments = ref(false)
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const timelineItems = ref([])
|
|
||||||
|
|
||||||
const headerHeight = import.meta.client
|
const headerHeight = import.meta.client
|
||||||
? parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 0
|
? parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 0
|
||||||
@@ -295,13 +290,8 @@ const gatherPostItems = () => {
|
|||||||
const main = mainContainer.value.querySelector('.info-content-container')
|
const main = mainContainer.value.querySelector('.info-content-container')
|
||||||
if (main) items.push({ el: main, top: getTop(main) })
|
if (main) items.push({ el: main, top: getTop(main) })
|
||||||
|
|
||||||
for (const c of timelineItems.value) {
|
for (const c of comments.value) {
|
||||||
let el
|
const el = document.getElementById('comment-' + c.id)
|
||||||
if (c.kind === 'comment') {
|
|
||||||
el = document.getElementById('comment-' + c.id)
|
|
||||||
} else {
|
|
||||||
el = document.getElementById('change-log-' + c.id)
|
|
||||||
}
|
|
||||||
if (el) {
|
if (el) {
|
||||||
items.push({ el, top: getTop(el) })
|
items.push({ el, top: getTop(el) })
|
||||||
}
|
}
|
||||||
@@ -333,66 +323,12 @@ const mapComment = (
|
|||||||
),
|
),
|
||||||
openReplies: level === 0,
|
openReplies: level === 0,
|
||||||
src: c.author.avatar,
|
src: c.author.avatar,
|
||||||
createdAt: c.createdAt,
|
|
||||||
iconClick: () => navigateTo(`/users/${c.author.id}`),
|
iconClick: () => navigateTo(`/users/${c.author.id}`),
|
||||||
parentUserName: parentUserName,
|
parentUserName: parentUserName,
|
||||||
parentUserAvatar: parentUserAvatar,
|
parentUserAvatar: parentUserAvatar,
|
||||||
parentUserClick: parentUserId ? () => navigateTo(`/users/${parentUserId}`) : null,
|
parentUserClick: parentUserId ? () => navigateTo(`/users/${parentUserId}`) : null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const changeLogIcon = (l) => {
|
|
||||||
if (l.type === 'CONTENT') {
|
|
||||||
return 'edit'
|
|
||||||
} else if (l.type === 'TITLE') {
|
|
||||||
return 'hashtag-key'
|
|
||||||
} else if (l.type === 'CATEGORY') {
|
|
||||||
return 'tag-one'
|
|
||||||
} else if (l.type === 'TAG') {
|
|
||||||
return 'tag-one'
|
|
||||||
} else if (l.type === 'CLOSED') {
|
|
||||||
if (l.newClosed) {
|
|
||||||
return 'lock-one'
|
|
||||||
} else {
|
|
||||||
return 'unlock'
|
|
||||||
}
|
|
||||||
} else if (l.type === 'PINNED') {
|
|
||||||
return 'pin-icon'
|
|
||||||
} else if (l.type === 'FEATURED') {
|
|
||||||
if (l.newFeatured) {
|
|
||||||
return 'star'
|
|
||||||
} else {
|
|
||||||
return 'dislike'
|
|
||||||
}
|
|
||||||
} else if (l.type === 'VOTE_RESULT') {
|
|
||||||
return 'check-one'
|
|
||||||
} else if (l.type === 'LOTTERY_RESULT') {
|
|
||||||
return 'gift'
|
|
||||||
} else {
|
|
||||||
return 'info'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapChangeLog = (l) => ({
|
|
||||||
id: l.id,
|
|
||||||
username: l.username,
|
|
||||||
userAvatar: l.userAvatar,
|
|
||||||
type: l.type,
|
|
||||||
createdAt: l.time,
|
|
||||||
time: TimeManager.format(l.time),
|
|
||||||
newClosed: l.newClosed,
|
|
||||||
newPinnedAt: l.newPinnedAt,
|
|
||||||
newFeatured: l.newFeatured,
|
|
||||||
oldContent: l.oldContent,
|
|
||||||
newContent: l.newContent,
|
|
||||||
oldTitle: l.oldTitle,
|
|
||||||
newTitle: l.newTitle,
|
|
||||||
oldCategory: l.oldCategory,
|
|
||||||
newCategory: l.newCategory,
|
|
||||||
oldTags: l.oldTags,
|
|
||||||
newTags: l.newTags,
|
|
||||||
icon: changeLogIcon(l),
|
|
||||||
})
|
|
||||||
|
|
||||||
const getTop = (el) => {
|
const getTop = (el) => {
|
||||||
return el.getBoundingClientRect().top + window.scrollY
|
return el.getBoundingClientRect().top + window.scrollY
|
||||||
}
|
}
|
||||||
@@ -486,21 +422,19 @@ watchEffect(() => {
|
|||||||
// router.replace('/404')
|
// router.replace('/404')
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const totalPosts = computed(() => timelineItems.value.length + 1)
|
const totalPosts = computed(() => comments.value.length + 1)
|
||||||
const lastReplyTime = computed(() =>
|
const lastReplyTime = computed(() =>
|
||||||
timelineItems.value.length
|
comments.value.length ? comments.value[comments.value.length - 1].time : postTime.value,
|
||||||
? timelineItems.value[timelineItems.value.length - 1].time
|
|
||||||
: postTime.value,
|
|
||||||
)
|
)
|
||||||
const firstReplyTime = computed(() =>
|
const firstReplyTime = computed(() =>
|
||||||
timelineItems.value.length ? timelineItems.value[0].time : postTime.value,
|
comments.value.length ? comments.value[0].time : postTime.value,
|
||||||
)
|
)
|
||||||
const scrollerTopTime = computed(() =>
|
const scrollerTopTime = computed(() =>
|
||||||
commentSort.value === 'OLDEST' ? postTime.value : firstReplyTime.value,
|
commentSort.value === 'OLDEST' ? postTime.value : firstReplyTime.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => timelineItems.value.length,
|
() => comments.value.length,
|
||||||
async () => {
|
async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
gatherPostItems()
|
gatherPostItems()
|
||||||
@@ -612,7 +546,6 @@ const approvePost = async () => {
|
|||||||
status.value = 'PUBLISHED'
|
status.value = 'PUBLISHED'
|
||||||
toast.success('已通过审核')
|
toast.success('已通过审核')
|
||||||
await refreshPost()
|
await refreshPost()
|
||||||
await fetchChangeLogs()
|
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -628,7 +561,6 @@ const pinPost = async () => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success('已置顶')
|
toast.success('已置顶')
|
||||||
await refreshPost()
|
await refreshPost()
|
||||||
await fetchChangeLogs()
|
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -644,7 +576,6 @@ const unpinPost = async () => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success('已取消置顶')
|
toast.success('已取消置顶')
|
||||||
await refreshPost()
|
await refreshPost()
|
||||||
await fetchChangeLogs()
|
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -660,7 +591,6 @@ const excludeRss = async () => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
rssExcluded.value = true
|
rssExcluded.value = true
|
||||||
toast.success('已标记为rss不推荐')
|
toast.success('已标记为rss不推荐')
|
||||||
await fetchChangeLogs()
|
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -676,7 +606,6 @@ const includeRss = async () => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
rssExcluded.value = false
|
rssExcluded.value = false
|
||||||
toast.success('已标记为rss推荐')
|
toast.success('已标记为rss推荐')
|
||||||
await fetchChangeLogs()
|
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -693,7 +622,6 @@ const closePost = async () => {
|
|||||||
closed.value = true
|
closed.value = true
|
||||||
toast.success('已关闭')
|
toast.success('已关闭')
|
||||||
await refreshPost()
|
await refreshPost()
|
||||||
await fetchChangeLogs()
|
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -710,7 +638,6 @@ const reopenPost = async () => {
|
|||||||
closed.value = false
|
closed.value = false
|
||||||
toast.success('已重新打开')
|
toast.success('已重新打开')
|
||||||
await refreshPost()
|
await refreshPost()
|
||||||
await fetchChangeLogs()
|
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -755,7 +682,6 @@ const rejectPost = async () => {
|
|||||||
status.value = 'REJECTED'
|
status.value = 'REJECTED'
|
||||||
toast.success('已驳回')
|
toast.success('已驳回')
|
||||||
await refreshPost()
|
await refreshPost()
|
||||||
await fetchChangeLogs()
|
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
@@ -814,42 +740,7 @@ const fetchComments = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchChangeLogs = async () => {
|
watch(commentSort, fetchComments)
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/posts/${postId}/change-logs`)
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json()
|
|
||||||
changeLogs.value = data.map(mapChangeLog)
|
|
||||||
await nextTick()
|
|
||||||
gatherPostItems()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.debug('Fetch change logs error', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// todo(tim): fetchComments, fetchChangeLogs 整合到一个请求,并且取消前端排序
|
|
||||||
//
|
|
||||||
const fetchTimeline = async () => {
|
|
||||||
await Promise.all([fetchComments(), fetchChangeLogs()])
|
|
||||||
const cs = comments.value.map((c) => ({ ...c, kind: 'comment' }))
|
|
||||||
const ls = changeLogs.value.map((l) => ({ ...l, kind: 'log' }))
|
|
||||||
|
|
||||||
if (commentSort.value === 'NEWEST') {
|
|
||||||
timelineItems.value = [...cs, ...ls].sort(
|
|
||||||
(a, b) => new Date(b.createdAt) - new Date(a.createdAt),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
timelineItems.value = [...cs, ...ls].sort(
|
|
||||||
(a, b) => new Date(a.createdAt) - new Date(b.createdAt),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(commentSort, async () => {
|
|
||||||
await fetchTimeline()
|
|
||||||
})
|
|
||||||
|
|
||||||
const jumpToHashComment = async () => {
|
const jumpToHashComment = async () => {
|
||||||
const hash = location.hash
|
const hash = location.hash
|
||||||
@@ -872,7 +763,7 @@ const gotoProfile = () => {
|
|||||||
|
|
||||||
const initPage = async () => {
|
const initPage = async () => {
|
||||||
scrollTo(0, 0)
|
scrollTo(0, 0)
|
||||||
await fetchTimeline()
|
await fetchComments()
|
||||||
const hash = location.hash
|
const hash = location.hash
|
||||||
const id = hash.startsWith('#comment-') ? hash.substring('#comment-'.length) : null
|
const id = hash.startsWith('#comment-') ? hash.substring('#comment-'.length) : null
|
||||||
if (id) expandCommentPath(id)
|
if (id) expandCommentPath(id)
|
||||||
@@ -1163,7 +1054,6 @@ onMounted(async () => {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-content-container {
|
.info-content-container {
|
||||||
@@ -1219,7 +1109,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.post-time {
|
.post-time {
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1285,6 +1175,10 @@ onMounted(async () => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-time {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.info-content-text {
|
.info-content-text {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,10 +73,6 @@ import {
|
|||||||
RobotOne,
|
RobotOne,
|
||||||
Server,
|
Server,
|
||||||
Protection,
|
Protection,
|
||||||
DoubleDown,
|
|
||||||
Open,
|
|
||||||
Dislike,
|
|
||||||
CheckOne,
|
|
||||||
} from '@icon-park/vue-next'
|
} from '@icon-park/vue-next'
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
@@ -153,8 +149,4 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
nuxtApp.vueApp.component('RobotOne', RobotOne)
|
nuxtApp.vueApp.component('RobotOne', RobotOne)
|
||||||
nuxtApp.vueApp.component('ServerIcon', Server)
|
nuxtApp.vueApp.component('ServerIcon', Server)
|
||||||
nuxtApp.vueApp.component('Protection', Protection)
|
nuxtApp.vueApp.component('Protection', Protection)
|
||||||
nuxtApp.vueApp.component('DoubleDown', DoubleDown)
|
|
||||||
nuxtApp.vueApp.component('OpenIcon', Open)
|
|
||||||
nuxtApp.vueApp.component('Dislike', Dislike)
|
|
||||||
nuxtApp.vueApp.component('CheckOne', CheckOne)
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user