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-16 18:23:31 +08:00
parent 040e4e3253
commit 98d8099b5d
3 changed files with 98 additions and 106 deletions

View File

@@ -5,15 +5,15 @@
<n-button quaternary @click="download" :disabled="!showDL">下载</n-button> <n-button quaternary @click="download" :disabled="!showDL">下载</n-button>
<n-button quaternary @click="open">全屏</n-button> <n-button quaternary @click="open">全屏</n-button>
<n-button quaternary v-if="props.clearable" @click="clear">清空</n-button> <n-button quaternary v-if="props.clearable" @click="clear">清空</n-button>
<n-button quaternary v-if="props.showCodeButton" @click="emits('showCode')">查看代码</n-button> <n-button quaternary v-if="props.showCodeButton" @click="emits('showCode')">代码</n-button>
<n-button quaternary v-if="props.submissionId" @click="copyLink"> <n-button quaternary v-if="props.submissionId" @click="copyLink">
复制链接 复制链接
</n-button> </n-button>
<n-flex v-if="!!submission.id"> <n-flex v-if="!!submission.id">
<n-button quaternary @click="emits('showCode')">查看代码</n-button> <n-button quaternary @click="emits('showCode')">代码</n-button>
<n-popover v-if="submission.my_score === 0"> <n-popover v-if="submission.my_score === 0">
<template #trigger> <template #trigger>
<n-button secondary type="primary">手动打分</n-button> <n-button secondary type="primary">打分</n-button>
</template> </template>
<n-rate :size="30" @update:value="updateScore" /> <n-rate :size="30" @update:value="updateScore" />
</n-popover> </n-popover>

View File

