refactor
This commit is contained in:
@@ -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-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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ function clearAll() {
|
||||
|
||||
function back() {
|
||||
disconnectPrompt()
|
||||
taskId.value = 0
|
||||
router.push({ name: "home-challenge-list" })
|
||||
}
|
||||
|
||||
|
||||
@@ -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
5
src/store/panel.ts
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ export interface ChallengeSlim {
|
||||
display: number
|
||||
title: string
|
||||
score: number
|
||||
pass_score: number | null
|
||||
submitted: boolean
|
||||
is_public: boolean
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user