ResultCard.vue 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <template>
  2. <div class="result-card">
  3. <h3 class="result-title" @click.stop="$emit('fragment', item)">
  4. <span class="title-text">{{ item.title }}</span>
  5. </h3>
  6. <div class="result-meta">
  7. <el-tag v-if="item.doc_type && item.doc_type !== '-'" size="small" class="meta-tag">
  8. {{ item.doc_type }}
  9. </el-tag>
  10. <el-tag v-if="item.department && item.department !== '-'" size="small" class="meta-tag dept-tag">
  11. {{ item.department }}
  12. </el-tag>
  13. <span v-if="item.publish_date" class="meta-info">
  14. <el-icon><Calendar /></el-icon>
  15. {{ formatDate(item.publish_date) }}
  16. </span>
  17. <span v-if="item.view_count !== undefined" class="meta-info">
  18. <el-icon><View /></el-icon>
  19. {{ item.view_count }} 次浏览
  20. </span>
  21. </div>
  22. <p
  23. class="result-abstract"
  24. v-html="renderMath(truncateText(item.highlighted_text || item.abstract || '', 300))"
  25. @click.stop="$emit('fragment', item)"
  26. ></p>
  27. <div class="result-tags" v-if="item.tags && item.tags.length">
  28. <el-tag v-for="tag in item.tags" :key="tag" size="small" class="custom-tag">
  29. {{ tag }}
  30. </el-tag>
  31. </div>
  32. <div class="result-actions">
  33. <el-button size="small" class="action-btn" @click.stop="$emit('preview', item)">
  34. <el-icon><View /></el-icon> 文档预览
  35. </el-button>
  36. <el-button size="small" class="action-btn download-btn" @click.stop="$emit('download', item)">
  37. <el-icon><Download /></el-icon> 下载
  38. </el-button>
  39. </div>
  40. </div>
  41. </template>
  42. <script setup lang="ts">
  43. import { View, Download, Calendar } from '@element-plus/icons-vue'
  44. import katex from 'katex'
  45. defineProps<{ item: any; keyword: string }>()
  46. defineEmits<{ preview: [item: any]; download: [item: any]; fragment: [item: any] }>()
  47. function formatDate(dateStr: string): string {
  48. if (!dateStr) return '-'
  49. return dateStr.split(' ')[0]
  50. }
  51. function renderMath(text: string): string {
  52. if (!text) return ''
  53. let result = text.replace(/\$\$([\s\S]*?)\$\$/g, (_, math) => {
  54. try {
  55. return katex.renderToString(math, { throwOnError: false, displayMode: true })
  56. } catch {
  57. return `<span class="katex-error">${_}</span>`
  58. }
  59. })
  60. result = result.replace(/\$([\s\S]*?)\$/g, (_, math) => {
  61. try {
  62. return katex.renderToString(math, { throwOnError: false, displayMode: false })
  63. } catch {
  64. return `<span class="katex-error">${_}</span>`
  65. }
  66. })
  67. return result
  68. }
  69. function truncateText(html: string, maxLength: number): string {
  70. if (html.length <= maxLength) return html
  71. let count = 0
  72. let cutIndex = 0
  73. let inTag = false
  74. for (let i = 0; i < html.length; i++) {
  75. const char = html[i]
  76. if (char === '<') {
  77. inTag = true
  78. } else if (char === '>') {
  79. inTag = false
  80. } else if (!inTag) {
  81. count++
  82. if (count >= maxLength) {
  83. cutIndex = i + 1
  84. break
  85. }
  86. }
  87. }
  88. let result = html.substring(0, cutIndex)
  89. const openTags: string[] = []
  90. const tagRegex = /<\/?([a-zA-Z]+)(?:\s[^>]*)?>/g
  91. let match
  92. const truncated = result
  93. while ((match = tagRegex.exec(truncated)) !== null) {
  94. const tag = match[1].toLowerCase()
  95. if (match[0].startsWith('</')) {
  96. if (openTags.length > 0 && openTags[openTags.length - 1] === tag) {
  97. openTags.pop()
  98. }
  99. } else {
  100. if (!['br', 'hr', 'img', 'input', 'meta', 'link'].includes(tag)) {
  101. openTags.push(tag)
  102. }
  103. }
  104. }
  105. while (openTags.length > 0) {
  106. result += `</${openTags.pop()}>`
  107. }
  108. return result + '...'
  109. }
  110. </script>
  111. <style scoped>
  112. .result-card {
  113. background: #fff;
  114. border-radius: 8px;
  115. padding: 20px 24px;
  116. margin-bottom: 16px;
  117. cursor: pointer;
  118. transition: all 0.2s;
  119. border: 1px solid #f0f0f0;
  120. }
  121. .result-card:hover {
  122. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
  123. }
  124. .result-title {
  125. font-size: 16px;
  126. color: #333;
  127. margin: 0 0 10px;
  128. font-weight: 600;
  129. line-height: 1.5;
  130. cursor: pointer;
  131. transition: color 0.2s;
  132. }
  133. .result-title:hover {
  134. color: #f5a623;
  135. }
  136. .title-text {
  137. flex: 1;
  138. }
  139. .result-meta {
  140. display: flex;
  141. align-items: center;
  142. gap: 10px;
  143. margin-bottom: 12px;
  144. flex-wrap: wrap;
  145. }
  146. .meta-tag {
  147. border: 1px solid #e8e8e8;
  148. background: #fafafa;
  149. color: #909399;
  150. font-size: 12px;
  151. padding: 1px 8px;
  152. border-radius: 2px;
  153. height: auto;
  154. }
  155. .dept-tag {
  156. border-color: #d9ecff;
  157. background: #ecf5ff;
  158. color: #409eff;
  159. }
  160. .meta-info {
  161. font-size: 12px;
  162. color: #b0b0b0;
  163. display: flex;
  164. align-items: center;
  165. gap: 4px;
  166. }
  167. .meta-info :deep(.el-icon) {
  168. font-size: 14px;
  169. }
  170. .result-abstract {
  171. font-size: 13px;
  172. color: #666;
  173. line-height: 1.8;
  174. margin: 0 0 12px;
  175. cursor: pointer;
  176. transition: color 0.2s;
  177. }
  178. .result-abstract:hover {
  179. color: #333;
  180. }
  181. .result-abstract :deep(em) {
  182. color: #f5a623;
  183. font-style: normal;
  184. font-weight: 600;
  185. }
  186. .result-tags {
  187. display: flex;
  188. gap: 6px;
  189. margin-bottom: 12px;
  190. flex-wrap: wrap;
  191. }
  192. .custom-tag {
  193. border: 1px solid #d9ecff;
  194. background: #f5f9ff;
  195. color: #409eff;
  196. padding: 2px 8px;
  197. font-size: 12px;
  198. border-radius: 2px;
  199. height: auto;
  200. }
  201. .result-actions {
  202. display: flex;
  203. gap: 12px;
  204. padding-top: 4px;
  205. }
  206. .action-btn {
  207. border-radius: 4px;
  208. padding: 6px 16px;
  209. font-size: 13px;
  210. }
  211. .action-btn :deep(.el-icon) {
  212. margin-right: 4px;
  213. }
  214. .download-btn {
  215. background: #409eff;
  216. border-color: #409eff;
  217. color: #fff;
  218. }
  219. .download-btn:hover {
  220. background: #66b1ff;
  221. border-color: #66b1ff;
  222. }
  223. </style>