CurrentDayChatHistory.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. <script setup lang="ts">
  2. import { onMounted, onUnmounted, watch } from 'vue';
  3. import { Image, Input, Button } from 'ant-design-vue';
  4. import { UserOutlined, RobotOutlined, SearchOutlined, CustomerServiceOutlined } from '@ant-design/icons-vue';
  5. import dayjs from 'dayjs';
  6. import type { ChatMessageModel } from '@/model/consult.model';
  7. import { watchDebounced } from '@vueuse/core';
  8. defineOptions({ name: 'CurrentDayChatHistory' });
  9. // 扩展的消息接口(用于添加开始/结束标记)
  10. type DisplayMessage = ChatMessageModel & {
  11. id: number | string; // 确保 id 可以是字符串(用于标记消息)
  12. isRecordStart?: boolean; // 是否是记录开始标记
  13. isRecordEnd?: boolean; // 是否是记录结束标记
  14. };
  15. // 传入的聊天记录对象结构
  16. interface ChatRecordData {
  17. startTime?: string; // 开始咨询时间
  18. endTime?: string; // 结束咨询时间
  19. items?: ChatMessageModel[]; // 聊天记录列表
  20. id?: number; // 记录ID
  21. }
  22. interface Props {
  23. row?: ChatRecordData; // 传入的聊天记录对象
  24. onClose?: () => void;
  25. }
  26. const props = defineProps<Props>();
  27. const emits = defineEmits<{
  28. close: [];
  29. }>();
  30. // 搜索关键词(支持内容和时间搜索)
  31. const searchKeyword = ref('');
  32. // 搜索模式下的消息列表
  33. const searchMessages = ref<DisplayMessage[]>([]);
  34. // 是否处于搜索模式
  35. const isSearchMode = ref(false);
  36. // 本地消息列表(从传入的 row 中提取)
  37. const localMessages = ref<DisplayMessage[]>([]);
  38. // 初始化数据
  39. onMounted(() => {
  40. console.log('props.row===传入的聊天记录', props);
  41. // 处理传入的聊天记录
  42. if (props.row) {
  43. processChatRecord(props.row);
  44. }
  45. });
  46. // 监听传入的 row 变化
  47. watch(
  48. () => props.row,
  49. (newRow) => {
  50. if (newRow) {
  51. processChatRecord(newRow);
  52. // 重置搜索状态
  53. isSearchMode.value = false;
  54. searchMessages.value = [];
  55. searchKeyword.value = '';
  56. }
  57. },
  58. { deep: true }
  59. );
  60. onUnmounted(() => {
  61. // 清理工作(如果需要)
  62. });
  63. // 处理传入的聊天记录对象
  64. function processChatRecord(record: ChatRecordData) {
  65. const messages: DisplayMessage[] = [];
  66. const recordId = record.id || 0;
  67. // 添加开始咨询标记
  68. if (record.startTime) {
  69. messages.push({
  70. id: `start-${recordId}`,
  71. sendType: '3', // 系统消息
  72. messageType: '1', // 文本
  73. messageContent: `开始咨询 ${formatTime(record.startTime)}`,
  74. sendTime: record.startTime,
  75. consultRecordId: record.id,
  76. isRecordStart: true,
  77. } as unknown as DisplayMessage);
  78. }
  79. // 添加聊天记录 items
  80. if (record.items && record.items.length > 0) {
  81. for (let i = 0; i < record.items.length; i++) {
  82. messages.push(record.items[i] as DisplayMessage);
  83. }
  84. }
  85. // 添加结束咨询标记
  86. if (record.endTime) {
  87. messages.push({
  88. id: `end-${recordId}`,
  89. sendType: '3', // 系统消息
  90. messageType: '1', // 文本
  91. messageContent: `咨询结束 ${formatTime(record.endTime)}`,
  92. sendTime: record.endTime,
  93. consultRecordId: record.id,
  94. isRecordEnd: true,
  95. } as unknown as DisplayMessage);
  96. }
  97. localMessages.value = messages;
  98. }
  99. // 本地搜索功能
  100. function performLocalSearch(keyword: string) {
  101. if (!keyword || !keyword.trim()) {
  102. isSearchMode.value = false;
  103. searchMessages.value = [];
  104. return;
  105. }
  106. isSearchMode.value = true;
  107. const keywordLower = keyword.toLowerCase();
  108. searchMessages.value = localMessages.value.filter((msg) => {
  109. // 排除图片消息,只搜索文本消息
  110. if (msg.messageType === '2') {
  111. return false;
  112. }
  113. // 搜索消息内容
  114. if (msg.messageContent && msg.messageContent.toLowerCase().includes(keywordLower)) {
  115. return true;
  116. }
  117. // 搜索时间
  118. if (msg.sendTime && formatTime(msg.sendTime).includes(keyword)) {
  119. return true;
  120. }
  121. return false;
  122. });
  123. }
  124. // 将所有记录展平为消息列表,并添加开始/结束标记
  125. const filteredMessages = computed(() => {
  126. // 如果是搜索模式,直接返回搜索消息列表
  127. if (isSearchMode.value && searchMessages.value.length > 0) {
  128. return searchMessages.value;
  129. }
  130. // 正常模式:返回本地消息列表
  131. return localMessages.value;
  132. });
  133. watchDebounced(
  134. searchKeyword,
  135. (newVal: any) => {
  136. if (newVal && newVal.trim()) {
  137. // 有搜索关键词,执行本地搜索
  138. performLocalSearch(newVal.trim());
  139. } else {
  140. // 清空搜索,恢复正常模式
  141. isSearchMode.value = false;
  142. searchMessages.value = [];
  143. }
  144. },
  145. { debounce: 500 }
  146. );
  147. // 格式化时间
  148. function formatTime(time: string) {
  149. return dayjs(time).format('MM-DD HH:mm:ss');
  150. }
  151. // 关闭弹窗
  152. function handleClose() {
  153. emits('close');
  154. if (props.onClose) {
  155. props.onClose();
  156. }
  157. }
  158. </script>
  159. <template>
  160. <div class="chat-history-container">
  161. <!-- 搜索区域 -->
  162. <div class="search-container">
  163. <Input v-model:value="searchKeyword" placeholder="请输入搜索内容或时间" allow-clear>
  164. <template #prefix>
  165. <SearchOutlined />
  166. </template>
  167. </Input>
  168. </div>
  169. <!-- 聊天记录列表 -->
  170. <div class="messages-container">
  171. <div v-for="msg in filteredMessages" :key="msg.id" class="message-item" :class="msg.sendType">
  172. <div class="message-avatar" v-if="msg.sendType !== '3'">
  173. <UserOutlined v-if="msg.sendType === '1'" />
  174. <RobotOutlined v-else-if="msg.sendType === '4'" />
  175. <CustomerServiceOutlined v-else-if="msg.sendType === '2'" />
  176. </div>
  177. <div class="message-content-wrapper" :class="msg.sendType">
  178. <div class="message-content" :class="msg.sendType">
  179. <Image
  180. v-if="msg.messageType === '2'"
  181. :src="msg.messageContent"
  182. alt="图片"
  183. style="width: 200px; height: 200px"
  184. class="message-image"
  185. :preview="{
  186. src: msg.messageContent,
  187. }"
  188. />
  189. <div v-else class="message-text">{{ msg.messageContent }}</div>
  190. </div>
  191. <div class="message-time" v-if="msg.sendType !== '3'">{{ formatTime(msg.sendTime) }}</div>
  192. </div>
  193. </div>
  194. <!-- 空状态提示 -->
  195. <div v-if="filteredMessages.length === 0" class="no-more">暂无聊天记录</div>
  196. </div>
  197. <!-- 关闭按钮 -->
  198. <div class="close-button-container">
  199. <Button type="primary" @click="handleClose" block class="close-btn">关闭</Button>
  200. </div>
  201. </div>
  202. </template>
  203. <style scoped lang="scss">
  204. .chat-history-container {
  205. display: flex;
  206. flex-direction: column;
  207. height: 100%;
  208. padding: 20px;
  209. box-sizing: border-box;
  210. }
  211. .search-container {
  212. flex-shrink: 0;
  213. padding-bottom: 16px;
  214. :deep(.ant-input) {
  215. width: 100%;
  216. }
  217. }
  218. .messages-container {
  219. flex: 1;
  220. overflow-y: auto;
  221. padding: 16px 0;
  222. min-height: 0;
  223. display: flex;
  224. flex-direction: column;
  225. .loading-more,
  226. .no-more {
  227. text-align: center;
  228. padding: 12px;
  229. color: #999;
  230. font-size: 12px;
  231. flex-shrink: 0;
  232. }
  233. }
  234. .close-button-container {
  235. flex-shrink: 0;
  236. padding-top: 16px;
  237. margin-top: 16px;
  238. display: flex;
  239. align-items: center;
  240. justify-content: center;
  241. .close-btn {
  242. width: 10%;
  243. }
  244. }
  245. .message-item {
  246. display: flex;
  247. margin-bottom: 16px;
  248. width: 100%;
  249. // sendType: 1-患者
  250. &[class*=' 1'],
  251. &[class^='1 '],
  252. &[class$=' 1'],
  253. &[class='1'] {
  254. justify-content: flex-start;
  255. .message-avatar {
  256. order: 1;
  257. }
  258. .message-content-wrapper {
  259. order: 2;
  260. }
  261. .message-content {
  262. // background: #fff;
  263. background: #f0f0f0;
  264. color: #333;
  265. border-radius: 8px 8px 8px 0;
  266. }
  267. }
  268. // sendType: 2-医生 4-AI
  269. &[class*=' 2'],
  270. &[class^='2 '],
  271. &[class$=' 2'],
  272. &[class='2'],
  273. &[class*=' 4'],
  274. &[class^='4 '],
  275. &[class$=' 4'],
  276. &[class='4'] {
  277. justify-content: flex-end;
  278. .message-avatar {
  279. order: 2;
  280. }
  281. .message-content-wrapper {
  282. order: 1;
  283. align-items: flex-end;
  284. }
  285. .message-content {
  286. background: #1890ff;
  287. color: #fff;
  288. border-radius: 8px 8px 0 8px;
  289. }
  290. }
  291. // sendType: 3-系统
  292. &[class*=' 3'],
  293. &[class^='3 '],
  294. &[class$=' 3'],
  295. &[class='3'] {
  296. justify-content: center;
  297. margin: 8px 0;
  298. .message-content {
  299. background: transparent;
  300. color: #999;
  301. font-size: 12px;
  302. padding: 4px 0;
  303. }
  304. }
  305. }
  306. .message-avatar {
  307. width: 32px;
  308. height: 32px;
  309. border-radius: 50%;
  310. display: flex;
  311. align-items: center;
  312. justify-content: center;
  313. background: #f0f0f0;
  314. margin: 0 8px;
  315. flex-shrink: 0;
  316. }
  317. .message-content-wrapper {
  318. max-width: 60%;
  319. display: flex;
  320. flex-direction: column;
  321. // sendType: 1-患者
  322. &[class*=' 1'],
  323. &[class^='1 '],
  324. &[class$=' 1'],
  325. &[class='1'] {
  326. align-items: flex-start;
  327. .message-time {
  328. text-align: left;
  329. padding-left: 4px;
  330. margin-top: 4px;
  331. }
  332. }
  333. // sendType: 2-医生 4-AI
  334. &[class*=' 2'],
  335. &[class^='2 '],
  336. &[class$=' 2'],
  337. &[class='2'],
  338. &[class*=' 4'],
  339. &[class^='4 '],
  340. &[class$=' 4'],
  341. &[class='4'] {
  342. align-items: flex-end;
  343. .message-time {
  344. text-align: right;
  345. padding-right: 4px;
  346. margin-top: 4px;
  347. }
  348. }
  349. // sendType: 3-系统
  350. &[class*=' 3'],
  351. &[class^='3 '],
  352. &[class$=' 3'],
  353. &[class='3'] {
  354. align-items: center;
  355. width: 100%;
  356. max-width: 100%;
  357. }
  358. }
  359. .message-content {
  360. padding: 8px 12px;
  361. word-wrap: break-word;
  362. word-break: break-word;
  363. white-space: pre-wrap;
  364. overflow-wrap: break-word;
  365. max-width: 100%;
  366. box-sizing: border-box;
  367. .message-text {
  368. display: block;
  369. word-wrap: break-word;
  370. word-break: break-word;
  371. white-space: pre-wrap;
  372. overflow-wrap: break-word;
  373. width: 100%;
  374. line-height: 1.5;
  375. }
  376. .message-image {
  377. max-width: 120px;
  378. max-height: 120px;
  379. border-radius: 8px;
  380. display: block;
  381. cursor: pointer;
  382. :deep(img) {
  383. max-width: 120px !important;
  384. max-height: 120px !important;
  385. width: auto !important;
  386. height: auto !important;
  387. border-radius: 8px;
  388. display: block;
  389. object-fit: contain;
  390. }
  391. }
  392. }
  393. .message-time {
  394. font-size: 12px;
  395. color: #999;
  396. padding: 0 4px;
  397. }
  398. </style>