重构部分接口,改普通分页查询为游标滚动分页查询,优化缓存结构,完善消息模块

main
forely 6 days ago
commit 11d6d818ae

@ -16,6 +16,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
registry.addInterceptor(authInterceptor) registry.addInterceptor(authInterceptor)
.excludePathPatterns("/user/login", .excludePathPatterns("/user/login",
"/user/register", "/user/register",
"/user/captcha",
"/user/verify-captcha",
"/post/list", "/post/list",
"/post/detail", "/post/detail",
"/comment/list", "/comment/list",

@ -1,11 +0,0 @@
com\luojia\luojia_channel\advice\GlobalExceptionHandler.class
com\luojia\luojia_channel\domain\UserDTO.class
com\luojia\luojia_channel\utils\UserContext.class
com\luojia\luojia_channel\utils\JWTUtil.class
com\luojia\luojia_channel\config\RedisConfig.class
com\luojia\luojia_channel\utils\RedisUtil$ZSetItem.class
com\luojia\luojia_channel\domain\UserDTO$UserDTOBuilder.class
com\luojia\luojia_channel\exception\BaseException.class
com\luojia\luojia_channel\utils\RedisUtil.class
com\luojia\luojia_channel\domain\Result.class
com\luojia\luojia_channel\exception\UserException.class

@ -1,9 +1,19 @@
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\advice\GlobalExceptionHandler.java D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\advice\GlobalExceptionHandler.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\config\RedisConfig.java D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\MybatisConfig.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\domain\Result.java D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\OpenApiConfig.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\domain\UserDTO.java D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\RedisConfig.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\exception\BaseException.java D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\WebMvcConfig.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\exception\UserException.java D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\constants\RedisConstant.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\utils\JWTUtil.java D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\page\PageRequest.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\utils\RedisUtil.java D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\page\PageResponse.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\utils\UserContext.java D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\Result.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\UserDTO.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\BaseException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\FileException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\PostException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\UserException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\interceptor\AuthInterceptor.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\JWTUtil.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\PageUtil.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\RedisUtil.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\UserContext.java

@ -0,0 +1,84 @@
package com.luojia_channel.modules.captcha.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.modules.captcha.utils.CaptchaUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/user")
@Tag(name = "图形验证码", description = "图形验证码相关接口")
public class CaptchaController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
*
* @param request
* @param res
* @throws IOException
*/
@GetMapping("/captcha")
@Operation(
summary = "生成验证码图片"
)
public void generateCaptcha(HttpServletRequest request,
HttpServletResponse res) throws IOException {
CaptchaUtils captcha = new CaptchaUtils();
BufferedImage image = captcha.getImage();
String text = captcha.getText();
String captchaKey = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("captcha:" + captchaKey, text, 60, TimeUnit.SECONDS);
Cookie cookie = new Cookie("captchaKey", captchaKey);
cookie.setPath("/");
res.addCookie(cookie);
CaptchaUtils.output(image,res.getOutputStream());
}
/**
*
* @param session
* @param params
* @return
*/
@PostMapping("/verify-captcha")
public Result verifyCaptcha(@RequestBody Map<String, String> params, @CookieValue(value = "captchaKey", required = false) String captchaKey, HttpSession session) {
String captcha = params.get("captcha");
if (captchaKey == null) {
return Result.fail(500, "验证码已失效,请重新获取");
}
String redisKey = "captcha:" + captchaKey;
String correctCaptcha = redisTemplate.opsForValue().get(redisKey);
if (correctCaptcha == null) {
return Result.fail(500, "验证码已过期,请重新获取");
}
if (captcha.equalsIgnoreCase(correctCaptcha)) {
redisTemplate.delete(redisKey);
return Result.success();
} else {
return Result.fail(500, "图形验证码错误");
}
}
}

@ -0,0 +1,135 @@
package com.luojia_channel.modules.captcha.utils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
public class CaptchaUtils {
/**
*
*/
private int width = 180;
/**
*
*/
private int height = 30;
/**
*
*/
private String[] fontNames = { "宋体", "楷体", "隶书", "微软雅黑" };
/**
*
*/
private Color bgColor = new Color(255, 255, 255);
/**
*
*/
private Random random = new Random();
/**
* code
*/
private String codes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
/**
*
*/
private String text;
/**
*
* @return
*/
private Color randomColor() {
int red = random.nextInt(150);
int green = random.nextInt(150);
int blue = random.nextInt(150);
return new Color(red, green, blue);
}
/**
*
*
* @return
*/
private Font randomFont() {
String name = fontNames[random.nextInt(fontNames.length)];
int style = random.nextInt(4);
int size = random.nextInt(5) + 24;
return new Font(name, style, size);
}
/**
*
*
* @return
*/
private char randomChar() {
return codes.charAt(random.nextInt(codes.length()));
}
/**
* BufferedImage
*
* @return
*/
private BufferedImage createImage() {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
//设置验证码图片的背景颜色
g2.setColor(bgColor);
g2.fillRect(0, 0, width, height);
return image;
}
public BufferedImage getImage() {
BufferedImage image = createImage();
Graphics2D g2 = (Graphics2D) image.getGraphics();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 4; i++) {
String s = randomChar() + "";
sb.append(s);
g2.setColor(randomColor());
g2.setFont(randomFont());
float x = i * width * 1.0f / 4;
g2.drawString(s, x, height - 8);
}
this.text = sb.toString();
drawLine(image);
return image;
}
/**
* 线
*
* @param image
*/
private void drawLine(BufferedImage image) {
Graphics2D g2 = (Graphics2D) image.getGraphics();
int num = 5;
for (int i = 0; i < num; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g2.setColor(randomColor());
g2.setStroke(new BasicStroke(1.5f));
g2.drawLine(x1, y1, x2, y2);
}
}
public String getText() {
return text;
}
public static void output(BufferedImage image, OutputStream out) throws IOException {
ImageIO.write(image, "PNG", out);
}
}

