This commit is contained in:
Leon 2025-06-09 14:59:40 +08:00
commit 605a3041e1
82 changed files with 19756 additions and 0 deletions

17
.cursor/rules/myrule.mdc Normal file
View File

@ -0,0 +1,17 @@
---
description:
globs:
alwaysApply: true
---
1.无论我输入什么语言,你都使用中文回答我
2.第一次对话你应该先查看目录,了解项目情况
3.每次对话结束你都应该礼貌的说,尊敬的李昂先生,以上就是我能为你提供的全部内容如果有其他需要请继续找我
4.项目根目录下的PYTHON_ENV_SETUP_README.md为python环境启动文档 如果你需要调用python环境 参照文档方式通过conda启动
5.后端使用python fastapi sqlite 存储在根目录的backend文件夹下
6.每次更改代码都要看看是否有需要写入readme.md的内容如果有写入其中
7.每次都要阅读readme.md文件
8.我的电脑是windows的 无法使用&& 这个命令 例如cd backend && uvicorn main:app --reload 这样的语句是无法运行的 需要拆分运行
9.我的环境使用了conda 需要使用conda activate fast-dashboard-env 进入对应环境
10.根目录下backend内为后端代码
11. 我的电脑使用的windows系统 如果需要调用windows系统功能执行命令 需要执行windows下有的命令

11
.dockerignore Normal file
View File

@ -0,0 +1,11 @@
node_modules
npm-debug.log
yarn-debug.log
yarn-error.log
dist
.git
.github
.vscode
.DS_Store
*.md
!README.md

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
# 第一阶段:构建应用
FROM node:20 as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 第二阶段:运行应用
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 48100
CMD ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,78 @@
# Python 环境配置指南
## 前提条件
- 已安装 CondaAnaconda 或 Miniconda
- Windows 操作系统
## 自动配置方法
1. 双击项目根目录中的 `setup_py_env.bat` 文件
2. 等待脚本完成环境配置和依赖安装
3. 环境配置完成后,窗口会显示使用说明
## 手动配置方法
如果自动配置方法不起作用,请按照以下步骤手动配置:
1. 打开命令提示符CMD或 PowerShell
2. 创建新的 Conda 环境:
```
conda create -n fast-dashboard-env python=3.11 -y
```
3. 激活环境:
```
conda activate fast-dashboard-env
```
4. 安装项目依赖:
```
cd 项目根目录
pip install -r backend/requirements.txt
```
## 运行后端服务器
1. 激活 Conda 环境(如果尚未激活):
```
conda activate fast-dashboard-env
```
2. 进入后端目录:
```
cd backend
```
3. 启动服务器:
```
uvicorn main:app --reload
```
4. 服务器将在 http://127.0.0.1:8000 启动
## 环境依赖列表
本项目使用以下 Python 库:
- fastapi==0.109.2
- uvicorn==0.27.1
- pydantic==2.6.1
- python-multipart==0.0.9
- python-jose==3.3.0
- passlib==1.7.4
- bcrypt==4.1.2
- requests==2.31.0
- beautifulsoup4==4.12.2
- sqlalchemy==2.0.26
- aiosqlite==0.19.0
- alembic==1.13.1
## 常见问题
1. **无法激活 Conda 环境**
- 确保已正确安装 Conda
- 尝试重新打开终端
- 运行 `conda init powershell`(如果使用 PowerShell
2. **依赖安装失败**
- 检查网络连接
- 尝试使用镜像源:`pip install -r backend/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple`
3. **启动服务器时出错**
- 确保所有依赖都已正确安装
- 检查 backend 目录下是否有 data 文件夹,如没有则创建

155
README.md Normal file
View File

@ -0,0 +1,155 @@
# Fast Dashboard 项目
## 项目概述
智能评估仪表板系统,包含人才评估和工程研究中心评估功能。
## 技术栈
- 前端Vue 3 + Element Plus + ECharts
- 后端Python FastAPI + SQLite
- 部署Docker + Nginx
## 近期问题记录
### 2024年 - 爬虫图片URL路径问题
**问题描述:**
- 爬虫返回的图片URL多了一个 `/szdw/` 路径
- 正确地址应该是:`https://ac.bit.edu.cn/images/2024-02/f5a28fd026884e328c2280ef42566f6b.png`
- 错误地址返回:`https://ac.bit.edu.cn/szdw/images/2024-02/f5a28fd026884e328c2280ef42566f6b.png`
**问题原因:**
- 在处理相对路径 `../../../`原有逻辑从当前URL推断基础路径不准确
- 导致多拼接了 `/szdw/` 路径段
**解决方案:**
1. **修改图片URL处理逻辑** (`backend/main.py` 第610-625行)
- 简化相对路径处理,直接移除 `../../../` 前缀
- 统一使用基础URL (`https://ac.bit.edu.cn`) 拼接图片路径
- 增加对 `../../``../` 路径的处理
**修复后的代码逻辑:**
```python
if img_src.startswith('../../../'):
img_relative = img_src[9:] # 移除 '../../../'
img_url = f"{base_url}/{img_relative}"
elif img_src.startswith('../../'):
img_relative = img_src[6:] # 移除 '../../'
img_url = f"{base_url}/{img_relative}"
elif img_src.startswith('../'):
img_relative = img_src[3:] # 移除 '../'
img_url = f"{base_url}/{img_relative}"
```
### 2024年 - 人才评估接口网络连接问题
**问题描述:**
- 人才评估调用接口 `/api/scrape-url`
- 本地测试正常服务器部署后报错500
- 错误信息:`HTTPSConnectionPool(host='ac.bit.edu.cn', port=443): Read timed out. (read timeout=30)`
**问题原因:**
1. **服务器网络环境限制**:服务器无法正常访问外部网站 ac.bit.edu.cn
2. **网络延迟问题**服务器到目标网站的网络延迟较高30秒超时不够
3. **DNS解析问题**可能存在DNS配置问题
4. **防火墙限制**:服务器出站规则可能阻止了对目标域名的访问
**解决方案:**
1. **创建了改进版本的URL抓取函数** (`backend/scrape_url_improved.py`)
- 增加DNS解析检查
- 实现重试机制最多3次重试
- 延长超时时间连接30秒读取90秒
- 添加详细的错误日志
- 临时禁用SSL验证避免证书问题
2. **创建了网络诊断工具** (`backend/network_diagnostic.py`)
- DNS解析测试
- TCP连接测试
- HTTP请求测试
- 系统网络配置检查
- 自动生成解决建议
**使用方法:**
```bash
# 进入后端目录
cd backend
# 激活conda环境
conda activate fast-dashboard-env
# 运行网络诊断工具
python network_diagnostic.py
# 或者在main.py中集成改进版本的抓取函数
```
**建议的部署检查项:**
1. 检查服务器DNS配置
2. 确认防火墙出站规则
3. 测试到目标网站的网络连通性
4. 考虑配置代理服务器
5. 检查云服务器安全组设置
## 环境配置
### Python环境
参考 `PYTHON_ENV_SETUP_README.md` 文档进行环境配置。
### 启动项目
#### 本地开发启动(仅本机访问)
```bash
# 后端启动
cd backend
conda activate fast-dashboard-env
uvicorn main:app --reload
# 前端启动
npm run dev
```
#### IP网络访问启动局域网内其他设备可访问
**方法1使用便捷启动脚本**
```bash
# 双击运行根目录下的 start_with_ip.bat 文件
# 或在命令行中运行:
start_with_ip.bat
```
**方法2手动启动**
```bash
# 后端启动 (监听所有IP)
cd backend
conda activate fast-dashboard-env
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
# 前端启动 (监听所有IP)
npm run dev -- --host 0.0.0.0
```
#### 访问地址
- **本地访问**: http://localhost:5173 (前端) | http://localhost:8000 (后端)
- **网络访问**: http://您的IP:5173 (前端) | http://您的IP:8000 (后端)
- **当前您的IP**: http://192.168.18.108:5173 (前端) | http://192.168.18.108:8000 (后端)
#### 防火墙配置
如果其他设备无法访问请确保Windows防火墙允许以下端口
- 端口 5173 (前端)
- 端口 8000 (后端)
## 文件结构
```
fast-dashboard/
├── backend/ # 后端代码
│ ├── main.py # 主应用文件
│ ├── scrape_url_improved.py # 改进版URL抓取
│ ├── network_diagnostic.py # 网络诊断工具
│ └── ...
├── src/ # 前端代码
│ ├── components/ # Vue组件
│ └── ...
├── README.md # 项目文档
└── PYTHON_ENV_SETUP_README.md # Python环境配置文档
```

29
backend/Dockerfile Normal file
View File

@ -0,0 +1,29 @@
FROM python:3.10-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*
# 复制并安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 创建数据目录并设置权限
RUN mkdir -p data && chmod 777 data
RUN mkdir -p static/images && chmod 777 static/images
# 初始化数据库
RUN python init_db.py
# 暴露端口
EXPOSE 48996
# 启动应用
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "48996"]

View File

