fix
This commit is contained in:
83
public/tailwindcss.min.js
vendored
83
public/tailwindcss.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,78 +1,42 @@
|
|||||||
<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-empty v-if="!challenges.length">暂无挑战,敬请期待</n-empty>
|
||||||
<n-flex align="center" style="margin-bottom: 12px">
|
<n-flex v-else vertical :size="12">
|
||||||
<n-button text @click="back">
|
<n-card
|
||||||
<Icon :width="20" icon="pepicons-pencil:arrow-left"></Icon>
|
v-for="item in challenges"
|
||||||
</n-button>
|
:key="item.display"
|
||||||
<span style="font-weight: bold">返回挑战列表</span>
|
hoverable
|
||||||
</n-flex>
|
class="challenge-card"
|
||||||
<div class="markdown-body" v-html="content" />
|
@click="select(item)"
|
||||||
</template>
|
>
|
||||||
<template v-else>
|
<template #header>
|
||||||
<n-empty v-if="!challenges.length">暂无挑战,敬请期待</n-empty>
|
{{ item.title }}
|
||||||
<n-grid v-else :cols="3" x-gap="12" y-gap="12">
|
</template>
|
||||||
<n-gi v-for="item in challenges" :key="item.display">
|
<template #header-extra>
|
||||||
<n-card hoverable class="challenge-card" @click="select(item)">
|
<n-tag type="warning" size="small">{{ item.score }}分</n-tag>
|
||||||
<template #header>
|
</template>
|
||||||
{{ item.title }}
|
</n-card>
|
||||||
</template>
|
</n-flex>
|
||||||
<template #header-extra>
|
|
||||||
<n-tag type="warning" size="small">{{ item.score }}分</n-tag>
|
|
||||||
</template>
|
|
||||||
</n-card>
|
|
||||||
</n-gi>
|
|
||||||
</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) {
|
||||||
|
router.push({ name: "home-challenge", params: { display: item.display } })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
challenges.value = await Challenge.listDisplay()
|
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 } })
|
|
||||||
}
|
|
||||||
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() {
|
|
||||||
currentChallenge.value = null
|
|
||||||
challengeDisplay.value = 0
|
|
||||||
taskId.value = 0
|
|
||||||
content.value = ""
|
|
||||||
router.push({ name: "home-challenge-list" })
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadList)
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
.container {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,353 +39,356 @@
|
|||||||
</n-flex>
|
</n-flex>
|
||||||
|
|
||||||
<n-spin :show="loading">
|
<n-spin :show="loading">
|
||||||
<!-- 关键指标 -->
|
<!-- 关键指标 -->
|
||||||
<div
|
|
||||||
style="
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-for="(metric, i) in metrics"
|
style="
|
||||||
:key="metric.label"
|
display: grid;
|
||||||
:style="{
|
grid-template-columns: repeat(5, 1fr);
|
||||||
padding: '12px 8px',
|
border: 1px solid #eee;
|
||||||
textAlign: 'center',
|
border-radius: 6px;
|
||||||
borderRight: i < metrics.length - 1 ? '1px solid #eee' : 'none',
|
overflow: hidden;
|
||||||
}"
|
margin-bottom: 12px;
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
v-for="(metric, i) in metrics"
|
||||||
|
:key="metric.label"
|
||||||
:style="{
|
:style="{
|
||||||
fontSize: '20px',
|
padding: '12px 8px',
|
||||||
fontWeight: '700',
|
|
||||||
color: metric.color,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ metric.value }}
|
|
||||||
</div>
|
|
||||||
<div style="color: #888; font-size: 11px; margin-top: 2px">
|
|
||||||
{{ metric.label }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 未提交名单(可折叠) -->
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
padding: 10px 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff8f8;
|
|
||||||
"
|
|
||||||
@click="showUnsubmitted = !showUnsubmitted"
|
|
||||||
>
|
|
||||||
<n-flex align="center" :size="6">
|
|
||||||
<span
|
|
||||||
style="
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
background: #d03050;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
"
|
|
||||||
></span>
|
|
||||||
<span style="font-weight: 600; color: #d03050; font-size: 12px"
|
|
||||||
>未提交({{ stats.unsubmitted_count }}人)</span
|
|
||||||
>
|
|
||||||
</n-flex>
|
|
||||||
<Icon
|
|
||||||
:icon="
|
|
||||||
showUnsubmitted ? 'lucide:chevron-down' : 'lucide:chevron-right'
|
|
||||||
"
|
|
||||||
:width="14"
|
|
||||||
style="color: #aaa"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="showUnsubmitted"
|
|
||||||
style="
|
|
||||||
padding: 10px 14px;
|
|
||||||
background: #fff8f8;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 5px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<n-tag
|
|
||||||
v-for="u in stats.unsubmitted_users"
|
|
||||||
:key="u.username"
|
|
||||||
size="small"
|
|
||||||
:bordered="true"
|
|
||||||
style="border-color: #ffd0d0; background: #fff"
|
|
||||||
>{{ displayName(u.username, u.classname) }}</n-tag
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="!stats.unsubmitted_users.length"
|
|
||||||
style="color: #aaa; font-size: 12px"
|
|
||||||
>暂无</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 未打分名单(可折叠) -->
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
padding: 10px 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fffaf5;
|
|
||||||
"
|
|
||||||
@click="showUnrated = !showUnrated"
|
|
||||||
>
|
|
||||||
<n-flex align="center" :size="6">
|
|
||||||
<span
|
|
||||||
style="
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
background: #e07800;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
"
|
|
||||||
></span>
|
|
||||||
<span style="font-weight: 600; color: #e07800; font-size: 12px"
|
|
||||||
>未打分({{ stats.unrated_count }}人)</span
|
|
||||||
>
|
|
||||||
<span style="font-size: 11px; color: #aaa"
|
|
||||||
>— 还没给任何提交打过分</span
|
|
||||||
>
|
|
||||||
</n-flex>
|
|
||||||
<Icon
|
|
||||||
:icon="
|
|
||||||
showUnrated ? 'lucide:chevron-down' : 'lucide:chevron-right'
|
|
||||||
"
|
|
||||||
:width="14"
|
|
||||||
style="color: #aaa"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="showUnrated"
|
|
||||||
style="
|
|
||||||
padding: 10px 14px;
|
|
||||||
background: #fffaf5;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 5px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<n-tag
|
|
||||||
v-for="u in stats.unrated_users"
|
|
||||||
:key="u.username"
|
|
||||||
size="small"
|
|
||||||
:bordered="true"
|
|
||||||
style="border-color: #ffd0a0; background: #fff"
|
|
||||||
>{{ displayName(u.username, u.classname) }}</n-tag
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="!stats.unrated_users.length"
|
|
||||||
style="color: #aaa; font-size: 12px"
|
|
||||||
>暂无</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 提交次数分布 -->
|
|
||||||
<div style="margin-bottom: 12px">
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #333;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
提交次数分布
|
|
||||||
<span style="font-size: 11px; color: #aaa; font-weight: 400"
|
|
||||||
>(已提交的 {{ stats.submitted_count }} 人)</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 8px">
|
|
||||||
<div
|
|
||||||
v-for="bucket in countBuckets"
|
|
||||||
:key="bucket.label"
|
|
||||||
:style="{
|
|
||||||
flex: 1,
|
|
||||||
border: `1px solid ${bucket.borderColor}`,
|
|
||||||
borderRadius: '6px',
|
|
||||||
padding: '10px 8px',
|
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
background: bucket.bg,
|
borderRight: i < metrics.length - 1 ? '1px solid #eee' : 'none',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:style="{
|
:style="{
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: bucket.color,
|
color: metric.color,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ bucket.value }}
|
{{ metric.value }}
|
||||||
</div>
|
</div>
|
||||||
<div style="color: #aaa; font-size: 11px; margin-top: 2px">
|
<div style="color: #888; font-size: 11px; margin-top: 2px">
|
||||||
{{ bucket.label }}
|
{{ metric.label }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
style="
|
</div>
|
||||||
margin-top: 8px;
|
|
||||||
background: #e8e8e8;
|
<!-- 未提交名单(可折叠) -->
|
||||||
border-radius: 3px;
|
<div
|
||||||
height: 4px;
|
style="
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff8f8;
|
||||||
|
"
|
||||||
|
@click="showUnsubmitted = !showUnsubmitted"
|
||||||
|
>
|
||||||
|
<n-flex align="center" :size="6">
|
||||||
|
<span
|
||||||
|
style="
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
background: #d03050;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
<span style="font-weight: 600; color: #d03050; font-size: 12px"
|
||||||
|
>未提交({{ stats.unsubmitted_count }}人)</span
|
||||||
|
>
|
||||||
|
</n-flex>
|
||||||
|
<Icon
|
||||||
|
:icon="
|
||||||
|
showUnsubmitted
|
||||||
|
? 'lucide:chevron-down'
|
||||||
|
: 'lucide:chevron-right'
|
||||||
"
|
"
|
||||||
|
:width="14"
|
||||||
|
style="color: #aaa"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showUnsubmitted"
|
||||||
|
style="
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #fff8f8;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<n-tag
|
||||||
|
v-for="u in stats.unsubmitted_users"
|
||||||
|
:key="u.username"
|
||||||
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
style="border-color: #ffd0d0; background: #fff"
|
||||||
|
>{{ displayName(u.username, u.classname) }}</n-tag
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!stats.unsubmitted_users.length"
|
||||||
|
style="color: #aaa; font-size: 12px"
|
||||||
|
>暂无</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未打分名单(可折叠) -->
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fffaf5;
|
||||||
|
"
|
||||||
|
@click="showUnrated = !showUnrated"
|
||||||
|
>
|
||||||
|
<n-flex align="center" :size="6">
|
||||||
|
<span
|
||||||
|
style="
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
background: #e07800;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
<span style="font-weight: 600; color: #e07800; font-size: 12px"
|
||||||
|
>未打分({{ stats.unrated_count }}人)</span
|
||||||
|
>
|
||||||
|
<span style="font-size: 11px; color: #aaa"
|
||||||
|
>— 还没给任何提交打过分</span
|
||||||
|
>
|
||||||
|
</n-flex>
|
||||||
|
<Icon
|
||||||
|
:icon="
|
||||||
|
showUnrated ? 'lucide:chevron-down' : 'lucide:chevron-right'
|
||||||
|
"
|
||||||
|
:width="14"
|
||||||
|
style="color: #aaa"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showUnrated"
|
||||||
|
style="
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #fffaf5;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<n-tag
|
||||||
|
v-for="u in stats.unrated_users"
|
||||||
|
:key="u.username"
|
||||||
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
style="border-color: #ffd0a0; background: #fff"
|
||||||
|
>{{ displayName(u.username, u.classname) }}</n-tag
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!stats.unrated_users.length"
|
||||||
|
style="color: #aaa; font-size: 12px"
|
||||||
|
>暂无</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交次数分布 -->
|
||||||
|
<div style="margin-bottom: 12px">
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
提交次数分布
|
||||||
|
<span style="font-size: 11px; color: #aaa; font-weight: 400"
|
||||||
|
>(已提交的 {{ stats.submitted_count }} 人)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px">
|
||||||
|
<div
|
||||||
|
v-for="bucket in countBuckets"
|
||||||
|
:key="bucket.label"
|
||||||
|
:style="{
|
||||||
|
flex: 1,
|
||||||
|
border: `1px solid ${bucket.borderColor}`,
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '10px 8px',
|
||||||
|
textAlign: 'center',
|
||||||
|
background: bucket.bg,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:style="{
|
:style="{
|
||||||
background: bucket.color,
|
fontSize: '20px',
|
||||||
height: '4px',
|
fontWeight: '700',
|
||||||
borderRadius: '3px',
|
color: bucket.color,
|
||||||
width: bucketPct(bucket.value),
|
|
||||||
}"
|
}"
|
||||||
></div>
|
>
|
||||||
</div>
|
{{ bucket.value }}
|
||||||
<div style="color: #bbb; font-size: 10px; margin-top: 3px">
|
</div>
|
||||||
{{ bucketPct(bucket.value) }}
|
<div style="color: #aaa; font-size: 11px; margin-top: 2px">
|
||||||
|
{{ bucket.label }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
margin-top: 8px;
|
||||||
|
background: #e8e8e8;
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 4px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
background: bucket.color,
|
||||||
|
height: '4px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
width: bucketPct(bucket.value),
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div style="color: #bbb; font-size: 10px; margin-top: 3px">
|
||||||
|
{{ bucketPct(bucket.value) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 人气提交 Top 5 -->
|
<!-- 人气提交 Top 5 -->
|
||||||
<div style="margin-bottom: 12px">
|
<div style="margin-bottom: 12px">
|
||||||
<div
|
|
||||||
style="
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #333;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
人气提交 Top 5
|
|
||||||
<span style="font-size: 11px; color: #aaa; font-weight: 400"
|
|
||||||
>(按打分人数)</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 5px">
|
|
||||||
<div
|
<div
|
||||||
v-for="(sub, i) in stats.top_submissions"
|
style="
|
||||||
:key="sub.submission_id"
|
font-weight: 600;
|
||||||
:style="{
|
font-size: 13px;
|
||||||
display: 'flex',
|
margin-bottom: 8px;
|
||||||
alignItems: 'center',
|
color: #333;
|
||||||
gap: '10px',
|
"
|
||||||
padding: '6px 10px',
|
|
||||||
background: rankBg(i),
|
|
||||||
borderRadius: '6px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}"
|
|
||||||
@click="viewSubmission(sub.submission_id)"
|
|
||||||
>
|
>
|
||||||
|
人气提交 Top 5
|
||||||
|
<span style="font-size: 11px; color: #aaa; font-weight: 400"
|
||||||
|
>(按打分人数)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 5px">
|
||||||
<div
|
<div
|
||||||
|
v-for="(sub, i) in stats.top_submissions"
|
||||||
|
:key="sub.submission_id"
|
||||||
:style="{
|
:style="{
|
||||||
width: '20px',
|
|
||||||
height: '20px',
|
|
||||||
background: rankColor(i),
|
|
||||||
borderRadius: '50%',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
gap: '10px',
|
||||||
color: '#fff',
|
padding: '6px 10px',
|
||||||
fontWeight: '700',
|
background: rankBg(i),
|
||||||
fontSize: '11px',
|
borderRadius: '6px',
|
||||||
flexShrink: 0,
|
cursor: 'pointer',
|
||||||
|
}"
|
||||||
|
@click="viewSubmission(sub.submission_id)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: rankColor(i),
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: '11px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ i + 1 }}
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1">
|
||||||
|
<div style="font-weight: 500; font-size: 13px">
|
||||||
|
{{ displayName(sub.username, sub.classname) }}
|
||||||
|
</div>
|
||||||
|
<div style="color: #aaa; font-size: 11px">
|
||||||
|
{{ sub.score.toFixed(1) }} 分 ·
|
||||||
|
{{ sub.rating_count }} 人打分
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="color: #2080f0; font-size: 12px">查看 →</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="!stats.top_submissions.length"
|
||||||
|
style="color: #aaa; font-size: 12px"
|
||||||
|
>暂无打分记录</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标记统计 -->
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
标记统计
|
||||||
|
</div>
|
||||||
|
<n-flex :size="8" style="flex-wrap: wrap">
|
||||||
|
<div
|
||||||
|
v-for="flag in flagBadges"
|
||||||
|
:key="flag.label"
|
||||||
|
:style="{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
padding: '4px 10px',
|
||||||
|
background: flag.bg,
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: `1px solid ${flag.border}`,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ i + 1 }}
|
<span
|
||||||
|
:style="{
|
||||||
|
width: '7px',
|
||||||
|
height: '7px',
|
||||||
|
background: flag.color,
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'inline-block',
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
|
<span :style="{ color: flag.color, fontSize: '12px' }">{{
|
||||||
|
flag.label
|
||||||
|
}}</span>
|
||||||
|
<span style="font-weight: 700; font-size: 13px; color: #333">{{
|
||||||
|
flag.value
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 1">
|
</n-flex>
|
||||||
<div style="font-weight: 500; font-size: 13px">
|
|
||||||
{{ displayName(sub.username, sub.classname) }}
|
|
||||||
</div>
|
|
||||||
<div style="color: #aaa; font-size: 11px">
|
|
||||||
{{ sub.score.toFixed(1) }} 分 · {{ sub.rating_count }} 人打分
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="color: #2080f0; font-size: 12px">查看 →</div>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
v-if="!stats.top_submissions.length"
|
|
||||||
style="color: #aaa; font-size: 12px"
|
|
||||||
>暂无打分记录</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 标记统计 -->
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #333;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
标记统计
|
|
||||||
</div>
|
|
||||||
<n-flex :size="8" style="flex-wrap: wrap">
|
|
||||||
<div
|
|
||||||
v-for="flag in flagBadges"
|
|
||||||
:key="flag.label"
|
|
||||||
:style="{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '6px',
|
|
||||||
padding: '4px 10px',
|
|
||||||
background: flag.bg,
|
|
||||||
borderRadius: '4px',
|
|
||||||
border: `1px solid ${flag.border}`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:style="{
|
|
||||||
width: '7px',
|
|
||||||
height: '7px',
|
|
||||||
background: flag.color,
|
|
||||||
borderRadius: '50%',
|
|
||||||
display: 'inline-block',
|
|
||||||
}"
|
|
||||||
></span>
|
|
||||||
<span :style="{ color: flag.color, fontSize: '12px' }">{{
|
|
||||||
flag.label
|
|
||||||
}}</span>
|
|
||||||
<span style="font-weight: 700; font-size: 13px; color: #333">{{
|
|
||||||
flag.value
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</n-flex>
|
|
||||||
</div>
|
|
||||||
</n-spin>
|
</n-spin>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -1,40 +1,36 @@
|
|||||||
<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">
|
<Icon :width="20" icon="pepicons-pencil:arrow-left" />
|
||||||
<Icon :width="20" icon="pepicons-pencil:arrow-left" />
|
</n-button>
|
||||||
</n-button>
|
</template>
|
||||||
</template>
|
<n-tab-pane name="desc" tab="挑战描述" display-directive="show">
|
||||||
<n-tab-pane name="desc" tab="挑战描述" display-directive="show">
|
<div
|
||||||
<div
|
class="markdown-body"
|
||||||
class="markdown-body"
|
style="padding: 12px; overflow-y: auto; height: 100%"
|
||||||
style="padding: 12px; overflow-y: auto; height: 100%"
|
v-html="challengeContent"
|
||||||
v-html="challengeContent"
|
/>
|
||||||
/>
|
</n-tab-pane>
|
||||||
</n-tab-pane>
|
<n-tab-pane name="chat" tab="AI 对话" display-directive="show">
|
||||||
<n-tab-pane name="chat" tab="AI 对话" display-directive="show">
|
<PromptPanel />
|
||||||
<PromptPanel />
|
</n-tab-pane>
|
||||||
</n-tab-pane>
|
</n-tabs>
|
||||||
</n-tabs>
|
</n-layout-sider>
|
||||||
</div>
|
<n-layout-content content-style="height: 100%; overflow: hidden;">
|
||||||
</template>
|
<Preview
|
||||||
<template #2>
|
:html="html"
|
||||||
<div class="right-panel">
|
:css="css"
|
||||||
<Preview
|
:js="js"
|
||||||
:html="html"
|
show-code-button
|
||||||
:css="css"
|
clearable
|
||||||
:js="js"
|
@showCode="showCode = true"
|
||||||
show-code-button
|
@clear="clearAll"
|
||||||
clearable
|
/>
|
||||||
@showCode="showCode = true"
|
</n-layout-content>
|
||||||
@clear="clearAll"
|
</n-layout>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user