戰(zhàn):從Vue技術(shù)棧到用戶體驗(yàn)全面升級(jí))
簡(jiǎn)介ZHZXOJ-Web 是一套基于 SYZOJ 前端改版的在線評(píng)測(cè)系統(tǒng)OJ前端項(xiàng)目主要由 JavaScript 編寫適合 Web 前端開發(fā)者、OJ 二次開發(fā)愛好者參考。雖然作者已停止維護(hù)但代碼中保留了前端頁面結(jié)構(gòu)與 Nginx 啟用 HTTPS 的配置片段可作為搭建或改造評(píng)測(cè)系統(tǒng)時(shí)的備查素材。包內(nèi)共 524 個(gè)文件壓縮包約 12.48MB。其中 211 個(gè) js、105 個(gè) css 和 49 個(gè) ejs 覆蓋交互邏輯、樣式布局與模板渲染另有 ts、json、svg 及多種字體文件支撐前端工程化構(gòu)建與圖標(biāo)字體顯示整體結(jié)構(gòu)清晰便于按需查看核心模塊。已有 209 人瀏覽學(xué)習(xí)。對(duì)想快速了解 SYZOJ 前端結(jié)構(gòu)、或準(zhǔn)備在 OJ 前臺(tái)上做二次開發(fā)的讀者可直接從這套代碼中提取 Nginx 配置思路與前端頁面設(shè)計(jì)節(jié)省從零搭建環(huán)境與排查 HTTPS 問題的精力。 接手ZHZXOJ-Web之前我對(duì)SYZOJ這個(gè)開源在線評(píng)測(cè)系統(tǒng)Online Judge并不陌生——高校ACM集訓(xùn)隊(duì)、算法訓(xùn)練營、課程實(shí)驗(yàn)平臺(tái)里到處能看到它的身影。但說實(shí)話SYZOJ默認(rèn)前端給我最直觀的感受是“功能齊全但視覺和交互還停留在工具型階段”尤其是題目列表、提交記錄和個(gè)人中心這幾個(gè)高頻頁面在2025年的Web環(huán)境下已經(jīng)有點(diǎn)跟不上用戶預(yù)期了。ZHZXOJ-Web這個(gè)項(xiàng)目做的就是基于SYZOJ前端的系統(tǒng)性改版不動(dòng)后端評(píng)測(cè)核心只把前端體驗(yàn)徹底重做一遍。這篇文章就把我改版過程中的完整思路、實(shí)操細(xì)節(jié)、踩坑記錄都整理出來給準(zhǔn)備做OJ二次開發(fā)或者接手SYZOJ、HustOJ這類開源項(xiàng)目前端的朋友一個(gè)可參考的路線圖。1. 改版前先看清楚底子SYZOJ前端架構(gòu)拆解1.1 核心依賴與技術(shù)棧動(dòng)手改版之前我花了兩天時(shí)間把SYZOJ的源碼倉庫完整過了一遍尤其是前端目錄。這里要強(qiáng)調(diào)一點(diǎn)做二次開發(fā)最忌諱上來就改你得先摸清項(xiàng)目底子是什么、為什么這樣設(shè)計(jì)后面改起來才不會(huì)踩雷。SYZOJ的前端是典型的Vue中后臺(tái)技術(shù)棧我這邊拉到的版本核心依賴大概是這樣的依賴版本區(qū)間負(fù)責(zé)什么Vue2.6.x基礎(chǔ)框架組件化開發(fā)Vue Router3.x前端路由控制頁面跳轉(zhuǎn)Vuex3.x全局狀態(tài)管理存用戶信息、題目列表緩存等Element UI2.15.x桌面端組件庫表格、彈窗、表單就靠它axios0.21.xHTTP請(qǐng)求和后端API通信CodeMirror5.x代碼編輯器支持多語言語法高亮marked / katex對(duì)應(yīng)版本題目描述Markdown渲染 數(shù)學(xué)公式解析這個(gè)組合在當(dāng)年的開源OJ生態(tài)里非常典型選擇它的理由也很務(wù)實(shí)Vue 2 Element UI 能快速搭出后臺(tái)管理系統(tǒng)風(fēng)格的界面組件覆蓋度高社區(qū)資料多學(xué)校實(shí)驗(yàn)室或者個(gè)人開發(fā)者接手成本低。代價(jià)就是視覺風(fēng)格偏“中后臺(tái)”默認(rèn)的藍(lán)白主題、表格密度、彈窗交互和現(xiàn)在流行的極簡(jiǎn)主義技術(shù)社區(qū)風(fēng)確實(shí)有差距——這正是我們改版的核心切入點(diǎn)。1.2 目錄結(jié)構(gòu)與前端數(shù)據(jù)流SYZOJ前端的目錄結(jié)構(gòu)是標(biāo)準(zhǔn)的Vue CLI工程布局這里我列一個(gè)精簡(jiǎn)版src/ ├── api/ // 所有后端接口封裝按模塊拆文件 ├── router/ // 路由配置含動(dòng)態(tài)路由邏輯 ├── store/ // Vuex模塊處理用戶態(tài)、評(píng)測(cè)狀態(tài)等 ├── views/ // 頁面組件登錄、題目、提交記錄、管理后臺(tái) ├── components/ // 公共組件如Markdown渲染、代碼編輯器封裝 └── utils/ // 工具函數(shù)axios封裝、token操作等理解這個(gè)項(xiàng)目的數(shù)據(jù)流比記住目錄結(jié)構(gòu)更重要。整個(gè)OJ前端的數(shù)據(jù)流其實(shí)是一條線用戶登錄后拿到tokenaxios請(qǐng)求攔截器給每個(gè)請(qǐng)求帶Authorization頭后端返回題目列表、題解、排行、提交記錄用戶提交代碼后前端提交一次POST /api/submission拿到 submission id然后通過輪詢接口查狀態(tài)直到評(píng)測(cè)完成。整個(gè)鏈路不復(fù)雜但改版過程中涉及到的權(quán)限控制、狀態(tài)刷新、接口字段適配全都圍繞這條線展開。2. 改版整體設(shè)計(jì)改什么、不改什么、先后順序定清楚2.1 改版目標(biāo)與風(fēng)格定位拿到項(xiàng)目我做的第一件事不是寫代碼而是拉了一份需求清單把“必須改”“可以改”“堅(jiān)決不動(dòng)”三部分分開。必須改的是用戶感知最強(qiáng)的地方首頁布局、題目列表展示形態(tài)、代碼編輯器配色、提交結(jié)果反饋動(dòng)效。這些直接決定用戶打開網(wǎng)站的第一印象。可以改的是暗色模式、移動(dòng)端適配、管理后臺(tái)的表格密度調(diào)整。堅(jiān)決不動(dòng)的是后端接口字段、登錄鑒權(quán)邏輯、評(píng)測(cè)狀態(tài)機(jī)——前兩者動(dòng)了就要和后端聯(lián)調(diào)返工評(píng)測(cè)狀態(tài)機(jī)更是OJ的命脈前端只能展示不能改變定義。風(fēng)格定位上我選擇了偏“極簡(jiǎn)技術(shù)社區(qū)”的路線主色調(diào)從SYZOJ默認(rèn)的深藍(lán)改成冷靜的中性深灰品牌點(diǎn)綴色卡片化布局代替?zhèn)鹘y(tǒng)的通欄表格留白加大字體用系統(tǒng)默認(rèn)字體棧保證不管在什么設(shè)備上打開都干凈利落。2.2 功能模塊優(yōu)先級(jí)劃分改版范圍確認(rèn)后我把所有改動(dòng)項(xiàng)排了個(gè)優(yōu)先級(jí)核心原則是先保證“能正常用”再追求“用得爽”。優(yōu)先級(jí)模塊改動(dòng)內(nèi)容P0登錄/注冊(cè)、路由權(quán)限保持邏輯不變只更新UI樣式和校驗(yàn)反饋P0題目列表與題目詳情重構(gòu)列表為卡片/列表雙形態(tài)Markdown渲染升級(jí)P0評(píng)測(cè)流程提交按鈕、等待動(dòng)畫、結(jié)果狀態(tài)展示重做P1個(gè)人中心與提交歷史新增統(tǒng)計(jì)卡片優(yōu)化長列表展示P1排行榜表格視覺瘦身新增按題目/按通過數(shù)篩選P2暗色模式、移動(dòng)端響應(yīng)式全站CSS變量化適配小屏我個(gè)人強(qiáng)烈建議排序時(shí)把“評(píng)測(cè)流程”放在很高的優(yōu)先級(jí)OJ的本質(zhì)是“提交流程順暢”如果用戶寫完代碼點(diǎn)提交后按鈕狀態(tài)、等待反饋、結(jié)果展示任何一個(gè)環(huán)節(jié)體驗(yàn)拉胯前端做得再好看也沒有用。這個(gè)思路適用于所有類似工具型產(chǎn)品的改版。3. 核心模塊改版實(shí)操從登錄到評(píng)測(cè)全鏈路3.1 登錄態(tài)與權(quán)限路由改造SYZOJ的權(quán)限模型分普通用戶、管理員、超級(jí)管理員三層路由配置里用meta.role標(biāo)注了每個(gè)頁面的可見權(quán)限。改版時(shí)我保留了這套模型但把路由守衛(wèi)的邏輯重寫得更健壯。先看下改造后的核心邏輯我用的是Vue Router的beforeEach守衛(wèi)加動(dòng)態(tài)路由注冊(cè)的組合拳// src/router/index.js 核心片段 const whiteList [/login, /register, /problem/list, /home] router.beforeEach((to, from, next) { const token store.getters.token if (token) { if (to.path /login) { next({ path: / }) } else if (store.getters.roles.length 0) { // 刷新頁面時(shí)用戶信息丟失先拉取再放行 store.dispatch(fetchUserInfo).then(() { store.dispatch(generateRoutes).then(accessRoutes { router.addRoutes(accessRoutes) next({ ...to, replace: true }) }) }).catch(() { store.dispatch(logout) next(/login?redirect${to.path}) }) } else { next() } } else { if (whiteList.includes(to.path)) next() else next(/login?redirect${to.path}) } })這里最關(guān)鍵也最容易踩坑的是“刷新頁面后路由丟失”。因?yàn)閯?dòng)態(tài)路由是登錄后addRoutes掛上去的F5一刷新Vuex狀態(tài)清空動(dòng)態(tài)路由就沒了直接跳404。解決辦法就是上面代碼里注釋那一行刷新后先調(diào)fetchUserInfo根據(jù)角色重新生成路由再放行。這個(gè)方案我實(shí)測(cè)穩(wěn)定但要注意generateRoutes必須返回Promise否則then里拿不到路由數(shù)組。3.2 題目列表與Markdown渲染優(yōu)化題目列表頁是OJ流量最大的頁面之一。SYZOJ默認(rèn)是傳統(tǒng)表格視圖每一行一個(gè)題目字段是編號(hào)、標(biāo)題、通過率、難度。對(duì)于體驗(yàn)要求高的站點(diǎn)這種形態(tài)太素了。我的做法是提供“表格/卡片”雙視圖切換默認(rèn)展示卡片視圖每張卡片突出題目標(biāo)題右上角標(biāo)注難度色塊入門綠色、提高橙色、困難紅色下方展示通過率進(jìn)度條和提交人數(shù)。切換按鈕放在頁面右上角用localStorage記憶用戶偏好下次進(jìn)來還是上次的視圖。題目詳情的Markdown渲染是另一個(gè)重點(diǎn)。SYZOJ的題面包含大量公式、圖表和代碼塊默認(rèn)渲染方案是marked katex但直接v-html輸出存在兩個(gè)問題XSS注入風(fēng)險(xiǎn)、代碼高亮缺失。我的渲染管線長這樣import { marked } from marked import hljs from highlight.js import katex from katex import DOMPurify from dompurify function renderMarkdown(md) { // 先處理公式塊避免marked把$符號(hào)當(dāng)普通文本 let html md.replace(/\$\$([\s\S]?)\$\$/g, (match, tex) { return katex.renderToString(tex, { throwOnError: false, displayMode: true }) }) // 再用marked渲染開啟代碼高亮 html marked.parse(html, { highlight: code hljs.highlightAuto(code).value }) // 最后統(tǒng)一過白名單過濾 return DOMPurify.sanitize(html) }順序很重要先處理公式再轉(zhuǎn)Markdown最后sanitize——如果顛倒過來katex.renderToString生成的HTML會(huì)被Markdown解析器二次轉(zhuǎn)義公式直接亂掉。DOMPurify這一步絕對(duì)不能省OJ題面里有大量用戶生成內(nèi)容不過濾等于給XSS開大門。3.3 代碼編輯器與評(píng)測(cè)流程對(duì)接SYZOJ默認(rèn)用CodeMirror 5我保留了它——CodeMirror 6雖然新但遷移成本高而且5的生態(tài)對(duì)OJ場(chǎng)景覆蓋得很完整。你要做的不是換編輯器而是把交互體驗(yàn)做細(xì)。編輯器改造重點(diǎn)有三個(gè)第一語言模式自動(dòng)切換。題目詳情接口會(huì)返回languages字段用戶選題號(hào)后直接根據(jù)默認(rèn)語言創(chuàng)建編輯器實(shí)例。CodeMirror的mode需要映射表const langToMode { cpp: text/x-csrc, c: text/x-csrc, java: text/x-java, python: text/x-python, javascript: text/javascript }第二快捷鍵提交流程。我綁定了Ctrl/Cmd Enter直接提交代碼并且在提交前做二次確認(rèn)彈窗。這個(gè)彈窗看起來是小事實(shí)際上對(duì)防止手誤點(diǎn)錯(cuò)題號(hào)提交幫助巨大。第三提交后的狀態(tài)輪詢。這個(gè)是OJ前端的靈魂。提交成功后前端開始以1秒為間隔輪詢?cè)u(píng)測(cè)結(jié)果接口比對(duì)狀態(tài)碼映射成中文提示const statusMap { 0: 等待評(píng)測(cè), 1: 正在編譯, 2: 編譯失敗, 3: 運(yùn)行中, 4: 通過 (AC), 5: 答案錯(cuò)誤 (WA), 6: 時(shí)間超限 (TLE), 7: 內(nèi)存超限 (MLE), 8: 運(yùn)行時(shí)錯(cuò)誤 (RE), 9: 系統(tǒng)錯(cuò)誤 }輪詢不能無限跑我加了一個(gè)30次的上限大約30秒超過后提示用戶刷新查看結(jié)果。實(shí)際評(píng)測(cè)快的題兩三秒就出結(jié)果30秒足夠覆蓋絕大多數(shù)情況而且避免了極端情況下的請(qǐng)求堆積。4. 暗色模式、性能優(yōu)化與加餐功能4.1 全站CSS變量化與暗色模式P2級(jí)別的暗色模式我放在了基礎(chǔ)功能穩(wěn)定之后做。SYZOJ原版大量使用Element UI默認(rèn)主題色直接寫死成#409EFF想支持暗色模式必須先把顏色抽成CSP變量。做法是在全局樣式文件里定義一套語義化變量:root { --app-bg: #f5f6fa; --card-bg: #ffffff; --text-primary: #2c3e50; --text-secondary: #7f8c9b; --border-color: #e4e7ed; --accent-color: #3861fb; } [data-themedark] { --app-bg: #1e1f22; --card-bg: #2b2d31; --text-primary: #e0e0e0; --text-secondary: #9aa0aa; --border-color: #4b4d52; --accent-color: #6c8cff; }Element UI 的主題變量通過element-variables.scss覆蓋切換暗色模式時(shí)給document.documentElement設(shè)置>devServer: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true, pathRewrite: { ^/api: } } } }難的是之前有人直接在前端代碼里寫死了后端地址導(dǎo)致生產(chǎn)環(huán)境也走代理邏輯上線后接口全掛。改版時(shí)我統(tǒng)一改成讀取環(huán)境變量VUE_APP_API_BASE徹底解決這個(gè)問題。第二個(gè)坑是Markdown渲染的XSS漏洞。原版代碼直接v-html渲染用戶提交的題解我測(cè)試的時(shí)候用一段包含img onerroralert(1)的題解直接彈窗了。這個(gè)在高危漏洞里屬于典型存儲(chǔ)型XSS如果OJ被寫惡意題解的人盯上所有瀏覽題解的用戶都可能中招。DOMPurify是必須上的而且要在渲染管線的最后一步調(diào)用。第三個(gè)坑是輪詢接口的請(qǐng)求堆積。用戶連續(xù)提交多次代碼時(shí)如果每次提交都開一個(gè)獨(dú)立的輪詢定時(shí)器頁面一卡定時(shí)器全擠在一起發(fā)請(qǐng)求接口壓力直接翻倍。我的解法是全局只保留一個(gè)輪詢隊(duì)列新的提交進(jìn)來先取消舊的定時(shí)器確保同一時(shí)刻最多只有一個(gè)活躍輪詢循環(huán)。寫在最后的實(shí)際體會(huì)ZHZXOJ-Web這個(gè)項(xiàng)目做下來最大的感觸是開源項(xiàng)目二次開發(fā)真正難的不是寫代碼而是克制——知道哪些地方該大改哪些地方該保持原樣。SYZOJ的評(píng)測(cè)邏輯、權(quán)限模型、接口設(shè)計(jì)是多年積累沉淀下來的輕易改動(dòng)只會(huì)給自己挖坑。前端改版的價(jià)值恰恰在于不改邏輯也能讓一個(gè)老系統(tǒng)的體驗(yàn)煥然一新。如果你正準(zhǔn)備改版一個(gè)類似的開源項(xiàng)目我的建議是先把接口文檔和權(quán)限模型啃透再談視覺升級(jí)——地基穩(wěn)了上面怎么裝修都行。另外就是暗色模式和移動(dòng)端適配盡量提前規(guī)劃不要等頁面全寫完了再回頭補(bǔ)成本完全不是一個(gè)量級(jí)。本文還有配套的精品資源點(diǎn)擊獲取