yink #17
| @ -1,70 +1,172 @@ | ||||
| <template> | ||||
|   <span> | ||||
|     <template v-for="(part, index) in parts" :key="index"> | ||||
|       <span v-if="part.highlighted" :class="highlightClass"> | ||||
|         {{ part.text }} | ||||
|     <template v-if="isHtml"> | ||||
|       <span v-html="highlightedHtml" /> | ||||
|     </template> | ||||
|     <template v-else> | ||||
|       <span class="text-content"> | ||||
|         <template v-for="(part, index) in parts" :key="index"> | ||||
|           <span | ||||
|             v-if="part.highlighted" | ||||
|             :class="highlightClass" | ||||
|             v-html="textReplaceEmoji(part.text)" | ||||
|           /> | ||||
|           <span v-else v-html="textReplaceEmoji(part.text)" /> | ||||
|         </template> | ||||
|       </span> | ||||
|       <span v-else>{{ part.text }}</span> | ||||
|     </template> | ||||
|   </span> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| import { computed } from 'vue' | ||||
| import { textReplaceEmoji } from '@/utils/emojis' | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   text: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|     required: true | ||||
|   }, | ||||
|   searchText: { | ||||
|     type: String, | ||||
|     default: '', | ||||
|     default: '' | ||||
|   }, | ||||
|   highlightClass: { | ||||
|     type: String, | ||||
|     default: 'highlight', | ||||
|   }, | ||||
|     default: 'highlight' | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| // 检测是否为HTML内容 | ||||
| const isHtml = computed(() => { | ||||
|   return /<[^>]*>/g.test(props.text) | ||||
| }) | ||||
| 
 | ||||
| const escapedSearchText = computed(() => | ||||
|   String(props.searchText).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), | ||||
|   String(props.searchText).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') | ||||
| ) | ||||
| 
 | ||||
| const pattern = computed(() => new RegExp(escapedSearchText.value, 'gi')) | ||||
| 
 | ||||
| const parts = computed(() => { | ||||
|   if (!props.searchText || !props.text) | ||||
|     return [{ text: props.text, highlighted: false }]; | ||||
|   if (!props.searchText || !props.text) return [{ text: props.text, highlighted: false }] | ||||
| 
 | ||||
|   const result = []; | ||||
|   let currentIndex = 0; | ||||
|   const escapedSearchTextValue = escapedSearchText.value; | ||||
|   const searchPattern = new RegExp(`(${escapedSearchTextValue})`, 'gi'); | ||||
|   const result = [] | ||||
|   let currentIndex = 0 | ||||
|   const escapedSearchTextValue = escapedSearchText.value | ||||
|   const searchPattern = new RegExp(`(${escapedSearchTextValue})`, 'gi') | ||||
| 
 | ||||
|   props.text.replace(searchPattern, (match, p1, offset) => { | ||||
|     // 添加非高亮文本 | ||||
|     if (currentIndex < offset) { | ||||
|       result.push({ text: props.text.slice(currentIndex, offset), highlighted: false }); | ||||
|       result.push({ text: props.text.slice(currentIndex, offset), highlighted: false }) | ||||
|     } | ||||
|     // 添加高亮文本 | ||||
|     result.push({ text: p1, highlighted: true }); | ||||
|     result.push({ text: p1, highlighted: true }) | ||||
|     // 更新当前索引 | ||||
|     currentIndex = offset + p1.length; | ||||
|     return p1; // 这个返回值不影响最终结果,只是replace方法的要求 | ||||
|   }); | ||||
|     currentIndex = offset + p1.length | ||||
|     return p1 // 这个返回值不影响最终结果,只是replace方法的要求 | ||||
|   }) | ||||
| 
 | ||||
|   // 添加剩余的非高亮文本(如果有的话) | ||||
|   if (currentIndex < props.text.length) { | ||||
|     result.push({ text: props.text.slice(currentIndex), highlighted: false }); | ||||
|     result.push({ text: props.text.slice(currentIndex), highlighted: false }) | ||||
|   } | ||||
| 
 | ||||
|   return result; | ||||
| }); | ||||
|   return result | ||||
| }) | ||||
| 
 | ||||
