fix: 修改大屏中的学术产出展示!调整智能助手接口
This commit is contained in:
parent
848159a618
commit
55ad1b125f
@ -46,8 +46,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 学术产出 -->
|
||||
<div class="dashboard-panel" style="flex: 1 1 0;">
|
||||
<h2>学术产出</h2>
|
||||
<div class="dashboard-panel" style="flex: 1.4;">
|
||||
<h2 style="margin-bottom: 0;">学术产出</h2>
|
||||
<div class="output-content">
|
||||
<div ref="outputChartRef" class="chart-container-65"></div>
|
||||
<!-- <div class="international-impact">
|
||||
@ -138,10 +138,18 @@
|
||||
<div v-html="dashboardData2?.prompt"></div>
|
||||
</div>
|
||||
<div v-for="(message, index) in chatMessages" :key="index"
|
||||
:class="['message', message.role === 'user' ? 'user-message' : 'assistant-message']">
|
||||
<div v-html="message.content"></div>
|
||||
:class="['message', message.role === 'user' ? 'user-message' : 'assistant-message']" v-show="!message.isHidden">
|
||||
<!-- 修改这里的显示逻辑 -->
|
||||
<div v-if="message.isThinking" class="thinking-message">
|
||||
<span class="loading-dot"></span>
|
||||
<span class="loading-dot"></span>
|
||||
<span class="loading-dot"></span>
|
||||
</div>
|
||||
<div v-else-if="message.isTyping" v-html="message.content"></div>
|
||||
<div v-else v-html="message.content"></div>
|
||||
</div>
|
||||
<div v-if="isLoading" class="assistant-message loading-message">
|
||||
<div v-if="isLoading && (!chatMessages.length || !chatMessages[chatMessages.length-1].isThinking)"
|
||||
class="assistant-message loading-message">
|
||||
<span class="loading-dot"></span>
|
||||
<span class="loading-dot"></span>
|
||||
<span class="loading-dot"></span>
|
||||
@ -367,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 () => {
|
||||
@ -374,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
|
||||
})
|
||||
});
|
||||
|
||||
@ -412,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();
|
||||
}
|
||||
};
|
||||
@ -451,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;
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -504,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 () => {
|
||||
@ -1087,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',
|
||||
@ -1106,7 +1173,8 @@ const initCharts = () => {
|
||||
type: 'dashed',
|
||||
color: 'rgba(255, 255, 255, 0.2)'
|
||||
}
|
||||
}
|
||||
},
|
||||
interval: 1
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@ -1114,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 : '';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -1128,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 : '';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -1142,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 : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -1313,6 +1390,12 @@ const newsData = ref([]);
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (typingInterval) {
|
||||
clearInterval(typingInterval);
|
||||
}
|
||||
if (thinkingInterval) {
|
||||
clearInterval(thinkingInterval);
|
||||
}
|
||||
window.removeEventListener('resize', () => {});
|
||||
})
|
||||
</script>
|
||||
@ -1766,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;
|
||||
}
|
||||
</style>
|
@ -3,16 +3,23 @@ const env = import.meta.env.MODE || 'development';
|
||||
|
||||
const config = {
|
||||
development: {
|
||||
apiBaseUrl: 'http://192.168.5.49:48080',
|
||||
// apiBaseUrl: 'http://192.168.5.49:48080',
|
||||
apiBaseUrl: 'http://36.103.199.218:48089',
|
||||
chatApiUrl: 'http://36.103.199.218:8093'
|
||||
},
|
||||
production: {
|
||||
apiBaseUrl: 'http://221.238.217.216:4160',
|
||||
chatApiUrl: 'http://221.238.217.216:8093',
|
||||
// apiBaseUrl: 'http://36.103.199.218:48089',
|
||||
// chatApiUrl: 'http://36.103.199.218:8093',
|
||||
}
|
||||
};
|
||||
|
||||
export const getApiBaseUrl = () => {
|
||||
return config[env].apiBaseUrl;
|
||||
};
|
||||
export const getChatApiUrl = () => {
|
||||
return config[env].chatApiUrl;
|
||||
}
|
||||
|
||||
export default config;
|
Loading…
x
Reference in New Issue
Block a user