@ -0,0 +1,292 @@
# 工程研究中心数据更新指南
## 概述
本文档详细说明如何将 `src/assets/实验室.json` 中的工程研究中心数据导入到数据库中。当有新的工程研究中心数据或者数据需要更新时,请按照本指南进行操作。
## 文件说明
### 主要文件
- **数据源文件**: `src/assets/实验室.json` - 包含所有工程研究中心的多年度数据
- **导入脚本**: `backend/import_lab_data_full.py` - 完整的数据导入脚本
- **JSON修复脚本**: `backend/fix_json_format.py` - 修复JSON格式问题
- **数据类型修复脚本**: `backend/fix_year_data_types.py` - 修复年份字段类型问题
- **数据检查脚本**: `backend/check_specific_lab.py` - 检查特定工程研究中心数据
- **数据库模型**: `backend/models.py` - 定义Lab表结构
- **本文档**: `backend/LAB_DATA_UPDATE_GUIDE.md` - 操作指南
### 数据结构
JSON文件包含工程研究中心数组每个工程研究中心包含
```json
{
"中心名称": "工程研究中心名称",
"中心编号": "工程研究中心编号",
"年度数据": [
{
"归属年份": "2024",
"所属学校": "学校名称",
"主管部门": "部门名称",
... // 详细的年度数据
}
]
}
```
## 数据库字段映射
### 基本信息字段
| JSON字段 | 数据库字段 | 类型 | 说明 |
|---------|-----------|------|------|
| 中心名称 | name | String | 工程研究中心名称 |
| 中心编号 | center_number | String | 工程研究中心编号 |
| 所属学校 | school | String | 所属学校 |
| 主管部门 | department | String | 主管部门 |
| 所属领域 | field | String | 所属领域 |
| 归属年份 | current_year | String | 当前评估年份 |
### 详细信息字段
| JSON字段 | 数据库字段 | 类型 | 说明 |
|---------|-----------|------|------|
| 技术攻关与创新情况 | innovation_situation | Text | 技术创新描述 |
| 1.总体情况 | overall_situation | Text | 总体情况描述 |
| 2.工程化案例 | engineering_cases | Text | 工程化案例 |
| 3.行业服务情况 | industry_service | Text | 行业服务情况 |
| 1.学科发展支撑情况 | discipline_support | Text | 学科发展支撑 |
| 2.人才培养情况 | talent_cultivation | Text | 人才培养情况 |
| 3.研究队伍建设情况 | team_building | Text | 队伍建设情况 |
### 统计数据字段
| JSON字段 | 数据库字段 | 类型 | 说明 |
|---------|-----------|------|------|
| 国家级科技奖励一等奖(项) | national_awards_first | Integer | 国家一等奖数量 |
| 国家级科技奖励二等奖(项) | national_awards_second | Integer | 国家二等奖数量 |
| 省、部级科技奖励一等奖(项) | provincial_awards_first | Integer | 省部一等奖数量 |
| 省、部级科技奖励二等奖(项) | provincial_awards_second | Integer | 省部二等奖数量 |
| 有效专利(项) | valid_patents | Integer | 有效专利数量 |
| 在读博士生 | doctoral_students | Integer | 博士生数量 |
| 在读硕士生 | master_students | Integer | 硕士生数量 |
| 固定人员(人) | fixed_personnel | Integer | 固定人员数量 |
| 流动人员(人) | mobile_personnel | Integer | 流动人员数量 |
| 当年项目到账总经费(万元) | total_funding | Float | 总经费 |
### 特殊字段
| 字段 | 说明 |
|-----|------|
| annual_data | JSON格式存储所有年度数据包含多年完整信息 |
| id | 自动生成的UUID作为主键 |
| idcode | 使用中心编号作为显示ID |
## 操作步骤
### 1. 准备工作
确保以下条件满足:
- [x] 后端服务已停止运行
- [x] 数据库文件 `backend/data/app.db` 存在
- [x] Python环境已激活 (`conda activate fast-dashboard-env`)
- [x] JSON数据文件 `src/assets/实验室.json` 已更新
### 2. 环境准备
在命令行中执行:
```powershell
# 激活conda环境
conda activate fast-dashboard-env
# 进入后端目录
cd backend
```
### 3. 执行数据导入
运行导入脚本:
```powershell
python import_lab_data_full.py
```
### 4. 导入过程说明
脚本会执行以下操作:
1. **数据验证**检查JSON文件是否存在
2. **数据读取**解析JSON文件内容
3. **数据处理**
- 遍历每个工程研究中心
- 检查是否已存在(根据中心编号或名称)
- 如果存在则更新,否则创建新记录
- 处理多年度数据,提取最新年份作为当前数据
- 安全转换数据类型(整数、浮点数、字符串)
4. **数据库操作**
- 添加新记录或更新现有记录
- 提交事务
- 显示统计信息
### 5. 输出信息解读
脚本运行时会显示:
- 📖 正在读取数据文件
- ✅ 成功读取数据,共 X 个工程研究中心
- 🔄 正在处理工程研究中心: XXX (编号: XXX)
- 创建新工程研究中心 / 📝 工程研究中心已存在,更新数据
- ✅ 年度数据: X 年, 最新年份: XXXX
- 💾 正在保存到数据库
- 📊 统计信息
### 6. 验证导入结果
导入完成后,可以通过以下方式验证:
1. **启动后端服务**
```powershell
uvicorn main:app --reload
```
2. **访问API接口**
```
GET http://localhost:8000/labs/
```
3. **查看前端页面**
打开前端应用,查看工程研究中心列表和详情页
## 常见问题及解决方案
### 1. JSON格式错误
**问题**: JSON文件中包含Python的`None`值,导致解析失败
**解决方案**:
```bash
python fix_json_format.py
```
### 2. 年份字段类型错误
**问题**: 前端报错 `TypeError: b.year.localeCompare is not a function`
**原因**: 年度数据中的`归属年份`字段是数字类型,前端期望字符串类型
**解决方案**:
```bash
python fix_year_data_types.py
```
### 3. 检查特定工程研究中心数据
**用途**: 当某个工程研究中心出现问题时,可以单独检查其数据格式
**使用方法**:
```bash
python check_specific_lab.py
```
修改脚本中的工程研究中心名称来检查不同工程研究中心。
## 故障排除
### 常见错误及解决方案
#### 1. 文件不存在错误
```
❌ 错误:找不到数据文件
```
**解决方案**:确认 `src/assets/实验室.json` 文件存在且路径正确
#### 2. 数据库连接错误
```
❌ 数据库操作失败
```
**解决方案**
- 确认数据库文件 `backend/data/app.db` 存在
- 确认没有其他进程占用数据库
- 确认有足够的磁盘空间
#### 3. JSON格式错误
```
❌ 导入失败: JSON decode error
```
**解决方案**
- 使用JSON验证工具检查文件格式
- 确认文件编码为UTF-8
- 检查是否有多余的逗号或括号
#### 4. 数据类型转换错误
```
❌ 处理工程研究中心 XXX 时出错
```
**解决方案**
- 检查JSON中数值字段是否包含非数字字符
- 脚本有safe_int和safe_float函数来处理大部分类型错误
- 如果持续出错,可以手动检查该工程研究中心的数据
### 数据一致性检查
导入后建议进行以下检查:
1. **数量检查**
```sql
SELECT COUNT(*) FROM labs;
```
2. **年度数据检查**
```sql
SELECT name, current_year, json_length(annual_data) as year_count
FROM labs
WHERE annual_data IS NOT NULL;
```
3. **统计数据检查**
```sql
SELECT name, valid_patents, doctoral_students, total_funding
FROM labs
ORDER BY total_funding DESC;
```
## 数据更新策略
### 完全重新导入
如果数据变化很大,建议:
1. 备份现有数据库
2. 清空labs表
3. 重新导入所有数据
### 增量更新
如果只是部分数据更新:
1. 脚本会自动检测已存在的工程研究中心
2. 根据中心编号或名称匹配
3. 更新现有记录的数据
### 数据备份
在大规模更新前,建议备份:
```powershell
copy backend\data\app.db backend\data\app_backup_$(Get-Date -Format "yyyyMMdd_HHmmss").db
```
## 性能优化
### 大量数据处理
如果数据量很大(>100个工程研究中心
1. 考虑分批处理
2. 添加进度条显示
3. 使用批量插入操作
### 内存优化
- 避免一次性加载所有数据到内存
- 使用流式处理方式
- 及时释放不需要的对象
## 维护建议
### 定期任务
1. **每月检查**:验证数据一致性
2. **每季度备份**:完整备份数据库
3. **每年更新**:根据新的数据字段要求更新脚本
### 版本控制
- 对导入脚本进行版本控制
- 记录每次数据更新的变更日志
- 保留历史数据备份
## 联系信息
如有问题或需要技术支持,请联系:
- 开发团队AI助手
- 文档更新:每次数据模型变更时同步更新
---
**最后更新时间**2024年度
**文档版本**v1.0
**适用环境**Windows + Python + FastAPI + SQLite

83
backend/alter_table.py Normal file
View File

@ -0,0 +1,83 @@
import sqlite3
import os
import logging
import json
# 设置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 数据库路径
DB_DIR = "data"
DB_PATH = os.path.join(DB_DIR, "app.db")
def check_and_alter_table():
# 检查数据库文件是否存在
if not os.path.exists(DB_PATH):
logger.error(f"数据库文件 {DB_PATH} 不存在")
return
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# 检查talents表是否存在idcode字段
cursor.execute("PRAGMA table_info(talents)")
columns = [column[1] for column in cursor.fetchall()]
if 'idcode' not in columns:
logger.info("talents表中添加idcode字段")
cursor.execute("ALTER TABLE talents ADD COLUMN idcode TEXT")
conn.commit()
else:
logger.info("talents表已包含idcode字段无需修改")
# 检查talents表是否存在educationBackground字段
if 'educationBackground' not in columns:
logger.info("talents表中添加educationBackground字段")
cursor.execute("ALTER TABLE talents ADD COLUMN educationBackground TEXT")
conn.commit()
else:
logger.info("talents表已包含educationBackground字段无需修改")
# 检查labs表是否存在idcode字段
cursor.execute("PRAGMA table_info(labs)")
labs_columns = [column[1] for column in cursor.fetchall()]
if 'idcode' not in labs_columns:
logger.info("labs表中添加idcode字段")
cursor.execute("ALTER TABLE labs ADD COLUMN idcode TEXT")
conn.commit()
else:
logger.info("labs表已包含idcode字段无需修改")
# 检查dimensions表是否存在sub_dimensions字段
cursor.execute("PRAGMA table_info(dimensions)")
columns = [column[1] for column in cursor.fetchall()]
if 'sub_dimensions' not in columns:
logger.info("dimensions表中添加sub_dimensions字段")
cursor.execute("ALTER TABLE dimensions ADD COLUMN sub_dimensions JSON")
conn.commit()
else:
logger.info("dimensions表已包含sub_dimensions字段无需修改")
# 检查dimensions表是否存在parent_id字段
if 'parent_id' not in columns:
logger.info("dimensions表中添加parent_id字段")
cursor.execute("ALTER TABLE dimensions ADD COLUMN parent_id INTEGER REFERENCES dimensions(id)")
conn.commit()
else:
logger.info("dimensions表已包含parent_id字段无需修改")
# 检查labs表是否存在sub_dimension_evaluations字段
if 'sub_dimension_evaluations' not in labs_columns:
logger.info("labs表中添加sub_dimension_evaluations字段")
cursor.execute("ALTER TABLE labs ADD COLUMN sub_dimension_evaluations TEXT")
conn.commit()
else:
logger.info("labs表已包含sub_dimension_evaluations字段无需修改")
conn.close()
if __name__ == "__main__":
check_and_alter_table()

181
backend/crud.py Normal file
View File

@ -0,0 +1,181 @@
from sqlalchemy.orm import Session
import models
import schemas
from jose import JWTError, jwt
from passlib.context import CryptContext
from typing import List, Optional, Dict, Any
import datetime
import json
# 密码处理上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# 验证密码
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
# 获取密码哈希
def get_password_hash(password):
return pwd_context.hash(password)
# 用户相关操作
def get_user(db: Session, username: str):
return db.query(models.User).filter(models.User.username == username).first()
def get_users(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.User).offset(skip).limit(limit).all()
def create_user(db: Session, username: str, password: str, email: Optional[str] = None, full_name: Optional[str] = None):
hashed_password = get_password_hash(password)
db_user = models.User(
username=username,
email=email,
full_name=full_name,
hashed_password=hashed_password
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
def authenticate_user(db: Session, username: str, password: str):
user = get_user(db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
# 人才相关操作
def get_talent(db: Session, talent_id: str):
return db.query(models.Talent).filter(models.Talent.id == talent_id).first()
def get_talents(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Talent).offset(skip).limit(limit).all()
def create_talent(db: Session, talent: schemas.TalentCreate):
db_talent = models.Talent(**talent.dict())
db.add(db_talent)
db.commit()
db.refresh(db_talent)
return db_talent
def update_talent(db: Session, talent_id: str, talent_data: Dict[str, Any]):
db_talent = get_talent(db, talent_id)
if db_talent:
for key, value in talent_data.items():
setattr(db_talent, key, value)
db.commit()
db.refresh(db_talent)
return db_talent
def delete_talent(db: Session, talent_id: str):
db_talent = get_talent(db, talent_id)
if db_talent:
db.delete(db_talent)
db.commit()
return True
return False
# 工程研究中心相关操作
def get_lab(db: Session, lab_id: str):
return db.query(models.Lab).filter(models.Lab.id == lab_id).first()
def get_labs(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Lab).offset(skip).limit(limit).all()
def create_lab(db: Session, lab: schemas.LabCreate):
db_lab = models.Lab(**lab.dict())
db.add(db_lab)
db.commit()
db.refresh(db_lab)
return db_lab
def update_lab(db: Session, lab_id: str, lab_data: Dict[str, Any]):
db_lab = get_lab(db, lab_id)
if db_lab:
for key, value in lab_data.items():
setattr(db_lab, key, value)
db.commit()
db.refresh(db_lab)
return db_lab
# 仪表盘数据相关操作
def get_dashboard(db: Session):
dashboard = db.query(models.DashboardData).first()
return dashboard
def get_news(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.News).offset(skip).limit(limit).all()
def create_news(db: Session, title: str, date: str, dashboard_id: int):
db_news = models.News(title=title, date=date, dashboard_id=dashboard_id)
db.add(db_news)
db.commit()
db.refresh(db_news)
return db_news
def update_dashboard(db: Session, dashboard_data: Dict[str, Any]):
dashboard = get_dashboard(db)
if dashboard:
# 更新简单字段
for key, value in dashboard_data.items():
if key != "newsData": # 新闻数据单独处理
setattr(dashboard, key, value)
# 如果有新闻数据需要更新
if "newsData" in dashboard_data:
# 删除所有旧新闻
db.query(models.News).filter(models.News.dashboard_id == dashboard.id).delete()
# 添加新的新闻
for news_item in dashboard_data["newsData"]:
db_news = models.News(
title=news_item["title"],
date=news_item["date"],
dashboard_id=dashboard.id
)
db.add(db_news)
db.commit()
db.refresh(dashboard)
return dashboard
# 维度相关操作
def get_dimension(db: Session, dimension_id: int):
return db.query(models.Dimension).filter(models.Dimension.id == dimension_id).first()
def get_dimensions_by_category(db: Session, category: str):
return db.query(models.Dimension).filter(models.Dimension.category == category).all()
def get_all_dimensions(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Dimension).offset(skip).limit(limit).all()
def create_dimension(db: Session, name: str, weight: float = 1.0, category: str = None, description: str = None):
db_dimension = models.Dimension(
name=name,
weight=weight,
category=category,
description=description
)
db.add(db_dimension)
db.commit()
db.refresh(db_dimension)
return db_dimension
def update_dimension(db: Session, dimension_id: int, dimension_data: Dict[str, Any]):
db_dimension = get_dimension(db, dimension_id)
if db_dimension:
for key, value in dimension_data.items():
setattr(db_dimension, key, value)
db.commit()
db.refresh(db_dimension)
return db_dimension
def delete_dimension(db: Session, dimension_id: int):
db_dimension = get_dimension(db, dimension_id)
if db_dimension:
db.delete(db_dimension)
db.commit()
return True
return False

BIN
backend/data/app.db Normal file

Binary file not shown.

BIN
backend/data/app.db.bak Normal file

Binary file not shown.

30
backend/database.py Normal file
View File

@ -0,0 +1,30 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os
# 确保数据目录存在
DB_DIR = "data"
os.makedirs(DB_DIR, exist_ok=True)
# 数据库URL
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/app.db"
# 创建引擎
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
# 创建会话
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 声明基类
Base = declarative_base()
# 获取数据库会话依赖项
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@ -0,0 +1,81 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
修复JSON文件格式问题
将Python的None值替换为JSON标准的null值
使用方法:
python fix_json_format.py
"""
import json
import sys
import os
from pathlib import Path
def fix_json_file():
"""修复JSON文件中的None值问题"""
# 源文件和目标文件路径
source_file = Path(__file__).parent.parent / "src" / "assets" / "工程研究中心.json"
if not source_file.exists():
print(f"❌ 错误:找不到文件 {source_file}")
return False
try:
print(f"📖 正在读取文件: {source_file}")
# 读取文件内容
with open(source_file, 'r', encoding='utf-8') as f:
content = f.read()
print(f"✅ 文件读取成功,大小: {len(content)} 字符")
# 替换None为null
print("🔄 正在修复None值...")
fixed_content = content.replace(': None,', ': null,')
fixed_content = fixed_content.replace(': None}', ': null}')
fixed_content = fixed_content.replace(': None]', ': null]')
# 统计替换数量
none_count = content.count(': None,') + content.count(': None}') + content.count(': None]')
print(f"🔧 找到并修复了 {none_count} 个None值")
# 验证JSON格式
print("📝 正在验证JSON格式...")
try:
json.loads(fixed_content)
print("✅ JSON格式验证通过")
except json.JSONDecodeError as e:
print(f"❌ JSON格式仍有问题: {e}")
return False
# 保存修复后的文件
print("💾 正在保存修复后的文件...")
with open(source_file, 'w', encoding='utf-8') as f:
f.write(fixed_content)
print("🎉 文件修复完成!")
return True
except Exception as e:
print(f"❌ 修复失败: {str(e)}")
return False
def main():
"""主函数"""
print("🚀 开始修复JSON文件格式...")
print("=" * 50)
success = fix_json_file()
print("=" * 50)
if success:
print("🎉 修复成功!现在可以运行导入脚本了。")
else:
print("💥 修复失败!")
input("\n按回车键退出...")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,130 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
修复年度数据中年份字段的数据类型问题
将所有年份从数字类型转换为字符串类型
"""
import sys
import os
import json
# 添加父目录到路径以便导入模块
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from sqlalchemy.orm import sessionmaker
from database import SessionLocal
from models import Lab
def fix_year_data_types():
"""修复所有工程研究中心年度数据中的年份类型问题"""
try:
print("🔄 正在连接数据库...")
db = SessionLocal()
print("🔄 正在查询所有工程研究中心...")
labs = db.query(Lab).all()
print(f"✅ 找到 {len(labs)} 个工程研究中心")
fixed_count = 0
error_count = 0
for lab in labs:
try:
if not lab.annual_data:
continue
if isinstance(lab.annual_data, str):
# 解析JSON数据
data = json.loads(lab.annual_data)
# 检查是否需要修复
needs_fix = False
for year_data in data:
year_field = year_data.get("归属年份")
if isinstance(year_field, int):
needs_fix = True
break
if needs_fix:
print(f"🔧 修复工程研究中心: {lab.name}")
# 修复所有年份字段
for year_data in data:
year_field = year_data.get("归属年份")
if isinstance(year_field, int):
year_data["归属年份"] = str(year_field)
print(f" - 将年份 {year_field} 转换为字符串")
# 保存修复后的数据
lab.annual_data = json.dumps(data, ensure_ascii=False)
fixed_count += 1
except Exception as e:
print(f"❌ 处理工程研究中心 {lab.name} 时出错: {str(e)}")
error_count += 1
continue
# 提交更改
if fixed_count > 0:
print(f"\n💾 正在保存修复结果...")
db.commit()
print(f"✅ 修复完成!")
else:
print(" 没有需要修复的数据")
print(f"📊 统计信息:")
print(f" - 修复的工程研究中心: {fixed_count}")
print(f" - 错误数量: {error_count}")
print(f" - 总计处理: {len(labs)}")
db.close()
return True
except Exception as e:
print(f"❌ 修复失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def verify_fix():
"""验证修复结果"""
try:
print("\n🔍 验证修复结果...")
db = SessionLocal()
# 检查西部优势矿产资源高效利用工程研究中心
lab = db.query(Lab).filter(Lab.name == "西部优势矿产资源高效利用").first()
if lab and lab.annual_data:
data = json.loads(lab.annual_data)
for i, year_data in enumerate(data):
year_field = year_data.get("归属年份")
print(f"📅 年度 {i+1}: {year_field} (类型: {type(year_field)})")
db.close()
return True
except Exception as e:
print(f"❌ 验证失败: {str(e)}")
return False
def main():
"""主函数"""
print("🚀 开始修复年度数据类型...")
print("=" * 50)
success = fix_year_data_types()
if success:
verify_fix()
print("=" * 50)
if success:
print("🎉 修复完成!")
else:
print("💥 修复失败!")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,270 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
工程研究中心数据完整导入脚本
用于导入assets/工程研究中心.json中的所有工程研究中心数据到数据库
使用方法:
1. 确保后端服务未运行
2. 在backend目录下执行: python import_lab_data_full.py
主要功能:
- 导入所有工程研究中心的基本信息
- 导入多年度数据到annual_data JSON字段
- 自动生成lab_id
- 支持数据更新如果lab已存在
"""
import json
import sys
import os
from pathlib import Path
# 添加父目录到路径以便导入模块
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from sqlalchemy.orm import sessionmaker
from database import SessionLocal, engine
from models import Lab
import uuid
def safe_int(value):
"""安全转换为整数"""
if value is None or value == "":
return 0
if isinstance(value, (int, float)):
return int(value)
if isinstance(value, str):
try:
return int(float(value))
except (ValueError, TypeError):
return 0
return 0
def safe_float(value):
"""安全转换为浮点数"""
if value is None or value == "":
return 0.0
if isinstance(value, (int, float)):
return float(value)
if isinstance(value, str):
try:
return float(value)
except (ValueError, TypeError):
return 0.0
return 0.0
def safe_str(value):
"""安全转换为字符串"""
if value is None:
return ""
return str(value)
def import_lab_data():
"""导入工程研究中心数据"""
# 检查JSON文件是否存在
json_file = Path(__file__).parent.parent / "src" / "assets" / "工程研究中心.json"
if not json_file.exists():
print(f"❌ 错误:找不到数据文件 {json_file}")
return False
try:
# 读取JSON数据
print(f"📖 正在读取数据文件: {json_file}")
with open(json_file, 'r', encoding='utf-8') as f:
labs_data = json.load(f)
print(f"✅ 成功读取数据,共 {len(labs_data)} 个工程研究中心")
# 创建数据库会话
db = SessionLocal()
imported_count = 0
updated_count = 0
error_count = 0
try:
for lab_info in labs_data:
try:
center_name = lab_info.get("中心名称", "")
center_number = lab_info.get("中心编号", "")
annual_data = lab_info.get("年度数据", [])
if not center_name:
print(f"⚠️ 跳过无名称的工程研究中心")
continue
print(f"\n🔄 正在处理工程研究中心: {center_name} (编号: {center_number})")
# 检查是否已存在
existing_lab = None
if center_number:
existing_lab = db.query(Lab).filter(Lab.center_number == center_number).first()
if not existing_lab and center_name:
existing_lab = db.query(Lab).filter(Lab.name == center_name).first()
if existing_lab:
print(f" 📝 工程研究中心已存在,更新数据...")
lab = existing_lab
updated_count += 1
else:
print(f" 创建新工程研究中心...")
lab = Lab()
lab.id = str(uuid.uuid4())
imported_count += 1
# 设置基本信息
lab.name = center_name
lab.center_number = center_number
lab.idcode = center_number # 用编号作为显示ID
# 处理年度数据
if annual_data:
# 提取基本信息(从最新年度数据)
latest_data = max(annual_data, key=lambda x: x.get("归属年份", "0"))
lab.school = latest_data.get("所属学校", "")
lab.department = latest_data.get("主管部门", "")
lab.field = latest_data.get("所属领域", "")
lab.current_year = latest_data.get("归属年份", "")
# 存储多年度数据到annual_data JSON字段
lab.annual_data = json.dumps(annual_data, ensure_ascii=False)
# 从最新数据中提取主要信息字段
lab.innovation_situation = latest_data.get("技术攻关与创新情况", "")
lab.overall_situation = latest_data.get("1.总体情况", "")
lab.engineering_cases = latest_data.get("2.工程化案例", "")
lab.industry_service = latest_data.get("3.行业服务情况", "")
lab.discipline_support = latest_data.get("1.学科发展支撑情况", "") or latest_data.get("1.支撑学科发展情况", "")
lab.talent_cultivation = latest_data.get("2.人才培养情况", "")
lab.team_building = latest_data.get("3.研究队伍建设情况", "")
lab.department_support = latest_data.get("1.主管部门、依托单位支持情况", "")
lab.equipment_sharing = latest_data.get("2.仪器设备开放共享情况", "")
lab.academic_style = latest_data.get("3.学风建设情况", "")
lab.technical_committee = latest_data.get("4.技术委员会工作情况", "")
lab.next_year_plan = latest_data.get("下一年度工作计划", "")
lab.problems_suggestions = latest_data.get("问题与建议", "")
lab.director_opinion = latest_data.get("1.工程中心负责人意见", "")
lab.institution_opinion = latest_data.get("2.依托单位意见", "")
lab.research_directions = latest_data.get("研究方向/学术带头人", "")
# 统计数据字段
lab.national_awards_first = safe_int(latest_data.get("国家级科技奖励一等奖(项)", 0))
lab.national_awards_second = safe_int(latest_data.get("国家级科技奖励二等奖(项)", 0))
lab.provincial_awards_first = safe_int(latest_data.get("省、部级科技奖励一等奖(项)", 0))
lab.provincial_awards_second = safe_int(latest_data.get("省、部级科技奖励二等奖(项)", 0))
lab.valid_patents = safe_int(latest_data.get("有效专利(项)", 0))
lab.other_ip = safe_int(latest_data.get("其他知识产权(项)", 0))
lab.international_standards = safe_int(latest_data.get("国际/国家标准(项)", 0))
lab.industry_standards = safe_int(latest_data.get("行业/地方标准(项)", 0))
# 专利转化数据
lab.patent_transfer_contracts = safe_int(latest_data.get("合同项数(项)", 0))
lab.patent_transfer_amount = safe_float(latest_data.get("合同金额(万元)", 0))
lab.patent_license_contracts = safe_int(latest_data.get("合同项数(项)_1", 0))
lab.patent_license_amount = safe_float(latest_data.get("合同金额(万元)_1", 0))
lab.patent_valuation_contracts = safe_int(latest_data.get("合同项数(项)_2", 0))
lab.patent_valuation_amount = safe_float(latest_data.get("作价金额(万元)", 0))
# 项目合作
lab.project_contracts = safe_int(latest_data.get("项目合同项数(项)", 0))
lab.project_amount = safe_float(latest_data.get("项目合同金额(万元)", 0))
# 学科信息
lab.discipline_1 = latest_data.get("依托学科1", "")
lab.discipline_2 = latest_data.get("依托学科2", "")
lab.discipline_3 = latest_data.get("依托学科3", "")
# 人才培养数据
lab.doctoral_students = safe_int(latest_data.get("在读博士生", 0))
lab.master_students = safe_int(latest_data.get("在读硕士生", 0))
lab.graduated_doctoral = safe_int(latest_data.get("当年毕业博士", 0))
lab.graduated_master = safe_int(latest_data.get("当年毕业硕士", 0))
lab.undergraduate_courses = safe_int(latest_data.get("承担本科课程", 0))
lab.graduate_courses = safe_int(latest_data.get("承担研究生课程", 0))
lab.textbooks = safe_int(latest_data.get("大专院校教材", 0))
# 人员结构
lab.professors = safe_int(latest_data.get("科技人才-教授(人)", 0))
lab.associate_professors = safe_int(latest_data.get("科技人才-副教授(人)", 0))
lab.lecturers = safe_int(latest_data.get("科技人才-讲师(人)", 0))
lab.domestic_visitors = safe_int(latest_data.get("访问学者-国内(人)", 0))
lab.foreign_visitors = safe_int(latest_data.get("访问学者-国外(人)", 0))
lab.postdoc_in = safe_int(latest_data.get("本年度进站博士后(人)", 0))
lab.postdoc_out = safe_int(latest_data.get("本年度出站博士后(人)", 0))
# 基础设施
lab.center_area = safe_float(latest_data.get("工程中心面积(m²)", 0))
lab.new_area = safe_float(latest_data.get("当年新增面积(m²)", 0))
lab.fixed_personnel = safe_int(latest_data.get("固定人员(人)", 0))
lab.mobile_personnel = safe_int(latest_data.get("流动人员(人)", 0))
# 经费情况
lab.total_funding = safe_float(latest_data.get("当年项目到账总经费(万元)", 0))
lab.vertical_funding = safe_float(latest_data.get("纵向经费(万元)", 0))
lab.horizontal_funding = safe_float(latest_data.get("横向经费(万元)", 0))
# 服务情况
lab.technical_consultations = safe_int(latest_data.get("技术咨询(次)", 0))
lab.training_services = safe_int(latest_data.get("培训服务(人次)", 0))
# 设置兼容性字段
lab.personnel = f"{lab.fixed_personnel + lab.mobile_personnel}"
lab.nationalProjects = str(lab.project_contracts)
lab.otherProjects = "0"
lab.achievements = str(lab.valid_patents)
lab.image = "/image/实验室1.png" # 默认图片
print(f" ✅ 年度数据: {len(annual_data)} 年, 最新年份: {lab.current_year}")
# 添加到数据库(如果是新记录)
if lab not in db.query(Lab).all():
db.add(lab)
except Exception as e:
print(f" ❌ 处理工程研究中心 {center_name} 时出错: {str(e)}")
error_count += 1
continue
# 提交更改
print(f"\n💾 正在保存到数据库...")
db.commit()
print(f"✅ 数据导入完成!")
print(f"📊 统计信息:")
print(f" - 新增工程研究中心: {imported_count}")
print(f" - 更新工程研究中心: {updated_count}")
print(f" - 错误数量: {error_count}")
print(f" - 总计处理: {imported_count + updated_count}")
return True
except Exception as e:
print(f"❌ 数据库操作失败: {str(e)}")
db.rollback()
return False
finally:
db.close()
except Exception as e:
print(f"❌ 导入失败: {str(e)}")
return False
def main():
"""主函数"""
print("🚀 开始导入工程研究中心数据...")
print("=" * 50)
success = import_lab_data()
print("=" * 50)
if success:
print("🎉 导入完成!")
else:
print("💥 导入失败!")
input("\n按回车键退出...")
if __name__ == "__main__":
main()

226
backend/init_db.py Normal file
View File

@ -0,0 +1,226 @@
import json
from sqlalchemy.orm import Session
from database import engine, SessionLocal, Base
import models
import main # 导入原有假数据
import logging
# 设置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 默认仪表盘数据如果main.py中没有定义
default_dashboard_data = {
"paperCount": 3500,
"patentCount": 2000,
"highImpactPapers": 100,
"keyProjects": 50,
"fundingAmount": "500万元",
"researcherStats": {
"academician": 12,
"chiefScientist": 28,
"distinguishedProfessor": 56,
"youngScientist": 120
},
"newsData": [
{"title": "我校科研团队在量子计算领域取得重大突破", "date": "2023-05-15"},
{"title": "张教授团队论文被Nature收录", "date": "2023-04-28"},
{"title": "校长率团访问美国麻省理工学院商讨合作事宜", "date": "2023-04-15"},
{"title": "我校获批3项国家重点研发计划", "date": "2023-03-20"},
{"title": "2023年度国家自然科学基金申请工作启动", "date": "2023-02-10"}
]
}
# 创建所有表
def create_tables():
Base.metadata.create_all(bind=engine)
logger.info("数据库表已创建")
# 导入用户数据
def import_users(db: Session):
# 检查是否已存在用户
existing_users = db.query(models.User).count()
if existing_users == 0:
for username, user_data in main.fake_users_db.items():
db_user = models.User(
username=user_data["username"],
email=user_data.get("email"),
full_name=user_data.get("full_name"),
hashed_password=user_data["hashed_password"],
disabled=user_data.get("disabled", False)
)
db.add(db_user)
db.commit()
logger.info("用户数据已导入")
else:
logger.info("用户数据已存在,跳过导入")
# 导入人才数据
def import_talents(db: Session):
# 检查是否已存在人才数据
existing_talents = db.query(models.Talent).count()
if existing_talents == 0:
for talent_data in main.talents:
# 处理evaluationData字段 - 确保是JSON格式
evaluation_data = talent_data.get("evaluationData")
db_talent = models.Talent(
id=talent_data["id"],
name=talent_data["name"],
gender=talent_data.get("gender"),
birthDate=talent_data.get("birthDate"),
title=talent_data.get("title"),
position=talent_data.get("position"),
education=talent_data.get("education"),
address=talent_data.get("address"),
academicDirection=talent_data.get("academicDirection"),
talentPlan=talent_data.get("talentPlan"),
officeLocation=talent_data.get("officeLocation"),
email=talent_data.get("email"),
phone=talent_data.get("phone"),
tutorType=talent_data.get("tutorType"),
papers=talent_data.get("papers"),
projects=talent_data.get("projects"),
photo=talent_data.get("photo"),
eduWorkHistory=talent_data.get("eduWorkHistory"),
researchDirection=talent_data.get("researchDirection"),
recentProjects=talent_data.get("recentProjects"),
representativePapers=talent_data.get("representativePapers"),
patents=talent_data.get("patents"),
evaluationData=evaluation_data
)
db.add(db_talent)
db.commit()
logger.info("人才数据已导入")
else:
logger.info("人才数据已存在,跳过导入")
# 导入工程研究中心数据
def import_labs(db: Session):
# 检查是否已存在工程研究中心数据
existing_labs = db.query(models.Lab).count()
if existing_labs == 0:
for lab_data in main.labs:
# 处理evaluationData字段 - 确保是JSON格式
evaluation_data = lab_data.get("evaluationData")
db_lab = models.Lab(
id=lab_data["id"],
name=lab_data["name"],
personnel=lab_data.get("personnel"),
nationalProjects=lab_data.get("nationalProjects"),
otherProjects=lab_data.get("otherProjects"),
achievements=lab_data.get("achievements"),
labAchievements=lab_data.get("labAchievements"),
image=lab_data.get("image"),
score=lab_data.get("score"),
evaluationData=evaluation_data
)
db.add(db_lab)
db.commit()
logger.info("工程研究中心数据已导入")
else:
logger.info("工程研究中心数据已存在,跳过导入")
# 导入仪表盘数据
def import_dashboard(db: Session):
# 检查是否已存在仪表盘数据
existing_dashboard = db.query(models.DashboardData).count()
if existing_dashboard == 0:
# 优先使用main.py中的数据如果不存在则使用默认数据
dashboard_data = getattr(main, "dashboard_data", default_dashboard_data)
# 创建仪表盘记录
db_dashboard = models.DashboardData(
id=1, # 主键ID设为1
paperCount=dashboard_data["paperCount"],
patentCount=dashboard_data["patentCount"],
highImpactPapers=dashboard_data["highImpactPapers"],
keyProjects=dashboard_data["keyProjects"],
fundingAmount=dashboard_data["fundingAmount"],
researcherStats=dashboard_data["researcherStats"]
)
db.add(db_dashboard)
db.flush() # 立即写入数据库获取ID
# 创建新闻数据记录
for news_item in dashboard_data["newsData"]:
db_news = models.News(
title=news_item["title"],
date=news_item["date"],
dashboard_id=db_dashboard.id
)
db.add(db_news)
db.commit()
logger.info("仪表盘和新闻数据已导入")
else:
logger.info("仪表盘数据已存在,跳过导入")
# 导入默认维度
def import_dimensions(db: Session):
# 人才评估维度
talent_dimensions = [
{"name": "学术成果", "weight": 1.0, "category": "talent", "description": "包括论文发表、专著、专利等"},
{"name": "科研项目", "weight": 1.0, "category": "talent", "description": "承担的科研项目数量和级别"},
{"name": "人才引进", "weight": 1.0, "category": "talent", "description": "引进的人才数量和质量"},
{"name": "学术影响力", "weight": 1.0, "category": "talent", "description": "学术引用和影响力指标"},
{"name": "教学质量", "weight": 1.0, "category": "talent", "description": "教学评估和学生反馈"},
{"name": "社会服务", "weight": 1.0, "category": "talent", "description": "社会服务和贡献"}
]
# 工程研究中心评估维度
lab_dimensions = [
{"name": "科研产出", "weight": 1.0, "category": "lab", "description": "工程研究中心科研成果产出"},
{"name": "人才培养", "weight": 1.0, "category": "lab", "description": "培养的研究生和博士后数量"},
{"name": "项目承担", "weight": 1.0, "category": "lab", "description": "承担的科研项目数量和级别"},
{"name": "设备利用", "weight": 1.0, "category": "lab", "description": "设备利用率和效益"},
{"name": "学术交流", "weight": 1.0, "category": "lab", "description": "国内外学术交流和合作"},
{"name": "社会服务", "weight": 1.0, "category": "lab", "description": "社会服务和贡献"}
]
# 检查是否已存在维度
existing_dimensions = db.query(models.Dimension).count()
if existing_dimensions == 0:
# 添加人才评估维度
for dim in talent_dimensions:
db_dimension = models.Dimension(**dim)
db.add(db_dimension)
# 添加工程研究中心评估维度
for dim in lab_dimensions:
db_dimension = models.Dimension(**dim)
db.add(db_dimension)
db.commit()
logger.info("默认评估维度已导入")
else:
logger.info("评估维度已存在,跳过导入")
# 主函数
def init_db():
# 创建表结构
create_tables()
# 获取数据库会话
db = SessionLocal()
try:
# 导入各类数据
import_users(db)
import_talents(db)
import_labs(db)
import_dashboard(db)
import_dimensions(db)
logger.info("数据库初始化完成!")
except Exception as e:
logger.error(f"数据库初始化失败: {e}")
finally:
db.close()
# 直接运行脚本时执行初始化
if __name__ == "__main__":
init_db()

View File

@ -0,0 +1,68 @@
from sqlalchemy.orm import Session
from database import SessionLocal, engine, Base
import models
import crud
def init_dimensions():
# 创建会话
db = SessionLocal()
try:
# 检查是否已经存在维度数据
existing_dimensions = db.query(models.Dimension).count()
if existing_dimensions == 0:
print("初始化维度数据...")
# 教师科研人才评估维度
talent_dimensions = [
{"name": "教育和工作经历", "weight": 10, "category": "talent", "description": "教育背景和工作经历评估"},
{"name": "研究方向前沿性", "weight": 8, "category": "talent", "description": "研究是否处于学科前沿"},
{"name": "主持科研项目情况", "weight": 12, "category": "talent", "description": "项目规模、数量及影响力"},
{"name": "科研成果质量", "weight": 16, "category": "talent", "description": "论文、专利等成果的质量与影响"},
{"name": "教学能力与效果", "weight": 14, "category": "talent", "description": "教学水平及学生评价"},
{"name": "学术服务与影响力", "weight": 40, "category": "talent", "description": "学术服务与社会影响力"}
]
# 工程研究中心评估维度
lab_dimensions = [
{"name": "工程技术研发能力与水平", "weight": 30, "category": "lab", "description": "工程研究中心整体工程技术研发水平"},
{"name": "创新水平", "weight": 10, "category": "lab", "description": "工程研究中心科研创新程度"},
{"name": "人才与队伍", "weight": 10, "category": "lab", "description": "工程研究中心人才梯队建设情况"},
{"name": "装备与场地", "weight": 10, "category": "lab", "description": "工程研究中心设备和场地条件"},
{"name": "成果转化与行业贡献", "weight": 30, "category": "lab", "description": "成果产业化情况与行业贡献"},
{"name": "学科发展与人才培养", "weight": 20, "category": "lab", "description": "对学科发展与人才培养的贡献"},
{"name": "开放与运行管理", "weight": 20, "category": "lab", "description": "工程研究中心开放程度与管理水平"}
]
# 添加教师科研人才评估维度
for dim in talent_dimensions:
crud.create_dimension(
db,
name=dim["name"],
weight=dim["weight"],
category=dim["category"],
description=dim["description"]
)
# 添加工程研究中心评估维度
for dim in lab_dimensions:
crud.create_dimension(
db,
name=dim["name"],
weight=dim["weight"],
category=dim["category"],
description=dim["description"]
)
print("维度数据初始化完成!")
else:
print("数据库中已存在维度数据,跳过初始化。")
finally:
db.close()
if __name__ == "__main__":
# 确保表已创建
Base.metadata.create_all(bind=engine)
# 初始化维度数据
init_dimensions()

1556
backend/main.py Normal file

File diff suppressed because it is too large Load Diff

221
backend/models.py Normal file
View File

@ -0,0 +1,221 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Float, Text, JSON
from sqlalchemy.orm import relationship
from database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
email = Column(String, nullable=True)
full_name = Column(String, nullable=True)
hashed_password = Column(String)
disabled = Column(Boolean, default=False)
class Talent(Base):
__tablename__ = "talents"
id = Column(String, primary_key=True, index=True)
idcode = Column(String, nullable=True, index=True)
name = Column(String, index=True)
gender = Column(String, nullable=True)
birthDate = Column(String, nullable=True)
title = Column(String, nullable=True)
position = Column(String, nullable=True)
education = Column(String, nullable=True)
educationBackground = Column(String, nullable=True)
address = Column(String, nullable=True)
academicDirection = Column(String, nullable=True)
talentPlan = Column(String, nullable=True)
officeLocation = Column(String, nullable=True)
email = Column(String, nullable=True)
phone = Column(String, nullable=True)
tutorType = Column(String, nullable=True)
papers = Column(String, nullable=True)
projects = Column(String, nullable=True)
photo = Column(Text, nullable=True)
eduWorkHistory = Column(Text, nullable=True)
researchDirection = Column(Text, nullable=True)
recentProjects = Column(Text, nullable=True)
representativePapers = Column(Text, nullable=True)
patents = Column(Text, nullable=True)
evaluationData = Column(JSON, nullable=True)
class Lab(Base):
__tablename__ = "labs"
id = Column(String, primary_key=True, index=True)
idcode = Column(String, nullable=True, index=True)
# 基本信息
name = Column(String, index=True) # 中心名称
center_number = Column(String, nullable=True) # 中心编号
school = Column(String, nullable=True) # 所属学校
department = Column(String, nullable=True) # 主管部门
field = Column(String, nullable=True) # 所属领域
# 年度数据 - 存储为JSON格式包含多年数据
annual_data = Column(JSON, nullable=True)
# 当前年度的主要信息(用于快速查询和显示)
current_year = Column(String, nullable=True) # 当前评估年份
# 技术攻关与创新情况
innovation_situation = Column(Text, nullable=True)
# 总体情况
overall_situation = Column(Text, nullable=True)
# 工程化案例
engineering_cases = Column(Text, nullable=True)
# 行业服务情况
industry_service = Column(Text, nullable=True)
# 学科发展支撑情况
discipline_support = Column(Text, nullable=True)
# 人才培养情况
talent_cultivation = Column(Text, nullable=True)
# 研究队伍建设情况
team_building = Column(Text, nullable=True)
# 主管部门、依托单位支持情况
department_support = Column(Text, nullable=True)
# 仪器设备开放共享情况
equipment_sharing = Column(Text, nullable=True)
# 学风建设情况
academic_style = Column(Text, nullable=True)
# 技术委员会工作情况
technical_committee = Column(Text, nullable=True)
# 下一年度工作计划
next_year_plan = Column(Text, nullable=True)
# 问题与建议
problems_suggestions = Column(Text, nullable=True)
# 工程中心负责人意见
director_opinion = Column(Text, nullable=True)
# 依托单位意见
institution_opinion = Column(Text, nullable=True)
# 研究方向/学术带头人
research_directions = Column(Text, nullable=True)
# 统计数据字段
national_awards_first = Column(Integer, default=0) # 国家级科技奖励一等奖
national_awards_second = Column(Integer, default=0) # 国家级科技奖励二等奖
provincial_awards_first = Column(Integer, default=0) # 省、部级科技奖励一等奖
provincial_awards_second = Column(Integer, default=0) # 省、部级科技奖励二等奖
valid_patents = Column(Integer, default=0) # 有效专利
other_ip = Column(Integer, default=0) # 其他知识产权
international_standards = Column(Integer, default=0) # 国际/国家标准
industry_standards = Column(Integer, default=0) # 行业/地方标准
# 专利转化相关
patent_transfer_contracts = Column(Integer, default=0) # 专利转让合同项数
patent_transfer_amount = Column(Float, default=0.0) # 专利转让合同金额
patent_license_contracts = Column(Integer, default=0) # 专利许可合同项数
patent_license_amount = Column(Float, default=0.0) # 专利许可合同金额
patent_valuation_contracts = Column(Integer, default=0) # 专利作价合同项数
patent_valuation_amount = Column(Float, default=0.0) # 专利作价金额
# 项目合作
project_contracts = Column(Integer, default=0) # 项目合同项数
project_amount = Column(Float, default=0.0) # 项目合同金额
# 学科信息
discipline_1 = Column(String, nullable=True) # 依托学科1
discipline_2 = Column(String, nullable=True) # 依托学科2
discipline_3 = Column(String, nullable=True) # 依托学科3
# 人才培养数据
doctoral_students = Column(Integer, default=0) # 在读博士生
master_students = Column(Integer, default=0) # 在读硕士生
graduated_doctoral = Column(Integer, default=0) # 当年毕业博士
graduated_master = Column(Integer, default=0) # 当年毕业硕士
undergraduate_courses = Column(Integer, default=0) # 承担本科课程
graduate_courses = Column(Integer, default=0) # 承担研究生课程
textbooks = Column(Integer, default=0) # 大专院校教材
# 人员结构
professors = Column(Integer, default=0) # 教授人数
associate_professors = Column(Integer, default=0) # 副教授人数
lecturers = Column(Integer, default=0) # 讲师人数
domestic_visitors = Column(Integer, default=0) # 国内访问学者
foreign_visitors = Column(Integer, default=0) # 国外访问学者
postdoc_in = Column(Integer, default=0) # 本年度进站博士后
postdoc_out = Column(Integer, default=0) # 本年度出站博士后
# 基础设施
center_area = Column(Float, default=0.0) # 工程中心面积
new_area = Column(Float, default=0.0) # 当年新增面积
fixed_personnel = Column(Integer, default=0) # 固定人员
mobile_personnel = Column(Integer, default=0) # 流动人员
# 经费情况
total_funding = Column(Float, default=0.0) # 当年项目到账总经费
vertical_funding = Column(Float, default=0.0) # 纵向经费
horizontal_funding = Column(Float, default=0.0) # 横向经费
# 服务情况
technical_consultations = Column(Integer, default=0) # 技术咨询次数
training_services = Column(Integer, default=0) # 培训服务人次
# 原有字段保留兼容性
personnel = Column(String, nullable=True)
nationalProjects = Column(String, nullable=True)
otherProjects = Column(String, nullable=True)
achievements = Column(String, nullable=True)
labAchievements = Column(Text, nullable=True)
image = Column(Text, nullable=True)
score = Column(Integer, nullable=True)
evaluationData = Column(JSON, nullable=True)
sub_dimension_evaluations = Column(JSON, nullable=True)
class DashboardData(Base):
__tablename__ = "dashboard"
id = Column(Integer, primary_key=True)
paperCount = Column(Integer)
patentCount = Column(Integer)
highImpactPapers = Column(Integer)
keyProjects = Column(Integer)
fundingAmount = Column(String)
researcherStats = Column(JSON)
class News(Base):
__tablename__ = "news"
id = Column(Integer, primary_key=True, index=True)
title = Column(String)
date = Column(String)
dashboard_id = Column(Integer, ForeignKey("dashboard.id"))
dashboard = relationship("DashboardData", back_populates="news_items")
# 添加反向关系
DashboardData.news_items = relationship("News", back_populates="dashboard")
class Dimension(Base):
__tablename__ = "dimensions"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
weight = Column(Float, default=1.0)
category = Column(String, nullable=True) # talent 或 lab
description = Column(Text, nullable=True)
parent_id = Column(Integer, ForeignKey("dimensions.id"), nullable=True)
# 自引用关系,用于树形结构
children = relationship("Dimension", back_populates="parent", cascade="all, delete-orphan")
parent = relationship("Dimension", back_populates="children", remote_side=[id])
# 存储子维度的JSON数据
sub_dimensions = Column(JSON, nullable=True)

View File

@ -0,0 +1,304 @@
#!/usr/bin/env python3
"""
网络诊断工具
用于诊断服务器部署后访问外部网站时的网络连接问题
"""
import socket
import requests
import time
import urllib.parse
import subprocess
import platform
from typing import Dict, List
def diagnose_network_connectivity(url: str) -> Dict:
"""
综合诊断网络连接问题
"""
print(f"开始诊断网络连接: {url}")
results = {
'url': url,
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
'tests': {}
}
# 解析URL
try:
parsed_url = urllib.parse.urlparse(url)
hostname = parsed_url.hostname
port = parsed_url.port or (443 if parsed_url.scheme == 'https' else 80)
results['hostname'] = hostname
results['port'] = port
except Exception as e:
results['error'] = f"URL解析失败: {e}"
return results
# 1. DNS解析测试
results['tests']['dns_resolution'] = test_dns_resolution(hostname)
# 2. TCP连接测试
results['tests']['tcp_connection'] = test_tcp_connection(hostname, port)
# 3. HTTP请求测试
results['tests']['http_request'] = test_http_request(url)
# 4. 系统网络配置检查
results['tests']['system_info'] = get_system_network_info()
# 5. 建议解决方案
results['suggestions'] = generate_suggestions(results['tests'])
return results
def test_dns_resolution(hostname: str) -> Dict:
"""测试DNS解析"""
print(f"测试DNS解析: {hostname}")
test_result = {
'status': 'unknown',
'details': {}
}
try:
start_time = time.time()
ip_address = socket.gethostbyname(hostname)
dns_time = time.time() - start_time
test_result['status'] = 'success'
test_result['details'] = {
'ip_address': ip_address,
'resolution_time': f"{dns_time:.3f}"
}
print(f"DNS解析成功: {hostname} -> {ip_address} ({dns_time:.3f}秒)")
except socket.gaierror as e:
test_result['status'] = 'failed'
test_result['details'] = {
'error': str(e),
'error_code': e.errno if hasattr(e, 'errno') else 'unknown'
}
print(f"DNS解析失败: {e}")
return test_result
def test_tcp_connection(hostname: str, port: int) -> Dict:
"""测试TCP连接"""
print(f"测试TCP连接: {hostname}:{port}")
test_result = {
'status': 'unknown',
'details': {}
}
try:
start_time = time.time()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10) # 10秒超时
result = sock.connect_ex((hostname, port))
connect_time = time.time() - start_time
sock.close()
if result == 0:
test_result['status'] = 'success'
test_result['details'] = {
'connection_time': f"{connect_time:.3f}",
'port_open': True
}
print(f"TCP连接成功: {hostname}:{port} ({connect_time:.3f}秒)")
else:
test_result['status'] = 'failed'
test_result['details'] = {
'connection_time': f"{connect_time:.3f}",
'port_open': False,
'error_code': result
}
print(f"TCP连接失败: {hostname}:{port} (错误代码: {result})")
except Exception as e:
test_result['status'] = 'failed'
test_result['details'] = {
'error': str(e)
}
print(f"TCP连接测试异常: {e}")
return test_result
def test_http_request(url: str) -> Dict:
"""测试HTTP请求"""
print(f"测试HTTP请求: {url}")
test_result = {
'status': 'unknown',
'details': {}
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
try:
start_time = time.time()
response = requests.get(
url,
headers=headers,
timeout=(10, 30), # (连接超时, 读取超时)
verify=False,
allow_redirects=True
)
request_time = time.time() - start_time
test_result['status'] = 'success'
test_result['details'] = {
'status_code': response.status_code,
'request_time': f"{request_time:.3f}",
'content_length': len(response.content),
'encoding': response.encoding,
'headers_count': len(response.headers)
}
print(f"HTTP请求成功: {response.status_code} ({request_time:.3f}秒)")
except requests.exceptions.Timeout as e:
test_result['status'] = 'timeout'
test_result['details'] = {
'error': '请求超时',
'timeout_type': str(type(e).__name__)
}
print(f"HTTP请求超时: {e}")
except requests.exceptions.ConnectionError as e:
test_result['status'] = 'connection_error'
test_result['details'] = {
'error': '连接错误',
'error_detail': str(e)
}
print(f"HTTP连接错误: {e}")
except Exception as e:
test_result['status'] = 'failed'
test_result['details'] = {
'error': str(e)
}
print(f"HTTP请求失败: {e}")
return test_result
def get_system_network_info() -> Dict:
"""获取系统网络信息"""
print("收集系统网络信息...")
info = {
'platform': platform.system(),
'platform_version': platform.version(),
'python_version': platform.python_version()
}
try:
# 获取本机IP地址
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
info['hostname'] = hostname
info['local_ip'] = local_ip
except:
info['hostname'] = 'unknown'
info['local_ip'] = 'unknown'
# 检查网络配置仅在Linux/Unix系统上
if platform.system() in ['Linux', 'Darwin']:
try:
# 检查DNS服务器
with open('/etc/resolv.conf', 'r') as f:
dns_servers = []
for line in f:
if line.startswith('nameserver'):
dns_servers.append(line.split()[1])
info['dns_servers'] = dns_servers
except:
info['dns_servers'] = 'unavailable'
try:
# 检查默认网关
result = subprocess.run(['ip', 'route', 'show', 'default'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
info['default_gateway'] = result.stdout.strip()
else:
info['default_gateway'] = 'unavailable'
except:
info['default_gateway'] = 'unavailable'
return info
def generate_suggestions(test_results: Dict) -> List[str]:
"""根据测试结果生成建议"""
suggestions = []
# DNS问题
if test_results.get('dns_resolution', {}).get('status') == 'failed':
suggestions.extend([
"DNS解析失败请检查服务器的DNS配置",
"尝试使用公共DNS服务器 (8.8.8.8, 114.114.114.114)",
"检查 /etc/resolv.conf 文件中的DNS服务器配置"
])
# TCP连接问题
if test_results.get('tcp_connection', {}).get('status') == 'failed':
suggestions.extend([
"TCP连接失败可能是防火墙阻止了连接",
"检查服务器的出站规则设置",
"确认目标端口是否开放"
])
# HTTP请求问题
http_status = test_results.get('http_request', {}).get('status')
if http_status == 'timeout':
suggestions.extend([
"HTTP请求超时网络延迟较高",
"增加超时时间设置",
"考虑使用重试机制"
])
elif http_status == 'connection_error':
suggestions.extend([
"HTTP连接错误可能是网络配置问题",
"检查代理设置",
"确认SSL/TLS证书配置"
])
# 通用建议
if not suggestions:
suggestions.append("网络连接测试基本正常,问题可能在应用层面")
suggestions.extend([
"联系系统管理员检查网络配置",
"考虑使用VPN或代理服务器",
"检查服务器所在云平台的安全组设置"
])
return suggestions
def main():
"""主函数,用于命令行测试"""
test_url = "https://ac.bit.edu.cn"
print("=" * 60)
print("网络连接诊断工具")
print("=" * 60)
results = diagnose_network_connectivity(test_url)
print("\n" + "=" * 60)
print("诊断结果")
print("=" * 60)
for test_name, test_result in results['tests'].items():
print(f"\n{test_name}: {test_result['status']}")
if 'details' in test_result:
for key, value in test_result['details'].items():
print(f" {key}: {value}")
print("\n" + "=" * 60)
print("建议解决方案")
print("=" * 60)
for i, suggestion in enumerate(results['suggestions'], 1):
print(f"{i}. {suggestion}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1 @@

46
backend/quick_test.py Normal file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""
快速网络连接测试脚本
"""
import socket
import time
import requests
def quick_test():
target_url = "https://ac.bit.edu.cn"
target_host = "ac.bit.edu.cn"
print("快速网络连接测试")
print("=" * 40)
# DNS测试
print("1. DNS解析测试...")
try:
ip = socket.gethostbyname(target_host)
print(f" 成功: {target_host} -> {ip}")
except Exception as e:
print(f" 失败: {e}")
return
# HTTP测试
print("\n2. HTTP请求测试...")
try:
start = time.time()
response = requests.get(
target_url,
timeout=(30, 90),
verify=False,
headers={'User-Agent': 'Mozilla/5.0'}
)
duration = time.time() - start
print(f" 成功: {response.status_code} ({duration:.2f}秒)")
except requests.exceptions.Timeout:
print(" 超时: 网络延迟过高")
except Exception as e:
print(f" 失败: {e}")
print("\n测试完成")
if __name__ == "__main__":
quick_test()

13
backend/requirements.txt Normal file
View File

@ -0,0 +1,13 @@
fastapi==0.109.2
uvicorn==0.27.1
pydantic==2.6.1
python-multipart==0.0.9
python-jose==3.3.0
passlib==1.7.4
bcrypt==4.1.2
requests==2.31.0
beautifulsoup4==4.12.2
sqlalchemy==2.0.26
aiosqlite==0.19.0
alembic==1.13.1
python-docx==0.8.11

246
backend/schemas.py Normal file
View File

@ -0,0 +1,246 @@
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
# 用户模型
class UserBase(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
disabled: Optional[bool] = None
class Config:
from_attributes = True
# 令牌模型
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str
# 人才模型
class TalentBase(BaseModel):
name: str
idcode: Optional[str] = None # 新增展示用ID字段
gender: Optional[str] = None
birthDate: Optional[str] = None
title: Optional[str] = None
position: Optional[str] = None
education: Optional[str] = None
educationBackground: Optional[str] = None
address: Optional[str] = None
academicDirection: Optional[str] = None
talentPlan: Optional[str] = None
officeLocation: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
tutorType: Optional[str] = None
papers: Optional[str] = None
projects: Optional[str] = None
photo: Optional[str] = None
eduWorkHistory: Optional[str] = None
researchDirection: Optional[str] = None
recentProjects: Optional[str] = None
representativePapers: Optional[str] = None
patents: Optional[str] = None
evaluationData: Optional[List[float]] = None
class TalentCreate(TalentBase):
id: str
class Talent(TalentBase):
id: str
class Config:
from_attributes = True
# 工程研究中心模型
class LabBase(BaseModel):
name: str
idcode: Optional[str] = None # 新增展示用ID字段
# 基本信息
center_number: Optional[str] = None # 中心编号
school: Optional[str] = None # 所属学校
department: Optional[str] = None # 主管部门
field: Optional[str] = None # 所属领域
# 年度数据 - 修改为可以接受字符串或字典列表
annual_data: Optional[str] = None # 存储为JSON字符串
current_year: Optional[str] = None # 当前评估年份
# 详细信息
innovation_situation: Optional[str] = None # 技术攻关与创新情况
overall_situation: Optional[str] = None # 总体情况
engineering_cases: Optional[str] = None # 工程化案例
industry_service: Optional[str] = None # 行业服务情况
discipline_support: Optional[str] = None # 学科发展支撑情况
talent_cultivation: Optional[str] = None # 人才培养情况
team_building: Optional[str] = None # 研究队伍建设情况
department_support: Optional[str] = None # 主管部门、依托单位支持情况
equipment_sharing: Optional[str] = None # 仪器设备开放共享情况
academic_style: Optional[str] = None # 学风建设情况
technical_committee: Optional[str] = None # 技术委员会工作情况
next_year_plan: Optional[str] = None # 下一年度工作计划
problems_suggestions: Optional[str] = None # 问题与建议
director_opinion: Optional[str] = None # 工程中心负责人意见
institution_opinion: Optional[str] = None # 依托单位意见
research_directions: Optional[str] = None # 研究方向/学术带头人
# 统计数据
national_awards_first: Optional[int] = 0 # 国家级科技奖励一等奖
national_awards_second: Optional[int] = 0 # 国家级科技奖励二等奖
provincial_awards_first: Optional[int] = 0 # 省、部级科技奖励一等奖
provincial_awards_second: Optional[int] = 0 # 省、部级科技奖励二等奖
valid_patents: Optional[int] = 0 # 有效专利
other_ip: Optional[int] = 0 # 其他知识产权
international_standards: Optional[int] = 0 # 国际/国家标准
industry_standards: Optional[int] = 0 # 行业/地方标准
# 专利转化相关
patent_transfer_contracts: Optional[int] = 0 # 专利转让合同项数
patent_transfer_amount: Optional[float] = 0.0 # 专利转让合同金额
patent_license_contracts: Optional[int] = 0 # 专利许可合同项数
patent_license_amount: Optional[float] = 0.0 # 专利许可合同金额
patent_valuation_contracts: Optional[int] = 0 # 专利作价合同项数
patent_valuation_amount: Optional[float] = 0.0 # 专利作价金额
# 项目合作
project_contracts: Optional[int] = 0 # 项目合同项数
project_amount: Optional[float] = 0.0 # 项目合同金额
# 学科信息
discipline_1: Optional[str] = None # 依托学科1
discipline_2: Optional[str] = None # 依托学科2
discipline_3: Optional[str] = None # 依托学科3
# 人才培养数据
doctoral_students: Optional[int] = 0 # 在读博士生
master_students: Optional[int] = 0 # 在读硕士生
graduated_doctoral: Optional[int] = 0 # 当年毕业博士
graduated_master: Optional[int] = 0 # 当年毕业硕士
undergraduate_courses: Optional[int] = 0 # 承担本科课程
graduate_courses: Optional[int] = 0 # 承担研究生课程
textbooks: Optional[int] = 0 # 大专院校教材
# 人员结构
professors: Optional[int] = 0 # 教授人数
associate_professors: Optional[int] = 0 # 副教授人数
lecturers: Optional[int] = 0 # 讲师人数
domestic_visitors: Optional[int] = 0 # 国内访问学者
foreign_visitors: Optional[int] = 0 # 国外访问学者
postdoc_in: Optional[int] = 0 # 本年度进站博士后
postdoc_out: Optional[int] = 0 # 本年度出站博士后
# 基础设施
center_area: Optional[float] = 0.0 # 工程中心面积
new_area: Optional[float] = 0.0 # 当年新增面积
fixed_personnel: Optional[int] = 0 # 固定人员
mobile_personnel: Optional[int] = 0 # 流动人员
# 经费情况
total_funding: Optional[float] = 0.0 # 当年项目到账总经费
vertical_funding: Optional[float] = 0.0 # 纵向经费
horizontal_funding: Optional[float] = 0.0 # 横向经费
# 服务情况
technical_consultations: Optional[int] = 0 # 技术咨询次数
training_services: Optional[int] = 0 # 培训服务人次
# 原有字段保留兼容性
personnel: Optional[str] = None
nationalProjects: Optional[str] = None
otherProjects: Optional[str] = None
achievements: Optional[str] = None
labAchievements: Optional[str] = None
image: Optional[str] = None
score: Optional[int] = None
evaluationData: Optional[List[float]] = None
sub_dimension_evaluations: Optional[Dict[str, Any]] = None # 存储二级维度评估数据
class LabCreate(LabBase):
id: str
class Lab(LabBase):
id: str
class Config:
from_attributes = True
# 新闻模型
class NewsBase(BaseModel):
title: str
date: str
class NewsCreate(NewsBase):
dashboard_id: int
class News(NewsBase):
id: int
dashboard_id: int
class Config:
from_attributes = True
# 仪表盘数据模型
class DashboardData(BaseModel):
paperCount: int
patentCount: int
highImpactPapers: int
keyProjects: int
fundingAmount: str
researcherStats: Dict[str, int]
newsData: List[Dict[str, str]]
class Config:
from_attributes = True
# URL抓取请求模型
class ScrapeRequest(BaseModel):
url: str
# 保存评估数据请求模型
class SaveDataRequest(BaseModel):
data_type: str # "talent" 或 "lab"
data: Dict[str, Any]
# 维度模型
class SubDimensionBase(BaseModel):
name: str
weight: float = 1.0
description: Optional[str] = None
class SubDimension(SubDimensionBase):
id: Optional[int] = None
class Config:
from_attributes = True
class DimensionBase(BaseModel):
name: str
weight: float = 0.0 # 一级维度不需要权重
category: Optional[str] = None
description: Optional[str] = None
sub_dimensions: Optional[List[SubDimension]] = None
subDimensions: Optional[List[SubDimension]] = None # 添加subDimensions作为别名兼容前端
class DimensionCreate(DimensionBase):
pass
class Dimension(DimensionBase):
id: int
class Config:
from_attributes = True
# 批量保存维度的请求模型
class SaveDimensionsRequest(BaseModel):
dimensions: List[DimensionBase]
category: str # "talent" 或 "lab"

View File

@ -0,0 +1,256 @@
import requests
import socket
import time
import urllib.parse
import random
from bs4 import BeautifulSoup
from fastapi.responses import JSONResponse
async def scrape_url_improved(request):
"""
改进版本的URL抓取函数解决服务器部署后的网络连接问题
"""
try:
print(f"开始抓取URL: {request.url}")
# 设置请求头,模拟浏览器访问
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Cache-Control': 'max-age=0'
}
# 添加DNS解析测试
try:
url_parts = urllib.parse.urlparse(request.url)
hostname = url_parts.hostname
print(f"正在解析域名: {hostname}")
ip_address = socket.gethostbyname(hostname)
print(f"域名解析成功: {hostname} -> {ip_address}")
except socket.gaierror as dns_error:
print(f"DNS解析失败: {dns_error}")
return JSONResponse(
status_code=500,
content={"error": f"DNS解析失败无法访问 {hostname}: {str(dns_error)}"},
)
# 使用重试机制,增加连接和读取超时时间
max_retries = 3
timeout_settings = (30, 90) # (连接超时, 读取超时)
response = None
last_error = None
for attempt in range(max_retries):
try:
print(f"{attempt + 1} 次尝试连接...")
start_time = time.time()
# 发送HTTP请求获取页面内容增加超时时间和重试
response = requests.get(
request.url,
headers=headers,
timeout=timeout_settings,
verify=False, # 临时禁用SSL验证避免证书问题
allow_redirects=True
)
elapsed_time = time.time() - start_time
print(f"请求成功,耗时: {elapsed_time:.2f}")
response.raise_for_status() # 如果请求失败,抛出异常
break
except requests.exceptions.Timeout as timeout_error:
last_error = timeout_error
print(f"{attempt + 1} 次尝试超时: {timeout_error}")
if attempt < max_retries - 1:
wait_time = (attempt + 1) * 2 # 递增等待时间
print(f"等待 {wait_time} 秒后重试...")
time.sleep(wait_time)
continue
except requests.exceptions.ConnectionError as conn_error:
last_error = conn_error
print(f"{attempt + 1} 次尝试连接错误: {conn_error}")
if attempt < max_retries - 1:
wait_time = (attempt + 1) * 2
print(f"等待 {wait_time} 秒后重试...")
time.sleep(wait_time)
continue
except requests.exceptions.RequestException as req_error:
last_error = req_error
print(f"{attempt + 1} 次尝试请求错误: {req_error}")
if attempt < max_retries - 1:
wait_time = (attempt + 1) * 2
print(f"等待 {wait_time} 秒后重试...")
time.sleep(wait_time)
continue
# 如果所有重试都失败了
if response is None:
error_msg = f"经过 {max_retries} 次重试后仍然无法连接到 {request.url}"
if last_error:
error_msg += f",最后错误: {str(last_error)}"
print(error_msg)
return JSONResponse(
status_code=500,
content={
"error": error_msg,
"suggestions": [
"检查服务器网络连接",
"确认目标网站是否可访问",
"检查防火墙设置",
"考虑配置代理服务器",
"联系系统管理员检查网络配置"
]
},
)
# 设置编码以正确处理中文字符
response.encoding = 'utf-8'
# 解析HTML
soup = BeautifulSoup(response.text, 'html.parser')
# 获取基础URL用于解析相对路径
url_parts = urllib.parse.urlparse(request.url)
base_url = f"{url_parts.scheme}://{url_parts.netloc}"
# 初始化数据字典
teacher_data = {
"id": f"BLG{random.randint(10000, 99999)}",
"photo": f"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='120' viewBox='0 0 100 120'%3E%3Crect width='100' height='120' fill='%234986ff' opacity='0.3'/%3E%3Ccircle cx='50' cy='45' r='25' fill='%234986ff' opacity='0.6'/%3E%3Ccircle cx='50' cy='95' r='35' fill='%234986ff' opacity='0.6'/%3E%3C/svg%3E",
"evaluationData": [
round(min(100, max(60, 70 + 20 * (0.5 - random.random())))) for _ in range(6)
]
}
# 从教师信息表提取基本信息
info_table = soup.find('div', class_='wz_teacher')
if info_table:
table = info_table.find('table')
if table:
rows = table.find_all('tr')
# 提取姓名、性别、出生年月
if len(rows) > 0:
cells = rows[0].find_all('td')
if len(cells) >= 6:
teacher_data["name"] = cells[1].text.strip()
teacher_data["gender"] = cells[3].text.strip()
teacher_data["birthDate"] = cells[5].text.strip()
# 提取职称、职务、最高学历
if len(rows) > 1:
cells = rows[1].find_all('td')
if len(cells) >= 6:
teacher_data["title"] = cells[1].text.strip()
position = cells[3].text.strip()
teacher_data["position"] = position if position else ""
teacher_data["education"] = cells[5].text.strip()
# 提取学科方向
if len(rows) > 2:
cells = rows[2].find_all('td')
if len(cells) >= 2:
teacher_data["academicDirection"] = cells[1].text.strip()
# 提取人才计划和办公地点
if len(rows) > 3:
cells = rows[3].find_all('td')
if len(cells) >= 6:
talent_plan = cells[1].text.strip()
teacher_data["talentPlan"] = talent_plan if talent_plan else ""
teacher_data["officeLocation"] = cells[5].text.strip()
# 提取电子邮件和联系方式
if len(rows) > 4:
cells = rows[4].find_all('td')
if len(cells) >= 6:
email = cells[1].text.strip()
teacher_data["email"] = email if email else ""
phone = cells[5].text.strip()
teacher_data["phone"] = phone if phone else ""
# 提取通讯地址
if len(rows) > 5:
cells = rows[5].find_all('td')
if len(cells) >= 2:
teacher_data["address"] = cells[1].text.strip()
# 提取导师类型
if len(rows) > 6:
cells = rows[6].find_all('td')
if len(cells) >= 2:
teacher_data["tutorType"] = cells[1].text.strip()
# 提取照片
photo_element = soup.select_one('.teacherInfo .img img')
if photo_element and photo_element.get('src'):
img_src = photo_element['src']
# 处理相对路径构建完整的图片URL
if img_src.startswith('../../../'):
# 从URL获取基础路径移除文件名和最后两级目录
url_parts = request.url.split('/')
if len(url_parts) >= 4:
base_path = '/'.join(url_parts[:-3])
img_url = f"{base_path}/{img_src[9:]}" # 移除 '../../../'
else:
img_url = urllib.parse.urljoin(base_url, img_src)
else:
img_url = urllib.parse.urljoin(base_url, img_src)
# 直接保存完整的图片URL不下载到本地
teacher_data["photo"] = img_url
# 提取详细信息部分
content_divs = soup.select('.con01_t')
for div in content_divs:
heading = div.find('h3')
if not heading:
continue
heading_text = heading.text.strip()
# 获取该部分的所有段落文本
paragraphs = [p.text.strip() for p in div.find_all('p') if p.text.strip()]
section_content = '\n'.join(paragraphs)
# 根据标题将内容映射到相应字段
if '教育与工作经历' in heading_text:
teacher_data["eduWorkHistory"] = section_content
elif '研究方向' in heading_text:
teacher_data["researchDirection"] = section_content
elif '近5年承担的科研项目' in heading_text or '近五年承担的科研项目' in heading_text:
teacher_data["recentProjects"] = section_content
# 计算项目数量
project_count = len([p for p in paragraphs if p.strip().startswith(str(len(paragraphs) - paragraphs.index(p))+".")])
if project_count > 0:
teacher_data["projects"] = f"{project_count}"
else:
teacher_data["projects"] = f"{len(paragraphs)}"
elif '代表性学术论文' in heading_text:
teacher_data["representativePapers"] = section_content
# 计算论文数量
paper_count = len([p for p in paragraphs if p.strip().startswith("[")])
if paper_count > 0:
teacher_data["papers"] = f"{paper_count}"
else:
teacher_data["papers"] = f"{len(paragraphs)}"
elif '授权国家发明专利' in heading_text or '专利' in heading_text:
teacher_data["patents"] = section_content
print(f"抓取成功,提取到教师数据: {teacher_data.get('name', '未知')}")
return teacher_data
except Exception as e:
print(f"抓取错误: {str(e)}")
return JSONResponse(
status_code=500,
content={"error": f"抓取网页失败: {str(e)}"},
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

28
docker-compose.yml Normal file
View File

@ -0,0 +1,28 @@
version: '3'
services:
frontend:
build:
context: .
dockerfile: Dockerfile
ports:
- "48100:48100"
restart: always
depends_on:
- backend
networks:
- app-network
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "48996:48996"
restart: always
networks:
- app-network
networks:
app-network:
driver: bridge

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

22
nginx.conf Normal file
View File

@ -0,0 +1,22 @@
server {
listen 48100;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# 如果有API请求可以配置代理
# location /api {
# proxy_pass http://backend-service:8080;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# }
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "fast-dashboard",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev:local": "cross-env NODE_ENV=development vite",
"dev:prod": "cross-env NODE_ENV=production vite",
"build": "vite build",
"preview": "vite preview",
"backend": "cd backend && python app.py",
"start": "concurrently \"npm run dev\" \"npm run backend\""
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.8.4",
"echarts": "^5.6.0",
"element-plus": "^2.9.8",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.2",
"concurrently": "^7.6.0",
"cross-env": "^7.0.3",
"vite": "^6.3.1"
}
}

1425
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/image/人1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

BIN
public/image/人2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

BIN
public/image/人3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

BIN
public/image/人4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

BIN
public/image/人5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

BIN
public/image/人6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
public/image/人7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
public/image/人8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

BIN
public/image/实验室1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

BIN
public/image/实验室2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

BIN
public/image/实验室3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 KiB

BIN
public/image/实验室4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

BIN
public/image/实验室5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/image/实验室6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/实验室简历.docx Normal file

Binary file not shown.

51
setup_firewall.bat Normal file
View File

@ -0,0 +1,51 @@
@echo off
echo ===========================================
echo 配置 Fast Dashboard 防火墙规则
echo ===========================================
echo.
echo 此脚本将为 Fast Dashboard 项目配置防火墙规则
echo 需要管理员权限来修改防火墙设置
echo.
pause
echo 正在添加防火墙规则...
echo.
echo 添加前端端口 5173 的入站规则...
netsh advfirewall firewall add rule name="Fast Dashboard Frontend (Port 5173)" dir=in action=allow protocol=TCP localport=5173
if %errorlevel% equ 0 (
echo ✓ 前端端口 5173 规则添加成功
) else (
echo ✗ 前端端口 5173 规则添加失败,请以管理员身份运行此脚本
)
echo.
echo 添加后端端口 8000 的入站规则...
netsh advfirewall firewall add rule name="Fast Dashboard Backend (Port 8000)" dir=in action=allow protocol=TCP localport=8000
if %errorlevel% equ 0 (
echo ✓ 后端端口 8000 规则添加成功
) else (
echo ✗ 后端端口 8000 规则添加失败,请以管理员身份运行此脚本
)
echo.
echo ===========================================
echo 防火墙配置完成!
echo ===========================================
echo.
echo 已添加以下防火墙规则:
echo - Fast Dashboard Frontend (Port 5173)
echo - Fast Dashboard Backend (Port 8000)
echo.
echo 现在其他设备应该可以通过以下地址访问您的服务:
echo - 前端: http://192.168.18.108:5173
echo - 后端: http://192.168.18.108:8000
echo.
echo 如果仍然无法访问,请检查:
echo 1. 路由器是否有端口转发设置
echo 2. 网络是否在同一局域网内
echo 3. 是否有其他安全软件阻止访问
echo ===========================================
echo.
pause

23
setup_py_env.bat Normal file
View File

@ -0,0 +1,23 @@
@echo off
echo Setting up Python environment for fast-dashboard project...
REM Call conda activate in batch mode
call conda activate fast-dashboard-env || (
echo Failed to activate environment. Creating it...
call conda create -n fast-dashboard-env python=3.11 -y
call conda activate fast-dashboard-env
)
echo Installing dependencies...
pip install -r backend/requirements.txt
echo Environment setup complete!
echo.
echo To activate this environment in the future, run:
echo conda activate fast-dashboard-env
echo.
echo To run the backend server, run:
echo cd backend
echo uvicorn main:app --reload
echo.
pause

183
src/App.vue Normal file
View File

@ -0,0 +1,183 @@
<template>
<div class="app-container">
<!-- 登录页面 -->
<Login v-if="!isLoggedIn" @login-success="loginSuccess" />
<!-- 显示Token测试页面的按钮 -->
<!-- <div v-if="isLoggedIn" class="token-test-button">-->
<!-- <el-button type="info" size="small" @click="toggleTokenTest">-->
<!-- {{ showTokenTest ? '关闭测试工具' : '打开Token测试工具' }}-->
<!-- </el-button>-->
<!-- </div>-->
<!-- Token测试工具 -->
<TokenTest v-if="isLoggedIn && showTokenTest" />
<!-- 主仪表盘 -->
<Dashboard v-if="isLoggedIn && currentPage === 'dashboard' && !showTokenTest"
@navigate="changePage"
@logout="handleLogout" />
<!-- 科研成果详情页 -->
<ResearchDetail v-if="isLoggedIn && currentPage === 'research' && !showTokenTest"
@back-to-dashboard="backToDashboard"
@logout="handleLogout" />
<!-- 教师科研人才页面 -->
<TalentDetail v-if="isLoggedIn && currentPage === 'talent' && !showTokenTest"
@back-to-dashboard="backToDashboard"
@navigate="changePage"
@logout="handleLogout" />
<!-- 工程研究中心页面 -->
<LabDetail v-if="isLoggedIn && currentPage === 'lab' && !showTokenTest"
@back-to-dashboard="backToDashboard"
@navigate="changePage"
@logout="handleLogout" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessageBox } from 'element-plus'
import Login from './components/login.vue'
import Dashboard from './components/Dashboard.vue'
import ResearchDetail from './components/ResearchDetail.vue'
import TalentDetail from './components/TalentDetail.vue'
import LabDetail from './components/LabDetail.vue'
import TokenTest from './components/TokenTest.vue'
import axios from 'axios'
import { getApiBaseUrl } from './config'
//
const isLoggedIn = ref(false)
//
const currentPage = ref('dashboard')
// Token
const showTokenTest = ref(false)
//
onMounted(() => {
const token = localStorage.getItem('token')
const loginStatus = localStorage.getItem('isLoggedIn')
if (token && loginStatus === 'true') {
isLoggedIn.value = true
}
})
//
const loginSuccess = () => {
isLoggedIn.value = true
}
//
const changePage = (page) => {
currentPage.value = page
}
//
const backToDashboard = () => {
currentPage.value = 'dashboard'
}
// Token
const toggleTokenTest = () => {
showTokenTest.value = !showTokenTest.value
}
//
const handleLogout = () => {
ElMessageBox.confirm(
'确定要退出登录吗?',
'退出提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: 'custom-message-box',
confirmButtonClass: 'custom-confirm-button',
cancelButtonClass: 'custom-cancel-button',
}
).then(async () => {
try {
// token
const token = localStorage.getItem('token')
if (token) {
// API
// await axios.post(
// `${getApiBaseUrl()}/api/clear-all-data`,
// {},
// {
// headers: {
// 'Authorization': `Bearer ${token}`
// }
// }
// )
}
} catch (error) {
console.error('清空数据失败:', error)
} finally {
//
localStorage.removeItem('token')
localStorage.removeItem('username')
localStorage.removeItem('isLoggedIn')
isLoggedIn.value = false
currentPage.value = 'dashboard'
showTokenTest.value = false
}
}).catch(() => {
//
})
}
</script>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Microsoft YaHei', 'PingFang SC', Arial, sans-serif;
background-color: #0c1633;
color: white;
}
.app-container {
width: 100%;
min-height: 100vh;
}
.token-test-button {
position: fixed;
top: 10px;
right: 10px;
z-index: 1000;
}
/* 自定义确认框样式 */
.custom-message-box {
background-color: #0c1633;
border: 1px solid #1d3160;
color: white;
}
.custom-message-box .el-message-box__title {
color: white;
}
.custom-message-box .el-message-box__content {
color: #c0c5d6;
}
.custom-confirm-button {
background-color: #1989fa;
border-color: #1989fa;
color: white;
}
.custom-cancel-button {
background-color: transparent;
border-color: #3a5794;
color: #c0c5d6;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
src/assets/logo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

BIN
src/assets/logo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

1
src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 KiB

BIN
src/assets/人才贴图.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

2979
src/assets/实验室.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

1272
src/components/Dashboard.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,483 @@
<template>
<el-drawer
v-model="props.visible"
title=""
direction="rtl"
size="600px"
:before-close="handleClose"
custom-class="dimension-drawer"
>
<div class="drawer-content">
<div class="dimension-section">
<h2 class="section-title">评估维度设置</h2>
<!-- 维度列表 -->
<div class="dimensions-list">
<div v-for="(dim, index) in dimensions" :key="index" class="dimension-item">
<div class="dimension-name">
<el-input
v-model="dim.name"
placeholder="请输入维度名称"
/>
</div>
<div class="dimension-weight">
<div class="weight-input-group">
<el-input-number
v-model="dim.weight"
:min="1"
:max="100"
:step="1"
/>
<span class="weight-unit">%</span>
</div>
<el-button
type="danger"
icon="Delete"
circle
@click="removeDimension(index)"
class="delete-btn"
/>
</div>
</div>
</div>
<!-- 新增维度表单 -->
<div class="new-dimension-form">
<div class="form-row">
<div class="form-item">
<span class="label">新增维度名称</span>
<el-input
v-model="newDimension.name"
placeholder="请输入维度名称"
class="dimension-input"
/>
</div>
<div class="form-item weight-item">
<span class="label">维度权重</span>
<div class="weight-input-container">
<el-input-number
v-model="newDimension.weight"
:min="1"
:max="100"
:step="1"
class="weight-input"
/>
<span class="weight-unit">%</span>
<el-button
type="primary"
icon="Plus"
circle
class="add-btn"
@click="addDimension"
:disabled="!newDimension.name"
/>
</div>
</div>
</div>
</div>
<!-- 权重总计信息 -->
<div class="weight-summary">
<div class="weight-total">
<span>权重维度要求:</span>
<span class="weight-value">100%</span>
</div>
<div class="weight-current" :class="{ 'weight-error': totalWeight !== 100 }">
<span>当前权重维度总计:</span>
<span class="weight-value">{{ totalWeight }}%</span>
</div>
</div>
</div>
</div>
<template #footer>
<div class="drawer-footer">
<button class="drawer-btn cancel-btn" @click="handleClose">取消</button>
<button
class="drawer-btn confirm-btn"
@click="handleSave"
>确认</button>
</div>
</template>
</el-drawer>
</template>
<script setup>
import { ref, computed, reactive, watch } from 'vue';
import { ElMessage } from 'element-plus';
import axios from 'axios';
import { getApiBaseUrl } from '../config'; // APIURL
const props = defineProps({
visible: {
type: Boolean,
default: false
},
dimensions: {
type: Array,
default: () => []
},
category: {
type: String,
default: 'talent' // 'talent''lab'
}
});
const emit = defineEmits(['update:visible', 'save']);
// visible
const drawerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
});
//
const dimensions = ref([]);
//
watch(() => props.dimensions, (newDimensions) => {
if (newDimensions && newDimensions.length > 0) {
// props
dimensions.value = JSON.parse(JSON.stringify(newDimensions));
} else {
dimensions.value = [];
}
}, { immediate: true, deep: true });
//
const newDimension = reactive({
name: '',
weight: 5
});
//
const totalWeight = computed(() => {
return dimensions.value.reduce((sum, dim) => sum + (dim.weight || 0), 0);
});
//
const addDimension = () => {
if (!newDimension.name) {
ElMessage.warning('请输入维度名称');
return;
}
//
dimensions.value.push({
name: newDimension.name,
weight: newDimension.weight
});
//
newDimension.name = '';
newDimension.weight = 5;
};
//
const removeDimension = (index) => {
dimensions.value.splice(index, 1);
};
// -
const validateTotalWeight = () => {
//
};
// -
const handleClose = (done) => {
// done ( before-close)
if (typeof done === 'function') {
done();
}
//
drawerVisible.value = false;
};
//
const handleSave = async () => {
if (totalWeight.value !== 100) {
ElMessage.error('权重总计必须为100%,请调整后再保存');
return;
}
try {
//
const dimensionsData = dimensions.value.map(dim => ({
name: dim.name,
weight: dim.weight,
category: props.category,
id: dim.id
}));
// localStorageJWT
const token = localStorage.getItem('token');
if (!token) {
ElMessage.error('未登录或登录已过期,请重新登录');
return;
}
// ()API
const response = await axios.post(`${getApiBaseUrl()}/api/save-dimensions`, {
dimensions: dimensionsData,
category: props.category
}, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.data.success) {
ElMessage.success('维度设置保存成功');
//
emit('save', dimensions.value);
//
handleClose();
} else {
throw new Error(response.data.message || '保存失败');
}
} catch (error) {
console.error('保存维度失败:', error);
ElMessage.error('保存维度失败,请重试');
}
};
</script>
<style>
/* 确保引入common.css */
@import './common.css';
</style>
<style scoped>
.dimension-drawer :deep(.el-drawer__header) {
margin-bottom: 0;
color: white;
background-color: #0c1633;
border-bottom: 1px solid rgba(73,134,255,0.3);
}
.dimension-drawer :deep(.el-drawer__body) {
padding: 0;
overflow: hidden;
background-color: #0c1633;
}
.drawer-content {
padding: 20px;
color: white;
height: 100%;
display: flex;
flex-direction: column;
background-color: #0c1633;
}
.dimension-section {
display: flex;
flex-direction: column;
height: 100%;
}
.section-title {
margin: 15px 0;
font-size: 18px;
color: rgba(255,255,255,0.9);
text-align: center;
border-bottom: 1px solid rgba(73,134,255,0.3);
padding-bottom: 10px;
}
.dimensions-list {
background-color: #1f3266;
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(73,134,255,0.3);
margin-bottom: 20px;
overflow-y: auto;
flex: 1; /* 让这个元素占满可用空间 */
}
.dimension-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid rgba(73,134,255,0.1);
}
.dimension-name {
flex: 1;
margin-right: 15px;
}
.dimension-weight {
display: flex;
align-items: center;
gap: 10px;
}
.weight-input-group {
display: flex;
align-items: center;
}
.delete-btn {
font-size: 12px;
padding: 6px;
}
.new-dimension-form {
background-color: #1f3266;
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(73,134,255,0.3);
margin-bottom: 20px;
}
.form-row {
display: flex;
gap: 15px;
}
.form-item {
flex: 1;
display: flex;
flex-direction: column;
}
.weight-item {
flex: 0.7;
}
.label {
display: block;
margin-bottom: 8px;
color: rgba(255,255,255,0.7);
}
.weight-input-container {
display: flex;
align-items: center;
}
.weight-input {
width: 80px;
}
.weight-unit {
margin: 0 5px;
color: #4986ff;
}
.add-btn {
margin-left: 10px;
}
.weight-summary {
background-color: #1f3266;
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(73,134,255,0.3);
margin-top: 20px;
}
.weight-total, .weight-current {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.weight-value {
font-weight: bold;
color: #4986ff;
}
.weight-error .weight-value {
color: #f56c6c;
}
/* 抽屉页脚 */
.dimension-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;
}
.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);
}
.confirm-btn {
background-color: rgba(14,62,167,1);
color: rgba(255,255,255,1);
border: 1px solid rgba(73,134,255,1);
}
.confirm-btn:disabled {
background-color: rgba(14,62,167,0.5);
border: 1px solid rgba(73,134,255,0.5);
cursor: not-allowed;
}
/* 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-input-number) {
width: 100px;
}
:deep(.el-input-number__decrease),
:deep(.el-input-number__increase) {
background-color: rgba(73,134,255,0.2);
color: white;
}
/* 滚动条样式 */
.drawer-content::-webkit-scrollbar,
.dimensions-list::-webkit-scrollbar {
width: 6px;
}
.drawer-content::-webkit-scrollbar-track,
.dimensions-list::-webkit-scrollbar-track {
background: transparent;
}
.drawer-content::-webkit-scrollbar-thumb,
.dimensions-list::-webkit-scrollbar-thumb {
background-color: #4986ff;
border-radius: 10px;
border: none;
}
</style>