| // 处理特殊字符的函数 | ||||
| const processSpecialChars = (text) => { | ||||
|   return ( | ||||
|     text | ||||
|       // 处理换行符 | ||||
|       .replace(/\n/g, '<br>') | ||||
|       // 处理制表符 | ||||
|       .replace(/\t/g, '    ') | ||||
|       // 处理连续空格(保留第一个,其余转换为 ) | ||||
|       .replace(/ {2,}/g, (match) => { | ||||
|         return ' '.repeat(match.length) | ||||
|       }) | ||||
|       // 处理不可见字符(零宽空格等) | ||||
|       .replace(/[\u200B-\u200D\uFEFF]/g, '') | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| // 处理HTML内容的高亮 - 使用字符串处理方法 | ||||
| const highlightedHtml = computed(() => { | ||||
|   if (!props.searchText || !props.text) { | ||||
|     // 先处理特殊字符,再处理表情 | ||||
|     return textReplaceEmoji(processSpecialChars(props.text)) | ||||
|   } | ||||
| 
 | ||||
|   // 对于富文本,使用一个更安全的方法 | ||||
|   // 将HTML内容按标签分割,只对文本部分进行高亮 | ||||
| 
 | ||||
|   // 分割HTML字符串,保护标签 | ||||
|   const parts = [] | ||||
|   let lastIndex = 0 | ||||
|   const tagRegex = /<[^>]*>/g | ||||
|   let tagMatch | ||||
| 
 | ||||
|   // 重置正则表达式的lastIndex | ||||
|   tagRegex.lastIndex = 0 | ||||
| 
 | ||||
|   while ((tagMatch = tagRegex.exec(props.text)) !== null) { | ||||
|     // 添加标签前的文本 | ||||
|     if (tagMatch.index > lastIndex) { | ||||
|       const textBeforeTag = props.text.slice(lastIndex, tagMatch.index) | ||||
|       if (textBeforeTag) { | ||||
|         // 先处理特殊字符,再处理高亮 | ||||
|         const processedText = processSpecialChars(textBeforeTag) | ||||
|         const searchPattern = new RegExp(`(${escapedSearchText.value})`, 'gi') | ||||
|         const highlightedText = processedText.replace( | ||||
|           searchPattern, | ||||
|           `<span class="${props.highlightClass}">$1</span>` | ||||
|         ) | ||||
|         parts.push(highlightedText) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // 添加标签本身(不处理) | ||||
|     parts.push(tagMatch[0]) | ||||
|     lastIndex = tagMatch.index + tagMatch[0].length | ||||
|   } | ||||
| 
 | ||||
|   // 添加最后一个标签后的文本 | ||||
|   if (lastIndex < props.text.length) { | ||||
|     const textAfterLastTag = props.text.slice(lastIndex) | ||||
|     if (textAfterLastTag) { | ||||
|       // 先处理特殊字符,再处理高亮 | ||||
|       const processedText = processSpecialChars(textAfterLastTag) | ||||
|       const searchPattern = new RegExp(`(${escapedSearchText.value})`, 'gi') | ||||
|       const highlightedText = processedText.replace( | ||||
|         searchPattern, | ||||
|         `<span class="${props.highlightClass}">$1</span>` | ||||
|       ) | ||||
|       parts.push(highlightedText) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   let html = parts.join('') | ||||
|   // 最后处理表情 | ||||
|   return textReplaceEmoji(html) | ||||
| }) | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .highlight { | ||||
|   color: #7a58de; | ||||
| } | ||||
| 
 | ||||
| .text-content { | ||||
|   white-space: pre-wrap; | ||||
|   word-break: break-word; | ||||
|   :deep(.emoji) { | ||||
|     vertical-align: text-bottom!important; | ||||
|     margin: 0 5px !important; | ||||
|     width: 22px !important; | ||||
|     height: 22px !important; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | ||||
| @ -137,7 +137,7 @@ | ||||
|                         @click="toDialogueByMember(item)" | ||||
|                         :searchResultKey="'search_by_member_condition'" | ||||
|                         :searchItem="item" | ||||
|                         :searchText="state.searchText" | ||||
|                         :searchText="props?.searchRecordByConditionText" | ||||
|                         :searchRecordDetail="true" | ||||
|                       ></searchItem> | ||||
|                     </div> | ||||
| @ -305,6 +305,7 @@ import { parseTime } from '@/utils/datetime' | ||||
| import { fileFormatSize, fileSuffix } from '@/utils/strings' | ||||
| import { NImage, NInfiniteScroll, NScrollbar, NIcon, NDatePicker } from 'naive-ui' | ||||
| import { MessageComponents } from '@/constant/message' | ||||
| import { checkFileCanPreview } from '@/utils/helper/form' | ||||
| 
 | ||||
| const emits = defineEmits([ | ||||
|   'clearSearchMemberByAlphabet', | ||||
| @ -667,15 +668,23 @@ const queryAllSearch = () => { | ||||
| 
 | ||||
| //文件类型图标 | ||||
| const fileTypeAvatar = (fileType) => { | ||||
|   //PDF文件扩展名映射 | ||||
|   const PDF_EXTENSIONS = ['PDF', 'pdf'] | ||||
|   // Excel文件扩展名映射 | ||||
|   const EXCEL_EXTENSIONS = ['XLS', 'XLSX', 'CSV', 'xls', 'xlsx', 'csv'] | ||||
|   // Word文件扩展名映射 | ||||
|   const WORD_EXTENSIONS = ['DOC', 'DOCX', 'RTF', 'DOT', 'DOTX', 'doc', 'docx', 'rtf', 'dot', 'dotx'] | ||||
|   // PPT文件扩展名映射 | ||||
|   const PPT_EXTENSIONS = ['PPT', 'PPTX', 'PPS', 'PPSX', 'ppt', 'pptx', 'pps', 'ppsx'] | ||||
|   let file_type_avatar = fileType_Files | ||||
|   if (fileType) { | ||||
|     if (fileType === 'ppt' || fileType === 'pptx') { | ||||
|     if (PPT_EXTENSIONS.includes(fileType)) { | ||||
|       file_type_avatar = fileType_PPT | ||||
|     } else if (fileType === 'pdf') { | ||||
|     } else if (PDF_EXTENSIONS.includes(fileType)) { | ||||
|       file_type_avatar = fileType_PDF | ||||
|     } else if (fileType === 'doc' || fileType === 'docx') { | ||||
|     } else if (WORD_EXTENSIONS.includes(fileType)) { | ||||
|       file_type_avatar = fileType_WORD | ||||
|     } else if (fileType === 'xls' || fileType === 'xlsx') { | ||||
|     } else if (EXCEL_EXTENSIONS.includes(fileType)) { | ||||
|       file_type_avatar = fileType_EXCEL | ||||
|     } else { | ||||
|       file_type_avatar = fileType_Files | ||||
| @ -693,11 +702,15 @@ const previewPDF = (item) => { | ||||
|   //     downloadAndOpenFile(item) | ||||
|   //   }) | ||||
|   // } | ||||
|   window.open( | ||||
|     `${import.meta.env.VITE_PAGE_URL}/office?url=${item.extra.path}`, | ||||
|     '_blank', | ||||
|     'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no' | ||||
|   ) | ||||
|   if (checkFileCanPreview(item?.extra?.path || '')) { | ||||
|     window.open( | ||||
|       `${import.meta.env.VITE_PAGE_URL}/office?url=${item.extra.path}`, | ||||
|       '_blank', | ||||
|       'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no' | ||||
|     ) | ||||
|   } else { | ||||
|     toDialogueByMember(item) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const downloadAndOpenFile = (item) => { | ||||
| @ -951,11 +964,11 @@ body:deep(.round-3) { | ||||
|               border-radius: 4px; | ||||
|               cursor: pointer; | ||||
|               border-bottom: 1px solid #f8f8f8; | ||||
|                | ||||
| 
 | ||||
|               &:hover { | ||||
|                 background-color: rgba(70, 41, 157, 0.1) | ||||
|                 background-color: rgba(70, 41, 157, 0.1); | ||||
|               } | ||||
|                | ||||
| 
 | ||||
|               .attachment-avatar { | ||||
|                 display: flex; | ||||
|                 flex-direction: row; | ||||
|  | ||||
| @ -69,9 +69,17 @@ | ||||
|           class="text-[12px] font-regular" | ||||
|           :text="resultDetail" | ||||
|           :searchText="props.searchText" | ||||
|           v-if="props.searchItem?.msg_type !== 3 && props.searchItem?.msg_type !== 6" | ||||
|           v-if=" | ||||
|             props.searchItem?.msg_type !== 3 && | ||||
|             props.searchItem?.msg_type !== 5 && | ||||
|             props.searchItem?.msg_type !== 6 | ||||
|           " | ||||
|         /> | ||||
|         <div class="message-component-wrapper" v-if="props.searchItem?.msg_type === 3" @click.stop> | ||||
|         <div | ||||
|           class="message-component-wrapper" | ||||
|           v-if="props.searchItem?.msg_type === 3 || props.searchItem?.msg_type === 5" | ||||
|           @click.stop | ||||
|         > | ||||
|           <component | ||||
|             :is="MessageComponents[props.searchItem?.msg_type] || 'unknown-message'" | ||||
|             :extra="resultDetail" | ||||
| @ -122,6 +130,7 @@ import { ref, watch, computed, onMounted, onUnmounted, reactive, defineProps } f | ||||
| import HighlightText from './highLightText.vue' | ||||
| import { beautifyTime } from '@/utils/datetime' | ||||
| import { ChatMsgTypeMapping, MessageComponents } from '@/constant/message' | ||||
| import { checkFileCanPreview } from '@/utils/helper/form' | ||||
| const props = defineProps({ | ||||
|   searchItem: Object | Number, | ||||
|   searchResultKey: { | ||||
| @ -291,7 +300,9 @@ const resultDetail = computed(() => { | ||||
|       result_detail = | ||||
|         props.searchItem?.msg_type === 1 | ||||
|           ? props.searchItem?.extra?.content | ||||
|           : props.searchItem?.msg_type === 3 || props.searchItem?.msg_type === 6 | ||||
|           : props.searchItem?.msg_type === 3 || | ||||
|             props.searchItem?.msg_type === 5 || | ||||
|             props.searchItem?.msg_type === 6 | ||||
|           ? props.searchItem?.extra | ||||
|           : ChatMsgTypeMapping[props.searchItem?.msg_type] | ||||
|       break | ||||
| @ -310,11 +321,16 @@ const previewPDF = (item) => { | ||||
|   //     downloadAndOpenFile(item) | ||||
|   //   }) | ||||
|   // } | ||||
|   window.open( | ||||
|     `${import.meta.env.VITE_PAGE_URL}/office?url=${item}`, | ||||
|     '_blank', | ||||
|     'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no' | ||||
|   ) | ||||
|   if (checkFileCanPreview(item || '')) { | ||||
|     window.open( | ||||
|       `${import.meta.env.VITE_PAGE_URL}/office?url=${item}`, | ||||
|       '_blank', | ||||
|       'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no' | ||||
|     ) | ||||
|   } else { | ||||
|     //由于聊天记录本身有跳转到指定位置的逻辑,所以这里不需要再做跳转 | ||||
|     window['$message'].warning('暂不支持在线预览该类型文件') | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| @ -377,7 +393,8 @@ const previewPDF = (item) => { | ||||
|       } | ||||
|       .file-message-wrapper { | ||||
|         .condition-each-result-attachments { | ||||
|           width: 289px; | ||||
|           min-width: 289px; | ||||
|           max-width: 660px; | ||||
|           height: 62px; | ||||
|           display: flex; | ||||
|           flex-direction: row; | ||||
| @ -447,6 +464,7 @@ const previewPDF = (item) => { | ||||
|       span { | ||||
|         color: #191919; | ||||
|         word-break: break-all; | ||||
|         max-width: 660px; | ||||
|       } | ||||
|       .searchRecordDetail-fastLocal { | ||||
|         display: none; | ||||
|  | ||||
| @ -345,3 +345,29 @@ export const formatNumberWithCommas = (num) => { | ||||
|   } | ||||
|   return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | ||||
| }; | ||||
| // 判断文件是否可以预览
 | ||||
| export const checkFileCanPreview = (path) => { | ||||
|   if (!path) { | ||||
|     return false | ||||
|   } | ||||
|   //PDF文件扩展名映射
 | ||||
|   const PDF_EXTENSIONS = ['PDF', 'pdf'] | ||||
|   // Excel文件扩展名映射
 | ||||
|   const EXCEL_EXTENSIONS = ['XLS', 'XLSX', 'CSV', 'xls', 'xlsx', 'csv'] | ||||
|   // Word文件扩展名映射
 | ||||
|   const WORD_EXTENSIONS = ['DOC', 'DOCX', 'RTF', 'DOT', 'DOTX', 'doc', 'docx', 'rtf', 'dot', 'dotx'] | ||||
|   // PPT文件扩展名映射
 | ||||
|   const PPT_EXTENSIONS = ['PPT', 'PPTX', 'PPS', 'PPSX', 'ppt', 'pptx', 'pps', 'ppsx'] | ||||
| 
 | ||||
|   // 获取文件扩展名
 | ||||
|   function getFileExtension(filepath) { | ||||
|     const parts = filepath?.split('.') | ||||
|     return parts?.length > 1 ? parts?.pop()?.toUpperCase() : '' | ||||
|   } | ||||
| 
 | ||||
|   const extension = getFileExtension(path) | ||||
|   return PDF_EXTENSIONS.includes(extension) ||  | ||||
|          EXCEL_EXTENSIONS.includes(extension) ||  | ||||
|          WORD_EXTENSIONS.includes(extension) ||  | ||||
|          PPT_EXTENSIONS.includes(extension) | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user