浏览代码

优化考试工坊前端界面

FanHong 1 周之前
父节点
当前提交
c47afcc24d

+ 2 - 0
shudao-vue-frontend/index.html

@@ -6,6 +6,8 @@
     <link rel="icon" href="/favicon.ico">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
     <title>蜀道安全管理AI智能助手</title>
+    <!-- 引入 Material Symbols Outlined 字体图标 -->
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
     <!-- 在任何JS模块加载之前保存原始URL(用于票据认证) -->
     <script>
         window.__ORIGINAL_URL__ = window.location.href;

+ 158 - 0
shudao-vue-frontend/replace_layout2.py

@@ -0,0 +1,158 @@
+import re
+
+with open('src/views/ExamWorkshop.vue', 'r', encoding='utf-8') as f:
+    content = f.read()
+
+pattern = re.compile(r'(<!-- 考试工坊主界面 -->\s*<div v-if="!showExamDetail" class="exam-workshop-card">)(.*?)(^\s*<!-- 考试详情页 -->)', re.DOTALL | re.MULTILINE)
+
+new_content = """
+        <div v-if="!showExamDetail" class="flex-1 flex flex-row w-full h-full relative overflow-hidden" style="background-color: #f7f9fb; margin: -20px;">
+          <-5 中间主要工作区 -->
+          <div class="flex-1 flex flex-col h-full relative min-w-0">
+            <div class="flex-1 overflow-y-auto p-10 pb-32">
+              <div class="max-w-4xl mx-auto w-full space-y-8">
+                <-5 试卷名称 -->
+                <section class="space-y-3">
+                  <label class="block text-[15px] font-bold text-gray-800">试卷名称</label>
+                  <div class="relative">
+                    <input v-model="examName" class="w-full bg-white border border-gray-100 shadow-sm rounded-2xl px-5 py-4 text-[15px] focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none text-gray-800 placeholder-gray-400" maxlength="32" placeholder="请输入试卷名称..." type="text" :disabled="isGenerating"/>
+                    <span class="absolute right-5 bottom-4 text-xs text-gray-400">{{ examName?.length || 0 }}/32</span>
+                  </div>
+                </section>
+
+                <-5 出题依据内容 -->
+                <section class="space-y-3">
+                  <label class="block text-[15px] font-bold text-gray-800">出题依据内容</label>
+                  <textarea v-model="questionBasis" class="w-full h-48 bg-white border border-gray-100 shadow-sm rounded-2xl px-5 py-4 text-[15px] focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none resize-none text-gray-800 placeholder-gray-400 leading-relaxed" placeholder="在此输入知识点、章节或培训内容..." :disabled="isGenerating || selectedFile"></textarea>
+
+                  <-5 从PPT生成考题 -->
+                  <div class="w-full bg-white border border-gray-100 shadow-sm rounded-2xl p-5 flex items-center gap-5 cursor-pointer hover:shadow-md transition-all group mt-6" @click="!isGenerating && !selectedFile ? triggerFileUpload() : null">
+                    <div class="w-14 h-14 rounded-2xl bg-[#f3f4f6] flex items-center justify-center text-gray-500 group-hover:text-blue-600 transition-colors">
+                      <span class="material-symbols-outlined text-3xl">cloud_upload</span>
+                    </div>
+                    <div class="flex-1">
+                      <div class="flex items-center gap-2 mb-1">
+                        <h3 class="text-base font-bold text-gray-900">从PPT生成考题</h3>
+                      </div>
+                      <p class="text-[13px] text-gray-500">上传培训PPT,智能提取关键内容生成考题(单个文件可上传20M内)</p>
+                      <div v-if="selectedFile" class="mt-3 p-2.5 bg-blue-50/80 rounded-xl text-blue-600 text-sm flex justify-between items-center border border-blue-100">
+                        <span class="font-medium truncate mr-4">已上传: {{ selectedFile.name }}</span>
+                        <span @click.stop="removeSelectedFile" class="cursor-pointer text-red-400 hover:text-red-600 font-bold px-2 py-1 bg-red-50 hover:bg-red-100 rounded-lg transition-colors">×</span>
+                      </div>
+                    </div>
+                    <span class="material-symbols-outlined text-gray-300">chevron_right</span>
+                  </div>
+                </section>
+
+                <-5 题型配置 -->
+                <section class="space-y-4 pt-4">
+                  <div class="flex items-center justify-between w-full mb-6">
+                    <span class="text-[15px] font-bold text-gray-800">题型配置</span>
+                    <div class="flex items-center gap-3">
+                      <span class="text-[13px] text-gray-500">试卷总分</span>
+                      <div class="bg-white border border-gray-100 shadow-sm rounded-xl px-6 py-2.5 text-lg font-bold text-gray-900 min-w-[80px] text-center">
+                        {{ calculatedTotalScore }}
+                      </div>
+                    </div>
+                  </div>
+
+                  <div class="grid grid-cols-2 gap-5">
+                    <div v-for="(type, index) in questionTypes" :key="index" class="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 hover:shadow-md transition-shadow">
+                      <div class="flex justify-between items-center mb-8">
+                        <span class="text-[15px] font-bold text-gray-900">{{ type.name }}</span>
+                        <span class="text-[13px] font-medium bg-[#ebf3ff] text-blue-600 px-3.5 py-1.5 rounded-full">每题 {{ type.scorePerQuestion }} 分</span>
+                      </div>
+                      <div class="space-y-4">
+                        <div class="flex justify-between items-center text-[13px] font-bold mb-2">
+                          <span class="text-gray-500">数量</span>
+                          <span class="text-blue-600 text-base">{{ type.questionCount }}</span>
+                        </div>
+                        <div class="relative w-full h-1.5 bg-gray-100 rounded-full mt-2">
+                          <input type="range" v-model.number="type.questionCount" min="0" :max="type.max || 50" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer z-10" :disabled="isGenerating">
+                          <div class="absolute top-0 left-0 h-full bg-blue-600 rounded-full pointer-events-none" :style="{ width: (type.questionCount / (type.max || 50) * 100) + '%' }"></div>
+                          <div class="absolute top-1/2 -translate-y-1/2 w-4 h-4 bg-white border-[3px] border-blue-600 rounded-full shadow-sm pointer-events-none transition-all" :style="{ left: (type.questionCount / (type.max || 50) * 100) + '%', transform: 'translate(-50%, -50%)' }"></div>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </section>
+              </div>
+            </div>
+
+            <-5 底部悬浮操作栏 -->
+            <footer class="absolute bottom-0 left-0 w-full bg-white/95 backdrop-blur-xl border-t border-gray-100 px-8 py-5 flex items-center justify-between z-40">
+              <div class="flex items-center gap-4">
+                <button class="flex items-center gap-2.5 text-gray-500 hover:text-red-500 transition-colors px-2" @click="clearSettings" :disabled="isGenerating">
+                  <span class="material-symbols-outlined text-[20px]">delete</span>
+                  <span class="text-[14px] font-medium">清空当前配置</span>
+                </button>
+              </div>
+              
+              <button v-if="!isGenerating" class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-xl text-[15px] font-bold shadow-lg shadow-blue-600/20 flex items-center justify-center transition-all active:scale-[0.98]" @click="generateExam">
+                开始智能生成试卷
+              </button>
+              <button v-else class="bg-blue-400 text-white px-8 py-3 rounded-xl text-[15px] font-bold shadow-md flex items-center justify-center cursor-not-allowed">
+                <span class="flex items-center gap-2">
+                  <span class="material-symbols-outlined animate-spin text-lg">autorenew</span>
+                  生成中...
+                </span>
+              </button>
+            </footer>
+          </div>
+
+          <-5 右侧边栏 (实时预览) -->
+          <aside class="w-[280px] bg-[#f7f9fb] border-l border-gray-200 flex flex-col h-full flex-shrink-0 z-30">
+            <div class="p-7">
+              <h2 class="text-gray-900 font-bold text-[17px] mb-7">实时预览</h2>
+              
+              <div class="space-y-8">
+                <-5 试卷名称 -->
+                <div class="bg-white p-5 rounded-2xl shadow-sm border border-gray-100">
+                  <h3 class="text-[13px] font-bold text-gray-500 mb-3.5">试卷名称</h3>
+                  <p class="text-gray-800 font-medium text-[15px] italic leading-relaxed" :class="{'opacity-40': !examName}">{{ examName || '未命名试卷...' }}</p>
+                </div>
+                
+                <-5 结构大纲 -->
+                <div class="space-y-5">
+                  <h3 class="text-[13px] font-bold text-gray-500">结构大纲</h3>
+                  <ul class="space-y-5">
+                    <li v-for="(type, index) in questionTypes" :key="index" class="flex flex-col gap-1.5 group">
+                      <div class="flex items-center justify-between">
+                        <div class="flex items-center gap-2.5">
+                          <div class="w-1.5 h-1.5 rounded-full" :style="{ backgroundColor: ['#2563eb', '#60a5fa', '#93c5fd', '#dbeafe'][index % 4] }"></div>
+                          <span class="text-[14px] font-medium text-gray-800">{{ type.name }}</span>
+                        </div>
+                        <span class="text-[14px] font-bold text-gray-800">{{ type.questionCount }}题</span>
+                      </div>
+                      <span class="text-[12px] text-gray-400 ml-4">{{ type.questionCount * type.scorePerQuestion }} 分</span>
+                    </li>
+                  </ul>
+                </div>
+                
+                <-5 总分统计 -->
+                <div class="pt-8 border-t border-gray-200 space-y-4">
+                  <div class="flex justify-between items-center">
+                    <span class="text-[13px] text-gray-500 font-medium">配置总分</span>
+                    <span class="text-[15px] font-bold text-gray-900">{{ totalScore }}</span>
+                  </div>
+                  <div class="flex justify-between items-center">
+                    <span class="text-[13px] text-gray-500 font-medium">试卷总分</span>
+                    <span class="text-[15px] font-bold text-gray-900">{{ calculatedTotalScore }}</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </aside>
+        </div>
+"""
+
+replaced = pattern.sub(r'\1\n' + new_content + r'\n\3', content)
+
+# 隐藏原来的 work-header 并修改 work-content 的样式
+replaced = replaced.replace('<div class="work-header">', '<div class="work-header" v-if="showExamDetail">')
+replaced = replaced.replace('<div class="work-content" :class="{ \'exam-detail-mode\': showExamDetail }">', '<div class="work-content" :class="{ \'exam-detail-mode\': showExamDetail }" :style="showExamDetail ? {} : { padding: 0, height: \'100%\', display: \'flex\', flexDirection: \'column\' }">')
+
+with open('src/views/ExamWorkshop.vue', 'w', encoding='utf-8') as f:
+    f.write(replaced)
+
+print("Replacement complete.")

