feat: 改版阶段 2:挑战和自由创作共用工作台,学生自己选版本提交

- 新的 Workbench:左边题目(可收起)+ AI 对话,右边预览 / 代码(可编辑),底部版本条;
  挑战页和自由创作页都用它,原来的“手动提交”“历史对话”标签换成“粘贴外部代码”和“全部历史”
- 不再每轮 AI 回复自动提交:学生点“交这一版”,可交多次,成绩仍取最高分;已交的版本不能重复点
- 手改代码、粘贴外部 AI 代码都存成版本(POST /prompt/versions,source 为 edit / manual),
  在版本条和提示词链里标“手改”“外部 AI”;它们进入 AI 上下文,AI 在手改后的代码上继续改;
  以前手动提交的回复正文为空时用代码补上;edit 的占位提问不参与提示词层级分类
- 互评从全屏弹窗改成对话里的卡片,打分前不显示作者,每个分数写明含义
- 编辑器代码按任务各存一份(useTaskCode),挑战页“清空”不再清掉教程的代码;
  提交列表“复制到编辑器”对挑战写入该任务自己的代码
- 抽出 utils/code.ts(整理代码、拆分整页 HTML);删除 RandomRatingModal、ExternalAIPanel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 23:59:24 +08:00
1 parent ff1eb2982e
commit 9c6c01eba1
29 files changed
+1748 -1069

No files matched your search

+8 -69
View File
@@ -70,6 +70,7 @@ import Editor from "./Editor.vue"
import Toolbar from "./Toolbar.vue"
import { html, css, js, tab, size, reset } from "../../store/editors"
import { taskId } from "../../store/task"
import { formatCode } from "../../utils/code"
import { Submission } from "../../api"
import { NCode, useDialog, useMessage } from "naive-ui"
import { h, ref } from "vue"
@@ -86,43 +87,16 @@ function changeSize(num: number) {
size.value = num
}
async function formatCode() {
// prettier 加上各语言插件近 1MB,只在点“格式化”时才加载
const [prettier, htmlParser, cssParser, babelParser, estreeParser] =
await Promise.all([
import("prettier/standalone"),
import("prettier/parser-html"),
import("prettier/parser-postcss"),
import("prettier/parser-babel"),
import("prettier/plugins/estree"),
])
const [htmlFormatted, cssFormatted, jsFormatted] = await Promise.all([
prettier.format(html.value, {
parser: "html",
//@ts-ignore
plugins: [htmlParser, babelParser, estreeParser, cssParser],
tabWidth: 4,
}),
prettier.format(css.value, {
parser: "css",
plugins: [cssParser],
tabWidth: 4,
}),
prettier.format(js.value, {
parser: "babel",
//@ts-ignore
plugins: [babelParser, estreeParser],
tabWidth: 2,
}),
])
html.value = htmlFormatted
css.value = cssFormatted
js.value = jsFormatted
async function formatEditors() {
const out = await formatCode({ html: html.value, css: css.value, js: js.value })
html.value = out.html
css.value = out.css
js.value = out.js
}
async function format() {
try {
await formatCode()
await formatEditors()
} catch (err: any) {
dialog.error({
title: "格式化失败",
@@ -148,7 +122,7 @@ async function doSubmit() {
async function formatAndSubmit() {
submitLoading.value = true
try {
await formatCode()
await formatEditors()
await doSubmit()
} catch (err: any) {
dialog.warning({
@@ -180,41 +154,6 @@ async function formatAndSubmit() {
font-size: 16px;
}
/* HTML / CSS / JS 各有固定颜色,见 theme.ts */
.lang-tab {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--wp-font-mono);
font-weight: 600;
font-size: 13px;
}
.lang-tab.lang-html::before,
.lang-tab.lang-css::before,
.lang-tab.lang-js::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
}
.lang-html {
color: var(--wp-html);
}
.lang-html::before {
background: var(--wp-html);
}
.lang-css {
color: var(--wp-css);
}
.lang-css::before {
background: var(--wp-css);
}
.lang-js {
color: var(--wp-js);
}
.lang-js::before {
background: var(--wp-js);
}
:deep(.n-tabs-nav .n-tabs-tab[data-name="html"]) {
background-color: var(--wp-html-soft) !important;
+7 -4
View File
@@ -1,7 +1,7 @@
<template>
<n-flex align="center" justify="space-between" class="title">
<div>
<n-text class="titleText">预览</n-text>
<slot name="title"><n-text class="titleText">预览</n-text></slot>
<n-text v-if="!!submission.id" depth="3">
({{ submission.view_count || 0 }})
</n-text>
@@ -214,9 +214,11 @@ onUnmounted(listenPreviewStorage(window, () => frame.value, storageScope))
<style scoped>
.title {
height: 43px;
padding: 0 20px;
border-bottom: 1px solid rgb(239, 239, 245);
padding: 0 12px 0 16px;
border-bottom: 1px solid var(--wp-line);
background: var(--wp-surface);
box-sizing: border-box;
flex-wrap: nowrap !important;
}
.titleText {
@@ -225,7 +227,8 @@ onUnmounted(listenPreviewStorage(window, () => frame.value, storageScope))
.iframe-wrapper {
width: 100%;
height: 100%;
/* 减去上面标题栏的高度,否则底部 43px 会被裁掉 */
height: calc(100% - 43px);
margin: 0 auto;
transition: max-width 0.3s ease;
overflow: hidden;