|
|
@@ -0,0 +1,702 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { getProblemController } from '@/api/problem-controller'
|
|
|
+import { getCategoryController } from '@/api/category-controller'
|
|
|
+import type {
|
|
|
+ CategoryVo,
|
|
|
+ ProblemCaseDto,
|
|
|
+ ProblemCaseVo,
|
|
|
+ ProblemDetailVo,
|
|
|
+ ProblemDto,
|
|
|
+ ProblemTestDto,
|
|
|
+ ProblemTestVo,
|
|
|
+ ProblemVo,
|
|
|
+ QueryByPage1Params
|
|
|
+} from '@/api/models'
|
|
|
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
|
|
+import { Delete, Edit, Plus, RefreshRight, Search, View } from '@element-plus/icons-vue'
|
|
|
+import { computed, onMounted, reactive, ref } from 'vue'
|
|
|
+import { useLoginUserStore } from '@/store'
|
|
|
+
|
|
|
+const api = getProblemController()
|
|
|
+const loginUserStore = useLoginUserStore()
|
|
|
+const isAdmin = computed(() => loginUserStore.loginUser.user?.role === 'admin')
|
|
|
+
|
|
|
+// 搜索
|
|
|
+const searchForm = reactive({
|
|
|
+ title: '',
|
|
|
+ difficulty: '',
|
|
|
+ isPublic: undefined as number | undefined,
|
|
|
+ categoryId: undefined as number | undefined
|
|
|
+})
|
|
|
+
|
|
|
+const categoryOptions = ref<CategoryVo[]>([])
|
|
|
+const loadCategories = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getCategoryController().list()
|
|
|
+ categoryOptions.value = res?.data || []
|
|
|
+ } catch {
|
|
|
+ categoryOptions.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const difficultyOptions = [
|
|
|
+ { label: '简单', value: 'easy' },
|
|
|
+ { label: '中等', value: 'medium' },
|
|
|
+ { label: '困难', value: 'hard' }
|
|
|
+]
|
|
|
+
|
|
|
+const difficultyTag = (d?: string) => {
|
|
|
+ if (d === 'easy') return 'success'
|
|
|
+ if (d === 'medium') return 'warning'
|
|
|
+ return 'danger'
|
|
|
+}
|
|
|
+
|
|
|
+const difficultyLabel = (d?: string) => difficultyOptions.find((i) => i.value === d)?.label || d || '-'
|
|
|
+
|
|
|
+// 表格
|
|
|
+const tableData = ref<ProblemVo[]>([])
|
|
|
+const loading = ref(false)
|
|
|
+const selectedRows = ref<ProblemVo[]>([])
|
|
|
+const pagination = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
|
|
+
|
|
|
+// 新增/编辑
|
|
|
+const dialogVisible = ref(false)
|
|
|
+const dialogTitle = ref('新增题目')
|
|
|
+const formRef = ref<FormInstance>()
|
|
|
+const problemForm = reactive<Partial<ProblemDto>>({
|
|
|
+ id: undefined,
|
|
|
+ title: '',
|
|
|
+ description: '',
|
|
|
+ timeLimitMs: 1000,
|
|
|
+ memoryLimitMb: 256,
|
|
|
+ isPublic: 1 as ProblemDto['isPublic'],
|
|
|
+ difficulty: 'easy' as ProblemDto['difficulty'],
|
|
|
+ categoryIds: []
|
|
|
+})
|
|
|
+const formRules: FormRules = {
|
|
|
+ title: [{ required: true, message: '请输入题目标题', trigger: 'blur' }],
|
|
|
+ description: [{ required: true, message: '请输入题目描述', trigger: 'blur' }],
|
|
|
+ timeLimitMs: [{ required: true, message: '请输入时间限制', trigger: 'blur' }],
|
|
|
+ memoryLimitMb: [{ required: true, message: '请输入内存限制', trigger: 'blur' }],
|
|
|
+ difficulty: [{ required: true, message: '请选择难度', trigger: 'change' }],
|
|
|
+ isPublic: [{ required: true, message: '请选择公开状态', trigger: 'change' }]
|
|
|
+}
|
|
|
+const isEdit = computed(() => !!problemForm.id)
|
|
|
+
|
|
|
+// 详情弹窗(样例 + 评测用例)
|
|
|
+const detailVisible = ref(false)
|
|
|
+const detail = ref<ProblemDetailVo>({})
|
|
|
+const currentProblemId = ref<number | undefined>(undefined)
|
|
|
+
|
|
|
+// 样例
|
|
|
+const caseTable = ref<ProblemCaseVo[]>([])
|
|
|
+const caseDialogVisible = ref(false)
|
|
|
+const caseFormRef = ref<FormInstance>()
|
|
|
+const caseForm = reactive<Partial<ProblemCaseDto>>({})
|
|
|
+const caseRules: FormRules = {
|
|
|
+ input: [{ required: true, message: '请输入样例输入', trigger: 'blur' }],
|
|
|
+ output: [{ required: true, message: '请输入样例输出', trigger: 'blur' }]
|
|
|
+}
|
|
|
+
|
|
|
+// 评测用例
|
|
|
+const testTable = ref<ProblemTestVo[]>([])
|
|
|
+const testDialogVisible = ref(false)
|
|
|
+const testFormRef = ref<FormInstance>()
|
|
|
+const testForm = reactive<Partial<ProblemTestDto>>({})
|
|
|
+const testRules: FormRules = {
|
|
|
+ input: [{ required: true, message: '请输入测试输入', trigger: 'blur' }],
|
|
|
+ output: [{ required: true, message: '请输入测试输出', trigger: 'blur' }]
|
|
|
+}
|
|
|
+
|
|
|
+// ---------- 列表 ----------
|
|
|
+const fetchData = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const condition: QueryByPage1Params = {
|
|
|
+ title: searchForm.title || undefined,
|
|
|
+ difficulty: searchForm.difficulty || undefined,
|
|
|
+ isPublic: searchForm.isPublic,
|
|
|
+ categoryId: searchForm.categoryId,
|
|
|
+ pageNum: pagination.pageNum,
|
|
|
+ pageSize: pagination.pageSize
|
|
|
+ }
|
|
|
+ const res = await api.queryByPage1(condition)
|
|
|
+ const pageData = res?.data
|
|
|
+ tableData.value = pageData?.data || []
|
|
|
+ pagination.total = pageData?.total || 0
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('获取题目列表失败')
|
|
|
+ tableData.value = []
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleSearch = () => {
|
|
|
+ pagination.pageNum = 1
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+const handleReset = () => {
|
|
|
+ searchForm.title = ''
|
|
|
+ searchForm.difficulty = ''
|
|
|
+ searchForm.isPublic = undefined
|
|
|
+ searchForm.categoryId = undefined
|
|
|
+ handleSearch()
|
|
|
+}
|
|
|
+
|
|
|
+const handlePageChange = (page: number) => {
|
|
|
+ pagination.pageNum = page
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+const handleSizeChange = (size: number) => {
|
|
|
+ pagination.pageSize = size
|
|
|
+ pagination.pageNum = 1
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+// ---------- 新增/编辑 ----------
|
|
|
+const handleAdd = () => {
|
|
|
+ dialogTitle.value = '新增题目'
|
|
|
+ problemForm.id = undefined
|
|
|
+ problemForm.title = ''
|
|
|
+ problemForm.description = ''
|
|
|
+ problemForm.timeLimitMs = 1000
|
|
|
+ problemForm.memoryLimitMb = 256
|
|
|
+ problemForm.isPublic = 1 as ProblemDto['isPublic']
|
|
|
+ problemForm.difficulty = 'easy' as ProblemDto['difficulty']
|
|
|
+ problemForm.categoryIds = []
|
|
|
+ dialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const handleEdit = (row: ProblemVo) => {
|
|
|
+ dialogTitle.value = '编辑题目'
|
|
|
+ const id = row.id
|
|
|
+ if (id === undefined) return
|
|
|
+ api
|
|
|
+ .getDetail(id)
|
|
|
+ .then((res) => {
|
|
|
+ const d = res?.data
|
|
|
+ if (!d) return
|
|
|
+ problemForm.id = d.id
|
|
|
+ problemForm.title = d.title || ''
|
|
|
+ problemForm.description = d.description || ''
|
|
|
+ problemForm.timeLimitMs = d.timeLimitMs
|
|
|
+ problemForm.memoryLimitMb = d.memoryLimitMb
|
|
|
+ problemForm.isPublic = d.isPublic
|
|
|
+ problemForm.difficulty = d.difficulty
|
|
|
+ problemForm.categoryIds = d.categories?.map((c) => c.id).filter((c): c is number => c !== undefined) || []
|
|
|
+ dialogVisible.value = true
|
|
|
+ })
|
|
|
+ .catch(() => ElMessage.error('获取题目信息失败'))
|
|
|
+}
|
|
|
+
|
|
|
+const handleSave = async () => {
|
|
|
+ const valid = await formRef.value?.validate().catch(() => false)
|
|
|
+ if (!valid) return
|
|
|
+ try {
|
|
|
+ const data: ProblemDto = {
|
|
|
+ id: problemForm.id,
|
|
|
+ title: problemForm.title!,
|
|
|
+ description: problemForm.description!,
|
|
|
+ timeLimitMs: problemForm.timeLimitMs!,
|
|
|
+ memoryLimitMb: problemForm.memoryLimitMb!,
|
|
|
+ isPublic: problemForm.isPublic as ProblemDto['isPublic'],
|
|
|
+ difficulty: problemForm.difficulty as ProblemDto['difficulty'],
|
|
|
+ categoryIds: problemForm.categoryIds || []
|
|
|
+ }
|
|
|
+ const res = isEdit.value ? await api.edit1(data) : await api.add1(data)
|
|
|
+ if (res?.code === 200) {
|
|
|
+ ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
|
|
|
+ dialogVisible.value = false
|
|
|
+ fetchData()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res?.message || '保存失败')
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('保存失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleDelete = (row: ProblemVo) => {
|
|
|
+ if (row.id === undefined) return
|
|
|
+ ElMessageBox.confirm('确认删除该题目吗?', '提示', { type: 'warning' })
|
|
|
+ .then(async () => {
|
|
|
+ try {
|
|
|
+ const res = await api.deleteByIds({ ids: [row.id!] })
|
|
|
+ if (res?.code === 200) {
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ fetchData()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res?.message || '删除失败')
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('删除失败')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
+const handleBatchDelete = () => {
|
|
|
+ if (!selectedRows.value.length) {
|
|
|
+ ElMessage.warning('请选择要删除的数据')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条数据吗?`, '提示', { type: 'warning' })
|
|
|
+ .then(async () => {
|
|
|
+ try {
|
|
|
+ const ids = selectedRows.value.map((r) => r.id).filter((id): id is number => id !== undefined)
|
|
|
+ const res = await api.deleteByIds({ ids })
|
|
|
+ if (res?.code === 200) {
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ selectedRows.value = []
|
|
|
+ fetchData()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res?.message || '删除失败')
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('删除失败')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
+// ---------- 详情弹窗 ----------
|
|
|
+const loadDetail = async (id: number) => {
|
|
|
+ const [detailRes, testRes] = await Promise.all([api.getDetail(id), api.queryTests(id)])
|
|
|
+ detail.value = detailRes?.data || {}
|
|
|
+ caseTable.value = detail.value.cases || []
|
|
|
+ testTable.value = testRes?.data || []
|
|
|
+}
|
|
|
+
|
|
|
+const openDetail = (row: ProblemVo) => {
|
|
|
+ const id = row.id
|
|
|
+ if (id === undefined) return
|
|
|
+ currentProblemId.value = id
|
|
|
+ detailVisible.value = true
|
|
|
+ loadDetail(id)
|
|
|
+}
|
|
|
+
|
|
|
+// 样例
|
|
|
+const openCaseAdd = () => {
|
|
|
+ caseForm.id = undefined
|
|
|
+ caseForm.problemId = currentProblemId.value
|
|
|
+ caseForm.input = ''
|
|
|
+ caseForm.output = ''
|
|
|
+ caseForm.description = ''
|
|
|
+ caseForm.sortOrder = caseTable.value.length + 1
|
|
|
+ caseDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const openCaseEdit = (row: ProblemCaseVo) => {
|
|
|
+ caseForm.id = row.id
|
|
|
+ caseForm.problemId = row.problemId
|
|
|
+ caseForm.input = row.input || ''
|
|
|
+ caseForm.output = row.output || ''
|
|
|
+ caseForm.description = row.description || ''
|
|
|
+ caseForm.sortOrder = row.sortOrder
|
|
|
+ caseDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const saveCase = async () => {
|
|
|
+ const valid = await caseFormRef.value?.validate().catch(() => false)
|
|
|
+ if (!valid) return
|
|
|
+ try {
|
|
|
+ const data: ProblemCaseDto = {
|
|
|
+ id: caseForm.id,
|
|
|
+ problemId: currentProblemId.value!,
|
|
|
+ input: caseForm.input!,
|
|
|
+ output: caseForm.output!,
|
|
|
+ description: caseForm.description || undefined,
|
|
|
+ sortOrder: caseForm.sortOrder
|
|
|
+ }
|
|
|
+ const res = caseForm.id ? await api.updateCase(data) : await api.addCase(currentProblemId.value!, data)
|
|
|
+ if (res?.code === 200) {
|
|
|
+ ElMessage.success('保存成功')
|
|
|
+ caseDialogVisible.value = false
|
|
|
+ loadDetail(currentProblemId.value!)
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res?.message || '保存失败')
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('保存失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const deleteCase = (row: ProblemCaseVo) => {
|
|
|
+ if (row.id === undefined) return
|
|
|
+ ElMessageBox.confirm('确认删除该样例吗?', '提示', { type: 'warning' })
|
|
|
+ .then(async () => {
|
|
|
+ try {
|
|
|
+ const res = await api.deleteCase({ id: row.id! })
|
|
|
+ if (res?.code === 200) {
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ loadDetail(currentProblemId.value!)
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res?.message || '删除失败')
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('删除失败')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
+// 评测用例
|
|
|
+const openTestAdd = () => {
|
|
|
+ testForm.id = undefined
|
|
|
+ testForm.problemId = currentProblemId.value
|
|
|
+ testForm.input = ''
|
|
|
+ testForm.output = ''
|
|
|
+ testForm.sortOrder = testTable.value.length + 1
|
|
|
+ testForm.isHidden = 0 as ProblemTestDto['isHidden']
|
|
|
+ testDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const openTestEdit = (row: ProblemTestVo) => {
|
|
|
+ testForm.id = row.id
|
|
|
+ testForm.problemId = row.problemId
|
|
|
+ testForm.input = row.input || ''
|
|
|
+ testForm.output = row.output || ''
|
|
|
+ testForm.sortOrder = row.sortOrder
|
|
|
+ testForm.isHidden = row.isHidden
|
|
|
+ testDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const saveTest = async () => {
|
|
|
+ const valid = await testFormRef.value?.validate().catch(() => false)
|
|
|
+ if (!valid) return
|
|
|
+ try {
|
|
|
+ const data: ProblemTestDto = {
|
|
|
+ id: testForm.id,
|
|
|
+ problemId: currentProblemId.value!,
|
|
|
+ input: testForm.input!,
|
|
|
+ output: testForm.output!,
|
|
|
+ sortOrder: testForm.sortOrder,
|
|
|
+ isHidden: testForm.isHidden as ProblemTestDto['isHidden']
|
|
|
+ }
|
|
|
+ const res = testForm.id ? await api.updateTest(data) : await api.addTest(currentProblemId.value!, data)
|
|
|
+ if (res?.code === 200) {
|
|
|
+ ElMessage.success('保存成功')
|
|
|
+ testDialogVisible.value = false
|
|
|
+ loadDetail(currentProblemId.value!)
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res?.message || '保存失败')
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('保存失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const deleteTest = (row: ProblemTestVo) => {
|
|
|
+ if (row.id === undefined) return
|
|
|
+ ElMessageBox.confirm('确认删除该评测用例吗?', '提示', { type: 'warning' })
|
|
|
+ .then(async () => {
|
|
|
+ try {
|
|
|
+ const res = await api.deleteTest({ id: row.id! })
|
|
|
+ if (res?.code === 200) {
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ loadDetail(currentProblemId.value!)
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res?.message || '删除失败')
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('删除失败')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
+const toggleTestHidden = async (row: ProblemTestVo, val: number) => {
|
|
|
+ const old = row.isHidden
|
|
|
+ row.isHidden = val as ProblemTestVo['isHidden']
|
|
|
+ try {
|
|
|
+ const data: ProblemTestDto = {
|
|
|
+ id: row.id,
|
|
|
+ problemId: row.problemId!,
|
|
|
+ input: row.input,
|
|
|
+ output: row.output,
|
|
|
+ sortOrder: row.sortOrder,
|
|
|
+ isHidden: val as ProblemTestDto['isHidden']
|
|
|
+ }
|
|
|
+ const res = await api.updateTest(data)
|
|
|
+ if (res?.code === 200) {
|
|
|
+ ElMessage.success('更新成功')
|
|
|
+ } else {
|
|
|
+ row.isHidden = old
|
|
|
+ ElMessage.error(res?.message || '更新失败')
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ row.isHidden = old
|
|
|
+ ElMessage.error('更新失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleSelectionChange = (rows: ProblemVo[]) => {
|
|
|
+ selectedRows.value = rows
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadCategories()
|
|
|
+ fetchData()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="problem-manage-view">
|
|
|
+ <el-card class="search-card" shadow="never">
|
|
|
+ <el-form inline>
|
|
|
+ <el-form-item label="标题">
|
|
|
+ <el-input v-model="searchForm.title" placeholder="请输入题目标题" clearable style="width: 200px" @keyup.enter="handleSearch" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="难度">
|
|
|
+ <el-select v-model="searchForm.difficulty" placeholder="全部" clearable style="width: 120px">
|
|
|
+ <el-option v-for="item in difficultyOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="公开状态">
|
|
|
+ <el-select v-model="searchForm.isPublic" placeholder="全部" clearable style="width: 120px">
|
|
|
+ <el-option label="公开" :value="1" />
|
|
|
+ <el-option label="未公开" :value="0" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="分类">
|
|
|
+ <el-select v-model="searchForm.categoryId" placeholder="全部" clearable style="width: 160px">
|
|
|
+ <el-option v-for="item in categoryOptions" :key="item.id" :label="item.categoryName" :value="item.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="handleSearch">
|
|
|
+ <el-icon class="el-icon--left"><Search /></el-icon>搜索
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="handleReset">
|
|
|
+ <el-icon class="el-icon--left"><RefreshRight /></el-icon>重置
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <div class="toolbar">
|
|
|
+ <el-button type="primary" @click="handleAdd">
|
|
|
+ <el-icon class="el-icon--left"><Plus /></el-icon>新增
|
|
|
+ </el-button>
|
|
|
+ <el-button v-if="isAdmin" type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
|
|
+ <el-icon class="el-icon--left"><Delete /></el-icon>批量删除
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-card class="table-card" shadow="never">
|
|
|
+ <el-table v-loading="loading" :data="tableData" border stripe @selection-change="handleSelectionChange" style="width: 100%">
|
|
|
+ <el-table-column type="selection" width="55" align="center" />
|
|
|
+ <el-table-column prop="id" label="ID" width="80" align="center" />
|
|
|
+ <el-table-column prop="title" label="标题" min-width="200" />
|
|
|
+ <el-table-column label="难度" width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="difficultyTag(row.difficulty)">{{ difficultyLabel(row.difficulty) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="公开状态" width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.isPublic === 1 ? 'success' : 'info'">{{ row.isPublic === 1 ? '公开' : '未公开' }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="时间/内存" width="140" align="center">
|
|
|
+ <template #default="{ row }">{{ row.timeLimitMs ?? '-' }}ms / {{ row.memoryLimitMb ?? '-' }}MB</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="通过/提交" width="110" align="center">
|
|
|
+ <template #default="{ row }">{{ row.acceptedCount ?? 0 }} / {{ row.tryCount ?? 0 }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="createdAt" label="创建时间" width="170" align="center" />
|
|
|
+ <el-table-column label="操作" width="200" align="center" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link @click="openDetail(row)">
|
|
|
+ <el-icon><View /></el-icon>详情
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" link @click="handleEdit(row)">
|
|
|
+ <el-icon><Edit /></el-icon>编辑
|
|
|
+ </el-button>
|
|
|
+ <el-button v-if="isAdmin" type="danger" link @click="handleDelete(row)">
|
|
|
+ <el-icon><Delete /></el-icon>删除
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div class="pagination">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pagination.pageNum"
|
|
|
+ v-model:page-size="pagination.pageSize"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ :total="pagination.total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @current-change="handlePageChange"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 新增/编辑题目 -->
|
|
|
+ <el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" draggable>
|
|
|
+ <el-form ref="formRef" :model="problemForm" :rules="formRules" label-width="90px">
|
|
|
+ <el-form-item label="标题" prop="title">
|
|
|
+ <el-input v-model="problemForm.title" placeholder="请输入题目标题" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="描述" prop="description">
|
|
|
+ <el-input v-model="problemForm.description" type="textarea" :rows="5" placeholder="请输入题目描述" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="时间限制" prop="timeLimitMs">
|
|
|
+ <el-input-number v-model="problemForm.timeLimitMs" :min="1" /> ms
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="内存限制" prop="memoryLimitMb">
|
|
|
+ <el-input-number v-model="problemForm.memoryLimitMb" :min="1" /> MB
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="难度" prop="difficulty">
|
|
|
+ <el-select v-model="problemForm.difficulty" style="width: 200px">
|
|
|
+ <el-option v-for="item in difficultyOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="公开" prop="isPublic">
|
|
|
+ <el-switch v-model="problemForm.isPublic" :active-value="1" :inactive-value="0" active-text="公开" inactive-text="未公开" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="分类">
|
|
|
+ <el-checkbox-group v-model="problemForm.categoryIds">
|
|
|
+ <el-checkbox v-for="item in categoryOptions" :key="item.id" :value="item.id" :label="item.id">
|
|
|
+ {{ item.categoryName }}
|
|
|
+ </el-checkbox>
|
|
|
+ </el-checkbox-group>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleSave">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 详情弹窗(样例 + 评测用例) -->
|
|
|
+ <el-dialog v-model="detailVisible" :title="`题目详情 - ${detail.title || ''}`" width="900px" draggable>
|
|
|
+ <el-tabs>
|
|
|
+ <el-tab-pane label="样例" name="cases">
|
|
|
+ <div class="sub-toolbar">
|
|
|
+ <el-button type="primary" size="small" @click="openCaseAdd">
|
|
|
+ <el-icon class="el-icon--left"><Plus /></el-icon>新增样例
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="caseTable" border stripe size="small">
|
|
|
+ <el-table-column label="序号" width="80" align="center">
|
|
|
+ <template #default="{ row }">{{ row.sortOrder ?? '-' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="input" label="输入" min-width="180" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="output" label="输出" min-width="180" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="description" label="说明" min-width="140" show-overflow-tooltip />
|
|
|
+ <el-table-column label="操作" width="120" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link size="small" @click="openCaseEdit(row)">编辑</el-button>
|
|
|
+ <el-button type="danger" link size="small" @click="deleteCase(row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane label="评测用例" name="tests">
|
|
|
+ <div class="sub-toolbar">
|
|
|
+ <el-button type="primary" size="small" @click="openTestAdd">
|
|
|
+ <el-icon class="el-icon--left"><Plus /></el-icon>新增用例
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="testTable" border stripe size="small">
|
|
|
+ <el-table-column label="序号" width="80" align="center">
|
|
|
+ <template #default="{ row }">{{ row.sortOrder ?? '-' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="input" label="输入" min-width="180" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="output" label="输出" min-width="180" show-overflow-tooltip />
|
|
|
+ <el-table-column label="隐藏" width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-switch
|
|
|
+ :model-value="row.isHidden"
|
|
|
+ :active-value="1"
|
|
|
+ :inactive-value="0"
|
|
|
+ @change="(val: number) => toggleTestHidden(row, val)"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="120" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link size="small" @click="openTestEdit(row)">编辑</el-button>
|
|
|
+ <el-button type="danger" link size="small" @click="deleteTest(row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 样例编辑 -->
|
|
|
+ <el-dialog v-model="caseDialogVisible" :title="caseForm.id ? '编辑样例' : '新增样例'" width="560px" draggable>
|
|
|
+ <el-form ref="caseFormRef" :model="caseForm" :rules="caseRules" label-width="80px">
|
|
|
+ <el-form-item label="序号">
|
|
|
+ <el-input-number v-model="caseForm.sortOrder" :min="1" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="输入" prop="input">
|
|
|
+ <el-input v-model="caseForm.input" type="textarea" :rows="4" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="输出" prop="output">
|
|
|
+ <el-input v-model="caseForm.output" type="textarea" :rows="4" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="说明">
|
|
|
+ <el-input v-model="caseForm.description" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="caseDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="saveCase">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 评测用例编辑 -->
|
|
|
+ <el-dialog v-model="testDialogVisible" :title="testForm.id ? '编辑评测用例' : '新增评测用例'" width="560px" draggable>
|
|
|
+ <el-form ref="testFormRef" :model="testForm" :rules="testRules" label-width="80px">
|
|
|
+ <el-form-item label="序号">
|
|
|
+ <el-input-number v-model="testForm.sortOrder" :min="1" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="输入" prop="input">
|
|
|
+ <el-input v-model="testForm.input" type="textarea" :rows="4" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="输出" prop="output">
|
|
|
+ <el-input v-model="testForm.output" type="textarea" :rows="4" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="隐藏">
|
|
|
+ <el-switch v-model="testForm.isHidden" :active-value="1" :inactive-value="0" active-text="隐藏" inactive-text="显示" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="testDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="saveTest">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.problem-manage-view {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-card {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar,
|
|
|
+.sub-toolbar {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.table-card {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-top: 16px;
|
|
|
+}
|
|
|
+</style>
|