Compare commits

..
5 Commits
Author SHA1 Message Date
xuyue dd29f04664 change to vite8
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
2026-07-02 18:28:47 -06:00
xuyueandClaude Fable 5 5d418a2280 修复测试用例区域温馨提醒 tooltip 不换行
n-tooltip 的内容容器默认按内容宽度撑开(naive-ui 没有设默认 max-width/
white-space),SQL 提示文案变长后整段挤成一条超宽横线。加 max-width 约束
宽度使其正常换行。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:02:49 -06:00
xuyueandClaude Fable 5 b4ac42b736 出题表单:修复 SQL 题崩溃并隐藏流程图区域
流程图区域的"AI 生成"按钮读取 problem.answers 里 language==='Python3' 的
第一项 .code,SQL 题(languages=["SQL"])没有 Python3 答案,[0] 为
undefined,导致模板抛 TypeError 崩溃整个出题页。

流程图功能本身依赖 Python 答案生成 Mermaid 代码,对 SQL 题没有意义,
和已隐藏的"代码规则检查"一样直接整块隐藏(切到 SQL 时顺带重置
allow_flowchart/show_flowchart,避免脏数据)。顺手给禁用态判断加了
可选链,修掉未选 Python3 时同样会崩的潜在问题(如仅选 C/C++ 的题目)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:45:44 -06:00
xuyueandClaude Fable 5 c227c1d506 出题表单:SQL 题隐藏代码规则检查
AstRulesEditor 基于 tree-sitter 对 Python/C 做 AST 解析(后端 ast_checker
只注册了这两种语言),对 SQL 没有意义;此前只要 SQL 在已选语言里就会渲染
出对应 tab,选项全是 for/while 循环等编程语言概念。切到 SQL 时隐藏该区块
并清空 ast_rules,避免出题界面出现无意义配置。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:41:50 -06:00
xuyueandClaude Fable 5 38500671af 新增 SQL 题型前端支持
- LANGUAGE 类型与六张语言映射表补 SQL;编辑器 SQLite 方言高亮
- 出题表单:SQL 语言选项、题型/行序配置卡片、数据脚本压缩包上传、标准答案必填校验
- 修复做题页语言回退硬编码 Python3 的问题(SQL 题会提交被拒)
- SQL 题隐藏自测猫/自测屏/复制到自测猫(外部运行器不支持 SQL)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:23:23 -06:00
69 changed files with 3221 additions and 1081 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
**ojnext** is the frontend for an Online Judge platform. Built with Vue 3 + TypeScript using Rsbuild (Rust-based bundler), Naive UI component library, Pinia for state management, and Vue Router. **ojnext** is the frontend for an Online Judge platform. Built with Vue 3 + TypeScript using Vite (Rolldown-based bundler), Naive UI component library, Pinia for state management, and Vue Router.
## Commands ## Commands
+1 -8
View File
@@ -9,16 +9,9 @@
<script> <script>
window.localStorage.setItem("maxkbMaskTip", true) window.localStorage.setItem("maxkbMaskTip", true)
</script> </script>
<% if (process.env.PUBLIC_MAXKB_URL) { %>
<script
async
defer
src="<%= process.env.PUBLIC_MAXKB_URL %>"
></script>
<% } %>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+2840 -831
View File
File diff suppressed because it is too large Load Diff
+19 -17
View File
@@ -1,18 +1,19 @@
{ {
"name": "oj-next", "name": "oj-next",
"version": "1.8.0", "version": "1.9.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "rsbuild dev", "start": "vite",
"build": "rsbuild build", "build": "vite build",
"build:staging": "rsbuild build --env-mode=staging", "build:staging": "vite build --mode staging",
"build:test": "rsbuild build --env-mode=test", "build:test": "vite build --mode test",
"fmt": "prettier --write src *.ts" "fmt": "prettier --write src *.ts"
}, },
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.20.3", "@codemirror/autocomplete": "^6.20.3",
"@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-python": "^6.2.1", "@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-sql": "^6.10.0",
"@vue-flow/background": "^1.3.2", "@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.3", "@vue-flow/controls": "^1.1.3",
"@vue-flow/core": "^1.48.2", "@vue-flow/core": "^1.48.2",
@@ -21,9 +22,9 @@
"@vue-flow/node-toolbar": "^1.1.1", "@vue-flow/node-toolbar": "^1.1.1",
"@vueuse/core": "^14.3.0", "@vueuse/core": "^14.3.0",
"@vueuse/router": "^14.3.0", "@vueuse/router": "^14.3.0",
"@wangeditor-next/editor": "^5.7.12", "@wangeditor-next/editor": "^5.7.13",
"@wangeditor-next/editor-for-vue": "^5.1.14", "@wangeditor-next/editor-for-vue": "^5.1.14",
"axios": "^1.18.0", "axios": "^1.18.1",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"chartjs-chart-wordcloud": "^4.4.5", "chartjs-chart-wordcloud": "^4.4.5",
@@ -33,15 +34,15 @@
"date-fns": "^4.4.0", "date-fns": "^4.4.0",
"fflate": "^0.8.3", "fflate": "^0.8.3",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"md-editor-v3": "^6.5.1", "md-editor-v3": "^6.5.3",
"mermaid": "^11.15.0", "mermaid": "^11.16.0",
"mermaid-legacy": "npm:mermaid@^9.1.7", "mermaid-legacy": "npm:mermaid@^9.4.3",
"naive-ui": "^2.44.1", "naive-ui": "^2.44.1",
"nanoid": "^5.1.15", "nanoid": "^5.1.16",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"skulpt": "^1.2.0", "skulpt": "^1.2.0",
"vue": "^3.5.38", "vue": "^3.5.39",
"vue-chartjs": "^5.3.3", "vue-chartjs": "^5.3.3",
"vue-codemirror": "^6.1.1", "vue-codemirror": "^6.1.1",
"vue-router": "^5.1.0", "vue-router": "^5.1.0",
@@ -51,14 +52,15 @@
}, },
"devDependencies": { "devDependencies": {
"@iconify/vue": "^5.0.1", "@iconify/vue": "^5.0.1",
"@rsbuild/core": "^1.7.5",
"@rsbuild/plugin-vue": "^1.2.9",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"@types/node": "^26.0.0", "@types/node": "^26.1.0",
"@vitejs/plugin-legacy": "^8.1.0",
"@vitejs/plugin-vue": "^6.0.7",
"@vue/tsconfig": "^0.9.1", "@vue/tsconfig": "^0.9.1",
"prettier": "^3.8.4", "prettier": "^3.9.4",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"unplugin-auto-import": "^21.0.0", "unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^32.1.0" "unplugin-vue-components": "^32.1.0",
"vite": "^8.1.3"
} }
} }
-104
View File
@@ -1,104 +0,0 @@
import { defineConfig, loadEnv } from "@rsbuild/core"
import { pluginVue } from "@rsbuild/plugin-vue"
import AutoImport from "unplugin-auto-import/rspack"
import Components from "unplugin-vue-components/rspack"
import { NaiveUiResolver } from "unplugin-vue-components/resolvers"
const config: ReturnType<typeof defineConfig> = defineConfig(({ envMode }) => {
const { publicVars, rawPublicVars } = loadEnv({
cwd: process.cwd(),
mode: envMode,
})
const proxyConfig = {
target: rawPublicVars["PUBLIC_OJ_URL"],
changeOrigin: true,
}
const wsProxyConfig = {
target: rawPublicVars["PUBLIC_WS_URL"],
ws: true,
changeOrigin: true,
}
return {
plugins: [pluginVue()],
tools: {
rspack: {
plugins: [
AutoImport({
imports: [
"vue",
"vue-router",
"@vueuse/core",
"pinia",
{
"naive-ui": [
"useDialog",
"useMessage",
"useNotification",
"useLoadingBar",
],
},
{
from: "naive-ui",
imports: [
"DataTableColumn",
"FormRules",
"FormItemRule",
"SelectOption",
"UploadCustomRequestOptions",
"UploadFileInfo",
"MenuOption",
"DropdownDividerOption",
"DropdownOption",
],
type: true,
},
],
dts: "./src/auto-imports.d.ts",
}),
Components({
resolvers: [NaiveUiResolver()],
dts: "./src/components.d.ts",
}),
],
},
},
html: {
template: "./index.html",
},
source: {
entry: {
index: "./src/main.ts",
},
define: publicVars,
},
output: {
polyfill: "usage",
},
performance: {
chunkSplit: {
strategy: "split-by-module",
},
},
resolve: {
alias: {
utils: "./src/utils",
oj: "./src/oj",
admin: "./src/admin",
shared: "./src/shared",
},
},
server: {
port: 5173,
proxy: {
"/api": proxyConfig,
"/public": proxyConfig,
"/ws": wsProxyConfig,
},
},
}
})
export default config
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import TextEditor from "shared/components/TextEditor.vue" import TextEditor from "shared/components/TextEditor.vue"
import { AnnouncementEdit } from "utils/types" import type { AnnouncementEdit } from "utils/types"
import { createAnnouncement, editAnnouncement, getAnnouncement } from "../api" import { createAnnouncement, editAnnouncement, getAnnouncement } from "../api"
interface Props { interface Props {
+1 -1
View File
@@ -2,7 +2,7 @@
import { NSwitch } from "naive-ui" import { NSwitch } from "naive-ui"
import Pagination from "shared/components/Pagination.vue" import Pagination from "shared/components/Pagination.vue"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { Announcement } from "utils/types" import type { Announcement } from "utils/types"
import { editAnnouncement, getAnnouncementList } from "../api" import { editAnnouncement, getAnnouncementList } from "../api"
import Actions from "./components/Actions.vue" import Actions from "./components/Actions.vue"
+5 -2
View File
@@ -133,10 +133,13 @@ export async function uploadImage(file: File): Promise<string> {
return res.success ? res.file_path : "" return res.success ? res.file_path : ""
} }
// 上传测试用例 // 上传测试用例SQL 题的压缩包是 1.sql..N.sql(每个文件一个测试点的建表+数据脚本)
export function uploadTestcases(file: File) { export function uploadTestcases(file: File, options: { sql?: boolean } = {}) {
const form = new window.FormData() const form = new window.FormData()
form.append("file", file) form.append("file", file)
if (options.sql) {
form.append("sql", "1")
}
return http.post<TestcaseUploadedReturns>("admin/test_case", form, { return http.post<TestcaseUploadedReturns>("admin/test_case", form, {
headers: { "content-type": "multipart/form-data" }, headers: { "content-type": "multipart/form-data" },
}) })
+1 -1
View File
@@ -20,7 +20,7 @@
import { NButton } from "naive-ui" import { NButton } from "naive-ui"
import Pagination from "shared/components/Pagination.vue" import Pagination from "shared/components/Pagination.vue"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { Comment } from "utils/types" import type { Comment } from "utils/types"
import { getCommentList } from "../api" import { getCommentList } from "../api"
import CommentActions from "./components/CommentActions.vue" import CommentActions from "./components/CommentActions.vue"
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Contest } from "utils/types" import type { Contest } from "utils/types"
import { cloneContest } from "../../api" import { cloneContest } from "../../api"
interface Props { interface Props {
+100 -9
View File
@@ -10,7 +10,13 @@ import {
} from "utils/constants" } from "utils/constants"
import download from "utils/download" import download from "utils/download"
import { unique } from "utils/functions" import { unique } from "utils/functions"
import type { BlankProblem, LANGUAGE, Tag, Testcase } from "utils/types" import type {
BlankProblem,
LANGUAGE,
SQLConfig,
Tag,
Testcase,
} from "utils/types"
import { import {
createContestProblem, createContestProblem,
createProblem, createProblem,
@@ -89,6 +95,7 @@ const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
flowchart_hint: "", flowchart_hint: "",
show_flowchart: false, show_flowchart: false,
ast_rules: null as { [key: string]: any[] } | null, ast_rules: null as { [key: string]: any[] } | null,
sql_config: null as SQLConfig | null,
}) })
// 从服务器来的tag列表 // 从服务器来的tag列表
@@ -171,8 +178,40 @@ const languageOptions = [
{ label: LANGUAGE_SHOW_VALUE["Python3"], value: "Python3" }, { label: LANGUAGE_SHOW_VALUE["Python3"], value: "Python3" },
{ label: LANGUAGE_SHOW_VALUE["C"], value: "C" }, { label: LANGUAGE_SHOW_VALUE["C"], value: "C" },
{ label: LANGUAGE_SHOW_VALUE["C++"], value: "C++" }, { label: LANGUAGE_SHOW_VALUE["C++"], value: "C++" },
{ label: LANGUAGE_SHOW_VALUE["SQL"], value: "SQL" },
] ]
const isSQLProblem = computed(() => !!problem.value?.languages.includes("SQL"))
// SQL 题联动:SQL 必须是唯一语言(后端强校验),不需要预制代码,自动初始化 sql_config
watch(
() => problem.value?.languages,
(langs) => {
if (!langs) return
if (langs.includes("SQL")) {
if (langs.length > 1) {
problem.value.languages = ["SQL"]
return
}
needTemplate.value = false
if (!problem.value.sql_config) {
problem.value.sql_config = { mode: "query", order_sensitive: false }
}
currentActiveAnswer.value = "SQL"
// 代码规则检查基于 Python/C 的 AST 解析,对 SQL 没有意义,清空避免脏数据
if (problem.value.ast_rules) {
problem.value.ast_rules = null
}
// 流程图依赖 Python 答案生成,对 SQL 没有意义
problem.value.allow_flowchart = false
problem.value.show_flowchart = false
} else if (problem.value.sql_config) {
problem.value.sql_config = null
}
},
{ immediate: true },
)
async function getProblemDetail() { async function getProblemDetail() {
if (!props.problemID) { if (!props.problemID) {
syncTagInputsFromProblemTags() syncTagInputsFromProblemTags()
@@ -211,6 +250,7 @@ async function getProblemDetail() {
problem.value.flowchart_hint = data.flowchart_hint ?? "" problem.value.flowchart_hint = data.flowchart_hint ?? ""
problem.value.flowchart_data = data.flowchart_data problem.value.flowchart_data = data.flowchart_data
problem.value.ast_rules = data.ast_rules ?? null problem.value.ast_rules = data.ast_rules ?? null
problem.value.sql_config = data.sql_config ?? null
if (data.answers && data.answers.length) { if (data.answers && data.answers.length) {
problem.value.answers = data.answers problem.value.answers = data.answers
} else { } else {
@@ -262,7 +302,7 @@ function resetTemplate(language: LANGUAGE) {
async function handleUploadTestcases({ file }: UploadCustomRequestOptions) { async function handleUploadTestcases({ file }: UploadCustomRequestOptions) {
try { try {
const res = await uploadTestcases(file.file!) const res = await uploadTestcases(file.file!, { sql: isSQLProblem.value })
// @ts-ignore // @ts-ignore
if (res.error) { if (res.error) {
message.error("上传测试用例失败") message.error("上传测试用例失败")
@@ -332,6 +372,19 @@ async function validateProblem() {
message.error("编程语言没有选择") message.error("编程语言没有选择")
hasErrors = true hasErrors = true
} }
// SQL 题验证
else if (isSQLProblem.value && !problem.value.sql_config?.mode) {
message.error("SQL 题需要选择题型(查询题/增删改题)")
hasErrors = true
} else if (
isSQLProblem.value &&
!problem.value.answers.find(
(ans) => ans.language === "SQL" && ans.code.trim() !== "",
)
) {
message.error("SQL 题必须填写标准答案(判题时用它生成期望结果)")
hasErrors = true
}
// 流程图验证 // 流程图验证
else if (problem.value.show_flowchart || problem.value.allow_flowchart) { else if (problem.value.show_flowchart || problem.value.allow_flowchart) {
if ( if (
@@ -611,7 +664,7 @@ watch(
</n-flex> </n-flex>
</n-checkbox-group> </n-checkbox-group>
</n-form-item> </n-form-item>
<n-form-item> <n-form-item v-if="!isSQLProblem">
<n-checkbox <n-checkbox
v-model:checked="needTemplate" v-model:checked="needTemplate"
label="预制代码(显示在编辑器中,帮助快速上手)" label="预制代码(显示在编辑器中,帮助快速上手)"
@@ -630,10 +683,37 @@ watch(
</n-form-item> </n-form-item>
</n-form> </n-form>
<n-form
v-if="isSQLProblem && problem.sql_config"
inline
label-placement="left"
>
<n-form-item label="SQL 题型">
<n-radio-group v-model:value="problem.sql_config.mode">
<n-radio-button value="query">查询题比对查询结果</n-radio-button>
<n-radio-button value="modify">
增删改题比对执行后的表数据
</n-radio-button>
</n-radio-group>
</n-form-item>
<n-form-item label="严格比对行顺序">
<n-switch v-model:value="problem.sql_config.order_sensitive" />
<n-text depth="3" style="margin-left: 12px">
题目要求 ORDER BY 时开启关闭则按无序集合比对
</n-text>
</n-form-item>
</n-form>
<n-grid :cols="2" x-gap="20"> <n-grid :cols="2" x-gap="20">
<n-gi> <n-gi>
<n-form> <n-form>
<n-form-item label="本题参考答案(选填,用于 AI 分析,不会泄露)"> <n-form-item
:label="
isSQLProblem
? '标准答案(必填,判题依据:每个测试点会运行它生成期望结果)'
: '本题参考答案(选填,用于 AI 分析,不会泄露)'
"
>
<n-tabs <n-tabs
type="segment" type="segment"
default-value="Python3" default-value="Python3"
@@ -681,7 +761,7 @@ watch(
</n-gi> </n-gi>
</n-grid> </n-grid>
<n-grid :cols="2"> <n-grid v-if="!isSQLProblem" :cols="2">
<n-gi :span="1"> <n-gi :span="1">
<AstRulesEditor <AstRulesEditor
v-model="problem.ast_rules!" v-model="problem.ast_rules!"
@@ -695,7 +775,7 @@ watch(
<h2 class="title">测试用例区域</h2> <h2 class="title">测试用例区域</h2>
<n-flex align="center" style="margin-bottom: 12px"> <n-flex align="center" style="margin-bottom: 12px">
<div> <div v-if="!isSQLProblem">
<n-button type="success" @click="showGeneratorModal = true"> <n-button type="success" @click="showGeneratorModal = true">
直接生成 直接生成
</n-button> </n-button>
@@ -706,14 +786,23 @@ watch(
accept=".zip" accept=".zip"
:custom-request="handleUploadTestcases" :custom-request="handleUploadTestcases"
> >
<n-button type="info">手动上传</n-button> <n-button type="info">
{{ isSQLProblem ? "上传数据脚本压缩包" : "(老)手动上传" }}
</n-button>
</n-upload> </n-upload>
</div> </div>
<n-tooltip placement="right"> <n-tooltip placement="right" style="max-width: 320px; white-space: normal">
<template #trigger> <template #trigger>
<n-button text>温馨提醒</n-button> <n-button text>温馨提醒</n-button>
</template> </template>
<template v-if="isSQLProblem">
压缩包内放 1.sql2.sql每个文件是一个测试点的建表+插入数据脚本
判题时对每个测试点分别运行标准答案和学生 SQL 比对结果 建议至少 2-3
个数据不同的测试点防止学生硬编码答案
</template>
<template v-else>
测试用例最好要有10个要考虑边界情况且不要跟测试样例一模一样 测试用例最好要有10个要考虑边界情况且不要跟测试样例一模一样
</template>
</n-tooltip> </n-tooltip>
</n-flex> </n-flex>
@@ -758,6 +847,7 @@ watch(
/> />
</n-modal> </n-modal>
<template v-if="!isSQLProblem">
<n-divider /> <n-divider />
<h2 class="title">流程图区域</h2> <h2 class="title">流程图区域</h2>
@@ -769,7 +859,7 @@ watch(
type="primary" type="primary"
size="small" size="small"
:disabled=" :disabled="
!problem.answers.filter((a) => a.language === 'Python3')[0].code !problem.answers.filter((a) => a.language === 'Python3')[0]?.code
.length .length
" "
:loading="isAIGenerating" :loading="isAIGenerating"
@@ -800,6 +890,7 @@ watch(
/> />
</n-form-item> </n-form-item>
</n-form> </n-form>
</template>
<n-flex style="margin-bottom: 120px" align="center" justify="end"> <n-flex style="margin-bottom: 120px" align="center" justify="end">
<n-button type="primary" @click="submit">提交</n-button> <n-button type="primary" @click="submit">提交</n-button>
</n-flex> </n-flex>
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from "vue" import { h } from "vue"
import { ProblemSetBadge } from "utils/types" import type { ProblemSetBadge } from "utils/types"
import { NButton, NImage } from "naive-ui" import { NButton, NImage } from "naive-ui"
interface Props { interface Props {
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ProblemSetBadge } from "utils/types" import type { ProblemSetBadge } from "utils/types"
interface Props { interface Props {
show: boolean show: boolean
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ProblemSetProblem } from "utils/types" import type { ProblemSetProblem } from "utils/types"
interface Props { interface Props {
show: boolean show: boolean
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from "vue" import { h } from "vue"
import { NDataTable, NButton, NFlex } from "naive-ui" import { NDataTable, NButton, NFlex } from "naive-ui"
import { ProblemSetProblem } from "utils/types" import type { ProblemSetProblem } from "utils/types"
interface Props { interface Props {
problems: ProblemSetProblem[] problems: ProblemSetProblem[]
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { ProblemSet } from "utils/types" import type { ProblemSet } from "utils/types"
interface Props { interface Props {
problemSet: ProblemSet problemSet: ProblemSet
@@ -2,7 +2,7 @@
import { h } from "vue" import { h } from "vue"
import { NDataTable, NButton, NFlex } from "naive-ui" import { NDataTable, NButton, NFlex } from "naive-ui"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { ProblemSetProgress } from "utils/types" import type { ProblemSetProgress } from "utils/types"
interface Props { interface Props {
progress: ProblemSetProgress[] progress: ProblemSetProgress[]
+1 -1
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { NTabPane, NTabs, NButton, NFlex } from "naive-ui" import { NTabPane, NTabs, NButton, NFlex } from "naive-ui"
import { import type {
ProblemSet, ProblemSet,
ProblemSetProblem, ProblemSetProblem,
ProblemSetBadge, ProblemSetBadge,
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { CreateProblemSetData, EditProblemSetData } from "utils/types" import type { CreateProblemSetData, EditProblemSetData } from "utils/types"
import { getProblemSetDetail, createProblemSet, editProblemSet } from "../api" import { getProblemSetDetail, createProblemSet, editProblemSet } from "../api"
const route = useRoute() const route = useRoute()
+1 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { NButton, NTag } from "naive-ui" import { NButton, NTag } from "naive-ui"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { Server } from "utils/types" import type { Server } from "utils/types"
import { useConfigStore } from "shared/store/config" import { useConfigStore } from "shared/store/config"
import { useConfigWebSocket } from "shared/composables/websocket" import { useConfigWebSocket } from "shared/composables/websocket"
import { import {
+1 -1
View File
@@ -6,7 +6,7 @@ import { getRank } from "oj/api"
import Pagination from "shared/components/Pagination.vue" import Pagination from "shared/components/Pagination.vue"
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
import { getACRate } from "utils/functions" import { getACRate } from "utils/functions"
import { Rank } from "utils/types" import type { Rank } from "utils/types"
import { getBaseInfo, randomUser10 } from "../api" import { getBaseInfo, randomUser10 } from "../api"
const userCount = ref(0) const userCount = ref(0)
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { import type {
Exercise, Exercise,
ExerciseType, ExerciseType,
ExerciseMcqData, ExerciseMcqData,
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import CodeEditor from "shared/components/CodeEditor.vue" import CodeEditor from "shared/components/CodeEditor.vue"
import MarkdownEditor from "shared/components/MarkdownEditor.vue" import MarkdownEditor from "shared/components/MarkdownEditor.vue"
import { Tutorial } from "utils/types" import type { Tutorial } from "utils/types"
import { createTutorial, getTutorial, updateTutorial } from "../api" import { createTutorial, getTutorial, updateTutorial } from "../api"
import ExerciseManager from "./components/ExerciseManager.vue" import ExerciseManager from "./components/ExerciseManager.vue"
+1 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { NSwitch } from "naive-ui" import { NSwitch } from "naive-ui"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { Tutorial } from "utils/types" import type { Tutorial } from "utils/types"
import { getTutorialList, setTutorialVisibility } from "../api" import { getTutorialList, setTutorialVisibility } from "../api"
import Actions from "./components/Actions.vue" import Actions from "./components/Actions.vue"
+1 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { PROBLEM_PERMISSION, USER_TYPE } from "utils/constants" import { PROBLEM_PERMISSION, USER_TYPE } from "utils/constants"
import { getUserRole } from "utils/functions" import { getUserRole } from "utils/functions"
import { User } from "utils/types" import type { User } from "utils/types"
import TextCopy from "shared/components/TextCopy.vue" import TextCopy from "shared/components/TextCopy.vue"
interface Props { interface Props {
+1 -1
View File
@@ -3,7 +3,7 @@ import { DataTableRowKey, SelectOption } from "naive-ui"
import Pagination from "shared/components/Pagination.vue" import Pagination from "shared/components/Pagination.vue"
import { usePagination } from "shared/composables/pagination" import { usePagination } from "shared/composables/pagination"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { User } from "utils/types" import type { User } from "utils/types"
import { import {
deleteUsers, deleteUsers,
editUser, editUser,
+1 -9
View File
@@ -1,4 +1,4 @@
/// <reference types="@rsbuild/core/types" /> /// <reference types="vite/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly PUBLIC_ENV: string readonly PUBLIC_ENV: string
@@ -14,11 +14,3 @@ interface ImportMetaEnv {
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv
} }
interface Document {
startViewTransition?: (callback: () => void | Promise<void>) => {
ready: Promise<void>
finished: Promise<void>
updateCallbackDone: Promise<void>
}
}
+1 -1
View File
@@ -36,7 +36,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { NButton, NTooltip } from "naive-ui" import { NButton, NTooltip } from "naive-ui"
import TagTitle from "./TagTitle.vue" import TagTitle from "./TagTitle.vue"
import { FlowchartSummary, SolvedProblem } from "utils/types" import type { FlowchartSummary, SolvedProblem } from "utils/types"
import { useAIStore } from "oj/store/ai" import { useAIStore } from "oj/store/ai"
import { useBreakpoints } from "shared/composables/breakpoints" import { useBreakpoints } from "shared/composables/breakpoints"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
+1 -1
View File
@@ -5,7 +5,7 @@ import Pagination from "shared/components/Pagination.vue"
import { useBreakpoints } from "shared/composables/breakpoints" import { useBreakpoints } from "shared/composables/breakpoints"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { renderTableTitle } from "utils/renders" import { renderTableTitle } from "utils/renders"
import { Announcement } from "utils/types" import type { Announcement } from "utils/types"
import TitleWithTag from "./components/TitleWithTag.vue" import TitleWithTag from "./components/TitleWithTag.vue"
const total = ref(0) const total = ref(0)
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ContestRank } from "utils/types" import type { ContestRank } from "utils/types"
interface Props { interface Props {
rank: ContestRank rank: ContestRank
+1 -1
View File
@@ -15,7 +15,7 @@ import {
Tooltip, Tooltip,
Legend, Legend,
} from "chart.js" } from "chart.js"
import { ContestRank } from "utils/types" import type { ContestRank } from "utils/types"
ChartJS.register( ChartJS.register(
CategoryScale, CategoryScale,
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ProblemFiltered } from "utils/types" import type { ProblemFiltered } from "utils/types"
import ProblemStatus from "oj/problem/components/ProblemStatus.vue" import ProblemStatus from "oj/problem/components/ProblemStatus.vue"
import { useContestStore } from "oj/store/contest" import { useContestStore } from "oj/store/contest"
import { renderTableTitle } from "utils/renders" import { renderTableTitle } from "utils/renders"
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Exercise, ExerciseDebugData } from "utils/types" import type { Exercise, ExerciseDebugData } from "utils/types"
import { highlightLines } from "../composables/useCodeHighlight" import { highlightLines } from "../composables/useCodeHighlight"
import "./exercise-highlight.css" import "./exercise-highlight.css"
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Exercise, ExerciseGroupData } from "utils/types" import type { Exercise, ExerciseGroupData } from "utils/types"
const props = defineProps<{ exercise: Exercise; lang?: string }>() const props = defineProps<{ exercise: Exercise; lang?: string }>()
const data = computed(() => props.exercise.data as ExerciseGroupData) const data = computed(() => props.exercise.data as ExerciseGroupData)
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Exercise, ExerciseMatchData } from "utils/types" import type { Exercise, ExerciseMatchData } from "utils/types"
const props = defineProps<{ exercise: Exercise; lang?: string }>() const props = defineProps<{ exercise: Exercise; lang?: string }>()
const data = computed(() => props.exercise.data as ExerciseMatchData) const data = computed(() => props.exercise.data as ExerciseMatchData)
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Exercise, ExerciseMcqData } from "utils/types" import type { Exercise, ExerciseMcqData } from "utils/types"
const props = defineProps<{ exercise: Exercise }>() const props = defineProps<{ exercise: Exercise }>()
const data = computed(() => props.exercise.data as ExerciseMcqData) const data = computed(() => props.exercise.data as ExerciseMcqData)
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Exercise, ExercisePredictData } from "utils/types" import type { Exercise, ExercisePredictData } from "utils/types"
import { highlight } from "../composables/useCodeHighlight" import { highlight } from "../composables/useCodeHighlight"
import "./exercise-highlight.css" import "./exercise-highlight.css"
+1 -1
View File
@@ -2,7 +2,7 @@
import hljs from "highlight.js/lib/core" import hljs from "highlight.js/lib/core"
import python from "highlight.js/lib/languages/python" import python from "highlight.js/lib/languages/python"
import c from "highlight.js/lib/languages/c" import c from "highlight.js/lib/languages/c"
import { Exercise, ExerciseSortData } from "utils/types" import type { Exercise, ExerciseSortData } from "utils/types"
hljs.registerLanguage("python", python) hljs.registerLanguage("python", python)
hljs.registerLanguage("c", c) hljs.registerLanguage("c", c)
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Exercise } from "utils/types" import type { Exercise } from "utils/types"
const ExerciseMcq = defineAsyncComponent(() => import("./ExerciseMcq.vue")) const ExerciseMcq = defineAsyncComponent(() => import("./ExerciseMcq.vue"))
const ExerciseSort = defineAsyncComponent(() => import("./ExerciseSort.vue")) const ExerciseSort = defineAsyncComponent(() => import("./ExerciseSort.vue"))
+1 -1
View File
@@ -1,4 +1,4 @@
import { Exercise } from "utils/types" import type { Exercise } from "utils/types"
type Segment = type Segment =
| { type: "md"; content: string } | { type: "md"; content: string }
+1 -1
View File
@@ -7,7 +7,7 @@ import CodeEditor from "shared/components/CodeEditor.vue"
import { useBreakpoints } from "shared/composables/breakpoints" import { useBreakpoints } from "shared/composables/breakpoints"
import { provideSyncStatus } from "oj/composables/syncStatus" import { provideSyncStatus } from "oj/composables/syncStatus"
import storage from "utils/storage" import storage from "utils/storage"
import { LANGUAGE } from "utils/types" import type { LANGUAGE } from "utils/types"
import Form from "./Form.vue" import Form from "./Form.vue"
const route = useRoute() const route = useRoute()
+4 -2
View File
@@ -15,7 +15,7 @@ import {
import { useBreakpoints } from "shared/composables/breakpoints" import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
import storage from "utils/storage" import storage from "utils/storage"
import { LANGUAGE } from "utils/types" import type { LANGUAGE } from "utils/types"
import StatisticsPanel from "shared/components/StatisticsPanel.vue" import StatisticsPanel from "shared/components/StatisticsPanel.vue"
import IconButton from "shared/components/IconButton.vue" import IconButton from "shared/components/IconButton.vue"
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
@@ -140,7 +140,8 @@ defineExpose({
onMounted(() => { onMounted(() => {
if (!languages.value.includes(codeStore.code.language)) { if (!languages.value.includes(codeStore.code.language)) {
codeStore.code.language = "Python3" // 退 SQL "SQL" Python3
codeStore.code.language = languages.value[0] ?? "Python3"
} }
}) })
</script> </script>
@@ -175,6 +176,7 @@ onMounted(() => {
<template v-if="codeStore.code.language !== 'Flowchart'"> <template v-if="codeStore.code.language !== 'Flowchart'">
<IconButton <IconButton
v-if="codeStore.code.language !== 'SQL'"
icon="streamline-ultimate-color:business-lucky-cat" icon="streamline-ultimate-color:business-lucky-cat"
tip="自测猫" tip="自测猫"
@click="goTestCat" @click="goTestCat"
+10
View File
@@ -130,6 +130,16 @@ onBeforeUnmount(() => {
watch(isMobile, (value) => { watch(isMobile, (value) => {
if (value) screenModeStore.resetScreenMode() if (value) screenModeStore.resetScreenMode()
}) })
// SQL "" SQL
watch(
() => screenModeStore.isCodeOnlyMode,
(codeOnly) => {
if (codeOnly && problem.value?.languages.includes("SQL")) {
screenModeStore.switchScreenMode()
}
},
)
</script> </script>
<template> <template>
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { ProblemSet, UserBadge as UserBadgeType } from "utils/types" import type { ProblemSet, UserBadge as UserBadgeType } from "utils/types"
import UserBadge from "shared/components/UserBadge.vue" import UserBadge from "shared/components/UserBadge.vue"
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { ProblemSetProblem } from "utils/types" import type { ProblemSetProblem } from "utils/types"
import { DIFFICULTY } from "utils/constants" import { DIFFICULTY } from "utils/constants"
import { getTagColor } from "utils/functions" import { getTagColor } from "utils/functions"
import { useBreakpoints } from "shared/composables/breakpoints" import { useBreakpoints } from "shared/composables/breakpoints"
@@ -2,7 +2,7 @@
import { h, computed, ref, onMounted, watch } from "vue" import { h, computed, ref, onMounted, watch } from "vue"
import { watchDebounced } from "@vueuse/core" import { watchDebounced } from "@vueuse/core"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { ProblemSetProgress } from "utils/types" import type { ProblemSetProgress } from "utils/types"
import { getProblemSetUserProgress } from "../../api" import { getProblemSetUserProgress } from "../../api"
import { NFlex, NTag } from "naive-ui" import { NFlex, NTag } from "naive-ui"
import { usePagination } from "shared/composables/pagination" import { usePagination } from "shared/composables/pagination"
+1 -1
View File
@@ -5,7 +5,7 @@ import {
joinProblemSet, joinProblemSet,
getUserBadges, getUserBadges,
} from "../api" } from "../api"
import { import type {
ProblemSet, ProblemSet,
ProblemSetProblem, ProblemSetProblem,
UserBadge as UserBadgeType, UserBadge as UserBadgeType,
+1 -1
View File
@@ -11,7 +11,7 @@ import {
Colors, Colors,
} from "chart.js" } from "chart.js"
import { ChartType } from "utils/constants" import { ChartType } from "utils/constants"
import { Rank } from "utils/types" import type { Rank } from "utils/types"
// Chart.js // Chart.js
ChartJS.register( ChartJS.register(
+1 -1
View File
@@ -1,4 +1,4 @@
import { DetailsData, DurationData } from "utils/types" import type { DetailsData, DurationData } from "utils/types"
import { consumeJSONEventStream } from "utils/stream" import { consumeJSONEventStream } from "utils/stream"
import { import {
getAIDetailData, getAIDetailData,
+1 -1
View File
@@ -1,7 +1,7 @@
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { STORAGE_KEY } from "utils/constants" import { STORAGE_KEY } from "utils/constants"
import storage from "utils/storage" import storage from "utils/storage"
import { Code, LANGUAGE } from "utils/types" import type { Code, LANGUAGE } from "utils/types"
/** /**
* Store * Store
+1 -1
View File
@@ -2,7 +2,7 @@ import { formatISO, getTime, parseISO } from "date-fns"
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
import { ContestStatus, ContestType } from "utils/constants" import { ContestStatus, ContestType } from "utils/constants"
import { duration } from "utils/functions" import { duration } from "utils/functions"
import { Contest, Problem } from "utils/types" import type { Contest, Problem } from "utils/types"
import { import {
checkContestPassword, checkContestPassword,
getContest, getContest,
@@ -109,7 +109,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { FlowchartSubmission } from "utils/types" import type { FlowchartSubmission } from "utils/types"
import { useMermaid } from "shared/composables/useMermaid" import { useMermaid } from "shared/composables/useMermaid"
interface Props { interface Props {
+1 -1
View File
@@ -5,7 +5,7 @@
</n-text> </n-text>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Grade } from "utils/types" import type { Grade } from "utils/types"
defineProps<{ defineProps<{
score: number score: number
grade: Grade grade: Grade
+8 -2
View File
@@ -11,7 +11,7 @@ import {
submissionTimeFormat, submissionTimeFormat,
utoa, utoa,
} from "utils/functions" } from "utils/functions"
import { Submission } from "utils/types" import type { Submission } from "utils/types"
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue" import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
import { useBreakpoints } from "shared/composables/breakpoints" import { useBreakpoints } from "shared/composables/breakpoints"
import { useCodeStore } from "oj/store/code" import { useCodeStore } from "oj/store/code"
@@ -127,7 +127,13 @@ onMounted(init)
</n-flex> </n-flex>
</n-alert> </n-alert>
<n-flex :vertical="isDesktop" justify="center"> <n-flex :vertical="isDesktop" justify="center">
<n-button secondary @click="copyToCat">复制到自测猫</n-button> <n-button
v-if="submission.language !== 'SQL'"
secondary
@click="copyToCat"
>
复制到自测猫
</n-button>
<n-button secondary @click="copyToProblem">复制回到题目</n-button> <n-button secondary @click="copyToProblem">复制回到题目</n-button>
</n-flex> </n-flex>
</n-flex> </n-flex>
+1 -1
View File
@@ -48,7 +48,7 @@ import { JUDGE_STATUS, LANGUAGE_FORMAT_VALUE } from "utils/constants"
import Copy from "shared/components/Copy.vue" import Copy from "shared/components/Copy.vue"
import Pagination from "shared/components/Pagination.vue" import Pagination from "shared/components/Pagination.vue"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
import { Message } from "utils/types" import type { Message } from "utils/types"
const router = useRouter() const router = useRouter()
const messages = ref<Message[]>([]) const messages = ref<Message[]>([])
+1 -1
View File
@@ -1,5 +1,5 @@
import http from "utils/http" import http from "utils/http"
import { Profile, Tag } from "utils/types" import type { Profile, Tag } from "utils/types"
export function login(data: { username: string; password: string }) { export function login(data: { username: string; password: string }) {
return http.post("login", data) return http.post("login", data)
+1 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from "@iconify/vue" import { Icon } from "@iconify/vue"
import { ContestType } from "utils/constants" import { ContestType } from "utils/constants"
import { Contest } from "utils/types" import type { Contest } from "utils/types"
defineProps<{ contest: Contest }>() defineProps<{ contest: Contest }>()
</script> </script>
+1 -1
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ContestType } from "utils/constants" import { ContestType } from "utils/constants"
import { Contest } from "utils/types" import type { Contest } from "utils/types"
interface Props { interface Props {
contest: Contest contest: Contest
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { JUDGE_STATUS } from "utils/constants" import { JUDGE_STATUS } from "utils/constants"
import { SUBMISSION_RESULT } from "utils/types" import type { SUBMISSION_RESULT } from "utils/types"
interface Props { interface Props {
result: SUBMISSION_RESULT result: SUBMISSION_RESULT
+3
View File
@@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cpp } from "@codemirror/lang-cpp" import { cpp } from "@codemirror/lang-cpp"
import { python } from "@codemirror/lang-python" import { python } from "@codemirror/lang-python"
import { sql, SQLite } from "@codemirror/lang-sql"
import { bracketMatching } from "@codemirror/language" import { bracketMatching } from "@codemirror/language"
import { EditorView } from "@codemirror/view" import { EditorView } from "@codemirror/view"
import { Codemirror } from "vue-codemirror" import { Codemirror } from "vue-codemirror"
@@ -57,6 +58,8 @@ const emit = defineEmits<{
const { isDesktop } = useBreakpoints() const { isDesktop } = useBreakpoints()
const langExtension = computed((): Extension => { const langExtension = computed((): Extension => {
if (language === "SQL")
return sql({ dialect: SQLite, upperCaseKeywords: true })
return ["Python2", "Python3"].includes(language) ? python() : cpp() return ["Python2", "Python3"].includes(language) ? python() : cpp()
}) })
+1 -1
View File
@@ -23,7 +23,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { UserBadge } from "utils/types" import type { UserBadge } from "utils/types"
import { parseTime } from "utils/functions" import { parseTime } from "utils/functions"
interface Props { interface Props {
badge: UserBadge badge: UserBadge
+3
View File
@@ -27,6 +27,9 @@ export function enhanceCompletion(language: LANGUAGE): CompletionSource {
const word = context.matchBefore(/\w+/) const word = context.matchBefore(/\w+/)
if (!word && !context.explicit) return null if (!word && !context.explicit) return null
// SQL 没有中文注释提示,关键字补全由 @codemirror/lang-sql 提供
if (language === "SQL") return null
const trulyLanguage = language.startsWith("Python") ? "python" : "c" const trulyLanguage = language.startsWith("Python") ? "python" : "c"
const completions: Completion[] = ( const completions: Completion[] = (
chineseAnnotations[trulyLanguage] || [] chineseAnnotations[trulyLanguage] || []
+1 -1
View File
@@ -1,5 +1,5 @@
import { getWebsiteConfig } from "oj/api" import { getWebsiteConfig } from "oj/api"
import { WebsiteConfig } from "utils/types" import type { WebsiteConfig } from "utils/types"
export const useConfigStore = defineStore("config", () => { export const useConfigStore = defineStore("config", () => {
const config = ref<WebsiteConfig>({ const config = ref<WebsiteConfig>({
+1 -1
View File
@@ -1,6 +1,6 @@
import { PROBLEM_PERMISSION, STORAGE_KEY, USER_TYPE } from "utils/constants" import { PROBLEM_PERMISSION, STORAGE_KEY, USER_TYPE } from "utils/constants"
import storage from "utils/storage" import storage from "utils/storage"
import { Profile, User } from "utils/types" import type { Profile, User } from "utils/types"
import { getProfile } from "../api" import { getProfile } from "../api"
import { useConfigStore } from "./config" import { useConfigStore } from "./config"
+6
View File
@@ -175,6 +175,7 @@ export const SOURCES = {
JavaScript: "", JavaScript: "",
Golang: "", Golang: "",
Flowchart: "", Flowchart: "",
SQL: "",
} as const } as const
export const LANGUAGE_ID = { export const LANGUAGE_ID = {
@@ -186,6 +187,7 @@ export const LANGUAGE_ID = {
JavaScript: 0, JavaScript: 0,
Golang: 0, Golang: 0,
Flowchart: 0, Flowchart: 0,
SQL: 0,
} as const } as const
export const LANGUAGE_FORMAT_VALUE = { export const LANGUAGE_FORMAT_VALUE = {
@@ -197,6 +199,7 @@ export const LANGUAGE_FORMAT_VALUE = {
JavaScript: "javascript", JavaScript: "javascript",
Golang: "go", Golang: "go",
Flowchart: "flowchart", Flowchart: "flowchart",
SQL: "sql",
} as const } as const
export const LANGUAGE_SHOW_VALUE = { export const LANGUAGE_SHOW_VALUE = {
@@ -208,6 +211,7 @@ export const LANGUAGE_SHOW_VALUE = {
Python3: "Python", Python3: "Python",
JavaScript: "JS", JavaScript: "JS",
Golang: "Go", Golang: "Go",
SQL: "SQL",
} as const } as const
export const ICON_SET = { export const ICON_SET = {
@@ -219,6 +223,7 @@ export const ICON_SET = {
Java: "devicon:java", Java: "devicon:java",
JavaScript: "devicon:javascript", JavaScript: "devicon:javascript",
Golang: "devicon:go", Golang: "devicon:go",
SQL: "devicon:sqlite",
} as const } as const
const cTemplate = `//TEMPLATE BEGIN const cTemplate = `//TEMPLATE BEGIN
@@ -256,6 +261,7 @@ export const CODE_TEMPLATES = {
JavaScript: blankTemplate, JavaScript: blankTemplate,
Golang: blankTemplate, Golang: blankTemplate,
Flowchart: blankTemplate, Flowchart: blankTemplate,
SQL: blankTemplate,
} as const } as const
export enum ScreenMode { export enum ScreenMode {
+9
View File
@@ -65,6 +65,12 @@ export type LANGUAGE =
| "JavaScript" | "JavaScript"
| "Golang" | "Golang"
| "Flowchart" | "Flowchart"
| "SQL"
export interface SQLConfig {
mode: "query" | "modify"
order_sensitive: boolean
}
export type LANGUAGE_SHOW_LABEL = export type LANGUAGE_SHOW_LABEL =
(typeof LANGUAGE_SHOW_VALUE)[keyof typeof LANGUAGE_SHOW_VALUE] (typeof LANGUAGE_SHOW_VALUE)[keyof typeof LANGUAGE_SHOW_VALUE]
@@ -164,6 +170,9 @@ export interface Problem {
}[] }[]
} | null } | null
has_ast_rules?: boolean has_ast_rules?: boolean
// SQL 题配置(非 SQL 题为 null
sql_config?: SQLConfig | null
} }
export type AdminProblem = Problem & AlterProblem export type AdminProblem = Problem & AlterProblem
+3
View File
@@ -2,6 +2,9 @@
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
// Vite(oxc) verbatimModuleSyntax type
// Rsbuild
"verbatimModuleSyntax": false,
"paths": { "paths": {
"utils/*": ["./src/utils/*"], "utils/*": ["./src/utils/*"],
"oj/*": ["./src/oj/*"], "oj/*": ["./src/oj/*"],
+1 -1
View File
@@ -20,5 +20,5 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["rsbuild.config.ts"] "include": ["vite.config.ts"]
} }
+112
View File
@@ -0,0 +1,112 @@
import { fileURLToPath, URL } from "node:url"
import { defineConfig, loadEnv, type Plugin } from "vite"
import vue from "@vitejs/plugin-vue"
import legacy from "@vitejs/plugin-legacy"
import AutoImport from "unplugin-auto-import/vite"
import Components from "unplugin-vue-components/vite"
import { NaiveUiResolver } from "unplugin-vue-components/resolvers"
// index.html 里按需注入 MaxKB 脚本(原 Rsbuild EJS 模板的等价实现)
function injectMaxkb(maxkbUrl: string | undefined): Plugin {
return {
name: "inject-maxkb",
transformIndexHtml(html) {
if (!maxkbUrl) return html
return {
html,
tags: [
{
tag: "script",
attrs: { async: true, defer: true, src: maxkbUrl },
injectTo: "head",
},
],
}
},
}
}
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "PUBLIC_")
const proxyConfig = {
target: env["PUBLIC_OJ_URL"],
changeOrigin: true,
}
const wsProxyConfig = {
target: env["PUBLIC_WS_URL"],
ws: true,
changeOrigin: true,
}
return {
plugins: [
vue(),
// 机房存在 Chrome 91 等旧浏览器:不做 SystemJS 双构建,
// 只按使用情况给现代产物注入 core-js API polyfillArray.at 等)
legacy({
renderLegacyChunks: false,
modernTargets: "chrome>=90",
modernPolyfills: true,
}),
AutoImport({
imports: [
"vue",
"vue-router",
"@vueuse/core",
"pinia",
{
"naive-ui": [
"useDialog",
"useMessage",
"useNotification",
"useLoadingBar",
],
},
{
from: "naive-ui",
imports: [
"DataTableColumn",
"FormRules",
"FormItemRule",
"SelectOption",
"UploadCustomRequestOptions",
"UploadFileInfo",
"MenuOption",
"DropdownDividerOption",
"DropdownOption",
],
type: true,
},
],
dts: "./src/auto-imports.d.ts",
}),
Components({
resolvers: [NaiveUiResolver()],
dts: "./src/components.d.ts",
}),
injectMaxkb(env["PUBLIC_MAXKB_URL"]),
],
envPrefix: "PUBLIC_",
resolve: {
alias: {
// mermaid-legacy (mermaid@9) 写死了 UMD 路径,新版 cytoscape 的 exports
// 不允许 import 条件访问它,转到 ESM 产物
"cytoscape/dist/cytoscape.umd.js": "cytoscape/dist/cytoscape.esm.mjs",
utils: fileURLToPath(new URL("./src/utils", import.meta.url)),
oj: fileURLToPath(new URL("./src/oj", import.meta.url)),
admin: fileURLToPath(new URL("./src/admin", import.meta.url)),
shared: fileURLToPath(new URL("./src/shared", import.meta.url)),
},
},
server: {
port: 5173,
proxy: {
"/api": proxyConfig,
"/public": proxyConfig,
"/ws": wsProxyConfig,
},
},
}
})