index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. <!-- eslint-disable vue/multi-word-component-names -->
  2. <!-- eslint-disable vue/no-v-html -->
  3. <template>
  4. <transition name="fade">
  5. <div v-if="visible" class="dialog-overlay">
  6. <!-- 左侧折叠区域 -->
  7. <div class="left-side" :class="{ collapsed: isFold }" id="leftSide">
  8. <div
  9. class="addBtn"
  10. :style="{
  11. backgroundImage: `url(${isFold ? '/src/assets/images/vent/home/add.svg' : ''})`,
  12. backgroundColor: isFold ? '' : '#2cb6ff',
  13. width: isFold ? '20px' : 'auto',
  14. }"
  15. @click="addNew"
  16. >
  17. <span
  18. class="btn-text-bg"
  19. :style="{
  20. backgroundImage: `url(${!isFold ? '/src/assets/images/vent/home/addB.svg' : ''})`,
  21. }"
  22. ></span>
  23. <span v-if="!isFold" class="btn-text">添加新对话</span>
  24. </div>
  25. <div class="divider0"></div>
  26. <div
  27. v-if="isFold"
  28. class="historyBtn"
  29. :style="{ backgroundImage: `url(${isFold ? '/src/assets/images/vent/home/history.svg' : '/src/assets/images/vent/home/history.svg'})` }"
  30. @click="addNew"
  31. ></div>
  32. <div v-else class="historyBtn1">
  33. <span
  34. class="btn-text-bg"
  35. :style="{
  36. backgroundImage: `url(${!isFold ? '/src/assets/images/vent/home/history.svg' : ''})`,
  37. }"
  38. ></span>
  39. <span v-if="!isFold" class="btn-text">历史对话</span>
  40. <a-list style="width: 110px" :split="false" :data-source="store.sessionHistory" :scroll="200" class="custom-list">
  41. <template #renderItem="{ item }">
  42. <a-list-item
  43. :style="{
  44. padding: '8px 10px 0 8px',
  45. color: '#5e7081',
  46. fontSize: '10px',
  47. position: 'relative', // 新增定位
  48. }"
  49. @click="sessionsHistory(item.id)"
  50. >
  51. <!-- 新增flex布局容器 -->
  52. <div style="display: flex; justify-content: space-between; width: 100%">
  53. <div v-if="editingId !== item.id" class="text-container">
  54. <span :class="{ 'color-white': item.id === store.currentSessionID }" class="edit-text">{{ item.title || '新会话' }}</span>
  55. <edit-outlined class="edit-icon" @click="startEditing(item)" />
  56. </div>
  57. <!-- 输入框 -->
  58. <a-input
  59. size="small"
  60. v-else
  61. v-model:value="editText"
  62. v-focus
  63. @blur="handleSave(item)"
  64. @keyup.enter="handleSave(item)"
  65. class="edit-input"
  66. />
  67. </div>
  68. </a-list-item>
  69. </template>
  70. </a-list>
  71. </div>
  72. <div
  73. class="foldBtn"
  74. :style="{ backgroundImage: `url(${isFold ? '/src/assets/images/vent/home/Fold.svg' : '/src/assets/images/vent/home/unfold.svg'})` }"
  75. @click="fold"
  76. ></div>
  77. </div>
  78. <!-- 右侧对话框 -->
  79. <div class="right-side">
  80. <div class="input-content">
  81. <!-- 对话区域 -->
  82. <div ref="dialogRef" class="dialog-area">
  83. <div v-for="message in store.getMessageHistory" :key="message.id" :class="['message-item', message.type]">
  84. <!-- 用户提问样式 -->
  85. <div v-if="message.type === 'user'" class="ask-message">
  86. <span>{{ message.content }}</span>
  87. </div>
  88. <!-- 系统回答样式 -->
  89. <div v-else class="system-message">
  90. <div class="answerIcon"></div>
  91. <div class="answer-message">
  92. <div v-if="message.contentR1" class="thinking-text" v-html="formatMessage(message.contentR1)"> </div>
  93. <div class="answer-text" v-html="formatMessage(message.content)"> </div>
  94. </div>
  95. </div>
  96. </div>
  97. </div>
  98. <!-- 文本输入区域 -->
  99. <div v-if="store.streaming" class="thinking-area">
  100. <span style="color: #fff">思考中···</span>
  101. <a-spin :spinning="store.streaming" />
  102. </div>
  103. <div class="input-area">
  104. <textarea v-model="inputText" placeholder="请输入你的问题"> </textarea>
  105. <!-- 底部操作栏 -->
  106. <div class="action-bar">
  107. <!-- 左侧深度思考按钮 -->
  108. <div class="think-btn" :class="{ active: store.deepseekR1Enable }" @click="toggleThinking"> <span>深度思考</span> </div>
  109. <!-- 右侧操作按钮 -->
  110. <div class="right-actions">
  111. <label class="upload-btn">
  112. <div class="send-file"></div>
  113. <span class="divider"> | </span>
  114. <div class="send-img"></div>
  115. <div class="send-btn" @click="handleSend"></div>
  116. </label>
  117. </div>
  118. </div>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. </transition>
  124. </template>
  125. <script lang="ts" setup>
  126. import { ref, onMounted } from 'vue';
  127. import { EditOutlined } from '@ant-design/icons-vue';
  128. import { useAIChat } from '/@/store/modules/AIChat';
  129. // 响应式变量声明
  130. const store = useAIChat(); //获取用户信息
  131. const dialogRef = ref<HTMLElement | null>(null);
  132. const isFold = ref(false); // 是否折叠
  133. const inputText = ref(''); // 输入框内容
  134. const hasCreated = ref(false); // 标志位,防止重复调用create接口
  135. const editingId = ref<number | null>(null);
  136. const editText = ref('');
  137. const props = defineProps({
  138. visible: {
  139. type: Boolean,
  140. required: true,
  141. },
  142. });
  143. const vFocus = {
  144. mounted: (el: HTMLElement) => el.querySelector('input')?.focus(),
  145. };
  146. const openMenu = () => {
  147. if (props.visible) {
  148. hasCreated.value = true;
  149. }
  150. };
  151. const fold = () => {
  152. isFold.value = !isFold.value;
  153. if (!isFold.value) {
  154. store.getSessionHistory();
  155. }
  156. };
  157. const addNew = () => {
  158. store.createSession();
  159. };
  160. //编辑标题
  161. const startEditing = (item) => {
  162. editingId.value = item.id;
  163. editText.value = item.title || '';
  164. };
  165. // 保存修改
  166. const handleSave = (item) => {
  167. store.changeSessionTitle(editText.value, item.id).then(() => {
  168. editingId.value = null;
  169. store.getSessionHistory();
  170. });
  171. };
  172. //启用深度思考
  173. const toggleThinking = () => {
  174. store.deepseekR1Enable = !store.deepseekR1Enable;
  175. };
  176. //获取消息列表
  177. async function handleSend() {
  178. store
  179. .sendQuestion(inputText.value, () => {
  180. if (dialogRef.value) {
  181. dialogRef.value.scrollTop = dialogRef.value.scrollHeight;
  182. }
  183. })
  184. .then(() => {
  185. inputText.value = '';
  186. });
  187. }
  188. //获取具体会话记录
  189. function sessionsHistory(id: string) {
  190. store.getSessionHistoryByID(id);
  191. }
  192. //格式化消息
  193. function formatMessage(text: string) {
  194. let formatted = text
  195. // 处理换行
  196. .replace(/\n\n/g, '<br>')
  197. .replace(/\n###/g, '<br> ')
  198. .replace(/###/g, '')
  199. .replace(/---/g, '')
  200. // 处理粗体
  201. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  202. // 处理斜体
  203. .replace(/\*(.*?)\*/g, '<em>$1</em>')
  204. // 处理行内代码
  205. .replace(/`([^`]+)`/g, '<code>$1</code>');
  206. return formatted;
  207. }
  208. // 初始化按钮定位
  209. onMounted(() => {
  210. store.getSessionHistory();
  211. openMenu();
  212. });
  213. </script>
  214. <style lang="less" scoped>
  215. @keyframes menuShow {
  216. 0% {
  217. width: 0;
  218. height: 0;
  219. }
  220. 100% {
  221. width: 480px;
  222. height: 100vh;
  223. }
  224. }
  225. .custom-list {
  226. height: 360px;
  227. overflow-y: auto;
  228. }
  229. /* 穿透组件作用域 */
  230. ::v-deep .custom-list {
  231. scrollbar-width: thin;
  232. scrollbar-color: #1890ff #f0f0f0;
  233. &::-webkit-scrollbar {
  234. width: 4px;
  235. height: 6px;
  236. }
  237. &::-webkit-scrollbar-thumb {
  238. background: #1890ff;
  239. border-radius: 4px;
  240. }
  241. &::-webkit-scrollbar-track {
  242. background: #f0f0f0;
  243. border-radius: 4px;
  244. }
  245. }
  246. ::v-deep .zxm-list-items {
  247. color: #1890ff;
  248. }
  249. ::v-deep .zxm-list-item:hover {
  250. text-decoration: underline;
  251. color: #1890ff !important;
  252. }
  253. .text-container {
  254. display: flex;
  255. align-items: center;
  256. width: 100%;
  257. overflow: hidden;
  258. }
  259. .text-ellipsis {
  260. flex: 1;
  261. }
  262. .edit-text {
  263. overflow: hidden;
  264. text-overflow: ellipsis;
  265. white-space: nowrap;
  266. min-width: 0;
  267. }
  268. .edit-icon {
  269. flex-shrink: 0;
  270. cursor: pointer;
  271. margin-left: auto;
  272. }
  273. .edit-input {
  274. font-size: 10px;
  275. }
  276. .trigger-button {
  277. position: fixed;
  278. bottom: 10px;
  279. right: 10px;
  280. z-index: 1000000;
  281. .icon {
  282. width: 60px;
  283. height: 60px;
  284. position: relative;
  285. background-image: url('/@/assets/images/vent/home/wakeBtn.png');
  286. background-position: center;
  287. background-size: 100% 100%;
  288. }
  289. }
  290. .dialog-overlay {
  291. display: flex;
  292. background-color: #09172c;
  293. }
  294. /* 遮罩层淡入淡出 */
  295. .fade-enter-active,
  296. .fade-leave-active {
  297. transition: opacity 0.3s;
  298. }
  299. .fade-enter-from,
  300. .fade-leave-to {
  301. opacity: 0;
  302. }
  303. /* 弹窗缩放动画 */
  304. .scale-enter-active,
  305. .scale-leave-active {
  306. transition: all 0.3s ease;
  307. }
  308. .scale-enter-from {
  309. transform: scale(0.5) translate(-50%, -50%);
  310. opacity: 0;
  311. }
  312. .scale-leave-to {
  313. transform: scale(1.2) translate(-50%, -50%);
  314. opacity: 0;
  315. }
  316. .left-side {
  317. background: #0c2842;
  318. transition: width 0.5s ease; /* 平滑过渡动画 */
  319. width: 120px; /* 展开时宽度 */
  320. position: relative; /* 用于按钮定位 */
  321. }
  322. .left-side.collapsed {
  323. width: 40px; /* 折叠时宽度 */
  324. }
  325. .addBtn {
  326. height: 30px;
  327. position: absolute;
  328. background-size: 100% 100%;
  329. background-position: center;
  330. padding: 2px;
  331. right: 10px;
  332. top: 10px;
  333. left: 10px;
  334. align-items: center;
  335. border-radius: 3px;
  336. cursor: pointer;
  337. }
  338. .btn-text-bg {
  339. width: 14px;
  340. height: 14px;
  341. position: absolute;
  342. background-size: 100% 100%;
  343. right: 10px;
  344. top: 9px;
  345. left: 10px;
  346. bottom: 10px;
  347. }
  348. .btn-text {
  349. margin-left: 3px;
  350. font-size: 12px;
  351. color: #fff;
  352. white-space: nowrap;
  353. margin-left: 30px;
  354. line-height: 26px;
  355. }
  356. .historyBtn {
  357. width: 20px;
  358. height: 20px;
  359. position: absolute;
  360. background-size: 100% 100%;
  361. background-position: center;
  362. padding: 2px;
  363. right: 10px;
  364. top: 100px;
  365. }
  366. .historyBtn1 {
  367. width: 20px;
  368. height: 20px;
  369. position: absolute;
  370. background-size: 100% 100%;
  371. background-position: center;
  372. left: 3px;
  373. top: 80px;
  374. }
  375. .divider0 {
  376. border-bottom: 1px solid #1074c1;
  377. width: auto;
  378. margin: 0 10px;
  379. height: 13%;
  380. display: block;
  381. background: transparent;
  382. }
  383. .foldBtn {
  384. width: 20px;
  385. height: 20px;
  386. position: absolute;
  387. background-size: 100% 100%;
  388. background-position: center;
  389. padding: 2px;
  390. right: 10px;
  391. bottom: 10px;
  392. cursor: pointer;
  393. }
  394. .right-side {
  395. flex: 1; /* 占据剩余空间 */
  396. background: #09172c;
  397. }
  398. .input-content {
  399. display: flex;
  400. flex-direction: column;
  401. justify-content: flex-end; /* 内容底部对齐 */
  402. height: 100%;
  403. padding: 20px; /* 统一内边距 */
  404. }
  405. .ask-message {
  406. align-self: flex-end;
  407. float: right;
  408. max-width: 70%;
  409. padding: 10px;
  410. margin: 10px;
  411. border-radius: 5px;
  412. color: #fff;
  413. background: #0c2842;
  414. align-self: flex-end; /* 右侧对齐‌:ml-citation{ref="2" data="citationList"} */
  415. }
  416. .answer {
  417. display: flex;
  418. flex-direction: row;
  419. }
  420. .answerIcon {
  421. flex-shrink: 0;
  422. margin-top: 10px;
  423. width: 35px;
  424. height: 35px;
  425. background-image: url('/@/assets/images/vent/home/answerIcon.svg');
  426. background-size: 100% 100%;
  427. }
  428. .answer-message {
  429. float: left;
  430. padding: 10px;
  431. margin: 10px;
  432. border-radius: 5px;
  433. background: #0c2842;
  434. }
  435. .thinking-text {
  436. color: gray;
  437. font-size: 12px;
  438. }
  439. .answer-text {
  440. color: #fff;
  441. }
  442. /** 系统返回信息**/
  443. .system-message {
  444. display: flex;
  445. flex-direction: row;
  446. align-self: flex-start;
  447. width: 100%;
  448. padding: 12px;
  449. display: flex;
  450. }
  451. .answerIcon {
  452. margin-top: 10px;
  453. width: 35px;
  454. height: 35px;
  455. background-image: url('/@/assets/images/vent/home/answerIcon.svg');
  456. background-size: 100% 100%;
  457. }
  458. .think-area {
  459. color: #7979799f;
  460. }
  461. .answer-area {
  462. color: #fff;
  463. }
  464. .dialog-area {
  465. flex: 1; /* 占据剩余空间 */
  466. gap: 50px; /* 消息块间隔统一控制 */
  467. overflow-y: auto; /* 垂直滚动条 */
  468. margin-bottom: 10px;
  469. }
  470. .loading-wrapper,
  471. .content-wrapper {
  472. min-height: 40px;
  473. }
  474. .message-item.user {
  475. margin-bottom: 50px;
  476. }
  477. .input-area {
  478. background-color: #043256 !important;
  479. display: flex;
  480. flex-direction: column;
  481. gap: 10px;
  482. }
  483. textarea {
  484. background-color: #043256 !important;
  485. width: 100%;
  486. height: 40px;
  487. border: none;
  488. resize: none;
  489. outline: none;
  490. overflow: hidden;
  491. padding: 10px; /* 统一内边距 */
  492. color: #fff;
  493. }
  494. .action-bar {
  495. display: flex;
  496. justify-content: space-between;
  497. align-items: center;
  498. padding: 8px 16px;
  499. }
  500. .think-btn {
  501. border: 1px solid #ccc;
  502. width: 120px;
  503. height: 20px;
  504. line-height: 20px;
  505. text-align: center;
  506. border-radius: 50px;
  507. cursor: pointer;
  508. background: white;
  509. transition: background 0.3s;
  510. }
  511. .think-btn.active {
  512. background: #1890ff;
  513. color: white;
  514. border-color: #1890ff;
  515. }
  516. .right-actions {
  517. display: flex;
  518. align-items: center;
  519. gap: 8px;
  520. }
  521. .upload-btn {
  522. display: flex;
  523. align-items: center;
  524. gap: 8px;
  525. }
  526. .upload-btn {
  527. float: right;
  528. display: flex;
  529. cursor: pointer;
  530. padding: 6px 12px;
  531. }
  532. .divider {
  533. color: #ccc;
  534. font-weight: 300;
  535. margin: 0 10px;
  536. }
  537. .send-file {
  538. width: 20px;
  539. height: 20px;
  540. background-image: url('/@/assets/images/vent/home/sendFile.svg');
  541. background-size: 100% 100%;
  542. border-radius: 4px;
  543. cursor: pointer;
  544. }
  545. .send-img {
  546. width: 20px;
  547. height: 20px;
  548. background-image: url('/@/assets/images/vent/home/sendImg.svg');
  549. background-size: 100% 100%;
  550. border-radius: 4px;
  551. cursor: pointer;
  552. }
  553. .send-btn {
  554. width: 20px;
  555. height: 20px;
  556. margin-left: 10px;
  557. margin-right: 10px;
  558. background-color: #1074c1;
  559. background-image: url('/@/assets/images/vent/home/send.svg');
  560. background-position: center;
  561. background-size: 100% 100%;
  562. border-radius: 2px;
  563. cursor: pointer;
  564. }
  565. </style>