Compare commits
8 Commits
3bad5b423d
...
bc83efc14e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bc83efc14e | ||
![]() |
1300959ee0 | ||
![]() |
55ad1b125f | ||
![]() |
848159a618 | ||
![]() |
776fb6dc15 | ||
![]() |
e244c3b38e | ||
![]() |
9635735c47 | ||
![]() |
295f9cb304 |
@ -46,8 +46,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 学术产出 -->
|
<!-- 学术产出 -->
|
||||||
<div class="dashboard-panel" style="flex: 1 1 0;">
|
<div class="dashboard-panel" style="flex: 1.4;">
|
||||||
<h2>学术产出</h2>
|
<h2 style="margin-bottom: 0;">学术产出</h2>
|
||||||
<div class="output-content">
|
<div class="output-content">
|
||||||
<div ref="outputChartRef" class="chart-container-65"></div>
|
<div ref="outputChartRef" class="chart-container-65"></div>
|
||||||
<!-- <div class="international-impact">
|
<!-- <div class="international-impact">
|
||||||
@ -138,10 +138,18 @@
|
|||||||
<div v-html="dashboardData2?.prompt"></div>
|
<div v-html="dashboardData2?.prompt"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(message, index) in chatMessages" :key="index"
|
<div v-for="(message, index) in chatMessages" :key="index"
|
||||||
:class="['message', message.role === 'user' ? 'user-message' : 'assistant-message']">
|
:class="['message', message.role === 'user' ? 'user-message' : 'assistant-message']" v-show="!message.isHidden">
|
||||||
<div v-html="message.content"></div>
|
<!-- 修改这里的显示逻辑 -->
|
||||||
|
<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>
|
||||||
<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>
|
<span class="loading-dot"></span>
|
||||||
<span class="loading-dot"></span>
|
<span class="loading-dot"></span>
|
||||||
@ -194,14 +202,15 @@ const redirectToResearchEvaluation = async () => {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.code === 0) {
|
if (result.code === 0) {
|
||||||
// 跳转到目标网站
|
// 跳转到目标网站
|
||||||
await localStorage.setItem('access_token', result.data.access);
|
// await localStorage.setItem('access_token', result.data.access);
|
||||||
await localStorage.setItem('refresh_token', result.data.refresh);
|
// await localStorage.setItem('refresh_token', result.data.refresh);
|
||||||
await localStorage.setItem('user_info', JSON.stringify({
|
// await localStorage.setItem('user_info', JSON.stringify({
|
||||||
user_id: result.data.user_id,
|
// user_id: result.data.user_id,
|
||||||
username: result.data.username,
|
// username: result.data.username,
|
||||||
permission: '2'
|
// permission: '2'
|
||||||
}));
|
// }));
|
||||||
window.location.href = 'http://82.156.236.221:10004/user/file'
|
window.open(`http://82.156.236.221:10004/user/file?refresh_token=${result.data.refresh}`), '_blank';
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.error('代理登录失败:', result.error);
|
console.error('代理登录失败:', result.error);
|
||||||
}
|
}
|
||||||
@ -218,14 +227,14 @@ const redirectToResearchEvaluation2 = async () => {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.code === 0) {
|
if (result.code === 0) {
|
||||||
// 跳转到目标网站
|
// 跳转到目标网站
|
||||||
await localStorage.setItem('access_token', result.data.access);
|
// await localStorage.setItem('access_token', result.data.access);
|
||||||
await localStorage.setItem('refresh_token', result.data.refresh);
|
// await localStorage.setItem('refresh_token', result.data.refresh);
|
||||||
await localStorage.setItem('user_info', JSON.stringify({
|
// await localStorage.setItem('user_info', JSON.stringify({
|
||||||
user_id: result.data.user_id,
|
// user_id: result.data.user_id,
|
||||||
username: result.data.username,
|
// username: result.data.username,
|
||||||
permission: '1'
|
// permission: '1'
|
||||||
}))
|
// }))
|
||||||
window.location.href = 'http://82.156.236.221:10004/admin/manage'
|
window.open(`http://82.156.236.221:10004/admin/manage?refresh_token=${result.data.refresh}`), '_blank';
|
||||||
} else {
|
} else {
|
||||||
console.error('代理登录失败:', result.error);
|
console.error('代理登录失败:', result.error);
|
||||||
}
|
}
|
||||||
@ -366,6 +375,8 @@ const chatMessagesRef = ref(null)
|
|||||||
const userInput = ref('')
|
const userInput = ref('')
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const chatMessages = ref([])
|
const chatMessages = ref([])
|
||||||
|
let typingInterval = null
|
||||||
|
let thinkingInterval = null
|
||||||
|
|
||||||
// DeepSeek API 调用函数
|
// DeepSeek API 调用函数
|
||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
@ -373,35 +384,41 @@ const sendMessage = async () => {
|
|||||||
|
|
||||||
// 添加用户消息到对话
|
// 添加用户消息到对话
|
||||||
const userMessage = userInput.value.trim();
|
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 = '';
|
userInput.value = '';
|
||||||
|
|
||||||
// 立即滚动到底部(用户消息)
|
// 立即滚动到底部
|
||||||
await nextTick();
|
// await nextTick();
|
||||||
scrollToBottom();
|
// scrollToBottom();
|
||||||
|
|
||||||
// 设置加载状态并添加占位消息
|
// 设置加载状态并添加占位消息
|
||||||
isLoading.value = true;
|
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;
|
const aiMessageIndex = chatMessages.value.length - 1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 只保留最近5条消息以优化性能
|
|
||||||
const apiMessages = chatMessages.value
|
|
||||||
.slice(-5)
|
|
||||||
.map(msg => ({ role: msg.role, content: msg.content }));
|
|
||||||
|
|
||||||
// 调用API(启用流式传输)
|
// 调用API(启用流式传输)
|
||||||
const response = await fetch('https://api.deepseek.com/chat/completions', {
|
const response = await fetch(`${getChatApiUrl()}/chat/chat`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer sk-06801499dd52426fa7cf3b0670931e3a'
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'deepseek-chat',
|
model: "deepseek-r1:7b",
|
||||||
messages: apiMessages,
|
messages: [{ role: "user", content: userMessage }],
|
||||||
stream: true // 关键优化:启用流式响应
|
options: { temperature: 0.6 },
|
||||||
|
keep_thinking: false
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -411,37 +428,75 @@ const sendMessage = async () => {
|
|||||||
|
|
||||||
// 流式处理数据
|
// 流式处理数据
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
let fullResponse = '';
|
|
||||||
const decoder = new TextDecoder();
|
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) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
|
|
||||||
// 解析流式数据(假设API返回ndjson格式)
|
buffer += decoder.decode(value, { stream: true });
|
||||||
const chunk = decoder.decode(value);
|
const lines = buffer.split("\n");
|
||||||
const lines = chunk.split('\n').filter(line => line.trim());
|
buffer = lines.pop() || "";
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(line.replace('data: ', ''));
|
const obj = JSON.parse(line);
|
||||||
if (data.choices?.[0]?.delta?.content) {
|
if (obj.message?.content) {
|
||||||
fullResponse += data.choices[0].delta.content;
|
fullResponse += obj.message.content;
|
||||||
// 实时更新UI(逐字显示)
|
|
||||||
chatMessages.value[aiMessageIndex].content = md.render(fullResponse);
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('请求失败:', error);
|
console.error('请求失败:', error);
|
||||||
chatMessages.value[aiMessageIndex].content = '抱歉,回答时遇到问题,请重试。';
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
chatMessages.value[aiMessageIndex] = {
|
||||||
|
role: 'assistant',
|
||||||
|
content: '抱歉,回答时遇到问题,请重试。',
|
||||||
|
isTyping: false,
|
||||||
|
isThinking: false,
|
||||||
|
isHidden: false // 确保错误消息显示
|
||||||
|
};
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -450,7 +505,7 @@ const sendMessage = async () => {
|
|||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
nextTick().then(() => {
|
nextTick().then(() => {
|
||||||
if (chatMessagesRef.value) {
|
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);
|
const loading = ref(true);
|
||||||
|
|
||||||
// 引入API配置
|
// 引入API配置
|
||||||
import { getApiBaseUrl } from '../config';
|
import { getApiBaseUrl, getChatApiUrl } from '../config';
|
||||||
|
|
||||||
// 从API获取仪表盘数据
|
// 从API获取仪表盘数据
|
||||||
const fetchDashboardData = async () => {
|
const fetchDashboardData = async () => {
|
||||||
@ -1086,14 +1141,27 @@ const initCharts = () => {
|
|||||||
'博士': outputLegendStatus.value[2],
|
'博士': outputLegendStatus.value[2],
|
||||||
'硕士': outputLegendStatus.value[1],
|
'硕士': outputLegendStatus.value[1],
|
||||||
'学士': outputLegendStatus.value[0],
|
'学士': 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: {
|
yAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: outputData.value.months,
|
data: outputData.value.months,
|
||||||
axisLine: { lineStyle: { color: '#fff' } },
|
axisLine: { lineStyle: { color: '#fff' } },
|
||||||
axisLabel: { color: '#fff' }
|
axisLabel: {
|
||||||
|
color: '#fff',
|
||||||
|
margin: 15 // 增加y轴标签边距
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
@ -1105,7 +1173,8 @@ const initCharts = () => {
|
|||||||
type: 'dashed',
|
type: 'dashed',
|
||||||
color: 'rgba(255, 255, 255, 0.2)'
|
color: 'rgba(255, 255, 255, 0.2)'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
interval: 1
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@ -1113,13 +1182,16 @@ const initCharts = () => {
|
|||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: outputData.value.doctor,
|
data: outputData.value.doctor,
|
||||||
itemStyle: { color: '#4080ff' },
|
itemStyle: { color: '#4080ff' },
|
||||||
barWidth: '20%',
|
barWidth: '25%', // 减小柱宽度
|
||||||
barGap: '20%',
|
barGap: '20%', // 增加柱子间距
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: 'right',
|
position: 'right',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
formatter: '{c}'
|
formatter: function(params) {
|
||||||
|
// 只显示非零值,避免0重叠
|
||||||
|
return params.value > 0 ? params.value : '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1127,13 +1199,16 @@ const initCharts = () => {
|
|||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: outputData.value.master,
|
data: outputData.value.master,
|
||||||
itemStyle: { color: 'rgb(107,187,196)' },
|
itemStyle: { color: 'rgb(107,187,196)' },
|
||||||
barWidth: '20%',
|
barWidth: '25%',
|
||||||
barGap: '20%',
|
barGap: '20%',
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: 'right',
|
position: 'right',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
formatter: '{c}'
|
formatter: function(params) {
|
||||||
|
// 只显示非零值,避免0重叠
|
||||||
|
return params.value > 0 ? params.value : '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1141,13 +1216,16 @@ const initCharts = () => {
|
|||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: outputData.value.bachelor,
|
data: outputData.value.bachelor,
|
||||||
itemStyle: { color: 'rgb(107,187,19)' },
|
itemStyle: { color: 'rgb(107,187,19)' },
|
||||||
barWidth: '20%',
|
barWidth: '25%',
|
||||||
barGap: '20%',
|
barGap: '20%',
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: 'right',
|
position: 'right',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
formatter: '{c}'
|
formatter: function(params) {
|
||||||
|
// 只显示非零值,避免0重叠
|
||||||
|
return params.value > 0 ? params.value : '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -1312,6 +1390,12 @@ const newsData = ref([]);
|
|||||||
|
|
||||||
// 组件卸载时清理定时器
|
// 组件卸载时清理定时器
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
if (typingInterval) {
|
||||||
|
clearInterval(typingInterval);
|
||||||
|
}
|
||||||
|
if (thinkingInterval) {
|
||||||
|
clearInterval(thinkingInterval);
|
||||||
|
}
|
||||||
window.removeEventListener('resize', () => {});
|
window.removeEventListener('resize', () => {});
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -1765,7 +1849,31 @@ onUnmounted(() => {
|
|||||||
.assistant-panel .panel-header {
|
.assistant-panel .panel-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: move; /* 整个头部可拖动 */
|
cursor: move;
|
||||||
user-select: none; /* 防止拖动时选中文本 */
|
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>
|
</style>
|
@ -3,16 +3,23 @@ const env = import.meta.env.MODE || 'development';
|
|||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
development: {
|
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: {
|
production: {
|
||||||
apiBaseUrl: 'http://221.238.217.216:4162',
|
// apiBaseUrl: 'http://221.238.217.216:4162',
|
||||||
// apiBaseUrl: 'http://36.103.199.218:48088',
|
// chatApiUrl: 'http://221.238.217.216:8093',
|
||||||
|
apiBaseUrl: 'http://36.103.199.218:48088',
|
||||||
|
chatApiUrl: 'http://36.103.199.218:8093',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getApiBaseUrl = () => {
|
export const getApiBaseUrl = () => {
|
||||||
return config[env].apiBaseUrl;
|
return config[env].apiBaseUrl;
|
||||||
};
|
};
|
||||||
|
export const getChatApiUrl = () => {
|
||||||
|
return config[env].chatApiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
export default config;
|
export default config;
|
Loading…
x
Reference in New Issue
Block a user