【sduoj】题目集设计

2021SC@SDUSC

架构设计

SDUOJ 效仿 PTA,通过题目集的方式为用户提供答题功能。教师可随时发布一份题目集,并修改题目集的基础信息,如名称、简介、开放状态等。学生也可进入向自己开放的题目集中选择题目编写代码作答,并可实时查看解题结果。
整个题目集页面分为以下几个模块:

  • 题目集详情页面:作为题目集的首页面模块,在这里呈现了整个题目集的基本信息。教师可在该页面对自己的发布题目集进行信息更新。
  • 题目列表:这个模块将题目集中的所有题目都展示了出来,用户可点击链接跳转至相应的答题区。
  • 成绩排行:该模块负责计算所有用户当前题目集的得分,并排序。可导出当前排名的 XLSX 文件。
  • 题目管理该模块仅对题目集创建者开放。创建该题目集的教师可在该模块中看见当前系统中所有有效的题目,教师可自行将题目加入/移出该题目集中。
  • 开放信息管理该模块仅对题目集创建者开放。创建该题目集的教师可在该模块中看见所有的用户组,并向某些用户组给予/删除答题权限。
  • 题目详情页面:该模块用于展示特定题目的详情,并提供答题区域。

界面设计

该模块界面主要由两部分组成:

  • 其一,是位于页面左侧的导航栏。通过导航栏,用户可以在不同的模块间切换。
  • 其二,是占据页面大部分区域的内容容器,在这里通过一个 <router-view /> 标签,根据对应的嵌套路由将不同的模块载入内容容器展示给用户。

导航栏设计

导航栏模块采用 <el-aside> 作为侧边栏容器,整体由两部分组成。上半部分是由 <el-menu /> 组成的菜单导航按钮,通过菜单可以导航至题目集详情页面题目列表成绩排行题目管理开放信息管理五个模块。
下半部分是仿照 PTA 的题目集设计的题目按钮,用户可通过点击对应题号的按钮快速定向至对应的题目。同时,按钮的样式也会反映出用户对该题的答题状态。

<template>
	<div>
		<el-container>
			<el-aside style="width: 15%; height: 100vh; position: fixed; overflow-y: overlay;">
				<el-menu>
					<!-- 菜单导航模块
						<el-menu-item />
					 -->
					<el-divider />
					<!-- 分割线 -->
					<div style="display: flex; padding: 0 1em; flex-wrap: wrap;">
						<button v-for="(item, index) in problemSetInfo.problems" :key="index + 1" :class="{'problemBtn': true, 'passed': item.selfComplication===2, 'failed': item.selfComplication===3}" :style="menuIndex===(index+1).toString()?{}:{borderColor: 'rgba(0,0,0,0)'}" @click="getToProblem(index + 1, item)">
							<div class="passStatusCover">
								{{
									(function(passedStatus) {
										switch(passedStatus) {
											case 1:
												return index + 1;
											case 2:
												return '✔';
											case 3:
												return '✘';
										}
									})(item.selfComplication)
								}}
							</div>
							<div class="problemIndex">{{index + 1}}</div>
						</button>	
					</div>
				</el-menu>
			</el-aside>
		</el-container>
	</div>
</template>

<script>
export default {
	name: "problemSet",
	data() {
		return {
			menuIndex: undefined,
			// ...
		}
	},
	methods: {
		// ... ,
		getToProblem(index, problem) { // 跳转至对应题目的答题模块
			if (this.menuIndex != index) {
				this.menuIndex = index.toString();
				this.$router.push({
					path: `/problem-set/${this.problemSetInfo.problemSetId}/problems/${problem.id}`,
				});
			}
		},
	},
	computed: {
		problemSetInfo() {
			return this.$store.state.problemSetInfo;
		},
	},
}
</script>

<style scoped>
.problemBtn {
	background: hsl(0, 0%, 97%);
	border: none;
	cursor: pointer;
	font-size: 1rem;
	width: 1.75rem;
	height: 1.75rem;
	margin-inline: 0.2rem;
	margin-block: 0.15rem;
	border-radius: 0.15rem;
	color: #2794f8;
	border: 0.1em solid #2794f8;
	transition: 0.35s;
	outline: none;
	line-height: 1;
}

