| | |
| | | :with-header="true" |
| | | class="ai-chat-drawer" |
| | | :modal="false" |
| | | :show-close="true" |
| | | modal-class="ai-chat-overlay" |
| | | :show-close="false" |
| | | :append-to-body="false" |
| | | @close="handleClose" |
| | | > |
| | |
| | | <el-tooltip content="开启新会话" placement="bottom"> |
| | | <el-button link @click="newChat"> |
| | | <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> |
| | |
| | | } |
| | | |
| | | // 解析历史消息中的 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) |
| | |
| | | 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) { |
| | |
| | | currentMsg.content = fullText |
| | | |
| | | // 解析 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) |
| | |
| | | currentAbortController.value = new AbortController() |
| | | |
| | | // 用户消息 |
| | | if (messages.value.length > 0) { |
| | | messages.value.push({ |
| | | isUser: true, |
| | | content: message, |
| | | htmlContent: convertTextToHtml(message), |
| | | isTyping: false |
| | | }) |
| | | } |
| | | |
| | | // 机器人占位 |
| | | const botMsgIndex = messages.value.length |
| | |
| | | |
| | | currentMsg.content = fullText |
| | | |
| | | // 解析 JSON 数据(针对嵌入式 JSON) |
| | | const jsonRegex = /\{"success":\s*true,[\s\S]*\}/ |
| | | const jsonMatch = fullText.match(jsonRegex) |
| | | // 尝试提取并解析 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 |
| | | |
| | | if (jsonMatch) { |
| | | // 从后往前找最后一个 '}' |
| | | const lastBraceIdx = text.lastIndexOf('}') |
| | | if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null |
| | | |
| | | const potentialJson = text.substring(startIdx, lastBraceIdx + 1) |
| | | try { |
| | | const parsedData = JSON.parse(jsonMatch[0]) |
| | | if (parsedData.success) { |
| | | 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 |
| | |
| | | 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) {} |
| | | |
| | | } |
| | | |
| | | updateOutputState(fullText, botMsgIndex) |
| | |
| | | currentAbortController.value = null |
| | | |
| | | // 最终解析 |
| | | const fullText = currentMsg.content |
| | | const jsonRegex = /\{"success":\s*true,[\s\S]*\}/ |
| | | const jsonMatch = fullText.match(jsonRegex) |
| | | if (jsonMatch) { |
| | | 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 { |
| | | const parsedData = JSON.parse(jsonMatch[0]) |
| | | if (parsedData.success) { |
| | | currentMsg.type = parsedData.type || '' |
| | | if (currentMsg.type === 'todo_list' && parsedData.data) { |
| | | currentMsg.tableData = parsedData.data |
| | | return JSON.parse(potentialJson) |
| | | } catch (err) { |
| | | return null |
| | | } |
| | | 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) |
| | | } |
| | | } catch (err) {} |
| | | } |
| | | |
| | | // 兜底渲染 |
| | | if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) { |
| | | 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') { |
| | |
| | | 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) + '... (正在生成数据图表)' |
| | | } |
| | | } |
| | | // 尝试提取 JSON 部分 |
| | | const extracted = extractEmbeddedSuccessJson(output) |
| | | const startIdx = extracted ? extracted.startIdx : output.indexOf('{"success"') |
| | | |
| | | 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) |
| | | } |
| | | |
| | | 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>') |
| | | |
| | | // 如果还在代码块中且未结束,显示提示文字 |
| | | 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 || '正在分析数据并生成图表...' |
| | | 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 = '正在为您展示分析结果...' |
| | | } |
| | | } |
| | | } 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) |
| | | // 成功解析,移除 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 = '正在为您展示分析结果...' |
| | | } |
| | | } |
| | | } catch (e) { |
| | | // 解析失败,说明 JSON 还在传输中或格式不正确 |
| | | display = output.substring(0, startIdx).trim() || '正在分析数据并生成图表...' |
| | | } |
| | | } else { |
| | | // 找到了开始但还没找到结束 |
| | | display = output.substring(0, startIdx).trim() || '正在分析数据并生成图表...' |
| | | } |
| | | } |
| | | |
| | | let html = convertTextToHtml(display) |
| | | |
| | | // 还原代码块 |
| | | 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 html || '...' |
| | | } |
| | | |
| | | const renderCharts = (msgIndex, chartOptions) => { |
| | |
| | | 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) |
| | | } |
| | | } |
| | |
| | | }) |
| | | } |
| | | |
| | | // 格式化 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; |
| | | |
| | | :deep(.el-drawer__container) { |
| | | pointer-events: none; |
| | | <style lang="scss"> |
| | | .ai-chat-overlay { |
| | | pointer-events: none !important; |
| | | background: transparent !important; |
| | | } |
| | | |
| | | .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; |
| | |
| | | .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); |
| | | } |
| | | } |
| | | |
| | |
| | | :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; |
| | | } |
| | | } |
| | | |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | |
| | | 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 { |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | |
| | | .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; |
| | | } |
| | | } |
| | | } |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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; |
| | | gap: 14px; |
| | | margin-bottom: 12px; |
| | | align-items: center; |
| | | |
| | | .file-upload-trigger { |
| | |
| | | } |
| | | |
| | | .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> |