View File

@ -0,0 +1,886 @@
<template>
<div class="evaluation-page">
<!-- 顶部导航栏 -->
<header class="dashboard-header">
<div class="logo">
<img src="../assets/logo1.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
<img src="../assets/logo2.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
<h1 class="main-title">
<div class="title-line"></div>
<span class="title-text">智慧科研评估系统</span>
<div class="title-line"></div>
</h1>
</div>
</header>
<!-- 主内容区域 -->
<div class="content-container">
<!-- 左侧维度设置 -->
<div class="dimension-sidebar">
<div class="sidebar-header">
<h1 class="sidebar-title">
<span class="home-link" @click="jumpToDashboard">首页</span>&nbsp;>&nbsp;工程研究中心评估
</h1>
</div>
<div class="dimension-content">
<h2 class="dimension-section-title">评估维度设置</h2>
<div class="dimension-list custom-scrollbar">
<div v-for="(dim, index) in dimensions" :key="index" class="dimension-item primary-dimension">
<div class="dimension-header" @click="editDimension(dim, index)">
<div class="dimension-name">
<label>{{ dim.name }}</label>
</div>
<div class="dimension-expand">
<span class="expand-icon"></span>
</div>
</div>
<!-- 二级维度列表 -->
<div class="sub-dimensions">
<div v-for="(subDim, subIndex) in dim.subDimensions" :key="`${index}-${subIndex}`" class="dimension-item sub-dimension">
<div class="dimension-name">
<label>{{ subDim.name }}</label>
</div>
<div class="dimension-weight">
<span class="weight-label">W:</span>
<span class="weight-value">{{ subDim.weight }}%</span>
</div>
</div>
</div>
</div>
<div class="dimension-add" @click="openDimensionDrawer">
<span class="add-icon">+</span>
<span class="add-text">添加自定义维度</span>
</div>
</div>
</div>
</div>
<!-- 右侧内容区 -->
<div class="main-content">
<!-- 搜索和操作栏 -->
<div class="action-bar">
<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>
</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;" @click="openLabDetail(lab)">
<div class="card-header">
<span class="lab-id">ID: {{ lab.idcode || lab.id }}</span>
<!-- <span class="total-score">综合评估分数: <span class="score-value">{{ lab.score }}</span></span> -->
</div>
<div class="card-content">
<div class="lab-image">
<img :src="lab.image" alt="工程研究中心图片" />
</div>
<div class="lab-info">
<div class="info-item">
<span class="info-label">研究中心名称:</span>
<span class="info-value">{{ lab.name }}</span>
</div>
<div class="info-item">
<span class="info-label">所属领域:</span>
<span class="info-value">{{ lab.field }}</span>
</div>
<div class="info-item">
<span class="info-label">所属学校:</span>
<span class="info-value">{{ lab.school }}</span>
</div>
<div class="info-item">
<span class="info-label">主管部门:</span>
<span class="info-value">{{ lab.department }}</span>
</div>
</div>
</div>
<div class="evaluation-chart">
<div :id="`lab-chart-${index}`" class="radar-chart"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 维度设置抽屉 -->
<LabDimensionDrawer
v-model:visible="dimensionDrawerVisible"
:dimensions="dimensions"
@save="handleSaveDimensions"
/>
<LabDrawerDetail
v-model:visible="drawerVisible"
:is-edit="isEditMode"
:dimensions="dimensions"
:lab-data="selectedLab"
@save="handleSaveEvaluation"
/>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts/core';
import { RadarChart } from 'echarts/charts';
import LabDrawerDetail from './LabDrawerDetail.vue';
import LabDimensionDrawer from './LabDimensionDrawer.vue';
import {
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import axios from 'axios';
import { ElMessage } from 'element-plus';
import { getApiBaseUrl } from '../config'; // APIURL
// echarts
echarts.use([
RadarChart,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
CanvasRenderer
]);
const drawerVisible = ref(false);
const isEditMode = ref(false);
const selectedLab = ref(null);
const dimensionDrawerVisible = ref(false);
//
const openAddEvaluationDrawer = () => {
isEditMode.value = false;
selectedLab.value = null;
drawerVisible.value = true;
};
//
const openDimensionDrawer = () => {
dimensionDrawerVisible.value = true;
};
//
const handleSaveEvaluation = async (data) => {
//
//
await loadLabs();
//
drawerVisible.value = false;
//
ElMessage.success(isEditMode.value ? '工程研究中心评估数据更新成功' : '工程研究中心评估数据新增成功');
};
//
const handleSaveDimensions = (newDimensions) => {
dimensions.value = newDimensions;
//
updateAllRadarCharts();
};
//
const emit = defineEmits(['navigate', 'back-to-dashboard', 'logout']);
//
const jumpToDashboard = () => {
emit('back-to-dashboard');
};
// Logo
const handleLogoClick = () => {
emit('logout');
};
//
const dimensions = ref([]);
//
const labs = ref([]);
//
const filteredLabs = ref([]);
//
function assignUniqueLabImages() {
//
labs.value.forEach((lab, index) => {
// 1-6
const photoIndex = (index % 6) + 1;
lab.image = `/image/实验室${photoIndex}.png`;
});
}
//
onMounted(async () => {
try {
//
const response = await axios.get(`${getApiBaseUrl()}/dimensions/lab`);
dimensions.value = response.data;
//
await loadLabs();
// handleSearchloadLabs
} catch (error) {
console.error('获取维度数据失败:', error);
ElMessage.error('获取维度数据失败');
}
});
//
const loadLabs = async () => {
try {
const response = await axios.get(`${getApiBaseUrl()}/labs`);
labs.value = response.data;
//
assignUniqueLabImages();
//
labs.value.forEach(lab => {
//
if (!lab.score && lab.evaluationData && lab.evaluationData.length > 0) {
//
lab.score = Math.round(lab.evaluationData.reduce((a, b) => a + b, 0) / lab.evaluationData.length);
}
});
//
handleSearch();
// DOM
setTimeout(() => {
updateAllRadarCharts();
}, 100);
} catch (error) {
console.error('获取工程研究中心数据失败:', error);
ElMessage.error('获取工程研究中心数据失败');
}
};
//
const editDimension = (dim, index) => {
//
openDimensionDrawer();
};
//
const updateAllRadarCharts = () => {
nextTick(() => {
filteredLabs.value.forEach((lab, index) => {
const chartDom = document.getElementById(`lab-chart-${index}`);
if (!chartDom) return;
//
echarts.dispose(chartDom);
const chart = echarts.init(chartDom);
//
const indicators = [];
if (dimensions.value && dimensions.value.length > 0) {
dimensions.value.forEach(dim => {
if (dim.subDimensions && dim.subDimensions.length > 0) {
dim.subDimensions.forEach(subDim => {
indicators.push({
name: subDim.name,
max: 100
});
});
}
});
}
// 使
if (indicators.length === 0) {
indicators.push(
{ name: '创新水平', max: 100 },
{ name: '研究能力', max: 100 },
{ name: '成果转化', max: 100 },
{ name: '学科建设', max: 100 },
{ name: '行业贡献', max: 100 },
{ name: '发展潜力', max: 100 }
);
}
//
let radarData = [];
//
let hasSubDimensionData = false;
if (lab.sub_dimension_evaluations && Object.keys(lab.sub_dimension_evaluations).length > 0) {
dimensions.value.forEach(dim => {
if (dim.subDimensions && dim.subDimensions.length > 0 &&
lab.sub_dimension_evaluations[dim.name]) {
dim.subDimensions.forEach(subDim => {
if (lab.sub_dimension_evaluations[dim.name][subDim.name] !== undefined) {
hasSubDimensionData = true;
radarData.push(lab.sub_dimension_evaluations[dim.name][subDim.name]);
}
});
}
});
}
// 使evaluationData
if (!hasSubDimensionData) {
//
if (!lab.evaluationData || lab.evaluationData.length !== indicators.length) {
// 60-90
lab.evaluationData = indicators.map(() => Math.floor(Math.random() * 31) + 60);
}
radarData = lab.evaluationData;
}
chart.setOption({
radar: {
indicator: indicators,
splitArea: {
show: false
},
axisLine: {
lineStyle: {
color: 'rgba(211, 253, 250, 0.8)'
}
},
splitLine: {
lineStyle: {
color: 'rgba(211, 253, 250, 0.8)'
}
},
name: {
textStyle: {
color: '#fff',
fontSize: 10
}
}
},
series: [
{
type: 'radar',
data: [
{
value: radarData,
name: '评估结果',
areaStyle: {
color: 'rgba(255, 0, 255, 0.3)'
},
lineStyle: {
color: 'rgba(255, 0, 255, 0.8)',
width: 1
},
itemStyle: {
color: 'rgba(255, 0, 255, 0.8)'
}
}
]
}
]
});
//
window.addEventListener('resize', () => {
chart && chart.resize();
});
});
});
};
//
const searchQuery = ref('');
//
const handleSearch = () => {
if (searchQuery.value === '') {
filteredLabs.value = labs.value;
} else {
filteredLabs.value = labs.value.filter(lab =>
lab.name.includes(searchQuery.value) ||
(lab.idcode && lab.idcode.includes(searchQuery.value))
);
}
//
nextTick(() => {
updateAllRadarCharts();
});
};
//
const openLabDetail = (lab) => {
selectedLab.value = lab;
isEditMode.value = true;
drawerVisible.value = true;
};
</script>
<style>
@import './common.css';
</style>
<style scoped>
.evaluation-page {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
background-color: #0c1633;
color: white;
overflow: hidden; /* 防止页面整体出现滚动条 */
}
/* 自定义滚动条样式 */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(73,134,255,0.5) rgba(38,47,80,0.3);
}
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(38,47,80,0.3);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(73,134,255,0.5);
border-radius: 4px;
border: 2px solid rgba(38,47,80,0.3);
}
.dashboard-header {
height: 60px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
display: flex;
align-items: center;
position: relative;
width: 100%;
}
.logo img {
height: 40px;
margin-right: 10px;
}
.main-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 28px;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
display: flex;
align-items: center;
white-space: nowrap;
}
.title-line {
border: 2px solid rgba(73,134,255,1);
width: 150px;
}
.title-text {
margin: 0 30px;
}
.content-container {
display: flex;
flex: 1;
padding: 20px;
overflow: hidden;
gap: 20px;
}
/* 特定于LabDetail的样式 */
.sidebar-header {
height: 64px;
display: flex;
align-items: center;
justify-content: left;
background-color: transparent;
}
.sidebar-title {
font-size: 22px;
font-weight: bold;
color: white;
margin: 0;
text-align: left;
}
.home-link {
text-decoration: underline;
cursor: pointer;
color: #4986ff;
}
.dimension-sidebar {
width: 280px;
display: flex;
flex-direction: column;
max-height: calc(100vh - 100px); /* 限制最大高度 */
}
.dimension-content {
flex: 1;
background-color: #262F50;
border-radius: 10px;
display: flex;
flex-direction: column;
overflow: hidden; /* 加上这个防止内容溢出 */
}
.dimension-section-title {
margin: 15px;
font-size: 16px;
text-align: center;
padding-bottom: 10px;
border-bottom: 1px solid rgba(73,134,255,0.3);
}
.dimension-list {
padding: 0 15px 15px 15px;
display: flex;
flex-direction: column;
gap: 15px;
overflow-y: auto; /* 允许列表滚动 */
flex: 1; /* 让列表占满剩余空间 */
}
.dimension-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background-color: rgba(73,134,255,0.1);
border-radius: 4px;
border-left: 3px solid #4986ff;
cursor: pointer; /* 添加指针样式,提示可点击 */
transition: background-color 0.2s;
}
.dimension-item:hover {
background-color: rgba(73,134,255,0.2);
}
.dimension-checkbox {
display: flex;
align-items: center;
}
.dimension-checkbox input[type="checkbox"] {
margin-right: 8px;
accent-color: #4986ff;
}
.dimension-weight {
display: flex;
align-items: center;
color: #4986ff;
}
.weight-label {
margin-right: 5px;
}
.weight-value {
font-weight: bold;
}
.dimension-add {
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
background-color: rgba(73,134,255,0.1);
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
transition: background-color 0.2s;
}
.dimension-add:hover {
background-color: rgba(73,134,255,0.2);
}
.add-icon {
font-size: 18px;
margin-right: 5px;
color: #4986ff;
}
.add-text {
color: #4986ff;
font-weight: bold;
}
/* 右侧内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 10px;
overflow: hidden;
max-height: calc(100vh - 100px); /* 限制最大高度 */
}
/* 搜索和操作栏 */
.action-bar {
height: 64px;
display: flex;
justify-content: space-between;
align-items: center;
}
.search-box {
display: flex;
align-items: center;
width: 300px;
background-color: rgba(255,255,255,0.1);
border-radius: 20px;
overflow: hidden;
}
.search-box input {
flex: 1;
background: transparent;
border: none;
padding: 10px 15px;
color: white;
outline: none;
}
.search-box input::placeholder {
color: rgba(255,255,255,0.5);
}
.search-button {
background: transparent;
border: none;
color: white;
padding: 0 15px;
cursor: pointer;
display: flex;
align-items: center;
}
.search-icon {
fill: white;
}
.add-evaluation-btn {
background-color: rgba(14,62,167,1);
color: rgba(255,255,255,1);
border: 1px solid rgba(73,134,255,1);
border-radius: 10px;
padding: 8px 15px;
font-size: 14px;
text-align: center;
font-family: PingFangSC-regular;
cursor: pointer;
}
/* 工程研究中心卡片网格 */
.lab-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
gap: 20px;
overflow-y: auto; /* 允许卡片区域滚动 */
flex: 1;
padding: 20px;
background-color: #262F50;
border-radius: 10px;
}
.lab-card {
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; /* 调整最小高度,确保雷达图有足够空间 */
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.lab-card:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: rgba(73,134,255,0.1);
border-bottom: 1px solid rgba(73,134,255,0.3);
}
.lab-id {
font-size: 14px;
color: rgba(255,255,255,0.7);
}
.total-score {
font-size: 14px;
color: rgba(255,255,255,0.7);
}
.score-value {
font-size: 18px;
font-weight: bold;
color: rgb(63, 196, 15);
}
.card-content {
padding: 15px;
display: flex;
gap: 15px;
}
.lab-image {
width: 40%;
height: 120px;
background-color: rgba(73,134,255,0.1);
border-radius: 4px;
overflow: hidden;
}
.lab-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.lab-info {
width: 60%;
display: flex;
flex-direction: column;
gap: 10px;
}
.info-item {
margin-bottom: 5px;
}
.info-label {
color: rgba(255,255,255,0.7);
margin-right: 5px;
}
.info-value {
color: white;
font-weight: 500;
}
.evaluation-chart {
flex: 1; /* 让雷达图占据剩余空间 */
min-height: 200px; /* 确保雷达图最小高度 */
padding: 10px 15px 15px;
display: flex;
align-items: center;
justify-content: center;
}
.radar-chart {
width: 100%;
height: 100%;
min-height: 180px;
}
/* 二级维度样式 */
.primary-dimension {
flex-direction: column;
align-items: stretch;
margin-bottom: 10px;
}
.dimension-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.dimension-name {
font-weight: 500;
}
.dimension-expand {
color: #4986ff;
font-size: 12px;
}
.sub-dimensions {
margin-top: 5px;
margin-left: 15px;
}
.sub-dimension {
background-color: rgba(255,255,255,0.05);
border-left: 2px solid rgba(73,134,255,0.6);
margin-bottom: 5px;
}
@media (max-width: 1200px) {
.lab-card-grid {
grid-template-columns: 1fr;
}
.dimension-sidebar {
width: 100%;
height: auto;
max-height: 300px;
margin-bottom: 10px;
}
.sidebar-header {
height: auto;
padding: 10px 0;
}
.card-content {
flex-direction: column;
}
.lab-image, .lab-info {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,572 @@
<template>
<el-drawer
v-model="props.visible"
title=""
direction="rtl"
size="600px"
:before-close="handleClose"
custom-class="dimension-drawer"
>
<div class="drawer-content">
<div class="dimension-section">
<h2 class="section-title">工程研究中心评估维度设置</h2>
<!-- 维度列表 -->
<div class="dimensions-list">
<div v-for="(dim, index) in dimensions" :key="index" class="dimension-item primary-dimension">
<div class="dimension-name">
<el-input
v-model="dim.name"
placeholder="请输入一级维度名称"
/>
</div>
<div class="dimension-actions">
<el-button
type="primary"
icon="Plus"
circle
@click="addSubDimension(index)"
class="add-sub-btn"
/>
<el-button
type="danger"
icon="Delete"
circle
@click="removeDimension(index)"
class="delete-btn"
/>
</div>
<!-- 二级维度列表 -->
<div class="sub-dimensions-list">
<div v-for="(subDim, subIndex) in dim.subDimensions" :key="`${index}-${subIndex}`" class="dimension-item sub-dimension">
<div class="dimension-name">
<el-input
v-model="subDim.name"
placeholder="请输入二级维度名称"
/>
</div>
<div class="dimension-weight">
<div class="weight-input-group">
<el-input-number
v-model="subDim.weight"
:min="1"
:max="100"
:step="1"
/>
<span class="weight-unit">%</span>
</div>
<el-button
type="danger"
icon="Delete"
circle
@click="removeSubDimension(index, subIndex)"
class="delete-btn"
/>
</div>
</div>
</div>
</div>
</div>
<!-- 新增一级维度表单 -->
<div class="new-dimension-form">
<div class="form-row">
<div class="form-item">
<span class="label">新增一级维度</span>
<el-input
v-model="newDimension.name"
placeholder="请输入一级维度名称"
class="dimension-input"
/>
</div>
<div class="form-item action-item">
<span class="label">&nbsp;</span>
<div class="action-container">
<el-button
type="primary"
icon="Plus"
circle
class="add-btn"
@click="addDimension"
:disabled="!newDimension.name"
/>
</div>
</div>
</div>
</div>
<!-- 权重总计信息 -->
<div class="weight-summary">
<div class="weight-total">
<span>权重维度要求:</span>
<span class="weight-value">100%</span>
</div>
<div class="weight-current" :class="{ 'weight-error': totalWeight !== 100 }">
<span>当前权重维度总计:</span>
<span class="weight-value">{{ totalWeight }}%</span>
</div>
</div>
</div>
</div>
<template #footer>
<div class="drawer-footer">
<button class="drawer-btn cancel-btn" @click="handleClose">取消</button>
<button
class="drawer-btn confirm-btn"
@click="handleSave"
:disabled="totalWeight !== 100"
>确认</button>
</div>
</template>
</el-drawer>
</template>
<script setup>
import { ref, computed, reactive, watch } from 'vue';
import { ElMessage } from 'element-plus';
import axios from 'axios';
import { getApiBaseUrl } from '../config'; // APIURL
const props = defineProps({
visible: {
type: Boolean,
default: false
},
dimensions: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:visible', 'save']);
// visible
const drawerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
});
//
const dimensions = ref([]);
//
watch(() => props.dimensions, (newDimensions) => {
if (newDimensions && newDimensions.length > 0) {
// props
const dimensionsCopy = JSON.parse(JSON.stringify(newDimensions));
// subDimensions
dimensionsCopy.forEach(dim => {
if (!dim.subDimensions) {
dim.subDimensions = [];
}
});
dimensions.value = dimensionsCopy;
} else {
dimensions.value = [];
}
}, { immediate: true, deep: true });
//
const newDimension = reactive({
name: '',
subDimensions: []
});
//
const totalWeight = computed(() => {
let total = 0;
dimensions.value.forEach(dim => {
dim.subDimensions.forEach(subDim => {
total += (subDim.weight || 0);
});
});
return total;
});
//
const addDimension = () => {
if (!newDimension.name) {
ElMessage.warning('请输入维度名称');
return;
}
//
dimensions.value.push({
name: newDimension.name,
subDimensions: []
});
//
newDimension.name = '';
};
//
const addSubDimension = (parentIndex) => {
// subDimensions
if (!dimensions.value[parentIndex]) {
console.error('维度不存在:', parentIndex);
return;
}
// subDimensions
if (!dimensions.value[parentIndex].subDimensions) {
dimensions.value[parentIndex].subDimensions = [];
}
//
dimensions.value[parentIndex].subDimensions.push({
name: '',
weight: 5
});
};
//
const removeDimension = (index) => {
dimensions.value.splice(index, 1);
};
//
const removeSubDimension = (parentIndex, subIndex) => {
dimensions.value[parentIndex].subDimensions.splice(subIndex, 1);
};
//
const handleClose = (done) => {
// done ( before-close)
if (typeof done === 'function') {
done();
}
//
drawerVisible.value = false;
};
//
const handleSave = async () => {
if (totalWeight.value !== 100) {
ElMessage.error('权重总计必须为100%,请调整后再保存');
return;
}
//
let hasEmptyName = false;
dimensions.value.forEach(dim => {
if (!dim.name) {
hasEmptyName = true;
}
dim.subDimensions.forEach(subDim => {
if (!subDim.name) {
hasEmptyName = true;
}
});
});
if (hasEmptyName) {
ElMessage.error('请为所有维度填写名称');
return;
}
try {
// localStorageJWT
const token = localStorage.getItem('token');
if (!token) {
ElMessage.error('未登录或登录已过期,请重新登录');
return;
}
//
const response = await axios.post(`${getApiBaseUrl()}/dimensions/save`, {
dimensions: dimensions.value,
category: 'lab'
}, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.data && response.data.success) {
//
emit('save', dimensions.value);
//
handleClose();
ElMessage.success('维度设置保存成功');
} else {
throw new Error(response.data?.message || '保存失败');
}
} catch (error) {
console.error('保存维度数据失败:', error);
ElMessage.error(`保存维度数据失败: ${error.message}`);
}
};
</script>
<style>
/* 确保引入common.css */
@import './common.css';
</style>
<style scoped>
.dimension-drawer :deep(.el-drawer__header) {
margin-bottom: 0;
color: white;
background-color: #0c1633;
border-bottom: 1px solid rgba(73,134,255,0.3);
}
.dimension-drawer :deep(.el-drawer__body) {
padding: 0;
overflow: hidden;
background-color: #0c1633;
}
.drawer-content {
padding: 20px;
color: white;
height: 100%;
display: flex;
flex-direction: column;
background-color: #0c1633;
overflow-y: auto;
}
.dimension-section {
display: flex;
flex-direction: column;
height: 100%;
}
.section-title {
margin: 15px 0;
font-size: 18px;
color: rgba(255,255,255,0.9);
text-align: center;
border-bottom: 1px solid rgba(73,134,255,0.3);
padding-bottom: 10px;
}
.dimensions-list {
background-color: #1f3266;
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(73,134,255,0.3);
margin-bottom: 20px;
overflow-y: auto;
flex: 1; /* 让这个元素占满可用空间 */
}
.dimension-item {
padding: 10px;
transition: all 0.3s ease;
}
.primary-dimension {
background-color: rgba(73,134,255,0.1);
border-radius: 8px;
margin-bottom: 15px;
border-left: 3px solid #4986ff;
}
.dimension-name {
flex: 1;
margin-bottom: 10px;
}
.dimension-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-bottom: 10px;
}
.sub-dimensions-list {
margin-left: 20px;
}
.sub-dimension {
display: flex;
justify-content: space-between;
align-items: center;
background-color: rgba(255,255,255,0.05);
border-radius: 4px;
margin-bottom: 8px;
padding: 8px;
}
.sub-dimension .dimension-name {
flex: 1;
margin-bottom: 0;
margin-right: 10px;
}
.dimension-weight {
display: flex;
align-items: center;
gap: 10px;
min-width: 120px;
justify-content: flex-end;
}
.weight-input-group {
display: flex;
align-items: center;
}
.delete-btn, .add-sub-btn {
font-size: 12px;
padding: 6px;
}
.new-dimension-form {
background-color: #1f3266;
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(73,134,255,0.3);
margin-bottom: 20px;
}
.form-row {
display: flex;
gap: 15px;
align-items: flex-end;
}
.form-item {
flex: 1;
display: flex;
flex-direction: column;
}
.action-item {
flex: 0.3;
}
.label {
display: block;
margin-bottom: 8px;
color: rgba(255,255,255,0.7);
}
.action-container {
display: flex;
justify-content: flex-end;
}
.weight-unit {
margin: 0 5px;
color: #4986ff;
}
.weight-summary {
background-color: #1f3266;
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(73,134,255,0.3);
margin-top: 20px;
}
.weight-total, .weight-current {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.weight-value {
font-weight: bold;
color: #4986ff;
}
.weight-error .weight-value {
color: #f56c6c;
}
/* 抽屉页脚 */
.dimension-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;
}
.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);
}
.confirm-btn {
background-color: rgba(14,62,167,1);
color: rgba(255,255,255,1);
border: 1px solid rgba(73,134,255,1);
}
.confirm-btn:disabled {
background-color: rgba(14,62,167,0.5);
border: 1px solid rgba(73,134,255,0.5);
cursor: not-allowed;
}
/* 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-input-number) {
width: 100px;
}
:deep(.el-input-number__decrease),
:deep(.el-input-number__increase) {
background-color: rgba(73,134,255,0.2);
color: white;
}
/* 滚动条样式 */
.drawer-content::-webkit-scrollbar,
.dimensions-list::-webkit-scrollbar {
width: 6px;
}
.drawer-content::-webkit-scrollbar-track,
.dimensions-list::-webkit-scrollbar-track {
background: transparent;
}
.drawer-content::-webkit-scrollbar-thumb,
.dimensions-list::-webkit-scrollbar-thumb {
background-color: #4986ff;
border-radius: 10px;
border: none;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,315 @@
<template>
<div class="detail-page">
<!-- 顶部导航栏 -->
<header class="dashboard-header">
<div class="logo">
<img src="../assets/logo1.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
<img src="../assets/logo2.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
<h1 class="main-title">
<div class="title-line"></div>
<span class="title-text">智慧科研评估系统</span>
<div class="title-line"></div>
</h1>
</div>
</header>
<div class="page-header">
<button class="back-button" @click="goBack">
<el-icon><back /></el-icon> 返回仪表盘
</button>
<h1>科研成果详情</h1>
</div>
<div class="page-content">
<!-- 筛选条件 -->
<div class="filter-panel">
<h2>筛选条件</h2>
<div class="filter-form">
<div class="filter-row">
<div class="filter-item">
<label>时间范围</label>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</div>
<div class="filter-item">
<label>成果类型</label>
<el-select v-model="researchType">
<el-option label="全部" value="" />
<el-option label="论文" value="paper" />
<el-option label="专利" value="patent" />
</el-select>
</div>
<div class="filter-item">
<label>学院</label>
<el-select v-model="college">
<el-option label="全部" value="" />
<el-option label="计算机学院" value="cs" />
<el-option label="材料学院" value="material" />
</el-select>
</div>
</div>
<div class="filter-buttons">
<el-button type="primary">应用筛选</el-button>
<el-button>重置</el-button>
</div>
</div>
</div>
<!-- 数据表格 -->
<div class="table-panel">
<h2>研究成果列表</h2>
<el-table :data="tableData" style="width: 100%" border>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="标题" min-width="250" />
<el-table-column prop="author" label="作者" width="150" />
<el-table-column prop="type" label="类型" width="100" />
<el-table-column prop="journal" label="期刊/来源" width="180" />
<el-table-column prop="date" label="发表日期" width="120" />
<el-table-column prop="citations" label="引用次数" width="100" />
<el-table-column label="操作" width="120">
<template #default>
<el-button size="small">查看</el-button>
<el-button size="small" type="primary">导出</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
background
layout="prev, pager, next"
:total="100"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
//
const emit = defineEmits(['back-to-dashboard', 'logout'])
const goBack = () => {
emit('back-to-dashboard')
}
// Logo
const handleLogoClick = () => {
emit('logout')
}
//
const dateRange = ref([])
const researchType = ref('')
const college = ref('')
//
const tableData = ref([
{
id: 1,
title: '基于深度学习的图像识别算法研究',
author: '张三, 李四',
type: '论文',
journal: 'IEEE Transactions on Image Processing',
date: '2024-10-15',
citations: 25
},
{
id: 2,
title: '新型高效电池材料的合成与表征',
author: '王五, 赵六',
type: '论文',
journal: 'Advanced Materials',
date: '2024-09-22',
citations: 18
},
{
id: 3,
title: '一种新型智能机器人控制方法',
author: '张三, 王五',
type: '专利',
journal: '国家知识产权局',
date: '2024-08-10',
citations: 0
},
{
id: 4,
title: '高性能计算在流体力学中的应用',
author: '李四, 赵六',
type: '论文',
journal: 'Journal of Computational Physics',
date: '2024-07-05',
citations: 12
},
{
id: 5,
title: '纳米材料在催化领域的应用研究',
author: '王五, 张三',
type: '论文',
journal: 'ACS Catalysis',
date: '2024-06-18',
citations: 30
}
])
</script>
<style scoped>
.detail-page {
width: 100%;
min-height: 100vh;
background-color: #0c1633;
color: white;
}
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 60px;
background-color: #1a2b56;
padding: 0 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.logo {
display: flex;
align-items: center;
}
.logo img {
height: 40px;
margin-right: 10px;
}
.main-title {
display: flex;
align-items: center;
margin: 0;
font-size: 18px;
font-weight: bold;
}
.title-line {
width: 20px;
height: 2px;
background-color: #fff;
margin: 0 10px;
}
.title-text {
color: #fff;
}
.page-header {
display: flex;
align-items: center;
padding: 15px 20px;
background-color: #1a2b56;
}
.back-button {
display: flex;
align-items: center;
background: none;
border: none;
color: #4080ff;
cursor: pointer;
margin-right: 20px;
font-size: 14px;
}
.page-header h1 {
margin: 0;
font-size: 20px;
}
.page-content {
padding: 20px;
}
.filter-panel, .table-panel {
background-color: #1a2b56;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
}
.filter-panel h2, .table-panel h2 {
margin-top: 0;
margin-bottom: 20px;
font-size: 18px;
}
.filter-row {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.filter-item {
flex: 1;
}
.filter-item label {
display: block;
margin-bottom: 5px;
}
.filter-buttons {
display: flex;
gap: 10px;
}
.pagination {
margin-top: 20px;
text-align: right;
}
/* Element Plus 暗色主题定制 */
:deep(.el-input__wrapper),
:deep(.el-select .el-input__wrapper) {
background-color: #2a3b66;
box-shadow: none;
}
:deep(.el-input__inner) {
color: white;
}
:deep(.el-table) {
background-color: transparent;
color: white;
}
:deep(.el-table th.el-table__cell) {
background-color: #2a3b66;
color: white;
}
:deep(.el-table tr) {
background-color: #1a2b56;
}
:deep(.el-table td.el-table__cell) {
color: white;
border-bottom-color: rgba(255, 255, 255, 0.1);
}
:deep(.el-button) {
border: 1px solid #4080ff;
color: #4080ff;
background: transparent;
}
:deep(.el-button--primary) {
background-color: #4080ff;
color: white;
}
</style>

View File

@ -0,0 +1,806 @@
<template>
<div class="evaluation-page">
<!-- 顶部导航栏 -->
<header class="dashboard-header">
<div class="logo">
<img src="../assets/logo1.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
<img src="../assets/logo2.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
<h1 class="main-title">
<div class="title-line"></div>
<span class="title-text">智慧科研评估系统</span>
<div class="title-line"></div>
</h1>
</div>
</header>
<!-- 主内容区域 -->
<div class="content-container">
<!-- 左侧维度设置 -->
<div class="dimension-sidebar">
<div class="sidebar-header">
<h1 class="sidebar-title">
<span class="home-link" @click="jumpToDashboard">首页</span>&nbsp;>&nbsp;教师科研人才评估
</h1>
</div>
<div class="dimension-content">
<h2 class="dimension-section-title">评估维度设置</h2>
<div class="dimension-list">
<div v-for="(dim, index) in dimensions" :key="index" class="dimension-item" >
<div class="dimension-checkbox">
<label :for="`dim-${index}`">{{ dim.name }}</label>
</div>
<div class="dimension-weight">
<span class="weight-label">W:</span>
<span class="weight-value">{{ dim.weight }}%</span>
</div>
</div>
<div class="dimension-add" @click="showAddDimensionDialog">
<span class="add-icon">+</span>
<span class="add-text">添加自定义维度</span>
</div>
</div>
</div>
</div>
<!-- 右侧内容区 -->
<div class="main-content">
<!-- 搜索和操作栏 -->
<div class="action-bar">
<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>
</div>
<button class="add-evaluation-btn" @click="openAddEvaluationDrawer">
新增评估
</button>
</div>
<!-- 教师卡片列表 -->
<div class="teacher-card-grid custom-scrollbar">
<div v-for="(teacher, index) in filteredTeachers" :key="index" class="teacher-card" style="height: 300px;" @click="openTeacherDetail(teacher)">
<div class="card-top">
<div class="teacher-left">
<div class="teacher-photo">
<img :src="teacher.photo" alt="教师照片" />
</div>
<div class="teacher-id">ID: {{ teacher.idcode || teacher.id }}</div>
</div>
<div class="teacher-info">
<div class="info-row">
<span class="info-label">姓名:</span>
<span class="info-value">{{ teacher.name }}</span>
</div>
<div class="info-row">
<span class="info-label">教育背景:</span>
<span class="info-value">{{ teacher.education }}</span>
</div>
<div class="info-row">
<span class="info-label">论文:</span>
<span class="info-value">{{ teacher.papers }}</span>
</div>
<div class="info-row">
<span class="info-label">项目:</span>
<span class="info-value">{{ teacher.projects }}</span>
</div>
</div>
</div>
<div class="evaluation-chart">
<div :id="`chart-${index}`" class="radar-chart"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 自定义维度对话框 -->
<el-dialog v-model="dimensionDialogVisible" :title="isEditingDimension ? '编辑维度' : '添加维度'" width="30%" custom-class="dimension-dialog">
<el-form :model="dimensionForm" :rules="dimensionRules" ref="dimensionFormRef" label-position="top">
<el-form-item label="维度名称" prop="name">
<el-input v-model="dimensionForm.name" placeholder="请输入维度名称" />
</el-form-item>
<el-form-item label="权重(W)" prop="weight">
<el-input-number v-model="dimensionForm.weight" :min="1" :max="100" :step="1" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dimensionDialogVisible = false">取消</el-button>
<el-button type="danger" v-if="isEditingDimension" @click="deleteDimension">删除</el-button>
<el-button type="primary" @click="saveDimension">确定</el-button>
</span>
</template>
</el-dialog>
<TalentDrawerDetail
v-model:visible="drawerVisible"
:is-edit="isEditMode"
:dimensions="dimensions"
:teacher-data="selectedTeacher"
@save="handleSaveEvaluation"
/>
<DimensionDrawer
v-model:visible="dimensionDrawerVisible"
:dimensions="dimensions"
category="talent"
@save="handleSaveDimensions"
/>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts/core';
import { RadarChart } from 'echarts/charts';
//
import DimensionDrawer from './DimensionDrawer.vue';
import TalentDrawerDetail from './TalentDrawerDetail.vue';
import {
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import axios from 'axios'; // axios
import { getApiBaseUrl } from '../config'; // APIURL
import { ElMessage } from 'element-plus'; // Element Plus
// echarts
echarts.use([
RadarChart,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
CanvasRenderer
]);
const drawerVisible = ref(false);
const isEditMode = ref(false);
const selectedTeacher = ref(null);
// drawer
const dimensionDrawerVisible = ref(false);
//
const showAddDimensionDialog = () => {
// 使dialog
// dimensionDialogVisible.value = true;
// 使drawer
dimensionDrawerVisible.value = true;
};
//
const handleSaveDimensions = (updatedDimensions) => {
//
dimensions.value = updatedDimensions;
//
updateAllRadarCharts();
dimensionDrawerVisible.value = false;
//
ElMessage.success('维度设置保存成功');
};
// Function to open the drawer for adding a new evaluation
const openAddEvaluationDrawer = () => {
isEditMode.value = false;
selectedTeacher.value = null;
drawerVisible.value = true;
};
// Function to handle save from drawer
const handleSaveEvaluation = (data) => {
if (data._deleted) {
//
const index = teachers.value.findIndex(t => t.id === data.id);
if (index !== -1) {
teachers.value.splice(index, 1);
}
} else if (isEditMode.value) {
// Update existing teacher data
const index = teachers.value.findIndex(t => t.id === data.id);
if (index !== -1) {
teachers.value[index] = { ...data };
}
} else {
// Add new teacher
teachers.value.push({ ...data });
}
// Update filtered teachers
handleSearch();
};
//
const emit = defineEmits(['navigate', 'back-to-dashboard', 'logout']);
// Logo
const handleLogoClick = () => {
emit('logout');
};
// Jump to dashboard page function
const jumpToDashboard = () => {
emit('back-to-dashboard');
};
//
const dimensions = ref([]); // API
//
onMounted(async () => {
try {
//
const response = await axios.get(`${getApiBaseUrl()}/dimensions/talent`);
dimensions.value = response.data;
//
await loadTeachers();
//
handleSearch();
} catch (error) {
console.error('获取维度数据失败:', error);
ElMessage.error('获取维度数据失败');
}
});
//
const loadTeachers = async () => {
try {
const response = await axios.get(`${getApiBaseUrl()}/talents`);
teachers.value = response.data;
//
teachers.value.forEach(teacher => {
// ID
if (!teacher.id) {
teacher.id = 'T' + Math.floor(Math.random() * 10000).toString();
}
// 使使
if (!teacher.photo) {
teacher.photo = '/image/人1.png';
}
//
if (!teacher.educationBackground) {
const eduOptions = [
"清华大学 博士",
"北京大学 博士",
"北京理工大学 博士",
"上海交通大学 博士",
"中国科学院 博士",
"哈尔滨工业大学 博士",
"华中科技大学 博士",
"武汉大学 博士"
];
teacher.educationBackground = eduOptions[Math.floor(Math.random() * eduOptions.length)];
}
//
const baseValue = teacher.id ? parseInt(teacher.id.slice(-2)) : Math.floor(Math.random() * 30);
// dimensions.value
if (dimensions.value.length > 0) {
teacher.evaluationData = dimensions.value.map((_, dimIndex) => {
const offset = (dimIndex * 7 + baseValue) % 35;
return Math.min(95, Math.max(20, Math.floor(Math.random() * 40) + 30 + offset));
});
}
});
handleSearch();
} catch (error) {
console.error('获取教师数据失败:', error);
ElMessage.error('获取教师数据失败');
}
};
//
const dimensionDialogVisible = ref(false);
const isEditingDimension = ref(false);
const currentDimensionIndex = ref(-1);
const dimensionForm = ref({
name: '',
weight: 10
});
const dimensionFormRef = ref(null);
const dimensionRules = {
name: [{ required: true, message: '请输入维度名称', trigger: 'blur' }],
weight: [{ required: true, message: '请输入权重', trigger: 'blur' }]
};
//
const editDimension = (dim, index) => {
isEditingDimension.value = true;
dimensionForm.value = { ...dim };
currentDimensionIndex.value = index;
dimensionDialogVisible.value = true;
};
//
const saveDimension = async () => {
if (!dimensionFormRef.value) return;
await dimensionFormRef.value.validate((valid) => {
if (valid) {
if (isEditingDimension.value && currentDimensionIndex.value >= 0) {
//
dimensions.value[currentDimensionIndex.value] = { ...dimensionForm.value, enabled: true };
} else {
//
dimensions.value.push({ ...dimensionForm.value, enabled: true });
}
dimensionDialogVisible.value = false;
//
updateAllRadarCharts();
}
});
};
//
const deleteDimension = () => {
if (currentDimensionIndex.value >= 0) {
dimensions.value.splice(currentDimensionIndex.value, 1);
dimensionDialogVisible.value = false;
//
updateAllRadarCharts();
}
};
//
const updateAllRadarCharts = () => {
// tick DOM
nextTick(() => {
initRadarCharts();
});
};
//
const searchQuery = ref('');
//
const teachers = ref([]);
//
const filteredTeachers = ref([]);
//
const handleSearch = () => {
if (searchQuery.value === '') {
filteredTeachers.value = teachers.value;
} else {
filteredTeachers.value = teachers.value.filter(teacher =>
teacher.name.includes(searchQuery.value) ||
teacher.id.includes(searchQuery.value)
);
}
};
//
const initRadarCharts = () => {
filteredTeachers.value.forEach((teacher, index) => {
const chartDom = document.getElementById(`chart-${index}`);
if (!chartDom) return;
//
echarts.dispose(chartDom);
const chart = echarts.init(chartDom);
//
const indicators = dimensions.value.map(dim => ({
name: dim.name,
max: 100
}));
//
const baseValue = teacher.id ? parseInt(teacher.id.slice(-2)) : Math.floor(Math.random() * 30);
//
let evaluationData = teacher.evaluationData || [];
if (evaluationData.length !== dimensions.value.length) {
//
evaluationData = dimensions.value.map((_, dimIndex) => {
// 使ID
const offset = (dimIndex * 7 + baseValue) % 35;
// 20-95
return Math.min(95, Math.max(20, Math.floor(Math.random() * 40) + 30 + offset));
});
//
teacher.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 }, // 线RGB(63, 196, 15)
itemStyle: { color: 'rgb(63, 196, 15)' } //
}
]
}
]
});
//
window.addEventListener('resize', () => {
chart.resize();
});
});
};
//
watch(filteredTeachers, () => {
// 使 nextTick DOM
nextTick(() => {
initRadarCharts();
});
}, { deep: true });
onMounted(() => {
//
filteredTeachers.value = teachers.value;
// DOM
nextTick(() => {
initRadarCharts();
});
});
//
const openTeacherDetail = (teacher) => {
selectedTeacher.value = teacher;
isEditMode.value = true;
drawerVisible.value = true;
};
</script>
<style>
@import './common.css';
</style>
<style scoped>
.evaluation-page {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
background-color: #0c1633;
color: white;
overflow: hidden; /* 防止页面整体出现滚动条 */
}
.dashboard-header {
height: 60px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
display: flex;
align-items: center;
position: relative;
width: 100%;
}
.logo img {
height: 40px;
margin-right: 10px;
}
.main-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 28px;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
display: flex;
align-items: center;
white-space: nowrap;
}
.title-line {
border: 2px solid rgba(73,134,255,1);
width: 150px;
}
.title-text {
margin: 0 30px;
}
.content-container {
display: flex;
flex: 1;
padding: 20px;
overflow: hidden;
gap: 20px;
}
/* 特定于TalentDetail的样式 */
.sidebar-header {
height: 64px;
display: flex;
align-items: center;
justify-content: left;
background-color: transparent;
}
.sidebar-title {
font-size: 22px;
font-weight: bold;
color: white;
margin: 0;
text-align: left;
}
.home-link {
text-decoration: underline;
cursor: pointer;
color: #4986ff;
}
.dimension-sidebar {
width: 280px;
display: flex;
flex-direction: column;
max-height: calc(100vh - 100px); /* 限制最大高度 */
}
.dimension-content {
flex: 1;
background-color: #262F50;
border-radius: 10px;
display: flex;
flex-direction: column;
overflow: hidden; /* 加上这个防止内容溢出 */
}
.dimension-section-title {
margin: 15px;
font-size: 16px;
text-align: center;
padding-bottom: 10px;
border-bottom: 1px solid rgba(73,134,255,0.3);
}
.dimension-list {
padding: 0 15px 15px 15px;
display: flex;
flex-direction: column;
gap: 15px;
overflow-y: auto; /* 允许列表滚动 */
flex: 1; /* 让列表占满剩余空间 */
}
.dimension-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background-color: rgba(73,134,255,0.1);
border-radius: 4px;
border-left: 3px solid #4986ff;
cursor: pointer; /* 添加指针样式,提示可点击 */
transition: background-color 0.2s;
}
.dimension-item:hover {
background-color: rgba(73,134,255,0.2);
}
.dimension-checkbox {
display: flex;
align-items: center;
}
.dimension-checkbox input[type="checkbox"] {
margin-right: 8px;
accent-color: #4986ff;
}
.dimension-weight {
display: flex;
align-items: center;
color: #4986ff;
}
.weight-label {
margin-right: 5px;
}
.weight-value {
font-weight: bold;
}
.dimension-add {
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
background-color: rgba(73,134,255,0.1);
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
transition: background-color 0.2s;
}
.dimension-add:hover {
background-color: rgba(73,134,255,0.2);
}
.add-icon {
font-size: 18px;
margin-right: 5px;
color: #4986ff;
}
.add-text {
color: #4986ff;
font-weight: bold;
}
/* 右侧内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 10px;
overflow: hidden;
max-height: calc(100vh - 100px); /* 限制最大高度 */
}
/* 搜索和操作栏 */
.action-bar {
height: 64px;
display: flex;
justify-content: space-between;
align-items: center;
}
.search-box {
display: flex;
align-items: center;
width: 300px;
background-color: rgba(255,255,255,0.1);
border-radius: 20px;
overflow: hidden;
}
.search-box input {
flex: 1;
background: transparent;
border: none;
padding: 10px 15px;
color: white;
outline: none;
}
.search-box input::placeholder {
color: rgba(255,255,255,0.5);
}
.search-button {
background: transparent;
border: none;
color: white;
padding: 0 15px;
cursor: pointer;
display: flex;
align-items: center;
}
.search-icon {
fill: white;
}
.add-evaluation-btn {
background-color: rgba(14,62,167,1);
color: rgba(255,255,255,1);
border: 1px solid rgba(73,134,255,1);
border-radius: 10px;
padding: 8px 15px;
font-size: 14px;
text-align: center;
font-family: PingFangSC-regular;
cursor: pointer;
}
/* 教师卡片网格 */
.teacher-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
overflow-y: auto; /* 允许卡片区域滚动 */
flex: 1;
padding: 20px;
background-color: #262F50;
border-radius: 10px;
}
/* 维度对话框样式 */
:deep(.dimension-dialog) {
background-color: #1f3266;
color: white;
border-radius: 10px;
}
:deep(.dimension-dialog .el-dialog__header) {
color: white;
border-bottom: 1px solid rgba(73,134,255,0.3);
}
:deep(.dimension-dialog .el-dialog__body) {
color: white;
}
:deep(.dimension-dialog .el-input__inner),
:deep(.dimension-dialog .el-input-number__decrease),
:deep(.dimension-dialog .el-input-number__increase) {
background-color: rgba(255,255,255,0.1);
border-color: rgba(73,134,255,0.3);
color: white;
}
:deep(.dimension-dialog .el-form-item__label) {
color: rgba(255,255,255,0.8);
}
:deep(.dimension-dialog .el-dialog__footer) {
border-top: 1px solid rgba(73,134,255,0.3);
}
@media (max-width: 1200px) {
.dimension-sidebar {
width: 100%;
height: auto;
max-height: 300px;
margin-bottom: 10px;
}
.sidebar-header {
height: auto;
padding: 10px 0;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,197 @@
<template>
<div class="token-test">
<h2>Token 测试工具</h2>
<div class="token-info">
<p>当前token信息</p>
<pre>{{ tokenInfo }}</pre>
</div>
<div class="buttons">
<el-button type="primary" @click="testToken">测试Token</el-button>
<el-button type="info" @click="refreshToken">重新登录刷新Token</el-button>
<el-button type="warning" @click="checkUserInfo">获取用户信息</el-button>
</div>
<div v-if="apiTestResult" class="api-result">
<h3>API测试结果</h3>
<pre>{{ apiTestResult }}</pre>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
import { ElMessage } from 'element-plus';
import { getApiBaseUrl } from '../config';
const tokenInfo = ref('未加载');
const apiTestResult = ref('');
onMounted(() => {
loadTokenInfo();
});
const loadTokenInfo = () => {
const token = localStorage.getItem('token');
const username = localStorage.getItem('username');
const isLoggedIn = localStorage.getItem('isLoggedIn');
if (token) {
const tokenParts = token.split('.');
let payload = {};
try {
if (tokenParts.length >= 2) {
payload = JSON.parse(atob(tokenParts[1]));
}
} catch (e) {
console.error('解析token失败', e);
}
tokenInfo.value = {
token: token.substring(0, 15) + '...',
username,
isLoggedIn,
expires: payload.exp ? new Date(payload.exp * 1000).toLocaleString() : '未知',
payload
};
} else {
tokenInfo.value = '未找到token';
}
};
const testToken = async () => {
const token = localStorage.getItem('token');
if (!token) {
ElMessage.warning('未找到token请先登录');
return;
}
try {
// API
const response = await axios.get(`${getApiBaseUrl()}/health`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
apiTestResult.value = {
status: response.status,
data: response.data
};
ElMessage.success('Token有效API测试成功');
} catch (error) {
console.error('API测试失败', error);
apiTestResult.value = {
error: true,
status: error.response?.status,
message: error.response?.data?.detail || error.message
};
ElMessage.error(`API测试失败: ${error.response?.data?.detail || error.message}`);
}
};
const checkUserInfo = async () => {
const token = localStorage.getItem('token');
if (!token) {
ElMessage.warning('未找到token请先登录');
return;
}
try {
// API
const response = await axios.get(`${getApiBaseUrl()}/users/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
apiTestResult.value = {
status: response.status,
data: response.data
};
ElMessage.success('获取用户信息成功');
} catch (error) {
console.error('获取用户信息失败', error);
apiTestResult.value = {
error: true,
status: error.response?.status,
message: error.response?.data?.detail || error.message
};
ElMessage.error(`获取用户信息失败: ${error.response?.data?.detail || error.message}`);
}
};
const refreshToken = async () => {
try {
// 使token
const response = await axios.post(`${getApiBaseUrl()}/token`,
new URLSearchParams({
username: 'admin',
password: '123456'
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
// token
localStorage.setItem('token', response.data.access_token);
localStorage.setItem('username', 'admin');
localStorage.setItem('isLoggedIn', 'true');
loadTokenInfo();
ElMessage.success('Token已刷新');
} catch (error) {
console.error('刷新Token失败', error);
ElMessage.error(`刷新Token失败: ${error.response?.data?.detail || error.message}`);
}
};
</script>
<style scoped>
.token-test {
padding: 20px;
background-color: #0c1633;
color: white;
border-radius: 5px;
max-width: 800px;
margin: 0 auto;
}
.token-info {
background-color: #1f3266;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border: 1px solid rgba(73,134,255,0.3);
}
pre {
white-space: pre-wrap;
word-break: break-word;
background-color: #121c38;
padding: 10px;
border-radius: 5px;
overflow: auto;
max-height: 300px;
}
.buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.api-result {
background-color: #1f3266;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
border: 1px solid rgba(73,134,255,0.3);
}
</style>

303
src/components/common.css Normal file
View File

@ -0,0 +1,303 @@
/* ========================================
共享样式文件 (common.css)
为智慧科研评估系统提供的公共样式
包含基础色彩主题布局结构组件样式和响应式设计
======================================== */
/* ========= 基础色彩变量 ========= */
:root {
/* 背景色系 */
--primary-bg: #0c1633; /* 主背景色 - 深蓝 */
--secondary-bg: #1a2b56; /* 次级背景色 - 面板背景 */
--tertiary-bg: #262F50; /* 三级背景色 - 卡片背景 */
--card-bg: #1f3266; /* 卡片背景色 */
/* 边框色系 */
--primary-border: rgba(73,134,255,1); /* 主边框色 - 蓝色 */
--secondary-border: rgba(73,134,255,0.3); /* 次级边框色 - 半透明蓝 */
/* 强调色系 */
--accent-color: #4080ff; /* 主强调色 - 蓝色 */
--accent-color-alt: #4986ff; /* 次级强调色 */
--accent-color-success: rgb(63, 196, 15); /* 成功色 - 绿色 */
/* 文本色系 */
--text-color: white; /* 主文本色 - 白色 */
--text-color-dim: rgba(255,255,255,0.7); /* 次级文本色 - 半透明白 */
--text-color-dimmer: rgba(255,255,255,0.5); /* 更弱的文本色 */
/* 按钮相关 */
--button-primary-bg: rgba(14,62,167,1); /* 主按钮背景色 */
--button-border: rgba(73,134,255,1); /* 按钮边框色 */
--button-border-light: rgba(73,134,255,0.5); /* 次要按钮边框色 */
}
/* ========= 基础布局和页面结构 ========= */
/* 主容器样式 */
.dashboard, .evaluation-page {
width: 100%;
min-height: 100vh;
background-color: var(--primary-bg);
color: var(--text-color);
display: flex;
flex-direction: column;
}
/* ========= 头部导航栏样式 ========= */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
height: 60px;
}
/* Logo区域样式 */
.logo {
display: flex;
align-items: center;
position: relative;
width: 100%;
}
.logo img {
height: 40px;
margin-right: 10px;
}
/* 标题样式 */
.main-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
font-family: PingFangSC-bold;
display: flex;
align-items: center;
white-space: nowrap;
}
.title-line {
border: 2px solid var(--primary-border);
width: 200px;
}
.title-text {
margin: 0 50px;
}
/* ========= 内容区域布局 ========= */
.dashboard-content, .content-container {
padding: 15px;
display: flex;
gap: 15px;
flex: 1;
overflow: hidden;
}
.dashboard-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
}
/* ========= 面板和卡片样式 ========= */
.dashboard-panel {
background-color: var(--secondary-bg);
border-radius: 5px;
padding: 15px;
flex: 1;
display: flex;
flex-direction: column;
}
.dashboard-panel h2 {
margin-top: 0;
margin-bottom: 15px;
font-size: 16px;
text-align: center;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.panel-header h2 {
margin: 0;
text-align: center;
flex: 1;
}
.panel-link {
color: var(--accent-color);
background: none;
border: none;
font-size: 14px;
cursor: pointer;
}
/* ========= 教师卡片和信息样式 ========= */
.teacher-card {
background-color: var(--card-bg);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
border: 1px solid var(--secondary-border);
min-height: 450px;
}
.card-top {
display: flex;
padding: 15px;
border-bottom: 1px solid var(--secondary-border);
flex: 0 0 auto;
}
.teacher-left {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 15px;
}
.teacher-photo {
width: 100px;
height: 120px;
overflow: hidden;
margin-bottom: 5px;
background-color: rgba(73,134,255,0.1);
border-radius: 4px;
}
.teacher-photo img {
width: 100%;
height: 100%;
object-fit: cover;
}
.teacher-id {
font-size: 12px;
color: rgba(255,255,255,0.8);
}
.teacher-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.info-row {
display: flex;
margin-bottom: 8px;
}
.info-label {
width: 80px;
color: var(--text-color-dim);
}
.info-value {
font-weight: bold;
}
/* ========= 按钮样式 ========= */
.add-evaluation-btn, .drawer-btn {
padding: 8px 15px;
border-radius: 10px;
font-size: 14px;
text-align: center;
font-family: PingFangSC-regular;
cursor: pointer;
}
.add-evaluation-btn, .confirm-btn {
background-color: var(--button-primary-bg);
color: var(--text-color);
border: 1px solid var(--button-border);
}
.cancel-btn {
background-color: transparent;
color: rgba(255,255,255,0.8);
border: 1px solid var(--button-border-light);
}
/* ========= 图表容器样式 ========= */
.chart-container {
width: 100%;
flex: 1;
}
.radar-chart {
width: 100%;
height: 100%;
}
.evaluation-chart {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
min-height: 250px;
}
/* ========= 自定义滚动条 ========= */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: var(--accent-color-alt) transparent;
}
.custom-scrollbar::-webkit-scrollbar,
.drawer-content::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track,
.drawer-content::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb,
.drawer-content::-webkit-scrollbar-thumb {
background-color: var(--accent-color-alt);
border-radius: 10px;
border: none;
}
.custom-scrollbar::-webkit-scrollbar-button {
display: none;
}
/* ========= 响应式布局 ========= */
@media (max-width: 1200px) {
.dashboard-content, .content-container {
flex-direction: column;
}
.main-title {
font-size: 24px;
}
.title-line {
width: 100px;
}
.title-text {
margin: 0 20px;
}
.teacher-card-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.teacher-card {
min-height: 400px;
}
}

171
src/components/login.vue Normal file
View File

@ -0,0 +1,171 @@
<script setup>
import { ref } from 'vue';
import { User, Lock } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { getApiBaseUrl } from '../config';
const username = ref('');
const password = ref('');
const emit = defineEmits(['login-success']);
const loading = ref(false);
const login = async () => {
if (!username.value || !password.value) {
ElMessage.warning('请输入用户名和密码');
return;
}
loading.value = true;
try {
// API
const response = await fetch(`${getApiBaseUrl()}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
username: username.value,
password: password.value,
}),
});
const data = await response.json();
if (response.ok) {
// tokenlocalStorage
localStorage.setItem('token', data.access_token);
// localStorage
localStorage.setItem('username', username.value);
//
localStorage.setItem('isLoggedIn', 'true');
//
ElMessage.success('登录成功');
emit('login-success');
} else {
//
ElMessage.error(data.detail || '用户名或密码错误');
}
} catch (error) {
console.error('登录请求出错:', error);
ElMessage.error('登录请求失败,请稍后再试');
} finally {
loading.value = false;
}
};
</script>
<template>
<div class="login-container">
<div class="login-left">
<img src="../assets/loginBackground.png" alt="Background" class="login-background" />
</div>
<div class="login-right">
<div class="login-header">
<img src="../assets/logo1.png" alt="北京理工大学" class="bit-logo" />
<h1 class="system-title">智慧科研评估系统</h1>
</div>
<div class="login-form">
<div class="form-item">
<el-input
v-model="username"
placeholder="用户名"
prefix-icon="User"
size="large"
/>
</div>
<div class="form-item">
<el-input
v-model="password"
type="password"
placeholder="密码"
prefix-icon="Lock"
size="large"
/>
</div>
<el-button
type="primary"
@click="login"
class="login-button"
size="large"
:loading="loading"
>登录</el-button>
<div class="forgot-password">
忘记密码请联系系统管理员
</div>
</div>
</div>
</div>
</template>
<style scoped>
.login-container {
display: flex;
width: 100%;
height: 100vh;
background-color: #fff;
}
.login-left {
flex: 1;
background: linear-gradient(to bottom right, #e6f7ff, #f0f9ff);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.login-background {
width: 100%;
height: 100%;
object-fit: cover;
}
.login-right {
width: 500px;
padding: 40px;
display: flex;
flex-direction: column;
justify-content: center;
}
.login-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px;
}
.bit-logo {
width: 80px;
height: 80px;
margin-bottom: 20px;
}
.system-title {
font-size: 24px;
color: #333;
font-weight: bold;
text-align: center;
}
.login-form {
width: 100%;
}
.form-item {
margin-bottom: 20px;
}
.login-button {
width: 100%;
margin-top: 10px;
}
.forgot-password {
text-align: center;
margin-top: 15px;
color: #909399;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,484 @@
<template>
<div class="evaluation-page">
<!-- 顶部导航栏 -->
<header class="dashboard-header">
<div class="logo">
<img src="../assets/logo1.png" alt="北京理工大学" @click="jumpToDashboard" style="cursor: pointer;" />
<img src="../assets/logo2.png" alt="北京理工大学" @click="jumpToDashboard" style="cursor: pointer;" />
<h1 class="main-title">
<div class="title-line"></div>
<span class="title-text">智慧科研评估系统</span>
<div class="title-line"></div>
</h1>
</div>
</header>
<!-- 主内容区域 -->
<div class="content-container">
<!-- 右侧内容区 -->
<div class="main-content">
<!-- 搜索和操作栏 -->
<div class="action-bar" style="margin-left: 170px;">
<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>
</div>
<button class="add-evaluation-btn" @click="openAddEvaluationDrawer" style="margin-right: 170px;">
新增评估
</button>
</div>
<!-- 工程研究中心卡片列表 -->
<div class="lab-card-grid custom-scrollbar">
<img src="../assets/实验室贴图.png" alt="工程研究中心列表" class="lab-list-image" />
</div>
</div>
</div>
</div>
<LabDrawerDetail
v-model:visible="drawerVisible"
:is-edit="isEditMode"
:dimensions="dimensions"
:lab-data="selectedLab"
@save="handleSaveEvaluation"
/>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts/core';
import { RadarChart } from 'echarts/charts';
import LabDrawerDetail from './LabDrawerDetail.vue';
import {
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import { getApiBaseUrl } from '../../config';
// echarts
echarts.use([
RadarChart,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
CanvasRenderer
]);
const drawerVisible = ref(false);
const isEditMode = ref(false);
const selectedLab = ref(null);
//
const openAddEvaluationDrawer = () => {
isEditMode.value = false;
selectedLab.value = null;
drawerVisible.value = true;
};
//
const handleSaveEvaluation = (data) => {
if (isEditMode.value) {
//
const index = labs.value.findIndex(l => l.id === data.id);
if (index !== -1) {
labs.value[index] = { ...data };
}
} else {
//
labs.value.push({ ...data });
}
//
handleSearch();
};
//
const emit = defineEmits(['navigate']);
//
const jumpToDashboard = () => {
console.log('正在尝试跳转到仪表盘...');
// emit
if (typeof emit !== 'function') {
console.error('emit 不是一个函数,无法触发事件');
return;
}
//
emit('navigate', 'dashboard');
//
console.log('已触发 navigate 事件,参数为:', 'dashboard');
//
// <LabDetail @navigate="handleNavigate" />
};
//
const dimensions = ref([
{ name: '工程技术研发能力与水平 (30%)', weight: 30, enabled: true },
{ name: '创新水平', weight: 10, enabled: true },
{ name: '人才与队伍', weight: 10, enabled: true },
{ name: '装备与场地', weight: 10, enabled: true },
{ name: '成果转化与行业贡献 (30%)', weight: 30, enabled: true },
{ name: '学科发展与人才培养 (20%)', weight: 20, enabled: true },
{ name: '开放与运行管理 (20%)', weight: 20, enabled: true }
]);
//
const searchQuery = ref('');
// token
const getToken = () => {
return localStorage.getItem('token');
};
// API
const labs = ref([]);
const filteredLabs = ref([]);
const loading = ref(true);
// API
const fetchLabs = async () => {
try {
const response = await fetch(`${getApiBaseUrl()}/labs`, {
headers: {
'Authorization': `Bearer ${getToken()}`
}
});
if (response.ok) {
const data = await response.json();
labs.value = data;
handleSearch(); // filteredLabs
} else {
console.error('获取工程研究中心数据失败:', response.statusText);
}
} catch (error) {
console.error('获取工程研究中心数据出错:', error);
} finally {
loading.value = false;
}
};
// -
onMounted(() => {
fetchLabs();
});
//
const handleSearch = () => {
if (!searchQuery.value.trim()) {
filteredLabs.value = [...labs.value];
return;
}
const query = searchQuery.value.toLowerCase().trim();
filteredLabs.value = labs.value.filter(lab =>
lab.name.toLowerCase().includes(query) ||
lab.id.toLowerCase().includes(query)
);
};
//
function generateLabImagePlaceholder() {
//
const imageTypes = [
//
`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%234986ff' opacity='0.2'/%3E%3Cpath d='M50,180 L50,80 L150,30 L250,80 L250,180 Z' fill='%234986ff' opacity='0.4'/%3E%3Crect x='90' y='120' width='40' height='60' fill='%234986ff' opacity='0.6'/%3E%3Crect x='170' y='120' width='40' height='60' fill='%234986ff' opacity='0.6'/%3E%3C/svg%3E`,
//
`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%234986ff' opacity='0.2'/%3E%3Crect x='20' y='40' width='260' height='120' fill='%234986ff' opacity='0.3'/%3E%3Ccircle cx='70' cy='70' r='20' fill='%234986ff' opacity='0.5'/%3E%3Crect x='120' y='50' width='140' height='40' fill='%234986ff' opacity='0.4'/%3E%3Crect x='120' y='110' width='140' height='30' fill='%234986ff' opacity='0.4'/%3E%3C/svg%3E`,
//
`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%234986ff' opacity='0.2'/%3E%3Cpath d='M50,30 L250,30 L250,170 L50,170 Z' fill='%234986ff' opacity='0.3'/%3E%3Cpath d='M80,50 L220,50 L220,150 L80,150 Z' fill='%23fff' opacity='0.2'/%3E%3Cpath d='M90,60 L110,60 L110,80 L90,80 Z' fill='%234986ff' opacity='0.5'/%3E%3Cpath d='M130,60 L150,60 L150,80 L130,80 Z' fill='%234986ff' opacity='0.5'/%3E%3Cpath d='M170,60 L190,60 L190,80 L170,80 Z' fill='%234986ff' opacity='0.5'/%3E%3Cpath d='M90,100 L110,100 L110,120 L90,120 Z' fill='%234986ff' opacity='0.5'/%3E%3Cpath d='M130,100 L150,100 L150,120 L130,120 Z' fill='%234986ff' opacity='0.5'/%3E%3Cpath d='M170,100 L190,100 L190,120 L170,120 Z' fill='%234986ff' opacity='0.5'/%3E%3C/svg%3E`,
//
`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%234986ff' opacity='0.1'/%3E%3Cpath d='M60,20 L240,20 L240,180 L60,180 Z' fill='%234986ff' opacity='0.2'/%3E%3Cpath d='M80,40 L140,40 L140,160 L80,160 Z' fill='%234986ff' opacity='0.3'/%3E%3Cpath d='M160,40 L220,40 L220,160 L160,160 Z' fill='%234986ff' opacity='0.3'/%3E%3Cline x1='80' y1='60' x2='140' y2='60' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='80' y1='80' x2='140' y2='80' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='80' y1='100' x2='140' y2='100' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='80' y1='120' x2='140' y2='120' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='80' y1='140' x2='140' y2='140' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='160' y1='60' x2='220' y2='60' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='160' y1='80' x2='220' y2='80' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='160' y1='100' x2='220' y2='100' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='160' y1='120' x2='220' y2='120' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='160' y1='140' x2='220' y2='140' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3C/svg%3E`
];
//
return imageTypes[Math.floor(Math.random() * imageTypes.length)];
}
//
const initRadarCharts = () => {
filteredLabs.value.forEach((lab, index) => {
const chartDom = document.getElementById(`lab-chart-${index}`);
if (!chartDom) return;
//
echarts.dispose(chartDom);
const chart = echarts.init(chartDom);
//
const indicators = [
{ name: '创新水平', max: 100 },
{ name: '研究能力', max: 100 },
{ name: '成果转化', max: 100 },
{ name: '学科建设', max: 100 },
{ name: '行业贡献', max: 100 },
{ name: '发展潜力', max: 100 }
];
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: lab.evaluationData,
name: '评估结果',
areaStyle: { opacity: 0 },
lineStyle: { color: 'rgb(238, 23, 143)', width: 2 }, // 线
itemStyle: { color: 'rgb(238, 23, 143)' } //
}
]
}
]
});
//
window.addEventListener('resize', () => {
chart.resize();
});
});
};
//
watch(filteredLabs, () => {
// 使 setTimeout DOM
setTimeout(() => {
initRadarCharts();
}, 100);
}, { deep: true });
onMounted(() => {
//
filteredLabs.value = labs.value;
// DOM
setTimeout(() => {
initRadarCharts();
}, 100);
});
</script>
<style>
@import './common.css';
</style>
<style scoped>
.evaluation-page {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
/* background-color: #0c1633; */
color: white;
overflow: hidden;
}
.dashboard-header {
height: 60px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
display: flex;
align-items: center;
position: relative;
width: 100%;
}
.logo img {
height: 40px;
margin-right: 10px;
}
.main-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 28px;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
display: flex;
align-items: center;
white-space: nowrap;
}
.title-line {
border: 2px solid rgba(73,134,255,1);
width: 150px;
}
.title-text {
margin: 0 30px;
}
.content-container {
display: flex;
flex: 1;
padding: 20px;
overflow: hidden;
gap: 20px;
}
.dimension-image, .lab-list-image {
width: 100%;
height: 100%;
object-fit: contain;
}
/* 特定于LabDetail的样式 */
.sidebar-header {
height: 64px;
display: flex;
align-items: center;
justify-content: left;
background-color: transparent;
}
.sidebar-title {
font-size: 22px;
font-weight: bold;
color: white;
margin: 0;
text-align: left;
}
.home-link {
text-decoration: underline;
cursor: pointer;
color: #4986ff;
}
.dimension-sidebar {
width: 280px;
display: flex;
flex-direction: column;
}
.dimension-content {
flex: 1;
/* background-color: #262F50; */
border-radius: 10px;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 右侧内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 10px;
overflow: hidden;
}
/* 搜索和操作栏 */
.action-bar {
height: 64px;
display: flex;
justify-content: space-between;
align-items: center;
}
.search-box {
display: flex;
align-items: center;
width: 300px;
background-color: rgba(255,255,255,0.1);
border-radius: 20px;
overflow: hidden;
}
.search-box input {
flex: 1;
background: transparent;
border: none;
padding: 10px 15px;
color: white;
outline: none;
}
.search-box input::placeholder {
color: rgba(255,255,255,0.5);
}
.search-button {
background: transparent;
border: none;
color: white;
padding: 0 15px;
cursor: pointer;
display: flex;
align-items: center;
}
.search-icon {
fill: white;
}
.add-evaluation-btn {
/* background-color: rgba(14,62,167,1); */
color: rgba(255,255,255,1);
border: 1px solid rgba(73,134,255,1);
border-radius: 10px;
padding: 8px 15px;
font-size: 14px;
text-align: center;
font-family: PingFangSC-regular;
cursor: pointer;
}
/* 工程研究中心卡片网格 */
.lab-card-grid {
display: flex;
flex: 1;
overflow: hidden;
/* background-color: #262F50; */
border-radius: 10px;
}
@media (max-width: 1200px) {
.dimension-sidebar {
width: 100%;
height: auto;
max-height: 300px;
margin-bottom: 10px;
}
.sidebar-header {
height: auto;
padding: 10px 0;
}
}
</style>

