一些功能及页面的添加和修改

耗时一个月开发的OJ在线判题系统,文末有项目地址,目前还在更新代码~
今天我们来实现一些功能及页面的添加和修改

右上角由用户名改为头像显示

坐标:D:\project\yoj-frontend\src\components\GlobalHeader.vue

      <div>
        <a-avatar shape="circle">
          <img
            alt="avatar"
            :src="userAvatar"
            class="userAvatar"
        /></a-avatar>
      </div>

//获取登录用户的头像信息
const loginUser = store.state.user.loginUser;
const userAvatar = loginUser.userAvatar;

鼠标悬停头像展示登录或者退出登录的样式

未登录时显示登录
登录后显示个人信息和退出登录
1、main.ts中引icon

 // 额外引入图标库
import ArcoVueIcon from "@arco-design/web-vue/es/icon";
 app.use(ArcoVueIcon);

2、分别展示不同的悬停样式

      <div>
        <a-dropdown trigger="hover">
          <a-avatar shape="circle">
            <template
              v-if="loginUser && loginUser.userRole as string !== ACCESS_ENUM.NOT_LOGIN"
            >
              <template v-if="userAvatar">
                <img alt="avatar" :src="userAvatar" class="userAvatar" />
              </template>
              <template v-else>
                <a-avatar>
                  <IconUser />
                </a-avatar>
              </template>
            </template>
          </a-avatar>
          <!--登录时 鼠标悬停头像显示个人信息和退出登录,未登录时只显示登录 -->
          <template #content>
            <template
              v-if="loginUser && loginUser.userRole as 
string !== ACCESS_ENUM.NOT_LOGIN"
            >
              <a-doption>
                <template #icon>
                  <icon-idcard />
                </template>
                <template #default>
                  <a-anchor-link>个人信息</a-anchor-link>
                </template>
              </a-doption>
              <a-doption>
                <template #icon>
                  <icon-poweroff />
                </template>
                <template #default>
                  <a-anchor-link @click="logout">退出登录 </a-anchor-link>
                </template>
              </a-doption>
            </template>
            <template v-else>
              <a-doption>
                <template #icon>
                  <icon-user />
                </template>
                <template #default>
                  <a-anchor-link href="/user/login">登录 </a-anchor-link>
                </template>
              </a-doption>
            </template>
          </template>
        </a-dropdown>
      </div>

3、实现退出登录函数

//退出登录
const logout = () => {
  UserControllerService.userLogoutUsingPost();
  location.reload;
};

优化题目管理页面

优化前:
image.png
去除内容和答案,这两个一般比较长,不适合在列表页查看
判题配置和判题用例也先去了,后端先让他返回VO
最后效果
image.png

解决 markdown编辑器全屏后表单组件也浮现在上层

如图:
image.png

设置z-index,当用户点击内容的文档编辑器时,即可设置内容在上,设置答案时,答案在上
坐标:src\views\question\AddQuestionView.vue
1、首先定义两个变量

const contentZIndex = ref(1);
 const answerZIndex = ref(1);

2、然后在文档编辑器引入

 <a-form-item field="content" label="题目内容: ">
        <md-editor
          :style="`z-index: ${contentZIndex}`"
          mode="split"
          :value="form.content"
          :handle-change="onContentChange"
        />
      </a-form-item>
      <a-form-item field="answer" label="标准答案: ">
        <md-editor
          :style="`z-index: ${answerZIndex}`"
          mode="split"
          :value="form.answer"
          :handle-change="onAnswerChange"
        />
      </a-form-item>

3、最后在用户点击时修改值

 <a-form-item field="content" label="题目内容: ">
        <md-editor
          @click="
            contentZIndex = 2;
            answerZIndex = 1;
          "
          :style="`z-index: ${contentZIndex}`"
          mode="split"
          :value="form.content"
          :handle-change="onContentChange"
        />
      </a-form-item>
      <a-form-item field="answer" label="标准答案: ">
        <md-editor
          @click="
            contentZIndex = 1;
            answerZIndex = 2;
          "
          :style="`z-index: ${answerZIndex}`"
          mode="split"
          :value="form.answer"
          :handle-change="onAnswerChange"
        />
      </a-form-item>

