2 Комити

Аутор SHA1 Порука Датум
  liuzhuo be8638feb2 Merge branch 'develop' of http://123.206.9.27:3000/ShinSoft_Xxhsyb/Proj_SafePlat_Vue_Sgh5 into develop пре 3 недеља
  liuzhuo 0ef67fb0cf 移动端-安全检查-语音转文字语句间隔优化,ai研判功能提示词优化 пре 3 недеља
2 измењених фајлова са 857 додато и 45 уклоњено
  1. 124
    43
      src/view/safeCheck/safeCheck_edit/index.vue
  2. 733
    2
      src/view/yinhuan/JuBaoregistration_edit/index.vue

+ 124
- 43
src/view/safeCheck/safeCheck_edit/index.vue Прегледај датотеку

@@ -474,6 +474,8 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
474 474
   const recognizedText = ref('');
475 475
   const voiceErrorMessage = ref('');
476 476
   let recognition = null;
477
+  // 累积的最终识别文本(用于保存已确认的识别结果)
478
+  let accumulatedFinalText = '';
477 479
 
478 480
   // 检查浏览器是否支持语音识别
479 481
   function checkSpeechRecognitionSupport() {
@@ -509,23 +511,49 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
509 511
       isRecording.value = true;
510 512
       recognizedText.value = '';
511 513
       voiceErrorMessage.value = '';
514
+      accumulatedFinalText = ''; // 重置累积的最终文本
512 515
     };
513 516
 
517
+    // 移除所有句号的辅助函数(移除所有句号,保留其他标点符号)
518
+    function removePeriods(text) {
519
+      if (!text) return text;
520
+      // 移除所有的中文句号和英文句号(包括中间和末尾的)
521
+      return text.replace(/[。.]/g, '');
522
+    }
523
+
514 524
     // 识别结果
515 525
     recognition.onresult = (event) => {
516 526
       let interimTranscript = '';
517
-      let finalTranscript = '';
527
+      let newFinalTranscript = '';
518 528
 
529
+      // 只处理新产生的结果(从resultIndex开始)
519 530
       for (let i = event.resultIndex; i < event.results.length; i++) {
520
-        const transcript = event.results[i][0].transcript;
531
+        let transcript = event.results[i][0].transcript;
521 532
         if (event.results[i].isFinal) {
522
-          finalTranscript += transcript;
533
+          // 最终结果:移除所有句号后追加到累积文本中(保留其他标点符号)
534
+          newFinalTranscript += removePeriods(transcript);
523 535
         } else {
524
-          interimTranscript += transcript;
536
+          // 临时结果:移除所有句号后显示当前正在识别的部分(保留其他标点符号)
537
+          interimTranscript += removePeriods(transcript);
538
+        }
539
+      }
540
+
541
+      // 如果有新的最终结果,追加到累积文本(已移除标点符号)
542
+      if (newFinalTranscript) {
543
+        // 如果累积文本不为空,在追加前添加空格(避免文本粘连)
544
+        if (accumulatedFinalText) {
545
+          accumulatedFinalText += ' ';
525 546
         }
547
+        accumulatedFinalText += newFinalTranscript;
526 548
       }
527 549
 
528
-      recognizedText.value = finalTranscript || interimTranscript;
550
+      // 显示:累积的最终文本 + 当前的临时文本(识别过程中不包含标点符号)
551
+      // 如果累积文本不为空且临时文本不为空,在它们之间添加空格
552
+      if (accumulatedFinalText && interimTranscript) {
553
+        recognizedText.value = accumulatedFinalText + ' ' + interimTranscript;
554
+      } else {
555
+        recognizedText.value = accumulatedFinalText + interimTranscript;
556
+      }
529 557
     };
530 558
 
531 559
     // 识别错误
@@ -584,6 +612,7 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
584 612
     showVoiceDialog.value = true;
585 613
     recognizedText.value = '';
586 614
     voiceErrorMessage.value = '';
615
+    accumulatedFinalText = ''; // 重置累积文本
587 616
   }
588 617
 
589 618
   // 开始识别
@@ -592,9 +621,10 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
592 621
       initSpeechRecognition();
593 622
     }
594 623
 
595
-    // 清除之前的错误信息
624
+    // 清除之前的错误信息和文本
596 625
     voiceErrorMessage.value = '';
597 626
     recognizedText.value = '';
627
+    accumulatedFinalText = ''; // 重置累积文本
598 628
 