.problemBtn:hover {
	background: hsl(0, 0%, 90%);
}

.problemBtn:hover .passStatusCover {
	display: none;
}

.problemIndex {
	display: none;
}

.problemBtn:hover .problemIndex {
	display: block;
}

.passed {
	color: limegreen;
	border: 0.1em solid limegreen;
}

.failed {
	color: red;
	border: 0.1em solid red;
}
</style>

以上是聚焦至答题按钮的 ProblemSet.vue 的代码展示。
可以看到,我们利用 v-for 语法糖,从 $store.state.problemSetInfo.problems 中提取出该题目集包含的所有题目的信息,为每个题目循环生成对应的 按钮DOM 。在每个 <button /> 标签中,又存在两个 <div /> 元素,一个类名为 passStatusCover ,是用作展示该题的答题状态使用的,覆盖在 button 上方;另一个类名为 problemIndex ,内部展示该题对应的次序。
当我们将鼠标移动至 passStatusCover 上方时,将它的 display 修改为 none ,同时将 problemIndexdisplaynone 修改为 block ,从而实现两个 <div /> 的交替显示。
passStatusCover 内部,在 Mustache 中编写一个匿名函数,为其传入该题的答题状态,立即执行。根据不同的答题状态返回不同的字符展示在该 DOM 元素中。

(function(passedStatus) {
	switch(passedStatus) {
		case 1: // 该题未回答过
			return index + 1;
		case 2: // 该题回答正确过
			return '✔';
		case 3: // 该题回答过,但没有正确记录
			return '✘';
	}
})(item.selfComplication)
/**
 * 这是一种 js 的匿名函数写法
 * 通过 '()' 将这个函数整体作为一个对象
 * 返回了一个匿名的 Function 对象
 * 因此,这段代码即是当场定义一个 Function 对象并当场调用它
 */

数据管理(VueX实战)

在针对该模块的架构分析中,我们提到,该模块是由多个子模块共同构成的。同时,作为一个题目集,这个模块是拥有一个完整的数据对象的。因此,该数据对象中的部分数据可能会被多个子模块共同使用。
为了让所有的子模块都能够共享数据,同时也为了降低由于耦合度过高导致的难以维护等软件开发高风险问题,我们采用引入 VueX 实例的方法进行数据管理。
该模块作为题目集的父模块,理应承担起管理数据对象的功能。因此,我们在该模块的 created 生命周期中,做上初始化数据对象的操作。

methods: {
	// ... ,
	async getProblemSet() {
		let res = await this.$ajax.post(
			"/problemset/getProblemset",
			{
				id: this.problemSetInfo.problemSetId,
			},
			{
				headers: {
					Authorization: `Bearer ${this.$store.state.token}`,
				},
			}
		);
		if (res.data.code === 0) {
			const data = res.data.data;
			const author = data.author.toString();
			const problems = data.problems;
			let problemIds = [];
			problems.forEach((element) => {
				problemIds.push(element.id);
			});
			const jwt = require("jsonwebtoken");
			const TOKEN = jwt.decode(this.$store.state.token);
			const isMyProblemSet = author == TOKEN.USER_ID; // 判断当前用户是否是该题目集的创建者
			this.$store
				.dispatch("setProblemSetInfo", {
					name: data.name,
					announcement: data.announcement,
					author,
					open: data.open,
					endTime: data.endTime,			
					beginTime: data.beginTime,
					problems: data.problems,
					status: data.status,
					problemIds,
					isMyProblemSet,
					canUseOnlineJudge: data.canUseOnlineJudge === 1,
					canViewTestPoint: data.canViewTestPoint === 1,
				});
		}
	}
},
created() {
	this.$store
		.dispatch("setProblemSetInfo", {
			// 从路由中提取该题目集对应的 Id ,将其存放至 VueX 实例对象中
			problemSetId: Number(this.$route.params.problemSetId),
		})
		.then(() => {
			/**
			 * 由于 dispatch 是异步操作
			 * 我们需要在 then 回调中通过初始化的题目集id,获取题目集的其他基本信息
			 */
			this.getProblemSet();
		});
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小栗帽今天吃什么

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值