分布式在线判题系统(分布式OJ系统)客户端

yangyi 4501809245 refactor: 题目描述单栏显示(恢复全宽编辑器)并移除实时预览 há 2 semanas atrás
.vscode c1b1d32b4c # init project há 7 meses atrás
public 94479b270d # feat:全局状态管理网站的元数据(标题,logo等等) há 7 meses atrás
src 4501809245 refactor: 题目描述单栏显示(恢复全宽编辑器)并移除实时预览 há 2 semanas atrás
.gitignore c1b1d32b4c # init project há 7 meses atrás
AGENTS.md e5063c53b5 docs: 添加 AGENTS.md 协作约定 há 2 semanas atrás
README.md 1eecae7f33 fix: 统一接口错误提示并消除重复消息 há 2 semanas atrás
index.html 0005725c9b fix:修正网页标题 há 2 semanas atrás
orval.config.ts 8f73102fbf # feat:使用orval对openAPI进行借口生成;完成orval的配置; há 6 meses atrás
package-lock.json 7de7e767db # feat: 题目代码提交与判题结果展示(Monaco+bytemd),新增我的提交记录页 há 1 mês atrás
package.json 7de7e767db # feat: 题目代码提交与判题结果展示(Monaco+bytemd),新增我的提交记录页 há 1 mês atrás
todo.md ee1bc7973e # feat: 顶部菜单过滤详情路由 há 2 meses atrás
todoDetail.md 6b37bf545c # api: 题目与比赛模块api(通过orval生成) há 2 meses atrás
tsconfig.app.json 24d708772a # feat:用户管理模块的增删改查 há 6 meses atrás
tsconfig.json c1b1d32b4c # init project há 7 meses atrás
tsconfig.node.json c1b1d32b4c # init project há 7 meses atrás
vite.config.ts 24d708772a # feat:用户管理模块的增删改查 há 6 meses atrás

README.md

OJ 在线判题系统 · 前端

基于 Vue 3 + TypeScript + Vite 6 的在线判题(Online Judge)前端,提供题目浏览与提交、比赛与排名、后台管理等功能。

技术栈

分类 选型
框架 Vue 3.5(<script setup> SFC)
语言 TypeScript 5.8
构建 Vite 6
路由 Vue Router 4
状态管理 Pinia 3 + pinia-plugin-persistedstate
UI 组件库 Element Plus 2(中文语言环境)
HTTP Axios(统一拦截器)
接口生成 Orval 7(基于 OpenAPI 生成 TypeScript 客户端与类型)
代码编辑器 Monaco Editor(当前仅 Java)
Markdown ByteMD(GFM + 代码高亮)
时间处理 Day.js

功能特性

用户端

  • 题目:列表支持按标题、难度、分类搜索与分页;详情页展示描述、限制信息、样例、统计与分类标签,并可在页内使用 Monaco 提交代码、查看判题结果。
  • 比赛:列表按状态/类型筛选;详情页展示赛题列表与分值,支持报名(密码型比赛先校验口令);独立排名页展示名次、用户、总分与 AC 最大耗时。
  • 我的提交:分页表格展示提交记录,展开行可查看提交的代码与判题详情。
  • 账号:登录/退出,用户信息与登录态持久化。

管理后台

  • 题目管理:题目 CRUD,维护分类勾选,详情弹窗管理样例与评测用例(含隐藏标记)。
  • 分类管理:题目分类 CRUD。
  • 比赛管理:比赛 CRUD,配置赛题(多选题目 + 显示顺序 + 分值),查看报名名单。
  • 用户管理:用户分页查询、状态与角色管理、改密与头像。

快速开始

环境要求

  • Node.js 18+
  • 后端服务运行在 http://localhost:8080(提供 /v3/api-docs OpenAPI 文档)

安装与运行

npm install
npm run dev        # 本地开发,默认 http://localhost:5173
npm run build      # vue-tsc 类型检查 + 生产构建,产物在 dist/
npm run preview    # 本地预览构建产物
npm run api:gen    # 依据后端 OpenAPI 文档重新生成接口代码

后端联调

开发环境通过 Vite 代理转发请求,/api 前缀会被重写后转发到 http://localhost:8080:

// vite.config.ts
server: {
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, '')
    }
  }
}

Axios 实例的 baseURL 取 import.meta.env.VITE_API_BASE_URL,未配置时回退为 /api(即走上面的代理)。如需直连后端或部署到其他地址,创建 .env.local 覆盖即可:

VITE_API_BASE_URL=http://localhost:8080

请求与响应约定

src/util/axios-instance.ts 集中处理鉴权与提示:

  • 请求拦截器从 localStorage.token 读取令牌并注入 Authorization: Bearer <token>。
  • 响应拦截器按业务状态码判断:code 为 0 或 200 视为成功并直接返回 response.data;失败时统一弹出后端返回的 message 并 reject。
  • HTTP/网络异常同样由拦截器统一提示(相同文案 1.5 秒内去重);HTTP 401 统一提示并跳转登录页。
  • 后端成功响应的 message 固定为 success,因此拦截器不再弹成功提示;操作成功文案与表单校验提示保留在视图层。