用户管理页面开发

效果:
image.png
代码:
坐标:src\views\user\UserManageView.vue

<template>
  <div id="userManageView">
    <a-form
      :model="searchParams"
      layout="inline"
      style="justify-content: center; align-content: center; margin: 25px"
    >
      <a-form-item field="title" label="用户编号:" tooltip="请输入用户的编号">
        <a-input
          v-model="searchParams.id"
          placeholder="请输入要搜索的用户编号"
        />
      </a-form-item>
      <a-form-item field="title" label="用户名称:" tooltip="请输入用户名称">
        <a-input
          v-model="searchParams.userName"
          placeholder="请输入要搜索的用户名称"
        />
      </a-form-item>
      <a-form-item>
        <a-button type="outline" shape="round" status="normal" @click="doSubmit"
          >搜 索
        </a-button>
      </a-form-item>
      <a-form-item>
        <a-button type="outline" shape="round" status="normal" @click="loadData"
          >刷 新
        </a-button>
      </a-form-item>
    </a-form>
    <a-table
      :column-resizable="true"
      :ref="tableRef"
      :columns="columns"
      :data="dataList"
      :pagination="{
        showTotal: true,
        pageSize: searchParams.pageSize,
        current: searchParams.current,
        total,
        showJumper: true,
        showPageSize: true,
      }"
      @page-change="onPageChange"
      @pageSizeChange="onPageSizeChange"
    >
      <template #userAvatar="{ record }">
        <a-avatar :size="70" shape="circle">
          <img alt="userAvatar" :src="record.userAvatar" />
        </a-avatar>
      </template>
      <template #userRole="{ record }">
        <!-- user普通用户 admin管理员 -->
        <a-tag v-if="record.userRole === 'user'" color="arcoblue"
          >普通用户
        </a-tag>
        <a-tag v-if="record.userRole === 'admin'" color="green">管理员</a-tag>
      </template>
      <template #createTime="{ record }">
        {{ moment(record.createTime).format("YYYY-MM-DD HH:mm:ss") }}
      </template>
      <template #updateTime="{ record }">
        {{ moment(record.updateTime).format("YYYY-MM-DD HH:mm:ss") }}
      </template>
      <!-- <template #userState="{ record }">
        <a-tag v-if="record.userState === '正常'" color="blue">正常</a-tag>
        <a-tag v-if="record.userState === '注销'" color="grey">注销</a-tag>
        <a-tag v-if="record.userState === '封号'" color="red">封号</a-tag>
      </template> -->
      <template #optional="{ record }">
        <a-space>
          <a-button
            shape="round"
            type="outline"
            @click="openModalForm(record.id)"
            >修改
          </a-button>
          <!-- <a-popconfirm
            content="确定要删除此题目吗?"
            type="error"
            okText="是"
            cancelText="否"
            @cancel="
              () => {
                console.log(`已取消`);
              }
            "
            @ok="doDelete(record)"
          > -->
          <a-button
            shape="round"
            type="outline"
            status="danger"
            @click="doDelete(record)"
            >删除
          </a-button>
          <!-- </a-popconfirm> -->
        </a-space>
      </template>
    </a-table>
    <a-modal
      width="30%"
      :visible="visible"
      placement="right"
      @ok="handleOk"
      @cancel="closeModel"
      unmountOnClose
    >
      <div style="text-align: center">
        <a-upload
          action="/"
          :fileList="file ? [file] : []"
          :show-file-list="false"
          @change="onChange"
          :custom-request="uploadAvatar"
        >
          <template #upload-button>
            <a-avatar :size="70" shape="circle">
              <img alt="头像" :src="userInfo?.userAvatar" />
            </a-avatar>
          </template>
        </a-upload>
      </div>
      <a-form
        label-align="right"
        title="个人信息"
        style="max-width: 480px; margin: 0 auto"
      >
        <a-form-item field="名称" label="名称 :">
          <a-input v-model="userInfo.userName" placeholder="请输入用户名称" />
        </a-form-item>
        <a-form-item field="账号" label="账号 :">
          <a-input v-model="userInfo.userAccount" placeholder="请输入账号" />
        </a-form-item>
        <!-- <a-form-item field="邮箱" label="邮箱 :">
          <a-input v-model="userInfo.email" placeholder="请输入邮箱" />
        </a-form-item>
        <a-form-item field="电话" label="电话 :">
          <a-input v-model="userInfo.phone" placeholder="请输入电话号码" />
        </a-form-item>
        <a-form-item field="用户状态" label="状态 :">
          <a-select v-model="userInfo.userState" placeholder="请输入用户状态">
            <a-option value="正常">正常</a-option>
            <a-option value="注销">注销</a-option>
            <a-option value="封号">封号</a-option>
          </a-select>
        </a-form-item> -->
        <a-form-item field="用户角色" label="角色 :">
          <a-select v-model="userInfo.userRole" placeholder="请输入用户角色">
            <a-option value="admin">管理员</a-option>
            <a-option value="user">普通用户</a-option>
          </a-select>
        </a-form-item>
        <!-- <a-form-item field="性别" label="性别 :">
          <a-input v-model="userInfo.gender" placeholder="请输入性别" />
        </a-form-item> -->
        <a-form-item field="userProfile" label="简介 :">
          <a-textarea v-model="userInfo.userProfile" placeholder="请输入简介" />
        </a-form-item>
      </a-form>
    </a-modal>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref, watchEffect } from "vue";

