This commit is contained in:
106
src/admin/api.ts
106
src/admin/api.ts
@@ -285,3 +285,109 @@ export function updateACMHelperChecked(
|
|||||||
checked,
|
checked,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 题单管理 API
|
||||||
|
export function getProblemSetList(
|
||||||
|
offset = 0,
|
||||||
|
limit = 10,
|
||||||
|
keyword = "",
|
||||||
|
difficulty = "",
|
||||||
|
status = "",
|
||||||
|
) {
|
||||||
|
return http.get("admin/problemset/", {
|
||||||
|
params: {
|
||||||
|
paging: true,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
keyword,
|
||||||
|
difficulty,
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProblemSetDetail(id: number) {
|
||||||
|
return http.get(`admin/problemset/${id}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProblemSet(data: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
difficulty: string
|
||||||
|
is_public: boolean
|
||||||
|
status: string
|
||||||
|
}) {
|
||||||
|
return http.post("admin/problemset/", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editProblemSet(data: {
|
||||||
|
id: number
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
difficulty?: string
|
||||||
|
is_public?: boolean
|
||||||
|
status?: string
|
||||||
|
visible?: boolean
|
||||||
|
}) {
|
||||||
|
return http.put("admin/problemset/", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteProblemSet(id: number) {
|
||||||
|
return http.delete("admin/problemset/", { params: { id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleProblemSetVisible(id: number) {
|
||||||
|
return http.put("admin/problemset/visible/", { id })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateProblemSetStatus(id: number, status: string) {
|
||||||
|
return http.put("admin/problemset/status/", { id, status })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 题单题目管理 API
|
||||||
|
export function getProblemSetProblems(problemSetId: number) {
|
||||||
|
return http.get(`admin/problemset/${problemSetId}/problems/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addProblemToSet(problemSetId: number, data: {
|
||||||
|
problem_id: number
|
||||||
|
order?: number
|
||||||
|
is_required?: boolean
|
||||||
|
score?: number
|
||||||
|
hint?: string
|
||||||
|
}) {
|
||||||
|
return http.post(`admin/problemset/${problemSetId}/problems/`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeProblemFromSet(problemSetId: number, problemId: number) {
|
||||||
|
return http.delete(`admin/problemset/${problemSetId}/problems/${problemId}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 题单奖章管理 API
|
||||||
|
export function getProblemSetBadges(problemSetId: number) {
|
||||||
|
return http.get(`admin/problemset/${problemSetId}/badges/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProblemSetBadge(problemSetId: number, data: {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
icon: string
|
||||||
|
condition_type: string
|
||||||
|
condition_value: number
|
||||||
|
level?: number
|
||||||
|
}) {
|
||||||
|
return http.post(`admin/problemset/${problemSetId}/badges/`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteProblemSetBadge(problemSetId: number, badgeId: number) {
|
||||||
|
return http.delete(`admin/problemset/${problemSetId}/badges/${badgeId}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 题单进度管理 API
|
||||||
|
export function getProblemSetProgress(problemSetId: number) {
|
||||||
|
return http.get(`admin/problemset/${problemSetId}/progress/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeUserFromProblemSet(problemSetId: number, userId: number) {
|
||||||
|
return http.delete(`admin/problemset/${problemSetId}/progress/${userId}/`)
|
||||||
|
}
|
||||||
|
|||||||
105
src/admin/problemset/components/Actions.vue
Normal file
105
src/admin/problemset/components/Actions.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { deleteProblemSet, updateProblemSetStatus } from "admin/api"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
problemSetId: number
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits(["updated"])
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const showStatusModal = ref(false)
|
||||||
|
const newStatus = ref<"active" | "archived" | "draft">("active")
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: "活跃", value: "active" },
|
||||||
|
{ label: "已归档", value: "archived" },
|
||||||
|
{ label: "草稿", value: "draft" },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function handleDeleteProblemSet() {
|
||||||
|
try {
|
||||||
|
await deleteProblemSet(props.problemSetId)
|
||||||
|
message.success("删除成功")
|
||||||
|
emit("updated")
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("删除失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goEdit() {
|
||||||
|
router.push({
|
||||||
|
name: "admin problemset edit",
|
||||||
|
params: { problemSetId: props.problemSetId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDetail() {
|
||||||
|
router.push({
|
||||||
|
name: "admin problemset detail",
|
||||||
|
params: { problemSetId: props.problemSetId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function openStatusModal() {
|
||||||
|
showStatusModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdateStatus() {
|
||||||
|
try {
|
||||||
|
await updateProblemSetStatus(props.problemSetId, newStatus.value)
|
||||||
|
message.success("状态更新成功")
|
||||||
|
showStatusModal.value = false
|
||||||
|
emit("updated")
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("状态更新失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-flex>
|
||||||
|
<n-button size="small" secondary type="primary" @click="goEdit">
|
||||||
|
编辑
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" secondary type="info" @click="goDetail">
|
||||||
|
详情
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" secondary type="warning" @click="openStatusModal">
|
||||||
|
状态
|
||||||
|
</n-button>
|
||||||
|
<n-popconfirm @positive-click="handleDeleteProblemSet">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button secondary size="small" type="error">删除</n-button>
|
||||||
|
</template>
|
||||||
|
确定删除这个题单吗?删除后题单将不可见。
|
||||||
|
</n-popconfirm>
|
||||||
|
</n-flex>
|
||||||
|
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showStatusModal"
|
||||||
|
preset="card"
|
||||||
|
title="更新题单状态"
|
||||||
|
style="width: 400px"
|
||||||
|
>
|
||||||
|
<n-space vertical>
|
||||||
|
<n-form>
|
||||||
|
<n-form-item label="状态" required>
|
||||||
|
<n-select
|
||||||
|
v-model:value="newStatus"
|
||||||
|
:options="statusOptions"
|
||||||
|
placeholder="选择状态"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-space>
|
||||||
|
<template #footer>
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button @click="showStatusModal = false">取消</n-button>
|
||||||
|
<n-button type="primary" @click="handleUpdateStatus">确认</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
427
src/admin/problemset/detail.vue
Normal file
427
src/admin/problemset/detail.vue
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { h } from "vue"
|
||||||
|
import { NTabPane, NTabs, NCard, NTag, NButton, NModal, NForm, NFormItem, NInput, NSelect, NSwitch, NInputNumber } from "naive-ui"
|
||||||
|
import { parseTime } from "utils/functions"
|
||||||
|
import { ProblemSet, ProblemSetProblem, ProblemSetBadge, ProblemSetProgress } from "utils/types"
|
||||||
|
import {
|
||||||
|
getProblemSetDetail,
|
||||||
|
getProblemSetProblems,
|
||||||
|
getProblemSetBadges,
|
||||||
|
getProblemSetProgress,
|
||||||
|
addProblemToSet,
|
||||||
|
removeProblemFromSet,
|
||||||
|
createProblemSetBadge,
|
||||||
|
deleteProblemSetBadge,
|
||||||
|
removeUserFromProblemSet,
|
||||||
|
} from "../api"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const problemSetId = computed(() => Number(route.params.problemSetId))
|
||||||
|
|
||||||
|
const problemSet = ref<ProblemSet | null>(null)
|
||||||
|
const problems = ref<ProblemSetProblem[]>([])
|
||||||
|
const badges = ref<ProblemSetBadge[]>([])
|
||||||
|
const progress = ref<ProblemSetProgress[]>([])
|
||||||
|
|
||||||
|
// 添加题目相关
|
||||||
|
const showAddProblemModal = ref(false)
|
||||||
|
const newProblemId = ref<number | null>(null)
|
||||||
|
const newProblemOrder = ref(0)
|
||||||
|
const newProblemRequired = ref(true)
|
||||||
|
const newProblemScore = ref(0)
|
||||||
|
const newProblemHint = ref("")
|
||||||
|
|
||||||
|
// 添加奖章相关
|
||||||
|
const showAddBadgeModal = ref(false)
|
||||||
|
const newBadgeName = ref("")
|
||||||
|
const newBadgeDescription = ref("")
|
||||||
|
const newBadgeIcon = ref("")
|
||||||
|
const newBadgeConditionType = ref<"all_problems" | "problem_count" | "score">("all_problems")
|
||||||
|
const newBadgeConditionValue = ref(1)
|
||||||
|
const newBadgeLevel = ref(1)
|
||||||
|
|
||||||
|
const conditionTypeOptions = [
|
||||||
|
{ label: "完成所有题目", value: "all_problems" },
|
||||||
|
{ label: "完成指定数量题目", value: "problem_count" },
|
||||||
|
{ label: "达到指定分数", value: "score" },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 定义表格列
|
||||||
|
const progressColumns = [
|
||||||
|
{ title: '用户', key: 'user.username', width: 120 },
|
||||||
|
{
|
||||||
|
title: '加入时间',
|
||||||
|
key: 'join_time',
|
||||||
|
width: 180,
|
||||||
|
render: (row: ProblemSetProgress) => parseTime(row.join_time, "YYYY-MM-DD HH:mm:ss")
|
||||||
|
},
|
||||||
|
{ title: '已完成', key: 'completed_problems_count', width: 100 },
|
||||||
|
{ title: '总题目', key: 'total_problems_count', width: 100 },
|
||||||
|
{
|
||||||
|
title: '进度',
|
||||||
|
key: 'progress_percentage',
|
||||||
|
width: 100,
|
||||||
|
render: (row: ProblemSetProgress) => `${row.progress_percentage}%`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '是否完成',
|
||||||
|
key: 'is_completed',
|
||||||
|
width: 100,
|
||||||
|
render: (row: ProblemSetProgress) => row.is_completed ? '是' : '否'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 120,
|
||||||
|
render: (row: ProblemSetProgress) => h(NButton, {
|
||||||
|
size: 'small',
|
||||||
|
type: 'error',
|
||||||
|
secondary: true,
|
||||||
|
onClick: () => handleRemoveUser(row.user.id)
|
||||||
|
}, { default: () => '移除' })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
async function loadProblemSetDetail() {
|
||||||
|
try {
|
||||||
|
const res = await getProblemSetDetail(problemSetId.value)
|
||||||
|
problemSet.value = res.data
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("加载题单详情失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProblems() {
|
||||||
|
try {
|
||||||
|
const res = await getProblemSetProblems(problemSetId.value)
|
||||||
|
problems.value = res.data
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("加载题目列表失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBadges() {
|
||||||
|
try {
|
||||||
|
const res = await getProblemSetBadges(problemSetId.value)
|
||||||
|
badges.value = res.data
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("加载奖章列表失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProgress() {
|
||||||
|
try {
|
||||||
|
const res = await getProblemSetProgress(problemSetId.value)
|
||||||
|
progress.value = res.data
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("加载进度列表失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddProblem() {
|
||||||
|
if (!newProblemId.value) {
|
||||||
|
message.error("请输入题目ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addProblemToSet(problemSetId.value, {
|
||||||
|
problem_id: newProblemId.value,
|
||||||
|
order: newProblemOrder.value,
|
||||||
|
is_required: newProblemRequired.value,
|
||||||
|
score: newProblemScore.value,
|
||||||
|
hint: newProblemHint.value,
|
||||||
|
})
|
||||||
|
message.success("题目添加成功")
|
||||||
|
showAddProblemModal.value = false
|
||||||
|
loadProblems()
|
||||||
|
loadProblemSetDetail() // 刷新题目数量
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("添加题目失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRemoveProblem(problemId: number) {
|
||||||
|
try {
|
||||||
|
await removeProblemFromSet(problemSetId.value, problemId)
|
||||||
|
message.success("题目移除成功")
|
||||||
|
loadProblems()
|
||||||
|
loadProblemSetDetail() // 刷新题目数量
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("移除题目失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddBadge() {
|
||||||
|
if (!newBadgeName.value.trim()) {
|
||||||
|
message.error("请输入奖章名称")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createProblemSetBadge(problemSetId.value, {
|
||||||
|
name: newBadgeName.value,
|
||||||
|
description: newBadgeDescription.value,
|
||||||
|
icon: newBadgeIcon.value,
|
||||||
|
condition_type: newBadgeConditionType.value,
|
||||||
|
condition_value: newBadgeConditionValue.value,
|
||||||
|
level: newBadgeLevel.value,
|
||||||
|
})
|
||||||
|
message.success("奖章创建成功")
|
||||||
|
showAddBadgeModal.value = false
|
||||||
|
loadBadges()
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("创建奖章失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteBadge(badgeId: number) {
|
||||||
|
try {
|
||||||
|
await deleteProblemSetBadge(problemSetId.value, badgeId)
|
||||||
|
message.success("奖章删除成功")
|
||||||
|
loadBadges()
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("删除奖章失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRemoveUser(userId: number) {
|
||||||
|
try {
|
||||||
|
await removeUserFromProblemSet(problemSetId.value, userId)
|
||||||
|
message.success("用户移除成功")
|
||||||
|
loadProgress()
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("移除用户失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAddProblemModal() {
|
||||||
|
newProblemId.value = null
|
||||||
|
newProblemOrder.value = 0
|
||||||
|
newProblemRequired.value = true
|
||||||
|
newProblemScore.value = 0
|
||||||
|
newProblemHint.value = ""
|
||||||
|
showAddProblemModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAddBadgeModal() {
|
||||||
|
newBadgeName.value = ""
|
||||||
|
newBadgeDescription.value = ""
|
||||||
|
newBadgeIcon.value = ""
|
||||||
|
newBadgeConditionType.value = "all_problems"
|
||||||
|
newBadgeConditionValue.value = 1
|
||||||
|
newBadgeLevel.value = 1
|
||||||
|
showAddBadgeModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadProblemSetDetail()
|
||||||
|
loadProblems()
|
||||||
|
loadBadges()
|
||||||
|
loadProgress()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="problemSet">
|
||||||
|
<n-flex class="titleWrapper" justify="space-between" align="center">
|
||||||
|
<n-flex align="center">
|
||||||
|
<n-button @click="router.back()" secondary>
|
||||||
|
← 返回
|
||||||
|
</n-button>
|
||||||
|
<h2 class="title">{{ problemSet.title }}</h2>
|
||||||
|
</n-flex>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="router.push({ name: 'admin problemset edit', params: { problemSetId } })"
|
||||||
|
>
|
||||||
|
编辑题单
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
|
||||||
|
<n-card title="题单信息" style="margin-bottom: 16px">
|
||||||
|
<n-flex vertical gap="medium">
|
||||||
|
<n-flex>
|
||||||
|
<span style="width: 100px; font-weight: bold">描述:</span>
|
||||||
|
<span>{{ problemSet.description }}</span>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex>
|
||||||
|
<span style="width: 100px; font-weight: bold">创建者:</span>
|
||||||
|
<span>{{ problemSet.created_by.username }}</span>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex>
|
||||||
|
<span style="width: 100px; font-weight: bold">难度:</span>
|
||||||
|
<n-tag :type="problemSet.difficulty === 'Easy' ? 'success' : problemSet.difficulty === 'Medium' ? 'warning' : 'error'">
|
||||||
|
{{ problemSet.difficulty === 'Easy' ? '简单' : problemSet.difficulty === 'Medium' ? '中等' : '困难' }}
|
||||||
|
</n-tag>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex>
|
||||||
|
<span style="width: 100px; font-weight: bold">状态:</span>
|
||||||
|
<n-tag :type="problemSet.status === 'active' ? 'success' : problemSet.status === 'archived' ? 'default' : 'info'">
|
||||||
|
{{ problemSet.status === 'active' ? '活跃' : problemSet.status === 'archived' ? '已归档' : '草稿' }}
|
||||||
|
</n-tag>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex>
|
||||||
|
<span style="width: 100px; font-weight: bold">公开:</span>
|
||||||
|
<span>{{ problemSet.is_public ? '是' : '否' }}</span>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex>
|
||||||
|
<span style="width: 100px; font-weight: bold">可见:</span>
|
||||||
|
<span>{{ problemSet.visible ? '是' : '否' }}</span>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex>
|
||||||
|
<span style="width: 100px; font-weight: bold">题目数量:</span>
|
||||||
|
<span>{{ problemSet.problems_count }}</span>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex>
|
||||||
|
<span style="width: 100px; font-weight: bold">创建时间:</span>
|
||||||
|
<span>{{ parseTime(problemSet.create_time, "YYYY-MM-DD HH:mm:ss") }}</span>
|
||||||
|
</n-flex>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-tabs type="line">
|
||||||
|
<n-tab-pane name="problems" tab="题目管理">
|
||||||
|
<n-flex justify="space-between" align="center" style="margin-bottom: 16px">
|
||||||
|
<h3>题目列表</h3>
|
||||||
|
<n-button type="primary" @click="openAddProblemModal">
|
||||||
|
添加题目
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
<n-data-table
|
||||||
|
:columns="[
|
||||||
|
{ title: '题目ID', key: 'problem.id', width: 80 },
|
||||||
|
{ title: '题目标题', key: 'problem.title', minWidth: 200 },
|
||||||
|
{ title: '顺序', key: 'order', width: 80 },
|
||||||
|
{ title: '必做', key: 'is_required', width: 80, render: (row) => row.is_required ? '是' : '否' },
|
||||||
|
{ title: '分数', key: 'score', width: 80 },
|
||||||
|
{ title: '提示', key: 'hint', minWidth: 200 },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 120,
|
||||||
|
render: (row) => h(NButton, {
|
||||||
|
size: 'small',
|
||||||
|
type: 'error',
|
||||||
|
secondary: true,
|
||||||
|
onClick: () => handleRemoveProblem(row.problem.id)
|
||||||
|
}, { default: () => '移除' })
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
:data="problems"
|
||||||
|
/>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane name="badges" tab="奖章管理">
|
||||||
|
<n-flex justify="space-between" align="center" style="margin-bottom: 16px">
|
||||||
|
<h3>奖章列表</h3>
|
||||||
|
<n-button type="primary" @click="openAddBadgeModal">
|
||||||
|
添加奖章
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
<n-data-table
|
||||||
|
:columns="[
|
||||||
|
{ title: '名称', key: 'name', minWidth: 150 },
|
||||||
|
{ title: '描述', key: 'description', minWidth: 200 },
|
||||||
|
{ title: '图标', key: 'icon', width: 100 },
|
||||||
|
{ title: '条件类型', key: 'condition_type', width: 120 },
|
||||||
|
{ title: '条件值', key: 'condition_value', width: 100 },
|
||||||
|
{ title: '等级', key: 'level', width: 80 },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 120,
|
||||||
|
render: (row) => h(NButton, {
|
||||||
|
size: 'small',
|
||||||
|
type: 'error',
|
||||||
|
secondary: true,
|
||||||
|
onClick: () => handleDeleteBadge(row.id)
|
||||||
|
}, { default: () => '删除' })
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
:data="badges"
|
||||||
|
/>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane name="progress" tab="进度管理">
|
||||||
|
<n-flex justify="space-between" align="center" style="margin-bottom: 16px">
|
||||||
|
<h3>用户进度</h3>
|
||||||
|
</n-flex>
|
||||||
|
<n-data-table
|
||||||
|
:columns="progressColumns"
|
||||||
|
:data="progress"
|
||||||
|
/>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
|
||||||
|
<!-- 添加题目模态框 -->
|
||||||
|
<n-modal v-model:show="showAddProblemModal" preset="card" title="添加题目" style="width: 500px">
|
||||||
|
<n-form>
|
||||||
|
<n-form-item label="题目ID" required>
|
||||||
|
<n-input-number v-model:value="newProblemId" placeholder="请输入题目ID" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="顺序">
|
||||||
|
<n-input-number v-model:value="newProblemOrder" placeholder="题目在题单中的顺序" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="是否必做">
|
||||||
|
<n-switch v-model:value="newProblemRequired" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="分数">
|
||||||
|
<n-input-number v-model:value="newProblemScore" placeholder="题目分数" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="提示">
|
||||||
|
<n-input v-model:value="newProblemHint" type="textarea" placeholder="题目提示" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<template #footer>
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button @click="showAddProblemModal = false">取消</n-button>
|
||||||
|
<n-button type="primary" @click="handleAddProblem">确认</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
|
||||||
|
<!-- 添加奖章模态框 -->
|
||||||
|
<n-modal v-model:show="showAddBadgeModal" preset="card" title="添加奖章" style="width: 500px">
|
||||||
|
<n-form>
|
||||||
|
<n-form-item label="奖章名称" required>
|
||||||
|
<n-input v-model:value="newBadgeName" placeholder="请输入奖章名称" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="描述">
|
||||||
|
<n-input v-model:value="newBadgeDescription" type="textarea" placeholder="奖章描述" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="图标">
|
||||||
|
<n-input v-model:value="newBadgeIcon" placeholder="奖章图标" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="条件类型">
|
||||||
|
<n-select v-model:value="newBadgeConditionType" :options="conditionTypeOptions" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="条件值">
|
||||||
|
<n-input-number v-model:value="newBadgeConditionValue" placeholder="条件值" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="等级">
|
||||||
|
<n-input-number v-model:value="newBadgeLevel" placeholder="奖章等级" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<template #footer>
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button @click="showAddBadgeModal = false">取消</n-button>
|
||||||
|
<n-button type="primary" @click="handleAddBadge">确认</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.titleWrapper {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
167
src/admin/problemset/edit.vue
Normal file
167
src/admin/problemset/edit.vue
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NForm, NFormItem, NInput, NSelect, NSwitch, NButton, NCard } from "naive-ui"
|
||||||
|
import { ProblemSet, CreateProblemSetData, EditProblemSetData } from "utils/types"
|
||||||
|
import { getProblemSetDetail, createProblemSet, editProblemSet } from "../api"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const problemSetId = computed(() => Number(route.params.problemSetId))
|
||||||
|
const isEdit = computed(() => !!problemSetId.value)
|
||||||
|
|
||||||
|
const formData = ref<CreateProblemSetData & EditProblemSetData>({
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
difficulty: "Easy",
|
||||||
|
is_public: true,
|
||||||
|
status: "active",
|
||||||
|
})
|
||||||
|
|
||||||
|
const difficultyOptions = [
|
||||||
|
{ label: "简单", value: "Easy" },
|
||||||
|
{ label: "中等", value: "Medium" },
|
||||||
|
{ label: "困难", value: "Hard" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: "活跃", value: "active" },
|
||||||
|
{ label: "已归档", value: "archived" },
|
||||||
|
{ label: "草稿", value: "draft" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function loadProblemSetDetail() {
|
||||||
|
if (!isEdit.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getProblemSetDetail(problemSetId.value)
|
||||||
|
const data = res.data
|
||||||
|
formData.value = {
|
||||||
|
id: data.id,
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
difficulty: data.difficulty,
|
||||||
|
is_public: data.is_public,
|
||||||
|
status: data.status,
|
||||||
|
visible: data.visible,
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error("加载题单详情失败:" + (err.data || "未知错误"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!formData.value.title.trim()) {
|
||||||
|
message.error("请输入题单标题")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.value.description.trim()) {
|
||||||
|
message.error("请输入题单描述")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
await editProblemSet(formData.value as EditProblemSetData)
|
||||||
|
message.success("题单更新成功")
|
||||||
|
} else {
|
||||||
|
await createProblemSet(formData.value as CreateProblemSetData)
|
||||||
|
message.success("题单创建成功")
|
||||||
|
}
|
||||||
|
router.push({ name: "admin problemset list" })
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error((isEdit.value ? "更新" : "创建") + "题单失败:" + (err.data || "未知错误"))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (isEdit.value) {
|
||||||
|
loadProblemSetDetail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-card>
|
||||||
|
<template #header>
|
||||||
|
<n-flex align="center">
|
||||||
|
<n-button @click="router.back()" secondary>
|
||||||
|
← 返回
|
||||||
|
</n-button>
|
||||||
|
<h2 class="title">{{ isEdit ? '编辑题单' : '创建题单' }}</h2>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<n-form :model="formData" label-placement="left" label-width="100px">
|
||||||
|
<n-form-item label="题单标题" required>
|
||||||
|
<n-input
|
||||||
|
v-model:value="formData.title"
|
||||||
|
placeholder="请输入题单标题"
|
||||||
|
maxlength="200"
|
||||||
|
show-count
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="题单描述" required>
|
||||||
|
<n-input
|
||||||
|
v-model:value="formData.description"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入题单描述"
|
||||||
|
:rows="4"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="难度">
|
||||||
|
<n-select
|
||||||
|
v-model:value="formData.difficulty"
|
||||||
|
:options="difficultyOptions"
|
||||||
|
placeholder="选择难度"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="状态">
|
||||||
|
<n-select
|
||||||
|
v-model:value="formData.status"
|
||||||
|
:options="statusOptions"
|
||||||
|
placeholder="选择状态"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item label="是否公开">
|
||||||
|
<n-switch v-model:value="formData.is_public" />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item v-if="isEdit" label="是否可见">
|
||||||
|
<n-switch v-model:value="formData.visible" />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item>
|
||||||
|
<n-flex gap="medium">
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
{{ isEdit ? '更新' : '创建' }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="router.back()">
|
||||||
|
取消
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
211
src/admin/problemset/list.vue
Normal file
211
src/admin/problemset/list.vue
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NSwitch, NSelect, NTag } from "naive-ui"
|
||||||
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
|
import { usePagination } from "shared/composables/pagination"
|
||||||
|
import { parseTime } from "utils/functions"
|
||||||
|
import { ProblemSetList } from "utils/types"
|
||||||
|
import {
|
||||||
|
getProblemSetList,
|
||||||
|
toggleProblemSetVisible,
|
||||||
|
updateProblemSetStatus,
|
||||||
|
deleteProblemSet,
|
||||||
|
} from "../api"
|
||||||
|
import Actions from "./components/Actions.vue"
|
||||||
|
|
||||||
|
const total = ref(0)
|
||||||
|
const problemSets = ref<ProblemSetList[]>([])
|
||||||
|
|
||||||
|
interface ProblemSetQuery {
|
||||||
|
keyword: string
|
||||||
|
difficulty: string
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用分页 composable
|
||||||
|
const { query, clearQuery } = usePagination<ProblemSetQuery>({
|
||||||
|
keyword: "",
|
||||||
|
difficulty: "",
|
||||||
|
status: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const difficultyOptions = [
|
||||||
|
{ label: "全部", value: "" },
|
||||||
|
{ label: "简单", value: "Easy" },
|
||||||
|
{ label: "中等", value: "Medium" },
|
||||||
|
{ label: "困难", value: "Hard" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: "全部", value: "" },
|
||||||
|
{ label: "活跃", value: "active" },
|
||||||
|
{ label: "已归档", value: "archived" },
|
||||||
|
{ label: "草稿", value: "draft" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const columns: DataTableColumn<ProblemSetList>[] = [
|
||||||
|
{ title: "ID", key: "id", width: 80 },
|
||||||
|
{ title: "标题", key: "title", minWidth: 200 },
|
||||||
|
{ title: "描述", key: "description", minWidth: 300, ellipsis: true },
|
||||||
|
{
|
||||||
|
title: "创建者",
|
||||||
|
key: "created_by",
|
||||||
|
width: 120,
|
||||||
|
render: (row) => row.created_by.username,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "难度",
|
||||||
|
key: "difficulty",
|
||||||
|
width: 100,
|
||||||
|
render: (row) => {
|
||||||
|
const difficultyMap = {
|
||||||
|
Easy: { type: "success" as const, text: "简单" },
|
||||||
|
Medium: { type: "warning" as const, text: "中等" },
|
||||||
|
Hard: { type: "error" as const, text: "困难" },
|
||||||
|
}
|
||||||
|
const config = difficultyMap[row.difficulty]
|
||||||
|
return h(NTag, { type: config.type }, { default: () => config.text })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
key: "status",
|
||||||
|
width: 100,
|
||||||
|
render: (row) => {
|
||||||
|
const statusMap = {
|
||||||
|
active: { type: "success" as const, text: "活跃" },
|
||||||
|
archived: { type: "default" as const, text: "已归档" },
|
||||||
|
draft: { type: "info" as const, text: "草稿" },
|
||||||
|
}
|
||||||
|
const config = statusMap[row.status]
|
||||||
|
return h(NTag, { type: config.type }, { default: () => config.text })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "题目数量",
|
||||||
|
key: "problems_count",
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "创建时间",
|
||||||
|
key: "create_time",
|
||||||
|
width: 180,
|
||||||
|
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "可见",
|
||||||
|
key: "visible",
|
||||||
|
width: 80,
|
||||||
|
render: (row) =>
|
||||||
|
h(NSwitch, {
|
||||||
|
value: row.visible,
|
||||||
|
size: "small",
|
||||||
|
rubberBand: false,
|
||||||
|
onUpdateValue: () => toggleVisible(row.id),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "选项",
|
||||||
|
key: "actions",
|
||||||
|
width: 200,
|
||||||
|
render: (row) =>
|
||||||
|
h(Actions, {
|
||||||
|
problemSetId: row.id,
|
||||||
|
onUpdated: listProblemSets,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function listProblemSets() {
|
||||||
|
if (query.page < 1) query.page = 1
|
||||||
|
const offset = (query.page - 1) * query.limit
|
||||||
|
const res = await getProblemSetList(
|
||||||
|
offset,
|
||||||
|
query.limit,
|
||||||
|
query.keyword,
|
||||||
|
query.difficulty,
|
||||||
|
query.status,
|
||||||
|
)
|
||||||
|
total.value = res.data.total
|
||||||
|
problemSets.value = res.data.results
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleVisible(problemSetId: number) {
|
||||||
|
await toggleProblemSetVisible(problemSetId)
|
||||||
|
problemSets.value = problemSets.value.map((it) => {
|
||||||
|
if (it.id === problemSetId) {
|
||||||
|
it.visible = !it.visible
|
||||||
|
}
|
||||||
|
return it
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(listProblemSets)
|
||||||
|
|
||||||
|
// 监听搜索关键词变化(防抖)
|
||||||
|
watchDebounced(() => query.keyword, listProblemSets, {
|
||||||
|
debounce: 500,
|
||||||
|
maxWait: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听其他查询条件变化
|
||||||
|
watch(() => [query.page, query.limit, query.difficulty, query.status], listProblemSets)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-flex class="titleWrapper" justify="space-between">
|
||||||
|
<n-flex align="center">
|
||||||
|
<h2 class="title">题单管理</h2>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="$router.push({ name: 'admin problemset create' })"
|
||||||
|
>
|
||||||
|
新建题单
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex align="center" gap="medium">
|
||||||
|
<n-flex align="center">
|
||||||
|
<span>难度:</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="query.difficulty"
|
||||||
|
:options="difficultyOptions"
|
||||||
|
placeholder="选择难度"
|
||||||
|
style="width: 120px"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex align="center">
|
||||||
|
<span>状态:</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="query.status"
|
||||||
|
:options="statusOptions"
|
||||||
|
placeholder="选择状态"
|
||||||
|
style="width: 120px"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</n-flex>
|
||||||
|
<n-input
|
||||||
|
v-model:value="query.keyword"
|
||||||
|
placeholder="输入标题关键字"
|
||||||
|
clearable
|
||||||
|
@clear="clearQuery"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</n-flex>
|
||||||
|
</n-flex>
|
||||||
|
<n-data-table striped :columns="columns" :data="problemSets" />
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:limit="query.limit"
|
||||||
|
v-model:page="query.page"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.titleWrapper {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
0
src/oj/problemset/list.vue
Normal file
0
src/oj/problemset/list.vue
Normal file
@@ -249,5 +249,32 @@ export const admins: RouteRecordRaw = {
|
|||||||
props: true,
|
props: true,
|
||||||
meta: { requiresSuperAdmin: true },
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
|
// 题单管理路由
|
||||||
|
{
|
||||||
|
path: "problemset/list",
|
||||||
|
name: "admin problemset list",
|
||||||
|
component: () => import("admin/problemset/list.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "problemset/create",
|
||||||
|
name: "admin problemset create",
|
||||||
|
component: () => import("admin/problemset/edit.vue"),
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "problemset/edit/:problemSetId",
|
||||||
|
name: "admin problemset edit",
|
||||||
|
component: () => import("admin/problemset/edit.vue"),
|
||||||
|
props: true,
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "problemset/:problemSetId",
|
||||||
|
name: "admin problemset detail",
|
||||||
|
component: () => import("admin/problemset/detail.vue"),
|
||||||
|
props: true,
|
||||||
|
meta: { requiresSuperAdmin: true },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,15 @@ const options = computed<MenuOption[]>(() => {
|
|||||||
),
|
),
|
||||||
key: "admin contest list",
|
key: "admin contest list",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: () =>
|
||||||
|
h(
|
||||||
|
RouterLink,
|
||||||
|
{ to: "/admin/problemset/list" },
|
||||||
|
{ default: () => "题单" },
|
||||||
|
),
|
||||||
|
key: "admin problemset list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: () =>
|
label: () =>
|
||||||
h(RouterLink, { to: "/admin/user/list" }, { default: () => "用户" }),
|
h(RouterLink, { to: "/admin/user/list" }, { default: () => "用户" }),
|
||||||
|
|||||||
@@ -182,6 +182,90 @@ export interface AdminProblemFiltered {
|
|||||||
create_time: string
|
create_time: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 题单相关类型
|
||||||
|
export interface ProblemSet {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
created_by: SampleUser
|
||||||
|
create_time: Date
|
||||||
|
difficulty: "Easy" | "Medium" | "Hard"
|
||||||
|
status: "active" | "archived" | "draft"
|
||||||
|
is_public: boolean
|
||||||
|
visible: boolean
|
||||||
|
problems_count: number
|
||||||
|
completed_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProblemSetList {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
created_by: SampleUser
|
||||||
|
create_time: Date
|
||||||
|
difficulty: "Easy" | "Medium" | "Hard"
|
||||||
|
status: "active" | "archived" | "draft"
|
||||||
|
problems_count: number
|
||||||
|
visible: boolean
|
||||||
|
user_progress: {
|
||||||
|
is_joined: boolean
|
||||||
|
progress_percentage: number
|
||||||
|
completed_count: number
|
||||||
|
total_count: number
|
||||||
|
is_completed: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProblemSetProblem {
|
||||||
|
id: number
|
||||||
|
problemset: number
|
||||||
|
problem: Problem
|
||||||
|
order: number
|
||||||
|
is_required: boolean
|
||||||
|
score: number
|
||||||
|
hint: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProblemSetBadge {
|
||||||
|
id: number
|
||||||
|
problemset: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
icon: string
|
||||||
|
condition_type: "all_problems" | "problem_count" | "score"
|
||||||
|
condition_value: number
|
||||||
|
level: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProblemSetProgress {
|
||||||
|
id: number
|
||||||
|
problemset: ProblemSetList
|
||||||
|
user: SampleUser
|
||||||
|
join_time: Date
|
||||||
|
completed_problems_count: number
|
||||||
|
total_problems_count: number
|
||||||
|
progress_percentage: number
|
||||||
|
is_completed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateProblemSetData {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
difficulty: "Easy" | "Medium" | "Hard"
|
||||||
|
is_public: boolean
|
||||||
|
status: "active" | "archived" | "draft"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditProblemSetData {
|
||||||
|
id: number
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
difficulty?: "Easy" | "Medium" | "Hard"
|
||||||
|
is_public?: boolean
|
||||||
|
status?: "active" | "archived" | "draft"
|
||||||
|
visible?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface Code {
|
export interface Code {
|
||||||
language: LANGUAGE
|
language: LANGUAGE
|
||||||
value: string
|
value: string
|
||||||
|
|||||||
Reference in New Issue
Block a user