599 629
     try {
600 630
       recognition.start();
@@ -618,13 +648,23 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
618 648
   // 确认使用识别的文本
619 649
   function confirmVoiceText() {
620 650
     if (recognizedText.value) {
651
+      let textToAdd = recognizedText.value.trim();
652
+      
653
+      // 只在确认使用时,检查并添加句号
654
+      // 如果文本末尾没有标点符号(句号、问号、感叹号、逗号等),则添加句号
655
+      const punctuationRegex = /[。!?,;:、]$/;
656
+      if (textToAdd && !punctuationRegex.test(textToAdd)) {
657
+        textToAdd += '。';
658
+      }
659
+      
621 660
       // 如果已有内容,追加;否则直接设置
622 661
       if (form.value.checkResult) {
623
-        form.value.checkResult += recognizedText.value;
662
+        form.value.checkResult += textToAdd;
624 663
       } else {
625
-        form.value.checkResult = recognizedText.value;
664
+        form.value.checkResult = textToAdd;
626 665
       }
627 666
       recognizedText.value = '';
667
+      accumulatedFinalText = ''; // 重置累积文本
628 668
       voiceErrorMessage.value = '';
629 669
       showVoiceDialog.value = false;
630 670
       showToast({
@@ -657,6 +697,14 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
657 697
       const markedModule = await import('marked');
658 698
       marked = markedModule.marked || markedModule.default || markedModule;
659 699
       
700
+      // 配置 marked 选项
701
+      if (marked.setOptions) {
702
+        marked.setOptions({
703
+          breaks: true, // 支持换行
704
+          gfm: true, // 支持 GitHub Flavored Markdown
705
+        });
706
+      }
707
+      
660 708
       // 动态导入 DOMPurify
661 709
       const dompurifyModule = await import('dompurify');
662 710
       DOMPurify = dompurifyModule.default || dompurifyModule;
@@ -698,47 +746,37 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
698 746
   function formatPlainText(text) {
699 747
     if (!text) return '';
700 748
     
701
-    // 将 Markdown 格式转换为 HTML
749
+    // 处理隐患点格式:以冒号结尾的行(隐患点描述)+ 下一行是违反说明
702 750
     let formatted = text
703
-      // 标题(按顺序处理,从多级到单级)
751
+      // 先处理标题
704 752
       .replace(/^####\s+(.+)$/gm, '<h4>$1</h4>')
705 753
       .replace(/^###\s+(.+)$/gm, '<h3>$1</h3>')
706 754
       .replace(/^##\s+(.+)$/gm, '<h2>$1</h2>')
707 755
       .replace(/^#\s+(.+)$/gm, '<h1>$1</h1>')
708
-      // 粗体(支持 **text** 和 **text** 格式)
756
+      // 处理隐患点格式:以冒号结尾的行,后面跟着违反说明
757
+      .replace(/^([^:::\n]+[::])\s*\n\s*(违反了[^\n]+)/gm, '<p class="hazard-point">$1</p><p class="violation-desc">$2</p>')
758
+      // 粗体
709 759
       .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
710 760
       // 斜体
711 761
       .replace(/\*(.+?)\*/g, '<em>$1</em>')
712
-      // 列表项(数字列表)
713
-      .replace(/^\d+\.\s+(.+)$/gm, '<p class="list-item">$1</p>')
714
-      // 列表项(无序列表)
715
-      .replace(/^[-*]\s+(.+)$/gm, '<p class="list-item">• $1</p>')
716
-      // 处理特殊格式:条款号、具体内容等
717
-      .replace(/\*\*条款号\*\*:\s*(.+)/g, '<p class="article-num"><strong>条款号:</strong>$1</p>')
718
-      .replace(/\*\*具体内容\*\*:\s*(.+)/g, '<p class="article-content"><strong>具体内容:</strong>$1</p>')
719 762
       // 多个连续换行转换为段落分隔
720 763
       .replace(/\n\n+/g, '</p><p>')
721 764
       // 单个换行转换为换行符
722 765
       .replace(/\n/g, '<br/>');
723 766
     
724
-    // 确保每个段落都有正确的标签
725
-    formatted = formatted
726
-      .split('</p><p>')
727
-      .map(para => {
728
-        para = para.trim();
729
-        if (!para) return '';
730
-        if (para.startsWith('<h') || para.startsWith('<p') || para.startsWith('<ul') || para.startsWith('<ol')) {
731
-          return para;
732
-        }
733
-        return '<p>' + para + '</p>';
734
-      })
735
-      .filter(para => para)
736
-      .join('');
737
-    
738
-    // 包装整个内容
739
-    if (!formatted.startsWith('<h') && !formatted.startsWith('<p') && !formatted.startsWith('<ul')) {
740
-      formatted = '<p>' + formatted + '</p>';
741
-    }
767
+    // 确保每个段落都有正确的标签(但保留已有的段落标签)
768
+    const parts = formatted.split(/(<[^>]+>)/);
769
+    formatted = parts.map(part => {
770
+      if (part.startsWith('<') || !part.trim()) {
771
+        return part;
772
+      }
773
+      // 如果已经是段落标签,直接返回
774
+      if (part.includes('<p') || part.includes('</p>')) {
775
+        return part;
776
+      }
777
+      // 否则包装成段落
778
+      return '<p>' + part + '</p>';
779
+    }).join('');
742 780
     
743 781
     return formatted;
744 782
   }
@@ -787,17 +825,39 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
787 825
 
788 826
   // 构造判断提示词
789 827
   function buildJudgePrompt(checkResult) {
790
-    const prompt = `请根据以下检查结果内容,判断违反了哪一条法律法规,并详细说明
828
+    const prompt = `请根据以下检查结果内容,分析违反法律法规的原因
791 829
 
792 830
 检查结果内容:
793 831
 ${checkResult}
794 832
 
795
-请提供以下内容:
796
-1. 明确指出违反了哪些法律法规(包括法律名称、条款号、具体条款内容)
797
-2. 说明为什么该检查结果违反了这些法律法规
798
-3. 如果可能,提供相关的处罚依据或整改建议
833
+请按照以下Markdown格式,只返回"违反法律法规的原因"部分:
834
+
835
+## 违反法律法规的原因
799 836
 
800
-请用清晰、专业的语言进行回答,确保法律法规名称和条款号准确无误。`;
837
+**[从检查结果中提取的第一个具体隐患点,要详细描述隐患的具体情况]**:
838
+
839
+违反了《[法律法规名称]》第[条款号],[详细说明为什么违反,包括具体原因和可能导致的后果]。
840
+
841
+**[从检查结果中提取的第二个具体隐患点,要详细描述隐患的具体情况]**:
842
+
843
+违反了《[法律法规名称]》第[条款号],[详细说明为什么违反,包括具体原因和可能导致的后果]。
844
+
845
+**[从检查结果中提取的第三个具体隐患点,要详细描述隐患的具体情况]**:
846
+
847
+违反了《[法律法规名称]》第[条款号],[详细说明为什么违反,包括具体原因和可能导致的后果]。
848
+
849
+重要格式要求:
850
+1. 使用Markdown格式,标题使用 ##
851
+2. 每个隐患点描述使用 **粗体** 格式,后面加冒号
852
+3. 每个隐患点描述和违反说明之间必须换行
853
+4. 每个隐患点之间必须空一行
854
+5. 必须从检查结果中提取具体的隐患点,每个隐患点要有详细、具体的描述,不能使用占位符
855
+6. 每个隐患点要明确指出违反了哪部法律法规的哪个条款
856
+7. 详细说明违反的原因,包括具体原因和可能导致的后果
857
+8. 使用清晰、专业的语言,确保法律法规名称和条款号准确无误
858
+9. 不要返回其他内容(如法律法规的具体条款内容、处罚依据、整改建议等)
859
+
860
+请严格按照Markdown格式输出。`;
801 861
     
802 862
     return prompt;
803 863
   }
@@ -939,11 +999,31 @@ ${checkResult}
939 999
 }
940 1000
 
941 1001
 .ai-judge-result :deep(p) {
942
-  margin: 10px 0;
1002
+  margin: 12px 0;
943 1003
   line-height: 1.8;
944 1004
   color: #606266;
945 1005
 }
946 1006
 
1007
+/* 优化隐患点格式 - 包含冒号的段落(隐患点描述) */
1008
+.ai-judge-result :deep(p:first-line) {
1009
+  font-weight: 500;
1010
+}
1011
+
1012
+/* 隐患点描述(以冒号结尾的段落) */
1013
+.ai-judge-result :deep(p:first-child) {
1014
+  margin-top: 0;
1015
+}
1016
+
1017
+/* 确保段落之间的间距一致 */
1018
+.ai-judge-result :deep(p + p) {
1019
+  margin-top: 12px;
1020
+}
1021
+
1022
+/* 优化段落内换行 */
1023
+.ai-judge-result :deep(br) {
1024
+  line-height: 1.8;
1025
+}
1026
+
947 1027
 .ai-judge-result :deep(p.list-item) {
948 1028
   margin: 6px 0;
949 1029
   padding-left: 8px;
@@ -1021,3 +1101,4 @@ ${checkResult}
1021 1101
 }
1022 1102
 
1023 1103
 </style>
1104
+

+ 733
- 2
src/view/yinhuan/JuBaoregistration_edit/index.vue Прегледај датотеку

@@ -106,7 +106,45 @@
106 106
         type="textarea"
107 107
         rows="1"
108 108
         autosize
109
-      />
109
+      >
110
+        <template #right-icon>
111
+          <van-icon 
112
+            name="volume-o" 
113
+            size="20" 
114
+            :color="isRecording ? '#ee0a24' : '#1989fa'"
115
+            @click="toggleVoiceInput"
116
+            style="margin-right: 8px; cursor: pointer;"
117
+          />
118
+        </template>
119
+      </van-field>
120
+
121
+      <!-- AI研判按钮 -->
122
+      <div style="margin: 10px 0; padding: 0 16px;">
123
+        <van-button 
124
+          type="primary" 
125
+          size="small" 
126
+          block
127
+          :loading="aiJudging"
128
+          @click="handleAIJudge"
129
+          :disabled="!form.hdDescription || form.hdDescription.trim() === ''"
130
+        >
131
+          <van-icon name="chat-o" style="margin-right: 5px;" />
132
+          {{ aiJudging ? 'AI判断中...' : 'AI研判' }}
133
+        </van-button>
134
+      </div>
135
+
136
+      <!-- AI判断结果显示 -->
137
+      <van-field
138
+        v-if="aiJudgeResult"
139
+        border
140
+        readonly
141
+        label="AI判断结果"
142
+        :colon="true"
143
+      >
144
+        <template #input>
145
+          <div class="ai-judge-result" v-html="renderedJudgeResult"></div>
146
+        </template>
147
+      </van-field>
110 148
 
111 149
       <van-field
112 150
         is-link
@@ -225,10 +263,69 @@
225 263
 
226 264
   <van-dialog v-model:show="showDialogVisible" title="删除文件" show-cancel-button
227 265
               confirm-button-color="#ee0124" message="确定删除该文件吗?" @confirm="onDelete" />
266
+
267
+  <!-- 语音识别弹窗 -->
268
+  <van-dialog
269
+    v-model:show="showVoiceDialog"
270
+    title="语音输入"
271
+    :show-cancel-button="false"
272
+    :show-confirm-button="false"
273
+    :close-on-click-overlay="true"
274
+    width="90%"
275
+  >
276
+    <div style="padding: 20px; text-align: center;">
277
+      <van-icon 
278
+        name="volume-o" 
279
+        size="60" 
280
+        :color="isRecording ? '#ee0a24' : '#1989fa'"
281
+        :class="{ 'pulsing': isRecording }"
282
+      />
283
+      <div style="margin-top: 20px; font-size: 16px; color: #323233;">
284
+        {{ isRecording ? '正在录音,请说话...' : voiceErrorMessage || '点击开始录音' }}
285
+      </div>
286
+      <!-- 错误信息显示 -->
287
+      <div v-if="voiceErrorMessage && !isRecording" style="margin-top: 15px; padding: 10px; background: #fff7e6; border-radius: 4px; border-left: 3px solid #ff9800;">
288
+        <div style="font-size: 14px; color: #ff9800; text-align: left;">
289
+          {{ voiceErrorMessage }}
290
+        </div>
291
+      </div>
292
+      <!-- 识别结果显示 -->
293
+      <div v-if="recognizedText" style="margin-top: 15px; padding: 10px; background: #f7f8fa; border-radius: 4px; text-align: left; max-height: 150px; overflow-y: auto;">
294
+        <div style="font-size: 14px; color: #666; margin-bottom: 5px;">识别结果:</div>
295
+        <div style="font-size: 14px; color: #323233; word-break: break-all;">{{ recognizedText }}</div>
296
+      </div>
297
+      <div style="margin-top: 20px; display: flex; justify-content: center; gap: 10px;">
298
+        <van-button 
299
+          v-if="!isRecording" 
300
+          type="primary" 
301
+          size="small" 
302
+          @click="startRecognition"
303
+        >
304
+          开始录音
305
+        </van-button>
306
+        <van-button 
307
+          v-else 
308
+          type="danger" 
309
+          size="small" 
310
+          @click="stopRecognition"
311
+        >
312
+          停止录音
313
+        </van-button>
314
+        <van-button 
315
+          v-if="recognizedText" 
316
+          type="primary" 
317
+          size="small" 
318
+          @click="confirmVoiceText"
319
+        >
320
+          确认使用
321
+        </van-button>
322
+      </div>
323
+    </div>
324
+  </van-dialog>
228 325
 </template>
229 326
 
230 327
 <script setup>
231
-import { ref, reactive, onMounted, getCurrentInstance, nextTick } from 'vue';
328
+import { ref, reactive, onMounted, getCurrentInstance, nextTick, onUnmounted, computed } from 'vue';
232 329
 import { closeToast, Dialog, showFailToast, showLoadingToast, showSuccessToast, showToast } from 'vant';
233 330
 import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
234 331
 import AttachmentS3 from '@/components/AttachmentS3.vue';
@@ -305,6 +402,12 @@ onMounted(async () => {
305 402
     await proxy.$axios.get(url, param).then(response => {
306 403
       if (response.data.code === 0) {
307 404
         form.value = response.data.data;
405
+        // 如果有 AI 判断结果,同步到显示变量
406
+        if (form.value.aiJudge) {
407
+          aiJudgeResult.value = form.value.aiJudge;
408
+        } else {
409
+          aiJudgeResult.value = '';
410
+        }
308 411
       } else {
309 412
         showToast({
310 413
           message: '操作失败!' + response.data.msg
@@ -341,6 +444,7 @@ const form = ref({
341 444
   discoverer: '',
342 445
   discovererOther: '',
343 446
   hdDescription: '',
447
+  aiJudge: '', // AI判断结果字段
344 448
   hdLevel: '',
345 449
   bz: '',
346 450
   hdLocation: '',
@@ -464,6 +568,11 @@ const baocun = async () => {
464 568
     form.value.status = '0';
465 569
   } 
466 570
 
571
+  // 强制同步 AI 判断结果到 form(确保数据不丢失)
572
+  if (aiJudgeResult.value) {
573
+    form.value.aiJudge = aiJudgeResult.value;
574
+  }
575
+
467 576
   console.log('保存的参数', form.value);
468 577
 
469 578
   // 原有保存逻辑保持不变
@@ -762,6 +871,477 @@ const cancelTimePicker = () => {
762 871
 };
763 872
 /**修改发现时间**/
764 873
 
874
+  /***********************语音识别功能******************************/
875
+  // 语音识别相关状态
876
+  const isRecording = ref(false);
877
+  const showVoiceDialog = ref(false);
878
+  const recognizedText = ref('');
879
+  const voiceErrorMessage = ref('');
880
+  let recognition = null;
881
+  // 累积的最终识别文本(用于保存已确认的识别结果)
882
+  let accumulatedFinalText = '';
883
+
884
+  // 检查浏览器是否支持语音识别
885
+  function checkSpeechRecognitionSupport() {
886
+    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
887
+    if (!SpeechRecognition) {
888
+      showToast({
889
+        type: 'fail',
890
+        message: '您的浏览器不支持语音识别功能'
891
+      });
892
+      return false;
893
+    }
894
+    return true;
895
+  }
896
+
897
+  // 初始化语音识别
898
+  function initSpeechRecognition() {
899
+    if (!checkSpeechRecognitionSupport()) {
900
+      return;
901
+    }
902
+
903
+    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
904
+    recognition = new SpeechRecognition();
905
+    
906
+    // 设置语言为中文
907
+    recognition.lang = 'zh-CN';
908
+    // 连续识别
909
+    recognition.continuous = true;
910
+    // 返回临时结果
911
+    recognition.interimResults = true;
912
+
913
+    // 识别开始
914
+    recognition.onstart = () => {
915
+      isRecording.value = true;
916
+      recognizedText.value = '';
917
+      voiceErrorMessage.value = '';
918
+      accumulatedFinalText = ''; // 重置累积的最终文本
919
+    };
920
+
921
+    // 移除所有句号的辅助函数(移除所有句号,保留其他标点符号)
922
+    function removePeriods(text) {
923
+      if (!text) return text;
924
+      // 移除所有的中文句号和英文句号(包括中间和末尾的)
925
+      return text.replace(/[。.]/g, '');
926
+    }
927
+
928
+    // 识别结果
929
+    recognition.onresult = (event) => {
930
+      let interimTranscript = '';
931
+      let newFinalTranscript = '';
932
+
933
+      // 只处理新产生的结果(从resultIndex开始)
934
+      for (let i = event.resultIndex; i < event.results.length; i++) {
935
+        let transcript = event.results[i][0].transcript;
936
+        if (event.results[i].isFinal) {
937
+          // 最终结果:移除所有句号后追加到累积文本中(保留其他标点符号)
938
+          newFinalTranscript += removePeriods(transcript);
939
+        } else {
940
+          // 临时结果:移除所有句号后显示当前正在识别的部分(保留其他标点符号)
941
+          interimTranscript += removePeriods(transcript);
942
+        }
943
+      }
944
+
945
+      // 如果有新的最终结果,追加到累积文本(已移除标点符号)
946
+      if (newFinalTranscript) {
947
+        // 如果累积文本不为空,在追加前添加空格(避免文本粘连)
948
+        if (accumulatedFinalText) {
949
+          accumulatedFinalText += ' ';
950
+        }
951
+        accumulatedFinalText += newFinalTranscript;
952
+      }
953
+
954
+      // 显示:累积的最终文本 + 当前的临时文本(识别过程中不包含标点符号)
955
+      // 如果累积文本不为空且临时文本不为空,在它们之间添加空格
956
+      if (accumulatedFinalText && interimTranscript) {
957
+        recognizedText.value = accumulatedFinalText + ' ' + interimTranscript;
958
+      } else {
959
+        recognizedText.value = accumulatedFinalText + interimTranscript;
960
+      }
961
+    };
962
+
963
+    // 识别错误
964
+    recognition.onerror = (event) => {
965
+      console.error('语音识别错误:', event.error);
966
+      isRecording.value = false;
967
+      
968
+      let errorMessage = '语音识别出错';
969
+      switch (event.error) {
970
+        case 'no-speech':
971
+          errorMessage = '未检测到语音,请重试';
972
+          break;
973
+        case 'audio-capture':
974
+          errorMessage = '无法访问麦克风,请检查权限';
975
+          break;
976
+        case 'not-allowed':
977
+          errorMessage = '麦克风权限被拒绝,请在浏览器设置中允许';
978
+          break;
979
+        case 'network':
980
+          errorMessage = '网络错误,请检查网络连接';
981
+          break;
982
+        case 'aborted':
983
+          errorMessage = '语音识别已中断';
984
+          break;
985
+        case 'service-not-allowed':
986
+          errorMessage = '语音识别服务不可用';
987
+          break;
988
+        default:
989
+          errorMessage = `语音识别出错: ${event.error}`;
990
+      }
991
+      
992
+      voiceErrorMessage.value = errorMessage;
993
+      
994
+      showToast({
995
+        type: 'fail',
996
+        message: errorMessage
997
+      });
998
+    };
999
+
1000
+    // 识别结束
1001
+    recognition.onend = () => {
1002
+      isRecording.value = false;
1003
+    };
1004
+  }
1005
+
1006
+  // 切换语音输入
1007
+  function toggleVoiceInput() {
1008
+    if (!checkSpeechRecognitionSupport()) {
1009
+      return;
1010
+    }
1011
+
1012
+    if (!recognition) {
1013
+      initSpeechRecognition();
1014
+    }
1015
+
1016
+    showVoiceDialog.value = true;
1017
+    recognizedText.value = '';
1018
+    voiceErrorMessage.value = '';
1019
+    accumulatedFinalText = ''; // 重置累积文本
1020
+  }
1021
+
1022
+  // 开始识别
1023
+  function startRecognition() {
1024
+    if (!recognition) {
1025
+      initSpeechRecognition();
1026
+    }
1027
+
1028
+    // 清除之前的错误信息和文本
1029
+    voiceErrorMessage.value = '';
1030
+    recognizedText.value = '';
1031
+    accumulatedFinalText = ''; // 重置累积文本
1032
+
1033
+    try {
1034
+      recognition.start();
1035
+    } catch (error) {
1036
+      console.error('启动语音识别失败:', error);
1037
+      voiceErrorMessage.value = '启动语音识别失败,请重试';
1038
+      showToast({
1039
+        type: 'fail',
1040
+        message: '启动语音识别失败,请重试'
1041
+      });
1042
+    }
1043
+  }
1044
+
1045
+  // 停止识别
1046
+  function stopRecognition() {
1047
+    if (recognition && isRecording.value) {
1048
+      recognition.stop();
1049
+    }
1050
+  }
1051
+
1052
+  // 确认使用识别的文本
1053
+  function confirmVoiceText() {
1054
+    if (recognizedText.value) {
1055
+      let textToAdd = recognizedText.value.trim();
1056
+      
1057
+      // 只在确认使用时,检查并添加句号
1058
+      // 如果文本末尾没有标点符号(句号、问号、感叹号、逗号等),则添加句号
1059
+      const punctuationRegex = /[。!?,;:、]$/;
1060
+      if (textToAdd && !punctuationRegex.test(textToAdd)) {
1061
+        textToAdd += '。';
1062
+      }
1063
+      
1064
+      // 如果已有内容,追加;否则直接设置
1065
+      if (form.value.hdDescription) {
1066
+        form.value.hdDescription += textToAdd;
1067
+      } else {
1068
+        form.value.hdDescription = textToAdd;
1069
+      }
1070
+      recognizedText.value = '';
1071
+      accumulatedFinalText = ''; // 重置累积文本
1072
+      voiceErrorMessage.value = '';
1073
+      showVoiceDialog.value = false;
1074
+      showToast({
1075
+        type: 'success',
1076
+        message: '已添加语音识别内容'
1077
+      });
1078
+    }
1079
+  }
1080
+
1081
+  // 组件卸载时清理
1082
+  onUnmounted(() => {
1083
+    if (recognition && isRecording.value) {
1084
+      recognition.stop();
1085
+    }
1086
+  });
1087
+
1088
+  /***********************AI判断法律法规功能******************************/
1089
+  // AI判断相关状态
1090
+  const aiJudging = ref(false);
1091
+  const aiJudgeResult = ref('');
1092
+  
1093
+  // Markdown 渲染库
1094
+  let marked = null;
1095
+  let DOMPurify = null;
1096
+  
1097
+  // 初始化 Markdown 库
1098
+  async function initMarkdownLibs() {
1099
+    try {
1100
+      // 动态导入 marked
1101
+      const markedModule = await import('marked');
1102
+      marked = markedModule.marked || markedModule.default || markedModule;
1103
+      
1104
+      // 配置 marked 选项
1105
+      if (marked.setOptions) {
1106
+        marked.setOptions({
1107
+          breaks: true, // 支持换行
1108
+          gfm: true, // 支持 GitHub Flavored Markdown
1109
+        });
1110
+      }
1111
+      
1112
+      // 动态导入 DOMPurify
1113
+      const dompurifyModule = await import('dompurify');
1114
+      DOMPurify = dompurifyModule.default || dompurifyModule;
1115
+    } catch (error) {
1116
+      console.warn('Markdown libraries not available, using plain text', error);
1117
+      marked = {
1118
+        parse: (text) => text
1119
+      };
1120
+      DOMPurify = {
1121
+        sanitize: (html) => html
1122
+      };
1123
+    }
1124
+  }
1125
+  
1126
+  // 初始化 Markdown 库
1127
+  initMarkdownLibs();
1128
+  
1129
+  // 渲染后的判断结果
1130
+  const renderedJudgeResult = computed(() => {
1131
+    if (!aiJudgeResult.value) return '';
1132
+    
1133
+    try {
1134
+      if (!marked || !DOMPurify) {
1135
+        // 如果库还没加载,先返回纯文本,但格式化一下
1136
+        return formatPlainText(aiJudgeResult.value);
1137
+      }
1138
+      
1139
+      // 使用 marked 解析 markdown
1140
+      const html = marked.parse ? marked.parse(aiJudgeResult.value) : marked(aiJudgeResult.value);
1141
+      // 使用 DOMPurify 清理 HTML
1142
+      return DOMPurify.sanitize ? DOMPurify.sanitize(html) : html;
1143
+    } catch (error) {
1144
+      console.error('Markdown解析错误:', error);
1145
+      return formatPlainText(aiJudgeResult.value);
1146
+    }
1147
+  });
1148
+  
1149
+  // 格式化纯文本(当 Markdown 库不可用时)
1150
+  function formatPlainText(text) {
1151
+    if (!text) return '';
1152
+    
1153
+    // 处理隐患点格式:以冒号结尾的行(隐患点描述)+ 下一行是违反说明
1154
+    let formatted = text
1155
+      // 先处理标题
1156
+      .replace(/^####\s+(.+)$/gm, '<h4>$1</h4>')
1157
+      .replace(/^###\s+(.+)$/gm, '<h3>$1</h3>')
1158
+      .replace(/^##\s+(.+)$/gm, '<h2>$1</h2>')
1159
+      .replace(/^#\s+(.+)$/gm, '<h1>$1</h1>')
1160
+      // 处理隐患点格式:以冒号结尾的行,后面跟着违反说明
1161
+      .replace(/^([^:::\n]+[::])\s*\n\s*(违反了[^\n]+)/gm, '<p class="hazard-point">$1</p><p class="violation-desc">$2</p>')
1162
+      // 粗体
1163
+      .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
1164
+      // 斜体
1165
+      .replace(/\*(.+?)\*/g, '<em>$1</em>')
1166
+      // 多个连续换行转换为段落分隔
1167
+      .replace(/\n\n+/g, '</p><p>')
1168
+      // 单个换行转换为换行符
1169
+      .replace(/\n/g, '<br/>');
1170
+    
1171
+    // 确保每个段落都有正确的标签(但保留已有的段落标签)
1172
+    const parts = formatted.split(/(<[^>]+>)/);
1173
+    formatted = parts.map(part => {
1174
+      if (part.startsWith('<') || !part.trim()) {
1175
+        return part;
1176
+      }
1177
+      // 如果已经是段落标签,直接返回
1178
+      if (part.includes('<p') || part.includes('</p>')) {
1179
+        return part;
1180
+      }
1181
+      // 否则包装成段落
1182
+      return '<p>' + part + '</p>';
1183
+    }).join('');
1184
+    
1185
+    return formatted;
1186
+  }
1187
+
1188
+  // 处理AI判断
1189
+  async function handleAIJudge() {
1190
+    if (!form.value.hdDescription || form.value.hdDescription.trim() === '') {
1191
+      showToast({
1192
+        type: 'fail',
1193
+        message: '请先输入隐患描述'
1194
+      });
1195
+      return;
1196
+    }
1197
+
1198
+    aiJudging.value = true;
1199
+    aiJudgeResult.value = '';
1200
+
1201
+    try {
1202
+      const prompt = buildJudgePrompt(form.value.hdDescription);
1203
+      const messages = [{ role: 'user', content: prompt }];
1204
+
1205
+      await callAIStream(messages, (content, isEnd) => {
1206
+        aiJudgeResult.value = content;
1207
+        // 实时同步到 form.aiJudge
1208
+        form.value.aiJudge = content;
1209
+        
1210
+        if (isEnd) {
1211
+          aiJudging.value = false;
1212
+          // 确保最终结果保存到 form
1213
+          form.value.aiJudge = content;
1214
+          showToast({
1215
+            type: 'success',
1216
+            message: 'AI判断完成'
1217
+          });
1218
+        }
1219
+      });
1220
+    } catch (error) {
1221
+      console.error('AI判断失败:', error);
1222
+      aiJudging.value = false;
1223
+      showToast({
1224
+        type: 'fail',
1225
+        message: 'AI判断失败,请重试'
1226
+      });
1227
+    }
1228
+  }
1229
+
1230
+  // 构造判断提示词
1231
+  function buildJudgePrompt(hdDescription) {
1232
+    const prompt = `请根据以下隐患描述内容,分析违反法律法规的原因:
1233
+
1234
+隐患描述内容:
1235
+${hdDescription}
1236
+
1237
+请按照以下Markdown格式,只返回"违反法律法规的原因"部分:
1238
+
1239
+## 违反法律法规的原因
1240
+
1241
+**[从隐患描述中提取的第一个具体隐患点,要详细描述隐患的具体情况]**:
1242
+
1243
+违反了《[法律法规名称]》第[条款号],[详细说明为什么违反,包括具体原因和可能导致的后果]。
1244
+
1245
+**[从隐患描述中提取的第二个具体隐患点,要详细描述隐患的具体情况]**:
1246
+
1247
+违反了《[法律法规名称]》第[条款号],[详细说明为什么违反,包括具体原因和可能导致的后果]。
1248
+
1249
+**[从隐患描述中提取的第三个具体隐患点,要详细描述隐患的具体情况]**:
1250
+
1251
+违反了《[法律法规名称]》第[条款号],[详细说明为什么违反,包括具体原因和可能导致的后果]。
1252
+
1253
+重要格式要求:
1254
+1. 使用Markdown格式,标题使用 ##
1255
+2. 每个隐患点描述使用 **粗体** 格式,后面加冒号
1256
+3. 每个隐患点描述和违反说明之间必须换行
1257
+4. 每个隐患点之间必须空一行
1258
+5. 必须从隐患描述中提取具体的隐患点,每个隐患点要有详细、具体的描述,不能使用占位符
1259
+6. 每个隐患点要明确指出违反了哪部法律法规的哪个条款
1260
+7. 详细说明违反的原因,包括具体原因和可能导致的后果
1261
+8. 使用清晰、专业的语言,确保法律法规名称和条款号准确无误
1262
+9. 不要返回其他内容(如法律法规的具体条款内容、处罚依据、整改建议等)
1263
+
1264
+请严格按照Markdown格式输出。`;
1265
+    
1266
+    return prompt;
1267
+  }
1268
+
1269
+  // 调用AI流式接口
1270
+  async function callAIStream(messageHistory, callback) {
1271
+    try {
1272
+      const url = `${import.meta.env.VITE_BASE_API}/sgsafe/deepseek/chat-stream`;
1273
+      
1274
+      const response = await fetch(url, {
1275
+        method: 'POST',
1276
+        headers: {
1277
+          'Content-Type': 'application/json',
1278
+          'token': localStorage.getItem('token') || '',
1279
+          'userId': localStorage.getItem('userId') || ''
1280
+        },
1281
+        body: JSON.stringify({ messages: messageHistory })
1282
+      });
1283
+      
1284
+      if (!response.ok) {
1285
+        throw new Error(`HTTP error! status: ${response.status}`);
1286
+      }
1287
+      
1288
+      if (!response.body) {
1289
+        throw new Error('ReadableStream not supported');
1290
+      }
1291
+      
1292
+      const reader = response.body.getReader();
1293
+      const decoder = new TextDecoder('utf-8');
1294
+      let buffer = '';
1295
+      let fullResponse = '';
1296
+      
1297
+      while (true) {
1298
+        const { done, value } = await reader.read();
1299
+        if (done) {
1300
+          callback(fullResponse, true);
1301
+          break;
1302
+        }
1303
+        
1304
+        const chunk = decoder.decode(value, { stream: true });
1305
+        buffer += chunk;
1306
+        
1307
+        const lines = buffer.split('\n');
1308
+        buffer = lines.pop();
1309
+        
1310
+        for (const line of lines) {
1311
+          if (line.trim() === '' || !line.startsWith('data:')) continue;
1312
+          
1313
+          const data = line.slice(5).trim();
1314
+          if (data === '[DONE]') {
1315
+            callback(fullResponse, true);
1316
+            return;
1317
+          }
1318
+          
1319
+          try {
1320
+            const jsonData = JSON.parse(data);
1321
+            if (jsonData.choices?.[0]?.delta) {
1322
+              let content = '';
1323
+              if (jsonData.choices[0].delta.reasoning_content) {
1324
+                content += jsonData.choices[0].delta.reasoning_content;
1325
+              }
1326
+              if (jsonData.choices[0].delta.content) {
1327
+                content += jsonData.choices[0].delta.content;
1328
+              }
1329
+              if (content) {
1330
+                fullResponse += content;
1331
+                callback(fullResponse, false);
1332
+              }
1333
+            }
1334
+          } catch (e) {
1335
+            console.error('JSON解析错误:', e);
1336
+          }
1337
+        }
1338
+      }
1339
+    } catch (error) {
1340
+      console.error('AI调用失败:', error);
1341
+      throw error;
1342
+    }
1343
+  }
1344
+
765 1345
 </script>
766 1346
 
767 1347
 <style scoped>
@@ -799,4 +1379,155 @@ const cancelTimePicker = () => {
799 1379
   color: white;
800 1380
   border-color: #4285f4;
801 1381
 }
1382
+
1383
+.pulsing {
1384
+  animation: pulse 1.5s ease-in-out infinite;
1385
+}
1386
+
1387
+@keyframes pulse {
1388
+  0%, 100% {
1389
+    transform: scale(1);
1390
+    opacity: 1;
1391
+  }
1392
+  50% {
1393
+    transform: scale(1.2);
1394
+    opacity: 0.7;
1395
+  }
1396
+}
1397
+
1398
+/* AI判断结果样式 */
1399
+.ai-judge-result {
1400
+  padding: 15px 0;
1401
+  color: #323233;
1402
+  line-height: 1.8;
1403
+  font-size: 14px;
1404
+  word-break: break-word;
1405
+}
1406
+
1407
+.ai-judge-result :deep(h1) {
1408
+  font-size: 18px;
1409
+  font-weight: 600;
1410
+  color: #303133;
1411
+  margin: 15px 0 10px 0;
1412
+  padding-bottom: 8px;
1413
+  border-bottom: 2px solid #e4e7ed;
1414
+}
1415
+
1416
+.ai-judge-result :deep(h2) {
1417
+  font-size: 16px;
1418
+  font-weight: 600;
1419
+  color: #303133;
1420
+  margin: 15px 0 10px 0;
1421
+  padding-bottom: 6px;
1422
+  border-bottom: 1px solid #e4e7ed;
1423
+}
1424
+
1425
+.ai-judge-result :deep(h3) {
1426
+  font-size: 15px;
1427
+  font-weight: 600;
1428
+  color: #409eff;
1429
+  margin: 12px 0 8px 0;
1430
+}
1431
+
1432
+.ai-judge-result :deep(p) {
1433
+  margin: 12px 0;
1434
+  line-height: 1.8;
1435
+  color: #606266;
1436
+}
1437
+
1438
+/* 优化隐患点格式 - 包含冒号的段落(隐患点描述) */
1439
+.ai-judge-result :deep(p:first-line) {
1440
+  font-weight: 500;
1441
+}
1442
+
1443
+/* 隐患点描述(以冒号结尾的段落) */
1444
+.ai-judge-result :deep(p:first-child) {
1445
+  margin-top: 0;
1446
+}
1447
+
1448
+/* 确保段落之间的间距一致 */
1449
+.ai-judge-result :deep(p + p) {
1450
+  margin-top: 12px;
1451
+}
1452
+
1453
+/* 优化段落内换行 */
1454
+.ai-judge-result :deep(br) {
1455
+  line-height: 1.8;
1456
+}
1457
+
1458
+.ai-judge-result :deep(p.list-item) {
1459
+  margin: 6px 0;
1460
+  padding-left: 8px;
1461
+  position: relative;
1462
+}
1463
+
1464
+.ai-judge-result :deep(p.article-num) {
1465
+  margin: 8px 0;
1466
+  padding: 8px 12px;
1467
+  background: #f0f9ff;
1468
+  border-left: 3px solid #409eff;
1469
+  border-radius: 4px;
1470
+}
1471
+
1472
+.ai-judge-result :deep(p.article-content) {
1473
+  margin: 8px 0;
1474
+  padding: 10px 12px;
1475
+  background: #fafafa;
1476
+  border-radius: 4px;
1477
+  line-height: 1.8;
1478
+}
1479
+
1480
+.ai-judge-result :deep(strong) {
1481
+  font-weight: 600;
1482
+  color: #303133;
1483
+}
1484
+
1485
+.ai-judge-result :deep(ul),
1486
+.ai-judge-result :deep(ol) {
1487
+  margin: 10px 0;
1488
+  padding-left: 25px;
1489
+}
1490
+
1491
+.ai-judge-result :deep(li) {
1492
+  margin: 6px 0;
1493
+  line-height: 1.8;
1494
+  color: #606266;
1495
+}
1496
+
1497
+.ai-judge-result :deep(blockquote) {
1498
+  margin: 10px 0;
1499
+  padding: 10px 15px;
1500
+  background: #f5f7fa;
1501
+  border-left: 4px solid #409eff;
1502
+  border-radius: 4px;
1503
+}
1504
+
1505
+.ai-judge-result :deep(code) {
1506
+  background: #f5f7fa;
1507
+  padding: 2px 6px;
1508
+  border-radius: 3px;
1509
+  font-family: 'Courier New', monospace;
1510
+  font-size: 13px;
1511
+  color: #e6a23c;
1512
+}
1513
+
1514
+.ai-judge-result :deep(pre) {
1515
+  background: #f5f7fa;
1516
+  padding: 12px;
1517
+  border-radius: 4px;
1518
+  overflow-x: auto;
1519
+  margin: 10px 0;
1520
+}
1521
+
1522
+.ai-judge-result :deep(pre code) {
1523
+  background: transparent;
1524
+  padding: 0;
1525
+  color: #303133;
1526
+}
1527
+
1528
+.ai-judge-result :deep(hr) {
1529
+  border: none;
1530
+  border-top: 1px solid #e4e7ed;
1531
+  margin: 15px 0;
1532
+}
802 1533
 </style>

Loading…
Откажи
Сачувај