Forráskód Böngészése

修改时区等问题

yemeishu 3 hete
szülő
commit
aaad039fef

+ 34 - 0
app/Filament/Pages/IntelligentExamGeneration.php

@@ -61,6 +61,16 @@ class IntelligentExamGeneration extends Page
     public array $generatedQuestions = [];
     public ?string $generatedPaperId = null;
 
+    #[Computed]
+    public function canGenerate(): bool
+    {
+        return !$this->isGenerating
+            && $this->totalQuestions >= 6
+            && !empty($this->selectedTeacherId)
+            && !empty($this->selectedStudentId)
+            && count($this->selectedKpCodes) > 0;
+    }
+
     public function mount(): void
     {
         $this->selectedTeacherId = request()->query('teacher_id', $this->selectedTeacherId);
@@ -404,6 +414,24 @@ class IntelligentExamGeneration extends Page
             $this->totalQuestions = 6;
         }
 
+        if (empty($this->selectedTeacherId) || empty($this->selectedStudentId)) {
+            Notification::make()
+                ->title('请选择教师与学生')
+                ->body('请选择出卷对应的教师与学生后再生成试卷,确保个性化规则生效。')
+                ->danger()
+                ->send();
+            return;
+        }
+
+        if (empty($this->selectedKpCodes)) {
+            Notification::make()
+                ->title('请选择知识点')
+                ->body('请至少选择 1 个知识点后再生成试卷。可勾选学生薄弱点或手动选择知识点。')
+                ->danger()
+                ->send();
+            return;
+        }
+
         // 自动生成试卷名称
         if (empty($this->paperName)) {
             $studentName = '学生' . ($this->selectedStudentId ?? '未选择');
@@ -626,6 +654,9 @@ Cache::put('generated_exam_' . $paperId, $examData, now()->addHour());
      */
     protected function batchGenerateQuestions(int $count)
     {
+        // 增加PHP脚本执行时间到120秒,给足够时间启动异步任务和等待完成
+        set_time_limit(120);
+
         $questionBankService = app(QuestionBankService::class);
         $generatedTasks = [];
 
@@ -963,6 +994,9 @@ Cache::put('generated_exam_' . $paperId, $examData, now()->addHour());
             return;
         }
 
+        // 增加PHP脚本执行时间到120秒,给足够时间启动异步任务
+        set_time_limit(120);
+
         \Illuminate\Support\Facades\Log::info("开始生成缺失题型题目", ['missing_types' => $missingTypes]);
 
         // 为每个缺失题型生成3-5道题

+ 3 - 0
app/Filament/Pages/QuestionGeneration.php

@@ -147,6 +147,9 @@ class QuestionGeneration extends Page
         $this->currentTaskId = null;
 
         try {
+            // 增加PHP脚本执行时间到120秒,给足够时间启动异步任务
+            set_time_limit(120);
+
             $service = app(QuestionBankService::class);
             $callbackUrl = route('api.questions.callback');
 

+ 3 - 1
app/Services/QuestionBankService.php

@@ -167,7 +167,9 @@ class QuestionBankService
                 $params['callback_url'] = $callbackUrl;
             }
 
-            $response = Http::timeout(10)
+            // 增加超时时间到60秒,确保有足够时间启动异步任务
+            // 注意:API是异步的,只需等待任务启动(1-2秒),不需要等待AI生成完成
+            $response = Http::timeout(60)
                 ->post($this->baseUrl . '/generate-intelligent-questions', $params);
 
             if ($response->successful()) {

+ 3 - 0
public/index.php

@@ -1,5 +1,8 @@
 <?php
 
+// 设置PHP默认时区为上海时间
+date_default_timezone_set('Asia/Shanghai');
+
 use Illuminate\Foundation\Application;
 use Illuminate\Http\Request;
 

+ 46 - 11
public/js/knowledge-mindmap-graph.js

@@ -23,6 +23,8 @@ class KnowledgeMindmapGraph {
         this.clickTimer = null;
         this.clickDelay = 220;
         this.focusListener = null;
+        this.showEdges = options.showEdges ?? true;
+        this.showRelationEdges = options.showRelationEdges ?? true;
 
         this.setMasteryData(options.masteryData || {});
     }
@@ -63,7 +65,7 @@ class KnowledgeMindmapGraph {
         this.logMasteryCoverage();
         this.stats = {
             nodes: this.countNodes(this.treeData),
-            extraEdges: this.relationEdges.length,
+            extraEdges: this.showRelationEdges ? this.relationEdges.length : 0,
         };
     }
 
@@ -269,10 +271,16 @@ class KnowledgeMindmapGraph {
             },
             defaultEdge: {
                 type: 'cubic-horizontal',
-                style: {
-                    stroke: '#cbd5e1',
-                    lineWidth: 2,
-                },
+                style: this.showEdges
+                    ? {
+                          stroke: '#cbd5e1',
+                          lineWidth: 2,
+                      }
+                    : {
+                          stroke: 'transparent',
+                          lineWidth: 0,
+                          opacity: 0,
+                      },
             },
             nodeStateStyles: {
                 hover: { shadowColor: '#38bdf8', shadowBlur: 24 },
@@ -308,12 +316,19 @@ class KnowledgeMindmapGraph {
         window.focusMindmapNode = (id) => this.focusNodeById(id);
 
         // 边提示
-        this.bindEdgeTooltip();
+        if (this.showEdges) {
+            this.bindEdgeTooltip();
+        }
 
         this.applyNodeStates();
-        this.startEdgeFlows();
+        if (this.showEdges) {
+            this.startEdgeFlows();
+        }
         this.focusOnLowestMastery();
         this.repaintNodes();
+        if (!this.showEdges) {
+            this.hideAllEdges();
+        }
 
         // 折叠/展开或重新布局后重新挂载关联线
         this.graph.on('afterlayout', () => {
@@ -350,7 +365,13 @@ class KnowledgeMindmapGraph {
     }
 
     drawRelationEdges() {
-        if (!this.graph || !this.relationEdges.length) return;
+        if (
+            !this.graph ||
+            !this.relationEdges.length ||
+            !this.showRelationEdges ||
+            !this.showEdges
+        )
+            return;
         this.relationEdges.forEach((edge) => {
             // 尝试将隐藏节点映射到可见的父节点,避免关联线丢失
             let sourceId = edge.source;
@@ -413,7 +434,7 @@ class KnowledgeMindmapGraph {
     }
 
     startEdgeFlows() {
-        if (!this.graph) return;
+        if (!this.graph || !this.showEdges) return;
         const nodeMap = new Map(
             this.graph
                 .getNodes()
@@ -535,6 +556,7 @@ class KnowledgeMindmapGraph {
     }
 
     highlightNeighbors(nodeId) {
+        if (!this.showEdges) return;
         const connected = new Set([nodeId]);
         this.graph.getEdges().forEach((edge) => {
             const model = edge.getModel();
@@ -560,6 +582,7 @@ class KnowledgeMindmapGraph {
     }
 
     clearNeighborHighlight() {
+        if (!this.showEdges) return;
         this.graph.getEdges().forEach((edge) => {
             this.graph.clearItemStates(edge, ['hover', 'crosshover']);
         });
@@ -727,14 +750,18 @@ class KnowledgeMindmapGraph {
         this.clearRelationEdges();
         this.drawRelationEdges();
         this.applyNodeStates();
-        this.startEdgeFlows();
+        if (!this.showEdges) {
+            this.hideAllEdges();
+        } else {
+            this.startEdgeFlows();
+        }
         this.focusOnLowestMastery();
         this.graph.paint();
         this.setupFocusListener();
     }
 
     bindEdgeTooltip() {
-        if (!this.graph) return;
+        if (!this.graph || !this.showEdges) return;
         const tooltipEl = document.createElement('div');
         tooltipEl.style.position = 'fixed';
         tooltipEl.style.pointerEvents = 'none';
@@ -777,6 +804,7 @@ class KnowledgeMindmapGraph {
     }
 
     redrawRelationEdges() {
+        if (!this.showRelationEdges || !this.showEdges) return;
         this.clearRelationEdges();
         this.drawRelationEdges();
     }
@@ -1059,6 +1087,13 @@ class KnowledgeMindmapGraph {
         );
     }
 
+    hideAllEdges() {
+        if (!this.graph) return;
+        this.graph.getEdges().forEach((edge) => {
+            this.graph.hideItem(edge);
+        });
+    }
+
 }
 
 // 定义KnowledgeMindmapGraph类,确保G6已加载

+ 41 - 4
public/js/math-formula-processor.js

@@ -15,9 +15,19 @@ window.MathFormulaProcessor = {
             return content;
         }
 
+        // 0. 先清理重复分隔符(如 $$$ -> $$),该步始终执行
+        content = this.cleanupDuplicateMarkers(content);
+
+        if (!this.shouldPreprocess(content)) {
+            return content;
+        }
+
         // 1. 标准化数学公式分隔符
         content = this.standardizeDelimiters(content);
 
+        // 1.5 清理重复分隔符(如 $$$ -> $ 或 $$)
+        content = this.cleanupDuplicateMarkers(content);
+
         // 2. 转义特殊字符
         content = this.escapeSpecialChars(content);
 
@@ -27,6 +37,30 @@ window.MathFormulaProcessor = {
         return content;
     },
 
+    /**
+     * 是否需要预处理(平衡的 $…$ 且无占位词时跳过,避免过度修改)
+     */
+    shouldPreprocess: function(content) {
+        // 已有占位/异常字符则需要处理
+        if (content.includes('\\text{空') || content.includes('题目内容')) {
+            return true;
+        }
+        // 连续三个及以上 $ 需要清理
+        if (/\${3,}/.test(content)) {
+            return true;
+        }
+        // 统计 $ 个数,偶数且>0 时视为平衡
+        const dollarCount = (content.match(/\$/g) || []).length;
+        if (dollarCount > 0 && dollarCount % 2 === 0) {
+            return false;
+        }
+        // 含有 \(\ 或 \[\ 等特殊定界符时仍可处理
+        if (content.includes('\\(') || content.includes('\\[')) {
+            return true;
+        }
+        return true;
+    },
+
     /**
      * 标准化数学公式分隔符
      */
@@ -132,9 +166,9 @@ window.MathFormulaProcessor = {
             content += '$';
         }
 
-        // 2. 修复空的数学公式
-        content = content.replace(/\$\s*\$/g, ' $\\text{空}$ ');
-        content = content.replace(/\$\$\s*\$\$/g, '$$\\text{空}$$$');
+        // 2. 清理空的数学公式(不再插入“空”占位,直接移除)
+        content = content.replace(/\$\s*\$/g, ' ');
+        content = content.replace(/\$\$\s*\$\$/g, ' ');
 
         return content;
     },
@@ -147,6 +181,9 @@ window.MathFormulaProcessor = {
         content = content.replace(/\$\$\$\$\$/g, '$$');
         content = content.replace(/\$\$\$/g, '$');
 
+        // 处理起始为 "$$...$" 的不匹配情况,改为单行公式 "$...$"
+        content = content.replace(/^\$\$(.+)\$/s, (_, inner) => `$${inner}$`);
+
         return content;
     }
-};
+};

+ 64 - 67
resources/js/knowledge-mindmap-graph.js

@@ -23,6 +23,8 @@ class KnowledgeMindmapGraph {
         this.clickTimer = null;
         this.clickDelay = 220;
         this.focusListener = null;
+        this.showEdges = options.showEdges ?? true;
+        this.showRelationEdges = options.showRelationEdges ?? true;
 
         this.setMasteryData(options.masteryData || {});
     }
@@ -63,7 +65,7 @@ class KnowledgeMindmapGraph {
         this.logMasteryCoverage();
         this.stats = {
             nodes: this.countNodes(this.treeData),
-            extraEdges: this.relationEdges.length,
+            extraEdges: this.showRelationEdges ? this.relationEdges.length : 0,
         };
     }
 
@@ -186,16 +188,11 @@ class KnowledgeMindmapGraph {
     normalizeEdges(rawEdges) {
         const seen = new Set();
         const normalized = [];
-        const neutral = {
-            stroke: '#cbd5e1',
-            lineWidth: 2,
-            lineDash: [6, 6],
-        };
         const styleMap = {
-            prerequisite: { ...neutral, label: '前置' },
-            successor: { ...neutral, label: '后继' },
-            crosslink: { ...neutral, label: '跨联' },
-            sibling: { ...neutral, label: '同级' },
+            prerequisite: { stroke: '#8C8986FF', lineDash: [8, 6], lineWidth: 1, label: '前置' },
+            successor: { stroke: '#8C8986FF', lineDash: [8, 6], lineWidth: 1, label: '后继' },
+            crosslink: { stroke: '#8c8a89', lineDash: [8, 6], lineWidth: 1, label: '跨联' },
+            sibling: { stroke: '#8C8986FF', lineDash: [8, 6], lineWidth: 1, label: '同级' },
         };
 
         (rawEdges || []).forEach((edge, index) => {
@@ -208,7 +205,10 @@ class KnowledgeMindmapGraph {
             const category = edge.type || 'successor';
             const renderType =
                 category === 'successor' ? 'cubic-horizontal' : 'quadratic';
-            const baseStyle = styleMap[category] || neutral;
+            const baseStyle = styleMap[category] || {
+                stroke: '#cbd5e1',
+                lineWidth: 2.5,
+            };
             const label = baseStyle.label || edge.label || category;
             const arrowStroke = baseStyle.stroke || '#cbd5e1';
             const style = {
@@ -271,10 +271,16 @@ class KnowledgeMindmapGraph {
             },
             defaultEdge: {
                 type: 'cubic-horizontal',
-                style: {
-                    stroke: '#cbd5e1',
-                    lineWidth: 2,
-                },
+                style: this.showEdges
+                    ? {
+                          stroke: '#cbd5e1',
+                          lineWidth: 2,
+                      }
+                    : {
+                          stroke: 'transparent',
+                          lineWidth: 0,
+                          opacity: 0,
+                      },
             },
             nodeStateStyles: {
                 hover: { shadowColor: '#38bdf8', shadowBlur: 24 },
@@ -310,19 +316,23 @@ class KnowledgeMindmapGraph {
         window.focusMindmapNode = (id) => this.focusNodeById(id);
 
         // 边提示
-        this.bindEdgeTooltip();
+        if (this.showEdges) {
+            this.bindEdgeTooltip();
+        }
 
         this.applyNodeStates();
-        this.startEdgeFlows();
+        if (this.showEdges) {
+            this.startEdgeFlows();
+        }
         this.focusOnLowestMastery();
         this.repaintNodes();
+        if (!this.showEdges) {
+            this.hideAllEdges();
+        }
 
         // 折叠/展开或重新布局后重新挂载关联线
         this.graph.on('afterlayout', () => {
             this.redrawRelationEdges();
-            this.repaintNodes();
-            this.applyNodeStates();
-            this.graph.paint();
         });
 
         this.setupFocusListener();
@@ -355,7 +365,13 @@ class KnowledgeMindmapGraph {
     }
 
     drawRelationEdges() {
-        if (!this.graph || !this.relationEdges.length) return;
+        if (
+            !this.graph ||
+            !this.relationEdges.length ||
+            !this.showRelationEdges ||
+            !this.showEdges
+        )
+            return;
         this.relationEdges.forEach((edge) => {
             // 尝试将隐藏节点映射到可见的父节点,避免关联线丢失
             let sourceId = edge.source;
@@ -393,15 +409,7 @@ class KnowledgeMindmapGraph {
                 if (!resolved || !this.graph.findById(resolved)) return;
                 targetId = resolved;
             }
-            const added = this.graph.addItem('edge', { ...edge, source: sourceId, target: targetId });
-            const shape = added?.getKeyShape?.();
-            if (shape) {
-                shape.attr({
-                    lineDash: [6, 6],
-                    stroke: '#cbd5e1',
-                    lineWidth: 2,
-                });
-            }
+            this.graph.addItem('edge', { ...edge, source: sourceId, target: targetId });
         });
     }
 
@@ -426,7 +434,7 @@ class KnowledgeMindmapGraph {
     }
 
     startEdgeFlows() {
-        if (!this.graph) return;
+        if (!this.graph || !this.showEdges) return;
         const nodeMap = new Map(
             this.graph
                 .getNodes()
@@ -497,15 +505,15 @@ class KnowledgeMindmapGraph {
     bindEvents() {
         if (!this.graph) return;
 
-        this.graph.on('node:mouseenter', (evt) => {
+        this.graph.on('node:mouseover', (evt) => {
             const item = evt.item;
-            if (!item || item.getModel().locked) return;
+            // if (!item || item.getModel().locked) return;
             this.graph.setItemState(item, 'hover', true);
             this.highlightNeighbors(item.getModel().id);
             this.showTooltip(evt, item.getModel());
         });
 
-        this.graph.on('node:mouseleave', (evt) => {
+        this.graph.on('node:mouseout', (evt) => {
             const item = evt.item;
             if (!item) return;
             this.graph.setItemState(item, 'hover', false);
@@ -513,10 +521,6 @@ class KnowledgeMindmapGraph {
             this.hideTooltip();
         });
 
-        this.graph.on('canvas:mouseleave', () => {
-            this.hideTooltip();
-        });
-
         this.graph.on('node:click', (evt) => this.handleNodeClick(evt));
 
         // 双击用于折叠/展开
@@ -532,9 +536,6 @@ class KnowledgeMindmapGraph {
                 this.graph.updateItem(evt.item, { collapsed: nextState });
                 this.graph.layout?.();
                 this.redrawRelationEdges();
-                this.repaintNodes();
-                this.applyNodeStates?.();
-                this.graph.paint();
             }
         });
 
@@ -555,6 +556,7 @@ class KnowledgeMindmapGraph {
     }
 
     highlightNeighbors(nodeId) {
+        if (!this.showEdges) return;
         const connected = new Set([nodeId]);
         this.graph.getEdges().forEach((edge) => {
             const model = edge.getModel();
@@ -580,6 +582,7 @@ class KnowledgeMindmapGraph {
     }
 
     clearNeighborHighlight() {
+        if (!this.showEdges) return;
         this.graph.getEdges().forEach((edge) => {
             this.graph.clearItemStates(edge, ['hover', 'crosshover']);
         });
@@ -685,13 +688,13 @@ class KnowledgeMindmapGraph {
     }
 
     focusOnLowestMastery() {
-        if (!this.graph) return null;
+        if (!this.graph) return;
 
         const entries = Object.entries(this.masteryData || {}).filter(
             ([, value]) =>
                 value && typeof value.mastery_level === 'number'
         );
-        if (!entries.length) return null;
+        if (!entries.length) return;
 
         let targetId = null;
         let minLevel = Infinity;
@@ -703,7 +706,7 @@ class KnowledgeMindmapGraph {
             }
         });
 
-        if (!targetId) return null;
+        if (!targetId) return;
 
         this.graph.getNodes().forEach((node) => {
             this.graph.clearItemStates(node);
@@ -711,14 +714,13 @@ class KnowledgeMindmapGraph {
 
         const item = this.graph.findById(targetId);
         if (item) {
+            this.graph.zoom(2);
             this.graph.focusItem(item, true, {
                 easing: 'easeCubic',
                 duration: 500,
             });
             this.graph.setItemState(item, 'selected', true);
-            return item;
         }
-        return null;
     }
 
     refreshGraph() {
@@ -748,30 +750,18 @@ class KnowledgeMindmapGraph {
         this.clearRelationEdges();
         this.drawRelationEdges();
         this.applyNodeStates();
-        this.startEdgeFlows();
-        const focused = this.focusOnLowestMastery();
-        this.zoomAfterFocus(focused);
+        if (!this.showEdges) {
+            this.hideAllEdges();
+        } else {
+            this.startEdgeFlows();
+        }
+        this.focusOnLowestMastery();
         this.graph.paint();
         this.setupFocusListener();
     }
 
-    zoomAfterFocus(focusedItem = null) {
-        if (!this.graph) return;
-        const hasMastery = Object.keys(this.masteryData || {}).length > 0;
-        if (!hasMastery) return;
-        const model = focusedItem?.getModel?.() || null;
-        const center = model?.x && model?.y
-            ? { x: model.x, y: model.y }
-            : {
-                  x: this.graph.get('width') / 2,
-                  y: this.graph.get('height') / 2,
-              };
-        const targetZoom = Math.min(2.0, Math.max(1.4, this.graph.getZoom() * 1.35));
-        this.graph.zoomTo(targetZoom, center);
-    }
-
     bindEdgeTooltip() {
-        if (!this.graph) return;
+        if (!this.graph || !this.showEdges) return;
         const tooltipEl = document.createElement('div');
         tooltipEl.style.position = 'fixed';
         tooltipEl.style.pointerEvents = 'none';
@@ -811,10 +801,10 @@ class KnowledgeMindmapGraph {
         });
 
         this.graph.on('edge:mouseleave', () => hide());
-        this.graph.on('canvas:mouseleave', () => hide());
     }
 
     redrawRelationEdges() {
+        if (!this.showRelationEdges || !this.showEdges) return;
         this.clearRelationEdges();
         this.drawRelationEdges();
     }
@@ -832,7 +822,7 @@ class KnowledgeMindmapGraph {
         this.clickTimer = setTimeout(() => {
             const model = evt.item?.getModel();
             if (!model) return;
-            if (model.locked) return;
+            // if (model.locked) return;
 
             this.graph.getNodes().forEach((node) => {
                 this.graph.clearItemStates(node);
@@ -1097,6 +1087,13 @@ class KnowledgeMindmapGraph {
         );
     }
 
+    hideAllEdges() {
+        if (!this.graph) return;
+        this.graph.getEdges().forEach((edge) => {
+            this.graph.hideItem(edge);
+        });
+    }
+
 }
 
 // 定义KnowledgeMindmapGraph类,确保G6已加载

+ 71 - 1
resources/views/filament/pages/intelligent-exam-generation-simple.blade.php

@@ -338,12 +338,82 @@
         </div>
 
         <!-- 生成按钮 -->
-        <div class="bg-white p-6 rounded-lg border shadow-sm">
+        @php
+            $hasTeacherStudent = !empty($selectedTeacherId) && !empty($selectedStudentId);
+            $hasKnowledgePoints = count($selectedKpCodes) > 0;
+            $questionCountValid = $totalQuestions >= 6;
+            $readyToGenerate = $this->canGenerate();
+            $missingSteps = [];
+            if (!$hasTeacherStudent) { $missingSteps[] = '选择教师与学生'; }
+            if (!$hasKnowledgePoints) { $missingSteps[] = '勾选至少 1 个知识点'; }
+            if (!$questionCountValid) { $missingSteps[] = '题目数量需 ≥ 6 题'; }
+        @endphp
+        <div class="bg-white p-6 rounded-lg border shadow-sm space-y-4">
+            <div class="flex items-center justify-between">
+                <h3 class="text-lg font-semibold text-gray-900 flex items-center gap-2">
+                    <svg class="w-5 h-5 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
+                    </svg>
+                    出卷前检查
+                </h3>
+                <span class="inline-flex items-center gap-2 px-3 py-1 text-xs font-semibold rounded-full {{ $readyToGenerate ? 'bg-green-100 text-green-800' : 'bg-amber-100 text-amber-800' }}">
+                    <span class="h-2 w-2 rounded-full {{ $readyToGenerate ? 'bg-green-500' : 'bg-amber-500' }}"></span>
+                    {{ $readyToGenerate ? '可以生成' : '待完善' }}
+                </span>
+            </div>
+
+            <div class="grid grid-cols-1 md:grid-cols-3 gap-3">
+                <div class="p-3 rounded-lg border {{ $hasTeacherStudent ? 'border-green-200 bg-green-50' : 'border-amber-200 bg-amber-50' }}">
+                    <div class="flex items-center gap-2 text-sm font-medium {{ $hasTeacherStudent ? 'text-green-800' : 'text-amber-800' }}">
+                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ $hasTeacherStudent ? 'M5 13l4 4L19 7' : 'M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }}" />
+                        </svg>
+                        教师 / 学生
+                    </div>
+                    <p class="mt-1 text-xs text-gray-700">
+                        {{ $hasTeacherStudent ? '已选择:' . $this->getSelectedTeacherName() . ' / ' . $this->getSelectedStudentName() : '请选择教师并选择其学生后出卷' }}
+                    </p>
+                </div>
+
+                <div class="p-3 rounded-lg border {{ $hasKnowledgePoints ? 'border-green-200 bg-green-50' : 'border-amber-200 bg-amber-50' }}">
+                    <div class="flex items-center gap-2 text-sm font-medium {{ $hasKnowledgePoints ? 'text-green-800' : 'text-amber-800' }}">
+                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ $hasKnowledgePoints ? 'M5 13l4 4L19 7' : 'M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }}" />
+                        </svg>
+                        知识点选择
+                    </div>
+                    <p class="mt-1 text-xs text-gray-700">
+                        {{ $hasKnowledgePoints ? '已选 ' . count($selectedKpCodes) . ' 个知识点' : '请勾选学生薄弱点或手动选择知识点' }}
+                    </p>
+                </div>
+
+                <div class="p-3 rounded-lg border {{ $questionCountValid ? 'border-green-200 bg-green-50' : 'border-amber-200 bg-amber-50' }}">
+                    <div class="flex items-center gap-2 text-sm font-medium {{ $questionCountValid ? 'text-green-800' : 'text-amber-800' }}">
+                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ $questionCountValid ? 'M5 13l4 4L19 7' : 'M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }}" />
+                        </svg>
+                        题目数量
+                    </div>
+                    <p class="mt-1 text-xs text-gray-700">
+                        {{ $questionCountValid ? '将生成 ' . $totalQuestions . ' 题' : '题目数量需不少于 6 题' }}
+                    </p>
+                </div>
+            </div>
+
+            @unless($readyToGenerate)
+                <div class="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
+                    完成以下步骤后再点击生成:{{ implode(' / ', $missingSteps) ?: '所有条件已满足' }}
+                </div>
+            @endunless
+
             <button
                 wire:click="generateExam"
                 type="button"
                 class="filament-button filament-button-size-lg filament-button-color-primary filament-button-icon-start inline-flex items-center justify-center w-full px-6 py-3 text-base font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
                 wire:loading.attr="disabled"
+                @if(!$readyToGenerate) disabled @endif
+                aria-disabled="{{ $readyToGenerate ? 'false' : 'true' }}"
+                title="{{ $readyToGenerate ? '根据当前选择生成试卷' : '请先完成必选项再生成' }}"
             >
                 @if($isGenerating)
                     <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">

+ 1 - 0
resources/views/filament/pages/student-dashboard.blade.php

@@ -712,6 +712,7 @@
                         livewireMethod: 'openMindmapDrawer',
                         highlightLowMastery: true,
                         livewireId: this.livewireId,
+                        showRelationEdges: false,
                         onNodeSelect: (model) => {
                             const code = model?.meta?.code || model?.id;
                             const mastery = model?.meta?.mastery_level ?? this.graphInstance?.masteryData?.[code]?.mastery_level ?? 0;