Compare commits

..

No commits in common. 'main' and 'main' have entirely different histories.
main ... main

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

@ -55,4 +55,13 @@
<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/check-login", "/user/info/getuserinfo",
"/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,7 +10,6 @@ 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);
} }
@ -40,10 +39,6 @@ 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,4 +127,13 @@
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project> </project>

@ -70,22 +70,6 @@
<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,8 +35,7 @@ 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.getCategoryId(), post.getId(), newScore); redisUtil.zAdd("post:hot:", post.getId(), newScore);
redisUtil.zAdd("post:hot:all", post.getId(), newScore);
} }
public void calculateCommentScore(Comment comment){ public void calculateCommentScore(Comment comment){

@ -14,6 +14,4 @@ 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,8 +77,5 @@ public class PostBasicInfoDTO {
@Schema( @Schema(
description = "帖子创建时间" description = "帖子创建时间"
) )
private Long categoryId;
private LocalDateTime createTime; private LocalDateTime createTime;
} }

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

@ -91,8 +91,7 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
// TODO 消息通知? // TODO 消息通知?
} }
postSelector.calculatePostScore(post); postSelector.calculatePostScore(post);
redisUtil.zAdd("post:time:"+post.getCategoryId(), post.getId(), System.currentTimeMillis()); redisUtil.zAdd("post:time:", 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();
} }
@ -120,7 +119,6 @@ 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("删除帖子失败");
@ -128,10 +126,8 @@ 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:"+post.getCategoryId(), id); redisUtil.zRemove("post:time:", id);
redisUtil.zRemove("post:hot:"+post.getCategoryId(), id); redisUtil.zRemove("post:hot:", 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);
} }
@ -188,12 +184,7 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
return postBasicInfoDTO; return postBasicInfoDTO;
}); });
*/ */
String key; String key = postPageQueryDTO.getType().equals(0) ? "post:time:" : "post:hot:";
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,7 +20,6 @@ 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;
@ -33,7 +32,6 @@ 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;
@ -64,28 +62,10 @@ 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) {
log.info("检查登录状态 - 接收到的Authorization: {}", accessToken); if (accessToken != null && accessToken.startsWith("Bearer ")) {
log.info("检查登录状态 - 接收到的X-Refresh-Token: {}", refreshToken); accessToken = accessToken.substring(7);
// 检查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,3 +1,4 @@
##本地开发环境
#lj: #lj:
# db: # db:
# host: 192.168.59.129 # host: 192.168.59.129
@ -48,6 +49,6 @@ lj:
username: guest username: guest
password: guest password: guest
minio: minio:
endpoint: http://localhost:9000 endpoint: http://localhost:9005
accessKey: root accessKey: leezt
secretKey: 12345678 secretKey: lzt264610

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

Binary file not shown.

After

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 %>logo.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>珞珈岛</title> <title>Responsive Login And Registration</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.

Before

Width:  |  Height:  |  Size: 688 KiB

@ -23,11 +23,11 @@ function getRandomInt(min, max) {
onMounted(() => { onMounted(() => {
new Snow('#snow', new Snow('#snow',
{ {
num: getRandomInt(1,2), num: getRandomInt(1,4),
color: '#fff', color: '#fff',
maxR: 3, maxR: 3,
minR: 12, minR: 12,
maxSpeed: 0.3, maxSpeed: 0.4,
minSpeed: 0.1, minSpeed: 0.1,
swing: true, swing: true,
swingProbability: 0.1, swingProbability: 0.1,
@ -46,21 +46,6 @@ 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.

Before

Width:  |  Height:  |  Size: 992 KiB

File diff suppressed because one or more lines are too long

@ -97,18 +97,11 @@ 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(() => {
// userStore return userStore.userInfo;
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;
}); });
@ -160,47 +153,16 @@ const hideDropdown = () => {
}, 400); }, 400);
}; };
// //
onMounted(async () => { onMounted(() => {
if (userStore.isLoggedIn) { if (userStore.isLoggedIn) {
// token console.log('Header组件挂载: 检测到用户已登录:', userStore.userInfo.username, '角色:', userStore.userInfo.role);
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, () => { watch(() => userStore.userInfo, (newInfo) => {
console.log('用户信息更新:', newInfo.username, '角色:', newInfo.role);
}, { deep: true }); }, { deep: true });
</script> </script>
@ -211,13 +173,12 @@ watch(() => userStore.userInfo, () => {
align-items: center; align-items: center;
padding: 0 20px; /* 增加左右内边距,避免内容过于靠边 */ padding: 0 20px; /* 增加左右内边距,避免内容过于靠边 */
border-bottom: 2px solid #e0e0e0; border-bottom: 2px solid #e0e0e0;
background: rgba(255, 255, 255, 0.5); background: white;
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,47 +205,14 @@ 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,20 +2,19 @@
<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"
@ -30,54 +29,26 @@
<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([
const currentIndex = ref(0); { src: require('@/assets/whu1.jpg'), alt: '公告图片1' },
const router = useRouter(); { src: require('@/assets/whu2.jpg'), alt: '公告图片2' },
{ src: require('@/assets/whu3.jpg'), alt: '公告图片3' },
// 5 { src: require('@/assets/whu4.jpg'), alt: '公告图片4' },
const fetchImages = async () => { { src: require('@/assets/whu5.jpg'), alt: '公告图片5' },
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 currentIndex = ref(0);
const goToPostDetail = (postId) => {
if (postId) {
router.push({ name: 'PostDetail', params: { id: postId } });
}
};
// //
const nextImage = () => { const nextImage = () => {
if (images.value.length) currentIndex.value = (currentIndex.value + 1) % images.value.length;
currentIndex.value = (currentIndex.value + 1) % images.value.length;
}; };
// //
const prevImage = () => { const prevImage = () => {
if (images.value.length) currentIndex.value =
currentIndex.value = (currentIndex.value - 1 + images.value.length) % images.value.length; (currentIndex.value - 1 + images.value.length) % images.value.length;
}; };
// //
@ -88,8 +59,7 @@ const goToImage = (index) => {
// //
let interval = null; let interval = null;
onMounted(async () => { onMounted(() => {
await fetchImages();
interval = setInterval(nextImage, 5000); interval = setInterval(nextImage, 5000);
}); });
@ -101,17 +71,16 @@ onUnmounted(() => {
<style scoped> <style scoped>
.notice-board { .notice-board {
position: relative; position: relative;
background-color: rgb(90, 167, 111,0.8); background-color: #5aa76f;
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: 100%; width: 500px; /* 固定宽度 */
height: 100%; height: 350px; /* 固定高度 */
min-width: 220px; overflow: hidden; /* 防止图片溢出 */
max-width: 700px; padding: 10px; /* 内边距 */
overflow: hidden; box-sizing: border-box; /* 包括内边距在宽高内 */
box-sizing: border-box;
} }
.notice-title { .notice-title {
@ -128,34 +97,20 @@ 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 {
@ -189,7 +144,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-horizontal" ref="categoryBarRef"> <div class="category-buttons">
<button <button
v-for="(category, index) in categories" v-for="(category, index) in categories"
:key="index" :key="index"
@ -13,114 +13,72 @@
</button> </button>
</div> </div>
<!-- 帖子列表 --> <!-- 右侧帖子列表 -->
<div class="post-list"> <div class="post-list">
<div <div
v-for="(post, index) in postListStore.posts" v-for="(post, index) in filteredPosts"
: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 <img :src="post.avatar" :alt="头像" class="post-avatar" />
:src="(post.userName === '匿名用户' || post?.status === 1) <h3 class="post-title">{{ post.title }}</h3>
? 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, onMounted, onUnmounted, watch } from 'vue'; import { ref, computed, 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 categoryMap = [ const categories = ref(['全部', '学习', '娱乐', '二手交易']);
{ 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 showBackToTop = ref(false); const filteredPosts = computed(() => {
if (selectedCategory.value === '全部') return postListStore.posts;
return postListStore.posts.filter(post => post.category === selectedCategory.value);
});
const selectCategory = async (categoryName) => { //
selectedCategory.value = categoryName; const selectCategory = async (category) => {
selectedCategory.value = category;
postListStore.resetList(); postListStore.resetList();
const categoryObj = categoryMap.find(item => item.name === categoryName); await postListStore.getList({});
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
) { ) {
const categoryObj = categoryMap.find(item => item.name === selectedCategory.value); await postListStore.getList({});
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);
}); });
@ -128,175 +86,115 @@ 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;
background-color: rgba(249, 227, 238, 0.8); flex-direction: row;
flex-direction: column; align-items: flex-start;
align-items: stretch; padding: 20px;
padding: 5px 0;
box-sizing: border-box; box-sizing: border-box;
gap: 1px; gap: 20px;
width: 100%;
} }
/* 顶部横向分类按钮样式 */ /* 左侧分类按钮样式 */
.category-buttons-horizontal { .category-buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
gap: 0; gap: 0;
width: 100%; width: 200px;
margin-bottom: 10px;
border-bottom: 1px solid #eee;
background: rgba(218, 213, 213, 0.6);
padding-bottom: 2px;
} }
.category-button { .category-button {
padding: 10px 24px; padding: 10px 20px;
font-size: 14px; font-size: 14px;
border: none; border: 1px solid #ccc;
background-color: transparent; border-bottom: none;
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;
outline: none; .category-button:last-child {
border-bottom: 1px solid #ccc;
} }
.category-button.active { .category-button.active {
background-color: #f6fff8; background-color: #5aa76f;
color: #5aa76f; color: white;
border-bottom: 2px solid #5aa76f; border-color: #5aa76f;
font-weight: bold;
} }
.category-button:hover { .category-button:hover {
background-color: #f0f0f0; background-color: #e0e0e0;
} }
/* 帖子列表区域占满父容器左右margin为10px */ /* 右侧帖子列表样式 */
.post-list { .post-list {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); grid-template-columns: repeat(2, 1fr);
gap: 15px; gap: 20px;
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: 6px; margin-bottom: 10px;
} }
.post-avatar { .post-avatar {
width: 36px; width: 40px;
height: 36px; height: 40px;
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 0 8px 0; margin: 0;
color: #333;
word-break: break-all;
padding-left: 30px;
} }
/* 图片展示 */ /* 帖子摘要样式 */
.post-image-wrapper { .post-summary {
width: 100%; font-size: 14px;
height: 160px; color: #666;
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: 16px; font-size: 12px;
color: #999; color: #999;
position: absolute; position: absolute; /* 绝对定位 */
bottom: 5px; bottom: 10px; /* 距离卡片底部 10px */
left: 15px; left: 15px; /* 距离卡片左侧 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,7 +33,6 @@
<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();
@ -41,31 +40,10 @@ 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 = [
@ -77,7 +55,11 @@ 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;
}; };
// //
@ -90,12 +72,8 @@ initializeDate();
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 300px;
height: 100%; height: 350px;
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;
@ -103,8 +81,6 @@ 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 {
@ -112,7 +88,7 @@ initializeDate();
font-weight: bold; font-weight: bold;
color: #5aa76f; color: #5aa76f;
text-align: center; text-align: center;
margin-bottom: 1px; margin-bottom: 20px;
} }
.welcome-body { .welcome-body {
@ -120,7 +96,7 @@ initializeDate();
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
width: 100%; width: 100%;
margin-bottom: 1px; margin-bottom: 20px;
} }
.month-column, .month-column,
@ -128,19 +104,17 @@ initializeDate();
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
font-size: 20px; font-size: 16px;
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: 10vw; /* 用vw自适应字体大小 */ font-size: 180px; /* 日期号字体放大 */
min-font-size: 40px;
max-font-size: 120px;
font-weight: bold; font-weight: bold;
color: #5aa76f; color: #5aa76f;
flex: 1; /* 占据中间空间 */ flex: 1; /* 占据中间空间 */

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

@ -142,25 +142,16 @@ router.beforeEach(async (to, from, next) => {
}); });
console.log('从localStorage恢复用户登录状态:', username); console.log('从localStorage恢复用户登录状态:', username);
// 立即验证token有效性 // 异步验证token有效性但不阻塞导航
try { setTimeout(() => {
console.log('验证恢复的登录状态...'); checkLoginStatus().catch(err => {
const isLoggedIn = await checkLoginStatus(); console.error('Token验证失败但不影响当前导航:', err);
if (!isLoggedIn) { });
console.warn('恢复的登录状态无效'); }, 100);
ElMessage.warning('登录已过期,请重新登录');
next({ path: '/user/login', query: { redirect: to.fullPath } }); // 继续导航
return; next();
} 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验证登录状态
@ -169,29 +160,15 @@ router.beforeEach(async (to, from, next) => {
const isLoggedIn = await checkLoginStatus(); const isLoggedIn = await checkLoginStatus();
if (!isLoggedIn) { if (!isLoggedIn) {
ElMessage.warning('请先登录'); ElMessage.warning('请先登录');
next({ path: '/user/login', query: { redirect: to.fullPath } }); next({ path: '/' });
return; return;
} }
} catch (error) { } catch (error) {
console.error('登录状态验证失败:', error); console.error('登录状态验证失败:', error);
ElMessage.warning('登录状态验证失败,请重新登录'); ElMessage.warning('登录状态验证失败,请重新登录');
next({ path: '/user/login', query: { redirect: to.fullPath } }); next({ path: '/' });
return; return;
} }
} else {
// 用户已登录但仍然验证一下token有效性不阻塞导航
setTimeout(async () => {
try {
const isLoggedIn = await checkLoginStatus();
if (!isLoggedIn) {
console.warn('登录状态已失效');
// 不中断用户操作,只显示提示
ElMessage.warning('登录已过期,请重新登录');
}
} catch (error) {
console.error('登录状态验证失败:', error);
}
}, 100);
} }
} }
@ -200,7 +177,7 @@ router.beforeEach(async (to, from, next) => {
// 检查用户是否登录且是否有管理员权限 // 检查用户是否登录且是否有管理员权限
if (!userStore.isLoggedIn) { if (!userStore.isLoggedIn) {
ElMessage.error('请先登录'); ElMessage.error('请先登录');
next({ path: '/user/login', query: { redirect: to.fullPath } }); next({ path: '/' });
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,27 +6,41 @@ export const usePostListStore = defineStore('postList', {
posts: [], // 帖子列表 posts: [], // 帖子列表
total: 0, // 帖子总数 total: 0, // 帖子总数
page: 1, // 当前页码 page: 1, // 当前页码
pageSize: 6, // 每页帖子数 pageSize: 10, // 每页帖子数
lastVal: Date.now(), // 用于滚动分页的时间戳 lastVal: Date.now(), // 用于滚动分页的时间戳
offset: 0, // 偏移量 offset: 0, // 偏移量
loading: false, // 加载状态 loading: false, // 加载状态
finished: false, // 是否加载完全部 finished: false, // 是否加载完全部
currentCategory: null, // 当前分类id
}), }),
actions: { actions: {
async getList(categoryId = null) { setPosts(posts) {
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
let url = `/post/list?lastVal=${lastVal}&offset=${this.offset}&size=${this.pageSize}`; const 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;
@ -37,10 +51,11 @@ 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,
categoryId: post.categoryId, category: post.category || '全部',
createTime: post.createTime, createTime: post.createTime,
userName: post.userName, userName: post.userName,
})); }));
@ -50,7 +65,6 @@ 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 || userData.username || '', 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 !== undefined ? userData.gender : 0, gender: userData.gender || 0,
role: userData.role || 1, role: userData.role || 1,
status: userData.status || 1 status: userData.status || 1
}; };
@ -38,22 +38,19 @@ 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 || '');
localStorage.setItem('phone', this.userInfo.phone || ''); console.log('用户登录成功,状态已更新:', this.userInfo);
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,
@ -65,14 +62,10 @@ 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);
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');
}, },
logout() { logout() {
@ -96,10 +89,8 @@ 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');
localStorage.removeItem('phone'); console.log('用户已登出');
localStorage.removeItem('college'); },
localStorage.removeItem('gender'); },
}
}
}); });

@ -13,105 +13,49 @@ 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': tokenWithPrefix, 'Authorization': `Bearer ${accessToken}`,
'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('登录状态有效'); console.log('登录状态有效,用户信息:', response.data.data);
// 获取用户数据
const userData = response.data.data;
// 更新用户状态 // 更新用户状态
const userStore = useUserStore(); const userStore = useUserStore();
// 使用后端返回的用户数据更新前端状态
userStore.login({ userStore.login({
userid: userData.userId, userid: response.data.data.userId,
userName: userData.username, userName: response.data.data.username,
avatar: userData.avatar, avatar: response.data.data.avatar,
role: userData.role || 1, role: response.data.data.role,
accessToken: userData.accessToken || accessToken, accessToken: response.data.data.accessToken || accessToken,
refreshToken: userData.refreshToken || refreshToken refreshToken: response.data.data.refreshToken || refreshToken
}); });
// 如果返回了新的token更新本地存储 // 如果返回了新的token更新本地存储
if (userData.accessToken) { if (response.data.data.accessToken) {
console.log('更新accessToken'); localStorage.setItem('accessToken', response.data.data.accessToken);
localStorage.setItem('accessToken', userData.accessToken);
} }
if (userData.refreshToken) { if (response.data.data.refreshToken) {
console.log('更新refreshToken'); localStorage.setItem('refreshToken', response.data.data.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;
@ -124,15 +68,11 @@ 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('网络错误或服务器未响应'); console.log('网络错误或服务器未响应尝试从localStorage恢复登录状态');
// 如果localStorage中有用户信息则维持登录状态 // 如果localStorage中有用户信息则维持登录状态
const userId = localStorage.getItem('userId'); const userId = localStorage.getItem('userId');
@ -148,12 +88,9 @@ 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,7 +2,6 @@ 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
}); });
@ -48,6 +47,8 @@ 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')))) {
@ -58,6 +59,7 @@ 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('登录已失效,请重新登录');
@ -86,29 +88,21 @@ 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': tokenWithPrefix, 'Authorization': `Bearer ${accessToken}`,
'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'] = newTokenWithPrefix; originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
originalRequest.headers['X-Refresh-Token'] = refreshToken; originalRequest.headers['X-Refresh-Token'] = refreshToken;
// 处理队列中的请求 // 处理队列中的请求
@ -117,7 +111,6 @@ 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只提示用户
@ -126,7 +119,6 @@ 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格式错误时才清除
@ -145,9 +137,7 @@ request.interceptors.response.use(
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
requestsQueue.push({ requestsQueue.push({
resolve: token => { resolve: token => {
// 确保token有Bearer前缀 originalRequest.headers['Authorization'] = `Bearer ${token}`;
const tokenWithPrefix = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
originalRequest.headers['Authorization'] = tokenWithPrefix;
resolve(axios(originalRequest)); resolve(axios(originalRequest));
}, },
reject: err => { reject: err => {
@ -165,6 +155,7 @@ 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')) {
@ -173,14 +164,13 @@ request.interceptors.request.use(
// 检查token格式是否正确 // 检查token格式是否正确
if (token && refreshToken && typeof token === 'string' && typeof refreshToken === 'string') { if (token && refreshToken && typeof token === 'string' && typeof refreshToken === 'string') {
// 确保添加Bearer前缀 config.headers['Authorization'] = `Bearer ${token}`;
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,7 +15,6 @@
<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>
@ -144,14 +143,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_1.png'); const defaultAvatar = require('@/assets/default-avatar/boy_4.png');
// //
const handleAvatarError = "this.onerror=null;this.src='" + defaultAvatar + "'"; const handleAvatarError = "this.onerror=null;this.src='" + defaultAvatar + "'";
@ -385,10 +384,9 @@ 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: #fff; background: #f8f9fa;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px rgba(52, 152, 219, 0.08);
position: relative;
} }
.title { .title {
@ -398,8 +396,6 @@ 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 {
@ -432,15 +428,13 @@ onMounted(() => {
} }
.new-avatar { .new-avatar {
border-color: #ff88aa; border-color: #3498db;
} }
.upload-btn { .upload-btn {
display: flex; display: inline-block;
align-items: center;
gap: 8px;
padding: 0.5rem 1.2rem; padding: 0.5rem 1.2rem;
background: #ff88aa; background: linear-gradient(90deg, #3498db 60%, #6dd5fa 100%);
color: white; color: white;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
@ -451,12 +445,7 @@ onMounted(() => {
} }
.upload-btn:hover { .upload-btn:hover {
background: #ff6699; background: linear-gradient(90deg, #2980b9 60%, #3498db 100%);
}
.plus-icon {
font-size: 20px;
font-weight: bold;
} }
.file-input { .file-input {
@ -494,17 +483,17 @@ onMounted(() => {
.input, .textarea, select.input { .input, .textarea, select.input {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem;
border: 1px solid #e0e0e0; border: 1px solid #bdc3c7;
border-radius: 8px; border-radius: 4px;
font-size: 0.95rem; font-size: 0.95rem;
transition: border-color 0.3s ease; transition: border-color 0.3s ease;
background: #f9f9f9; background: #fff;
} }
.input:focus, .textarea:focus, select.input:focus { .input:focus, .textarea:focus, select.input:focus {
outline: none; outline: none;
border-color: #ff88aa; border-color: #3498db;
box-shadow: 0 0 0 2px rgba(255, 136, 170, 0.08); box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.08);
} }
.input-error { .input-error {
@ -525,10 +514,10 @@ onMounted(() => {
.submit-btn { .submit-btn {
width: 100%; width: 100%;
padding: 0.85rem; padding: 0.85rem;
background: #ff88aa; background: linear-gradient(90deg, #3498db 60%, #6dd5fa 100%);
color: white; color: white;
border: none; border: none;
border-radius: 8px; border-radius: 4px;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
@ -538,34 +527,6 @@ onMounted(() => {
} }
.submit-btn:hover { .submit-btn:hover {
background: #ff6699; background: linear-gradient(90deg, #2980b9 60%, #3498db 100%);
}
/* 花瓣动画(与背景呼应) */
@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,12 +1,17 @@
<template> <template>
<div> <div>
<div class="top-container"> <div class="top-container">
<NoticeBoard class="notice-flex"/> <NoticeBoard />
<WelcomeCalendar class="calendar-flex"/> <WelcomeCalendar />
</div> </div>
<div class="post-container"> <div class="post-container">
<PostPage /> <PostPage />
</div> </div>
<!-- <UserPage /> -->
<!-- <PostDetail /> -->
</div> </div>
</template> </template>
@ -14,29 +19,33 @@
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;
align-items: stretch; justify-content: center; /* 居中排列 */
gap: 16px; align-items: center; /* 顶部对齐 */
width: 100%; padding: 10px;
max-width: 1000px; gap: 10px; /* 设置较小的间距 */
height: 300px; width: 820px; /* 固定宽度 */
margin: 0 auto; height: 350px; /* 固定高度 */
margin:20px auto 0; /* 上边距20px左右居中 */
overflow: hidden; /* 防止溢出 */
box-sizing: border-box; /* 包括内边距在宽高内 */
} }
.notice-flex, .calendar-flex { .top-container > * {
height: 100%; flex-shrink: 0;
} }
.post-container { .post-container {
display: flex; display: flex;
justify-content: center; justify-content: center; /* 居中排列 */
align-items: flex-start; align-items: center; /* 顶部对齐 */
padding: 10px; padding: 10px;
width: 100%; width: 820px; /* 固定宽度 */
max-width: 1000px; margin: 20px auto; /* 上边距20px左右居中 */
margin: 10px auto; box-sizing: border-box; /* 包括内边距在宽高内 */
box-sizing: border-box;
} }
</style> </style>

@ -1,164 +1,148 @@
<template> <template>
<div class="main-content"> <div class="post-detail-container">
<!-- 帖子内容 --> <!-- 作者信息栏 -->
<div class="post-content"> <div class="author-info" v-if="author && author.userName">
<div class="author-row"> <img
<img :src="author.userAvatar || require('@/assets/default-avatar/boy_1.png')"
:src="(author.userName === '匿名用户' || postDetailStore.post?.status === 1 ) alt="头像"
? require('@/assets/default-avatar/boy_4.png') class="author-avatar"
: author.userAvatar|| require('@/assets/default-avatar/boy_1.png') " @click="goUserHome(author.userId)"
alt="头像" style="cursor: pointer;"
class="author-avatar" />
:style="{ cursor: (author.userName === '匿名用户' || postDetailStore.post?.status === 1 ) ? 'not-allowed' : 'pointer' }" <div class="author-details">
@click="handleAuthorAvatarClick" <h3 class="author-name">{{ author.userName || '匿名用户' }}</h3>
/> <p class="author-stats">粉丝数{{ author.followers ?? 0 }}</p>
<div class="author-info"> <button @click="toggleFollow" class="follow-button">
<h3 class="author-name">{{ author.userName || '匿名用户' }}</h3> {{ isFollowing ? '取消关注' : '关注' }}
<button @click="toggleFollow" class="follow-button"> </button>
{{ isFollowing ? '取消关注' : '关注' }} </div>
</button> </div>
</div>
<h1 class="post-title">{{ postDetailStore.post?.title || '' }}</h1> <!-- 帖子内容 -->
</div> <div class="post-content">
<p class="post-body">{{ postDetailStore.post?.content || '' }}</p> <img :src="author?.userAvatar || require('@/assets/default-avatar/boy_1.png')" alt="作者头像" class="post-author-avatar" />
<img <h1 class="post-title">{{ postDetailStore.post?.title || '' }}</h1>
v-if="postDetailStore.post?.image" <p class="post-body">{{ postDetailStore.post?.content || '' }}</p>
:src="postDetailStore.post.image" <div class="post-stats">
alt="帖子封面" <span> 👁 {{ postDetailStore.post?.viewCount ?? 0 }}</span>
class="post-cover" <span> 🗨 {{ postDetailStore.post?.commentCount ?? 0 }}</span>
@click="showImagePreview = true" <span>
/> <button
<!-- 大图预览遮罩 --> @click="postDetailStore.PostLike"
<div v-if="showImagePreview" class="image-preview-mask" @click="showImagePreview = false"> class="like-btn"
<img :src="postDetailStore.post.image" class="image-preview-big" @click.stop /> :class="{ liked: postDetailStore.isLike }"
</div> >
<div class="post-stats"> <span v-if="!postDetailStore.isLike"></span>
<span> 👁 {{ postDetailStore.post?.viewCount ?? 0 }}</span> <span v-else></span>
<span> 🗨 {{ postDetailStore.post?.commentCount ?? 0 }}</span> {{ postDetailStore.post?.likeCount ?? 0 }}
<span> </button>
<button </span>
@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>
<!-- 评论区 --> <div class="post-time">
<div class="comments-section"> 发布时间{{ postDetailStore.post?.createTime ? formatTime(postDetailStore.post.createTime) : '' }}
<h2 class="comments-title">评论</h2> </div>
<ul class="comments-list"> </div>
<li v-if="postDetailStore.comments.length === 0" class="comments-finished-tip">
暂时没有评论哦快来发表吧 <!-- 评论区 -->
</li> <div class="comments-section">
<li v-for="comment in postDetailStore.comments" :key="comment.id" class="comment-item"> <h2 class="comments-title">评论</h2>
<img <ul class="comments-list">
:src="comment.userAvatar || require('@/assets/default-avatar/boy_1.png')" <li v-for="comment in postDetailStore.comments" :key="comment.id" class="comment-item">
alt="评论者头像" <img
class="comment-avatar" :src="comment.userAvatar || require('@/assets/default-avatar/boy_1.png')"
@click="goUserHome(comment.userId)" alt="评论者头像"
style="cursor: pointer;" class="comment-avatar"
/> @click="goUserHome(comment.userId)"
<div class="comment-content"> style="cursor: pointer;"
<p class="comment-name">{{ comment.userName || '匿名用户' }}</p> />
<p class="comment-text">{{ comment.content || '' }}</p> <div class="comment-content">
<p class="comment-name">{{ comment.userName || '匿名用户' }}</p>
<p class="comment-text">{{ comment.content || '' }}</p>
<div class="comment-meta">
<span class="comment-time">{{ comment.createTime ? formatTime(comment.createTime) : '' }}</span>
<button <button
class="delete-btn" @click="postDetailStore.CommentLike(comment)"
: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 class="comment-meta"> </div>
<span class="comment-time">{{ comment.createTime ? formatTime(comment.createTime) : '' }}</span> <!-- 子评论列表 -->
<button class="reply-btn" @click="startReply(comment)"></button> <ul v-if="comment.showReplies && comment.replies && comment.replies.length > 0" class="replies-list">
<span v-if="comment.replyCount > 0"> <li v-for="reply in comment.replies" :key="reply.id" class="comment-item reply-item">
<button @click="loadReplies(comment)" class="expand-reply-btn"> <img
{{ comment.showReplies> 0 ? '收起回复' : '展开回复' }} ({{ comment.replyCount }}) :src="reply.userAvatar || require('@/assets/default-avatar/boy_1.png')"
</button> alt="评论者头像"
</span> class="comment-avatar"
<button @click="goUserHome(reply.userId)"
@click="postDetailStore.CommentLike(comment)" style="cursor: pointer;"
:class="['like-btn', { liked: comment.isLike }]" />
> <div class="comment-content">
<span v-if="!comment.isLike"></span> <p class="comment-name">
<span v-else></span> {{ reply.userName || '匿名用户' }}
{{ comment.likeCount ?? 0 }} <span v-if="reply.replyUserName" class="reply-user"> @{{ reply.replyUserName }}</span>
</button> </p>
</div> <p class="comment-text">{{ reply.content || '' }}</p>
<!-- 子评论列表 --> <div class="comment-meta">
<ul v-if="comment.showReplies && comment.replies && comment.replies.length > 0" class="replies-list"> <span class="comment-time">{{ reply.createTime ? formatTime(reply.createTime) : '' }}</span>
<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
class="delete-btn" @click="postDetailStore.CommentLike(reply)"
: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>
</li> </div>
<li v-if="comment.repliesLoading" class="reply-loading">...</li> </li>
<li v-if="comment.repliesFinished" class="reply-finished"></li> <li v-if="comment.repliesLoading" class="reply-loading">...</li>
</ul> <li v-if="comment.repliesFinished" class="reply-finished"></li>
</div> </ul>
</li> </div>
<li v-if="postDetailStore.comments.length > 0 && postDetailStore.commentsFinished" class="comments-finished-tip"> </li>
没有更多评论啦...... </ul>
</li> </div>
</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, ElMessage } from 'element-plus'; import { ElMessageBox } 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';
@ -168,7 +152,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 || {});
@ -254,19 +238,6 @@ 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,
@ -288,32 +259,30 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
.main-content { .post-detail-container {
max-width: 800px; display: grid;
margin: 0 auto; grid-template-areas:
display: flex; "post-content author-info"
flex-direction: column; "comments-section author-info"
height: 100%; "comment-box author-info";
grid-template-columns: 3fr 1fr; /* 左侧内容占 3/4右侧作者信息占 1/4 */
gap: 20px; gap: 20px;
min-width: 0; padding: 20px;
min-height: 0; box-sizing: border-box;
background: transparent; max-width: 1200px; /* 设置页面最大宽度 */
border-radius: 8px; margin: 0 auto; /* 居中页面并添加左右留白 */
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: column; flex-direction: row; /* 水平排列头像和详情 */
align-items: flex-start; align-items: flex-start;
min-width: 90px; background-color: #f5f5f5;
margin-right: 18px; padding: 20px;
border: 1px solid #ccc;
max-height: 200px;
border-radius: 8px;
} }
.author-avatar { .author-avatar {
@ -324,11 +293,24 @@ onUnmounted(() => {
margin-right: 15px; /* 与作者详情的间距 */ margin-right: 15px; /* 与作者详情的间距 */
} }
.author-details {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.author-name { .author-name {
font-size: 28px; font-size: 18px;
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;
@ -347,64 +329,33 @@ onUnmounted(() => {
.post-content { .post-content {
grid-area: post-content; grid-area: post-content;
background-color: #ffffff; background-color: #ffffff;
padding: 5px; padding: 20px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 8px; border-radius: 8px;
position: relative; position: relative; /* 为绝对定位的元素提供参考 */
} }
.post-cover { .post-author-avatar {
position: static; position: absolute;
display: block; top: 20px;
margin: 5px auto 0 auto; right: 20px;
width: auto; width: 100px;
height: 200px; height: 100px;
max-width: 100%; border-radius: 50%;
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: 32px; font-size: 24px;
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: 20px; font-size: 16px;
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 {
@ -418,20 +369,18 @@ onUnmounted(() => {
.post-time { .post-time {
position: absolute; position: absolute;
bottom: 5px; /* 距离底部 20px */ bottom: 20px; /* 距离底部 20px */
right: 20px; /* 距离右侧 20px */ right: 20px; /* 距离右侧 20px */
font-size: 12px; font-size: 12px;
color: #999; color: #999;
} }
.comments-section { .comments-section {
flex: 1 1 0; grid-area: comments-section;
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 {
@ -467,7 +416,6 @@ onUnmounted(() => {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-left: 12px;
} }
.comment-name { .comment-name {
@ -479,9 +427,6 @@ 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 {
@ -496,50 +441,32 @@ 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 80px 32px 10px; /* 右下留空间给按钮 */ padding: 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 {
/* 位置已由内联style控制 */ align-self: flex-end;
padding: 8px 20px; padding: 10px 20px;
background-color: #5aa76f; background-color: #5aa76f;
color: white; color: white;
border: none; border: none;
@ -563,7 +490,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;
} }
@ -631,28 +558,21 @@ onUnmounted(() => {
border: none; border: none;
color: #f56c6c; color: #f56c6c;
cursor: pointer; cursor: pointer;
font-size: 18px; font-size: 13px;
margin-left: 10px; margin-left: 10px;
position: absolute;
top: 0;
right: 0;
z-index: 2;
} }
.delete-btn:hover { .delete-btn:hover {
color: #ff2222; text-decoration: underline;
background: #f9eaea;
border-radius: 50%;
} }
.like-btn { .like-btn {
margin-left: 8px; margin-left: 8px;
background: none; background: none;
border: none; border: none;
color: #222323; color: #409eff;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
} }
.like-btn.liked { .like-btn.liked {
color: #f56c6c; color: #f56c6c;
} }
</style> </style>

@ -12,51 +12,28 @@
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 class="input-field"> <select v-model="form.categoryId" required>
<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>
<div class="upload-wrapper"> <input type="file" accept="image/*" @change="onFileChange" />
<!-- 隐藏的文件选择input --> <div v-if="form.imagePreview" class="img-preview">
<input <img :src="form.imagePreview" alt="预览" />
type="file" <button type="button" @click="removeImage"></button>
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>
@ -65,13 +42,12 @@
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" class="submit-btn"> <button type="submit" :disabled="submitting">
{{ submitting ? '发布中...' : '立即发布' }} {{ submitting ? '发布中...' : '立即发布' }}
</button> </button>
</div> </div>
@ -95,10 +71,9 @@ 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: '',
@ -118,7 +93,9 @@ 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 {
@ -146,10 +123,6 @@ 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
@ -170,6 +143,7 @@ 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)
@ -204,181 +178,94 @@ 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: 12px; border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 12px 0 rgba(0,0,0,0.08);
} }
.page-title { .page-title {
text-align: center; text-align: center;
margin-bottom: 24px; margin-bottom: 24px;
font-size: 1.8em; font-size: 1.6em;
color: #2c3e50; color: #2c3e50;
border-bottom: 2px solid #ff88aa;
padding-bottom: 10px;
} }
.editor-wrapper {
padding: 20px;
}
.form-row { .form-row {
margin-bottom: 20px; margin-bottom: 18px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.form-row label { .form-row label {
font-weight: bold; font-weight: bold;
margin-bottom: 8px; margin-bottom: 6px;
color: #333;
} }
.form-row input[type="text"],
.input-field, .form-row select,
.textarea-field { .form-row textarea {
padding: 10px; padding: 8px;
border: 1px solid #e0e0e0; border: 1px solid #dcdcdc;
border-radius: 8px; border-radius: 4px;
font-size: 15px; font-size: 15px;
transition: border-color 0.3s;
}
.input-field:focus,
.textarea-field:focus {
outline: none;
border-color: #ff88aa;
} }
.form-row textarea {
.anonymous-option { resize: vertical;
flex-direction: row;
align-items: center;
justify-content: flex-start;
margin-bottom: 15px;
} }
.img-preview {
.anonymous-label { margin-top: 8px;
display: flex; position: relative;
align-items: center; display: inline-block;
gap: 8px;
color: #666;
} }
.img-preview img {
.anonymous-checkbox { max-width: 120px;
width: 18px; max-height: 120px;
height: 18px; border-radius: 4px;
accent-color: #ff88aa; border: 1px solid #eee;
} }
.img-preview button {
.textarea-field { position: absolute;
resize: vertical; top: 2px;
min-height: 150px; right: 2px;
background: #f56c6c;
color: #fff;
border: none;
border-radius: 3px;
padding: 2px 8px;
cursor: pointer;
font-size: 12px;
} }
.submit-area { .submit-area {
text-align: center; text-align: center;
margin-top: 30px; margin-top: 24px;
} }
.submit-area button {
.submit-btn { background: #409eff;
background: #ff88aa; color: #fff;
color: white;
border: none; border: none;
padding: 12px 40px; padding: 10px 32px;
border-radius: 8px; border-radius: 4px;
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
transition: background 0.3s;
} }
.submit-area button:disabled {
.submit-btn:disabled { background: #b3d8ff;
background: #ffcccc;
cursor: not-allowed; cursor: not-allowed;
} }
.submit-btn:hover {
background: #ff6699;
}
.result-alert { .result-alert {
margin-top: 20px; margin-top: 20px;
padding: 15px 20px; padding: 12px 18px;
border-radius: 8px; border-radius: 4px;
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,22 +226,15 @@ 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);
} }
} }
@ -252,24 +245,10 @@ const loadUserInfo = async () => {
timeout: 5000 timeout: 5000
}); });
console.log('获取用户信息响应'); console.log('获取用户信息响应:', response);
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) {
@ -281,17 +260,14 @@ 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 {
// //
@ -311,7 +287,7 @@ const loadUserPosts = async () => {
...pageParams ...pageParams
} }
}); });
console.log('加载用户帖子响应'); console.log('加载用户帖子响应:', response.data.records);
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