Переглянути джерело

移动端-安全检查-ai研判功能支持数据库保存

liuzhuo 10 години тому
джерело
коміт
f958035815
1 змінених файлів з 398 додано та 4 видалено
  1. 398
    4
      src/view/safeCheck/safeCheck_edit/index.vue

+ 398
- 4
src/view/safeCheck/safeCheck_edit/index.vue Переглянути файл

@@ -1,6 +1,6 @@
1 1
 <template>
2 2
   <div class="h5-container">
3
-    <van-nav-bar title="隐患登记" @click-left="onClickLeft">
3
+    <van-nav-bar title="检查任务编辑" @click-left="onClickLeft">
4 4
     </van-nav-bar>
5 5
     <van-form @submit="baocun" ref="formRef">
6 6
         <van-field
@@ -103,6 +103,34 @@
103 103
           </template>
104 104
         </van-field>
105 105
 
106
+        <!-- AI判断按钮 -->
107
+        <div style="margin: 10px 0; padding: 0 16px;">
108
+          <van-button 
109
+            type="primary" 
110
+            size="small" 
111
+            block
112
+            :loading="aiJudging"
113
+            @click="handleAIJudge"
114
+            :disabled="!form.checkResult || form.checkResult.trim() === ''"
115
+          >
116
+            <van-icon name="chat-o" style="margin-right: 5px;" />
117
+            {{ aiJudging ? 'AI判断中...' : 'AI研判' }}
118
+          </van-button>
119
+        </div>
120
+
121
+        <!-- AI判断结果显示 -->
122
+        <van-field
123
+          v-if="aiJudgeResult"
124
+          border
125
+          readonly
126
+          label="AI判断结果"
127
+          :colon="true"
128
+        >
129
+          <template #input>
130
+            <div class="ai-judge-result" v-html="renderedJudgeResult"></div>
131
+          </template>
132
+        </van-field>
133
+
106 134
         <van-field
107 135
           border
108 136
           readonly
@@ -217,7 +245,7 @@
217 245
 </template>
218 246
 
219 247
 <script setup>
220
-import { ref, reactive, onMounted, getCurrentInstance, nextTick, onUnmounted } from 'vue';
248
+import { ref, reactive, onMounted, getCurrentInstance, nextTick, onUnmounted, computed } from 'vue';
221 249
 import { closeToast, Dialog, showFailToast, showLoadingToast, showSuccessToast, showToast } from 'vant';
222 250
 
223 251
 const { proxy } = getCurrentInstance();
@@ -235,6 +263,7 @@ const form = ref({
235 263
   notes: '',
236 264
   ifFlag: '',
237 265
   checkResult: '',
266
+  aiJudge: '', // AI判断结果字段
238 267
   hdId: '',
239 268
   hdSubmitTime: ''
240 269
 });