@ -21,7 +21,7 @@ public class ValidateUserUtil {
private final UserMapper userMapper; private final UserMapper userMapper;
/** /**
* *
* *
* @param userRegisterDTO * @param userRegisterDTO
*/ */
@ -30,10 +30,6 @@ public class ValidateUserUtil {
String password = userRegisterDTO.getPassword(); String password = userRegisterDTO.getPassword();
String phone = userRegisterDTO.getPhone(); String phone = userRegisterDTO.getPhone();
String email = userRegisterDTO.getEmail(); String email = userRegisterDTO.getEmail();
String studentId = userRegisterDTO.getStudentId();
if (StrUtil.isBlank(username)){
throw new UserException("用户名不能为空");
}
if (StrUtil.isBlank(password)){ if (StrUtil.isBlank(password)){
throw new UserException("密码不能为空"); throw new UserException("密码不能为空");
} }
@ -42,12 +38,12 @@ public class ValidateUserUtil {
int cnt = 0; int cnt = 0;
if(StrUtil.isNotBlank(phone)) cnt++; if(StrUtil.isNotBlank(phone)) cnt++;
if(StrUtil.isNotBlank(email)) cnt++; if(StrUtil.isNotBlank(email)) cnt++;
if(StrUtil.isNotBlank(studentId)) cnt++; if(StrUtil.isNotBlank(username)) cnt++;
if (cnt == 0) { if (cnt == 0) {
throw new UserException("必须填写手机号、邮箱或学号其中一种注册方式"); throw new UserException("必须填写手机号、邮箱或用户名其中一种注册方式");
} }
if (cnt > 1) { if (cnt > 1) {
throw new UserException("只能选择一种注册方式(手机/邮箱/学号"); throw new UserException("只能选择一种注册方式(手机/邮箱/用户名");
} }
// 格式校验 // 格式校验
validateFormats(userRegisterDTO); validateFormats(userRegisterDTO);
@ -60,7 +56,6 @@ public class ValidateUserUtil {
String password = userRegisterDTO.getPassword(); String password = userRegisterDTO.getPassword();
String phone = userRegisterDTO.getPhone(); String phone = userRegisterDTO.getPhone();
String email = userRegisterDTO.getEmail(); String email = userRegisterDTO.getEmail();
String studentId = userRegisterDTO.getStudentId();
// 仅对非空字段做格式校验 // 仅对非空字段做格式校验
if (userMapper.exists(Wrappers.<User>lambdaQuery() if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, username))) { .eq(User::getUsername, username))) {
@ -69,14 +64,13 @@ public class ValidateUserUtil {
if(!password.matches(PASSWORD_REGEX)){ if(!password.matches(PASSWORD_REGEX)){
throw new UserException("密码格式错误"); throw new UserException("密码格式错误");
} }
validateUserFlag(phone, email, studentId, null); validateUserFlag(phone, email, null);
} }
public void validateFormats(UserChangeInfoDTO userChangeInfoDTO, Long currentUserId){ public void validateFormats(UserChangeInfoDTO userChangeInfoDTO, Long currentUserId){
String username = userChangeInfoDTO.getUsername(); String username = userChangeInfoDTO.getUsername();
String phone = userChangeInfoDTO.getPhone(); String phone = userChangeInfoDTO.getPhone();
String email = userChangeInfoDTO.getEmail(); String email = userChangeInfoDTO.getEmail();
String studentId = userChangeInfoDTO.getStudentId();
// String college = userChangeInfoDTO.getCollege(); // String college = userChangeInfoDTO.getCollege();
if (userMapper.exists(Wrappers.<User>lambdaQuery() if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, userChangeInfoDTO.getUsername()) .eq(User::getUsername, userChangeInfoDTO.getUsername())
@ -84,10 +78,10 @@ public class ValidateUserUtil {
throw new UserException("用户名已被使用"); throw new UserException("用户名已被使用");
} }
validateUserFlag(phone, email, studentId, currentUserId); validateUserFlag(phone, email, currentUserId);
} }
private void validateUserFlag(String phone, String email, String studentId, Long currentUserId) { private void validateUserFlag(String phone, String email, Long currentUserId) {
if(StrUtil.isNotBlank(phone)){ if(StrUtil.isNotBlank(phone)){
if(!phone.matches(PHONE_REGEX)) if(!phone.matches(PHONE_REGEX))
throw new UserException("手机号格式错误"); throw new UserException("手机号格式错误");
@ -106,15 +100,6 @@ public class ValidateUserUtil {
throw new UserException("邮箱已存在"); throw new UserException("邮箱已存在");
} }
} }
if(StrUtil.isNotBlank(studentId)){
if(!studentId.matches(STUDENTID_REGEX))
throw new UserException("学号格式错误");
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getStudentId, studentId)
.ne(currentUserId != null, User::getId, currentUserId))) {
throw new UserException("学号已存在");
}
}
} }
} }

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luojia_channel.modules.message.mapper.MessageMapper">
<!-- 定义 ChatItemDTO 结果映射 -->
<resultMap id="ChatItemDTOResultMap" type="com.luojia_channel.modules.interact.dto.ChatItemDTO">
<id property="chatUserId" column="chat_user_id"/>
<result property="avatar" column="avatar"/>
<result property="username" column="username"/>
<result property="latestMessage" column="latest_message"/>
<result property="latestTime" column="create_time"/>
</resultMap>
<!-- 查询用户的所有聊天对象及最新消息 -->
<select id="selectChatList" resultMap="ChatItemDTOResultMap">
SELECT
CASE
WHEN m.sender_id = #{userId} THEN m.receiver_id
ELSE m.sender_id
END AS chat_user_id,
u.avatar,
u.username,
m.content AS latest_message,
m.create_time
FROM message m
JOIN (
-- 子查询获取每个聊天对象的最新消息ID
SELECT
MAX(id) AS max_id
FROM message
WHERE sender_id = #{userId} OR receiver_id = #{userId}
GROUP BY
CASE
WHEN sender_id = #{userId} THEN receiver_id
ELSE sender_id
END
) latest ON m.id = latest.max_id
LEFT JOIN user u ON
CASE
WHEN m.sender_id = #{userId} THEN m.receiver_id
ELSE m.sender_id
END = u.id
WHERE
m.sender_id = #{userId} OR m.receiver_id = #{userId}
GROUP BY
CASE
WHEN m.sender_id = #{userId} THEN m.receiver_id
ELSE m.sender_id
END
ORDER BY
m.create_time DESC
</select>
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.message.entity.MessageDO">
SELECT * FROM message
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
ORDER BY FIELD(id,
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
</select>
</mapper>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luojia_channel.modules.post.mapper.CommentMapper">
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.post.entity.Comment">
SELECT * FROM comment
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
ORDER BY FIELD(id,
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
</select>
</mapper>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luojia_channel.modules.post.mapper.PostMapper">
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.post.entity.Post">
SELECT * FROM post
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
ORDER BY FIELD(id,
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
</select>
</mapper>

File diff suppressed because it is too large Load Diff

@ -15,6 +15,7 @@
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.16", "@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16", "@babel/eslint-parser": "^7.12.16",
@ -28,7 +29,8 @@
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,
"env": { "env": {
"node": true "node": true,
"vue/setup-compiler-macros": true
}, },
"extends": [ "extends": [
"plugin:vue/vue3-essential", "plugin:vue/vue3-essential",

@ -31,20 +31,28 @@
</li> </li>
</ul> </ul>
</div> </div>
<router-link to="/feedback" class="nav-btn">反馈站</router-link>
<router-link to="/notificationlist" class="nav-btn">通知</router-link>
</div> </div>
<!-- 登录/注册按钮 --> <!-- 登录/注册按钮 -->
<div class="nav-section"> <div class="nav-section">
<button v-if="!isLoggedIn" @click="showModal" class="login-btn">/</button> <button v-if="!isLoggedIn" @click="showModal" class="login-btn">/</button>
<div v-else class="user-avatar"> <div v-else class="user-menu">
<img :src="userInfo.avatar || defaultAvatar" alt="用户头像" class="avatar-img" /> <router-link to="/notificationlist" class="nav-btn">通知</router-link>
<div class="dropdown-menu"> <router-link to="/feedback" class="nav-btn">反馈站</router-link>
<button @click="goToProfile"></button> <div class="user-avatar" @mouseenter="showDropdown" @mouseleave="hideDropdown">
<button @click="logout">退</button> <img :src="userInfo.avatar || defaultAvatar" alt="用户头像" class="avatar-img" />
<!-- 悬浮板块 -->
<div class="user-dropdown-menu" v-show="isDropdownVisible">
<p class="user-name">{{ userInfo.userName }}</p>
<div class="button-container">
<button @click="goToProfile" class="dropdown-btn">个人中心</button>
<button @click="logout" class="dropdown-btn">退出登录</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 登录/注册模态框组件仅在 isModalVisible true 时显示 --> <!-- 登录/注册模态框组件仅在 isModalVisible true 时显示 -->
@ -52,16 +60,17 @@
</header> </header>
</template> </template>
<script setup name="Header"> <script lang="js" setup name="Header">
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useUserStore } from '@/stores/user.ts'; import { useUserStore } from '@/stores/user.js';
import LoginRegisterModal from './LoginRegisterModal.vue'; import LoginRegisterModal from './LoginRegisterModal.vue';
import { useRouter } from 'vue-router';
// //
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter();
// //
const isModalVisible = ref(false); const isModalVisible = ref(false);
const isDropdownVisible = ref(false);
const colleges = ref([ const colleges = ref([
{ name: '武大官网', url: 'https://www.whu.edu.cn/' }, { name: '武大官网', url: 'https://www.whu.edu.cn/' },
{ name: '计算机学院', url: 'https://cs.whu.edu.cn/' }, { name: '计算机学院', url: 'https://cs.whu.edu.cn/' },
@ -82,7 +91,7 @@ const colleges = ref([
]); ]);
// //
const defaultAvatar = '@/assets/default-avatar.png'; const defaultAvatar = require("@/assets/default-avatar/boy_4.png");
// //
const isLoggedIn = computed(() => userStore.isLoggedIn); const isLoggedIn = computed(() => userStore.isLoggedIn);
@ -98,13 +107,31 @@ const hideModal = () => {
}; };
const goToProfile = () => { const goToProfile = () => {
// router.push({name: 'UserPage',params: { userId: userInfo.value.userid }});
//window.location.href = '/profile'; isDropdownVisible.value = false; //
}; };
const logout = () => { const logout = () => {
userStore.logout(); userStore.logout();
window.location.href = '/'; router.push({ name: 'Home' }); //
isDropdownVisible.value = false; //
};
let showTimer = null; //
let hideTimer = null; //
const showDropdown = () => {
clearTimeout(hideTimer); //
showTimer = setTimeout(() => {
isDropdownVisible.value = true; // 0.5
}, 200);
};
const hideDropdown = () => {
clearTimeout(showTimer); //
hideTimer = setTimeout(() => {
isDropdownVisible.value = false; // 1
}, 400);
}; };
</script> </script>
@ -217,7 +244,7 @@ const logout = () => {
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
background-color: white; background-color: rgb(255, 255, 255);
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
@ -293,4 +320,68 @@ const logout = () => {
color: #6fbd87; color: #6fbd87;
background: #f0f0f0; background: #f0f0f0;
} }
.user-menu{
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.avatar-img {
width: 35px;
height: 35px;
border-radius: 50%;
cursor: pointer;
}
/* 头像悬浮板块样式 */
.user-dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 10px;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center; /* 水平居中用户名 */
text-align: center; /* 确保文字居中 */
gap: 10px; /* 增加子元素之间的间距 */
width: 200px; /* 固定宽度,确保布局一致 */
}
/* 用户名样式 */
.user-name {
font-size: 20px;
font-weight: bold;
margin-bottom: 10px;
color: #54ac52;
}
.button-container {
display: flex;
justify-content: space-between; /* 按钮左右排列 */
width: 100%; /* 按钮容器占满父容器宽度 */
}
/* 悬浮板块按钮样式 */
.dropdown-btn {
width: 45%; /* 按钮占据父容器的 45% 宽度 */
padding: 8px 10px;
background: none;
border: 0;
border-radius: 4px;
text-align: center;
font-size: 14px;
color: #333;
cursor: pointer;
transition: background-color 0.3s, color 0.3s;
}
.dropdown-btn:hover {
background-color: #f0f0f0;
color: #6fbd87;
}
</style> </style>

@ -15,6 +15,7 @@
</div> </div>
<!-- 登录表单 --> <!-- 登录表单 -->
<button @click="login" v-if="false"></button>
<form @submit.prevent="login" class="login-form"> <form @submit.prevent="login" class="login-form">
<div class="input-group"> <div class="input-group">
<!-- 用户标识输入框用户名/邮箱/手机 --> <!-- 用户标识输入框用户名/邮箱/手机 -->
@ -70,195 +71,182 @@
</div> </div>
</template> </template>
<script> <script setup name="UserLogin">
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import axios from 'axios';
import request from '@/utils/request'; import request from '@/utils/request';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { useUserStore } from '@/stores/user.ts'; import { useUserStore } from '@/stores/user.js';
export default { const currentType = ref('username'); //
name: 'UserLogin', const cooldown = ref(0); //
//TODO: props, { emit } const userStore = useUserStore(); //
setup() { const emit = defineEmits(['toggleForm', 'LoginSuccess']); //
const currentType = ref('username'); // //
const cooldown = ref(0); // const loginTypes = [
const userStore = useUserStore(); // { label: '用户名登录', value: 'username' },
// { label: '邮箱登录', value: 'email' },
const loginTypes = [ { label: '手机登录', value: 'phone' }
{ label: '用户名登录', value: 'username' }, ];
{ label: '邮箱登录', value: 'email' },
{ label: '手机登录', value: 'phone' } //
]; const loginForm = ref({
userFlag: '', //
// password: '', //
const loginForm = ref({ verifyCode: '', // /使
userFlag: '', // remember: false //
password: '', // });
verifyCode: '', // /使
remember: false // //
}); const inputType = computed(() => {
switch (currentType.value) {
// case 'email':
const inputType = computed(() => { return 'email'; //
switch(currentType.value) { case 'phone':
case 'email': return 'email'; // return 'tel'; //
case 'phone': return 'tel'; // default:
default: return 'text'; // return 'text'; //
} }
}); });
// //
const placeholder = computed(() => { const placeholder = computed(() => {
switch(currentType.value) { switch (currentType.value) {
case 'username': return '请输入用户名'; case 'username':
case 'email': return '请输入邮箱'; return '请输入用户名';
case 'phone': return '请输入手机号'; case 'email':
default: return ''; return '请输入邮箱';
} case 'phone':
}); return '请输入手机号';
default:
return '';
}
});
//
const showVerifyCode = computed(() => {
return ['email', 'phone'].includes(currentType.value);
});
//
const isValidInput = computed(() => {
const value = loginForm.value.userFlag;
if (!value) return false; //
switch (currentType.value) {
case 'email':
return /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/.test(value);
case 'phone':
return /^1[3-9]\d{9}$/.test(value);
default:
return value.length > 0; //
}
});
// //
const showVerifyCode = computed(() => { const isvalidForm = computed(() => {
return ['email', 'phone'].includes(currentType.value); if (!isValidInput.value) return false;
});
// if (showVerifyCode.value) {
const isValidInput = computed(() => { return loginForm.value.verifyCode.length === 6; //
const value = loginForm.value.userFlag } else {
if(!value) return false; // return loginForm.value.password.length >= 6; //
}
switch(currentType.value) { });
case 'email':
return /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/.test(value);
case 'phone':
return /^1[3-9]\d{9}$/.test(value);
default:
return value.length > 0; //
}
});
// //
const isvalidForm = computed(() => { async function sendCode() {
if(!isValidInput.value) return false; if (cooldown.value > 0 || !isValidInput.value) return; //
if(showVerifyCode.value){ try {
return loginForm.value.verifyCode.length === 6; // // TODO:
} else { const response = await request.post('/user/sendCode', {
return loginForm.value.password.length >= 6; // type: currentType.value,
} target: loginForm.value.userFlag
}); });
// if (response.data.success) {
async function sendCode() { // 60
if (cooldown.value > 0 || !isValidInput.value) return; // cooldown.value = 60;
const timer = setInterval(() => {
try { cooldown.value--;
//TODO: if (cooldown.value <= 0) {
const response = await axios.post('/user/sendCode', { clearInterval(timer);
type: currentType.value,
target: loginForm.value.userFlag
});
if (response.data.success) {
//60
cooldown.value = 60;
const timer = setInterval(() => {
cooldown.value--;
if (cooldown.value <= 0) {
clearInterval(timer);
}
}, 1000);
} }
} catch (error) { }, 1000);
console.error('发送验证码失败:', error);
}
} }
} catch (error) {
console.error('发送验证码失败:', error);
}
}
//
/*
function login1() {
userStore.login({
avatar: '/assets/default-avatar/boy_1.png',
userName: '珈人一号'
})
}*/
async function login() {
if (!isvalidForm.value) return;
try {
const loginData = {
userFlag: loginForm.value.userFlag,
password: loginForm.value.password
}
if (showVerifyCode.value) { //
loginData.verifyCode = loginForm.value.verifyCode; // //function Login1() {
} // userStore.login({
// avatar:require('@/assets/default-avatar/boy_1.png'),
// userName: '',
// userid:1
// });
// emit('LoginSuccess');
//}
//post
const response = await request.post('/user/login', loginData);
if (response.code === 200) {
//token
//
const { accessToken, refreshToken } = response.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
userStore.login({
avatar: '/assets/default-avatar/boy_1.png',
userName: '珈人一号'
})
ElMessage({
message: '登录成功',
type: 'success',
duration: 500
});
//TODO:
//or
}else{
//
ElMessage({
message: '登陆失败,用户名或密码错误',
type: 'error',
duration: 500
});
}
} catch (error) { //
console.error('登录失败:', error); async function login() {
alert(error.response?.message ||'登录失败,请稍后重试'); if (!isvalidForm.value) return;
}
} try {
const loginData = {
userFlag: loginForm.value.userFlag,
password: loginForm.value.password
};
// if (showVerifyCode.value) {
const switchLoginType = (type) => { loginData.verifyCode = loginForm.value.verifyCode; //
currentType.value = type;
loginForm.value.userFlag = '';
loginForm.value.password = '';
loginForm.value.verifyCode = '';
} }
// 使 // post
return { const response = await request.post('/user/login', loginData);
currentType,
loginTypes, if (response.code === 200) {
loginForm, // token
inputType, const { accessToken, refreshToken } = response.data;
placeholder, localStorage.setItem('accessToken', accessToken);
showVerifyCode, localStorage.setItem('refreshToken', refreshToken);
cooldown,
sendCode, userStore.login({
//login1, avatar: '/assets/default-avatar/boy_1.png',
login, userName: '珈人一号',
switchLoginType, userid:1
isValidInput });
};
ElMessage({
message: '登录成功',
type: 'success',
duration: 500
});
emit('LoginSuccess'); //
} else {
//
ElMessage({
message: '登陆失败,用户名或密码错误',
type: 'error',
duration: 500
});
}
} catch (error) {
console.error('登录失败:', error);
alert(error.response?.message || '登录失败,请稍后重试');
} }
} }
//
const switchLoginType = (type) => {
currentType.value = type;
loginForm.value.userFlag = '';
loginForm.value.password = '';
loginForm.value.verifyCode = '';
};
</script> </script>
<style scoped> <style scoped>

@ -8,7 +8,7 @@
<!-- 模态框内容区域 --> <!-- 模态框内容区域 -->
<div class="modal-content"> <div class="modal-content">
<!-- 根据 isLogin 状态动态切换登录/注册组件 --> <!-- 根据 isLogin 状态动态切换登录/注册组件 -->
<UserLogin v-if="isLogin" @toggleForm="toggleForm" /> <UserLogin v-if="isLogin" @toggleForm="toggleForm" @LoginSuccess="close"/>
<UserRegister v-else @toggleForm="toggleForm" /> <UserRegister v-else @toggleForm="toggleForm" />
</div> </div>
</div> </div>
@ -39,7 +39,7 @@ export default {
return { return {
isLogin, isLogin,
toggleForm, toggleForm,
close: () => emit('close') // close: () => {emit('close')} //
}; };
} }
} }