视图层不再处理接口错误提示,只需 try/catch 保留必要的状态重置、页面跳转等控制流。

接口代码生成

Orval 读取后端 OpenAPI 文档,按标签拆分为控制器文件,类型输出到 src/api/models,并使用自定义 mutator 走统一的 Axios 实例:

// orval.config.ts 关键配置
input: { target: 'http://localhost:8080/v3/api-docs' },
output: {
  mode: 'tags',
  client: 'axios',
  target: './src/api/client.ts',
  schemas: './src/api/models',
  clean: true,
  override: {
    mutator: { path: './src/util/axios-instance.ts', name: 'customAxiosInstance' },
    query: { useQuery: true, useMutation: true }
  }
},
hooks: { afterAllFilesWrite: 'prettier --write' }

后端每完成一批接口后执行 npm run api:gen,产物示例:

src/api/
├── auth-controller.ts        # 登录认证
├── category-controller.ts    # 题目分类
├── contest-controller.ts     # 比赛
├── problem-controller.ts     # 题目
├── submission-controller.ts  # 代码提交与判题
├── user-controller.ts        # 用户管理
└── models/                   # 请求/响应 DTO、VO 与枚举

视图层按控制器调用接口,类型统一从 @/api/models 引入:

import { getProblemController } from '@/api/problem-controller'
import type { ProblemVo, QueryByPage1Params } from '@/api/models'

const page = await getProblemController().queryByPage1({ pageNum, pageSize } as QueryByPage1Params)

src/api 目录由 Orval 生成(clean: true,每次生成会清空重建),不要手动修改。

目录结构

src/
├── api/                 # Orval 生成的接口客户端与数据模型
├── components/          # 公共组件(MonacoEditor)
├── layout/              # 布局:UserLayout(顶部导航)、AdminLayout(侧边菜单)
├── router/              # 路由定义与权限守卫
├── store/               # Pinia:meta(站点配置)、user(登录态)
├── type/                # 手工维护的公共类型
├── util/                # axios-instance、markdown、format-time、submission 状态映射
├── view/
│   ├── admin/           # 后台:题目/分类/比赛/用户管理
│   ├── contest/         # 比赛列表、详情、排名
│   ├── problem/         # 题目列表、详情
│   └── submission/      # 我的提交
├── App.vue
└── main.ts              # 应用入口:注册 Router、Pinia、Element Plus(zh-cn)

路径别名 @ 指向 src(见 vite.config.ts)。

路由与权限

路径 页面 访问权限
/login 登录 公开
/ 首页 公开
/about 关于 公开
/problems 题目列表 公开
/problems/:id 题目详情 公开(提交需登录)
/contests 比赛列表 公开
/contests/:id 比赛详情 公开(报名需登录)
/contests/:id/ranking 比赛排名 公开
/submissions 我的提交 公开(仅展示本人记录)
/admin/problems 题目管理 admin / coach
/admin/categories 分类管理 admin / coach
/admin/contests 比赛管理 admin / coach
/admin/users 用户管理 admin

权限通过路由 meta 声明,由 router.beforeEach 统一守卫:

  • meta.requiresAdmin:仅 admin 可访问。
  • meta.requiresCoach:admin 与 coach 均可访问。
  • 未登录时携带 redirect 参数跳转登录页,登录后返回原页面。

布局菜单同样按角色过滤:顶部导航的“后台管理”入口只对 admin/coach 展示,后台侧边菜单按每条路由的 meta 过滤,用户管理仅对 admin 可见。

角色取值:admin(管理员)、coach(教练)、user(普通用户)、guest(未登录)。

状态管理

Pinia 采用 setup 语法 store,并开启 persist 持久化,刷新后登录态与站点配置保留在 localStorage:

  • useLoginUserStore(store/user.ts):loginUser 状态、userId getter、loginSuccess / logoutUser 等 action。
  • useMetaStore(store/meta.ts):站点标题、Logo、后端地址。

判题状态

util/submission.ts 维护提交状态到展示文案的映射(getStatusInfo),覆盖 SUBMITTED、JUDGING、AC、WA、TLE、MLE、RE、CE、SE,并给出对应的 Element Plus 标签类型。

开发约定

  • 时间展示统一使用 util/format-time.ts 的 formatTime()(YYYY-MM-DD HH:mm),无效值回退为 -。
  • Markdown 渲染统一使用 util/markdown.ts 导出的异步组件 MdEditor / MdViewer(按需加载,避免首屏引入编辑器体积)。
  • 组件使用 <script setup lang="ts">,路径引用使用 @/ 别名。
  • 提交代码前先跑 npm run build(含 vue-tsc 严格类型检查)。