0841b1a103
- 前端:Vue3 + TS + Element Plus,24 个页面路由(课件/组题/教案/动画/思维导图/作文批改/命题/课堂/资源/社区等) - 后端:FastAPI + SQLAlchemy + SQLite,17 个路由模块,AI 服务层含降级模板 - AI:glm-5.x 推理模型已禁用思维链,确保输出真实内容 - 修复:ai_service 两处请求体注入 enable_thinking/thinking disabled - 测试账号:13900999999 / test1234
1602 lines
44 KiB
Vue
1602 lines
44 KiB
Vue
<template>
|
||
<div class="zj-home" :class="{ 'task-mode': activeMode !== 'courseware' }">
|
||
<section v-if="activeMode === 'courseware'" class="courseware-shell">
|
||
<div class="top-composer">
|
||
<div class="composer-title">
|
||
<span class="mascot">☻</span>
|
||
<h1>智教助手,一句话生成专业级互动课件</h1>
|
||
</div>
|
||
|
||
<div class="prompt-card">
|
||
<textarea v-model="prompt" :placeholder="activeConfig.placeholder"></textarea>
|
||
<div v-if="uploadedMaterials.length" class="materials-row">
|
||
<span v-for="item in uploadedMaterials" :key="item.id">
|
||
<el-icon><component :is="item.icon" /></el-icon>
|
||
{{ item.name }}
|
||
<button type="button" @click="removeMaterial(item.id)">×</button>
|
||
</span>
|
||
</div>
|
||
<div class="prompt-toolbar">
|
||
<div class="toolbar-left">
|
||
<button type="button" title="上传文件" @click="pickFile('file')"><el-icon><Paperclip /></el-icon></button>
|
||
<button type="button" title="上传图片" @click="pickFile('image')"><el-icon><Picture /></el-icon></button>
|
||
<input ref="fileInputRef" type="file" class="hidden-input" :accept="uploadAccept" @change="handleFileChange" />
|
||
<MaterialPicker v-model="uploadedMaterials" :show-selected="false" />
|
||
<button
|
||
v-for="chip in creatorChips"
|
||
:key="chip.mode"
|
||
type="button"
|
||
class="chip-btn"
|
||
:class="{ active: chip.mode === activeMode }"
|
||
@click="setMode(chip.mode)"
|
||
>
|
||
<el-icon><component :is="chip.icon" /></el-icon>
|
||
{{ chip.label }}
|
||
</button>
|
||
</div>
|
||
<div class="toolbar-right">
|
||
<button type="button" class="voice-btn" title="语音输入" :class="{ active: listening }" @click="toggleVoice"><el-icon><Microphone /></el-icon></button>
|
||
<button type="button" class="send-btn" title="开始生成" @click="startCreate"><el-icon><Top /></el-icon></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="resource-toolbar">
|
||
<nav class="category-tabs">
|
||
<button
|
||
v-for="cat in categories"
|
||
:key="cat"
|
||
type="button"
|
||
:class="{ active: activeCategory === cat }"
|
||
@click="activeCategory = cat"
|
||
>
|
||
{{ cat }}
|
||
<span v-if="cat === '数学'" class="hot-mini">Hot</span>
|
||
</button>
|
||
</nav>
|
||
<form class="search-pill" @submit.prevent="goResourceSearch">
|
||
<el-icon><Search /></el-icon>
|
||
<input v-model="keyword" placeholder="快速搜索资源" />
|
||
</form>
|
||
</section>
|
||
|
||
<section class="resource-grid">
|
||
<article v-for="item in filteredResources" :key="item.id" class="resource-card" @click="openResource(item)">
|
||
<div class="cover" :class="item.coverClass">
|
||
<span v-if="item.hot" class="hot-badge">🔥热门</span>
|
||
<span v-if="item.featured" class="hot-badge featured">精选</span>
|
||
<span class="views"><el-icon><View /></el-icon>{{ item.views }}</span>
|
||
<div class="cover-content">
|
||
<small>{{ item.kicker }}</small>
|
||
<strong>{{ item.coverTitle }}</strong>
|
||
<em>{{ item.coverSub }}</em>
|
||
</div>
|
||
<div v-if="item.visual === 'slides'" class="mini-slides"><span></span><span></span><span></span></div>
|
||
<div v-if="item.visual === 'chart'" class="chart-art"><span></span><span></span><span></span></div>
|
||
<div v-if="item.visual === 'tower'" class="tower-art"></div>
|
||
<div v-if="item.visual === 'people'" class="people-art"><span></span><span></span></div>
|
||
<div v-if="item.visual === 'shape'" class="shape-art"><span></span><span></span><span></span></div>
|
||
<div v-if="item.visual === 'grid'" class="grid-art"><span></span><span></span><span></span><span></span></div>
|
||
<div v-if="item.visual === 'cave'" class="cave-art"></div>
|
||
<div v-if="item.visual === 'festival'" class="festival-art"></div>
|
||
</div>
|
||
<div class="card-body">
|
||
<h3>{{ item.title }}</h3>
|
||
<div class="author-row">
|
||
<span class="avatar-dot" :style="{ background: item.avatar }"></span>
|
||
<span>{{ item.author }}</span>
|
||
<span class="star"><el-icon><Star /></el-icon>{{ item.likes }}</span>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
|
||
<form class="floating-composer" @submit.prevent="startCreate">
|
||
<span>{{ activeConfig.shortLabel }}|</span>
|
||
<input v-model="prompt" :placeholder="activeConfig.floatPlaceholder" />
|
||
<button type="button" class="voice-btn" title="语音输入" @click="toggleVoice"><el-icon><Microphone /></el-icon></button>
|
||
<button type="submit" class="send-btn" title="开始生成"><el-icon><Top /></el-icon></button>
|
||
</form>
|
||
</section>
|
||
|
||
<section v-else class="task-shell">
|
||
<div class="task-center">
|
||
<div class="task-heading">
|
||
<span class="mascot large">☻</span>
|
||
<h1>Hi, 我是智教助手</h1>
|
||
</div>
|
||
|
||
<div class="task-workbench">
|
||
<aside class="tip-card" :class="activeMode">
|
||
<div class="tip-title">
|
||
<strong>{{ activeConfig.label }}</strong>
|
||
<span>小提示</span>
|
||
</div>
|
||
<p>{{ activeConfig.tip }}</p>
|
||
<div class="tip-preview">
|
||
<div class="mini-tabs">
|
||
<span
|
||
v-for="item in previewTabs"
|
||
:key="item.mode"
|
||
:class="{ active: activeMode === item.mode }"
|
||
>{{ item.label }}</span>
|
||
</div>
|
||
<div class="doc-chip" v-if="activeMode === 'exam'">
|
||
<strong>ChinaDaily.doc</strong>
|
||
<small>File · 54KB</small>
|
||
</div>
|
||
<p>{{ activeConfig.previewText }}</p>
|
||
</div>
|
||
<div class="tip-actions">
|
||
<button type="button" @click="prompt = ''">好的</button>
|
||
<button type="button" @click="applyPromptTemplate(activeConfig.examples[0])">试一试</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<div class="task-main">
|
||
<div class="task-tabs">
|
||
<button
|
||
v-for="item in taskTabs"
|
||
:key="item.mode"
|
||
type="button"
|
||
:class="{ active: activeMode === item.mode }"
|
||
@click="setMode(item.mode)"
|
||
>
|
||
<el-icon><component :is="item.icon" /></el-icon>
|
||
{{ item.label }}
|
||
</button>
|
||
</div>
|
||
<div class="task-input-card">
|
||
<textarea v-model="prompt" :placeholder="activeConfig.inputPlaceholder"></textarea>
|
||
<div v-if="uploadedMaterials.length" class="materials-row task-materials-row">
|
||
<span v-for="item in uploadedMaterials" :key="item.id">
|
||
<el-icon><component :is="item.icon" /></el-icon>
|
||
{{ item.name }}
|
||
<button type="button" @click="removeMaterial(item.id)">×</button>
|
||
</span>
|
||
</div>
|
||
<div class="task-toolbar">
|
||
<div>
|
||
<button type="button" title="上传文件" @click="pickFile('file')"><el-icon><Paperclip /></el-icon></button>
|
||
<button type="button" title="上传图片" @click="pickFile('image')"><el-icon><Picture /></el-icon></button>
|
||
<MaterialPicker v-model="uploadedMaterials" :show-selected="false" />
|
||
</div>
|
||
<div>
|
||
<button type="button" class="voice-btn" title="语音输入" :class="{ active: listening }" @click="toggleVoice"><el-icon><Microphone /></el-icon></button>
|
||
<button type="button" class="send-btn muted" title="开始生成" @click="startCreate"><el-icon><Top /></el-icon></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="hot-prompts">
|
||
<h2>大家都在用</h2>
|
||
<button v-for="item in activeConfig.examples" :key="item.prompt" type="button" @click="applyPromptTemplate(item)">
|
||
<el-icon><component :is="activeConfig.icon" /></el-icon>
|
||
<span>{{ item.prompt }}</span>
|
||
<em v-if="item.hot">热门</em>
|
||
<small>{{ item.heat }} 热度</small>
|
||
</button>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="case-section">
|
||
<div class="case-tabs">
|
||
<button
|
||
v-for="tab in caseTabs"
|
||
:key="tab.mode"
|
||
type="button"
|
||
:class="{ active: caseFilter === tab.mode }"
|
||
@click="caseFilter = tab.mode"
|
||
>
|
||
{{ tab.label }}
|
||
</button>
|
||
</div>
|
||
<div class="case-grid">
|
||
<article v-for="item in filteredCases" :key="item.title" class="case-card">
|
||
<div class="case-cover" :class="item.theme">
|
||
<span>{{ item.type }}</span>
|
||
<div></div>
|
||
</div>
|
||
<h3>{{ item.title }}</h3>
|
||
<p><el-icon><Document /></el-icon>{{ item.file }}</p>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { parseMaterial } from '@/api/ai'
|
||
import MaterialPicker from '@/components/common/MaterialPicker.vue'
|
||
import { useUserStore } from '@/stores/user'
|
||
import { buildMaterialPromptBlock, fileParseToContext, formatMaterialFileSize, type MaterialContextItem } from '@/utils/materialContext'
|
||
import { createSpeechController } from '@/utils/speech'
|
||
|
||
type Mode = 'courseware' | 'exam' | 'exercise' | 'lesson' | 'animation' | 'analytics'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const userStore = useUserStore()
|
||
const prompt = ref('')
|
||
const keyword = ref('')
|
||
const activeCategory = ref('推荐')
|
||
const caseFilter = ref('all')
|
||
const fileInputRef = ref<HTMLInputElement>()
|
||
const uploadAccept = ref('.txt,.md,.csv,.json,.docx,.pptx,.pdf,.xlsx,image/*')
|
||
const listening = ref(false)
|
||
const uploadedMaterials = ref<MaterialContextItem[]>([])
|
||
const speech = createSpeechController({
|
||
onStart: () => { listening.value = true },
|
||
onText: (text) => { prompt.value = `${prompt.value} ${text}`.trim() },
|
||
onError: () => {
|
||
listening.value = false
|
||
ElMessage.warning('语音识别失败,请检查浏览器权限或直接输入文字')
|
||
},
|
||
onEnd: () => { listening.value = false },
|
||
})
|
||
|
||
const modeFromRoute = computed<Mode>(() => {
|
||
const mode = String(route.query.mode || 'courseware') as Mode
|
||
return ['courseware', 'exam', 'exercise', 'lesson', 'animation', 'analytics'].includes(mode) ? mode : 'courseware'
|
||
})
|
||
|
||
const activeMode = computed(() => modeFromRoute.value)
|
||
|
||
const taskTabs = [
|
||
{ mode: 'exam', label: 'AI命题', icon: 'Finished' },
|
||
{ mode: 'exercise', label: 'AI组题', icon: 'List' },
|
||
{ mode: 'lesson', label: 'AI教案 · 大单元', icon: 'Tickets' },
|
||
] as const
|
||
|
||
const creatorChips = [
|
||
{ mode: 'animation', label: '教学动画', icon: 'Film' },
|
||
{ mode: 'exercise', label: '教学游戏', icon: 'Trophy' },
|
||
{ mode: 'exam', label: 'AI命题', icon: 'Finished' },
|
||
{ mode: 'lesson', label: '大单元教案', icon: 'Tickets' },
|
||
{ mode: 'courseware', label: '互动课件', icon: 'Document' },
|
||
{ mode: 'analytics', label: '数据回收', icon: 'DataLine' },
|
||
] as const
|
||
|
||
const modeConfigs: Record<string, any> = {
|
||
courseware: {
|
||
label: 'AI互动课件',
|
||
shortLabel: '互动课件',
|
||
icon: 'MagicStick',
|
||
placeholder: '教学游戏|以《西游记》取经之路为故事线,生成六年级课内古诗词闯关游戏,让学生在游戏化体验中变“被动背诵”为“主动探究”。',
|
||
floatPlaceholder: '帮我生成初英人教七下U1 Grammar Focus的互动课件,通过精简而高效的课堂活动练习because的用法。',
|
||
},
|
||
exam: {
|
||
label: 'AI命题',
|
||
icon: 'Finished',
|
||
tip: '上传命题素材的资料和要求,AI即可命制一道原创且专业的题目。',
|
||
inputPlaceholder: '请输入内容',
|
||
previewText: '请根据chinadaily这篇文章,帮我命制北京英语中考阅读理解模拟题。',
|
||
examples: [
|
||
{ prompt: '根据我上传的ChinaDaily这篇文章,帮我命制北京英语中考阅读理解模拟题', heat: 27466, hot: true },
|
||
{ prompt: '请你找一篇适合中学生读的冰心散文,出一道中考记叙文阅读', heat: 26197, hot: true },
|
||
{ prompt: '根据人民日报“人工智能”主题报道,出一道北京西城区六年级难度应用题', heat: 24937 },
|
||
{ prompt: '结合古代科技相关情境,出一道“密度相关”中考难度的物理计算题', heat: 24250 },
|
||
],
|
||
},
|
||
exercise: {
|
||
label: 'AI组题',
|
||
icon: 'List',
|
||
tip: '输入组题的知识点范围难度等要求,AI即可从海量题库智能挑选组成一份题单。',
|
||
inputPlaceholder: '请输入内容',
|
||
previewText: '帮我按二次函数压轴题和模拟题中考查方式,组一份有难度梯度的练习。',
|
||
examples: [
|
||
{ prompt: '结合过往三年北京中考题和模拟题中考查“二次函数”的真题,组一份有难度梯度的专项练习', heat: 27105, hot: true },
|
||
{ prompt: '根据图中这名同学的错题,为他进行错因分析并组一套针对性练习', heat: 25882, hot: true },
|
||
{ prompt: '结合北上广深近三年中考语文的文言文阅读题,组一份题单,至少30题', heat: 24302 },
|
||
{ prompt: '我是广州一名初中英语老师,请帮我找过去五年广州中考英语真题', heat: 23898 },
|
||
],
|
||
},
|
||
lesson: {
|
||
label: 'AI教案 · 大单元',
|
||
icon: 'Tickets',
|
||
tip: '输入课程单元等信息,AI即可生成一份专业的大单元教案设计。',
|
||
inputPlaceholder: '请输入内容',
|
||
previewText: '帮我出一份课时教学设计,关于人教版新一上第四单元11-20的认识。',
|
||
examples: [
|
||
{ prompt: '帮我出一份课时教学设计,关于人教版新一上第四单元11~20的认识第1课时', heat: 27347, hot: true },
|
||
{ prompt: '基于这份教案进行修改,在“0的意义”的部分更体现游戏化教学', heat: 27190, hot: true },
|
||
{ prompt: '结合“我校的玉兰树春天开花”这一情境,帮我生成语文部编版七上第一单元教案', heat: 24961 },
|
||
{ prompt: '基于这份教案进行修改,不改变其他内容,在一个环节中增加使用AI工具', heat: 24025 },
|
||
],
|
||
},
|
||
animation: {
|
||
label: '教学动画',
|
||
shortLabel: '教学动画',
|
||
icon: 'Film',
|
||
placeholder: '用动画演示二次函数 y=ax²+bx+c 的图像随参数 a、b、c 变化的过程,包含坐标轴、参数滑块和课堂提问。',
|
||
floatPlaceholder: '说出一个知识点,生成可投屏的教学动画。',
|
||
tip: '把抽象概念、实验过程或知识推演转成可播放的课堂动画。',
|
||
inputPlaceholder: '请输入动画主题或上传图片/材料后生成动画',
|
||
previewText: '展示圆柱体积公式从切拼到推导的全过程。',
|
||
examples: [
|
||
{ prompt: '展示二次函数 y=ax²+bx+c 的图像随参数 a、b、c 变化的过程', heat: 28120, hot: true },
|
||
{ prompt: '用动画演示圆柱体积公式推导,突出底面积和高的关系', heat: 26788, hot: true },
|
||
{ prompt: '用动画解释光合作用中水、二氧化碳、光照和氧气的变化', heat: 24806 },
|
||
],
|
||
},
|
||
analytics: {
|
||
label: '课堂数据回收',
|
||
shortLabel: '数据回收',
|
||
icon: 'DataLine',
|
||
placeholder: '设计一个课堂数据回收活动,收集学生对圆柱体积公式的理解程度、易错点和课后反馈。',
|
||
floatPlaceholder: '描述你要收集的课堂反馈或答题数据。',
|
||
tip: '创建投票、问答、测评等活动,课堂上实时收集学生反馈。',
|
||
inputPlaceholder: '请输入课堂数据回收目标',
|
||
previewText: '收集学生对本节课知识点掌握程度,并统计正确率和高频错因。',
|
||
examples: [
|
||
{ prompt: '创建一个课堂即时反馈,统计学生对圆柱体积公式的掌握程度', heat: 23340, hot: true },
|
||
{ prompt: '设计一个课后 exit ticket,收集学生最困惑的问题和自评等级', heat: 21902 },
|
||
{ prompt: '为英语阅读课创建投票和简答回收,分析学生对文章主旨的理解', heat: 20660 },
|
||
],
|
||
},
|
||
}
|
||
|
||
const activeConfig = computed(() => modeConfigs[activeMode.value] || modeConfigs.courseware)
|
||
const previewTabs = computed(() => {
|
||
const base = [
|
||
{ mode: 'exam', label: 'AI命题' },
|
||
{ mode: 'exercise', label: 'AI组题' },
|
||
{ mode: 'lesson', label: 'AI教案' },
|
||
]
|
||
if (['exam', 'exercise', 'lesson'].includes(activeMode.value)) return base
|
||
return [
|
||
{ mode: 'animation', label: '教学动画' },
|
||
{ mode: 'courseware', label: '互动课件' },
|
||
{ mode: 'analytics', label: '数据回收' },
|
||
]
|
||
})
|
||
|
||
const categories = ['推荐', '语文', '数学', '英语', '物理', '化学', '信息科技', '课堂工具', '学前教育', '高等教育', '职业教育', '其他']
|
||
|
||
const resources = [
|
||
{ id: 'guide', category: '推荐', title: '新手指引', author: 'TeacherYO', likes: 6266, views: '9.2万', kicker: 'AI TEACHING', coverTitle: '新手使用攻略', coverSub: '一句话生成专业级教学动画,让教学更生动', coverClass: 'green', visual: 'chart', avatar: '#8B5E34' },
|
||
{ id: 'tips', category: '课堂工具', hot: true, title: '【玩转互动课件】必看小技巧', author: 'TeacherYO', likes: 6108, views: '7.9万', kicker: '快速入门', coverTitle: '玩转互动课件', coverSub: '必看小技巧', coverClass: 'yellow', visual: 'slides', avatar: '#8B5E34' },
|
||
{ id: 'data', category: '课堂工具', hot: true, title: '【数据回收】新手指引', author: 'TeacherYO', likes: 6036, views: '6.7万', kicker: '课堂工具', coverTitle: '玩转数据回收', coverSub: '案例合集', coverClass: 'mint', visual: 'chart', avatar: '#8B5E34' },
|
||
{ id: 'case', category: '推荐', hot: true, title: '【持续更新】智教助手经典案例解析合集', author: 'TeacherYO', likes: 5925, views: '8.9万', kicker: '案例解析', coverTitle: '经典案例解析系列合集', coverSub: '想学习那些课件怎么做?', coverClass: 'blue', visual: 'slides', avatar: '#8B5E34' },
|
||
{ id: 'english-food', category: '英语', hot: true, title: 'Unit4 Healthy Food 第一课时', author: '海淀老师勇闯AI圈', likes: 5914, views: '7万', kicker: 'Unit 4 Healthy Food', coverTitle: "Let's speak English!", coverSub: 'Start', coverClass: 'english', visual: 'people', avatar: '#C08457' },
|
||
{ id: 'cylinder', category: '数学', hot: true, title: '【互动课件】小学数学六年级圆柱的体积', author: '小叶老师用AI', likes: 5569, views: '8万', kicker: '小学数学 人教版六年级下册', coverTitle: '圆柱的体积', coverSub: '天坛 · 祈年殿', coverClass: 'cream', visual: 'tower', avatar: '#5B8DEF' },
|
||
{ id: 'oral', category: '语文', featured: true, title: '一年级烟花口算', author: '智教用户0227', likes: 5553, views: '5.7万', kicker: '数学游戏', coverTitle: '星空烟花口算', coverSub: '开始练习', coverClass: 'space', visual: 'grid', avatar: '#94a3b8' },
|
||
{ id: 'vision', category: '信息科技', hot: true, title: 'AI如何“看”世界 —— 揭秘图像识别', author: '蒋老师的AI实验室', likes: 5479, views: '5.8万', kicker: '人工智能体验课', coverTitle: 'AI如何“看”世界?', coverSub: '揭秘图像识别的工作过程', coverClass: 'dark', visual: 'grid', avatar: '#B08968' },
|
||
{ id: 'festival', category: '语文', hot: true, title: '《祝福》教学动画(4.9号已更新)', author: '巧克力麦片', likes: 5413, views: '7.3万', kicker: '文学作品', coverTitle: '《祝福》', coverSub: '鲁迅小说中的悲剧意味', coverClass: 'night', visual: 'festival', avatar: '#e11d48' },
|
||
{ id: 'ranking', category: '历史', hot: true, title: '大明书院科举榜', author: '智教用户', likes: 5370, views: '8万', kicker: '历史课堂', coverTitle: '大明书院科举榜', coverSub: '排行榜互动游戏', coverClass: 'green', visual: 'shape', avatar: '#0f766e' },
|
||
{ id: 'number', category: '数学', hot: true, title: '大数的认识', author: '智教用户', likes: 5320, views: '5.9万', kicker: 'DIGITAL RECOVERY', coverTitle: '大数的认识', coverSub: 'ACCESS SYSTEM', coverClass: 'cyber', visual: 'grid', avatar: '#2563eb' },
|
||
{ id: 'cave', category: '语文', hot: true, title: '《记金华的双龙洞》互动课件61', author: '智教用户', likes: 5299, views: '5.8万', kicker: '游记阅读', coverTitle: '记金华的双龙洞', coverSub: '跟着叶圣陶先生拜访洞中奇景', coverClass: 'cave', visual: 'cave', avatar: '#b45309' },
|
||
]
|
||
|
||
const caseTabs = [
|
||
{ mode: 'all', label: '全部案例' },
|
||
{ mode: 'exam', label: 'AI命题' },
|
||
{ mode: 'exercise', label: 'AI组题' },
|
||
{ mode: 'lesson', label: 'AI教案 · 大单元' },
|
||
]
|
||
|
||
const cases = [
|
||
{ mode: 'exercise', type: 'AI组题', title: '错因分析并组针对性练习', file: '三角形花坛装饰瓷砖问题错因分析与针对...', theme: 'blue' },
|
||
{ mode: 'exam', type: 'AI命题', title: '生成中考叙述文阅读题', file: '长辈教诲晚辈悟守人生信条 中考记...', theme: 'green' },
|
||
{ mode: 'lesson', type: 'AI教案 · 大单元', title: '人教版七上第二章有理数运算教学...', file: '人教版七年级上册第二章 有理数的运...', theme: 'yellow' },
|
||
{ mode: 'exam', type: 'AI命题', title: '天干地支中等难度周期解答题', file: '天干地支周期问题解答题.docx', theme: 'green' },
|
||
{ mode: 'exercise', type: 'AI组题', title: '查找北京中考字形高频题', file: '北京地区近3年中考及一模二模字形考查...', theme: 'blue' },
|
||
{ mode: 'lesson', type: 'AI教案 · 大单元', title: '设计人教版七上整式加减教学方案', file: '人教版七上第四章《整式的加减》第二...', theme: 'yellow' },
|
||
{ mode: 'exam', type: 'AI命题', title: '生成中国航天反比例函数应用题', file: '中国航天相关反比例函数应用题.docx', theme: 'green' },
|
||
{ mode: 'exercise', type: 'AI组题', title: '整理近3年北京中考信息匹配题', file: '近3年北京中考英语真题信息匹配题汇总...', theme: 'blue' },
|
||
]
|
||
|
||
const filteredResources = computed(() => {
|
||
const kw = keyword.value.trim().toLowerCase()
|
||
return resources.filter(item => {
|
||
const categoryOk = activeCategory.value === '推荐' || item.category === activeCategory.value
|
||
const keywordOk = !kw || `${item.title}${item.author}${item.coverTitle}${item.category}`.toLowerCase().includes(kw)
|
||
return categoryOk && keywordOk
|
||
})
|
||
})
|
||
|
||
const filteredCases = computed(() => {
|
||
return caseFilter.value === 'all' ? cases : cases.filter(item => item.mode === caseFilter.value)
|
||
})
|
||
|
||
watch(() => route.query.mode, () => {
|
||
prompt.value = ''
|
||
})
|
||
|
||
function setMode(mode: Mode) {
|
||
router.push({ path: '/', query: { mode } })
|
||
}
|
||
|
||
function modePath(mode: string) {
|
||
if (mode === 'exam') return '/exam'
|
||
if (mode === 'exercise') return '/exercise'
|
||
if (mode === 'lesson') return '/lesson-plan'
|
||
if (mode === 'animation') return '/animation'
|
||
if (mode === 'analytics') return '/classroom'
|
||
return '/courseware/create'
|
||
}
|
||
|
||
function startCreate() {
|
||
const basePrompt = prompt.value.trim() || activeConfig.value.placeholder || activeConfig.value.examples?.[0]?.prompt || ''
|
||
const text = `${basePrompt}${buildMaterialPromptBlock(uploadedMaterials.value, '参考素材')}`.trim()
|
||
localStorage.setItem('quick_create_prompt', text)
|
||
router.push(modePath(activeMode.value))
|
||
}
|
||
|
||
function applyPromptTemplate(item: any) {
|
||
prompt.value = item.prompt
|
||
}
|
||
|
||
function pickFile(type: 'file' | 'image') {
|
||
uploadAccept.value = type === 'image' ? 'image/*' : '.txt,.md,.csv,.json,.docx,.pptx,.pdf,.xlsx'
|
||
fileInputRef.value?.click()
|
||
}
|
||
|
||
async function handleFileChange(event: Event) {
|
||
const file = (event.target as HTMLInputElement).files?.[0]
|
||
if (!file) return
|
||
try {
|
||
const parsed: any = await parseMaterial(file)
|
||
userStore.refreshCreditsFromResponse(parsed)
|
||
const data = parsed.data || parsed
|
||
uploadedMaterials.value.push(fileParseToContext(file, data))
|
||
ElMessage.success('材料已解析并加入任务')
|
||
} catch {
|
||
await addFallbackMaterial(file)
|
||
} finally {
|
||
;(event.target as HTMLInputElement).value = ''
|
||
}
|
||
}
|
||
|
||
function inferTypeLabel(filename: string) {
|
||
const ext = filename.toLowerCase().split('.').pop() || ''
|
||
if (ext === 'pptx') return 'PPT材料'
|
||
if (ext === 'pdf') return 'PDF材料'
|
||
if (ext === 'xlsx' || ext === 'xls' || ext === 'csv') return '表格材料'
|
||
return 'Word材料'
|
||
}
|
||
|
||
async function addFallbackMaterial(file: File) {
|
||
if (file.type.startsWith('image/')) {
|
||
uploadedMaterials.value.push({
|
||
id: `${Date.now()}-${file.name}`,
|
||
name: file.name,
|
||
icon: 'Picture',
|
||
typeLabel: '图片',
|
||
sizeLabel: formatMaterialFileSize(file.size),
|
||
excerpt: '请根据图片内容识别知识点、题目信息或板书结构。',
|
||
})
|
||
} else if (/\.(docx|pptx|pdf|xlsx|xls)$/i.test(file.name)) {
|
||
uploadedMaterials.value.push({
|
||
id: `${Date.now()}-${file.name}`,
|
||
name: file.name,
|
||
icon: 'Document',
|
||
typeLabel: inferTypeLabel(file.name),
|
||
sizeLabel: formatMaterialFileSize(file.size),
|
||
excerpt: '该材料暂未解析成功,请结合文件名和当前输入要求生成教学内容。',
|
||
})
|
||
} else {
|
||
const text = await file.text()
|
||
uploadedMaterials.value.push({
|
||
id: `${Date.now()}-${file.name}`,
|
||
name: file.name,
|
||
icon: 'Document',
|
||
typeLabel: '文本材料',
|
||
sizeLabel: formatMaterialFileSize(file.size),
|
||
excerpt: text.slice(0, 4000),
|
||
})
|
||
}
|
||
ElMessage.warning('材料解析失败,已改用本地摘要')
|
||
}
|
||
|
||
function removeMaterial(id: string) {
|
||
uploadedMaterials.value = uploadedMaterials.value.filter(item => item.id !== id)
|
||
}
|
||
|
||
function toggleVoice() {
|
||
if (listening.value) {
|
||
speech.stop()
|
||
return
|
||
}
|
||
if (!speech.isSupported()) {
|
||
ElMessage.warning('当前浏览器不支持语音识别,请使用 Chrome/Edge 或直接输入文字')
|
||
return
|
||
}
|
||
speech.start()
|
||
}
|
||
|
||
function openResource(item: any) {
|
||
const demoMap: Record<string, string> = {
|
||
'english-food': 'food',
|
||
oral: 'data',
|
||
ranking: 'case',
|
||
number: 'vision',
|
||
festival: 'guide',
|
||
cave: 'cylinder',
|
||
}
|
||
router.push(`/demo/${demoMap[item.id] || item.id}`)
|
||
}
|
||
|
||
function goResourceSearch() {
|
||
const kw = keyword.value.trim()
|
||
router.push({ path: '/resources', query: kw ? { keyword: kw } : undefined })
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.zj-home {
|
||
min-height: 100vh;
|
||
color: #173f35;
|
||
background: #f8faf7;
|
||
}
|
||
|
||
.courseware-shell {
|
||
min-height: 100vh;
|
||
padding: 46px 52px 80px;
|
||
}
|
||
|
||
.top-composer,
|
||
.resource-toolbar,
|
||
.resource-grid {
|
||
width: min(100%, 1474px);
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
}
|
||
|
||
.top-composer {
|
||
margin-bottom: 38px;
|
||
}
|
||
|
||
.composer-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.mascot {
|
||
width: 38px;
|
||
height: 38px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px solid #dbe8de;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
color: #0f4f3e;
|
||
font-size: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.mascot.large {
|
||
width: 44px;
|
||
height: 44px;
|
||
}
|
||
|
||
.composer-title h1,
|
||
.task-heading h1 {
|
||
margin: 0;
|
||
color: #164f3e;
|
||
font-size: 24px;
|
||
font-weight: 900;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.prompt-card {
|
||
min-height: 148px;
|
||
border: 1px solid #cfe0d3;
|
||
border-radius: 18px;
|
||
background: rgba(255, 255, 255, 0.86);
|
||
box-shadow: 0 18px 42px rgba(17, 69, 52, 0.07);
|
||
padding: 22px 24px 15px;
|
||
}
|
||
|
||
.prompt-card textarea,
|
||
.task-input-card textarea {
|
||
width: 100%;
|
||
resize: none;
|
||
border: none;
|
||
outline: none;
|
||
background: transparent;
|
||
color: #173f35;
|
||
font-family: inherit;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.prompt-card textarea {
|
||
height: 62px;
|
||
font-size: 16px;
|
||
line-height: 1.75;
|
||
}
|
||
|
||
.prompt-card textarea::placeholder,
|
||
.task-input-card textarea::placeholder,
|
||
.floating-composer input::placeholder {
|
||
color: #a7b2ab;
|
||
}
|
||
|
||
.materials-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin: 4px 0 11px;
|
||
}
|
||
|
||
.task-materials-row {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.materials-row span {
|
||
min-width: 0;
|
||
height: 30px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
border: 1px solid #dbe8de;
|
||
border-radius: 15px;
|
||
background: #f6fbf6;
|
||
color: #31584d;
|
||
padding: 0 8px 0 11px;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.materials-row button {
|
||
border: none;
|
||
background: transparent;
|
||
color: #78908a;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.hidden-input {
|
||
display: none;
|
||
}
|
||
|
||
.prompt-toolbar,
|
||
.task-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 14px;
|
||
}
|
||
|
||
.toolbar-left,
|
||
.toolbar-right,
|
||
.task-toolbar > div {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.prompt-toolbar button,
|
||
.task-toolbar button,
|
||
.floating-composer button {
|
||
height: 39px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px solid #dfebe2;
|
||
border-radius: 20px;
|
||
background: #fff;
|
||
color: #51665f;
|
||
padding: 0 13px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.prompt-toolbar button:first-child,
|
||
.prompt-toolbar button:nth-child(2),
|
||
.voice-btn {
|
||
width: 39px;
|
||
padding: 0;
|
||
}
|
||
|
||
.chip-btn {
|
||
gap: 6px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.chip-btn.active,
|
||
.chip-btn:hover,
|
||
.voice-btn:hover,
|
||
.voice-btn.active {
|
||
border-color: #b4d6bd;
|
||
background: #f5fbf5;
|
||
color: #0f4f3e;
|
||
}
|
||
|
||
.send-btn,
|
||
.prompt-toolbar .send-btn,
|
||
.floating-composer .send-btn {
|
||
width: 40px;
|
||
border: none;
|
||
border-radius: 13px;
|
||
background: #0f5a42;
|
||
color: #fff;
|
||
padding: 0;
|
||
}
|
||
|
||
.send-btn.muted {
|
||
background: #9aa7a0;
|
||
}
|
||
|
||
.resource-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 22px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.category-tabs {
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 30px;
|
||
overflow-x: auto;
|
||
scrollbar-width: none;
|
||
}
|
||
|
||
.category-tabs::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.category-tabs button {
|
||
position: relative;
|
||
border: none;
|
||
background: transparent;
|
||
color: #47635a;
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.category-tabs button.active {
|
||
color: #00543d;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.category-tabs button.active::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 3px;
|
||
height: 3px;
|
||
border-radius: 2px;
|
||
background: #00543d;
|
||
}
|
||
|
||
.hot-mini {
|
||
margin-left: 2px;
|
||
color: #ff4b20;
|
||
font-size: 9px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.search-pill {
|
||
width: 220px;
|
||
height: 36px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
border-radius: 18px;
|
||
background: #eef1ec;
|
||
color: #a5aea7;
|
||
padding: 0 14px;
|
||
}
|
||
|
||
.search-pill input {
|
||
min-width: 0;
|
||
flex: 1;
|
||
border: none;
|
||
outline: none;
|
||
background: transparent;
|
||
color: #173f35;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.resource-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(240px, 1fr));
|
||
gap: 24px;
|
||
}
|
||
|
||
.resource-card {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
border: 1px solid #e1e8e2;
|
||
border-radius: 14px;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
transition: transform .18s ease, box-shadow .18s ease;
|
||
}
|
||
|
||
.resource-card:hover {
|
||
transform: translateY(-3px);
|
||
box-shadow: 0 12px 24px rgba(17, 69, 52, 0.1);
|
||
}
|
||
|
||
.cover {
|
||
position: relative;
|
||
height: 190px;
|
||
overflow: hidden;
|
||
border-radius: 10px;
|
||
margin: 5px;
|
||
padding: 24px;
|
||
}
|
||
|
||
.hot-badge,
|
||
.views {
|
||
position: absolute;
|
||
z-index: 2;
|
||
height: 24px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.hot-badge {
|
||
top: 8px;
|
||
left: 8px;
|
||
background: #fff;
|
||
color: #ff4b20;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.hot-badge.featured {
|
||
color: #d97706;
|
||
}
|
||
|
||
.views {
|
||
top: 8px;
|
||
right: 8px;
|
||
gap: 4px;
|
||
background: rgba(0, 0, 0, .26);
|
||
color: #fff;
|
||
padding: 0 9px;
|
||
}
|
||
|
||
.cover-content {
|
||
position: relative;
|
||
z-index: 1;
|
||
max-width: 74%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.cover-content small {
|
||
color: inherit;
|
||
opacity: .76;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.cover-content strong {
|
||
font-size: 25px;
|
||
line-height: 1.18;
|
||
font-weight: 900;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.cover-content em {
|
||
font-style: normal;
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.card-body {
|
||
padding: 10px 16px 14px;
|
||
}
|
||
|
||
.card-body h3 {
|
||
min-height: 22px;
|
||
color: #233c35;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
line-height: 1.4;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.author-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-top: 8px;
|
||
color: #87958f;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.avatar-dot {
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.star {
|
||
margin-left: auto;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.floating-composer {
|
||
position: fixed;
|
||
left: calc(var(--active-sidebar-width, var(--sidebar-width)) + 450px);
|
||
right: 450px;
|
||
bottom: 20px;
|
||
z-index: 15;
|
||
height: 76px;
|
||
min-width: 520px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
border: 1px solid #cfe0d3;
|
||
border-radius: 18px;
|
||
background: rgba(255, 255, 255, .94);
|
||
box-shadow: 0 12px 34px rgba(17, 69, 52, .12);
|
||
padding: 0 16px 0 22px;
|
||
}
|
||
|
||
.floating-composer span {
|
||
color: #a0aaa4;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.floating-composer input {
|
||
min-width: 0;
|
||
flex: 1;
|
||
border: none;
|
||
outline: none;
|
||
background: transparent;
|
||
color: #173f35;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.task-shell {
|
||
min-height: 100vh;
|
||
padding: 148px 42px 76px;
|
||
}
|
||
|
||
.task-center {
|
||
width: min(100%, 940px);
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.task-heading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
margin-bottom: 38px;
|
||
}
|
||
|
||
.task-heading h1 {
|
||
font-size: 32px;
|
||
}
|
||
|
||
.task-workbench {
|
||
display: grid;
|
||
grid-template-columns: 340px minmax(0, 720px);
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.tip-card {
|
||
position: relative;
|
||
min-height: 344px;
|
||
border-radius: 13px;
|
||
background: #164f3e;
|
||
color: #fff;
|
||
padding: 17px 15px 16px;
|
||
}
|
||
|
||
.tip-card::after {
|
||
content: "";
|
||
position: absolute;
|
||
right: -8px;
|
||
top: 78px;
|
||
border-left: 8px solid #164f3e;
|
||
border-top: 7px solid transparent;
|
||
border-bottom: 7px solid transparent;
|
||
}
|
||
|
||
.tip-card.exercise {
|
||
background: #1f526f;
|
||
}
|
||
|
||
.tip-card.exercise::after {
|
||
border-left-color: #1f526f;
|
||
}
|
||
|
||
.tip-card.lesson {
|
||
background: #755300;
|
||
}
|
||
|
||
.tip-card.lesson::after {
|
||
border-left-color: #755300;
|
||
}
|
||
|
||
.tip-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-bottom: 13px;
|
||
}
|
||
|
||
.tip-title strong {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.tip-title span {
|
||
height: 18px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
border-radius: 4px;
|
||
background: #ff5b2e;
|
||
padding: 0 5px;
|
||
font-size: 10px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.tip-card > p {
|
||
min-height: 63px;
|
||
margin: 0;
|
||
font-size: 16px;
|
||
line-height: 1.52;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.tip-preview {
|
||
height: 176px;
|
||
margin-top: 12px;
|
||
overflow: hidden;
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
color: #173f35;
|
||
padding: 0 14px 14px;
|
||
}
|
||
|
||
.mini-tabs {
|
||
height: 35px;
|
||
display: flex;
|
||
justify-content: space-around;
|
||
align-items: center;
|
||
margin: 0 -14px 14px;
|
||
background: #f0f6ee;
|
||
color: #8a9992;
|
||
font-size: 12px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.mini-tabs span.active {
|
||
color: #0f4f3e;
|
||
border-bottom: 2px solid #0f4f3e;
|
||
}
|
||
|
||
.doc-chip {
|
||
width: 164px;
|
||
height: 47px;
|
||
display: grid;
|
||
justify-content: center;
|
||
border-radius: 8px;
|
||
background: #f4f6f7;
|
||
margin: 2px 0 12px 20px;
|
||
padding: 7px 9px;
|
||
}
|
||
|
||
.doc-chip strong {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.doc-chip small {
|
||
color: #9aa7a0;
|
||
}
|
||
|
||
.tip-preview p {
|
||
color: #173f35;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.tip-actions {
|
||
display: grid;
|
||
grid-template-columns: .48fr 1fr;
|
||
gap: 9px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.tip-actions button {
|
||
height: 32px;
|
||
border: none;
|
||
border-radius: 16px;
|
||
background: rgba(255,255,255,.24);
|
||
color: #fff;
|
||
font-weight: 900;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.tip-actions button:last-child {
|
||
background: #dff6d8;
|
||
color: #164f3e;
|
||
}
|
||
|
||
.task-main {
|
||
min-width: 0;
|
||
}
|
||
|
||
.task-tabs {
|
||
height: 49px;
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
border: 1px solid #cfe0d3;
|
||
border-bottom: none;
|
||
border-radius: 18px 18px 0 0;
|
||
background: #f1f6ef;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.task-tabs button {
|
||
position: relative;
|
||
border: none;
|
||
background: transparent;
|
||
color: #637a72;
|
||
font-size: 15px;
|
||
font-weight: 900;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.task-tabs button.active {
|
||
color: #0f4f3e;
|
||
}
|
||
|
||
.task-tabs button.active::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: 50%;
|
||
bottom: 0;
|
||
width: 26px;
|
||
height: 2px;
|
||
transform: translateX(-50%);
|
||
background: #0f4f3e;
|
||
}
|
||
|
||
.task-input-card {
|
||
min-height: 148px;
|
||
border: 1px solid #cfe0d3;
|
||
border-radius: 0 0 18px 18px;
|
||
background: rgba(255,255,255,.84);
|
||
box-shadow: 0 18px 42px rgba(17, 69, 52, 0.07);
|
||
padding: 23px 22px 16px;
|
||
}
|
||
|
||
.task-input-card textarea {
|
||
height: 55px;
|
||
font-size: 16px;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.hot-prompts {
|
||
margin-top: 22px;
|
||
}
|
||
|
||
.hot-prompts h2 {
|
||
margin: 0 0 14px;
|
||
font-size: 16px;
|
||
color: #164f3e;
|
||
}
|
||
|
||
.hot-prompts button {
|
||
width: 100%;
|
||
min-height: 44px;
|
||
display: grid;
|
||
grid-template-columns: 21px minmax(0, 1fr) auto 92px;
|
||
align-items: center;
|
||
gap: 7px;
|
||
border: none;
|
||
background: transparent;
|
||
color: #50665e;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.hot-prompts button span {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.hot-prompts em {
|
||
border: 1px solid #ff7f5c;
|
||
border-radius: 4px;
|
||
color: #ff5b2e;
|
||
padding: 0 4px;
|
||
font-style: normal;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.hot-prompts small {
|
||
color: #9aa7a0;
|
||
text-align: right;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.case-section {
|
||
width: min(100%, 1138px);
|
||
margin: 208px auto 0;
|
||
}
|
||
|
||
.case-tabs {
|
||
height: 36px;
|
||
width: min(100%, 720px);
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
align-items: center;
|
||
border-radius: 18px;
|
||
background: #eef5ee;
|
||
margin: 0 auto 28px;
|
||
padding: 3px;
|
||
}
|
||
|
||
.case-tabs button {
|
||
height: 30px;
|
||
border: none;
|
||
border-radius: 15px;
|
||
background: transparent;
|
||
color: #6a7f77;
|
||
font-weight: 900;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.case-tabs button.active {
|
||
background: #fff;
|
||
color: #0f4f3e;
|
||
}
|
||
|
||
.case-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(190px, 1fr));
|
||
gap: 16px 22px;
|
||
}
|
||
|
||
.case-card {
|
||
min-width: 0;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
padding: 7px;
|
||
}
|
||
|
||
.case-cover {
|
||
height: 153px;
|
||
overflow: hidden;
|
||
border-radius: 7px;
|
||
padding: 7px;
|
||
}
|
||
|
||
.case-cover span {
|
||
display: inline-flex;
|
||
height: 24px;
|
||
align-items: center;
|
||
border-radius: 4px;
|
||
background: rgba(255,255,255,.65);
|
||
padding: 0 6px;
|
||
font-size: 13px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.case-cover div {
|
||
width: 80%;
|
||
height: 76%;
|
||
margin: 15px auto 0;
|
||
border-radius: 7px;
|
||
background: #fff;
|
||
}
|
||
|
||
.case-card h3 {
|
||
margin: 10px 7px 6px;
|
||
color: #173f35;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.case-card p {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
margin: 0 7px 8px;
|
||
color: #7f8d87;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.green {
|
||
color: #fff;
|
||
background: radial-gradient(circle at 78% 70%, rgba(101,255,0,.58), transparent 22%), linear-gradient(135deg, #063a2d, #00854f);
|
||
}
|
||
|
||
.yellow {
|
||
color: #714a00;
|
||
background: radial-gradient(circle at 82% 50%, rgba(255,255,255,.55), transparent 28%), linear-gradient(135deg, #fff2a8, #ffd84d);
|
||
}
|
||
|
||
.mint {
|
||
color: #00543d;
|
||
background: linear-gradient(90deg, rgba(255,255,255,.74), rgba(255,255,255,.1)), linear-gradient(135deg, #e7ffe9, #dff7e5);
|
||
}
|
||
|
||
.blue {
|
||
color: #003f68;
|
||
background: linear-gradient(135deg, #dbe8ff, #c8ddff);
|
||
}
|
||
|
||
.english {
|
||
color: #f48b00;
|
||
background: radial-gradient(circle at 28% 70%, rgba(255,255,255,.65), transparent 32%), linear-gradient(135deg, #f9e0b9, #eed0a8);
|
||
}
|
||
|
||
.cream {
|
||
color: #7a5600;
|
||
background: linear-gradient(135deg, #fff3c4, #f7e3a1);
|
||
}
|
||
|
||
.dark,
|
||
.cyber {
|
||
color: #fff;
|
||
background: radial-gradient(circle at 70% 46%, rgba(0,195,255,.26), transparent 28%), linear-gradient(135deg, #050505, #111827);
|
||
}
|
||
|
||
.space {
|
||
color: #fff;
|
||
background: radial-gradient(circle at 60% 40%, rgba(143,125,255,.46), transparent 30%), linear-gradient(135deg, #15162b, #48438f);
|
||
}
|
||
|
||
.night {
|
||
color: #fff;
|
||
background: linear-gradient(135deg, #101828, #3a1f1f);
|
||
}
|
||
|
||
.cave {
|
||
color: #4b2b00;
|
||
background: linear-gradient(135deg, #e7bd74, #6d4216);
|
||
}
|
||
|
||
.mini-slides {
|
||
position: absolute;
|
||
right: 18px;
|
||
top: 44px;
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.mini-slides span {
|
||
width: 90px;
|
||
height: 42px;
|
||
border-radius: 7px;
|
||
background: rgba(255,255,255,.78);
|
||
box-shadow: 0 5px 12px rgba(0,0,0,.16);
|
||
}
|
||
|
||
.chart-art {
|
||
position: absolute;
|
||
right: 28px;
|
||
bottom: 24px;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 8px;
|
||
}
|
||
|
||
.chart-art span {
|
||
width: 24px;
|
||
border-radius: 5px 5px 0 0;
|
||
background: rgba(255,255,255,.58);
|
||
}
|
||
|
||
.chart-art span:nth-child(1) { height: 42px; }
|
||
.chart-art span:nth-child(2) { height: 76px; }
|
||
.chart-art span:nth-child(3) { height: 56px; }
|
||
|
||
.tower-art {
|
||
position: absolute;
|
||
right: 34px;
|
||
bottom: 26px;
|
||
width: 112px;
|
||
height: 118px;
|
||
background:
|
||
linear-gradient(#0a6b7b, #0a6b7b) 28px 0 / 56px 13px no-repeat,
|
||
linear-gradient(#be5b2c, #be5b2c) 18px 28px / 76px 16px no-repeat,
|
||
linear-gradient(#0a6b7b, #0a6b7b) 10px 44px / 92px 18px no-repeat,
|
||
linear-gradient(#be5b2c, #be5b2c) 24px 72px / 64px 18px no-repeat,
|
||
linear-gradient(#e8dcc5, #e8dcc5) 0 94px / 112px 22px no-repeat;
|
||
border-radius: 50% 50% 4px 4px;
|
||
}
|
||
|
||
.people-art {
|
||
position: absolute;
|
||
right: 22px;
|
||
bottom: 16px;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 14px;
|
||
}
|
||
|
||
.people-art span {
|
||
width: 62px;
|
||
height: 92px;
|
||
border-radius: 30px 30px 12px 12px;
|
||
background: rgba(255,255,255,.55);
|
||
}
|
||
|
||
.people-art span:nth-child(2) {
|
||
width: 48px;
|
||
height: 72px;
|
||
}
|
||
|
||
.shape-art {
|
||
position: absolute;
|
||
right: 18px;
|
||
bottom: 22px;
|
||
width: 150px;
|
||
height: 110px;
|
||
}
|
||
|
||
.shape-art span {
|
||
position: absolute;
|
||
display: block;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.shape-art span:nth-child(1) {
|
||
width: 96px;
|
||
height: 96px;
|
||
right: 12px;
|
||
bottom: 0;
|
||
background: #9ed797;
|
||
}
|
||
|
||
.shape-art span:nth-child(2) {
|
||
width: 72px;
|
||
height: 72px;
|
||
left: 0;
|
||
bottom: 8px;
|
||
background: #b8d1ff;
|
||
}
|
||
|
||
.shape-art span:nth-child(3) {
|
||
width: 0;
|
||
height: 0;
|
||
left: 16px;
|
||
top: 2px;
|
||
border-left: 18px solid transparent;
|
||
border-right: 18px solid transparent;
|
||
border-bottom: 36px solid #ff6b2b;
|
||
border-radius: 0;
|
||
background: transparent;
|
||
}
|
||
|
||
.grid-art {
|
||
position: absolute;
|
||
inset: 0;
|
||
opacity: .55;
|
||
background-image:
|
||
linear-gradient(rgba(0,195,255,.18) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(0,195,255,.18) 1px, transparent 1px);
|
||
background-size: 30px 30px;
|
||
}
|
||
|
||
.grid-art span {
|
||
position: absolute;
|
||
width: 54px;
|
||
height: 36px;
|
||
border: 1px solid rgba(0,225,255,.35);
|
||
background: rgba(0,225,255,.08);
|
||
}
|
||
|
||
.grid-art span:nth-child(1) { left: 18%; top: 25%; }
|
||
.grid-art span:nth-child(2) { left: 55%; top: 18%; }
|
||
.grid-art span:nth-child(3) { left: 36%; top: 58%; }
|
||
.grid-art span:nth-child(4) { left: 72%; top: 62%; }
|
||
|
||
.cave-art,
|
||
.festival-art {
|
||
position: absolute;
|
||
inset: 0;
|
||
opacity: .42;
|
||
}
|
||
|
||
.cave-art {
|
||
background: radial-gradient(ellipse at 50% 76%, rgba(255,225,143,.78), transparent 34%), radial-gradient(ellipse at 50% -10%, rgba(0,0,0,.52), transparent 44%);
|
||
}
|
||
|
||
.festival-art {
|
||
background: radial-gradient(circle at 20% 25%, rgba(220,38,38,.56), transparent 10%), radial-gradient(circle at 78% 32%, rgba(220,38,38,.46), transparent 11%);
|
||
}
|
||
|
||
@media (max-width: 1380px) {
|
||
.resource-grid,
|
||
.case-grid {
|
||
grid-template-columns: repeat(3, minmax(220px, 1fr));
|
||
}
|
||
|
||
.floating-composer {
|
||
left: calc(var(--active-sidebar-width, var(--sidebar-width)) + 220px);
|
||
right: 220px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1080px) {
|
||
.courseware-shell {
|
||
padding: 30px 28px 80px;
|
||
}
|
||
|
||
.resource-grid,
|
||
.case-grid {
|
||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||
}
|
||
|
||
.task-workbench {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.tip-card {
|
||
display: none;
|
||
}
|
||
|
||
.floating-composer {
|
||
left: calc(var(--active-sidebar-width, var(--sidebar-width)) + 30px);
|
||
right: 30px;
|
||
min-width: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 760px) {
|
||
.courseware-shell,
|
||
.task-shell {
|
||
padding: 20px 14px 90px;
|
||
}
|
||
|
||
.resource-toolbar,
|
||
.prompt-toolbar,
|
||
.task-toolbar {
|
||
align-items: stretch;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.search-pill {
|
||
width: 100%;
|
||
}
|
||
|
||
.resource-grid,
|
||
.case-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.floating-composer {
|
||
left: 96px;
|
||
right: 12px;
|
||
}
|
||
}
|
||
</style>
|