ProblemManageView.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. <script setup lang="ts">
  2. import { getProblemController } from '@/api/problem-controller'
  3. import { getCategoryController } from '@/api/category-controller'
  4. import type {
  5. CategoryVo,
  6. ProblemCaseDto,
  7. ProblemCaseVo,
  8. ProblemDetailVo,
  9. ProblemDto,
  10. ProblemTestDto,
  11. ProblemTestVo,
  12. ProblemVo,
  13. QueryByPage1Params
  14. } from '@/api/models'
  15. import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
  16. import { Delete, Edit, Plus, RefreshRight, Search, View } from '@element-plus/icons-vue'
  17. import { computed, onMounted, reactive, ref } from 'vue'
  18. import { useLoginUserStore } from '@/store'
  19. import { formatTime } from '@/util/format-time'
  20. import { MdEditor, mdPlugins } from '@/util/markdown'
  21. const api = getProblemController()
  22. const loginUserStore = useLoginUserStore()
  23. const isAdmin = computed(() => loginUserStore.loginUser.user?.role === 'admin')
  24. // 搜索
  25. const searchForm = reactive({
  26. title: '',
  27. difficulty: '',
  28. isPublic: undefined as number | undefined,
  29. categoryId: undefined as number | undefined
  30. })
  31. const categoryOptions = ref<CategoryVo[]>([])
  32. const loadCategories = async () => {
  33. try {
  34. const res = await getCategoryController().list()
  35. categoryOptions.value = res?.data || []
  36. } catch {
  37. categoryOptions.value = []
  38. }
  39. }
  40. const difficultyOptions = [
  41. { label: '简单', value: 'easy' },
  42. { label: '中等', value: 'medium' },
  43. { label: '困难', value: 'hard' }
  44. ]
  45. const difficultyTag = (d?: string) => {
  46. if (d === 'easy') return 'success'
  47. if (d === 'medium') return 'warning'
  48. return 'danger'
  49. }
  50. const difficultyLabel = (d?: string) => difficultyOptions.find((i) => i.value === d)?.label || d || '-'
  51. // 表格
  52. const tableData = ref<ProblemVo[]>([])
  53. const loading = ref(false)
  54. const selectedRows = ref<ProblemVo[]>([])
  55. const pagination = reactive({ pageNum: 1, pageSize: 10, total: 0 })
  56. // 新增/编辑
  57. const dialogVisible = ref(false)
  58. const dialogTitle = ref('新增题目')
  59. const formRef = ref<FormInstance>()
  60. const problemForm = reactive<Partial<ProblemDto>>({
  61. id: undefined,
  62. title: '',
  63. description: '',
  64. timeLimitMs: 1000,
  65. memoryLimitMb: 256,
  66. isPublic: 1 as ProblemDto['isPublic'],
  67. difficulty: 'easy' as ProblemDto['difficulty'],
  68. categoryIds: []
  69. })
  70. const formRules: FormRules = {
  71. title: [{ required: true, message: '请输入题目标题', trigger: 'blur' }],
  72. description: [{ required: true, message: '请输入题目描述', trigger: 'blur' }],
  73. timeLimitMs: [{ required: true, message: '请输入时间限制', trigger: 'blur' }],
  74. memoryLimitMb: [{ required: true, message: '请输入内存限制', trigger: 'blur' }],
  75. difficulty: [{ required: true, message: '请选择难度', trigger: 'change' }],
  76. isPublic: [{ required: true, message: '请选择公开状态', trigger: 'change' }]
  77. }
  78. const isEdit = computed(() => !!problemForm.id)
  79. // 详情弹窗(样例 + 评测用例)
  80. const detailVisible = ref(false)
  81. const detail = ref<ProblemDetailVo>({})
  82. const currentProblemId = ref<number | undefined>(undefined)
  83. // 样例
  84. const caseTable = ref<ProblemCaseVo[]>([])
  85. const caseDialogVisible = ref(false)
  86. const caseFormRef = ref<FormInstance>()
  87. const caseForm = reactive<Partial<ProblemCaseDto>>({})
  88. const caseRules: FormRules = {
  89. input: [{ required: true, message: '请输入样例输入', trigger: 'blur' }],
  90. output: [{ required: true, message: '请输入样例输出', trigger: 'blur' }]
  91. }
  92. // 评测用例
  93. const testTable = ref<ProblemTestVo[]>([])
  94. const testDialogVisible = ref(false)
  95. const testFormRef = ref<FormInstance>()
  96. const testForm = reactive<Partial<ProblemTestDto>>({})
  97. const testRules: FormRules = {
  98. input: [{ required: true, message: '请输入测试输入', trigger: 'blur' }],
  99. output: [{ required: true, message: '请输入测试输出', trigger: 'blur' }]
  100. }
  101. // ---------- 列表 ----------
  102. const fetchData = async () => {
  103. loading.value = true
  104. try {
  105. const condition: QueryByPage1Params = {
  106. title: searchForm.title || undefined,
  107. difficulty: searchForm.difficulty || undefined,
  108. isPublic: searchForm.isPublic,
  109. categoryId: searchForm.categoryId,
  110. pageNum: pagination.pageNum,
  111. pageSize: pagination.pageSize
  112. }
  113. const res = await api.queryByPage1(condition)
  114. const pageData = res?.data
  115. tableData.value = pageData?.data || []
  116. pagination.total = pageData?.total || 0
  117. } catch {
  118. tableData.value = []
  119. } finally {
  120. loading.value = false
  121. }
  122. }
  123. const handleSearch = () => {
  124. pagination.pageNum = 1
  125. fetchData()
  126. }
  127. const handleReset = () => {
  128. searchForm.title = ''
  129. searchForm.difficulty = ''
  130. searchForm.isPublic = undefined
  131. searchForm.categoryId = undefined
  132. handleSearch()
  133. }
  134. const handlePageChange = (page: number) => {
  135. pagination.pageNum = page
  136. fetchData()
  137. }
  138. const handleSizeChange = (size: number) => {
  139. pagination.pageSize = size
  140. pagination.pageNum = 1
  141. fetchData()
  142. }
  143. // ---------- 新增/编辑 ----------
  144. const handleAdd = () => {
  145. dialogTitle.value = '新增题目'
  146. problemForm.id = undefined
  147. problemForm.title = ''
  148. problemForm.description = ''
  149. problemForm.timeLimitMs = 1000
  150. problemForm.memoryLimitMb = 256
  151. problemForm.isPublic = 1 as ProblemDto['isPublic']
  152. problemForm.difficulty = 'easy' as ProblemDto['difficulty']
  153. problemForm.categoryIds = []
  154. dialogVisible.value = true
  155. }
  156. const handleEdit = (row: ProblemVo) => {
  157. dialogTitle.value = '编辑题目'
  158. const id = row.id
  159. if (id === undefined) return
  160. api
  161. .getDetail1(id)
  162. .then((res) => {
  163. const d = res?.data
  164. if (!d) return
  165. problemForm.id = d.id
  166. problemForm.title = d.title || ''
  167. problemForm.description = d.description || ''
  168. problemForm.timeLimitMs = d.timeLimitMs
  169. problemForm.memoryLimitMb = d.memoryLimitMb
  170. problemForm.isPublic = d.isPublic
  171. problemForm.difficulty = d.difficulty
  172. problemForm.categoryIds = d.categories?.map((c) => c.id).filter((c): c is number => c !== undefined) || []
  173. dialogVisible.value = true
  174. })
  175. .catch(() => {})
  176. }
  177. const handleSave = async () => {
  178. const valid = await formRef.value?.validate().catch(() => false)
  179. if (!valid) return
  180. try {
  181. const data: ProblemDto = {
  182. id: problemForm.id,
  183. title: problemForm.title!,
  184. description: problemForm.description!,
  185. timeLimitMs: problemForm.timeLimitMs!,
  186. memoryLimitMb: problemForm.memoryLimitMb!,
  187. isPublic: problemForm.isPublic as ProblemDto['isPublic'],
  188. difficulty: problemForm.difficulty as ProblemDto['difficulty'],
  189. categoryIds: problemForm.categoryIds || []
  190. }
  191. const res = isEdit.value ? await api.edit1(data) : await api.add1(data)
  192. if (res?.code === 200) {
  193. ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
  194. dialogVisible.value = false
  195. fetchData()
  196. }
  197. } catch {
  198. }
  199. }
  200. const handleDelete = (row: ProblemVo) => {
  201. if (row.id === undefined) return
  202. ElMessageBox.confirm('确认删除该题目吗?', '提示', { type: 'warning' })
  203. .then(async () => {
  204. try {
  205. const res = await api.deleteByIds({ ids: [row.id!] })
  206. if (res?.code === 200) {
  207. ElMessage.success('删除成功')
  208. fetchData()
  209. }
  210. } catch {
  211. }
  212. })
  213. .catch(() => {})
  214. }
  215. const handleBatchDelete = () => {
  216. if (!selectedRows.value.length) {
  217. ElMessage.warning('请选择要删除的数据')
  218. return
  219. }
  220. ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条数据吗?`, '提示', { type: 'warning' })
  221. .then(async () => {
  222. try {
  223. const ids = selectedRows.value.map((r) => r.id).filter((id): id is number => id !== undefined)
  224. const res = await api.deleteByIds({ ids })
  225. if (res?.code === 200) {
  226. ElMessage.success('删除成功')
  227. selectedRows.value = []
  228. fetchData()
  229. }
  230. } catch {
  231. }
  232. })
  233. .catch(() => {})
  234. }
  235. // ---------- 详情弹窗 ----------
  236. const loadDetail = async (id: number) => {
  237. const [detailRes, testRes] = await Promise.all([api.getDetail1(id), api.queryTests(id)])
  238. detail.value = detailRes?.data || {}
  239. caseTable.value = detail.value.cases || []
  240. testTable.value = testRes?.data || []
  241. }
  242. const openDetail = (row: ProblemVo) => {
  243. const id = row.id
  244. if (id === undefined) return
  245. currentProblemId.value = id
  246. detailVisible.value = true
  247. loadDetail(id)
  248. }
  249. // 样例
  250. const openCaseAdd = () => {
  251. caseForm.id = undefined
  252. caseForm.problemId = currentProblemId.value
  253. caseForm.input = ''
  254. caseForm.output = ''
  255. caseForm.description = ''
  256. caseForm.sortOrder = caseTable.value.length + 1
  257. caseDialogVisible.value = true
  258. }
  259. const openCaseEdit = (row: ProblemCaseVo) => {
  260. caseForm.id = row.id
  261. caseForm.problemId = row.problemId
  262. caseForm.input = row.input || ''
  263. caseForm.output = row.output || ''
  264. caseForm.description = row.description || ''
  265. caseForm.sortOrder = row.sortOrder
  266. caseDialogVisible.value = true
  267. }
  268. const saveCase = async () => {
  269. const valid = await caseFormRef.value?.validate().catch(() => false)
  270. if (!valid) return
  271. try {
  272. const data: ProblemCaseDto = {
  273. id: caseForm.id,
  274. problemId: currentProblemId.value!,
  275. input: caseForm.input!,
  276. output: caseForm.output!,
  277. description: caseForm.description || undefined,
  278. sortOrder: caseForm.sortOrder
  279. }
  280. const res = caseForm.id ? await api.updateCase(data) : await api.addCase(currentProblemId.value!, data)
  281. if (res?.code === 200) {
  282. ElMessage.success('保存成功')
  283. caseDialogVisible.value = false
  284. loadDetail(currentProblemId.value!)
  285. }
  286. } catch {
  287. }
  288. }
  289. const deleteCase = (row: ProblemCaseVo) => {
  290. if (row.id === undefined) return
  291. ElMessageBox.confirm('确认删除该样例吗?', '提示', { type: 'warning' })
  292. .then(async () => {
  293. try {
  294. const res = await api.deleteCase({ id: row.id! })
  295. if (res?.code === 200) {
  296. ElMessage.success('删除成功')
  297. loadDetail(currentProblemId.value!)
  298. }
  299. } catch {
  300. }
  301. })
  302. .catch(() => {})
  303. }
  304. // 评测用例
  305. const openTestAdd = () => {
  306. testForm.id = undefined
  307. testForm.problemId = currentProblemId.value
  308. testForm.input = ''
  309. testForm.output = ''
  310. testForm.sortOrder = testTable.value.length + 1
  311. testForm.isHidden = 0 as ProblemTestDto['isHidden']
  312. testDialogVisible.value = true
  313. }
  314. const openTestEdit = (row: ProblemTestVo) => {
  315. testForm.id = row.id
  316. testForm.problemId = row.problemId
  317. testForm.input = row.input || ''
  318. testForm.output = row.output || ''
  319. testForm.sortOrder = row.sortOrder
  320. testForm.isHidden = row.isHidden
  321. testDialogVisible.value = true
  322. }
  323. const saveTest = async () => {
  324. const valid = await testFormRef.value?.validate().catch(() => false)
  325. if (!valid) return
  326. try {
  327. const data: ProblemTestDto = {
  328. id: testForm.id,
  329. problemId: currentProblemId.value!,
  330. input: testForm.input!,
  331. output: testForm.output!,
  332. sortOrder: testForm.sortOrder,
  333. isHidden: testForm.isHidden as ProblemTestDto['isHidden']
  334. }
  335. const res = testForm.id ? await api.updateTest(data) : await api.addTest(currentProblemId.value!, data)
  336. if (res?.code === 200) {
  337. ElMessage.success('保存成功')
  338. testDialogVisible.value = false
  339. loadDetail(currentProblemId.value!)
  340. }
  341. } catch {
  342. }
  343. }
  344. const deleteTest = (row: ProblemTestVo) => {
  345. if (row.id === undefined) return
  346. ElMessageBox.confirm('确认删除该评测用例吗?', '提示', { type: 'warning' })
  347. .then(async () => {
  348. try {
  349. const res = await api.deleteTest({ id: row.id! })
  350. if (res?.code === 200) {
  351. ElMessage.success('删除成功')
  352. loadDetail(currentProblemId.value!)
  353. }
  354. } catch {
  355. }
  356. })
  357. .catch(() => {})
  358. }
  359. const toggleTestHidden = async (row: ProblemTestVo, val: number) => {
  360. const old = row.isHidden
  361. row.isHidden = val as ProblemTestVo['isHidden']
  362. try {
  363. const data: ProblemTestDto = {
  364. id: row.id,
  365. problemId: row.problemId!,
  366. input: row.input,
  367. output: row.output,
  368. sortOrder: row.sortOrder,
  369. isHidden: val as ProblemTestDto['isHidden']
  370. }
  371. const res = await api.updateTest(data)
  372. if (res?.code === 200) {
  373. ElMessage.success('更新成功')
  374. }
  375. } catch {
  376. row.isHidden = old
  377. }
  378. }
  379. const handleSelectionChange = (rows: ProblemVo[]) => {
  380. selectedRows.value = rows
  381. }
  382. onMounted(() => {
  383. loadCategories()
  384. fetchData()
  385. })
  386. </script>
  387. <template>
  388. <div class="problem-manage-view">
  389. <el-card class="search-card" shadow="never">
  390. <el-form inline>
  391. <el-form-item label="标题">
  392. <el-input v-model="searchForm.title" placeholder="请输入题目标题" clearable style="width: 200px" @keyup.enter="handleSearch" />
  393. </el-form-item>
  394. <el-form-item label="难度">
  395. <el-select v-model="searchForm.difficulty" placeholder="全部" clearable style="width: 120px">
  396. <el-option v-for="item in difficultyOptions" :key="item.value" :label="item.label" :value="item.value" />
  397. </el-select>
  398. </el-form-item>
  399. <el-form-item label="公开状态">
  400. <el-select v-model="searchForm.isPublic" placeholder="全部" clearable style="width: 120px">
  401. <el-option label="公开" :value="1" />
  402. <el-option label="未公开" :value="0" />
  403. </el-select>
  404. </el-form-item>
  405. <el-form-item label="分类">
  406. <el-select v-model="searchForm.categoryId" placeholder="全部" clearable style="width: 160px">
  407. <el-option v-for="item in categoryOptions" :key="item.id" :label="item.categoryName" :value="item.id" />
  408. </el-select>
  409. </el-form-item>
  410. <el-form-item>
  411. <el-button type="primary" @click="handleSearch">
  412. <el-icon class="el-icon--left"><Search /></el-icon>搜索
  413. </el-button>
  414. <el-button @click="handleReset">
  415. <el-icon class="el-icon--left"><RefreshRight /></el-icon>重置
  416. </el-button>
  417. </el-form-item>
  418. </el-form>
  419. </el-card>
  420. <div class="toolbar">
  421. <el-button type="primary" @click="handleAdd">
  422. <el-icon class="el-icon--left"><Plus /></el-icon>新增
  423. </el-button>
  424. <el-button v-if="isAdmin" type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
  425. <el-icon class="el-icon--left"><Delete /></el-icon>批量删除
  426. </el-button>
  427. </div>
  428. <el-card class="table-card" shadow="never">
  429. <el-table v-loading="loading" :data="tableData" border stripe @selection-change="handleSelectionChange" style="width: 100%">
  430. <el-table-column type="selection" width="55" align="center" />
  431. <el-table-column prop="id" label="ID" width="80" align="center" />
  432. <el-table-column prop="title" label="标题" min-width="200" />
  433. <el-table-column label="难度" width="100" align="center">
  434. <template #default="{ row }">
  435. <el-tag :type="difficultyTag(row.difficulty)">{{ difficultyLabel(row.difficulty) }}</el-tag>
  436. </template>
  437. </el-table-column>
  438. <el-table-column label="公开状态" width="100" align="center">
  439. <template #default="{ row }">
  440. <el-tag :type="row.isPublic === 1 ? 'success' : 'info'">{{ row.isPublic === 1 ? '公开' : '未公开' }}</el-tag>
  441. </template>
  442. </el-table-column>
  443. <el-table-column label="时间/内存" width="140" align="center">
  444. <template #default="{ row }">{{ row.timeLimitMs ?? '-' }}ms / {{ row.memoryLimitMb ?? '-' }}MB</template>
  445. </el-table-column>
  446. <el-table-column label="通过/提交" width="110" align="center">
  447. <template #default="{ row }">{{ row.acceptedCount ?? 0 }} / {{ row.tryCount ?? 0 }}</template>
  448. </el-table-column>
  449. <el-table-column label="创建时间" width="170" align="center">
  450. <template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
  451. </el-table-column>
  452. <el-table-column label="操作" width="200" align="center" fixed="right">
  453. <template #default="{ row }">
  454. <el-button type="primary" link @click="openDetail(row)">
  455. <el-icon><View /></el-icon>详情
  456. </el-button>
  457. <el-button type="primary" link @click="handleEdit(row)">
  458. <el-icon><Edit /></el-icon>编辑
  459. </el-button>
  460. <el-button v-if="isAdmin" type="danger" link @click="handleDelete(row)">
  461. <el-icon><Delete /></el-icon>删除
  462. </el-button>
  463. </template>
  464. </el-table-column>
  465. </el-table>
  466. <div class="pagination">
  467. <el-pagination
  468. v-model:current-page="pagination.pageNum"
  469. v-model:page-size="pagination.pageSize"
  470. :page-sizes="[10, 20, 50, 100]"
  471. :total="pagination.total"
  472. layout="total, sizes, prev, pager, next, jumper"
  473. @current-change="handlePageChange"
  474. @size-change="handleSizeChange"
  475. />
  476. </div>
  477. </el-card>
  478. <!-- 新增/编辑题目 -->
  479. <el-dialog v-model="dialogVisible" width="960px" draggable>
  480. <template #header>
  481. <div class="dialog-header">{{ dialogTitle }}</div>
  482. </template>
  483. <el-form ref="formRef" :model="problemForm" :rules="formRules" label-width="90px" label-position="left">
  484. <el-form-item label="标题" prop="title">
  485. <el-input v-model="problemForm.title" placeholder="请输入题目标题" />
  486. </el-form-item>
  487. <el-form-item label="描述" prop="description">
  488. <div class="markdown-editor-wrapper">
  489. <MdEditor :value="problemForm.description || ''" :plugins="mdPlugins" @change="(v: string) => (problemForm.description = v)" />
  490. </div>
  491. </el-form-item>
  492. <el-form-item label="时间限制" prop="timeLimitMs">
  493. <el-input-number v-model="problemForm.timeLimitMs" :min="1" /> ms
  494. </el-form-item>
  495. <el-form-item label="内存限制" prop="memoryLimitMb">
  496. <el-input-number v-model="problemForm.memoryLimitMb" :min="1" /> MB
  497. </el-form-item>
  498. <el-form-item label="难度" prop="difficulty">
  499. <el-select v-model="problemForm.difficulty" style="width: 200px">
  500. <el-option v-for="item in difficultyOptions" :key="item.value" :label="item.label" :value="item.value" />
  501. </el-select>
  502. </el-form-item>
  503. <el-form-item label="公开" prop="isPublic">
  504. <el-switch v-model="problemForm.isPublic" :active-value="1" :inactive-value="0" active-text="公开" inactive-text="未公开" />
  505. </el-form-item>
  506. <el-form-item label="分类">
  507. <el-checkbox-group v-model="problemForm.categoryIds">
  508. <el-checkbox v-for="item in categoryOptions" :key="item.id" :value="item.id" :label="item.id">
  509. {{ item.categoryName }}
  510. </el-checkbox>
  511. </el-checkbox-group>
  512. </el-form-item>
  513. </el-form>
  514. <template #footer>
  515. <div class="dialog-footer">
  516. <el-button @click="dialogVisible = false">取消</el-button>
  517. <el-button type="primary" @click="handleSave">确定</el-button>
  518. </div>
  519. </template>
  520. </el-dialog>
  521. <!-- 详情弹窗(样例 + 评测用例) -->
  522. <el-dialog v-model="detailVisible" :title="`题目详情 - ${detail.title || ''}`" width="900px" draggable>
  523. <el-tabs>
  524. <el-tab-pane label="样例" name="cases">
  525. <div class="sub-toolbar">
  526. <el-button type="primary" size="small" @click="openCaseAdd">
  527. <el-icon class="el-icon--left"><Plus /></el-icon>新增样例
  528. </el-button>
  529. </div>
  530. <el-table :data="caseTable" border stripe size="small">
  531. <el-table-column label="序号" width="80" align="center">
  532. <template #default="{ row }">{{ row.sortOrder ?? '-' }}</template>
  533. </el-table-column>
  534. <el-table-column prop="input" label="输入" min-width="180" show-overflow-tooltip />
  535. <el-table-column prop="output" label="输出" min-width="180" show-overflow-tooltip />
  536. <el-table-column prop="description" label="说明" min-width="140" show-overflow-tooltip />
  537. <el-table-column label="操作" width="120" align="center">
  538. <template #default="{ row }">
  539. <el-button type="primary" link size="small" @click="openCaseEdit(row)">编辑</el-button>
  540. <el-button type="danger" link size="small" @click="deleteCase(row)">删除</el-button>
  541. </template>
  542. </el-table-column>
  543. </el-table>
  544. </el-tab-pane>
  545. <el-tab-pane label="评测用例" name="tests">
  546. <div class="sub-toolbar">
  547. <el-button type="primary" size="small" @click="openTestAdd">
  548. <el-icon class="el-icon--left"><Plus /></el-icon>新增用例
  549. </el-button>
  550. </div>
  551. <el-table :data="testTable" border stripe size="small">
  552. <el-table-column label="序号" width="80" align="center">
  553. <template #default="{ row }">{{ row.sortOrder ?? '-' }}</template>
  554. </el-table-column>
  555. <el-table-column prop="input" label="输入" min-width="180" show-overflow-tooltip />
  556. <el-table-column prop="output" label="输出" min-width="180" show-overflow-tooltip />
  557. <el-table-column label="隐藏" width="100" align="center">
  558. <template #default="{ row }">
  559. <el-switch
  560. :model-value="row.isHidden"
  561. :active-value="1"
  562. :inactive-value="0"
  563. @change="(val: number) => toggleTestHidden(row, val)"
  564. />
  565. </template>
  566. </el-table-column>
  567. <el-table-column label="操作" width="120" align="center">
  568. <template #default="{ row }">
  569. <el-button type="primary" link size="small" @click="openTestEdit(row)">编辑</el-button>
  570. <el-button type="danger" link size="small" @click="deleteTest(row)">删除</el-button>
  571. </template>
  572. </el-table-column>
  573. </el-table>
  574. </el-tab-pane>
  575. </el-tabs>
  576. </el-dialog>
  577. <!-- 样例编辑 -->
  578. <el-dialog v-model="caseDialogVisible" :title="caseForm.id ? '编辑样例' : '新增样例'" width="700px" draggable>
  579. <el-form ref="caseFormRef" :model="caseForm" :rules="caseRules" label-width="80px">
  580. <el-form-item label="序号">
  581. <el-input-number v-model="caseForm.sortOrder" :min="1" />
  582. </el-form-item>
  583. <el-form-item label="输入" prop="input">
  584. <el-input v-model="caseForm.input" type="textarea" :rows="4" />
  585. </el-form-item>
  586. <el-form-item label="输出" prop="output">
  587. <el-input v-model="caseForm.output" type="textarea" :rows="4" />
  588. </el-form-item>
  589. <el-form-item label="说明">
  590. <div class="markdown-editor-wrapper case-description">
  591. <MdEditor :value="caseForm.description || ''" :plugins="mdPlugins" @change="(v: string) => (caseForm.description = v)" />
  592. </div>
  593. </el-form-item>
  594. </el-form>
  595. <template #footer>
  596. <el-button @click="caseDialogVisible = false">取消</el-button>
  597. <el-button type="primary" @click="saveCase">确定</el-button>
  598. </template>
  599. </el-dialog>
  600. <!-- 评测用例编辑 -->
  601. <el-dialog v-model="testDialogVisible" :title="testForm.id ? '编辑评测用例' : '新增评测用例'" width="560px" draggable>
  602. <el-form ref="testFormRef" :model="testForm" :rules="testRules" label-width="80px">
  603. <el-form-item label="序号">
  604. <el-input-number v-model="testForm.sortOrder" :min="1" />
  605. </el-form-item>
  606. <el-form-item label="输入" prop="input">
  607. <el-input v-model="testForm.input" type="textarea" :rows="4" />
  608. </el-form-item>
  609. <el-form-item label="输出" prop="output">
  610. <el-input v-model="testForm.output" type="textarea" :rows="4" />
  611. </el-form-item>
  612. <el-form-item label="隐藏">
  613. <el-switch v-model="testForm.isHidden" :active-value="1" :inactive-value="0" active-text="隐藏" inactive-text="显示" />
  614. </el-form-item>
  615. </el-form>
  616. <template #footer>
  617. <el-button @click="testDialogVisible = false">取消</el-button>
  618. <el-button type="primary" @click="saveTest">确定</el-button>
  619. </template>
  620. </el-dialog>
  621. </div>
  622. </template>
  623. <style scoped>
  624. .problem-manage-view {
  625. padding: 20px;
  626. }
  627. .search-card {
  628. margin-bottom: 16px;
  629. }
  630. .toolbar,
  631. .sub-toolbar {
  632. margin-bottom: 16px;
  633. }
  634. .table-card {
  635. margin-bottom: 16px;
  636. }
  637. .pagination {
  638. display: flex;
  639. justify-content: flex-end;
  640. margin-top: 16px;
  641. }
  642. .dialog-header {
  643. text-align: center;
  644. }
  645. .dialog-footer {
  646. display: flex;
  647. justify-content: center;
  648. gap: 12px;
  649. }
  650. .markdown-editor-wrapper {
  651. width: 100%;
  652. flex: 1;
  653. }
  654. .markdown-editor-wrapper :deep(.bytemd) {
  655. height: 300px;
  656. width: 100%;
  657. }
  658. .markdown-editor-wrapper.case-description :deep(.bytemd) {
  659. height: 180px;
  660. }
  661. </style>