refactor
This commit is contained in:
@@ -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-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>
|
</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
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 { 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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user