import message from "@arco-design/web-vue/es/message";
import moment from "moment";
import { useRouter } from "vue-router";
import { FileItem, Message } from "@arco-design/web-vue";
import {
  FileControllerService,
  User,
  UserControllerService,
} from "../../../generated";

const router = useRouter();
const tableRef = ref();
const file = ref();

const visible = ref(false);
const userInfo = ref<User>();

const dataList = ref([]);
const total = ref(0);
const searchParams = ref({
  id: undefined,
  userName: "",
  email: "",
  phone: "",
  pageSize: 10,
  current: 1,
});

const loadData = async () => {
  const res = await UserControllerService.listUserByPageUsingPost({
    ...searchParams.value,
    sortField: "createTime",
    sortOrder: "descend",
  });
  if (res.code === 0) {
    dataList.value = res.data.records;
    total.value = res.data.total;
  } else {
    message.error("加载失败," + res.message);
  }
};

/**
 * 监听 searchParams 变量,改变时触发页面的重新加载
 */
watchEffect(() => {
  loadData();
});

/**
 * 页面加载时,请求数据
 */
onMounted(() => {
  loadData();
});

const columns = [
  {
    title: "编号",
    dataIndex: "id",
    align: "center",
  },
  {
    title: "账号",
    dataIndex: "userAccount",
    align: "center",
  },
  {
    title: "名称",
    dataIndex: "userName",
    align: "center",
  },
  {
    title: "头像",
    slotName: "userAvatar",
    align: "center",
    width: 64,
  },
  {
    title: "简介",
    dataIndex: "userProfile",
    align: "center",
  },
  // {
  //   title: "状态",
  //   slotName: "userState",
  //   align: "center",
  // },
  {
    title: "角色",
    slotName: "userRole",
    align: "center",
  },
  {
    title: "创建时间",
    slotName: "createTime",
    align: "center",
  },
  {
    title: "更新时间",
    slotName: "updateTime",
    align: "center",
  },
  {
    title: "操作",
    slotName: "optional",
    align: "center",
  },
];
/**
 * 分页
 * @param page
 */
const onPageChange = (page: number) => {
  searchParams.value = {
    ...searchParams.value,
    current: page,
  };
};

/**
 * 分页大小
 * @param size
 */
const onPageSizeChange = (size: number) => {
  searchParams.value = {
    ...searchParams.value,
    pageSize: size,
  };
};
/**
 * 打开弹窗,获取到用户信息
 */
