Compare commits

..

39 Commits

Author SHA1 Message Date
“zhuzihan” 
10282967a3 暂存 2025-08-07 16:33:13 +08:00
“zhuzihan” 
6b6e66327c 暂存 2025-08-07 14:10:31 +08:00
“zhuzihan” 
ec13daa698 暂存 2025-08-07 14:05:20 +08:00
“zhuzihan” 
20d6bbecd6 冲突提交 2025-08-07 14:03:08 +08:00
“zhuzihan” 
535042b67b 需求修改1 2025-08-07 12:57:42 +08:00
liuzhiyuan
4c44096f41 fix: 解决名称过长显示不全的问题 2025-08-07 09:22:00 +08:00
liuzhiyuan
611adc0bff Merge branch 'view_dev' into view 2025-07-31 16:21:00 +08:00
liuzhiyuan
e09641d7a4 fix: 修改工程研究中心评估 tips 遮挡问题 2025-07-31 16:20:36 +08:00
liuzhiyuan
ee9c91d653 Merge branch 'view_dev' into view 2025-07-17 15:35:03 +08:00
liuzhiyuan
b87baf1e6b fix: 1 修改大屏标题 不同分辨率下避免遮挡问题 2、智慧助手增加可以上下调节尺寸功能 2025-07-17 15:32:42 +08:00
“zhuzihan” 
48c8dd7dc0 合并 2025-07-14 16:59:18 +08:00
“zhuzihan” 
8ecaf99a9a 合并提交 2025-07-14 16:55:43 +08:00
“zhuzihan” 
27552c04a6 合并提交 2025-07-14 13:16:20 +08:00
“zhuzihan” 
40bd12dba1 Merge remote-tracking branch 'origin/view' into view_dev 2025-07-12 17:17:46 +08:00
“zhuzihan” 
f52641d417 超出提示 2025-07-12 16:08:19 +08:00
“zhuzihan” 
ed8ce81cb8 合并 2025-07-12 15:13:51 +08:00
“zhuzihan” 
91bba8de8a 合并 2025-07-12 15:08:55 +08:00
“zhuzihan” 
a5f67793b6 工程得分对比功能添加 2025-07-12 14:58:38 +08:00
“zhuzihan” 
7b7496dae1 客户需求修改 2025-07-11 14:46:04 +08:00
“zhuzihan” 
f549b084a8 合并view 2025-07-10 17:14:40 +08:00
“zhuzihan” 
1507e1a725 暂存 2025-07-09 14:16:36 +08:00
“zhuzihan” 
0be3e9b263 非空判断 2025-07-09 10:20:16 +08:00
“zhuzihan” 
13cef0b0bf ds流式加载 2025-07-08 18:29:12 +08:00
“zhuzihan” 
084f859ad3 合并 2025-07-07 13:19:23 +08:00
“zhuzihan” 
bd08d3a988 暂存 2025-07-05 17:27:57 +08:00
“zhuzihan” 
15adb29bd3 暂存 2025-07-05 17:05:50 +08:00
“zhuzihan” 
637de14e0e 工程图表增加年限维度 2025-07-05 15:52:03 +08:00
“zhuzihan” 
a8d28e94e6 满分新增、布局修改、工程维度增加 2025-07-04 16:52:29 +08:00
“zhuzihan” 
afc2c36178 Merge remote-tracking branch 'origin/view' into view_dev 2025-07-03 17:55:43 +08:00
“zhuzihan” 
2d152c5e2d 教师模块 2025-07-03 15:10:49 +08:00
“zhuzihan” 
6dd6f8ebf1 科研人才模块首页 2025-07-02 16:56:06 +08:00
“zhuzihan” 
35c0a36412 测试部署 2025-06-27 16:57:58 +08:00
“zhuzihan” 
fa76b4e719 新增详情页面 2025-06-27 15:54:55 +08:00
“zhuzihan” 
6042e75ae0 修改数据结构 2025-06-26 19:39:39 +08:00
“zhuzihan” 
9ce656b9e5 MarkdownIt 2025-06-26 15:58:51 +08:00
“zhuzihan” 
20530c8c17 远程发布配置 2025-06-26 15:01:21 +08:00
“zhuzihan” 
9c8b3b240e 工程研究中心列+详情页接口对接 2025-06-26 14:10:48 +08:00
“zhuzihan” 
a8127a4bfa 二期 2025-06-13 16:21:35 +08:00
“zhuzihan” 
89130eaef2 暂存 2025-06-12 10:35:31 +08:00
36 changed files with 3057 additions and 7404 deletions

1
.env.prod Normal file
View File

@ -0,0 +1 @@
NODE_ENV=production

View File

@ -1,29 +0,0 @@
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

