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