const openModalForm = async (userId: any) => {
  const res = await UserControllerService.getUserByIdUsingGet(userId);
  console.log("res:", res.data);
  userInfo.value = res.data;
  console.log(userInfo.value);
  visible.value = true;
};
/**
 * 删除
 * @param user
 */
const doDelete = async (user: User) => {
  const res = await UserControllerService.deleteUserUsingPost({
    id: user.id,
  });
  if (res.code === 0) {
    message.success("删除成功");
    loadData();
  } else {
    message.error("删除失败");
  }
};

/**
 * 确认搜索,重新加载数据
 */
const doSubmit = () => {
  // 这里需要重置搜索页号
  searchParams.value = {
    ...searchParams.value,
    current: 1,
  };
};

// 从表单中获取的用户头像
let userAvatarImg = userInfo.value?.userAvatar;

/**
 * 上传头像
 */
const uploadAvatar = async () => {
  const res = await FileControllerService.uploadFileUsingPost(file?.value.file);
  if (res.code === 0) {
    userAvatarImg = res.data;
    Message.success("上传成功,点击确认即可修改头像");
  } else {
    Message.error("上传失败!" + res.data);
  }
};

/**
 * 确定修改按钮
 */
const handleOk = async () => {
  const res = await UserControllerService.updateUserUsingPost({
    ...userInfo.value,
    userAvatar: userAvatarImg,
  });
  if (res.code === 0) {
    Message.success("更新成功!");
    visible.value = false;
    location.reload();
  } else {
    Message.error("更新失败!", res.msg);
  }
};
const closeModel = () => {
  visible.value = false;
};

const onChange = async (_: never, currentFile: FileItem) => {
  file.value = {
    ...currentFile,
  };
};
</script>

<style scoped>
#userManageView {
  padding: 5px;
  box-shadow: 0px 0px 10px rgba(35, 7, 7, 0.21);
  border-radius: 10px;
}
</style>

修改题目提交界面,增加题目提交详情页

效果:
题目提交界面新增查看详情按钮,可以查看题目提交记录的详细信息,还修改了判题信息展示为标签,判题状态展示为文字

image.png

点击查看详情可以查看题目提交记录的详情
如图:
image.png
目前还有两个Bug,一个题目标题展示问题数据加载中,一个是测试用例无法展示

题目标题展示不出来是两个方面出了问题:一个是后端QusestionSubmit 转 QusestionSubmitVO后没有带上QusertionVO,所以前端拿不到,这个在方法里set进去就好了;第二个问题是粗心的问题,前端拿QusertionVO的时候最后一个O小写了,应该是questionVO,写成了questionVo,找半天错误,服了。

测试用例无法展示:是因为后端判题信息JudgeInfo根本就没有总用例和通过用例这两个字段,增加字段后,然后记得在判题哪里更改逻辑,根据输入用例和输出用例设置值。
最终效果:
image.png

不同语言展示不同的语言模版

前端:

代码编辑器组件

<template>
  <div
    id="code-editor"
    ref="codeEditorRef"
    style="min-height: 400px; height: 60vh"
  ></div>
</template>

<script setup lang="ts">
import * as monaco from "monaco-editor"; // 全部导入
import { ref, onMounted, toRaw, withDefaults, defineProps, watch } from "vue";

const codeEditor = ref();
const codeEditorRef = ref();

interface Props {
  code: any;
  handleChange: (value: string) => void;
  language?: string;
}

const props = withDefaults(defineProps<Props>(), {
  code: "import java.util.Scanner;\n// 1:无需package\n// 2: 类名必须Main, 不可修改\n\npublic class Main {\n    public static void main(String[] args) {\n        Scanner scan = new Scanner(System.in);\n        //在此输入您的代码...\n        scan.close();\n    }\n}",
  handleChange: (v: string) => {
    console.log(v);
  },
  language: () => "java",
});

/**
 * 监听编辑器中语言的变化
 */
watch(
  () => props.language,
  () => {
    if (codeEditor.value) {
      monaco.editor.setModelLanguage(
        toRaw(codeEditor.value).getModel(),
        props.language
      );
    }
  }
);

