Compare commits
5 Commits
83537face1
...
ecce21aaaf
| Author | SHA1 | Date | |
|---|---|---|---|
| ecce21aaaf | |||
| 951e53c1dd | |||
| f26b06877c | |||
| 1744c405a5 | |||
| 5f95b88914 |
@@ -181,7 +181,6 @@ export const Submission = {
|
||||
score_min?: number
|
||||
score_max_exclusive?: number
|
||||
score_lt_threshold?: number
|
||||
nominated?: boolean
|
||||
ordering?: string
|
||||
grouped?: boolean
|
||||
}) {
|
||||
@@ -223,11 +222,6 @@ export const Submission = {
|
||||
return res.data as { cleared: number }
|
||||
},
|
||||
|
||||
async nominate(id: string) {
|
||||
const res = await http.put(`/submission/${id}/nominate`)
|
||||
return res.data as { nominated: boolean }
|
||||
},
|
||||
|
||||
async getStats(taskId: number, classname?: string): Promise<TaskStatsOut> {
|
||||
const params: Record<string, string | number> = {}
|
||||
if (classname) params.classname = classname
|
||||
|
||||
139
src/components/ExternalAIPanel.vue
Normal file
139
src/components/ExternalAIPanel.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="external-panel">
|
||||
<div class="content-area">
|
||||
<div class="field-label">提示词</div>
|
||||
<n-input
|
||||
v-model:value="promptText"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 8 }"
|
||||
placeholder="粘贴你发给外部 AI 的提示词..."
|
||||
/>
|
||||
<div class="field-label" style="margin-top: 12px">AI 代码</div>
|
||||
<n-input
|
||||
v-model:value="rawCode"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 6, maxRows: 16 }"
|
||||
placeholder="粘贴外部 AI 返回的完整 HTML 代码..."
|
||||
/>
|
||||
<div v-if="splitResult" class="split-result">
|
||||
<n-tag size="small" type="success">HTML · {{ splitResult.html.length }} 字符</n-tag>
|
||||
<n-tag size="small" type="info">CSS · {{ splitResult.css.length }} 字符</n-tag>
|
||||
<n-tag size="small" type="warning">JS · {{ splitResult.js.length }} 字符</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-bar">
|
||||
<n-button :disabled="!rawCode.trim()" @click="applyPreview">应用预览</n-button>
|
||||
<n-button type="primary" :disabled="!splitResult" :loading="submitting" @click="submit">
|
||||
提交
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
import { useMessage } from "naive-ui"
|
||||
import { html, css, js } from "../store/editors"
|
||||
import { Submission } from "../api"
|
||||
|
||||
const props = defineProps<{ taskId: number }>()
|
||||
const message = useMessage()
|
||||
|
||||
const promptText = ref("")
|
||||
const rawCode = ref("")
|
||||
const splitResult = ref<{ html: string; css: string; js: string } | null>(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
watch(rawCode, () => {
|
||||
splitResult.value = null
|
||||
})
|
||||
|
||||
function splitHtml(raw: string): { html: string; css: string; js: string } {
|
||||
let result = raw
|
||||
const cssBlocks: string[] = []
|
||||
const jsBlocks: string[] = []
|
||||
|
||||
result = result.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, (_, content) => {
|
||||
cssBlocks.push(content.trim())
|
||||
return ""
|
||||
})
|
||||
result = result.replace(
|
||||
/<script(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi,
|
||||
(_, content) => {
|
||||
jsBlocks.push(content.trim())
|
||||
return ""
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
html: result.trim(),
|
||||
css: cssBlocks.join("\n\n"),
|
||||
js: jsBlocks.join("\n\n"),
|
||||
}
|
||||
}
|
||||
|
||||
function applyPreview() {
|
||||
const result = splitHtml(rawCode.value)
|
||||
splitResult.value = result
|
||||
html.value = result.html
|
||||
css.value = result.css
|
||||
js.value = result.js
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!splitResult.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
await Submission.create(props.taskId, {
|
||||
html: splitResult.value.html,
|
||||
css: splitResult.value.css,
|
||||
js: splitResult.value.js,
|
||||
})
|
||||
message.success("提交成功")
|
||||
promptText.value = ""
|
||||
rawCode.value = ""
|
||||
splitResult.value = null
|
||||
} catch {
|
||||
message.error("提交失败,请重试")
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.external-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.split-result {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
padding: 12px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
@@ -279,72 +279,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 人气提交 Top 5 -->
|
||||
<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
|
||||
v-for="(sub, i) in stats.top_submissions"
|
||||
:key="sub.submission_id"
|
||||
:style="{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '6px 10px',
|
||||
background: rankBg(i),
|
||||
borderRadius: '6px',
|
||||
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
|
||||
@@ -436,20 +370,6 @@ function viewSubmission(id: string) {
|
||||
window.open(href, "_blank")
|
||||
}
|
||||
|
||||
function rankColor(i: number) {
|
||||
return (
|
||||
(["#f0a020", "#909090", "#cd7f32", "#8899aa", "#7a8fa0"] as const)[i] ??
|
||||
"#aaa"
|
||||
)
|
||||
}
|
||||
|
||||
function rankBg(i: number) {
|
||||
return (
|
||||
(["#fffbef", "#f8f8f8", "#fdf5ee", "#f2f5f8", "#eef2f5"] as const)[i] ??
|
||||
"#f8f8f8"
|
||||
)
|
||||
}
|
||||
|
||||
function bucketPct(value: number) {
|
||||
const total = stats.value?.submitted_count ?? 0
|
||||
if (!total) return "0%"
|
||||
@@ -467,7 +387,6 @@ const metrics = computed(() => {
|
||||
color: "#2080f0",
|
||||
},
|
||||
{ label: "未打分", value: stats.value.unrated_count, color: "#d03050" },
|
||||
{ label: "参与排名", value: stats.value.nominated_count, color: "#f0a020" },
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -77,15 +77,6 @@ const menu = computed(() => [
|
||||
width: 20,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "排名榜",
|
||||
key: "ranking",
|
||||
icon: () =>
|
||||
h(Icon, {
|
||||
icon: "streamline-emojis:sunglasses",
|
||||
width: 20,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "退出账号",
|
||||
key: "logout",
|
||||
@@ -117,9 +108,6 @@ function clickMenu(name: string) {
|
||||
query: { username: user.username },
|
||||
})
|
||||
break
|
||||
case "ranking":
|
||||
router.push({ name: "ranking" })
|
||||
break
|
||||
case "logout":
|
||||
handleLogout()
|
||||
break
|
||||
|
||||
@@ -37,7 +37,6 @@ const emit = defineEmits<{
|
||||
select: [id: string]
|
||||
delete: [row: SubmissionOut, parentId: string]
|
||||
"show-chain": [conversationId: string]
|
||||
nominate: [row: SubmissionOut]
|
||||
}>()
|
||||
|
||||
const isChallenge = computed(() => props.row.task_type === TASK_TYPE.Challenge)
|
||||
@@ -77,30 +76,6 @@ const subColumns = computed((): DataTableColumn<SubmissionOut>[] => [
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "排名",
|
||||
key: "nominated",
|
||||
width: 60,
|
||||
render: (r: SubmissionOut) => {
|
||||
if (r.username !== user.username) {
|
||||
return r.nominated
|
||||
? h("span", { style: { color: "#f0a020" } }, "🏅")
|
||||
: null
|
||||
}
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
title: r.nominated ? "已参与排名(点击可重新提名)" : "参与排名",
|
||||
onClick: (e: Event) => {
|
||||
e.stopPropagation()
|
||||
emit("nominate", r)
|
||||
},
|
||||
},
|
||||
() => (r.nominated ? "🏅" : "☆"),
|
||||
)
|
||||
},
|
||||
},
|
||||
...(isChallenge.value
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
<n-tab-pane name="chat" tab="AI 对话" display-directive="show">
|
||||
<PromptPanel />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="external" tab="手动提交" display-directive="show">
|
||||
<ExternalAIPanel :task-id="taskId" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
<div class="challenge-content">
|
||||
@@ -72,6 +75,7 @@ import { useMessage } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { marked } from "marked"
|
||||
import PromptPanel from "../components/PromptPanel.vue"
|
||||
import ExternalAIPanel from "../components/ExternalAIPanel.vue"
|
||||
import Preview from "../components/Preview.vue"
|
||||
import { Challenge, Submission } from "../api"
|
||||
import { html, css, js } from "../store/editors"
|
||||
|
||||
@@ -195,7 +195,6 @@ const columns: DataTableColumn<SubmissionOut>[] = [
|
||||
onSelect: (id) => getSubmissionByID(id),
|
||||
onDelete: (r, parentId) => handleDelete(r, parentId),
|
||||
"onShow-chain": (id) => showChain(id),
|
||||
onNominate: (r) => handleNominateChild(r, row.id),
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -313,23 +312,6 @@ async function getSubmissionByID(id: string) {
|
||||
submission.value = await Submission.get(id)
|
||||
}
|
||||
|
||||
async function handleNominateChild(row: SubmissionOut, parentId: string) {
|
||||
await Submission.nominate(row.id)
|
||||
const items = expandedData.get(parentId)
|
||||
if (items) {
|
||||
expandedData.set(
|
||||
parentId,
|
||||
items.map((d) => ({ ...d, nominated: d.id === row.id })),
|
||||
)
|
||||
}
|
||||
data.value = data.value.map((d) => {
|
||||
if (d.username === user.username && d.task_id === row.task_id) {
|
||||
d.nominated = d.id === row.id
|
||||
}
|
||||
return d
|
||||
})
|
||||
}
|
||||
|
||||
function afterScore() {
|
||||
data.value = data.value.map((d) => {
|
||||
if (d.id === submission.value.id) d.my_score = submission.value.my_score
|
||||
|
||||
@@ -78,7 +78,6 @@ export interface SubmissionOut {
|
||||
my_score: number
|
||||
conversation_id?: string
|
||||
flag?: FlagType
|
||||
nominated: boolean
|
||||
submit_count: number
|
||||
created: Date
|
||||
modified: Date
|
||||
@@ -107,14 +106,6 @@ export interface UserTag {
|
||||
classname: string
|
||||
}
|
||||
|
||||
export interface TopSubmission {
|
||||
submission_id: string
|
||||
username: string
|
||||
classname: string
|
||||
score: number
|
||||
rating_count: number
|
||||
}
|
||||
|
||||
export interface SubmissionCountBucket {
|
||||
count_1: number
|
||||
count_2: number
|
||||
@@ -142,12 +133,10 @@ export interface TaskStatsOut {
|
||||
unsubmitted_count: number
|
||||
average_score: number | null
|
||||
unrated_count: number
|
||||
nominated_count: number
|
||||
unsubmitted_users: UserTag[]
|
||||
unrated_users: UserTag[]
|
||||
submission_count_distribution: SubmissionCountBucket
|
||||
score_distribution: ScoreBucket
|
||||
top_submissions: TopSubmission[]
|
||||
flag_stats: FlagStats
|
||||
classes: string[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user