伙伴匹配优化方案
注册功能p0✅
![]() | ![]() |
---|
全局样式
js
<template>
<div class="login-and-register">
<p class="text" >登录</p>
<div style="text-align: center;">
<van-image
round
width="10rem"
height="10rem"
:src="loginImg"
/>
</div>
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="userAccount"
name="用户名"
label="用户名"
placeholder="用户名"
:rules="[{ required: true, message: '请填写用户名' }]"
/>
<van-field
v-model="userPassword"
type="password"
name="密码"
label="密码"
placeholder="密码"
:rules="[{ required: true, message: '请填写密码' }]"
/>
</van-cell-group>
<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
<div style="margin: 16px; display: flex; justify-content: space-between; align-items: center;">
<van-button plain hairline type="primary" @click="goToRegister">
没有账号?去注册
</van-button>
<van-button plain hairline type="primary" @click="goToContact">
忘记密码?联系站长
</van-button>
</div>
</van-form>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue';
import myAxios from "../plugins/myAxios.ts";
import {showSuccessToast} from "vant";
import {useRoute, useRouter} from "vue-router";
import loginImg from '../assets/login.gif'
const router = useRouter();
const route = useRoute();
const userAccount = ref('');
const userPassword = ref('');
const onSubmit = async () => {
const res = await myAxios.post('/user/login', {
userAccount: userAccount.value,
userPassword: userPassword.value
})
console.log(res, '用户登录');
if (res.code === 0 && res.data) {
// Toast.success('登录成功');
showSuccessToast('登录成功');
const redirectUrl = route.query?.redirect as string ?? '/';
window.location.href = redirectUrl; // oktodo 跳转到首页 替换历史记录 不是压入 点击返回 不会再回到登录页
}
};
const goToRegister = () => {
router.push('/user/register');
};
const goToContact = () => {
window.location.href = 'https://lihuibear.cn/';
};
</script>
<style scoped>
</style>
css
.login-and-register {
height: 100vh;
padding: 20px;
background-color: #f5f5f5; /* 淡灰色背景 */
backdrop-filter: blur(10px);
.text {
text-align: center;
font-size: 24px;
font-weight: bold;
color: #333;
line-height: 1.5;
margin-bottom: 16px;
}
}
编辑标签 p0✅
- 获取标签 getCurrentUser
- 编辑标签(增删改)
后端✅
java
/**
* @param request
* @param request
* @return
*/
// 更新标签
@PostMapping("/updateTags")
public BaseResponse<Integer> updateTags(@RequestBody UpdateTagsRequest request, HttpServletRequest httpRequest) {
// 1. 校验参数是否为空
if (request == null || request.getOperation() == null || request.getOldTag() == null || request.getNewTag() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "标签列表、操作类型、旧标签和新标签不能为空");
}
// 2. 校验权限
User loginUser = userService.getLoginUser(httpRequest);
if (loginUser == null) {
throw new BusinessException(ErrorCode.NO_AUTH, "用户未登录");
}
// 3. 调用服务层更新标签
int result = userService.updateTags(request.getOldTag(), request.getNewTag(), request.getOperation(), loginUser);
return ResultUtils.success(result);
}
java
package com.lihui.yupao_backend.model.request;
import java.util.List;
public class UpdateTagsRequest {
// 旧标签(即要替换的标签)
private String oldTag;
// 新标签(用于替换的标签)
private String newTag;
// 操作类型(用于指定操作,比如 "add", "remove", "update")
private String operation;
// getter 和 setter 方法
public String getOldTag() {
return oldTag;
}
public void setOldTag(String oldTag) {
this.oldTag = oldTag;
}
public String getNewTag() {
return newTag;
}
public void setNewTag(String newTag) {
this.newTag = newTag;
}
public String getOperation() {
return operation;
}
public void setOperation(String operation) {
this.operation = operation;
}
@Override
public String toString() {
return "UpdateTagsRequest{" +
"oldTag='" + oldTag + '\'' +
", newTag='" + newTag + '\'' +
", operation='" + operation + '\'' +
'}';
}
}
java
@Override
public int updateTags(String oldTag, String newTag, String operation, User loginUser) {
long userId = loginUser.getId();
if (userId < 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID无效");
}
User oldUser = userMapper.selectById(userId);
if (oldUser == null) {
throw new BusinessException(ErrorCode.NULL_ERROR, "用户不存在");
}
// 1. 获取当前用户的标签并转换成列表
List<String> currentTags = convertJsonToTags(oldUser.getTags());
// 2. 根据操作类型处理标签
switch (operation.toLowerCase()) {
case "add":
if (!currentTags.contains(newTag)) {
currentTags.add(newTag); // 添加新标签
} else {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "标签已存在");
}
break;
case "remove":
if (currentTags.contains(oldTag)) {
currentTags.remove(oldTag); // 删除旧标签
// 如果标签列表中删除后出现空字符串,移除空字符串
currentTags.removeIf(tag -> tag.isEmpty());
} else {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "未找到指定的标签");
}
break;
case "update":
if (currentTags.contains(oldTag)) {
int index = currentTags.indexOf(oldTag); // 获取旧标签的索引
currentTags.set(index, newTag); // 替换为新标签
} else {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "未找到指定的标签");
}
break;
default:
throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的操作类型");
}
// 3. 更新数据库中的标签字段
String updatedTagsJson = convertTagsToJson(currentTags);
oldUser.setTags(updatedTagsJson);
return userMapper.updateById(oldUser); // 更新数据库中的用户信息
}
// 转换标签字符串为列表
private List<String> convertJsonToTags(String tagsJson) {
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(tagsJson, new TypeReference<List<String>>() {
});
} catch (JsonProcessingException e) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "标签格式错误");
}
}
// 转换标签列表为JSON字符串
private String convertTagsToJson(List<String> tags) {
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(tags);
} catch (JsonProcessingException e) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "标签格式错误");
}
}
前端✅
js
<template>
<van-button color="#7232dd" plain @click="onEditClick">编辑</van-button>
<van-cell-group>
<!-- 显示用户标签并给每个标签不同的颜色 -->
<van-cell
v-for="(tag, index) in usertags"
:key="index"
:title="tag"
value=""
/>
</van-cell-group>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { getCurrentUser } from "../services/user";
import {useRouter} from "vue-router";
const router = useRouter();
// 定义变量存储用户标签
const usertags = ref<string[]>([]);
// 组件加载时获取用户数据并处理标签
onMounted(async () => {
const user = await getCurrentUser();
// 解析 tags 字符串为数组
if (user && user.tags) {
usertags.value = JSON.parse(user.tags); // 解析 tags 字符串为数组
}
});
// 编辑按钮点击事件
const onEditClick = () => {
console.log("点击编辑按钮");
// 在此处可以跳转到编辑页面或者打开编辑表单
router.push('/user/tags/edit');
};
</script>
<style scoped>
/* 按钮样式 */
.van-button {
margin-bottom: 20px; /* 按钮和标签之间增加间距 */
transition: background-color 0.3s ease;
}
.van-button:hover {
background-color: #5e2ec0; /* 鼠标悬停时改变按钮颜色 */
}
/* 标签样式 */
.van-cell {
display: inline-block; /* 使标签水平排列 */
white-space: nowrap; /* 防止标签文字换行 */
}
</style>
js
<template>
<van-cell-group>
<van-cell v-for="(tag, index) in usertags" :key="index" class="tag-cell">
<template #title>
<!-- 如果正在编辑该标签,显示输入框,否则显示标签文本 -->
<div v-if="editIndex === index" class="edit-input">
<van-field
v-model="editedTag"
placeholder="编辑标签"
@blur="saveEditedTag"
clearable
class="tag-edit-field"
/>
</div>
<div v-else class="tag-text">
{{ tag }}
</div>
</template>
<template #value>
<van-icon name="delete" class="delete-icon" @click="removeTag(index)"/>
<van-icon name="edit" class="edit-icon" @click="editTag(index)"/>
</template>
</van-cell>
<!-- 输入框:用于添加新标签 -->
<van-cell>
<van-field v-model="newTag" placeholder="输入新的标签" class="add-tag-field"/>
</van-cell>
<van-button color="#ff7f50" @click="addTag" class="add-tag-button">添加标签</van-button>
<van-button color="#7232dd" plain @click="saveTags" class="complete-button">完成</van-button>
</van-cell-group>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { getCurrentUser } from "../services/user";
import myAxios from "../plugins/myAxios.ts"; // 引入获取当前用户信息的 API
const router = useRouter();
// 定义变量存储用户标签和新标签
const usertags = ref<string[]>([]);
const newTag = ref("");
const editIndex = ref<number | null>(null); // 用于标识正在编辑的标签
const editedTag = ref(""); // 用于存储正在编辑的标签
// 组件加载时获取用户数据并处理标签
onMounted(async () => {
const user = await getCurrentUser();
// 解析 tags 字符串为数组
if (user && user.tags) {
usertags.value = JSON.parse(user.tags); // 解析 tags 字符串为数组
}
});
// 删除标签
const removeTag = async (index: number) => {
const tagToRemove = usertags.value[index];
try {
// 调用后端接口删除标签
const response = await updateUserTags("remove", tagToRemove, tagToRemove);
if (response.status === 200) {
usertags.value.splice(index, 1); // 成功删除标签
} else {
console.error("删除标签失败", response);
}
} catch (error) {
console.error("删除标签请求失败", error);
}
};
// 添加新标签
const addTag = async () => {
if (newTag.value.trim()) {
try {
// 调用后端接口添加标签
const response = await updateUserTags("add", newTag.value.trim(), newTag.value.trim());
if (response.status === 200) {
usertags.value.push(newTag.value.trim()); // 添加到标签列表
newTag.value = ""; // 清空输入框
} else {
console.error("添加标签失败", response);
}
} catch (error) {
console.error("添加标签请求失败", error);
}
}
};
// 编辑标签
const editTag = (index: number) => {
editIndex.value = index; // 设置正在编辑的标签的索引
editedTag.value = usertags.value[index]; // 设置正在编辑的标签的内容
};
// 保存编辑后的标签
const saveEditedTag = async () => {
if (editIndex.value !== null && editedTag.value.trim()) {
const oldTag = usertags.value[editIndex.value];
const newTagValue = editedTag.value.trim();
// 调用后端接口更新标签
try {
const response = await updateUserTags("update", oldTag, newTagValue);
if (response.status === 200) {
usertags.value[editIndex.value] = newTagValue; // 更新标签
editIndex.value = null; // 清空编辑状态
editedTag.value = ""; // 清空编辑内容
} else {
console.error("更新标签失败", response);
}
} catch (error) {
console.error("更新标签请求失败", error);
}
}
};
// 保存标签并更新用户
const saveTags = async () => {
const updatedTags = JSON.stringify(usertags.value); // 将标签列表转为 JSON 字符串
try {
// 调用 API 更新用户标签
const response = await updateUserTags("update", "", updatedTags);
if (response.status === 200) {
router.back(); // 保存完后返回上一页
} else {
console.error("更新标签失败", response);
}
} catch (error) {
console.error("更新标签请求失败", error);
}
};
// 更新用户标签(API 请求)
const updateUserTags = async (operation: string, oldTag: string, newTag: string) => {
try {
const response = await myAxios.post("/user/updateTags", {
oldTag,
newTag,
operation
});
return response;
} catch (error) {
console.error("API 请求失败:", error);
throw error; // 抛出错误以便上层捕获
}
};
</script>
<style scoped>
/* 按钮样式 */
.van-button {
margin-bottom: 20px; /* 按钮和标签之间增加间距 */
transition: background-color 0.3s ease;
}
.complete-button {
width: 100%; /* 完成按钮占满整行 */
margin-bottom: 30px;
font-size: 16px;
height: 45px;
}
/* 标签样式 */
.van-cell {
display: inline-block;
white-space: nowrap;
}
.tag-cell {
background-color: #f9f9f9;
margin-bottom: 12px;
border-radius: 8px;
padding: 10px 15px;
}
.tag-text {
font-size: 16px;
color: #333;
display: inline-block;
}
.tag-edit-field {
width: 100%;
}
.edit-input {
width: 100%;
}
/* 编辑、删除按钮样式 */
.van-icon {
cursor: pointer;
margin-left: 10px;
}
/* 输入框和添加按钮样式 */
.add-tag-field {
margin-top: 15px;
}
.van-cell-group {
display: flex;
flex-direction: column;
height: calc(100vh - 120px); /* 确保标签区域可以滚动,减去按钮的高度 */
overflow-y: auto; /* 使标签区域可滚动 */
padding-bottom: 60px; /* 确保底部不被按钮遮挡 */
}
.add-tag-button {
width: 100%;
font-size: 16px;
height: 45px;
position: fixed; /* 保持按钮固定在屏幕底部 */
bottom: 30px; /* 离底部有一个间距 */
left: 0;
z-index: 10;
}
.complete-button {
width: 100%; /* 完成按钮占满整行 */
margin-bottom: 30px;
font-size: 16px;
height: 45px;
position: fixed;
bottom: 70px; /* 完成按钮距离底部 */
left: 0;
z-index: 10;
}
</style>
搜索标签 Bug不显示卡片 p00 ✅
不显示卡片 => 未提供userList
js
<template>
<!-- 加载中动画 -->
<div v-if="loading" class="loading-container">
<van-loading size="24px" /> 💓正在为您搜索💓...
</div>
<!-- 用户卡片列表 -->
<user-card-list :user-list="userList" :loading="loading"/>
</template>
<script setup>
import { useRoute } from "vue-router";
import { onMounted, ref } from "vue";
import qs from "qs";
import { showFailToast, showSuccessToast } from "vant";
import myAxios from "../plugins/myAxios.ts";
import UserCardList from "../components/UserCardList.vue";
// 获取路由中的标签参数
const route = useRoute();
const { tags } = route.query; // 从路由参数中获取tags
const userList = ref([]);
const loading = ref(true); // 控制加载状态
onMounted(async () => {
loading.value = true; // 开始加载
// // 如果tags为空,不发起请求,直接返回
// if (!tags || tags.length === 0) {
// showFailToast('没有标签参数');
// loading.value = false;
// return;
// }
try {
// 发起搜索请求,根据标签搜索用户
const userListData = await myAxios.get('/user/search/tags', {
params: {
tagNameList: tags, // 传入标签列表
},
paramsSerializer: params => {
// 使用qs.stringify确保参数正确格式化
return qs.stringify(params, { indices: false });
}
});
console.log('/user/search/tags succeed', userListData);
if (userListData?.data) {
// 如果返回的数据存在,处理标签数据
userListData.data.forEach(user => {
if (user.tags) {
user.tags = JSON.parse(user.tags); // 解析标签字符串
}
});
// 更新用户列表
userList.value = userListData.data;
showSuccessToast('搜索成功');
}
} catch (error) {
// 处理请求失败
console.error('/user/search/tags error', error);
showFailToast('搜索失败');
} finally {
loading.value = false; // 请求结束,关闭加载动画
}
});
</script>
<style scoped>
.loading-container {
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #666;
margin-top: 20px;
}
.loading-container van-loading {
margin-right: 8px;
}
</style>
优化搜索 p0 ✅
切换搜索模式(搜索用户、搜索标签)
js
<template>
<van-cell-group>
<!-- 显示用户标签并给每个标签不同的颜色 -->
<van-cell
v-for="(tag, index) in usertags"
:key="index"
:title="tag"
value=""
/>
</van-cell-group>
<van-button color="#7232dd" class="edit-button" plain @click="onEditClick">编辑</van-button>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { getCurrentUser } from "../services/user";
import {useRouter} from "vue-router";
const router = useRouter();
// 定义变量存储用户标签
const usertags = ref<string[]>([]);
// 组件加载时获取用户数据并处理标签
onMounted(async () => {
const user = await getCurrentUser();
// 解析 tags 字符串为数组
if (user && user.tags) {
usertags.value = JSON.parse(user.tags); // 解析 tags 字符串为数组
}
});
// 编辑按钮点击事件
const onEditClick = () => {
console.log("点击编辑按钮");
// 在此处可以跳转到编辑页面或者打开编辑表单
router.push('/user/tags/edit');
};
</script>
<style scoped>
/* 按钮样式 */
.van-button {
margin-bottom: 20px; /* 按钮和标签之间增加间距 */
transition: background-color 0.3s ease;
}
.van-button:hover {
background-color: #5e2ec0; /* 鼠标悬停时改变按钮颜色 */
}
/* 标签样式 */
.van-cell {
display: inline-block; /* 使标签水平排列 */
white-space: nowrap; /* 防止标签文字换行 */
}
.edit-button {
width: 100%;
font-size: 16px;
height: 45px;
position: fixed; /* 保持按钮固定在屏幕底部 */
bottom: 35px; /* 离底部有一个间距 */
left: 0;
z-index: 10;
}
</style>
js
<template>
<van-cell-group>
<van-cell v-for="(tag, index) in usertags" :key="index" class="tag-cell">
<template #title>
<!-- 如果正在编辑该标签,显示输入框,否则显示标签文本 -->
<div v-if="editIndex === index" class="edit-input">
<van-field
v-model="editedTag"
placeholder="编辑标签"
@blur="saveEditedTag"
clearable
class="tag-edit-field"
/>
</div>
<div v-else class="tag-text">
{{ tag }}
</div>
</template>
<template #value>
<van-icon name="delete" class="delete-icon" @click="removeTag(index)"/>
<van-icon name="edit" class="edit-icon" @click="editTag(index)"/>
</template>
</van-cell>
<!-- 输入框:用于添加新标签 -->
<van-cell>
<van-field v-model="newTag" placeholder="输入新的标签" class="add-tag-field"/>
</van-cell>
<van-button color="#ff7f50" @click="addTag" class="add-tag-button">添加标签</van-button>
<van-button color="#7232dd" plain @click="saveTags" class="complete-button">完成</van-button>
</van-cell-group>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { getCurrentUser } from "../services/user";
import myAxios from "../plugins/myAxios.ts"; // 引入获取当前用户信息的 API
const router = useRouter();
// 定义变量存储用户标签和新标签
const usertags = ref<string[]>([]);
const newTag = ref("");
const editIndex = ref<number | null>(null); // 用于标识正在编辑的标签
const editedTag = ref(""); // 用于存储正在编辑的标签
// 组件加载时获取用户数据并处理标签
onMounted(async () => {
const user = await getCurrentUser();
// 解析 tags 字符串为数组
if (user && user.tags) {
usertags.value = JSON.parse(user.tags); // 解析 tags 字符串为数组
}
});
// 删除标签
const removeTag = async (index: number) => {
const tagToRemove = usertags.value[index];
try {
// 调用后端接口删除标签
const response = await updateUserTags("remove", tagToRemove, tagToRemove);
if (response.status === 200) {
usertags.value.splice(index, 1); // 成功删除标签
} else {
console.error("删除标签失败", response);
}
} catch (error) {
console.error("删除标签请求失败", error);
}
};
// 添加新标签
const addTag = async () => {
if (newTag.value.trim()) {
try {
// 调用后端接口添加标签
const response = await updateUserTags("add", newTag.value.trim(), newTag.value.trim());
if (response.status === 200) {
usertags.value.push(newTag.value.trim()); // 添加到标签列表
newTag.value = ""; // 清空输入框
} else {
console.error("添加标签失败", response);
}
} catch (error) {
console.error("添加标签请求失败", error);
}
}
};
// 编辑标签
const editTag = (index: number) => {
editIndex.value = index; // 设置正在编辑的标签的索引
editedTag.value = usertags.value[index]; // 设置正在编辑的标签的内容
};
// 保存编辑后的标签
const saveEditedTag = async () => {
if (editIndex.value !== null && editedTag.value.trim()) {
const oldTag = usertags.value[editIndex.value];
const newTagValue = editedTag.value.trim();
// 调用后端接口更新标签
try {
const response = await updateUserTags("update", oldTag, newTagValue);
if (response.status === 200) {
usertags.value[editIndex.value] = newTagValue; // 更新标签
editIndex.value = null; // 清空编辑状态
editedTag.value = ""; // 清空编辑内容
} else {
console.error("更新标签失败", response);
}
} catch (error) {
console.error("更新标签请求失败", error);
}
}
};
// 保存标签并更新用户
const saveTags = async () => {
const updatedTags = JSON.stringify(usertags.value); // 将标签列表转为 JSON 字符串
try {
// 调用 API 更新用户标签
const response = await updateUserTags("update", "", updatedTags);
if (response.status === 200) {
router.back(); // 保存完后返回上一页
} else {
console.error("更新标签失败", response);
}
} catch (error) {
console.error("更新标签请求失败", error);
}
};
// 更新用户标签(API 请求)
const updateUserTags = async (operation: string, oldTag: string, newTag: string) => {
try {
const response = await myAxios.post("/user/updateTags", {
oldTag,
newTag,
operation
});
return response;
} catch (error) {
console.error("API 请求失败:", error);
throw error; // 抛出错误以便上层捕获
}
};
</script>
<style scoped>
/* 按钮样式 */
.van-button {
margin-bottom: 20px; /* 按钮和标签之间增加间距 */
transition: background-color 0.3s ease;
}
/* 标签样式 */
.van-cell {
display: inline-block;
white-space: nowrap;
}
.tag-cell {
background-color: #f9f9f9;
margin-bottom: 12px;
border-radius: 8px;
padding: 10px 15px;
}
.tag-text {
font-size: 16px;
color: #333;
display: inline-block;
}
.tag-edit-field {
width: 100%;
}
.edit-input {
width: 100%;
}
/* 编辑、删除按钮样式 */
.van-icon {
cursor: pointer;
margin-left: 10px;
}
/* 输入框和添加按钮样式 */
.add-tag-field {
margin-top: 15px;
}
.van-cell-group {
display: flex;
flex-direction: column;
height: calc(100vh - 120px); /* 确保标签区域可以滚动,减去按钮的高度 */
overflow-y: auto; /* 使标签区域可滚动 */
padding-bottom: 60px; /* 确保底部不被按钮遮挡 */
}
.add-tag-button {
width: 100%; /* 完成按钮占满整行 */
margin-bottom: 40px;
font-size: 16px;
height: 45px;
position: fixed;
bottom: 70px; /* 完成按钮距离底部 */
left: 0;
z-index: 10;
}
.complete-button {
width: 100%;
font-size: 16px;
height: 45px;
position: fixed; /* 保持按钮固定在屏幕底部 */
bottom: 35px; /* 离底部有一个间距 */
left: 0;
z-index: 10;
}
</style>
我创建的队伍Bug p0✅
不显示全部的 => 过期的,私有的
java
/**
* 获取当前用户创建的队伍
*
* @param teamQuery
* @param request
* @return
* @Author: lihui
*/
@GetMapping("/list/my/create")
public BaseResponse<List<TeamUserVO>> listMyCreateTeams(TeamQuery teamQuery, HttpServletRequest request) {
if (teamQuery == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 获取当前登录用户
User loginUser = userService.getLoginUser(request);
// 设置查询条件,查询当前用户创建的团队
teamQuery.setUserId(loginUser.getId());
// 构建查询条件
QueryWrapper<Team> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userId", teamQuery.getUserId()); // 根据 `userId` 字段进行查询
// 查询所有队伍
List<Team> teamList = teamService.list(queryWrapper);
// 转换为 TeamUserVO 并加入必要字段
List<TeamUserVO> teamUserVOList = teamList.stream()
.map(team -> {
TeamUserVO teamUserVO = new TeamUserVO();
teamUserVO.setId(team.getId());
teamUserVO.setName(team.getName());
teamUserVO.setDescription(team.getDescription());
teamUserVO.setUserId(team.getUserId());
teamUserVO.setCreateTime(team.getCreateTime());
teamUserVO.setUpdateTime(team.getUpdateTime());
teamUserVO.setMaxNum(team.getMaxNum());
teamUserVO.setExpireTime(team.getExpireTime());
teamUserVO.setDescription(team.getDescription());
teamUserVO.setStatus(team.getStatus());
// 其他字段的映射
return teamUserVO;
}).collect(Collectors.toList());
// 获取当前用户已加入的队伍
List<Long> teamIdList = teamUserVOList.stream().map(TeamUserVO::getId).collect(Collectors.toList());
QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
try {
userTeamQueryWrapper.eq("userId", loginUser.getId());
userTeamQueryWrapper.in("teamId", teamIdList);
List<UserTeam> userTeamList = userTeamService.list(userTeamQueryWrapper);
// 已加入的队伍 id 集合
Set<Long> hasJoinTeamIdSet = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet());
teamUserVOList.forEach(team -> {
boolean hasJoin = hasJoinTeamIdSet.contains(team.getId());
team.setHasJoin(hasJoin);
});
} catch (Exception e) {
// 处理异常情况,日志记录等
}
// 查询已加入队伍的人数
QueryWrapper<UserTeam> userTeamJoinQueryWrapper = new QueryWrapper<>();
userTeamJoinQueryWrapper.in("teamId", teamIdList);
List<UserTeam> userTeamList = userTeamService.list(userTeamJoinQueryWrapper);
// 队伍 id => 加入这个队伍的用户列表
Map<Long, List<UserTeam>> teamIdUserTeamList = userTeamList.stream()
.collect(Collectors.groupingBy(UserTeam::getTeamId));
teamUserVOList.forEach(team ->
team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(), new ArrayList<>()).size())
);
// 返回所有队伍信息
return ResultUtils.success(teamUserVOList);
}
我加入的队伍Bug p0✅
不显示全部的 => 过期的,私有的
java
/**
* 获取当前用户加入的队伍
*
* @Author: lihui
*/
@GetMapping("/list/my/join")
public BaseResponse<List<TeamUserVO>> listMyJoinTeams(TeamQuery teamQuery, HttpServletRequest request) {
if (teamQuery == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 获取当前登录用户
User loginUser = userService.getLoginUser(request);
// 先查询用户已加入的队伍
QueryWrapper<UserTeam> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userId", loginUser.getId());
List<UserTeam> userTeamList = userTeamService.list(queryWrapper);
// 提取不重复的队伍 ID
Map<Long, List<UserTeam>> listMap = userTeamList.stream().collect(Collectors.groupingBy(UserTeam::getTeamId));
ArrayList<Long> idList = new ArrayList<>(listMap.keySet());
// 设置查询条件,只查询当前用户已加入的队伍
teamQuery.setIdList(idList);
// 构建查询条件
QueryWrapper<Team> teamQueryWrapper = new QueryWrapper<>();
if (teamQuery.getIdList() != null && !teamQuery.getIdList().isEmpty()) {
teamQueryWrapper.in("id", teamQuery.getIdList()); // 根据队伍ID查询
}
// 如果有搜索条件,加入搜索条件
if (teamQuery.getSearchText() != null && !teamQuery.getSearchText().isEmpty()) {
teamQueryWrapper.like("name", teamQuery.getSearchText()) // 根据队伍名称进行模糊搜索
.or().like("description", teamQuery.getSearchText()); // 或者根据队伍描述进行模糊搜索
}
// 分页处理
teamQueryWrapper.last("LIMIT " + ((teamQuery.getPageNum() - 1) * teamQuery.getPageSize()) + ", " + teamQuery.getPageSize());
// 查询队伍列表
List<Team> teamList = teamService.list(teamQueryWrapper);
// 转换为 TeamUserVO 并加入必要字段
List<TeamUserVO> teamUserVOList = teamList.stream()
.map(team -> {
TeamUserVO teamUserVO = new TeamUserVO();
teamUserVO.setId(team.getId());
teamUserVO.setName(team.getName());
teamUserVO.setDescription(team.getDescription());
teamUserVO.setUserId(team.getUserId());
teamUserVO.setCreateTime(team.getCreateTime());
teamUserVO.setUpdateTime(team.getUpdateTime());
teamUserVO.setMaxNum(team.getMaxNum());
teamUserVO.setExpireTime(team.getExpireTime());
teamUserVO.setStatus(team.getStatus());
// 其他字段的映射
return teamUserVO;
}).collect(Collectors.toList());
// 获取当前用户已加入的队伍
Set<Long> hasJoinTeamIdSet = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet());
teamUserVOList.forEach(team -> {
boolean hasJoin = hasJoinTeamIdSet.contains(team.getId());
team.setHasJoin(hasJoin); // 是否已经加入
});
// 查询已加入队伍的人数
QueryWrapper<UserTeam> userTeamJoinQueryWrapper = new QueryWrapper<>();
userTeamJoinQueryWrapper.in("teamId", idList);
List<UserTeam> userTeamListForCount = userTeamService.list(userTeamJoinQueryWrapper);
// 队伍 ID => 加入该队伍的用户列表
Map<Long, List<UserTeam>> teamIdUserTeamList = userTeamListForCount.stream()
.collect(Collectors.groupingBy(UserTeam::getTeamId));
teamUserVOList.forEach(team ->
team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(), new ArrayList<>()).size()) // 设置已加入人数
);
// 返回所有队伍信息
return ResultUtils.success(teamUserVOList);
}
个人信息页美化 p1✅
性别选择优化 p2✅
js
<template>
<template v-if="user">
<van-cell title="昵称" is-link to="/user/edit" :value="user.username"
@click="toEdit('username','昵称',user.username)"/>
<van-cell title="账号" :value="user.userAccount"/>
<van-cell title="头像" is-link to="/user/edit">
<img style="height:48px" :src="user.avatarUrl"/>
</van-cell>
<!-- 性别字段,根据值显示为 '男' 或 '女' -->
<van-cell title="性别" is-link to="/user/edit" :value="genderText" @click="toEdit('gender','性别',user.gender)"/>
<van-cell title="电话" is-link to="/user/edit" :value="user.phone" @click="toEdit('phone','电话',user.phone)"/>
<van-cell title="邮箱" is-link to="/user/edit" :value="user.email" @click="toEdit('email','邮箱',user.email)"/>
<van-cell title="职业" is-link to="/user/edit" :value="user.profile" @click="toEdit('profile','职业',user.profile)"/>
<van-cell title="星球编号" :value="user.planetCode"/>
<van-cell title="注册时间" :value="user.createTime"/>
</template>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import { onMounted, ref, computed } from "vue";
import { getCurrentUser } from "../services/user.ts";
onMounted(async () => {
user.value = await getCurrentUser();
})
const user = ref<any>();
const router = useRouter();
// 计算属性,用于根据性别值显示 '男' 或 '女'
const genderText = computed(() => {
return user.value?.gender === 1 ? '男' : '女';
});
// 跳转到编辑页面
const toEdit = (editKey: string, editName: string, currentValue: string) => {
router.push({
path: '/user/edit',
query: {
editKey,
editName,
currentValue,
},
});
}
</script>
<style scoped>
</style>
js
<template>
<van-form @submit="onSubmit">
<van-cell-group inset>
<!-- 性别单选框 -->
<van-radio-group v-model="editUser.currentValue">
<van-radio name="1">男</van-radio>
<van-radio name="0">女</van-radio>
</van-radio-group>
</van-cell-group>
<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
</van-form>
</template>
<script setup lang="ts">
import {ref} from "vue";
import {useRoute, useRouter} from "vue-router";
import {showFailToast, showSuccessToast} from "vant";
import myAxios from "../plugins/myAxios.ts";
import {getCurrentUser} from "../services/user.ts";
const router = useRouter();
// 获取页面传递的参数
const editUser = ref({
editKey: useRoute().query.editKey,
editName: useRoute().query.editName,
currentValue: useRoute().query.currentValue // 默认为用户当前的性别值
});
const onSubmit = async () => {
const currentUser = await getCurrentUser(); // 获取当前用户
if (!currentUser) {
showFailToast('请先登录');
return;
}
const res = await myAxios.post('/user/update', {
'id': currentUser.id,
[editUser.value.editKey as string]: editUser.value.currentValue
});
if (res.data > 0) {
showSuccessToast('修改成功');
router.back();
} else {
showFailToast('修改失败');
}
};
</script>
<style scoped>
</style>
注册时间优化 p2✅
js
<van-cell title="注册时间" :value="formatDate(user.createTime)"/>
退出功能 p1✅
js
<van-button color="#7232dd" plain @click="Logout" class="logout-button">退出登录</van-button>
const Logout = async () => {
try {
// 调用后端接口退出登录
const res = await myAxios.post('/user/logout');
if (res?.code === 0) {
// 退出成功,跳转到登录页
router.push("/user/login");
} else {
// 退出失败,显示错误信息
// alert("退出登录失败,请稍后重试");
}
} catch (error) {
// 捕获错误并显示提示
console.error("Logout error:", error);
showFailToast("退出登录失败,请稍后重试")
// alert("退出登录失败,请稍后重试");
}
}
通知 p2 ✅
NoticeBar 通知栏
js
<van-notice-bar
left-icon="volume-o"
background="#ECF9FF"
color="#1989FA"
text="富强 民主 文明 和谐 自由 平等 公正 法制 爱国 敬业 诚信 友善"
/>
联系用户p1 ✅
采用Dialog 弹出框
js
<template>
<van-skeleton title avatar :row="3" :loading="props.loading" v-for="user in props.userList">
<van-card
:desc="user.profile"
:title="`${user.username}(${user.planetCode})`"
:thumb="user.avatarUrl"
>
<template #tags>
<van-tag plain type="danger" v-for="tag in user.tags" style="margin-right: 8px; margin-top: 8px">
{{ tag }}
</van-tag>
</template>
<template #footer>
<van-button size="mini" @click="handleContact(user.email)">
联系我
</van-button>
</template>
</van-card>
</van-skeleton>
</template>
<script setup lang="ts">
import {showDialog} from 'vant';
import {UserType} from "../models/user";
interface UserCardListProps {
loading: boolean;
userList: UserType[];
}
const props = withDefaults(defineProps<UserCardListProps>(), {
loading: true,
userList: [] as UserType[],
});
// 处理联系按钮的点击
const handleContact = (email: string) => {
if (email) {
showDialog({
message: `联系邮件: ${email}`,
showCancelButton: false, // 不展示取消按钮
});
} else {
showDialog({
message: '该用户还没有联系方式',
showCancelButton: false,
});
}
};
</script>
<style scoped>
</style>
私密房间分享加入(思路)
生成邀请链接:
- 在创建私密队伍时,生成一个带有唯一标识(如队伍 ID 和一个邀请码或 token)的链接。
- 链接:
https://xxx.xxx.cn/join?teamId={teamId}&token={inviteToken}
。
验证链接:
- 在用户点击分享链接后,系统需要验证链接中的
teamId
和token
。 - 如果
teamId
和token
匹配且有效,则允许用户加入队伍,否则提示用户链接无效或过期。
验证队伍的隐私和密码:
- 如果是私密队伍(
TeamStatusEnum.PRIVATE
),需要判断当前用户是否已经是该队伍的成员,或者提供密码加入队伍的验证(如果设置了密码)。
加入队伍的逻辑:
- 用户点击链接后,会提交加入请求,系统会根据
teamId
和token
进行验证,确认可以加入后,执行加入队伍的操作。
todo
性别自动加入标签?
性别不作为标签展示?