/**
 * 监听编辑器中语言模板的变化
 */
watch(
  () => props.code,
  () => {
    //vue3一定要用toRaw不然会直接卡死
    toRaw(codeEditor?.value).getModel().setValue(props.code);
  }
);

onMounted(() => {
  if (!codeEditorRef.value) {
    return;
  }
  codeEditor.value = monaco.editor.create(codeEditorRef.value, {
    value: props.code,
    language: "java",
    minimap: {
      enabled: true,
      size: "fit",
    },
    automaticLayout: true,
    colorDecorators: true, //颜色装饰器
    readOnly: false, //是否开启已读功能
    theme: "vs-dark", //主题
    lineNumbers: "on",
  });
  // 监听编辑器内容变化
  codeEditor.value.onDidChangeModelContent(() => {
    // 触发父组件的 change 事件,通知编辑器内容变化,使用toRaw防止卡死
    props.handleChange(toRaw(codeEditor.value).getValue());
  });
});
</script>

<style scoped></style>

父组件src\views\question\ViewQuestionView.vue

<template>
  <div id="viewQuestionsView" :gutter="[24, 24]">
    <a-row>
      <a-col :md="12" :xs="24">
        <a-tabs default-active-key="question">
          <a-tab-pane key="question" title="題目">
            <a-card v-if="question" :title="question.title">
              <a-space direction="vertical" size="large" fill>
                <a-descriptions
                  title="判题条件"
                  :column="{ xs: 1, md: 2, lg: 3 }"
                >
                  <a-descriptions-item label="时间限制">
                    {{ question.judgeConfig?.timeLimit ?? 0 }}
                  </a-descriptions-item>
                  <a-descriptions-item label="内存限制">
                    {{ question.judgeConfig?.memoryLimit ?? 0 }}
                  </a-descriptions-item>
                  <a-descriptions-item label="堆栈限制">
                    {{ question.judgeConfig?.stackLimit ?? 0 }}
                  </a-descriptions-item>
                </a-descriptions>
              </a-space>
              <MdViewer :value="question.content || ''" />
              <template #extra>
                <a-space wrap>
                  <a-tag
                    v-for="(tag, index) of question.tags"
                    :key="index"
                    color="green"
                    >{{ tag }}</a-tag
                  >
                </a-space>
              </template>
            </a-card>
          </a-tab-pane>
          <a-tab-pane key="comment" title="评论"> 评论区 </a-tab-pane>
          <a-tab-pane key="answer" title="题解"> 暂时无题解 </a-tab-pane>
        </a-tabs>
      </a-col>
      <a-col :md="12" :xs="24">
        <a-form :model="form" layout="inline">
          <a-form-item
            field="language"
            label="编程语言"
            style="min-width: 240px"
          >
            <a-select
              v-model="form.language"
              :style="{ width: '320px' }"
              placeholder="选择编程语言"
            >
              <a-option>java</a-option>
              <a-option>python</a-option>
              <a-option>cpp</a-option>
              <a-option>c</a-option>
            </a-select>
          </a-form-item>
        </a-form>

        <CodeEditor
          :code="form.code as string"
          :language="form.language"
          :handle-change="changeCode"
        />
        <a-divider size="0" />
        <a-button type="primary" style="min-width: 200px" @click="doSubmit"
          >提交</a-button
        >
      </a-col>
    </a-row>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref, watch, watchEffect } from "vue";
import {
  QuestionControllerService,
  QuestionSubmitControllerService,
  QuestionVO,
  QuestionSubmitAddRequest,
} from "../../../generated";
import message from "@arco-design/web-vue/es/message";
import CodeEditor from "@/components/CodeEditor.vue";
import MdViewer from "@/components/MdViewer.vue";
import { languages } from "monaco-editor/esm/metadata";
interface Props {
  id: string;
}
const props = withDefaults(defineProps<Props>(), {
  id: () => "",
});
const question = ref<QuestionVO>();
const loadData = async () => {
  const res = await QuestionControllerService.getQuestionVoByIdUsingGet(
    props.id as any
  );
  if (res.code === 0) {
    question.value = res.data;
  } else {
    message.error("加载失败," + res.message);
  }
};