@ -1,292 +0,0 @@
# 工程研究中心数据更新指南
## 概述
本文档详细说明如何将 `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

View File

@ -1,83 +0,0 @@
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()

View File

@ -1,181 +0,0 @@
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

Binary file not shown.

Binary file not shown.

View File

@ -1,30 +0,0 @@
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

@ -1,81 +0,0 @@
#!/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

@ -1,130 +0,0 @@
#!/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

@ -1,270 +0,0 @@
#!/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()

View File

@ -1,226 +0,0 @@
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

@ -1,68 +0,0 @@
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()

File diff suppressed because it is too large Load Diff

View File

@ -1,221 +0,0 @@
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

@ -1,304 +0,0 @@
#!/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

@ -1 +0,0 @@

View File

@ -1,46 +0,0 @@
#!/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()

View File

@ -1,13 +0,0 @@
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

View File

@ -1,246 +0,0 @@
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

@ -1,256 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 121 KiB

0
docker
View File

View File

@ -8,18 +8,6 @@ services:
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

View File

@ -1,5 +1,5 @@
server {
listen 48100;
listen 48103;
server_name localhost;
location / {

View File

@ -7,22 +7,24 @@
"dev": "vite",
"dev:local": "cross-env NODE_ENV=development vite",
"dev:prod": "cross-env NODE_ENV=production vite",
"build": "vite build",
"build": "vite build --mode production",
"build:upload": "node upload.js",
"preview": "vite preview",
"backend": "cd backend && python app.py",
"start": "concurrently \"npm run dev\" \"npm run backend\""
"start": "concurrently \"npm run dev\""
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.8.4",
"echarts": "^5.6.0",
"element-plus": "^2.9.8",
"markdown-it": "^14.1.0",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.2",
"concurrently": "^7.6.0",
"cross-env": "^7.0.3",
"ssh2-sftp-client": "^12.0.0",
"vite": "^6.3.1"
}
}

179
pnpm-lock.yaml generated
View File

@ -20,6 +20,9 @@ importers:
element-plus:
specifier: ^2.9.8
version: 2.9.8(vue@3.5.13)
markdown-it:
specifier: ^14.1.0
version: 14.1.0
vue:
specifier: ^3.5.13
version: 3.5.13
@ -33,6 +36,9 @@ importers:
cross-env:
specifier: ^7.0.3
version: 7.0.3
ssh2-sftp-client:
specifier: ^12.0.0
version: 12.0.1
vite:
specifier: ^6.3.1
version: 6.3.2
@ -265,56 +271,67 @@ packages:
resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.40.0':
resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.40.0':
resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.40.0':
resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.40.0':
resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.40.0':
resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.40.0':
resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.40.0':
resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.40.0':
resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.40.0':
resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.40.0':
resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==}
@ -399,6 +416,12 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
asn1@0.2.6:
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
async-validator@4.2.5:
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
@ -408,6 +431,16 @@ packages:
axios@1.8.4:
resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
buildcheck@0.0.6:
resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==}
engines: {node: '>=10.0.0'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@ -431,11 +464,19 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
concat-stream@2.0.0:
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
engines: {'0': node >= 6.0}
concurrently@7.6.0:
resolution: {integrity: sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==}
engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0}
hasBin: true
cpu-features@0.0.10:
resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==}
engines: {node: '>=10.0.0'}
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
@ -570,6 +611,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
@ -577,6 +621,9 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
@ -593,10 +640,17 @@ packages:
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
@ -608,6 +662,9 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
nan@2.22.2:
resolution: {integrity: sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -634,6 +691,14 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
@ -649,6 +714,12 @@ packages:
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -668,10 +739,21 @@ packages:
spawn-command@0.0.2:
resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==}
ssh2-sftp-client@12.0.1:
resolution: {integrity: sha512-ICJ1L2PmBel2Q2ctbyxzTFZCPKSHYYD6s2TFZv7NXmZDrDNGk8lHBb/SK2WgXLMXNANH78qoumeJzxlWZqSqWg==}
engines: {node: '>=18.20.4'}
ssh2@1.16.0:
resolution: {integrity: sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==}
engines: {node: '>=10.16.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@ -695,6 +777,18 @@ packages:
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
tweetnacl@0.14.5:
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vite@6.3.2:
resolution: {integrity: sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -1047,6 +1141,12 @@ snapshots:
dependencies:
color-convert: 2.0.1
argparse@2.0.1: {}
asn1@0.2.6:
dependencies:
safer-buffer: 2.1.2
async-validator@4.2.5: {}
asynckit@0.4.0: {}
@ -1059,6 +1159,15 @@ snapshots:
transitivePeerDependencies:
- debug
bcrypt-pbkdf@1.0.2:
dependencies:
tweetnacl: 0.14.5
buffer-from@1.1.2: {}
buildcheck@0.0.6:
optional: true
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@ -1085,6 +1194,13 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
concat-stream@2.0.0:
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 3.6.2
typedarray: 0.0.6
concurrently@7.6.0:
dependencies:
chalk: 4.1.2
@ -1097,6 +1213,12 @@ snapshots:
tree-kill: 1.2.2
yargs: 17.7.2
cpu-features@0.0.10:
dependencies:
buildcheck: 0.0.6
nan: 2.22.2
optional: true
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
@ -1254,10 +1376,16 @@ snapshots:
dependencies:
function-bind: 1.1.2
inherits@2.0.4: {}
is-fullwidth-code-point@3.0.0: {}
isexe@2.0.0: {}
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
lodash-es@4.17.21: {}
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
@ -1272,8 +1400,19 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
math-intrinsics@1.1.0: {}
mdurl@2.0.0: {}
memoize-one@6.0.0: {}
mime-db@1.52.0: {}
@ -1282,6 +1421,9 @@ snapshots:
dependencies:
mime-db: 1.52.0
nan@2.22.2:
optional: true
nanoid@3.3.11: {}
normalize-wheel-es@1.2.0: {}
@ -1300,6 +1442,14 @@ snapshots:
proxy-from-env@1.1.0: {}
punycode.js@2.3.1: {}
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
regenerator-runtime@0.14.1: {}
require-directory@2.1.1: {}
@ -1334,6 +1484,10 @@ snapshots:
dependencies:
tslib: 2.3.0
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@ -1346,12 +1500,29 @@ snapshots:
spawn-command@0.0.2: {}
ssh2-sftp-client@12.0.1:
dependencies:
concat-stream: 2.0.0
ssh2: 1.16.0
ssh2@1.16.0:
dependencies:
asn1: 0.2.6
bcrypt-pbkdf: 1.0.2
optionalDependencies:
cpu-features: 0.0.10
nan: 2.22.2
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@ -1373,6 +1544,14 @@ snapshots:
tslib@2.3.0: {}
tweetnacl@0.14.5: {}
typedarray@0.0.6: {}
uc.micro@2.1.0: {}
util-deprecate@1.0.2: {}
vite@6.3.2:
dependencies:
esbuild: 0.25.2

View File

@ -50,7 +50,7 @@ import axios from 'axios'
import { getApiBaseUrl } from './config'
//
const isLoggedIn = ref(false)
const isLoggedIn = ref(true)
//
const currentPage = ref('dashboard')

File diff suppressed because it is too large Load Diff

View File

@ -3,66 +3,53 @@
<!-- 顶部导航栏 -->
<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>
<img src="../assets/logo1.png" alt="北京理工大学" @click="handleLogoClick" />
<img src="../assets/logo2.png" alt="北京理工大学" @click="handleLogoClick" />
</div>
<h1 class="main-title">
<span class="title-text">智慧科研评估系统</span>
</h1>
<div class="header-placeholder"></div> <!-- 用于平衡布局的占位元素 -->
</header>
<!-- 主内容区域 -->
<div class="content-container">
<!-- 左侧维度设置 -->
<div class="dimension-sidebar">
<!-- 右侧内容区 -->
<div class="main-content">
<!-- 搜索和操作栏 -->
<div class="action-bar">
<div class="sidebar-header">
<h1 class="sidebar-title">
<span class="home-link" @click="jumpToDashboard">首页</span>&nbsp;>&nbsp;工程研究中心评估
</h1>
</div>
<div class="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 class="searchbox">
<!-- 新增得分比较按钮组 -->
<div class="score-comparison-wrapper">
<el-button
type="primary"
v-if="!scoreComparisonMode"
class="compare-score-btn"
@click="toggleScoreComparisonMode"
>
得分比较
</el-button>
<template v-else>
<el-button class="compare-score-btn close-btn" @click="toggleScoreComparisonMode">
关闭
</el-button>
<el-button
class="compare-score-btn start-compare-btn"
@click="openScoreComparisonDialog"
:disabled="selectedLabs.length < 2"
:class="{ 'disabled-btn': selectedLabs.length < 2 }"
>
开始比较 ({{ selectedLabs.length }})
</el-button>
</template>
</div>
<div class="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"
@ -77,47 +64,61 @@
</button>
</div>
<button class="add-evaluation-btn" @click="openAddEvaluationDrawer">
<!-- <button class="add-evaluation-btn" @click="openAddEvaluationDrawer">
新增评估
</button>
</button> -->
</div>
</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 v-for="(lab) in filteredLabs" :key="lab.id" class="lab-card">
<!-- 复选框 -->
<div v-if="scoreComparisonMode" class="card-checkbox-wrapper">
<input
type="checkbox"
v-model="lab.selected"
@change="handleLabSelection(lab)"
/>
</div>
<div class="card-content">
<div class="lab-image">
<img :src="lab.image" alt="工程研究中心图片" />
<div class="card-header" @click="openLabDetail(lab)">
<span class="lab-id" :style="scoreComparisonMode?'margin-left: 40px;':''">ID: {{ lab.basicInformation.name1 || lab.id }}</span>
<span class="total-score">年份: <span class="score-value">{{ lab.basicInformation.name0 }}</span></span>
</div>
<div class="card-content" @click="openLabDetail(lab)">
<div class="lab-info">
<div class="info-item">
<span class="info-label">研究中心名称:</span>
<span class="info-value">{{ lab.name }}</span>
<span class="info-label">中心名称:</span>
<span class="info-value">{{ lab.basicInformation.name2 }}</span>
</div>
<div class="info-item">
<span class="info-label">所属领域:</span>
<span class="info-value">{{ lab.field }}</span>
<span class="info-value">{{ lab.basicInformation.name3 }}</span>
</div>
<div class="info-item">
<span class="info-label">所属学校:</span>
<span class="info-value">{{ lab.school }}</span>
<span class="info-value">{{ lab.basicInformation.name4 }}</span>
</div>
<div class="info-item">
<span class="info-label">主管部门:</span>
<span class="info-value">{{ lab.department }}</span>
<span class="info-value">{{ lab.basicInformation.name5 }}</span>
</div>
</div>
</div>
<div class="evaluation-chart">
<div :id="`lab-chart-${index}`" class="radar-chart"></div>
<div class="evaluation-chart" @click="openLabDetail(lab)">
<div :id="`lab-chart-${lab.id}`" class="radar-chart"></div> <!-- 根据lab.id确保唯一性 -->
</div>
<div class="evaluation-info-wrapper"
@mouseenter="showTooltip(lab.id, lab.abstracts)"
@mouseleave="hideTooltip">
<div class="evaluation-info">评估摘要{{ lab.abstracts }}</div>
<div v-if="currentTooltip.labId === lab.id && currentTooltip.visible" class="tooltip">
{{ currentTooltip.content }}
</div>
</div>
</div>
</div>
@ -139,10 +140,60 @@
:lab-data="selectedLab"
@save="handleSaveEvaluation"
/>
<!-- 得分比较弹窗 -->
<el-dialog
v-model="scoreComparisonDialogVisible"
title="得分比较"
width="80%"
center
:append-to-body="true"
class="score-comparison-dialog"
>
<el-table
:data="scoreComparisonTableData"
style="width: 100%"
border
stripe
class="default-table score-comparison-table"
:row-class-name="tableRowClassName"
>
<el-table-column prop="category" label=""></el-table-column>
<el-table-column
v-for="(header) in scoreComparisonHeaders"
:key="header.prop"
:prop="header.prop"
:label="header.label"
align="center"
>
<template #default="{ row }">
<span :class="{
'max-score-in-column': Number(row[header.prop]) === columnStats[header.prop]?.max,
'min-score-in-column': Number(row[header.prop]) === columnStats[header.prop]?.min
}">
{{ row[header.prop] }}
</span>
</template>
</el-table-column>
<el-table-column
prop="totalScore"
label="总分"
align="center">
<template #default="{ row }">
<span :class="{
'max-score-in-column': Number(row.totalScore) === columnStats.totalScore?.max,
'min-score-in-column': Number(row.totalScore) === columnStats.totalScore?.min
}">
{{ row.totalScore }}
</span>
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue';
import { ref, onMounted, watch, nextTick, computed, onBeforeUnmount } from 'vue'; // onBeforeUnmount
import * as echarts from 'echarts/core';
import { RadarChart } from 'echarts/charts';
import LabDrawerDetail from './LabDrawerDetail.vue';
@ -155,7 +206,7 @@ import {
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import axios from 'axios';
import { ElMessage } from 'element-plus';
import { ElMessage, ElDialog, ElTable, ElTableColumn } from 'element-plus';
import { getApiBaseUrl } from '../config'; // APIURL
// echarts
@ -173,6 +224,34 @@ const isEditMode = ref(false);
const selectedLab = ref(null);
const dimensionDrawerVisible = ref(false);
//
const scoreComparisonMode = ref(false); //
const selectedLabs = ref([]); // Lab Card
const scoreComparisonDialogVisible = ref(false); //
// Echarts
const chartInstances = new Map();
// Tooltip
const currentTooltip = ref({
labId: null,
content: '',
visible: false
});
const showTooltip = (labId, content) => {
currentTooltip.value = {
labId: labId,
content: content,
visible: true
};
};
const hideTooltip = () => {
currentTooltip.value.visible = false;
};
//
const openAddEvaluationDrawer = () => {
isEditMode.value = false;
@ -227,134 +306,90 @@ 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('获取维度数据失败');
}
});
// Echarts
onBeforeUnmount(() => {
chartInstances.forEach(chart => {
chart.dispose();
});
chartInstances.clear();
window.removeEventListener('resize', debounceResize); // resize
});
//
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);
}
});
const response = await axios.get(`${getApiBaseUrl()}/admin-api/pg/evaluation-results/get-release`);
// labselected
// result
labs.value = response.data.data.map(lab => ({
...lab,
selected: false,
result: lab.result.map(Number) // radarData
}));
//
handleSearch();
// DOM
setTimeout(() => {
updateAllRadarCharts();
}, 100);
} catch (error) {
console.error('获取工程研究中心数据失败:', error);
ElMessage.error('获取工程研究中心数据失败');
}
};
//
const editDimension = (dim, index) => {
//
openDimensionDrawer();
// for resize
const debounce = (fn, delay) => {
let timer = null;
return function() {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
};
const debounceResize = debounce(() => {
chartInstances.forEach(chart => {
chart && chart.resize();
});
}, 300); // 300ms
//
const updateAllRadarCharts = () => {
nextTick(() => {
filteredLabs.value.forEach((lab, index) => {
const chartDom = document.getElementById(`lab-chart-${index}`);
//
const currentLabIds = new Set(filteredLabs.value.map(lab => lab.id));
chartInstances.forEach((chart, id) => {
if (!currentLabIds.has(id)) {
chart.dispose();
chartInstances.delete(id);
}
});
filteredLabs.value.forEach((lab) => {
const chartDom = document.getElementById(`lab-chart-${lab.id}`);
if (!chartDom) return;
//
echarts.dispose(chartDom);
const chart = echarts.init(chartDom);
let chart = chartInstances.get(lab.id);
if (!chart) {
chart = echarts.init(chartDom);
chartInstances.set(lab.id, chart);
// resize
window.addEventListener('resize', debounceResize);
}
//
const indicators = [];
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 }
);
}
const indicators = lab.dimension.map(name => ({ name: name, max: 50 })); // 50
//
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;
}
let radarData = lab.result; //
chart.setOption({
radar: {
@ -395,17 +430,25 @@ const updateAllRadarCharts = () => {
},
itemStyle: {
color: 'rgba(255, 0, 255, 0.8)'
},
label: {
show: true, //
position: 'top', //
color: '#fff', //
fontSize: 10 //
},
emphasis: {
label: {
show: true, //
fontSize: 12, //
fontWeight: 'bold' //
}
}
}
]
}
]
});
//
window.addEventListener('resize', () => {
chart && chart.resize();
});
});
});
};
@ -419,8 +462,8 @@ const handleSearch = () => {
filteredLabs.value = labs.value;
} else {
filteredLabs.value = labs.value.filter(lab =>
lab.name.includes(searchQuery.value) ||
(lab.idcode && lab.idcode.includes(searchQuery.value))
(lab.basicInformation.name2 && lab.basicInformation.name2.includes(searchQuery.value)) ||
(lab.basicInformation.name1 && lab.basicInformation.name1.includes(searchQuery.value))
);
}
@ -431,18 +474,193 @@ const handleSearch = () => {
};
//
const openLabDetail = (lab) => {
const openLabDetail = async (lab) => {
//
if (scoreComparisonMode.value) return;
selectedLab.value = lab;
isEditMode.value = true;
drawerVisible.value = true;
};
// --- ---
//
const toggleScoreComparisonMode = () => {
scoreComparisonMode.value = !scoreComparisonMode.value;
if (!scoreComparisonMode.value) {
//
filteredLabs.value.forEach(lab => {
lab.selected = false;
});
selectedLabs.value = [];
}
};
// Lab Card
const handleLabSelection = (lab) => {
// 使 Vue
if (lab.selected) {
selectedLabs.value.push(lab);
} else {
selectedLabs.value = selectedLabs.value.filter(item => item.id !== lab.id);
}
};
//
const scoreComparisonHeaders = computed(() => {
if (selectedLabs.value.length === 0) {
return [];
}
// Lab dimension
const firstLabDimensions = selectedLabs.value[0].dimension;
return firstLabDimensions.map((dim, index) => ({
prop: `score${index + 1}`, // score1, score2
label: dim
}));
});
//
const scoreComparisonTableData = computed(() => {
if (selectedLabs.value.length === 0) {
return [];
}
return selectedLabs.value.map(lab => {
const row = {
category: `${lab.basicInformation.name0}-${lab.basicInformation.name4}-${lab.basicInformation.name2}`
};
if (Array.isArray(lab.result)) {
let total = 0;
lab.result.forEach((score, index) => {
const numericScore = Number(score) || 0;
row[`score${index + 1}`] = numericScore;
total += numericScore;
});
// 使Math.round()
row.totalScore = Math.round(total);
} else {
row.totalScore = 0; // 0
}
return row;
});
});
//
const columnStats = computed(() => {
const stats = {};
if (scoreComparisonTableData.value.length === 0 || scoreComparisonHeaders.value.length === 0) {
return stats;
}
scoreComparisonHeaders.value.forEach(header => {
const prop = header.prop;
//
const values = scoreComparisonTableData.value.map(row => Number(row[prop]));
if (values.length > 0) {
stats[prop] = {
max: Math.max(...values),
min: Math.min(...values)
};
}
});
//
const totalScores = scoreComparisonTableData.value.map(row => Number(row.totalScore));
if (totalScores.length > 0) {
stats.totalScore = {
max: Math.max(...totalScores),
min: Math.min(...totalScores)
};
}
// console.log("Calculated columnStats:", stats); //
return stats;
});
//
const tableRowClassName = ({ rowIndex }) => {
if (rowIndex % 2 === 1) {
return 'striped-row'; //
}
return '';
};
//
const openScoreComparisonDialog = () => {
if (selectedLabs.value.length < 2) {
ElMessage.warning('请至少选择两个工程研究中心进行比较!');
return;
}
scoreComparisonDialogVisible.value = true;
};
// filteredLabs
// lab.id ECharts
watch(filteredLabs, () => {
nextTick(() => {
updateAllRadarCharts();
});
});
</script>
<style>
/* common.css 保持不变,用于全局样式 */
@import './common.css';
</style>
<style scoped>
.default-table {
background-color: white; /* 表格背景色为白色 */
color: black; /* 文字颜色为黑色 */
border-radius: 8px;
overflow: hidden;
}
/* 新增:得分比较表格的特定样式 */
.score-comparison-table {
max-height: 60vh; /* 设置最大高度例如视口高度的60% */
overflow-y: auto; /* 当内容超出最大高度时,显示垂直滚动条 */
overflow-x: hidden;
}
.default-table th.el-table__cell {
background-color: #f5f7fa !important; /* 表头背景色浅灰 */
color: black !important;
font-weight: bold;
border-right: 1px solid #ebeef5 !important;
border-bottom: 1px solid #ebeef5 !important;
}
.default-table td.el-table__cell {
background-color: white !important; /* 单元格背景色白色 */
color: black !important;
border-right: 1px solid #ebeef5 !important;
border-bottom: 1px solid #ebeef5 !important;
}
.default-table tr:hover > td {
background-color: #f0f2f5 !important; /* 行悬停背景色更浅的灰色 */
}
/* 条纹样式 */
.default-table.el-table--striped .el-table__body tr.striped-row td {
background-color: #fafafa !important; /* 条纹行背景色,比默认单元格略深 */
}
/* 最大值和最小值高亮 */
.max-score-in-column {
color: red; /* 红色 */
font-weight: bold;
}
.min-score-in-column {
color: blue; /* 蓝色 */
font-weight: bold;
}
.evaluation-page {
position: absolute;
top: 0;
@ -477,7 +695,7 @@ const openLabDetail = (lab) => {
border: 2px solid rgba(38,47,80,0.3);
}
.dashboard-header {
/* .dashboard-header {
height: 60px;
padding: 0 20px;
display: flex;
@ -515,7 +733,7 @@ const openLabDetail = (lab) => {
.title-text {
margin: 0 30px;
}
} */
.content-container {
display: flex;
@ -655,21 +873,30 @@ const openLabDetail = (lab) => {
flex-direction: column;
padding: 0 10px;
overflow: hidden;
max-height: calc(100vh - 100px); /* 限制最大高度 */
max-height: none;
height: auto;
}
/* 搜索和操作栏 */
.action-bar {
height: 64px;
display: flex;
justify-content: space-between;
justify-content: space-between; /* 使得左中右元素分散对齐 */
align-items: center;
}
/* 新增:得分比较按钮组容器,与搜索框分离 */
.score-comparison-wrapper {
display: flex;
gap: 10px; /* 按钮之间的间距 */
margin-right: 20px; /* 与搜索框的间距 */
}
.search-box {
display: flex;
align-items: center;
width: 300px;
width: 300px; /* 调整宽度,不再包含得分比较按钮 */
background-color: rgba(255,255,255,0.1);
border-radius: 20px;
overflow: hidden;
@ -716,26 +943,29 @@ const openLabDetail = (lab) => {
/* 工程研究中心卡片网格 */
.lab-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
gap: 20px;
overflow-y: auto; /* 允许卡片区域滚动 */
flex: 1;
display: flex; /* 使用 flex 布局 */
flex-wrap: wrap; /* 允许项目换行 */
gap: 20px; /* 项目间的间距 */
padding: 20px;
background-color: #262F50;
border-radius: 10px;
overflow-y: auto;
justify-content: flex-start;
}
.lab-card {
box-sizing: border-box;
flex: 0 0 calc((100% - 41px) / 3);
background-color: #1f3266;
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(73,134,255,0.3);
display: flex;
flex-direction: column;
min-height: 350px; /* 调整最小高度,确保雷达图有足够空间 */
min-height: 350px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
position: relative; /* 添加此行以便tooltip定位 */
}
.lab-card:hover {
@ -743,6 +973,31 @@ const openLabDetail = (lab) => {
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
/* 复选框容器样式 */
.card-checkbox-wrapper {
position: absolute;
top: 4px; /* 距离顶部 */
left: 8px; /* 距离左侧 */
z-index: 10; /* 确保在最上层 */
background-color: rgba(0, 0, 0, 0.3); /* 半透明黑色背景 */
border-radius: 4px;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
width: 28px; /* 确保有足够点击区域 */
height: 28px;
}
.card-checkbox-wrapper input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #4986ff; /* 改变复选框颜色 */
margin: 0; /* 移除默认外边距 */
}
.card-header {
display: flex;
justify-content: space-between;
@ -755,6 +1010,7 @@ const openLabDetail = (lab) => {
.lab-id {
font-size: 14px;
color: rgba(255,255,255,0.7);
/* 确保ID不被复选框遮挡如果复选框在左边这里可能需要调整padding-left或margin-left */
}
.total-score {
@ -789,14 +1045,16 @@ const openLabDetail = (lab) => {
}
.lab-info {
width: 60%;
width: 100%;
display: flex;
flex-direction: column;
flex-direction: row; /* 修改为横向排列 */
flex-wrap: wrap; /* 允许元素换行 */
gap: 10px;
}
.info-item {
margin-bottom: 5px;
width: calc(50% - 5px); /* 每个元素占一半宽度,减去 gap 的一半 */
}
.info-label {
@ -858,6 +1116,49 @@ const openLabDetail = (lab) => {
margin-bottom: 5px;
}
/* 得分比较按钮样式 */
.compare-score-btn {
background-color: rgb(13, 70, 192);
border: none;
color: rgba(255,255,255,1);
border-radius: 10px;
padding: 8px 15px;
font-size: 14px;
text-align: center;
font-family: PingFangSC-regular;
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s;
}
.compare-score-btn:hover {
background-color: rgba(14,62,167,0.8);
}
.compare-score-btn.close-btn {
background-color: #f44336; /* 红色 */
border-color: #f44336;
}
.compare-score-btn.close-btn:hover {
background-color: #d32f2f;
}
.compare-score-btn.start-compare-btn {
background-color: #4CAF50; /* 绿色 */
border-color: #4CAF50;
}
.compare-score-btn.start-compare-btn:hover {
background-color: #388E3C;
}
.compare-score-btn.disabled-btn {
background-color: #616161; /* 灰色 */
border-color: #616161;
cursor: not-allowed;
opacity: 0.7;
}
@media (max-width: 1200px) {
.lab-card-grid {
grid-template-columns: 1fr;
@ -883,4 +1184,57 @@ const openLabDetail = (lab) => {
width: 100%;
}
}
.searchbox{
display: flex;
}
/* 评估摘要的容器用于定位tooltip */
.evaluation-info-wrapper {
position: relative;
padding: 20px 15px; /* 保持原有的padding */
}
/* 评估摘要文本样式 */
.evaluation-info {
white-space: normal; /* 允许文本正常换行 */
overflow: hidden; /* 隐藏超出容器的内容 */
text-overflow: ellipsis; /* 显示省略号 */
display: -webkit-box;
-webkit-line-clamp: 2; /* 限制文本为2行 */
-webkit-box-orient: vertical;
font-size: 12px;
}
/* 鼠标悬停时显示的气泡样式 */
.tooltip {
position: absolute;
bottom: 100%; /* 定位在文本上方 */
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 8px 12px;
border-radius: 4px;
white-space: pre-wrap; /* 保持文本换行 */
z-index: 100;
max-width: 90%; /* 限制最大宽度 */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
font-size: 14px;
line-height: 1.5;
pointer-events: none; /* 确保不影响鼠标事件 */
margin-bottom: 10px; /* 与文本的距离 */
width: 25vw;
}
/* 气泡的小箭头 */
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: rgba(0, 0, 0, 0.85) transparent transparent transparent;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -3,25 +3,19 @@
<!-- 顶部导航栏 -->
<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>
<img src="../assets/logo1.png" alt="北京理工大学" @click="handleLogoClick" />
<img src="../assets/logo2.png" alt="北京理工大学" @click="handleLogoClick" />
</div>
<h1 class="main-title">
<span class="title-text">智慧科研评估系统</span>
</h1>
<div class="header-placeholder"></div> <!-- 用于平衡布局的占位元素 -->
</header>
<!-- 主内容区域 -->
<div class="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-sidebar">
<div class="dimension-content">
<h2 class="dimension-section-title">评估维度设置</h2>
@ -43,59 +37,76 @@
</div>
</div>
</div>
</div>
</div> -->
<!-- 右侧内容区 -->
<div class="main-content">
<!-- 搜索和操作栏 -->
<div class="action-bar">
<div class="sidebar-header">
<h1 class="sidebar-title">
<span class="home-link" @click="jumpToDashboard">首页</span>&nbsp;>&nbsp;教师科研人才评估
</h1>
</div>
<div class="search-box">
<input
type="text"
placeholder="请输入教师姓名或ID"
v-model="searchQuery"
@input="handleSearch"
/>
<input type="text" placeholder="请输入教师姓名" 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"/>
<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 class="add-evaluation-btn" @click="openAddEvaluationDrawer">
新增评估
</button>
</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 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="教师照片" />
<img :src="teacher.basicInformation.name7" alt="教师照片" />
</div>
<div class="teacher-id">ID: {{ teacher.idcode || teacher.id }}</div>
<!-- <div class="teacher-id">姓名{{ teacher.basicInformation.name0 }}</div> -->
</div>
<div class="teacher-info">
<div class="info-row">
<span class="info-label">姓名:</span>
<span class="info-value">{{ teacher.name }}</span>
<el-tooltip effect="dark" :content="teacher.basicInformation.name0" placement="top">
<span class="info-value">{{ teacher.basicInformation.name0 }}</span>
</el-tooltip>
</div>
<div class="info-row">
<span class="info-label">教育背景:</span>
<span class="info-value">{{ teacher.education }}</span>
<span class="info-label">职称 / 职务:</span>
<el-tooltip effect="dark" :content="teacher.basicInformation.name2 + (teacher.basicInformation.name3 ? ' / ' + teacher.basicInformation.name3 : '')" placement="top">
<span class="info-value">
{{ teacher.basicInformation.name2 }}{{ teacher.basicInformation.name3 ? ' / ' + teacher.basicInformation.name3 : '' }}
</span>
</el-tooltip>
</div>
<div class="info-row">
<span class="info-label">论文:</span>
<span class="info-value">{{ teacher.papers }}</span>
<span class="info-label">所属学院:</span>
<el-tooltip effect="dark" :content="teacher.basicInformation.name4" placement="top">
<span class="info-value">{{ teacher.basicInformation.name4 }}</span>
</el-tooltip>
</div>
<div class="info-row">
<span class="info-label">项目:</span>
<span class="info-value">{{ teacher.projects }}</span>
<span class="info-label">最高学历:</span>
<el-tooltip effect="dark" :content="teacher.basicInformation.name5" placement="top">
<span class="info-value">{{ teacher.basicInformation.name5 }}</span>
</el-tooltip>
</div>
<div class="info-row">
<span class="info-label">学科方向:</span>
<el-tooltip effect="dark" :content="teacher.basicInformation.name6" placement="top">
<span class="info-value">{{ teacher.basicInformation.name6 }}</span>
</el-tooltip>
</div>
</div>
</div>
@ -109,46 +120,17 @@
</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" />
<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 { ElTooltip } from 'element-plus';
//
import DimensionDrawer from './DimensionDrawer.vue';
import TalentDrawerDetail from './TalentDrawerDetail.vue';
import {
TitleComponent,
@ -188,19 +170,6 @@ const showAddDimensionDialog = () => {
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 = () => {
@ -250,69 +219,17 @@ 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('获取维度数据失败');
}
await teacherLabs();
});
//
const loadTeachers = async () => {
const teacherLabs = 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));
});
}
});
const response = await axios.get(`${getApiBaseUrl()}/admin-api/pg/teacher-evaluation-results/get-release`);
teachers.value = response.data.data;
handleSearch();
} catch (error) {
console.error('获取教师数据失败:', error);
ElMessage.error('获取教师数据失败');
ElMessage.error('获科研人才中心数据失败');
}
};
@ -321,8 +238,7 @@ const dimensionDialogVisible = ref(false);
const isEditingDimension = ref(false);
const currentDimensionIndex = ref(-1);
const dimensionForm = ref({
name: '',
weight: 10
name: ''
});
const dimensionFormRef = ref(null);
const dimensionRules = {
@ -395,15 +311,14 @@ const handleSearch = () => {
filteredTeachers.value = teachers.value;
} else {
filteredTeachers.value = teachers.value.filter(teacher =>
teacher.name.includes(searchQuery.value) ||
teacher.id.includes(searchQuery.value)
teacher.basicInformation.name0.includes(searchQuery.value)
);
}
};
//
const initRadarCharts = () => {
filteredTeachers.value.forEach((teacher, index) => {
filteredTeachers.value?.forEach((teacher, index) => {
const chartDom = document.getElementById(`chart-${index}`);
if (!chartDom) return;
@ -412,28 +327,11 @@ const initRadarCharts = () => {
const chart = echarts.init(chartDom);
//
const indicators = dimensions.value.map(dim => ({
name: dim.name,
max: 100
const indicators = teacher.dimension.map(dim => ({
name: dim,
max: 50
}));
//
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,
@ -441,18 +339,31 @@ const initRadarCharts = () => {
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%'
radius: '50%'
},
series: [
{
type: 'radar',
data: [
{
value: evaluationData,
value: teacher.result,
name: '评估结果',
areaStyle: { opacity: 0 }, //
lineStyle: { color: 'rgb(63, 196, 15)', width: 2 }, // 线RGB(63, 196, 15)
itemStyle: { color: 'rgb(63, 196, 15)' } //
itemStyle: { color: 'rgb(63, 196, 15)' }, //
label: {
show: true, //
position: 'top', //
color: '#fff', //
fontSize: 10 //
},
emphasis: {
label: {
show: true, //
fontSize: 12, //
fontWeight: 'bold' //
}
}
}
]
}
@ -496,6 +407,14 @@ const openTeacherDetail = (teacher) => {
</style>
<style scoped>
.teacher-info span{
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
display: block;
}
.evaluation-page {
position: absolute;
top: 0;
@ -506,10 +425,11 @@ const openTeacherDetail = (teacher) => {
flex-direction: column;
background-color: #0c1633;
color: white;
overflow: hidden; /* 防止页面整体出现滚动条 */
overflow: hidden;
/* 防止页面整体出现滚动条 */
}
.dashboard-header {
/* .dashboard-header {
height: 60px;
padding: 0 20px;
display: flex;
@ -541,13 +461,13 @@ const openTeacherDetail = (teacher) => {
}
.title-line {
border: 2px solid rgba(73,134,255,1);
border: 2px solid rgba(73, 134, 255, 1);
width: 150px;
}
.title-text {
margin: 0 30px;
}
} */
.content-container {
display: flex;
@ -584,7 +504,8 @@ const openTeacherDetail = (teacher) => {
width: 280px;
display: flex;
flex-direction: column;
max-height: calc(100vh - 100px); /* 限制最大高度 */
max-height: calc(100vh - 100px);
/* 限制最大高度 */
}
.dimension-content {
@ -593,7 +514,8 @@ const openTeacherDetail = (teacher) => {
border-radius: 10px;
display: flex;
flex-direction: column;
overflow: hidden; /* 加上这个防止内容溢出 */
overflow: hidden;
/* 加上这个防止内容溢出 */
}
.dimension-section-title {
@ -601,7 +523,7 @@ const openTeacherDetail = (teacher) => {
font-size: 16px;
text-align: center;
padding-bottom: 10px;
border-bottom: 1px solid rgba(73,134,255,0.3);
border-bottom: 1px solid rgba(73, 134, 255, 0.3);
}
.dimension-list {
@ -609,8 +531,10 @@ const openTeacherDetail = (teacher) => {
display: flex;
flex-direction: column;
gap: 15px;
overflow-y: auto; /* 允许列表滚动 */
flex: 1; /* 让列表占满剩余空间 */
overflow-y: auto;
/* 允许列表滚动 */
flex: 1;
/* 让列表占满剩余空间 */
}
.dimension-item {
@ -618,15 +542,16 @@ const openTeacherDetail = (teacher) => {
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background-color: rgba(73,134,255,0.1);
background-color: rgba(73, 134, 255, 0.1);
border-radius: 4px;
border-left: 3px solid #4986ff;
cursor: pointer; /* 添加指针样式,提示可点击 */
cursor: pointer;
/* 添加指针样式,提示可点击 */
transition: background-color 0.2s;
}
.dimension-item:hover {
background-color: rgba(73,134,255,0.2);
background-color: rgba(73, 134, 255, 0.2);
}
.dimension-checkbox {
@ -658,7 +583,7 @@ const openTeacherDetail = (teacher) => {
align-items: center;
justify-content: center;
padding: 10px;
background-color: rgba(73,134,255,0.1);
background-color: rgba(73, 134, 255, 0.1);
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
@ -666,7 +591,7 @@ const openTeacherDetail = (teacher) => {
}
.dimension-add:hover {
background-color: rgba(73,134,255,0.2);
background-color: rgba(73, 134, 255, 0.2);
}
.add-icon {
@ -687,7 +612,8 @@ const openTeacherDetail = (teacher) => {
flex-direction: column;
padding: 0 10px;
overflow: hidden;
max-height: calc(100vh - 100px); /* 限制最大高度 */
max-height: calc(100vh - 100px);
/* 限制最大高度 */
}
/* 搜索和操作栏 */
@ -702,7 +628,7 @@ const openTeacherDetail = (teacher) => {
display: flex;
align-items: center;
width: 300px;
background-color: rgba(255,255,255,0.1);
background-color: rgba(255, 255, 255, 0.1);
border-radius: 20px;
overflow: hidden;
}
@ -717,7 +643,7 @@ const openTeacherDetail = (teacher) => {
}
.search-box input::placeholder {
color: rgba(255,255,255,0.5);
color: rgba(255, 255, 255, 0.5);
}
.search-button {
@ -735,9 +661,9 @@ const openTeacherDetail = (teacher) => {
}
.add-evaluation-btn {
background-color: rgba(14,62,167,1);
color: rgba(255,255,255,1);
border: 1px solid rgba(73,134,255,1);
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;
@ -749,9 +675,10 @@ const openTeacherDetail = (teacher) => {
/* 教师卡片网格 */
.teacher-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
overflow-y: auto; /* 允许卡片区域滚动 */
overflow-y: auto;
/* 允许卡片区域滚动 */
flex: 1;
padding: 20px;
background-color: #262F50;
@ -767,7 +694,7 @@ const openTeacherDetail = (teacher) => {
:deep(.dimension-dialog .el-dialog__header) {
color: white;
border-bottom: 1px solid rgba(73,134,255,0.3);
border-bottom: 1px solid rgba(73, 134, 255, 0.3);
}
:deep(.dimension-dialog .el-dialog__body) {
@ -777,17 +704,17 @@ const openTeacherDetail = (teacher) => {
: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);
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);
color: rgba(255, 255, 255, 0.8);
}
:deep(.dimension-dialog .el-dialog__footer) {
border-top: 1px solid rgba(73,134,255,0.3);
border-top: 1px solid rgba(73, 134, 255, 0.3);
}
@media (max-width: 1200px) {

File diff suppressed because it is too large Load Diff

View File

@ -118,7 +118,9 @@
font-size: 16px;
text-align: center;
}
h2{
text-align: left!important;
}
.panel-header {
display: flex;
justify-content: space-between;
@ -204,6 +206,10 @@
.info-value {
font-weight: bold;
/* white-space: nowrap; */
text-overflow: ellipsis;
overflow: hidden;
font-size: 12px;
}
/* ========= 按钮样式 ========= */
@ -301,3 +307,108 @@
min-height: 400px;
}
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 80px;
background-color: rgba(12, 22, 51, 0.8);
position: relative;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
flex: 1; /* 占据可用空间 */
}
.logo img {
height: 40px;
object-fit: contain;
cursor: pointer;
}
.main-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
margin: 0;
font-size: 32px;
font-weight: bold;
color: white;
white-space: nowrap;
/* background: linear-gradient(90deg, transparent, rgba(73, 134, 255, 0.3), transparent); */
padding: 0 200px;
}
.main-title::before,
.main-title::after {
content: "";
position: absolute;
top: 50%;
width: 200px;
height: 4px;
background: linear-gradient(90deg, transparent, rgba(73, 134, 255, 0.8));
}
.main-title::before {
left: 0;
}
.main-title::after {
right: 0;
transform: scaleX(-1);
}
.header-placeholder {
flex: 1; /* 与logo对称的占位元素 */
}
/* 响应式调整 */
@media (max-width: 1200px) {
.dashboard-header {
height: 60px;
padding: 0 10px;
}
.logo img {
height: 30px;
}
.main-title {
font-size: 28px;
padding: 0 150px;
}
.main-title::before,
.main-title::after {
width: 150px;
height: 3px;
}
}
/* 响应式调整 */
@media (max-width: 768px) {
.dashboard-header {
height: 60px;
padding: 0 10px;
}
.logo img {
height: 30px;
}
.main-title {
font-size: 24px;
padding: 0 30px;
}
.main-title::before,
.main-title::after {
width: 30px;
height: 2px;
}
}

View File

@ -3,10 +3,10 @@ const env = import.meta.env.MODE || 'development';
const config = {
development: {
apiBaseUrl: 'http://127.0.0.1:48996',
apiBaseUrl: 'http://192.168.5.49:48080',
},
production: {
apiBaseUrl: 'http://36.103.203.89:48996',
apiBaseUrl: 'http://36.103.203.89:48088',
}
};

77
upload.js Normal file
View File

@ -0,0 +1,77 @@
import SftpClient from 'ssh2-sftp-client';
import path from 'path';
import fs from 'fs/promises'; // 使用 fs 的 Promise 版本
import { fileURLToPath } from 'url';
// 如果使用环境变量,取消下面这行的注释
// require('dotenv').config();
// 获取当前模块的文件路径
const __filename = fileURLToPath(import.meta.url);
// 获取当前模块所在的目录路径
const __dirname = path.dirname(__filename);
const config = {
// 如果使用环境变量
// host: process.env.SFTP_HOST,
// port: parseInt(process.env.SFTP_PORT) || 22,
// username: process.env.SFTP_USER,
// password: process.env.SFTP_PASS,
// 直接配置
host: '36.103.203.89',
port: 22,
username: 'liuyang',
password: 'Asdf!234',
// 或者使用私钥认证
// privateKey: fs.readFileSync('/path/to/your/key')
};
// 本地打包目录
const localDir = path.join(__dirname, 'dist');
// 服务器目标目录
// const remoteDir = process.env.SFTP_REMOTE_DIR; // 如果使用环境变量
const remoteDir = '/home/ubuntu/mnt/teacher_test/nginx-1/dist';
async function uploadToServer() {
const sftp = new SftpClient();
try {
console.log('开始连接服务器...');
await sftp.connect(config);
console.log('连接成功,检查远程目录是否存在...');
const exists = await sftp.exists(remoteDir);
if (exists) {
console.log('清空远程目录中的内容...');
// 获取远程目录中的所有文件和文件夹
const remoteList = await sftp.list(remoteDir);
// 依次删除每个文件和文件夹
for (const item of remoteList) {
const remotePath = `${remoteDir}/${item.name}`;
if (item.type === 'd') {
// 如果是目录,递归删除
await sftp.rmdir(remotePath, true);
} else {
// 如果是文件,直接删除
await sftp.delete(remotePath);
}
}
} else {
console.log('远程目录不存在,创建目录...');
await sftp.mkdir(remoteDir, true);
}
console.log('开始上传文件...');
await sftp.uploadDir(localDir, remoteDir);
console.log('文件上传完成!');
return true;
} catch (err) {
console.error('上传过程中发生错误:', err);
return false;
} finally {
sftp.end();
}
}
uploadToServer();