diff --git a/src/components/Dashboard.vue b/src/components/Dashboard.vue index 6c62e48..72cb33d 100644 --- a/src/components/Dashboard.vue +++ b/src/components/Dashboard.vue @@ -46,8 +46,8 @@ -
-

学术产出

+
+

学术产出

+
+ + + +
+
+
-
+
@@ -194,14 +202,15 @@ const redirectToResearchEvaluation = async () => { const result = await response.json(); if (result.code === 0) { // 跳转到目标网站 - await localStorage.setItem('access_token', result.data.access); - await localStorage.setItem('refresh_token', result.data.refresh); - await localStorage.setItem('user_info', JSON.stringify({ - user_id: result.data.user_id, - username: result.data.username, - permission: '2' - })); - window.location.href = 'http://82.156.236.221:10004/user/file' + // await localStorage.setItem('access_token', result.data.access); + // await localStorage.setItem('refresh_token', result.data.refresh); + // await localStorage.setItem('user_info', JSON.stringify({ + // user_id: result.data.user_id, + // username: result.data.username, + // permission: '2' + // })); + window.open(`http://82.156.236.221:10004/user/file?refresh_token=${result.data.refresh}`), '_blank'; + } else { console.error('代理登录失败:', result.error); } @@ -218,14 +227,14 @@ const redirectToResearchEvaluation2 = async () => { const result = await response.json(); if (result.code === 0) { // 跳转到目标网站 - await localStorage.setItem('access_token', result.data.access); - await localStorage.setItem('refresh_token', result.data.refresh); - await localStorage.setItem('user_info', JSON.stringify({ - user_id: result.data.user_id, - username: result.data.username, - permission: '1' - })) - window.location.href = 'http://82.156.236.221:10004/admin/manage' + // await localStorage.setItem('access_token', result.data.access); + // await localStorage.setItem('refresh_token', result.data.refresh); + // await localStorage.setItem('user_info', JSON.stringify({ + // user_id: result.data.user_id, + // username: result.data.username, + // permission: '1' + // })) + window.open(`http://82.156.236.221:10004/admin/manage?refresh_token=${result.data.refresh}`), '_blank'; } else { console.error('代理登录失败:', result.error); } @@ -366,6 +375,8 @@ const chatMessagesRef = ref(null) const userInput = ref('') const isLoading = ref(false) const chatMessages = ref([]) +let typingInterval = null +let thinkingInterval = null // DeepSeek API 调用函数 const sendMessage = async () => { @@ -373,35 +384,41 @@ const sendMessage = async () => { // 添加用户消息到对话 const userMessage = userInput.value.trim(); - chatMessages.value.push({ role: 'user', content: userMessage }); + chatMessages.value.push({ + role: 'user', + content: userMessage, + isTyping: false, + isThinking: false + }); userInput.value = ''; - // 立即滚动到底部(用户消息) - await nextTick(); - scrollToBottom(); + // 立即滚动到底部 + // await nextTick(); + // scrollToBottom(); // 设置加载状态并添加占位消息 isLoading.value = true; - chatMessages.value.push({ role: 'assistant', content: '思考中...' }); + chatMessages.value.push({ + role: 'assistant', + content: '', + isTyping: false, + isThinking: true, + isHidden: true // 添加一个标志表示消息暂时隐藏 + }); const aiMessageIndex = chatMessages.value.length - 1; try { - // 只保留最近5条消息以优化性能 - const apiMessages = chatMessages.value - .slice(-5) - .map(msg => ({ role: msg.role, content: msg.content })); - // 调用API(启用流式传输) - const response = await fetch('https://api.deepseek.com/chat/completions', { + const response = await fetch(`${getChatApiUrl()}/chat/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': 'Bearer sk-06801499dd52426fa7cf3b0670931e3a' }, body: JSON.stringify({ - model: 'deepseek-chat', - messages: apiMessages, - stream: true // 关键优化:启用流式响应 + model: "deepseek-r1:7b", + messages: [{ role: "user", content: userMessage }], + options: { temperature: 0.6 }, + keep_thinking: false }) }); @@ -411,37 +428,75 @@ const sendMessage = async () => { // 流式处理数据 const reader = response.body.getReader(); - let fullResponse = ''; const decoder = new TextDecoder(); - + let buffer = ""; + let fullResponse = ""; + + // 替换思考中消息为实际响应消息 + chatMessages.value[aiMessageIndex] = { + role: 'assistant', + content: '', + isTyping: false, + isThinking: false, + isHidden: true // 添加一个标志表示消息暂时隐藏 + }; + // 立即滚动到底部 + await nextTick(); + scrollToBottom(); + // 等待API响应完成 while (true) { const { done, value } = await reader.read(); if (done) break; - - // 解析流式数据(假设API返回ndjson格式) - const chunk = decoder.decode(value); - const lines = chunk.split('\n').filter(line => line.trim()); - + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + for (const line of lines) { + if (!line.trim()) continue; + try { - const data = JSON.parse(line.replace('data: ', '')); - if (data.choices?.[0]?.delta?.content) { - fullResponse += data.choices[0].delta.content; - // 实时更新UI(逐字显示) - chatMessages.value[aiMessageIndex].content = md.render(fullResponse); - scrollToBottom(); + const obj = JSON.parse(line); + if (obj.message?.content) { + fullResponse += obj.message.content; } } catch (e) { - console.warn('解析流数据失败:', e); + console.warn('解析JSON失败:', e); } } } + // API响应完成后,设置isLoading为false + setTimeout(() => { + isLoading.value = false; + // 移除隐藏标志,开始打字机效果 + chatMessages.value[aiMessageIndex].isHidden = false; + + // 然后开始打字机效果 + let typingIndex = 0; + const typingInterval = setInterval(() => { + if (typingIndex < fullResponse.length) { + chatMessages.value[aiMessageIndex].content = + fullResponse.substring(0, typingIndex + 1); + typingIndex++; + scrollToBottom(); + } else { + clearInterval(typingInterval); + } + }, 20); + }, 100) + + } catch (error) { console.error('请求失败:', error); - chatMessages.value[aiMessageIndex].content = '抱歉,回答时遇到问题,请重试。'; - } finally { isLoading.value = false; + chatMessages.value[aiMessageIndex] = { + role: 'assistant', + content: '抱歉,回答时遇到问题,请重试。', + isTyping: false, + isThinking: false, + isHidden: false // 确保错误消息显示 + }; scrollToBottom(); } }; @@ -450,7 +505,7 @@ const sendMessage = async () => { const scrollToBottom = () => { nextTick().then(() => { if (chatMessagesRef.value) { - chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight; + chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight; } }); }; @@ -503,7 +558,7 @@ const dashboardData2 = ref(null); const loading = ref(true); // 引入API配置 -import { getApiBaseUrl } from '../config'; +import { getApiBaseUrl, getChatApiUrl } from '../config'; // 从API获取仪表盘数据 const fetchDashboardData = async () => { @@ -1086,14 +1141,27 @@ const initCharts = () => { '博士': outputLegendStatus.value[2], '硕士': outputLegendStatus.value[1], '学士': outputLegendStatus.value[0], - } + }, + top: 10 // 调整图例位置 + }, + grid: { + left: '3%', + right: '12%', + bottom: '3%', + top: '20%', // 增加顶部间距给图例 + containLabel: true }, - grid: { left: '3%', right: '12%', bottom: '3%', containLabel: true }, yAxis: { type: 'category', data: outputData.value.months, axisLine: { lineStyle: { color: '#fff' } }, - axisLabel: { color: '#fff' } + axisLabel: { + color: '#fff', + margin: 15 // 增加y轴标签边距 + }, + axisTick: { + alignWithLabel: true + } }, xAxis: { type: 'value', @@ -1105,7 +1173,8 @@ const initCharts = () => { type: 'dashed', color: 'rgba(255, 255, 255, 0.2)' } - } + }, + interval: 1 }, series: [ { @@ -1113,13 +1182,16 @@ const initCharts = () => { type: 'bar', data: outputData.value.doctor, itemStyle: { color: '#4080ff' }, - barWidth: '20%', - barGap: '20%', + barWidth: '25%', // 减小柱宽度 + barGap: '20%', // 增加柱子间距 label: { show: true, position: 'right', color: '#fff', - formatter: '{c}' + formatter: function(params) { + // 只显示非零值,避免0重叠 + return params.value > 0 ? params.value : ''; + } } }, { @@ -1127,13 +1199,16 @@ const initCharts = () => { type: 'bar', data: outputData.value.master, itemStyle: { color: 'rgb(107,187,196)' }, - barWidth: '20%', + barWidth: '25%', barGap: '20%', label: { show: true, position: 'right', color: '#fff', - formatter: '{c}' + formatter: function(params) { + // 只显示非零值,避免0重叠 + return params.value > 0 ? params.value : ''; + } } }, { @@ -1141,13 +1216,16 @@ const initCharts = () => { type: 'bar', data: outputData.value.bachelor, itemStyle: { color: 'rgb(107,187,19)' }, - barWidth: '20%', + barWidth: '25%', barGap: '20%', label: { show: true, position: 'right', color: '#fff', - formatter: '{c}' + formatter: function(params) { + // 只显示非零值,避免0重叠 + return params.value > 0 ? params.value : ''; + } } } ] @@ -1312,6 +1390,12 @@ const newsData = ref([]); // 组件卸载时清理定时器 onUnmounted(() => { + if (typingInterval) { + clearInterval(typingInterval); + } + if (thinkingInterval) { + clearInterval(thinkingInterval); + } window.removeEventListener('resize', () => {}); }) @@ -1765,7 +1849,31 @@ onUnmounted(() => { .assistant-panel .panel-header { display: flex; align-items: center; - cursor: move; /* 整个头部可拖动 */ - user-select: none; /* 防止拖动时选中文本 */ + cursor: move; + user-select: none; +} + +/* 思考消息样式 */ +.thinking-message { + display: flex; + align-items: center; +} + +.thinking-dots::after { + content: '...'; + display: inline-block; + width: 20px; + animation: blinkDots 1.5s infinite steps(4, end); + +} + +@keyframes blinkDots { + 0%, 100% { opacity: 0; } + 25% { opacity: 0.5; } + 50% { opacity: 1; } +} + +.message { + transition: all 0.3s ease; } \ No newline at end of file diff --git a/src/config.js b/src/config.js index d275462..f7f29f8 100644 --- a/src/config.js +++ b/src/config.js @@ -3,16 +3,23 @@ const env = import.meta.env.MODE || 'development'; const config = { development: { - apiBaseUrl: 'http://192.168.5.102:48080', + // apiBaseUrl: 'http://192.168.5.49:48080', + apiBaseUrl: 'http://36.103.199.218:48088', + chatApiUrl: 'http://36.103.199.218:8093' }, production: { - apiBaseUrl: 'http://221.238.217.216:4162', - // apiBaseUrl: 'http://36.103.199.218:48088', + // apiBaseUrl: 'http://221.238.217.216:4162', + // chatApiUrl: 'http://221.238.217.216:8093', + apiBaseUrl: 'http://36.103.199.218:48088', + chatApiUrl: 'http://36.103.199.218:8093', } }; export const getApiBaseUrl = () => { return config[env].apiBaseUrl; }; +export const getChatApiUrl = () => { + return config[env].chatApiUrl; +} export default config; \ No newline at end of file