yink #17
| @ -1,70 +1,172 @@ | |||||||
| <template> | <template> | ||||||
|   <span> |   <span> | ||||||
|     <template v-for="(part, index) in parts" :key="index"> |     <template v-if="isHtml"> | ||||||
|       <span v-if="part.highlighted" :class="highlightClass"> |       <span v-html="highlightedHtml" /> | ||||||
|         {{ part.text }} |     </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> | ||||||
|       <span v-else>{{ part.text }}</span> |  | ||||||
|     </template> |     </template> | ||||||
|   </span> |   </span> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup> | <script setup> | ||||||
| import { computed } from 'vue' | import { computed } from 'vue' | ||||||
|  | import { textReplaceEmoji } from '@/utils/emojis' | ||||||
| 
 | 
 | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   text: { |   text: { | ||||||
|     type: String, |     type: String, | ||||||
|     required: true, |     required: true | ||||||
|   }, |   }, | ||||||
|   searchText: { |   searchText: { | ||||||
|     type: String, |     type: String, | ||||||
|     default: '', |     default: '' | ||||||
|   }, |   }, | ||||||
|   highlightClass: { |   highlightClass: { | ||||||
|     type: String, |     type: String, | ||||||
|     default: 'highlight', |     default: 'highlight' | ||||||
|   }, |   } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // 检测是否为HTML内容 | ||||||
|  | const isHtml = computed(() => { | ||||||
|  |   return /<[^>]*>/g.test(props.text) | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| const escapedSearchText = computed(() => | const escapedSearchText = computed(() => | ||||||
|   String(props.searchText).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), |   String(props.searchText).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const pattern = computed(() => new RegExp(escapedSearchText.value, 'gi')) | const pattern = computed(() => new RegExp(escapedSearchText.value, 'gi')) | ||||||
| 
 | 
 | ||||||
| const parts = computed(() => { | const parts = computed(() => { | ||||||
|   if (!props.searchText || !props.text) |   if (!props.searchText || !props.text) return [{ text: props.text, highlighted: false }] | ||||||
|     return [{ text: props.text, highlighted: false }]; |  | ||||||
| 
 | 
 | ||||||
|   const result = []; |   const result = [] | ||||||
|   let currentIndex = 0; |   let currentIndex = 0 | ||||||
|   const escapedSearchTextValue = escapedSearchText.value; |   const escapedSearchTextValue = escapedSearchText.value | ||||||
|   const searchPattern = new RegExp(`(${escapedSearchTextValue})`, 'gi'); |   const searchPattern = new RegExp(`(${escapedSearchTextValue})`, 'gi') | ||||||
| 
 | 
 | ||||||
|   props.text.replace(searchPattern, (match, p1, offset) => { |   props.text.replace(searchPattern, (match, p1, offset) => { | ||||||
|     // 添加非高亮文本 |     // 添加非高亮文本 | ||||||
|     if (currentIndex < 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; |     currentIndex = offset + p1.length | ||||||
|     return p1; // 这个返回值不影响最终结果,只是replace方法的要求 |     return p1 // 这个返回值不影响最终结果,只是replace方法的要求 | ||||||
|   }); |   }) | ||||||
| 
 | 
 | ||||||
|   // 添加剩余的非高亮文本(如果有的话) |   // 添加剩余的非高亮文本(如果有的话) | ||||||
|   if (currentIndex < props.text.length) { |   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> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .highlight { | .highlight { | ||||||
|   color: #7a58de; |   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> | </style> | ||||||
|  | |||||||
| @ -137,7 +137,7 @@ | |||||||
|                         @click="toDialogueByMember(item)" |                         @click="toDialogueByMember(item)" | ||||||
|                         :searchResultKey="'search_by_member_condition'" |                         :searchResultKey="'search_by_member_condition'" | ||||||
|                         :searchItem="item" |                         :searchItem="item" | ||||||
|                         :searchText="state.searchText" |                         :searchText="props?.searchRecordByConditionText" | ||||||
|                         :searchRecordDetail="true" |                         :searchRecordDetail="true" | ||||||
|                       ></searchItem> |                       ></searchItem> | ||||||
|                     </div> |                     </div> | ||||||
| @ -305,6 +305,7 @@ import { parseTime } from '@/utils/datetime' | |||||||
| import { fileFormatSize, fileSuffix } from '@/utils/strings' | import { fileFormatSize, fileSuffix } from '@/utils/strings' | ||||||
| import { NImage, NInfiniteScroll, NScrollbar, NIcon, NDatePicker } from 'naive-ui' | import { NImage, NInfiniteScroll, NScrollbar, NIcon, NDatePicker } from 'naive-ui' | ||||||
| import { MessageComponents } from '@/constant/message' | import { MessageComponents } from '@/constant/message' | ||||||
|  | import { checkFileCanPreview } from '@/utils/helper/form' | ||||||
| 
 | 
 | ||||||
| const emits = defineEmits([ | const emits = defineEmits([ | ||||||
|   'clearSearchMemberByAlphabet', |   'clearSearchMemberByAlphabet', | ||||||
| @ -667,15 +668,23 @@ const queryAllSearch = () => { | |||||||
| 
 | 
 | ||||||
| //文件类型图标 | //文件类型图标 | ||||||
| const fileTypeAvatar = (fileType) => { | 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 |   let file_type_avatar = fileType_Files | ||||||
|   if (fileType) { |   if (fileType) { | ||||||
|     if (fileType === 'ppt' || fileType === 'pptx') { |     if (PPT_EXTENSIONS.includes(fileType)) { | ||||||
|       file_type_avatar = fileType_PPT |       file_type_avatar = fileType_PPT | ||||||
|     } else if (fileType === 'pdf') { |     } else if (PDF_EXTENSIONS.includes(fileType)) { | ||||||
|       file_type_avatar = fileType_PDF |       file_type_avatar = fileType_PDF | ||||||
|     } else if (fileType === 'doc' || fileType === 'docx') { |     } else if (WORD_EXTENSIONS.includes(fileType)) { | ||||||
|       file_type_avatar = fileType_WORD |       file_type_avatar = fileType_WORD | ||||||
|     } else if (fileType === 'xls' || fileType === 'xlsx') { |     } else if (EXCEL_EXTENSIONS.includes(fileType)) { | ||||||
|       file_type_avatar = fileType_EXCEL |       file_type_avatar = fileType_EXCEL | ||||||
|     } else { |     } else { | ||||||
|       file_type_avatar = fileType_Files |       file_type_avatar = fileType_Files | ||||||
| @ -693,11 +702,15 @@ const previewPDF = (item) => { | |||||||
|   //     downloadAndOpenFile(item) |   //     downloadAndOpenFile(item) | ||||||
|   //   }) |   //   }) | ||||||
|   // } |   // } | ||||||
|   window.open( |   if (checkFileCanPreview(item?.extra?.path || '')) { | ||||||
|     `${import.meta.env.VITE_PAGE_URL}/office?url=${item.extra.path}`, |     window.open( | ||||||
|     '_blank', |       `${import.meta.env.VITE_PAGE_URL}/office?url=${item.extra.path}`, | ||||||
|     'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no' |       '_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) => { | const downloadAndOpenFile = (item) => { | ||||||
| @ -951,11 +964,11 @@ body:deep(.round-3) { | |||||||
|               border-radius: 4px; |               border-radius: 4px; | ||||||
|               cursor: pointer; |               cursor: pointer; | ||||||
|               border-bottom: 1px solid #f8f8f8; |               border-bottom: 1px solid #f8f8f8; | ||||||
|                | 
 | ||||||
|               &:hover { |               &:hover { | ||||||
|                 background-color: rgba(70, 41, 157, 0.1) |                 background-color: rgba(70, 41, 157, 0.1); | ||||||
|               } |               } | ||||||
|                | 
 | ||||||
|               .attachment-avatar { |               .attachment-avatar { | ||||||
|                 display: flex; |                 display: flex; | ||||||
|                 flex-direction: row; |                 flex-direction: row; | ||||||
|  | |||||||
| @ -69,9 +69,17 @@ | |||||||
|           class="text-[12px] font-regular" |           class="text-[12px] font-regular" | ||||||
|           :text="resultDetail" |           :text="resultDetail" | ||||||
|           :searchText="props.searchText" |           :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 |           <component | ||||||
|             :is="MessageComponents[props.searchItem?.msg_type] || 'unknown-message'" |             :is="MessageComponents[props.searchItem?.msg_type] || 'unknown-message'" | ||||||
|             :extra="resultDetail" |             :extra="resultDetail" | ||||||
| @ -122,6 +130,7 @@ import { ref, watch, computed, onMounted, onUnmounted, reactive, defineProps } f | |||||||
| import HighlightText from './highLightText.vue' | import HighlightText from './highLightText.vue' | ||||||
| import { beautifyTime } from '@/utils/datetime' | import { beautifyTime } from '@/utils/datetime' | ||||||
| import { ChatMsgTypeMapping, MessageComponents } from '@/constant/message' | import { ChatMsgTypeMapping, MessageComponents } from '@/constant/message' | ||||||
|  | import { checkFileCanPreview } from '@/utils/helper/form' | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   searchItem: Object | Number, |   searchItem: Object | Number, | ||||||
|   searchResultKey: { |   searchResultKey: { | ||||||
| @ -291,7 +300,9 @@ const resultDetail = computed(() => { | |||||||
|       result_detail = |       result_detail = | ||||||
|         props.searchItem?.msg_type === 1 |         props.searchItem?.msg_type === 1 | ||||||
|           ? props.searchItem?.extra?.content |           ? 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 |           ? props.searchItem?.extra | ||||||
|           : ChatMsgTypeMapping[props.searchItem?.msg_type] |           : ChatMsgTypeMapping[props.searchItem?.msg_type] | ||||||
|       break |       break | ||||||
| @ -310,11 +321,16 @@ const previewPDF = (item) => { | |||||||
|   //     downloadAndOpenFile(item) |   //     downloadAndOpenFile(item) | ||||||
|   //   }) |   //   }) | ||||||
|   // } |   // } | ||||||
|   window.open( |   if (checkFileCanPreview(item || '')) { | ||||||
|     `${import.meta.env.VITE_PAGE_URL}/office?url=${item}`, |     window.open( | ||||||
|     '_blank', |       `${import.meta.env.VITE_PAGE_URL}/office?url=${item}`, | ||||||
|     'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no' |       '_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> | </script> | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @ -377,7 +393,8 @@ const previewPDF = (item) => { | |||||||
|       } |       } | ||||||
|       .file-message-wrapper { |       .file-message-wrapper { | ||||||
|         .condition-each-result-attachments { |         .condition-each-result-attachments { | ||||||
|           width: 289px; |           min-width: 289px; | ||||||
|  |           max-width: 660px; | ||||||
|           height: 62px; |           height: 62px; | ||||||
|           display: flex; |           display: flex; | ||||||
|           flex-direction: row; |           flex-direction: row; | ||||||
| @ -447,6 +464,7 @@ const previewPDF = (item) => { | |||||||
|       span { |       span { | ||||||
|         color: #191919; |         color: #191919; | ||||||
|         word-break: break-all; |         word-break: break-all; | ||||||
|  |         max-width: 660px; | ||||||
|       } |       } | ||||||
|       .searchRecordDetail-fastLocal { |       .searchRecordDetail-fastLocal { | ||||||
|         display: none; |         display: none; | ||||||
|  | |||||||
| @ -345,3 +345,29 @@ export const formatNumberWithCommas = (num) => { | |||||||
|   } |   } | ||||||
|   return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); |   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