const form = ref({
  language: "java",
  code: question.value?.codeTemplate?.java,
});
//监听语言切换代码模版
watch(
  () => form.value.language,
  (language) => {
    if (language === "java") {
      form.value.code = question.value?.codeTemplate?.java;
    } else if (language === "cpp") {
      form.value.code = question.value?.codeTemplate?.cpp;
    } else if (language === "python") {
      form.value.code = question.value?.codeTemplate?.python;
    } else if (language === "c") {
      form.value.code = question.value?.codeTemplate?.c;
    }
  }
);
//提交代码
const doSubmit = async () => {
  if (!question.value?.id) {
    return;
  }
  const res = await QuestionSubmitControllerService.doQuestionSubmitUsingPost({
    ...form.value,
    questionId: question.value.id,
  });
  if (res.code === 0) {
    message.success("提交成功");
  } else {
    message.error("提交失败" + res.message);
  }
};
//页面加载时请求数据
onMounted(() => {
  loadData();
});
watchEffect(() => {
  loadData();
});
const changeCode = (value: string) => {
  form.value.code = value;
};
</script>

<style>
#viewQuestionsView {
  max-width: 1400px;
  margin: 0 auto;
}

#viewQuestionsView .arco-space-horizontal .arco-space-item {
  margin-bottom: 0 !important;
}
</style>

后端

模版类

package com.xin.xinoj.model.dto.question;

import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author 15712
 */
@NoArgsConstructor
@Data
public class CodeTemplate {
    private String java;
    private String python;
    private String cpp;
    private String c;
}

模版通过本地 json 文件读取

{
  "java": "import java.util.Scanner;\n// 1:无需package\n// 2: 类名必须Main, 不可修改\n\npublic class Main {\n    public static void main(String[] args) {\n        Scanner scan = new Scanner(System.in);\n        //在此输入您的代码...\n        scan.close();\n    }\n}",
  "python": "import os\nimport sys\n\n# 请在此输入您的代码",
  "cpp": "#include <iostream>\nusing namespace std;\nint main()\n{\n  // 请在此输入您的代码\n  return 0;\n}",
  "c": "#include <stdio.h>\n#include <stdlib.h>\n\nint main(int argc, char *argv[])\n{\n  // 请在此输入您的代码\n  return 0;\n}"
}

