index.vue 25 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000
  1. <template>
  2. <el-container class="ai-layout">
  3. <!-- 左侧:会话列表 -->
  4. <el-aside width="260px" class="conversation-container">
  5. <div>
  6. <!-- 左顶部:新建对话 -->
  7. <el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation">
  8. <Icon icon="ep:plus" class="mr-5px" />
  9. 新建对话
  10. </el-button>
  11. <!-- 左顶部:搜索对话 -->
  12. <el-input
  13. v-model="searchName"
  14. size="large"
  15. class="mt-10px search-input"
  16. placeholder="搜索历史记录"
  17. @keyup="searchConversation"
  18. >
  19. <template #prefix>
  20. <Icon icon="ep:search" />
  21. </template>
  22. </el-input>
  23. <!-- 左中间:对话列表 -->
  24. <div class="conversation-list">
  25. <!-- TODO @fain:置顶、聊天记录、一星期钱、30天前,前端对数据重新做一下分组,或者后端接口改一下 -->
  26. <div>
  27. <el-text class="mx-1" size="small" tag="b">置顶</el-text>
  28. </div>
  29. <el-row v-for="conversation in conversationList" :key="conversation.id">
  30. <div
  31. :class="conversation.id === conversationId ? 'conversation active' : 'conversation'"
  32. @click="changeConversation(conversation.id)"
  33. >
  34. <div class="title-wrapper">
  35. <img class="avatar" :src="conversation.roleAvatar" />
  36. <span class="title">{{ conversation.title }}</span>
  37. </div>
  38. <!-- TODO @fan:缺一个【置顶】按钮,效果改成 hover 上去展示 -->
  39. <div class="button-wrapper">
  40. <el-icon title="编辑" @click="updateConversationTitle(conversation)">
  41. <Icon icon="ep:edit" />
  42. </el-icon>
  43. <el-icon title="删除会话" @click="deleteChatConversation(conversation)">
  44. <Icon icon="ep:delete" />
  45. </el-icon>
  46. </div>
  47. </div>
  48. </el-row>
  49. </div>
  50. </div>
  51. <!-- 左底部:工具栏 -->
  52. <div class="tool-box">
  53. <div>
  54. <Icon icon="ep:user" />
  55. <el-text size="small">角色仓库</el-text>
  56. </div>
  57. <div>
  58. <Icon icon="ep:delete" />
  59. <el-text size="small">清空未置顶对话</el-text>
  60. </div>
  61. </div>
  62. </el-aside>
  63. <!-- 右侧:会话详情 -->
  64. <el-container class="detail-container">
  65. <!-- 右顶部 TODO 芋艿:右对齐 -->
  66. <el-header class="header">
  67. <div class="title">
  68. {{ useConversation?.title }}
  69. </div>
  70. <div>
  71. <!-- TODO @fan:样式改下;这里我已经改成点击后,弹出了 -->
  72. <el-button type="primary" @click="openChatConversationUpdateForm">
  73. <span v-html="useConversation?.modelName"></span>
  74. <Icon icon="ep:setting" style="margin-left: 10px" />
  75. </el-button>
  76. <el-button>
  77. <Icon icon="ep:user" />
  78. </el-button>
  79. <el-button>
  80. <Icon icon="ep:download" />
  81. </el-button>
  82. <el-button>
  83. <Icon icon="ep:arrow-up" />
  84. </el-button>
  85. </div>
  86. </el-header>
  87. <!-- main -->
  88. <el-main class="main-container">
  89. <div class="message-container" ref="messageContainer">
  90. <div class="chat-list" v-for="(item, index) in list" :key="index">
  91. <!-- 靠左 message -->
  92. <!-- TODO 芋艿:类型判断 -->
  93. <div class="left-message message-item" v-if="item.type === 'system'">
  94. <div class="avatar">
  95. <el-avatar
  96. src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
  97. />
  98. </div>
  99. <div class="message">
  100. <div>
  101. <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
  102. </div>
  103. <div class="left-text-container" ref="markdownViewRef">
  104. <div class="left-text markdown-view" v-html="item.content"></div>
  105. <!-- <mdPreview :content="item.content" :delay="false" />-->
  106. </div>
  107. <div class="left-btns">
  108. <div class="btn-cus" @click="noCopy(item.content)">
  109. <img class="btn-image" src="../../../assets/ai/copy.svg" />
  110. <el-text class="btn-cus-text">复制</el-text>
  111. </div>
  112. <div class="btn-cus" style="margin-left: 20px" @click="onDelete(item.id)">
  113. <img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px" />
  114. <el-text class="btn-cus-text">删除</el-text>
  115. </div>
  116. </div>
  117. </div>
  118. </div>
  119. <!-- 靠右 message -->
  120. <div class="right-message message-item" v-if="item.type === 'user'">
  121. <div class="avatar">
  122. <el-avatar
  123. src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
  124. />
  125. </div>
  126. <div class="message">
  127. <div>
  128. <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
  129. </div>
  130. <div class="right-text-container">
  131. <div class="right-text">{{ item.content }}</div>
  132. </div>
  133. <div class="right-btns">
  134. <div class="btn-cus" @click="noCopy(item.content)">
  135. <img class="btn-image" src="@/assets/ai/copy.svg" />
  136. <el-text class="btn-cus-text">复制</el-text>
  137. </div>
  138. <div class="btn-cus" style="margin-left: 20px" @click="onDelete(item.id)">
  139. <img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px" />
  140. <el-text class="btn-cus-text">删除</el-text>
  141. </div>
  142. </div>
  143. </div>
  144. </div>
  145. </div>
  146. </div>
  147. </el-main>
  148. <el-footer class="footer-container">
  149. <form @submit.prevent="onSend" class="prompt-from">
  150. <textarea
  151. class="prompt-input"
  152. v-model="prompt"
  153. @keyup.enter="onSend"
  154. @input="onPromptInput"
  155. @compositionstart="onCompositionstart"
  156. @compositionend="onCompositionend"
  157. placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"
  158. ></textarea>
  159. <div class="prompt-btns">
  160. <el-switch />
  161. <el-button
  162. type="primary"
  163. size="default"
  164. @click="onSend()"
  165. :loading="conversationInProgress"
  166. v-if="conversationInProgress == false"
  167. >
  168. {{ conversationInProgress ? '进行中' : '发送' }}
  169. </el-button>
  170. <el-button
  171. type="danger"
  172. size="default"
  173. @click="stopStream()"
  174. v-if="conversationInProgress == true"
  175. >
  176. 停止
  177. </el-button>
  178. </div>
  179. </form>
  180. </el-footer>
  181. </el-container>
  182. </el-container>
  183. <ChatConversationUpdateForm
  184. ref="chatConversationUpdateFormRef"
  185. @success="getChatConversationList"
  186. />
  187. </template>
  188. <script setup lang="ts">
  189. import { ChatMessageApi, ChatMessageSendVO, ChatMessageVO } from '@/api/ai/chat/message'
  190. import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
  191. import ChatConversationUpdateForm from './components/ChatConversationUpdateForm.vue'
  192. import { formatDate } from '@/utils/formatTime'
  193. import { useClipboard } from '@vueuse/core'
  194. // 转换 markdown
  195. import { marked } from 'marked'
  196. // 代码高亮 https://highlightjs.org/
  197. import 'highlight.js/styles/vs2015.min.css'
  198. import hljs from 'highlight.js'
  199. const message = useMessage() // 消息弹窗
  200. // 自定义渲染器
  201. const renderer = {
  202. code(code, language, c) {
  203. const highlightHtml = hljs.highlight(code, { language: language, ignoreIllegals: true }).value
  204. const copyHtml = `<div id="copy" data-copy='${code}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`
  205. return `<pre>${copyHtml}<code class="hljs">${highlightHtml}</code></pre>`
  206. }
  207. }
  208. marked.use({
  209. renderer: renderer
  210. })
  211. const conversationList = ref([] as ChatConversationVO[])
  212. // 初始化 copy 到粘贴板
  213. const { copy } = useClipboard()
  214. const searchName = ref('') // 查询的内容
  215. const inputTimeout = ref<any>() // 处理输入中回车的定时器
  216. const conversationId = ref(0) // 选中的对话编号
  217. const conversationInProgress = ref(false) // 对话进行中
  218. const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
  219. const prompt = ref<string>() // prompt
  220. // 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方)
  221. const messageContainer: any = ref(null)
  222. const isScrolling = ref(false) //用于判断用户是否在滚动
  223. const isComposing = ref(false) // 判断用户是否在输入
  224. /** chat message 列表 */
  225. // defineOptions({ name: 'chatMessageList' })
  226. const list = ref<ChatMessageVO[]>([]) // 列表的数据
  227. const useConversation = ref<ChatConversationVO>() // 使用的 Conversation
  228. /** 新建对话 */
  229. const createConversation = async () => {
  230. // 新建对话
  231. const conversationId = await ChatConversationApi.createChatConversationMy(
  232. {} as unknown as ChatConversationVO
  233. )
  234. changeConversation(conversationId)
  235. // 刷新对话列表
  236. await getChatConversationList()
  237. }
  238. const changeConversation = (id: number) => {
  239. conversationId.value = id
  240. // TODO 芋艿:待实现
  241. }
  242. /** 更新聊天会话的标题 */
  243. const updateConversationTitle = async (conversation: ChatConversationVO) => {
  244. // 二次确认
  245. const { value } = await ElMessageBox.prompt('修改标题', {
  246. inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
  247. inputErrorMessage: '标题不能为空',
  248. inputValue: conversation.title
  249. })
  250. // 发起修改
  251. await ChatConversationApi.updateChatConversationMy({
  252. id: conversation.id,
  253. title: value
  254. } as ChatConversationVO)
  255. message.success('重命名成功')
  256. // 刷新列表
  257. await getChatConversationList()
  258. }
  259. /** 删除聊天会话 */
  260. const deleteChatConversation = async (conversation: ChatConversationVO) => {
  261. try {
  262. // 删除的二次确认
  263. await message.delConfirm(`是否确认删除会话 - ${conversation.title}?`)
  264. // 发起删除
  265. await ChatConversationApi.deleteChatConversationMy(conversation.id)
  266. message.success('会话已删除')
  267. // 刷新列表
  268. await getChatConversationList()
  269. } catch {}
  270. }
  271. const searchConversation = () => {
  272. // TODO fan:待实现
  273. }
  274. /** send */
  275. const onSend = async () => {
  276. // 判断用户是否在输入
  277. if (isComposing.value) {
  278. return
  279. }
  280. // 进行中不允许发送
  281. if (conversationInProgress.value) {
  282. return
  283. }
  284. const content = prompt.value?.trim()
  285. if (content.length < 2) {
  286. ElMessage({
  287. message: '请输入内容!',
  288. type: 'error'
  289. })
  290. return
  291. }
  292. // TODO 芋艿:这块交互要在优化;应该是先插入到 UI 界面,里面会有当前的消息,和正在思考中;之后发起请求;
  293. // 清空输入框
  294. prompt.value = ''
  295. // const requestParams = {
  296. // conversationId: conversationId.value,
  297. // content: content
  298. // } as unknown as ChatMessageSendVO
  299. // // 添加 message
  300. const userMessage = {
  301. conversationId: conversationId.value,
  302. content: content
  303. }
  304. // list.value.push(userMessage)
  305. // // 滚动到住下面
  306. // scrollToBottom()
  307. // stream
  308. await doSendStream(userMessage)
  309. //
  310. }
  311. const doSendStream = async (userMessage: ChatMessageVO) => {
  312. // 创建AbortController实例,以便中止请求
  313. conversationInAbortController.value = new AbortController()
  314. // 标记对话进行中
  315. conversationInProgress.value = true
  316. try {
  317. // 发送 event stream
  318. let isFirstMessage = true
  319. let content = ''
  320. ChatMessageApi.sendStream(
  321. userMessage.conversationId, // TODO 芋艿:这里可能要在优化;
  322. userMessage.content,
  323. conversationInAbortController.value,
  324. (message) => {
  325. console.log('message', message)
  326. const data = JSON.parse(message.data) // TODO 芋艿:类型处理;
  327. // debugger
  328. // 如果没有内容结束链接
  329. if (data.receive.content === '') {
  330. // 标记对话结束
  331. conversationInProgress.value = false
  332. // 结束 stream 对话
  333. conversationInAbortController.value.abort()
  334. }
  335. // 首次返回需要添加一个 message 到页面,后面的都是更新
  336. if (isFirstMessage) {
  337. isFirstMessage = false
  338. // debugger
  339. list.value.push(data.send)
  340. list.value.push(data.receive)
  341. } else {
  342. // debugger
  343. content = content + data.receive.content
  344. const lastMessage = list.value[list.value.length - 1]
  345. lastMessage.content = marked(content) as unknown as string
  346. list.value[list.value - 1] = lastMessage
  347. }
  348. // 滚动到最下面
  349. scrollToBottom()
  350. },
  351. (error) => {
  352. console.log('error', error)
  353. // 标记对话结束
  354. conversationInProgress.value = false
  355. // 结束 stream 对话
  356. conversationInAbortController.value.abort()
  357. },
  358. () => {
  359. console.log('close')
  360. // 标记对话结束
  361. conversationInProgress.value = false
  362. // 结束 stream 对话
  363. conversationInAbortController.value.abort()
  364. }
  365. )
  366. } finally {
  367. }
  368. }
  369. /** 查询列表 */
  370. const messageList = async () => {
  371. try {
  372. // 获取列表数据
  373. const res = await ChatMessageApi.messageList(conversationId.value)
  374. // 处理 markdown
  375. // marked(this.markdownText)
  376. res.map((item) => {
  377. // item.content = marked(item.content)
  378. if (item.type !== 'user') {
  379. item.content = marked(item.content)
  380. }
  381. })
  382. list.value = res
  383. // 滚动到最下面
  384. scrollToBottom()
  385. } finally {
  386. }
  387. }
  388. function scrollToBottom() {
  389. nextTick(() => {
  390. //注意要使用nexttick以免获取不到dom
  391. console.log('isScrolling.value', isScrolling.value)
  392. if (!isScrolling.value) {
  393. messageContainer.value.scrollTop =
  394. messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
  395. }
  396. })
  397. }
  398. function handleScroll() {
  399. const scrollContainer = messageContainer.value
  400. const scrollTop = scrollContainer.scrollTop
  401. const scrollHeight = scrollContainer.scrollHeight
  402. const offsetHeight = scrollContainer.offsetHeight
  403. if (scrollTop + offsetHeight < scrollHeight) {
  404. // 用户开始滚动并在最底部之上,取消保持在最底部的效果
  405. isScrolling.value = true
  406. } else {
  407. // 用户停止滚动并滚动到最底部,开启保持到最底部的效果
  408. isScrolling.value = false
  409. }
  410. }
  411. function noCopy(content) {
  412. copy(content)
  413. ElMessage({
  414. message: '复制成功!',
  415. type: 'success'
  416. })
  417. }
  418. const onDelete = async (id) => {
  419. // 删除 message
  420. await ChatMessageApi.delete(id)
  421. ElMessage({
  422. message: '删除成功!',
  423. type: 'success'
  424. })
  425. // tip:如果 stream 进行中的 message,就需要调用 controller 结束
  426. stopStream()
  427. // 重新获取 message 列表
  428. await messageList()
  429. }
  430. const stopStream = async () => {
  431. // tip:如果 stream 进行中的 message,就需要调用 controller 结束
  432. if (conversationInAbortController.value) {
  433. conversationInAbortController.value.abort()
  434. }
  435. // 设置为 false
  436. conversationInProgress.value = false
  437. }
  438. /** 修改聊天会话 */
  439. const chatConversationUpdateFormRef = ref()
  440. const openChatConversationUpdateForm = async () => {
  441. chatConversationUpdateFormRef.value.open(conversationId.value)
  442. }
  443. // 输入
  444. const onCompositionstart = () => {
  445. console.log('onCompositionstart。。。.')
  446. isComposing.value = true
  447. }
  448. const onCompositionend = () => {
  449. // console.log('输入结束...')
  450. setTimeout(() => {
  451. console.log('输入结束...')
  452. isComposing.value = false
  453. }, 200)
  454. }
  455. const onPromptInput = (event) => {
  456. // 非输入法 输入设置为 true
  457. if (!isComposing.value) {
  458. // 回车 event data 是 null
  459. if (event.data == null) {
  460. return
  461. }
  462. console.log('setTimeout 输入开始...')
  463. isComposing.value = true
  464. }
  465. // 清理定时器
  466. if (inputTimeout.value) {
  467. clearTimeout(inputTimeout.value)
  468. }
  469. // 重置定时器
  470. inputTimeout.value = setTimeout(() => {
  471. console.log('setTimeout 输入结束...')
  472. isComposing.value = false
  473. }, 400)
  474. }
  475. const getConversation = async (conversationId: string) => {
  476. // 获取对话信息
  477. useConversation.value = await ChatConversationApi.getChatConversationMy(conversationId)
  478. console.log('useConversation.value', useConversation.value)
  479. }
  480. /** 获得聊天会话列表 */
  481. const getChatConversationList = async () => {
  482. conversationList.value = await ChatConversationApi.getChatConversationMyList()
  483. // 默认选中第一条
  484. if (conversationList.value.length === 0) {
  485. conversationId.value = 0
  486. // TODO 芋艿:清空对话
  487. } else {
  488. if (conversationId.value === 0) {
  489. conversationId.value = conversationList.value[0].id
  490. changeConversation(conversationList.value[0].id)
  491. }
  492. }
  493. }
  494. /** 初始化 **/
  495. onMounted(async () => {
  496. // 获得聊天会话列表
  497. await getChatConversationList()
  498. // 获取对话信息
  499. getConversation(conversationId.value)
  500. // 获取列表数据
  501. messageList()
  502. // scrollToBottom();
  503. // await nextTick
  504. // 监听滚动事件,判断用户滚动状态
  505. messageContainer.value.addEventListener('scroll', handleScroll)
  506. // 添加 copy 监听
  507. messageContainer.value.addEventListener('click', (e: any) => {
  508. console.log(e)
  509. if (e.target.id === 'copy') {
  510. copy(e.target?.dataset?.copy)
  511. ElMessage({
  512. message: '复制成功!',
  513. type: 'success'
  514. })
  515. }
  516. })
  517. })
  518. </script>
  519. <style lang="scss" scoped>
  520. .ai-layout {
  521. // TODO @范 这里height不能 100% 先这样临时处理
  522. position: absolute;
  523. flex: 1;
  524. top: 0;
  525. left: 0;
  526. height: 100%;
  527. width: 100%;
  528. //padding: 15px 15px;
  529. }
  530. .conversation-container {
  531. position: relative;
  532. display: flex;
  533. flex-direction: column;
  534. justify-content: space-between;
  535. padding: 0 10px;
  536. padding-top: 10px;
  537. .btn-new-conversation {
  538. padding: 18px 0;
  539. }
  540. .search-input {
  541. margin-top: 20px;
  542. }
  543. .conversation-list {
  544. margin-top: 20px;
  545. .conversation {
  546. display: flex;
  547. flex-direction: row;
  548. justify-content: space-between;
  549. flex: 1;
  550. padding: 0 5px;
  551. margin-top: 10px;
  552. cursor: pointer;
  553. border-radius: 5px;
  554. align-items: center;
  555. line-height: 30px;
  556. &.active {
  557. background-color: #e6e6e6;
  558. .button {
  559. display: inline-block;
  560. }
  561. }
  562. .title-wrapper {
  563. display: flex;
  564. flex-direction: row;
  565. align-items: center;
  566. }
  567. .title {
  568. padding: 5px 10px;
  569. max-width: 220px;
  570. font-size: 14px;
  571. overflow: hidden;
  572. white-space: nowrap;
  573. text-overflow: ellipsis;
  574. }
  575. .avatar {
  576. width: 28px;
  577. height: 28px;
  578. display: flex;
  579. flex-direction: row;
  580. justify-items: center;
  581. }
  582. // 对话编辑、删除
  583. .button-wrapper {
  584. right: 2px;
  585. display: flex;
  586. flex-direction: row;
  587. justify-items: center;
  588. color: #606266;
  589. .el-icon {
  590. margin-right: 5px;
  591. }
  592. }
  593. }
  594. }
  595. // 角色仓库、清空未设置对话
  596. .tool-box {
  597. line-height: 35px;
  598. display: flex;
  599. justify-content: space-between;
  600. align-items: center;
  601. color: var(--el-text-color);
  602. > div {
  603. display: flex;
  604. align-items: center;
  605. color: #606266;
  606. padding: 0;
  607. margin: 0;
  608. > span {
  609. margin-left: 5px;
  610. }
  611. }
  612. }
  613. }
  614. // 头部
  615. .detail-container {
  616. background: #ffffff;
  617. .header {
  618. display: flex;
  619. flex-direction: row;
  620. align-items: center;
  621. justify-content: space-between;
  622. background: #fbfbfb;
  623. box-shadow: 0 0 0 0 #dcdfe6;
  624. .title {
  625. font-size: 18px;
  626. font-weight: bold;
  627. }
  628. }
  629. }
  630. // main 容器
  631. .main-container {
  632. margin: 0;
  633. padding: 0;
  634. position: relative;
  635. }
  636. .message-container {
  637. position: absolute;
  638. top: 0;
  639. bottom: 0;
  640. left: 0;
  641. right: 0;
  642. //width: 100%;
  643. //height: 100%;
  644. overflow-y: scroll;
  645. padding: 0 15px;
  646. }
  647. // 中间
  648. .chat-list {
  649. display: flex;
  650. flex-direction: column;
  651. overflow-y: hidden;
  652. .message-item {
  653. margin-top: 50px;
  654. }
  655. .left-message {
  656. display: flex;
  657. flex-direction: row;
  658. }
  659. .right-message {
  660. display: flex;
  661. flex-direction: row-reverse;
  662. justify-content: flex-start;
  663. }
  664. .avatar {
  665. //height: 170px;
  666. //width: 170px;
  667. }
  668. .message {
  669. display: flex;
  670. flex-direction: column;
  671. text-align: left;
  672. margin: 0 15px;
  673. .time {
  674. text-align: left;
  675. line-height: 30px;
  676. }
  677. .left-text-container {
  678. display: flex;
  679. flex-direction: column;
  680. overflow-wrap: break-word;
  681. background-color: rgba(228, 228, 228, 0.8);
  682. box-shadow: 0 0 0 1px rgba(228, 228, 228, 0.8);
  683. border-radius: 10px;
  684. padding: 10px 10px 5px 10px;
  685. .left-text {
  686. color: #393939;
  687. font-size: 0.95rem;
  688. }
  689. }
  690. .right-text-container {
  691. display: flex;
  692. flex-direction: row-reverse;
  693. .right-text {
  694. font-size: 0.95rem;
  695. color: #fff;
  696. display: inline;
  697. background-color: #267fff;
  698. color: #fff;
  699. box-shadow: 0 0 0 1px #267fff;
  700. border-radius: 10px;
  701. padding: 10px;
  702. width: auto;
  703. overflow-wrap: break-word;
  704. }
  705. }
  706. .left-btns,
  707. .right-btns {
  708. display: flex;
  709. flex-direction: row;
  710. margin-top: 8px;
  711. }
  712. }
  713. // 复制、删除按钮
  714. .btn-cus {
  715. display: flex;
  716. background-color: transparent;
  717. align-items: center;
  718. .btn-image {
  719. height: 20px;
  720. margin-right: 5px;
  721. }
  722. .btn-cus-text {
  723. color: #757575;
  724. }
  725. }
  726. .btn-cus:hover {
  727. cursor: pointer;
  728. }
  729. .btn-cus:focus {
  730. background-color: #8c939d;
  731. }
  732. }
  733. // 底部
  734. .footer-container {
  735. display: flex;
  736. flex-direction: column;
  737. height: auto;
  738. margin: 0;
  739. padding: 0;
  740. .prompt-from {
  741. display: flex;
  742. flex-direction: column;
  743. height: auto;
  744. border: 1px solid #e3e3e3;
  745. border-radius: 10px;
  746. margin: 20px 20px;
  747. padding: 9px 10px;
  748. }
  749. .prompt-input {
  750. height: 80px;
  751. //box-shadow: none;
  752. border: none;
  753. box-sizing: border-box;
  754. resize: none;
  755. padding: 0px 2px;
  756. //padding: 5px 5px;
  757. overflow: hidden;
  758. }
  759. .prompt-input:focus {
  760. outline: none;
  761. }
  762. .prompt-btns {
  763. display: flex;
  764. justify-content: space-between;
  765. padding-bottom: 0px;
  766. padding-top: 5px;
  767. }
  768. }
  769. </style>
  770. <style lang="scss">
  771. .markdown-view {
  772. font-family: PingFang SC;
  773. font-size: 0.95rem;
  774. font-weight: 400;
  775. line-height: 1.6rem;
  776. letter-spacing: 0em;
  777. text-align: left;
  778. color: #3b3e55;
  779. max-width: 100%;
  780. pre {
  781. position: relative;
  782. }
  783. pre code.hljs {
  784. width: auto;
  785. }
  786. code.hljs {
  787. border-radius: 6px;
  788. padding-top: 20px;
  789. width: auto;
  790. @media screen and (min-width: 1536px) {
  791. width: 960px;
  792. }
  793. @media screen and (max-width: 1536px) and (min-width: 1024px) {
  794. width: calc(100vw - 400px - 64px - 32px * 2);
  795. }
  796. @media screen and (max-width: 1024px) and (min-width: 768px) {
  797. width: calc(100vw - 32px * 2);
  798. }
  799. @media screen and (max-width: 768px) {
  800. width: calc(100vw - 16px * 2);
  801. }
  802. }
  803. p,
  804. code.hljs {
  805. margin-bottom: 16px;
  806. }
  807. p {
  808. //margin-bottom: 1rem !important;
  809. margin: 0;
  810. margin-bottom: 3px;
  811. }
  812. /* 标题通用格式 */
  813. h1,
  814. h2,
  815. h3,
  816. h4,
  817. h5,
  818. h6 {
  819. color: var(--color-G900);
  820. margin: 24px 0 8px;
  821. font-weight: 600;
  822. }
  823. h1 {
  824. font-size: 22px;
  825. line-height: 32px;
  826. }
  827. h2 {
  828. font-size: 20px;
  829. line-height: 30px;
  830. }
  831. h3 {
  832. font-size: 18px;
  833. line-height: 28px;
  834. }
  835. h4 {
  836. font-size: 16px;
  837. line-height: 26px;
  838. }
  839. h5 {
  840. font-size: 16px;
  841. line-height: 24px;
  842. }
  843. h6 {
  844. font-size: 16px;
  845. line-height: 24px;
  846. }
  847. /* 列表(有序,无序) */
  848. ul,
  849. ol {
  850. margin: 0 0 8px 0;
  851. padding: 0;
  852. font-size: 16px;
  853. line-height: 24px;
  854. color: #3b3e55; // var(--color-CG600);
  855. }
  856. li {
  857. margin: 4px 0 0 20px;
  858. margin-bottom: 1rem;
  859. }
  860. ol > li {
  861. list-style-type: decimal;
  862. margin-bottom: 1rem;
  863. // 表达式,修复有序列表序号展示不全的问题
  864. // &:nth-child(n + 10) {
  865. // margin-left: 30px;
  866. // }
  867. // &:nth-child(n + 100) {
  868. // margin-left: 30px;
  869. // }
  870. }
  871. ul > li {
  872. list-style-type: disc;
  873. font-size: 16px;
  874. line-height: 24px;
  875. margin-right: 11px;
  876. margin-bottom: 1rem;
  877. color: #3b3e55; // var(--color-G900);
  878. }
  879. ol ul,
  880. ol ul > li,
  881. ul ul,
  882. ul ul li {
  883. // list-style: circle;
  884. font-size: 16px;
  885. list-style: none;
  886. margin-left: 6px;
  887. margin-bottom: 1rem;
  888. }
  889. ul ul ul,
  890. ul ul ul li,
  891. ol ol,
  892. ol ol > li,
  893. ol ul ul,
  894. ol ul ul > li,
  895. ul ol,
  896. ul ol > li {
  897. list-style: square;
  898. }
  899. }
  900. </style>