fix
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-26 08:11:08 -06:00
parent 4b58cee204
commit 597f1f0f93
14 changed files with 408 additions and 532 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,19 +1,14 @@
<template> <template>
<div class="container" v-if="taskTab === TASK_TYPE.Challenge"> <div class="container" v-if="taskTab === TASK_TYPE.Challenge">
<template v-if="currentChallenge">
<n-flex align="center" style="margin-bottom: 12px">
<n-button text @click="back">
<Icon :width="20" icon="pepicons-pencil:arrow-left"></Icon>
</n-button>
<span style="font-weight: bold">返回挑战列表</span>
</n-flex>
<div class="markdown-body" v-html="content" />
</template>
<template v-else>
<n-empty v-if="!challenges.length">暂无挑战敬请期待</n-empty> <n-empty v-if="!challenges.length">暂无挑战敬请期待</n-empty>
<n-grid v-else :cols="3" x-gap="12" y-gap="12"> <n-flex v-else vertical :size="12">
<n-gi v-for="item in challenges" :key="item.display"> <n-card
<n-card hoverable class="challenge-card" @click="select(item)"> v-for="item in challenges"
:key="item.display"
hoverable
class="challenge-card"
@click="select(item)"
>
<template #header> <template #header>
{{ item.title }} {{ item.title }}
</template> </template>
@@ -21,58 +16,27 @@
<n-tag type="warning" size="small">{{ item.score }}</n-tag> <n-tag type="warning" size="small">{{ item.score }}</n-tag>
</template> </template>
</n-card> </n-card>
</n-gi> </n-flex>
</n-grid>
</template>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue" import { ref, onMounted } from "vue"
import { Icon } from "@iconify/vue"
import { marked } from "marked"
import { useRouter } from "vue-router" import { useRouter } from "vue-router"
import { Challenge } from "../api" import { Challenge } from "../api"
import { taskTab, taskId, challengeDisplay } from "../store/task" import { taskTab } from "../store/task"
import { TASK_TYPE } from "../utils/const" import { TASK_TYPE } from "../utils/const"
import type { ChallengeSlim } from "../utils/type" import type { ChallengeSlim } from "../utils/type"
const router = useRouter() const router = useRouter()
const challenges = ref<ChallengeSlim[]>([]) const challenges = ref<ChallengeSlim[]>([])
const currentChallenge = ref<ChallengeSlim | null>(null)
const content = ref("")
async function loadList() { function select(item: ChallengeSlim) {
challenges.value = await Challenge.listDisplay()
// 从 URL 恢复选中状态
if (challengeDisplay.value) {
const item = challenges.value.find(
(c) => c.display === challengeDisplay.value,
)
if (item) await select(item, false)
}
}
async function select(item: ChallengeSlim, updateUrl = true) {
currentChallenge.value = item
challengeDisplay.value = item.display
if (updateUrl) {
router.push({ name: "home-challenge", params: { display: item.display } }) router.push({ name: "home-challenge", params: { display: item.display } })
} }
const data = await Challenge.get(item.display)
taskId.value = data.task_ptr
const merged = `# #${data.display} ${data.title}\n${data.content}`
content.value = await marked.parse(merged, { async: true })
}
function back() { onMounted(async () => {
currentChallenge.value = null challenges.value = await Challenge.listDisplay()
challengeDisplay.value = 0 })
taskId.value = 0
content.value = ""
router.push({ name: "home-challenge-list" })
}
onMounted(loadList)
</script> </script>
<style scoped> <style scoped>
.container { .container {

View File

@@ -62,7 +62,6 @@
<n-flex align="center"> <n-flex align="center">
<span class="label">预加载</span> <span class="label">预加载</span>
<n-tag type="success">Normalize.css</n-tag> <n-tag type="success">Normalize.css</n-tag>
<n-tag type="success">Tailwind CSS</n-tag>
</n-flex> </n-flex>
</n-flex> </n-flex>
</n-tab-pane> </n-tab-pane>

View File

@@ -64,7 +64,6 @@ function getContent() {
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>${props.css}</style> <style>${props.css}</style>
<link rel="stylesheet" href="/normalize.min.css" /> <link rel="stylesheet" href="/normalize.min.css" />
<script src="/tailwindcss.min.js"><\/script>
</head> </head>
<body> <body>
${props.html} ${props.html}

View File

@@ -83,11 +83,11 @@ renderer.code = function ({ lang }: { text: string; lang?: string }) {
{ bg: string; fg: string; dot: string; border: string; shimmer: string } { bg: string; fg: string; dot: string; border: string; shimmer: string }
> = { > = {
html: { html: {
bg: "#fff5f0", bg: "#f0fff4",
fg: "#e05020", fg: "#18a058",
dot: "#e05020", dot: "#18a058",
border: "#f0d0c0", border: "#b8e8cc",
shimmer: "#fff5f0, #ffeee5, #fff5f0", shimmer: "#f0fff4, #e0f7ea, #f0fff4",
}, },
css: { css: {
bg: "#f0f0ff", bg: "#f0f0ff",
@@ -118,7 +118,7 @@ renderer.code = function ({ lang }: { text: string; lang?: string }) {
border: "#e0eaf5", border: "#e0eaf5",
shimmer: "#f0f7ff, #e8f4f8, #f0f7ff", shimmer: "#f0f7ff, #e8f4f8, #f0f7ff",
} }
return `<div class="code-placeholder" style="background: linear-gradient(90deg, ${c.shimmer}); background-size: 200% 100%; border-color: ${c.border}"><span class="code-placeholder-dot" style="background: ${c.dot}"></span><span class="code-placeholder-label" style="color: ${c.fg}; background: ${c.fg}18">${label}</span><span class="code-placeholder-text">代码自动应用到预览区</span></div>` return `<div class="code-placeholder" style="background: linear-gradient(90deg, ${c.shimmer}); background-size: 200% 100%; border-color: ${c.border}"><span class="code-placeholder-dot" style="background: ${c.dot}"></span><span class="code-placeholder-label" style="color: ${c.fg}; background: ${c.fg}18">${label}</span><span class="code-placeholder-text">代码正在生成中,结束后会自动应用到预览区</span></div>`
} }
function renderMarkdown(text: string): string { function renderMarkdown(text: string): string {
@@ -149,6 +149,7 @@ watch([() => messages.value.length, streamingContent], () => {
.messages { .messages {
flex: 1; flex: 1;
min-height: 0;
overflow-y: auto; overflow-y: auto;
padding: 12px; padding: 12px;
} }

View File

@@ -110,7 +110,9 @@
</n-flex> </n-flex>
<Icon <Icon
:icon=" :icon="
showUnsubmitted ? 'lucide:chevron-down' : 'lucide:chevron-right' showUnsubmitted
? 'lucide:chevron-down'
: 'lucide:chevron-right'
" "
:width="14" :width="14"
style="color: #aaa" style="color: #aaa"
@@ -329,7 +331,8 @@
{{ displayName(sub.username, sub.classname) }} {{ displayName(sub.username, sub.classname) }}
</div> </div>
<div style="color: #aaa; font-size: 11px"> <div style="color: #aaa; font-size: 11px">
{{ sub.score.toFixed(1) }} · {{ sub.rating_count }} 人打分 {{ sub.score.toFixed(1) }} ·
{{ sub.rating_count }} 人打分
</div> </div>
</div> </div>
<div style="color: #2080f0; font-size: 12px">查看 </div> <div style="color: #2080f0; font-size: 12px">查看 </div>
@@ -434,11 +437,17 @@ function viewSubmission(id: string) {
} }
function rankColor(i: number) { function rankColor(i: number) {
return (["#f0a020", "#909090", "#cd7f32", "#8899aa", "#7a8fa0"] as const)[i] ?? "#aaa" return (
(["#f0a020", "#909090", "#cd7f32", "#8899aa", "#7a8fa0"] as const)[i] ??
"#aaa"
)
} }
function rankBg(i: number) { function rankBg(i: number) {
return (["#fffbef", "#f8f8f8", "#fdf5ee", "#f2f5f8", "#eef2f5"] as const)[i] ?? "#f8f8f8" return (
(["#fffbef", "#f8f8f8", "#fdf5ee", "#f2f5f8", "#eef2f5"] as const)[i] ??
"#f8f8f8"
)
} }
function bucketPct(value: number) { function bucketPct(value: number) {

View File

@@ -1,8 +1,6 @@
<template> <template>
<n-flex align="center" class="corner"> <n-flex align="center" class="corner">
<n-button quaternary v-if="!show" @click="showTutorial"> <n-button quaternary v-if="!show" @click="showTutorial"> 教程 </n-button>
打开{{ TASK_LABEL[taskTab] }}
</n-button>
<template v-if="user.loaded && authed"> <template v-if="user.loaded && authed">
<n-button quaternary @click="emit('format')">整理</n-button> <n-button quaternary @click="emit('format')">整理</n-button>
<n-button <n-button
@@ -38,7 +36,7 @@ import { taskId, taskTab } 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"
import { ADMIN_URL, TASK_LABEL } from "../utils/const" import { ADMIN_URL } from "../utils/const"
const props = defineProps<{ const props = defineProps<{
submitLoading: boolean submitLoading: boolean

View File

@@ -63,6 +63,7 @@ async function prepare() {
tutorialIds.value = await Tutorial.listDisplay() tutorialIds.value = await Tutorial.listDisplay()
if (!tutorialIds.value.length) { if (!tutorialIds.value.length) {
content.value = "暂无教程" content.value = "暂无教程"
return
} }
if (!tutorialIds.value.includes(step.value)) { if (!tutorialIds.value.includes(step.value)) {
step.value = tutorialIds.value[0] as number step.value = tutorialIds.value[0] as number

View File

@@ -101,7 +101,13 @@ const canSubmit = computed(
) )
async function getContent() { async function getContent() {
list.value = await Challenge.list() list.value = await Challenge.list()
show(Number(route.params.display)) const display = Number(route.params.display)
const target = list.value.find((item) => item.display === display)
if (target) {
show(display)
} else if (list.value.length > 0) {
show(list.value[0].display)
}
} }
function createNew() { function createNew() {

View File

@@ -1,7 +1,6 @@
<template> <template>
<n-split :size="leftSize" min="350px" max="700px"> <n-layout has-sider style="height: 100vh">
<template #1> <n-layout-sider width="40%" bordered content-style="height: 100%; overflow: hidden;">
<div class="left-panel">
<n-tabs v-model:value="activeTab" type="line" class="left-tabs"> <n-tabs v-model:value="activeTab" type="line" class="left-tabs">
<template #prefix> <template #prefix>
<n-button text @click="back" style="margin: 0 8px"> <n-button text @click="back" style="margin: 0 8px">
@@ -19,10 +18,8 @@
<PromptPanel /> <PromptPanel />
</n-tab-pane> </n-tab-pane>
</n-tabs> </n-tabs>
</div> </n-layout-sider>
</template> <n-layout-content content-style="height: 100%; overflow: hidden;">
<template #2>
<div class="right-panel">
<Preview <Preview
:html="html" :html="html"
:css="css" :css="css"
@@ -32,9 +29,8 @@
@showCode="showCode = true" @showCode="showCode = true"
@clear="clearAll" @clear="clearAll"
/> />
</div> </n-layout-content>
</template> </n-layout>
</n-split>
<n-modal <n-modal
v-model:show="showCode" v-model:show="showCode"
preset="card" preset="card"
@@ -66,6 +62,7 @@ import Preview from "../components/Preview.vue"
import { Challenge, Submission } from "../api" import { Challenge, Submission } from "../api"
import { html, css, js } from "../store/editors" import { html, css, js } from "../store/editors"
import { taskId } from "../store/task" import { taskId } from "../store/task"
import { authed } from "../store/user"
import { import {
connectPrompt, connectPrompt,
disconnectPrompt, disconnectPrompt,
@@ -79,9 +76,7 @@ const route = useRoute()
const router = useRouter() const router = useRouter()
const message = useMessage() const message = useMessage()
const leftSize = ref(0.4)
const activeTab = ref("desc") const activeTab = ref("desc")
const challengeTitle = ref("")
const challengeContent = ref("") const challengeContent = ref("")
const showCode = ref(false) const showCode = ref(false)
@@ -93,8 +88,8 @@ async function loadChallenge() {
const display = Number(route.params.display) const display = Number(route.params.display)
const data = await Challenge.get(display) const data = await Challenge.get(display)
taskId.value = data.task_ptr taskId.value = data.task_ptr
challengeTitle.value = `#${data.display} ${data.title}`
challengeContent.value = await marked.parse(data.content, { async: true }) challengeContent.value = await marked.parse(data.content, { async: true })
if (!authed.value) return
loadHistory(data.task_ptr) // HTTP preload — async, non-blocking loadHistory(data.task_ptr) // HTTP preload — async, non-blocking
connectPrompt(data.task_ptr) // WebSocket — synchronous open connectPrompt(data.task_ptr) // WebSocket — synchronous open
setOnCodeComplete(async (code) => { setOnCodeComplete(async (code) => {
@@ -129,11 +124,6 @@ onUnmounted(disconnectPrompt)
</script> </script>
<style scoped> <style scoped>
.left-panel {
height: 100%;
overflow: hidden;
}
.left-tabs { .left-tabs {
height: 100%; height: 100%;
display: flex; display: flex;
@@ -150,9 +140,4 @@ onUnmounted(disconnectPrompt)
padding: 0; padding: 0;
} }
.right-panel {
height: 100%;
display: flex;
flex-direction: column;
}
</style> </style>

View File

@@ -25,7 +25,6 @@ async function init() {
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>${submission.css}</style> <style>${submission.css}</style>
<link rel="stylesheet" href="/normalize.min.css" /> <link rel="stylesheet" href="/normalize.min.css" />
<script src="/tailwindcss.min.js"><\/script>
</head> </head>
<body> <body>
${submission.html} ${submission.html}

View File

@@ -93,7 +93,13 @@ const canSubmit = computed(
) )
async function getContent() { async function getContent() {
list.value = await Tutorial.list() list.value = await Tutorial.list()
show(Number(route.params.display)) const display = Number(route.params.display)
const target = list.value.find((item) => item.display === display)
if (target) {
show(display)
} else if (list.value.length > 0) {
show(list.value[0].display)
}
} }
function createNew() { function createNew() {

View File

@@ -1,11 +1,8 @@
import { useStorage } from "@vueuse/core" import { useStorage } from "@vueuse/core"
import { STORAGE_KEY } from "../utils/const" import { STORAGE_KEY } from "../utils/const"
const defaultHTML = `<div class="welcome">黄岩一职</div>` const defaultHTML = ``
const defaultCSS = `.welcome { const defaultCSS = ``
color: red;
font-size: 24px;
}`
export const html = useStorage(STORAGE_KEY.HTML, defaultHTML) export const html = useStorage(STORAGE_KEY.HTML, defaultHTML)
export const css = useStorage(STORAGE_KEY.CSS, defaultCSS) export const css = useStorage(STORAGE_KEY.CSS, defaultCSS)

View File

@@ -47,8 +47,3 @@ export enum TASK_TYPE {
Tutorial = "tutorial", Tutorial = "tutorial",
Challenge = "challenge", Challenge = "challenge",
} }
export const TASK_LABEL = {
[TASK_TYPE.Tutorial]: "教程",
[TASK_TYPE.Challenge]: "挑战",
} as const