index.vue 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417
  1. <script setup lang="ts">
  2. import { ref, nextTick, onMounted, computed, onUnmounted, reactive, isReactive, watch } from 'vue'
  3. import { Local } from '/@/utils/storage'
  4. import {
  5. User,
  6. ChatDotRound,
  7. Delete,
  8. Edit,
  9. Check,
  10. Close,
  11. ArrowDown,
  12. Star,
  13. StarFilled,
  14. Search,
  15. Download,
  16. MoreFilled,
  17. Setting,
  18. Loading,
  19. Promotion,
  20. VideoPause,
  21. CopyDocument,
  22. Plus,
  23. } from '@element-plus/icons-vue'
  24. import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
  25. import EChartsPlugin from '/@/components/markdown/plugins/echarts'
  26. import ToolsLoadingPlugin from '/@/components/markdown/plugins/tools-loading'
  27. import TablePlugin from '/@/components/markdown/plugins/table'
  28. import Markdown from '/@/components/markdown/Markdown.vue'
  29. import assist from '/@/api/assist'
  30. import { ChatResponse, LmConfigInfo, LmSession, Message, Prompt } from '/@/api/assist/type'
  31. import { useLoading } from '/@/utils/loading-util'
  32. import { Setting as EleSetting } from '@element-plus/icons-vue'
  33. import { useRouter } from 'vue-router'
  34. import { ElMessage, ElMessageBox } from 'element-plus'
  35. import StructDataPlugin from '/@/components/markdown/plugins/struct-data'
  36. import { useFileDialog } from '@vueuse/core'
  37. const plugins: Array<MarkdownPlugin<any>> = [EChartsPlugin(), ToolsLoadingPlugin(), TablePlugin(), StructDataPlugin()]
  38. //聊天管理接口
  39. // 消息列表
  40. const messages = ref<Message[]>([])
  41. // 输入框内容
  42. const inputMessage = ref('')
  43. const {open,reset,files: attachments} = useFileDialog({
  44. multiple: true,
  45. })
  46. const messagesContainer = ref<HTMLElement>()
  47. // 附件管理:使用本地列表以支持单个移除
  48. const selectedFiles = ref<File[]>([])
  49. // 监听文件选择并合并到本地列表(按 name+size+lastModified 去重)
  50. watch(
  51. attachments,
  52. (newFiles) => {
  53. const incoming = Array.from(newFiles ?? [])
  54. const merged = [...selectedFiles.value, ...incoming]
  55. const map = new Map<string, File>()
  56. for (const f of merged) {
  57. const key = `${f.name}_${f.size}_${(f as any).lastModified ?? ''}`
  58. if (!map.has(key)) map.set(key, f)
  59. }
  60. selectedFiles.value = Array.from(map.values())
  61. },
  62. { immediate: true }
  63. )
  64. const removeAttachment = (index: number) => {
  65. if (index < 0 || index >= selectedFiles.value.length) return
  66. selectedFiles.value.splice(index, 1)
  67. }
  68. // // 选中的工具和模型
  69. // const selectedTool = ref([])
  70. // // 工具选择
  71. // const toolOptions = ref([
  72. // {
  73. // value: 'code',
  74. // label: '代码工具',
  75. // children: [
  76. // {
  77. // value: 'codebase-retrieval',
  78. // label: '代码库检索',
  79. // },
  80. // {
  81. // value: 'str-replace-editor',
  82. // label: '代码编辑器',
  83. // },
  84. // {
  85. // value: 'save-file',
  86. // label: '文件保存',
  87. // },
  88. // ],
  89. // },
  90. // {
  91. // value: 'web',
  92. // label: '网络工具',
  93. // children: [
  94. // {
  95. // value: 'web-search',
  96. // label: '网络搜索',
  97. // },
  98. // {
  99. // value: 'web-fetch',
  100. // label: '网页获取',
  101. // },
  102. // ],
  103. // },
  104. // {
  105. // value: 'system',
  106. // label: '系统工具',
  107. // children: [
  108. // {
  109. // value: 'execute-command',
  110. // label: '命令执行',
  111. // },
  112. // {
  113. // value: 'launch-process',
  114. // label: '进程启动',
  115. // },
  116. // ],
  117. // },
  118. // ])
  119. // 模型选择
  120. const modelOptions = ref<LmConfigInfo[]>([])
  121. const modelLabel = computed(() => {
  122. if (!loadingModels.value && selectedModel.value === undefined) {
  123. return '未配置模型'
  124. }
  125. const select = modelOptions.value.filter((i) => i.id === selectedModel.value)
  126. if (select.length === 0) {
  127. return '加载中'
  128. }
  129. return select[0].modelName
  130. })
  131. const { loading: loadingModels, doLoading: loadModel } = useLoading(async () => {
  132. const data: { list: LmConfigInfo[]; total: number } = await assist.model.getList({ modelType: 'chat' }).catch(() => {
  133. return {
  134. list: [],
  135. }
  136. })
  137. modelOptions.value = data.list ?? []
  138. selectedModel.value = modelOptions.value[0]?.id ?? undefined
  139. })
  140. // 词嵌入模型选择
  141. const embeddingModelOptions = ref<LmConfigInfo[]>([])
  142. const selectedEmbeddingModel = ref<number | undefined>(undefined)
  143. const embeddingModelLabel = computed(() => {
  144. if (!loadingEmbeddingModels.value && selectedEmbeddingModel.value === undefined) {
  145. return '未配置词嵌入'
  146. }
  147. const select = embeddingModelOptions.value.filter((i) => i.id === selectedEmbeddingModel.value)
  148. if (select.length === 0) {
  149. return '加载中'
  150. }
  151. return select[0].modelName
  152. })
  153. onMounted(loadModel)
  154. const { loading: loadingEmbeddingModels, doLoading: loadEmbeddingModel } = useLoading(async () => {
  155. const data: { list: LmConfigInfo[]; total: number } = await assist.model.getList({ modelType: 'embedding' }).catch(() => {
  156. return {
  157. list: [],
  158. }
  159. })
  160. embeddingModelOptions.value = data.list ?? []
  161. if (embeddingModelOptions.value.length !== 0) {
  162. embeddingModelOptions.value.unshift({
  163. id: -1,
  164. modelName: '不启用词嵌入',
  165. status: true,
  166. })
  167. }
  168. selectedEmbeddingModel.value = embeddingModelOptions.value[0]?.id ?? undefined
  169. })
  170. onMounted(loadEmbeddingModel)
  171. // 提示词列表相关状态
  172. const customPrompt = ref('')
  173. const promptList = ref<Prompt[]>([])
  174. const displayPromptList = computed(() => {
  175. //有提示词展示自定义提示词
  176. let r = [...promptList.value]
  177. if (customPrompt.value.trim() !== '') {
  178. r.splice(1, 0, {
  179. id: -2,
  180. title: `自定义提示词 (${customPrompt.value.length}) 字`,
  181. prompt: customPrompt.value,
  182. })
  183. }
  184. return r
  185. })
  186. const selectPromptId = ref<number | undefined>(undefined)
  187. watch(selectPromptId,(newVal)=>{
  188. inputMessage.value = displayPromptList.value.find(i=>i.id === newVal)?.placeholder ?? ''
  189. })
  190. const promptLabel = computed(() => {
  191. if (!loadingPromptList.value && selectPromptId.value === undefined) {
  192. return '未配置提示词'
  193. }
  194. const select = displayPromptList.value.filter((i) => i.id === selectPromptId.value)
  195. if (select.length === 0) {
  196. return '加载中'
  197. }
  198. return select[0].title
  199. })
  200. const { loading: loadingPromptList, doLoading: loadPromptList } = useLoading(async () => {
  201. const data: { list: Prompt[]; total: number } = await assist.chat.prompt.list({ pageSize: 10, pageNum: 1, keyWord: '' }).catch(() => {
  202. return {
  203. list: [],
  204. total: 0,
  205. }
  206. })
  207. promptList.value = data.list ?? []
  208. if (promptList.value.length !== 0) {
  209. promptList.value.unshift({
  210. id: -1,
  211. title: '不启用提示词',
  212. prompt: '',
  213. })
  214. }
  215. selectPromptId.value = promptList.value[0]?.id ?? undefined
  216. })
  217. onMounted(loadPromptList)
  218. const selectedModel = ref<number | undefined>(undefined)
  219. // 提示词管理对话框可见性
  220. const promptDialogVisible = ref(false)
  221. const chatInstance = ref<(() => void) | undefined>(undefined)
  222. onUnmounted(() => chatInstance.value?.())
  223. // 是否正在对话
  224. const isConversationActive = computed(() => chatInstance.value !== undefined)
  225. const { loading: loadingClearMessage, doLoading: clearMessage } = useLoading(async () => {
  226. stopConversation()
  227. if (messages.value.length !== 0 && activeConversationId.value !== undefined) {
  228. const res = await assist.session.message
  229. .save({
  230. sessionId: activeConversationId.value,
  231. messages: [],
  232. })
  233. .then(() => true)
  234. .catch(() => false)
  235. if (!res) {
  236. return
  237. }
  238. }
  239. messages.value = []
  240. })
  241. // 发送消息
  242. const sendMessage = async () => {
  243. if (!inputMessage.value.trim()) return
  244. if (activeConversationId.value === undefined) {
  245. //未选中任何会话则创建新会话
  246. await createConversationAndSetItActive()
  247. }
  248. await nextTick()
  249. messages.value.push({
  250. id: messages.value.length,
  251. role: 'user',
  252. render_content: inputMessage.value,
  253. content: inputMessage.value,
  254. timestamp: Date.now(),
  255. })
  256. const rtn = reactive<Message>({
  257. id: messages.value.length,
  258. role: 'assistant',
  259. render_content: '',
  260. content: '',
  261. timestamp: Date.now(),
  262. tool_calls: [],
  263. })
  264. inputMessage.value = ''
  265. scrollToBottom()
  266. chatInternal(rtn)
  267. messages.value.push(rtn)
  268. }
  269. const replaceMessage = async (index: number) => {
  270. // 获取当前用户消息
  271. const userMessage = messages.value[index]
  272. if (!userMessage || userMessage.role !== 'user') {
  273. return
  274. }
  275. // 查找对应的AI回复消息(通常是下一条消息)
  276. let aiMessageIndex = -1
  277. for (let i = index + 1; i < messages.value.length; i++) {
  278. if (messages.value[i].role === 'assistant') {
  279. aiMessageIndex = i
  280. break
  281. }
  282. }
  283. let rtn: Message
  284. if (aiMessageIndex !== -1) {
  285. // 找到了AI回复消息
  286. const aiMessage = messages.value[aiMessageIndex]
  287. if (aiMessage?.like === true) {
  288. const confirm = await ElMessageBox({
  289. title: '提示',
  290. message: '替换消息会导致收藏内容被修改,是否继续?',
  291. type: 'warning',
  292. confirmButtonText: '确认',
  293. cancelButtonText: '取消',
  294. })
  295. if (!confirm) {
  296. return
  297. }
  298. }
  299. if (isReactive(aiMessage)) {
  300. // 如果是reactive对象,清空内容
  301. aiMessage.render_content = ''
  302. aiMessage.content = ''
  303. aiMessage.tool_calls = []
  304. rtn = aiMessage
  305. } else {
  306. // 如果不是reactive对象,创建新的reactive对象替换
  307. rtn = reactive<Message>({
  308. id: aiMessage.id,
  309. role: 'assistant',
  310. render_content: '',
  311. content: '',
  312. timestamp: aiMessage.timestamp,
  313. tool_calls: [],
  314. })
  315. messages.value[aiMessageIndex] = rtn
  316. }
  317. } else {
  318. // 没有找到AI回复消息,创建新的
  319. rtn = reactive<Message>({
  320. id: messages.value.length,
  321. role: 'assistant',
  322. render_content: '',
  323. content: '',
  324. timestamp: Date.now(),
  325. tool_calls: [],
  326. })
  327. messages.value.push(rtn)
  328. }
  329. // 重新发起对话
  330. chatInternal(rtn, messages.value.slice(0, aiMessageIndex))
  331. }
  332. const chatInternal = (rtn: Message, context: Message[] = messages.value) => {
  333. let r = [...context]
  334. if (selectPromptId.value != undefined && selectPromptId.value != -1) {
  335. const prompt = displayPromptList.value.find((i) => i.id === selectPromptId.value)?.prompt ?? ''
  336. //insert element on top
  337. r.unshift({
  338. id: messages.value.length,
  339. role: 'system',
  340. render_content: prompt,
  341. content: prompt,
  342. timestamp: Date.now(),
  343. })
  344. }
  345. chatInstance.value = assist.chat.sse({
  346. chatRequest: {
  347. session_id: activeConversationId.value!,
  348. message: r,
  349. modelClassId: selectedModel.value,
  350. modelEmbeddingId: (selectedEmbeddingModel.value ?? -1) > 0 ? selectedEmbeddingModel.value : undefined,
  351. },
  352. onReceive: (resp: ChatResponse) => {
  353. switch (resp.type) {
  354. case 'structdata':
  355. rtn.render_content += `
  356. \`\`\`structdata
  357. ${resp.uuid}
  358. \`\`\`
  359. `
  360. break
  361. case 'message':
  362. rtn.render_content += resp.message
  363. rtn.content += resp.message
  364. break
  365. case 'toolres': {
  366. if (showToolCalls.value) {
  367. rtn.render_content += `
  368. \`\`\`tools-loading
  369. resp
  370. ${resp.response.name}
  371. ${resp.response.data.replace('\n', '')}
  372. \`\`\`
  373. `
  374. } else {
  375. if (rtn.render_content.at(-1) !== '\n') {
  376. rtn.render_content += '\n\n'
  377. }
  378. }
  379. //防止重试时底部有聊天记录无脑插入到底部出现后端无法识别的问题
  380. messages.value.splice(messages.value.indexOf(rtn) + 1, 0, {
  381. id: messages.value.length,
  382. tool_call_id: resp.response.id,
  383. role: 'tool',
  384. render_content: '',
  385. name: resp.response.name,
  386. content: resp.response.data,
  387. timestamp: Date.now(),
  388. tool_calls: [],
  389. })
  390. break
  391. }
  392. case 'toolcall': {
  393. if (showToolCalls.value) {
  394. rtn.render_content += `
  395. \`\`\`tools-loading
  396. request
  397. ${resp.request.name}
  398. ${resp.request.data.replace('\n', '')}
  399. \`\`\`
  400. `
  401. }
  402. rtn.tool_calls?.push({
  403. id: resp.request.id,
  404. type: 'function',
  405. function: {
  406. name: resp.request.name,
  407. arguments: resp.request.data,
  408. },
  409. })
  410. break
  411. }
  412. case 'error':
  413. rtn.render_content += `
  414. > ### 系统报错
  415. >
  416. >
  417. > ${resp.error.split('\n').join('\n> ')}
  418. `
  419. //过滤ai消息下的所有tool防止报错,鬼知道后端怎么想的
  420. messages.value = messages.value.slice(0, messages.value.indexOf(rtn) + 1)
  421. break
  422. }
  423. },
  424. onComplete: async (e) => {
  425. if (e !== undefined) {
  426. rtn.content += `
  427. `
  428. }
  429. rtn.render_content += '\n'
  430. const save_status = await assist.session.message
  431. .save({
  432. sessionId: activeConversationId.value!,
  433. messages: messages.value,
  434. })
  435. .then(() => true)
  436. .catch(() => false)
  437. if (!save_status) {
  438. ElMessage.warning('消息保存失败,切换对话会导致消息记录丢失')
  439. }
  440. chatInstance.value = undefined
  441. },
  442. })
  443. }
  444. // 终止对话
  445. const stopConversation = () => {
  446. chatInstance.value?.()
  447. chatInstance.value = undefined
  448. }
  449. // 滚动到底部
  450. const scrollToBottom = () => {
  451. nextTick(() => {
  452. if (messagesContainer.value) {
  453. messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
  454. }
  455. })
  456. }
  457. // 会话管理模块
  458. // 所有会话
  459. const conversations = ref<LmSession[]>([])
  460. const displayConversations = computed(() => {
  461. return [{ session_id: -1, title: '收藏夹' }, ...conversations.value]
  462. })
  463. const { loading: loadConversations, doLoading: doLoadConversations } = useLoading(async () => {
  464. const data: { list: LmSession[]; total: number } = await assist.session
  465. .list({
  466. pageNum: 1,
  467. pageSize: 30,
  468. })
  469. .catch(() => {
  470. return {
  471. list: [],
  472. total: 0,
  473. }
  474. })
  475. conversations.value = data.list ?? []
  476. })
  477. onMounted(doLoadConversations)
  478. // 当前活跃会话
  479. const activeConversationId = ref<number | undefined>(undefined)
  480. // watch(activeConversationId, async (newVal) => {
  481. // if (newVal === undefined) {
  482. // return
  483. // }
  484. // await doLoadingMessage(newVal)
  485. // })
  486. const bookmarkOptions = ref({
  487. pageSize: 30,
  488. pageNum: 1,
  489. keyWord: '',
  490. })
  491. // 收藏消息总数
  492. const bookmarkTotal = ref(0)
  493. const { loading: loadingMessage, doLoading: doLoadingMessage } = useLoading(async (id: number) => {
  494. //如果id是-1,拉取收藏的记录
  495. if (id === -1) {
  496. const data: {
  497. messages: Message[]
  498. total: number
  499. } = await assist.session.message.bookmark_list(bookmarkOptions.value).catch(() => {
  500. return {
  501. list: [],
  502. total: 0,
  503. }
  504. })
  505. messages.value = data.messages ?? []
  506. bookmarkTotal.value = data.total ?? 0
  507. return
  508. }
  509. const data: {
  510. messages: Message[]
  511. total: number
  512. } = await assist.session.message.list({ sessionId: id }).catch(() => {
  513. return {
  514. list: [],
  515. total: 0,
  516. }
  517. })
  518. messages.value = data.messages ?? []
  519. })
  520. // 选择会话
  521. const selectConversation = async (id: number) => {
  522. if (activeConversationId.value === id) {
  523. return
  524. }
  525. activeConversationId.value = id
  526. await doLoadingMessage(id)
  527. }
  528. // 删除会话
  529. const currentDeletingConversation = ref(-1)
  530. const { loading: loadingDeleteConversation, doLoading: deleteConversation } = useLoading(async (id: number) => {
  531. currentDeletingConversation.value = id
  532. const res = await assist.session
  533. .del([id])
  534. .then(() => true)
  535. .catch(() => false)
  536. .finally(() => (currentDeletingConversation.value = -1))
  537. if (!res) {
  538. return
  539. }
  540. ElMessage.success('删除成功')
  541. activeConversationId.value = undefined
  542. messages.value = []
  543. await nextTick()
  544. await doLoadConversations()
  545. })
  546. const multiDeleteConversationModel = ref({
  547. visible: false,
  548. selectedConversations: [] as number[],
  549. })
  550. const multiDeleteConversationClear = () => {
  551. multiDeleteConversationModel.value.selectedConversations = []
  552. multiDeleteConversationModel.value.visible = false
  553. }
  554. // 多选删除会话
  555. const startMultidelete = async () => {
  556. if (multiDeleteConversationModel.value.selectedConversations.length === 0) {
  557. ElMessage.warning('请选择要删除的会话')
  558. return
  559. }
  560. const confirm = await ElMessageBox.confirm(
  561. `确定要删除选中的 ${multiDeleteConversationModel.value.selectedConversations.length} 个会话吗?此操作不可恢复!`,
  562. '警告',
  563. {
  564. confirmButtonText: '确定',
  565. cancelButtonText: '取消',
  566. type: 'error',
  567. }
  568. )
  569. if (confirm !== 'confirm') {
  570. return
  571. }
  572. const res = await assist.session
  573. .del(multiDeleteConversationModel.value.selectedConversations)
  574. .then(() => true)
  575. .catch(() => false)
  576. if (!res) {
  577. return
  578. }
  579. ElMessage.success('删除成功')
  580. await nextTick()
  581. await doLoadConversations()
  582. }
  583. // 创建新对话
  584. const { loading: creatingConversation, doLoading: createConversationAndSetItActive } = useLoading(async () => {
  585. // 调用API创建新对话,默认标题为"新对话"
  586. const { id } = await assist.session.add('新对话')
  587. // 刷新对话列表
  588. await doLoadConversations()
  589. await nextTick()
  590. activeConversationId.value = id
  591. messages.value = []
  592. })
  593. // 编辑会话状态管理
  594. const editingConversationId = ref<number | undefined>(undefined)
  595. const editingTitle = ref('')
  596. // 编辑会话摘要
  597. const editSummary = (id: number) => {
  598. const conversation = conversations.value.find((conv) => conv.session_id === id)
  599. if (conversation) {
  600. // 设置当前编辑的会话ID
  601. editingConversationId.value = id
  602. editingTitle.value = conversation.title
  603. // 下一帧聚焦到输入框
  604. nextTick(() => {
  605. const editInput = document.querySelector('.edit-input .el-input__inner') as HTMLInputElement
  606. if (editInput) {
  607. editInput.focus()
  608. editInput.select()
  609. }
  610. })
  611. }
  612. }
  613. // 确认编辑
  614. const { doLoading: confirmEdit, loading: loadingConfirmEdit } = useLoading(async (id: number) => {
  615. const conversation = conversations.value.find((conv) => conv.session_id === id)
  616. if (conversation && editingTitle.value.trim()) {
  617. const edit = await assist.session
  618. .edit(id, editingTitle.value.trim())
  619. .then(() => true)
  620. .catch(() => false)
  621. if (!edit) {
  622. return
  623. }
  624. conversation.title = editingTitle.value.trim()
  625. // 清除编辑状态
  626. editingConversationId.value = undefined
  627. editingTitle.value = ''
  628. }
  629. })
  630. // 取消编辑
  631. const cancelEdit = () => {
  632. // 清除编辑状态
  633. editingConversationId.value = undefined
  634. editingTitle.value = ''
  635. }
  636. onMounted(() => {
  637. scrollToBottom()
  638. })
  639. //杂项
  640. const getUserInfos = ref<{
  641. avatar: string
  642. userName: string
  643. }>(Local.get('userInfo') || {})
  644. const canSendMessage = computed(() => {
  645. return !inputMessage.value.trim() || loadingModels.value || loadConversations.value || loadingMessage.value
  646. })
  647. const router = useRouter()
  648. const redirectToModelManager = () => router.push('model')
  649. // 设置面板相关状态
  650. const showSettingsPanel = ref(false)
  651. const showToolCalls = ref(false)
  652. // 收藏消息功能
  653. const favoriteMessageIdx = ref(-1)
  654. const { loading: loadingFavoriteMessage, doLoading: toggleFavorite } = useLoading(async (messageIndex: number) => {
  655. favoriteMessageIdx.value = messageIndex
  656. const data = messages.value[messageIndex]
  657. //@ts-ignore
  658. const active = activeConversationId.value === -1 ? data['session_id'] : activeConversationId.value ?? undefined
  659. if (active === undefined) {
  660. return
  661. }
  662. const success = assist.session.message
  663. .bookmark(active, data.id, !(data.like ?? false))
  664. .then(() => true)
  665. .catch(() => false)
  666. .finally(() => (favoriteMessageIdx.value = -1))
  667. if (!success) {
  668. return
  669. }
  670. messages.value[messageIndex] = {
  671. ...data,
  672. like: !(data.like ?? false),
  673. }
  674. })
  675. // 收藏面板功能
  676. // 搜索功能
  677. const handleBookmarkSearch = async () => {
  678. bookmarkOptions.value.pageNum = 1
  679. await doLoadingMessage(-1)
  680. }
  681. // 分页变化
  682. const handleBookmarkPageChange = async (page: number) => {
  683. bookmarkOptions.value.pageNum = page
  684. await doLoadingMessage(-1)
  685. }
  686. // 每页数目变化
  687. const handleBookmarkPageSizeChange = async (pageSize: number) => {
  688. bookmarkOptions.value.pageSize = pageSize
  689. bookmarkOptions.value.pageNum = 1 // 重置到第一页
  690. await doLoadingMessage(-1)
  691. }
  692. // 重置功能
  693. const handleBookmarkReset = async () => {
  694. bookmarkOptions.value.keyWord = ''
  695. bookmarkOptions.value.pageNum = 1
  696. await doLoadingMessage(-1)
  697. }
  698. // 每页数目选项
  699. const pageSizeOptions = [3, 10, 20, 30, 50, 100]
  700. // 最大页数限制
  701. const maxPages = 100
  702. const exportToMarkDown = (message: Message) => {
  703. // 创建CSV内容
  704. const csvContent = message.render_content
  705. // 创建Blob对象
  706. const blob = new Blob([csvContent], { type: 'text/markdown;charset=utf-8;' })
  707. // 创建下载链接
  708. const link = document.createElement('a')
  709. const url = URL.createObjectURL(blob)
  710. link.setAttribute('href', url)
  711. link.setAttribute('download', `export_${new Date().getTime()}.md`)
  712. link.style.visibility = 'hidden'
  713. // 触发下载
  714. document.body.appendChild(link)
  715. link.click()
  716. document.body.removeChild(link)
  717. // 清理URL对象
  718. URL.revokeObjectURL(url)
  719. }
  720. // 导出对话历史
  721. const exportId = ref<number>(-1)
  722. const { loading: exportConversationLoading, doLoading: exportConversation } = useLoading(async (id: number) => {
  723. exportId.value = id
  724. const result: {
  725. messages: Message[]
  726. total: number
  727. } = await assist.session.message.list({ sessionId: id }).catch(() => {
  728. return {
  729. list: [],
  730. total: 0,
  731. }
  732. })
  733. let md = ''
  734. for (const resultElement of result.messages) {
  735. switch (resultElement.role) {
  736. case 'user': {
  737. md += '\n\n>' + resultElement.render_content + '\n\n'
  738. continue
  739. }
  740. case 'assistant': {
  741. md += '\n\n' + resultElement.render_content + '\n\n'
  742. continue
  743. }
  744. }
  745. }
  746. exportToMarkDown({
  747. id: 0,
  748. role: 'assistant',
  749. render_content: md,
  750. content: md,
  751. timestamp: Date.now(),
  752. })
  753. exportId.value = -1
  754. })
  755. const isBlank = (str: string) => {
  756. return str == null || str.trim().length === 0
  757. }
  758. </script>
  759. <template>
  760. <el-container class="chat-container">
  761. <!-- 左侧会话列表 -->
  762. <el-aside width="300px" class="chat-sidebar">
  763. <div class="sidebar-header">
  764. <h3>对话历史</h3>
  765. <el-dropdown v-model="showSettingsPanel" trigger="click" placement="bottom-end">
  766. <el-button round :icon="EleSetting" size="small">
  767. <el-icon class="el-icon--right">
  768. <ArrowDown />
  769. </el-icon>
  770. </el-button>
  771. <template #dropdown>
  772. <el-dropdown-menu v-if="!multiDeleteConversationModel.visible">
  773. <el-dropdown-item class="settings-item">
  774. <div class="settings-row">
  775. <span class="settings-label">新对话自动记录工具调用</span>
  776. <el-switch v-model="showToolCalls" size="small" />
  777. </div>
  778. </el-dropdown-item>
  779. <el-dropdown-item @click="redirectToModelManager">
  780. <span class="settings-label">模型管理</span>
  781. </el-dropdown-item>
  782. <el-dropdown-item @click="promptDialogVisible = true">
  783. <span class="settings-label">提示词管理</span>
  784. </el-dropdown-item>
  785. <el-dropdown-item @click="multiDeleteConversationModel.visible = true">
  786. <span class="settings-label">对话管理</span>
  787. </el-dropdown-item>
  788. </el-dropdown-menu>
  789. <el-dropdown-menu v-else>
  790. <el-dropdown-item @click="multiDeleteConversationClear">
  791. <el-icon>
  792. <Close />
  793. </el-icon>
  794. <span>取消选择</span>
  795. </el-dropdown-item>
  796. <el-dropdown-item @click="startMultidelete">
  797. <el-icon>
  798. <Delete />
  799. </el-icon>
  800. <span>删除选中</span>
  801. </el-dropdown-item>
  802. </el-dropdown-menu>
  803. </template>
  804. </el-dropdown>
  805. </div>
  806. <el-scrollbar class="conversation-list" v-loading="loadConversations">
  807. <div
  808. v-for="conv in displayConversations"
  809. :key="conv.session_id"
  810. @click="editingConversationId !== conv.session_id ? selectConversation(conv.session_id) : () => {}"
  811. :class="['conversation-item', { active: activeConversationId === conv.session_id, editing: editingConversationId === conv.session_id }]"
  812. >
  813. <!-- 非编辑状态 -->
  814. <div v-if="editingConversationId !== conv.session_id" class="conversation-content">
  815. <el-checkbox
  816. v-if="multiDeleteConversationModel.visible && conv.session_id !== -1 && conv.session_id !== activeConversationId"
  817. :model-value="multiDeleteConversationModel.selectedConversations.includes(conv.session_id)"
  818. @change="(enable: boolean) => enable ? multiDeleteConversationModel.selectedConversations.push(conv.session_id) : multiDeleteConversationModel.selectedConversations = multiDeleteConversationModel.selectedConversations.filter((id: number) => id !== conv.session_id)"
  819. :disabled="isConversationActive && activeConversationId === conv.session_id"
  820. @click.stop
  821. />
  822. <span class="conversation-title">{{ conv.title }}</span>
  823. </div>
  824. <!-- 编辑状态 -->
  825. <div v-else class="conversation-edit-content">
  826. <el-input
  827. v-model="editingTitle"
  828. size="small"
  829. @keydown.enter="confirmEdit(conv.session_id)"
  830. @keydown.esc="cancelEdit"
  831. class="edit-input"
  832. />
  833. </div>
  834. <!-- 操作按钮 -->
  835. <div class="conversation-actions" v-if="conv.session_id !== -1 && !multiDeleteConversationModel.visible">
  836. <!-- 非编辑状态的三点菜单 -->
  837. <template v-if="editingConversationId !== conv.session_id">
  838. <el-dropdown trigger="click" placement="bottom-end" @click.stop>
  839. <el-button type="primary" size="small" :icon="MoreFilled" class="action-btn more-btn" title="更多操作" plain circle @click.stop />
  840. <template #dropdown>
  841. <el-dropdown-menu>
  842. <el-dropdown-item @click="exportConversation(conv.session_id)">
  843. <el-button v-if="conv.session_id === exportId && exportConversationLoading" size="small" loading circle plain />
  844. <el-icon v-else>
  845. <Download />
  846. </el-icon>
  847. <span>导出</span>
  848. </el-dropdown-item>
  849. <el-dropdown-item @click="editSummary(conv.session_id)">
  850. <el-icon>
  851. <Edit />
  852. </el-icon>
  853. <span>编辑</span>
  854. </el-dropdown-item>
  855. <el-dropdown-item
  856. @click="deleteConversation(conv.session_id)"
  857. :disabled="isConversationActive || (loadingDeleteConversation && currentDeletingConversation == conv.session_id)"
  858. >
  859. <el-icon>
  860. <Delete />
  861. </el-icon>
  862. <span>删除</span>
  863. </el-dropdown-item>
  864. </el-dropdown-menu>
  865. </template>
  866. </el-dropdown>
  867. </template>
  868. <!-- 编辑状态的按钮 -->
  869. <template v-else>
  870. <el-button
  871. type="success"
  872. size="small"
  873. :icon="Check"
  874. :loading="loadingConfirmEdit"
  875. @click.stop="confirmEdit(conv.session_id)"
  876. class="action-btn confirm-btn"
  877. title="确认修改"
  878. plain
  879. circle
  880. />
  881. <el-button
  882. type="info"
  883. size="small"
  884. @click.stop="cancelEdit(conv.session_id)"
  885. class="action-btn cancel-btn"
  886. title="取消编辑"
  887. plain
  888. circle
  889. >
  890. <el-icon>
  891. <Close />
  892. </el-icon>
  893. </el-button>
  894. </template>
  895. </div>
  896. </div>
  897. </el-scrollbar>
  898. <el-button
  899. type="primary"
  900. size="large"
  901. class="create-conversation-btn"
  902. @click="createConversationAndSetItActive"
  903. :loading="creatingConversation"
  904. :disabled="isConversationActive"
  905. >创建对话
  906. </el-button>
  907. </el-aside>
  908. <!-- 右侧聊天区域 -->
  909. <el-main class="chat-main">
  910. <!-- 消息展示区域 -->
  911. <div class="messages-container" ref="messagesContainer" v-loading="loadingMessage">
  912. <div v-if="messages.length !== 0">
  913. <div v-for="(message, idx) in messages" :key="message.id" :class="['message-wrapper', message.role]">
  914. <!-- AI消息 -->
  915. <div v-if="message.role === 'assistant'" class="ai-message-container">
  916. <el-avatar class="message-avatar" :icon="ChatDotRound" />
  917. <div class="ai-message-content">
  918. <div class="message-bubble ai-bubble">
  919. <Markdown v-if="!isBlank(message.render_content)" :content="message.render_content" :plugins="plugins" class="markdown-content" />
  920. <div v-else class="loading-container">
  921. <div class="loading-dots">
  922. <span class="dot"></span>
  923. <span class="dot"></span>
  924. <span class="dot"></span>
  925. </div>
  926. <span class="loading-text">AI正在思考中...</span>
  927. </div>
  928. </div>
  929. <div class="ai-message-actions">
  930. <el-button
  931. :loading="favoriteMessageIdx === idx && loadingFavoriteMessage"
  932. @click="toggleFavorite(idx)"
  933. class="favorite-btn"
  934. :class="{ favorited: message.like ?? false }"
  935. :disabled="isConversationActive"
  936. :icon="message.like ?? false ? StarFilled : Star"
  937. plain
  938. circle
  939. />
  940. <el-button
  941. :loading="favoriteMessageIdx === idx && loadingFavoriteMessage"
  942. @click="exportToMarkDown(message)"
  943. class="favorite-btn"
  944. :disabled="isConversationActive"
  945. :icon="Download"
  946. plain
  947. circle
  948. />
  949. </div>
  950. </div>
  951. </div>
  952. <!-- 用户消息 -->
  953. <div v-if="message.role === 'user'" class="user-message-container">
  954. <div class="user-message-content">
  955. <div class="message-bubble user-bubble">
  956. {{ message.render_content }}
  957. </div>
  958. <div class="user-message-actions">
  959. <el-button
  960. type="primary"
  961. size="small"
  962. @click="replaceMessage(messages.indexOf(message))"
  963. class="retry-btn"
  964. plain
  965. :disabled="isConversationActive"
  966. >
  967. 重试
  968. </el-button>
  969. </div>
  970. </div>
  971. <el-avatar class="message-avatar" :src="getUserInfos.avatar" :icon="User" />
  972. </div>
  973. </div>
  974. </div>
  975. <!-- 空状态页面 -->
  976. <div v-else class="empty-content">
  977. <!-- 收藏页面空状态 -->
  978. <div v-if="activeConversationId === -1" class="bookmark-empty">
  979. <div class="empty-icon">
  980. <el-icon :size="80" color="#f59e0b">
  981. <StarFilled />
  982. </el-icon>
  983. </div>
  984. <div class="empty-text">
  985. <h2 class="empty-title">暂无收藏消息</h2>
  986. <p class="empty-description">您还没有收藏任何对话消息</p>
  987. </div>
  988. <div class="empty-tips">
  989. <div class="tip-item">
  990. <el-icon color="#409eff">
  991. <InfoFilled />
  992. </el-icon>
  993. <span>在对话中点击 ⭐ 按钮即可收藏消息</span>
  994. </div>
  995. <div class="tip-item">
  996. <el-icon color="#67c23a">
  997. <Search />
  998. </el-icon>
  999. <span>收藏的消息支持关键词搜索</span>
  1000. </div>
  1001. <div class="tip-item">
  1002. <el-icon color="#e6a23c">
  1003. <Collection />
  1004. </el-icon>
  1005. <span>收藏消息会保存在云端,永不丢失</span>
  1006. </div>
  1007. </div>
  1008. </div>
  1009. <!-- 普通对话空状态 -->
  1010. <div v-else class="chat-empty">
  1011. <div class="empty-icon">
  1012. <el-icon :size="80" color="#d1d5db">
  1013. <ChatDotRound />
  1014. </el-icon>
  1015. </div>
  1016. <div class="empty-text">
  1017. <h2 class="empty-title">开始新的对话</h2>
  1018. </div>
  1019. <div class="example-questions">
  1020. <h4>试试这些问题:</h4>
  1021. <div class="question-tags">
  1022. <el-tag class="question-tag" @click="inputMessage = '帮我查看设备运行状态和告警信息'" type="info">
  1023. 帮我查看设备运行状态和告警信息
  1024. </el-tag>
  1025. <el-tag class="question-tag" @click="inputMessage = '分析用户权限配置和角色分配情况'" type="success">
  1026. 分析用户权限配置和角色分配情况
  1027. </el-tag>
  1028. <el-tag class="question-tag" @click="inputMessage = '检查系统性能和在线用户统计'" type="warning"> 检查系统性能和在线用户统计 </el-tag>
  1029. </div>
  1030. </div>
  1031. </div>
  1032. </div>
  1033. <div class="messages-spacer"></div>
  1034. </div>
  1035. <!-- 附件栏(内嵌到输入框上方) -->
  1036. <div class="input-container" v-if="activeConversationId !== -1">
  1037. <!-- 大输入框容器 -->
  1038. <div class="large-input-container">
  1039. <!-- 附件栏:紧贴输入框上方,位于容器内部 -->
  1040. <div class="attachments-inline">
  1041. <el-scrollbar>
  1042. <div class="attachments-inline-scroll">
  1043. <div
  1044. v-for="(file, fIdx) in selectedFiles"
  1045. :key="file.name + '_' + file.size + '_' + (file as any).lastModified"
  1046. class="attachment-card"
  1047. title=""
  1048. >
  1049. <button class="control-btn attachment-item" :title="`${file.name}`">
  1050. <el-icon :size="10"><CopyDocument /></el-icon>
  1051. <span class="attachment-name">{{ file.name }}</span>
  1052. </button>
  1053. <button class="remove-attachment-icon" @click="removeAttachment(fIdx)">
  1054. <el-icon><Close /></el-icon>
  1055. </button>
  1056. </div>
  1057. </div>
  1058. </el-scrollbar>
  1059. <button class="control-btn add-attachment-btn" @click="open">
  1060. <el-icon :size="10"><Plus /></el-icon>
  1061. <span>添加附件</span>
  1062. </button>
  1063. </div>
  1064. <!-- 输入框 -->
  1065. <textarea
  1066. v-model="inputMessage"
  1067. class="large-textarea"
  1068. placeholder="请输入您的问题..."
  1069. @keydown.enter.ctrl="sendMessage"
  1070. @keydown.enter.meta="sendMessage"
  1071. :disabled="isConversationActive"
  1072. rows="3"
  1073. ></textarea>
  1074. <!-- 内嵌按钮区域 -->
  1075. <div class="embedded-controls">
  1076. <!-- 左下角按钮组 -->
  1077. <div class="left-controls">
  1078. <!-- 模型选择按钮 -->
  1079. <el-dropdown trigger="click" placement="top-start" :disabled="loadingModels || modelLabel == '未配置模型'">
  1080. <button class="control-btn model-btn">
  1081. <el-icon>
  1082. <Setting v-if="!loadingModels" />
  1083. <Loading v-else class="spin" />
  1084. </el-icon>
  1085. <span>{{ modelLabel }}</span>
  1086. </button>
  1087. <template #dropdown>
  1088. <el-dropdown-menu>
  1089. <el-dropdown-item
  1090. v-for="item in modelOptions"
  1091. :key="item.id"
  1092. @click="selectedModel = item.id"
  1093. :class="{ 'is-selected': selectedModel === item.id }"
  1094. >
  1095. {{ item.modelName }}
  1096. </el-dropdown-item>
  1097. </el-dropdown-menu>
  1098. </template>
  1099. </el-dropdown>
  1100. <!-- 词嵌入模型选择按钮 -->
  1101. <el-dropdown trigger="click" placement="top-start" :disabled="loadingModels || embeddingModelLabel == '未配置词嵌入'">
  1102. <button class="control-btn embedding-model-btn">
  1103. <el-icon>
  1104. <CopyDocument v-if="!loadingEmbeddingModels" />
  1105. <Loading v-else class="spin" />
  1106. </el-icon>
  1107. <span>{{ embeddingModelLabel }}</span>
  1108. </button>
  1109. <template #dropdown>
  1110. <el-dropdown-menu>
  1111. <el-dropdown-item
  1112. v-for="item in embeddingModelOptions"
  1113. :key="item.id"
  1114. @click="selectedEmbeddingModel = item.id"
  1115. :class="{ 'is-selected': selectedEmbeddingModel === item.id }"
  1116. >
  1117. {{ item.modelName }}
  1118. </el-dropdown-item>
  1119. </el-dropdown-menu>
  1120. </template>
  1121. </el-dropdown>
  1122. <!-- 词嵌入模型选择按钮 -->
  1123. <el-dropdown trigger="click" placement="top-start" :disabled="loadingModels || promptLabel == '未配置提示词'">
  1124. <button class="control-btn embedding-model-btn">
  1125. <el-icon>
  1126. <CopyDocument v-if="!loadingPromptList" />
  1127. <Loading v-else class="spin" />
  1128. </el-icon>
  1129. <span>{{ promptLabel }}</span>
  1130. </button>
  1131. <template #dropdown>
  1132. <el-dropdown-menu>
  1133. <el-dropdown-item
  1134. v-for="item in displayPromptList"
  1135. :key="item.id"
  1136. @click="selectPromptId = item.id"
  1137. :class="{ 'is-selected': selectPromptId === item.id }"
  1138. >
  1139. {{ item.title }}
  1140. </el-dropdown-item>
  1141. </el-dropdown-menu>
  1142. </template>
  1143. </el-dropdown>
  1144. </div>
  1145. <!-- 右下角按钮组 -->
  1146. <div class="right-controls">
  1147. <!-- 清空按钮 -->
  1148. <button v-show="messages.length !== 0" class="control-btn clear-btn" @click="clearMessage" :disabled="loadingClearMessage">
  1149. <el-icon v-if="loadingClearMessage"><Loading /></el-icon>
  1150. <el-icon v-else><Delete /></el-icon>
  1151. </button>
  1152. <!-- 发送按钮 -->
  1153. <button v-if="!isConversationActive" class="control-btn send-btn" @click="sendMessage" :disabled="canSendMessage">
  1154. <el-icon><Promotion /></el-icon>
  1155. </button>
  1156. <button v-else class="control-btn stop-btn" @click="stopConversation">
  1157. <el-icon><VideoPause /></el-icon>
  1158. </button>
  1159. </div>
  1160. </div>
  1161. </div>
  1162. </div>
  1163. <!-- 收藏面板底部栏 -->
  1164. <div class="input-container bookmark-footer" v-else>
  1165. <div class="bookmark-controls">
  1166. <!-- 搜索框 -->
  1167. <div class="bookmark-search">
  1168. <el-input
  1169. v-model="bookmarkOptions.keyWord"
  1170. placeholder="搜索收藏的消息..."
  1171. :prefix-icon="Search"
  1172. clearable
  1173. @keydown.enter="handleBookmarkSearch"
  1174. @clear="handleBookmarkReset"
  1175. style="width: 300px"
  1176. />
  1177. <el-button type="primary" :icon="Search" @click="handleBookmarkSearch" :loading="loadingMessage"> 搜索 </el-button>
  1178. </div>
  1179. <!-- 分页和重置 -->
  1180. <div class="bookmark-pagination">
  1181. <el-button @click="handleBookmarkReset" :loading="loadingMessage"> 重置</el-button>
  1182. <el-pagination
  1183. v-model:current-page="bookmarkOptions.pageNum"
  1184. v-model:page-size="bookmarkOptions.pageSize"
  1185. :total="bookmarkTotal"
  1186. :page-sizes="pageSizeOptions"
  1187. :pager-count="7"
  1188. :max-page="maxPages"
  1189. layout="total, sizes, prev, pager, next, jumper"
  1190. @current-change="handleBookmarkPageChange"
  1191. @size-change="handleBookmarkPageSizeChange"
  1192. :disabled="loadingMessage"
  1193. small
  1194. />
  1195. </div>
  1196. </div>
  1197. </div>
  1198. </el-main>
  1199. <!-- 提示词管理对话框 -->
  1200. <el-dialog v-model="promptDialogVisible" title="提示词管理" width="60%" append-to-body>
  1201. <div class="prompt-dialog-content">
  1202. <div class="prompt-input-section">
  1203. <h4>自定义提示词</h4>
  1204. <el-input
  1205. v-model="customPrompt"
  1206. type="textarea"
  1207. :autosize="{ minRows: 12, maxRows: 24 }"
  1208. placeholder="在此编写你的提示词...(将用于本次会话的系统提示)"
  1209. />
  1210. </div>
  1211. </div>
  1212. <template #footer>
  1213. <div class="dialog-footer">
  1214. <el-button @click="promptDialogVisible = false">取 消</el-button>
  1215. <el-button type="primary" @click="promptDialogVisible = false">确 认</el-button>
  1216. </div>
  1217. </template>
  1218. </el-dialog>
  1219. </el-container>
  1220. </template>
  1221. <style scoped lang="scss">
  1222. :deep(.el-icon) {
  1223. margin-right: 0 !important;
  1224. }
  1225. .chat-container {
  1226. height: 100%;
  1227. background: var(--el-bg-color-page);
  1228. }
  1229. .create-conversation-btn {
  1230. margin: 16px;
  1231. }
  1232. /* 左侧边栏样式 */
  1233. .chat-sidebar {
  1234. background: var(--el-bg-color);
  1235. border-right: 1px solid var(--el-border-color-light);
  1236. display: flex;
  1237. flex-direction: column;
  1238. }
  1239. .sidebar-header {
  1240. padding: 20px;
  1241. border-bottom: 1px solid var(--el-border-color-light);
  1242. display: flex;
  1243. justify-content: space-between;
  1244. align-items: center;
  1245. h3 {
  1246. margin: 0;
  1247. color: var(--el-text-color-primary);
  1248. font-size: 16px;
  1249. font-weight: 600;
  1250. }
  1251. }
  1252. /* 设置面板样式 */
  1253. :deep(.el-dropdown-menu) {
  1254. .settings-item {
  1255. padding: 0;
  1256. &:hover {
  1257. background: none;
  1258. }
  1259. }
  1260. .settings-row {
  1261. display: flex;
  1262. align-items: center;
  1263. justify-content: space-between;
  1264. padding: 8px 16px;
  1265. min-width: 160px;
  1266. &:hover {
  1267. background: var(--el-fill-color-light);
  1268. }
  1269. }
  1270. .settings-label {
  1271. font-size: 14px;
  1272. color: var(--el-text-color-primary);
  1273. flex: 1;
  1274. }
  1275. }
  1276. .conversation-list {
  1277. flex: 1;
  1278. padding: 10px;
  1279. }
  1280. .conversation-item {
  1281. display: flex;
  1282. align-items: center;
  1283. justify-content: space-between;
  1284. padding: 12px 16px;
  1285. margin-bottom: 8px;
  1286. border-radius: 8px;
  1287. transition: all 0.2s;
  1288. color: var(--el-text-color-regular);
  1289. border: 1px solid transparent;
  1290. position: relative;
  1291. cursor: pointer;
  1292. &:hover {
  1293. background: var(--el-fill-color-light);
  1294. color: var(--el-text-color-primary);
  1295. .conversation-actions {
  1296. opacity: 1;
  1297. }
  1298. }
  1299. &.active {
  1300. background: var(--el-color-primary-light-9);
  1301. color: var(--el-color-primary);
  1302. border-color: var(--el-color-primary-light-7);
  1303. .conversation-actions {
  1304. opacity: 1;
  1305. }
  1306. }
  1307. &.editing {
  1308. background: var(--el-color-warning-light-9);
  1309. border-color: var(--el-color-warning-light-7);
  1310. .conversation-actions {
  1311. opacity: 1;
  1312. }
  1313. }
  1314. }
  1315. .conversation-content {
  1316. flex: 1;
  1317. min-width: 0;
  1318. cursor: pointer;
  1319. display: flex;
  1320. align-items: center;
  1321. .el-checkbox {
  1322. margin-right: 8px;
  1323. }
  1324. }
  1325. .conversation-edit-content {
  1326. flex: 1;
  1327. min-width: 0;
  1328. }
  1329. .edit-input {
  1330. width: 100%;
  1331. :deep(.el-input__inner) {
  1332. font-size: 14px;
  1333. padding: 4px 8px;
  1334. height: 28px;
  1335. line-height: 20px;
  1336. }
  1337. }
  1338. .conversation-title {
  1339. display: block;
  1340. overflow: hidden;
  1341. text-overflow: ellipsis;
  1342. white-space: nowrap;
  1343. }
  1344. .conversation-actions {
  1345. display: flex;
  1346. align-items: center;
  1347. gap: 4px;
  1348. opacity: 0;
  1349. transition: opacity 0.2s;
  1350. flex-shrink: 0;
  1351. }
  1352. .action-btn {
  1353. width: 28px !important;
  1354. height: 28px !important;
  1355. padding: 0 !important;
  1356. margin: 0 2px;
  1357. }
  1358. .more-btn {
  1359. &:hover {
  1360. background: var(--el-color-primary-light-9) !important;
  1361. border-color: var(--el-color-primary-light-7) !important;
  1362. }
  1363. }
  1364. /* 对话历史三点菜单样式 */
  1365. .conversation-actions {
  1366. :deep(.el-dropdown-menu) {
  1367. .el-dropdown-menu__item {
  1368. display: flex;
  1369. align-items: center;
  1370. gap: 8px;
  1371. padding: 8px 16px;
  1372. .el-icon {
  1373. font-size: 14px;
  1374. color: var(--el-text-color-regular);
  1375. }
  1376. span {
  1377. font-size: 14px;
  1378. color: var(--el-text-color-primary);
  1379. }
  1380. &:hover {
  1381. background: var(--el-fill-color-light);
  1382. .el-icon {
  1383. color: var(--el-color-primary);
  1384. }
  1385. }
  1386. &.is-disabled {
  1387. .el-icon,
  1388. span {
  1389. color: var(--el-text-color-disabled);
  1390. }
  1391. }
  1392. }
  1393. }
  1394. }
  1395. /* 右侧聊天区域样式 */
  1396. .chat-main {
  1397. display: flex;
  1398. flex-direction: column;
  1399. padding: 0;
  1400. background: var(--el-bg-color-page);
  1401. position: relative;
  1402. }
  1403. .messages-container {
  1404. flex: 1;
  1405. padding: 20px;
  1406. overflow-y: auto;
  1407. }
  1408. .messages-spacer {
  1409. height: 160px;
  1410. }
  1411. .message-wrapper {
  1412. margin-bottom: 20px;
  1413. &:last-child {
  1414. margin-bottom: 0;
  1415. }
  1416. }
  1417. /* AI消息样式 */
  1418. .ai-message-container {
  1419. display: flex;
  1420. align-items: flex-start;
  1421. gap: 12px;
  1422. }
  1423. .ai-message-content {
  1424. display: flex;
  1425. flex-direction: column;
  1426. align-items: flex-start;
  1427. gap: 8px;
  1428. width: 70%;
  1429. }
  1430. .ai-message-actions {
  1431. display: flex;
  1432. justify-content: flex-start;
  1433. transition: opacity 0.2s ease;
  1434. }
  1435. .favorite-btn {
  1436. width: 16px !important;
  1437. height: 16px !important;
  1438. padding: 0 !important;
  1439. border: none !important;
  1440. background: transparent !important;
  1441. color: var(--el-text-color-regular);
  1442. transition: all 0.2s ease;
  1443. &:hover {
  1444. background: var(--el-fill-color-light) !important;
  1445. }
  1446. &.favorited {
  1447. color: #ff4757;
  1448. }
  1449. .el-icon {
  1450. font-size: 16px;
  1451. }
  1452. }
  1453. .ai-bubble {
  1454. background: var(--el-fill-color-light);
  1455. color: var(--el-text-color-primary);
  1456. position: relative;
  1457. max-width: 100%;
  1458. border: 1px solid var(--el-border-color-lighter);
  1459. &::before {
  1460. content: '';
  1461. position: absolute;
  1462. left: -8px;
  1463. top: 12px;
  1464. width: 0;
  1465. height: 0;
  1466. border-right: 8px solid var(--el-fill-color-light);
  1467. border-top: 8px solid transparent;
  1468. border-bottom: 8px solid transparent;
  1469. }
  1470. }
  1471. /* 用户消息样式 */
  1472. .user-message-container {
  1473. display: flex;
  1474. align-items: flex-start;
  1475. gap: 12px;
  1476. justify-content: flex-end;
  1477. }
  1478. .user-message-content {
  1479. display: flex;
  1480. flex-direction: column;
  1481. align-items: flex-end;
  1482. gap: 8px;
  1483. width: 70%;
  1484. }
  1485. .user-message-actions {
  1486. display: flex;
  1487. justify-content: flex-end;
  1488. opacity: 0;
  1489. transition: opacity 0.2s ease;
  1490. }
  1491. .user-message-container:hover .user-message-actions {
  1492. opacity: 1;
  1493. }
  1494. .retry-btn {
  1495. font-size: 12px;
  1496. padding: 4px 12px;
  1497. height: 24px;
  1498. border-radius: 12px;
  1499. }
  1500. .user-bubble {
  1501. background: var(--el-color-primary);
  1502. color: white;
  1503. position: relative;
  1504. max-width: 100%;
  1505. &::after {
  1506. content: '';
  1507. position: absolute;
  1508. right: -8px;
  1509. top: 12px;
  1510. width: 0;
  1511. height: 0;
  1512. border-left: 8px solid var(--el-color-primary);
  1513. border-top: 8px solid transparent;
  1514. border-bottom: 8px solid transparent;
  1515. }
  1516. }
  1517. /* 消息气泡通用样式 */
  1518. .message-bubble {
  1519. padding: 12px 16px;
  1520. border-radius: 12px;
  1521. line-height: 1.5;
  1522. word-wrap: break-word;
  1523. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  1524. }
  1525. .message-avatar {
  1526. flex-shrink: 0;
  1527. margin-top: 2px;
  1528. }
  1529. /* 加载动画样式 */
  1530. .loading-container {
  1531. display: flex;
  1532. align-items: center;
  1533. gap: 12px;
  1534. padding: 8px 0;
  1535. color: var(--el-text-color-secondary);
  1536. }
  1537. .loading-dots {
  1538. display: flex;
  1539. gap: 4px;
  1540. }
  1541. .dot {
  1542. width: 6px;
  1543. height: 6px;
  1544. border-radius: 50%;
  1545. background-color: var(--el-color-primary);
  1546. animation: loading-bounce 1.4s ease-in-out infinite both;
  1547. }
  1548. .dot:nth-child(1) {
  1549. animation-delay: -0.32s;
  1550. }
  1551. .dot:nth-child(2) {
  1552. animation-delay: -0.16s;
  1553. }
  1554. .dot:nth-child(3) {
  1555. animation-delay: 0s;
  1556. }
  1557. @keyframes loading-bounce {
  1558. 0%,
  1559. 80%,
  1560. 100% {
  1561. transform: scale(0.8);
  1562. opacity: 0.5;
  1563. }
  1564. 40% {
  1565. transform: scale(1);
  1566. opacity: 1;
  1567. }
  1568. }
  1569. .loading-text {
  1570. font-size: 14px;
  1571. font-weight: 500;
  1572. }
  1573. .tools-button {
  1574. margin-bottom: 10px;
  1575. color: var(--el-text-color-regular);
  1576. }
  1577. .message-input {
  1578. margin-bottom: 12px;
  1579. :deep(.el-textarea__inner) {
  1580. border-radius: 8px;
  1581. resize: none;
  1582. }
  1583. }
  1584. .input-actions {
  1585. display: flex;
  1586. justify-content: flex-end;
  1587. gap: 12px;
  1588. }
  1589. /* Markdown 内容样式优化 - 接入Element Plus颜色系统 */
  1590. :deep(.markdown-content) {
  1591. /* 标题样式 */
  1592. h1,
  1593. h2,
  1594. h3,
  1595. h4,
  1596. h5,
  1597. h6 {
  1598. margin-top: 24px;
  1599. margin-bottom: 16px;
  1600. font-weight: 600;
  1601. line-height: 1.25;
  1602. color: var(--el-text-color-primary);
  1603. }
  1604. h1 {
  1605. font-size: 2em;
  1606. border-bottom: 1px solid var(--el-border-color-light);
  1607. padding-bottom: 8px;
  1608. }
  1609. h2 {
  1610. font-size: 1.5em;
  1611. border-bottom: 1px solid var(--el-border-color-lighter);
  1612. padding-bottom: 6px;
  1613. }
  1614. h3 {
  1615. font-size: 1.25em;
  1616. }
  1617. /* 段落样式 */
  1618. p {
  1619. margin-bottom: 16px;
  1620. line-height: 1.6;
  1621. color: var(--el-text-color-regular);
  1622. }
  1623. /* 代码块样式 */
  1624. pre {
  1625. background-color: var(--el-fill-color-light);
  1626. border: 1px solid var(--el-border-color-light);
  1627. border-radius: 6px;
  1628. padding: 16px;
  1629. overflow: auto;
  1630. margin: 16px 0;
  1631. color: var(--el-text-color-primary);
  1632. }
  1633. /* 行内代码样式 */
  1634. code {
  1635. background-color: var(--el-fill-color-light);
  1636. color: var(--el-color-danger);
  1637. padding: 2px 4px;
  1638. border-radius: 3px;
  1639. font-size: 85%;
  1640. border: 1px solid var(--el-border-color-lighter);
  1641. }
  1642. /* 引用块样式 */
  1643. blockquote {
  1644. border-left: 4px solid var(--el-color-primary);
  1645. padding-left: 16px;
  1646. margin: 16px 0;
  1647. color: var(--el-text-color-secondary);
  1648. background-color: var(--el-fill-color-extra-light);
  1649. padding: 12px 16px;
  1650. border-radius: 4px;
  1651. }
  1652. /* 列表样式 */
  1653. ul,
  1654. ol {
  1655. margin: 16px 0;
  1656. padding-left: 32px;
  1657. color: var(--el-text-color-regular);
  1658. }
  1659. li {
  1660. margin: 4px 0;
  1661. line-height: 1.6;
  1662. }
  1663. /* 表格样式 */
  1664. table {
  1665. border-collapse: collapse;
  1666. margin: 16px 0;
  1667. width: 100%;
  1668. border: 1px solid var(--el-border-color-light);
  1669. }
  1670. th,
  1671. td {
  1672. border: 1px solid var(--el-border-color-light);
  1673. padding: 8px 12px;
  1674. text-align: left;
  1675. }
  1676. th {
  1677. background-color: var(--el-fill-color-light);
  1678. font-weight: 600;
  1679. color: var(--el-text-color-primary);
  1680. }
  1681. td {
  1682. color: var(--el-text-color-regular);
  1683. }
  1684. /* 分割线样式 */
  1685. hr {
  1686. border: none;
  1687. border-top: 1px solid var(--el-border-color-light);
  1688. margin: 24px 0;
  1689. }
  1690. /* 链接样式 */
  1691. a {
  1692. color: var(--el-color-primary);
  1693. text-decoration: none;
  1694. &:hover {
  1695. color: var(--el-color-primary-light-3);
  1696. text-decoration: underline;
  1697. }
  1698. }
  1699. /* 强调文本样式 */
  1700. strong {
  1701. font-weight: 600;
  1702. color: var(--el-text-color-primary);
  1703. }
  1704. em {
  1705. font-style: italic;
  1706. color: var(--el-text-color-regular);
  1707. }
  1708. }
  1709. /* 新的输入容器样式 */
  1710. .input-container {
  1711. position: absolute;
  1712. width: 90%;
  1713. left: 50%;
  1714. bottom: 25px;
  1715. transform: translate(-50%, 0);
  1716. background: var(--el-bg-color);
  1717. border: 1px solid var(--el-border-color-light);
  1718. border-radius: 8px;
  1719. padding: 0;
  1720. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  1721. }
  1722. /* 附件栏(内嵌)样式 */
  1723. .attachments-inline {
  1724. width: 100%;
  1725. padding: 8px;
  1726. display: flex;
  1727. align-items: center;
  1728. gap: 8px;
  1729. }
  1730. .attachments-inline-scroll {
  1731. flex: 1;
  1732. //overflow-x: auto;
  1733. display: flex;
  1734. align-items: center;
  1735. gap: 8px;
  1736. padding: 6px;
  1737. }
  1738. .attachment-card {
  1739. position: relative;
  1740. display: inline-flex;
  1741. align-items: center;
  1742. gap: 0;
  1743. }
  1744. .attachment-item {
  1745. /* 对齐下方控制按钮的尺寸与风格 */
  1746. max-width: 260px;
  1747. white-space: nowrap;
  1748. overflow: hidden;
  1749. text-overflow: ellipsis;
  1750. font-size: 10px!important;
  1751. }
  1752. .attachment-name {
  1753. max-width: 200px;
  1754. white-space: nowrap;
  1755. overflow: hidden;
  1756. text-overflow: ellipsis;
  1757. }
  1758. .add-attachment-btn {
  1759. flex-shrink: 0;
  1760. font-size: 10px!important;
  1761. }
  1762. .remove-attachment-icon {
  1763. position: absolute;
  1764. top: -6px;
  1765. right: -6px;
  1766. width: 18px;
  1767. height: 18px;
  1768. border-radius: 50%;
  1769. border: 1px solid var(--el-border-color-light);
  1770. background: var(--el-bg-color);
  1771. display: flex;
  1772. align-items: center;
  1773. justify-content: center;
  1774. cursor: pointer;
  1775. color: var(--el-text-color-regular);
  1776. padding: 0;
  1777. }
  1778. .large-input-container {
  1779. position: relative;
  1780. width: 100%;
  1781. }
  1782. .large-textarea {
  1783. width: 100%;
  1784. min-height: 120px;
  1785. padding: 16px 16px 60px 16px;
  1786. border: none;
  1787. outline: none;
  1788. resize: none;
  1789. font-size: 14px;
  1790. line-height: 1.5;
  1791. background: transparent;
  1792. color: var(--el-text-color-primary);
  1793. border-radius: 8px;
  1794. font-family: inherit;
  1795. }
  1796. .large-textarea::placeholder {
  1797. color: var(--el-text-color-placeholder);
  1798. }
  1799. .large-textarea:disabled {
  1800. color: var(--el-text-color-disabled);
  1801. cursor: not-allowed;
  1802. }
  1803. .embedded-controls {
  1804. position: absolute;
  1805. bottom: 0;
  1806. left: 0;
  1807. right: 0;
  1808. display: flex;
  1809. justify-content: space-between;
  1810. align-items: center;
  1811. padding: 12px 16px;
  1812. border-top: 1px solid var(--el-border-color-lighter);
  1813. background: var(--el-fill-color-extra-light);
  1814. border-radius: 0 0 8px 8px;
  1815. }
  1816. .left-controls,
  1817. .right-controls {
  1818. display: flex;
  1819. align-items: center;
  1820. gap: 8px;
  1821. }
  1822. .control-btn {
  1823. display: flex;
  1824. align-items: center;
  1825. gap: 6px;
  1826. padding: 6px 12px;
  1827. border: 1px solid var(--el-border-color-light);
  1828. border-radius: 6px;
  1829. background: var(--el-bg-color);
  1830. color: var(--el-text-color-regular);
  1831. font-size: 12px;
  1832. cursor: pointer;
  1833. transition: all 0.2s ease;
  1834. white-space: nowrap;
  1835. }
  1836. .control-btn:hover:not(:disabled) {
  1837. background: var(--el-fill-color-light);
  1838. border-color: var(--el-color-primary-light-7);
  1839. color: var(--el-color-primary);
  1840. }
  1841. .control-btn:disabled {
  1842. opacity: 0.5;
  1843. cursor: not-allowed;
  1844. }
  1845. .control-btn .el-icon {
  1846. font-size: 14px;
  1847. }
  1848. /* 特定按钮样式 */
  1849. .send-btn {
  1850. background: var(--el-color-primary);
  1851. color: white;
  1852. border-color: var(--el-color-primary);
  1853. }
  1854. .send-btn:hover:not(:disabled) {
  1855. background: var(--el-color-primary-light-3);
  1856. border-color: var(--el-color-primary-light-3);
  1857. }
  1858. .stop-btn {
  1859. background: var(--el-color-danger);
  1860. color: white;
  1861. border-color: var(--el-color-danger);
  1862. }
  1863. .stop-btn:hover:not(:disabled) {
  1864. background: var(--el-color-danger-light-3);
  1865. border-color: var(--el-color-danger-light-3);
  1866. }
  1867. .clear-btn:hover:not(:disabled) {
  1868. background: var(--el-color-warning-light-9);
  1869. border-color: var(--el-color-warning-light-7);
  1870. color: var(--el-color-warning);
  1871. }
  1872. /* 下拉菜单样式 */
  1873. :deep(.el-dropdown-menu) {
  1874. .el-dropdown-menu__item.is-selected {
  1875. background: var(--el-color-primary-light-9);
  1876. color: var(--el-color-primary);
  1877. }
  1878. }
  1879. /* 对话框样式 */
  1880. .dialog-footer {
  1881. text-align: right;
  1882. }
  1883. /* 提示词对话框样式 */
  1884. .prompt-dialog-content {
  1885. display: flex;
  1886. flex-direction: column;
  1887. gap: 24px;
  1888. max-height: 600px;
  1889. }
  1890. .prompt-input-section {
  1891. h4 {
  1892. margin: 0 0 12px 0;
  1893. color: var(--el-text-color-primary);
  1894. font-size: 14px;
  1895. font-weight: 600;
  1896. }
  1897. }
  1898. .prompt-list-section {
  1899. border-top: 1px solid var(--el-border-color-lighter);
  1900. padding-top: 20px;
  1901. h4 {
  1902. margin: 0 0 16px 0;
  1903. color: var(--el-text-color-primary);
  1904. font-size: 14px;
  1905. font-weight: 600;
  1906. }
  1907. }
  1908. .prompt-list-header {
  1909. display: flex;
  1910. justify-content: space-between;
  1911. align-items: center;
  1912. margin-bottom: 16px;
  1913. .prompt-search {
  1914. display: flex;
  1915. align-items: center;
  1916. gap: 8px;
  1917. }
  1918. }
  1919. .prompt-list-container {
  1920. min-height: 200px;
  1921. max-height: 300px;
  1922. overflow-y: auto;
  1923. border: 1px solid var(--el-border-color-light);
  1924. border-radius: 6px;
  1925. background: var(--el-fill-color-extra-light);
  1926. }
  1927. .prompt-list-empty {
  1928. display: flex;
  1929. align-items: center;
  1930. justify-content: center;
  1931. height: 200px;
  1932. }
  1933. .prompt-list {
  1934. padding: 8px;
  1935. }
  1936. .prompt-item {
  1937. padding: 12px;
  1938. margin-bottom: 8px;
  1939. border: 1px solid var(--el-border-color-lighter);
  1940. border-radius: 6px;
  1941. background: var(--el-bg-color);
  1942. cursor: pointer;
  1943. transition: all 0.2s ease;
  1944. &:hover {
  1945. border-color: var(--el-color-primary-light-7);
  1946. background: var(--el-color-primary-light-9);
  1947. transform: translateY(-1px);
  1948. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1949. }
  1950. &:last-child {
  1951. margin-bottom: 0;
  1952. }
  1953. }
  1954. .prompt-item-header {
  1955. display: flex;
  1956. justify-content: space-between;
  1957. align-items: center;
  1958. margin-bottom: 8px;
  1959. }
  1960. .prompt-title {
  1961. font-weight: 600;
  1962. color: var(--el-text-color-primary);
  1963. font-size: 14px;
  1964. }
  1965. .prompt-preview {
  1966. color: var(--el-text-color-regular);
  1967. font-size: 12px;
  1968. line-height: 1.4;
  1969. word-break: break-all;
  1970. }
  1971. .prompt-pagination {
  1972. margin-top: 16px;
  1973. display: flex;
  1974. justify-content: center;
  1975. }
  1976. /* 响应式调整 */
  1977. /* 空状态页面样式 */
  1978. .empty-content {
  1979. text-align: center;
  1980. max-width: 600px;
  1981. width: 100%;
  1982. position: absolute;
  1983. left: 50%;
  1984. top: 50%;
  1985. transform: translate(-50%, -50%);
  1986. }
  1987. .empty-icon {
  1988. margin-bottom: 24px;
  1989. opacity: 0.6;
  1990. }
  1991. .empty-text {
  1992. margin-bottom: 40px;
  1993. }
  1994. .empty-title {
  1995. font-size: 28px;
  1996. font-weight: 600;
  1997. color: var(--el-text-color-primary);
  1998. margin: 0 0 12px 0;
  1999. }
  2000. .empty-description {
  2001. font-size: 16px;
  2002. color: var(--el-text-color-regular);
  2003. margin: 0;
  2004. line-height: 1.5;
  2005. }
  2006. .quick-start {
  2007. display: flex;
  2008. justify-content: center;
  2009. gap: 40px;
  2010. margin-bottom: 40px;
  2011. flex-wrap: wrap;
  2012. }
  2013. .quick-start-item {
  2014. display: flex;
  2015. align-items: center;
  2016. gap: 12px;
  2017. color: var(--el-text-color-regular);
  2018. }
  2019. .step-number {
  2020. display: flex;
  2021. align-items: center;
  2022. justify-content: center;
  2023. width: 24px;
  2024. height: 24px;
  2025. background: var(--el-color-primary);
  2026. color: white;
  2027. border-radius: 50%;
  2028. font-size: 12px;
  2029. font-weight: 600;
  2030. flex-shrink: 0;
  2031. }
  2032. .step-text {
  2033. font-size: 14px;
  2034. font-weight: 500;
  2035. }
  2036. .example-questions {
  2037. h4 {
  2038. font-size: 16px;
  2039. font-weight: 600;
  2040. color: var(--el-text-color-primary);
  2041. margin: 0 0 16px 0;
  2042. }
  2043. }
  2044. .question-tags {
  2045. display: flex;
  2046. flex-wrap: wrap;
  2047. gap: 12px;
  2048. justify-content: center;
  2049. }
  2050. .question-tag {
  2051. cursor: pointer;
  2052. transition: all 0.2s ease;
  2053. font-size: 13px;
  2054. padding: 8px 16px;
  2055. border-radius: 20px;
  2056. &:hover {
  2057. transform: translateY(-1px);
  2058. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  2059. }
  2060. }
  2061. /* 收藏面板底部栏样式 */
  2062. .bookmark-footer {
  2063. border-radius: 8px;
  2064. padding: 16px;
  2065. .bookmark-controls {
  2066. display: flex;
  2067. justify-content: space-between;
  2068. align-items: center;
  2069. gap: 16px;
  2070. width: 100%;
  2071. }
  2072. .bookmark-search {
  2073. display: flex;
  2074. align-items: center;
  2075. gap: 12px;
  2076. }
  2077. .bookmark-pagination {
  2078. display: flex;
  2079. align-items: center;
  2080. gap: 16px;
  2081. flex-wrap: wrap;
  2082. }
  2083. }
  2084. /* 响应式调整 */
  2085. @media (max-width: 768px) {
  2086. .bookmark-footer {
  2087. .bookmark-controls {
  2088. flex-direction: column;
  2089. gap: 12px;
  2090. }
  2091. .bookmark-search {
  2092. width: 100%;
  2093. justify-content: center;
  2094. .el-input {
  2095. width: 100% !important;
  2096. max-width: 300px;
  2097. }
  2098. }
  2099. .bookmark-pagination {
  2100. width: 100%;
  2101. justify-content: center;
  2102. flex-direction: column;
  2103. gap: 8px;
  2104. .el-pagination {
  2105. justify-content: center;
  2106. }
  2107. }
  2108. }
  2109. }
  2110. // 无限旋转动画
  2111. @keyframes spin {
  2112. to {
  2113. transform: rotate(360deg);
  2114. }
  2115. }
  2116. // 基础旋转类
  2117. .spin {
  2118. display: inline-block;
  2119. animation: spin 1s linear infinite;
  2120. transform-origin: center center;
  2121. // 变体:慢速
  2122. &-slow {
  2123. animation-duration: 3s;
  2124. }
  2125. // 变体:快速
  2126. &-fast {
  2127. animation-duration: 0.6s;
  2128. }
  2129. // 变体:反向
  2130. &-rev {
  2131. animation-direction: reverse;
  2132. }
  2133. // 变体:悬停暂停
  2134. &-pause:hover {
  2135. animation-play-state: paused;
  2136. }
  2137. }
  2138. </style>