From d967265949039cdd1804c78242bc7e2570ad67eb Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 28 四月 2026 16:59:54 +0800
Subject: [PATCH] refactor(AIChatSidebar): 重构AI聊天侧边栏组件
---
src/components/AIChatSidebar/index.vue | 1472 +++++++++++++++++++++++++++++++++++++++++++---------------
1 files changed, 1,092 insertions(+), 380 deletions(-)
diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
index d14978c..1f1a580 100644
--- a/src/components/AIChatSidebar/index.vue
+++ b/src/components/AIChatSidebar/index.vue
@@ -11,15 +11,16 @@
<!-- 渚ц竟鏍忓璇濇 -->
<el-drawer
- v-model="visible"
- :size="drawerSize"
- direction="rtl"
- :with-header="true"
- class="ai-chat-drawer"
- :modal="false"
- :show-close="true"
- :append-to-body="false"
- @close="handleClose"
+ v-model="visible"
+ :size="drawerSize"
+ direction="rtl"
+ :with-header="true"
+ class="ai-chat-drawer"
+ :modal="false"
+ modal-class="ai-chat-overlay"
+ :show-close="false"
+ :append-to-body="false"
+ @close="handleClose"
>
<template #header>
<div class="drawer-header">
@@ -38,10 +39,16 @@
<el-icon :size="18"><Plus /></el-icon>
</el-button>
</el-tooltip>
+ <div class="action-divider"></div>
+ <el-tooltip content="鍏抽棴" placement="bottom">
+ <el-button link class="close-btn" @click="visible = false">
+ <el-icon :size="18"><Close /></el-icon>
+ </el-button>
+ </el-tooltip>
</div>
</div>
</template>
-
+
<div class="chat-container">
<!-- 鍘嗗彶浼氳瘽鍒楄〃 -->
<div v-if="showHistory" class="history-panel">
@@ -56,21 +63,21 @@
</div>
</template>
<div class="session-list">
- <div
- v-for="session in sessions"
- :key="session.memoryId"
- :class="['session-item', { active: uuid === session.memoryId }]"
- @click="selectSession(session)"
+ <div
+ v-for="session in sessions"
+ :key="session.memoryId"
+ :class="['session-item', { active: uuid === session.memoryId }]"
+ @click="selectSession(session)"
>
<el-icon><ChatDotSquare /></el-icon>
<span class="session-name" :title="session.lastMessage || '鏂颁細璇�'">
{{ session.lastMessage || '鏂颁細璇�' }}
</span>
- <el-button
- link
- type="danger"
- class="delete-btn"
- @click.stop="handleDeleteSession(session.memoryId)"
+ <el-button
+ link
+ type="danger"
+ class="delete-btn"
+ @click.stop="handleDeleteSession(session.memoryId)"
>
<el-icon><Delete /></el-icon>
</el-button>
@@ -82,98 +89,98 @@
<div v-else class="chat-main">
<div class="message-list" ref="messageListRef">
- <div
- v-for="(message, index) in messages"
- :key="index"
- :class="['message-item', message.isUser ? 'user-message' : 'bot-message']"
- >
- <div class="avatar">
- <el-icon v-if="message.isUser"><User /></el-icon>
- <el-icon v-else><Cpu /></el-icon>
- </div>
- <div class="message-content">
- <!-- 鏂囨湰鍐呭 -->
- <div class="text-box" v-html="message.htmlContent"></div>
-
- <!-- 鍥捐〃鍐呭 -->
- <div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
- <div
- v-for="(option, key) in message.chartOptions"
- :key="key"
- class="chart-item"
- :id="`ai-chart-${index}-${key}`"
- ></div>
+ <div
+ v-for="(message, index) in messages"
+ :key="index"
+ :class="['message-item', message.isUser ? 'user-message' : 'bot-message']"
+ >
+ <div class="avatar">
+ <el-icon v-if="message.isUser"><User /></el-icon>
+ <el-icon v-else><Cpu /></el-icon>
</div>
+ <div class="message-content">
+ <!-- 鏂囨湰鍐呭 -->
+ <div class="text-box" v-html="message.htmlContent"></div>
- <!-- 琛ㄦ牸鍐呭 -->
- <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
- <el-table :data="message.tableData.items" border stripe size="small" style="width: 100%">
- <el-table-column
- v-for="col in message.tableData.columns"
- :key="col"
- :prop="col"
- :label="columnLabelMap[col] || col"
- min-width="100"
- show-overflow-tooltip
- />
- </el-table>
- </div>
+ <!-- 鍥捐〃鍐呭 -->
+ <div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
+ <div
+ v-for="(option, key) in message.chartOptions"
+ :key="key"
+ class="chart-item"
+ :id="`ai-chart-${index}-${key}`"
+ ></div>
+ </div>
- <!-- 鎵撳瓧涓姩鐢� -->
- <div v-if="message.isTyping" class="typing-indicator">
- <span class="dot"></span>
- <span class="dot"></span>
- <span class="dot"></span>
+ <!-- 琛ㄦ牸鍐呭 -->
+ <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
+ <el-table :data="message.tableData.items" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in message.tableData.columns"
+ :key="col"
+ :prop="col"
+ :label="columnLabelMap[col] || col"
+ min-width="100"
+ show-overflow-tooltip
+ />
+ </el-table>
+ </div>
+
+ <!-- 鎵撳瓧涓姩鐢� -->
+ <div v-if="message.isTyping" class="typing-indicator">
+ <span class="dot"></span>
+ <span class="dot"></span>
+ <span class="dot"></span>
+ </div>
</div>
</div>
</div>
- </div>
- <div class="input-area">
- <div class="input-actions">
- <el-button link type="primary" size="small" @click="newChat">
- <el-icon><Plus /></el-icon>鏂颁細璇�
- </el-button>
- <el-button v-if="isSending" link type="danger" size="small" @click="stopGeneration">
- <el-icon><VideoPause /></el-icon>鍋滄鐢熸垚
- </el-button>
- <el-upload
- class="file-upload-trigger"
- action="#"
- :auto-upload="false"
- :show-file-list="false"
- :on-change="handleFileChange"
- :disabled="isSending"
- >
- <el-button link type="primary" size="small" :disabled="isSending">
- <el-icon><Upload /></el-icon>鍒嗘瀽鏂囦欢
+ <div class="input-area">
+ <div class="input-actions">
+ <el-button link type="primary" size="small" @click="newChat">
+ <el-icon><Plus /></el-icon>鏂颁細璇�
</el-button>
- </el-upload>
- </div>
- <div class="input-box">
- <div v-if="selectedFile" class="selected-file-tag">
- <el-icon><Document /></el-icon>
- <span class="file-name">{{ selectedFile.name }}</span>
- <el-icon class="remove-file" @click="removeSelectedFile"><Close /></el-icon>
+ <el-button v-if="isSending" link type="danger" size="small" @click="stopGeneration">
+ <el-icon><VideoPause /></el-icon>鍋滄鐢熸垚
+ </el-button>
+ <el-upload
+ class="file-upload-trigger"
+ action="#"
+ :auto-upload="false"
+ :show-file-list="false"
+ :on-change="handleFileChange"
+ :disabled="isSending"
+ >
+ <el-button link type="primary" size="small" :disabled="isSending">
+ <el-icon><Upload /></el-icon>鍒嗘瀽鏂囦欢
+ </el-button>
+ </el-upload>
</div>
- <el-input
- v-model="inputMessage"
- type="textarea"
- :rows="selectedFile ? 2 : 3"
- placeholder="璇疯緭鍏ユ偍鐨勯棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)"
- resize="none"
- @keydown.enter.exact.prevent="sendMessage"
- />
- <el-button
- type="primary"
- class="send-btn"
- :disabled="isSending || (!inputMessage.trim() && !selectedFile)"
- @click="sendMessage"
- >
- 鍙戦��
- </el-button>
+ <div class="input-box">
+ <div v-if="selectedFile" class="selected-file-tag">
+ <el-icon><Document /></el-icon>
+ <span class="file-name">{{ selectedFile.name }}</span>
+ <el-icon class="remove-file" @click="removeSelectedFile"><Close /></el-icon>
+ </div>
+ <el-input
+ v-model="inputMessage"
+ type="textarea"
+ :rows="selectedFile ? 2 : 3"
+ placeholder="璇疯緭鍏ユ偍鐨勯棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)"
+ resize="none"
+ @keydown.enter.exact.prevent="sendMessage"
+ />
+ <el-button
+ type="primary"
+ class="send-btn"
+ :disabled="isSending || (!inputMessage.trim() && !selectedFile)"
+ @click="sendMessage"
+ >
+ 鍙戦��
+ </el-button>
+ </div>
</div>
- </div>
</div>
</div>
</el-drawer>
@@ -235,7 +242,7 @@
showHistory.value = false
uuid.value = session.memoryId
localStorage.setItem('ai_chat_uuid', uuid.value)
-
+
// 鍔犺浇浼氳瘽娑堟伅
try {
const res = await request.get(`/xiaozhi/history/messages/${uuid.value}`)
@@ -243,12 +250,12 @@
disposeCharts()
messages.value = []
const historyMsgs = res.data || []
-
+
// 閲嶆柊鏋勯�犳秷鎭垪琛ㄥ苟瑙f瀽
historyMsgs.forEach((msg, idx) => {
const isUser = msg.role === 'user'
const botMsgIndex = messages.value.length
-
+
const messageObj = {
isUser,
content: msg.content,
@@ -259,9 +266,9 @@
type: '',
tableData: null
}
-
+
messages.value.push(messageObj)
-
+
if (!isUser) {
outputState.value[botMsgIndex] = {
isPaused: false,
@@ -270,27 +277,13 @@
blockEndPos: -1,
hasRenderedChart: false
}
-
+
// 瑙f瀽鍘嗗彶娑堟伅涓殑 JSON
- const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
- const jsonMatch = msg.content.match(jsonRegex)
- if (jsonMatch) {
- try {
- const parsedData = JSON.parse(jsonMatch[0])
- if (parsedData.success) {
- messageObj.type = parsedData.type || ''
- if (messageObj.type === 'todo_list' && parsedData.data) {
- messageObj.tableData = parsedData.data
- }
- if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
- messageObj.chartOptions = parsedData.charts
- messageObj.chartRenderReady = true
- renderCharts(botMsgIndex, messageObj.chartOptions)
- }
- }
- } catch (err) {}
+ const extracted = extractEmbeddedSuccessJson(msg.content)
+ if (extracted) {
+ applyStructuredMessageData(messageObj, extracted.data, botMsgIndex)
}
-
+
updateOutputState(msg.content, botMsgIndex)
messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex)
} else {
@@ -386,6 +379,86 @@
resizeHandlers.value = []
}
+const extractEmbeddedSuccessJson = (text) => {
+ if (!text || typeof text !== 'string') return null
+
+ const startIdx = text.indexOf('{"success"')
+ if (startIdx === -1) return null
+
+ for (let i = startIdx; i < text.length; i++) {
+ if (text[i] !== '{') continue
+
+ let depth = 0
+ let inString = false
+ let escaped = false
+
+ for (let j = i; j < text.length; j++) {
+ const char = text[j]
+
+ if (inString) {
+ if (escaped) {
+ escaped = false
+ } else if (char === '\\') {
+ escaped = true
+ } else if (char === '"') {
+ inString = false
+ }
+ continue
+ }
+
+ if (char === '"') {
+ inString = true
+ continue
+ }
+
+ if (char === '{') {
+ depth++
+ } else if (char === '}') {
+ depth--
+ if (depth === 0) {
+ const candidate = text.slice(i, j + 1)
+ try {
+ const parsed = JSON.parse(candidate)
+ if (parsed?.success === true) {
+ return {
+ data: parsed,
+ startIdx: i,
+ endIdx: j + 1
+ }
+ }
+ } catch (err) {
+ continue
+ }
+ }
+ }
+ }
+ }
+
+ return null
+}
+
+const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
+ if (!messageObj || !parsedData?.success) return
+
+ messageObj.type = parsedData.type || ''
+
+ if (messageObj.type === 'todo_list' && parsedData.data) {
+ messageObj.tableData = parsedData.data
+ }
+
+ if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
+ messageObj.chartOptions = parsedData.charts
+ messageObj.chartRenderReady = true
+
+ if (shouldRenderCharts) {
+ renderCharts(msgIndex, messageObj.chartOptions)
+ if (outputState.value[msgIndex]) {
+ outputState.value[msgIndex].hasRenderedChart = true
+ }
+ }
+ }
+}
+
const scrollToBottom = () => {
nextTick(() => {
if (messageListRef.value) {
@@ -416,7 +489,7 @@
if (isSending.value) return
isSending.value = true
currentAbortController.value = new AbortController()
-
+
const userMsg = message ? `${message}\n[涓婁紶鏂囦欢鍒嗘瀽] ${file.name}` : `[涓婁紶鏂囦欢鍒嗘瀽] ${file.name}`
messages.value.push({
isUser: true,
@@ -462,34 +535,16 @@
onDownloadProgress: (e) => {
const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
if (!fullText) return
-
+
const currentMsg = messages.value[botMsgIndex]
if (!currentMsg) return
-
+
currentMsg.content = fullText
// 瑙f瀽 JSON 鏁版嵁锛堥拡瀵瑰祵鍏ュ紡 JSON锛�
- const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
- const jsonMatch = fullText.match(jsonRegex)
-
- if (jsonMatch) {
- try {
- const parsedData = JSON.parse(jsonMatch[0])
- if (parsedData.success) {
- currentMsg.type = parsedData.type || ''
- if (currentMsg.type === 'todo_list' && parsedData.data) {
- currentMsg.tableData = parsedData.data
- }
- if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
- currentMsg.chartOptions = parsedData.charts
- currentMsg.chartRenderReady = true
- if (!outputState.value[botMsgIndex].hasRenderedChart) {
- renderCharts(botMsgIndex, currentMsg.chartOptions)
- outputState.value[botMsgIndex].hasRenderedChart = true
- }
- }
- }
- } catch (err) {}
+ const extracted = extractEmbeddedSuccessJson(fullText)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
}
updateOutputState(fullText, botMsgIndex)
@@ -501,7 +556,7 @@
currentMsg.isTyping = false
isSending.value = false
currentAbortController.value = null
-
+
// 鏈�缁堣В鏋愮‘淇濆浘琛ㄦ覆鏌�
if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
renderCharts(botMsgIndex, currentMsg.chartOptions)
@@ -542,7 +597,7 @@
currentAbortController.value.abort()
currentAbortController.value = null
isSending.value = false
-
+
// 灏嗘渶鍚庝竴鏉℃秷鎭爣璁颁负闈炴墦瀛楃姸鎬�
const lastMsg = messages.value[messages.value.length - 1]
if (lastMsg && !lastMsg.isUser) {
@@ -556,14 +611,12 @@
currentAbortController.value = new AbortController()
// 鐢ㄦ埛娑堟伅
- if (messages.value.length > 0) {
- messages.value.push({
- isUser: true,
- content: message,
- htmlContent: convertTextToHtml(message),
- isTyping: false
- })
- }
+ messages.value.push({
+ isUser: true,
+ content: message,
+ htmlContent: convertTextToHtml(message),
+ isTyping: false
+ })
// 鏈哄櫒浜哄崰浣�
const botMsgIndex = messages.value.length
@@ -578,7 +631,7 @@
tableData: null
}
messages.value.push(botMsg)
-
+
outputState.value[botMsgIndex] = {
isPaused: false,
jsonBlockStartPos: -1,
@@ -586,31 +639,46 @@
blockEndPos: -1,
hasRenderedChart: false
}
-
+
scrollToBottom()
- request.post('/xiaozhi/chat',
- { memoryId: uuid.value, message },
- {
- signal: currentAbortController.value.signal,
- onDownloadProgress: (e) => {
- // 鍏煎涓嶅悓鐗堟湰鐨� axios 鑾峰彇鍝嶅簲鏂囨湰鐨勬柟寮�
- const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
- if (!fullText) return
-
- const currentMsg = messages.value[botMsgIndex]
- if (!currentMsg) return
-
- currentMsg.content = fullText
+ request.post('/xiaozhi/chat',
+ { memoryId: uuid.value, message },
+ {
+ signal: currentAbortController.value.signal,
+ onDownloadProgress: (e) => {
+ // 鍏煎涓嶅悓鐗堟湰鐨� axios 鑾峰彇鍝嶅簲鏂囨湰鐨勬柟寮�
+ const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
+ if (!fullText) return
- // 瑙f瀽 JSON 鏁版嵁锛堥拡瀵瑰祵鍏ュ紡 JSON锛�
- const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
- const jsonMatch = fullText.match(jsonRegex)
+ const currentMsg = messages.value[botMsgIndex]
+ if (!currentMsg) return
- if (jsonMatch) {
- try {
- const parsedData = JSON.parse(jsonMatch[0])
- if (parsedData.success) {
+ currentMsg.content = fullText
+
+ // 灏濊瘯鎻愬彇骞惰В鏋� JSON
+ const extracted = extractEmbeddedSuccessJson(fullText)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
+ } else {
+ const extractJson = (text) => {
+ const startIdx = text.indexOf('{"success": true')
+ if (startIdx === -1) return null
+
+ // 浠庡悗寰�鍓嶆壘鏈�鍚庝竴涓� '}'
+ const lastBraceIdx = text.lastIndexOf('}')
+ if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
+
+ const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
+ try {
+ return JSON.parse(potentialJson)
+ } catch (err) {
+ return null
+ }
+ }
+
+ const parsedData = extractJson(fullText)
+ if (parsedData) {
currentMsg.type = parsedData.type || ''
if (currentMsg.type === 'todo_list' && parsedData.data) {
currentMsg.tableData = parsedData.data
@@ -618,54 +686,54 @@
if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
currentMsg.chartOptions = parsedData.charts
currentMsg.chartRenderReady = true
- if (!outputState.value[botMsgIndex].hasRenderedChart) {
- renderCharts(botMsgIndex, currentMsg.chartOptions)
- outputState.value[botMsgIndex].hasRenderedChart = true
- }
+ // 姣忔瑙f瀽鎴愬姛閮藉皾璇曟覆鏌�/鏇存柊鍥捐〃锛屼互鏀寔娴佸紡鏇存柊
+ renderCharts(botMsgIndex, currentMsg.chartOptions)
}
}
- } catch (err) {}
- }
- updateOutputState(fullText, botMsgIndex)
- currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
- scrollToBottom()
+ }
+
+ updateOutputState(fullText, botMsgIndex)
+ currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
+ scrollToBottom()
+ }
}
- }
).then(() => {
const currentMsg = messages.value[botMsgIndex]
currentMsg.isTyping = false
isSending.value = false
currentAbortController.value = null
-
+
// 鏈�缁堣В鏋�
- const fullText = currentMsg.content
- const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
- const jsonMatch = fullText.match(jsonRegex)
- if (jsonMatch) {
- try {
- const parsedData = JSON.parse(jsonMatch[0])
- if (parsedData.success) {
- currentMsg.type = parsedData.type || ''
- if (currentMsg.type === 'todo_list' && parsedData.data) {
- currentMsg.tableData = parsedData.data
- }
- if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
- currentMsg.chartOptions = parsedData.charts
- currentMsg.chartRenderReady = true
- if (!outputState.value[botMsgIndex].hasRenderedChart) {
- renderCharts(botMsgIndex, currentMsg.chartOptions)
- outputState.value[botMsgIndex].hasRenderedChart = true
- }
- }
- currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
+ const extracted = extractEmbeddedSuccessJson(currentMsg.content)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
+ } else {
+ const extractJson = (text) => {
+ const startIdx = text.indexOf('{"success": true')
+ if (startIdx === -1) return null
+ const lastBraceIdx = text.lastIndexOf('}')
+ if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
+ const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
+ try {
+ return JSON.parse(potentialJson)
+ } catch (err) {
+ return null
}
- } catch (err) {}
- }
-
- // 鍏滃簳娓叉煋
- if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
- renderCharts(botMsgIndex, currentMsg.chartOptions)
+ }
+
+ const finalParsed = extractJson(currentMsg.content)
+ if (finalParsed) {
+ currentMsg.type = finalParsed.type || ''
+ if (currentMsg.type === 'todo_list' && finalParsed.data) {
+ currentMsg.tableData = finalParsed.data
+ }
+ if (finalParsed.charts && Object.keys(finalParsed.charts).length > 0) {
+ currentMsg.chartOptions = finalParsed.charts
+ currentMsg.chartRenderReady = true
+ renderCharts(botMsgIndex, currentMsg.chartOptions)
+ }
+ }
}
}).catch(err => {
if (err.name === 'CanceledError' || err.name === 'AbortError') {
@@ -704,10 +772,10 @@
const convertTextToHtml = (text) => {
if (!text) return ''
return text
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/\n/g, '<br>')
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/\n/g, '<br>')
}
const convertStreamOutput = (output, msgIndex) => {
@@ -715,29 +783,82 @@
const state = outputState.value[msgIndex]
let display = output
- const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
- const jsonMatch = output.match(jsonRegex)
- if (jsonMatch) {
- try {
- const parsed = JSON.parse(jsonMatch[0])
- display = output.replace(jsonMatch[0], '').trim()
- if (!display && parsed.description) display = parsed.description
- } catch (e) {
- const start = output.search(/\{"success":\s*true/)
- display = output.substring(0, start) + '... (姝e湪鐢熸垚鏁版嵁鍥捐〃)'
+ // 灏濊瘯鎻愬彇 JSON 閮ㄥ垎
+ const extracted = extractEmbeddedSuccessJson(output)
+ const startIdx = extracted ? extracted.startIdx : output.indexOf('{"success"')
+
+ // 濡傛灉杩樺湪浠g爜鍧椾腑涓旀湭缁撴潫锛屾樉绀烘彁绀烘枃瀛�
+ if (state && ((state.jsonBlockStartPos !== -1) || (state.jsBlockStartPos !== -1)) && state.blockEndPos === -1) {
+ const startPos = state.jsonBlockStartPos !== -1 ? state.jsonBlockStartPos : state.jsBlockStartPos
+ const textBeforeBlock = display.substring(0, startPos).trim()
+ display = textBeforeBlock || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+ return convertTextToHtml(display)
+ }
+
+ if (extracted) {
+ const parsed = extracted.data
+ display = (output.substring(0, extracted.startIdx) + output.substring(extracted.endIdx)).trim()
+
+ if (/^[\s}\],锛屻��.:锛�;锛沒+$/.test(display)) {
+ display = ''
+ }
+
+ if (parsed.description) {
+ display = parsed.description
+ }
+
+ if (!display) {
+ if (parsed.type === 'todo_list') {
+ display = '宸蹭负鎮ㄦ暣鐞嗗ソ鐩稿叧鏁版嵁銆�'
+ } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
+ display = '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛ㄣ��'
+ } else {
+ display = '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+ }
+ }
+ } else if (startIdx !== -1) {
+ const lastBraceIdx = output.lastIndexOf('}')
+ if (lastBraceIdx !== -1 && lastBraceIdx > startIdx) {
+ const potentialJson = output.substring(startIdx, lastBraceIdx + 1)
+ try {
+ const parsed = JSON.parse(potentialJson)
+ // 鎴愬姛瑙f瀽锛岀Щ闄� JSON 閮ㄥ垎鏄剧ず鏂囧瓧
+ display = (output.substring(0, startIdx) + output.substring(lastBraceIdx + 1)).trim()
+
+ if (/^[\s}\],锛屻��.:锛�;锛沒+$/.test(display)) {
+ display = ''
+ }
+
+ if (parsed.description) {
+ display = parsed.description
+ }
+
+ if (!display) {
+ if (parsed.type === 'todo_list') {
+ display = '宸蹭负鎮ㄦ暣鐞嗗ソ鐩稿叧鏁版嵁锛�'
+ } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
+ display = '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛細'
+ } else {
+ display = '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+ }
+ }
+ } catch (e) {
+ // 瑙f瀽澶辫触锛岃鏄� JSON 杩樺湪浼犺緭涓垨鏍煎紡涓嶆纭�
+ display = output.substring(0, startIdx).trim() || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+ }
+ } else {
+ // 鎵惧埌浜嗗紑濮嬩絾杩樻病鎵惧埌缁撴潫
+ display = output.substring(0, startIdx).trim() || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
}
}
- if (state.jsonBlockStartPos !== -1 && state.blockEndPos === -1) {
- display = display.substring(0, state.jsonBlockStartPos)
- } else if (state.jsBlockStartPos !== -1 && state.blockEndPos === -1) {
- display = display.substring(0, state.jsBlockStartPos)
- }
+ let html = convertTextToHtml(display)
- display = display.replace(/```(javascript|js)([\s\S]*?)```/g, '<pre class="code-block js-code">$2</pre>')
- display = display.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>')
+ // 杩樺師浠g爜鍧�
+ html = html.replace(/```(javascript|js)([\s\S]*?)```/g, '<pre class="code-block js-code">$2</pre>')
+ html = html.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>')
- return convertTextToHtml(display)
+ return html || '...'
}
const renderCharts = (msgIndex, chartOptions) => {
@@ -747,14 +868,27 @@
const tryInit = (count = 0) => {
const dom = document.getElementById(id)
if (dom) {
- if (chartInstances.value[id]) chartInstances.value[id].dispose()
+ if (chartInstances.value[id]) {
+ // 濡傛灉宸茬粡鍒濆鍖栬繃锛岀洿鎺ユ洿鏂版暟鎹�
+ const chart = chartInstances.value[id]
+ const option = normalizeAiChartOption(chartOptions[key])
+ if (option) chart.setOption(option)
+ return
+ }
+
const chart = echarts.init(dom)
chartInstances.value[id] = chart
- chart.setOption(chartOptions[key])
+ const option = normalizeAiChartOption(chartOptions[key])
+ if (option) {
+ chart.setOption(option)
+ } else {
+ console.warn('Invalid chart option for:', id, chartOptions[key])
+ }
+
const handler = () => chart.resize()
resizeHandlers.value.push(handler)
window.addEventListener('resize', handler)
- } else if (count < 10) {
+ } else if (count < 15) { // 绋嶅井澧炲姞閲嶈瘯娆℃暟
setTimeout(() => tryInit(count + 1), 200)
}
}
@@ -763,19 +897,198 @@
})
}
+// 鏍煎紡鍖� AI 杩斿洖鐨勫浘琛ㄩ厤缃紝灏嗗叾杞崲涓烘爣鍑嗙殑 ECharts 閰嶇疆
+const formatChartOption = (rawOption) => {
+ if (!rawOption) return null
+
+ // 濡傛灉宸茬粡鏄爣鍑� ECharts 閰嶇疆锛堝寘鍚� series锛夛紝鍒欑洿鎺ヨ繑鍥�
+ const hasSeries = rawOption.series && Array.isArray(rawOption.series)
+
+ // 灏濊瘯杞崲绠�鏄撴牸寮�
+ try {
+ const isPie = rawOption.type === 'pie' || (rawOption.title && rawOption.title.includes('鍗犳瘮'))
+
+ const option = {
+ title: {
+ text: rawOption.title || '',
+ left: 'center',
+ textStyle: { fontSize: 14 }
+ },
+ tooltip: {
+ trigger: isPie ? 'item' : 'axis'
+ },
+ legend: {
+ bottom: '0'
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '15%',
+ containLabel: true
+ },
+ xAxis: isPie ? undefined : {
+ type: 'category',
+ data: rawOption.xAxisData || (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : []),
+ name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : ''
+ },
+ yAxis: isPie ? undefined : {
+ type: 'value',
+ name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
+ },
+ series: rawOption.series || [{
+ name: rawOption.title || '鏁板��',
+ type: rawOption.type || 'line',
+ data: rawOption.seriesData || (Array.isArray(rawOption.data) ? rawOption.data : []),
+ smooth: true,
+ radius: isPie ? '50%' : undefined
+ }]
+ }
+
+ // 閽堝楗煎浘鐨勭壒娈婂鐞�
+ if (isPie && !option.series[0].data && Array.isArray(rawOption.data)) {
+ option.series[0].data = rawOption.data
+ }
+
+ return option
+ } catch (err) {
+ console.error('Chart option conversion failed:', err)
+ return null
+ }
+}
+
+const normalizeAiChartOption = (rawOption) => {
+ if (!rawOption) return null
+
+ try {
+ const hasSeries = Array.isArray(rawOption.series) && rawOption.series.length > 0
+ const firstSeriesType = hasSeries ? rawOption.series[0]?.type : rawOption.type
+ const titleConfig = rawOption.title && typeof rawOption.title === 'object'
+ ? rawOption.title
+ : null
+ const tooltipConfig = rawOption.tooltip && typeof rawOption.tooltip === 'object'
+ ? rawOption.tooltip
+ : null
+ const legendConfig = rawOption.legend && typeof rawOption.legend === 'object'
+ ? rawOption.legend
+ : null
+ const rawXAxisConfig = rawOption.xAxis && typeof rawOption.xAxis === 'object' && !Array.isArray(rawOption.xAxis)
+ ? rawOption.xAxis
+ : null
+ const rawYAxisConfig = rawOption.yAxis && typeof rawOption.yAxis === 'object' && !Array.isArray(rawOption.yAxis)
+ ? rawOption.yAxis
+ : null
+ const titleText = typeof rawOption.title === 'string' ? rawOption.title : rawOption.title?.text || ''
+ const isPie = firstSeriesType === 'pie' || titleText.includes('鍗犳瘮')
+ const baseXAxisData = Array.isArray(rawOption.xAxisData)
+ ? rawOption.xAxisData
+ : (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : (Array.isArray(rawXAxisConfig?.data) ? rawXAxisConfig.data : []))
+ const fallbackSeries = [{
+ name: titleText || '鏁版嵁',
+ type: rawOption.type || 'line',
+ data: Array.isArray(rawOption.seriesData) ? rawOption.seriesData : (Array.isArray(rawOption.data) ? rawOption.data : [])
+ }]
+ const normalizedSeries = (hasSeries ? rawOption.series : fallbackSeries).map((seriesItem, index) => {
+ const seriesType = seriesItem?.type || rawOption.type || 'line'
+ const nextSeries = {
+ ...seriesItem,
+ name: seriesItem?.name || titleText || `绯诲垪${index + 1}`,
+ type: seriesType
+ }
+
+ if (isPie) {
+ nextSeries.radius = nextSeries.radius || '55%'
+ nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : (Array.isArray(rawOption.data) ? rawOption.data : [])
+ } else {
+ nextSeries.smooth = typeof nextSeries.smooth === 'boolean' ? nextSeries.smooth : seriesType === 'line'
+ nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : []
+ }
+
+ return nextSeries
+ })
+ const categorySource = !isPie
+ ? normalizedSeries.find(seriesItem => Array.isArray(seriesItem.data) && seriesItem.data.every(item => item && typeof item === 'object' && 'name' in item && 'value' in item))
+ : null
+ const xAxisData = categorySource
+ ? categorySource.data.map(item => item.name)
+ : baseXAxisData
+ const finalSeries = !isPie && categorySource
+ ? normalizedSeries.map(seriesItem => ({
+ ...seriesItem,
+ data: Array.isArray(seriesItem.data)
+ ? seriesItem.data.map(item => (item && typeof item === 'object' && 'value' in item ? item.value : item))
+ : []
+ }))
+ : normalizedSeries
+
+ return {
+ title: titleConfig || {
+ text: titleText,
+ left: 'center',
+ textStyle: { fontSize: 14 }
+ },
+ tooltip: tooltipConfig || {
+ trigger: isPie ? 'item' : 'axis'
+ },
+ legend: legendConfig || {
+ bottom: '0'
+ },
+ grid: isPie ? undefined : {
+ left: '3%',
+ right: '4%',
+ bottom: '15%',
+ containLabel: true
+ },
+ xAxis: isPie ? undefined : {
+ ...(rawXAxisConfig || {}),
+ type: 'category',
+ data: xAxisData,
+ name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : (rawXAxisConfig?.name || '')
+ },
+ yAxis: isPie ? undefined : (rawYAxisConfig || {
+ type: 'value',
+ name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
+ }),
+ series: finalSeries
+ }
+ } catch (err) {
+ console.error('AI chart normalization failed:', err, rawOption)
+ return formatChartOption(rawOption)
+ }
+}
+
watch(messages, () => scrollToBottom(), { deep: true })
</script>
-<style scoped lang="scss">
-.ai-chat-sidebar-wrapper {
- position: fixed;
- inset: 0;
- z-index: 2000;
- pointer-events: none;
+<style lang="scss">
+.ai-chat-overlay {
+ pointer-events: none !important;
+ background: transparent !important;
+}
- :deep(.el-drawer__container) {
- pointer-events: none;
- }
+.ai-chat-overlay .el-drawer {
+ pointer-events: auto;
+}
+</style>
+
+<style scoped lang="scss">
+$primary-blue: #0055d4;
+$secondary-blue: #2e8ce0;
+$light-blue: #7ab8ff;
+$pale-blue: #c5dcff;
+$ice-white: #e8f2ff;
+$deep-blue: #003b8e;
+$deepest-blue: #002b66;
+$gradient-blue: linear-gradient(145deg, #004fc7 0%, #0066e0 40%, #2580e8 70%, #5a9fe0 100%);
+$gradient-dark: linear-gradient(145deg, #003b8e 0%, #0055d4 50%, #0077e8 100%);
+$gradient-ice: linear-gradient(180deg, #e0ecff 0%, #d4e5ff 50%, #e8f0ff 100%);
+$shadow-blue: 0 8px 40px rgba(0, 85, 212, 0.35);
+$shadow-deep: 0 12px 48px rgba(0, 40, 120, 0.4);
+$shadow-card: 0 6px 24px rgba(0, 51, 136, 0.12);
+
+.ai-chat-sidebar-wrapper {
+ position: static;
+ z-index: 2000;
+ pointer-events: auto;
:deep(.el-drawer) {
pointer-events: auto;
@@ -785,29 +1098,82 @@
.ai-chat-trigger {
pointer-events: auto;
position: fixed;
- right: 20px;
+ right: 24px;
bottom: 100px;
- width: 60px;
- height: 60px;
- background: linear-gradient(135deg, #409eff 0%, #007aff 100%);
+ width: 56px;
+ height: 56px;
+ background: $gradient-dark;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
- box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);
- transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ box-shadow: $shadow-deep, 0 0 0 2px rgba(0, 85, 212, 0.3) inset, 0 0 30px rgba(0, 119, 232, 0.2);
+ transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 2001;
+ animation: triggerPulse 3s ease-in-out infinite;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: -6px;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.4), rgba(0, 136, 232, 0.3), rgba(90, 159, 224, 0.2));
+ border-radius: 50%;
+ z-index: -1;
+ filter: blur(16px);
+ animation: glowPulse 2s ease-in-out infinite alternate;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.3) 0%, transparent 50%);
+ pointer-events: none;
+ }
&:hover {
- transform: scale(1.1) translateY(-5px);
- box-shadow: 0 8px 20px rgba(0, 122, 255, 0.5);
+ transform: scale(1.12) translateY(-4px);
+ box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.4) inset, 0 0 50px rgba(0, 136, 232, 0.3);
+
+ &::before {
+ animation: glowPulse 1s ease-in-out infinite alternate;
+ }
+
+ .trigger-icon {
+ transform: rotate(-8deg) scale(1.05);
+ filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.5));
+ }
}
.trigger-icon {
display: flex;
align-items: center;
justify-content: center;
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ color: #fff;
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
+ }
+}
+
+@keyframes triggerPulse {
+ 0%, 100% {
+ box-shadow: $shadow-blue, 0 0 0 2px rgba(0, 85, 212, 0.25) inset, 0 0 20px rgba(0, 119, 232, 0.15);
+ }
+ 50% {
+ box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.35) inset, 0 0 40px rgba(0, 136, 232, 0.25);
+ }
+}
+
+@keyframes glowPulse {
+ 0% {
+ opacity: 0.6;
+ transform: scale(1);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1.1);
}
}
@@ -815,13 +1181,15 @@
:deep(.el-drawer__body) {
padding: 0;
overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
}
:deep(.el-drawer__header) {
margin-bottom: 0;
- padding: 12px 16px;
- background: #fff;
- border-bottom: 1px solid #ebeef5;
- color: #303133;
+ padding: 0;
+ background: $gradient-dark;
+ color: #fff;
}
}
@@ -830,27 +1198,124 @@
justify-content: space-between;
align-items: center;
width: 100%;
- padding-right: 32px;
+ padding: 18px 20px;
+ background: $gradient-dark;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: -60%;
+ right: -25%;
+ width: 250px;
+ height: 250px;
+ background: radial-gradient(circle, rgba(0, 136, 232, 0.4) 0%, transparent 70%);
+ pointer-events: none;
+ animation: headerGlow 4s ease-in-out infinite alternate;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: -40%;
+ left: -15%;
+ width: 200px;
+ height: 200px;
+ background: radial-gradient(circle, rgba(0, 85, 212, 0.3) 0%, transparent 70%);
+ pointer-events: none;
+ animation: headerGlow 5s ease-in-out infinite alternate-reverse;
+ }
.header-left {
display: flex;
align-items: center;
- gap: 8px;
-
+ gap: 12px;
+ position: relative;
+ z-index: 1;
+
.header-icon {
- color: #409eff;
+ color: rgba(255, 255, 255, 0.95);
+ filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.2));
+ animation: iconFloat 3s ease-in-out infinite;
}
-
+
.title {
- font-size: 16px;
+ font-size: 17px;
font-weight: 600;
- color: #303133;
+ color: #fff;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
+ letter-spacing: 0.5px;
}
}
.header-actions {
display: flex;
- gap: 8px;
+ align-items: center;
+ gap: 10px;
+ position: relative;
+ z-index: 1;
+
+ .action-divider {
+ width: 1px;
+ height: 16px;
+ background: rgba(255, 255, 255, 0.2);
+ margin: 0 2px;
+ }
+
+ :deep(.el-button) {
+ color: rgba(255, 255, 255, 0.85);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ background: rgba(255, 255, 255, 0.12);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ padding: 8px;
+ height: 32px;
+ width: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ color: #fff;
+ background: rgba(255, 255, 255, 0.25);
+ border-color: rgba(255, 255, 255, 0.3);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ &.close-btn {
+ background: rgba(255, 255, 255, 0.1);
+ &:hover {
+ background: rgba(245, 108, 108, 0.8);
+ border-color: rgba(245, 108, 108, 0.5);
+ }
+ }
+ }
+ }
+}
+
+@keyframes headerGlow {
+ 0% {
+ opacity: 0.6;
+ transform: scale(1);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1.15);
+ }
+}
+
+@keyframes iconFloat {
+ 0%, 100% {
+ transform: translateY(0) rotate(0);
+ }
+ 50% {
+ transform: translateY(-2px) rotate(3deg);
}
}
@@ -858,61 +1323,122 @@
display: flex;
flex-direction: column;
height: 100%;
- background-color: #f5f7fa;
+ width: 100%;
+ background: $ice-white;
position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 240px;
+ background: linear-gradient(180deg, rgba(0, 85, 212, 0.06) 0%, transparent 100%);
+ pointer-events: none;
+ }
}
.history-panel {
position: absolute;
inset: 0;
- background: #fff;
+ background: linear-gradient(180deg, #fff 0%, $ice-white 100%);
z-index: 10;
display: flex;
flex-direction: column;
-
+ box-shadow: -8px 0 32px rgba(0, 85, 212, 0.15);
+
.history-header {
- padding: 16px;
- border-bottom: 1px solid #ebeef5;
+ padding: 18px 20px;
+ border-bottom: 1px solid rgba(0, 85, 212, 0.12);
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
font-size: 14px;
+ color: $deep-blue;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.08) 0%, rgba(0, 136, 232, 0.05) 100%);
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.2), transparent);
+ }
}
.session-list {
flex: 1;
overflow-y: auto;
- padding: 8px;
+ padding: 12px 16px;
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: linear-gradient(180deg, $secondary-blue, $primary-blue);
+ border-radius: 4px;
+ box-shadow: 0 0 6px rgba(0, 85, 212, 0.25);
+ }
.session-item {
display: flex;
align-items: center;
- padding: 12px;
- margin-bottom: 4px;
- border-radius: 8px;
+ padding: 14px 16px;
+ margin-bottom: 6px;
+ border-radius: 12px;
cursor: pointer;
- transition: all 0.2s;
- gap: 10px;
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ gap: 12px;
position: relative;
border: 1px solid transparent;
+ background: #fff;
+ animation: sessionSlideIn 0.35s ease;
+
+ @keyframes sessionSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-15px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
&:hover {
- background-color: #f5f7fa;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.06) 0%, rgba(0, 136, 232, 0.08) 100%);
+ border-color: rgba(0, 85, 212, 0.12);
+ box-shadow: 0 4px 16px rgba(0, 85, 212, 0.1);
+ transform: translateX(4px);
+
.delete-btn {
opacity: 1;
+ transform: scale(1);
}
}
&.active {
- background-color: #ecf5ff;
- border-color: #d9ecff;
- color: #409eff;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.12) 0%, rgba(0, 136, 232, 0.15) 100%);
+ border-color: rgba(0, 85, 212, 0.25);
+ color: $primary-blue;
+ box-shadow: 0 4px 16px rgba(0, 85, 212, 0.15);
+
+ .el-icon {
+ color: $primary-blue;
+ }
}
.el-icon {
- font-size: 16px;
+ font-size: 18px;
flex-shrink: 0;
+ color: $secondary-blue;
+ transition: color 0.2s;
}
.session-name {
@@ -921,14 +1447,22 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ color: #1a1a2e;
+ font-weight: 500;
}
.delete-btn {
opacity: 0;
- transition: opacity 0.2s;
- padding: 4px;
+ transform: scale(0.8);
+ transition: all 0.25s ease;
+ padding: 6px;
+ border-radius: 6px;
+ color: #c0c4cc;
+
&:hover {
- color: #f56c6c;
+ color: #fff;
+ background: rgba(245, 108, 108, 0.85);
+ transform: scale(1.1) rotate(8deg);
}
}
}
@@ -946,58 +1480,98 @@
.message-list {
flex: 1;
overflow-y: auto;
- padding: 20px;
+ padding: 24px 20px;
display: flex;
flex-direction: column;
gap: 20px;
+ background: linear-gradient(180deg, transparent 0%, rgba(0, 85, 212, 0.02) 100%);
&::-webkit-scrollbar {
- width: 6px;
+ width: 8px;
}
&::-webkit-scrollbar-thumb {
- background: #dcdfe6;
- border-radius: 3px;
+ background: linear-gradient(180deg, $secondary-blue, $primary-blue);
+ border-radius: 4px;
+ box-shadow: 0 0 8px rgba(0, 85, 212, 0.3);
}
}
.message-item {
display: flex;
- gap: 12px;
+ gap: 14px;
width: 100%;
+ animation: messageSlideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+
+ @keyframes messageSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ }
.avatar {
- width: 36px;
- height: 36px;
- border-radius: 8px;
+ width: 42px;
+ height: 42px;
+ border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
- font-size: 20px;
+ font-size: 24px;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: inherit;
+ filter: blur(10px);
+ opacity: 0.5;
+ z-index: -1;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: linear-gradient(45deg, transparent 40%, rgba(255, 255, 255, 0.2) 50%, transparent 60%);
+ animation: shimmer 3s infinite;
+ }
}
.message-content {
flex: 1;
- overflow-x: hidden; // 淇敼涓� hidden锛屽唴閮ㄥ鍣ㄥ鐞嗘粴鍔�
+ overflow-x: hidden;
display: flex;
flex-direction: column;
- max-width: calc(100% - 48px); // 鍑忓幓澶村儚鍜岄棿璺�
-
+ max-width: calc(100% - 56px);
+
.text-box {
- padding: 12px 16px;
- border-radius: 12px;
+ padding: 14px 20px;
+ border-radius: 18px;
font-size: 14px;
- line-height: 1.6;
+ line-height: 1.7;
word-break: break-word;
max-width: 100%;
width: fit-content;
overflow-x: auto;
+ transition: all 0.3s ease;
+ position: relative;
+
&::-webkit-scrollbar {
- height: 6px;
+ height: 4px;
}
&::-webkit-scrollbar-thumb {
- background: #dcdfe6;
- border-radius: 3px;
+ background: rgba(0, 85, 212, 0.25);
+ border-radius: 2px;
}
}
}
@@ -1007,13 +1581,26 @@
align-items: flex-start;
}
.avatar {
- background-color: #409eff;
+ background: $gradient-dark;
color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.35);
}
.text-box {
- background-color: #fff;
- color: #303133;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ background: #fff;
+ color: #1a1a2e;
+ box-shadow: $shadow-card;
+ border: 1px solid rgba(0, 85, 212, 0.08);
+ border-top-left-radius: 6px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, rgba(0, 85, 212, 0.15), transparent);
+ }
}
}
@@ -1023,29 +1610,52 @@
align-items: flex-end;
}
.avatar {
- background-color: #95d475;
+ background: linear-gradient(145deg, #5a9fe0, #3d8bd4);
color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
}
.text-box {
- background-color: #409eff;
+ background: $gradient-dark;
color: #fff;
+ border-top-right-radius: 6px;
+ box-shadow: 0 6px 24px rgba(0, 85, 212, 0.3);
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3));
+ }
}
}
}
+@keyframes shimmer {
+ 0% {
+ transform: translateX(-100%) rotate(45deg);
+ }
+ 100% {
+ transform: translateX(100%) rotate(45deg);
+ }
+}
+
.charts-wrapper {
- margin-top: 10px;
+ margin-top: 12px;
display: flex;
flex-direction: column;
- gap: 10px;
+ gap: 12px;
overflow-x: auto;
width: 100%;
padding-bottom: 8px;
+
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-thumb {
- background: #dcdfe6;
+ background: linear-gradient(90deg, $light-blue, $secondary-blue);
border-radius: 3px;
}
}
@@ -1054,153 +1664,255 @@
width: 100%;
min-width: 300px;
height: 300px;
- background: #fff;
- border-radius: 8px;
- padding: 10px;
+ border-radius: 12px;
+ padding: 12px;
+ margin-bottom: 12px;
}
.table-wrapper {
- margin-top: 10px;
+ margin-top: 12px;
background: #fff;
- border-radius: 8px;
+ border-radius: 12px;
overflow: hidden;
overflow-x: auto;
width: 100%;
+ box-shadow: $shadow-card;
+ border: 1px solid rgba(0, 122, 255, 0.06);
+
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-thumb {
- background: #dcdfe6;
+ background: linear-gradient(90deg, $light-blue, $secondary-blue);
border-radius: 3px;
}
+
.el-table {
min-width: 300px;
+ --el-table-border-color: rgba(0, 122, 255, 0.08);
+ --el-table-header-bg-color: $ice-white;
}
}
.input-area {
- padding: 16px;
- background-color: #fff;
- border-top: 1px solid #dcdfe6;
+ padding: 18px 20px;
+ background: linear-gradient(180deg, rgba(232, 242, 255, 0.95) 0%, #fff 100%);
+ border-top: 1px solid rgba(0, 85, 212, 0.1);
+ position: relative;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 20px;
+ right: 20px;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.15), transparent);
+ }
.input-actions {
- display: flex;
- gap: 12px;
- margin-bottom: 8px;
- align-items: center;
-
- .file-upload-trigger {
- display: inline-flex;
+ display: flex;
+ gap: 14px;
+ margin-bottom: 12px;
align-items: center;
+
+ .file-upload-trigger {
+ display: inline-flex;
+ align-items: center;
+ }
}
-}
.input-box {
- padding: 12px;
+ padding: 16px;
position: relative;
background: #fff;
- border: 1px solid #dcdfe6;
- border-radius: 8px;
- margin: 0 16px 16px;
- transition: border-color 0.2s;
+ border: 2px solid rgba(0, 85, 212, 0.12);
+ border-radius: 16px;
+ margin: 0 4px;
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
&:focus-within {
- border-color: #409eff;
+ border-color: $primary-blue;
+ box-shadow: 0 0 0 4px rgba(0, 85, 212, 0.12), $shadow-deep;
+ transform: translateY(-2px);
+ background: #fff;
}
.selected-file-tag {
display: flex;
align-items: center;
- background: #f0f7ff;
- border: 1px solid #d9ecff;
- border-radius: 4px;
- padding: 4px 8px;
- margin-bottom: 8px;
- gap: 6px;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.1) 0%, rgba(0, 136, 232, 0.15) 100%);
+ border: 1px solid rgba(0, 85, 212, 0.2);
+ border-radius: 10px;
+ padding: 8px 12px;
+ margin-bottom: 12px;
+ gap: 10px;
width: fit-content;
max-width: 100%;
+ animation: tagSlideIn 0.3s ease;
+
+ @keyframes tagSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
.el-icon {
- color: #409eff;
- font-size: 14px;
+ color: $primary-blue;
+ font-size: 18px;
}
.file-name {
- font-size: 12px;
- color: #606266;
+ font-size: 13px;
+ color: $deep-blue;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ font-weight: 600;
}
.remove-file {
cursor: pointer;
- color: #909399;
- transition: color 0.2s;
+ color: $secondary-blue;
+ transition: all 0.2s;
+ padding: 4px;
+ border-radius: 50%;
+
&:hover {
- color: #f56c6c;
+ color: #fff;
+ background: rgba(245, 108, 108, 0.8);
+ transform: scale(1.1) rotate(90deg);
}
}
}
:deep(.el-textarea__inner) {
padding: 0;
+ padding-bottom: 35px;
border: none;
box-shadow: none;
background: transparent;
font-family: inherit;
font-size: 14px;
- line-height: 1.5;
- color: #303133;
+ line-height: 1.6;
+ color: #1a1a2e;
+
&::placeholder {
- color: #c0c4cc;
+ color: #7ab8ff;
+ }
+
+ &:focus {
+ box-shadow: none;
}
}
.send-btn {
position: absolute;
- right: 12px;
- bottom: 12px;
- padding: 8px 16px;
+ right: 16px;
+ bottom: 16px;
+ padding: 10px 22px;
+ background: $gradient-dark;
+ border: none;
+ border-radius: 10px;
+ font-weight: 600;
+ font-size: 14px;
+ color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ transition: left 0.5s;
+ }
+
+ &:hover:not(:disabled) {
+ transform: translateY(-3px) scale(1.02);
+ box-shadow: 0 10px 30px rgba(0, 85, 212, 0.5);
+
+ &::before {
+ left: 100%;
+ }
+ }
+
+ &:active:not(:disabled) {
+ transform: translateY(-1px) scale(0.98);
+ }
+
+ &:disabled {
+ background: linear-gradient(145deg, #b0b0b0, #c5c5c5);
+ box-shadow: none;
+ cursor: not-allowed;
+ }
}
}
}
.typing-indicator {
display: flex;
- gap: 4px;
- padding: 8px 12px;
+ gap: 5px;
+ padding: 10px 14px;
background: #fff;
- border-radius: 12px;
+ border-radius: 14px;
width: fit-content;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
- margin-top: 4px;
+ box-shadow: $shadow-card;
+ margin-top: 6px;
+ border: 1px solid rgba(0, 122, 255, 0.06);
+ border-top-left-radius: 4px;
+
.dot {
- width: 6px;
- height: 6px;
- background-color: #909399;
+ width: 7px;
+ height: 7px;
+ background: $secondary-blue;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
- &:nth-child(2) { animation-delay: 0.2s; }
- &:nth-child(3) { animation-delay: 0.4s; }
+
+ &:nth-child(2) {
+ animation-delay: 0.2s;
+ background: $primary-blue;
+ }
+ &:nth-child(3) {
+ animation-delay: 0.4s;
+ background: $deep-blue;
+ }
}
}
@keyframes typing {
- 0%, 80%, 100% { transform: scale(0); }
- 40% { transform: scale(1); }
+ 0%, 80%, 100% {
+ transform: scale(0.6);
+ opacity: 0.4;
+ }
+ 40% {
+ transform: scale(1);
+ opacity: 1;
+ }
}
.code-block {
- background: #2d2d2d;
- color: #ccc;
- padding: 12px;
- border-radius: 6px;
- font-family: monospace;
- margin: 8px 0;
+ background: linear-gradient(145deg, #1a1a2e, #16213e);
+ color: #a8d8ff;
+ padding: 14px;
+ border-radius: 10px;
+ font-family: 'Fira Code', 'Consolas', monospace;
+ margin: 10px 0;
overflow-x: auto;
+ border: 1px solid rgba(90, 200, 250, 0.15);
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2);
+
&.js-code {
- color: #f08d49;
+ color: #5ac8fa;
}
}
</style>
--
Gitblit v1.9.3