@@ -42,13 +42,7 @@ const columns: DataTableColumn<AdminProblemFiltered>[] = [
|
|||||||
|
|
||||||
async function getList() {
|
async function getList() {
|
||||||
const offset = (query.page - 1) * query.limit
|
const offset = (query.page - 1) * query.limit
|
||||||
const res = await getProblemList(
|
const res = await getProblemList(offset, query.limit, query.keyword, "", "")
|
||||||
offset,
|
|
||||||
query.limit,
|
|
||||||
query.keyword,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
total.value = res.total
|
total.value = res.total
|
||||||
problems.value = res.results
|
problems.value = res.results
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NModal, NForm, NFormItem, NInput, NInputNumber, NSelect, NButton, NFlex, NImage } from "naive-ui"
|
import {
|
||||||
|
NModal,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NSelect,
|
||||||
|
NButton,
|
||||||
|
NFlex,
|
||||||
|
NImage,
|
||||||
|
} from "naive-ui"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean
|
show: boolean
|
||||||
@@ -7,13 +17,16 @@ interface Props {
|
|||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: "update:show", value: boolean): void
|
(e: "update:show", value: boolean): void
|
||||||
(e: "confirm", data: {
|
(
|
||||||
|
e: "confirm",
|
||||||
|
data: {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
icon: string
|
icon: string
|
||||||
condition_type: "all_problems" | "problem_count" | "score"
|
condition_type: "all_problems" | "problem_count" | "score"
|
||||||
condition_value?: number
|
condition_value?: number
|
||||||
}): void
|
},
|
||||||
|
): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -22,7 +35,9 @@ const emit = defineEmits<Emits>()
|
|||||||
const newBadgeName = ref("")
|
const newBadgeName = ref("")
|
||||||
const newBadgeDescription = ref("")
|
const newBadgeDescription = ref("")
|
||||||
const newBadgeIcon = ref("")
|
const newBadgeIcon = ref("")
|
||||||
const newBadgeConditionType = ref<"all_problems" | "problem_count" | "score">("all_problems")
|
const newBadgeConditionType = ref<"all_problems" | "problem_count" | "score">(
|
||||||
|
"all_problems",
|
||||||
|
)
|
||||||
const newBadgeConditionValue = ref(1)
|
const newBadgeConditionValue = ref(1)
|
||||||
|
|
||||||
// 预设奖章图标选项
|
// 预设奖章图标选项
|
||||||
@@ -61,7 +76,9 @@ function handleCancel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
watch(() => props.show, (newVal) => {
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
newBadgeName.value = ""
|
newBadgeName.value = ""
|
||||||
newBadgeDescription.value = ""
|
newBadgeDescription.value = ""
|
||||||
@@ -69,7 +86,8 @@ watch(() => props.show, (newVal) => {
|
|||||||
newBadgeConditionType.value = "all_problems"
|
newBadgeConditionType.value = "all_problems"
|
||||||
newBadgeConditionValue.value = 1
|
newBadgeConditionValue.value = 1
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NModal, NForm, NFormItem, NInput, NInputNumber, NSwitch, NButton, NFlex } from "naive-ui"
|
import {
|
||||||
|
NModal,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NSwitch,
|
||||||
|
NButton,
|
||||||
|
NFlex,
|
||||||
|
} from "naive-ui"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean
|
show: boolean
|
||||||
@@ -7,13 +16,16 @@ interface Props {
|
|||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: "update:show", value: boolean): void
|
(e: "update:show", value: boolean): void
|
||||||
(e: "confirm", data: {
|
(
|
||||||
|
e: "confirm",
|
||||||
|
data: {
|
||||||
problem_id: string
|
problem_id: string
|
||||||
order: number
|
order: number
|
||||||
is_required: boolean
|
is_required: boolean
|
||||||
score: number
|
score: number
|
||||||
hint: string
|
hint: string
|
||||||
}): void
|
},
|
||||||
|
): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -40,7 +52,9 @@ function handleCancel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
watch(() => props.show, (newVal) => {
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
newProblemId.value = ""
|
newProblemId.value = ""
|
||||||
newProblemOrder.value = 0
|
newProblemOrder.value = 0
|
||||||
@@ -48,7 +62,8 @@ watch(() => props.show, (newVal) => {
|
|||||||
newProblemScore.value = 0
|
newProblemScore.value = 0
|
||||||
newProblemHint.value = ""
|
newProblemHint.value = ""
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -19,15 +19,9 @@ defineEmits<Emits>()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-flex
|
<n-flex justify="space-between" align="center" style="margin-bottom: 16px">
|
||||||
justify="space-between"
|
|
||||||
align="center"
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
>
|
|
||||||
<h3>奖章列表</h3>
|
<h3>奖章列表</h3>
|
||||||
<n-button type="primary" @click="$emit('add-badge')">
|
<n-button type="primary" @click="$emit('add-badge')"> 添加奖章 </n-button>
|
||||||
添加奖章
|
|
||||||
</n-button>
|
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-data-table
|
<n-data-table
|
||||||
:columns="[
|
:columns="[
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NModal, NForm, NFormItem, NInput, NInputNumber, NSelect, NButton, NFlex, NImage } from "naive-ui"
|
import {
|
||||||
|
NModal,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NSelect,
|
||||||
|
NButton,
|
||||||
|
NFlex,
|
||||||
|
NImage,
|
||||||
|
} from "naive-ui"
|
||||||
import { ProblemSetBadge } from "utils/types"
|
import { ProblemSetBadge } from "utils/types"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -9,13 +19,16 @@ interface Props {
|
|||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: "update:show", value: boolean): void
|
(e: "update:show", value: boolean): void
|
||||||
(e: "confirm", data: {
|
(
|
||||||
|
e: "confirm",
|
||||||
|
data: {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
icon: string
|
icon: string
|
||||||
condition_type: "all_problems" | "problem_count" | "score"
|
condition_type: "all_problems" | "problem_count" | "score"
|
||||||
condition_value?: number
|
condition_value?: number
|
||||||
}): void
|
},
|
||||||
|
): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -24,7 +37,9 @@ const emit = defineEmits<Emits>()
|
|||||||
const editBadgeName = ref("")
|
const editBadgeName = ref("")
|
||||||
const editBadgeDescription = ref("")
|
const editBadgeDescription = ref("")
|
||||||
const editBadgeIcon = ref("")
|
const editBadgeIcon = ref("")
|
||||||
const editBadgeConditionType = ref<"all_problems" | "problem_count" | "score">("all_problems")
|
const editBadgeConditionType = ref<"all_problems" | "problem_count" | "score">(
|
||||||
|
"all_problems",
|
||||||
|
)
|
||||||
const editBadgeConditionValue = ref(1)
|
const editBadgeConditionValue = ref(1)
|
||||||
|
|
||||||
// 预设奖章图标选项
|
// 预设奖章图标选项
|
||||||
@@ -63,7 +78,9 @@ function handleCancel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 当奖章数据变化时,更新表单数据
|
// 当奖章数据变化时,更新表单数据
|
||||||
watch(() => props.badge, (newBadge) => {
|
watch(
|
||||||
|
() => props.badge,
|
||||||
|
(newBadge) => {
|
||||||
if (newBadge) {
|
if (newBadge) {
|
||||||
editBadgeName.value = newBadge.name
|
editBadgeName.value = newBadge.name
|
||||||
editBadgeDescription.value = newBadge.description
|
editBadgeDescription.value = newBadge.description
|
||||||
@@ -71,7 +88,9 @@ watch(() => props.badge, (newBadge) => {
|
|||||||
editBadgeConditionType.value = newBadge.condition_type
|
editBadgeConditionType.value = newBadge.condition_type
|
||||||
editBadgeConditionValue.value = newBadge.condition_value
|
editBadgeConditionValue.value = newBadge.condition_value
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NModal, NForm, NFormItem, NInput, NInputNumber, NSwitch, NButton, NFlex } from "naive-ui"
|
import {
|
||||||
|
NModal,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NSwitch,
|
||||||
|
NButton,
|
||||||
|
NFlex,
|
||||||
|
} from "naive-ui"
|
||||||
import { ProblemSetProblem } from "utils/types"
|
import { ProblemSetProblem } from "utils/types"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -9,12 +18,15 @@ interface Props {
|
|||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: "update:show", value: boolean): void
|
(e: "update:show", value: boolean): void
|
||||||
(e: "confirm", data: {
|
(
|
||||||
|
e: "confirm",
|
||||||
|
data: {
|
||||||
order: number
|
order: number
|
||||||
is_required: boolean
|
is_required: boolean
|
||||||
score: number
|
score: number
|
||||||
hint: string
|
hint: string
|
||||||
}): void
|
},
|
||||||
|
): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -30,7 +42,7 @@ function handleConfirm() {
|
|||||||
order: editProblemOrder.value,
|
order: editProblemOrder.value,
|
||||||
is_required: editProblemRequired.value,
|
is_required: editProblemRequired.value,
|
||||||
score: editProblemScore.value,
|
score: editProblemScore.value,
|
||||||
hint: editProblemHint.value,
|
hint: editProblemHint.value || "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,14 +51,18 @@ function handleCancel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 当问题数据变化时,更新表单数据
|
// 当问题数据变化时,更新表单数据
|
||||||
watch(() => props.problem, (newProblem) => {
|
watch(
|
||||||
|
() => props.problem,
|
||||||
|
(newProblem) => {
|
||||||
if (newProblem) {
|
if (newProblem) {
|
||||||
editProblemOrder.value = newProblem.order
|
editProblemOrder.value = newProblem.order
|
||||||
editProblemRequired.value = newProblem.is_required
|
editProblemRequired.value = newProblem.is_required
|
||||||
editProblemScore.value = newProblem.score
|
editProblemScore.value = newProblem.score
|
||||||
editProblemHint.value = newProblem.hint
|
editProblemHint.value = newProblem.hint || ""
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -19,11 +19,7 @@ defineEmits<Emits>()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-flex
|
<n-flex justify="space-between" align="center" style="margin-bottom: 16px">
|
||||||
justify="space-between"
|
|
||||||
align="center"
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
>
|
|
||||||
<h3>题目列表</h3>
|
<h3>题目列表</h3>
|
||||||
<n-button type="primary" @click="$emit('add-problem')">
|
<n-button type="primary" @click="$emit('add-problem')">
|
||||||
添加题目
|
添加题目
|
||||||
|
|||||||
@@ -60,11 +60,7 @@ const progressColumns = [
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-flex
|
<n-flex justify="space-between" align="center" style="margin-bottom: 16px">
|
||||||
justify="space-between"
|
|
||||||
align="center"
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
>
|
|
||||||
<h3>用户进度</h3>
|
<h3>用户进度</h3>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-data-table :columns="progressColumns" :data="progress" />
|
<n-data-table :columns="progressColumns" :data="progress" />
|
||||||
|
|||||||
@@ -249,10 +249,7 @@ onMounted(() => {
|
|||||||
@confirm="handleEditProblem"
|
@confirm="handleEditProblem"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddBadgeModal
|
<AddBadgeModal v-model:show="showAddBadgeModal" @confirm="handleAddBadge" />
|
||||||
v-model:show="showAddBadgeModal"
|
|
||||||
@confirm="handleAddBadge"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditBadgeModal
|
<EditBadgeModal
|
||||||
v-model:show="showEditBadgeModal"
|
v-model:show="showEditBadgeModal"
|
||||||
|
|||||||
@@ -148,7 +148,10 @@ watchDebounced(() => query.keyword, listProblemSets, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 监听其他查询条件变化
|
// 监听其他查询条件变化
|
||||||
watch(() => [query.page, query.limit, query.difficulty, query.status], listProblemSets)
|
watch(
|
||||||
|
() => [query.page, query.limit, query.difficulty, query.status],
|
||||||
|
listProblemSets,
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ const { problem } = storeToRefs(problemStore)
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const contestID = <string>route.params.contestID ?? ""
|
const contestID = <string>route.params.contestID ?? ""
|
||||||
const problemSetId = <string>route.params.problemSetId ?? ""
|
const problemSetId = <string>route.params.problemSetId ?? ""
|
||||||
console.log(problemSetId, "problemSetId")
|
|
||||||
|
const router = useRouter()
|
||||||
const [commentPanel] = useToggle()
|
const [commentPanel] = useToggle()
|
||||||
|
|
||||||
const { isDesktop } = useBreakpoints()
|
const { isDesktop } = useBreakpoints()
|
||||||
@@ -61,6 +62,19 @@ const { start: showCommentPanelDelayed } = useTimeoutFn(
|
|||||||
{ immediate: false },
|
{ immediate: false },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { start: goToProblemSetDelayed } = useTimeoutFn(
|
||||||
|
() => {
|
||||||
|
router.push({
|
||||||
|
name: "problemset",
|
||||||
|
params: {
|
||||||
|
problemSetId: problemSetId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
1500,
|
||||||
|
{ immediate: false },
|
||||||
|
)
|
||||||
|
|
||||||
// ==================== 计算属性 ====================
|
// ==================== 计算属性 ====================
|
||||||
// 按钮禁用逻辑
|
// 按钮禁用逻辑
|
||||||
const submitDisabled = computed(() => {
|
const submitDisabled = computed(() => {
|
||||||
@@ -121,15 +135,11 @@ watch(
|
|||||||
|
|
||||||
// 2. 创建ProblemSetSubmission记录,更新题单进度
|
// 2. 创建ProblemSetSubmission记录,更新题单进度
|
||||||
if (problemSetId) {
|
if (problemSetId) {
|
||||||
try {
|
|
||||||
await updateProblemSetProgress(
|
await updateProblemSetProgress(
|
||||||
Number(problemSetId),
|
Number(problemSetId),
|
||||||
problem.value!.id,
|
problem.value!.id,
|
||||||
submission.value!.id,
|
submission.value!.id,
|
||||||
)
|
)
|
||||||
} catch (error) {
|
|
||||||
console.error("更新题单进度失败:", error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 放烟花
|
// 3. 放烟花
|
||||||
@@ -139,6 +149,11 @@ watch(
|
|||||||
if (!contestID && !problemSetId) {
|
if (!contestID && !problemSetId) {
|
||||||
showCommentPanelDelayed()
|
showCommentPanelDelayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (problemSetId) {
|
||||||
|
// 延迟回到题单页面
|
||||||
|
goToProblemSetDelayed()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import {
|
|||||||
} from "shared/composables/websocket"
|
} from "shared/composables/websocket"
|
||||||
|
|
||||||
// API 和状态管理
|
// API 和状态管理
|
||||||
import { getCurrentProblemFlowchartSubmission, submitFlowchart, updateProblemSetProgress } from "oj/api"
|
import {
|
||||||
|
getCurrentProblemFlowchartSubmission,
|
||||||
|
submitFlowchart,
|
||||||
|
updateProblemSetProgress,
|
||||||
|
} from "oj/api"
|
||||||
import { useProblemStore } from "oj/store/problem"
|
import { useProblemStore } from "oj/store/problem"
|
||||||
|
|
||||||
// ==================== 类型定义 ====================
|
// ==================== 类型定义 ====================
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export function useSubmissionMonitor() {
|
|||||||
// ==================== 启动监控 ====================
|
// ==================== 启动监控 ====================
|
||||||
const startMonitoring = (id: string) => {
|
const startMonitoring = (id: string) => {
|
||||||
submissionId.value = id
|
submissionId.value = id
|
||||||
submission.value = { result: 9 } as Submission // 9 = submitting
|
submission.value = { id, result: 9 } as Submission // 9 = submitting
|
||||||
|
|
||||||
// 取消之前的断开计划
|
// 取消之前的断开计划
|
||||||
cancelScheduledDisconnect()
|
cancelScheduledDisconnect()
|
||||||
@@ -116,14 +116,17 @@ export function useSubmissionMonitor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 等待WebSocket连接并订阅
|
// 等待WebSocket连接并订阅
|
||||||
const unwatch = watch(
|
let unwatch: (() => void) | null = null
|
||||||
|
unwatch = watch(
|
||||||
wsStatus,
|
wsStatus,
|
||||||
(status) => {
|
(status) => {
|
||||||
if (status === "connected") {
|
if (status === "connected") {
|
||||||
console.log("[SubmissionMonitor] WebSocket已连接,订阅提交:", id)
|
console.log("[SubmissionMonitor] WebSocket已连接,订阅提交:", id)
|
||||||
subscribe(id)
|
subscribe(id)
|
||||||
|
if (unwatch) {
|
||||||
unwatch() // 订阅成功后停止监听
|
unwatch() // 订阅成功后停止监听
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,17 +6,23 @@ import {
|
|||||||
joinProblemSet,
|
joinProblemSet,
|
||||||
getUserBadges,
|
getUserBadges,
|
||||||
} from "../api"
|
} from "../api"
|
||||||
import { getTagColor, getACRate } from "utils/functions"
|
import { getTagColor } from "utils/functions"
|
||||||
import { ProblemSet, ProblemSetProblem, UserBadge as UserBadgeType } from "utils/types"
|
import {
|
||||||
|
ProblemSet,
|
||||||
|
ProblemSetProblem,
|
||||||
|
UserBadge as UserBadgeType,
|
||||||
|
} from "utils/types"
|
||||||
import { DIFFICULTY } from "utils/constants"
|
import { DIFFICULTY } from "utils/constants"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import UserBadge from "shared/components/UserBadge.vue"
|
import UserBadge from "shared/components/UserBadge.vue"
|
||||||
|
import { useFireworks } from "../problem/composables/useFireworks"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const { isDesktop } = useBreakpoints()
|
const { isDesktop } = useBreakpoints()
|
||||||
|
const { celebrate } = useFireworks()
|
||||||
|
|
||||||
const problemSetId = computed(() => Number(route.params.problemSetId))
|
const problemSetId = computed(() => Number(route.params.problemSetId))
|
||||||
|
|
||||||
@@ -25,20 +31,6 @@ const problems = ref<ProblemSetProblem[]>([])
|
|||||||
const isJoined = ref(false)
|
const isJoined = ref(false)
|
||||||
const isJoining = ref(false)
|
const isJoining = ref(false)
|
||||||
const userBadges = ref<UserBadgeType[]>([])
|
const userBadges = ref<UserBadgeType[]>([])
|
||||||
const isLoadingBadges = ref(false)
|
|
||||||
|
|
||||||
// 刷新题单详情的函数
|
|
||||||
async function refreshProblemSetDetail() {
|
|
||||||
try {
|
|
||||||
const res = await getProblemSetDetail(problemSetId.value)
|
|
||||||
problemSet.value = res.data
|
|
||||||
// 更新加入状态
|
|
||||||
isJoined.value = res.data.user_progress?.is_joined || false
|
|
||||||
console.log(`[ProblemSet] 题单详情已刷新: ${problemSet.value?.title}`)
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error("刷新题单详情失败:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDifficultyTag(difficulty: string) {
|
function getDifficultyTag(difficulty: string) {
|
||||||
const difficultyMap: Record<
|
const difficultyMap: Record<
|
||||||
@@ -64,40 +56,47 @@ function getProgressPercentage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadProblemSetDetail() {
|
async function loadProblemSetDetail() {
|
||||||
try {
|
|
||||||
const res = await getProblemSetDetail(problemSetId.value)
|
const res = await getProblemSetDetail(problemSetId.value)
|
||||||
problemSet.value = res.data
|
problemSet.value = res.data
|
||||||
// 更新加入状态
|
|
||||||
isJoined.value = res.data.user_progress?.is_joined || false
|
isJoined.value = res.data.user_progress?.is_joined || false
|
||||||
} catch (err: any) {
|
|
||||||
message.error("加载题单详情失败:" + (err.data || "未知错误"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadProblems() {
|
async function loadProblems() {
|
||||||
try {
|
|
||||||
const res = await getProblemSetProblems(problemSetId.value)
|
const res = await getProblemSetProblems(problemSetId.value)
|
||||||
problems.value = res.data
|
problems.value = res.data
|
||||||
} catch (err: any) {
|
|
||||||
message.error("加载题目列表失败:" + (err.data || "未知错误"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadUserBadges() {
|
async function loadUserBadges() {
|
||||||
if (!isJoined.value) return
|
if (!isJoined.value) return
|
||||||
|
|
||||||
isLoadingBadges.value = true
|
|
||||||
try {
|
|
||||||
const res = await getUserBadges()
|
const res = await getUserBadges()
|
||||||
// 只显示当前题单的徽章
|
userBadges.value = res.data.filter(
|
||||||
userBadges.value = res.data.filter((badge: UserBadgeType) =>
|
(badge: UserBadgeType) => badge.badge.problemset === problemSetId.value,
|
||||||
badge.badge.problemset === problemSetId.value
|
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
}
|
||||||
console.error("加载用户徽章失败:", err)
|
|
||||||
} finally {
|
async function init() {
|
||||||
isLoadingBadges.value = false
|
await Promise.all([loadProblemSetDetail(), loadProblems()])
|
||||||
|
if (isJoined.value) {
|
||||||
|
if (problemSet.value?.user_progress?.is_completed) {
|
||||||
|
celebrate()
|
||||||
}
|
}
|
||||||
|
loadUserBadges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleProblemClick(problemId: string) {
|
||||||
|
if (!isJoined.value) {
|
||||||
|
message.warning("请先点击【加入题单】按钮!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push({
|
||||||
|
name: "problemset problem",
|
||||||
|
params: {
|
||||||
|
problemSetId: problemSetId.value,
|
||||||
|
problemID: problemId,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleJoinProblemSet() {
|
async function handleJoinProblemSet() {
|
||||||
@@ -117,30 +116,7 @@ async function handleJoinProblemSet() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(init)
|
||||||
await Promise.all([loadProblemSetDetail(), loadProblems()])
|
|
||||||
// 如果已加入题单,加载用户徽章
|
|
||||||
if (isJoined.value) {
|
|
||||||
await loadUserBadges()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听路由变化,当从题单题目页面返回时刷新题单详情
|
|
||||||
watch(
|
|
||||||
() => route.path,
|
|
||||||
(newPath, oldPath) => {
|
|
||||||
// 如果从题单题目页面返回到题单详情页面,刷新题单详情
|
|
||||||
if (
|
|
||||||
oldPath?.includes("/problem/") &&
|
|
||||||
newPath === `/problemset/${problemSetId.value}` &&
|
|
||||||
isJoined.value
|
|
||||||
) {
|
|
||||||
refreshProblemSetDetail()
|
|
||||||
// 刷新用户徽章
|
|
||||||
loadUserBadges()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -165,6 +141,16 @@ watch(
|
|||||||
</n-flex>
|
</n-flex>
|
||||||
|
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
|
<!-- 用户徽章显示区域 - 只在已加入且有徽章时显示 -->
|
||||||
|
<n-flex v-if="isJoined && userBadges.length > 0" align="center">
|
||||||
|
<n-text>已获徽章</n-text>
|
||||||
|
<UserBadge
|
||||||
|
v-for="badge in userBadges"
|
||||||
|
:key="badge.id"
|
||||||
|
:badge="badge"
|
||||||
|
/>
|
||||||
|
</n-flex>
|
||||||
|
|
||||||
<!-- 完成进度 - 只在已加入时显示 -->
|
<!-- 完成进度 - 只在已加入时显示 -->
|
||||||
<n-flex align="center" v-if="isJoined">
|
<n-flex align="center" v-if="isJoined">
|
||||||
<n-text strong>完成进度</n-text>
|
<n-text strong>完成进度</n-text>
|
||||||
@@ -198,24 +184,6 @@ watch(
|
|||||||
</n-flex>
|
</n-flex>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- 用户徽章显示区域 -->
|
|
||||||
<n-card v-if="isJoined && userBadges.length > 0" style="margin-bottom: 24px">
|
|
||||||
<template #header>
|
|
||||||
<n-flex align="center">
|
|
||||||
<Icon icon="material-symbols:emoji-events" width="20" />
|
|
||||||
<n-text strong>我的徽章</n-text>
|
|
||||||
<n-spin v-if="isLoadingBadges" size="small" />
|
|
||||||
</n-flex>
|
|
||||||
</template>
|
|
||||||
<n-flex :wrap="true" :gap="12">
|
|
||||||
<UserBadge
|
|
||||||
v-for="badge in userBadges"
|
|
||||||
:key="badge.id"
|
|
||||||
:badge="badge"
|
|
||||||
/>
|
|
||||||
</n-flex>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<!-- 题目列表 -->
|
<!-- 题目列表 -->
|
||||||
<n-grid :cols="isDesktop ? 4 : 1" :x-gap="16" :y-gap="16">
|
<n-grid :cols="isDesktop ? 4 : 1" :x-gap="16" :y-gap="16">
|
||||||
<n-grid-item
|
<n-grid-item
|
||||||
@@ -224,12 +192,12 @@ watch(
|
|||||||
>
|
>
|
||||||
<n-card
|
<n-card
|
||||||
hoverable
|
hoverable
|
||||||
@click="goToProblem(problemSetProblem.problem._id)"
|
@click="handleProblemClick(problemSetProblem.problem._id)"
|
||||||
style="cursor: pointer"
|
style="cursor: pointer"
|
||||||
>
|
>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<Icon
|
<Icon
|
||||||
style="margin-right: 12px;"
|
style="margin-right: 12px"
|
||||||
width="50"
|
width="50"
|
||||||
icon="noto:check-mark-button"
|
icon="noto:check-mark-button"
|
||||||
v-if="problemSetProblem.is_completed"
|
v-if="problemSetProblem.is_completed"
|
||||||
@@ -237,7 +205,7 @@ watch(
|
|||||||
|
|
||||||
<n-flex vertical style="flex: 1">
|
<n-flex vertical style="flex: 1">
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<n-h4 style="margin: 0;">#{{ index + 1 }}</n-h4>
|
<n-h4 style="margin: 0">#{{ index + 1 }}</n-h4>
|
||||||
|
|
||||||
<n-h4 style="margin: 0">
|
<n-h4 style="margin: 0">
|
||||||
{{ problemSetProblem.problem.title }}
|
{{ problemSetProblem.problem.title }}
|
||||||
|
|||||||
@@ -162,7 +162,11 @@ watch(
|
|||||||
<n-tag type="warning" v-if="problemSet.status === 'archived'">
|
<n-tag type="warning" v-if="problemSet.status === 'archived'">
|
||||||
已归档
|
已归档
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag v-if="problemSet.user_progress?.is_joined" type="success" size="small">
|
<n-tag
|
||||||
|
v-if="problemSet.user_progress?.is_joined"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Icon icon="material-symbols:check-circle" width="12" />
|
<Icon icon="material-symbols:check-circle" width="12" />
|
||||||
</template>
|
</template>
|
||||||
@@ -173,17 +177,28 @@ watch(
|
|||||||
|
|
||||||
<!-- 用户进度显示 -->
|
<!-- 用户进度显示 -->
|
||||||
<div v-if="problemSet.user_progress?.is_joined">
|
<div v-if="problemSet.user_progress?.is_joined">
|
||||||
<n-flex align="center" justify="space-between" style="margin-bottom: 8px">
|
<n-flex
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
style="margin-bottom: 8px"
|
||||||
|
>
|
||||||
<n-text depth="3" style="font-size: 12px">
|
<n-text depth="3" style="font-size: 12px">
|
||||||
我的进度: {{ problemSet.user_progress.completed_count }} / {{ problemSet.user_progress.total_count }}
|
我的进度: {{ problemSet.user_progress.completed_count }} /
|
||||||
|
{{ problemSet.user_progress.total_count }}
|
||||||
</n-text>
|
</n-text>
|
||||||
<n-progress
|
<n-progress
|
||||||
type="line"
|
type="line"
|
||||||
:percentage="Math.round(problemSet.user_progress.progress_percentage)"
|
:percentage="
|
||||||
|
Math.round(problemSet.user_progress.progress_percentage)
|
||||||
|
"
|
||||||
:height="4"
|
:height="4"
|
||||||
:border-radius="2"
|
:border-radius="2"
|
||||||
style="width: 100px"
|
style="width: 100px"
|
||||||
:color="getProgressColor(problemSet.user_progress.progress_percentage)"
|
:color="
|
||||||
|
getProgressColor(
|
||||||
|
problemSet.user_progress.progress_percentage,
|
||||||
|
)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const props = defineProps<{
|
|||||||
hideList?: boolean
|
hideList?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
@@ -76,8 +77,8 @@ async function copyToProblem() {
|
|||||||
message.error("代码复制失败")
|
message.error("代码复制失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否是竞赛题目
|
|
||||||
const contestID = submission.value!.contest
|
const contestID = submission.value!.contest
|
||||||
|
const problemSetId = <string>route.params.problemSetId ?? ""
|
||||||
if (contestID) {
|
if (contestID) {
|
||||||
// 竞赛题目
|
// 竞赛题目
|
||||||
router.push({
|
router.push({
|
||||||
@@ -87,6 +88,15 @@ async function copyToProblem() {
|
|||||||
problemID: props.problemID,
|
problemID: props.problemID,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} else if (problemSetId) {
|
||||||
|
// 题单题目
|
||||||
|
router.push({
|
||||||
|
name: "problemset problem",
|
||||||
|
params: {
|
||||||
|
problemSetId: problemSetId,
|
||||||
|
problemID: props.problemID,
|
||||||
|
},
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// 普通题目
|
// 普通题目
|
||||||
router.push({
|
router.push({
|
||||||
|
|||||||
@@ -215,7 +215,6 @@ onUnmounted(() => {
|
|||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 节点内容区域 */
|
/* 节点内容区域 */
|
||||||
.node-content {
|
.node-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ const menus = computed<MenuOption[]>(() => [
|
|||||||
icon: renderIcon("streamline-emojis:blossom"),
|
icon: renderIcon("streamline-emojis:blossom"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () => h(RouterLink, { to: "/problemset" }, { default: () => "题单" }),
|
label: () =>
|
||||||
|
h(RouterLink, { to: "/problemset" }, { default: () => "题单" }),
|
||||||
key: "problemset",
|
key: "problemset",
|
||||||
icon: renderIcon("streamline-emojis:green-book"),
|
icon: renderIcon("streamline-emojis:green-book"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-tooltip trigger="hover" :show-arrow="false">
|
<n-popover trigger="hover" placement="top">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div class="badge-container">
|
<div class="badge-container">
|
||||||
<img
|
<img
|
||||||
@@ -10,17 +10,21 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="badge-tooltip">
|
<n-card size="small" class="badge-popover">
|
||||||
<div class="badge-name">{{ badge.badge.name }}</div>
|
<n-flex vertical>
|
||||||
<div class="badge-description">{{ badge.badge.description }}</div>
|
<n-text strong>{{ badge.badge.name }}</n-text>
|
||||||
<div class="badge-time">获得时间:{{ formatTime(badge.earned_time) }}</div>
|
<n-tag type="info"> 获得条件:{{ getConditionText() }} </n-tag>
|
||||||
</div>
|
<n-text depth="3">
|
||||||
</n-tooltip>
|
获得时间:{{ parseTime(badge.earned_time, "YYYY-MM-DD HH:mm:ss") }}
|
||||||
|
</n-text>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
</n-popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { UserBadge } from "utils/types"
|
import { UserBadge } from "utils/types"
|
||||||
|
import { parseTime } from "utils/functions"
|
||||||
interface Props {
|
interface Props {
|
||||||
badge: UserBadge
|
badge: UserBadge
|
||||||
}
|
}
|
||||||
@@ -30,17 +34,22 @@ const props = defineProps<Props>()
|
|||||||
function handleImageError(event: Event) {
|
function handleImageError(event: Event) {
|
||||||
const img = event.target as HTMLImageElement
|
const img = event.target as HTMLImageElement
|
||||||
// 如果图片加载失败,显示默认图标
|
// 如果图片加载失败,显示默认图标
|
||||||
img.src = '/badge-1.png' // 使用默认徽章图标
|
img.src = "/badge-1.png" // 使用默认徽章图标
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(date: Date) {
|
function getConditionText() {
|
||||||
return new Date(date).toLocaleString('zh-CN', {
|
const { condition_type, condition_value } = props.badge.badge
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
switch (condition_type) {
|
||||||
day: '2-digit',
|
case "all_problems":
|
||||||
hour: '2-digit',
|
return "完成所有题目"
|
||||||
minute: '2-digit'
|
case "problem_count":
|
||||||
})
|
return `完成 ${condition_value} 道题目`
|
||||||
|
case "score":
|
||||||
|
return `获得 ${condition_value} 分`
|
||||||
|
default:
|
||||||
|
return "未知条件"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -49,6 +58,8 @@ function formatTime(date: Date) {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-icon {
|
.badge-icon {
|
||||||
@@ -66,27 +77,11 @@ function formatTime(date: Date) {
|
|||||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-popover {
|
||||||
.badge-tooltip {
|
max-width: 280px;
|
||||||
max-width: 250px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-name {
|
.badge-popover .n-space {
|
||||||
font-weight: bold;
|
gap: 6px;
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-description {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-time {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #999;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ export function useMermaid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 渲染流程图的函数
|
// 渲染流程图的函数
|
||||||
const renderFlowchart = async (container: HTMLElement | null, mermaidCode: string) => {
|
const renderFlowchart = async (
|
||||||
|
container: HTMLElement | null,
|
||||||
|
mermaidCode: string,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
renderError.value = null
|
renderError.value = null
|
||||||
|
|
||||||
@@ -37,7 +40,9 @@ export function useMermaid() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
renderError.value =
|
renderError.value =
|
||||||
error instanceof Error ? error.message : "流程图渲染失败,请检查代码格式"
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "流程图渲染失败,请检查代码格式"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -610,5 +610,3 @@ export interface DetailsData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Grade = "S" | "A" | "B" | "C"
|
export type Grade = "S" | "A" | "B" | "C"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user