|
|
@@ -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>
|