在QuestionVO中新增CodeTemplate字段
修改获取QuestionVO方法,从 json 文件中读取代码模版

    /**
     *
     * @param question
     * @param request
     * @return
     */
    @Override
    public QuestionVO getQuestionVO(Question question, HttpServletRequest request) {
        QuestionVO questionVO = QuestionVO.objToVo(question);
        long questionId = question.getId();
        // 1. 关联查询用户信息
        Long userId = question.getUserId();
        User user = null;
        if (userId != null && userId > 0) {
            user = userService.getById(userId);
        }
        UserVO userVO = userService.getUserVO(user);
        questionVO.setUserVO(userVO);
        CodeTemplate codeTemplate = new CodeTemplate();
        //从文件中读取,获取代码模版
        Gson gson = new Gson();
        FileReader reader = null;
        try {
            reader = new FileReader("D:\\project\\yoj-backend\\src\\main\\resources\\codeTemplate.json");
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        Map<String,String> codeMap = gson.fromJson(reader, Map.class);
        codeTemplate.setJava(codeMap.get("java"));
        codeTemplate.setPython(codeMap.get("python"));
        codeTemplate.setCpp(codeMap.get("cpp"));
        codeTemplate.setC(codeMap.get("c"));
        questionVO.setCodeTemplate(codeTemplate);
        return questionVO;
    }

个人简介+修改个人信息

两个弹窗
坐标:src\components\GlobalHeader.vue

<template>
  <a-row id="globalHeader" align="center" :wrap="false">
    <a-col flex="auto">
      <a-menu
        mode="horizontal"
        :selected-keys="secretedKeys"
        @menu-item-click="doMenuClick"
      >
        <a-menu-item
          key="0"
          :style="{ padding: 0, marginRight: '38px' }"
          disabled
        >
          <div class="title-bar">
            <img class="logo" src="../assets/icon22.jpg" />
            <div class="title">Y OJ</div>
          </div>
        </a-menu-item>
        <a-menu-item v-for="item in visibleRoutes" :key="item.path">
          {{ item.name }}
        </a-menu-item>
      </a-menu>
    </a-col>
    <a-col flex="100px">
      <div>
        <a-dropdown trigger="hover">
          <a-avatar shape="circle">
            <template
              v-if="loginUser && loginUser.userRole as string !== ACCESS_ENUM.NOT_LOGIN"
            >
              <template v-if="userAvatar">
                <img alt="avatar" :src="userAvatar" class="userAvatar" />
              </template>
              <template v-else>
                <a-avatar>
                  <IconUser />
                </a-avatar>
              </template>
            </template>
          </a-avatar>
          <!--登录时 鼠标悬停头像显示个人信息和退出登录,未登录时只显示登录 -->
          <template #content>
            <template
              v-if="loginUser && loginUser.userRole as 
string !== ACCESS_ENUM.NOT_LOGIN"
            >
              <a-doption>
                <template #icon>
                  <icon-idcard />
                </template>
                <template #default>
                  <a-anchor-link @click="handleClick">个人简介</a-anchor-link>
                </template>
              </a-doption>
              <a-doption>
                <template #icon>
                  <icon-poweroff />
                </template>
                <template #default>
                  <a-anchor-link @click="logout">退出登录 </a-anchor-link>
                </template>
              </a-doption>
            </template>
            <template v-else>
              <a-doption>
                <template #icon>
                  <icon-user />
                </template>
                <template #default>
                  <a-anchor-link href="/user/login">登录 </a-anchor-link>
                </template>
              </a-doption>
            </template>
          </template>
        </a-dropdown>
      </div>
    </a-col>
  </a-row>
  <!-- 对话框 -->
  <a-modal v-model:visible="visible" @ok="handleOk" :footer="false">
    <template #title>用户简介</template>
    <div>
      <a-descriptions
        size="mini"
        layout="inline-vertical"
        :data="data"
        bordered
      />
    </div>
    <a-button
      @click="openModalForm(loginUser.id)"
      style="margin-top: 10px"
      :visible="visible2"
    >
      修改个人信息
    </a-button>
  </a-modal>

  <a-modal
    width="30%"
    :visible="visible2"
    placement="right"
    @ok="handleOk2"
    @cancel="closeModel"
    unmountOnClose
  >
    <div style="text-align: center">
      <a-upload
        action="/"
        :fileList="file ? [file] : []"
        :show-file-list="false"
        @change="onChange"
        :custom-request="uploadAvatar"
      >
        <template #upload-button>
          <a-avatar :size="70" shape="circle">
            <img alt="头像" :src="userInfo?.userAvatar" />
          </a-avatar>
        </template>
      </a-upload>
    </div>
    <a-form
      label-align="right"
      title="个人信息"
      style="max-width: 480px; margin: 0 auto"
    >
      <a-form-item field="名称" label="名称 :">
        <a-input v-model="userInfo.userName" placeholder="请输入用户名称" />
      </a-form-item>
      <a-form-item field="账号" label="账号 :">
        <a-input v-model="userInfo.userAccount" placeholder="请输入账号" />
      </a-form-item>
      <a-form-item field="用户角色" label="角色 :">
        <a-select v-model="userInfo.userRole" placeholder="请输入用户角色">
          <a-option value="admin">管理员</a-option>
          <a-option value="user">普通用户</a-option>
        </a-select>
      </a-form-item>
      <a-form-item field="userProfile" label="简介 :">
        <a-textarea v-model="userInfo.userProfile" placeholder="请输入简介" />
      </a-form-item>
    </a-form>
  </a-modal>
</template>

<script setup lang="ts">
import { routes } from "../router/routes";
import { useRouter } from "vue-router";
import { computed, ref } from "vue";
import { useStore } from "vuex";
import checkAccess from "@/access/checkAccess";
import ACCESS_ENUM from "../access/accessEnum";
import { FileControllerService, User, UserControllerService } from "../../generated";
import moment from "moment";
import { FileItem, Message } from "@arco-design/web-vue";
const router = useRouter();
const secretedKeys = ref(["/"]);

router.afterEach((to, from, failure) => {
  secretedKeys.value = [to.path];
});
const doMenuClick = (key: string) => {
  router.push({
    path: key,
  });
};
const store = useStore();
const loginUser = computed(() => store.state.user.loginUser);
const userAvatar = loginUser.value.userAvatar;
const visibleRoutes = computed(() => {
  return routes.filter((item, index) => {
    if (item.meta?.hideInMenu) {
      return false;
    }
    //根据权限过滤菜单
    if (
      !checkAccess(store.state.user.loginUser, item?.meta?.access as string)
    ) {
      return false;
    }
    return true;
  });
});
//退出登录
const logout = () => {
  UserControllerService.userLogoutUsingPost();
  location.reload();
};
//这个变量是干什么的啊
const visible = ref(false);
const handleClick = () => {
  visible.value = true;
};
const handleOk = () => {
  visible.value = false;
};
const handleCancel = () => {
  visible.value = false;
};
const data = computed(() => {
  return [
    {
      label: "用户名称",
      value: loginUser.value.userName,
    },
    {
      label: "id",
      value: loginUser.value.id,
    },
    {
      label: "用户简介",
      value: loginUser.value.userProfile,
    },
    {
      label: "注册时间",
      value: moment(loginUser.value.createTime).format("YYYY-MM-DD hh:mm"),
    },
  ];
});
//弹窗
const visible2 = ref(false);
const userInfo = ref<User>();
const openModalForm = async (userId: any) => {
  const res = await UserControllerService.getUserByIdUsingGet(userId);
  userInfo.value = res.data;
  visible2.value = true;
};
/**
 * 确定修改按钮
 */
// 从表单中获取的用户头像
let userAvatarImg = userInfo.value?.userAvatar;
const handleOk2 = async () => {
  const res = await UserControllerService.updateUserUsingPost({
    ...userInfo.value,
    userAvatar: userAvatarImg,
  });
  if (res.code === 0) {
    Message.success("更新成功!");
    visible2.value = false;
    location.reload();
  } else {
    Message.error("更新失败!", res.msg);
  }
};
const closeModel = () => {
  visible2.value = false;
};
const file = ref();
const onChange = async (_: never, currentFile: FileItem) => {
  file.value = {
    ...currentFile,
  };
};
//上传头像
/**
 * 上传头像
 */
const uploadAvatar = async () => {
  const res = await FileControllerService.uploadFileUsingPost(file?.value.file);
  if (res.code === 0) {
    userAvatarImg = res.data;
    Message.success("上传成功,点击确认即可修改头像");
  } else {
    Message.error("上传失败!" + res.data);
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.title-bar {
  display: flex;
  align-items: center;
}
.logo {
  height: 48px;
}
.title {
  color: #444;
  margin-left: 16px;
}
</style>

修改信息后,立刻不登录了

确保登录用户的信息从vuex中取,而不是从刷新后的页面状态中获取

const loginUser = computed(() => store.state.user.loginUser);

todo流程跑通

差不多了,修复了不少BUG

todo上线

todebug 右上角用户头像容易变成默认的

这个暂时不清楚是哪里的问题

todo评论区

有可能不做了

todo扩展实现c++语言的代码沙箱

可以看看后面添加

项目地址

(求求大佬们赏个star~)

前端:https://github.com/IMZHEYA/yoj-frontend
后端:https://github.com/IMZHEYA/yoj-backend
代码沙箱:https://github.com/IMZHEYA/yoj-code-sandbox

  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值