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

2026-03-10 0 614

豫唐智能教案在线生成平台,基于 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]

  1. <script setup>
  2. import { ref, onMounted, watch, nextTick } from ‘vue’
  3. import ExamPaper from ‘../components/ExamPaper.vue’
  4. import AIChatAssistant from ‘../components/AIChatAssistant.vue’
  5. import Toolbar from ‘../components/Toolbar.vue’
  6. import SettingsModal from ‘../components/SettingsModal.vue’
  7. import html2canvas from ‘html2canvas’
  8. import jsPDF from ‘jspdf’
  9. import { saveAs } from ‘file-saver’
  10. import localforage from ‘localforage’
  11. import { useRoute } from ‘vue-router’
  12. import { sendToQwenAIDialogue } from ‘../api/qwenAPI’
  13. import { useSettingsStore } from ‘../store/settings’
  14. import { DEFAULT_MODEL_ID } from ‘../config/models’
  15. const settings = useSettingsStore()
  16. const currentDocId = ref(”)
  17. const examData = ref(null)
  18. const isGeneratingExam = ref(false)
  19. const TEMPLATES_KEY = ‘exam_paper_templates_v1’
  20. const LAST_ACTIVE_KEY = ‘last_active_doc_v1’
  21. const savedTemplates = ref([])
  22. const showSaveModal = ref(false)
  23. const showLoadModal = ref(false)
  24. const showDeleteConfirmModal = ref(false)
  25. const showResetConfirmModal = ref(false)
  26. // 添加AI生成确认弹窗状态
  27. const showAIGenConfirmModal = ref(false)
  28. const pendingDeleteTemplateIndex = ref(-1)
  29. const pendingLoadTemplate = ref(null)
  30. const templateName = ref(”)
  31. const isExporting = ref(false)
  32. const showApiKeyAlertModal = ref(false) // New Alert Modal
  33. const route = useRoute()
  34. // Helper to create empty exam structure
  35. const createEmptyExam = (title = ‘新试题’) => ({
  36.   title: title,
  37.   subTitle: ‘考试时间:__分钟  满分:__分’,
  38.   items: []
  39. })
  40. const getStorageKey = (docId) => {
  41.   return `exam_data_v1_paper_${currentModelId.value}_${docId}`
  42. }
  43. const currentModelId = ref(localStorage.getItem(‘last_active_model_id’) || DEFAULT_MODEL_ID)
  44. const handleModelChange = async (newModelId) => {
  45.     currentModelId.value = newModelId
  46.     localStorage.setItem(‘last_active_model_id’, newModelId)
  47.     await loadCurrentData()
  48. }
  49. const loadCurrentData = async () => {
  50.   const storageKey = getStorageKey(currentDocId.value)
  51.   let loaded = false
  52.   try {
  53.     const cached = await localforage.getItem(storageKey)
  54.     if (cached) {
  55.       examData.value = typeof cached === ‘string’ ? JSON.parse(cached) : cached
  56.       loaded = true
  57.     }
  58.   } catch (e) {
  59.     console.error(‘Failed to parse cached data’, e)
  60.   }
  61.   if (!loaded) {
  62.     // Default initialization logic
  63.     let title = route.query.title || currentDocId.value
  64.     let subTitle = ”
  65.     // Attempt to sync with Generator State if applicable
  66.     const { courseName, chapterId } = route.query
  67.     if (courseName && chapterId) {
  68.         const GENERATOR_STORAGE_KEY = ‘lesson_plan_generator_state_v3’
  69.         try {
  70.             const rawState = localStorage.getItem(GENERATOR_STORAGE_KEY)
  71.             if (rawState) {
  72.                 const state = JSON.parse(rawState)
  73.                 if (state.courseName === courseName) {
  74.                     const foundChapter = state.generatedChapters.find(c => c.id === Number(chapterId))
  75.                     if (foundChapter) {
  76.                         title = `${courseName} – ${foundChapter.mainTitle}`
  77.                         subTitle = foundChapter.subTitle ? `章节:${foundChapter.subTitle}` : ”
  78.                     }
  79.                 }
  80.             }
  81.         } catch(e) { console.error(e) }
  82.     }
  83.     if (currentDocId.value === ‘default_doc’) {
  84.          try {
  85.             const baseUrl = import.meta.env.BASE_URL.endsWith(‘/’) ? import.meta.env.BASE_URL : import.meta.env.BASE_URL + ‘/’
  86.             const response = await fetch(`${baseUrl}exam_data.json`)
  87.             examData.value = await response.json()
  88.          } catch(e) {
  89.             examData.value = createEmptyExam(title)
  90.          }
  91.     } else {
  92.          examData.value = createEmptyExam(title)
  93.          if (subTitle) examData.value.subTitle = subTitle
  94.     }
  95.   }
  96. }
  97. onMounted(async () => {
  98.   // 1. Load Templates
  99.   try {
  100.     const cachedTemplates = await localforage.getItem(TEMPLATES_KEY)
  101.     if (cachedTemplates) {
  102.       savedTemplates.value = typeof cachedTemplates === ‘string’ ? JSON.parse(cachedTemplates) : cachedTemplates
  103.     }
  104.   } catch (e) {
  105.     console.error(‘Failed to load templates’, e)
  106.   }
  107.   // 2. Determine Document ID (Persistence Key)
  108.   const { courseName, chapterId } = route.query
  109.   if (courseName && chapterId) {
  110.     currentDocId.value = `${courseName}_ch${chapterId}`
  111.   } else if (route.query.title) {
  112.     currentDocId.value = route.query.title
  113.   } else {
  114.     currentDocId.value = localStorage.getItem(LAST_ACTIVE_KEY) || ‘default_doc’
  115.   }
  116.   localStorage.setItem(LAST_ACTIVE_KEY, currentDocId.value)
  117.   // 3. Load Data
  118.   await loadCurrentData()
  119. })
  120. // Auto-save to local storage (IndexedDB)
  121. watch(examData, async (newVal) => {
  122.   if (newVal && currentDocId.value) {
  123.     try {
  124.       const storageKey = getStorageKey(currentDocId.value)
  125.       await localforage.setItem(storageKey, JSON.parse(JSON.stringify(newVal)))
  126.     } catch (e) {
  127.       console.error(‘Auto-save failed’, e)
  128.     }
  129.   }
  130. }, { deep: true })
  131. const saveTemplatesToStorage = async () => {
  132.   try {
  133.     await localforage.setItem(TEMPLATES_KEY, JSON.parse(JSON.stringify(savedTemplates.value)))
  134.   } catch (e) {
  135.     alert(‘保存模板失败: ‘ + e.message)
  136.   }
  137. }
  138. const handleExportPDF = async () => {
  139.   const element = document.getElementById(‘exam-paper’)
  140.   if (!element) return
  141.   isExporting.value = true
  142.   await nextTick()
  143.   try {
  144.     const scale = 2
  145.     const canvas = await html2canvas(element, {
  146.       scale: scale,
  147.       useCORS: true,
  148.       backgroundColor: ‘#ffffff’
  149.     })
  150.     const contentWidth = canvas.width
  151.     const contentHeight = canvas.height
  152.     const pdf = new jsPDF(‘p’, ‘mm’, ‘a4’)
  153.     const pdfPageWidth = pdf.internal.pageSize.getWidth()
  154.     const pdfPageHeight = pdf.internal.pageSize.getHeight()
  155.     const pxPerMm = contentWidth / pdfPageWidth
  156.     const marginMm = 20
  157.     const marginPx = marginMm * pxPerMm
  158.     const pageHeightInPx = (pdfPageHeight * pxPerMm) – (marginPx * 2) // Printable area height in px
  159.     // Get all question items to check for cuts
  160.     const questionElements = element.querySelectorAll(‘.question-item’)
  161.     // Calculate logical positions (unscaled) then scale them
  162.     // Note: html2canvas scale affects the image size, but DOM offsetTop is unscaled.
  163.     // We need to map DOM coordinates to Canvas coordinates.
  164.     // Canvas is scaled by ‘scale’. DOM offsets are 1x.
  165.     // So we multiply DOM offsets by scale.
  166.     const questions = Array.from(questionElements).map(el => {
  167.       // Get offset relative to the exam-paper element
  168.       // offsetTop is relative to offsetParent.
  169.       // We assume exam-paper is the offsetParent or we calculate cumulative offset.
  170.       // safest is getBoundingClientRect
  171.       const rect = el.getBoundingClientRect()
  172.       const containerRect = element.getBoundingClientRect()
  173.       const top = (rect.top – containerRect.top) * scale
  174.       const height = rect.height * scale
  175.       return { top, bottom: top + height }
  176.     })
  177.     let currentY = 0
  178.     let remainingHeight = contentHeight
  179.     while (currentY < contentHeight) {
  180.       if (currentY > 0) pdf.addPage()
  181.       // Default: Fill the page
  182.       let sliceHeight = Math.min(pageHeightInPx, contentHeight – currentY)
  183.       let nextCutY = currentY + sliceHeight
  184.       // Check if we are cutting through a question
  185.       // A question is cut if: q.top < nextCutY AND q.bottom > nextCutY
  186.       // We look for the FIRST question that satisfies this
  187.       const crossingQuestion = questions.find(q => q.top < nextCutY && q.bottom > nextCutY)
  188.       if (crossingQuestion) {
  189.         // If the question is taller than the page itself, we can’t avoid cutting it.
  190.         // We only adjust if the question starts AFTER currentY (it fits on the page partially, but we prefer to push it)
  191.         // OR if it could fit on the NEXT page.
  192.         // Simplified logic: If the cut is strictly inside the question, and the question top is below currentY,
  193.         // we cut AT the question top (pushing it to next page).
  194.         if (crossingQuestion.top > currentY) {
  195.             // Adjust cut to be at the start of the question
  196.             nextCutY = crossingQuestion.top
  197.             sliceHeight = nextCutY – currentY
  198.         }
  199.         // If crossingQuestion.top <= currentY, it means a huge question starting before this page even began
  200.         // (or at the top) is continuing. We just have to cut it.
  201.       }
  202.       // Create a canvas for this slice
  203.       const sliceCanvas = document.createElement(‘canvas’)
  204.       sliceCanvas.width = contentWidth
  205.       sliceCanvas.height = sliceHeight
  206.       const sCtx = sliceCanvas.getContext(‘2d’)
  207.       // Draw the slice
  208.       // drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
  209.       sCtx.drawImage(canvas, 0, currentY, contentWidth, sliceHeight, 0, 0, contentWidth, sliceHeight)
  210.       const sliceData = sliceCanvas.toDataURL(‘image/png’)
  211.       // PDF height needs to be calculated based on the actual sliceHeight drawn
  212.       const pdfSliceHeight = sliceHeight / pxPerMm
  213.       pdf.addImage(sliceData, ‘PNG’, 0, marginMm, pdfPageWidth, pdfSliceHeight)
  214.       currentY += sliceHeight
  215.       // Add a tiny buffer to avoid potential rounding loops, though logic should be robust
  216.       if (sliceHeight <= 0) break; // Safety break
  217.     }
  218.     const now = new Date()
  219.     const timeStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, ‘0’)}${String(now.getDate()).padStart(2, ‘0’)}`
  220.     const fileName = `${examData.value.title || ‘Exam’}_${timeStr}.pdf`
  221.     pdf.save(fileName)
  222.   } catch (error) {
  223.     console.error(‘PDF Export Failed:’, error)
  224.     alert(‘导出失败,请重试’)
  225.   } finally {
  226.     isExporting.value = false
  227.   }
  228. }
  229. const handleExportJSON = () => {
  230.   const blob = new Blob([JSON.stringify(examData.value, null, 2)], { type: ‘application/json’ })
  231.   const now = new Date()
  232.   const timeStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, ‘0’)}${String(now.getDate()).padStart(2, ‘0’)}`
  233.   const fileName = `${examData.value.title || ‘Exam’}_${timeStr}.json`
  234.   saveAs(blob, fileName)
  235. }
  236. const handleSaveTemplate = () => {
  237.   if (!examData.value) return
  238.   templateName.value = `模板 ${savedTemplates.value.length + 1}`
  239.   showSaveModal.value = true
  240. }
  241. const confirmSaveTemplate = async () => {
  242.   if (!templateName.value) {
  243.     alert(‘请输入模板名称’)
  244.     return
  245.   }
  246.   const newTemplate = {
  247.     id: Date.now(),
  248.     name: templateName.value,
  249.     data: JSON.parse(JSON.stringify(examData.value)),
  250.     date: new Date().toLocaleString(),
  251.     type: ‘exam’
  252.   }
  253.   savedTemplates.value.unshift(newTemplate)
  254.   await saveTemplatesToStorage()
  255.   showSaveModal.value = false
  256. }
  257. const handleLoadTemplate = () => {
  258.   showLoadModal.value = true
  259. }
  260. const loadTemplate = (template) => {
  261.   if (template.type !== ‘exam’) {
  262.       alert(‘无法在试题编辑器中加载教案模板’)
  263.       return
  264.   }
  265.   pendingLoadTemplate.value = template
  266.   showLoadConfirmModal.value = true
  267. }
  268. const confirmLoadTemplate = () => {
  269.   if (pendingLoadTemplate.value) {
  270.     examData.value = JSON.parse(JSON.stringify(pendingLoadTemplate.value.data))
  271.     showLoadModal.value = false
  272.     pendingLoadTemplate.value = null
  273.   }
  274.   showLoadConfirmModal.value = false
  275. }
  276. const deleteTemplate = (index) => {
  277.   pendingDeleteTemplateIndex.value = index
  278.   showDeleteConfirmModal.value = true
  279. }
  280. const confirmDeleteTemplate = async () => {
  281.   if (pendingDeleteTemplateIndex.value > -1) {
  282.     savedTemplates.value.splice(pendingDeleteTemplateIndex.value, 1)
  283.     await saveTemplatesToStorage()
  284.     pendingDeleteTemplateIndex.value = -1
  285.   }
  286.   showDeleteConfirmModal.value = false
  287. }
  288. const cancelDeleteTemplate = () => {
  289.   pendingDeleteTemplateIndex.value = -1
  290.   showDeleteConfirmModal.value = false
  291. }
  292. const handleImportJSON = (json) => {
  293.   examData.value = json
  294. }
  295. const handleReset = () => {
  296.   showResetConfirmModal.value = true
  297. }
  298. const confirmReset = async () => {
  299.   try {
  300.     if (currentDocId.value) {
  301.         const key = getStorageKey(currentDocId.value)
  302.         await localforage.removeItem(key)
  303.     }
  304.     examData.value = createEmptyExamData(currentDocId.value || ‘示范课程 – 示范章节’)
  305.   } catch (e) {
  306.     console.error(‘Failed to reset’, e)
  307.   }
  308.   showResetConfirmModal.value = false
  309. }
  310. // 修改generateExamPaper函数,使用自定义弹窗
  311. const generateExamPaper = async () => {
  312.   if (isGeneratingExam.value) return
  313.   if (examData.value && examData.value.problems && examData.value.problems.length > 0) {
  314.     // 显示自定义确认弹窗而不是原生confirm
  315.     showAIGenConfirmModal.value = true
  316.     return
  317.   }
  318.   // 如果没有现有试题,直接生成
  319.   await confirmGenerateExamPaper()
  320. }
  321. // 新增确认生成函数
  322. const confirmGenerateExamPaper = async () => {
  323.   showAIGenConfirmModal.value = false
  324.   isGeneratingExam.value = true
  325.   // Extract course title
  326.   let title = “计算机相关课程”
  327.   if (route.query.title) {
  328.       title = route.query.title
  329.   } else if (examData.value && examData.value.title) {
  330.       title = examData.value.title
  331.   }
  332.   const questionCount = settings.examQuestionCount || 5
  333.   const prompt = `请为课程”${title}”生成一份包含 ${questionCount} 道题目的试题数据。
  334.   请严格按照以下 JSON 格式返回,不要包含代码块:
  335.   {
  336.     “title”: “${title}”,
  337.     “info”: [“姓名: _______________”, “学号: _______________”, “得分: ___________”],
  338.     “footer”: “~ End of Practice ~”,
  339.     “problems”: [
  340.       {
  341.         “qNum”: “Q1.”,
  342.         “title”: “题目名称”,
  343.         “tags”: “知识点”,
  344.         “desc”: “详细的题目描述(HTML supported)…”,
  345.         “input”: “输入样例(仅编程题需要, 非编程题请省略)”,
  346.         “output”: “输出样例(仅编程题需要, 非编程题请省略)”
  347.       }
  348.       // … 请生成一共 ${questionCount} 道题目
  349.     ]
  350.   }
  351.   注意:如果是编程类课程,请提供 input/output 样例;如果是理论、文学、数学等非编程类课程,请勿在JSON中包含 input 和 output 字段。`
  352.   const messages = [{ role: ‘user’, content: prompt }]
  353.   let fullText = ”
  354.   await sendToQwenAIDialogue(messages, (text, isComplete) => {
  355.     fullText = text
  356.     if (isComplete) {
  357.       isGeneratingExam.value = false
  358.       try {
  359.         const cleanText = fullText.replace(/“`json/g, ”).replace(/“`/g, ”).trim()
  360.         // Check for specific error message from worker/API
  361.         if (cleanText.includes(‘请先配置 API Key’) || cleanText.includes(‘API Key not configured’)) {
  362.             showApiKeyAlertModal.value = true
  363.             return
  364.         }
  365.         const newData = JSON.parse(cleanText)
  366.         // Ensure image fields exist even if empty
  367.         if (newData.problems) {
  368.             newData.problems.forEach(p => {
  369.                 if (!p.image) p.image = “”
  370.             })
  371.         }
  372.         examData.value = newData
  373.       } catch (e) {
  374.         console.error(‘Failed to parse AI exam’, e)
  375.         alert(‘生成失败,AI 返回格式不正确。’)
  376.       }
  377.     }
  378.   })
  379. }
  380. const showAIChat = ref(false)
  381. const handleAIUpdate = (newData) => {
  382.   if (!newData) return
  383.   // Merge or replace
  384.   // We’ll replace the fields that exist in newData
  385.   Object.keys(newData).forEach(key => {
  386.     examData.value[key] = newData[key]
  387.   })
  388. }
  389. </script>
  390. <template>
  391.   <div class=”app-container”>
  392.     <div class=”home-link”>
  393.       <router-link to=”/”>&#127968; 返回首页</router-link>
  394.     </div>
  395.     <Toolbar
  396.       @export-pdf=”handleExportPDF”
  397.       @export-json=”handleExportJSON”
  398.       @save-template=”handleSaveTemplate”
  399.       @load-template=”handleLoadTemplate”
  400.       @import-json=”handleImportJSON”
  401.       @reset-data=”handleReset”
  402.       @open-settings=”showSettingsModal = true”
  403.     />
  404.     <div class=”ai-actions”>
  405.       <button class=”ai-gen-btn” @click=”generateExamPaper” :disabled=”isGeneratingExam”>
  406.         {{ isGeneratingExam ? ‘AI 生成中…’ : `AI 一键生成试题 (${settings.examQuestionCount}题)` }}
  407.       </button>
  408.     </div>
  409.     <!– AI Chat Assistant –>
  410.     <AIChatAssistant
  411.       v-model=”showAIChat”
  412.       :currentContent=”examData”
  413.       systemContext=”您是试题助手。请根据用户的指令调整当前的试卷(JSON对象)。例如:’增加两道关于函数的选择题’ 或 ‘把最后一道题的难度加大’。”
  414.       @update-content=”handleAIUpdate”
  415.     />
  416.     <!– Floating AI Chat Button –>
  417.     <button class=”ai-chat-fab” @click=”showAIChat = !showAIChat” title=”AI 助手”>
  418.       &#129302; 试题助手
  419.     </button>
  420.     <div class=”content-area” v-if=”examData”>
  421.       <ExamPaper :examData=”examData” :class=”{ ‘exporting’: isExporting }” />
  422.     </div>
  423.     <div v-else class=”loading”>
  424.       Loading Data…
  425.     </div>
  426.     <!– Modals (Save, Load, Delete, Reset, Export, Settings, Chat) –>
  427.     <!– Copying existing modal structure directly –>
  428.     <!– Save Template Modal –>
  429.     <div class=”modal-overlay” v-if=”showSaveModal”>
  430.       <div class=”modal-content”>
  431.         <h3>&#128190; 保存为模板</h3>
  432.         <input v-model=”templateName” placeholder=”给模板起个名字…” class=”modal-input” @keyup.enter=”confirmSaveTemplate” />
  433.         <div class=”modal-actions”>
  434.           <button class=”modal-btn cancel” @click=”showSaveModal = false”>取消</button>
  435.           <button class=”modal-btn confirm” @click=”confirmSaveTemplate”>保存</button>
  436.         </div>
  437.       </div>
  438.     </div>
  439.     <!– Load Template Modal –>
  440.     <div class=”modal-overlay” v-if=”showLoadModal”>
  441.       <div class=”modal-content load-modal”>
  442.         <h3>&#128194; 导入模板 (仅展示试题)</h3>
  443.         <div class=”template-list” v-if=”savedTemplates.filter(t => t.type === ‘exam’).length > 0″>
  444.           <div v-for=”(template, index) in savedTemplates.filter(t => t.type === ‘exam’)” :key=”template.id” class=”template-item”>
  445.             <div class=”template-info” @click=”loadTemplate(template)”>
  446.               <div class=”t-name”>
  447.                 <span class=”tag-exam”>试题</span>
  448.                 {{ template.name }}
  449.               </div>
  450.               <div class=”t-date”>{{ template.date }}</div>
  451.             </div>
  452.             <button class=”delete-template-btn” @click.stop=”deleteTemplate(index)” title=”删除模板”>×</button>
  453.           </div>
  454.         </div>
  455.         <div v-else class=”empty-list”>
  456.           暂无保存的模板
  457.         </div>
  458.         <div class=”modal-actions”>
  459.           <button class=”modal-btn cancel” @click=”showLoadModal = false”>关闭</button>
  460.         </div>
  461.       </div>
  462.     </div>
  463.     <!– Delete Template Confirmation Modal –>
  464.     <div class=”modal-overlay” v-if=”showDeleteConfirmModal” style=”z-index: 2100;”>
  465.       <div class=”modal-content”>
  466.         <h3>&#128465;&#65039; 确认删除模板?</h3>
  467.         <p>确定要删除这个模板吗?此操作无法撤销。</p>
  468.         <div class=”modal-actions”>
  469.           <button class=”modal-btn cancel” @click=”cancelDeleteTemplate”>取消</button>
  470.           <button class=”modal-btn confirm” @click=”confirmDeleteTemplate”>删除</button>
  471.         </div>
  472.       </div>
  473.     </div>
  474.     <!– Reset Data Confirmation Modal –>
  475.     <div class=”modal-overlay” v-if=”showResetConfirmModal” style=”z-index: 2100;”>
  476.       <div class=”modal-content”>
  477.         <h3>&#129529; 确认重置?</h3>
  478.         <p>确定要清空所有修改吗?<br>这将恢复到默认状态。此操作无法撤销!</p>
  479.         <div class=”modal-actions”>
  480.           <button class=”modal-btn cancel” @click=”showResetConfirmModal = false”>取消</button>
  481.           <button class=”modal-btn confirm” @click=”confirmReset”>重置</button>
  482.         </div>
  483.       </div>
  484.     </div>
  485.     <!– Load Template Confirmation Modal –>
  486.     <div class=”modal-overlay” v-if=”showLoadConfirmModal” style=”z-index: 2200;”>
  487.       <div class=”modal-content”>
  488.         <h3>&#128214; 确认加载?</h3>
  489.         <p v-if=”pendingLoadTemplate”>确定要加载模板 “<b>{{ pendingLoadTemplate.name }}</b>” 吗?<br>当前未保存的修改将会丢失。</p>
  490.         <div class=”modal-actions”>
  491.           <button class=”modal-btn cancel” @click=”showLoadConfirmModal = false”>取消</button>
  492.           <button class=”modal-btn confirm” @click=”confirmLoadTemplate”>加载</button>
  493.         </div>
  494.       </div>
  495.     </div>
  496.     <!– API Key Alert Modal –>
  497.     <div class=”modal-overlay” v-if=”showApiKeyAlertModal” style=”z-index: 2300;”>
  498.       <div class=”modal-content”>
  499.         <h3>&#9888;&#65039; 需要配置 API Key</h3>
  500.         <p>AI 功能需要配置阿里云 DashScope API Key 才能使用。</p>
  501.         <div class=”modal-actions”>
  502.           <button class=”modal-btn cancel” @click=”showApiKeyAlertModal = false”>取消</button>
  503.           <button class=”modal-btn confirm” @click=”showApiKeyAlertModal = false; showSettingsModal = true”>去配置</button>
  504.         </div>
  505.       </div>
  506.     </div>
  507.     <!– AI Generation Confirmation Modal –>
  508.     <div class=”modal-overlay” v-if=”showAIGenConfirmModal” style=”z-index: 2200;”>
  509.       <div class=”modal-content”>
  510.         <h3>AI 一键生成</h3>
  511.         <p>AI 将根据当前的课程信息自动生成试题。<br><b>注意:此操作可能会覆盖您已手动输入的内容。</b></p>
  512.         <div class=”modal-actions”>
  513.           <button class=”modal-btn cancel” @click=”showAIGenConfirmModal = false”>取消</button>
  514.           <button class=”modal-btn confirm” @click=”confirmGenerateExamPaper”>&#10024; 开始生成</button>
  515.         </div>
  516.       </div>
  517.     </div>
  518.     <!– Export Loading Overlay –>
  519.     <div class=”modal-overlay” v-if=”isExporting” style=”z-index: 3000; cursor: wait;”>
  520.       <div class=”modal-content” style=”max-width: 300px;”>
  521.         <h3>&#128424;&#65039; 正在导出…</h3>
  522.         <p>正在努力生成高清 PDF,<br>请稍候片刻…</p>
  523.         <div class=”loading-spinner”>&#9999;&#65039;</div>
  524.       </div>
  525.     </div>
  526.     <!– AI Components –>
  527.     <!– AI Components –>
  528.     <SettingsModal
  529.       v-if=”showSettingsModal”
  530.       :currentModelId=”currentModelId”
  531.       :show-model-selector=”true”
  532.       @change-model=”handleModelChange”
  533.       @close=”showSettingsModal = false”
  534.     />
  535.   </div>
  536. </template>
  537. <style scoped>
  538. .app-container {
  539.   padding: 20px;
  540. }
  541. .home-link {
  542.   position: fixed;
  543.   top: 20px;
  544.   left: 20px;
  545.   z-index: 100;
  546. }
  547. .home-link a {
  548.   text-decoration: none;
  549.   font-weight: bold;
  550.   color: #2c3e50;
  551.   background: white;
  552.   padding: 10px 15px;
  553.   border-radius: 20px;
  554.   border: 2px solid #2c3e50;
  555.   box-shadow: 2px 2px 0 #2c3e50;
  556.   transition: transform 0.1s;
  557. }
  558. .home-link a:hover {
  559.   transform: scale(1.05);
  560. }
  561. .ai-actions {
  562.   text-align: center;
  563.   margin-bottom: 20px;
  564. }
  565. .ai-actions {
  566.   text-align: center;
  567.   margin-bottom: 20px;
  568. }
  569. .ai-gen-btn {
  570.     background: white;
  571.     color: #2c3e50;
  572.     border: 3px solid #2c3e50;
  573.     padding: 12px 30px;
  574.     font-size: 1.2em;
  575.     border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
  576.     cursor: pointer;
  577.     box-shadow: 4px 4px 0 #2c3e50;
  578.     font-weight: bold;
  579.     font-family: inherit;
  580.     transition: all 0.2s;
  581. }
  582. .ai-gen-btn:hover:not(:disabled) {
  583.   transform: translate(-2px, -2px);
  584.   box-shadow: 6px 6px 0 #2c3e50;
  585.   background: #f3e5f5;
  586. }
  587. .ai-gen-btn:disabled {
  588.     background: #eee;
  589.     color: #999;
  590.     border-color: #999;
  591.     box-shadow: none;
  592.     cursor: wait;
  593. }
  594. .tag-plan {
  595.   background: #e1f5fe;
  596.   color: #039be5;
  597.   font-size: 0.8em;
  598.   padding: 2px 6px;
  599.   border-radius: 4px;
  600.   margin-right: 5px;
  601. }
  602. .tag-exam {
  603.   background: #fff3e0;
  604.   color: #fb8c00;
  605.   font-size: 0.8em;
  606.   padding: 2px 6px;
  607.   border-radius: 4px;
  608.   margin-right: 5px;
  609. }
  610. .loading {
  611.   text-align: center;
  612.   font-size: 1.5em;
  613.   margin-top: 100px;
  614.   color: #666;
  615. }
  616. /* Modal Styles Global */
  617. .modal-overlay {
  618.   position: fixed;
  619.   top: 0;
  620.   left: 0;
  621.   right: 0;
  622.   bottom: 0;
  623.   background: rgba(0,0,0,0.5);
  624.   display: flex;
  625.   align-items: center;
  626.   justify-content: center;
  627.   z-index: 2000;
  628.   backdrop-filter: blur(3px);
  629. }
  630. .modal-content {
  631.   background: #fdfbf7;
  632.   padding: 30px;
  633.   border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
  634.   border: 3px solid #2c3e50;
  635.   box-shadow: 10px 10px 0 rgba(0,0,0,0.2);
  636.   width: 90%;
  637.   max-width: 400px;
  638.   text-align: center;
  639.   font-family: ‘Architects Daughter’, cursive;
  640. }
  641. .modal-content h3 {
  642.   font-size: 1.5em;
  643.   margin-bottom: 20px;
  644.   border-bottom: 1px dashed #ccc;
  645.   padding-bottom: 10px;
  646. }
  647. .modal-input {
  648.   width: 80%;
  649.   padding: 10px;
  650.   margin-bottom: 20px;
  651.   font-size: 1.2em;
  652.   font-family: inherit;
  653.   border: 2px solid #2c3e50;
  654.   border-radius: 5px;
  655.   outline: none;
  656. }
  657. .modal-actions {
  658.   display: flex;
  659.   justify-content: center;
  660.   gap: 20px;
  661.   margin-top: 20px;
  662. }
  663. .modal-btn {
  664.   padding: 8px 20px;
  665.   border: 2px solid #2c3e50;
  666.   background: transparent;
  667.   font-family: inherit;
  668.   font-size: 1.1em;
  669.   cursor: pointer;
  670.   border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
  671.   transition: transform 0.1s;
  672. }
  673. .modal-btn:hover {
  674.   transform: scale(1.05);
  675. }
  676. .modal-btn.confirm {
  677.   background: #e74c3c;
  678.   color: white;
  679.   border-color: #e74c3c;
  680. }
  681. .modal-btn.cancel {
  682.   border-style: dashed;
  683. }
  684. /* Template List */
  685. .load-modal {
  686.   max-width: 500px;
  687. }
  688. .template-list {
  689.   max-height: 300px;
  690.   overflow-y: auto;
  691.   text-align: left;
  692. }
  693. .template-item {
  694.   display: flex;
  695.   justify-content: space-between;
  696.   align-items: center;
  697.   padding: 10px;
  698.   border-bottom: 1px solid #eee;
  699.   cursor: pointer;
  700.   transition: background 0.2s;
  701. }
  702. .template-item:hover {
  703.   background: rgba(0,0,0,0.05);
  704. }
  705. .template-info {
  706.   flex: 1;
  707. }
  708. .t-name {
  709.   font-weight: bold;
  710.   font-size: 1.1em;
  711. }
  712. .ai-chat-fab {
  713.   position: fixed;
  714.   bottom: 20px;
  715.   right: 20px;
  716.   background: #2c3e50;
  717.   color: white;
  718.   border: none;
  719.   border-radius: 30px;
  720.   padding: 12px 24px;
  721.   font-size: 1.1em;
  722.   font-weight: bold;
  723.   box-shadow: 0 4px 10px rgba(0,0,0,0.3);
  724.   cursor: pointer;
  725.   z-index: 900;
  726.   transition: transform 0.2s;
  727.   font-family: ‘Architects Daughter’, cursive;
  728.   border: 2px solid white;
  729. }
  730. .ai-chat-fab:hover {
  731.   transform: scale(1.05);
  732.   background: #34495e;
  733. }
  734. .t-date {
  735.   font-size: 0.8em;
  736.   color: #888;
  737. }
  738. .delete-template-btn {
  739.   background: transparent;
  740.   border: none;
  741.   color: #ccc;
  742.   font-size: 1.5em;
  743.   cursor: pointer;
  744.   padding: 0 10px;
  745. }
  746. .delete-template-btn:hover {
  747.   color: #c0392b;
  748. }
  749. .empty-list {
  750.   color: #999;
  751.   padding: 20px;
  752. }
  753. .loading-spinner {
  754.   font-size: 3em;
  755.   animation: writing 1s infinite alternate;
  756.   margin-top: 20px;
  757. }
  758. @keyframes writing {
  759.   from { transform: translateX(-20px) rotate(-10deg); }
  760.   to { transform: translateX(20px) rotate(10deg); }
  761. }
  762. </style>
 

 

付费下载
当前内容需要支付免费 元宝才能下载
VIP折扣
    折扣详情
  • 体验VIP会员

    免费

  • 月卡VIP会员

    免费

  • 年卡VIP会员

    免费

  • 永久VIP会员

    免费

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

本站所有资源来源于网络,仅限用于学习研究;无任何技术支持!不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除内容。如果您喜欢,请支持正版。如有侵权请邮件与我们联系处理。

豫唐智能教案在线生成平台V1.0.0
下一篇:

已经没有下一篇了!

常见问题
  • 网盘有时候会因为名字 关键词导致失效 大家可以给管理员提供失效信息,我们会给大家适当积分进行奖励 我们会第一时间进行补充修正 感谢大家的配合 让我们共同努力 打造良好的资源分享平台
查看详情

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务