@@ -11,6 +11,27 @@ import { step } from "../store/tutorial"
import { taskId } from "../store/task" import { taskId } from "../store/task"
import { useRouter } from "vue-router" import { useRouter } from "vue-router"
marked.use({
renderer: {
code({ text, lang }) {
const language = lang?.toLowerCase() ?? "html"
return `<div class="codeblock-wrapper" data-lang="${language}">
<div class="codeblock-action">
<span class="lang">${language.toUpperCase()}</span>
<div class="btn-group">
<button class="action-btn" data-action="copy">复制</button>
<button class="action-btn" data-action="replace">替换</button>
</div>
</div>
<pre><code class="language-${language}">${text}</code></pre>
</div>`
},
link({ href, text }) {
return `<a href="${href}" target="_blank">${text}</a>`
},
},
})
const router = useRouter() const router = useRouter()
const tutorialIds = ref<number[]>([]) const tutorialIds = ref<number[]>([])
const content = ref("") const content = ref("")
@@ -28,12 +49,12 @@ const nextDisabled = () => {
function prev() { function prev() {
const i = tutorialIds.value.indexOf(step.value) const i = tutorialIds.value.indexOf(step.value)
step.value = tutorialIds.value[i - 1] step.value = tutorialIds.value[i - 1] as number
} }
function next() { function next() {
const i = tutorialIds.value.indexOf(step.value) const i = tutorialIds.value.indexOf(step.value)
step.value = tutorialIds.value[i + 1] step.value = tutorialIds.value[i + 1] as number
} }
defineExpose({ tutorialIds, prevDisabled, nextDisabled, prev, next }) defineExpose({ tutorialIds, prevDisabled, nextDisabled, prev, next })
@@ -44,91 +65,43 @@ async function prepare() {
content.value = "暂无教程" content.value = "暂无教程"
} }
if (!tutorialIds.value.includes(step.value)) { if (!tutorialIds.value.includes(step.value)) {
step.value = tutorialIds.value[0] step.value = tutorialIds.value[0] as number
} }
} }
async function getContent() { 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 })
} }
function addButton() { function flash(btn: HTMLButtonElement, done: string, original: string) {
const existing = $content.value?.querySelectorAll(".codeblock-action") ?? [] btn.textContent = done
for (const el of existing) el.remove() setTimeout(() => {
btn.textContent = original
const action = document.createElement("div")
action.className = "codeblock-action"
const pres = $content.value?.querySelectorAll("pre") ?? []
for (const pre of pres) {
let timer = 0
let copyTimer = 0
const actions = action.cloneNode() as HTMLDivElement
pre.insertBefore(actions, pre.children[0])
const $code = pre.childNodes[1] as HTMLPreElement
const match = $code.className.match(/-(.*)/)
let lang = "html"
if (match) lang = match[1].toLowerCase()
const langSpan = document.createElement("span")
langSpan.className = "lang"
langSpan.textContent = lang.toUpperCase()
const btnGroup = document.createElement("div")
btnGroup.className = "btn-group"
const copyBtn = document.createElement("button")
copyBtn.className = "action-btn"
copyBtn.textContent = "复制"
const replaceBtn = document.createElement("button")
replaceBtn.className = "action-btn"
replaceBtn.textContent = "替换"
btnGroup.appendChild(copyBtn)
btnGroup.appendChild(replaceBtn)
actions.appendChild(langSpan)
actions.appendChild(btnGroup)
copyBtn.onclick = () => {
const content = pre.children[1].textContent
copyFn(content ?? "")
copyBtn.textContent = "已复制"
clearTimeout(copyTimer)
copyTimer = setTimeout(() => {
copyBtn.textContent = "复制"
}, 1000) }, 1000)
} }
replaceBtn.onclick = () => { function setupCodeActions() {
$content.value?.addEventListener("click", (e: MouseEvent) => {
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>("[data-action]")
if (!btn) return
const wrapper = btn.closest<HTMLElement>("[data-lang]")!
const lang = wrapper.dataset.lang ?? "html"
const code = wrapper.querySelector("code")?.textContent ?? ""
if (btn.dataset.action === "copy") {
copyFn(code)
flash(btn, "已复制", "复制")
} else if (btn.dataset.action === "replace") {
tab.value = lang tab.value = lang
const content = pre.children[1].textContent if (lang === "html") html.value = code
if (lang === "html") html.value = content if (lang === "css") css.value = code
if (lang === "css") css.value = content if (lang === "js") js.value = code
if (lang === "js") js.value = content flash(btn, "已替换", "替换")
replaceBtn.textContent = "已替换"
clearTimeout(timer)
timer = setTimeout(() => {
replaceBtn.textContent = "替换"
}, 1000)
} }
} })
}
function modifyLink() {
const links = $content.value?.querySelectorAll("a") ?? []
for (const link of links) {
link.target = "_blank"
}
}
async function render() {
await getContent()
addButton()
modifyLink()
} }
async function init() { async function init() {
@@ -136,7 +109,10 @@ async function init() {
render() render()
} }
onMounted(init) onMounted(() => {
setupCodeActions()
init()
})
watch(step, (v) => { watch(step, (v) => {
router.push({ name: "home-tutorial", params: { display: v } }) router.push({ name: "home-tutorial", params: { display: v } })
render() render()
@@ -160,8 +136,24 @@ watch(step, (v) => {
font-family: Monaco; font-family: Monaco;
} }
.codeblock-action { .markdown-body .codeblock-wrapper {
padding: 1rem;
background-color: #f6f8fa;
border-radius: 6px;
margin-bottom: 1rem; margin-bottom: 1rem;
overflow: auto;
}
.markdown-body .codeblock-wrapper pre {
padding: 0;
background-color: transparent;
border-radius: 0;
margin-bottom: 0;
overflow: visible;
}
.codeblock-action {
margin-bottom: 0.5rem;
font-family: font-family:
v-sans, v-sans,
system-ui, system-ui,

View File

@@ -1,7 +1,7 @@
<template> <template>
<n-grid class="container" x-gap="10" :cols="2"> <n-split class="container" direction="horizontal" :default-size="0.333" :min="0.2" :max="0.8">
<n-gi :span="1"> <template #1>
<n-flex vertical> <n-flex vertical style="height: 100%; padding-right: 10px">
<n-flex justify="space-between"> <n-flex justify="space-between">
<n-button secondary @click="() => goHome($router, taskTab, step)"> <n-button secondary @click="() => goHome($router, taskTab, step)">
返回首页 返回首页
@@ -43,8 +43,9 @@
:row-class-name="rowClassName" :row-class-name="rowClassName"
></n-data-table> ></n-data-table>
</n-flex> </n-flex>
</n-gi> </template>
<n-gi :span="1"> <template #2>
<div style="height: 100%; padding-left: 10px">
<Preview <Preview
v-if="submission.id" v-if="submission.id"
:html="html" :html="html"
@@ -54,8 +55,9 @@
@after-score="afterScore" @after-score="afterScore"
@show-code="codeModal = true" @show-code="codeModal = true"
/> />
</n-gi> </div>
</n-grid> </template>
</n-split>
<n-modal preset="card" v-model:show="codeModal" style="max-width: 60%"> <n-modal preset="card" v-model:show="codeModal" style="max-width: 60%">
<template #header> <template #header>
<n-flex align="center"> <n-flex align="center">
@@ -304,19 +306,16 @@ const columns: DataTableColumn<SubmissionOut>[] = [
render: (submission) => h(TaskTitle, { submission }), render: (submission) => h(TaskTitle, { submission }),
}, },
{ {
title: "我打的分", title: "分",
key: "my_score",
render: (row) => {
if (row.my_score > 0) return row.my_score
else return "-"
},
},
{
title: "平均得分",
key: "score", key: "score",
width: 80,
render: (row) => { render: (row) => {
if (row.score > 0) return row.score.toFixed(2) const myScore = row.my_score > 0 ? String(row.my_score) : "-"
else return "-" const avgScore = row.score > 0 ? row.score.toFixed(2) : "-"
return h("div", { style: { display: "flex", gap: "6px", alignItems: "baseline" } }, [
h("span", avgScore),
h("span", { style: { fontSize: "11px", color: "#999" } }, myScore),
])
}, },
}, },
{ {
@@ -420,6 +419,7 @@ onUnmounted(() => {
padding: 10px; padding: 10px;
box-sizing: border-box; box-sizing: border-box;
height: calc(100% - 43px); height: calc(100% - 43px);
width: 100%;
} }
:deep(.row-active td) { :deep(.row-active td) {