refactor
Some checks failed
Deploy / deploy (build, debian, 22) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822) (push) Has been cancelled

This commit is contained in:
2026-03-31 07:41:35 -06:00
parent 91e1b2b48b
commit e539f9450a
10 changed files with 101 additions and 70 deletions

View File

@@ -6,14 +6,20 @@
v-for="item in challenges"
:key="item.display"
hoverable
class="challenge-card"
:class="['challenge-card', { submitted: item.submitted }]"
@click="select(item)"
>
<template #header>
{{ item.title }}
<n-flex align="center" :size="6">
<span v-if="item.submitted" class="check-icon"></span>
<span :class="{ 'submitted-title': item.submitted }">{{ item.title }}</span>
</n-flex>
</template>
<template #header-extra>
<n-tag type="warning" size="small">{{ item.score }}</n-tag>
<n-flex :size="6">
<n-tag type="warning" size="small">{{ item.score }} </n-tag>
<n-tag v-if="item.pass_score != null" size="small">及格 {{ item.pass_score }} </n-tag>
</n-flex>
</template>
</n-card>
</n-flex>
@@ -47,4 +53,19 @@ onMounted(async () => {
.challenge-card {
cursor: pointer;
}
.challenge-card.submitted {
background-color: #f6ffed;
border-color: #b7eb8f;
}
.check-icon {
color: #52c41a;
font-size: 14px;
font-weight: bold;
}
.submitted-title {
color: #888;
}
</style>

View File

@@ -21,18 +21,11 @@
<n-tab name="challenge" tab="挑战"></n-tab>
</n-tabs>
<template v-if="!hideNav">
<n-button
text
@click="tutorialRef?.prev()"
:disabled="tutorialRef?.prevDisabled()"
>
<n-button text @click="prev()" :disabled="prevDisabled()">
<Icon :width="24" icon="pepicons-pencil:arrow-left"></Icon>
</n-button>
<n-button
text
@click="tutorialRef?.next()"
:disabled="tutorialRef?.nextDisabled()"
>
<span v-if="progressText" class="progress-text">{{ progressText }}</span>
<n-button text @click="next()" :disabled="nextDisabled()">
<Icon :width="24" icon="pepicons-pencil:arrow-right"></Icon>
</n-button>
</template>
@@ -56,16 +49,16 @@
</n-button>
</n-flex>
</n-flex>
<Tutorial v-if="taskTab === TASK_TYPE.Tutorial" ref="tutorialRef" />
<Tutorial v-if="taskTab === TASK_TYPE.Tutorial" />
<Challenge v-else />
</div>
<TaskStatsModal v-model:show="statsModal" :task-id="taskId" />
</template>
<script lang="ts" setup>
import { Icon } from "@iconify/vue"
import { computed, onMounted, ref } from "vue"
import { step } from "../store/tutorial"
import { authed, roleAdmin, roleSuper } from "../store/user"
import { computed, ref } from "vue"
import { step, tutorialIds, prev, next, prevDisabled, nextDisabled } from "../store/tutorial"
import { authed, roleSuper } from "../store/user"
import { taskTab, taskId, challengeDisplay } from "../store/task"
import { useRoute, useRouter } from "vue-router"
import { TASK_TYPE } from "../utils/const"
@@ -75,18 +68,35 @@ import TaskStatsModal from "./TaskStatsModal.vue"
const route = useRoute()
const router = useRouter()
const tutorialRef = ref<InstanceType<typeof Tutorial>>()
const statsModal = ref(false)
// 路由同步:在 setup 阶段立即执行,不等 onMounted
const routeName = route.name as string
if (routeName.startsWith("home-tutorial")) {
taskTab.value = TASK_TYPE.Tutorial
if (route.params.display) step.value = Number(route.params.display)
} else if (routeName.startsWith("home-challenge")) {
taskTab.value = TASK_TYPE.Challenge
if (route.params.display)
challengeDisplay.value = Number(route.params.display)
}
defineEmits(["hide"])
const hideNav = computed(
() =>
taskTab.value !== TASK_TYPE.Tutorial ||
(tutorialRef.value?.tutorialIds?.length ?? 0) <= 1,
taskTab.value !== TASK_TYPE.Tutorial || tutorialIds.value.length <= 1,
)
const progressText = computed(() => {
const ids = tutorialIds.value
if (!ids.length) return ""
const i = ids.indexOf(step.value)
return i === -1 ? "" : `${i + 1} / ${ids.length}`
})
function changeTab(v: TASK_TYPE) {
taskId.value = 0
taskTab.value = v
if (v === TASK_TYPE.Tutorial) {
router.push(
@@ -109,20 +119,6 @@ function edit() {
taskTab.value === TASK_TYPE.Tutorial ? step.value : challengeDisplay.value
router.push({ name, params: { display } })
}
function init() {
const name = route.name as string
if (name.startsWith("home-tutorial")) {
taskTab.value = TASK_TYPE.Tutorial
if (route.params.display) step.value = Number(route.params.display)
} else if (name.startsWith("home-challenge")) {
taskTab.value = TASK_TYPE.Challenge
if (route.params.display)
challengeDisplay.value = Number(route.params.display)
}
}
onMounted(init)
</script>
<style scoped>
.container {
@@ -138,4 +134,12 @@ onMounted(init)
border-bottom: 1px solid rgb(239, 239, 245);
box-sizing: border-box;
}
.progress-text {
font-size: 12px;
color: #999;
min-width: 36px;
text-align: center;
user-select: none;
}
</style>

View File

@@ -31,8 +31,9 @@ import { computed, h } from "vue"
import { Icon } from "@iconify/vue"
import { authed, roleNormal, roleSuper, user } from "../store/user"
import { loginModal } from "../store/modal"
import { show, tutorialSize, step } from "../store/tutorial"
import { taskId, taskTab } from "../store/task"
import { show, panelSize } from "../store/panel"
import { step } from "../store/tutorial"
import { taskId } from "../store/task"
import { Account } from "../api"
import { Role } from "../utils/type"
import { router } from "../router"
@@ -90,7 +91,7 @@ const menu = computed(() => [
function showTutorial() {
show.value = true
tutorialSize.value = 2 / 5
panelSize.value = 2 / 5
}
function clickMenu(name: string) {

View File

@@ -7,7 +7,7 @@ import { marked } from "marked"
import copyFn from "copy-text-to-clipboard"
import { css, html, js, tab } from "../store/editors"
import { Tutorial } from "../api"
import { step } from "../store/tutorial"
import { step, tutorialIds } from "../store/tutorial"
import { taskId } from "../store/task"
import { useRouter } from "vue-router"
@@ -33,32 +33,9 @@ marked.use({
})
const router = useRouter()
const tutorialIds = ref<number[]>([])
const content = ref("")
const $content = useTemplateRef<any>("$content")
const prevDisabled = () => {
const i = tutorialIds.value.indexOf(step.value)
return i <= 0
}
const nextDisabled = () => {
const i = tutorialIds.value.indexOf(step.value)
return i === tutorialIds.value.length - 1
}
function prev() {
const i = tutorialIds.value.indexOf(step.value)
step.value = tutorialIds.value[i - 1] as number
}
function next() {
const i = tutorialIds.value.indexOf(step.value)
step.value = tutorialIds.value[i + 1] as number
}
defineExpose({ tutorialIds, prevDisabled, nextDisabled, prev, next })
async function prepare() {
tutorialIds.value = await Tutorial.listDisplay()
if (!tutorialIds.value.length) {
@@ -73,7 +50,7 @@ async function prepare() {
async function render() {
const data = await Tutorial.get(step.value)
taskId.value = data.task_ptr
const merged = `# #${data.display} ${data.title}\n${data.content}`
const merged = `# ${data.display}. ${data.title}\n${data.content}`
content.value = await marked.parse(merged, { async: true })
}

View File

@@ -134,6 +134,7 @@ function clearAll() {
function back() {
disconnectPrompt()
taskId.value = 0
router.push({ name: "home-challenge-list" })
}

View File

@@ -1,6 +1,6 @@
<template>
<n-split
:size="tutorialSize"
:size="panelSize"
@update-size="changeSize"
min="400px"
max="900px"
@@ -25,7 +25,7 @@ import { useMagicKeys, whenever } from "@vueuse/core"
import Editors from "../components/Editors.vue"
import Preview from "../components/Preview.vue"
import Task from "../components/Task.vue"
import { show, tutorialSize } from "../store/tutorial"
import { show, panelSize } from "../store/panel"
import { html, css, js } from "../store/editors"
const { ctrl_s } = useMagicKeys({
@@ -43,12 +43,12 @@ const { ctrl_r } = useMagicKeys({
})
function changeSize(n: number) {
tutorialSize.value = n
panelSize.value = n
if (n > 0) show.value = true
}
function hide() {
tutorialSize.value = 0
panelSize.value = 0
show.value = false
}

5
src/store/panel.ts Normal file
View File

@@ -0,0 +1,5 @@
// client/src/store/panel.ts
import { ref } from "vue"
export const show = ref(true)
export const panelSize = ref(2 / 5)

View File

@@ -1,8 +1,9 @@
import { ref } from "vue"
import { TASK_TYPE } from "../utils/const"
const urlParams = new URLSearchParams(window.location.search)
const currentTask = (urlParams.get("task") as TASK_TYPE) ?? TASK_TYPE.Tutorial
const currentTask = window.location.pathname.startsWith("/challenge")
? TASK_TYPE.Challenge
: TASK_TYPE.Tutorial
export const taskTab = ref(currentTask)
export const taskId = ref(0)

View File

@@ -4,6 +4,25 @@ const urlParams = new URLSearchParams(window.location.search)
const currentStep = urlParams.get("step") ?? "1"
export const step = ref(Number(currentStep))
export const tutorialIds = ref<number[]>([])
export const show = ref(true)
export const tutorialSize = ref(2 / 5)
export function prevDisabled(): boolean {
const i = tutorialIds.value.indexOf(step.value)
return i <= 0
}
export function nextDisabled(): boolean {
const i = tutorialIds.value.indexOf(step.value)
return i === -1 || i === tutorialIds.value.length - 1
}
export function prev(): void {
const i = tutorialIds.value.indexOf(step.value)
if (i > 0) step.value = tutorialIds.value[i - 1] as number
}
export function next(): void {
const i = tutorialIds.value.indexOf(step.value)
if (i !== -1 && i < tutorialIds.value.length - 1)
step.value = tutorialIds.value[i + 1] as number
}

View File

@@ -47,6 +47,8 @@ export interface ChallengeSlim {
display: number
title: string
score: number
pass_score: number | null
submitted: boolean
is_public: boolean
}