Compare commits

...

18 Commits
main ... main

@ -1,3 +1,4 @@
{ {
"java.compile.nullAnalysis.mode": "automatic" "java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
} }

@ -55,13 +55,4 @@
<artifactId>spring-web</artifactId> <artifactId>spring-web</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project> </project>

@ -18,7 +18,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
"/user/register", "/user/register",
"/user/captcha", "/user/captcha",
"/user/verify-captcha", "/user/verify-captcha",
"/user/info/getuserinfo", "/user/check-login",
"/post/list", "/post/list",
"/post/detail", "/post/detail",
"/comment/list", "/comment/list",

@ -151,7 +151,7 @@ public final class JWTUtil {
if(ttl < NEED_REFRESH_TTL) if(ttl < NEED_REFRESH_TTL)
redisUtil.set(redisKey, newRefreshToken, REFRESH_EXPIRATION, TimeUnit.MILLISECONDS); redisUtil.set(redisKey, newRefreshToken, REFRESH_EXPIRATION, TimeUnit.MILLISECONDS);
user.setAccessToken(newAccessToken); user.setAccessToken(newAccessToken);
user.setRefreshToken(newRefreshToken);
return user; return user;
} }

@ -10,6 +10,7 @@ import java.util.Optional;
public final class UserContext { public final class UserContext {
private static final ThreadLocal<UserDTO> USER_THREAD_LOCAL = new TransmittableThreadLocal<>(); private static final ThreadLocal<UserDTO> USER_THREAD_LOCAL = new TransmittableThreadLocal<>();
public static void setUser(UserDTO user) { public static void setUser(UserDTO user) {
USER_THREAD_LOCAL.set(user); USER_THREAD_LOCAL.set(user);
} }
@ -39,6 +40,10 @@ public final class UserContext {
return Optional.ofNullable(userInfoDTO).map(UserDTO::getRefreshToken).orElse(null); return Optional.ofNullable(userInfoDTO).map(UserDTO::getRefreshToken).orElse(null);
} }
public static UserDTO getUser() {
return USER_THREAD_LOCAL.get();
}
public static void removeUser() { public static void removeUser() {
USER_THREAD_LOCAL.remove(); USER_THREAD_LOCAL.remove();
} }

@ -127,13 +127,4 @@
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project> </project>

@ -70,6 +70,22 @@
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.luojia_channel.LuojiaChannelApplication</mainClass>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>

@ -35,7 +35,8 @@ public class PostSelector {
double newScore = (1 + likes + comments*0.7 + favorites*0.5) double newScore = (1 + likes + comments*0.7 + favorites*0.5)
/ Math.pow(1 + hours/24.0, 1.8); / Math.pow(1 + hours/24.0, 1.8);
redisUtil.zAdd("post:hot:", post.getId(), newScore); redisUtil.zAdd("post:hot:"+post.getCategoryId(), post.getId(), newScore);
redisUtil.zAdd("post:hot:all", post.getId(), newScore);
} }
public void calculateCommentScore(Comment comment){ public void calculateCommentScore(Comment comment){

@ -14,4 +14,6 @@ public class PostPageQueryDTO extends ScrollPageRequest {
@Schema(title = "排序类型0表示按时间1表示按热度2表示自定义的推荐算法(暂未实现)") @Schema(title = "排序类型0表示按时间1表示按热度2表示自定义的推荐算法(暂未实现)")
private Integer type = 0; private Integer type = 0;
private Long categoryId;
} }

@ -77,5 +77,8 @@ public class PostBasicInfoDTO {
@Schema( @Schema(
description = "帖子创建时间" description = "帖子创建时间"
) )
private Long categoryId;
private LocalDateTime createTime; private LocalDateTime createTime;
} }

@ -82,5 +82,8 @@ public class PostInfoDTO {
@Schema( @Schema(
description = "帖子创建时间" description = "帖子创建时间"
) )
private Long categoryId;
private LocalDateTime createTime; private LocalDateTime createTime;
} }

