工程得分对比功能添加

This commit is contained in:
“zhuzihan”  2025-07-12 14:58:38 +08:00
parent 7b7496dae1
commit a5f67793b6
2 changed files with 426 additions and 73 deletions

View File

@ -19,44 +19,76 @@
<div class="main-content">
<!-- 搜索和操作栏 -->
<div class="action-bar">
<div class="sidebar-header">
<h1 class="sidebar-title">
<span class="home-link" @click="jumpToDashboard">首页</span>&nbsp;>&nbsp;工程研究中心评估
</h1>
</div>
<div class="search-box">
<input
<div class="sidebar-header">
<h1 class="sidebar-title">
<span class="home-link" @click="jumpToDashboard">首页</span>&nbsp;>&nbsp;工程研究中心评估
</h1>
</div>
<div class="searchbox">
<!-- 新增得分比较按钮组 -->
<div class="score-comparison-wrapper">
<el-button
type="primary"
v-if="!scoreComparisonMode"
class="compare-score-btn"
@click="toggleScoreComparisonMode"
>
得分比较
</el-button>
<template v-else>
<el-button class="compare-score-btn close-btn" @click="toggleScoreComparisonMode">
关闭
</el-button>
<el-button
class="compare-score-btn start-compare-btn"
@click="openScoreComparisonDialog"
:disabled="selectedLabs.length < 2"
:class="{ 'disabled-btn': selectedLabs.length < 2 }"
>
开始比较 ({{ selectedLabs.length }})
</el-button>
</template>
</div>
<!-- 搜索框 -->
<div class="search-box">
<input
type="text"
placeholder="请输入工程研究中心名称或ID号"
v-model="searchQuery"
@input="handleSearch"
/>
<button class="search-button">
<svg class="search-icon" viewBox="0 0 24 24" width="20" height="20">
<path fill="white" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>
</button>
/>
<button class="search-button">
<svg class="search-icon" viewBox="0 0 24 24" width="20" height="20">
<path fill="white" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>
</button>
</div>
<!-- <button class="add-evaluation-btn" @click="openAddEvaluationDrawer">
新增评估
</button> -->
</div>
<!-- <button class="add-evaluation-btn" @click="openAddEvaluationDrawer">
新增评估
</button> -->
</div>
<!-- 工程研究中心卡片列表 -->
<div class="lab-card-grid custom-scrollbar">
<!-- <div v-for="(lab, index) in filteredLabs" :key="index" class="lab-card" style="height: 440px;"> -->
<div v-for="(lab, index) in filteredLabs" :key="index" class="lab-card" style="height: 440px;" @click="openLabDetail(lab)">
<div class="card-header">
<span class="lab-id">ID: {{ lab.basicInformation.name1 || lab.id }}</span>
<div v-for="(lab) in filteredLabs" :key="lab.id" class="lab-card">
<!-- 复选框 -->
<div v-if="scoreComparisonMode" class="card-checkbox-wrapper">
<input
type="checkbox"
v-model="lab.selected"
@change="handleLabSelection(lab)"
/>
</div>
<div class="card-header" @click="openLabDetail(lab)">
<span class="lab-id" :style="scoreComparisonMode?'margin-left: 40px;':''">ID: {{ lab.basicInformation.name1 || lab.id }}</span>
<span class="total-score">年份: <span class="score-value">{{ lab.basicInformation.name0 }}</span></span>
</div>
<div class="card-content">
<!-- <div class="lab-image">
<img :src="lab.basicInformation.name5" alt="工程研究中心图片" />
</div> -->
<div class="card-content" @click="openLabDetail(lab)">
<div class="lab-info">
<div class="info-item">
<span class="info-label">中心名称:</span>
@ -64,7 +96,7 @@
</div>
<div class="info-item">
<span class="info-label">所属领域:</span>
<span class="info-value">{{ lab.basicInformation.name3 }}</span>
<span class="info-value">{{ lab.basicInformation.name3 }}</span>
</div>
<div class="info-item">
<span class="info-label">所属学校:</span>
@ -74,13 +106,14 @@
<span class="info-label">主管部门:</span>
<span class="info-value">{{ lab.basicInformation.name5 }}</span>
</div>
</div>
</div>
<div class="evaluation-chart">
<div :id="`lab-chart-${index}`" class="radar-chart"></div>
<div class="evaluation-chart" @click="openLabDetail(lab)">
<div :id="`lab-chart-${lab.id}`" class="radar-chart"></div> <!-- 根据lab.id确保唯一性 -->
</div>
<div class="evaluation-info">评估摘要{{ lab.abstracts }}</div>
</div>
</div>
</div>
@ -95,16 +128,53 @@
/>
<LabDrawerDetail
v-model:visible="drawerVisible"
:is-edit="isEditMode"
:dimensions="dimensions"
:lab-data="selectedLab"
@save="handleSaveEvaluation"
v-model:visible="drawerVisible"
:is-edit="isEditMode"
:dimensions="dimensions"
:lab-data="selectedLab"
@save="handleSaveEvaluation"
/>
<!-- 得分比较弹窗 -->
<el-dialog
v-model="scoreComparisonDialogVisible"
title="得分比较"
width="70%"
center
:append-to-body="true"
class="score-comparison-dialog"
>
<el-table
:data="scoreComparisonTableData"
style="width: 100%"
border
stripe
class="default-table score-comparison-table"
:row-class-name="tableRowClassName"
>
<el-table-column prop="category" label="类别" width="180"></el-table-column>
<el-table-column
v-for="(header) in scoreComparisonHeaders"
:key="header.prop"
:prop="header.prop"
:label="header.label"
align="center"
>
<template #default="{ row }">
<span :class="{
'max-score-in-column': Number(row[header.prop]) === columnStats[header.prop]?.max,
'min-score-in-column': Number(row[header.prop]) === columnStats[header.prop]?.min
}">
{{ row[header.prop] }}
</span>
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue';
import { ref, onMounted, watch, nextTick, computed, onBeforeUnmount } from 'vue'; // onBeforeUnmount
import * as echarts from 'echarts/core';
import { RadarChart } from 'echarts/charts';
import LabDrawerDetail from './LabDrawerDetail.vue';
@ -117,7 +187,7 @@ import {
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import axios from 'axios';
import { ElMessage } from 'element-plus';
import { ElMessage, ElDialog, ElTable, ElTableColumn } from 'element-plus';
import { getApiBaseUrl } from '../config'; // APIURL
// echarts
@ -135,6 +205,14 @@ const isEditMode = ref(false);
const selectedLab = ref(null);
const dimensionDrawerVisible = ref(false);
//
const scoreComparisonMode = ref(false); //
const selectedLabs = ref([]); // Lab Card
const scoreComparisonDialogVisible = ref(false); //
// Echarts
const chartInstances = new Map();
//
const openAddEvaluationDrawer = () => {
isEditMode.value = false;
@ -194,11 +272,27 @@ onMounted(async () => {
await loadLabs();
});
// Echarts
onBeforeUnmount(() => {
chartInstances.forEach(chart => {
chart.dispose();
});
chartInstances.clear();
window.removeEventListener('resize', debounceResize); // resize
});
//
const loadLabs = async () => {
try {
const response = await axios.get(`${getApiBaseUrl()}/admin-api/pg/evaluation-results/get-release`);
labs.value = response.data.data;
// labselected
// result
labs.value = response.data.data.map(lab => ({
...lab,
selected: false,
result: lab.result.map(Number) // radarData
}));
//
handleSearch();
// DOM
@ -210,36 +304,55 @@ const loadLabs = async () => {
}
};
//
const editDimension = (dim, index) => {
//
openDimensionDrawer();
// for resize
const debounce = (fn, delay) => {
let timer = null;
return function() {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
};
const debounceResize = debounce(() => {
chartInstances.forEach(chart => {
chart && chart.resize();
});
}, 300); // 300ms
//
const updateAllRadarCharts = () => {
nextTick(() => {
filteredLabs.value.forEach((lab, index) => {
const chartDom = document.getElementById(`lab-chart-${index}`);
//
const currentLabIds = new Set(filteredLabs.value.map(lab => lab.id));
chartInstances.forEach((chart, id) => {
if (!currentLabIds.has(id)) {
chart.dispose();
chartInstances.delete(id);
}
});
filteredLabs.value.forEach((lab) => {
const chartDom = document.getElementById(`lab-chart-${lab.id}`);
if (!chartDom) return;
//
echarts.dispose(chartDom);
const chart = echarts.init(chartDom);
let chart = chartInstances.get(lab.id);
if (!chart) {
chart = echarts.init(chartDom);
chartInstances.set(lab.id, chart);
// resize
window.addEventListener('resize', debounceResize);
}
//
// const indicators = lab.dimension;
const indicators = lab.dimension.map(index => ({ name: index, max: 50}));
const indicators = lab.dimension.map(name => ({ name: name, max: 50 })); // 50
//
let radarData = lab.result;
let radarData = lab.result; //
console.log('radarData:', indicators,radarData);
chart.setOption({
// tooltip: {
// trigger: 'item', //
// formatter: '{b}: {c}' // b c
// },
radar: {
indicator: indicators,
splitArea: {
@ -297,11 +410,6 @@ const updateAllRadarCharts = () => {
}
]
});
//
window.addEventListener('resize', () => {
chart && chart.resize();
});
});
});
};
@ -315,7 +423,7 @@ const handleSearch = () => {
filteredLabs.value = labs.value;
} else {
filteredLabs.value = labs.value.filter(lab =>
lab.basicInformation.name2 && lab.basicInformation.name2.includes(searchQuery.value) ||
(lab.basicInformation.name2 && lab.basicInformation.name2.includes(searchQuery.value)) ||
(lab.basicInformation.name1 && lab.basicInformation.name1.includes(searchQuery.value))
);
}
@ -328,19 +436,173 @@ const handleSearch = () => {
//
const openLabDetail = async (lab) => {
// const response = await axios.get(`${getApiBaseUrl()}/pg/evaluation-results/get-preview?id=${lab.id}`);
// console.log('responseresponseresponseresponse',response)
//
if (scoreComparisonMode.value) return;
selectedLab.value = lab;
isEditMode.value = true;
drawerVisible.value = true;
};
// --- ---
//
const toggleScoreComparisonMode = () => {
scoreComparisonMode.value = !scoreComparisonMode.value;
if (!scoreComparisonMode.value) {
//
filteredLabs.value.forEach(lab => {
lab.selected = false;
});
selectedLabs.value = [];
}
};
// Lab Card
const handleLabSelection = (lab) => {
// 使 Vue
if (lab.selected) {
selectedLabs.value.push(lab);
} else {
selectedLabs.value = selectedLabs.value.filter(item => item.id !== lab.id);
}
};
//
const scoreComparisonHeaders = computed(() => {
if (selectedLabs.value.length === 0) {
return [];
}
// Lab dimension
const firstLabDimensions = selectedLabs.value[0].dimension;
return firstLabDimensions.map((dim, index) => ({
prop: `score${index + 1}`, // score1, score2
label: dim
}));
});
//
const scoreComparisonTableData = computed(() => {
if (selectedLabs.value.length === 0) {
return [];
}
return selectedLabs.value.map(lab => {
const row = {
category: lab.basicInformation.name2 //
};
// radarData row
lab.result.forEach((score, index) => {
row[`score${index + 1}`] = score;
});
return row;
});
});
//
const columnStats = computed(() => {
const stats = {};
if (scoreComparisonTableData.value.length === 0 || scoreComparisonHeaders.value.length === 0) {
return stats;
}
scoreComparisonHeaders.value.forEach(header => {
const prop = header.prop;
//
const values = scoreComparisonTableData.value.map(row => Number(row[prop]));
if (values.length > 0) {
stats[prop] = {
max: Math.max(...values),
min: Math.min(...values)
};
}
});
// console.log("Calculated columnStats:", stats); //
return stats;
});
//
const tableRowClassName = ({ rowIndex }) => {
if (rowIndex % 2 === 1) {
return 'striped-row'; //
}
return '';
};
//
const openScoreComparisonDialog = () => {
if (selectedLabs.value.length < 2) {
ElMessage.warning('请至少选择两个工程研究中心进行比较!');
return;
}
scoreComparisonDialogVisible.value = true;
};
// filteredLabs
// lab.id ECharts
watch(filteredLabs, () => {
nextTick(() => {
updateAllRadarCharts();
});
});
</script>
<style>
/* common.css 保持不变,用于全局样式 */
@import './common.css';
</style>
<style scoped>
.default-table {
background-color: white; /* 表格背景色为白色 */
color: black; /* 文字颜色为黑色 */
border-radius: 8px;
overflow: hidden;
}
/* 新增:得分比较表格的特定样式 */
.score-comparison-table {
max-height: 60vh; /* 设置最大高度例如视口高度的60% */
overflow-y: auto; /* 当内容超出最大高度时,显示垂直滚动条 */
overflow-x: hidden;
}
.default-table th.el-table__cell {
background-color: #f5f7fa !important; /* 表头背景色浅灰 */
color: black !important;
font-weight: bold;
border-right: 1px solid #ebeef5 !important;
border-bottom: 1px solid #ebeef5 !important;
}
.default-table td.el-table__cell {
background-color: white !important; /* 单元格背景色白色 */
color: black !important;
border-right: 1px solid #ebeef5 !important;
border-bottom: 1px solid #ebeef5 !important;
}
.default-table tr:hover > td {
background-color: #f0f2f5 !important; /* 行悬停背景色更浅的灰色 */
}
/* 条纹样式 */
.default-table.el-table--striped .el-table__body tr.striped-row td {
background-color: #fafafa !important; /* 条纹行背景色,比默认单元格略深 */
}
/* 最大值和最小值高亮 */
.max-score-in-column {
color: red; /* 红色 */
font-weight: bold;
}
.min-score-in-column {
color: blue; /* 蓝色 */
font-weight: bold;
}
.evaluation-page {
position: absolute;
top: 0;
@ -553,21 +815,30 @@ const openLabDetail = async (lab) => {
flex-direction: column;
padding: 0 10px;
overflow: hidden;
max-height: calc(100vh - 100px); /* 限制最大高度 */
max-height: none;
height: auto;
}
/* 搜索和操作栏 */
.action-bar {
height: 64px;
display: flex;
justify-content: space-between;
justify-content: space-between; /* 使得左中右元素分散对齐 */
align-items: center;
}
/* 新增:得分比较按钮组容器,与搜索框分离 */
.score-comparison-wrapper {
display: flex;
gap: 10px; /* 按钮之间的间距 */
margin-right: 20px; /* 与搜索框的间距 */
}
.search-box {
display: flex;
align-items: center;
width: 300px;
width: 300px; /* 调整宽度,不再包含得分比较按钮 */
background-color: rgba(255,255,255,0.1);
border-radius: 20px;
overflow: hidden;
@ -614,26 +885,29 @@ const openLabDetail = async (lab) => {
/* 工程研究中心卡片网格 */
.lab-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
gap: 20px;
overflow-y: auto; /* 允许卡片区域滚动 */
flex: 1;
display: flex; /* 使用 flex 布局 */
flex-wrap: wrap; /* 允许项目换行 */
gap: 20px; /* 项目间的间距 */
padding: 20px;
background-color: #262F50;
border-radius: 10px;
overflow-y: auto;
justify-content: flex-start;
}
.lab-card {
box-sizing: border-box;
flex: 0 0 calc((100% - 40px) / 3);
background-color: #1f3266;
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(73,134,255,0.3);
display: flex;
flex-direction: column;
min-height: 350px; /* 调整最小高度,确保雷达图有足够空间 */
min-height: 350px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
}
.lab-card:hover {
@ -641,6 +915,31 @@ const openLabDetail = async (lab) => {
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
/* 复选框容器样式 */
.card-checkbox-wrapper {
position: absolute;
top: 4px; /* 距离顶部 */
left: 8px; /* 距离左侧 */
z-index: 10; /* 确保在最上层 */
background-color: rgba(0, 0, 0, 0.3); /* 半透明黑色背景 */
border-radius: 4px;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
width: 28px; /* 确保有足够点击区域 */
height: 28px;
}
.card-checkbox-wrapper input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #4986ff; /* 改变复选框颜色 */
margin: 0; /* 移除默认外边距 */
}
.card-header {
display: flex;
justify-content: space-between;
@ -653,6 +952,7 @@ const openLabDetail = async (lab) => {
.lab-id {
font-size: 14px;
color: rgba(255,255,255,0.7);
/* 确保ID不被复选框遮挡如果复选框在左边这里可能需要调整padding-left或margin-left */
}
.total-score {
@ -758,6 +1058,49 @@ const openLabDetail = async (lab) => {
margin-bottom: 5px;
}
/* 得分比较按钮样式 */
.compare-score-btn {
background-color: rgb(13, 70, 192);
border: none;
color: rgba(255,255,255,1);
border-radius: 10px;
padding: 8px 15px;
font-size: 14px;
text-align: center;
font-family: PingFangSC-regular;
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s;
}
.compare-score-btn:hover {
background-color: rgba(14,62,167,0.8);
}
.compare-score-btn.close-btn {
background-color: #f44336; /* 红色 */
border-color: #f44336;
}
.compare-score-btn.close-btn:hover {
background-color: #d32f2f;
}
.compare-score-btn.start-compare-btn {
background-color: #4CAF50; /* 绿色 */
border-color: #4CAF50;
}
.compare-score-btn.start-compare-btn:hover {
background-color: #388E3C;
}
.compare-score-btn.disabled-btn {
background-color: #616161; /* 灰色 */
border-color: #616161;
cursor: not-allowed;
opacity: 0.7;
}
@media (max-width: 1200px) {
.lab-card-grid {
grid-template-columns: 1fr;
@ -783,4 +1126,10 @@ const openLabDetail = async (lab) => {
width: 100%;
}
}
.searchbox{
display: flex;
}
.evaluation-info{
padding: 20px 15px;
}
</style>

View File

@ -15,6 +15,10 @@
<span class="label">编号:</span>
<span class="display-text">{{ labData.basicInformation.name1 }}</span>
</div>
<div class="form-item">
<span class="label">年份:</span>
<span class="display-text">{{ labData.basicInformation.name0 }}</span>
</div>
<div class="form-item">
<span class="label">中心名称:</span>
<span class="display-text">{{ labData.basicInformation.name2 }}</span>