|
|
|
|
|
<template>
|
|
|
|
|
|
<main class="ai-gen-page">
|
|
|
|
|
|
<!-- ===== 顶部工具条(扁平,与教师端筛选条同风格)===== -->
|
|
|
|
|
|
<section class="ai-gen-toolbar">
|
|
|
|
|
|
<el-button text :icon="ArrowLeft" @click="goBack">返回题库</el-button>
|
|
|
|
|
|
<h2 class="ai-gen-title">AI 智能出题</h2>
|
|
|
|
|
|
<span class="ai-gen-sub">按要求生成,勾选后加入题库</span>
|
|
|
|
|
|
<div class="ai-gen-spacer" />
|
|
|
|
|
|
<el-button v-if="aiLoading" type="danger" :icon="VideoPause" @click="stopAiStream">停止生成</el-button>
|
|
|
|
|
|
<el-button v-else type="primary" :icon="MagicStick" @click="generateAi">生成试题</el-button>
|
|
|
|
|
|
<!-- 加入题库为 ghost 次级:白底 + 灰边 + 深字(skill 规范:主按钮唯一,次级一律幽灵化)-->
|
|
|
|
|
|
<el-button :icon="Plus" :disabled="!aiSelected.size || aiImporting" :loading="aiImporting" @click="importAiQuestions">加入题库{{ aiSelected.size ? `(${aiSelected.size})` : "" }}</el-button>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ===== 主体:左表单 + 右识别区 ===== -->
|
|
|
|
|
|
<section class="ai-gen-main">
|
|
|
|
|
|
<!-- 左侧:生成要求表单(白卡,左栏) -->
|
|
|
|
|
|
<aside class="ai-gen-form-card">
|
|
|
|
|
|
<el-form label-position="top" class="ai-gen-form">
|
|
|
|
|
|
<el-form-item label="题目标题">
|
|
|
|
|
|
<el-input v-model="aiForm.title" placeholder="请输入题目标题(如:网络营销基础测试)" maxlength="20" show-word-limit />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="生成要求描述">
|
|
|
|
|
|
<el-input v-model="aiForm.description" type="textarea" :rows="7" placeholder="请描述试题主题、考察知识点、难度侧重等要求" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="题型设置">
|
|
|
|
|
|
<div class="ai-gen-type-grid">
|
|
|
|
|
|
<div class="ai-gen-type-cell">
|
|
|
|
|
|
<span class="ai-gen-type-name">单选题</span>
|
|
|
|
|
|
<el-input-number v-model="aiForm.single" :min="0" :max="20" size="small" controls-position="right" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-gen-type-cell">
|
|
|
|
|
|
<span class="ai-gen-type-name">多选题</span>
|
|
|
|
|
|
<el-input-number v-model="aiForm.multiple" :min="0" :max="20" size="small" controls-position="right" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-gen-type-cell">
|
|
|
|
|
|
<span class="ai-gen-type-name">判断题</span>
|
|
|
|
|
|
<el-input-number v-model="aiForm.judgment" :min="0" :max="20" size="small" controls-position="right" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-gen-type-cell">
|
|
|
|
|
|
<span class="ai-gen-type-name">简答题</span>
|
|
|
|
|
|
<el-input-number v-model="aiForm.shortAnswer" :min="0" :max="20" size="small" controls-position="right" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-gen-type-tip">每类最多 20 道</div>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="试题难度">
|
|
|
|
|
|
<el-radio-group v-model="aiForm.difficulty">
|
|
|
|
|
|
<el-radio label="简单">简单</el-radio>
|
|
|
|
|
|
<el-radio label="一般">一般</el-radio>
|
|
|
|
|
|
<el-radio label="困难">困难</el-radio>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧:识别区结果(白卡,右栏) -->
|
|
|
|
|
|
<section class="ai-gen-preview-card">
|
|
|
|
|
|
<div class="ai-gen-preview-head">
|
|
|
|
|
|
<span class="ai-gen-preview-title">识别区</span>
|
|
|
|
|
|
<span class="ai-gen-count" v-if="aiQuestions.length">已识别 {{ aiQuestions.length }} 题</span>
|
|
|
|
|
|
<div class="ai-gen-preview-actions" v-if="aiQuestions.length">
|
|
|
|
|
|
<el-button size="small" :icon="Check" @click="toggleAllAi">全选</el-button>
|
|
|
|
|
|
<el-button size="small" :icon="Delete" @click="clearAi">清空</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-loading="aiLoading" class="ai-gen-preview-body">
|
|
|
|
|
|
<div v-if="aiLoading && !aiRawResult" class="ai-thinking">
|
|
|
|
|
|
<span class="ai-thinking-text">AI 正在思考</span>
|
|
|
|
|
|
<span class="ai-dot" /><span class="ai-dot" /><span class="ai-dot" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<pre v-else-if="aiLoading && aiRawResult" class="ai-raw-stream">{{ aiRawResult }}</pre>
|
|
|
|
|
|
<div v-else-if="aiQuestions.length" class="ai-question-list">
|
|
|
|
|
|
<div v-for="(q, i) in aiQuestions" :key="i" class="ai-question-card" :class="{ 'is-selected': aiSelected.has(i) }" @click="toggleAiQuestion(i)">
|
|
|
|
|
|
<div class="ai-question-head">
|
|
|
|
|
|
<el-checkbox :model-value="aiSelected.has(i)" />
|
|
|
|
|
|
<span class="ai-question-type">{{ aiTypeName(q.type) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-question-stem">{{ q.question }}</div>
|
|
|
|
|
|
<div v-if="q.items && q.items.length" class="ai-question-options">
|
|
|
|
|
|
<div v-for="(opt, idx) in q.items" :key="idx" class="ai-question-option">{{ optionLabel(idx) }}. {{ opt }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ai-question-meta"><span class="ai-meta-label">答案:</span><span class="ai-meta-value">{{ q.type === 'true_false' ? (q.answers === 'A' ? '正确' : '错误') : q.answers }}</span></div>
|
|
|
|
|
|
<div v-if="q.difficulty" class="ai-question-meta"><span class="ai-meta-label">难度:</span><span class="ai-meta-value">{{ q.difficulty }}</span></div>
|
|
|
|
|
|
<div v-if="q.knowledges" class="ai-question-meta"><span class="ai-meta-label">知识点:</span><span class="ai-meta-value">{{ q.knowledges }}</span></div>
|
|
|
|
|
|
<div v-if="q.explan" class="ai-question-meta"><span class="ai-meta-label">解析:</span><span class="ai-meta-value">{{ q.explan }}</span></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-empty v-else description="尚未生成试题,请在左侧填写要求后点击「生成试题」" :image-size="80" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { reactive, ref } from "vue";
|
|
|
|
|
|
import { useRouter } from "vue-router";
|
|
|
|
|
|
import { ElMessage } from "element-plus";
|
|
|
|
|
|
import { ArrowLeft, Check, Delete, MagicStick, Plus, VideoPause } from "@element-plus/icons-vue";
|
|
|
|
|
|
import * as examApi from "@/api/exam";
|
|
|
|
|
|
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
|
|
const aiLoading = ref(false);
|
|
|
|
|
|
const aiImporting = ref(false);
|
|
|
|
|
|
const aiRawResult = ref("");
|
|
|
|
|
|
const aiQuestions = ref([]);
|
|
|
|
|
|
const aiSelected = reactive(new Set());
|
|
|
|
|
|
let aiAbort = null;
|
|
|
|
|
|
const aiForm = reactive({ title: "", description: "", single: 0, multiple: 0, judgment: 0, shortAnswer: 0, difficulty: "简单" });
|
|
|
|
|
|
const aiTypeName = (t) => ({ single_choice: "单选题", multiple_choice: "多选题", true_false: "判断题", short_answer: "简答题" }[t] || "问答题");
|
|
|
|
|
|
const optionLabel = (i) => String.fromCharCode(65 + i);
|
|
|
|
|
|
|
|
|
|
|
|
function goBack() { stopAiStream(); router.push("/exam/index"); }
|
|
|
|
|
|
function clearAi() { aiQuestions.value = []; aiSelected.clear(); }
|
|
|
|
|
|
function toggleAiQuestion(i) { if (aiSelected.has(i)) aiSelected.delete(i); else aiSelected.add(i); }
|
|
|
|
|
|
function toggleAllAi() {
|
|
|
|
|
|
if (aiSelected.size === aiQuestions.value.length) aiSelected.clear();
|
|
|
|
|
|
else { aiSelected.clear(); aiQuestions.value.forEach((_, i) => aiSelected.add(i)); }
|
|
|
|
|
|
}
|
|
|
|
|
|
function stopAiStream() { if (aiAbort) { aiAbort.abort(); aiAbort = null; } aiLoading.value = false; }
|
|
|
|
|
|
|
|
|
|
|
|
function buildAiPrompt() {
|
|
|
|
|
|
const types = [];
|
|
|
|
|
|
if (aiForm.single > 0) types.push(`${aiForm.single}道单选题`);
|
|
|
|
|
|
if (aiForm.multiple > 0) types.push(`${aiForm.multiple}道多选题`);
|
|
|
|
|
|
if (aiForm.judgment > 0) types.push(`${aiForm.judgment}道判断题`);
|
|
|
|
|
|
if (aiForm.shortAnswer > 0) types.push(`${aiForm.shortAnswer}道简答题`);
|
|
|
|
|
|
return `请帮我生成一套${aiForm.title},具体要求如下:
|
|
|
|
|
|
1. 测试包含${types.join("、")}
|
|
|
|
|
|
2. 题目难度为${aiForm.difficulty}级别
|
|
|
|
|
|
3. 主题围绕"${aiForm.description}"
|
|
|
|
|
|
题目模版参考这个:
|
|
|
|
|
|
{题号}. {题干内容}(单选题)
|
|
|
|
|
|
A. 选项A内容
|
|
|
|
|
|
B. 选项B内容
|
|
|
|
|
|
C. 选项C内容
|
|
|
|
|
|
D. 选项D内容
|
|
|
|
|
|
答案: {A}
|
|
|
|
|
|
难度:简单 / 中等 / 难 / 较难
|
|
|
|
|
|
知识点:知识点名称
|
|
|
|
|
|
解析:{解释为什么是正确答案}
|
|
|
|
|
|
|
|
|
|
|
|
{题号}. {题干内容}(多选题)
|
|
|
|
|
|
A. 选项A内容
|
|
|
|
|
|
B. 选项B内容
|
|
|
|
|
|
C. 选项C内容
|
|
|
|
|
|
D. 选项D内容
|
|
|
|
|
|
答案: {A,C}
|
|
|
|
|
|
难度:简单 / 中等 / 难 / 较难
|
|
|
|
|
|
知识点:知识点名称
|
|
|
|
|
|
解析:{解释每个正确选项的理由,指出错误选项的问题}
|
|
|
|
|
|
|
|
|
|
|
|
{题号}. {题干内容}(判断题)
|
|
|
|
|
|
A. 正确
|
|
|
|
|
|
B. 错误
|
|
|
|
|
|
答案: {A 或 B}
|
|
|
|
|
|
难度:简单 / 中等 / 难 / 较难
|
|
|
|
|
|
知识点:知识点名称
|
|
|
|
|
|
解析:{说明判断依据或背景知识}
|
|
|
|
|
|
|
|
|
|
|
|
{题号}. {问题内容}(问答题)
|
|
|
|
|
|
答案: {参考答案或标准答案要点}
|
|
|
|
|
|
难度:简单 / 中等 / 难 / 较难
|
|
|
|
|
|
知识点:知识点名称
|
|
|
|
|
|
解析:{答题思路、评分标准、重点提示等}
|
|
|
|
|
|
|
|
|
|
|
|
必须根据题型和数量返回!不使用Markdown语法,开启严格模式严格按照我提供的题目数量和题目类型生成试题。`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function generateAi() {
|
|
|
|
|
|
if (!aiForm.title.trim()) return ElMessage.warning("请输入题目标题");
|
|
|
|
|
|
if (!aiForm.description.trim()) return ElMessage.warning("请输入生成要求描述");
|
|
|
|
|
|
const total = aiForm.single + aiForm.multiple + aiForm.judgment + aiForm.shortAnswer;
|
|
|
|
|
|
if (!total) return ElMessage.warning("请至少设置一种题型的数量");
|
|
|
|
|
|
aiLoading.value = true;
|
|
|
|
|
|
aiRawResult.value = "";
|
|
|
|
|
|
aiQuestions.value = [];
|
|
|
|
|
|
aiSelected.clear();
|
|
|
|
|
|
const messages = [
|
|
|
|
|
|
{ role: "system", content: "我是深圳市天择教育科技AI大模型,可以进行实时搜索和推理" },
|
|
|
|
|
|
{ role: "user", content: buildAiPrompt() }
|
|
|
|
|
|
];
|
|
|
|
|
|
try {
|
|
|
|
|
|
aiAbort = new AbortController();
|
|
|
|
|
|
const aiBase = (import.meta.env.VITE_APP_AI_API || "/").replace(/\/?$/, "/");
|
|
|
|
|
|
const resp = await fetch(`${aiBase}api/stream/chat`, {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({ type: 1, enableSearch: true, enableThinking: false, messages }),
|
|
|
|
|
|
signal: aiAbort.signal
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!resp.ok) throw new Error(`请求失败,状态码: ${resp.status}`);
|
|
|
|
|
|
const reader = resp.body.getReader();
|
|
|
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
|
let buffer = "";
|
|
|
|
|
|
let jsonParts = [];
|
|
|
|
|
|
let jsonDepth = 0;
|
|
|
|
|
|
let inString = false;
|
|
|
|
|
|
let escapeChar = false;
|
|
|
|
|
|
let complete = "";
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
const { done, value } = await reader.read();
|
|
|
|
|
|
if (done) break;
|
|
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
|
|
for (let i = 0; i < buffer.length; i++) {
|
|
|
|
|
|
const ch = buffer[i];
|
|
|
|
|
|
if (!inString && ch === "{") {
|
|
|
|
|
|
if (jsonDepth === 0) jsonParts = [];
|
|
|
|
|
|
jsonDepth++;
|
|
|
|
|
|
jsonParts.push(ch);
|
|
|
|
|
|
} else if (!inString && ch === "}") {
|
|
|
|
|
|
jsonDepth--;
|
|
|
|
|
|
jsonParts.push(ch);
|
|
|
|
|
|
if (jsonDepth === 0) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(jsonParts.join(""));
|
|
|
|
|
|
if (data.content) { complete += data.content; aiRawResult.value = complete; }
|
|
|
|
|
|
} catch (e) { /* 不完整 JSON 片段,跳过 */ }
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (jsonDepth > 0) {
|
|
|
|
|
|
if (ch === '"' && !escapeChar) inString = !inString;
|
|
|
|
|
|
jsonParts.push(ch);
|
|
|
|
|
|
escapeChar = ch === "\\" && !escapeChar;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
aiQuestions.value = parseAiQuestions(complete);
|
|
|
|
|
|
aiRawResult.value = "";
|
|
|
|
|
|
ElMessage.success(`成功识别 ${aiQuestions.value.length} 道题,勾选后可加入题库`);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (e.name !== "AbortError") ElMessage.error("AI 生成失败:" + e.message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
stopAiStream();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 解析 AI 文本输出为结构化题目数组(迁移自 AI-assistant parseQuestionsToJSON)*/
|
|
|
|
|
|
function parseAiQuestions(text) {
|
|
|
|
|
|
const cleaned = text.replace(/\n\s*\n/g, "\n");
|
|
|
|
|
|
const blocks = cleaned.trim().split(/(?=^\d)/gm);
|
|
|
|
|
|
return blocks.map((block) => {
|
|
|
|
|
|
const lines = block.split("\n");
|
|
|
|
|
|
const qLine = lines[0];
|
|
|
|
|
|
const items = [];
|
|
|
|
|
|
let answer = "", difficulty = "", knowledge = "", explan = "";
|
|
|
|
|
|
lines.forEach((line) => {
|
|
|
|
|
|
if (/^[A-E]\.\s*/.test(line)) items.push(line.replace(/^[A-E]\.\s*/, ""));
|
|
|
|
|
|
else if (line.startsWith("答案:") || line.startsWith("答案:")) answer = line.replace(/^答案[::]/, "").trim();
|
|
|
|
|
|
else if (line.startsWith("难度:")) difficulty = line.replace("难度:", "").trim();
|
|
|
|
|
|
else if (line.startsWith("知识点:")) knowledge = line.replace("知识点:", "").trim();
|
|
|
|
|
|
else if (line.startsWith("解析:")) explan = line.replace("解析:", "").trim();
|
|
|
|
|
|
});
|
|
|
|
|
|
return {
|
|
|
|
|
|
question: qLine.replace(/^\d+\.\s*/, "").replace(/[(\(].*?[)\)]/, "").replace(/".*?"/, ""),
|
|
|
|
|
|
type: aiDetectType(qLine),
|
|
|
|
|
|
difficulty, items, answers: answer, explan, knowledges: knowledge
|
|
|
|
|
|
};
|
|
|
|
|
|
}).filter((q) => q.question);
|
|
|
|
|
|
}
|
|
|
|
|
|
function aiDetectType(q) {
|
|
|
|
|
|
if (q.includes("单选题")) return "single_choice";
|
|
|
|
|
|
if (q.includes("多选题")) return "multiple_choice";
|
|
|
|
|
|
if (q.includes("判断题")) return "true_false";
|
|
|
|
|
|
if (q.includes("问答题") || q.includes("简答题")) return "short_answer";
|
|
|
|
|
|
return "short_answer";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 将 AI 题目转换为教师题库 schema 并批量入库 */
|
|
|
|
|
|
function convertAiQuestion(q) {
|
|
|
|
|
|
const typeMap = { single_choice: "SINGLE", multiple_choice: "MULTIPLE", true_false: "TRUE_FALSE", short_answer: "SHORT_ANSWER" };
|
|
|
|
|
|
const type = typeMap[q.type];
|
|
|
|
|
|
if (!type) return null;
|
|
|
|
|
|
const item = { id: null, taskKey: "", type, stem: q.question || "", options: [], answer: null, analysis: q.explan || "" };
|
|
|
|
|
|
if (type === "SINGLE" || type === "MULTIPLE") {
|
|
|
|
|
|
item.options = (q.items || []).map((c, i) => ({ label: String.fromCharCode(65 + i), content: c }));
|
|
|
|
|
|
const ans = String(q.answers || "").split(/[,,、]/).map((s) => s.trim()).filter(Boolean);
|
|
|
|
|
|
item.answer = type === "SINGLE" ? ans.slice(0, 1) : ans;
|
|
|
|
|
|
} else if (type === "TRUE_FALSE") {
|
|
|
|
|
|
item.answer = q.answers === "A" || q.answers === "正确";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
item.answer = q.answers || "";
|
|
|
|
|
|
}
|
|
|
|
|
|
return item;
|
|
|
|
|
|
}
|
|
|
|
|
|
async function importAiQuestions() {
|
|
|
|
|
|
const targets = [...aiSelected].map((i) => convertAiQuestion(aiQuestions.value[i])).filter(Boolean);
|
|
|
|
|
|
if (!targets.length) return ElMessage.warning("请选择要加入的题目");
|
|
|
|
|
|
aiImporting.value = true;
|
|
|
|
|
|
let ok = 0;
|
|
|
|
|
|
try {
|
|
|
|
|
|
for (const q of targets) {
|
|
|
|
|
|
try { await examApi.createQuestion(q); ok++; } catch (e) { console.error("导入失败", e); }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ok) {
|
|
|
|
|
|
ElMessage.success(`成功加入 ${ok} 道题到题库`);
|
|
|
|
|
|
goBack();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error("加入题库失败,请重试");
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally { aiImporting.value = false; }
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
/* 按 teacher-edu-style skill 规范:token 全来自 teacher-light.scss 第 0 节,scoped 只写布局 */
|
|
|
|
|
|
.ai-gen-page {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--sp-2);
|
|
|
|
|
|
margin-bottom: var(--sp-4);
|
|
|
|
|
|
/* 扁平:skill 禁止页头大卡背景 */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-title {
|
|
|
|
|
|
font-size: var(--fs-page);
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--text-strong);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-sub {
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
font-size: var(--fs-body);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-spacer {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-main {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 340px 1fr;
|
|
|
|
|
|
gap: var(--sp-4);
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-form-card,
|
|
|
|
|
|
.ai-gen-preview-card {
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
border-radius: var(--r-card);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-form {
|
|
|
|
|
|
padding: var(--sp-4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 关键:题型 2x2 grid 紧凑布局 */
|
|
|
|
|
|
.ai-gen-type-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-type-cell {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--sp-1);
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-type-name {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
font-size: var(--fs-body);
|
|
|
|
|
|
color: var(--text-strong);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-input-number--small {
|
|
|
|
|
|
width: 96px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-type-tip {
|
|
|
|
|
|
margin-top: var(--sp-1);
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
font-size: var(--fs-body);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-preview-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 540px;
|
|
|
|
|
|
max-height: calc(100vh - 200px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-preview-head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--sp-3);
|
|
|
|
|
|
padding: var(--sp-3) var(--sp-4);
|
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
|
/* skill 规范:白卡纯白,不用蓝底 */
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-preview-title {
|
|
|
|
|
|
font-size: var(--fs-card);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--text-strong);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-count {
|
|
|
|
|
|
color: var(--edu-primary);
|
|
|
|
|
|
font-size: var(--fs-body);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-preview-actions {
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--sp-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-gen-preview-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: var(--sp-4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-thinking {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--sp-1);
|
|
|
|
|
|
padding: 24px 0;
|
|
|
|
|
|
color: var(--edu-primary);
|
|
|
|
|
|
font-size: var(--fs-body);
|
|
|
|
|
|
|
|
|
|
|
|
.ai-thinking-text { font-weight: 600; }
|
|
|
|
|
|
|
|
|
|
|
|
.ai-dot {
|
|
|
|
|
|
width: 6px; height: 6px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--edu-primary);
|
|
|
|
|
|
animation: ai-bounce 1.2s infinite ease-in-out;
|
|
|
|
|
|
&:nth-child(3) { animation-delay: 0.2s; }
|
|
|
|
|
|
&:nth-child(4) { animation-delay: 0.4s; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-raw-stream {
|
|
|
|
|
|
margin: 0; padding: 8px 10px;
|
|
|
|
|
|
background: #f9fafb; border-radius: var(--r-control);
|
|
|
|
|
|
color: var(--text-strong); font-size: var(--fs-body); line-height: 1.6;
|
|
|
|
|
|
white-space: pre-wrap; word-break: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-question-list {
|
|
|
|
|
|
display: flex; flex-direction: column; gap: var(--sp-3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-question-card {
|
|
|
|
|
|
padding: var(--sp-3);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
border-radius: var(--r-control);
|
|
|
|
|
|
background: #fff; cursor: pointer;
|
|
|
|
|
|
transition: border-color 0.15s, background 0.15s;
|
|
|
|
|
|
&:hover { border-color: var(--edu-primary); }
|
|
|
|
|
|
&.is-selected { border-color: var(--edu-primary); background: var(--edu-primary-soft); }
|
|
|
|
|
|
|
|
|
|
|
|
.ai-question-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-1); }
|
|
|
|
|
|
.ai-question-type {
|
|
|
|
|
|
padding: 1px 8px; border: 1px solid var(--edu-primary); border-radius: 4px;
|
|
|
|
|
|
background: var(--edu-primary-soft); color: var(--edu-primary); font-size: var(--fs-tag);
|
|
|
|
|
|
}
|
|
|
|
|
|
.ai-question-stem { margin-bottom: var(--sp-1); color: var(--text-strong); font-size: var(--fs-card); line-height: 1.6; }
|
|
|
|
|
|
.ai-question-options { margin-bottom: var(--sp-1); }
|
|
|
|
|
|
.ai-question-option { padding: 2px 0; color: var(--text-body); font-size: var(--fs-body); line-height: 1.6; }
|
|
|
|
|
|
.ai-question-meta { display: flex; font-size: var(--fs-body); line-height: 1.6; }
|
|
|
|
|
|
.ai-meta-label { flex: 0 0 auto; color: var(--text-muted); }
|
|
|
|
|
|
.ai-meta-value { color: var(--text-body); word-break: break-all; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes ai-bounce {
|
|
|
|
|
|
0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
|
|
|
|
|
|
40% { transform: translateY(-6px); opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|