@@ -252,7 +281,13 @@ onMounted(async () => {
252 281
 
253 282
     if (response.data.code === 0) {
254 283
       form.value = response.data.data;
255
-      console.log('查询结果', response.data.data);
284
+      // 如果有 AI 判断结果,同步到显示变量
285
+      if (form.value.aiJudge) {
286
+        aiJudgeResult.value = form.value.aiJudge;
287
+      } else {
288
+        // 如果没有AI判断结果,清空显示
289
+        aiJudgeResult.value = '';
290
+      }
256 291
     } else {
257 292
       showToast({
258 293
         message: '操作失败!' + response.data.msg
@@ -305,14 +340,20 @@ onMounted(async () => {
305 340
     onClickLeft();
306 341
   };
307 342
   const baocun = async () => {
343
+    // 强制同步 AI 判断结果到 form(确保数据不丢失)
344
+    if (aiJudgeResult.value) {
345
+      form.value.aiJudge = aiJudgeResult.value;
346
+    }
347
+    
308 348
     var url = '/sgsafe/CheckResultItem/save';
309 349
     var param = {
310 350
       json: JSON.stringify(form.value)
311 351
     };
352
+    
312 353
     proxy.$axios.post(url, param).then(response => {
313 354
       if (response.data.code === 0) {
314 355
         const data = response.data.data;
315
-        console.log('插入时返回的数据', data)
356
+        console.log('保存成功,返回的数据:', data);
316 357
         onClickLeft()
317 358
       } else {
318 359
         showToast({
@@ -600,6 +641,243 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
600 641
     }
601 642
   });
602 643
 
644
+  /***********************AI判断法律法规功能******************************/
645
+  // AI判断相关状态
646
+  const aiJudging = ref(false);
647
+  const aiJudgeResult = ref('');
648
+  
649
+  // Markdown 渲染库
650
+  let marked = null;
651
+  let DOMPurify = null;
652
+  
653
+  // 初始化 Markdown 库
654
+  async function initMarkdownLibs() {
655
+    try {
656
+      // 动态导入 marked
657
+      const markedModule = await import('marked');
658
+      marked = markedModule.marked || markedModule.default || markedModule;
659
+      
660
+      // 动态导入 DOMPurify
661
+      const dompurifyModule = await import('dompurify');
662
+      DOMPurify = dompurifyModule.default || dompurifyModule;
663
+    } catch (error) {
664
+      console.warn('Markdown libraries not available, using plain text', error);
665
+      marked = {
666
+        parse: (text) => text
667
+      };
668
+      DOMPurify = {
669
+        sanitize: (html) => html
670
+      };
671
+    }
672
+  }
673
+  
674
+  // 初始化 Markdown 库
675
+  initMarkdownLibs();
676
+  
677
+  // 渲染后的判断结果
678
+  const renderedJudgeResult = computed(() => {
679
+    if (!aiJudgeResult.value) return '';
680
+    
681
+    try {
682
+      if (!marked || !DOMPurify) {
683
+        // 如果库还没加载,先返回纯文本,但格式化一下
684
+        return formatPlainText(aiJudgeResult.value);
685
+      }
686
+      
687
+      // 使用 marked 解析 markdown
688
+      const html = marked.parse ? marked.parse(aiJudgeResult.value) : marked(aiJudgeResult.value);
689
+      // 使用 DOMPurify 清理 HTML
690
+      return DOMPurify.sanitize ? DOMPurify.sanitize(html) : html;
691
+    } catch (error) {
692
+      console.error('Markdown解析错误:', error);
693
+      return formatPlainText(aiJudgeResult.value);
694
+    }
695
+  });
696
+  
697
+  // 格式化纯文本(当 Markdown 库不可用时)
698
+  function formatPlainText(text) {
699
+    if (!text) return '';
700
+    
701
+    // 将 Markdown 格式转换为 HTML
702
+    let formatted = text
703
+      // 标题(按顺序处理,从多级到单级)
704
+      .replace(/^####\s+(.+)$/gm, '<h4>$1</h4>')
705
+      .replace(/^###\s+(.+)$/gm, '<h3>$1</h3>')
706
+      .replace(/^##\s+(.+)$/gm, '<h2>$1</h2>')
707
+      .replace(/^#\s+(.+)$/gm, '<h1>$1</h1>')
708
+      // 粗体(支持 **text** 和 **text** 格式)
709
+      .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
710
+      // 斜体
711
+      .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
+      // 多个连续换行转换为段落分隔
720
+      .replace(/\n\n+/g, '</p><p>')
721
+      // 单个换行转换为换行符
722
+      .replace(/\n/g, '<br/>');
723
+    
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
+    }
742
+    
743
+    return formatted;
744
+  }
745
+
746
+  // 处理AI判断
747
+  async function handleAIJudge() {
748
+    if (!form.value.checkResult || form.value.checkResult.trim() === '') {
749
+      showToast({
750
+        type: 'fail',
751
+        message: '请先输入检查结果'
752
+      });
753
+      return;
754
+    }
755
+
756
+    aiJudging.value = true;
757
+    aiJudgeResult.value = '';
758
+
759
+    try {
760
+      const prompt = buildJudgePrompt(form.value.checkResult);
761
+      const messages = [{ role: 'user', content: prompt }];
762
+
763
+      await callAIStream(messages, (content, isEnd) => {
764
+        aiJudgeResult.value = content;
765
+        // 实时同步到 form.aiJudge
766
+        form.value.aiJudge = content;
767
+        
768
+        if (isEnd) {
769
+          aiJudging.value = false;
770
+          // 确保最终结果保存到 form
771
+          form.value.aiJudge = content;
772
+          showToast({
773
+            type: 'success',
774
+            message: 'AI判断完成'
775
+          });
776
+        }
777
+      });
778
+    } catch (error) {
779
+      console.error('AI判断失败:', error);
780
+      aiJudging.value = false;
781
+      showToast({
782
+        type: 'fail',
783
+        message: 'AI判断失败,请重试'
784
+      });
785
+    }
786
+  }
787
+
788
+  // 构造判断提示词
789
+  function buildJudgePrompt(checkResult) {
790
+    const prompt = `请根据以下检查结果内容,判断违反了哪一条法律法规,并详细说明:
791
+
792
+检查结果内容:
793
+${checkResult}
794
+
795
+请提供以下内容:
796
+1. 明确指出违反了哪些法律法规(包括法律名称、条款号、具体条款内容)
797
+2. 说明为什么该检查结果违反了这些法律法规
798
+3. 如果可能,提供相关的处罚依据或整改建议
799
+
800
+请用清晰、专业的语言进行回答,确保法律法规名称和条款号准确无误。`;
801
+    
802
+    return prompt;
803
+  }
804
+
805
+  // 调用AI流式接口
806
+  async function callAIStream(messageHistory, callback) {
807
+    try {
808
+      const url = `${import.meta.env.VITE_BASE_API}/sgsafe/deepseek/chat-stream`;
809
+      
810
+      const response = await fetch(url, {
811
+        method: 'POST',
812
+        headers: {
813
+          'Content-Type': 'application/json',
814
+          'token': localStorage.getItem('token') || '',
815
+          'userId': localStorage.getItem('userId') || ''
816
+        },
817
+        body: JSON.stringify({ messages: messageHistory })
818
+      });
819
+      
820
+      if (!response.ok) {
821
+        throw new Error(`HTTP error! status: ${response.status}`);
822
+      }
823
+      
824
+      if (!response.body) {
825
+        throw new Error('ReadableStream not supported');
826
+      }
827
+      
828
+      const reader = response.body.getReader();
829
+      const decoder = new TextDecoder('utf-8');
830
+      let buffer = '';
831
+      let fullResponse = '';
832
+      
833
+      while (true) {
834
+        const { done, value } = await reader.read();
835
+        if (done) {
836
+          callback(fullResponse, true);
837
+          break;
838
+        }
839
+        
840
+        const chunk = decoder.decode(value, { stream: true });
841
+        buffer += chunk;
842
+        
843
+        const lines = buffer.split('\n');
844
+        buffer = lines.pop();
845
+        
846
+        for (const line of lines) {
847
+          if (line.trim() === '' || !line.startsWith('data:')) continue;
848
+          
849
+          const data = line.slice(5).trim();
850
+          if (data === '[DONE]') {
851
+            callback(fullResponse, true);
852
+            return;
853
+          }
854
+          
855
+          try {
856
+            const jsonData = JSON.parse(data);
857
+            if (jsonData.choices?.[0]?.delta) {
858
+              let content = '';
859
+              if (jsonData.choices[0].delta.reasoning_content) {
860
+                content += jsonData.choices[0].delta.reasoning_content;
861
+              }
862
+              if (jsonData.choices[0].delta.content) {
863
+                content += jsonData.choices[0].delta.content;
864
+              }
865
+              if (content) {
866
+                fullResponse += content;
867
+                callback(fullResponse, false);
868
+              }
869
+            }
870
+          } catch (e) {
871
+            console.error('JSON解析错误:', e);
872
+          }
873
+        }
874
+      }
875
+    } catch (error) {
876
+      console.error('AI调用失败:', error);
877
+      throw error;
878
+    }
879
+  }
880
+
603 881
 </script>
604 882
 
605 883
 <style scoped>
@@ -626,4 +904,120 @@ import AttachmentS3Image from '@/components/AttachmentS3Image.vue';
626 904
   }
627 905
 }
628 906
 
907
+/* AI判断结果样式 */
908
+.ai-judge-result {
909
+  padding: 15px 0;
910
+  color: #323233;
911
+  line-height: 1.8;
912
+  font-size: 14px;
913
+  word-break: break-word;
914
+}
915
+
916
+.ai-judge-result :deep(h1) {
917
+  font-size: 18px;
918
+  font-weight: 600;
919
+  color: #303133;
920
+  margin: 15px 0 10px 0;
921
+  padding-bottom: 8px;
922
+  border-bottom: 2px solid #e4e7ed;
923
+}
924
+
925
+.ai-judge-result :deep(h2) {
926
+  font-size: 16px;
927
+  font-weight: 600;
928
+  color: #303133;
929
+  margin: 15px 0 10px 0;
930
+  padding-bottom: 6px;
931
+  border-bottom: 1px solid #e4e7ed;
932
+}
933
+
934
+.ai-judge-result :deep(h3) {
935
+  font-size: 15px;
936
+  font-weight: 600;
937
+  color: #409eff;
938
+  margin: 12px 0 8px 0;
939
+}
940
+
941
+.ai-judge-result :deep(p) {
942
+  margin: 10px 0;
943
+  line-height: 1.8;
944
+  color: #606266;
945
+}
946
+
947
+.ai-judge-result :deep(p.list-item) {
948
+  margin: 6px 0;
949
+  padding-left: 8px;
950
+  position: relative;
951
+}
952
+
953
+.ai-judge-result :deep(p.article-num) {
954
+  margin: 8px 0;
955
+  padding: 8px 12px;
956
+  background: #f0f9ff;
957
+  border-left: 3px solid #409eff;
958
+  border-radius: 4px;
959
+}
960
+
961
+.ai-judge-result :deep(p.article-content) {
962
+  margin: 8px 0;
963
+  padding: 10px 12px;
964
+  background: #fafafa;
965
+  border-radius: 4px;
966
+  line-height: 1.8;
967
+}
968
+
969
+.ai-judge-result :deep(strong) {
970
+  font-weight: 600;
971
+  color: #303133;
972
+}
973
+
974
+.ai-judge-result :deep(ul),
975
+.ai-judge-result :deep(ol) {
976
+  margin: 10px 0;
977
+  padding-left: 25px;
978
+}
979
+
980
+.ai-judge-result :deep(li) {
981
+  margin: 6px 0;
982
+  line-height: 1.8;
983
+  color: #606266;
984
+}
985
+
986
+.ai-judge-result :deep(blockquote) {
987
+  margin: 10px 0;
988
+  padding: 10px 15px;
989
+  background: #f5f7fa;
990
+  border-left: 4px solid #409eff;
991
+  border-radius: 4px;
992
+}
993
+
994
+.ai-judge-result :deep(code) {
995
+  background: #f5f7fa;
996
+  padding: 2px 6px;
997
+  border-radius: 3px;
998
+  font-family: 'Courier New', monospace;
999
+  font-size: 13px;
1000
+  color: #e6a23c;
1001
+}
1002
+
1003
+.ai-judge-result :deep(pre) {
1004
+  background: #f5f7fa;
1005
+  padding: 12px;
1006
+  border-radius: 4px;
1007
+  overflow-x: auto;
1008
+  margin: 10px 0;
1009
+}
1010
+
1011
+.ai-judge-result :deep(pre code) {
1012
+  background: transparent;
1013
+  padding: 0;
1014
+  color: #303133;
1015
+}
1016
+
1017
+.ai-judge-result :deep(hr) {
1018
+  border: none;
1019
+  border-top: 1px solid #e4e7ed;
1020
+  margin: 15px 0;
1021
+}
1022
+
629 1023
 </style>

Завантаження…
Відмінити
Зберегти