Merge branch 'view' into view_dev

# Conflicts:
#	src/config.js
This commit is contained in:
liuzhiyuan 2025-08-30 13:15:58 +08:00
commit bc83efc14e
2 changed files with 185 additions and 70 deletions

View File

@ -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;
// APIndjson 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);
} }
} }
} }
// APIisLoadingfalse
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>

View File

@ -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;