index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. <template>
  2. <div class="chat" :style="{ height: containerMinHeight }">
  3. <ElRow>
  4. <ElCol :span="12">
  5. <div class="grid-content ep-bg-purple" />
  6. </ElCol>
  7. <ElCol :span="12">
  8. <div class="grid-content ep-bg-purple-light" />
  9. </ElCol>
  10. </ElRow>
  11. <div class="person-list">
  12. <div class="person-item-header">
  13. <div class="user-info">
  14. <ElAvatar :size="50" :src="selectedPerson?.avatar" />
  15. <div class="user-details">
  16. <div class="name">{{ selectedPerson?.name }}</div>
  17. <div class="email">{{ selectedPerson?.email }}</div>
  18. </div>
  19. </div>
  20. <div class="search-box">
  21. <ElInput v-model="searchQuery" placeholder="搜索联系人" prefix-icon="Search" clearable />
  22. </div>
  23. <ElDropdown trigger="click" placement="bottom-start">
  24. <span class="sort-btn">
  25. 排序方式
  26. <ElIcon class="el-icon--right">
  27. <arrow-down />
  28. </ElIcon>
  29. </span>
  30. <template #dropdown>
  31. <ElDropdownMenu>
  32. <ElDropdownItem>按时间排序</ElDropdownItem>
  33. <ElDropdownItem>按名称排序</ElDropdownItem>
  34. <ElDropdownItem>全部标为已读</ElDropdownItem>
  35. </ElDropdownMenu>
  36. </template>
  37. </ElDropdown>
  38. </div>
  39. <ElScrollbar>
  40. <div
  41. v-for="item in personList"
  42. :key="item.id"
  43. class="person-item"
  44. :class="{ active: selectedPerson?.id === item.id }"
  45. @click="selectPerson(item)"
  46. >
  47. <div class="avatar-wrapper">
  48. <ElAvatar :size="40" :src="item.avatar">
  49. {{ item.name.charAt(0) }}
  50. </ElAvatar>
  51. <div class="status-dot" :class="{ online: item.online }"></div>
  52. </div>
  53. <div class="person-info">
  54. <div class="info-top">
  55. <span class="person-name">{{ item.name }}</span>
  56. <span class="last-time">{{ item.lastTime }}</span>
  57. </div>
  58. <div class="info-bottom">
  59. <span class="email">{{ item.email }}</span>
  60. </div>
  61. </div>
  62. </div>
  63. </ElScrollbar>
  64. </div>
  65. <div class="chat-modal">
  66. <div class="header">
  67. <div class="header-left">
  68. <span class="name">Art Bot</span>
  69. <div class="status">
  70. <div class="dot" :class="{ online: isOnline, offline: !isOnline }"></div>
  71. <span class="status-text">{{ isOnline ? '在线' : '离线' }}</span>
  72. </div>
  73. </div>
  74. <div class="header-right">
  75. <div class="btn">
  76. <i class="iconfont-sys">&#xe776;</i>
  77. </div>
  78. <div class="btn">
  79. <i class="iconfont-sys">&#xe778;</i>
  80. </div>
  81. <div class="btn">
  82. <i class="iconfont-sys">&#xe6df;</i>
  83. </div>
  84. </div>
  85. </div>
  86. <div class="chat-container">
  87. <!-- 聊天消息区域 -->
  88. <div class="chat-messages" ref="messageContainer">
  89. <template v-for="(message, index) in messages" :key="index">
  90. <div :class="['message-item', message.isMe ? 'message-right' : 'message-left']">
  91. <ElAvatar :size="32" :src="message.avatar" class="message-avatar" />
  92. <div class="message-content">
  93. <div class="message-info">
  94. <span class="sender-name">{{ message.sender }}</span>
  95. <span class="message-time">{{ message.time }}</span>
  96. </div>
  97. <div class="message-text">{{ message.content }}</div>
  98. </div>
  99. </div>
  100. </template>
  101. </div>
  102. <!-- 聊天输入区域 -->
  103. <div class="chat-input">
  104. <ElInput
  105. v-model="messageText"
  106. type="textarea"
  107. :rows="3"
  108. placeholder="输入消息"
  109. resize="none"
  110. @keyup.enter.prevent="sendMessage"
  111. >
  112. <template #append>
  113. <div class="input-actions">
  114. <ElButton :icon="Paperclip" circle plain />
  115. <ElButton :icon="Picture" circle plain />
  116. <ElButton type="primary" @click="sendMessage" v-ripple>发送</ElButton>
  117. </div>
  118. </template>
  119. </ElInput>
  120. <div class="chat-input-actions">
  121. <div class="left">
  122. <i class="iconfont-sys">&#xe634;</i>
  123. <i class="iconfont-sys">&#xe809;</i>
  124. </div>
  125. <ElButton type="primary" @click="sendMessage" v-ripple>发送</ElButton>
  126. </div>
  127. </div>
  128. </div>
  129. </div>
  130. </div>
  131. </template>
  132. <script setup lang="ts">
  133. import { ref, onMounted } from 'vue'
  134. import { Picture, Paperclip } from '@element-plus/icons-vue'
  135. import { mittBus } from '@/utils/sys'
  136. import meAvatar from '@/assets/img/avatar/avatar5.webp'
  137. import aiAvatar from '@/assets/img/avatar/avatar10.webp'
  138. import avatar2 from '@/assets/img/avatar/avatar2.webp'
  139. import avatar3 from '@/assets/img/avatar/avatar3.webp'
  140. import avatar4 from '@/assets/img/avatar/avatar4.webp'
  141. import avatar5 from '@/assets/img/avatar/avatar5.webp'
  142. import avatar6 from '@/assets/img/avatar/avatar6.webp'
  143. import avatar7 from '@/assets/img/avatar/avatar7.webp'
  144. import avatar8 from '@/assets/img/avatar/avatar8.webp'
  145. import avatar9 from '@/assets/img/avatar/avatar9.webp'
  146. import avatar10 from '@/assets/img/avatar/avatar10.webp'
  147. import { useCommon } from '@/composables/useCommon'
  148. const { containerMinHeight } = useCommon()
  149. const searchQuery = ref('')
  150. // 抽屉显示状态
  151. const isDrawerVisible = ref(false)
  152. // 是否在线
  153. const isOnline = ref(true)
  154. interface Person {
  155. id: number
  156. name: string
  157. email: string
  158. avatar: string
  159. online?: boolean
  160. lastTime: string
  161. unread?: number
  162. }
  163. const selectedPerson = ref<Person | null>(null)
  164. const personList = ref<Person[]>([
  165. {
  166. id: 1,
  167. name: '梅洛迪·梅西',
  168. email: 'melody@altbox.com',
  169. avatar: meAvatar,
  170. online: true,
  171. lastTime: '20小时前',
  172. unread: 0
  173. },
  174. {
  175. id: 2,
  176. name: '马克·史密斯',
  177. email: 'max@kt.com',
  178. avatar: avatar2,
  179. online: true,
  180. lastTime: '2周前',
  181. unread: 6
  182. },
  183. {
  184. id: 3,
  185. name: '肖恩·宾',
  186. email: 'sean@dellito.com',
  187. avatar: avatar3,
  188. online: false,
  189. lastTime: '5小时前',
  190. unread: 5
  191. },
  192. {
  193. id: 4,
  194. name: '爱丽丝·约翰逊',
  195. email: 'alice@domain.com',
  196. avatar: avatar4,
  197. online: true,
  198. lastTime: '1小时前',
  199. unread: 2
  200. },
  201. {
  202. id: 5,
  203. name: '鲍勃·布朗',
  204. email: 'bob@domain.com',
  205. avatar: avatar5,
  206. online: false,
  207. lastTime: '3天前',
  208. unread: 1
  209. },
  210. {
  211. id: 6,
  212. name: '查理·戴维斯',
  213. email: 'charlie@domain.com',
  214. avatar: avatar6,
  215. online: true,
  216. lastTime: '10分钟前',
  217. unread: 0
  218. },
  219. {
  220. id: 7,
  221. name: '戴安娜·普林斯',
  222. email: 'diana@domain.com',
  223. avatar: avatar7,
  224. online: true,
  225. lastTime: '15分钟前',
  226. unread: 3
  227. },
  228. {
  229. id: 8,
  230. name: '伊桑·亨特',
  231. email: 'ethan@domain.com',
  232. avatar: avatar8,
  233. online: true,
  234. lastTime: '5分钟前',
  235. unread: 0
  236. },
  237. {
  238. id: 9,
  239. name: '杰西卡·琼斯',
  240. email: 'jessica@domain.com',
  241. avatar: avatar9,
  242. online: false,
  243. lastTime: '1天前',
  244. unread: 4
  245. },
  246. {
  247. id: 10,
  248. name: '彼得·帕克',
  249. email: 'peter@domain.com',
  250. avatar: avatar10,
  251. online: true,
  252. lastTime: '2小时前',
  253. unread: 1
  254. },
  255. {
  256. id: 11,
  257. name: '克拉克·肯特',
  258. email: 'clark@domain.com',
  259. avatar: avatar3,
  260. online: true,
  261. lastTime: '30分钟前',
  262. unread: 2
  263. },
  264. {
  265. id: 12,
  266. name: '布鲁斯·韦恩',
  267. email: 'bruce@domain.com',
  268. avatar: avatar5,
  269. online: false,
  270. lastTime: '3天前',
  271. unread: 0
  272. },
  273. {
  274. id: 13,
  275. name: '韦德·威尔逊',
  276. email: 'wade@domain.com',
  277. avatar: avatar6,
  278. online: true,
  279. lastTime: '10分钟前',
  280. unread: 5
  281. }
  282. ])
  283. const selectPerson = (person: Person) => {
  284. selectedPerson.value = person
  285. }
  286. // 消息相关数据
  287. const messageText = ref('')
  288. const messages = ref([
  289. {
  290. id: 1,
  291. sender: 'Art Bot',
  292. content: '你好!我是你的AI助手,有什么我可以帮你的吗?',
  293. time: '10:00',
  294. isMe: false,
  295. avatar: aiAvatar
  296. },
  297. {
  298. id: 2,
  299. sender: 'Ricky',
  300. content: '我想了解一下系统的使用方法。',
  301. time: '10:01',
  302. isMe: true,
  303. avatar: meAvatar
  304. },
  305. {
  306. id: 3,
  307. sender: 'Art Bot',
  308. content: '好的,我来为您介绍系统的主要功能。首先,您可以通过左侧菜单访问不同的功能模块...',
  309. time: '10:02',
  310. isMe: false,
  311. avatar: aiAvatar
  312. },
  313. {
  314. id: 4,
  315. sender: 'Ricky',
  316. content: '听起来很不错,能具体讲讲数据分析部分吗?',
  317. time: '10:05',
  318. isMe: true,
  319. avatar: meAvatar
  320. },
  321. {
  322. id: 5,
  323. sender: 'Art Bot',
  324. content: '当然可以。数据分析模块可以帮助您实时监控关键指标,并生成详细的报表...',
  325. time: '10:06',
  326. isMe: false,
  327. avatar: aiAvatar
  328. },
  329. {
  330. id: 6,
  331. sender: 'Ricky',
  332. content: '太好了,那我如何开始使用呢?',
  333. time: '10:08',
  334. isMe: true,
  335. avatar: meAvatar
  336. },
  337. {
  338. id: 7,
  339. sender: 'Art Bot',
  340. content: '您可以先创建一个项目,然后在项目中添加相关的数据源,系统会自动进行分析。',
  341. time: '10:09',
  342. isMe: false,
  343. avatar: aiAvatar
  344. },
  345. {
  346. id: 8,
  347. sender: 'Ricky',
  348. content: '明白了,谢谢你的帮助!',
  349. time: '10:10',
  350. isMe: true,
  351. avatar: meAvatar
  352. },
  353. {
  354. id: 9,
  355. sender: 'Art Bot',
  356. content: '不客气,有任何问题随时联系我。',
  357. time: '10:11',
  358. isMe: false,
  359. avatar: aiAvatar
  360. }
  361. ])
  362. const messageId = ref(10) // 用于生成唯一的消息ID
  363. const userAvatar = ref(meAvatar) // 使用导入的头像
  364. // 发送消息
  365. const sendMessage = () => {
  366. const text = messageText.value.trim()
  367. if (!text) return
  368. messages.value.push({
  369. id: messageId.value++,
  370. sender: 'Ricky',
  371. content: text,
  372. time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
  373. isMe: true,
  374. avatar: userAvatar.value
  375. })
  376. messageText.value = ''
  377. scrollToBottom()
  378. }
  379. // 滚动到底部
  380. const messageContainer = ref<HTMLElement | null>(null)
  381. const scrollToBottom = () => {
  382. setTimeout(() => {
  383. if (messageContainer.value) {
  384. messageContainer.value.scrollTop = messageContainer.value.scrollHeight
  385. }
  386. }, 100)
  387. }
  388. const openChat = () => {
  389. isDrawerVisible.value = true
  390. }
  391. onMounted(() => {
  392. scrollToBottom()
  393. mittBus.on('openChat', openChat)
  394. selectedPerson.value = personList.value[0]
  395. })
  396. </script>
  397. <style lang="scss">
  398. .chat-modal {
  399. .el-overlay {
  400. background-color: rgb(0 0 0 / 20%) !important;
  401. }
  402. }
  403. </style>
  404. <style lang="scss" scoped>
  405. .chat {
  406. display: flex;
  407. overflow: hidden;
  408. background-color: var(--art-main-bg-color);
  409. border: 1px solid var(--art-border-color);
  410. border-radius: 10px;
  411. .person-list {
  412. box-sizing: border-box;
  413. width: 360px;
  414. height: 100%;
  415. padding: 20px;
  416. border-right: 1px solid var(--art-border-color);
  417. .person-item-header {
  418. padding-bottom: 20px;
  419. .user-info {
  420. display: flex;
  421. gap: 12px;
  422. align-items: center;
  423. .user-details {
  424. .name {
  425. font-size: 16px;
  426. font-weight: 500;
  427. color: var(--art-gray-900);
  428. }
  429. .email {
  430. margin-top: 4px;
  431. font-size: 13px;
  432. color: var(--art-gray-500);
  433. }
  434. }
  435. }
  436. .search-box {
  437. margin-top: 12px;
  438. }
  439. .sort-btn {
  440. margin-top: 20px;
  441. cursor: pointer;
  442. }
  443. }
  444. .person-item {
  445. display: flex;
  446. align-items: center;
  447. padding: 12px;
  448. cursor: pointer;
  449. border-radius: 8px;
  450. transition: all 0.3s ease;
  451. &:hover,
  452. &.active {
  453. background-color: var(--el-fill-color-light);
  454. }
  455. .avatar-wrapper {
  456. position: relative;
  457. margin-right: 12px;
  458. .status-dot {
  459. position: absolute;
  460. right: 1px;
  461. bottom: 1px;
  462. width: 9px;
  463. height: 9px;
  464. background-color: var(--el-color-error);
  465. border-radius: 50%;
  466. &.online {
  467. background-color: var(--el-color-success);
  468. }
  469. }
  470. }
  471. .person-info {
  472. flex: 1;
  473. min-width: 0;
  474. .info-top {
  475. display: flex;
  476. align-items: center;
  477. justify-content: space-between;
  478. margin-bottom: 4px;
  479. .person-name {
  480. font-size: 14px;
  481. font-weight: 500;
  482. color: var(--el-text-color-primary);
  483. }
  484. .last-time {
  485. font-size: 12px;
  486. color: var(--el-text-color-secondary);
  487. }
  488. }
  489. .info-bottom {
  490. display: flex;
  491. align-items: center;
  492. justify-content: space-between;
  493. .email {
  494. overflow: hidden;
  495. font-size: 12px;
  496. color: var(--el-text-color-secondary);
  497. text-overflow: ellipsis;
  498. white-space: nowrap;
  499. }
  500. .unread-badge {
  501. :deep(.el-badge__content) {
  502. border: none;
  503. }
  504. }
  505. }
  506. }
  507. }
  508. }
  509. .chat-modal {
  510. box-sizing: border-box;
  511. flex: 1;
  512. height: 100%;
  513. }
  514. .header {
  515. display: flex;
  516. align-items: center;
  517. justify-content: space-between;
  518. padding: 16px 16px 0;
  519. margin-bottom: 20px;
  520. .header-left {
  521. .name {
  522. font-size: 16px;
  523. font-weight: 500;
  524. }
  525. .status {
  526. display: flex;
  527. gap: 4px;
  528. align-items: center;
  529. margin-top: 6px;
  530. .dot {
  531. width: 8px;
  532. height: 8px;
  533. border-radius: 50%;
  534. &.online {
  535. background-color: var(--el-color-success);
  536. }
  537. &.offline {
  538. background-color: var(--el-color-danger);
  539. }
  540. }
  541. .status-text {
  542. font-size: 12px;
  543. color: var(--art-gray-600);
  544. }
  545. }
  546. }
  547. .header-right {
  548. display: flex;
  549. gap: 8px;
  550. align-items: center;
  551. .btn {
  552. width: 42px;
  553. height: 42px;
  554. line-height: 42px;
  555. text-align: center;
  556. cursor: pointer;
  557. border-radius: 50%;
  558. transition: background-color 0.2s ease;
  559. &:hover {
  560. background-color: var(--art-gray-200);
  561. }
  562. i {
  563. font-size: 20px;
  564. color: var(--art-text-gray-700);
  565. }
  566. }
  567. }
  568. }
  569. .chat-container {
  570. display: flex;
  571. flex-direction: column;
  572. height: calc(100% - 85px);
  573. .chat-messages {
  574. flex: 1;
  575. padding: 30px 16px;
  576. overflow-y: auto;
  577. border-top: 1px solid var(--el-border-color-lighter);
  578. &::-webkit-scrollbar {
  579. width: 5px !important;
  580. }
  581. .message-item {
  582. display: flex;
  583. flex-direction: row;
  584. gap: 8px;
  585. align-items: flex-start;
  586. width: 100%;
  587. margin-bottom: 30px;
  588. .message-text {
  589. font-size: 14px;
  590. color: var(--art-gray-900);
  591. border-radius: 6px;
  592. }
  593. &.message-left {
  594. justify-content: flex-start;
  595. .message-content {
  596. align-items: flex-start;
  597. .message-info {
  598. flex-direction: row;
  599. }
  600. .message-text {
  601. background-color: var(--art-gray-200);
  602. }
  603. }
  604. }
  605. &.message-right {
  606. flex-direction: row-reverse;
  607. .message-content {
  608. align-items: flex-end;
  609. .message-info {
  610. flex-direction: row-reverse;
  611. }
  612. .message-text {
  613. background-color: #e9f3ff;
  614. background-color: rgb(var(--art-bg-secondary));
  615. }
  616. }
  617. }
  618. .message-avatar {
  619. flex-shrink: 0;
  620. }
  621. .message-content {
  622. display: flex;
  623. flex-direction: column;
  624. max-width: 70%;
  625. .message-info {
  626. display: flex;
  627. gap: 8px;
  628. margin-bottom: 4px;
  629. font-size: 12px;
  630. .message-time {
  631. color: var(--el-text-color-secondary);
  632. }
  633. .sender-name {
  634. font-weight: 500;
  635. }
  636. }
  637. .message-text {
  638. padding: 10px 14px;
  639. line-height: 1.4;
  640. }
  641. }
  642. }
  643. }
  644. .chat-input {
  645. padding: 16px; // 增加填充以提升输入区域的布局
  646. .input-actions {
  647. display: flex;
  648. gap: 8px;
  649. padding: 8px 0;
  650. }
  651. .chat-input-actions {
  652. display: flex;
  653. align-items: center; // 修正为单数
  654. justify-content: space-between;
  655. margin-top: 12px;
  656. .left {
  657. display: flex;
  658. align-items: center;
  659. i {
  660. margin-right: 20px;
  661. font-size: 16px;
  662. color: var(--art-gray-500);
  663. cursor: pointer;
  664. }
  665. }
  666. // 确保发送按钮与输入框对齐
  667. el-button {
  668. min-width: 80px;
  669. }
  670. }
  671. }
  672. }
  673. }
  674. @media only screen and (max-width: $device-ipad-pro) {
  675. .chat {
  676. flex-direction: column;
  677. .person-list {
  678. width: 100%;
  679. height: 170px;
  680. border-right: none;
  681. .person-item-header {
  682. display: none;
  683. }
  684. }
  685. .chat-modal {
  686. height: calc(70% - 30px);
  687. }
  688. }
  689. }
  690. </style>