View File

@ -0,0 +1,707 @@
<template>
<el-drawer
v-model="drawerVisible"
:title="drawerTitle"
direction="rtl"
size="900px"
:before-close="handleClose"
custom-class="lab-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"
>
发送
</el-button>
</div>
<div class="detail-content-image">
<img src="../assets/实验室详情贴图.png" alt="工程研究中心详情" class="lab-detail-image" />
</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">选择文件</el-button>
<div class="el-upload__tip">请上传文档支持PDFDOCDOCX格式</div>
</el-upload>
<div class="detail-content-image">
<img src="../assets/实验室详情贴图.png" alt="工程研究中心详情" class="lab-detail-image" />
</div>
</div>
<!-- 手动输入标签页 -->
<div v-else class="tab-content">
<div class="manual-input-tip">请手动填写以下信息</div>
<div class="detail-content-image">
<img src="../assets/实验室详情贴图.png" alt="工程研究中心详情" class="lab-detail-image" />
</div>
</div>
<!-- 工程研究中心信息表单 - 仅在数据加载后显示 -->
<div v-if="dataLoaded" class="lab-info-form">
<div class="form-header">
<div class="image-section">
<div class="lab-image">
<img :src="formData.image || defaultImage" alt="工程研究中心照片" />
</div>
<div class="lab-id">ID: {{ formData.id || 'BLG00000' }}</div>
</div>
<div class="basic-info">
<div class="form-row">
<div class="form-item full-width">
<span class="label">工程研究中心名称:</span>
<el-input v-model="formData.name" placeholder="请输入工程研究中心名称" />
</div>
</div>
<div class="form-row">
<div class="form-item">
<span class="label">人员数量:</span>
<el-input v-model="formData.personnel" placeholder="请输入人员数量" />
</div>
<div class="form-item">
<span class="label">国家级项目:</span>
<el-input v-model="formData.nationalProjects" placeholder="请输入国家级项目数" />
</div>
<div class="form-item">
<span class="label">其它项目:</span>
<el-input v-model="formData.otherProjects" placeholder="请输入其它项目数" />
</div>
</div>
<div class="form-row">
<div class="form-item">
<span class="label">成果:</span>
<el-input v-model="formData.achievements" 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.labAchievements"
:rows="8"
placeholder="请输入工程研究中心其他成就信息"
/>
</div>
</div>
<!-- 雷达图 -->
<div class="evaluation-chart-section">
<div id="lab-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 class="drawer-btn confirm-btn" @click="handleSave">确定</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 } from 'element-plus';
// 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: () => []
},
labData: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['update:visible', 'save']);
// visible
const drawerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
});
//
const defaultImage = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%234986ff' opacity='0.2'/%3E%3Crect x='20' y='40' width='260' height='120' fill='%234986ff' opacity='0.3'/%3E%3Ccircle cx='70' cy='70' r='20' fill='%234986ff' opacity='0.5'/%3E%3Crect x='120' y='50' width='140' height='40' fill='%234986ff' opacity='0.4'/%3E%3Crect x='120' y='110' width='140' height='30' fill='%234986ff' opacity='0.4'/%3E%3C/svg%3E`;
//
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: 'BLG45187',
name: '',
personnel: '',
nationalProjects: '',
otherProjects: '',
achievements: '',
image: '',
labAchievements: '',
evaluationData: [60, 60, 60, 60, 60, 60] //
});
//
let chart = null;
// props
watch(() => props.labData, (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 && dataLoaded.value) {
//
nextTick(() => {
initRadarChart();
});
}
});
//
onMounted(() => {
if (props.visible && dataLoaded.value) {
initRadarChart();
}
});
//
const initRadarChart = () => {
const chartDom = document.getElementById('lab-evaluation-radar-chart');
if (!chartDom) return;
//
if (chart) {
chart.dispose();
}
chart = echarts.init(chartDom);
// dimensions
const indicators = [
{ name: '创新水平', max: 100 },
{ name: '研究能力', max: 100 },
{ name: '成果转化', max: 100 },
{ name: '学科建设', max: 100 },
{ name: '行业贡献', max: 100 },
{ name: '发展潜力', max: 100 }
];
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: formData.evaluationData,
name: '评估结果',
areaStyle: { opacity: 0 },
lineStyle: { color: 'rgb(238, 23, 143)', width: 2 }, // 线
itemStyle: { color: 'rgb(238, 23, 143)' } //
}
]
}
]
});
//
window.addEventListener('resize', () => {
chart && chart.resize();
});
};
//
const handleFileChange = (file) => {
selectedFile.value = file;
//
setTimeout(() => {
fetchMockData();
}, 500);
};
// URL
const fetchDataFromUrl = async () => {
if (!urlInput.value) {
return;
}
// 使
fetchMockData();
};
//
const fetchMockData = () => {
//
const response = {
id: 'BLG45187',
name: '基础力学教学实验中心',
personnel: '30人',
nationalProjects: '45项',
otherProjects: '11项',
achievements: '28项',
image: defaultImage,
labAchievements: `基础力学教学实验中心成立于2018年是北京理工大学重点建设的工程研究中心之一。致力于力学基础理论与应用研究为工业和国防科技提供科研支持。
工程研究中心拥有先进的力学测试设备和模拟系统在材料力学流体力学等领域具有领先优势近年来承担多项国家重点研发计划项目
工程研究中心现有研究人员26人其中教授5人副教授8人讲师和工程技术人员13人拥有博士生导师6人硕士生导师10人
近五年承担国家重点研发计划项目3项国家自然科学基金项目12项省部级科研项目25项企业合作项目30余项
近五年发表SCI论文120余篇获得国家发明专利35项省部级科技奖励5项多项成果已在航空航天高端装备制造等领域实现转化应用`,
evaluationData: [85, 78, 92, 76, 88, 80]
};
// 使
Object.assign(formData, response);
//
dataLoaded.value = true;
//
nextTick(() => {
initRadarChart();
});
};
//
const handleClose = () => {
drawerVisible.value = false;
//
if (!props.isEdit) {
dataLoaded.value = false;
}
};
// token
const getToken = () => {
return localStorage.getItem('token');
};
//
const handleSave = async () => {
try {
//
const labData = { ...formData.value };
// POSTAPI
const response = await fetch('http://localhost:8000/labs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`
},
body: JSON.stringify(labData)
});
if (response.ok) {
const savedData = await response.json();
emit('save', savedData);
ElMessage.success('保存成功');
handleClose();
} else {
const error = await response.json();
ElMessage.error(`保存失败: ${error.detail || '未知错误'}`);
}
} catch (error) {
console.error('保存请求出错:', error);
ElMessage.error('保存失败,请稍后再试');
}
};
</script>
<style>
@import './common.css';
.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>
.lab-drawer :deep(.el-drawer__header) {
margin-bottom: 0;
color: white;
background-color: #0c1633;
border-bottom: 1px solid rgba(73,134,255,0.3);
}
.lab-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;
}
.el-upload__tip {
color: rgba(255,255,255,0.7);
margin-top: 5px;
}
/* 手动输入提示 */
.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);
}
.image-section {
margin-right: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.lab-image {
width: 150px;
height: 100px;
overflow: hidden;
border-radius: 4px;
margin-bottom: 10px;
}
.lab-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.lab-id {
font-size: 14px;
color: rgba(255,255,255,0.7);
}
.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; /* 确保最小高度 */
}
/* 抽屉页脚 */
.lab-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;
}
/* 按钮样式 */
.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);
}
.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;
}
.detail-content-image {
margin-top: 20px;
width: 100%;
display: flex;
justify-content: center;
}
.lab-detail-image {
width: 100%;
max-width: 800px;
object-fit: contain;
}
</style>