+ 5 - 6
shudao-vue-frontend/src/views/Chat.vue

@@ -58,15 +58,14 @@
     <!-- 右侧AI问答区域 -->
     <div class="main-chat" :class="{ 'sidebar-open': webSearchSidebarVisible }">
       <!-- 聊天头部 -->
-      <div class="chat-header">
-        <div class="question-title-card" v-if="currentQuestion">
+      <div class="chat-header" v-if="currentMode !== 'exam-workshop' && currentQuestion">
+        <div class="question-title-card">
           <h2>{{ currentQuestion }}</h2>
         </div>
-        <h2 v-else class="default-title">AI问答</h2>
       </div>
 
       <!-- 考试工坊内容区域 -->
-      <div v-if="currentMode === 'exam-workshop'" class="exam-workshop-wrapper">
+      <div v-if="currentMode === 'exam-workshop'" class="exam-workshop-wrapper" style="height: 100%; flex: 1;">
         <ExamWorkshop :hideSidebar="true" />
       </div>
 
@@ -420,7 +419,7 @@
       </div>
 
       <!-- 底部输入区域 -->
-      <div class="chat-input-section">
+      <div class="chat-input-section" v-show="currentMode !== 'exam-workshop'">
         <!-- 推荐问题 (移至输入框上方) -->
         <div v-if="!showChat && !selectedFile" class="recommended-questions">
           <div 
