1445 lines
36 KiB
Vue
1445 lines
36 KiB
Vue
<script setup>
|
||
import { ref, computed, onUnmounted } from "vue";
|
||
import { useRouter } from "vue-router";
|
||
import axios from "axios";
|
||
|
||
import {
|
||
GRS_API_KEY,
|
||
QUESTION_API_URL,
|
||
QUESTION_MODEL,
|
||
QUESTION_TEMPERATURE,
|
||
QUESTION_MAX_TOKENS,
|
||
QUESTION_GENERATOR_PROMPT,
|
||
} from "@/config/index.js";
|
||
|
||
const router = useRouter();
|
||
|
||
const API_KEY = GRS_API_KEY;
|
||
const API_URL = QUESTION_API_URL;
|
||
|
||
// ── 状态 ──
|
||
const status = ref("idle"); // 'idle' | 'generating' | 'done' | 'error'
|
||
const errorMsg = ref("");
|
||
const rawContent = ref("");
|
||
const cancelTokenSource = ref(null);
|
||
|
||
// ── 参数配置 ──
|
||
const difficulty = ref("medium"); // 'easy' | 'medium' | 'hard'
|
||
const questionCount = ref(5);
|
||
|
||
// 新增维度:教材章节、知识点、题型
|
||
const textbookChapter = ref(""); // 教材章节(预设)
|
||
const customTextbookChapter = ref(""); // 自定义教材章节输入
|
||
const selectedKnowledgePoints = ref([]); // 选中的知识点
|
||
const customKnowledgePoint = ref(""); // 自定义知识点输入
|
||
const questionFormat = ref("单项选择"); // 题型(预设)
|
||
const customQuestionFormat = ref(""); // 自定义题型输入
|
||
|
||
// ── 配置选项 ──
|
||
const DIFFICULTY_OPTIONS = [
|
||
{ value: "very_easy", label: "容易", color: "#10b981" },
|
||
{ value: "easy", label: "较易", color: "#34d399" },
|
||
{ value: "medium", label: "适中", color: "#f59e0b" },
|
||
{ value: "hard", label: "较难", color: "#f97316" },
|
||
{ value: "very_hard", label: "困难", color: "#ef4444" },
|
||
];
|
||
|
||
// 教材章节预设选项
|
||
const CHAPTER_OPTIONS = [
|
||
{ value: "Starter Unit1 You and Me", label: "Starter Unit1 You and Me" },
|
||
{ value: "Starter Unit2 Keep Tidy", label: "Starter Unit2 Keep Tidy" },
|
||
{ value: "Starter Unit3 Welcome!", label: "Starter Unit3 Welcome!" },
|
||
{ value: "Unit 1 You and Me", label: "Unit 1 You and Me" },
|
||
{ value: "Unit 2 We're Family!", label: "Unit 2 We're Family!" },
|
||
{ value: "Unit 3 My School", label: "Unit 3 My School" },
|
||
];
|
||
|
||
// 知识点预设选项
|
||
const KNOWLEDGE_POINT_OPTIONS = [
|
||
{ value: "语法", label: "语法" },
|
||
{ value: "词汇", label: "词汇" },
|
||
{ value: "冠词", label: "冠词" },
|
||
{ value: "不定冠词", label: "不定冠词" },
|
||
{ value: "定冠词", label: "定冠词" },
|
||
{ value: "名词", label: "名词" },
|
||
{ value: "动词", label: "动词" },
|
||
{ value: "形容词", label: "形容词" },
|
||
{ value: "代词", label: "代词" },
|
||
{ value: "介词", label: "介词" },
|
||
{ value: "时态", label: "时态" },
|
||
{ value: "一般现在时", label: "一般现在时" },
|
||
{ value: "现在进行时", label: "现在进行时" },
|
||
{ value: "一般过去时", label: "一般过去时" },
|
||
{ value: "句型结构", label: "句型结构" },
|
||
{ value: "阅读理解", label: "阅读理解" },
|
||
{ value: "写作", label: "写作" },
|
||
];
|
||
|
||
// 题型预设选项
|
||
const FORMAT_OPTIONS = [
|
||
{ value: "单项选择", label: "单项选择" },
|
||
{ value: "完形填空", label: "完形填空" },
|
||
{ value: "句型转换", label: "句型转换" },
|
||
{ value: "词汇运用", label: "词汇运用" },
|
||
{ value: "翻译句子", label: "翻译句子" },
|
||
{ value: "阅读理解", label: "阅读理解" },
|
||
{ value: "书面表达", label: "书面表达" },
|
||
{ value: "短文填空", label: "短文填空" },
|
||
{ value: "语法填空", label: "语法填空" },
|
||
{ value: "选词填空", label: "选词填空" },
|
||
];
|
||
|
||
// 切换知识点选中状态
|
||
const toggleKnowledgePoint = (value) => {
|
||
const index = selectedKnowledgePoints.value.indexOf(value);
|
||
if (index > -1) {
|
||
selectedKnowledgePoints.value.splice(index, 1);
|
||
} else {
|
||
selectedKnowledgePoints.value.push(value);
|
||
}
|
||
};
|
||
|
||
// ── 解析试题内容 ──
|
||
const parsedQuestions = computed(() => {
|
||
const raw = rawContent.value;
|
||
if (!raw) return [];
|
||
|
||
const questions = [];
|
||
const questionBlocks = raw.split(/##\s*题目\s*\d+/i);
|
||
|
||
questionBlocks.forEach((block, index) => {
|
||
if (index === 0) return; // 跳过第一个空块
|
||
|
||
const question = {
|
||
id: index,
|
||
type: extractField(block, "题目类型"),
|
||
difficulty: extractField(block, "难度等级"),
|
||
content: extractField(block, "题目内容"),
|
||
options: extractOptions(block),
|
||
answer: extractField(block, "正确答案"),
|
||
explanation: extractField(block, "解析"),
|
||
knowledgePoint: extractField(block, "知识点"),
|
||
};
|
||
|
||
if (question.content) {
|
||
questions.push(question);
|
||
}
|
||
});
|
||
|
||
return questions;
|
||
});
|
||
|
||
function extractField(text, fieldName) {
|
||
const regex = new RegExp(`\\*\\*${fieldName}\\*\\*[::]\\s*([\\s\\S]*?)(?=\\*\\*|$)`, "i");
|
||
const match = text.match(regex);
|
||
if (!match) return "";
|
||
|
||
let content = match[1].trim();
|
||
// 清理可能混入的下一个字段
|
||
content = content.split(/\*\*/)[0].trim();
|
||
return content;
|
||
}
|
||
|
||
function extractOptions(text) {
|
||
const options = [];
|
||
|
||
// 先尝试提取"选项"字段的内容块
|
||
const optionsBlockMatch = text.match(/\*\*选项\*\*[::]\s*([\s\S]*?)(?=\*\*正确答案\*\*|$)/i);
|
||
|
||
if (optionsBlockMatch) {
|
||
const optionsBlock = optionsBlockMatch[1];
|
||
|
||
// 在选项块中提取各个选项
|
||
const lines = optionsBlock.split('\n');
|
||
for (const line of lines) {
|
||
const match = line.match(/^([A-D])[\..。::]\s*(.+)$/i);
|
||
if (match) {
|
||
options.push({
|
||
label: match[1].toUpperCase(),
|
||
content: match[2].trim(),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
return options;
|
||
}
|
||
|
||
// ── 构建请求体 ──
|
||
const buildRequestBody = () => {
|
||
const difficultyLabel =
|
||
DIFFICULTY_OPTIONS.find((d) => d.value === difficulty.value)?.label || "中等";
|
||
|
||
// 构建知识点字符串:预设选项 + 自定义输入
|
||
const allKnowledgePoints = [
|
||
...selectedKnowledgePoints.value,
|
||
...(customKnowledgePoint.value ? [customKnowledgePoint.value] : []),
|
||
];
|
||
const knowledgePointStr = allKnowledgePoints.length > 0 ? allKnowledgePoints.join("、") : "";
|
||
|
||
// 教材章节:优先使用自定义输入,否则使用预设选择
|
||
const chapterStr = customTextbookChapter.value || textbookChapter.value || "";
|
||
|
||
// 题型:优先使用自定义输入,否则使用预设选择
|
||
const formatStr = customQuestionFormat.value || questionFormat.value || "";
|
||
|
||
const userPrompt = `请生成 ${questionCount.value} 道${difficultyLabel}难度的英语试题。
|
||
|
||
要求:
|
||
1. 题型:${formatStr}
|
||
2. 难度等级:${difficultyLabel}
|
||
3. 题目数量:${questionCount.value} 道
|
||
${chapterStr ? `4. 教材章节:${chapterStr}` : ""}
|
||
${knowledgePointStr ? `5. 知识点:${knowledgePointStr}` : ""}
|
||
|
||
请严格按照指定的输出格式生成试题。`;
|
||
|
||
return {
|
||
model: QUESTION_MODEL,
|
||
messages: [
|
||
{ role: "system", content: QUESTION_GENERATOR_PROMPT },
|
||
{ role: "user", content: userPrompt },
|
||
],
|
||
temperature: QUESTION_TEMPERATURE,
|
||
max_tokens: QUESTION_MAX_TOKENS,
|
||
stream: true,
|
||
};
|
||
};
|
||
|
||
// ── 提取 SSE delta 文本 ──
|
||
const extractTextDelta = (chunk) => {
|
||
let result = "";
|
||
const lines = chunk.split("\n");
|
||
for (const line of lines) {
|
||
if (!line.startsWith("data: ")) continue;
|
||
const jsonStr = line.slice(6).trim();
|
||
if (!jsonStr || jsonStr === "[DONE]") continue;
|
||
try {
|
||
const data = JSON.parse(jsonStr);
|
||
const text = data?.choices?.[0]?.delta?.content;
|
||
if (text) result += text;
|
||
} catch {
|
||
// 忽略解析失败的行
|
||
}
|
||
}
|
||
return result;
|
||
};
|
||
|
||
// ── 过滤 HexString思考块 ──
|
||
const filterThinkBlocks = (text) => {
|
||
let filtered = text.replace(/HexString[\s\S]*?<\/think>/g, "");
|
||
filtered = filtered.replace(/HexString[\s\S]*$/, "");
|
||
return filtered;
|
||
};
|
||
|
||
// ── 开始生成 ──
|
||
const canGenerate = computed(() => {
|
||
return status.value !== "generating" && questionCount.value >= 1 && questionCount.value <= 20;
|
||
});
|
||
|
||
const startGeneration = async () => {
|
||
if (!canGenerate.value) return;
|
||
|
||
status.value = "generating";
|
||
errorMsg.value = "";
|
||
rawContent.value = "";
|
||
|
||
cancelTokenSource.value = axios.CancelToken.source();
|
||
let lastLength = 0;
|
||
|
||
await axios
|
||
.post(API_URL, buildRequestBody(), {
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${API_KEY}`,
|
||
},
|
||
responseType: "text",
|
||
cancelToken: cancelTokenSource.value.token,
|
||
onDownloadProgress: (event) => {
|
||
const text = (event.target || event.event?.target)?.responseText || "";
|
||
if (!text) return;
|
||
const newChunk = text.slice(lastLength);
|
||
lastLength = text.length;
|
||
const delta = extractTextDelta(newChunk);
|
||
if (delta) {
|
||
rawContent.value = filterThinkBlocks(rawContent.value + delta);
|
||
}
|
||
},
|
||
})
|
||
.then(() => {
|
||
status.value = "done";
|
||
})
|
||
.catch((err) => {
|
||
if (axios.isCancel(err)) return;
|
||
console.error(err);
|
||
errorMsg.value =
|
||
err?.response?.data?.error?.message || err.message || "请求失败,请稍后重试";
|
||
status.value = "error";
|
||
});
|
||
};
|
||
|
||
const resetAll = () => {
|
||
status.value = "idle";
|
||
rawContent.value = "";
|
||
errorMsg.value = "";
|
||
};
|
||
|
||
const loadExample = () => {
|
||
difficulty.value = "medium";
|
||
questionCount.value = 3;
|
||
textbookChapter.value = "Starter Unit2 Keep Tidy";
|
||
customTextbookChapter.value = "";
|
||
selectedKnowledgePoints.value = ["语法", "冠词", "不定冠词"];
|
||
customKnowledgePoint.value = "";
|
||
questionFormat.value = "句型转换";
|
||
customQuestionFormat.value = "";
|
||
};
|
||
|
||
const goBack = () => router.back();
|
||
|
||
onUnmounted(() => {
|
||
if (cancelTokenSource.value) {
|
||
cancelTokenSource.value.cancel("组件卸载,取消请求");
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page-container">
|
||
<!-- Nav Bar -->
|
||
<div class="nav-bar">
|
||
<button class="back-btn" @click="goBack">
|
||
<svg
|
||
width="20"
|
||
height="20"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2.2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<polyline points="15 18 9 12 15 6" />
|
||
</svg>
|
||
<span>返回</span>
|
||
</button>
|
||
<h1 class="nav-title">AI 试题生成</h1>
|
||
<div style="width: 72px"></div>
|
||
</div>
|
||
|
||
<!-- Error Banner -->
|
||
<div v-if="errorMsg" class="error-banner">
|
||
<span>{{ errorMsg }}</span>
|
||
<button class="error-close" @click="errorMsg = ''">✕</button>
|
||
</div>
|
||
|
||
<!-- Main Layout: Left + Right -->
|
||
<div class="main-layout">
|
||
<!-- Left Panel: Config -->
|
||
<div class="left-panel">
|
||
<div class="config-card">
|
||
<div class="config-header">
|
||
<svg
|
||
width="20"
|
||
height="20"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<circle cx="12" cy="12" r="3" />
|
||
<path
|
||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
|
||
/>
|
||
</svg>
|
||
<span>参数配置</span>
|
||
</div>
|
||
|
||
<div class="config-body">
|
||
<!-- 难度等级 -->
|
||
<div class="config-item">
|
||
<label class="config-label">难度等级</label>
|
||
<div class="difficulty-group">
|
||
<button
|
||
v-for="opt in DIFFICULTY_OPTIONS"
|
||
:key="opt.value"
|
||
class="difficulty-btn"
|
||
:class="{ active: difficulty === opt.value }"
|
||
:style="{ '--accent-color': opt.color }"
|
||
@click="difficulty = opt.value"
|
||
:disabled="status === 'generating'"
|
||
>
|
||
{{ opt.label }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 题目数量 -->
|
||
<div class="config-item">
|
||
<label class="config-label">
|
||
题目数量
|
||
<span class="count-value">{{ questionCount }} 道</span>
|
||
</label>
|
||
<input
|
||
type="range"
|
||
v-model.number="questionCount"
|
||
min="1"
|
||
max="20"
|
||
class="range-slider"
|
||
:disabled="status === 'generating'"
|
||
/>
|
||
<div class="range-labels">
|
||
<span>1</span>
|
||
<span>10</span>
|
||
<span>20</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 教材章节 -->
|
||
<div class="config-item">
|
||
<label class="config-label">教材章节(可选)</label>
|
||
<select
|
||
v-model="textbookChapter"
|
||
class="select-input"
|
||
:disabled="status === 'generating'"
|
||
>
|
||
<option value="">请选择教材章节</option>
|
||
<option
|
||
v-for="opt in CHAPTER_OPTIONS"
|
||
:key="opt.value"
|
||
:value="opt.value"
|
||
>
|
||
{{ opt.label }}
|
||
</option>
|
||
</select>
|
||
<input
|
||
type="text"
|
||
v-model="customTextbookChapter"
|
||
class="topic-input mt-2"
|
||
placeholder="或输入自定义教材章节..."
|
||
:disabled="status === 'generating'"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 知识点 -->
|
||
<div class="config-item">
|
||
<label class="config-label">知识点(可多选)</label>
|
||
<div class="knowledge-grid">
|
||
<button
|
||
v-for="opt in KNOWLEDGE_POINT_OPTIONS"
|
||
:key="opt.value"
|
||
class="knowledge-btn"
|
||
:class="{ active: selectedKnowledgePoints.includes(opt.value) }"
|
||
@click="toggleKnowledgePoint(opt.value)"
|
||
:disabled="status === 'generating'"
|
||
>
|
||
{{ opt.label }}
|
||
</button>
|
||
</div>
|
||
<input
|
||
type="text"
|
||
v-model="customKnowledgePoint"
|
||
class="topic-input mt-2"
|
||
placeholder="或输入自定义知识点..."
|
||
:disabled="status === 'generating'"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 题型 -->
|
||
<div class="config-item">
|
||
<label class="config-label">题型</label>
|
||
<select
|
||
v-model="questionFormat"
|
||
class="select-input"
|
||
:disabled="status === 'generating'"
|
||
>
|
||
<option
|
||
v-for="opt in FORMAT_OPTIONS"
|
||
:key="opt.value"
|
||
:value="opt.value"
|
||
>
|
||
{{ opt.label }}
|
||
</option>
|
||
</select>
|
||
<input
|
||
type="text"
|
||
v-model="customQuestionFormat"
|
||
class="topic-input mt-2"
|
||
placeholder="或输入自定义题型..."
|
||
:disabled="status === 'generating'"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 示例按钮 -->
|
||
<button class="example-btn" @click="loadExample" :disabled="status === 'generating'">
|
||
<svg
|
||
width="16"
|
||
height="16"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||
<polyline points="14 2 14 8 20 8" />
|
||
<line x1="16" y1="13" x2="8" y2="13" />
|
||
<line x1="16" y1="17" x2="8" y2="17" />
|
||
</svg>
|
||
<span>加载示例</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Generate Button -->
|
||
<div class="btn-wrap">
|
||
<button
|
||
class="generate-btn"
|
||
:disabled="!canGenerate"
|
||
@click="startGeneration"
|
||
>
|
||
<span v-if="status === 'generating'" class="btn-spinner"></span>
|
||
<svg
|
||
v-else
|
||
width="20"
|
||
height="20"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||
</svg>
|
||
<span>{{ status === "generating" ? "生成中..." : "开始生成" }}</span>
|
||
</button>
|
||
<button
|
||
v-if="status === 'done' || status === 'error'"
|
||
class="reset-btn"
|
||
@click="resetAll"
|
||
>
|
||
重新生成
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Panel: Result -->
|
||
<div class="right-panel">
|
||
<!-- Empty State -->
|
||
<div v-if="status === 'idle'" class="empty-state">
|
||
<div class="empty-icon">
|
||
<svg
|
||
width="48"
|
||
height="48"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||
</svg>
|
||
</div>
|
||
<p class="empty-title">等待生成</p>
|
||
<p class="empty-hint">在左侧配置参数,点击「开始生成」创建试题</p>
|
||
</div>
|
||
|
||
<!-- Result Area -->
|
||
<div
|
||
v-else-if="status === 'generating' || status === 'done'"
|
||
class="result-area"
|
||
>
|
||
<div class="result-header">
|
||
<div class="result-title-wrap">
|
||
<div
|
||
class="result-dot"
|
||
:class="{ pulse: status === 'generating' }"
|
||
></div>
|
||
<span class="result-title">{{
|
||
status === "generating" ? "AI 正在生成..." : "生成完成"
|
||
}}</span>
|
||
</div>
|
||
<span v-if="parsedQuestions.length > 0" class="question-count-badge">
|
||
已生成 {{ parsedQuestions.length }} 题
|
||
</span>
|
||
</div>
|
||
|
||
<div class="questions-list">
|
||
<div
|
||
v-for="(q, idx) in parsedQuestions"
|
||
:key="q.id"
|
||
class="question-card"
|
||
:style="{ animationDelay: `${idx * 0.1}s` }"
|
||
>
|
||
<div class="question-header">
|
||
<div class="question-number">题目 {{ q.id }}</div>
|
||
<div class="question-meta">
|
||
<span class="meta-tag type-tag">{{ q.type }}</span>
|
||
<span class="meta-tag difficulty-tag">{{ q.difficulty }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="question-content">
|
||
<p class="content-text">{{ q.content }}</p>
|
||
</div>
|
||
|
||
<div v-if="q.options && q.options.length > 0" class="options-grid">
|
||
<div
|
||
v-for="opt in q.options"
|
||
:key="opt.label"
|
||
class="option-item"
|
||
>
|
||
<span class="option-label">{{ opt.label }}.</span>
|
||
<span class="option-text">{{ opt.content }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="answer-section">
|
||
<div class="answer-row">
|
||
<span class="answer-label">正确答案:</span>
|
||
<span class="answer-value">{{ q.answer }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="explanation-section">
|
||
<div class="explanation-header">
|
||
<svg
|
||
width="16"
|
||
height="16"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<circle cx="12" cy="12" r="10" />
|
||
<line x1="12" y1="16" x2="12" y2="12" />
|
||
<line x1="12" y1="8" x2="12.01" y2="8" />
|
||
</svg>
|
||
<span>解析</span>
|
||
</div>
|
||
<p class="explanation-text">{{ q.explanation }}</p>
|
||
</div>
|
||
|
||
<div class="knowledge-section">
|
||
<span class="knowledge-label">知识点:</span>
|
||
<span class="knowledge-value">{{ q.knowledgePoint }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 生成中的占位符 -->
|
||
<div v-if="status === 'generating'" class="loading-card">
|
||
<div class="loading-dots">
|
||
<span class="dot"></span>
|
||
<span class="dot"></span>
|
||
<span class="dot"></span>
|
||
</div>
|
||
<p class="loading-text">正在生成试题,请稍候...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Error State -->
|
||
<div v-else-if="status === 'error'" class="empty-state error-state">
|
||
<div class="empty-icon error-icon">
|
||
<svg
|
||
width="48"
|
||
height="48"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<circle cx="12" cy="12" r="10" />
|
||
<line x1="12" y1="8" x2="12" y2="12" />
|
||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||
</svg>
|
||
</div>
|
||
<p class="empty-title">生成失败</p>
|
||
<p class="empty-hint">请检查网络连接后重试</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.page-container {
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
background: var(--bg);
|
||
display: flex;
|
||
flex-direction: column;
|
||
font-family: "Inter", "PingFang SC", -apple-system, sans-serif;
|
||
}
|
||
|
||
/* Nav */
|
||
.nav-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 1rem 1.5rem;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border-bottom: 1px solid var(--card-border);
|
||
backdrop-filter: blur(12px);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
.back-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.35rem;
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-secondary);
|
||
cursor: pointer;
|
||
font-size: 0.95rem;
|
||
padding: 0.4rem 0.75rem;
|
||
border-radius: 8px;
|
||
transition: all 0.2s;
|
||
}
|
||
.back-btn:hover {
|
||
background: rgba(255, 255, 255, 0.07);
|
||
color: var(--text-primary);
|
||
}
|
||
.nav-title {
|
||
font-size: 1.2rem;
|
||
font-weight: 600;
|
||
margin: 0;
|
||
background: linear-gradient(135deg, #10b981, #059669);
|
||
-webkit-background-clip: text;
|
||
background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
|
||
/* Main Layout */
|
||
.main-layout {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Left Panel */
|
||
.left-panel {
|
||
width: 380px;
|
||
min-width: 320px;
|
||
max-width: 440px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
padding: 1.5rem;
|
||
border-right: 1px solid var(--card-border);
|
||
overflow-y: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Right Panel */
|
||
.right-panel {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 1.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.right-panel::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
.right-panel::-webkit-scrollbar-track {
|
||
background: rgba(0, 0, 0, 0.1);
|
||
border-radius: 4px;
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.right-panel::-webkit-scrollbar-thumb {
|
||
background: rgba(16, 185, 129, 0.3);
|
||
border-radius: 4px;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.right-panel::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(16, 185, 129, 0.5);
|
||
}
|
||
|
||
/* Empty State */
|
||
.empty-state {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.75rem;
|
||
color: var(--text-secondary);
|
||
opacity: 0.5;
|
||
}
|
||
.empty-icon {
|
||
color: var(--text-secondary);
|
||
opacity: 0.4;
|
||
}
|
||
.empty-title {
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
margin: 0;
|
||
color: var(--text-primary);
|
||
}
|
||
.empty-hint {
|
||
font-size: 0.875rem;
|
||
margin: 0;
|
||
text-align: center;
|
||
}
|
||
.error-state {
|
||
opacity: 1;
|
||
}
|
||
.error-icon {
|
||
color: #f87171;
|
||
opacity: 1;
|
||
}
|
||
.error-state .empty-title {
|
||
color: #fca5a5;
|
||
}
|
||
|
||
/* Error Banner */
|
||
.error-banner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: rgba(239, 68, 68, 0.12);
|
||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||
border-bottom: 1px solid rgba(239, 68, 68, 0.3);
|
||
padding: 0.75rem 1.5rem;
|
||
color: #fca5a5;
|
||
font-size: 0.9rem;
|
||
animation: fadeInUp 0.2s ease;
|
||
flex-shrink: 0;
|
||
}
|
||
.error-close {
|
||
background: none;
|
||
border: none;
|
||
color: #fca5a5;
|
||
cursor: pointer;
|
||
font-size: 1rem;
|
||
opacity: 0.7;
|
||
transition: opacity 0.2s;
|
||
}
|
||
.error-close:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Config Card */
|
||
.config-card {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border: 1px solid var(--card-border);
|
||
border-radius: 20px;
|
||
overflow: hidden;
|
||
backdrop-filter: blur(12px);
|
||
}
|
||
|
||
.config-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
padding: 1.25rem;
|
||
border-bottom: 1px solid var(--card-border);
|
||
color: #10b981;
|
||
font-weight: 600;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.config-body {
|
||
padding: 1.5rem;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.5rem;
|
||
overflow-y: auto;
|
||
min-height: 0;
|
||
}
|
||
|
||
.config-body::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.config-body::-webkit-scrollbar-track {
|
||
background: rgba(0, 0, 0, 0.1);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.config-body::-webkit-scrollbar-thumb {
|
||
background: rgba(16, 185, 129, 0.3);
|
||
border-radius: 3px;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.config-body::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(16, 185, 129, 0.5);
|
||
}
|
||
|
||
.config-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.config-label {
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
color: var(--text-primary);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.count-value {
|
||
font-size: 0.85rem;
|
||
color: #10b981;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Difficulty Selection */
|
||
.difficulty-group {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.difficulty-btn {
|
||
flex: 1;
|
||
padding: 0.75rem;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
color: var(--text-secondary);
|
||
transition: all 0.2s;
|
||
}
|
||
.difficulty-btn:hover:not(:disabled) {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-color: var(--accent-color);
|
||
}
|
||
.difficulty-btn.active {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border-color: var(--accent-color);
|
||
color: var(--accent-color);
|
||
}
|
||
.difficulty-btn:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Range Slider */
|
||
.range-slider {
|
||
width: 100%;
|
||
height: 6px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 3px;
|
||
outline: none;
|
||
cursor: pointer;
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
}
|
||
.range-slider::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
width: 18px;
|
||
height: 18px;
|
||
background: #10b981;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
transition: transform 0.2s;
|
||
}
|
||
.range-slider::-webkit-slider-thumb:hover {
|
||
transform: scale(1.2);
|
||
}
|
||
.range-slider:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.range-labels {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 0.75rem;
|
||
color: var(--text-secondary);
|
||
opacity: 0.6;
|
||
}
|
||
|
||
/* Topic Input */
|
||
.topic-input {
|
||
width: 100%;
|
||
padding: 0.875rem 1rem;
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 10px;
|
||
color: var(--text-primary);
|
||
font-size: 0.9rem;
|
||
outline: none;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
box-sizing: border-box;
|
||
}
|
||
.topic-input::placeholder {
|
||
color: var(--text-secondary);
|
||
opacity: 0.5;
|
||
}
|
||
.topic-input:focus {
|
||
border-color: rgba(16, 185, 129, 0.5);
|
||
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
|
||
}
|
||
.topic-input:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Select Input */
|
||
.select-input {
|
||
width: 100%;
|
||
padding: 0.875rem 1rem;
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 10px;
|
||
color: var(--text-primary);
|
||
font-size: 0.9rem;
|
||
outline: none;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
box-sizing: border-box;
|
||
cursor: pointer;
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||
background-repeat: no-repeat;
|
||
background-position: right 1rem center;
|
||
padding-right: 2.5rem;
|
||
}
|
||
.select-input:focus {
|
||
border-color: rgba(16, 185, 129, 0.5);
|
||
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
|
||
}
|
||
.select-input:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
.select-input option {
|
||
background: #1a1a2e;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
/* Knowledge Points Grid */
|
||
.knowledge-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
}
|
||
.knowledge-btn {
|
||
padding: 0.5rem 0.875rem;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 8px;
|
||
color: var(--text-secondary);
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.knowledge-btn:hover:not(:disabled) {
|
||
background: rgba(16, 185, 129, 0.1);
|
||
border-color: rgba(16, 185, 129, 0.3);
|
||
color: #10b981;
|
||
}
|
||
.knowledge-btn.active {
|
||
background: rgba(16, 185, 129, 0.15);
|
||
border-color: #10b981;
|
||
color: #10b981;
|
||
}
|
||
.knowledge-btn:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.mt-2 {
|
||
margin-top: 0.75rem;
|
||
}
|
||
|
||
/* Example Button */
|
||
.example-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem;
|
||
background: rgba(16, 185, 129, 0.1);
|
||
border: 1px solid rgba(16, 185, 129, 0.25);
|
||
border-radius: 10px;
|
||
color: #10b981;
|
||
font-size: 0.9rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.example-btn:hover:not(:disabled) {
|
||
background: rgba(16, 185, 129, 0.2);
|
||
transform: translateY(-1px);
|
||
}
|
||
.example-btn:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Generate Button */
|
||
.btn-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 1rem;
|
||
}
|
||
.generate-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
background: linear-gradient(135deg, #10b981, #059669);
|
||
border: none;
|
||
color: white;
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
padding: 0.875rem 2.5rem;
|
||
border-radius: 50px;
|
||
cursor: pointer;
|
||
transition: all 0.25s;
|
||
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.35);
|
||
}
|
||
.generate-btn:hover:not(:disabled) {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 28px rgba(16, 185, 129, 0.5);
|
||
}
|
||
.generate-btn:disabled {
|
||
opacity: 0.45;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
.reset-btn {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
color: var(--text-secondary);
|
||
font-size: 0.9rem;
|
||
padding: 0.875rem 1.5rem;
|
||
border-radius: 50px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.reset-btn:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: var(--text-primary);
|
||
}
|
||
.btn-spinner {
|
||
width: 18px;
|
||
height: 18px;
|
||
border: 2px solid rgba(255, 255, 255, 0.35);
|
||
border-top-color: white;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
|
||
/* Result Area */
|
||
.result-area {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
.result-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.result-title-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
}
|
||
.result-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #10b981;
|
||
}
|
||
.result-dot.pulse {
|
||
animation: pulse 1.2s ease-in-out infinite;
|
||
}
|
||
.result-title {
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
.question-count-badge {
|
||
background: rgba(16, 185, 129, 0.15);
|
||
color: #10b981;
|
||
padding: 0.4rem 1rem;
|
||
border-radius: 20px;
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Questions List */
|
||
.questions-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.25rem;
|
||
}
|
||
|
||
.question-card {
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border: 1px solid var(--card-border);
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
animation: fadeInUp 0.4s ease both;
|
||
}
|
||
|
||
.question-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 1rem 1.25rem;
|
||
background: rgba(16, 185, 129, 0.04);
|
||
border-bottom: 1px solid var(--card-border);
|
||
}
|
||
|
||
.question-number {
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
color: #10b981;
|
||
}
|
||
|
||
.question-meta {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.meta-tag {
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 12px;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.type-tag {
|
||
background: rgba(16, 185, 129, 0.15);
|
||
color: #10b981;
|
||
}
|
||
|
||
.difficulty-tag {
|
||
background: rgba(245, 158, 11, 0.15);
|
||
color: #f59e0b;
|
||
}
|
||
|
||
.question-content {
|
||
padding: 1.25rem;
|
||
}
|
||
|
||
.content-text {
|
||
font-size: 0.95rem;
|
||
line-height: 1.7;
|
||
color: var(--text-primary);
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.options-grid {
|
||
padding: 0 1.25rem 1.25rem;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.option-item {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.option-label {
|
||
font-weight: 600;
|
||
color: #10b981;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.option-text {
|
||
color: var(--text-primary);
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.answer-section {
|
||
padding: 1rem 1.25rem;
|
||
background: rgba(16, 185, 129, 0.06);
|
||
border-top: 1px solid var(--card-border);
|
||
border-bottom: 1px solid var(--card-border);
|
||
}
|
||
|
||
.answer-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.answer-label {
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.answer-value {
|
||
font-size: 0.95rem;
|
||
font-weight: 600;
|
||
color: #10b981;
|
||
}
|
||
|
||
.explanation-section {
|
||
padding: 1.25rem;
|
||
}
|
||
|
||
.explanation-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
color: #10b981;
|
||
font-weight: 600;
|
||
font-size: 0.9rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.explanation-text {
|
||
font-size: 0.9rem;
|
||
line-height: 1.7;
|
||
color: var(--text-secondary);
|
||
margin: 0;
|
||
}
|
||
|
||
.knowledge-section {
|
||
padding: 0.875rem 1.25rem;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border-top: 1px solid var(--card-border);
|
||
}
|
||
|
||
.knowledge-label {
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.knowledge-value {
|
||
font-size: 0.85rem;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
/* Loading Card */
|
||
.loading-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 3rem;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border: 1px dashed rgba(16, 185, 129, 0.3);
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.loading-dots {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: rgba(16, 185, 129, 0.4);
|
||
animation: bounce 1.2s ease-in-out infinite;
|
||
}
|
||
|
||
.dot:nth-child(2) {
|
||
animation-delay: 0.2s;
|
||
}
|
||
|
||
.dot:nth-child(3) {
|
||
animation-delay: 0.4s;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 0.9rem;
|
||
color: var(--text-secondary);
|
||
margin: 0;
|
||
}
|
||
|
||
/* Animations */
|
||
@keyframes spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
@keyframes fadeInUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(12px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
@keyframes pulse {
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
50% {
|
||
opacity: 0.5;
|
||
transform: scale(0.75);
|
||
}
|
||
}
|
||
@keyframes bounce {
|
||
0%,
|
||
80%,
|
||
100% {
|
||
transform: translateY(0);
|
||
}
|
||
40% {
|
||
transform: translateY(-6px);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.main-layout {
|
||
flex-direction: column;
|
||
height: auto;
|
||
overflow: visible;
|
||
}
|
||
.left-panel {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
border-right: none;
|
||
border-bottom: 1px solid var(--card-border);
|
||
overflow-y: visible;
|
||
}
|
||
.right-panel {
|
||
padding: 1.25rem 1rem;
|
||
}
|
||
.options-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|