View File

@ -0,0 +1,455 @@
<template>
<div class="evaluation-page">
<!-- 顶部导航栏 -->
<header class="dashboard-header">
<div class="logo">
<img src="../assets/logo1.png" alt="北京理工大学" @click="jumpToDashboard" style="cursor: pointer;" />
<img src="../assets/logo2.png" alt="北京理工大学" @click="jumpToDashboard" style="cursor: pointer;" />
<h1 class="main-title">
<div class="title-line"></div>
<span class="title-text">智慧科研评估系统</span>
<div class="title-line"></div>
</h1>
</div>
</header>
<!-- 主内容区域 -->
<div class="content-container">
<!-- 右侧内容区 -->
<div class="main-content">
<!-- 搜索和操作栏 -->
<div class="action-bar">
<div class="search-box" style="margin-left: 170px;">
<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>
</div>
<button class="add-evaluation-btn" @click="openAddEvaluationDrawer" style="margin-right: 170px;">
新增评估
</button>
</div>
<!-- 教师卡片列表 -->
<div class="teacher-card-grid custom-scrollbar">
<img src="../assets/人才贴图.png" alt="人才列表" class="talent-list-image" />
</div>
</div>
</div>
</div>
<TalentDrawerDetail
v-model:visible="drawerVisible"
:is-edit="isEditMode"
:dimensions="dimensions"
:teacher-data="selectedTeacher"
@save="handleSaveEvaluation"
/>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts/core';
import { RadarChart } from 'echarts/charts';
import TalentDrawerDetail from './TalentDrawerDetail.vue';
import {
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import { getApiBaseUrl } from '../../config';
// echarts
echarts.use([
RadarChart,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
CanvasRenderer
]);
const drawerVisible = ref(false);
const isEditMode = ref(false);
const selectedTeacher = ref(null);
// Function to open the drawer for adding a new evaluation
const openAddEvaluationDrawer = () => {
isEditMode.value = false;
selectedTeacher.value = null;
drawerVisible.value = true;
};
// Function to handle save from drawer
const handleSaveEvaluation = (data) => {
if (isEditMode.value) {
// Update existing teacher data
const index = teachers.value.findIndex(t => t.id === data.id);
if (index !== -1) {
teachers.value[index] = { ...data };
}
} else {
// Add new teacher
teachers.value.push({ ...data });
}
// Update filtered teachers
handleSearch();
};
//
const emit = defineEmits(['navigate']);
//
const jumpToDashboard = () => {
emit('navigate', 'dashboard');
// 使 (使 vue-router)
// router.push('/dashboard');
};
//
const redirectToResearchEvaluation = () => {
window.open('http://82.156.236.221:10004/login', '_blank');
};
//
const dimensions = ref([
{ name: '教育和工作经历', weight: 10, enabled: true },
{ name: '研究方向前沿性', weight: 8, enabled: true },
{ name: '主持科研项目情况', weight: 12, enabled: true },
{ name: '科研成果质量', weight: 16, enabled: true },
{ name: '教学能力与效果', weight: 14, enabled: true },
{ name: '学术服务与影响力', weight: 40, enabled: true }
]);
//
const searchQuery = ref('');
// token
const getToken = () => {
return localStorage.getItem('token');
};
// API
const teachers = ref([]);
const filteredTeachers = ref([]);
const loading = ref(true);
// API
const fetchTalents = async () => {
try {
const response = await fetch(`${getApiBaseUrl()}/talents`, {
headers: {
'Authorization': `Bearer ${getToken()}`
}
});
if (response.ok) {
const data = await response.json();
teachers.value = data;
handleSearch(); // filteredTeachers
} else {
console.error('获取人才数据失败:', response.statusText);
}
} catch (error) {
console.error('获取人才数据出错:', error);
} finally {
loading.value = false;
}
};
// -
onMounted(() => {
fetchTalents();
});
//
const handleSearch = () => {
if (!searchQuery.value.trim()) {
filteredTeachers.value = [...teachers.value];
return;
}
const query = searchQuery.value.toLowerCase().trim();
filteredTeachers.value = teachers.value.filter(teacher =>
teacher.name.toLowerCase().includes(query) ||
teacher.id.toLowerCase().includes(query)
);
};
//
const initRadarCharts = () => {
filteredTeachers.value.forEach((teacher, index) => {
const chartDom = document.getElementById(`chart-${index}`);
if (!chartDom) return;
//
echarts.dispose(chartDom);
const chart = echarts.init(chartDom);
//
const indicators = dimensions.value.map(dim => ({
name: dim.name,
max: 100
}));
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: teacher.evaluationData,
name: '评估结果',
areaStyle: { opacity: 0 }, //
lineStyle: { color: 'rgb(63, 196, 15)', width: 2 }, // 线RGB(63, 196, 15)
itemStyle: { color: 'rgb(63, 196, 15)' } //
}
]
}
]
});
//
window.addEventListener('resize', () => {
chart.resize();
});
});
};
//
watch(filteredTeachers, () => {
// 使 nextTick DOM
setTimeout(() => {
initRadarCharts();
}, 100);
}, { deep: true });
onMounted(() => {
//
filteredTeachers.value = teachers.value;
// DOM
setTimeout(() => {
initRadarCharts();
}, 100);
});
</script>
<style>
@import './common.css';
</style>
<style scoped>
.evaluation-page {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
background-color: #0c1633;
color: white;
overflow: hidden;
}
.dashboard-header {
height: 60px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
display: flex;
align-items: center;
position: relative;
width: 100%;
}
.logo img {
height: 40px;
margin-right: 10px;
}
.main-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 28px;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
display: flex;
align-items: center;
white-space: nowrap;
}
.title-line {
border: 2px solid rgba(73,134,255,1);
width: 150px;
}
.title-text {
margin: 0 30px;
}
.content-container {
display: flex;
flex: 1;
padding: 20px;
overflow: hidden;
gap: 20px;
}
.dimension-image, .talent-list-image {
width: 100%;
height: 100%;
object-fit: contain;
}
/* 特定于TalentDetail的样式 */
.sidebar-header {
height: 64px;
display: flex;
align-items: center;
justify-content: left;
background-color: transparent;
}
.sidebar-title {
font-size: 22px;
font-weight: bold;
color: white;
margin: 0;
text-align: left;
}
.home-link {
text-decoration: underline;
cursor: pointer;
color: #4986ff;
}
.dimension-sidebar {
width: 280px;
display: flex;
flex-direction: column;
}
.dimension-content {
flex: 1;
background-color: #262F50;
border-radius: 10px;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 右侧内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 10px;
overflow: hidden;
}
/* 搜索和操作栏 */
.action-bar {
height: 64px;
display: flex;
justify-content: space-between;
align-items: center;
}
.search-box {
display: flex;
align-items: center;
width: 300px;
background-color: rgba(255,255,255,0.1);
border-radius: 20px;
overflow: hidden;
}
.search-box input {
flex: 1;
background: transparent;
border: none;
padding: 10px 15px;
color: white;
outline: none;
}
.search-box input::placeholder {
color: rgba(255,255,255,0.5);
}
.search-button {
background: transparent;
border: none;
color: white;
padding: 0 15px;
cursor: pointer;
display: flex;
align-items: center;
}
.search-icon {
fill: white;
}
.add-evaluation-btn {
background-color: rgba(14,62,167,1);
color: rgba(255,255,255,1);
border: 1px solid rgba(73,134,255,1);
border-radius: 10px;
padding: 8px 15px;
font-size: 14px;
text-align: center;
font-family: PingFangSC-regular;
cursor: pointer;
}
/* 教师卡片网格 */
.teacher-card-grid {
display: flex;
flex: 1;
overflow: hidden;
/* background-color: #262F50; */
border-radius: 10px;
}
@media (max-width: 1200px) {
.dimension-sidebar {
width: 100%;
height: auto;
max-height: 300px;
margin-bottom: 10px;
}
.sidebar-header {
height: auto;
padding: 10px 0;
}
}
</style>

