Prechádzať zdrojové kódy

# feat: 题目管理与分类管理后台页面

yangyi 2 mesiacov pred
rodič
commit
7a06c334e9

+ 23 - 0
src/router/index.ts

@@ -8,12 +8,15 @@ import AdminLayout from "../layout/AdminLayout.vue";
 import UserView from "../view/UserView.vue";
 import ProblemListView from "../view/problem/ProblemListView.vue";
 import ProblemDetailView from "../view/problem/ProblemDetailView.vue";
+import ProblemManageView from "../view/admin/ProblemManageView.vue";
+import CategoryManageView from "../view/admin/CategoryManageView.vue";
 import {
     HomeFilled,
     Menu,
     User,
     Document,
     Files,
+    Collection,
 } from '@element-plus/icons-vue'
 import {useLoginUserStore} from "../store";
 import {ElMessage} from "element-plus";
@@ -70,6 +73,26 @@ export const adminRoutes:RouteRecordRaw[] = [
         component: AdminLayout,
         redirect: '/admin/problems',
         children:[
+            {
+                path: '/admin/problems',
+                name: 'problemManage',
+                component: ProblemManageView,
+                meta:{
+                    title: '题目管理',
+                    icon: Document,
+                    requiresCoach: true
+                }
+            },
+            {
+                path: '/admin/categories',
+                name: 'categoryManage',
+                component: CategoryManageView,
+                meta:{
+                    title: '分类管理',
+                    icon: Collection,
+                    requiresCoach: true
+                }
+            },
             {
                 path: '/admin/users',
                 name: 'users',

+ 164 - 0
src/view/admin/CategoryManageView.vue

@@ -0,0 +1,164 @@
+<script setup lang="ts">
+import { getCategoryController } from '@/api/category-controller'
+import type { CategoryDto, CategoryVo } from '@/api/models'
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
+import { Delete, Edit, Plus, RefreshRight } from '@element-plus/icons-vue'
+import { computed, onMounted, reactive, ref } from 'vue'
+import { useLoginUserStore } from '@/store'
+
+const api = getCategoryController()
+const loginUserStore = useLoginUserStore()
+const isAdmin = computed(() => loginUserStore.loginUser.user?.role === 'admin')
+
+const tableData = ref<CategoryVo[]>([])
+const loading = ref(false)
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增分类')
+const formRef = ref<FormInstance>()
+const categoryForm = reactive<Partial<CategoryDto>>({
+  id: undefined,
+  categoryName: '',
+  description: ''
+})
+const formRules: FormRules = {
+  categoryName: [{ required: true, message: '请输入分类名称', trigger: 'blur' }]
+}
+const isEdit = computed(() => !!categoryForm.id)
+
+const fetchData = async () => {
+  loading.value = true
+  try {
+    const res = await api.list()
+    if (res?.code === 200) {
+      tableData.value = res.data || []
+    } else {
+      ElMessage.error(res?.message || '获取分类失败')
+    }
+  } catch {
+    ElMessage.error('获取分类失败')
+    tableData.value = []
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleAdd = () => {
+  dialogTitle.value = '新增分类'
+  categoryForm.id = undefined
+  categoryForm.categoryName = ''
+  categoryForm.description = ''
+  dialogVisible.value = true
+}
+
+const handleEdit = (row: CategoryVo) => {
+  dialogTitle.value = '编辑分类'
+  categoryForm.id = row.id
+  categoryForm.categoryName = row.categoryName || ''
+  categoryForm.description = row.description || ''
+  dialogVisible.value = true
+}
+
+const handleSave = async () => {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
+  try {
+    const data: CategoryDto = {
+      id: categoryForm.id,
+      categoryName: categoryForm.categoryName!,
+      description: categoryForm.description || undefined
+    }
+    const res = isEdit.value ? await api.edit3(data) : await api.add3(data)
+    if (res?.code === 200) {
+      ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
+      dialogVisible.value = false
+      fetchData()
+    } else {
+      ElMessage.error(res?.message || '保存失败')
+    }
+  } catch {
+    ElMessage.error('保存失败')
+  }
+}
+
+const handleDelete = (row: CategoryVo) => {
+  if (row.id === undefined) return
+  ElMessageBox.confirm('确认删除该分类吗?', '提示', { type: 'warning' })
+    .then(async () => {
+      try {
+        const res = await api._delete({ id: row.id! })
+        if (res?.code === 200) {
+          ElMessage.success('删除成功')
+          fetchData()
+        } else {
+          ElMessage.error(res?.message || '删除失败')
+        }
+      } catch {
+        ElMessage.error('删除失败')
+      }
+    })
+    .catch(() => {})
+}
+
+onMounted(fetchData)
+</script>
+
+<template>
+  <div class="category-manage-view">
+    <div class="toolbar">
+      <el-button type="primary" @click="handleAdd">
+        <el-icon class="el-icon--left"><Plus /></el-icon>新增分类
+      </el-button>
+      <el-button @click="fetchData">
+        <el-icon class="el-icon--left"><RefreshRight /></el-icon>刷新
+      </el-button>
+    </div>
+
+    <el-card class="table-card" shadow="never">
+      <el-table v-loading="loading" :data="tableData" border stripe style="width: 100%">
+        <el-table-column prop="id" label="ID" width="80" align="center" />
+        <el-table-column prop="categoryName" label="分类名称" min-width="180" />
+        <el-table-column prop="description" label="描述" min-width="240" show-overflow-tooltip />
+        <el-table-column label="操作" width="160" align="center" fixed="right">
+          <template #default="{ row }">
+            <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>
+    </el-card>
+
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="480px" draggable>
+      <el-form ref="formRef" :model="categoryForm" :rules="formRules" label-width="90px">
+        <el-form-item label="分类名称" prop="categoryName">
+          <el-input v-model="categoryForm.categoryName" placeholder="请输入分类名称" />
+        </el-form-item>
+        <el-form-item label="描述">
+          <el-input v-model="categoryForm.description" type="textarea" :rows="3" placeholder="请输入分类描述" />
+        </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>
+  </div>
+</template>
+
+<style scoped>
+.category-manage-view {
+  padding: 20px;
+}
+
+.toolbar {
+  margin-bottom: 16px;
+}
+
+.table-card {
+  margin-bottom: 16px;
+}
+</style>

+ 702 - 0
src/view/admin/ProblemManageView.vue

@@ -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>