豫唐智能教案在线生成平台V1.0.0
公告:铁柱资源网为用户提供最新的原创技术教程,还有电脑技巧以及其他日常信息 游戏资讯等 让我们的生活更加精彩有乐趣!铁柱网原先域名:qq8m.com,部分地区打不开,新域名:qq8m.com,qq8m.com,qq8m.com重要的事情说三遍
铁柱资源网网站源码豫唐智能教案在线生成平台V1.0.0

豫唐智能教案在线生成平台V1.0.0

网站源码傲轩小编2026-03-10 9:33:111.64 KA+A-
豫唐智能教案在线生成平台,基于 Vue 3 + Vite 开发的现代化教师辅助工具,旨在提升备课与教学效率。
经过从 0.0.0 内测阶段的深度打磨,我们正式迎来 1.0.0 正式版。本次更新不仅定义了全新的品牌名称,更在核心生成引擎上实现了质的飞跃,旨在为职业教育及企业内训提供高性能的辅助方案。
豫唐智能教案在线生成平台V1.0.0 豫唐智能教案在线生成平台V1.0.0 豫唐智能教案在线生成平台V1.0.0
项目定位
豫唐教师辅助教学平台(Yutang Teacher Assistant Platform)是一款专为教育工作者打造的生产力工具。项目深度结合职业教育教学需求,通过 Vue 3 与现代 AI 技术栈,将传统繁琐的备课、出卷、课件制作流程转化为高度自动化的数字化工作流。

当前版本:v1.0.0

行业背景与核心价值
在数字化教学改革背景下,教师面临着教学内容更新快、教研压力大等挑战。本项目通过对智能化工作流的整合与结构化数据处理,实现了从教案构思到多格式文件(Word, PPT, PDF)输出的全链路闭环,旨在为职业教育及企业内训提供高性能的辅助方案。

在线体验
项目链接: https://www.ytecn.com/teacher/
开源地址:https://github.com/tcshowhand/teacher

核心功能与技术实现
1. 结构化教案生成系统 (AI Lesson Planning)
利用提示词工程(Prompt Engineering)引导 AI 生成符合教育逻辑的结构化内容:
深度覆盖:预设教学目标、学情分析、重难点突破及教学反思模块。
专业定制:内置针对教育场景优化的提示词模板,生成更加专业的教学内容。
文档引擎:基于 docx 与 docxtemplater 实现 Office Open XML 协议的无损导出。

2. 智能幻灯片生成模块 (Smart PPT Editor)
解决了从教学大纲到视觉演示的转换问题:
智能解析:利用大模型提取教学大纲关键信息,自动规划演示结构。
标准化导出:集成 pptxgenjs 库,支持生成标准 .pptx 文件,确保多端演示兼容性。

3. 专业测评构建引擎 (Exam Editor)
针对不同学科课程的评价需求,优化了题目管理逻辑:
全题型支持:涵盖选择题、判断题、代码填空及综合应用题。
高清排版:利用 jspdf 与 html2canvas 组合技术,支持生成高质量 PDF 试卷。

4. 智能化教学助手 (AI Assistant)
上下文感知:支持读取当前编辑的内容(如大纲或PPT),提供针对性的润色与建议。
灵活配置:支持用户自定义 API Key 及切换不同版本的模型(如 Qwen-Turbo/Plus)。