View File

@ -0,0 +1,806 @@
<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"
>
发送
</el-button>
</div>
<div class="detail-content-image">
<img src="../assets/人才详情贴图.png" alt="人才详情" class="talent-detail-image" />
</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">选择文件</el-button>
<div class="el-upload__tip">请上传文档支持PDFDOCDOCX格式</div>
</el-upload>
<div class="detail-content-image">
<img src="../assets/人才详情贴图.png" alt="人才详情" class="talent-detail-image" />
</div>
</div>
<!-- 手动输入标签页 -->
<div v-else class="tab-content">
<div class="manual-input-tip">请手动填写以下信息</div>
<div class="detail-content-image">
<img src="../assets/人才详情贴图.png" alt="人才详情" class="talent-detail-image" />
</div>
</div>
<!-- 个人信息表单 - 仅在数据加载后显示 -->
<div v-if="dataLoaded" class="personal-info-form">
<div class="form-header">
<div class="photo-section">
<div class="teacher-photo">
<img :src="formData.photo || defaultPhoto" alt="教师照片" />
</div>
<div class="teacher-id">ID: {{ formData.id || 'BLG00000' }}</div>
</div>
<div class="basic-info">
<div class="form-row">
<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 class="form-item">
<span class="label">出生年月:</span>
<el-date-picker
v-model="formData.birthDate"
type="date"
placeholder="选择日期"
format="YYYY.MM.DD"
value-format="YYYY.MM.DD"
/>
</div>
</div>
<div class="form-row">
<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 class="form-item">
<span class="label">最高学历:</span>
<el-input v-model="formData.education" placeholder="请输入最高学历" />
</div>
</div>
<div class="form-row">
<div class="form-item full-width">
<span class="label">通讯地址:</span>
<el-input v-model="formData.address" placeholder="请输入通讯地址" />
</div>
</div>
<div class="form-row">
<div class="form-item">
<span class="label">学科方向:</span>
<el-input v-model="formData.academicDirection" placeholder="请输入学科方向" />
</div>
<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 class="drawer-btn confirm-btn" @click="handleSave">确定</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 } from 'element-plus';
// 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 = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='120' viewBox='0 0 100 120'%3E%3Crect width='100' height='120' fill='%234986ff' opacity='0.3'/%3E%3Ccircle cx='50' cy='45' r='25' fill='%234986ff' opacity='0.6'/%3E%3Ccircle cx='50' cy='95' r='35' fill='%234986ff' opacity='0.6'/%3E%3C/svg%3E`;
//
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: 'BLG45187',
name: '',
gender: '',
birthDate: '',
title: '',
position: '',
education: '',
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 && dataLoaded.value) {
//
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 }
];
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: formData.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 = (file) => {
selectedFile.value = file;
//
setTimeout(() => {
fetchMockData();
}, 500);
};
// URL
const fetchDataFromUrl = async () => {
if (!urlInput.value) {
return;
}
// 使
fetchMockData();
};
//
const fetchMockData = () => {
//
const response = {
id: 'BLG45187',
name: '张三',
gender: '男',
birthDate: '1991.11.12',
title: '预聘助理教授',
position: '教授',
education: '博士研究生',
address: '北京市海淀区北京理工大学自动化学院',
academicDirection: '控制理论与控制工程',
talentPlan: '无',
officeLocation: '6号楼509',
email: 'zhangsan@bit.edu.cn',
phone: '15487546574',
tutorType: '硕士生导师',
papers: '15篇',
projects: '26项',
photo: defaultPhoto,
eduWorkHistory: '2023.12 至今北京理工大学, 自动化学院, 预聘助理教授\n2020.07-2023.12 北京理工大学 自动化学院 博士后(合作导师:张国良 教授)\n2016.09-2021.09 北京航空航天大学 自动化科学与电气工程学院 控制科学与工程 博士 联培\n2011.09-2015.06 中国石油大学(华东)机械设计制造及其自动化 本科',
researchDirection: '伺服控制\n高天空地机器人协同',
recentProjects: '项目1\n项目2\n项目3',
representativePapers: '论文1\n论文2\n论文3',
patents: '专利1\n专利2',
evaluationData: [85, 90, 78, 82, 76, 88]
};
// 使
Object.assign(formData, response);
//
dataLoaded.value = true;
//
nextTick(() => {
initRadarChart();
});
};
//
const handleClose = () => {
drawerVisible.value = false;
//
if (!props.isEdit) {
dataLoaded.value = false;
}
};
// token
const getToken = () => {
return localStorage.getItem('token');
};
//
const handleSave = async () => {
try {
//
const talentData = { ...formData.value };
// POSTAPI
const response = await fetch('http://localhost:8000/talents', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`
},
body: JSON.stringify(talentData)
});
if (response.ok) {
const savedData = await response.json();
emit('save', savedData);
ElMessage.success('保存成功');
handleClose();
} else {
const error = await response.json();
ElMessage.error(`保存失败: ${error.detail || '未知错误'}`);
}
} catch (error) {
console.error('保存请求出错:', error);
ElMessage.error('保存失败,请稍后再试');
}
};
</script>
<style>
@import './common.css';
.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;
}
.el-upload__tip {
color: rgba(255,255,255,0.7);
margin-top: 5px;
}
/* 手动输入提示 */
.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);
}
.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;
}
.detail-content-image {
margin-top: 20px;
width: 100%;
display: flex;
justify-content: center;
}
.talent-detail-image {
width: 100%;
max-width: 800px;
object-fit: contain;
}
</style>

17
src/config.js Normal file
View File

@ -0,0 +1,17 @@
// API配置
const env = import.meta.env.MODE || 'development';
const config = {
development: {
apiBaseUrl: 'http://127.0.0.1:48996',
},
production: {
apiBaseUrl: 'http://36.103.199.107:48996',
}
};
export const getApiBaseUrl = () => {
return config[env].apiBaseUrl;
};
export default config;

17
src/main.js Normal file
View File

@ -0,0 +1,17 @@
// src/main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
// 创建应用
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus)
app.mount('#app')

79
src/style.css Normal file
View File

@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

58
start_with_ip.bat Normal file
View File

@ -0,0 +1,58 @@
@echo off
echo ===========================================
echo Fast Dashboard IP 访问启动脚本
echo ===========================================
echo.
echo 正在获取本机IP地址...
for /f "tokens=2 delims=:" %%i in ('ipconfig ^| findstr /c:"IPv4 地址"') do (
for /f "tokens=1" %%j in ("%%i") do (
echo 检测到IP地址: %%j
set "LOCAL_IP=%%j"
)
)
echo.
echo 激活Python环境...
call conda activate fast-dashboard-env
if errorlevel 1 (
echo 错误: 无法激活conda环境请确保已按照PYTHON_ENV_SETUP_README.md配置环境
pause
exit /b 1
)
echo.
echo 启动后端服务器 (端口: 8000)...
cd backend
start "后端服务器" cmd /k "uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
echo 等待后端启动...
timeout /t 3 /nobreak >nul
echo.
echo 启动前端服务器 (端口: 5173)...
cd ..
start "前端服务器" cmd /k "npm run dev -- --host 0.0.0.0"
echo.
echo ===========================================
echo 服务启动完成!
echo ===========================================
echo.
echo 本地访问地址:
echo 前端: http://localhost:5173
echo 后端: http://localhost:8000
echo.
echo 网络访问地址 (同局域网内其他设备可访问):
if defined LOCAL_IP (
echo 前端: http://%LOCAL_IP%:5173
echo 后端: http://%LOCAL_IP%:8000
) else (
echo 前端: http://192.168.18.108:5173
echo 后端: http://192.168.18.108:8000
)
echo.
echo 注意: 确保防火墙允许端口 5173 和 8000 的访问
echo ===========================================
echo.
pause

15
vite.config.js Normal file
View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
define: {
'import.meta.env.MODE': JSON.stringify(process.env.NODE_ENV || 'development')
},
server: {
host: '0.0.0.0', // 允许从任何IP访问
port: 5173, // 前端端口
strictPort: true
}
})