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

main
forely 6 days ago
commit 11d6d818ae

@ -16,6 +16,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
registry.addInterceptor(authInterceptor)
.excludePathPatterns("/user/login",
"/user/register",
"/user/captcha",
"/user/verify-captcha",
"/post/list",
"/post/detail",
"/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:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\config\RedisConfig.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\domain\Result.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\domain\UserDTO.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\exception\BaseException.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\exception\UserException.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\utils\JWTUtil.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\utils\RedisUtil.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\advice\GlobalExceptionHandler.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\MybatisConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\OpenApiConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\RedisConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\WebMvcConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\constants\RedisConstant.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\page\PageRequest.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\page\PageResponse.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;
/**
*
*
*
* @param userRegisterDTO
*/
@ -30,10 +30,6 @@ public class ValidateUserUtil {
String password = userRegisterDTO.getPassword();
String phone = userRegisterDTO.getPhone();
String email = userRegisterDTO.getEmail();
String studentId = userRegisterDTO.getStudentId();
if (StrUtil.isBlank(username)){
throw new UserException("用户名不能为空");
}
if (StrUtil.isBlank(password)){
throw new UserException("密码不能为空");
}
@ -42,12 +38,12 @@ public class ValidateUserUtil {
int cnt = 0;
if(StrUtil.isNotBlank(phone)) cnt++;
if(StrUtil.isNotBlank(email)) cnt++;
if(StrUtil.isNotBlank(studentId)) cnt++;
if(StrUtil.isNotBlank(username)) cnt++;
if (cnt == 0) {
throw new UserException("必须填写手机号、邮箱或学号其中一种注册方式");
throw new UserException("必须填写手机号、邮箱或用户名其中一种注册方式");
}
if (cnt > 1) {
throw new UserException("只能选择一种注册方式(手机/邮箱/学号");
throw new UserException("只能选择一种注册方式(手机/邮箱/用户名");
}
// 格式校验
validateFormats(userRegisterDTO);
@ -60,7 +56,6 @@ public class ValidateUserUtil {
String password = userRegisterDTO.getPassword();
String phone = userRegisterDTO.getPhone();
String email = userRegisterDTO.getEmail();
String studentId = userRegisterDTO.getStudentId();
// 仅对非空字段做格式校验
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, username))) {
@ -69,14 +64,13 @@ public class ValidateUserUtil {
if(!password.matches(PASSWORD_REGEX)){
throw new UserException("密码格式错误");
}
validateUserFlag(phone, email, studentId, null);
validateUserFlag(phone, email, null);
}
public void validateFormats(UserChangeInfoDTO userChangeInfoDTO, Long currentUserId){
String username = userChangeInfoDTO.getUsername();
String phone = userChangeInfoDTO.getPhone();
String email = userChangeInfoDTO.getEmail();
String studentId = userChangeInfoDTO.getStudentId();
// String college = userChangeInfoDTO.getCollege();
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, userChangeInfoDTO.getUsername())
@ -84,10 +78,10 @@ public class ValidateUserUtil {
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(!phone.matches(PHONE_REGEX))
throw new UserException("手机号格式错误");
@ -106,15 +100,6 @@ public class ValidateUserUtil {
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-router": "^4.5.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
@ -28,7 +29,8 @@
"eslintConfig": {
"root": true,
"env": {
"node": true
"node": true,
"vue/setup-compiler-macros": true
},
"extends": [
"plugin:vue/vue3-essential",

@ -31,20 +31,28 @@
</li>
</ul>
</div>
<router-link to="/feedback" class="nav-btn">反馈站</router-link>
<router-link to="/notificationlist" class="nav-btn">通知</router-link>
</div>
<!-- 登录/注册按钮 -->
<div class="nav-section">
<button v-if="!isLoggedIn" @click="showModal" class="login-btn">/</button>
<div v-else class="user-avatar">
<img :src="userInfo.avatar || defaultAvatar" alt="用户头像" class="avatar-img" />
<div class="dropdown-menu">
<button @click="goToProfile"></button>
<button @click="logout">退</button>
<div v-else class="user-menu">
<router-link to="/notificationlist" class="nav-btn">通知</router-link>
<router-link to="/feedback" class="nav-btn">反馈站</router-link>
<div class="user-avatar" @mouseenter="showDropdown" @mouseleave="hideDropdown">
<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>
<!-- 登录/注册模态框组件仅在 isModalVisible true 时显示 -->
@ -52,16 +60,17 @@
</header>
</template>
<script setup name="Header">
<script lang="js" setup name="Header">
import { ref, computed } from 'vue';
import { useUserStore } from '@/stores/user.ts';
import { useUserStore } from '@/stores/user.js';
import LoginRegisterModal from './LoginRegisterModal.vue';
import { useRouter } from 'vue-router';
//
const userStore = useUserStore();
const router = useRouter();
//
const isModalVisible = ref(false);
const isDropdownVisible = ref(false);
const colleges = ref([
{ name: '武大官网', url: 'https://www.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);
@ -98,13 +107,31 @@ const hideModal = () => {
};
const goToProfile = () => {
//
//window.location.href = '/profile';
router.push({name: 'UserPage',params: { userId: userInfo.value.userid }});
isDropdownVisible.value = false; //
};
const 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>
@ -217,7 +244,7 @@ const logout = () => {
position: absolute;
top: 100%;
left: 0;
background-color: white;
background-color: rgb(255, 255, 255);
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
@ -293,4 +320,68 @@ const logout = () => {
color: #6fbd87;
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>

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

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

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

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

@ -6,6 +6,7 @@ export const useUserStore = defineStore('user', {
userInfo: {
avatar: '', // 用户头像 URL
username: '', // 用户名
userid: 0, // 用户 ID
},
}),
actions: {
@ -15,7 +16,7 @@ export const useUserStore = defineStore('user', {
},
logout() {
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(
config => {
console.log('Request:',config);
const token = localStorage.getItem('accessToken');
if (token) {
config.headers['Authorization'] = token;
if (!config.url.includes('/captcha') && !config.url.includes('/verify-captcha')) {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers['Authorization'] = token;
}
}
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">
<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>
<p class="post-summary">{{ post.summary }}</p>
<div class="post-stats">
@ -59,12 +59,13 @@
<script>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
export default {
name: 'UserPage',
setup() {
const posts = ref([
{
id:1,
title: '雷霆连续两场或20分钟逆转什么水平',
summary: '今天保罗带领雷霆完成逆转',
likes: 1200,
@ -72,6 +73,7 @@ export default {
favorites: 100,
},
{
id:2,
title: '一个普通人需要多大努力才能考上985',
summary: 'mmmmmmmm',
likes: 2200,
@ -79,9 +81,13 @@ export default {
favorites: 200,
},
]);
const router = useRouter();
const goToPostDetail = (postId) => {
router.push({ name: 'PostDetail', params: { id: postId } });
};
return {
posts,
goToPostDetail,
};
},
};
@ -182,8 +188,16 @@ export default {
.post-item {
padding: 10px 0;
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 {
border-bottom: none;
}
@ -203,5 +217,6 @@ export default {
gap: 10px;
font-size: 12px;
color: #999;
margin-left: 20px;
}
</style>

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

Loading…
Cancel
Save