dashboard/src/components/Dashboard.vue
“zhuzihan”  518661cf77 fix:圆环宽度
2025-08-14 13:50:37 +08:00

1771 lines
46 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dashboard" style="display: flex; flex-direction: column;position:absolute;top:0;left:0;right:0;bottom:0;">
<!-- 顶部导航栏 -->
<header class="dashboard-header">
<div class="logo">
<img src="../assets/logo1.png" alt="北京理工大学" @click="handleLogoClick" />
<img src="../assets/logo2.png" alt="北京理工大学" @click="handleLogoClick" />
</div>
<h1 class="main-title">
<span class="title-text">智慧科研评估系统</span>
</h1>
<div class="header-placeholder"></div> <!-- 用于平衡布局的占位元素 -->
</header>
<!-- 仪表盘内容 - 三列布局 -->
<div class="dashboard-content" style="flex: 1; overflow: hidden;">
<!-- 第一列科研成果学术产出研究经费 -->
<div class="dashboard-column" style="overflow-y: auto;">
<!-- 科研成果 -->
<div class="dashboard-panel" style="flex: none; height: 130px;min-height:100px">
<div class="panel-header">
<h2>科研成果</h2>
<button class="panel-link" @click="redirectToResearchEvaluation">进入评估 >></button>
<button class="panel-link" @click="redirectToResearchEvaluation2">进入管理端 >></button>
</div>
<div class="research-stats">
<div class="stat-card">
<h3>论文总数</h3>
<div class="stat-value"><span class="stat-prefix">累计</span>0</div>
</div>
<div class="stat-card">
<h3>博士论文</h3>
<div class="stat-value"><span class="stat-prefix">累计</span>0</div>
</div>
<div class="stat-card">
<h3>硕士论文</h3>
<div class="stat-value"><span class="stat-prefix">累计</span>0</div>
</div>
<div class="stat-card">
<h3>学士论文</h3>
<div class="stat-value"><span class="stat-prefix">累计</span>0</div>
</div>
</div>
</div>
<!-- 学术产出 -->
<div class="dashboard-panel" style="flex: 1 1 0;">
<h2>学术产出</h2>
<div class="output-content">
<div ref="outputChartRef" class="chart-container-65"></div>
<!-- <div class="international-impact">
<h3 style="font-size:25px;border-bottom: 5px solid rgb(73,134,255);text-align: left;padding-bottom:5px ">国际影响力</h3>
<div class="journal-stat">
<span class="journal-name">Nature</span>
<span class="journal-count">15</span>
<span class="journal-name2"></span>
</div>
<div class="journal-stat">
<span class="journal-name">Science</span>
<span class="journal-count">8</span>
<span class="journal-name2"></span>
</div>
</div> -->
</div>
</div>
<!-- 研究经费 -->
<div class="dashboard-panel" style="flex: 1 1 0;">
<h2>学术详情</h2>
<div ref="fundingChartRef" class="chart-container-funding"></div>
</div>
</div>
<!-- 第二列教师科研人才学术奖项新闻与动态 -->
<div class="dashboard-column" style="overflow-y: auto;">
<!-- 教师科研人才 -->
<div class="dashboard-panel" style="flex: 1 1 0;">
<div class="panel-header">
<h2>教师科研人才</h2>
<button class="panel-link" @click="navigate('talent')">进入评估 >></button>
</div>
<div ref="researcherChartRef" class="chart-container"></div>
</div>
<!-- 学术奖项 -->
<div class="dashboard-panel" style="flex: 1 1 0;">
<h2>学术奖项</h2>
<div ref="awardsChartRef" class="chart-container"></div>
</div>
<!-- 教师服务与社会贡献项目分布 -->
<div class="dashboard-panel" style="flex: 1 1 0;">
<h2>教师服务与社会贡献项目分布</h2>
<div ref="teacherServiceChartRef" class="chart-container"></div>
</div>
</div>
<!-- 第三列工程研究中心智能助手 -->
<div class="dashboard-column" style="overflow-y: auto;">
<!-- 工程研究中心 -->
<div class="dashboard-panel" style="flex: 6 1 0;">
<div class="panel-header">
<h2>工程研究中心</h2>
<button class="panel-link" @click="navigate('lab')">进入评估 >></button>
</div>
<div class="lab-charts">
<div ref="labBarChartRef" class="lab-chart-container"></div>
<div class="lab-chart-container">
<div ref="labLineChartRef" class="lab-line-chart"></div>
</div>
</div>
</div>
<!-- 智能助手 -->
<div class="dashboard-panel" ref="assistantPanel" style="flex: 4 1 0; position: relative; z-index: 111111; ">
<div class="panel-header">
<div class="drag-handle" @mousedown="startResize">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 5H7V7H9V5Z" fill="currentColor"/>
<path d="M9 11H7V13H9V11Z" fill="currentColor"/>
<path d="M9 17H7V19H9V17Z" fill="currentColor"/>
<path d="M15 5H13V7H15V5Z" fill="currentColor"/>
<path d="M15 11H13V13H15V11Z" fill="currentColor"/>
<path d="M15 17H13V19H15V17Z" fill="currentColor"/>
</svg>
</div>
<h2>智能助手</h2>
</div>
<div class="assistant-container">
<div class="assistant-header">
<img :src="dashboardData2?.logoUrl" class="assistant-avatar" />
<h3>北京理工大学</h3>
</div>
<div class="assistant-interface">
<div class="assistant-messages custom-scrollbar" ref="chatMessagesRef">
<div class="assistant-message message">
<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>
</div>
<div v-if="isLoading" class="assistant-message loading-message">
<span class="loading-dot"></span>
<span class="loading-dot"></span>
<span class="loading-dot"></span>
</div>
</div>
<div class="assistant-input">
<input type="text" placeholder="请输入您的问题..." v-model="userInput" @keyup.enter="sendMessage" />
<button class="assistant-send" @click="sendMessage" :disabled="isLoading || !userInput.trim()">发送</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import * as echarts from 'echarts/core'
import { BarChart, PieChart, LineChart, RadarChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent
} from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt();
// 向父组件发送页面切换事件
const emit = defineEmits(['navigate', 'logout'])
const navigate = (page) => {
emit('navigate', page)
}
// 处理Logo点击事件
const handleLogoClick = () => {
emit('logout')
}
// 跳转到外部科研成果评估链接
const redirectToResearchEvaluation = async () => {
try {
const response = await fetch(`${getApiBaseUrl()}/admin-api/pg/paper/refresh`, {
method: 'get',
credentials: 'include' // 重要确保发送和接收cookie
});
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'
} else {
console.error('代理登录失败:', result.error);
}
} catch (error) {
console.error('请求失败:', error);
}
};
const redirectToResearchEvaluation2 = async () => {
try {
const response = await fetch(`${getApiBaseUrl()}/admin-api/pg/paper/adminLogin`, {
method: 'get',
credentials: 'include' // 重要确保发送和接收cookie
});
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'
} else {
console.error('代理登录失败:', result.error);
}
} catch (error) {
console.error('请求失败:', error);
}
};
// 注册必要的 echarts 组件
echarts.use([
BarChart,
PieChart,
LineChart,
RadarChart,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
CanvasRenderer
])
// 图表DOM引用
const researcherChartRef = ref(null)
const labBarChartRef = ref(null)
const labLineChartRef = ref(null)
const outputChartRef = ref(null)
const awardsChartRef = ref(null)
const fundingChartRef = ref(null)
const teacherServiceChartRef = ref(null) // 新增教师服务图表DOM引用
// 添加ref和响应式数据
const assistantPanel = ref(null)
const isResizing = ref(false)
const startY = ref(0)
const startHeight = ref(0)
// 开始调整大小
// 保持原有的resize逻辑不变但修改触发方式
const startResize = (e) => {
// 阻止事件冒泡,避免与面板内部交互冲突
e.stopPropagation()
isResizing.value = true
startY.value = e.clientY
startHeight.value = parseInt(document.defaultView.getComputedStyle(assistantPanel.value).height, 10)
// 添加鼠标移动和释放事件监听
document.addEventListener('mousemove', handleResize)
document.addEventListener('mouseup', stopResize)
// 设置光标样式
document.body.style.cursor = 'row-resize'
document.body.style.userSelect = 'none'
}
// 其他resize相关函数保持不变
const handleResize = (e) => {
if (!isResizing.value) return
const dy = e.clientY - startY.value
const newHeight = startHeight.value - dy
// 限制最小和最大高度
const minHeight = 200 // 最小高度
const maxHeight = window.innerHeight - 100 // 最大高度
if (newHeight > minHeight && newHeight < maxHeight) {
assistantPanel.value.style.flex = 'none'
assistantPanel.value.style.height = `${newHeight}px`
// 调整后立即重新计算工程研究中心图表
nextTick(() => {
const labBarChart = echarts.getInstanceByDom(labBarChartRef.value)
const labLineChart = echarts.getInstanceByDom(labLineChartRef.value)
if (labBarChart) labBarChart.resize()
if (labLineChart) labLineChart.resize()
})
}
}
const stopResize = () => {
isResizing.value = false
document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
// 教研人才图表数据
const researcherData = ref({
datax: [],
datay: [],
history: [],
ishistory: false
})
// 学术奖项图表数据
const studyData = ref({
datax: [],
datay: []
})
// 教师服务与社会贡献项目分布图表数据
const teacherServiceData = ref({
datax: [],
datay: []
})
// 工程研究中心图表数据
const labData = ref({
datax: [],
series: [],
years: []
})
const labLineData = ref({
datax: [],
datay: []
})
// 学术产出图表数据
const outputData = ref({
months: [],
doctor: [],
master: [],
bachelor: []
})
// 研究经费图表数据
const fundingData = ref({
doctor: 0,
master: 0,
bachelor: 0
})
// DeepSeek 智能助手相关变量
const chatMessagesRef = ref(null)
const userInput = ref('')
const isLoading = ref(false)
const chatMessages = ref([])
// DeepSeek API 调用函数
const sendMessage = async () => {
if (!userInput.value.trim() || isLoading.value) return;
// 添加用户消息到对话
const userMessage = userInput.value.trim();
chatMessages.value.push({ role: 'user', content: userMessage });
userInput.value = '';
// 立即滚动到底部(用户消息)
await nextTick();
scrollToBottom();
// 设置加载状态并添加占位消息
isLoading.value = true;
chatMessages.value.push({ role: 'assistant', content: '思考中...' });
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', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer sk-06801499dd52426fa7cf3b0670931e3a'
},
body: JSON.stringify({
model: 'deepseek-chat',
messages: apiMessages,
stream: true // 关键优化:启用流式响应
})
});
if (!response.ok) {
throw new Error(`API错误: ${response.status}`);
}
// 流式处理数据
const reader = response.body.getReader();
let fullResponse = '';
const decoder = new TextDecoder();
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());
for (const line of lines) {
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();
}
} catch (e) {
console.warn('解析流数据失败:', e);
}
}
}
} catch (error) {
console.error('请求失败:', error);
chatMessages.value[aiMessageIndex].content = '抱歉,回答时遇到问题,请重试。';
} finally {
isLoading.value = false;
scrollToBottom();
}
};
// 滚动辅助函数
const scrollToBottom = () => {
nextTick().then(() => {
if (chatMessagesRef.value) {
chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight;
}
});
};
// 研究经费图例状态
const fundingLegendStatus = ref([true, true, true])
// 学术产出图例状态
const outputLegendStatus = ref([true, true, true])
// 切换研究经费图例
const toggleFundingLegend = (index) => {
fundingLegendStatus.value[index] = !fundingLegendStatus.value[index]
updateFundingChart()
}
// 切换学术产出图例
const toggleOutputLegend = (index) => {
outputLegendStatus.value[index] = !outputLegendStatus.value[index]
updateOutputChart()
}
// 更新研究经费图表
const updateFundingChart = () => {
const fundingChart = echarts.getInstanceByDom(fundingChartRef.value)
if (!fundingChart) return
fundingChart.setOption({
legend: {
selected: {}
}
})
}
// 更新学术产出图表
const updateOutputChart = () => {
const outputChart = echarts.getInstanceByDom(outputChartRef.value)
if (!outputChart) return
outputChart.setOption({
legend: {
selected: {}
}
})
}
// 获取仪表盘数据
const dashboardData = ref(null);
const dashboardData2 = ref(null);
const loading = ref(true);
// 引入API配置
import { getApiBaseUrl } from '../config';
// 从API获取仪表盘数据
const fetchDashboardData = async () => {
try {
const response = await fetch(`${getApiBaseUrl()}/admin-api/pg/J-dashboard/dashboard`);
if (response.ok) {
const data = await response.json();
console.log("仪表盘数据:", data);
dashboardData.value = data.data;
if (data) {
updateChartsWithDashboardData();
}
} else {
console.error('获取仪表盘数据失败:', response.statusText);
}
} catch (error) {
console.error('获取仪表盘数据出错:', error);
} finally {
loading.value = false;
}
};
const fetchDashboardData2 = async () => {
try {
const response = await fetch(`${getApiBaseUrl()}/admin-api/pg/intelligent-assistant/get-release`);
const data = await response.json();
dashboardData2.value = data.data;
if (data) {
updateChartsWithDashboardData();
}
} catch (error) {
console.error('获取仪表盘数据出错:', error);
} finally {
loading.value = false;
}
};
const fetchTeacherbData = async (type) => {
try {
const res = await fetch(`${getApiBaseUrl()}/admin-api/pg/teacher-dashboard/get-release?type=${type}`);
if (res.ok) {
const data = await res.json();
// 处理 keyFields 数据
const keyFields = data.data.keyFields || [];
const datax = keyFields.map(item => item.fieldName);
const datay = keyFields.map(item => item.quantity);
if (type === 1) {
const highest = data.data.highestValue || [];
const ishistory = data.data.historyHighest;
const history = highest.map(item => item.quantity);
researcherData.value = {
datax,
datay,
history,
ishistory
};
// 数据更新后,手动更新图表
updateResearcherChart();
} else if (type === 2) {
studyData.value = {
datax: keyFields.map(item => ({ name: item.fieldName,max: Math.max(...datay)})),
datay
};
updateStudyChart();
} else if (type === 3) {
teacherServiceData.value = {
datax,
datay
};
updateTeacherServiceChart();
}
} else {
console.error(`获取教师数据失败 (type=${type}):`, res.statusText);
}
} catch (error) {
console.error(`获取教师数据出错 (type=${type}):`, error);
} finally {
loading.value = false;
}
};
const fetchTeacherbData1 = () => fetchTeacherbData(1);
const fetchTeacherbData2 = () => fetchTeacherbData(2);
const fetchTeacherbData3 = () => fetchTeacherbData(3);
const updateStudyChart = () => {
const awardsChart = echarts.getInstanceByDom(awardsChartRef.value);
if (awardsChart) {
awardsChart.setOption({
radar: {
indicator: studyData.value.datax
},
series: [
{
data: [
{
value: studyData.value.datay
}
]
}
]
});
}
}
const updateResearcherChart = () => {
const researcherChart = echarts.getInstanceByDom(researcherChartRef.value);
if (researcherChart) {
researcherChart.setOption({
legend: {
data: researcherData.value.ishistory ? ['当前数量', '历史最高'] : ['当前数量']
},
yAxis: {
data: researcherData.value.datax
},
series: [
{
name: '当前数量',
data: researcherData.value.datay
},
{
name: '历史最高',
data: researcherData.value.history
}
]
});
}
};
const updateTeacherServiceChart = () => {
const teacherServiceChart = echarts.getInstanceByDom(teacherServiceChartRef.value);
if (teacherServiceChart) {
teacherServiceChart.setOption({
legend: {
data: ['当前数量'],
textStyle: { color: '#fff' }
},
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff' },
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: 'rgba(255, 255, 255, 0.2)'
}
}
},
yAxis: {
type: 'category',
data: teacherServiceData.value.datax,
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff' },
splitLine: { show: false }
},
series: [
{
name: '当前数量',
type: 'bar',
stack: 'total',
data: teacherServiceData.value.datay,
itemStyle: { color: '#f39c12' }, // 更换颜色
barWidth: '40%',
label: {
show: true,
position: 'inside',
color: '#fff',
formatter: '{c}'
}
}
]
});
}
};
// 定义一个颜色数组,用于不同年份的柱子
const barColors = ['#4080ff', '#d7fc33', 'rgb(63, 196, 15)']; // 蓝色,金色,紫色
const initEngineeringKeyFields = (data) => {
const fieldNames = data.map(item => item.fieldName);
const years = data.length > 0 ? Array.from(new Set(data.flatMap(item => item.yearData.map(y => y.year)))) : [];
years.sort((a, b) => a - b); // 确保年份排序
const series = years.map((year, index) => { // 添加 index 参数
return {
name: year.toString(),
type: 'bar',
barWidth: '15%',
// 根据 index 从颜色数组中获取颜色
itemStyle: {
color: barColors[index % barColors.length] // 确保颜色循环使用
},
data: fieldNames.map(fieldName => {
const item = data.find(d => d.fieldName === fieldName);
const yearData = item ? item.yearData.find(y => y.year === year) : null;
return yearData ? yearData.quantity : 0;
}),
emphasis: {
focus: 'series'
}
};
});
labData.value.datax = fieldNames;
labData.value.series = series;
labData.value.years = years;
console.log("Processed labData.value:", labData.value);
};
const fetchPaperData = async () => {
try {
const res = await fetch('http://82.156.236.221:10005/api/v1/review/show/details/');
if (res.ok) {
const data = await res.json();
if (data.code === 200) {
// 更新论文数量
const paperCountEl = document.querySelector('.stat-card:nth-child(1) .stat-value');
const paperCountEl2 = document.querySelector('.stat-card:nth-child(2) .stat-value');
const paperCountEl3 = document.querySelector('.stat-card:nth-child(3) .stat-value');
const paperCountEl4 = document.querySelector('.stat-card:nth-child(4) .stat-value');
if (paperCountEl) {
paperCountEl.innerHTML = `<span class="stat-prefix" style="font-size: 12px;">累计</span>${data.data.total.sum || 0}`;
paperCountEl2.innerHTML = `<span class="stat-prefix" style="font-size: 12px;">累计</span>${data.data.total.doctor || 0}`;
paperCountEl3.innerHTML = `<span class="stat-prefix" style="font-size: 12px;">累计</span>${data.data.total.master || 0}`;
paperCountEl4.innerHTML = `<span class="stat-prefix" style="font-size: 12px;">累计</span>${data.data.total.bachelor || 0}`;
}
// 获取当前月份前6个月的月份名称
const currentDate = new Date();
const monthNames = [];
for (let i = 5; i >= 0; i--) {
const date = new Date(currentDate.getFullYear(), currentDate.getMonth() - i, 1);
monthNames.push(`${date.getMonth() + 1}`);
}
// 更新学术产出数据
outputData.value = {
months: monthNames,
doctor: data.data.time.map(item => item[2]).reverse(), // 添加reverse()
master: data.data.time.map(item => item[1]).reverse(), // 添加reverse()
bachelor: data.data.time.map(item => item[0]).reverse() // 添加reverse()
};
updateOutputChart();
// 更新研究经费数据
fundingData.value = {
doctor: data.data.total.doctor,
master: data.data.total.master,
bachelor: data.data.total.bachelor
};
updateFundingChart();
}
} else {
console.error('获取论文数据失败:', res.statusText);
}
} catch (error) {
console.error('获取论文数据出错:', error);
} finally {
loading.value = false;
}
};
const fetchLabData = async () => {
try {
const res = await fetch(`${getApiBaseUrl()}/admin-api/pg/research-data-dashboard/get-release`);
if (res) {
const data = await res.json();
const keyFields = data.data.keyFields || [];
const processedData = keyFields.map((group) => {
const firstItem = group[0];
const yearData = group.map((item) => ({
year: item.year,
quantity: item.quantity
}));
return {
fieldName: firstItem.fieldName,
yearData
};
});
initEngineeringKeyFields(processedData);
updateLabBarChart();
} else {
console.error('获取工程研究中心数据失败:', res.statusText);
}
} catch (error) {
console.error('获取工程研究中心数据出错:', error);
} finally {
loading.value = false;
}
};
const fetchLabData2 = async () => {
try {
const res = await fetch(`${getApiBaseUrl()}/admin-api/pg/changes-in-achievements/get-release`);
if (res) {
const data = await res.json();
const afterAnalysis = data.data.afterAnalysis || [];
const datax = afterAnalysis.map(item => item.year);
const datay = afterAnalysis.map(item => item.quantity);
labLineData.value = {
datax,
datay
};
updateLabLineChart();
} else {
console.error('获取工程研究中心数据失败:', res.statusText);
}
} catch (error) {
console.error('获取工程研究中心数据出错:', error);
} finally {
loading.value = false;
}
};
// 添加更新工程研究中心柱状图的函数
const updateLabBarChart = () => {
const labBarChart = echarts.getInstanceByDom(labBarChartRef.value);
if (labBarChart) {
console.log("Updating labBarChart with:", {
xAxisData: labData.value.datax,
seriesData: labData.value.series
});
labBarChart.setOption({
legend: {
data: labData.value.years.map(String),
textStyle: { color: '#fff' }
},
xAxis: {
data: labData.value.datax,
axisLabel: {
textStyle: { color: '#fff' } // 标签颜色
}
},
series: labData.value.series
}, true);
}
}
const updateLabLineChart = () => {
const labLineChart = echarts.getInstanceByDom(labLineChartRef.value);
if (labLineChart) {
labLineChart.setOption({
xAxis: {
data: labLineData.value.datax
},
series: [
{
data: labLineData.value.datay
}
]
});
}
};
// 使用仪表盘数据更新图表
const updateChartsWithDashboardData = () => {
if (!dashboardData.value) return;
if (!dashboardData2.value) return;
};
// 初始化图表
const initCharts = () => {
// 研究人员图表
const researcherChart = echarts.init(researcherChartRef.value)
researcherChart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: {
data: researcherData.value.ishistory ? ['当前数量', '历史最高'] : ['当前数量'],
textStyle: { color: '#fff' }
},
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff' },
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: 'rgba(255, 255, 255, 0.2)'
}
}
},
yAxis: {
type: 'category',
data: researcherData.value.datax,
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff' },
splitLine: { show: false }
},
series: [
{
name: '当前数量',
type: 'bar',
stack: 'total',
data: researcherData.value.datay,
itemStyle: { color: '#a95df3' },
barWidth: '40%', // 减小柱宽度
label: {
show: true,
position: 'inside',
color: '#fff',
formatter: '{c}'
}
},
{
name: '历史最高',
type: 'bar',
stack: 'total',
data: researcherData.value.history,
itemStyle: { color: '#7b49ca' },
label: {
show: true,
position: 'inside',
color: '#fff',
formatter: '{c}'
}
}
]
})
// 教师服务与社会贡献项目分布
const teacherServiceChart = echarts.init(teacherServiceChartRef.value)
teacherServiceChart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: {
data: ['当前数量'],
textStyle: { color: '#fff' }
},
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff' },
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: 'rgba(255, 255, 255, 0.2)'
}
}
},
yAxis: {
type: 'category',
data: teacherServiceData.value.datax,
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff' },
splitLine: { show: false }
},
series: [
{
name: '当前数量',
type: 'bar',
stack: 'total',
data: teacherServiceData.value.datay,
itemStyle: { color: '#f39c12' }, // 更换颜色
barWidth: '40%',
label: {
show: true,
position: 'inside',
color: '#fff',
formatter: '{c}'
}
}
]
})
// 工程研究中心柱状图(调整为占满全部宽度)
const labBarChart = echarts.init(labBarChartRef.value)
labBarChart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: {
data: labData.value.years.map(String),
textStyle: { color: '#fff' } //确保图例文字颜色
},
grid: {
left: '3%',
right: '4%',
top: '10%',
bottom: '15%', // 增加底部空间以容纳旋转的标签
containLabel: true
},
xAxis: {
type: 'category',
data: labData.value.datax,
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: {
color: '#fff',
// 换行显示,每 4 个字符换行
formatter: function (params) {
return params.split('').reduce((acc, char, index) => {
if (index % 9 === 0 && index > 0) {
return acc + '\n' + char;
}
return acc + char;
}, '');
},
// 缩小字体大小
fontSize: 10,
}
},
yAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff' },
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: 'rgba(255, 255, 255, 0.2)'
},
interval: 1
}
},
series: labData.value.series
})
// 工程研究中心折线图(调整为占满全部宽度)
const labLineChart = echarts.init(labLineChartRef.value)
labLineChart.setOption({
title: {
text: '科研成果增长曲线',
textStyle: {
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 14,
fontWeight: 'normal'
},
left: 'center',
bottom: '0%'
},
tooltip: { trigger: 'axis' },
grid: {
left: '3%',
right: '4%',
top: '10%',
bottom: '15%', // 为标题留出空间
containLabel: true
},
xAxis: {
type: 'category',
data: labLineData.value.datax,
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff' }
},
yAxis: {
type: 'value',
max: 100,
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff' },
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: 'rgba(255, 255, 255, 0.2)'
},
interval: 1
}
},
series: [
{
type: 'line',
data: labLineData.value.datay,
lineStyle: { color: '#ffd700', width: 3 },
itemStyle: { color: '#ffd700' },
symbolSize: 8
}
]
})
// 学术产出图表 - 使用API数据
const outputChart = echarts.init(outputChartRef.value)
outputChart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: {
data: ['博士', '硕士', '学士'],
textStyle: { color: '#fff' },
selected: {
'博士': outputLegendStatus.value[2],
'硕士': outputLegendStatus.value[1],
'学士': outputLegendStatus.value[0],
}
},
grid: { left: '3%', right: '12%', bottom: '3%', containLabel: true },
yAxis: {
type: 'category',
data: outputData.value.months,
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff' }
},
xAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#fff' } },
axisLabel: { color: '#fff' },
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: 'rgba(255, 255, 255, 0.2)'
}
}
},
series: [
{
name: '博士',
type: 'bar',
data: outputData.value.doctor,
itemStyle: { color: '#4080ff' },
barWidth: '20%',
barGap: '20%',
label: {
show: true,
position: 'right',
color: '#fff',
formatter: '{c}'
}
},
{
name: '硕士',
type: 'bar',
data: outputData.value.master,
itemStyle: { color: 'rgb(107,187,196)' },
barWidth: '20%',
barGap: '20%',
label: {
show: true,
position: 'right',
color: '#fff',
formatter: '{c}'
}
},
{
name: '学士',
type: 'bar',
data: outputData.value.bachelor,
itemStyle: { color: 'rgb(107,187,19)' },
barWidth: '20%',
barGap: '20%',
label: {
show: true,
position: 'right',
color: '#fff',
formatter: '{c}'
}
}
]
})
// 学术奖项图表 - 金色雷达图
const awardsChart = echarts.init(awardsChartRef.value)
awardsChart.setOption({
radar: {
indicator: studyData.value.datax,
splitArea: { show: false },
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
name: { textStyle: { color: '#fff' } },
radius: '70%'
},
series: [
{
type: 'radar',
data: [
{
value: studyData.value.datay,
name: '奖项分布',
areaStyle: { opacity: 0 },
lineStyle: { color: '#ffd700', width: 2 },
itemStyle: { color: '#ffd700' }
}
],
label: {
show: true,
position: 'top',
color: '#fff',
fontSize: 10
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
}
}
}
]
})
// 研究经费图表 - 使用API数据
const fundingChart = echarts.init(fundingChartRef.value)
fundingChart.setOption({
tooltip: { trigger: 'item' },
legend: {
orient: 'vertical',
left: '5%',
top: 'center',
textStyle: { color: '#fff' },
data: ['博士', '硕士', '学士'],
selected: {
'博士': fundingLegendStatus.value[2],
'硕士': fundingLegendStatus.value[1],
'学士': fundingLegendStatus.value[0]
}
},
series: [
{
type: 'pie', // 饼图 可选pie | ring
radius: ['30%', '50%'],
center: ['50%', '50%'],
// roseType: 'area',
label: {
show: true,
formatter: '{b}: {d}%',
position: 'outside',
color: '#fff',
distanceToLabelLine: 1
},
labelLine: {
show: true,
length: 15,
length2: 10,
lineStyle: {
color: 'rgba(255, 255, 255, 0.5)'
}
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
data: [
{
value: fundingData.value.doctor,
name: '博士',
itemStyle: { color: '#9966ff' }
},
{
value: fundingData.value.master,
name: '硕士',
itemStyle: { color: '#ff9933' }
},
{
value: fundingData.value.bachelor,
name: '学士',
itemStyle: { color: '#3399ff' }
}
]
}
]
})
// 响应窗口大小变化
window.addEventListener('resize', () => {
researcherChart.resize()
labBarChart.resize()
labLineChart.resize()
outputChart.resize()
awardsChart.resize()
fundingChart.resize()
teacherServiceChart.resize()
})
}
// 生命周期钩子 - 组件挂载后初始化
onMounted(async() => {
// 论文数据
await fetchPaperData();
// 教研人才数据
await fetchTeacherbData1();
await fetchTeacherbData2();
await fetchTeacherbData3();
// 工程研究中心数据
await fetchLabData();
await fetchLabData2();
// 获取仪表盘数据
await fetchDashboardData();
await fetchDashboardData2();
// 初始化图表
nextTick(() => {
initCharts();
});
});
// 监听仪表盘数据变化
watch(dashboardData, () => {
if (dashboardData.value) {
updateChartsWithData();
}
}, { deep: true });
// 更新图表数据
const updateChartsWithData = () => {
if (!dashboardData.value) return;
// 更新专利数量、高影响力论文和关键项目数量
const patentCountEl = document.querySelector('.stat-card:nth-child(2) .stat-value');
const highImpactPapersEl = document.querySelector('.stat-card:nth-child(3) .stat-value');
const keyProjectsEl = document.querySelector('.stat-card:nth-child(4) .stat-value');
};
// 定义newsData为ref使其可响应
const newsData = ref([]);
// 组件卸载时清理定时器
onUnmounted(() => {
window.removeEventListener('resize', () => {});
})
</script>
<style>
@import './common.css';
/* 自定义滚动条样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(73, 134, 255, 0.5);
border-radius: 3px;
transition: all 0.3s ease;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(73, 134, 255, 0.8);
}
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(73, 134, 255, 0.5) rgba(0, 0, 0, 0.1);
}
</style>
<style scoped>
/* 特定于Dashboard的样式 */
.dashboard {
color: white;
}
.dashboard-content {
display: flex;
padding: 15px;
gap: 15px;
}
.dashboard-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
min-width: 0; /* 防止flex子项溢出 */
max-height: 100%;
}
.dashboard-panel {
background-color: rgba(36, 69, 142, 0.5);
border-radius: 10px;
padding: 15px;
display: flex;
flex-direction: column;
flex: 1;
min-height: 200px; /* 设置最小高度 */
position: relative;
overflow: hidden;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.panel-link {
background: none;
border: none;
color: #4080ff;
cursor: pointer;
font-size: 14px;
}
.chart-container {
width: 100%;
height: 100%;
min-height: 160px;
}
.year-selector {
color: white;
font-size: 18px;
}
.year-selector .el-dropdown-link {
color: white;
font-size: 18px;
display: flex;
align-items: center;
white-space: nowrap;
}
.research-stats {
display: flex;
gap: 10px;
}
.stat-card {
flex: 1;
background-color: rgba(64, 128, 255, 0.2);
border-radius: 5px;
padding: 15px;
text-align: center;
border: 1px solid rgba(73,134,255,1);
color: #ffffff;
}
.stat-card h3 {
margin: 0 0 10px 0;
font-size: 12px;
font-weight: normal;
white-space: nowrap;
}
.stat-value {
font-size: 17px;
font-weight: bold;
color: #4080ff;
display: flex;
justify-content: center;
align-items: baseline;
}
.stat-value .stat-prefix {
font-size: 12px !important;
margin-right: 5px;
white-space: nowrap;
}
/* 学术产出布局 */
.output-content {
display: flex;
flex: 1;
}
.chart-container-65 {
width: 100%;
height: 100%;
}
.international-impact {
width: 35%;
padding: 0 15px;
display: flex;
flex-direction: column;
justify-content: center;
}
.international-impact h3 {
text-align: center;
margin-bottom: 20px;
}
.journal-stat {
margin-bottom: 15px;
font-size: 16px;
text-align: right;
}
.journal-name {
font-weight: bold;
width: 90px;
display: inline-block;
}
.journal-name2{
font-weight: bold;
width: 25px;
display: inline-block;
}
.journal-count {
color: #4080ff;
font-size: 30px;
font-weight: bold;
width: 45px;
display: inline-block;
}
/* 研究经费布局 */
.chart-container-funding {
width: 100%;
height: 100%;
}
/* 图例项 */
.legend-item {
display: flex;
align-items: center;
cursor: pointer;
}
.legend-color {
width: 15px;
height: 15px;
margin-right: 5px;
border-radius: 2px;
}
.legend-inactive {
opacity: 0.5;
}
.legend-inactive .legend-color {
background-color: #666 !important;
}
/* 工程研究中心图表 */
.lab-charts {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
}
.lab-chart-container {
height: 50%; /* 调整为均等高度 */
width: 100%; /* 确保占满全部宽度 */
position: relative;
margin: 5px 0;
}
.lab-line-chart {
width: 100%;
height: 100%;
}
/* 智能助手样式 */
.assistant-container {
flex: 1;
display: flex;
flex-direction: column;
height: 90%;
}
.assistant-header {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.assistant-avatar {
height: 30px;
margin-right: 10px;
}
.assistant-header h3 {
margin: 0;
font-size: 16px;
}
.assistant-interface {
display: flex;
flex-direction: column;
overflow-y: auto;
flex: 1;
background-color: rgba(20, 40, 80, 0.3);
border-radius: 5px;
margin-top: 10px;
}
.assistant-messages {
flex: 1;
padding: 10px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 10px;
}
.message {
padding: 10px;
border-radius: 5px;
max-width: 90%;
word-wrap: break-word;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.assistant-message {
background-color: rgba(64, 128, 255, 0.2);
align-self: flex-start;
border-bottom-left-radius: 0;
position: relative;
border-left: 3px solid rgba(73, 134, 255, 0.8);
}
.assistant-message:after {
content: '';
position: absolute;
bottom: 0;
left: -5px;
width: 10px;
height: 10px;
background-color: rgba(64, 128, 255, 0.2);
clip-path: polygon(0 0, 100% 100%, 100% 0);
}
.user-message {
background-color: rgba(73, 134, 255, 0.5);
align-self: flex-end;
border-bottom-right-radius: 0;
color: white;
position: relative;
border-right: 3px solid rgba(73, 134, 255, 0.8);
}
.user-message:after {
content: '';
position: absolute;
bottom: 0;
right: -5px;
width: 10px;
height: 10px;
background-color: rgba(73, 134, 255, 0.5);
clip-path: polygon(0 0, 0 100%, 100% 100%);
}
.loading-message {
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
border: none;
box-shadow: none;
}
.loading-message:after {
display: none;
}
.loading-dot {
width: 8px;
height: 8px;
background-color: #fff;
border-radius: 50%;
margin: 0 3px;
animation: bounce 1.4s infinite ease-in-out both;
}
.loading-dot:nth-child(1) {
animation-delay: -0.32s;
}
.loading-dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
} 40% {
transform: scale(1.0);
}
}
.assistant-input {
display: flex;
padding: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.assistant-input input {
flex: 1;
background-color: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 3px;
padding: 8px 12px;
color: white;
font-size: 14px;
transition: all 0.3s ease;
}
.assistant-input input:focus {
outline: none;
background-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 0 0 2px rgba(73, 134, 255, 0.3);
}
.assistant-input input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.assistant-send {
background-color: #4080ff;
color: white;
border: none;
border-radius: 3px;
padding: 0 15px;
margin-left: 10px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.assistant-send:hover {
background-color: #5090ff;
}
.assistant-send:disabled {
background-color: rgba(64, 128, 255, 0.5);
cursor: not-allowed;
}
@media (max-width: 1200px) {
.output-content {
flex-direction: column;
}
.chart-container-65 {
width: 100%;
height: 200px;
}
.international-impact {
width: 100%;
padding: 15px 0;
}
}
</style>
<style scoped>
/* 可拖动icon样式 */
.drag-handle {
cursor: move;
padding: 5px;
margin-right: 10px;
color: rgba(255, 255, 255, 0.6);
transition: all 0.2s;
display: inline-flex;
align-items: center;
}
.drag-handle:hover {
color: rgba(255, 255, 255, 0.9);
background-color: rgba(73, 134, 255, 0.3);
border-radius: 3px;
}
.drag-handle svg {
display: block;
}
/* 调整面板头部样式 */
.assistant-panel .panel-header {
display: flex;
align-items: center;
cursor: move; /* 整个头部可拖动 */
user-select: none; /* 防止拖动时选中文本 */
}
</style>