@ -91,7 +91,8 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
// TODO 消息通知? // TODO 消息通知?
} }
postSelector.calculatePostScore(post); postSelector.calculatePostScore(post);
redisUtil.zAdd("post:time:", post.getId(), System.currentTimeMillis()); redisUtil.zAdd("post:time:"+post.getCategoryId(), post.getId(), System.currentTimeMillis());
redisUtil.zAdd("post:time:"+"all", post.getId(), System.currentTimeMillis());
redisUtil.zAdd("post:user:" + userId, post.getId(), System.currentTimeMillis()); redisUtil.zAdd("post:user:" + userId, post.getId(), System.currentTimeMillis());
return post.getId(); return post.getId();
} }
@ -119,6 +120,7 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
public void deletePost(Long id) { public void deletePost(Long id) {
validatePostUtil.validatePostOwnership(id); validatePostUtil.validatePostOwnership(id);
Long userId = UserContext.getUserId(); Long userId = UserContext.getUserId();
Post post = postMapper.selectById(id);
int delete = postMapper.deleteById(id); int delete = postMapper.deleteById(id);
if(delete <= 0){ if(delete <= 0){
throw new PostException("删除帖子失败"); throw new PostException("删除帖子失败");
@ -126,8 +128,10 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
// redisUtil.delete("post:detail:" + id.toString()); // redisUtil.delete("post:detail:" + id.toString());
// redisUtil.delete("post:of:user:" + UserContext.getUserId()); // redisUtil.delete("post:of:user:" + UserContext.getUserId());
redisUtil.delete("post:detail:" + id); redisUtil.delete("post:detail:" + id);
redisUtil.zRemove("post:time:", id); redisUtil.zRemove("post:time:"+post.getCategoryId(), id);
redisUtil.zRemove("post:hot:", id); redisUtil.zRemove("post:hot:"+post.getCategoryId(), id);
redisUtil.zRemove("post:time:"+"all", id);
redisUtil.zRemove("post:hot:"+"all", id);
redisUtil.zRemove("post:user:" + userId, id); redisUtil.zRemove("post:user:" + userId, id);
} }
@ -184,7 +188,12 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
return postBasicInfoDTO; return postBasicInfoDTO;
}); });
*/ */
String key = postPageQueryDTO.getType().equals(0) ? "post:time:" : "post:hot:"; String key;
if(postPageQueryDTO.getCategoryId() == null || postPageQueryDTO.getCategoryId().equals(0L)){
key = (postPageQueryDTO.getType().equals(0) ? "post:time:" : "post:hot:") + "all";
}else {
key = (postPageQueryDTO.getType().equals(0) ? "post:time:" : "post:hot:") + postPageQueryDTO.getCategoryId();
}
return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO, return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO,
(postIds) -> { (postIds) -> {
List<Long> userIds = new ArrayList<>(); List<Long> userIds = new ArrayList<>();

@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@ -32,6 +33,7 @@ import java.util.concurrent.TimeUnit;
@RequestMapping("/user") @RequestMapping("/user")
@RequiredArgsConstructor @RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户登陆注册相关接口") @Tag(name = "用户管理", description = "用户登陆注册相关接口")
@Slf4j
public class UserLoginController { public class UserLoginController {
private final UserLoginService userLoginService; private final UserLoginService userLoginService;
private final RedisUtil redisUtil; private final RedisUtil redisUtil;
@ -62,10 +64,28 @@ public class UserLoginController {
}) })
public Result<UserDTO> checkLogin(@RequestHeader(value = "Authorization", required = false) String accessToken, public Result<UserDTO> checkLogin(@RequestHeader(value = "Authorization", required = false) String accessToken,
@RequestHeader(value = "X-Refresh-Token", required = false) String refreshToken) { @RequestHeader(value = "X-Refresh-Token", required = false) String refreshToken) {
if (accessToken != null && accessToken.startsWith("Bearer ")) { log.info("检查登录状态 - 接收到的Authorization: {}", accessToken);
accessToken = accessToken.substring(7); log.info("检查登录状态 - 接收到的X-Refresh-Token: {}", refreshToken);
// 检查token是否为空
if (accessToken == null || accessToken.isEmpty()) {
log.error("登录验证失败 - accessToken为空");
return Result.fail("未提供访问令牌");
}
if (refreshToken == null || refreshToken.isEmpty()) {
log.error("登录验证失败 - refreshToken为空");
return Result.fail("未提供刷新令牌");
}
try {
UserDTO userDTO = userLoginService.checkLogin(accessToken, refreshToken);
log.info("登录验证成功 - 用户ID: {}, 用户名: {}", userDTO.getUserId(), userDTO.getUsername());
return Result.success(userDTO);
} catch (Exception e) {
log.error("登录验证失败 - 错误信息: {}", e.getMessage(), e);
return Result.fail(e.getMessage());
} }
return Result.success(userLoginService.checkLogin(accessToken, refreshToken));
} }
@PostMapping("/register") @PostMapping("/register")

@ -1,4 +1,3 @@
##本地开发环境
#lj: #lj:
# db: # db:
# host: 192.168.59.129 # host: 192.168.59.129
@ -49,6 +48,6 @@ lj:
username: guest username: guest
password: guest password: guest
minio: minio:
endpoint: http://localhost:9005 endpoint: http://localhost:9000
accessKey: leezt accessKey: root
secretKey: lzt264610 secretKey: 12345678

@ -49,6 +49,10 @@ spring:
concurrency: 5 concurrency: 5
max-concurrency: 10 max-concurrency: 10
prefetch: 1 prefetch: 1
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
# minio配置 # minio配置
minio: minio:
endpoint: ${lj.minio.endpoint} endpoint: ${lj.minio.endpoint}

@ -72,9 +72,9 @@
} }
}, },
"node_modules/@babel/compat-data": { "node_modules/@babel/compat-data": {
"version": "7.27.2", "version": "7.27.5",
"resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.27.2.tgz", "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.27.5.tgz",
"integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -82,22 +82,22 @@
} }
}, },
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.27.1", "version": "7.27.4",
"resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.27.4.tgz",
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.1", "@babel/generator": "^7.27.3",
"@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.27.1", "@babel/helper-module-transforms": "^7.27.3",
"@babel/helpers": "^7.27.1", "@babel/helpers": "^7.27.4",
"@babel/parser": "^7.27.1", "@babel/parser": "^7.27.4",
"@babel/template": "^7.27.1", "@babel/template": "^7.27.2",
"@babel/traverse": "^7.27.1", "@babel/traverse": "^7.27.4",
"@babel/types": "^7.27.1", "@babel/types": "^7.27.3",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2", "gensync": "^1.0.0-beta.2",
@ -113,9 +113,9 @@
} }
}, },
"node_modules/@babel/eslint-parser": { "node_modules/@babel/eslint-parser": {
"version": "7.27.1", "version": "7.27.5",
"resolved": "https://registry.npmmirror.com/@babel/eslint-parser/-/eslint-parser-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/eslint-parser/-/eslint-parser-7.27.5.tgz",
"integrity": "sha512-q8rjOuadH0V6Zo4XLMkJ3RMQ9MSBqwaDByyYB0izsYdaIWGNLmEblbCOf1vyFHICcg16CD7Fsi51vcQnYxmt6Q==", "integrity": "sha512-HLkYQfRICudzcOtjGwkPvGc5nF1b4ljLZh1IRDj50lRZ718NAKVgQpIAUX8bfg6u/yuSKY3L7E0YzIV+OxrB8Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -132,14 +132,14 @@
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.27.1", "version": "7.27.5",
"resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.27.5.tgz",
"integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.27.1", "@babel/parser": "^7.27.5",
"@babel/types": "^7.27.1", "@babel/types": "^7.27.3",
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2" "jsesc": "^3.0.2"
@ -149,13 +149,13 @@
} }
}, },
"node_modules/@babel/helper-annotate-as-pure": { "node_modules/@babel/helper-annotate-as-pure": {
"version": "7.27.1", "version": "7.27.3",
"resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
"integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.27.1" "@babel/types": "^7.27.3"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -264,15 +264,15 @@
} }
}, },
"node_modules/@babel/helper-module-transforms": { "node_modules/@babel/helper-module-transforms": {
"version": "7.27.1", "version": "7.27.3",
"resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
"integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-module-imports": "^7.27.1", "@babel/helper-module-imports": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1",
"@babel/traverse": "^7.27.1" "@babel/traverse": "^7.27.3"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -398,14 +398,14 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.27.1", "version": "7.27.4",
"resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.27.4.tgz",
"integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/template": "^7.27.1", "@babel/template": "^7.27.2",
"@babel/types": "^7.27.1" "@babel/types": "^7.27.3"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -506,12 +506,12 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.27.2", "version": "7.27.5",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.2.tgz", "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.5.tgz",
"integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.27.1" "@babel/types": "^7.27.3"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@ -816,9 +816,9 @@
} }
}, },
"node_modules/@babel/plugin-transform-block-scoping": { "node_modules/@babel/plugin-transform-block-scoping": {
"version": "7.27.1", "version": "7.27.5",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.5.tgz",
"integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", "integrity": "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -904,9 +904,9 @@
} }
}, },
"node_modules/@babel/plugin-transform-destructuring": { "node_modules/@babel/plugin-transform-destructuring": {
"version": "7.27.1", "version": "7.27.3",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz",
"integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1252,15 +1252,15 @@
} }
}, },
"node_modules/@babel/plugin-transform-object-rest-spread": { "node_modules/@babel/plugin-transform-object-rest-spread": {
"version": "7.27.2", "version": "7.27.3",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.2.tgz", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz",
"integrity": "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g==", "integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.27.3",
"@babel/plugin-transform-parameters": "^7.27.1" "@babel/plugin-transform-parameters": "^7.27.1"
}, },
"engines": { "engines": {
@ -1388,9 +1388,9 @@
} }
}, },
"node_modules/@babel/plugin-transform-regenerator": { "node_modules/@babel/plugin-transform-regenerator": {
"version": "7.27.1", "version": "7.27.5",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.5.tgz",
"integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", "integrity": "sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1437,9 +1437,9 @@
} }
}, },
"node_modules/@babel/plugin-transform-runtime": { "node_modules/@babel/plugin-transform-runtime": {
"version": "7.27.1", "version": "7.27.4",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.27.4.tgz",
"integrity": "sha512-TqGF3desVsTcp3WrJGj4HfKokfCXCLcHpt4PJF0D8/iT6LPd9RS82Upw3KPeyr6B22Lfd3DO8MVrmp0oRkUDdw==", "integrity": "sha512-D68nR5zxU64EUzV8i7T3R5XP0Xhrou/amNnddsRQssx6GrTLdZl1rLxyjtVZBd+v/NVX4AbTPOB5aU8thAZV1A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1705,9 +1705,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.27.1", "version": "7.27.4",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.4.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "integrity": "sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1730,17 +1730,17 @@
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.27.1", "version": "7.27.4",
"resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.27.4.tgz",
"integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.1", "@babel/generator": "^7.27.3",
"@babel/parser": "^7.27.1", "@babel/parser": "^7.27.4",
"@babel/template": "^7.27.1", "@babel/template": "^7.27.2",
"@babel/types": "^7.27.1", "@babel/types": "^7.27.3",
"debug": "^4.3.1", "debug": "^4.3.1",
"globals": "^11.1.0" "globals": "^11.1.0"
}, },
@ -1749,9 +1749,9 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.27.1", "version": "7.27.3",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.1.tgz", "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.3.tgz",
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.27.1", "@babel/helper-string-parser": "^7.27.1",
@ -1840,21 +1840,21 @@
} }
}, },
"node_modules/@floating-ui/core": { "node_modules/@floating-ui/core": {
"version": "1.7.0", "version": "1.7.1",
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.0.tgz", "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.1.tgz",
"integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/utils": "^0.2.9" "@floating-ui/utils": "^0.2.9"
} }
}, },
"node_modules/@floating-ui/dom": { "node_modules/@floating-ui/dom": {
"version": "1.7.0", "version": "1.7.1",
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.0.tgz", "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.1.tgz",
"integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/core": "^1.7.0", "@floating-ui/core": "^1.7.1",
"@floating-ui/utils": "^0.2.9" "@floating-ui/utils": "^0.2.9"
} }
}, },
@ -2186,9 +2186,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/express": { "node_modules/@types/express": {
"version": "4.17.21", "version": "4.17.22",
"resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.21.tgz", "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.22.tgz",
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2256,9 +2256,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.17.16", "version": "4.17.17",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.16.tgz", "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.17.tgz",
"integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/lodash-es": { "node_modules/@types/lodash-es": {
@ -2285,9 +2285,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.15.17", "version": "22.15.29",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.15.17.tgz", "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.15.29.tgz",
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2319,9 +2319,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.9.18", "version": "6.14.0",
"resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.18.tgz", "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2876,53 +2876,53 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.5.13", "version": "3.5.16",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.16.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", "integrity": "sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.25.3", "@babel/parser": "^7.27.2",
"@vue/shared": "3.5.13", "@vue/shared": "3.5.16",
"entities": "^4.5.0", "entities": "^4.5.0",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.1"
} }
}, },
"node_modules/@vue/compiler-dom": { "node_modules/@vue/compiler-dom": {
"version": "3.5.13", "version": "3.5.16",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.16.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", "integrity": "sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.5.13", "@vue/compiler-core": "3.5.16",
"@vue/shared": "3.5.13" "@vue/shared": "3.5.16"
} }
}, },
"node_modules/@vue/compiler-sfc": { "node_modules/@vue/compiler-sfc": {
"version": "3.5.13", "version": "3.5.16",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.16.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", "integrity": "sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.25.3", "@babel/parser": "^7.27.2",
"@vue/compiler-core": "3.5.13", "@vue/compiler-core": "3.5.16",
"@vue/compiler-dom": "3.5.13", "@vue/compiler-dom": "3.5.16",
"@vue/compiler-ssr": "3.5.13", "@vue/compiler-ssr": "3.5.16",
"@vue/shared": "3.5.13", "@vue/shared": "3.5.16",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.11", "magic-string": "^0.30.17",
"postcss": "^8.4.48", "postcss": "^8.5.3",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.1"
} }
}, },
"node_modules/@vue/compiler-ssr": { "node_modules/@vue/compiler-ssr": {
"version": "3.5.13", "version": "3.5.16",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.16.tgz",
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", "integrity": "sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.13", "@vue/compiler-dom": "3.5.16",
"@vue/shared": "3.5.13" "@vue/shared": "3.5.16"
} }
}, },
"node_modules/@vue/component-compiler-utils": { "node_modules/@vue/component-compiler-utils": {
@ -3029,53 +3029,53 @@
} }
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.13", "version": "3.5.16",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz", "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.16.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", "integrity": "sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/shared": "3.5.13" "@vue/shared": "3.5.16"
} }
}, },
"node_modules/@vue/runtime-core": { "node_modules/@vue/runtime-core": {
"version": "3.5.13", "version": "3.5.16",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz", "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.16.tgz",
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", "integrity": "sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.13", "@vue/reactivity": "3.5.16",
"@vue/shared": "3.5.13" "@vue/shared": "3.5.16"
} }
}, },
"node_modules/@vue/runtime-dom": { "node_modules/@vue/runtime-dom": {
"version": "3.5.13", "version": "3.5.16",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.16.tgz",
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", "integrity": "sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.13", "@vue/reactivity": "3.5.16",
"@vue/runtime-core": "3.5.13", "@vue/runtime-core": "3.5.16",
"@vue/shared": "3.5.13", "@vue/shared": "3.5.16",
"csstype": "^3.1.3" "csstype": "^3.1.3"
} }
}, },
"node_modules/@vue/server-renderer": { "node_modules/@vue/server-renderer": {
"version": "3.5.13", "version": "3.5.16",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz", "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.16.tgz",
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", "integrity": "sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.5.13", "@vue/compiler-ssr": "3.5.16",
"@vue/shared": "3.5.13" "@vue/shared": "3.5.16"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "3.5.13" "vue": "3.5.16"
} }
}, },
"node_modules/@vue/shared": { "node_modules/@vue/shared": {
"version": "3.5.13", "version": "3.5.16",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz", "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.16.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "integrity": "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/vue-loader-v15": { "node_modules/@vue/vue-loader-v15": {
@ -3998,9 +3998,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.5", "version": "4.25.0",
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.24.5.tgz", "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.25.0.tgz",
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -4018,8 +4018,8 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001716", "caniuse-lite": "^1.0.30001718",
"electron-to-chromium": "^1.5.149", "electron-to-chromium": "^1.5.160",
"node-releases": "^2.0.19", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3" "update-browserslist-db": "^1.1.3"
}, },
@ -4166,9 +4166,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001718", "version": "1.0.30001721",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
"integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -5035,9 +5035,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.1",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5426,16 +5426,16 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.152", "version": "1.5.164",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz", "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.164.tgz",
"integrity": "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==", "integrity": "sha512-TXBrF2aZenRjY3wbj5Yc0mZn43lMiSHNkzwPkIxx+vWUB35Kf8Gm/uOYmOJFNQ7SUwWAinbfxX73ANIud65wSA==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/element-plus": { "node_modules/element-plus": {
"version": "2.9.10", "version": "2.9.11",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.9.10.tgz", "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.9.11.tgz",
"integrity": "sha512-W2v9jWnm1kl/zm4bSvCh8aFCVlxvhG3fmqiDZwyd6WQiWGE595J/mpjcCggEr+49QDgIymhXrpPMOPPSARUbng==", "integrity": "sha512-x4L/6YC8de4JtuE3vpaEugJdQIeHQaHtIYKyk67IeF6dTIiVax45aX4nWOygnh+xX+0gTvL6xO+9BZhPA3G82w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^3.4.1", "@ctrl/tinycolor": "^3.4.1",
@ -7616,7 +7616,7 @@
}, },
"node_modules/jparticles": { "node_modules/jparticles": {
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/jparticles/-/jparticles-3.5.0.tgz", "resolved": "https://registry.npmmirror.com/jparticles/-/jparticles-3.5.0.tgz",
"integrity": "sha512-qUKP56Xqh2G7TqFKHMPDYzfZKkvsbLGJu+xJI4dh0YGZL26zOCUVV31MkkPWmfd6SaST23mhSvvvEArFd8yApQ==", "integrity": "sha512-qUKP56Xqh2G7TqFKHMPDYzfZKkvsbLGJu+xJI4dh0YGZL26zOCUVV31MkkPWmfd6SaST23mhSvvvEArFd8yApQ==",
"license": "MIT" "license": "MIT"
}, },
@ -9148,9 +9148,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.3", "version": "8.5.4",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.4.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -9167,7 +9167,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.8", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@ -10661,9 +10661,9 @@
} }
}, },
"node_modules/shell-quote": { "node_modules/shell-quote": {
"version": "1.8.2", "version": "1.8.3",
"resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.2.tgz", "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -11179,9 +11179,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -11189,14 +11189,14 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.39.0", "version": "5.40.0",
"resolved": "https://registry.npmmirror.com/terser/-/terser-5.39.0.tgz", "resolved": "https://registry.npmmirror.com/terser/-/terser-5.40.0.tgz",
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "integrity": "sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2", "acorn": "^8.14.0",
"commander": "^2.20.0", "commander": "^2.20.0",
"source-map-support": "~0.5.20" "source-map-support": "~0.5.20"
}, },
@ -11660,16 +11660,16 @@
} }
}, },
"node_modules/vue": { "node_modules/vue": {
"version": "3.5.13", "version": "3.5.16",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.16.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", "integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.13", "@vue/compiler-dom": "3.5.16",
"@vue/compiler-sfc": "3.5.13", "@vue/compiler-sfc": "3.5.16",
"@vue/runtime-dom": "3.5.13", "@vue/runtime-dom": "3.5.16",
"@vue/server-renderer": "3.5.13", "@vue/server-renderer": "3.5.16",
"@vue/shared": "3.5.13" "@vue/shared": "3.5.16"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": "*"
@ -11870,9 +11870,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.2", "version": "2.4.4",
"resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.2.tgz", "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.4.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -11911,9 +11911,9 @@
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.99.8", "version": "5.99.9",
"resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.99.8.tgz", "resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.99.9.tgz",
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -12246,9 +12246,9 @@
} }
}, },
"node_modules/webpack-sources": { "node_modules/webpack-sources": {
"version": "3.2.3", "version": "3.3.2",
"resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz", "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.3.2.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "integrity": "sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

@ -4,8 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>logo.ico">
<title>Responsive Login And Registration</title> <title>珞珈岛</title>
<link href='<%= BASE_URL %>css/boxicons.min.css' rel='stylesheet'> <link href='<%= BASE_URL %>css/boxicons.min.css' rel='stylesheet'>
<link rel="stylesheet" href="<%= BASE_URL %>css/style.css"> <link rel="stylesheet" href="<%= BASE_URL %>css/style.css">
</head> </head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

@ -23,11 +23,11 @@ function getRandomInt(min, max) {
onMounted(() => { onMounted(() => {
new Snow('#snow', new Snow('#snow',
{ {
num: getRandomInt(1,4), num: getRandomInt(1,2),
color: '#fff', color: '#fff',
maxR: 3, maxR: 3,
minR: 12, minR: 12,
maxSpeed: 0.4, maxSpeed: 0.3,
minSpeed: 0.1, minSpeed: 0.1,
swing: true, swing: true,
swingProbability: 0.1, swingProbability: 0.1,
@ -46,6 +46,21 @@ onMounted(() => {
text-align: center; text-align: center;
color: #2c3e50; color: #2c3e50;
margin-top: 60px; margin-top: 60px;
position: relative;
z-index: 1;
}
/* 背景图层 */
#app::before {
content: "";
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
width: 100vw;
height: 100vh;
z-index: -1;
background: url('./assets/background.jpg') no-repeat center center / cover;
opacity: 0.3; /* 不透明度,可调整 */
pointer-events: none;
} }
.snow { .snow {
position: fixed; position: fixed;

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 KiB

File diff suppressed because one or more lines are too long

@ -97,11 +97,18 @@ const defaultAvatar = require("@/assets/default-avatar/boy_1.png");
// //
const isLoggedIn = computed(() => userStore.isLoggedIn); const isLoggedIn = computed(() => userStore.isLoggedIn);
const userInfo = computed(() => { const userInfo = computed(() => {
return userStore.userInfo; // userStore
const info = userStore.userInfo;
// userStorelocalStorage
if (!info.avatar) {
info.avatar = localStorage.getItem('avatar') || '';
}
return info;
}); });
const showAdmin = computed(() => { const showAdmin = computed(() => {
const isAdmin = userStore.isLoggedIn && (userInfo.value.role >= 2); const isAdmin = userStore.isLoggedIn && (userInfo.value.role >= 2);
console.log('检查管理员权限:', userStore.isLoggedIn, userInfo.value.role, isAdmin);
return isAdmin; return isAdmin;
}); });
@ -153,16 +160,47 @@ const hideDropdown = () => {
}, 400); }, 400);
}; };
// //
onMounted(() => { onMounted(async () => {
if (userStore.isLoggedIn) { if (userStore.isLoggedIn) {
console.log('Header组件挂载: 检测到用户已登录:', userStore.userInfo.username, '角色:', userStore.userInfo.role); // token
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
if (!accessToken || !refreshToken) {
console.warn('没有找到token跳过获取用户信息');
return;
}
try {
//
const response = await request.get('/user/info/getuserinfo');
if (response && response.code === 200) {
userStore.updateUserInfo({
userid: response.data.id,
username: response.data.username,
avatar: response.data.avatar,
email: response.data.email,
phone: response.data.phone,
college: response.data.college,
gender: response.data.gender,
role: response.data.role,
status: response.data.status
});
}
} catch (error) {
console.error('获取用户信息失败:', error);
// 401tokentoken
if (error.response && (error.response.status === 401 ||
(error.response.status === 500 && error.response.data?.message?.includes('token')))) {
console.warn('Token可能无效不再尝试获取用户信息');
}
}
} }
}); });
// //
watch(() => userStore.userInfo, (newInfo) => { watch(() => userStore.userInfo, () => {
console.log('用户信息更新:', newInfo.username, '角色:', newInfo.role);
}, { deep: true }); }, { deep: true });
</script> </script>
@ -173,12 +211,13 @@ watch(() => userStore.userInfo, (newInfo) => {
align-items: center; align-items: center;
padding: 0 20px; /* 增加左右内边距,避免内容过于靠边 */ padding: 0 20px; /* 增加左右内边距,避免内容过于靠边 */
border-bottom: 2px solid #e0e0e0; border-bottom: 2px solid #e0e0e0;
background: white; background: rgba(255, 255, 255, 0.5);
position: fixed; position: fixed;
top: 0; top: 0;
width: 100%; width: 100%;
z-index: 1000; z-index: 1000;
box-sizing: border-box; /* 确保内边距不会影响宽度 */ box-sizing: border-box; /* 确保内边距不会影响宽度 */
} }
/* Logo 区域样式 */ /* Logo 区域样式 */

@ -205,14 +205,47 @@ async function login() {
userData.userName = userData.username; userData.userName = userData.username;
userData.userid = userData.id; userData.userid = userData.id;
// storelocalStorage
userStore.login(userData); userStore.login(userData);
//
try {
const userInfoResponse = await request.get('/user/info/getuserinfo', {
params: { userId: userData.id },
headers: {
'Authorization': `Bearer ${userData.accessToken}`,
'X-Refresh-Token': userData.refreshToken
}
});
if (userInfoResponse && userInfoResponse.code === 200) {
//
userStore.updateUserInfo({
userid: userInfoResponse.data.id || userData.id,
username: userInfoResponse.data.username,
avatar: userInfoResponse.data.avatar,
email: userInfoResponse.data.email,
phone: userInfoResponse.data.phone,
college: userInfoResponse.data.college,
gender: userInfoResponse.data.gender,
role: userInfoResponse.data.role,
status: userInfoResponse.data.status
});
}
} catch (error) {
//
}
ElMessage({ ElMessage({
message: '登录成功', message: '登录成功',
type: 'success', type: 'success',
duration: 500 duration: 500
}); });
emit('LoginSuccess'); // emit('LoginSuccess'); //
// 使
window.location.reload();
} else { } else {
// //
ElMessage({ ElMessage({

@ -2,19 +2,20 @@
<div class="notice-board"> <div class="notice-board">
<h2 class="notice-title">公告栏</h2> <h2 class="notice-title">公告栏</h2>
<div class="notice-list"> <div class="notice-list">
<!-- 左箭头按钮 -->
<button class="arrow-button left-arrow" @click="prevImage"></button> <button class="arrow-button left-arrow" @click="prevImage"></button>
<div class="notice-item"> <div class="notice-item">
<img <img
v-if="images.length"
:src="images[currentIndex].src" :src="images[currentIndex].src"
:alt="images[currentIndex].alt" :alt="images[currentIndex].alt"
class="notice-image" class="notice-image"
style="cursor:pointer"
@click="goToPostDetail(images[currentIndex].postId)"
/> />
<div v-else class="notice-empty">暂无活动公告</div>
</div> </div>
<!-- 右箭头按钮 -->
<button class="arrow-button right-arrow" @click="nextImage"></button> <button class="arrow-button right-arrow" @click="nextImage"></button>
</div> </div>
<!-- 圆形按钮 -->
<div class="indicator-container"> <div class="indicator-container">
<button <button
v-for="(image, index) in images" v-for="(image, index) in images"
@ -29,26 +30,54 @@
<script setup lang="js" name="NoticeBoard"> <script setup lang="js" name="NoticeBoard">
import { ref, onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted } from 'vue';
import request from '@/utils/request';
import { useRouter } from 'vue-router';
const images = ref([ const images = ref([]);
{ src: require('@/assets/whu1.jpg'), alt: '公告图片1' },
{ src: require('@/assets/whu2.jpg'), alt: '公告图片2' },
{ src: require('@/assets/whu3.jpg'), alt: '公告图片3' },
{ src: require('@/assets/whu4.jpg'), alt: '公告图片4' },
{ src: require('@/assets/whu5.jpg'), alt: '公告图片5' },
]);
const currentIndex = ref(0); const currentIndex = ref(0);
const router = useRouter();
// 5
const fetchImages = async () => {
try {
// size
const lastVal = Date.now();
const offset = 0;
const size = 5;
let url = `/post/list?lastVal=${lastVal}&offset=${offset}&size=${size}`;
url += `&categoryId=1`;
const res = await request.get(url);
if (res.code === 200 && res.data.records) {
images.value = res.data.records
.filter(post => post.image) //
.map(post => ({
src: post.image,
alt: post.title || '校园活动',
postId: post.id
}));
}
} catch (e) {
images.value = [];
}
};
//
const goToPostDetail = (postId) => {
if (postId) {
router.push({ name: 'PostDetail', params: { id: postId } });
}
};
// //
const nextImage = () => { const nextImage = () => {
currentIndex.value = (currentIndex.value + 1) % images.value.length; if (images.value.length)
currentIndex.value = (currentIndex.value + 1) % images.value.length;
}; };
// //
const prevImage = () => { const prevImage = () => {
currentIndex.value = if (images.value.length)
(currentIndex.value - 1 + images.value.length) % images.value.length; currentIndex.value = (currentIndex.value - 1 + images.value.length) % images.value.length;
}; };
// //
@ -59,7 +88,8 @@ const goToImage = (index) => {
// //
let interval = null; let interval = null;
onMounted(() => { onMounted(async () => {
await fetchImages();
interval = setInterval(nextImage, 5000); interval = setInterval(nextImage, 5000);
}); });
@ -71,16 +101,17 @@ onUnmounted(() => {
<style scoped> <style scoped>
.notice-board { .notice-board {
position: relative; position: relative;
background-color: #5aa76f; background-color: rgb(90, 167, 111,0.8);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: flex-start; /* 顶部对齐 */ justify-content: flex-start;
width: 500px; /* 固定宽度 */ width: 100%;
height: 350px; /* 固定高度 */ height: 100%;
overflow: hidden; /* 防止图片溢出 */ min-width: 220px;
padding: 10px; /* 内边距 */ max-width: 700px;
box-sizing: border-box; /* 包括内边距在宽高内 */ overflow: hidden;
box-sizing: border-box;
} }
.notice-title { .notice-title {
@ -97,20 +128,34 @@ onUnmounted(() => {
justify-content: center; justify-content: center;
position: relative; position: relative;
width: 100%; width: 100%;
height: calc(100% - 60px); /* 除去标题和指示器的高度 */ height: calc(100% - 60px); /* 保证展示区高度 */
min-height: 120px;
} }
.notice-item { .notice-item {
text-align: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex;
align-items: center;
justify-content: center;
} }
.notice-image { .notice-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; /* 让图片适应公告栏 */ object-fit: cover;
border-radius: 8px; border-radius: 8px;
display: block;
}
.notice-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 36px;
} }
.arrow-button { .arrow-button {
@ -144,7 +189,7 @@ onUnmounted(() => {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-top: 10px; margin-top: 10px;
gap: 8px; /* 按钮之间的间距 */ gap: 8px;
} }
.indicator-button { .indicator-button {

@ -1,7 +1,7 @@
<template> <template>
<div class="post-page-container"> <div class="post-page-container">
<!-- 左侧分类按钮 --> <!-- 顶部横向分类按钮 -->
<div class="category-buttons"> <div class="category-buttons-horizontal" ref="categoryBarRef">
<button <button
v-for="(category, index) in categories" v-for="(category, index) in categories"
:key="index" :key="index"
@ -13,72 +13,114 @@
</button> </button>
</div> </div>
<!-- 右侧帖子列表 --> <!-- 帖子列表 -->
<div class="post-list"> <div class="post-list">
<div <div
v-for="(post, index) in filteredPosts" v-for="(post, index) in postListStore.posts"
:key="index" :key="index"
class="post-item" class="post-item"
@click="goToPostDetail(post.id)" @click="goToPostDetail(post.id)"
> >
<!-- 头像+用户名 -->
<div class="post-header"> <div class="post-header">
<img :src="post.avatar" :alt="头像" class="post-avatar" /> <img
<h3 class="post-title">{{ post.title }}</h3> :src="(post.userName === '匿名用户' || post?.status === 1)
? require('@/assets/default-avatar/boy_4.png')
: post.avatar || require('@/assets/default-avatar/boy_1.png')"
alt="头像"
class="post-avatar"
/>
<span class="post-username">{{ post.userName }}</span>
</div> </div>
<p class="post-summary">{{ post.summary }}</p> <!-- 标题 -->
<div class="post-title">{{ post.title }}</div>
<!-- 图片 -->
<div v-if="post.image" class="post-image-wrapper">
<img :src="post.image" alt="帖子图片" class="post-image" />
</div>
<!-- 统计信息 -->
<div class="post-stats"> <div class="post-stats">
<span>👁 {{ post.viewCount }}</span> <span>👁 {{ post.viewCount }}</span>
<span>🗨 {{ post.comments }}</span> <span>🗨 {{ post.comments }}</span>
<span> {{ post.likes }}</span> <span> {{ post.likes }}</span>
</div> </div>
<!-- 发布时间 -->
<div class="post-time">
{{ post.createTime ? post.createTime.slice(0, 10) : '' }}
</div>
</div> </div>
</div> </div>
<!-- 回到顶部按钮 -->
<button
v-if="showBackToTop"
class="back-to-top-btn"
@click="scrollToTop"
>
🔝
</button>
</div> </div>
</template> </template>
<script setup lang="js" name="PostPage"> <script setup lang="js" name="PostPage">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'; import { ref, onMounted, onUnmounted, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { usePostListStore } from '@/stores/postlist.js'; import { usePostListStore } from '@/stores/postlist.js';
const categories = ref(['全部', '学习', '娱乐', '二手交易']); const categoryMap = [
{ name: '全部', id: null },
{ name: '校园活动', id: 1 },
{ name: '学习', id: 2 },
{ name: '娱乐', id: 3 },
{ name: '二手交易', id: 4 }
];
const categories = ref(categoryMap.map(item => item.name));
const selectedCategory = ref('全部'); const selectedCategory = ref('全部');
const postListStore = usePostListStore(); const postListStore = usePostListStore();
const router = useRouter(); const router = useRouter();
// const categoryBarRef = ref(null);
const filteredPosts = computed(() => { const showBackToTop = ref(false);
if (selectedCategory.value === '全部') return postListStore.posts;
return postListStore.posts.filter(post => post.category === selectedCategory.value);
});
// const selectCategory = async (categoryName) => {
const selectCategory = async (category) => { selectedCategory.value = categoryName;
selectedCategory.value = category;
postListStore.resetList(); postListStore.resetList();
await postListStore.getList({}); const categoryObj = categoryMap.find(item => item.name === categoryName);
await postListStore.getList(categoryObj ? categoryObj.id : null);
}; };
//
const goToPostDetail = (postId) => { const goToPostDetail = (postId) => {
router.push({ name: 'PostDetail', params: { id: postId } }); router.push({ name: 'PostDetail', params: { id: postId } });
}; };
//
const handleScroll = async () => { const handleScroll = async () => {
const scrollContainer = document.documentElement; const scrollContainer = document.documentElement;
//
const bar = categoryBarRef.value;
if (bar) {
const rect = bar.getBoundingClientRect();
showBackToTop.value = rect.bottom < 0;
} else {
// 200px
showBackToTop.value = scrollContainer.scrollTop > 200;
}
//
if ( if (
scrollContainer.scrollTop + window.innerHeight >= scrollContainer.scrollHeight - 10 && scrollContainer.scrollTop + window.innerHeight >= scrollContainer.scrollHeight - 10 &&
!postListStore.loading && !postListStore.loading &&
!postListStore.finished !postListStore.finished
) { ) {
await postListStore.getList({}); const categoryObj = categoryMap.find(item => item.name === selectedCategory.value);
await postListStore.getList(categoryObj ? categoryObj.id : null);
} }
}; };
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
onMounted(async () => { onMounted(async () => {
// await postListStore.getList();
await postListStore.getList({});
window.addEventListener('scroll', handleScroll); window.addEventListener('scroll', handleScroll);
}); });
@ -86,115 +128,175 @@ onUnmounted(() => {
window.removeEventListener('scroll', handleScroll); window.removeEventListener('scroll', handleScroll);
}); });
//
watch(selectedCategory, () => { watch(selectedCategory, () => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
}); });
</script> </script>
<style scoped> <style scoped>
/* 页面整体布局 */
.post-page-container { .post-page-container {
display: flex; display: flex;
flex-direction: row; background-color: rgba(249, 227, 238, 0.8);
align-items: flex-start; flex-direction: column;
padding: 20px; align-items: stretch;
padding: 5px 0;
box-sizing: border-box; box-sizing: border-box;
gap: 20px; gap: 1px;
width: 100%;
} }
/* 左侧分类按钮样式 */ /* 顶部横向分类按钮样式 */
.category-buttons { .category-buttons-horizontal {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
gap: 0; gap: 0;
width: 200px; width: 100%;
margin-bottom: 10px;
border-bottom: 1px solid #eee;
background: rgba(218, 213, 213, 0.6);
padding-bottom: 2px;
} }
.category-button { .category-button {
padding: 10px 20px; padding: 10px 24px;
font-size: 14px; font-size: 14px;
border: 1px solid #ccc; border: none;
border-bottom: none; background-color: transparent;
background-color: #f9f9f9;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s, color 0.3s; transition: background-color 0.3s, color 0.3s;
} color: #333;
border-bottom: 2px solid transparent;
.category-button:last-child { outline: none;
border-bottom: 1px solid #ccc;
} }
.category-button.active { .category-button.active {
background-color: #5aa76f; background-color: #f6fff8;
color: white; color: #5aa76f;
border-color: #5aa76f; border-bottom: 2px solid #5aa76f;
font-weight: bold;
} }
.category-button:hover { .category-button:hover {
background-color: #e0e0e0; background-color: #f0f0f0;
} }
/* 右侧帖子列表样式 */ /* 帖子列表区域占满父容器左右margin为10px */
.post-list { .post-list {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px; gap: 15px;
padding: 10px;
flex: 1; flex: 1;
width: 100%;
box-sizing: border-box;
} }
/* 帖子卡片自适应宽度和高度 */
.post-item { .post-item {
position: relative; /* 设置为相对定位 */ position: relative;
width: 300px;
height: 150px;
padding: 15px; padding: 15px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 8px; border-radius: 8px;
background-color: #ffffff; background-color: #ffffff;
cursor: pointer; cursor: pointer;
transition: box-shadow 0.3s; transition: box-shadow 0.3s;
display: flex;
flex-direction: column;
justify-content: flex-start;
box-sizing: border-box;
width: 100%;
height: 280px;
max-width: 475px;
overflow: hidden;
} }
.post-item:hover { /* 头像+用户名 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* 帖子头部样式 */
.post-header { .post-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
margin-bottom: 10px; margin-bottom: 6px;
} }
.post-avatar { .post-avatar {
width: 40px; width: 36px;
height: 40px; height: 36px;
border-radius: 50%; /* 设置为圆形 */ border-radius: 50%;
object-fit: cover; /* 确保图片按比例填充 */ object-fit: cover;
} }
.post-username {
font-size: 15px;
color: #5aa76f;
font-weight: bold;
}
/* 标题 */
.post-title { .post-title {
font-size: 18px; font-size: 18px;
text-align: left;
font-weight: bold; font-weight: bold;
margin: 0; margin: 0 0 8px 0;
color: #333;
word-break: break-all;
padding-left: 30px;
} }
/* 帖子摘要样式 */ /* 图片展示 */
.post-summary { .post-image-wrapper {
font-size: 14px; width: 100%;
color: #666; height: 160px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px; margin-bottom: 10px;
overflow: hidden;
}
.post-image {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
} }
/* 帖子统计信息样式 */ /* 帖子统计信息样式 */
.post-stats { .post-stats {
display: flex; display: flex;
gap: 15px; /* 图标之间的间距 */ gap: 15px;
font-size: 12px; font-size: 16px;
color: #999; color: #999;
position: absolute; /* 绝对定位 */ position: absolute;
bottom: 10px; /* 距离卡片底部 10px */ bottom: 5px;
left: 15px; /* 距离卡片左侧 15px */ left: 15px;
}
/* 发布时间(右下角) */
.post-time {
position: absolute;
bottom: 5px;
right: 15px;
font-size: 13px;
color: #bbb;
}
/* 回到顶部按钮样式 */
.back-to-top-btn {
position: fixed;
right: 32px;
bottom: 48px;
z-index: 999;
background: #d5e9da;
color: #fff;
border: none;
border-radius: 5px;
font-size: 36px;
box-shadow: 0 2px 8px rgba(90,167,111,0.15);
cursor: pointer;
opacity: 0.85;
transition: opacity 0.2s;
}
.back-to-top-btn:hover {
opacity: 1;
background: #388e3c;
} }
</style> </style>

@ -2,7 +2,7 @@
<div class="welcome-container"> <div class="welcome-container">
<!-- 上部分欢迎文字 --> <!-- 上部分欢迎文字 -->
<div class="welcome-header"> <div class="welcome-header">
欢迎来到珞珈岛珈人 欢迎来到珞珈岛{{ userStore.userInfo?.username ? userStore.userInfo.username : '珈人' }}
</div> </div>
<!-- 中间部分月份日期星期 --> <!-- 中间部分月份日期星期 -->
@ -33,6 +33,7 @@
<script setup lang="js" name="WelcomeCalendar"> <script setup lang="js" name="WelcomeCalendar">
import { ref } from 'vue'; import { ref } from 'vue';
import { useUserStore } from '@/stores/user';
const today = new Date(); const today = new Date();
@ -40,10 +41,31 @@ const today = new Date();
const currentMonthText = ref(['一', '月']); // const currentMonthText = ref(['一', '月']); //
const currentDay = ref(today.getDate()); // const currentDay = ref(today.getDate()); //
const currentWeekday = ref(['星', '期', '日']); // const currentWeekday = ref(['星', '期', '日']); //
const userStore = useUserStore();
// //
const isCheckedIn = ref(false); const isCheckedIn = ref(false);
//
const getUserKey = () => {
return userStore.userInfo?.userid || userStore.userInfo?.username || 'guest';
};
//
const checkToday = () => {
const todayStr = new Date().toISOString().slice(0, 10);
const userKey = getUserKey();
return localStorage.getItem(`checkin-${userKey}-${todayStr}`) === '1';
};
//
const checkIn = () => {
const todayStr = new Date().toISOString().slice(0, 10);
const userKey = getUserKey();
isCheckedIn.value = true;
localStorage.setItem(`checkin-${userKey}-${todayStr}`, '1');
};
// //
const initializeDate = () => { const initializeDate = () => {
const months = [ const months = [
@ -55,11 +77,7 @@ const initializeDate = () => {
currentMonthText.value = months[today.getMonth()].split(''); currentMonthText.value = months[today.getMonth()].split('');
currentDay.value = today.getDate(); currentDay.value = today.getDate();
currentWeekday.value = weekdays[today.getDay()].split(''); currentWeekday.value = weekdays[today.getDay()].split('');
}; isCheckedIn.value = checkToday();
//
const checkIn = () => {
isCheckedIn.value = true;
}; };
// //
@ -72,8 +90,12 @@ initializeDate();
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 300px; width: 100%;
height: 350px; height: 100%;
min-width: 180px;
min-height: 220px;
max-width: 300px;
max-height: 300px;
margin: 0 auto; margin: 0 auto;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 8px; border-radius: 8px;
@ -81,6 +103,8 @@ initializeDate();
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
background: rgba(255, 255, 255, 0.7); /* 半透明白色背景 */
backdrop-filter: blur(4px); /* 可选,增加毛玻璃效果 */
} }
.welcome-header { .welcome-header {
@ -88,7 +112,7 @@ initializeDate();
font-weight: bold; font-weight: bold;
color: #5aa76f; color: #5aa76f;
text-align: center; text-align: center;
margin-bottom: 20px; margin-bottom: 1px;
} }
.welcome-body { .welcome-body {
@ -96,7 +120,7 @@ initializeDate();
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
width: 100%; width: 100%;
margin-bottom: 20px; margin-bottom: 1px;
} }
.month-column, .month-column,
@ -104,17 +128,19 @@ initializeDate();
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
font-size: 16px; font-size: 20px;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
gap: 2px; /* 紧凑排列 */ gap: 2px;
} }
.day-row { .day-row {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 180px; /* 日期号字体放大 */ font-size: 10vw; /* 用vw自适应字体大小 */
min-font-size: 40px;
max-font-size: 120px;
font-weight: bold; font-weight: bold;
color: #5aa76f; color: #5aa76f;
flex: 1; /* 占据中间空间 */ flex: 1; /* 占据中间空间 */

@ -30,7 +30,6 @@ if (userId && username && accessToken && refreshToken) {
accessToken, accessToken,
refreshToken refreshToken
}); });
console.log('从本地存储恢复用户登录状态:', username);
} }
app.use(router); // 注册 vue-router app.use(router); // 注册 vue-router
@ -39,13 +38,24 @@ app.use(ELementPlus); // 注册 element-plus
// 挂载应用 // 挂载应用
app.mount('#app'); app.mount('#app');
// 在应用启动后异步验证登录状态,不阻塞应用启动 // 在应用启动后立即验证登录状态
setTimeout(() => { async function validateLoginStatus() {
checkLoginStatus() try {
.then(isLoggedIn => { const isLoggedIn = await checkLoginStatus();
console.log('登录状态验证完成,用户登录状态:', isLoggedIn ? '已登录' : '未登录'); console.log('登录状态验证完成,用户登录状态:', isLoggedIn ? '已登录' : '未登录');
})
.catch(error => { // 如果用户未登录但本地存储中有token清除无效的token
console.error('登录状态验证失败,但不影响应用使用:', error); if (!isLoggedIn && (localStorage.getItem('accessToken') || localStorage.getItem('refreshToken'))) {
}); console.warn('本地存储中的token无效清除token');
}, 500); localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
// 不清除用户基本信息,允许显示用户名等非敏感信息
}
} catch (error) {
console.error('登录状态验证失败:', error);
// 不影响应用使用,只在控制台显示错误
}
}
// 立即执行,但不阻塞应用启动
validateLoginStatus();

@ -142,16 +142,25 @@ router.beforeEach(async (to, from, next) => {
}); });
console.log('从localStorage恢复用户登录状态:', username); console.log('从localStorage恢复用户登录状态:', username);
// 异步验证token有效性但不阻塞导航 // 立即验证token有效性
setTimeout(() => { try {
checkLoginStatus().catch(err => { console.log('验证恢复的登录状态...');
console.error('Token验证失败但不影响当前导航:', err); const isLoggedIn = await checkLoginStatus();
}); if (!isLoggedIn) {
}, 100); console.warn('恢复的登录状态无效');
ElMessage.warning('登录已过期,请重新登录');
// 继续导航 next({ path: '/user/login', query: { redirect: to.fullPath } });
next(); return;
return; }
console.log('恢复的登录状态有效,继续导航');
next();
return;
} catch (error) {
console.error('登录状态验证失败:', error);
ElMessage.warning('登录状态验证失败,请重新登录');
next({ path: '/user/login', query: { redirect: to.fullPath } });
return;
}
} }
// 如果localStorage中没有用户信息尝试通过token验证登录状态 // 如果localStorage中没有用户信息尝试通过token验证登录状态
@ -160,15 +169,29 @@ router.beforeEach(async (to, from, next) => {
const isLoggedIn = await checkLoginStatus(); const isLoggedIn = await checkLoginStatus();
if (!isLoggedIn) { if (!isLoggedIn) {
ElMessage.warning('请先登录'); ElMessage.warning('请先登录');
next({ path: '/' }); next({ path: '/user/login', query: { redirect: to.fullPath } });
return; return;
} }
} catch (error) { } catch (error) {
console.error('登录状态验证失败:', error); console.error('登录状态验证失败:', error);
ElMessage.warning('登录状态验证失败,请重新登录'); ElMessage.warning('登录状态验证失败,请重新登录');
next({ path: '/' }); next({ path: '/user/login', query: { redirect: to.fullPath } });
return; return;
} }
} else {
// 用户已登录但仍然验证一下token有效性不阻塞导航
setTimeout(async () => {
try {
const isLoggedIn = await checkLoginStatus();
if (!isLoggedIn) {
console.warn('登录状态已失效');
// 不中断用户操作,只显示提示
ElMessage.warning('登录已过期,请重新登录');
}
} catch (error) {
console.error('登录状态验证失败:', error);
}
}, 100);
} }
} }
@ -177,7 +200,7 @@ router.beforeEach(async (to, from, next) => {
// 检查用户是否登录且是否有管理员权限 // 检查用户是否登录且是否有管理员权限
if (!userStore.isLoggedIn) { if (!userStore.isLoggedIn) {
ElMessage.error('请先登录'); ElMessage.error('请先登录');
next({ path: '/' }); next({ path: '/user/login', query: { redirect: to.fullPath } });
return; return;
} }

@ -131,7 +131,7 @@ export const usePostDetailStore = defineStore("postDetail", {
userAvatar, userAvatar,
createTime createTime
} = postRes.data; } = postRes.data;
console.log("获取帖子详情返回:", postRes.data);
// 主要帖子信息 // 主要帖子信息
this.post = { this.post = {
postId: id, postId: id,

@ -6,41 +6,27 @@ export const usePostListStore = defineStore('postList', {
posts: [], // 帖子列表 posts: [], // 帖子列表
total: 0, // 帖子总数 total: 0, // 帖子总数
page: 1, // 当前页码 page: 1, // 当前页码
pageSize: 10, // 每页帖子数 pageSize: 6, // 每页帖子数
lastVal: Date.now(), // 用于滚动分页的时间戳 lastVal: Date.now(), // 用于滚动分页的时间戳
offset: 0, // 偏移量 offset: 0, // 偏移量
loading: false, // 加载状态 loading: false, // 加载状态
finished: false, // 是否加载完全部 finished: false, // 是否加载完全部
currentCategory: null, // 当前分类id
}), }),
actions: { actions: {
setPosts(posts) { async getList(categoryId = null) {
this.posts = posts;
},
setTotal(total) {
this.total = total;
},
setPage(page) {
this.page = page;
},
setPageSize(pageSize) {
this.pageSize = pageSize;
},
addPost(post) {
this.posts.push(post);
this.total += 1; // 更新总数
},
removePost(postId) {
this.posts = this.posts.filter(post => post.id !== postId);
this.total -= 1; // 更新总数
},
async getList() {
if (this.loading || this.finished) return; if (this.loading || this.finished) return;
this.loading = true; this.loading = true;
// 记录当前分类
this.currentCategory = categoryId;
try { try {
// 保证 lastVal 不为 null // 保证 lastVal 不为 null
const lastVal = (typeof this.lastVal === 'number' && this.lastVal > 0) ? this.lastVal : Date.now(); const lastVal = (typeof this.lastVal === 'number' && this.lastVal > 0) ? this.lastVal : Date.now();
// 拼接参数到URL // 拼接参数到URL
const url = `/post/list?lastVal=${lastVal}&offset=${this.offset}&size=${this.pageSize}`; let url = `/post/list?lastVal=${lastVal}&offset=${this.offset}&size=${this.pageSize}`;
if(categoryId!== null) {
url += `&categoryId=${categoryId}`;
}
const res = await request.get(url); const res = await request.get(url);
if (res.code === 200) { if (res.code === 200) {
const { records, lastVal: newLastVal, offset: newOffset, size: newSize } = res.data; const { records, lastVal: newLastVal, offset: newOffset, size: newSize } = res.data;
@ -51,11 +37,10 @@ export const usePostListStore = defineStore('postList', {
image: post.image, image: post.image,
avatar: post.userAvatar || require('@/assets/default-avatar/boy_1.png'), avatar: post.userAvatar || require('@/assets/default-avatar/boy_1.png'),
title: post.title, title: post.title,
summary: post.content ? post.content.slice(0, 40) + (post.content.length > 40 ? '...' : '') : '',
viewCount: post.viewCount || 0, viewCount: post.viewCount || 0,
likes: post.likeCount || 0, likes: post.likeCount || 0,
comments: post.commentCount || 0, comments: post.commentCount || 0,
category: post.category || '全部', categoryId: post.categoryId,
createTime: post.createTime, createTime: post.createTime,
userName: post.userName, userName: post.userName,
})); }));
@ -65,6 +50,7 @@ export const usePostListStore = defineStore('postList', {
this.offset = newOffset; this.offset = newOffset;
this.pageSize = newSize; this.pageSize = newSize;
} }
console.log('获取帖子列表成功', this.posts);
if (!records || records.length < this.pageSize) { if (!records || records.length < this.pageSize) {
this.finished = true; // 没有更多数据 this.finished = true; // 没有更多数据
} }

@ -20,12 +20,12 @@ export const useUserStore = defineStore('user', {
this.isLoggedIn = true; this.isLoggedIn = true;
this.userInfo = { this.userInfo = {
userid: userData.userid || 0, userid: userData.userid || 0,
username: userData.userName || '', username: userData.userName || userData.username || '',
avatar: userData.avatar || '', avatar: userData.avatar || '',
email: userData.email || '', email: userData.email || '',
phone: userData.phone || '', phone: userData.phone || '',
college: userData.college || '', college: userData.college || '',
gender: userData.gender || 0, gender: userData.gender !== undefined ? userData.gender : 0,
role: userData.role || 1, role: userData.role || 1,
status: userData.status || 1 status: userData.status || 1
}; };
@ -38,19 +38,22 @@ export const useUserStore = defineStore('user', {
localStorage.setItem('refreshToken', userData.refreshToken); localStorage.setItem('refreshToken', userData.refreshToken);
} }
// 保存基本用户信息到localStorage用于页面刷新时恢复 // 保存完整用户信息到localStorage用于页面刷新时恢复
localStorage.setItem('userId', this.userInfo.userid); localStorage.setItem('userId', this.userInfo.userid);
localStorage.setItem('username', this.userInfo.username); localStorage.setItem('username', this.userInfo.username);
localStorage.setItem('avatar', this.userInfo.avatar || ''); localStorage.setItem('avatar', this.userInfo.avatar || '');
localStorage.setItem('role', this.userInfo.role); localStorage.setItem('role', this.userInfo.role);
localStorage.setItem('email', this.userInfo.email || '');
console.log('用户登录成功,状态已更新:', this.userInfo); localStorage.setItem('phone', this.userInfo.phone || '');
localStorage.setItem('college', this.userInfo.college || '');
localStorage.setItem('gender', this.userInfo.gender !== undefined ? String(this.userInfo.gender) : '0');
}, },
// 更新用户信息 // 更新用户信息
updateUserInfo(userData) { updateUserInfo(userData) {
this.userInfo = { this.userInfo = {
...this.userInfo, ...this.userInfo,
userid: userData.userid || this.userInfo.userid,
username: userData.username || this.userInfo.username, username: userData.username || this.userInfo.username,
avatar: userData.avatar || this.userInfo.avatar, avatar: userData.avatar || this.userInfo.avatar,
email: userData.email || this.userInfo.email, email: userData.email || this.userInfo.email,
@ -62,10 +65,14 @@ export const useUserStore = defineStore('user', {
}; };
// 更新localStorage中的用户信息 // 更新localStorage中的用户信息
localStorage.setItem('userId', this.userInfo.userid);
localStorage.setItem('username', this.userInfo.username); localStorage.setItem('username', this.userInfo.username);
localStorage.setItem('avatar', this.userInfo.avatar || ''); localStorage.setItem('avatar', this.userInfo.avatar || '');
localStorage.setItem('role', this.userInfo.role);
console.log('用户信息已更新:', this.userInfo); localStorage.setItem('email', this.userInfo.email || '');
localStorage.setItem('phone', this.userInfo.phone || '');
localStorage.setItem('college', this.userInfo.college || '');
localStorage.setItem('gender', this.userInfo.gender !== undefined ? String(this.userInfo.gender) : '0');
}, },
logout() { logout() {
@ -89,8 +96,10 @@ export const useUserStore = defineStore('user', {
localStorage.removeItem('username'); localStorage.removeItem('username');
localStorage.removeItem('avatar'); localStorage.removeItem('avatar');
localStorage.removeItem('role'); localStorage.removeItem('role');
localStorage.removeItem('email');
console.log('用户已登出'); localStorage.removeItem('phone');
}, localStorage.removeItem('college');
}, localStorage.removeItem('gender');
}
}
}); });

@ -13,49 +13,105 @@ export async function checkLoginStatus() {
// 如果没有token直接返回未登录 // 如果没有token直接返回未登录
if (!accessToken || !refreshToken) { if (!accessToken || !refreshToken) {
console.log('本地没有找到token无需验证登录状态'); console.log('没有找到token用户未登录');
return false; return false;
} }
// 检查token格式是否正确 // 检查token格式是否正确
if (!isValidToken(accessToken) || !isValidToken(refreshToken)) { if (!isValidToken(accessToken) || !isValidToken(refreshToken)) {
console.log('Token格式不正确清除无效token'); console.log('token格式不正确清除token');
clearAuthTokens(); clearAuthTokens();
return false; return false;
} }
try { try {
console.log('正在验证登录状态...'); console.log('正在验证登录状态...');
// 确保accessToken有Bearer前缀
const tokenWithPrefix = accessToken.startsWith('Bearer ') ? accessToken : `Bearer ${accessToken}`;
// 使用axios直接发送请求避免request拦截器中可能的循环依赖 // 使用axios直接发送请求避免request拦截器中可能的循环依赖
const response = await axios.post('/user/check-login', null, { const response = await axios.post('/user/check-login', null, {
headers: { headers: {
'Authorization': `Bearer ${accessToken}`, 'Authorization': tokenWithPrefix,
'X-Refresh-Token': refreshToken 'X-Refresh-Token': refreshToken
}, },
// 设置超时时间,避免长时间等待 // 设置超时时间,避免长时间等待
timeout: 5000 timeout: 5000
}); });
if (response.data && response.data.code === 200 && response.data.data) { if (response.data && response.data.code === 200 && response.data.data) {
console.log('登录状态有效,用户信息:', response.data.data); console.log('登录状态有效');
// 获取用户数据
const userData = response.data.data;
// 更新用户状态 // 更新用户状态
const userStore = useUserStore(); const userStore = useUserStore();
// 使用后端返回的用户数据更新前端状态
userStore.login({ userStore.login({
userid: response.data.data.userId, userid: userData.userId,
userName: response.data.data.username, userName: userData.username,
avatar: response.data.data.avatar, avatar: userData.avatar,
role: response.data.data.role, role: userData.role || 1,
accessToken: response.data.data.accessToken || accessToken, accessToken: userData.accessToken || accessToken,
refreshToken: response.data.data.refreshToken || refreshToken refreshToken: userData.refreshToken || refreshToken
}); });
// 如果返回了新的token更新本地存储 // 如果返回了新的token更新本地存储
if (response.data.data.accessToken) { if (userData.accessToken) {
localStorage.setItem('accessToken', response.data.data.accessToken); console.log('更新accessToken');
localStorage.setItem('accessToken', userData.accessToken);
} }
if (response.data.data.refreshToken) { if (userData.refreshToken) {
localStorage.setItem('refreshToken', response.data.data.refreshToken); console.log('更新refreshToken');
localStorage.setItem('refreshToken', userData.refreshToken);
}
// 获取完整的用户信息
try {
// 确保使用最新的token
const currentAccessToken = userData.accessToken || accessToken;
const tokenWithPrefix = currentAccessToken.startsWith('Bearer ') ? currentAccessToken : `Bearer ${currentAccessToken}`;
const userInfoResponse = await axios.get('/user/info/getuserinfo', {
headers: {
'Authorization': tokenWithPrefix,
'X-Refresh-Token': userData.refreshToken || refreshToken
}
});
if (userInfoResponse.data && userInfoResponse.data.code === 200 && userInfoResponse.data.data) {
const fullUserInfo = userInfoResponse.data.data;
// 更新用户完整信息
userStore.updateUserInfo({
userid: fullUserInfo.id || userData.userId,
username: fullUserInfo.username,
avatar: fullUserInfo.avatar,
email: fullUserInfo.email,
phone: fullUserInfo.phone,
college: fullUserInfo.college,
gender: fullUserInfo.gender,
role: fullUserInfo.role,
status: fullUserInfo.status
});
// 确保localStorage中也有完整的用户信息
localStorage.setItem('userId', fullUserInfo.id || userData.userId);
localStorage.setItem('username', fullUserInfo.username);
localStorage.setItem('avatar', fullUserInfo.avatar || '');
localStorage.setItem('role', fullUserInfo.role);
localStorage.setItem('email', fullUserInfo.email || '');
localStorage.setItem('phone', fullUserInfo.phone || '');
localStorage.setItem('college', fullUserInfo.college || '');
localStorage.setItem('gender', fullUserInfo.gender !== undefined ? String(fullUserInfo.gender) : '0');
}
} catch (infoError) {
console.error('获取用户完整信息失败:', infoError);
// 获取用户完整信息失败,但不影响登录状态
} }
return true; return true;
@ -68,11 +124,15 @@ export async function checkLoginStatus() {
return false; return false;
} }
} catch (error) { } catch (error) {
console.error('验证登录状态失败:', error); console.error('验证登录状态时发生错误:', error);
if (error.response) {
console.error('错误响应:', error.response.status, error.response.data);
}
// 如果是网络错误、超时或服务器未响应保留token并尝试从localStorage恢复 // 如果是网络错误、超时或服务器未响应保留token并尝试从localStorage恢复
if (!error.response || error.code === 'ECONNABORTED') { if (!error.response || error.code === 'ECONNABORTED') {
console.log('网络错误或服务器未响应尝试从localStorage恢复登录状态'); console.log('网络错误或服务器未响应');
// 如果localStorage中有用户信息则维持登录状态 // 如果localStorage中有用户信息则维持登录状态
const userId = localStorage.getItem('userId'); const userId = localStorage.getItem('userId');
@ -88,9 +148,12 @@ export async function checkLoginStatus() {
avatar: avatar || '', avatar: avatar || '',
role: role || 1, role: role || 1,
accessToken: accessToken, accessToken: accessToken,
refreshToken: refreshToken refreshToken: refreshToken,
email: localStorage.getItem('email') || '',
phone: localStorage.getItem('phone') || '',
college: localStorage.getItem('college') || '',
gender: localStorage.getItem('gender') ? parseInt(localStorage.getItem('gender')) : 0
}); });
console.log('从localStorage成功恢复用户登录状态:', username);
return true; return true;
} }

@ -2,6 +2,7 @@ import axios from 'axios';
import { clearAuthTokens } from './auth'; import { clearAuthTokens } from './auth';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
// 创建axios实例
const request = axios.create({ const request = axios.create({
timeout: 5000 timeout: 5000
}); });
@ -47,8 +48,6 @@ request.interceptors.response.use(
return Promise.reject(response.data); return Promise.reject(response.data);
}, },
error => { error => {
console.error('请求错误:', error);
// 如果响应状态码是401或500可能是token过期或格式错误 // 如果响应状态码是401或500可能是token过期或格式错误
if (error.response && (error.response.status === 401 || if (error.response && (error.response.status === 401 ||
(error.response.status === 500 && error.response.data?.message?.includes('token')))) { (error.response.status === 500 && error.response.data?.message?.includes('token')))) {
@ -59,7 +58,6 @@ request.interceptors.response.use(
// 如果是500错误且与token相关直接清除token并拒绝请求 // 如果是500错误且与token相关直接清除token并拒绝请求
if (error.response.status === 500 && error.response.data?.message?.includes('token')) { if (error.response.status === 500 && error.response.data?.message?.includes('token')) {
console.error('Token格式错误:', error.response.data.message);
clearAuthTokens(); clearAuthTokens();
if (needAuth) { if (needAuth) {
ElMessage.warning('登录已失效,请重新登录'); ElMessage.warning('登录已失效,请重新登录');
@ -88,21 +86,29 @@ request.interceptors.response.use(
return Promise.reject(error); return Promise.reject(error);
} }
// 确保accessToken有Bearer前缀
const tokenWithPrefix = accessToken.startsWith('Bearer ') ? accessToken : `Bearer ${accessToken}`;
console.log('尝试刷新token...');
// 调用check-login接口刷新token // 调用check-login接口刷新token
return axios.post('/user/check-login', null, { return axios.post('/user/check-login', null, {
headers: { headers: {
'Authorization': `Bearer ${accessToken}`, 'Authorization': tokenWithPrefix,
'X-Refresh-Token': refreshToken 'X-Refresh-Token': refreshToken
} }
}).then(res => { }).then(res => {
if (res.data && res.data.code === 200 && res.data.data) { if (res.data && res.data.code === 200 && res.data.data) {
console.log('Token刷新成功');
// 更新token // 更新token
const { accessToken, refreshToken } = res.data.data; const { accessToken, refreshToken } = res.data.data;
localStorage.setItem('accessToken', accessToken); localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken); localStorage.setItem('refreshToken', refreshToken);
// 确保新token有Bearer前缀
const newTokenWithPrefix = accessToken.startsWith('Bearer ') ? accessToken : `Bearer ${accessToken}`;
// 更新原始请求的Authorization头 // 更新原始请求的Authorization头
originalRequest.headers['Authorization'] = `Bearer ${accessToken}`; originalRequest.headers['Authorization'] = newTokenWithPrefix;
originalRequest.headers['X-Refresh-Token'] = refreshToken; originalRequest.headers['X-Refresh-Token'] = refreshToken;
// 处理队列中的请求 // 处理队列中的请求
@ -111,6 +117,7 @@ request.interceptors.response.use(
// 重新发送原始请求 // 重新发送原始请求
return axios(originalRequest); return axios(originalRequest);
} else { } else {
console.warn('Token刷新失败:', res.data);
processQueue(new Error('刷新Token失败'), null); processQueue(new Error('刷新Token失败'), null);
if (needAuth) { if (needAuth) {
// 不清除token只提示用户 // 不清除token只提示用户
@ -119,6 +126,7 @@ request.interceptors.response.use(
return Promise.reject(error); return Promise.reject(error);
} }
}).catch(err => { }).catch(err => {
console.error('Token刷新出错:', err);
processQueue(err, null); processQueue(err, null);
if (needAuth) { if (needAuth) {
// 只有在确认token格式错误时才清除 // 只有在确认token格式错误时才清除
@ -137,7 +145,9 @@ request.interceptors.response.use(
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
requestsQueue.push({ requestsQueue.push({
resolve: token => { resolve: token => {
originalRequest.headers['Authorization'] = `Bearer ${token}`; // 确保token有Bearer前缀
const tokenWithPrefix = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
originalRequest.headers['Authorization'] = tokenWithPrefix;
resolve(axios(originalRequest)); resolve(axios(originalRequest));
}, },
reject: err => { reject: err => {
@ -155,7 +165,6 @@ request.interceptors.response.use(
//请求拦截器 //请求拦截器
request.interceptors.request.use( request.interceptors.request.use(
config => { config => {
console.log('Request:', config.url);
// 只对非认证相关的API添加token // 只对非认证相关的API添加token
if (!config.url.includes('/captcha') && !config.url.includes('/verify-captcha') && if (!config.url.includes('/captcha') && !config.url.includes('/verify-captcha') &&
!config.url.includes('/user/login') && !config.url.includes('/user/register')) { !config.url.includes('/user/login') && !config.url.includes('/user/register')) {
@ -164,13 +173,14 @@ request.interceptors.request.use(
// 检查token格式是否正确 // 检查token格式是否正确
if (token && refreshToken && typeof token === 'string' && typeof refreshToken === 'string') { if (token && refreshToken && typeof token === 'string' && typeof refreshToken === 'string') {
config.headers['Authorization'] = `Bearer ${token}`; // 确保添加Bearer前缀
config.headers['Authorization'] = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
config.headers['X-Refresh-Token'] = refreshToken; config.headers['X-Refresh-Token'] = refreshToken;
console.log('添加token到请求头:', config.url);
} else if ((token && typeof token !== 'string') || } else if ((token && typeof token !== 'string') ||
(refreshToken && typeof refreshToken !== 'string')) { (refreshToken && typeof refreshToken !== 'string')) {
// 只有当token格式明显错误时才清除 // 只有当token格式明显错误时才清除
console.warn('Token格式不正确清除token'); console.warn('Token格式错误清除token');
clearAuthTokens(); clearAuthTokens();
} else if (!token || !refreshToken) { } else if (!token || !refreshToken) {
// 如果没有token但访问需要认证的API尝试从localStorage恢复 // 如果没有token但访问需要认证的API尝试从localStorage恢复

@ -15,6 +15,7 @@
<img v-if="previewAvatar" :src="previewAvatar" alt="新头像预览" class="new-avatar"> <img v-if="previewAvatar" :src="previewAvatar" alt="新头像预览" class="new-avatar">
</div> </div>
<label class="upload-btn"> <label class="upload-btn">
<span class="plus-icon">+</span>
选择新头像 选择新头像
<input type="file" accept="image/*" @change="handleAvatarChange" class="file-input"> <input type="file" accept="image/*" @change="handleAvatarChange" class="file-input">
</label> </label>
@ -143,14 +144,14 @@ import { ref, computed, onMounted } from 'vue';
import { useUserStore } from '@/stores/user'; import { useUserStore } from '@/stores/user';
import request from '@/utils/request'; import request from '@/utils/request';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router';
const userStore = useUserStore(); const userStore = useUserStore();
const userInfo = computed(() => userStore.userInfo); const userInfo = computed(() => userStore.userInfo);
const router = useRouter(); const router = useRouter();
// //
const defaultAvatar = require('@/assets/default-avatar/boy_4.png'); const defaultAvatar = require('@/assets/default-avatar/boy_1.png');
// //
const handleAvatarError = "this.onerror=null;this.src='" + defaultAvatar + "'"; const handleAvatarError = "this.onerror=null;this.src='" + defaultAvatar + "'";
@ -384,9 +385,10 @@ onMounted(() => {
max-width: 700px; max-width: 700px;
margin: 2rem auto; margin: 2rem auto;
padding: 2.5rem 2rem 2rem 2rem; padding: 2.5rem 2rem 2rem 2rem;
background: #f8f9fa; background: #fff;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 16px rgba(52, 152, 219, 0.08); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
position: relative;
} }
.title { .title {
@ -396,6 +398,8 @@ onMounted(() => {
margin-bottom: 2.2rem; margin-bottom: 2.2rem;
letter-spacing: 1px; letter-spacing: 1px;
text-align: center; text-align: center;
border-bottom: 2px solid #ff88aa;
padding-bottom: 10px;
} }
.section-title { .section-title {
@ -428,13 +432,15 @@ onMounted(() => {
} }
.new-avatar { .new-avatar {
border-color: #3498db; border-color: #ff88aa;
} }
.upload-btn { .upload-btn {
display: inline-block; display: flex;
align-items: center;
gap: 8px;
padding: 0.5rem 1.2rem; padding: 0.5rem 1.2rem;
background: linear-gradient(90deg, #3498db 60%, #6dd5fa 100%); background: #ff88aa;
color: white; color: white;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
@ -445,7 +451,12 @@ onMounted(() => {
} }
.upload-btn:hover { .upload-btn:hover {
background: linear-gradient(90deg, #2980b9 60%, #3498db 100%); background: #ff6699;
}
.plus-icon {
font-size: 20px;
font-weight: bold;
} }
.file-input { .file-input {
@ -483,17 +494,17 @@ onMounted(() => {
.input, .textarea, select.input { .input, .textarea, select.input {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem;
border: 1px solid #bdc3c7; border: 1px solid #e0e0e0;
border-radius: 4px; border-radius: 8px;
font-size: 0.95rem; font-size: 0.95rem;
transition: border-color 0.3s ease; transition: border-color 0.3s ease;
background: #fff; background: #f9f9f9;
} }
.input:focus, .textarea:focus, select.input:focus { .input:focus, .textarea:focus, select.input:focus {
outline: none; outline: none;
border-color: #3498db; border-color: #ff88aa;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.08); box-shadow: 0 0 0 2px rgba(255, 136, 170, 0.08);
} }
.input-error { .input-error {
@ -514,10 +525,10 @@ onMounted(() => {
.submit-btn { .submit-btn {
width: 100%; width: 100%;
padding: 0.85rem; padding: 0.85rem;
background: linear-gradient(90deg, #3498db 60%, #6dd5fa 100%); background: #ff88aa;
color: white; color: white;
border: none; border: none;
border-radius: 4px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
@ -527,6 +538,34 @@ onMounted(() => {
} }
.submit-btn:hover { .submit-btn:hover {
background: linear-gradient(90deg, #2980b9 60%, #3498db 100%); background: #ff6699;
}
/* 花瓣动画(与背景呼应) */
@keyframes fall {
0% { transform: translateY(-100vh) rotate(0deg); opacity: 1; }
100% { transform: translateY(100vh) rotate(360deg); opacity: 0.5; }
}
.petal {
position: absolute;
width: 10px;
height: 15px;
background: pink;
border-radius: 50% 50% 0 0;
animation: fall 5s linear infinite;
z-index: -1;
}
/* 初始化花瓣可在mounted中动态生成此处简化 */
.change-info-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
animation: none; /* 避免重复动画 */
} }
</style> </style>

@ -1,17 +1,12 @@
<template> <template>
<div> <div>
<div class="top-container"> <div class="top-container">
<NoticeBoard /> <NoticeBoard class="notice-flex"/>
<WelcomeCalendar /> <WelcomeCalendar class="calendar-flex"/>
</div> </div>
<div class="post-container"> <div class="post-container">
<PostPage /> <PostPage />
</div> </div>
<!-- <UserPage /> -->
<!-- <PostDetail /> -->
</div> </div>
</template> </template>
@ -19,33 +14,29 @@
import NoticeBoard from '@/components/NoticeBoard.vue'; import NoticeBoard from '@/components/NoticeBoard.vue';
import WelcomeCalendar from '@/components/WelcomeCalendar.vue'; import WelcomeCalendar from '@/components/WelcomeCalendar.vue';
import PostPage from '@/components/PostPage.vue'; import PostPage from '@/components/PostPage.vue';
// import UserPage from './UserPage.vue';
// import PostDetail from './PostDetail.vue';
</script> </script>
<style scoped> <style scoped>
.top-container { .top-container {
display: flex; display: flex;
justify-content: center; /* 居中排列 */ align-items: stretch;
align-items: center; /* 顶部对齐 */ gap: 16px;
padding: 10px; width: 100%;
gap: 10px; /* 设置较小的间距 */ max-width: 1000px;
width: 820px; /* 固定宽度 */ height: 300px;
height: 350px; /* 固定高度 */ margin: 0 auto;
margin:20px auto 0; /* 上边距20px左右居中 */
overflow: hidden; /* 防止溢出 */
box-sizing: border-box; /* 包括内边距在宽高内 */
} }
.top-container > * { .notice-flex, .calendar-flex {
flex-shrink: 0; height: 100%;
} }
.post-container { .post-container {
display: flex; display: flex;
justify-content: center; /* 居中排列 */ justify-content: center;
align-items: center; /* 顶部对齐 */ align-items: flex-start;
padding: 10px; padding: 10px;
width: 820px; /* 固定宽度 */ width: 100%;
margin: 20px auto; /* 上边距20px左右居中 */ max-width: 1000px;
box-sizing: border-box; /* 包括内边距在宽高内 */ margin: 10px auto;
box-sizing: border-box;
} }
</style> </style>

@ -1,148 +1,164 @@
<template> <template>
<div class="post-detail-container"> <div class="main-content">
<!-- 作者信息栏 --> <!-- 帖子内容 -->
<div class="author-info" v-if="author && author.userName"> <div class="post-content">
<img <div class="author-row">
:src="author.userAvatar || require('@/assets/default-avatar/boy_1.png')" <img
alt="头像" :src="(author.userName === '匿名用户' || postDetailStore.post?.status === 1 )
class="author-avatar" ? require('@/assets/default-avatar/boy_4.png')
@click="goUserHome(author.userId)" : author.userAvatar|| require('@/assets/default-avatar/boy_1.png') "
style="cursor: pointer;" alt="头像"
/> class="author-avatar"
<div class="author-details"> :style="{ cursor: (author.userName === '匿名用户' || postDetailStore.post?.status === 1 ) ? 'not-allowed' : 'pointer' }"
<h3 class="author-name">{{ author.userName || '匿名用户' }}</h3> @click="handleAuthorAvatarClick"
<p class="author-stats">粉丝数{{ author.followers ?? 0 }}</p>
<button @click="toggleFollow" class="follow-button">
{{ isFollowing ? '取消关注' : '关注' }}
</button>
</div>
</div>
<!-- 帖子内容 -->
<div class="post-content">
<img :src="author?.userAvatar || require('@/assets/default-avatar/boy_1.png')" alt="作者头像" class="post-author-avatar" />
<h1 class="post-title">{{ postDetailStore.post?.title || '' }}</h1>
<p class="post-body">{{ postDetailStore.post?.content || '' }}</p>
<div class="post-stats">
<span> 👁 {{ postDetailStore.post?.viewCount ?? 0 }}</span>
<span> 🗨 {{ postDetailStore.post?.commentCount ?? 0 }}</span>
<span>
<button
@click="postDetailStore.PostLike"
class="like-btn"
:class="{ liked: postDetailStore.isLike }"
>
<span v-if="!postDetailStore.isLike"></span>
<span v-else></span>
{{ postDetailStore.post?.likeCount ?? 0 }}
</button>
</span>
</div>
<div class="post-time">
发布时间{{ postDetailStore.post?.createTime ? formatTime(postDetailStore.post.createTime) : '' }}
</div>
</div>
<!-- 评论区 -->
<div class="comments-section">
<h2 class="comments-title">评论</h2>
<ul class="comments-list">
<li v-for="comment in postDetailStore.comments" :key="comment.id" class="comment-item">
<img
:src="comment.userAvatar || require('@/assets/default-avatar/boy_1.png')"
alt="评论者头像"
class="comment-avatar"
@click="goUserHome(comment.userId)"
style="cursor: pointer;"
/> />
<div class="comment-content"> <div class="author-info">
<p class="comment-name">{{ comment.userName || '匿名用户' }}</p> <h3 class="author-name">{{ author.userName || '匿名用户' }}</h3>
<p class="comment-text">{{ comment.content || '' }}</p> <button @click="toggleFollow" class="follow-button">
<div class="comment-meta"> {{ isFollowing ? '取消关注' : '关注' }}
<span class="comment-time">{{ comment.createTime ? formatTime(comment.createTime) : '' }}</span> </button>
</div>
<h1 class="post-title">{{ postDetailStore.post?.title || '' }}</h1>
</div>
<p class="post-body">{{ postDetailStore.post?.content || '' }}</p>
<img
v-if="postDetailStore.post?.image"
:src="postDetailStore.post.image"
alt="帖子封面"
class="post-cover"
@click="showImagePreview = true"
/>
<!-- 大图预览遮罩 -->
<div v-if="showImagePreview" class="image-preview-mask" @click="showImagePreview = false">
<img :src="postDetailStore.post.image" class="image-preview-big" @click.stop />
</div>
<div class="post-stats">
<span> 👁 {{ postDetailStore.post?.viewCount ?? 0 }}</span>
<span> 🗨 {{ postDetailStore.post?.commentCount ?? 0 }}</span>
<span>
<button
@click="postDetailStore.PostLike"
class="like-btn"
:class="{ liked: postDetailStore.isLike }"
>
<span v-if="!postDetailStore.isLike"></span>
<span v-else></span>
{{ postDetailStore.post?.likeCount ?? 0 }}
</button>
</span>
</div>
<div class="post-time">
发布时间{{ postDetailStore.post?.createTime ? formatTime(postDetailStore.post.createTime) : '' }}
</div>
</div>
<!-- 评论区 -->
<div class="comments-section">
<h2 class="comments-title">评论</h2>
<ul class="comments-list">
<li v-if="postDetailStore.comments.length === 0" class="comments-finished-tip">
暂时没有评论哦快来发表吧
</li>
<li v-for="comment in postDetailStore.comments" :key="comment.id" class="comment-item">
<img
:src="comment.userAvatar || require('@/assets/default-avatar/boy_1.png')"
alt="评论者头像"
class="comment-avatar"
@click="goUserHome(comment.userId)"
style="cursor: pointer;"
/>
<div class="comment-content">
<p class="comment-name">{{ comment.userName || '匿名用户' }}</p>
<p class="comment-text">{{ comment.content || '' }}</p>
<button <button
@click="postDetailStore.CommentLike(comment)" class="delete-btn"
:class="['like-btn', { liked: comment.isLike }]"
>
<span v-if="!comment.isLike"></span>
<span v-else></span>
{{ comment.likeCount ?? 0 }}
</button>
<span v-if="comment.replyCount > 0">
<button @click="loadReplies(comment)">
{{ comment.showReplies> 0 ? '收起回复' : '展开回复' }} ({{ comment.replyCount }})
</button>
</span>
<button class="reply-btn" @click="startReply(comment)"></button>
<button
class="delete-btn"
v-if="String(comment.userId) === String(userStore.userInfo?.userid)" v-if="String(comment.userId) === String(userStore.userInfo?.userid)"
@click="handleDelete(comment)" @click="handleDelete(comment)"
>删除</button> ></button>
</div> <div class="comment-meta">
<!-- 子评论列表 --> <span class="comment-time">{{ comment.createTime ? formatTime(comment.createTime) : '' }}</span>
<ul v-if="comment.showReplies && comment.replies && comment.replies.length > 0" class="replies-list"> <button class="reply-btn" @click="startReply(comment)"></button>
<li v-for="reply in comment.replies" :key="reply.id" class="comment-item reply-item"> <span v-if="comment.replyCount > 0">
<img <button @click="loadReplies(comment)" class="expand-reply-btn">
:src="reply.userAvatar || require('@/assets/default-avatar/boy_1.png')" {{ comment.showReplies> 0 ? '收起回复' : '展开回复' }} ({{ comment.replyCount }})
alt="评论者头像" </button>
class="comment-avatar" </span>
@click="goUserHome(reply.userId)" <button
style="cursor: pointer;" @click="postDetailStore.CommentLike(comment)"
/> :class="['like-btn', { liked: comment.isLike }]"
<div class="comment-content"> >
<p class="comment-name"> <span v-if="!comment.isLike"></span>
{{ reply.userName || '匿名用户' }} <span v-else></span>
<span v-if="reply.replyUserName" class="reply-user"> @{{ reply.replyUserName }}</span> {{ comment.likeCount ?? 0 }}
</p> </button>
<p class="comment-text">{{ reply.content || '' }}</p> </div>
<div class="comment-meta"> <!-- 子评论列表 -->
<span class="comment-time">{{ reply.createTime ? formatTime(reply.createTime) : '' }}</span> <ul v-if="comment.showReplies && comment.replies && comment.replies.length > 0" class="replies-list">
<li v-for="reply in comment.replies" :key="reply.id" class="comment-item reply-item">
<img
:src="reply.userAvatar || require('@/assets/default-avatar/boy_1.png')"
alt="评论者头像"
class="comment-avatar"
@click="goUserHome(reply.userId)"
style="cursor: pointer;"
/>
<div class="comment-content">
<p class="comment-name">
{{ reply.userName || '匿名用户' }}
<span v-if="reply.replyUserName" class="reply-user"> @{{ reply.replyUserName }}</span>
</p>
<p class="comment-text">{{ reply.content || '' }}</p>
<button <button
@click="postDetailStore.CommentLike(reply)" class="delete-btn"
:class="['like-btn', { liked: reply.isLike }]"
>
<span v-if="!reply.isLike"></span>
<span v-else></span>
{{ reply.likeCount ?? 0 }}
</button>
<button class="reply-btn" @click="startReply(reply)"></button>
<button
class="delete-btn"
v-if="String(reply.userId) === String(userStore.userInfo?.userid)" v-if="String(reply.userId) === String(userStore.userInfo?.userid)"
@click="handleDelete(reply)" @click="handleDelete(reply)"
>删除</button> ></button>
<div class="comment-meta">
<span class="comment-time">{{ reply.createTime ? formatTime(reply.createTime) : '' }}</span>
<button class="reply-btn" @click="startReply(reply)"></button>
<button
@click="postDetailStore.CommentLike(reply)"
:class="['like-btn', { liked: reply.isLike }]"
>
<span v-if="!reply.isLike"></span>
<span v-else></span>
{{ reply.likeCount ?? 0 }}
</button>
</div>
</div> </div>
</div> </li>
</li> <li v-if="comment.repliesLoading" class="reply-loading">...</li>
<li v-if="comment.repliesLoading" class="reply-loading">...</li> <li v-if="comment.repliesFinished" class="reply-finished"></li>
<li v-if="comment.repliesFinished" class="reply-finished"></li> </ul>
</ul> </div>
</div> </li>
</li> <li v-if="postDetailStore.comments.length > 0 && postDetailStore.commentsFinished" class="comments-finished-tip">
</ul> 没有更多评论啦......
</div> </li>
</ul>
</div>
<!-- 发送评论 --> <!-- 发送评论 -->
<div class="comment-box"> <div class="comment-box">
<div v-if="replyingComment" class="replying-tip"> <div v-if="replyingComment" class="replying-tip">
正在回复 @{{ replyingComment.userName || '匿名用户' }} 正在回复 @{{ replyingComment.userName || '匿名用户' }}
<button class="cancel-reply-btn" @click="cancelReply"></button> <button class="cancel-reply-btn" @click="cancelReply"></button>
</div>
<div style="position:relative;">
<textarea
v-model="newComment"
placeholder="写下你的评论..."
class="comment-input"
></textarea>
<button @click="sendComment" class="send-button" style="position:absolute; right:16px; bottom:16px;">发送</button>
</div>
</div> </div>
<textarea
v-model="newComment"
placeholder="写下你的评论..."
class="comment-input"
></textarea>
<button @click="sendComment" class="send-button">发送</button>
</div> </div>
</div>
</template> </template>
<script setup lang="js" name="PostDetail"> <script setup lang="js" name="PostDetail">
import { ref, computed, onMounted , onUnmounted, watch } from 'vue'; import { ref, computed, onMounted , onUnmounted, watch } from 'vue';
import { useRoute,useRouter } from 'vue-router'; import { useRoute,useRouter } from 'vue-router';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox, ElMessage } from 'element-plus';
import { usePostDetailStore } from '@/stores/postdetail.js'; import { usePostDetailStore } from '@/stores/postdetail.js';
import { useUserStore } from '@/stores/user.js'; import { useUserStore } from '@/stores/user.js';
@ -152,7 +168,7 @@ const postDetailStore = usePostDetailStore();
const userStore = useUserStore(); const userStore = useUserStore();
const newComment = ref(''); const newComment = ref('');
const replyingComment = ref(null); const replyingComment = ref(null);
const showImagePreview = ref(false);
// author undefined // author undefined
const author = computed(() => postDetailStore.post?.author || {}); const author = computed(() => postDetailStore.post?.author || {});
@ -238,6 +254,19 @@ function handleDelete(comment) {
postDetailStore.deleteComment(comment); postDetailStore.deleteComment(comment);
}).catch(() => {}); }).catch(() => {});
} }
function handleAuthorAvatarClick() {
//
if (
(author.value.userName === '匿名用户' || postDetailStore.post?.status === 1) &&
(!author.value.userAvatar || author.value.userAvatar === '')
) {
ElMessage.info('该用户为匿名用户');
return;
}
goUserHome(author.value.userId);
}
// postDetailStore.post // postDetailStore.post
watch( watch(
() => postDetailStore.post, () => postDetailStore.post,
@ -259,30 +288,32 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
.post-detail-container { .main-content {
display: grid; max-width: 800px;
grid-template-areas: margin: 0 auto;
"post-content author-info" display: flex;
"comments-section author-info" flex-direction: column;
"comment-box author-info"; height: 100%;
grid-template-columns: 3fr 1fr; /* 左侧内容占 3/4右侧作者信息占 1/4 */
gap: 20px; gap: 20px;
padding: 20px; min-width: 0;
box-sizing: border-box; min-height: 0;
max-width: 1200px; /* 设置页面最大宽度 */ background: transparent;
margin: 0 auto; /* 居中页面并添加左右留白 */ border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.author-row {
display: flex;
align-items: center;
margin-bottom: 5px;
} }
.author-info { .author-info {
grid-area: author-info;
display: flex; display: flex;
flex-direction: row; /* 水平排列头像和详情 */ flex-direction: column;
align-items: flex-start; align-items: flex-start;
background-color: #f5f5f5; min-width: 90px;
padding: 20px; margin-right: 18px;
border: 1px solid #ccc;
max-height: 200px;
border-radius: 8px;
} }
.author-avatar { .author-avatar {
@ -293,24 +324,11 @@ onUnmounted(() => {
margin-right: 15px; /* 与作者详情的间距 */ margin-right: 15px; /* 与作者详情的间距 */
} }
.author-details {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.author-name { .author-name {
font-size: 18px; font-size: 28px;
margin: 0; margin: 0;
} }
.author-stats {
font-size: 14px;
color: #666;
margin: 5px 0;
text-align: left; /* 设置居左 */
}
.follow-button { .follow-button {
margin-top: 10px; margin-top: 10px;
padding: 8px 16px; padding: 8px 16px;
@ -329,33 +347,64 @@ onUnmounted(() => {
.post-content { .post-content {
grid-area: post-content; grid-area: post-content;
background-color: #ffffff; background-color: #ffffff;
padding: 20px; padding: 5px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 8px; border-radius: 8px;
position: relative; /* 为绝对定位的元素提供参考 */ position: relative;
} }
.post-author-avatar { .post-cover {
position: absolute; position: static;
top: 20px; display: block;
right: 20px; margin: 5px auto 0 auto;
width: 100px; width: auto;
height: 100px; height: 200px;
border-radius: 50%; max-width: 100%;
object-fit: cover; object-fit: cover;
border-radius: 12px;
border: 2px solid #ccc; border: 2px solid #ccc;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
cursor: pointer; /* 鼠标悬停变小手 */
transition: box-shadow 0.2s;
}
.post-cover:hover {
box-shadow: 0 4px 24px rgba(0,0,0,0.18);
}
.image-preview-mask {
position: fixed;
z-index: 9999;
inset: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
}
.image-preview-big {
max-width: 90vw;
max-height: 90vh;
border-radius: 12px;
box-shadow: 0 4px 32px rgba(0,0,0,0.25);
background: #fff;
} }
.post-title { .post-title {
font-size: 24px; font-size: 32px;
margin-bottom: 10px; margin-bottom: 10px;
text-align: left; /* 设置居左 */ text-align: left;
padding-left: 5px;
word-break: break-all; /* 长单词或长串自动换行 */
white-space: normal;
} }
.post-body { .post-body {
font-size: 16px; font-size: 20px;
line-height: 1.5; line-height: 1.5;
margin-bottom: 20px; margin-bottom: 20px;
text-align: left;
padding-left: 16px;
word-break: break-all; /* 长单词或长串自动换行 */
white-space: pre-line;
} }
.post-stats { .post-stats {
@ -369,18 +418,20 @@ onUnmounted(() => {
.post-time { .post-time {
position: absolute; position: absolute;
bottom: 20px; /* 距离底部 20px */ bottom: 5px; /* 距离底部 20px */
right: 20px; /* 距离右侧 20px */ right: 20px; /* 距离右侧 20px */
font-size: 12px; font-size: 12px;
color: #999; color: #999;
} }
.comments-section { .comments-section {
grid-area: comments-section; flex: 1 1 0;
min-height: 400px;
background-color: #ffffff; background-color: #ffffff;
padding: 20px; padding: 20px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 8px; border-radius: 8px;
overflow-y: auto;
} }
.comments-title { .comments-title {
@ -416,6 +467,7 @@ onUnmounted(() => {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-left: 12px;
} }
.comment-name { .comment-name {
@ -427,6 +479,9 @@ onUnmounted(() => {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
color: #333; color: #333;
text-align: left;
white-space: pre-line; /* 保留换行 */
word-break: break-all; /* 长单词或长串自动换行 */
} }
.comment-meta { .comment-meta {
@ -441,32 +496,50 @@ onUnmounted(() => {
font-size: 12px; font-size: 12px;
color: #666; color: #666;
} }
.expand-reply-btn {
background: none;
border: none;
color: #409eff;
cursor: pointer;
font-size: 13px;
padding: 2px 8px;
z-index: 1;
}
.comment-likes { .comment-likes {
font-size: 12px; font-size: 12px;
color: #666; color: #666;
} }
.comments-finished-tip {
color: #999;
font-size: 14px;
text-align: center;
padding: 16px 0 8px 0;
letter-spacing: 1px;
}
.comment-box { .comment-box {
grid-area: comment-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
background: #fff;
padding: 6px 24px 12px 24px;
box-shadow: 0 -2px 12px rgba(0,0,0,0.06);
border-radius: 8px;
} }
.comment-input { .comment-input {
width: 100%; width: 100%;
height: 80px; height: 80px;
padding: 10px; padding: 10px 80px 32px 10px; /* 右下留空间给按钮 */
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 8px; border-radius: 8px;
resize: none; resize: none;
font-size: 14px; font-size: 14px;
box-sizing: border-box;
} }
.send-button { .send-button {
align-self: flex-end; /* 位置已由内联style控制 */
padding: 10px 20px; padding: 8px 20px;
background-color: #5aa76f; background-color: #5aa76f;
color: white; color: white;
border: none; border: none;
@ -490,7 +563,7 @@ onUnmounted(() => {
.replies-list { .replies-list {
list-style: none; list-style: none;
padding-left: 48px; /* 更明显的缩进 */ padding-left: 48px;
margin: 8px 0 0 0; margin: 8px 0 0 0;
border-left: 2px solid #e0e0e0; border-left: 2px solid #e0e0e0;
} }
@ -558,21 +631,28 @@ onUnmounted(() => {
border: none; border: none;
color: #f56c6c; color: #f56c6c;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 18px;
margin-left: 10px; margin-left: 10px;
position: absolute;
top: 0;
right: 0;
z-index: 2;
} }
.delete-btn:hover { .delete-btn:hover {
text-decoration: underline; color: #ff2222;
background: #f9eaea;
border-radius: 50%;
} }
.like-btn { .like-btn {
margin-left: 8px; margin-left: 8px;
background: none; background: none;
border: none; border: none;
color: #409eff; color: #222323;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
} }
.like-btn.liked { .like-btn.liked {
color: #f56c6c; color: #f56c6c;
} }
</style> </style>

@ -12,28 +12,51 @@
maxlength="50" maxlength="50"
placeholder="请输入标题3-50个字符" placeholder="请输入标题3-50个字符"
required required
class="input-field"
/> />
</div> </div>
<!-- 分类选择 --> <!-- 分类选择 -->
<div class="form-row"> <div class="form-row">
<label>分类</label> <label>分类</label>
<select v-model="form.categoryId" required> <select v-model="form.categoryId" required class="input-field">
<option disabled value="">请选择分类</option> <option disabled value="">请选择分类</option>
<option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option> <option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
</select> </select>
</div> </div>
<!-- 图片上传 --> <!-- 图片上传自定义+号按钮 -->
<div class="form-row"> <div class="form-row">
<label>图片</label> <label>封面</label>
<input type="file" accept="image/*" @change="onFileChange" /> <div class="upload-wrapper">
<div v-if="form.imagePreview" class="img-preview"> <!-- 隐藏的文件选择input -->
<img :src="form.imagePreview" alt="预览" /> <input
<button type="button" @click="removeImage"></button> type="file"
accept="image/*"
@change="onFileChange"
id="image-upload"
class="hidden-input"
/>
<!-- 自定义+号按钮 -->
<label for="image-upload" class="upload-btn">
<span class="plus-icon">+</span>
</label>
<!-- 图片预览 -->
<div v-if="form.imagePreview" class="img-preview">
<img :src="form.imagePreview" alt="预览" class="preview-img" />
<button type="button" @click="removeImage" class="remove-btn">×</button>
</div>
</div> </div>
</div> </div>
<!-- 匿名发布选项 -->
<div class="form-row anonymous-option">
<label class="anonymous-label">
<input type="checkbox" v-model="form.status" true-value="1" false-value="0" class="anonymous-checkbox" />
匿名发布
</label>
</div>
<!-- 内容编辑器 --> <!-- 内容编辑器 -->
<div class="form-row"> <div class="form-row">
<label>内容</label> <label>内容</label>
@ -42,12 +65,13 @@
rows="8" rows="8"
placeholder="请输入帖子内容支持Markdown语法" placeholder="请输入帖子内容支持Markdown语法"
required required
class="textarea-field"
></textarea> ></textarea>
</div> </div>
<!-- 提交按钮 --> <!-- 提交按钮 -->
<div class="submit-area"> <div class="submit-area">
<button type="submit" :disabled="submitting"> <button type="submit" :disabled="submitting" class="submit-btn">
{{ submitting ? '发布中...' : '立即发布' }} {{ submitting ? '发布中...' : '立即发布' }}
</button> </button>
</div> </div>
@ -71,9 +95,10 @@ export default {
const router = useRouter() const router = useRouter()
const submitting = ref(false) const submitting = ref(false)
const categories = ref([ const categories = ref([
{ id: 1, name: '学习' }, { id: 1, name: '校园活动' },
{ id: 2, name: '娱乐' }, { id: 2, name: '学习' },
{ id: 3, name: '二手交易' } { id: 3, name: '娱乐' },
{ id: 4, name: '二手交易' }
]) ])
const form = ref({ const form = ref({
title: '', title: '',
@ -93,9 +118,7 @@ export default {
const onFileChange = async (e) => { const onFileChange = async (e) => {
const file = e.target.files[0] const file = e.target.files[0]
if (!file) return if (!file) return
//
form.value.imagePreview = URL.createObjectURL(file) form.value.imagePreview = URL.createObjectURL(file)
//
const fd = new FormData() const fd = new FormData()
fd.append('file', file) fd.append('file', file)
try { try {
@ -123,6 +146,10 @@ export default {
showResult('error', '请填写完整信息') showResult('error', '请填写完整信息')
return return
} }
if (!form.value.image) {
showResult('error', '请先选择帖子封面')
return
}
if (form.value.title.length < 3 || form.value.title.length > 50) { if (form.value.title.length < 3 || form.value.title.length > 50) {
showResult('error', '标题长度需3-50字符') showResult('error', '标题长度需3-50字符')
return return
@ -143,7 +170,6 @@ export default {
const res = await request.post('/post', postData) const res = await request.post('/post', postData)
if (res.code === 200) { if (res.code === 200) {
showResult('success', '帖子发布成功') showResult('success', '帖子发布成功')
console.log(res.data);
setTimeout(() => { setTimeout(() => {
router.push(`/`) router.push(`/`)
}, 1200) }, 1200)
@ -178,94 +204,181 @@ export default {
</script> </script>
<style scoped> <style scoped>
.upload-wrapper {
display: flex;
align-items: center;
gap: 15px;
}
.hidden-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.upload-btn {
width: 80px;
height: 80px;
border: 2px dashed #ff88aa;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.upload-btn:hover {
background-color: #fff5f8;
border-color: #ff6699;
}
.plus-icon {
font-size: 32px;
color: #ff88aa;
font-weight: lighter;
}
.img-preview {
position: relative;
display: inline-block;
}
.preview-img {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #eee;
}
.remove-btn {
position: absolute;
top: -8px;
right: -8px;
width: 20px;
height: 20px;
background: #ff6699;
color: white;
border: none;
border-radius: 50%;
padding: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.post-container { .post-container {
max-width: 600px; max-width: 600px;
margin: 30px auto; margin: 30px auto;
padding: 30px; padding: 30px;
background: #fff; background: #fff;
border-radius: 8px; border-radius: 12px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.08); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
} }
.page-title { .page-title {
text-align: center; text-align: center;
margin-bottom: 24px; margin-bottom: 24px;
font-size: 1.6em; font-size: 1.8em;
color: #2c3e50; color: #2c3e50;
border-bottom: 2px solid #ff88aa;
padding-bottom: 10px;
} }
.editor-wrapper {
padding: 20px;
}
.form-row { .form-row {
margin-bottom: 18px; margin-bottom: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.form-row label { .form-row label {
font-weight: bold; font-weight: bold;
margin-bottom: 6px; margin-bottom: 8px;
color: #333;
} }
.form-row input[type="text"],
.form-row select, .input-field,
.form-row textarea { .textarea-field {
padding: 8px; padding: 10px;
border: 1px solid #dcdcdc; border: 1px solid #e0e0e0;
border-radius: 4px; border-radius: 8px;
font-size: 15px; font-size: 15px;
transition: border-color 0.3s;
} }
.form-row textarea {
resize: vertical; .input-field:focus,
.textarea-field:focus {
outline: none;
border-color: #ff88aa;
} }
.img-preview {
margin-top: 8px; .anonymous-option {
position: relative; flex-direction: row;
display: inline-block; align-items: center;
justify-content: flex-start;
margin-bottom: 15px;
} }
.img-preview img {
max-width: 120px; .anonymous-label {
max-height: 120px; display: flex;
border-radius: 4px; align-items: center;
border: 1px solid #eee; gap: 8px;
color: #666;
} }
.img-preview button {
position: absolute; .anonymous-checkbox {
top: 2px; width: 18px;
right: 2px; height: 18px;
background: #f56c6c; accent-color: #ff88aa;
color: #fff;
border: none;
border-radius: 3px;
padding: 2px 8px;
cursor: pointer;
font-size: 12px;
} }
.textarea-field {
resize: vertical;
min-height: 150px;
}
.submit-area { .submit-area {
text-align: center; text-align: center;
margin-top: 24px; margin-top: 30px;
} }
.submit-area button {
background: #409eff; .submit-btn {
color: #fff; background: #ff88aa;
color: white;
border: none; border: none;
padding: 10px 32px; padding: 12px 40px;
border-radius: 4px; border-radius: 8px;
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
transition: background 0.3s;
} }
.submit-area button:disabled {
background: #b3d8ff; .submit-btn:disabled {
background: #ffcccc;
cursor: not-allowed; cursor: not-allowed;
} }
.submit-btn:hover {
background: #ff6699;
}
.result-alert { .result-alert {
margin-top: 20px; margin-top: 20px;
padding: 12px 18px; padding: 15px 20px;
border-radius: 4px; border-radius: 8px;
font-size: 15px; font-size: 15px;
text-align: center; text-align: center;
} }
.result-alert.success { .result-alert.success {
background: #e1f3d8; background: #e1f3d8;
color: #3a7a1c; color: #3a7a1c;
} }
.result-alert.error { .result-alert.error {
background: #fde2e2; background: #fde2e2;
color: #c0392b; color: #c0392b;

@ -226,15 +226,22 @@ const loadUserInfo = async () => {
if (isCurrentUser.value) { if (isCurrentUser.value) {
const storedUsername = localStorage.getItem('username'); const storedUsername = localStorage.getItem('username');
const storedAvatar = localStorage.getItem('avatar'); const storedAvatar = localStorage.getItem('avatar');
const storedEmail = localStorage.getItem('email');
const storedPhone = localStorage.getItem('phone');
const storedCollege = localStorage.getItem('college');
const storedGender = localStorage.getItem('gender');
if (storedUsername) { if (storedUsername) {
userInfo.value = { userInfo.value = {
username: storedUsername, username: storedUsername,
avatar: storedAvatar || '', avatar: storedAvatar || '',
role: localStorage.getItem('role') || 1, role: localStorage.getItem('role') || 1,
userId: localStorage.getItem('userId') userId: localStorage.getItem('userId'),
email: storedEmail || '',
phone: storedPhone || '',
college: storedCollege || '',
gender: storedGender ? parseInt(storedGender) : 0
}; };
console.log('从localStorage恢复用户基本信息:', storedUsername);
} }
} }
@ -245,10 +252,24 @@ const loadUserInfo = async () => {
timeout: 5000 timeout: 5000
}); });
console.log('获取用户信息响应:', response); console.log('获取用户信息响应');
if (response && response.code === 200) { if (response && response.code === 200) {
userInfo.value = response.data; userInfo.value = response.data;
console.log('加载到的用户信息:', userInfo.value);
// userStore
if (isCurrentUser.value && userStore) {
userStore.updateUserInfo({
userid: response.data.id,
username: response.data.username,
avatar: response.data.avatar,
email: response.data.email,
phone: response.data.phone,
college: response.data.college,
gender: response.data.gender,
role: response.data.role,
status: response.data.status
});
}
// //
if (isCurrentUser.value) { if (isCurrentUser.value) {
@ -260,14 +281,17 @@ const loadUserInfo = async () => {
// localStorage使 // localStorage使
if ((!error.response || error.code === 'ECONNABORTED') && userInfo.value.username) { if ((!error.response || error.code === 'ECONNABORTED') && userInfo.value.username) {
console.log('网络错误,但已恢复基本用户信息,继续使用');
// 使 // 使
} else if (isCurrentUser.value) { } else if (isCurrentUser.value) {
// userStore // userStore
userInfo.value = { userInfo.value = {
username: userStore.userInfo.username || '用户', username: userStore.userInfo.username || '用户',
avatar: userStore.userInfo.avatar || '', avatar: userStore.userInfo.avatar || '',
userId: userStore.userInfo.userid userId: userStore.userInfo.userid,
email: userStore.userInfo.email || '',
phone: userStore.userInfo.phone || '',
college: userStore.userInfo.college || '',
gender: userStore.userInfo.gender || 0
}; };
} else { } else {
// //
@ -287,7 +311,7 @@ const loadUserPosts = async () => {
...pageParams ...pageParams
} }
}); });
console.log('加载用户帖子响应:', response.data.records); console.log('加载用户帖子响应');
if (response && response.code === 200) { if (response && response.code === 200) {
const newPosts = response.data.records || []; const newPosts = response.data.records || [];
userPosts.value = [...userPosts.value, ...newPosts]; userPosts.value = [...userPosts.value, ...newPosts];

Loading…
Cancel
Save