@ -91,8 +91,7 @@
</template> </template>
<script> <script>
import { ref, computed } from 'vue'; import { ref, computed, onMounted } from 'vue';
import axios from 'axios';
import request from '@/utils/request'; import request from '@/utils/request';
import {ElMessage} from 'element-plus' import {ElMessage} from 'element-plus'
@ -199,10 +198,25 @@ export default {
captchaUrl.value = `/user/captcha?t=${new Date().getTime()}`; captchaUrl.value = `/user/captcha?t=${new Date().getTime()}`;
} }
//TODO: //TODO:/
// //
async function register() { async function register() {
try { try {
//
const captchaResponse = await request.post('/user/verify-captcha', {
captcha: registerForm.value.captcha
});
if (captchaResponse.code !== 200) {
ElMessage({
message: captchaResponse.msg,
type: 'error',
duration: 500
});
refreshCaptcha();
return;
}
// //
if (registerForm.value.password !== registerForm.value.confirmPassword) { if (registerForm.value.password !== registerForm.value.confirmPassword) {
ElMessage({ ElMessage({
@ -229,7 +243,7 @@ export default {
break; break;
} }
const response = await axios.post('/user/register', registerData); const response = await request.post('/user/register', registerData);
if (response.code === 200) { if (response.code === 200) {
ElMessage({ ElMessage({
@ -251,9 +265,15 @@ export default {
catch (error) { catch (error) {
console.error('注册失败:', error); console.error('注册失败:', error);
alert(error.response?.message || '注册失败,请稍后重试'); alert(error.response?.message || '注册失败,请稍后重试');
refreshCaptcha();
} }
} }
onMounted(() => {
//
refreshCaptcha();
});
// 使 // 使
return { return {
currentType, currentType,

@ -5,6 +5,7 @@ import PostDetail from '@/views/PostDetail.vue';
import MainPage from '@/views/MainPage.vue'; import MainPage from '@/views/MainPage.vue';
import UserPage from '@/views/UserPage.vue'; import UserPage from '@/views/UserPage.vue';
import NotificationList from '@/views/NotificationList.vue'; import NotificationList from '@/views/NotificationList.vue';
import FeedBack from '@/views/FeedBack.vue';
const routes = [ const routes = [
{ {
@ -28,7 +29,7 @@ const routes = [
component: PostDetail component: PostDetail
}, },
{ {
path: '/user', path: '/user/:userId',
name: 'UserPage', name: 'UserPage',
component: UserPage component: UserPage
}, },
@ -36,6 +37,11 @@ const routes = [
path: '/notificationlist', path: '/notificationlist',
name: 'NotificationList', name: 'NotificationList',
component: NotificationList component: NotificationList
},
{//反馈站页面
path: '/feedback',
name: 'FeedBack',
component: FeedBack
}, },
{//详细通知页面 {//详细通知页面
path: '/notification/:id', path: '/notification/:id',

@ -6,6 +6,7 @@ export const useUserStore = defineStore('user', {
userInfo: { userInfo: {
avatar: '', // 用户头像 URL avatar: '', // 用户头像 URL
username: '', // 用户名 username: '', // 用户名
userid: 0, // 用户 ID
}, },
}), }),
actions: { actions: {
@ -15,7 +16,7 @@ export const useUserStore = defineStore('user', {
}, },
logout() { logout() {
this.isLoggedIn = false; this.isLoggedIn = false;
this.userInfo = { avatar: '', username: '' }; this.userInfo = { avatar: '', username: '' , userid: 0 };
}, },
}, },
}); });

@ -22,9 +22,11 @@ request.interceptors.response.use(
request.interceptors.request.use( request.interceptors.request.use(
config => { config => {
console.log('Request:',config); console.log('Request:',config);
const token = localStorage.getItem('accessToken'); if (!config.url.includes('/captcha') && !config.url.includes('/verify-captcha')) {
if (token) { const token = localStorage.getItem('accessToken');
config.headers['Authorization'] = token; if (token) {
config.headers['Authorization'] = token;
}
} }
return config; return config;
}, },

@ -0,0 +1,213 @@
<template>
<div class="feedback-container">
<h1 class="page-title">意见反馈与举报</h1>
<!-- 反馈表单 -->
<div class="feedback-form">
<el-form
ref="feedbackFormRef"
:model="form"
:rules="rules"
label-width="120px"
@submit.prevent="submitForm"
>
<!-- 反馈类型 -->
<el-form-item label="反馈类型" prop="type">
<el-select
v-model="form.type"
placeholder="请选择反馈类型"
class="type-selector"
>
<el-option
v-for="item in feedbackTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- 关联帖子 -->
<el-form-item
v-if="form.type === 'report'"
label="关联帖子ID"
prop="postId"
>
<el-input
v-model="form.postId"
placeholder="请输入需要举报的帖子ID"
/>
</el-form-item>
<!-- 反馈内容 -->
<el-form-item label="详细内容" prop="content">
<el-input
v-model="form.content"
type="textarea"
:rows="6"
placeholder="请详细描述您的问题或建议"
maxlength="500"
show-word-limit
/>
</el-form-item>
<!-- 联系方式 -->
<el-form-item label="联系方式" prop="contact">
<el-input
v-model="form.contact"
placeholder="请输入邮箱/手机号(选填)"
/>
</el-form-item>
<!-- 提交按钮 -->
<el-form-item class="submit-btn">
<el-button
type="primary"
native-type="submit"
:loading="submitting"
>
提交反馈
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 成功提示 -->
<el-alert
v-if="submitSuccess"
title="提交成功"
type="success"
:closable="false"
class="result-alert"
>
感谢您的反馈我们将在3个工作日内处理
</el-alert>
<!-- 错误提示 -->
<el-alert
v-if="submitError"
title="提交失败"
type="error"
description="请检查网络连接或稍后重试"
class="result-alert"
@close="submitError = false"
/>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
//
const feedbackFormRef = ref(null)
//
const submitting = ref(false)
const submitSuccess = ref(false)
const submitError = ref(false)
const feedbackTypes = reactive([
{ value: 'suggestion', label: '意见反馈' },
{ value: 'complaint', label: '用户投诉' },
{ value: 'report', label: '内容举报' }
])
const form = reactive({
type: 'suggestion',
content: '',
contact: '',
postId: ''
})
const rules = reactive({
type: [{ required: true, message: '请选择反馈类型', trigger: 'change' }],
content: [
{ required: true, message: '请输入反馈内容', trigger: 'blur' },
{ min: 20, message: '内容长度至少20个字符', trigger: 'blur' }
],
postId: [
{ required: true, message: '举报必须提供帖子ID', trigger: 'blur' }
],
contact: [
{ pattern: /^1[3-9]\d{9}$|^\w+@\w+\.\w+$/, message: '格式不正确' }
]
})
//
const submitForm = async () => {
try {
submitting.value = true
//
await feedbackFormRef.value.validate()
// API
// await axios.post('/api/feedback', form)
submitSuccess.value = true
submitError.value = false
feedbackFormRef.value.resetFields()
setTimeout(() => {
submitSuccess.value = false
}, 3000)
} catch (error) {
if (error instanceof Error) {
ElMessage.error(error.message)
}
submitError.value = true
} finally {
submitting.value = false
}
}
</script>
<style scoped>
/* 保持原有样式不变 */
.feedback-container {
max-width: 800px;
margin: 20px auto;
padding: 30px;
background-color: #f9f9f9;
border-radius: 8px;
}
.page-title {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
}
.feedback-form {
background: white;
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.type-selector {
width: 100%;
}
.submit-btn {
text-align: center;
margin-top: 30px;
}
.result-alert {
margin-top: 20px;
border-radius: 6px;
}
.el-form-item__label {
color: #666;
font-size: 14px;
}
.el-textarea__inner,
.el-input__inner {
border-radius: 4px;
border: 1px solid #e4e4e4;
}
</style>

@ -17,7 +17,7 @@
<!-- 帖子列表 --> <!-- 帖子列表 -->
<div class="user-posts"> <div class="user-posts">
<h3>你的帖子</h3> <h3>你的帖子</h3>
<div class="post-item" v-for="(post, index) in posts" :key="index"> <div class="post-item" v-for="(post, index) in posts" :key="index" @click="goToPostDetail(post.id)">
<h4 class="post-title">{{ post.title }}</h4> <h4 class="post-title">{{ post.title }}</h4>
<p class="post-summary">{{ post.summary }}</p> <p class="post-summary">{{ post.summary }}</p>
<div class="post-stats"> <div class="post-stats">
@ -59,12 +59,13 @@
<script> <script>
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router';
export default { export default {
name: 'UserPage', name: 'UserPage',
setup() { setup() {
const posts = ref([ const posts = ref([
{ {
id:1,
title: '雷霆连续两场或20分钟逆转什么水平', title: '雷霆连续两场或20分钟逆转什么水平',
summary: '今天保罗带领雷霆完成逆转', summary: '今天保罗带领雷霆完成逆转',
likes: 1200, likes: 1200,
@ -72,6 +73,7 @@ export default {
favorites: 100, favorites: 100,
}, },
{ {
id:2,
title: '一个普通人需要多大努力才能考上985', title: '一个普通人需要多大努力才能考上985',
summary: 'mmmmmmmm', summary: 'mmmmmmmm',
likes: 2200, likes: 2200,
@ -79,9 +81,13 @@ export default {
favorites: 200, favorites: 200,
}, },
]); ]);
const router = useRouter();
const goToPostDetail = (postId) => {
router.push({ name: 'PostDetail', params: { id: postId } });
};
return { return {
posts, posts,
goToPostDetail,
}; };
}, },
}; };
@ -182,8 +188,16 @@ export default {
.post-item { .post-item {
padding: 10px 0; padding: 10px 0;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
cursor: pointer;
transition: box-shadow 0.3s;
}
.post-item:hover {
transform: translateY(-2px); /* 轻微上移不超过间距的1/4 */
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.05), /* 主阴影(更柔和) */
0 1px 2px rgba(0, 0, 0, 0.1); /* 内阴影增加层次感 */
border-color: #e0e0e0;
} }
.post-item:last-child { .post-item:last-child {
border-bottom: none; border-bottom: none;
} }
@ -203,5 +217,6 @@ export default {
gap: 10px; gap: 10px;
font-size: 12px; font-size: 12px;
color: #999; color: #999;
margin-left: 20px;
} }
</style> </style>

@ -11,6 +11,14 @@ module.exports = defineConfig({
'^/user': '/user' '^/user': '/user'
} }
}, },
'/post': {
target: 'http://localhost:8081',
changeOrigin: true
},
'/comment': {
target: 'http://localhost:8081',
changeOrigin: true
},
} }
} }
}) })

Loading…
Cancel
Save