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" v-for="item in challenges"
:key="item.display" :key="item.display"
hoverable hoverable
class="challenge-card" :class="['challenge-card', { submitted: item.submitted }]"
@click="select(item)" @click="select(item)"
> >
<template #header> <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>
<template #header-extra> <template #header-extra>
<n-flex :size="6">
<n-tag type="warning" size="small">{{ item.score }} </n-tag> <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> </template>
</n-card> </n-card>
</n-flex> </n-flex>
@@ -47,4 +53,19 @@ onMounted(async () => {
.challenge-card { .challenge-card {
cursor: pointer; 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> </style>

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import { marked } from "marked"
import copyFn from "copy-text-to-clipboard" import copyFn from "copy-text-to-clipboard"
import { css, html, js, tab } from "../store/editors" import { css, html, js, tab } from "../store/editors"
import { Tutorial } from "../api" import { Tutorial } from "../api"
import { step } from "../store/tutorial" import { step, tutorialIds } from "../store/tutorial"
import { taskId } from "../store/task" import { taskId } from "../store/task"
import { useRouter } from "vue-router" import { useRouter } from "vue-router"
@@ -33,32 +33,9 @@ marked.use({
}) })
const router = useRouter() const router = useRouter()
const tutorialIds = ref<number[]>([])
const content = ref("") const content = ref("")
const $content = useTemplateRef<any>("$content") 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() { async function prepare() {
tutorialIds.value = await Tutorial.listDisplay() tutorialIds.value = await Tutorial.listDisplay()
if (!tutorialIds.value.length) { if (!tutorialIds.value.length) {
@@ -73,7 +50,7 @@ async function prepare() {
async function render() { async function render() {
const data = await Tutorial.get(step.value) const data = await Tutorial.get(step.value)
taskId.value = data.task_ptr 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 }) content.value = await marked.parse(merged, { async: true })
} }

View File

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

View File

@@ -1,6 +1,6 @@
<template> <template>
<n-split <n-split
:size="tutorialSize" :size="panelSize"
@update-size="changeSize" @update-size="changeSize"
min="400px" min="400px"
max="900px" max="900px"
@@ -25,7 +25,7 @@ import { useMagicKeys, whenever } from "@vueuse/core"
import Editors from "../components/Editors.vue" import Editors from "../components/Editors.vue"
import Preview from "../components/Preview.vue" import Preview from "../components/Preview.vue"
import Task from "../components/Task.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" import { html, css, js } from "../store/editors"
const { ctrl_s } = useMagicKeys({ const { ctrl_s } = useMagicKeys({
@@ -43,12 +43,12 @@ const { ctrl_r } = useMagicKeys({
}) })
function changeSize(n: number) { function changeSize(n: number) {
tutorialSize.value = n panelSize.value = n
if (n > 0) show.value = true if (n > 0) show.value = true
} }
function hide() { function hide() {
tutorialSize.value = 0 panelSize.value = 0
show.value = false 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 { ref } from "vue"
import { TASK_TYPE } from "../utils/const" import { TASK_TYPE } from "../utils/const"
const urlParams = new URLSearchParams(window.location.search) const currentTask = window.location.pathname.startsWith("/challenge")
const currentTask = (urlParams.get("task") as TASK_TYPE) ?? TASK_TYPE.Tutorial ? TASK_TYPE.Challenge
: TASK_TYPE.Tutorial
export const taskTab = ref(currentTask) export const taskTab = ref(currentTask)
export const taskId = ref(0) export const taskId = ref(0)

View File

@@ -4,6 +4,25 @@ const urlParams = new URLSearchParams(window.location.search)
const currentStep = urlParams.get("step") ?? "1" const currentStep = urlParams.get("step") ?? "1"
export const step = ref(Number(currentStep)) export const step = ref(Number(currentStep))
export const tutorialIds = ref<number[]>([])
export const show = ref(true) export function prevDisabled(): boolean {
export const tutorialSize = ref(2 / 5) 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 display: number
title: string title: string
score: number score: number
pass_score: number | null
submitted: boolean
is_public: boolean is_public: boolean
} }