提交 | 用户 | 时间
|
820397
|
1 |
<template> |
H |
2 |
<el-container class="ai-layout"> |
|
3 |
<!-- 左侧:对话列表 --> |
|
4 |
<ConversationList |
|
5 |
:active-id="activeConversationId" |
|
6 |
ref="conversationListRef" |
|
7 |
@on-conversation-create="handleConversationCreateSuccess" |
|
8 |
@on-conversation-click="handleConversationClick" |
|
9 |
@on-conversation-clear="handleConversationClear" |
|
10 |
@on-conversation-delete="handlerConversationDelete" |
|
11 |
/> |
|
12 |
<!-- 右侧:对话详情 --> |
|
13 |
<el-container class="detail-container"> |
|
14 |
<el-header class="header"> |
|
15 |
<div class="title"> |
|
16 |
{{ activeConversation?.title ? activeConversation?.title : '对话' }} |
|
17 |
<span v-if="activeMessageList.length">({{ activeMessageList.length }})</span> |
|
18 |
</div> |
|
19 |
<div class="btns" v-if="activeConversation"> |
|
20 |
<el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm"> |
|
21 |
<span v-html="activeConversation?.modelName"></span> |
|
22 |
<Icon icon="ep:setting" class="ml-10px" /> |
|
23 |
</el-button> |
|
24 |
<el-button size="small" class="btn" @click="handlerMessageClear"> |
|
25 |
<Icon icon="heroicons-outline:archive-box-x-mark" color="#787878" /> |
|
26 |
</el-button> |
|
27 |
<el-button size="small" class="btn"> |
|
28 |
<Icon icon="ep:download" color="#787878" /> |
|
29 |
</el-button> |
|
30 |
<el-button size="small" class="btn" @click="handleGoTopMessage"> |
|
31 |
<Icon icon="ep:top" color="#787878" /> |
|
32 |
</el-button> |
|
33 |
</div> |
|
34 |
</el-header> |
|
35 |
|
|
36 |
<!-- main:消息列表 --> |
|
37 |
<el-main class="main-container"> |
|
38 |
<div> |
|
39 |
<div class="message-container"> |
|
40 |
<!-- 情况一:消息加载中 --> |
|
41 |
<MessageLoading v-if="activeMessageListLoading" /> |
|
42 |
<!-- 情况二:无聊天对话时 --> |
|
43 |
<MessageNewConversation |
|
44 |
v-if="!activeConversation" |
|
45 |
@on-new-conversation="handleConversationCreate" |
|
46 |
/> |
|
47 |
<!-- 情况三:消息列表为空 --> |
|
48 |
<MessageListEmpty |
|
49 |
v-if="!activeMessageListLoading && messageList.length === 0 && activeConversation" |
|
50 |
@on-prompt="doSendMessage" |
|
51 |
/> |
|
52 |
<!-- 情况四:消息列表不为空 --> |
|
53 |
<MessageList |
|
54 |
v-if="!activeMessageListLoading && messageList.length > 0" |
|
55 |
ref="messageRef" |
|
56 |
:conversation="activeConversation" |
|
57 |
:list="messageList" |
|
58 |
@on-delete-success="handleMessageDelete" |
|
59 |
@on-edit="handleMessageEdit" |
|
60 |
@on-refresh="handleMessageRefresh" |
|
61 |
/> |
|
62 |
</div> |
|
63 |
</div> |
|
64 |
</el-main> |
|
65 |
|
|
66 |
<!-- 底部 --> |
|
67 |
<el-footer class="footer-container"> |
|
68 |
<form class="prompt-from"> |
|
69 |
<textarea |
|
70 |
class="prompt-input" |
|
71 |
v-model="prompt" |
|
72 |
@keydown="handleSendByKeydown" |
|
73 |
@input="handlePromptInput" |
|
74 |
@compositionstart="onCompositionstart" |
|
75 |
@compositionend="onCompositionend" |
|
76 |
placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)" |
|
77 |
></textarea> |
|
78 |
<div class="prompt-btns"> |
|
79 |
<div> |
|
80 |
<el-switch v-model="enableContext" /> |
|
81 |
<span class="ml-5px text-14px text-#8f8f8f">上下文</span> |
|
82 |
</div> |
|
83 |
<el-button |
|
84 |
type="primary" |
|
85 |
size="default" |
|
86 |
@click="handleSendByButton" |
|
87 |
:loading="conversationInProgress" |
|
88 |
v-if="conversationInProgress == false" |
|
89 |
> |
|
90 |
{{ conversationInProgress ? '进行中' : '发送' }} |
|
91 |
</el-button> |
|
92 |
<el-button |
|
93 |
type="danger" |
|
94 |
size="default" |
|
95 |
@click="stopStream()" |
|
96 |
v-if="conversationInProgress == true" |
|
97 |
> |
|
98 |
停止 |
|
99 |
</el-button> |
|
100 |
</div> |
|
101 |
</form> |
|
102 |
</el-footer> |
|
103 |
</el-container> |
|
104 |
|
|
105 |
<!-- 更新对话 Form --> |
|
106 |
<ConversationUpdateForm |
|
107 |
ref="conversationUpdateFormRef" |
|
108 |
@success="handleConversationUpdateSuccess" |
|
109 |
/> |
|
110 |
</el-container> |
|
111 |
</template> |
|
112 |
|
|
113 |
<script setup lang="ts"> |
|
114 |
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message' |
|
115 |
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' |
|
116 |
import ConversationList from './components/conversation/ConversationList.vue' |
|
117 |
import ConversationUpdateForm from './components/conversation/ConversationUpdateForm.vue' |
|
118 |
import MessageList from './components/message/MessageList.vue' |
|
119 |
import MessageListEmpty from './components/message/MessageListEmpty.vue' |
|
120 |
import MessageLoading from './components/message/MessageLoading.vue' |
|
121 |
import MessageNewConversation from './components/message/MessageNewConversation.vue' |
|
122 |
|
|
123 |
/** AI 聊天对话 列表 */ |
|
124 |
defineOptions({ name: 'AiChat' }) |
|
125 |
|
|
126 |
const route = useRoute() // 路由 |
|
127 |
const message = useMessage() // 消息弹窗 |
|
128 |
|
|
129 |
// 聊天对话 |
|
130 |
const conversationListRef = ref() |
|
131 |
const activeConversationId = ref<number | null>(null) // 选中的对话编号 |
|
132 |
const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation |
|
133 |
const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作 |
|
134 |
|
|
135 |
// 消息列表 |
|
136 |
const messageRef = ref() |
|
137 |
const activeMessageList = ref<ChatMessageVO[]>([]) // 选中对话的消息列表 |
|
138 |
const activeMessageListLoading = ref<boolean>(false) // activeMessageList 是否正在加载中 |
|
139 |
const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Timer 定时器。如果加载速度很快,就不进入加载中 |
|
140 |
// 消息滚动 |
|
141 |
const textSpeed = ref<number>(50) // Typing speed in milliseconds |
|
142 |
const textRoleRunning = ref<boolean>(false) // Typing speed in milliseconds |
|
143 |
|
|
144 |
// 发送消息输入框 |
|
145 |
const isComposing = ref(false) // 判断用户是否在输入 |
|
146 |
const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话) |
|
147 |
const inputTimeout = ref<any>() // 处理输入中回车的定时器 |
|
148 |
const prompt = ref<string>() // prompt |
|
149 |
const enableContext = ref<boolean>(true) // 是否开启上下文 |
|
150 |
// 接收 Stream 消息 |
|
151 |
const receiveMessageFullText = ref('') |
|
152 |
const receiveMessageDisplayedText = ref('') |
|
153 |
|
|
154 |
// =========== 【聊天对话】相关 =========== |
|
155 |
|
|
156 |
/** 获取对话信息 */ |
|
157 |
const getConversation = async (id: number | null) => { |
|
158 |
if (!id) { |
|
159 |
return |
|
160 |
} |
|
161 |
const conversation: ChatConversationVO = await ChatConversationApi.getChatConversationMy(id) |
|
162 |
if (!conversation) { |
|
163 |
return |
|
164 |
} |
|
165 |
activeConversation.value = conversation |
|
166 |
activeConversationId.value = conversation.id |
|
167 |
} |
|
168 |
|
|
169 |
/** |
|
170 |
* 点击某个对话 |
|
171 |
* |
|
172 |
* @param conversation 选中的对话 |
|
173 |
* @return 是否切换成功 |
|
174 |
*/ |
|
175 |
const handleConversationClick = async (conversation: ChatConversationVO) => { |
|
176 |
// 对话进行中,不允许切换 |
|
177 |
if (conversationInProgress.value) { |
|
178 |
message.alert('对话中,不允许切换!') |
|
179 |
return false |
|
180 |
} |
|
181 |
|
|
182 |
// 更新选中的对话 id |
|
183 |
activeConversationId.value = conversation.id |
|
184 |
activeConversation.value = conversation |
|
185 |
// 刷新 message 列表 |
|
186 |
await getMessageList() |
|
187 |
// 滚动底部 |
|
188 |
scrollToBottom(true) |
|
189 |
// 清空输入框 |
|
190 |
prompt.value = '' |
|
191 |
return true |
|
192 |
} |
|
193 |
|
|
194 |
/** 删除某个对话*/ |
|
195 |
const handlerConversationDelete = async (delConversation: ChatConversationVO) => { |
|
196 |
// 删除的对话如果是当前选中的,那么就重置 |
|
197 |
if (activeConversationId.value === delConversation.id) { |
|
198 |
await handleConversationClear() |
|
199 |
} |
|
200 |
} |
|
201 |
/** 清空选中的对话 */ |
|
202 |
const handleConversationClear = async () => { |
|
203 |
// 对话进行中,不允许切换 |
|
204 |
if (conversationInProgress.value) { |
|
205 |
message.alert('对话中,不允许切换!') |
|
206 |
return false |
|
207 |
} |
|
208 |
activeConversationId.value = null |
|
209 |
activeConversation.value = null |
|
210 |
activeMessageList.value = [] |
|
211 |
} |
|
212 |
|
|
213 |
/** 修改聊天对话 */ |
|
214 |
const conversationUpdateFormRef = ref() |
|
215 |
const openChatConversationUpdateForm = async () => { |
|
216 |
conversationUpdateFormRef.value.open(activeConversationId.value) |
|
217 |
} |
|
218 |
const handleConversationUpdateSuccess = async () => { |
|
219 |
// 对话更新成功,刷新最新信息 |
|
220 |
await getConversation(activeConversationId.value) |
|
221 |
} |
|
222 |
|
|
223 |
/** 处理聊天对话的创建成功 */ |
|
224 |
const handleConversationCreate = async () => { |
|
225 |
// 创建对话 |
|
226 |
await conversationListRef.value.createConversation() |
|
227 |
} |
|
228 |
/** 处理聊天对话的创建成功 */ |
|
229 |
const handleConversationCreateSuccess = async () => { |
|
230 |
// 创建新的对话,清空输入框 |
|
231 |
prompt.value = '' |
|
232 |
} |
|
233 |
|
|
234 |
// =========== 【消息列表】相关 =========== |
|
235 |
|
|
236 |
/** 获取消息 message 列表 */ |
|
237 |
const getMessageList = async () => { |
|
238 |
try { |
|
239 |
if (activeConversationId.value === null) { |
|
240 |
return |
|
241 |
} |
|
242 |
// Timer 定时器,如果加载速度很快,就不进入加载中 |
|
243 |
activeMessageListLoadingTimer.value = setTimeout(() => { |
|
244 |
activeMessageListLoading.value = true |
|
245 |
}, 60) |
|
246 |
|
|
247 |
// 获取消息列表 |
|
248 |
activeMessageList.value = await ChatMessageApi.getChatMessageListByConversationId( |
|
249 |
activeConversationId.value |
|
250 |
) |
|
251 |
|
|
252 |
// 滚动到最下面 |
|
253 |
await nextTick() |
|
254 |
await scrollToBottom() |
|
255 |
} finally { |
|
256 |
// time 定时器,如果加载速度很快,就不进入加载中 |
|
257 |
if (activeMessageListLoadingTimer.value) { |
|
258 |
clearTimeout(activeMessageListLoadingTimer.value) |
|
259 |
} |
|
260 |
// 加载结束 |
|
261 |
activeMessageListLoading.value = false |
|
262 |
} |
|
263 |
} |
|
264 |
|
|
265 |
/** |
|
266 |
* 消息列表 |
|
267 |
* |
|
268 |
* 和 {@link #getMessageList()} 的差异是,把 systemMessage 考虑进去 |
|
269 |
*/ |
|
270 |
const messageList = computed(() => { |
|
271 |
if (activeMessageList.value.length > 0) { |
|
272 |
return activeMessageList.value |
|
273 |
} |
|
274 |
// 没有消息时,如果有 systemMessage 则展示它 |
|
275 |
if (activeConversation.value?.systemMessage) { |
|
276 |
return [ |
|
277 |
{ |
|
278 |
id: 0, |
|
279 |
type: 'system', |
|
280 |
content: activeConversation.value.systemMessage |
|
281 |
} |
|
282 |
] |
|
283 |
} |
|
284 |
return [] |
|
285 |
}) |
|
286 |
|
|
287 |
/** 处理删除 message 消息 */ |
|
288 |
const handleMessageDelete = () => { |
|
289 |
if (conversationInProgress.value) { |
|
290 |
message.alert('回答中,不能删除!') |
|
291 |
return |
|
292 |
} |
|
293 |
// 刷新 message 列表 |
|
294 |
getMessageList() |
|
295 |
} |
|
296 |
|
|
297 |
/** 处理 message 清空 */ |
|
298 |
const handlerMessageClear = async () => { |
|
299 |
if (!activeConversationId.value) { |
|
300 |
return |
|
301 |
} |
|
302 |
try { |
|
303 |
// 确认提示 |
|
304 |
await message.delConfirm('确认清空对话消息?') |
|
305 |
// 清空对话 |
|
306 |
await ChatMessageApi.deleteByConversationId(activeConversationId.value) |
|
307 |
// 刷新 message 列表 |
|
308 |
activeMessageList.value = [] |
|
309 |
} catch {} |
|
310 |
} |
|
311 |
|
|
312 |
/** 回到 message 列表的顶部 */ |
|
313 |
const handleGoTopMessage = () => { |
|
314 |
messageRef.value.handlerGoTop() |
|
315 |
} |
|
316 |
|
|
317 |
// =========== 【发送消息】相关 =========== |
|
318 |
|
|
319 |
/** 处理来自 keydown 的发送消息 */ |
|
320 |
const handleSendByKeydown = async (event) => { |
|
321 |
// 判断用户是否在输入 |
|
322 |
if (isComposing.value) { |
|
323 |
return |
|
324 |
} |
|
325 |
// 进行中不允许发送 |
|
326 |
if (conversationInProgress.value) { |
|
327 |
return |
|
328 |
} |
|
329 |
const content = prompt.value?.trim() as string |
|
330 |
if (event.key === 'Enter') { |
|
331 |
if (event.shiftKey) { |
|
332 |
// 插入换行 |
|
333 |
prompt.value += '\r\n' |
|
334 |
event.preventDefault() // 防止默认的换行行为 |
|
335 |
} else { |
|
336 |
// 发送消息 |
|
337 |
await doSendMessage(content) |
|
338 |
event.preventDefault() // 防止默认的提交行为 |
|
339 |
} |
|
340 |
} |
|
341 |
} |
|
342 |
|
|
343 |
/** 处理来自【发送】按钮的发送消息 */ |
|
344 |
const handleSendByButton = () => { |
|
345 |
doSendMessage(prompt.value?.trim() as string) |
|
346 |
} |
|
347 |
|
|
348 |
/** 处理 prompt 输入变化 */ |
|
349 |
const handlePromptInput = (event) => { |
|
350 |
// 非输入法 输入设置为 true |
|
351 |
if (!isComposing.value) { |
|
352 |
// 回车 event data 是 null |
|
353 |
if (event.data == null) { |
|
354 |
return |
|
355 |
} |
|
356 |
isComposing.value = true |
|
357 |
} |
|
358 |
// 清理定时器 |
|
359 |
if (inputTimeout.value) { |
|
360 |
clearTimeout(inputTimeout.value) |
|
361 |
} |
|
362 |
// 重置定时器 |
|
363 |
inputTimeout.value = setTimeout(() => { |
|
364 |
isComposing.value = false |
|
365 |
}, 400) |
|
366 |
} |
|
367 |
// TODO @芋艿:是不是可以通过 @keydown.enter、@keydown.shift.enter 来实现,回车发送、shift+回车换行;主要看看,是不是可以简化 isComposing 相关的逻辑 |
|
368 |
const onCompositionstart = () => { |
|
369 |
isComposing.value = true |
|
370 |
} |
|
371 |
const onCompositionend = () => { |
|
372 |
// console.log('输入结束...') |
|
373 |
setTimeout(() => { |
|
374 |
isComposing.value = false |
|
375 |
}, 200) |
|
376 |
} |
|
377 |
|
|
378 |
/** 真正执行【发送】消息操作 */ |
|
379 |
const doSendMessage = async (content: string) => { |
|
380 |
// 校验 |
|
381 |
if (content.length < 1) { |
|
382 |
message.error('发送失败,原因:内容为空!') |
|
383 |
return |
|
384 |
} |
|
385 |
if (activeConversationId.value == null) { |
|
386 |
message.error('还没创建对话,不能发送!') |
|
387 |
return |
|
388 |
} |
|
389 |
// 清空输入框 |
|
390 |
prompt.value = '' |
|
391 |
// 执行发送 |
|
392 |
await doSendMessageStream({ |
|
393 |
conversationId: activeConversationId.value, |
|
394 |
content: content |
|
395 |
} as ChatMessageVO) |
|
396 |
} |
|
397 |
|
|
398 |
/** 真正执行【发送】消息操作 */ |
|
399 |
const doSendMessageStream = async (userMessage: ChatMessageVO) => { |
|
400 |
// 创建 AbortController 实例,以便中止请求 |
|
401 |
conversationInAbortController.value = new AbortController() |
|
402 |
// 标记对话进行中 |
|
403 |
conversationInProgress.value = true |
|
404 |
// 设置为空 |
|
405 |
receiveMessageFullText.value = '' |
|
406 |
|
|
407 |
try { |
|
408 |
// 1.1 先添加两个假数据,等 stream 返回再替换 |
|
409 |
activeMessageList.value.push({ |
|
410 |
id: -1, |
|
411 |
conversationId: activeConversationId.value, |
|
412 |
type: 'user', |
|
413 |
content: userMessage.content, |
|
414 |
createTime: new Date() |
|
415 |
} as ChatMessageVO) |
|
416 |
activeMessageList.value.push({ |
|
417 |
id: -2, |
|
418 |
conversationId: activeConversationId.value, |
|
419 |
type: 'assistant', |
|
420 |
content: '思考中...', |
|
421 |
createTime: new Date() |
|
422 |
} as ChatMessageVO) |
|
423 |
// 1.2 滚动到最下面 |
|
424 |
await nextTick() |
|
425 |
await scrollToBottom() // 底部 |
|
426 |
// 1.3 开始滚动 |
|
427 |
textRoll() |
|
428 |
|
|
429 |
// 2. 发送 event stream |
|
430 |
let isFirstChunk = true // 是否是第一个 chunk 消息段 |
|
431 |
await ChatMessageApi.sendChatMessageStream( |
|
432 |
userMessage.conversationId, |
|
433 |
userMessage.content, |
|
434 |
conversationInAbortController.value, |
|
435 |
enableContext.value, |
|
436 |
async (res) => { |
|
437 |
const { code, data, msg } = JSON.parse(res.data) |
|
438 |
if (code !== 0) { |
|
439 |
message.alert(`对话异常! ${msg}`) |
|
440 |
return |
|
441 |
} |
|
442 |
|
|
443 |
// 如果内容为空,就不处理。 |
|
444 |
if (data.receive.content === '') { |
|
445 |
return |
|
446 |
} |
|
447 |
// 首次返回需要添加一个 message 到页面,后面的都是更新 |
|
448 |
if (isFirstChunk) { |
|
449 |
isFirstChunk = false |
|
450 |
// 弹出两个假数据 |
|
451 |
activeMessageList.value.pop() |
|
452 |
activeMessageList.value.pop() |
|
453 |
// 更新返回的数据 |
|
454 |
activeMessageList.value.push(data.send) |
|
455 |
activeMessageList.value.push(data.receive) |
|
456 |
} |
|
457 |
// debugger |
|
458 |
receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content |
|
459 |
// 滚动到最下面 |
|
460 |
await scrollToBottom() |
|
461 |
}, |
|
462 |
(error) => { |
|
463 |
message.alert(`对话异常! ${error}`) |
|
464 |
stopStream() |
|
465 |
}, |
|
466 |
() => { |
|
467 |
stopStream() |
|
468 |
} |
|
469 |
) |
|
470 |
} catch {} |
|
471 |
} |
|
472 |
|
|
473 |
/** 停止 stream 流式调用 */ |
|
474 |
const stopStream = async () => { |
|
475 |
// tip:如果 stream 进行中的 message,就需要调用 controller 结束 |
|
476 |
if (conversationInAbortController.value) { |
|
477 |
conversationInAbortController.value.abort() |
|
478 |
} |
|
479 |
// 设置为 false |
|
480 |
conversationInProgress.value = false |
|
481 |
} |
|
482 |
|
|
483 |
/** 编辑 message:设置为 prompt,可以再次编辑 */ |
|
484 |
const handleMessageEdit = (message: ChatMessageVO) => { |
|
485 |
prompt.value = message.content |
|
486 |
} |
|
487 |
|
|
488 |
/** 刷新 message:基于指定消息,再次发起对话 */ |
|
489 |
const handleMessageRefresh = (message: ChatMessageVO) => { |
|
490 |
doSendMessage(message.content) |
|
491 |
} |
|
492 |
|
|
493 |
// ============== 【消息滚动】相关 ============= |
|
494 |
|
|
495 |
/** 滚动到 message 底部 */ |
|
496 |
const scrollToBottom = async (isIgnore?: boolean) => { |
|
497 |
await nextTick() |
|
498 |
if (messageRef.value) { |
|
499 |
messageRef.value.scrollToBottom(isIgnore) |
|
500 |
} |
|
501 |
} |
|
502 |
|
|
503 |
/** 自提滚动效果 */ |
|
504 |
const textRoll = async () => { |
|
505 |
let index = 0 |
|
506 |
try { |
|
507 |
// 只能执行一次 |
|
508 |
if (textRoleRunning.value) { |
|
509 |
return |
|
510 |
} |
|
511 |
// 设置状态 |
|
512 |
textRoleRunning.value = true |
|
513 |
receiveMessageDisplayedText.value = '' |
|
514 |
const task = async () => { |
|
515 |
// 调整速度 |
|
516 |
const diff = |
|
517 |
(receiveMessageFullText.value.length - receiveMessageDisplayedText.value.length) / 10 |
|
518 |
if (diff > 5) { |
|
519 |
textSpeed.value = 10 |
|
520 |
} else if (diff > 2) { |
|
521 |
textSpeed.value = 30 |
|
522 |
} else if (diff > 1.5) { |
|
523 |
textSpeed.value = 50 |
|
524 |
} else { |
|
525 |
textSpeed.value = 100 |
|
526 |
} |
|
527 |
// 对话结束,就按 30 的速度 |
|
528 |
if (!conversationInProgress.value) { |
|
529 |
textSpeed.value = 10 |
|
530 |
} |
|
531 |
|
|
532 |
if (index < receiveMessageFullText.value.length) { |
|
533 |
receiveMessageDisplayedText.value += receiveMessageFullText.value[index] |
|
534 |
index++ |
|
535 |
|
|
536 |
// 更新 message |
|
537 |
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1] |
|
538 |
lastMessage.content = receiveMessageDisplayedText.value |
|
539 |
// 滚动到住下面 |
|
540 |
await scrollToBottom() |
|
541 |
// 重新设置任务 |
|
542 |
timer = setTimeout(task, textSpeed.value) |
|
543 |
} else { |
|
544 |
// 不是对话中可以结束 |
|
545 |
if (!conversationInProgress.value) { |
|
546 |
textRoleRunning.value = false |
|
547 |
clearTimeout(timer) |
|
548 |
} else { |
|
549 |
// 重新设置任务 |
|
550 |
timer = setTimeout(task, textSpeed.value) |
|
551 |
} |
|
552 |
} |
|
553 |
} |
|
554 |
let timer = setTimeout(task, textSpeed.value) |
|
555 |
} catch {} |
|
556 |
} |
|
557 |
|
|
558 |
/** 初始化 **/ |
|
559 |
onMounted(async () => { |
|
560 |
// 如果有 conversationId 参数,则默认选中 |
|
561 |
if (route.query.conversationId) { |
|
562 |
const id = route.query.conversationId as unknown as number |
|
563 |
activeConversationId.value = id |
|
564 |
await getConversation(id) |
|
565 |
} |
|
566 |
|
|
567 |
// 获取列表数据 |
|
568 |
activeMessageListLoading.value = true |
|
569 |
await getMessageList() |
|
570 |
}) |
|
571 |
</script> |
|
572 |
|
|
573 |
<style lang="scss" scoped> |
|
574 |
.ai-layout { |
|
575 |
position: absolute; |
|
576 |
flex: 1; |
|
577 |
top: 0; |
|
578 |
left: 0; |
|
579 |
height: 100%; |
|
580 |
width: 100%; |
|
581 |
} |
|
582 |
|
|
583 |
.conversation-container { |
|
584 |
position: relative; |
|
585 |
display: flex; |
|
586 |
flex-direction: column; |
|
587 |
justify-content: space-between; |
|
588 |
padding: 10px 10px 0; |
|
589 |
|
|
590 |
.btn-new-conversation { |
|
591 |
padding: 18px 0; |
|
592 |
} |
|
593 |
|
|
594 |
.search-input { |
|
595 |
margin-top: 20px; |
|
596 |
} |
|
597 |
|
|
598 |
.conversation-list { |
|
599 |
margin-top: 20px; |
|
600 |
|
|
601 |
.conversation { |
|
602 |
display: flex; |
|
603 |
flex-direction: row; |
|
604 |
justify-content: space-between; |
|
605 |
flex: 1; |
|
606 |
padding: 0 5px; |
|
607 |
margin-top: 10px; |
|
608 |
cursor: pointer; |
|
609 |
border-radius: 5px; |
|
610 |
align-items: center; |
|
611 |
line-height: 30px; |
|
612 |
|
|
613 |
&.active { |
|
614 |
background-color: #e6e6e6; |
|
615 |
|
|
616 |
.button { |
|
617 |
display: inline-block; |
|
618 |
} |
|
619 |
} |
|
620 |
|
|
621 |
.title-wrapper { |
|
622 |
display: flex; |
|
623 |
flex-direction: row; |
|
624 |
align-items: center; |
|
625 |
} |
|
626 |
|
|
627 |
.title { |
|
628 |
padding: 5px 10px; |
|
629 |
max-width: 220px; |
|
630 |
font-size: 14px; |
|
631 |
overflow: hidden; |
|
632 |
white-space: nowrap; |
|
633 |
text-overflow: ellipsis; |
|
634 |
} |
|
635 |
|
|
636 |
.avatar { |
|
637 |
width: 28px; |
|
638 |
height: 28px; |
|
639 |
display: flex; |
|
640 |
flex-direction: row; |
|
641 |
justify-items: center; |
|
642 |
} |
|
643 |
|
|
644 |
// 对话编辑、删除 |
|
645 |
.button-wrapper { |
|
646 |
right: 2px; |
|
647 |
display: flex; |
|
648 |
flex-direction: row; |
|
649 |
justify-items: center; |
|
650 |
color: #606266; |
|
651 |
|
|
652 |
.el-icon { |
|
653 |
margin-right: 5px; |
|
654 |
} |
|
655 |
} |
|
656 |
} |
|
657 |
} |
|
658 |
|
|
659 |
// 角色仓库、清空未设置对话 |
|
660 |
.tool-box { |
|
661 |
line-height: 35px; |
|
662 |
display: flex; |
|
663 |
justify-content: space-between; |
|
664 |
align-items: center; |
|
665 |
color: var(--el-text-color); |
|
666 |
|
|
667 |
> div { |
|
668 |
display: flex; |
|
669 |
align-items: center; |
|
670 |
color: #606266; |
|
671 |
padding: 0; |
|
672 |
margin: 0; |
|
673 |
cursor: pointer; |
|
674 |
|
|
675 |
> span { |
|
676 |
margin-left: 5px; |
|
677 |
} |
|
678 |
} |
|
679 |
} |
|
680 |
} |
|
681 |
|
|
682 |
// 头部 |
|
683 |
.detail-container { |
|
684 |
background: #ffffff; |
|
685 |
|
|
686 |
.header { |
|
687 |
display: flex; |
|
688 |
flex-direction: row; |
|
689 |
align-items: center; |
|
690 |
justify-content: space-between; |
|
691 |
background: #fbfbfb; |
|
692 |
box-shadow: 0 0 0 0 #dcdfe6; |
|
693 |
|
|
694 |
.title { |
|
695 |
font-size: 18px; |
|
696 |
font-weight: bold; |
|
697 |
} |
|
698 |
|
|
699 |
.btns { |
|
700 |
display: flex; |
|
701 |
width: 300px; |
|
702 |
flex-direction: row; |
|
703 |
justify-content: flex-end; |
|
704 |
//justify-content: space-between; |
|
705 |
|
|
706 |
.btn { |
|
707 |
padding: 10px; |
|
708 |
} |
|
709 |
} |
|
710 |
} |
|
711 |
} |
|
712 |
|
|
713 |
// main 容器 |
|
714 |
.main-container { |
|
715 |
margin: 0; |
|
716 |
padding: 0; |
|
717 |
position: relative; |
|
718 |
height: 100%; |
|
719 |
width: 100%; |
|
720 |
|
|
721 |
.message-container { |
|
722 |
position: absolute; |
|
723 |
top: 0; |
|
724 |
bottom: 0; |
|
725 |
left: 0; |
|
726 |
right: 0; |
|
727 |
overflow-y: hidden; |
|
728 |
padding: 0; |
|
729 |
margin: 0; |
|
730 |
} |
|
731 |
} |
|
732 |
|
|
733 |
// 底部 |
|
734 |
.footer-container { |
|
735 |
display: flex; |
|
736 |
flex-direction: column; |
|
737 |
height: auto; |
|
738 |
margin: 0; |
|
739 |
padding: 0; |
|
740 |
|
|
741 |
.prompt-from { |
|
742 |
display: flex; |
|
743 |
flex-direction: column; |
|
744 |
height: auto; |
|
745 |
border: 1px solid #e3e3e3; |
|
746 |
border-radius: 10px; |
|
747 |
margin: 10px 20px 20px 20px; |
|
748 |
padding: 9px 10px; |
|
749 |
} |
|
750 |
|
|
751 |
.prompt-input { |
|
752 |
height: 80px; |
|
753 |
//box-shadow: none; |
|
754 |
border: none; |
|
755 |
box-sizing: border-box; |
|
756 |
resize: none; |
|
757 |
padding: 0 2px; |
|
758 |
overflow: auto; |
|
759 |
} |
|
760 |
|
|
761 |
.prompt-input:focus { |
|
762 |
outline: none; |
|
763 |
} |
|
764 |
|
|
765 |
.prompt-btns { |
|
766 |
display: flex; |
|
767 |
justify-content: space-between; |
|
768 |
padding-bottom: 0; |
|
769 |
padding-top: 5px; |
|
770 |
} |
|
771 |
} |
|
772 |
</style> |