目录
[用acro design写](#用acro design写)
[当分页页号改变时 触发@page-change事件 通过改变searchParams的数值](#当分页页号改变时 触发@page-change事件 通过改变searchParams的数值)
前端创建页面的开发一
创建一个路由
{
path: "/add/question",
name: "创建题目",
component: AddQuestionView,
meta: {
access: ACCESS_ENUM.USER,
},
},
用acro design写
<template>
<div id="addQuestionView" style="max-width: 800px; margin: 0 auto">
<h2 style="margin-bottom: 16px">创建题目</h2>
<a-form :model="form" label-align="left">
<a-form-item label="标题">
<a-input v-model="form.title" placeholder="" />
</a-form-item>
<a-form-item label="标签">
<a-input-tag v-model="form.tags" placeholder="请选择标签" allow-clear />
</a-form-item>
<a-form-item label="题目内容">
<MdEditor :value="form.content" :handle-change="onContentChange" />
</a-form-item>
<a-form-item label="答案">
<MdEditor :value="form.answer" :handle-change="onAnswerChange" />
</a-form-item>
<a-form-item label="判题配置一">
<div>
<a-form-item label="时间限制">
<a-input-number
v-model="form.judgeConfig.timeLimit"
placeholder="请输入时间限制"
mode="button"
min="0"
size="large"
/>
</a-form-item>
</div>
</a-form-item>
<a-form-item label="判题配置二">
<div>
<a-form-item label="内存限制">
<a-input-number
v-model="form.judgeConfig.memoryLimit"
placeholder="请输入内存限制"
mode="button"
min="0"
size="large"
/>
</a-form-item>
</div>
</a-form-item>
<a-form-item label="判题配置三">
<div>
<a-form-item label="堆栈限制">
<a-input-number
v-model="form.judgeConfig.stackLimit"
placeholder="请输入堆栈限制"
mode="button"
min="0"
size="large"
/>
</a-form-item>
</div>
</a-form-item>
<a-form-item label="测试用例配置">
<!-- 使用表格布局或者 flex 布局 -->
<table>
<tbody>
<tr v-for="(judgeCaseItem, index) of form.judgeCase" :key="index">
<td>
<a-form-item :label="`输入用例-${index}`" no-style>
<a-input
v-model="judgeCaseItem.input"
placeholder="请输入测试输入用例"
/>
</a-form-item>
</td>
<td>
<a-form-item :label="`输出用例-${index}`" no-style>
<a-input
v-model="judgeCaseItem.output"
placeholder="请输入测试输出用例"
/>
</a-form-item>
</td>
<td>
<a-button status="danger" @click="handleDelete(index)"
>删除
</a-button>
</td>
</tr>
</tbody>
</table>
<div style="margin-top: 16px">
<a-button @click="handleAdd" type="outline" status="success"
>新增测试用例
</a-button>
</div>
</a-form-item>
<a-form-item style="text-align: center">
<a-button type="primary" style="min-width: 200px" @click="doSubmit"
>提交
</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import MdEditor from "@/components/MdEditor.vue";
import { QuestionControllerService } from "../../../generated";
import message from "@arco-design/web-vue/es/message";
import { useRoute } from "vue-router";
const route = useRoute();
// 如果页面地址包含 update,视为更新页面
const updatePage = route.path.includes("update");
let form = ref({
title: "",
tags: [],
answer: "",
content: "",
judgeConfig: {
memoryLimit: 1000,
stackLimit: 1000,
timeLimit: 1000,
},
judgeCase: [
{
input: "",
output: "",
},
],
});
/**
* 根据题目 id 获取老的数据
*/
const loadData = async () => {
const id = route.query.id;
if (!id) {
return;
}
const res = await QuestionControllerService.getQuestionByIdUsingGet(
id as any
);
if (res.code === 0) {
form.value = res.data as any;
// json 转 js 对象
if (!form.value.judgeCase) {
form.value.judgeCase = [
{
input: "",
output: "",
},
];
} else {
form.value.judgeCase = JSON.parse(form.value.judgeCase as any);
}
if (!form.value.judgeConfig) {
form.value.judgeConfig = {
memoryLimit: 1000,
stackLimit: 1000,
timeLimit: 1000,
};
} else {
form.value.judgeConfig = JSON.parse(form.value.judgeConfig as any);
}
if (!form.value.tags) {
form.value.tags = [];
} else {
form.value.tags = JSON.parse(form.value.tags as any);
}
} else {
message.error("加载失败," + res.message);
}
};
onMounted(() => {
loadData();
});
const doSubmit = async () => {
console.log(form.value);
// 区分更新还是创建
if (updatePage) {
const res = await QuestionControllerService.updateQuestionUsingPost(
form.value
);
if (res.code === 0) {
message.success("更新成功");
} else {
message.error("更新失败," + res.message);
}
} else {
const res = await QuestionControllerService.addQuestionUsingPost(
form.value
);
if (res.code === 0) {
message.success("创建成功");
} else {
message.error("创建失败," + res.message);
}
}
};
/**
* 新增判题用例
*/
const handleAdd = () => {
form.value.judgeCase.push({
input: "",
output: "",
});
};
/**
* 删除判题用例
*/
const handleDelete = (index: number) => {
form.value.judgeCase.splice(index, 1);
};
const onContentChange = (value: string) => {
form.value.content = value;
};
const onAnswerChange = (value: string) => {
form.value.answer = value;
};
</script>
<style scoped>
#addQuestionView {
}
</style>
前端创建页面的开发二
开发的是其他增删改查页面
题目管理页面
查看
搜索
<template>
<div id="manageQuestionView">
<a-table
:ref="tableRef"
:columns="columns"
:data="dataList"
:pagination="{
showTotal: true,
pageSize: searchParams.pageSize,
current: searchParams.current,
total,
}"
@page-change="onPageChange"
>
<template #optional="{ record }">
<a-space>
<a-button type="primary" @click="doUpdate(record)"> 修改</a-button>
<a-button status="danger" @click="doDelete(record)">删除</a-button>
</a-space>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watchEffect } from "vue";
import {
Page_Question_,
Question,
QuestionControllerService,
} from "../../../generated";
import message from "@arco-design/web-vue/es/message";
import * as querystring from "querystring";
import { useRouter } from "vue-router";
const tableRef = ref();
const dataList = ref([]);
const total = ref(0);
const searchParams = ref({
pageSize: 10,
current: 1,
});
const loadData = async () => {
const res = await QuestionControllerService.listQuestionByPageUsingPost(
searchParams.value
);
if (res.code === 0) {
dataList.value = res.data.records;
total.value = res.data.total;
} else {
message.error("加载失败," + res.message);
}
};
/**
* 监听 searchParams 变量,改变时触发页面的重新加载
*/
watchEffect(() => {
loadData();
});
/**
* 页面加载时,请求数据
*/
onMounted(() => {
loadData();
});
// {id: "1", title: "A+ D", content: "新的题目内容", tags: "["二叉树"]", answer: "新的答案", submitNum: 0,...}
const columns = [
{
title: "题目id",
dataIndex: "id",
},
{
title: "标题",
dataIndex: "title",
},
{
title: "内容",
dataIndex: "content",
},
{
title: "标签",
dataIndex: "tags",
},
{
title: "答案",
dataIndex: "answer",
},
{
title: "提交数",
dataIndex: "submitNum",
},
{
title: "通过数",
dataIndex: "acceptedNum",
},
{
title: "判题配置",
dataIndex: "judgeConfig",
},
{
title: "判题用例",
dataIndex: "judgeCase",
},
{
title: "创建者",
dataIndex: "userId",
},
{
title: "创建时间",
dataIndex: "createTime",
},
{
title: "操作",
slotName: "optional",
},
];
const onPageChange = (page: number) => {
searchParams.value = {
...searchParams.value,
current: page,
};
};
const doDelete = async (question: Question) => {
const res = await QuestionControllerService.deleteQuestionUsingPost({
id: question.id,
});
if (res.code === 0) {
message.success("删除成功");
loadData();
} else {
message.error("删除失败");
}
};
const router = useRouter();
const doUpdate = (question: Question) => {
router.push({
path: "/update/question",
query: {
id: question.id,
},
});
};
</script>
<style scoped>
#manageQuestionView {
}
</style>
最終效果
题目更新页面的开发
写一个动态路由
携带参数的那种
const router = useRouter();
const doUpdate = (question: Question) => {
router.push({
path: "/update/question",
query: {
id: question.id,
},
});
};
修改路由
{
path: "/update/question",
name: "更新题目",
component: AddQuestionView,
meta: {
access: ACCESS_ENUM.USER,
hideInMenu: true,
},
},
页码更新细节
我们的管理题目页面不能分页
我们先处理菜单项的权限控制和权限隐藏
在这里改
在组件里面定义属性
属性绑定一个函数
const onPageChange = (page: number) => {
searchParams.value = {
...searchParams.value,
current: page,
};
};
可以参考聚合搜索项目状态改变和url状态同步
当我们的属性改变了
就会触发loadData函数
当分页页号改变时 触发@page-change事件 通过改变searchParams的数值
并且通过watchEffect监听searchParams的改变
然后执行loadData重新加载速度
实现页号变化时触发数据的重新加载
然后
/**
* 监听 searchParams 变量,改变时触发页面的重新加载
*/
watchEffect(() => {
loadData();
});
题目列表搜索页
代码
<template>
<div id="questionsView">
<a-form :model="searchParams" layout="inline">
<a-form-item field="title" label="名称" style="min-width: 240px">
<a-input v-model="searchParams.title" placeholder="请输入名称" />
</a-form-item>
<a-form-item field="tags" label="标签" style="min-width: 240px">
<a-input-tag v-model="searchParams.tags" placeholder="请输入标签" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="doSubmit">提交</a-button>
</a-form-item>
</a-form>
<a-divider size="0" />
<a-table
:ref="tableRef"
:columns="columns"
:data="dataList"
:pagination="{
showTotal: true,
pageSize: searchParams.pageSize,
current: searchParams.current,
total,
}"
@page-change="onPageChange"
>
<template #tags="{ record }">
<a-space wrap>
<a-tag v-for="(tag, index) of record.tags" :key="index" color="green"
>{{ tag }}
</a-tag>
</a-space>
</template>
<template #acceptedRate="{ record }">
{{
`${
record.submitNum ? record.acceptedNum / record.submitNum : "0"
}% (${record.acceptedNum}/${record.submitNum})`
}}
</template>
<template #createTime="{ record }">
{{ moment(record.createTime).format("YYYY-MM-DD") }}
</template>
<template #optional="{ record }">
<a-space>
<a-button type="primary" @click="toQuestionPage(record)">
做题
</a-button>
</a-space>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watchEffect } from "vue";
import {
Page_Question_,
Question,
QuestionControllerService,
QuestionQueryRequest,
} from "../../../generated";
import message from "@arco-design/web-vue/es/message";
import * as querystring from "querystring";
import { useRouter } from "vue-router";
import moment from "moment";
const tableRef = ref();
const dataList = ref([]);
const total = ref(0);
const searchParams = ref<QuestionQueryRequest>({
title: "",
tags: [],
pageSize: 8,
current: 1,
});
const loadData = async () => {
const res = await QuestionControllerService.listQuestionVoByPageUsingPost(
searchParams.value
);
if (res.code === 0) {
dataList.value = res.data.records;
total.value = res.data.total;
} else {
message.error("加载失败," + res.message);
}
};
/**
* 监听 searchParams 变量,改变时触发页面的重新加载
*/
watchEffect(() => {
loadData();
});
/**
* 页面加载时,请求数据
*/
onMounted(() => {
loadData();
});
// {id: "1", title: "A+ D", content: "新的题目内容", tags: "["二叉树"]", answer: "新的答案", submitNum: 0,...}
const columns = [
{
title: "题号",
dataIndex: "id",
},
{
title: "题目名称",
dataIndex: "title",
},
{
title: "标签",
slotName: "tags",
},
{
title: "通过率",
slotName: "acceptedRate",
},
{
title: "创建时间",
slotName: "createTime",
},
{
slotName: "optional",
},
];
const onPageChange = (page: number) => {
searchParams.value = {
...searchParams.value,
current: page,
};
};
const router = useRouter();
/**
* 跳转到做题页面
* @param question
*/
const toQuestionPage = (question: Question) => {
router.push({
path: `/view/question/${question.id}`,
});
};
/**
* 确认搜索,重新加载数据
*/
const doSubmit = () => {
// 这里需要重置搜索页号
searchParams.value = {
...searchParams.value,
current: 1,
};
};
</script>
<style scoped>
#questionsView {
max-width: 1280px;
margin: 0 auto;
}
</style>
设置路由
{
path: "/questions",
name: "浏览题目",
component: QuestionsView,
},
做题页面
代码
<template>
<div id="viewQuestionView">
<a-row :gutter="[24, 24]">
<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-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>
<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="评论" disabled> 评论区</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>cpp</a-option>
<a-option>go</a-option>
<a-option>html</a-option>
</a-select>
</a-form-item>
</a-form>
<CodeEditor
:value="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, watchEffect, withDefaults, defineProps } from "vue";
import message from "@arco-design/web-vue/es/message";
import CodeEditor from "@/components/CodeEditor.vue";
import MdViewer from "@/components/MdViewer.vue";
import {
QuestionControllerService,
QuestionSubmitAddRequest,
QuestionVO,
} from "../../../generated";
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<QuestionSubmitAddRequest>({
language: "java",
code: "",
});
/**
* 提交代码
*/
const doSubmit = async () => {
if (!question.value?.id) {
return;
}
const res = await QuestionControllerService.doQuestionSubmitUsingPost({
...form.value,
questionId: question.value.id,
});
if (res.code === 0) {
message.success("提交成功");
} else {
message.error("提交失败," + res.message);
}
};
/**
* 页面加载时,请求数据
*/
onMounted(() => {
loadData();
});
const changeCode = (value: string) => {
form.value.code = value;
};
</script>
<style>
#viewQuestionView {
max-width: 1400px;
margin: 0 auto;
}
#viewQuestionView .arco-space-horizontal .arco-space-item {
margin-bottom: 0 !important;
}
</style>
路由
{
path: "/view/question/:id",
name: "在线做题",
component: ViewQuestionView,
props: true,
meta: {
access: ACCESS_ENUM.USER,
hideInMenu: true,
},
},