@@ -4998,7 +4997,7 @@ onActivated(async () => {
 
 .exam-workshop-wrapper {
   flex: 1;
-  overflow-y: auto;
+  overflow-y: hidden;
   position: relative;
   background: #f5f7fb;
 }

+ 623 - 234
shudao-vue-frontend/src/views/ExamWorkshop.vue

@@ -55,7 +55,7 @@
     <!-- 右侧工作区域 -->
     <div class="main-work" :style="{ background: showExamDetail ? 'transparent' : '#ebf3ff' }">
       <!-- 头部 -->
-      <div class="work-header">
+      <div class="work-header" v-if="showExamDetail">
         <h2>考试工坊</h2>
       </div>
 
@@ -67,237 +67,121 @@
           <p>正在加载历史记录...</p>
         </div>
         <!-- 考试工坊主界面 -->
-        <div v-if="!showExamDetail" class="exam-workshop-card">
-          <!-- 左侧配置区域 -->
-          <div class="config-section">
-            <!-- 1. 选择试卷类型 -->
-            <div class="config-item">
-              <div class="config-header">
-                <div class="step-number">1</div>
-                <h3>选择试卷类型</h3>
-        </div>
-              <div class="type-cards">
-                <div
-                  v-for="(type, key) in projectTypes"
-                  :key="key"
-                  :class="['type-card', { active: selectedProjectType === key }]"
-                  @click="(isGenerating || selectedFile) ? null : selectProjectType(key)"
-                  :style="{ cursor: (isGenerating || selectedFile) ? 'not-allowed' : 'pointer', opacity: (isGenerating || selectedFile) ? '0.5' : '1' }"
-                >
-                  <img :src="type.icon" :alt="type.name" class="type-icon" />
-                  <span>{{ type.name }}</span>
+        <div v-if="!showExamDetail" class="exam-workshop-card app-container">
+            <!-- 中间主操作区 -->
+            <main class="main-content">
+
+                <div class="form-group">
+                    <label class="form-label">试卷名称</label>
+                    <input type="text" class="form-control" v-model="examName" maxlength="32" placeholder="请输入试卷名称..." :disabled="isGenerating">
+                    <div class="char-count">{{ examName?.length || 0 }}/32</div>
                 </div>
-              </div>
-            </div>
 
-            <!-- 2. 选择生成方式 -->
-            <div class="config-item">
-              <div class="config-header">
-                <div class="step-number">2</div>
-                <h3>选择生成方式</h3>
-              </div>
-              <div class="generation-methods">
-                <div
-                  :class="['method-card', { active: selectedFunction === 'ai' }]"
-                  @click="(isGenerating || selectedFile) ? null : selectFunction('ai')"
-                  :style="{ cursor: (isGenerating || selectedFile) ? 'not-allowed' : 'pointer', opacity: (isGenerating || selectedFile) ? '0.5' : '1' }"
-                >
-                  <img
-                    :src="aiIcon"
-                    alt="智能生成试卷"
-                    class="method-icon"
-                  />
-                  <div class="method-content">
-                    <h4>智能生成试卷</h4>
-                    <p>基于AI技术,根据所选类型自动生成完整试卷</p>
-                  </div>
-                </div>
-                <div
-                  :class="['method-card', { active: selectedFunction === 'ppt' }]"
-                  @click="isGenerating ? null : (selectedFunction === 'ppt' && !selectedFile ? triggerFileUpload() : selectFunction('ppt'))"
-                  :style="{ cursor: isGenerating ? 'not-allowed' : 'pointer', opacity: isGenerating ? '0.5' : '1' }"
-                >
-                  <img
-                    :src="pptIcon"
-                    alt="从PPT生成考题"
-                    class="method-icon"
-                  />
-                  <div class="method-content">
-                    <h4>从PPT生成考题</h4>
-                    <p>上传培训PPT,智能提取关键内容生成考题(单个文件可上传20M内)</p>
+                <div class="form-group">
+                    <label class="form-label">出题依据内容</label>
+                    <textarea class="form-control" v-model="questionBasis" placeholder="在此输入知识点、章节或培训内容..." :disabled="isGenerating || selectedFile"></textarea>
                     
-                    <!-- PPT文件预览区域 -->
-                    <div v-if="selectedFunction === 'ppt' && selectedFile" class="ppt-file-preview">
-                      <div class="file-preview">
-                        <div class="file-icon">{{ selectedFile.icon }}</div>
-                        <div class="file-info">
-                          <div class="file-name">{{ selectedFile.name }}</div>
-                          <div class="file-size">{{ formatFileSize(selectedFile.size) }}</div>
+                    <div class="ppt-upload-section" @click="!isGenerating && !selectedFile ? triggerFileUpload() : null">
+                        <div class="ppt-upload-content">
+                            <div class="ppt-upload-icon-wrapper">
+                                <span class="material-symbols-outlined" style="font-size: 28px; color: #4b5563;">cloud_upload</span>
+                            </div>
+                            <div class="ppt-upload-text-wrapper">
+                                <div class="ppt-upload-title">从PPT生成考题</div>
+                                <div class="ppt-upload-hint">上传培训PPT,智能提取关键内容生成考题(单个文件可上传20M内)</div>
+                            </div>
+                        </div>
+                        <span class="material-symbols-outlined ppt-arrow">chevron_right</span>
+                        
+                        <div v-if="selectedFile" class="file-status-badge" @click.stop>
+                          <span class="file-name truncate">已上传: {{ selectedFile.name }}</span>
+                          <span @click.stop="removeSelectedFile" class="remove-btn">×</span>
                         </div>
-                        <button class="remove-file-btn" @click="removeSelectedFile">
-                          <span class="remove-icon">×</span>
-                        </button>
-                      </div>
                     </div>
-                  </div>
                 </div>
-              </div>
-            </div>
 
-            <!-- 3. 试卷配置 -->
-            <div class="config-item">
-              <div class="config-header">
-                <div class="step-number">3</div>
-                <h3>试卷配置</h3>
-              </div>
-              <div class="exam-config-container">
-                <!-- 左侧配置区域 -->
-                <div class="config-left">
-                  <div class="config-row">
-                    <div class="config-group">
-                      <label>试卷名称</label>
-                      <div class="input-wrapper">
-                        <input
-                          v-model="examName"
-                          type="text"
-                          placeholder="请输入试卷名称"
-                          class="config-input"
-                          maxlength="32"
-                          @input="validateExamName"
-                          :disabled="isGenerating || selectedFile"
-                        />
-                        <span class="char-count-inline" :class="{ 'warning': examName.length >= 18 }">
-                          {{ examName.length }}/32
-                        </span>
-                      </div>
-                    </div>
-                    <div class="config-group">
-                      <label>试卷总分</label>
-                      <div class="score-input">
-                        <input
-                          v-model="totalScore"
-                          type="number"
-                          class="config-input"
-                          min="1"
-                          max="1000"
-                          @input="validateTotalScore"
-                          :disabled="isGenerating || selectedFile"
-                        />
-                        <span class="unit">分</span>
-                      </div>
+                <!-- =============== 题型配置区域 开始 =============== -->
+                <div class="config-section">
+                    <div class="config-header">
+                        <h3>题型配置</h3>
+                        <div class="total-score">试卷总分 {{ calculatedTotalScore }}</div>
                     </div>
-                  </div>
 
-                  <!-- 题型选择与分数分配 -->
-                  <div class="section-title">题型选择与分数分配</div>
-                  <div class="question-types">
-                    <div
-                      class="question-type"
-                      v-for="(type, index) in questionTypes"
-                      :key="index"
-                    >
-                      <div class="type-row">
-                        <span class="type-name">{{ type.name }}</span>
-                        <div class="progress-bar">
-                          <div
-                            class="progress-fill"
-                            :style="{
-                              width:
-                                ((type.scorePerQuestion * type.questionCount) /
-                                  totalScore) *
-                                  100 +
-                                '%',
-                            }"
-                          ></div>
+                    <!-- 动态渲染各题型 -->
+                    <div class="question-types-grid">
+                        <div class="question-type-card" v-for="(type, index) in questionTypes" :key="index">
+                            <div class="question-type-header">
+                                <div class="question-type-title">{{ type.name }}</div>
+                                <div class="question-type-score">
+                                    每题 <input type="number" class="score-input" v-model.number="type.scorePerQuestion" min="1" max="100" :disabled="isGenerating"> 分
+                                </div>
+                            </div>
+                            <div class="slider-container">
+                                <span class="slider-label">数量</span>
+                                <input type="range" class="question-slider" v-model.number="type.questionCount" min="0" :max="type.max || 50" :disabled="isGenerating">
+                                <span class="question-count" style="text-align: right; min-width: 40px;">{{ type.questionCount }} 题</span>
+                            </div>
                         </div>
-                        <div class="score-config">
-                          <span>每题</span>
-                                                      <input
-                              v-model="type.scorePerQuestion"
-                              type="number"
-                              class="score-input-field"
-                              min="1"
-                              max="99"
-                              @input="validateScorePerQuestion(type)"
-                              :disabled="isGenerating || selectedFile"
-                            />
-                          <span>分</span>
-                          <span>一共</span>
-                                                      <input
-                              v-model="type.questionCount"
-                              type="number"
-                              class="count-input-field"
-                              min="1"
-                              max="99"
-                              @input="validateQuestionCount(type)"
-                              :disabled="isGenerating || selectedFile"
-                            />
-                          <span>题</span>
-                        </div>
-                      </div>
                     </div>
-                  </div>
+
+                    <div class="action-buttons">
+                        <button class="clear-btn" @click="clearSettings" :disabled="isGenerating">
+                            <span class="material-symbols-outlined" style="font-size: 18px;">delete</span>
+                            清空当前配置
+                        </button>
+                        <button class="generate-btn" @click="generateExam" :disabled="isGenerating">
+                            <span class="material-symbols-outlined" v-if="!isGenerating">auto_awesome</span>
+                            <span class="material-symbols-outlined animate-spin" v-else>autorenew</span>
+                            {{ isGenerating ? '生成中...' : '开始智能生成试卷' }}
+                        </button>
+                    </div>
                 </div>
+                <!-- =============== 题型配置区域 结束 =============== -->
+            </main>
 
-                <!-- 右侧预览面板 -->
-                <div class="preview-panel">
-                  <div class="preview-header">
-                    <img :src="previewIcon" alt="预览" class="preview-icon" />
-                    <h3>预览</h3>
-                  </div>
-                  <div class="preview-content">
-                    <h4 class="preview-title">{{ examName || "试卷名称" }}</h4>
-                    <div class="question-breakdown">
-                      <div
-                        class="breakdown-item"
-                        v-for="(type, index) in questionTypes"
-                        :key="index"
-                      >
-                        <div class="breakdown-row">
-                          <span class="breakdown-left"
-                            >{{ type.romanNumeral }}、{{ type.name }} (每题{{
-                              type.scorePerQuestion
-                            }}分,共{{
-                              type.scorePerQuestion * type.questionCount
-                            }}分)</span
-                          >
-                          <span class="breakdown-right">{{ type.questionCount }}题</span>
+            <!-- =============== 实时预览区域 开始 =============== -->
+            <aside class="preview-panel">
+                <div class="preview-header">
+                    <h3>实时预览</h3>
+                </div>
+
+                <div class="preview-name-card">
+                    <div class="preview-name-label">试卷名称</div>
+                    <div class="preview-title" :style="{ fontStyle: examName ? 'normal' : 'italic' }">{{ examName || '未命名试卷...' }}</div>
+                </div>
+
+                <div class="preview-section">
+                    <div class="preview-section-title">结构大纲</div>
+                    
+                    <div class="preview-item" v-for="(type, index) in questionTypes" :key="index">
+                        <div class="preview-item-top">
+                            <div class="preview-item-left">
+                                <div class="preview-dot" :style="{ backgroundColor: ['#2563eb', '#60a5fa', '#93c5fd', '#dbeafe'][index % 4] }"></div>
+                                <span class="preview-type-name">{{ type.name }}</span>
+                            </div>
+                            <span class="preview-type-count">{{ type.questionCount }}题</span>
+                        </div>
+                        <div class="preview-item-bottom">
+                            <span class="preview-type-score">{{ type.questionCount * type.scorePerQuestion }} 分</span>
                         </div>
-                      </div>
                     </div>
-                    <div class="divider"></div>
-                    <div class="calculated-score-row">
-                      <span class="calculated-label">配置总分</span>
-                      <span class="calculated-value">{{ calculatedTotalScore }}分</span>
+                </div>
+
+                <div class="preview-footer">
+                    <div class="preview-total">
+                        <span>配置总分</span>
+                        <span class="preview-total-score" style="color: #000000; font-size: 24px;">{{ totalScore }}</span>
                     </div>
-                    <div class="total-score-row">
-                      <span class="total-label">试卷总分</span>
-                      <span class="total-value">{{ totalScore }}分</span>
+                    <div class="preview-total" style="margin-top: 20px; font-size: 20px; color: #000000;">
+                        <span>试卷总分</span>
+                        <span style="color: var(--primary-color); font-size: 24px;">{{ calculatedTotalScore }}</span>
                     </div>
-                  </div>
                 </div>
-              </div>
-
-              <!-- 底部操作按钮 -->
-              <div class="bottom-actions">
-                <button class="clear-btn" @click="clearSettings" :disabled="isGenerating || selectedFile">
-                  <img :src="clearIcon" alt="一键清除" class="clear-icon" />
-                </button>
-                <button class="generate-btn" @click="generateExam" :disabled="isGenerating">
-                                      <img v-if="!isGenerating" :src="generateIcon" alt="生成试卷" class="generate-icon" />
-                  <span v-else class="generating-text">
-                    生成中<span class="loading-dots">
-                      <span class="dot"></span>
-                      <span class="dot"></span>
-                      <span class="dot"></span>
-                    </span>
-                  </span>
-                </button>
-              </div>
-            </div>
-          </div>
+            </aside>
+            <!-- =============== 实时预览区域 结束 =============== -->
         </div>
 
+
         <!-- 考试详情页 -->
         <div v-if="showExamDetail" class="exam-detail-card">
           <!-- 详情页头部 -->
@@ -900,10 +784,10 @@ const projectTypes = {
 
 // 题型配置
 const questionTypes = ref([
-  { name: "单选题", scorePerQuestion: 5, questionCount: 5, romanNumeral: "一" },
-  { name: "判断题", scorePerQuestion: 3, questionCount: 5, romanNumeral: "二" },
-  { name: "多选题", scorePerQuestion: 8, questionCount: 5, romanNumeral: "三" },
-  { name: "简答题", scorePerQuestion: 10, questionCount: 2, romanNumeral: "四" },
+  { name: "单选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "一" },
+  { name: "判断题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "二" },
+  { name: "多选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "三" },
+  { name: "简答题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "四" },
 ]);
 
 // 保存初始配置
@@ -1002,10 +886,10 @@ const createNewChat = async () => {
   } else {
     // 如果没有初始配置,使用默认配置
     questionTypes.value = [
-      { name: "单选题", scorePerQuestion: 2, questionCount: 8, romanNumeral: "一" },
-      { name: "判断题", scorePerQuestion: 2, questionCount: 5, romanNumeral: "二" },
-      { name: "多选题", scorePerQuestion: 3, questionCount: 5, romanNumeral: "三" },
-      { name: "简答题", scorePerQuestion: 10, questionCount: 2, romanNumeral: "四" },
+      { name: "单选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "一" },
+      { name: "判断题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "二" },
+      { name: "多选题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "三" },
+      { name: "简答题", scorePerQuestion: 0, questionCount: 0, romanNumeral: "四" },
     ];
   }
   
@@ -1118,13 +1002,12 @@ const clearSettings = () => {
   // 根据当前选择的工程类型设置试卷名称
   const projectTypeName = projectTypes[selectedProjectType.value].name;
   examName.value = `${projectTypeName}工程施工技术考核`;
-  totalScore.value = 100;
-  questionTypes.value = [
-    { name: "单选题", scorePerQuestion: 2, questionCount: 8, romanNumeral: "一" },
-    { name: "判断题", scorePerQuestion: 2, questionCount: 5, romanNumeral: "二" },
-    { name: "多选题", scorePerQuestion: 3, questionCount: 5, romanNumeral: "三" },
-    { name: "简答题", scorePerQuestion: 10, questionCount: 2, romanNumeral: "四" },
-  ];
+  totalScore.value = 0; // 清空时配置总分也应该为 0
+  // 保留原数组引用,更新每个对象的属性,避免破坏 Vue 3 响应式绑定
+  questionTypes.value.forEach(type => {
+    type.scorePerQuestion = 0;
+    type.questionCount = 0;
+  });
   console.log("清除设置");
 };
 
@@ -3663,11 +3546,12 @@ onUnmounted(() => {
 /* 工作内容区域 */
 .work-content {
   flex: 1;
-  padding: 22px;
+  padding: 0;
   // overflow-y: auto;
   display: flex;
   flex-direction: column;
-  align-items: center;
+  align-items: stretch;
+  height: 100%;
   
   /* 隐藏滚动条样式 */
   &::-webkit-scrollbar {
@@ -3684,14 +3568,519 @@ onUnmounted(() => {
   }
 }
 
+.app-container {
+    --primary-color: #0d6efd;
+    --danger-color: #dc3545;
+    --warning-color: #ffc107;
+    --border-color: #dee2e6;
+    --bg-light: #f8f9fa;
+    --text-dark: #212529;
+    --text-muted: #6c757d;
+
+    display: flex;
+    height: 100%;
+    width: 100%;
+    padding: 0 !important;
+    background-color: #f5f5f5;
+    overflow: hidden;
+    margin: 0 !important;
+    max-width: 100% !important;
+    border-radius: 0 !important;
+    box-shadow: none !important;
+
+    /* 中间主操作区 */
+    .main-content {
+        flex: 1;
+        padding: 20px;
+        overflow-y: hidden;
+        background: white;
+        scrollbar-width: none; /* Firefox */
+        -ms-overflow-style: none; /* IE and Edge */
+    }
+    .main-content::-webkit-scrollbar {
+        display: none; /* Chrome, Safari and Opera */
+    }
+
+    .form-group {
+        margin-bottom: 12px;
+        max-width: 1150px; /* 限制输入框模块的最大宽度 */
+        margin-left: auto;
+        margin-right: auto; /* 使其在工作区居中 */
+    }
+
+    .form-label {
+        font-weight: 600;
+        margin-bottom: 6px;
+        display: block;
+        font-size: 14px;
+    }
+
+    .form-control {
+        width: 100%;
+        border: 1px solid rgba(0, 0, 0, 0.06); /* 统一边框 */
+        border-radius: 12px; /* 统一圆角 */
+        padding: 12px 16px; /* 稍微增加内边距让它看起来更像卡片 */
+        font-size: 14px;
+        transition: all 0.3s;
+        box-sizing: border-box;
+        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 统一阴影 */
+    }
+
+    .form-control:focus {
+        outline: none;
+        border-color: var(--primary-color);
+        box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.1);
+    }
+
+    textarea.form-control {
+        resize: none;
+        height: 250px;
+    }
+
+    .char-count {
+        text-align: right;
+        font-size: 12px;
+        color: var(--text-muted);
+        margin-top: 4px;
+    }
+
+    /* PPT上传区域 */
+    .ppt-upload-section {
+        background: white;
+        border: 1px solid rgba(0, 0, 0, 0.06); /* 统一边框 */
+        border-radius: 12px; /* 统一圆角 */
+        padding: 16px 20px;
+        margin-top: 40px;
+        cursor: pointer;
+        transition: all 0.3s;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 统一阴影 */
+        position: relative;
+    }
+
+    .ppt-upload-section:hover {
+        border-color: var(--primary-color);
+        box-shadow: 0 8px 24px rgba(13, 110, 253, 0.12); /* 悬浮时加深发光阴影 */
+    }
+
+    .ppt-upload-content {
+        display: flex;
+        align-items: center;
+        gap: 16px;
+    }
+
+    .ppt-upload-icon-wrapper {
+        width: 48px;
+        height: 48px;
+        background: #f3f4f6;
+        border-radius: 12px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        flex-shrink: 0;
+    }
+
+    .ppt-upload-text-wrapper {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+    }
+
+    .ppt-upload-title {
+        font-size: 15px;
+        font-weight: 600;
+        color: #1f2937;
+        margin-bottom: 4px;
+    }
+
+    .ppt-upload-hint {
+        font-size: 13px;
+        color: #6b7280;
+    }
+
+    .ppt-arrow {
+        color: #9ca3af;
+        font-size: 24px;
+        transition: transform 0.3s;
+    }
+    
+    .ppt-upload-section:hover .ppt-arrow {
+        color: var(--primary-color);
+        transform: translateX(2px);
+    }
+
+    .file-status-badge {
+        position: absolute;
+        bottom: -40px;
+        left: 0;
+        background: #ebf3ff;
+        color: var(--primary-color);
+        padding: 8px 16px;
+        border-radius: 8px;
+        font-size: 13px;
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        border: 1px solid rgba(13, 110, 253, 0.1);
+        max-width: 300px;
+    }
+
+    .file-name {
+        font-weight: 500;
+    }
+
+    .remove-btn {
+        color: #ef4444;
+        font-size: 16px;
+        font-weight: bold;
+        cursor: pointer;
+        padding: 0 4px;
+    }
+
+    .remove-btn:hover {
+        color: #b91c1c;
+    }
+
+    /* =============== 题型配置区域 样式开始 =============== */
+    .config-section {
+        margin-top: 16px;
+    }
+
+    .config-header {
+        display: flex;
+        justify-content: center; /* 改为靠左对齐,而不是两端对齐 */
+        align-items: center;
+        gap: 960px; /* 控制“题型配置”和“试卷总分”之间的固定间距 */
+        margin-bottom: 6px;
+    }
+
+    .config-header h3 {
+        font-size: 18px;
+        font-weight: 600;
+    }
+
+    .total-score {
+        background: var(--bg-light);
+        padding: 8px 16px;
+        border-radius: 20px;
+        font-size: 14px;
+        font-weight: 600;
+        color: var(--primary-color);
+    }
+
+    .question-types-grid {
+        display: grid;
+        /* 为了减小卡片宽度,我们不再让它们自动拉伸占满,而是指定最大宽度并居中,或者留出更大的列间距 */
+        grid-template-columns: repeat(2, minmax(0, 500px));
+        justify-content: center; /* 让网格居中,而不是两端拉伸 */
+        gap: 20px 150px; /* 行间距(高度)20px,列间距(宽度)40px */
+        margin-bottom: 12px;
+    }
+
+    .question-type-card {
+        background: white; /* 改为白色背景 */
+        border-radius: 12px;
+        padding: 16px 20px; /* 根据截图稍微调大内边距以容纳阴影内容 */
+        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 加深阴影 */
+        border: 1px solid rgba(0, 0, 0, 0.06); /* 稍微加深边框线 */
+    }
+
+    .question-type-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 20px; /* 稍微增加与滑动条的间距 */
+    }
+
+    .question-type-title {
+        font-weight: 600;
+        font-size: 16px; /* 字体稍微调大 */
+        color: #1f2937;
+    }
+
+    .question-type-score {
+        font-size: 13px;
+        color: #3b82f6; /* 改变字体颜色为蓝色系 */
+        background: #eff6ff; /* 添加淡蓝色背景 */
+        padding: 4px 12px;
+        border-radius: 20px; /* 胶囊形状 */
+        display: flex;
+        align-items: center;
+        gap: 6px;
+    }
+
+    .score-input {
+        width: 32px;
+        text-align: center;
+        border: none; /* 移除输入框边框 */
+        border-radius: 4px;
+        padding: 0;
+        font-size: 14px;
+        font-weight: 600;
+        color: #2563eb; /* 加深数字颜色 */
+        background: transparent; /* 背景透明,融入胶囊 */
+        transition: all 0.3s;
+        -webkit-appearance: textfield;
+        -moz-appearance: textfield;
+        appearance: textfield;
+    }
+
+    .score-input::-webkit-outer-spin-button,
+    .score-input::-webkit-inner-spin-button {
+        -webkit-appearance: none;
+        margin: 0;
+    }
+
+    .score-input:focus {
+        outline: none;
+        background: white; /* 聚焦时背景变白 */
+        box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
+    }
+
+    .slider-container {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+    }
+
+    .slider-label {
+        font-size: 14px;
+        color: #4b5563;
+        font-weight: 500;
+        min-width: 40px;
+    }
+
+    .question-slider {
+        flex: 1;
+        height: 6px;
+        -webkit-appearance: none;
+        appearance: none;
+        background: #e5e7eb; /* 滑动条底色调浅 */
+        border-radius: 3px;
+        outline: none;
+    }
+
+    .question-slider::-webkit-slider-thumb {
+        -webkit-appearance: none;
+        appearance: none;
+        width: 18px; /* 滑块调大一点 */
+        height: 18px;
+        background: #2563eb; /* 蓝色滑块 */
+        border: 2px solid white; /* 添加白色边框 */
+        box-shadow: 0 1px 3px rgba(0,0,0,0.1); /* 滑块阴影 */
+        border-radius: 50%;
+        cursor: pointer;
+        transition: all 0.3s;
+    }
+
+    .question-slider::-webkit-slider-thumb:hover {
+        background: #1d4ed8;
+        transform: scale(1.1);
+    }
+
+    .question-count {
+        font-weight: bold; /* 数字加粗 */
+        font-size: 15px;
+        color: #1f2937;
+        min-width: 40px;
+        text-align: right;
+    }
+
+    .action-buttons {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-top: 16px;
+        padding-top: 16px;
+        border-top: 1px solid var(--border-color);
+    }
+
+    .clear-btn {
+        background: white;
+        border: 1px solid rgba(0, 0, 0, 0.06); /* 统一边框 */
+        border-radius: 8px; /* 添加圆角 */
+        color: var(--text-muted);
+        font-size: 14px;
+        cursor: pointer;
+        padding: 8px 16px;
+        transition: all 0.3s;
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 统一阴影 */
+    }
+
+    .clear-btn:hover {
+        color: var(--danger-color);
+        border-color: rgba(220, 53, 69, 0.2); /* 悬浮时边框微红 */
+        box-shadow: 0 6px 20px rgba(220, 53, 69, 0.1); /* 悬浮时带红色的发光阴影 */
+    }
+
+    .generate-btn {
+        background: var(--primary-color);
+        color: white;
+        border: none;
+        padding: 10px 24px;
+        border-radius: 8px;
+        font-size: 14px;
+        font-weight: 600;
+        cursor: pointer;
+        transition: all 0.3s;
+        display: flex;
+        align-items: center;
+        gap: 8px;
+    }
+
+    .generate-btn:hover:not(:disabled) {
+        background: #0b5ed7;
+        transform: translateY(-2px);
+        box-shadow: 0 4px 12px rgba(13, 110, 253, 0.3);
+    }
+    
+    .generate-btn:disabled {
+        background: #93c5fd; /* 浅蓝色背景 */
+        cursor: not-allowed;
+        box-shadow: 0 4px 12px rgba(147, 197, 253, 0.4); /* 浅蓝色阴影 */
+        opacity: 0.9;
+    }
+    /* =============== 题型配置区域 样式结束 =============== */
+
+    /* =============== 实时预览区域 样式开始 =============== */
+    .preview-panel {
+        width: 320px;
+        background: #f7f9fb; /* 匹配截图背景 */
+        border-left: 1px solid var(--border-color);
+        padding: 24px;
+        overflow-y: hidden;
+        flex-shrink: 0;
+        scrollbar-width: none;
+        -ms-overflow-style: none;
+    }
+    .preview-panel::-webkit-scrollbar {
+        display: none;
+    }
+
+    .preview-header {
+        margin-bottom: 24px;
+    }
+
+    .preview-header h3 {
+        font-size: 18px;
+        font-weight: bold;
+        color: #1f2937;
+        margin: 0;
+    }
+
+    .preview-name-card {
+        background: white;
+        border-radius: 16px;
+        padding: 16px 20px;
+        margin-bottom: 24px;
+        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); /* 加深阴影 */
+        border: 1px solid rgba(0, 0, 0, 0.06); /* 稍微加深边框线 */
+    }
+
+    .preview-name-label {
+        font-size: 13px;
+        font-weight: 600;
+        color: #4b5563;
+        margin-bottom: 12px;
+    }
+
+    .preview-title {
+        font-size: 15px;
+        color: #9ca3af;
+        line-height: 1.4;
+    }
+
+    .preview-section {
+        margin-bottom: 24px;
+    }
+
+    .preview-section-title {
+        font-size: 14px;
+        font-weight: bold;
+        color: #4b5563;
+        margin-bottom: 16px;
+    }
+
+    .preview-item {
+        margin-bottom: 16px;
+        display: flex;
+        flex-direction: column;
+        gap: 4px;
+    }
+
+    .preview-item-top {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+    }
+
+    .preview-item-left {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+    }
+
+    .preview-dot {
+        width: 6px;
+        height: 6px;
+        border-radius: 50%;
+    }
+
+    .preview-type-name {
+        font-size: 14px;
+        color: #1f2937;
+    }
+
+    .preview-type-count {
+        font-size: 14px;
+        font-weight: 600;
+        color: #1f2937;
+    }
+
+    .preview-item-bottom {
+        padding-left: 14px; /* Align with text (6px dot + 8px gap) */
+    }
+
+    .preview-type-score {
+        font-size: 12px;
+        color: #9ca3af;
+    }
+
+    .preview-footer {
+        margin-top: 24px;
+        padding-top: 20px;
+        border-top: 1px solid #e5e7eb;
+    }
+
+    .preview-total {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        font-size: 15px;
+        font-weight: 600;
+        color: #000000;
+        padding: 0 10px; /* 左右各加16px的内边距,使文字向中间靠拢 */
+    }
+
+    .preview-total-score {
+        font-weight: bold;
+    }
+    /* =============== 实时预览区域 样式结束 =============== */
+}
+
   .exam-workshop-card {
     background: white;
     width: 100%;
-    height: 960px;
-    padding: 32px 32px 14px 32px;
-    border-radius: 16px;
-    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
-    max-width: 1528px;
+    height: 100%;
+    padding: 0;
+    border-radius: 0;
+    box-shadow: none;
+    max-width: 100%;
 
     .config-section {
       flex: 1;

+ 0 - 0
shudao-vue-frontend/src/views/ExamWorkshop.vue.new