耗时一个月开发的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;
};
优化题目管理页面
优化前:
去除内容和答案,这两个一般比较长,不适合在列表页查看
判题配置和判题用例也先去了,后端先让他返回VO
最后效果
解决 markdown编辑器全屏后表单组件也浮现在上层
如图:
设置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>
用户管理页面开发
效果:
代码:
坐标: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>
修改题目提交界面,增加题目提交详情页
效果:
题目提交界面新增查看详情按钮,可以查看题目提交记录的详细信息,还修改了判题信息展示为标签,判题状态展示为文字
点击查看详情可以查看题目提交记录的详情
如图:
目前还有两个Bug,一个题目标题展示问题数据加载中,一个是测试用例无法展示
题目标题展示不出来是两个方面出了问题:一个是后端QusestionSubmit 转 QusestionSubmitVO后没有带上QusertionVO,所以前端拿不到,这个在方法里set进去就好了;第二个问题是粗心的问题,前端拿QusertionVO的时候最后一个O小写了,应该是questionVO,写成了questionVo,找半天错误,服了。
测试用例无法展示:是因为后端判题信息JudgeInfo根本就没有总用例和通过用例这两个字段,增加字段后,然后记得在判题哪里更改逻辑,根据输入用例和输出用例设置值。
最终效果:
不同语言展示不同的语言模版
前端:
代码编辑器组件
<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