dashboard/src/components/TalentDrawerDetail.vue

1036 lines
26 KiB
Vue
Raw Normal View History

2025-06-09 14:59:40 +08:00
<template>
<el-drawer
v-model="drawerVisible"
:title="drawerTitle"
direction="rtl"
size="900px"
:before-close="handleClose"
custom-class="talent-drawer"
>
<div class="drawer-content">
<!-- 标签导航 -->
<div class="tab-navigation">
<div
v-for="(tab, index) in tabs"
:key="index"
:class="['tab-item', { active: activeTab === tab.value }]"
@click="activeTab = tab.value"
>
{{ tab.label }}
</div>
</div>
<!-- URL输入标签页 -->
<div v-if="activeTab === 'url'" class="tab-content">
<div class="url-input-container">
<el-input
v-model="urlInput"
placeholder="请输入URL"
class="url-input"
/>
<el-button
type="primary"
@click="fetchDataFromUrl"
class="send-button primary-btn"
>
发送
</el-button>
</div>
</div>
<!-- 上传文档标签页 -->
<div v-else-if="activeTab === 'upload'" class="tab-content">
<el-upload
class="upload-container"
action="#"
:auto-upload="false"
:on-change="handleFileChange"
>
<el-button type="primary" class="primary-btn" style="display: inline-block;">选择文件</el-button>
<div class="el-upload__tip" style="display: inline-block;margin-left: 30px;">请上传文档支持PDFDOCDOCX格式</div>
</el-upload>
</div>
<!-- 手动输入标签页 -->
<div v-else class="tab-content">
<div class="manual-input-tip">请手动填写以下信息</div>
</div>
<!-- 个人信息表单 - 仅在数据加载后显示 -->
<div v-if="formData.name || activeTab == 'manual'" class="personal-info-form">
<div class="form-header">
<div class="photo-section">
<div class="teacher-photo">
<img :src="formData.photo || defaultPhoto" alt="教师照片" />
</div>
</div>
<div class="basic-info">
<div class="form-row">
<div class="form-item">
<span class="label">编号:</span>
<el-input v-model="formData.idcode" placeholder="请输入编号" />
</div>
<div class="form-item">
<span class="label">姓名:</span>
<el-input v-model="formData.name" placeholder="请输入姓名" />
</div>
<div class="form-item">
<span class="label">性别:</span>
<el-input v-model="formData.gender" placeholder="请输入性别" />
</div>
</div>
<div class="form-row">
<div class="form-item">
<span class="label">出生年月:</span>
<el-input v-model="formData.birthDate" placeholder="出生年月" />
</div>
<div class="form-item">
<span class="label">职称:</span>
<el-input v-model="formData.title" placeholder="请输入职称" />
</div>
<div class="form-item">
<span class="label">职务:</span>
<el-input v-model="formData.position" placeholder="请输入职务" />
</div>
</div>
<div class="form-row">
<div class="form-item">
<span class="label">最高学历:</span>
<el-input v-model="formData.education" placeholder="请输入最高学历" />
</div>
<div class="form-item">
<span class="label">教育背景:</span>
<el-input v-model="formData.educationBackground" placeholder="请输入教育背景" />
</div>
</div>
<div class="form-row">
<div class="form-item">
<span class="label">通讯地址:</span>
<el-input v-model="formData.address" placeholder="请输入通讯地址" />
</div>
<div class="form-item">
<span class="label">学科方向:</span>
<el-input v-model="formData.academicDirection" placeholder="请输入学科方向" />
</div>
</div>
<div class="form-row">
<div class="form-item">
<span class="label">人才计划:</span>
<el-input v-model="formData.talentPlan" placeholder="请输入人才计划" />
</div>
<div class="form-item">
<span class="label">办公地点:</span>
<el-input v-model="formData.officeLocation" placeholder="请输入办公地点" />
</div>
</div>
<div class="form-row">
<div class="form-item">
<span class="label">电子邮箱:</span>
<el-input v-model="formData.email" placeholder="请输入电子邮箱" />
</div>
<div class="form-item">
<span class="label">联系方式:</span>
<el-input v-model="formData.phone" placeholder="请输入联系方式" />
</div>
<div class="form-item">
<span class="label">导师类型:</span>
<el-input v-model="formData.tutorType" placeholder="请输入导师类型" />
</div>
</div>
<div class="form-row">
<div class="form-item">
<span class="label">论文:</span>
<el-input v-model="formData.papers" placeholder="请输入论文数量" />
</div>
<div class="form-item">
<span class="label">项目:</span>
<el-input v-model="formData.projects" placeholder="请输入项目数量" />
</div>
</div>
</div>
</div>
<!-- 详细信息部分 -->
<div class="detail-sections">
<h3 class="section-title">教育与工作经历</h3>
<div class="section-content">
<el-input
type="textarea"
v-model="formData.eduWorkHistory"
:rows="4"
placeholder="请输入教育与工作经历"
/>
</div>
<h3 class="section-title">研究方向</h3>
<div class="section-content">
<el-input
type="textarea"
v-model="formData.researchDirection"
:rows="4"
placeholder="请输入研究方向"
/>
</div>
<h3 class="section-title">近五年承担的科研项目</h3>
<div class="section-content">
<el-input
type="textarea"
v-model="formData.recentProjects"
:rows="4"
placeholder="请输入近五年承担的科研项目"
/>
</div>
<h3 class="section-title">代表性学术论文</h3>
<div class="section-content">
<el-input
type="textarea"
v-model="formData.representativePapers"
:rows="4"
placeholder="请输入代表性学术论文"
/>
</div>
<h3 class="section-title">授权国家发明专利及出版专著</h3>
<div class="section-content">
<el-input
type="textarea"
v-model="formData.patents"
:rows="4"
placeholder="请输入授权国家发明专利及出版专著"
/>
</div>
</div>
<!-- 雷达图 -->
<div class="evaluation-chart-section">
<div id="evaluation-radar-chart" class="radar-chart"></div>
</div>
</div>
</div>
<template #footer>
<div class="drawer-footer">
<button class="drawer-btn cancel-btn" @click="handleClose">取消</button>
<button v-if="props.isEdit" class="drawer-btn delete-btn" @click="handleDelete">删除</button>
<button class="drawer-btn confirm-btn" @click="handleSave">确定</button>
<!-- <button class="drawer-btn confirm-btn" >确定</button>-->
</div>
</template>
</el-drawer>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts/core';
import { RadarChart } from 'echarts/charts';
import {
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import { ElMessage, ElLoading, ElMessageBox } from 'element-plus';
import { getApiBaseUrl } from '../config';
import axios from 'axios';
// 注册必要的echarts组件
echarts.use([
RadarChart,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
CanvasRenderer
]);
const props = defineProps({
visible: {
type: Boolean,
default: false
},
isEdit: {
type: Boolean,
default: false
},
dimensions: {
type: Array,
default: () => []
},
teacherData: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['update:visible', 'save']);
// 用于visible属性的双向绑定
const drawerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
});
// 默认照片占位符
const defaultPhoto = `/image/人1.png`;
// 抽屉标题的计算属性
const drawerTitle = computed(() => props.isEdit ? '详情' : '新增评估');
// 标签页配置
const tabs = [
{ label: 'URL输入', value: 'url' },
{ label: '上传文档', value: 'upload' },
{ label: '手动录入', value: 'manual' }
];
// 激活的标签页
const activeTab = ref('url');
// URL输入
const urlInput = ref('');
// 数据是否已加载标志
const dataLoaded = ref(false);
// 选择的上传文件
const selectedFile = ref(null);
// 表单数据
const formData = reactive({
id: '',
idcode: '',
name: '',
gender: '',
birthDate: '',
title: '',
position: '',
education: '',
educationBackground: '',
address: '',
academicDirection: '',
talentPlan: '',
officeLocation: '',
email: '',
phone: '',
tutorType: '',
papers: '',
projects: '',
photo: '',
eduWorkHistory: '',
researchDirection: '',
recentProjects: '',
representativePapers: '',
patents: '',
evaluationData: [60, 60, 60, 60, 60, 60] // 默认评估数据
});
// 雷达图实例
let chart = null;
// 从props初始化表单数据
watch(() => props.teacherData, (newValue) => {
if (newValue && Object.keys(newValue).length > 0) {
Object.assign(formData, newValue);
if (props.isEdit) {
dataLoaded.value = true;
}
}
}, { immediate: true, deep: true });
// 监听抽屉可见性变化
watch(() => props.visible, (isVisible) => {
if (isVisible) {
if (props.isEdit && dataLoaded.value) {
// 编辑模式:当抽屉变为可见时,初始化或更新图表
nextTick(() => {
initRadarChart();
});
} else if (!props.isEdit) {
// 新增模式:清空表单数据
Object.assign(formData, {
id: '', // ID会在保存时生成
idcode: '', // 清空编号
name: '',
gender: '',
birthDate: '',
title: '',
position: '',
education: '',
educationBackground: '',
address: '',
academicDirection: '',
talentPlan: '',
officeLocation: '',
email: '',
phone: '',
tutorType: '',
papers: '',
projects: '',
photo: '',
eduWorkHistory: '',
researchDirection: '',
recentProjects: '',
representativePapers: '',
patents: '',
evaluationData: [60, 60, 60, 60, 60, 60] // 默认评估数据
});
dataLoaded.value = true;
nextTick(() => {
initRadarChart();
});
}
}
});
// 组件挂载时初始化
onMounted(() => {
if (props.visible && dataLoaded.value) {
initRadarChart();
}
});
// 初始化雷达图
const initRadarChart = () => {
const chartDom = document.getElementById('evaluation-radar-chart');
if (!chartDom) return;
// 清除任何现有图表
if (chart) {
chart.dispose();
}
chart = echarts.init(chartDom);
// 从dimensions生成指标
const indicators = props.dimensions.map(dim => ({
name: dim.name,
max: 100
})) || [
{ name: '工作经历', max: 100 },
{ name: '研究方向', max: 100 },
{ name: '科研项目', max: 100 },
{ name: '学术论文', max: 100 },
{ name: '专利专著', max: 100 },
{ name: '学术影响', max: 100 }
];
// 确保评估数据的长度与维度数量匹配
let evaluationData = formData.evaluationData || [];
if (evaluationData.length !== indicators.length) {
// 如果维度数量变化,要调整评估数据
evaluationData = indicators.map((_, i) => {
// 如果有原始数据就用原始数据否则设为默认值60
return evaluationData[i] || 60;
});
// 更新表单的评估数据
formData.evaluationData = evaluationData;
}
chart.setOption({
radar: {
indicator: indicators,
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', fontSize: 10 } },
radius: '70%'
},
series: [
{
type: 'radar',
data: [
{
value: evaluationData,
name: '评估结果',
areaStyle: { opacity: 0 },
lineStyle: { color: 'rgb(63, 196, 15)', width: 2 },
itemStyle: { color: 'rgb(63, 196, 15)' }
}
]
}
]
});
// 添加窗口大小变化事件监听器
window.addEventListener('resize', () => {
chart && chart.resize();
});
};
// 处理文件变更
const handleFileChange = async (file) => {
selectedFile.value = file;
// 检查文件类型
if (!file.name.endsWith('.pdf') && !file.name.endsWith('.doc') && !file.name.endsWith('.docx')) {
ElMessage.warning('请上传PDF、DOC或DOCX格式的文件');
return;
}
try {
// 显示加载状态
const loadingIndicator = ElLoading.service({
lock: true,
text: '正在解析文档...',
background: 'rgba(0, 0, 0, 0.7)'
});
// 创建FormData对象
const uploadFormData = new FormData();
uploadFormData.append('file', file.raw);
// 从localStorage获取JWT令牌
const token = localStorage.getItem('token');
if (!token) {
ElMessage.error('未登录或登录已过期,请重新登录');
loadingIndicator.close();
return;
}
// 调用后端API上传文档
const response = await axios.post(`${getApiBaseUrl()}/api/upload-talent-document`, uploadFormData, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'multipart/form-data'
}
});
// 关闭加载指示器
loadingIndicator.close();
if (response.data.success) {
// 使用响应数据更新表单
Object.assign(formData, response.data.data);
// 设置数据已加载标志
dataLoaded.value = true;
// 更新图表
nextTick(() => {
initRadarChart();
});
ElMessage.success('文档解析成功');
} else {
throw new Error(response.data.message || '文档解析失败');
}
} catch (error) {
ElMessage.error('文档解析失败: ' + (error.response?.data?.detail || error.message));
console.error('Error uploading document:', error);
}
};
// 从URL获取数据
const fetchDataFromUrl = async () => {
if (!urlInput.value) {
ElMessage.warning('请输入有效的URL');
return;
}
try {
// 显示加载状态
const loadingIndicator = ElLoading.service({
lock: true,
text: '正在获取数据...',
background: 'rgba(0, 0, 0, 0.7)'
});
// 调用后端API进行URL爬取
const response = await fetch(`${getApiBaseUrl()}/api/scrape-url`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: urlInput.value })
});
// 获取响应数据
const data = await response.json();
// 关闭加载指示器
loadingIndicator.close();
if (!response.ok) {
throw new Error(data.error || '网络请求失败');
}
// 检查数据是否有效
if (!data || Object.keys(data).length === 0) {
ElMessage.warning('未能从提供的URL中提取有效的数据');
return;
}
// 使用响应更新表单数据
Object.assign(formData, data);
// 设置数据已加载标志
dataLoaded.value = true;
// 更新图表
nextTick(() => {
initRadarChart();
});
ElMessage.success('数据获取成功');
} catch (error) {
ElMessage.error('获取数据失败: ' + error.message);
console.error('Error fetching data:', error);
}
};
// 处理关闭
const handleClose = () => {
drawerVisible.value = false;
// 重置数据加载状态
if (!props.isEdit) {
dataLoaded.value = false;
}
};
// 处理保存
const handleSave = async () => {
try {
// 如果是新增评估不需要传id字段
if (!props.isEdit) {
// 在新增模式下删除id字段因为后端通过是否有id字段来判断是新增还是修改
delete formData.id;
// 确保用户已经输入idcode
if (!formData.idcode) {
ElMessage.warning('请输入编号');
return;
}
}
// 准备请求体数据
const payload = {
data_type: "talent", // 指定为人才评估类型
data: { ...formData }
};
// 从localStorage获取JWT令牌
const token = localStorage.getItem('token');
console.log('当前token:', token ? token.substring(0, 20) + '...' : '无token');
if (!token) {
ElMessage.error('未登录或登录已过期,请重新登录');
return;
}
// 先测试token是否有效
try {
const testResponse = await axios.get(`${getApiBaseUrl()}/users/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
console.log('Token验证成功用户信息:', testResponse.data);
} catch (tokenError) {
console.error('Token验证失败:', tokenError);
ElMessage.error('登录已过期,请重新登录');
// 清除无效token
localStorage.removeItem('token');
localStorage.removeItem('username');
localStorage.removeItem('isLoggedIn');
return;
}
console.log('发送保存请求payload:', payload);
// 发送POST请求到后端API添加Authorization头
const response = await axios.post(`${getApiBaseUrl()}/api/save-data`, payload, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('保存响应:', response.data);
if (response.data.success) {
// 如果是新增操作后端会返回生成的id需要更新表单数据的id
if (response.data.id && !props.isEdit) {
formData.id = response.data.id;
}
// 通知父组件刷新数据并传递保存的数据
emit('save', { ...formData });
// 保存成功后关闭抽屉
drawerVisible.value = false;
// 显示成功消息
ElMessage.success(props.isEdit ? '修改成功' : '新增成功');
} else {
throw new Error(response.data.message || '保存失败');
}
} catch (error) {
console.error('保存评估数据失败:', error);
console.error('错误详情:', error.response?.data);
ElMessage.error(`保存评估数据失败: ${error.response?.data?.detail || error.message}`);
}
};
// 处理删除
const handleDelete = async () => {
if (!props.isEdit || !formData.id) {
ElMessage.warning('无法删除:缺少必要信息');
return;
}
try {
// 显示确认对话框
await ElMessageBox.confirm(
`确定要删除教师 "${formData.name}" 的评估数据吗?此操作不可恢复。`,
'确认删除',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'el-button--danger'
}
);
// 从localStorage获取JWT令牌
const token = localStorage.getItem('token');
if (!token) {
ElMessage.error('未登录或登录已过期,请重新登录');
return;
}
// 调用删除API
const response = await axios.delete(`${getApiBaseUrl()}/talents/${formData.id}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 200) {
ElMessage.success('删除成功');
// 通知父组件删除成功,传递被删除的数据
emit('save', { ...formData, _deleted: true });
// 关闭抽屉
drawerVisible.value = false;
} else {
throw new Error('删除失败');
}
} catch (error) {
if (error.message === 'cancel') {
// 用户取消删除
return;
}
console.error('删除失败:', error);
ElMessage.error(`删除失败: ${error.response?.data?.detail || error.message}`);
}
};
</script>
<style>
@import './common.css';
.el-drawer{
border-left: 1px solid #4986ff;
}
.el-drawer__body{
padding: 0px !important;
}
.el-drawer__header{
background-color: #0c1633 !important;
margin-bottom:0px !important;
}
.el-drawer__footer{
background-color: #0c1633 !important;
}
</style>
<style scoped>
.talent-drawer :deep(.el-drawer__header) {
margin-bottom: 0;
color: white;
background-color: #0c1633;
border-bottom: 1px solid rgba(73,134,255,0.3);
}
.talent-drawer :deep(.el-drawer__body) {
padding: 0;
overflow: hidden;
background-color: #0c1633;
}
.drawer-content {
padding: 20px;
color: white;
height: 100%;
overflow-y: auto;
background-color: #0c1633;
}
/* 标签导航 */
.tab-navigation {
display: flex;
border-bottom: 1px solid rgba(73,134,255,0.3);
margin-bottom: 20px;
}
.tab-item {
padding: 10px 20px;
cursor: pointer;
font-size: 16px;
color: rgba(255,255,255,0.7);
position: relative;
}
.tab-item.active {
color: #4986ff;
font-weight: bold;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 2px;
background-color: #4986ff;
}
/* URL输入部分 */
.url-input-container {
display: flex;
margin-bottom: 20px;
}
.url-input {
flex: 1;
margin-right: 10px;
}
/* 上传部分 */
.upload-container {
margin-bottom: 20px;
width: 100%;
display: block;
}
.upload-container :deep(.el-upload) {
width: 100%;
display: block;
}
.upload-container :deep(.el-upload-dragger) {
width: 100%;
}
.el-upload__tip {
color: rgba(255,255,255,0.7);
margin-top: 5px;
}
/* 主要按钮 - 统一按钮样式 */
.primary-btn {
background-color: rgba(14,62,167,1) !important;
color: rgba(255,255,255,1) !important;
border: 1px solid rgba(73,134,255,1) !important;
border-radius: 10px !important;
padding: 8px 15px !important;
}
/* 手动输入提示 */
.manual-input-tip {
margin-bottom: 20px;
color: rgba(255,255,255,0.7);
}
/* 个人信息表单 */
.form-header {
display: flex;
margin-bottom: 30px;
background-color: #1f3266;
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(73,134,255,0.3);
}
.photo-section {
margin-right: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.basic-info {
flex: 1;
}
.form-row {
display: flex;
margin-bottom: 15px;
}
.form-item {
flex: 1;
margin-right: 15px;
display: flex;
flex-direction: column;
}
.form-item:last-child {
margin-right: 0;
}
.form-item.full-width {
flex: 3;
}
.label {
display: block;
margin-bottom: 8px;
color: rgba(255,255,255,0.7);
}
/* 详细信息部分 */
.detail-sections {
margin-bottom: 30px;
background-color: #1f3266;
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(73,134,255,0.3);
}
.section-title {
margin: 15px 0 10px;
font-size: 16px;
color: rgba(255,255,255,0.9);
}
.section-content {
margin-bottom: 20px;
}
/* 雷达图部分 */
.evaluation-chart-section {
background-color: #1f3266;
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(73,134,255,0.3);
}
.radar-chart {
width: 100%;
height: 300px; /* 明确指定雷达图高度 */
min-height: 300px; /* 确保最小高度 */
}
/* 抽屉页脚 */
.talent-drawer :deep(.el-drawer__footer) {
border-top: 1px solid rgba(73,134,255,0.3);
padding: 10px 20px;
background-color: #0c1633;
}
.drawer-footer {
display: flex;
justify-content: flex-end;
}
/* 按钮样式 - 现在与TalentDetail中的按钮一致 */
.drawer-btn {
padding: 8px 15px;
border-radius: 10px;
font-size: 14px;
font-family: PingFangSC-regular;
cursor: pointer;
margin-left: 10px;
}
.cancel-btn {
background-color: transparent;
color: rgba(255,255,255,0.8);
border: 1px solid rgba(73,134,255,0.5);
}
.delete-btn {
background-color: #f56c6c;
color: white;
border: 1px solid #f56c6c;
}
.delete-btn:hover {
background-color: #f78989;
border-color: #f78989;
}
.confirm-btn {
background-color: rgba(14,62,167,1);
color: rgba(255,255,255,1);
border: 1px solid rgba(73,134,255,1);
}
/* Element Plus组件的深色主题覆盖样式 */
:deep(.el-input__wrapper),
:deep(.el-textarea__wrapper) {
background-color: rgba(255,255,255,0.1);
box-shadow: 0 0 0 1px rgba(73,134,255,0.3) inset;
}
:deep(.el-input__inner),
:deep(.el-textarea__inner) {
background-color: transparent;
color: white;
}
:deep(.el-input__inner::placeholder),
:deep(.el-textarea__inner::placeholder) {
color: rgba(255,255,255,0.5);
}
:deep(.el-select .el-input__wrapper) {
background-color: rgba(255,255,255,0.1);
box-shadow: 0 0 0 1px rgba(73,134,255,0.3) inset;
}
:deep(.el-select-dropdown__item) {
color: #606266;
}
:deep(.el-date-editor) {
background-color: rgba(255,255,255,0.1);
border-color: rgba(73,134,255,0.3);
color: white;
}
:deep(.el-upload),
:deep(.el-upload-dragger) {
background-color: rgba(255,255,255,0.1);
border-color: rgba(73,134,255,0.3);
}
/* 滚动条样式 */
.drawer-content::-webkit-scrollbar {
width: 6px;
}
.drawer-content::-webkit-scrollbar-track {
background: transparent;
}
.drawer-content::-webkit-scrollbar-thumb {
background-color: #4986ff;
border-radius: 10px;
border: none;
}
</style>