Jelajahi Sumber

doc:更新readme文件

yangyi 1 bulan lalu
induk
melakukan
38d1795a4c
1 mengubah file dengan 195 tambahan dan 234 penghapusan
  1. 195 234
      README.md

+ 195 - 234
README.md

@@ -1,237 +1,198 @@
-# Vue 3 + TypeScript + Vite
+# OJ 在线判题系统 · 前端
 
-This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+基于 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 最大耗时。
+- **我的提交**:分页表格展示提交记录,展开行可查看提交的代码与判题详情。
+- **账号**:登录/退出,用户信息与登录态持久化。
+
+### 管理后台
 
-Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
+- **题目管理**:题目 CRUD,维护分类勾选,详情弹窗管理样例与评测用例(含隐藏标记)。
+- **分类管理**:题目分类 CRUD。
+- **比赛管理**:比赛 CRUD,配置赛题(多选题目 + 显示顺序 + 分值),查看报名名单。
+- **用户管理**:用户分页查询、状态与角色管理、改密与头像。
+
+## 快速开始
+
+### 环境要求
+
+- Node.js 18+
+- 后端服务运行在 `http://localhost:8080`(提供 `/v3/api-docs` OpenAPI 文档)
+
+### 安装与运行
+
+```shell
+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`:
+
+```ts
+// 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` 视为成功;成功且带 `message` 时弹出 `ElMessage.success`,失败时弹出后端返回的 `message` 并 reject。
+- HTTP `401` 统一跳转登录页。
+
+视图层因此不需要各自处理错误提示,只需 `try/catch` 处理业务失败分支。
+
+## 接口代码生成
+
+Orval 读取后端 OpenAPI 文档,按标签拆分为控制器文件,类型输出到 `src/api/models`,并使用自定义 mutator 走统一的 Axios 实例:
+
+```ts
+// 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 与枚举
+```
 
-# 前端模板项目
-
-> node18
->
-> Vue 3 + TypeScript + Vite6
-
-## 构建过程
-
-1. 项目初始化
-
-   ```shell
-   npm create vite@6
-   ```
-
-   
-
-2. 添加vue-router和elementPlus依赖
-
-   - 安装
-
-   ```shell
-   npm install vue-router@4
-   npm install element-plus --save
-   ```
-
-   - 配置
-
-   ```typescript
-   const routes:RouteRecordRaw[] = [
-       {
-           path: '/',
-           name: 'commonLayout',
-           component: CommonLayout,
-           children: [
-               {
-                   path: '/',
-                   name: 'home',
-                   component: HomeView
-               },
-               {
-                   path: '/about',
-                   name: 'about',
-                   component: AboutView
-               },
-           ],
-       },
-   ]
-   
-   const router : Router = createRouter({
-       history: createWebHistory(),
-       routes: routes,
-   });
-   export default router
-   ```
-
-   
-
-   - 使用
-
-   ```typescript
-   app.use(router)
-   app.use(ElementPlus)
-   ```
-
-   
-
-3. 添加状态管理依赖
-
-   - 安装
-
-   ```shell
-   npm install pinia
-   ```
-
-   - 配置
-
-   ```
-   
-   ```
-
-   
-
-   - 使用
-
-   ```typescript
-   import { createPinia } from 'pinia'
-   const pinia = createPinia()
-   app.use(pinia)
-   ```
-
-   
-
-4. 添加axios依赖
-
-   ```shell
-   npm install axios
-   ```
-
-   配置axios-instance.ts
-
-   ```typescript
-   import axios, {type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios';
-   
-   // 自定义配置接口
-   export interface CustomInstanceConfig extends AxiosRequestConfig {
-       // 可以添加自定义配置
-       skipAuth?: boolean; // 是否跳过鉴权
-   }
-   
-   // 创建 Axios 实例
-   const axiosInstance: AxiosInstance = axios.create({
-       baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
-       timeout: 30000,
-       headers: {
-           'Content-Type': 'application/json',
-       },
-   });
-   
-   // 请求拦截器
-   axiosInstance.interceptors.request.use(
-       (config) => {
-           // 添加 token
-           const token = localStorage.getItem('token');
-           if (token) {
-               config.headers.Authorization = `Bearer ${token}`;
-           }
-           return config;
-       },
-       (error) => {
-           return Promise.reject(error);
-       }
-   );
-   
-   // 响应拦截器
-   axiosInstance.interceptors.response.use(
-       (response: AxiosResponse) => {
-           // 根据后端返回格式调整
-           if (response.data.code !== 0) {
-               return Promise.reject(new Error(response.data.message || '请求失败'));
-           }
-           return response.data;
-       },
-       (error) => {
-           // 统一错误处理
-           if (error.response?.status === 401) {
-               // 未授权,跳转登录
-               window.location.href = '/login';
-           }
-           return Promise.reject(error);
-       }
-   );
-   
-   // 自定义实例函数(供 Orval 使用)
-   export const customAxiosInstance = <T>(config: CustomInstanceConfig): Promise<T> => {
-       return axiosInstance(config);
-   };
-   
-   // 也可以导出第二参数形式(如需处理完整响应)
-   export const customAxiosInstanceWithFullResponse = <T>(
-       config: CustomInstanceConfig
-   ): Promise<AxiosResponse<T>> => {
-       return axiosInstance(config);
-   };
-   ```
-
-   
-
-5. openAPI自动生成接口调用
-
-   使用orval进行API生成
-
-   ```shell
-   npm install orval
-   ```
-
-   编写orval.config.ts配置文件
-
-   ```typescript
-   import { defineConfig } from 'orval';
-   
-   export default defineConfig({
-       'your-api': {
-           // OpenAPI 规范文件路径(支持本地文件或 URL)
-           input: {
-               target: 'http://localhost:8080/v3/api-docs', // 可改为你的 Swagger JSON 地址,如:http://localhost:8000/openapi.json
-           },
-           output: {
-               // 生成代码的模式:tags-按接口标签拆分文件
-               mode: 'tags',
-               // 客户端类型:使用 axios
-               client: 'axios',
-               // 生成 vue-query hooks
-               target: './src/api/client.ts',
-               // 生成的 TypeScript 类型存放目录
-               schemas: './src/api/models',
-               // 每次生成前自动清理旧文件
-               clean: true,
-               // 覆盖默认配置
-               override: {
-                   // 自定义 axios 实例
-                   mutator: {
-                       path: './src/util/axios-instance.ts',
-                       name: 'customAxiosInstance',
-                   },
-                   // 为每个接口生成 vue-query hooks
-                   query: {
-                       useQuery: true,      // 生成 useGetXXX hooks
-                       useMutation: true,   // 生成 usePostXXX hooks
-                   },
-               },
-           },
-           // 生成后自动格式化代码
-           hooks: {
-               afterAllFilesWrite: 'prettier --write',
-           },
-       },
-   });
-   ```
-
-   配置package.json文件中
-
-   ```json
-   {
-     "scripts": {
-      	...,
-       "api:gen": "orval"
-     },
-   }
-   ```
-
-   
+视图层按控制器调用接口,类型统一从 `@/api/models` 引入:
+
+```ts
+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` 严格类型检查)。