Skip to content

伙伴匹配优化方案

注册功能p0✅

image-20250119233058184image-20250119233114452

全局样式

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✅

  1. 获取标签 getCurrentUser
  2. 编辑标签(增删改)

后端✅

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}

验证链接

  • 在用户点击分享链接后,系统需要验证链接中的 teamIdtoken
  • 如果 teamIdtoken 匹配且有效,则允许用户加入队伍,否则提示用户链接无效或过期。

验证队伍的隐私和密码

  • 如果是私密队伍(TeamStatusEnum.PRIVATE),需要判断当前用户是否已经是该队伍的成员,或者提供密码加入队伍的验证(如果设置了密码)。

加入队伍的逻辑

  • 用户点击链接后,会提交加入请求,系统会根据 teamIdtoken 进行验证,确认可以加入后,执行加入队伍的操作。

todo

性别自动加入标签?

性别不作为标签展示?

Released under the MIT License.