快速开始
1. 下载项目
git clone https://github.com/tcshowhand/teacher.git
cd teacher
2. 安装依赖
npm install
3. 启动开发服务器
npm run dev
4. 构建生产版本
npm run build
ExamEditor.vue
[Asm]
<script setup>import { ref, onMounted, watch, nextTick } from 'vue'import ExamPaper from '../components/ExamPaper.vue'import AIChatAssistant from '../components/AIChatAssistant.vue'import Toolbar from '../components/Toolbar.vue'import SettingsModal from '../components/SettingsModal.vue' import html2canvas from 'html2canvas'import jsPDF from 'jspdf'import { saveAs } from 'file-saver'import localforage from 'localforage'import { useRoute } from 'vue-router' import { sendToQwenAIDialogue } from '../api/qwenAPI' import { useSettingsStore } from '../store/settings'import { DEFAULT_MODEL_ID } from '../config/models' const settings = useSettingsStore() const currentDocId = ref('')const examData = ref(null)const isGeneratingExam = ref(false)  const TEMPLATES_KEY = 'exam_paper_templates_v1'const LAST_ACTIVE_KEY = 'last_active_doc_v1' const savedTemplates = ref([])const showSaveModal = ref(false)const showLoadModal = ref(false)const showDeleteConfirmModal = ref(false)const showResetConfirmModal = ref(false) // 添加AI生成确认弹窗状态const showAIGenConfirmModal = ref(false) const pendingDeleteTemplateIndex = ref(-1)const pendingLoadTemplate = ref(null)const templateName = ref('')const isExporting = ref(false)const showApiKeyAlertModal = ref(false) // New Alert Modal  const route = useRoute() // Helper to create empty exam structureconst createEmptyExam = (title = '新试题') => ({  title: title,  subTitle: '考试时间:__分钟  满分:__分',  items: []}) const getStorageKey = (docId) => {  return `exam_data_v1_paper_${currentModelId.value}_${docId}`} const currentModelId = ref(localStorage.getItem('last_active_model_id') || DEFAULT_MODEL_ID) const handleModelChange = async (newModelId) => {    currentModelId.value = newModelId    localStorage.setItem('last_active_model_id', newModelId)    await loadCurrentData()} const loadCurrentData = async () => {  const storageKey = getStorageKey(currentDocId.value)  let loaded = false     try {    const cached = await localforage.getItem(storageKey)    if (cached) {      examData.value = typeof cached === 'string' ? JSON.parse(cached) : cached      loaded = true    }  } catch (e) {    console.error('Failed to parse cached data', e)  }   if (!loaded) {    // Default initialization logic    let title = route.query.title || currentDocId.value    let subTitle = ''         // Attempt to sync with Generator State if applicable    const { courseName, chapterId } = route.query    if (courseName && chapterId) {        const GENERATOR_STORAGE_KEY = 'lesson_plan_generator_state_v3'        try {            const rawState = localStorage.getItem(GENERATOR_STORAGE_KEY)            if (rawState) {                const state = JSON.parse(rawState)                if (state.courseName === courseName) {                    const foundChapter = state.generatedChapters.find(c => c.id === Number(chapterId))                    if (foundChapter) {                        title = `${courseName} - ${foundChapter.mainTitle}`                        subTitle = foundChapter.subTitle ? `章节:${foundChapter.subTitle}` : ''                    }                }            }        } catch(e) { console.error(e) }    }     if (currentDocId.value === 'default_doc') {         try {            const baseUrl = import.meta.env.BASE_URL.endsWith('/') ? import.meta.env.BASE_URL : import.meta.env.BASE_URL + '/'            const response = await fetch(`${baseUrl}exam_data.json`)            examData.value = await response.json()         } catch(e) {            examData.value = createEmptyExam(title)         }    } else {         examData.value = createEmptyExam(title)         if (subTitle) examData.value.subTitle = subTitle    }  }} onMounted(async () => {   // 1. Load Templates  try {    const cachedTemplates = await localforage.getItem(TEMPLATES_KEY)    if (cachedTemplates) {      savedTemplates.value = typeof cachedTemplates === 'string' ? JSON.parse(cachedTemplates) : cachedTemplates    }  } catch (e) {    console.error('Failed to load templates', e)  }   // 2. Determine Document ID (Persistence Key)  const { courseName, chapterId } = route.query     if (courseName && chapterId) {    currentDocId.value = `${courseName}_ch${chapterId}`  } else if (route.query.title) {    currentDocId.value = route.query.title  } else {    currentDocId.value = localStorage.getItem(LAST_ACTIVE_KEY) || 'default_doc'  }     localStorage.setItem(LAST_ACTIVE_KEY, currentDocId.value)   // 3. Load Data  await loadCurrentData()}) // Auto-save to local storage (IndexedDB)watch(examData, async (newVal) => {  if (newVal && currentDocId.value) {    try {      const storageKey = getStorageKey(currentDocId.value)      await localforage.setItem(storageKey, JSON.parse(JSON.stringify(newVal))) 
    } catch (e) {      console.error('Auto-save failed', e)    }  }}, { deep: true }) const saveTemplatesToStorage = async () => {  try {    await localforage.setItem(TEMPLATES_KEY, JSON.parse(JSON.stringify(savedTemplates.value)))  } catch (e) {    alert('保存模板失败: ' + e.message)  }} const handleExportPDF = async () => {  const element = document.getElementById('exam-paper')  if (!element) return   isExporting.value = true  await nextTick()   try {    const scale = 2    const canvas = await html2canvas(element, { 
      scale: scale,      useCORS: true,      backgroundColor: '#ffffff'    })         const contentWidth = canvas.width    const contentHeight = canvas.height    const pdf = new jsPDF('p', 'mm', 'a4')    const pdfPageWidth = pdf.internal.pageSize.getWidth()    const pdfPageHeight = pdf.internal.pageSize.getHeight()    const pxPerMm = contentWidth / pdfPageWidth    const marginMm = 20    const marginPx = marginMm * pxPerMm    const pageHeightInPx = (pdfPageHeight * pxPerMm) - (marginPx * 2) // Printable area height in px         // Get all question items to check for cuts    const questionElements = element.querySelectorAll('.question-item')    // Calculate logical positions (unscaled) then scale them    // Note: html2canvas scale affects the image size, but DOM offsetTop is unscaled.    // We need to map DOM coordinates to Canvas coordinates.    // Canvas is scaled by 'scale'. DOM offsets are 1x.    // So we multiply DOM offsets by scale.         const questions = Array.from(questionElements).map(el => {      // Get offset relative to the exam-paper element      // offsetTop is relative to offsetParent.       // We assume exam-paper is the offsetParent or we calculate cumulative offset.      // safest is getBoundingClientRect      const rect = el.getBoundingClientRect()      const containerRect = element.getBoundingClientRect()      const top = (rect.top - containerRect.top) * scale      const height = rect.height * scale      return { top, bottom: top + height }    })     let currentY = 0    let remainingHeight = contentHeight     while (currentY < contentHeight) {      if (currentY > 0) pdf.addPage()       
      // Default: Fill the page      let sliceHeight = Math.min(pageHeightInPx, contentHeight - currentY)      let nextCutY = currentY + sliceHeight       // Check if we are cutting through a question      // A question is cut if: q.top < nextCutY AND q.bottom > nextCutY      // We look for the FIRST question that satisfies this      const crossingQuestion = questions.find(q => q.top < nextCutY && q.bottom > nextCutY)       if (crossingQuestion) {        // If the question is taller than the page itself, we can't avoid cutting it.        // We only adjust if the question starts AFTER currentY (it fits on the page partially, but we prefer to push it)        // OR if it could fit on the NEXT page.        // Simplified logic: If the cut is strictly inside the question, and the question top is below currentY,        // we cut AT the question top (pushing it to next page).        if (crossingQuestion.top > currentY) {            // Adjust cut to be at the start of the question            nextCutY = crossingQuestion.top            sliceHeight = nextCutY - currentY        }        // If crossingQuestion.top <= currentY, it means a huge question starting before this page even began         // (or at the top) is continuing. We just have to cut it.      }       // Create a canvas for this slice      const sliceCanvas = document.createElement('canvas')      sliceCanvas.width = contentWidth      sliceCanvas.height = sliceHeight       
      const sCtx = sliceCanvas.getContext('2d')       
      // Draw the slice      // drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)      sCtx.drawImage(canvas, 0, currentY, contentWidth, sliceHeight, 0, 0, contentWidth, sliceHeight)       
      const sliceData = sliceCanvas.toDataURL('image/png')      // PDF height needs to be calculated based on the actual sliceHeight drawn      const pdfSliceHeight = sliceHeight / pxPerMm       
      pdf.addImage(sliceData, 'PNG', 0, marginMm, pdfPageWidth, pdfSliceHeight)       
      currentY += sliceHeight      // Add a tiny buffer to avoid potential rounding loops, though logic should be robust      if (sliceHeight <= 0) break; // Safety break    }         const now = new Date()    const timeStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`    const fileName = `${examData.value.title || 'Exam'}_${timeStr}.pdf`    pdf.save(fileName)  } catch (error) {    console.error('PDF Export Failed:', error)    alert('导出失败,请重试')  } finally {    isExporting.value = false  }} const handleExportJSON = () => {  const blob = new Blob([JSON.stringify(examData.value, null, 2)], { type: 'application/json' })  const now = new Date()  const timeStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`  const fileName = `${examData.value.title || 'Exam'}_${timeStr}.json`  saveAs(blob, fileName)} const handleSaveTemplate = () => {  if (!examData.value) return  templateName.value = `模板 ${savedTemplates.value.length + 1}`  showSaveModal.value = true} const confirmSaveTemplate = async () => {  if (!templateName.value) {    alert('请输入模板名称')    return  }  const newTemplate = {    id: Date.now(),    name: templateName.value,    data: JSON.parse(JSON.stringify(examData.value)),    date: new Date().toLocaleString(),    type: 'exam'  }  savedTemplates.value.unshift(newTemplate)  await saveTemplatesToStorage()  showSaveModal.value = false} const handleLoadTemplate = () => {  showLoadModal.value = true} const loadTemplate = (template) => {  if (template.type !== 'exam') {      alert('无法在试题编辑器中加载教案模板')      return  }  pendingLoadTemplate.value = template  showLoadConfirmModal.value = true} const confirmLoadTemplate = () => {  if (pendingLoadTemplate.value) {    examData.value = JSON.parse(JSON.stringify(pendingLoadTemplate.value.data))    showLoadModal.value = false    pendingLoadTemplate.value = null  }  showLoadConfirmModal.value = false} const deleteTemplate = (index) => {  pendingDeleteTemplateIndex.value = index  showDeleteConfirmModal.value = true} const confirmDeleteTemplate = async () => {  if (pendingDeleteTemplateIndex.value > -1) {    savedTemplates.value.splice(pendingDeleteTemplateIndex.value, 1)    await saveTemplatesToStorage()    pendingDeleteTemplateIndex.value = -1  }  showDeleteConfirmModal.value = false} const cancelDeleteTemplate = () => {  pendingDeleteTemplateIndex.value = -1  showDeleteConfirmModal.value = false} const handleImportJSON = (json) => {  examData.value = json} const handleReset = () => {  showResetConfirmModal.value = true} const confirmReset = async () => {  try {    if (currentDocId.value) {        const key = getStorageKey(currentDocId.value)        await localforage.removeItem(key)    }         examData.value = createEmptyExamData(currentDocId.value || '示范课程 - 示范章节')  } catch (e) {    console.error('Failed to reset', e)  }  showResetConfirmModal.value = false} // 修改generateExamPaper函数,使用自定义弹窗const generateExamPaper = async () => {  if (isGeneratingExam.value) return  if (examData.value && examData.value.problems && examData.value.problems.length > 0) {    // 显示自定义确认弹窗而不是原生confirm    showAIGenConfirmModal.value = true    return  }     // 如果没有现有试题,直接生成  await confirmGenerateExamPaper()} // 新增确认生成函数const confirmGenerateExamPaper = async () => {  showAIGenConfirmModal.value = false  isGeneratingExam.value = true  // Extract course title  let title = "计算机相关课程"  if (route.query.title) {      title = route.query.title  } else if (examData.value && examData.value.title) {      title = examData.value.title  }   const questionCount = settings.examQuestionCount || 5  const prompt = `请为课程"${title}"生成一份包含 ${questionCount} 道题目的试题数据。     请严格按照以下 JSON 格式返回,不要包含代码块:  {    "title": "${title}",    "info": ["姓名: _______________", "学号: _______________", "得分: ___________"],    "footer": "~ End of Practice ~",    "problems": [      {        "qNum": "Q1.",        "title": "题目名称",        "tags": "知识点",        "desc": "详细的题目描述(HTML supported)...",        "input": "输入样例(仅编程题需要, 非编程题请省略)",        "output": "输出样例(仅编程题需要, 非编程题请省略)"      }      // ... 请生成一共 ${questionCount} 道题目    ]  }  注意:如果是编程类课程,请提供 input/output 样例;如果是理论、文学、数学等非编程类课程,请勿在JSON中包含 input 和 output 字段。`   const messages = [{ role: 'user', content: prompt }]     let fullText = ''  await sendToQwenAIDialogue(messages, (text, isComplete) => {    fullText = text    if (isComplete) {      isGeneratingExam.value = false      try {        const cleanText = fullText.replace(/```json/g, '').replace(/```/g, '').trim()                 // Check for specific error message from worker/API        if (cleanText.includes('请先配置 API Key') || cleanText.includes('API Key not configured')) {            showApiKeyAlertModal.value = true            return        }         const newData = JSON.parse(cleanText)                 // Ensure image fields exist even if empty        if (newData.problems) {            newData.problems.forEach(p => {                if (!p.image) p.image = ""            })        }         examData.value = newData      } catch (e) {        console.error('Failed to parse AI exam', e)        alert('生成失败,AI 返回格式不正确。')      }    }  })} const showAIChat = ref(false) const handleAIUpdate = (newData) => {  if (!newData) return  // Merge or replace  // We'll replace the fields that exist in newData  Object.keys(newData).forEach(key => {    examData.value[key] = newData[key]  })}</script><template>  <div class="app-container">    <div class="home-link">      <router-link to="/">&#127968; 返回首页</router-link>    </div>    <Toolbar       @export-pdf="handleExportPDF"      @export-json="handleExportJSON"      @save-template="handleSaveTemplate"      @load-template="handleLoadTemplate"      @import-json="handleImportJSON"      @reset-data="handleReset"      @open-settings="showSettingsModal = true"    />         <div class="ai-actions">      <button class="ai-gen-btn" @click="generateExamPaper" :disabled="isGeneratingExam">        {{ isGeneratingExam ? 'AI 生成中...' : `AI 一键生成试题 (${settings.examQuestionCount}题)` }}      </button>    </div>    <!-- AI Chat Assistant -->    <AIChatAssistant       v-model="showAIChat"      :currentContent="examData"      systemContext="您是试题助手。请根据用户的指令调整当前的试卷(JSON对象)。例如:'增加两道关于函数的选择题' 或 '把最后一道题的难度加大'。"      @update-content="handleAIUpdate"    />    <!-- Floating AI Chat Button -->    <button class="ai-chat-fab" @click="showAIChat = !showAIChat" title="AI 助手">      &#129302; 试题助手    </button>    <div class="content-area" v-if="examData">      <ExamPaper :examData="examData" :class="{ 'exporting': isExporting }" />    </div>    <div v-else class="loading">      Loading Data...    </div>    <!-- Modals (Save, Load, Delete, Reset, Export, Settings, Chat) -->    <!-- Copying existing modal structure directly -->         <!-- Save Template Modal -->    <div class="modal-overlay" v-if="showSaveModal">      <div class="modal-content">        <h3>&#128190; 保存为模板</h3>        <input v-model="templateName" placeholder="给模板起个名字..." class="modal-input" @keyup.enter="confirmSaveTemplate" />        <div class="modal-actions">          <button class="modal-btn cancel" @click="showSaveModal = false">取消</button>          <button class="modal-btn confirm" @click="confirmSaveTemplate">保存</button>        </div>      </div>    </div>    <!-- Load Template Modal -->    <div class="modal-overlay" v-if="showLoadModal">      <div class="modal-content load-modal">        <h3>&#128194; 导入模板 (仅展示试题)</h3>        <div class="template-list" v-if="savedTemplates.filter(t => t.type === 'exam').length > 0">          <div v-for="(template, index) in savedTemplates.filter(t => t.type === 'exam')" :key="template.id" class="template-item">            <div class="template-info" @click="loadTemplate(template)">              <div class="t-name">                <span class="tag-exam">试题</span>                {{ template.name }}              </div>              <div class="t-date">{{ template.date }}</div>            </div>            <button class="delete-template-btn" @click.stop="deleteTemplate(index)" title="删除模板">×</button>          </div>        </div>        <div v-else class="empty-list">          暂无保存的模板        </div>        <div class="modal-actions">          <button class="modal-btn cancel" @click="showLoadModal = false">关闭</button>        </div>      </div>    </div>    <!-- Delete Template Confirmation Modal -->    <div class="modal-overlay" v-if="showDeleteConfirmModal" style="z-index: 2100;">      <div class="modal-content">        <h3>&#128465;&#65039; 确认删除模板?</h3>        <p>确定要删除这个模板吗?此操作无法撤销。</p>        <div class="modal-actions">          <button class="modal-btn cancel" @click="cancelDeleteTemplate">取消</button>          <button class="modal-btn confirm" @click="confirmDeleteTemplate">删除</button>        </div>      </div>    </div>    <!-- Reset Data Confirmation Modal -->    <div class="modal-overlay" v-if="showResetConfirmModal" style="z-index: 2100;">      <div class="modal-content">        <h3>&#129529; 确认重置?</h3>        <p>确定要清空所有修改吗?<br>这将恢复到默认状态。此操作无法撤销!</p>        <div class="modal-actions">          <button class="modal-btn cancel" @click="showResetConfirmModal = false">取消</button>          <button class="modal-btn confirm" @click="confirmReset">重置</button>        </div>      </div>    </div>    <!-- Load Template Confirmation Modal -->    <div class="modal-overlay" v-if="showLoadConfirmModal" style="z-index: 2200;">      <div class="modal-content">        <h3>&#128214; 确认加载?</h3>        <p v-if="pendingLoadTemplate">确定要加载模板 "<b>{{ pendingLoadTemplate.name }}</b>" 吗?<br>当前未保存的修改将会丢失。</p>        <div class="modal-actions">          <button class="modal-btn cancel" @click="showLoadConfirmModal = false">取消</button>          <button class="modal-btn confirm" @click="confirmLoadTemplate">加载</button>        </div>      </div>    </div>    <!-- API Key Alert Modal -->    <div class="modal-overlay" v-if="showApiKeyAlertModal" style="z-index: 2300;">      <div class="modal-content">        <h3>&#9888;&#65039; 需要配置 API Key</h3>        <p>AI 功能需要配置阿里云 DashScope API Key 才能使用。</p>        <div class="modal-actions">          <button class="modal-btn cancel" @click="showApiKeyAlertModal = false">取消</button>          <button class="modal-btn confirm" @click="showApiKeyAlertModal = false; showSettingsModal = true">去配置</button>        </div>      </div>    </div>    <!-- AI Generation Confirmation Modal -->    <div class="modal-overlay" v-if="showAIGenConfirmModal" style="z-index: 2200;">      <div class="modal-content">        <h3>AI 一键生成</h3>        <p>AI 将根据当前的课程信息自动生成试题。<br><b>注意:此操作可能会覆盖您已手动输入的内容。</b></p>        <div class="modal-actions">          <button class="modal-btn cancel" @click="showAIGenConfirmModal = false">取消</button>          <button class="modal-btn confirm" @click="confirmGenerateExamPaper">&#10024; 开始生成</button>        </div>      </div>    </div>    <!-- Export Loading Overlay -->    <div class="modal-overlay" v-if="isExporting" style="z-index: 3000; cursor: wait;">      <div class="modal-content" style="max-width: 300px;">        <h3>&#128424;&#65039; 正在导出...</h3>        <p>正在努力生成高清 PDF,<br>请稍候片刻...</p>        <div class="loading-spinner">&#9999;&#65039;</div>      </div>    </div>    <!-- AI Components -->    <!-- AI Components -->    <SettingsModal       v-if="showSettingsModal"      :currentModelId="currentModelId"      :show-model-selector="true"      @change-model="handleModelChange"      @close="showSettingsModal = false"    />  </div></template><style scoped>.app-container {  padding: 20px;}.home-link {  position: fixed;  top: 20px;  left: 20px;  z-index: 100;}.home-link a {  text-decoration: none;  font-weight: bold;  color: #2c3e50;  background: white;  padding: 10px 15px;  border-radius: 20px;  border: 2px solid #2c3e50;  box-shadow: 2px 2px 0 #2c3e50;  transition: transform 0.1s;}.home-link a:hover {  transform: scale(1.05);} .ai-actions {  text-align: center;  margin-bottom: 20px;}    .ai-actions {  text-align: center;  margin-bottom: 20px;} .ai-gen-btn {    background: white;    color: #2c3e50;    border: 3px solid #2c3e50;    padding: 12px 30px;    font-size: 1.2em;    border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;    cursor: pointer;    box-shadow: 4px 4px 0 #2c3e50;    font-weight: bold;    font-family: inherit;    transition: all 0.2s;} .ai-gen-btn:hover:not(:disabled) {  transform: translate(-2px, -2px);  box-shadow: 6px 6px 0 #2c3e50;  background: #f3e5f5;} .ai-gen-btn:disabled {    background: #eee;    color: #999;    border-color: #999;    box-shadow: none;    cursor: wait;} .tag-plan {  background: #e1f5fe;  color: #039be5;  font-size: 0.8em;  padding: 2px 6px;  border-radius: 4px;  margin-right: 5px;} .tag-exam {  background: #fff3e0;  color: #fb8c00;  font-size: 0.8em;  padding: 2px 6px;  border-radius: 4px;  margin-right: 5px;} .loading {  text-align: center;  font-size: 1.5em;  margin-top: 100px;  color: #666;} /* Modal Styles Global */.modal-overlay {  position: fixed;  top: 0;  left: 0;  right: 0;  bottom: 0;  background: rgba(0,0,0,0.5);  display: flex;  align-items: center;  justify-content: center;  z-index: 2000;  backdrop-filter: blur(3px);} .modal-content {  background: #fdfbf7;  padding: 30px;  border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;  border: 3px solid #2c3e50;  box-shadow: 10px 10px 0 rgba(0,0,0,0.2);  width: 90%;  max-width: 400px;  text-align: center;  font-family: 'Architects Daughter', cursive;} .modal-content h3 {  font-size: 1.5em;  margin-bottom: 20px;  border-bottom: 1px dashed #ccc;  padding-bottom: 10px;} .modal-input {  width: 80%;  padding: 10px;  margin-bottom: 20px;  font-size: 1.2em;  font-family: inherit;  border: 2px solid #2c3e50;  border-radius: 5px;  outline: none;} .modal-actions {  display: flex;  justify-content: center;  gap: 20px;  margin-top: 20px;} .modal-btn {  padding: 8px 20px;  border: 2px solid #2c3e50;  background: transparent;  font-family: inherit;  font-size: 1.1em;  cursor: pointer;  border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;  transition: transform 0.1s;} .modal-btn:hover {  transform: scale(1.05);} .modal-btn.confirm {  background: #e74c3c;  color: white;  border-color: #e74c3c;} .modal-btn.cancel {  border-style: dashed;} /* Template List */.load-modal {  max-width: 500px;} .template-list {  max-height: 300px;  overflow-y: auto;  text-align: left;} .template-item {  display: flex;  justify-content: space-between;  align-items: center;  padding: 10px;  border-bottom: 1px solid #eee;  cursor: pointer;  transition: background 0.2s;} .template-item:hover {  background: rgba(0,0,0,0.05);} .template-info {  flex: 1;} .t-name {  font-weight: bold;  font-size: 1.1em;} .ai-chat-fab {  position: fixed;  bottom: 20px;  right: 20px;  background: #2c3e50;  color: white;  border: none;  border-radius: 30px;  padding: 12px 24px;  font-size: 1.1em;  font-weight: bold;  box-shadow: 0 4px 10px rgba(0,0,0,0.3);  cursor: pointer;  z-index: 900;  transition: transform 0.2s;  font-family: 'Architects Daughter', cursive;  border: 2px solid white;} .ai-chat-fab:hover {  transform: scale(1.05);  background: #34495e;} .t-date {  font-size: 0.8em;  color: #888;} .delete-template-btn {  background: transparent;  border: none;  color: #ccc;  font-size: 1.5em;  cursor: pointer;  padding: 0 10px;} .delete-template-btn:hover {  color: #c0392b;} .empty-list {  color: #999;  padding: 20px;} .loading-spinner {  font-size: 3em;  animation: writing 1s infinite alternate;  margin-top: 20px;} @keyframes writing {  from { transform: translateX(-20px) rotate(-10deg); }  to { transform: translateX(20px) rotate(10deg); }}</style>复制代码


点击这里复制本文地址 以上内容由铁柱网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!


扫码关注“铁柱网”微信公众号
更多精彩,等你来撩

有好的文章或资源希望【铁柱网】帮助分享推广,猛戳这里我要投稿

支持Ctrl+Enter提交
qrcode

铁柱资源网 © All Rights Reserved.  

嘿,欢迎咨询
请先 登录 再评论,若不是会员请先 注册