init
17
.cursor/rules/myrule.mdc
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
1.无论我输入什么语言,你都使用中文回答我
|
||||
2.第一次对话你应该先查看目录,了解项目情况
|
||||
3.每次对话结束你都应该礼貌的说,尊敬的李昂先生,以上就是我能为你提供的全部内容如果有其他需要请继续找我
|
||||
4.项目根目录下的PYTHON_ENV_SETUP_README.md为python环境启动文档 如果你需要调用python环境 参照文档方式通过conda启动
|
||||
5.后端使用python fastapi sqlite 存储在根目录的backend文件夹下
|
||||
6.每次更改代码都要看看是否有需要写入readme.md的内容如果有写入其中
|
||||
7.每次都要阅读readme.md文件
|
||||
8.我的电脑是windows的 无法使用&& 这个命令 例如‘cd backend && uvicorn main:app --reload’ 这样的语句是无法运行的 需要拆分运行
|
||||
|
||||
9.我的环境使用了conda 需要使用conda activate fast-dashboard-env 进入对应环境
|
||||
10.根目录下backend内为后端代码
|
||||
11. 我的电脑使用的windows系统 如果需要调用windows系统功能执行命令 需要执行windows下有的命令
|
11
.dockerignore
Normal file
@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
dist
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
.DS_Store
|
||||
*.md
|
||||
!README.md
|
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
# 第一阶段:构建应用
|
||||
FROM node:20 as build-stage
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# 第二阶段:运行应用
|
||||
FROM nginx:stable-alpine as production-stage
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 48100
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
78
PYTHON_ENV_SETUP_README.md
Normal file
@ -0,0 +1,78 @@
|
||||
# Python 环境配置指南
|
||||
|
||||
## 前提条件
|
||||
- 已安装 Conda(Anaconda 或 Miniconda)
|
||||
- Windows 操作系统
|
||||
|
||||
## 自动配置方法
|
||||
|
||||
1. 双击项目根目录中的 `setup_py_env.bat` 文件
|
||||
2. 等待脚本完成环境配置和依赖安装
|
||||
3. 环境配置完成后,窗口会显示使用说明
|
||||
|
||||
## 手动配置方法
|
||||
|
||||
如果自动配置方法不起作用,请按照以下步骤手动配置:
|
||||
|
||||
1. 打开命令提示符(CMD)或 PowerShell
|
||||
2. 创建新的 Conda 环境:
|
||||
```
|
||||
conda create -n fast-dashboard-env python=3.11 -y
|
||||
```
|
||||
3. 激活环境:
|
||||
```
|
||||
conda activate fast-dashboard-env
|
||||
```
|
||||
4. 安装项目依赖:
|
||||
```
|
||||
cd 项目根目录
|
||||
pip install -r backend/requirements.txt
|
||||
```
|
||||
|
||||
## 运行后端服务器
|
||||
|
||||
1. 激活 Conda 环境(如果尚未激活):
|
||||
```
|
||||
conda activate fast-dashboard-env
|
||||
```
|
||||
2. 进入后端目录:
|
||||
```
|
||||
cd backend
|
||||
```
|
||||
3. 启动服务器:
|
||||
```
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
4. 服务器将在 http://127.0.0.1:8000 启动
|
||||
|
||||
## 环境依赖列表
|
||||
|
||||
本项目使用以下 Python 库:
|
||||
|
||||
- fastapi==0.109.2
|
||||
- uvicorn==0.27.1
|
||||
- pydantic==2.6.1
|
||||
- python-multipart==0.0.9
|
||||
- python-jose==3.3.0
|
||||
- passlib==1.7.4
|
||||
- bcrypt==4.1.2
|
||||
- requests==2.31.0
|
||||
- beautifulsoup4==4.12.2
|
||||
- sqlalchemy==2.0.26
|
||||
- aiosqlite==0.19.0
|
||||
- alembic==1.13.1
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **无法激活 Conda 环境**
|
||||
- 确保已正确安装 Conda
|
||||
- 尝试重新打开终端
|
||||
- 运行 `conda init powershell`(如果使用 PowerShell)
|
||||
|
||||
2. **依赖安装失败**
|
||||
- 检查网络连接
|
||||
- 尝试使用镜像源:`pip install -r backend/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple`
|
||||
|
||||
3. **启动服务器时出错**
|
||||
- 确保所有依赖都已正确安装
|
||||
- 检查 backend 目录下是否有 data 文件夹,如没有则创建
|
155
README.md
Normal file
@ -0,0 +1,155 @@
|
||||
# Fast Dashboard 项目
|
||||
|
||||
## 项目概述
|
||||
智能评估仪表板系统,包含人才评估和工程研究中心评估功能。
|
||||
|
||||
## 技术栈
|
||||
- 前端:Vue 3 + Element Plus + ECharts
|
||||
- 后端:Python FastAPI + SQLite
|
||||
- 部署:Docker + Nginx
|
||||
|
||||
## 近期问题记录
|
||||
|
||||
### 2024年 - 爬虫图片URL路径问题
|
||||
|
||||
**问题描述:**
|
||||
- 爬虫返回的图片URL多了一个 `/szdw/` 路径
|
||||
- 正确地址应该是:`https://ac.bit.edu.cn/images/2024-02/f5a28fd026884e328c2280ef42566f6b.png`
|
||||
- 错误地址返回:`https://ac.bit.edu.cn/szdw/images/2024-02/f5a28fd026884e328c2280ef42566f6b.png`
|
||||
|
||||
**问题原因:**
|
||||
- 在处理相对路径 `../../../` 时,原有逻辑从当前URL推断基础路径不准确
|
||||
- 导致多拼接了 `/szdw/` 路径段
|
||||
|
||||
**解决方案:**
|
||||
1. **修改图片URL处理逻辑** (`backend/main.py` 第610-625行)
|
||||
- 简化相对路径处理,直接移除 `../../../` 前缀
|
||||
- 统一使用基础URL (`https://ac.bit.edu.cn`) 拼接图片路径
|
||||
- 增加对 `../../` 和 `../` 路径的处理
|
||||
|
||||
**修复后的代码逻辑:**
|
||||
```python
|
||||
if img_src.startswith('../../../'):
|
||||
img_relative = img_src[9:] # 移除 '../../../'
|
||||
img_url = f"{base_url}/{img_relative}"
|
||||
elif img_src.startswith('../../'):
|
||||
img_relative = img_src[6:] # 移除 '../../'
|
||||
img_url = f"{base_url}/{img_relative}"
|
||||
elif img_src.startswith('../'):
|
||||
img_relative = img_src[3:] # 移除 '../'
|
||||
img_url = f"{base_url}/{img_relative}"
|
||||
```
|
||||
|
||||
### 2024年 - 人才评估接口网络连接问题
|
||||
|
||||
**问题描述:**
|
||||
- 人才评估调用接口 `/api/scrape-url`
|
||||
- 本地测试正常,服务器部署后报错500
|
||||
- 错误信息:`HTTPSConnectionPool(host='ac.bit.edu.cn', port=443): Read timed out. (read timeout=30)`
|
||||
|
||||
**问题原因:**
|
||||
1. **服务器网络环境限制**:服务器无法正常访问外部网站 ac.bit.edu.cn
|
||||
2. **网络延迟问题**:服务器到目标网站的网络延迟较高,30秒超时不够
|
||||
3. **DNS解析问题**:可能存在DNS配置问题
|
||||
4. **防火墙限制**:服务器出站规则可能阻止了对目标域名的访问
|
||||
|
||||
**解决方案:**
|
||||
|
||||
1. **创建了改进版本的URL抓取函数** (`backend/scrape_url_improved.py`)
|
||||
- 增加DNS解析检查
|
||||
- 实现重试机制(最多3次重试)
|
||||
- 延长超时时间(连接30秒,读取90秒)
|
||||
- 添加详细的错误日志
|
||||
- 临时禁用SSL验证避免证书问题
|
||||
|
||||
2. **创建了网络诊断工具** (`backend/network_diagnostic.py`)
|
||||
- DNS解析测试
|
||||
- TCP连接测试
|
||||
- HTTP请求测试
|
||||
- 系统网络配置检查
|
||||
- 自动生成解决建议
|
||||
|
||||
**使用方法:**
|
||||
|
||||
```bash
|
||||
# 进入后端目录
|
||||
cd backend
|
||||
|
||||
# 激活conda环境
|
||||
conda activate fast-dashboard-env
|
||||
|
||||
# 运行网络诊断工具
|
||||
python network_diagnostic.py
|
||||
|
||||
# 或者在main.py中集成改进版本的抓取函数
|
||||
```
|
||||
|
||||
**建议的部署检查项:**
|
||||
1. 检查服务器DNS配置
|
||||
2. 确认防火墙出站规则
|
||||
3. 测试到目标网站的网络连通性
|
||||
4. 考虑配置代理服务器
|
||||
5. 检查云服务器安全组设置
|
||||
|
||||
## 环境配置
|
||||
|
||||
### Python环境
|
||||
参考 `PYTHON_ENV_SETUP_README.md` 文档进行环境配置。
|
||||
|
||||
### 启动项目
|
||||
|
||||
#### 本地开发启动(仅本机访问)
|
||||
```bash
|
||||
# 后端启动
|
||||
cd backend
|
||||
conda activate fast-dashboard-env
|
||||
uvicorn main:app --reload
|
||||
|
||||
# 前端启动
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### IP网络访问启动(局域网内其他设备可访问)
|
||||
|
||||
**方法1:使用便捷启动脚本**
|
||||
```bash
|
||||
# 双击运行根目录下的 start_with_ip.bat 文件
|
||||
# 或在命令行中运行:
|
||||
start_with_ip.bat
|
||||
```
|
||||
|
||||
**方法2:手动启动**
|
||||
```bash
|
||||
# 后端启动 (监听所有IP)
|
||||
cd backend
|
||||
conda activate fast-dashboard-env
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
# 前端启动 (监听所有IP)
|
||||
npm run dev -- --host 0.0.0.0
|
||||
```
|
||||
|
||||
#### 访问地址
|
||||
- **本地访问**: http://localhost:5173 (前端) | http://localhost:8000 (后端)
|
||||
- **网络访问**: http://您的IP:5173 (前端) | http://您的IP:8000 (后端)
|
||||
- **当前您的IP**: http://192.168.18.108:5173 (前端) | http://192.168.18.108:8000 (后端)
|
||||
|
||||
#### 防火墙配置
|
||||
如果其他设备无法访问,请确保Windows防火墙允许以下端口:
|
||||
- 端口 5173 (前端)
|
||||
- 端口 8000 (后端)
|
||||
|
||||
## 文件结构
|
||||
```
|
||||
fast-dashboard/
|
||||
├── backend/ # 后端代码
|
||||
│ ├── main.py # 主应用文件
|
||||
│ ├── scrape_url_improved.py # 改进版URL抓取
|
||||
│ ├── network_diagnostic.py # 网络诊断工具
|
||||
│ └── ...
|
||||
├── src/ # 前端代码
|
||||
│ ├── components/ # Vue组件
|
||||
│ └── ...
|
||||
├── README.md # 项目文档
|
||||
└── PYTHON_ENV_SETUP_README.md # Python环境配置文档
|
||||
```
|
29
backend/Dockerfile
Normal file
@ -0,0 +1,29 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
sqlite3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制并安装Python依赖
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# 创建数据目录并设置权限
|
||||
RUN mkdir -p data && chmod 777 data
|
||||
RUN mkdir -p static/images && chmod 777 static/images
|
||||
|
||||
# 初始化数据库
|
||||
RUN python init_db.py
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 48996
|
||||
|
||||
# 启动应用
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "48996"]
|
292
backend/LAB_DATA_UPDATE_GUIDE.md
Normal file
@ -0,0 +1,292 @@
|
||||
# 工程研究中心数据更新指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档详细说明如何将 `src/assets/实验室.json` 中的工程研究中心数据导入到数据库中。当有新的工程研究中心数据或者数据需要更新时,请按照本指南进行操作。
|
||||
|
||||
## 文件说明
|
||||
|
||||
### 主要文件
|
||||
- **数据源文件**: `src/assets/实验室.json` - 包含所有工程研究中心的多年度数据
|
||||
- **导入脚本**: `backend/import_lab_data_full.py` - 完整的数据导入脚本
|
||||
- **JSON修复脚本**: `backend/fix_json_format.py` - 修复JSON格式问题
|
||||
- **数据类型修复脚本**: `backend/fix_year_data_types.py` - 修复年份字段类型问题
|
||||
- **数据检查脚本**: `backend/check_specific_lab.py` - 检查特定工程研究中心数据
|
||||
- **数据库模型**: `backend/models.py` - 定义Lab表结构
|
||||
- **本文档**: `backend/LAB_DATA_UPDATE_GUIDE.md` - 操作指南
|
||||
|
||||
### 数据结构
|
||||
JSON文件包含工程研究中心数组,每个工程研究中心包含:
|
||||
```json
|
||||
{
|
||||
"中心名称": "工程研究中心名称",
|
||||
"中心编号": "工程研究中心编号",
|
||||
"年度数据": [
|
||||
{
|
||||
"归属年份": "2024",
|
||||
"所属学校": "学校名称",
|
||||
"主管部门": "部门名称",
|
||||
... // 详细的年度数据
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 数据库字段映射
|
||||
|
||||
### 基本信息字段
|
||||
| JSON字段 | 数据库字段 | 类型 | 说明 |
|
||||
|---------|-----------|------|------|
|
||||
| 中心名称 | name | String | 工程研究中心名称 |
|
||||
| 中心编号 | center_number | String | 工程研究中心编号 |
|
||||
| 所属学校 | school | String | 所属学校 |
|
||||
| 主管部门 | department | String | 主管部门 |
|
||||
| 所属领域 | field | String | 所属领域 |
|
||||
| 归属年份 | current_year | String | 当前评估年份 |
|
||||
|
||||
### 详细信息字段
|
||||
| JSON字段 | 数据库字段 | 类型 | 说明 |
|
||||
|---------|-----------|------|------|
|
||||
| 技术攻关与创新情况 | innovation_situation | Text | 技术创新描述 |
|
||||
| 1.总体情况 | overall_situation | Text | 总体情况描述 |
|
||||
| 2.工程化案例 | engineering_cases | Text | 工程化案例 |
|
||||
| 3.行业服务情况 | industry_service | Text | 行业服务情况 |
|
||||
| 1.学科发展支撑情况 | discipline_support | Text | 学科发展支撑 |
|
||||
| 2.人才培养情况 | talent_cultivation | Text | 人才培养情况 |
|
||||
| 3.研究队伍建设情况 | team_building | Text | 队伍建设情况 |
|
||||
|
||||
### 统计数据字段
|
||||
| JSON字段 | 数据库字段 | 类型 | 说明 |
|
||||
|---------|-----------|------|------|
|
||||
| 国家级科技奖励一等奖(项) | national_awards_first | Integer | 国家一等奖数量 |
|
||||
| 国家级科技奖励二等奖(项) | national_awards_second | Integer | 国家二等奖数量 |
|
||||
| 省、部级科技奖励一等奖(项) | provincial_awards_first | Integer | 省部一等奖数量 |
|
||||
| 省、部级科技奖励二等奖(项) | provincial_awards_second | Integer | 省部二等奖数量 |
|
||||
| 有效专利(项) | valid_patents | Integer | 有效专利数量 |
|
||||
| 在读博士生 | doctoral_students | Integer | 博士生数量 |
|
||||
| 在读硕士生 | master_students | Integer | 硕士生数量 |
|
||||
| 固定人员(人) | fixed_personnel | Integer | 固定人员数量 |
|
||||
| 流动人员(人) | mobile_personnel | Integer | 流动人员数量 |
|
||||
| 当年项目到账总经费(万元) | total_funding | Float | 总经费 |
|
||||
|
||||
### 特殊字段
|
||||
| 字段 | 说明 |
|
||||
|-----|------|
|
||||
| annual_data | JSON格式存储所有年度数据,包含多年完整信息 |
|
||||
| id | 自动生成的UUID,作为主键 |
|
||||
| idcode | 使用中心编号作为显示ID |
|
||||
|
||||
## 操作步骤
|
||||
|
||||
### 1. 准备工作
|
||||
|
||||
确保以下条件满足:
|
||||
- [x] 后端服务已停止运行
|
||||
- [x] 数据库文件 `backend/data/app.db` 存在
|
||||
- [x] Python环境已激活 (`conda activate fast-dashboard-env`)
|
||||
- [x] JSON数据文件 `src/assets/实验室.json` 已更新
|
||||
|
||||
### 2. 环境准备
|
||||
|
||||
在命令行中执行:
|
||||
```powershell
|
||||
# 激活conda环境
|
||||
conda activate fast-dashboard-env
|
||||
|
||||
# 进入后端目录
|
||||
cd backend
|
||||
```
|
||||
|
||||
### 3. 执行数据导入
|
||||
|
||||
运行导入脚本:
|
||||
```powershell
|
||||
python import_lab_data_full.py
|
||||
```
|
||||
|
||||
### 4. 导入过程说明
|
||||
|
||||
脚本会执行以下操作:
|
||||
|
||||
1. **数据验证**:检查JSON文件是否存在
|
||||
2. **数据读取**:解析JSON文件内容
|
||||
3. **数据处理**:
|
||||
- 遍历每个工程研究中心
|
||||
- 检查是否已存在(根据中心编号或名称)
|
||||
- 如果存在则更新,否则创建新记录
|
||||
- 处理多年度数据,提取最新年份作为当前数据
|
||||
- 安全转换数据类型(整数、浮点数、字符串)
|
||||
4. **数据库操作**:
|
||||
- 添加新记录或更新现有记录
|
||||
- 提交事务
|
||||
- 显示统计信息
|
||||
|
||||
### 5. 输出信息解读
|
||||
|
||||
脚本运行时会显示:
|
||||
- 📖 正在读取数据文件
|
||||
- ✅ 成功读取数据,共 X 个工程研究中心
|
||||
- 🔄 正在处理工程研究中心: XXX (编号: XXX)
|
||||
- ➕ 创建新工程研究中心 / 📝 工程研究中心已存在,更新数据
|
||||
- ✅ 年度数据: X 年, 最新年份: XXXX
|
||||
- 💾 正在保存到数据库
|
||||
- 📊 统计信息
|
||||
|
||||
### 6. 验证导入结果
|
||||
|
||||
导入完成后,可以通过以下方式验证:
|
||||
|
||||
1. **启动后端服务**:
|
||||
```powershell
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
2. **访问API接口**:
|
||||
```
|
||||
GET http://localhost:8000/labs/
|
||||
```
|
||||
|
||||
3. **查看前端页面**:
|
||||
打开前端应用,查看工程研究中心列表和详情页
|
||||
|
||||
## 常见问题及解决方案
|
||||
|
||||
### 1. JSON格式错误
|
||||
**问题**: JSON文件中包含Python的`None`值,导致解析失败
|
||||
**解决方案**:
|
||||
```bash
|
||||
python fix_json_format.py
|
||||
```
|
||||
|
||||
### 2. 年份字段类型错误
|
||||
**问题**: 前端报错 `TypeError: b.year.localeCompare is not a function`
|
||||
**原因**: 年度数据中的`归属年份`字段是数字类型,前端期望字符串类型
|
||||
**解决方案**:
|
||||
```bash
|
||||
python fix_year_data_types.py
|
||||
```
|
||||
|
||||
### 3. 检查特定工程研究中心数据
|
||||
**用途**: 当某个工程研究中心出现问题时,可以单独检查其数据格式
|
||||
**使用方法**:
|
||||
```bash
|
||||
python check_specific_lab.py
|
||||
```
|
||||
修改脚本中的工程研究中心名称来检查不同工程研究中心。
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见错误及解决方案
|
||||
|
||||
#### 1. 文件不存在错误
|
||||
```
|
||||
❌ 错误:找不到数据文件
|
||||
```
|
||||
**解决方案**:确认 `src/assets/实验室.json` 文件存在且路径正确
|
||||
|
||||
#### 2. 数据库连接错误
|
||||
```
|
||||
❌ 数据库操作失败
|
||||
```
|
||||
**解决方案**:
|
||||
- 确认数据库文件 `backend/data/app.db` 存在
|
||||
- 确认没有其他进程占用数据库
|
||||
- 确认有足够的磁盘空间
|
||||
|
||||
#### 3. JSON格式错误
|
||||
```
|
||||
❌ 导入失败: JSON decode error
|
||||
```
|
||||
**解决方案**:
|
||||
- 使用JSON验证工具检查文件格式
|
||||
- 确认文件编码为UTF-8
|
||||
- 检查是否有多余的逗号或括号
|
||||
|
||||
#### 4. 数据类型转换错误
|
||||
```
|
||||
❌ 处理工程研究中心 XXX 时出错
|
||||
```
|
||||
**解决方案**:
|
||||
- 检查JSON中数值字段是否包含非数字字符
|
||||
- 脚本有safe_int和safe_float函数来处理大部分类型错误
|
||||
- 如果持续出错,可以手动检查该工程研究中心的数据
|
||||
|
||||
### 数据一致性检查
|
||||
|
||||
导入后建议进行以下检查:
|
||||
|
||||
1. **数量检查**:
|
||||
```sql
|
||||
SELECT COUNT(*) FROM labs;
|
||||
```
|
||||
|
||||
2. **年度数据检查**:
|
||||
```sql
|
||||
SELECT name, current_year, json_length(annual_data) as year_count
|
||||
FROM labs
|
||||
WHERE annual_data IS NOT NULL;
|
||||
```
|
||||
|
||||
3. **统计数据检查**:
|
||||
```sql
|
||||
SELECT name, valid_patents, doctoral_students, total_funding
|
||||
FROM labs
|
||||
ORDER BY total_funding DESC;
|
||||
```
|
||||
|
||||
## 数据更新策略
|
||||
|
||||
### 完全重新导入
|
||||
如果数据变化很大,建议:
|
||||
1. 备份现有数据库
|
||||
2. 清空labs表
|
||||
3. 重新导入所有数据
|
||||
|
||||
### 增量更新
|
||||
如果只是部分数据更新:
|
||||
1. 脚本会自动检测已存在的工程研究中心
|
||||
2. 根据中心编号或名称匹配
|
||||
3. 更新现有记录的数据
|
||||
|
||||
### 数据备份
|
||||
在大规模更新前,建议备份:
|
||||
```powershell
|
||||
copy backend\data\app.db backend\data\app_backup_$(Get-Date -Format "yyyyMMdd_HHmmss").db
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 大量数据处理
|
||||
如果数据量很大(>100个工程研究中心):
|
||||
1. 考虑分批处理
|
||||
2. 添加进度条显示
|
||||
3. 使用批量插入操作
|
||||
|
||||
### 内存优化
|
||||
- 避免一次性加载所有数据到内存
|
||||
- 使用流式处理方式
|
||||
- 及时释放不需要的对象
|
||||
|
||||
## 维护建议
|
||||
|
||||
### 定期任务
|
||||
1. **每月检查**:验证数据一致性
|
||||
2. **每季度备份**:完整备份数据库
|
||||
3. **每年更新**:根据新的数据字段要求更新脚本
|
||||
|
||||
### 版本控制
|
||||
- 对导入脚本进行版本控制
|
||||
- 记录每次数据更新的变更日志
|
||||
- 保留历史数据备份
|
||||
|
||||
## 联系信息
|
||||
|
||||
如有问题或需要技术支持,请联系:
|
||||
- 开发团队:AI助手
|
||||
- 文档更新:每次数据模型变更时同步更新
|
||||
|
||||
---
|
||||
|
||||
**最后更新时间**:2024年度
|
||||
**文档版本**:v1.0
|
||||
**适用环境**:Windows + Python + FastAPI + SQLite
|
83
backend/alter_table.py
Normal file
@ -0,0 +1,83 @@
|
||||
import sqlite3
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 数据库路径
|
||||
DB_DIR = "data"
|
||||
DB_PATH = os.path.join(DB_DIR, "app.db")
|
||||
|
||||
def check_and_alter_table():
|
||||
# 检查数据库文件是否存在
|
||||
if not os.path.exists(DB_PATH):
|
||||
logger.error(f"数据库文件 {DB_PATH} 不存在")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 检查talents表是否存在idcode字段
|
||||
cursor.execute("PRAGMA table_info(talents)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
if 'idcode' not in columns:
|
||||
logger.info("talents表中添加idcode字段")
|
||||
cursor.execute("ALTER TABLE talents ADD COLUMN idcode TEXT")
|
||||
conn.commit()
|
||||
else:
|
||||
logger.info("talents表已包含idcode字段,无需修改")
|
||||
|
||||
# 检查talents表是否存在educationBackground字段
|
||||
if 'educationBackground' not in columns:
|
||||
logger.info("talents表中添加educationBackground字段")
|
||||
cursor.execute("ALTER TABLE talents ADD COLUMN educationBackground TEXT")
|
||||
conn.commit()
|
||||
else:
|
||||
logger.info("talents表已包含educationBackground字段,无需修改")
|
||||
|
||||
# 检查labs表是否存在idcode字段
|
||||
cursor.execute("PRAGMA table_info(labs)")
|
||||
labs_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
if 'idcode' not in labs_columns:
|
||||
logger.info("labs表中添加idcode字段")
|
||||
cursor.execute("ALTER TABLE labs ADD COLUMN idcode TEXT")
|
||||
conn.commit()
|
||||
else:
|
||||
logger.info("labs表已包含idcode字段,无需修改")
|
||||
|
||||
# 检查dimensions表是否存在sub_dimensions字段
|
||||
cursor.execute("PRAGMA table_info(dimensions)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
if 'sub_dimensions' not in columns:
|
||||
logger.info("dimensions表中添加sub_dimensions字段")
|
||||
cursor.execute("ALTER TABLE dimensions ADD COLUMN sub_dimensions JSON")
|
||||
conn.commit()
|
||||
else:
|
||||
logger.info("dimensions表已包含sub_dimensions字段,无需修改")
|
||||
|
||||
# 检查dimensions表是否存在parent_id字段
|
||||
if 'parent_id' not in columns:
|
||||
logger.info("dimensions表中添加parent_id字段")
|
||||
cursor.execute("ALTER TABLE dimensions ADD COLUMN parent_id INTEGER REFERENCES dimensions(id)")
|
||||
conn.commit()
|
||||
else:
|
||||
logger.info("dimensions表已包含parent_id字段,无需修改")
|
||||
|
||||
# 检查labs表是否存在sub_dimension_evaluations字段
|
||||
if 'sub_dimension_evaluations' not in labs_columns:
|
||||
logger.info("labs表中添加sub_dimension_evaluations字段")
|
||||
cursor.execute("ALTER TABLE labs ADD COLUMN sub_dimension_evaluations TEXT")
|
||||
conn.commit()
|
||||
else:
|
||||
logger.info("labs表已包含sub_dimension_evaluations字段,无需修改")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_and_alter_table()
|
181
backend/crud.py
Normal file
@ -0,0 +1,181 @@
|
||||
from sqlalchemy.orm import Session
|
||||
import models
|
||||
import schemas
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from typing import List, Optional, Dict, Any
|
||||
import datetime
|
||||
import json
|
||||
|
||||
# 密码处理上下文
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
# 验证密码
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
# 获取密码哈希
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
# 用户相关操作
|
||||
def get_user(db: Session, username: str):
|
||||
return db.query(models.User).filter(models.User.username == username).first()
|
||||
|
||||
def get_users(db: Session, skip: int = 0, limit: int = 100):
|
||||
return db.query(models.User).offset(skip).limit(limit).all()
|
||||
|
||||
def create_user(db: Session, username: str, password: str, email: Optional[str] = None, full_name: Optional[str] = None):
|
||||
hashed_password = get_password_hash(password)
|
||||
db_user = models.User(
|
||||
username=username,
|
||||
email=email,
|
||||
full_name=full_name,
|
||||
hashed_password=hashed_password
|
||||
)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
def authenticate_user(db: Session, username: str, password: str):
|
||||
user = get_user(db, username)
|
||||
if not user:
|
||||
return False
|
||||
if not verify_password(password, user.hashed_password):
|
||||
return False
|
||||
return user
|
||||
|
||||
# 人才相关操作
|
||||
def get_talent(db: Session, talent_id: str):
|
||||
return db.query(models.Talent).filter(models.Talent.id == talent_id).first()
|
||||
|
||||
def get_talents(db: Session, skip: int = 0, limit: int = 100):
|
||||
return db.query(models.Talent).offset(skip).limit(limit).all()
|
||||
|
||||
def create_talent(db: Session, talent: schemas.TalentCreate):
|
||||
db_talent = models.Talent(**talent.dict())
|
||||
db.add(db_talent)
|
||||
db.commit()
|
||||
db.refresh(db_talent)
|
||||
return db_talent
|
||||
|
||||
def update_talent(db: Session, talent_id: str, talent_data: Dict[str, Any]):
|
||||
db_talent = get_talent(db, talent_id)
|
||||
if db_talent:
|
||||
for key, value in talent_data.items():
|
||||
setattr(db_talent, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_talent)
|
||||
return db_talent
|
||||
|
||||
def delete_talent(db: Session, talent_id: str):
|
||||
db_talent = get_talent(db, talent_id)
|
||||
if db_talent:
|
||||
db.delete(db_talent)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
# 工程研究中心相关操作
|
||||
def get_lab(db: Session, lab_id: str):
|
||||
return db.query(models.Lab).filter(models.Lab.id == lab_id).first()
|
||||
|
||||
def get_labs(db: Session, skip: int = 0, limit: int = 100):
|
||||
return db.query(models.Lab).offset(skip).limit(limit).all()
|
||||
|
||||
def create_lab(db: Session, lab: schemas.LabCreate):
|
||||
db_lab = models.Lab(**lab.dict())
|
||||
db.add(db_lab)
|
||||
db.commit()
|
||||
db.refresh(db_lab)
|
||||
return db_lab
|
||||
|
||||
def update_lab(db: Session, lab_id: str, lab_data: Dict[str, Any]):
|
||||
db_lab = get_lab(db, lab_id)
|
||||
if db_lab:
|
||||
for key, value in lab_data.items():
|
||||
setattr(db_lab, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_lab)
|
||||
return db_lab
|
||||
|
||||
# 仪表盘数据相关操作
|
||||
def get_dashboard(db: Session):
|
||||
dashboard = db.query(models.DashboardData).first()
|
||||
return dashboard
|
||||
|
||||
def get_news(db: Session, skip: int = 0, limit: int = 100):
|
||||
return db.query(models.News).offset(skip).limit(limit).all()
|
||||
|
||||
def create_news(db: Session, title: str, date: str, dashboard_id: int):
|
||||
db_news = models.News(title=title, date=date, dashboard_id=dashboard_id)
|
||||
db.add(db_news)
|
||||
db.commit()
|
||||
db.refresh(db_news)
|
||||
return db_news
|
||||
|
||||
def update_dashboard(db: Session, dashboard_data: Dict[str, Any]):
|
||||
dashboard = get_dashboard(db)
|
||||
if dashboard:
|
||||
# 更新简单字段
|
||||
for key, value in dashboard_data.items():
|
||||
if key != "newsData": # 新闻数据单独处理
|
||||
setattr(dashboard, key, value)
|
||||
|
||||
# 如果有新闻数据需要更新
|
||||
if "newsData" in dashboard_data:
|
||||
# 删除所有旧新闻
|
||||
db.query(models.News).filter(models.News.dashboard_id == dashboard.id).delete()
|
||||
|
||||
# 添加新的新闻
|
||||
for news_item in dashboard_data["newsData"]:
|
||||
db_news = models.News(
|
||||
title=news_item["title"],
|
||||
date=news_item["date"],
|
||||
dashboard_id=dashboard.id
|
||||
)
|
||||
db.add(db_news)
|
||||
|
||||
db.commit()
|
||||
db.refresh(dashboard)
|
||||
return dashboard
|
||||
|
||||
# 维度相关操作
|
||||
def get_dimension(db: Session, dimension_id: int):
|
||||
return db.query(models.Dimension).filter(models.Dimension.id == dimension_id).first()
|
||||
|
||||
def get_dimensions_by_category(db: Session, category: str):
|
||||
return db.query(models.Dimension).filter(models.Dimension.category == category).all()
|
||||
|
||||
def get_all_dimensions(db: Session, skip: int = 0, limit: int = 100):
|
||||
return db.query(models.Dimension).offset(skip).limit(limit).all()
|
||||
|
||||
def create_dimension(db: Session, name: str, weight: float = 1.0, category: str = None, description: str = None):
|
||||
db_dimension = models.Dimension(
|
||||
name=name,
|
||||
weight=weight,
|
||||
category=category,
|
||||
description=description
|
||||
)
|
||||
db.add(db_dimension)
|
||||
db.commit()
|
||||
db.refresh(db_dimension)
|
||||
return db_dimension
|
||||
|
||||
def update_dimension(db: Session, dimension_id: int, dimension_data: Dict[str, Any]):
|
||||
db_dimension = get_dimension(db, dimension_id)
|
||||
if db_dimension:
|
||||
for key, value in dimension_data.items():
|
||||
setattr(db_dimension, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_dimension)
|
||||
return db_dimension
|
||||
|
||||
def delete_dimension(db: Session, dimension_id: int):
|
||||
db_dimension = get_dimension(db, dimension_id)
|
||||
if db_dimension:
|
||||
db.delete(db_dimension)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
BIN
backend/data/app.db
Normal file
BIN
backend/data/app.db.bak
Normal file
30
backend/database.py
Normal file
@ -0,0 +1,30 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
import os
|
||||
|
||||
# 确保数据目录存在
|
||||
DB_DIR = "data"
|
||||
os.makedirs(DB_DIR, exist_ok=True)
|
||||
|
||||
# 数据库URL
|
||||
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/app.db"
|
||||
|
||||
# 创建引擎
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
|
||||
# 创建会话
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# 声明基类
|
||||
Base = declarative_base()
|
||||
|
||||
# 获取数据库会话依赖项
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
81
backend/fix_json_format.py
Normal file
@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
修复JSON文件格式问题
|
||||
将Python的None值替换为JSON标准的null值
|
||||
|
||||
使用方法:
|
||||
python fix_json_format.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def fix_json_file():
|
||||
"""修复JSON文件中的None值问题"""
|
||||
# 源文件和目标文件路径
|
||||
source_file = Path(__file__).parent.parent / "src" / "assets" / "工程研究中心.json"
|
||||
|
||||
if not source_file.exists():
|
||||
print(f"❌ 错误:找不到文件 {source_file}")
|
||||
return False
|
||||
|
||||
try:
|
||||
print(f"📖 正在读取文件: {source_file}")
|
||||
|
||||
# 读取文件内容
|
||||
with open(source_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
print(f"✅ 文件读取成功,大小: {len(content)} 字符")
|
||||
|
||||
# 替换None为null
|
||||
print("🔄 正在修复None值...")
|
||||
fixed_content = content.replace(': None,', ': null,')
|
||||
fixed_content = fixed_content.replace(': None}', ': null}')
|
||||
fixed_content = fixed_content.replace(': None]', ': null]')
|
||||
|
||||
# 统计替换数量
|
||||
none_count = content.count(': None,') + content.count(': None}') + content.count(': None]')
|
||||
print(f"🔧 找到并修复了 {none_count} 个None值")
|
||||
|
||||
# 验证JSON格式
|
||||
print("📝 正在验证JSON格式...")
|
||||
try:
|
||||
json.loads(fixed_content)
|
||||
print("✅ JSON格式验证通过")
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ JSON格式仍有问题: {e}")
|
||||
return False
|
||||
|
||||
# 保存修复后的文件
|
||||
print("💾 正在保存修复后的文件...")
|
||||
with open(source_file, 'w', encoding='utf-8') as f:
|
||||
f.write(fixed_content)
|
||||
|
||||
print("🎉 文件修复完成!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 修复失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("🚀 开始修复JSON文件格式...")
|
||||
print("=" * 50)
|
||||
|
||||
success = fix_json_file()
|
||||
|
||||
print("=" * 50)
|
||||
if success:
|
||||
print("🎉 修复成功!现在可以运行导入脚本了。")
|
||||
else:
|
||||
print("💥 修复失败!")
|
||||
|
||||
input("\n按回车键退出...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
130
backend/fix_year_data_types.py
Normal file
@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
修复年度数据中年份字段的数据类型问题
|
||||
将所有年份从数字类型转换为字符串类型
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
# 添加父目录到路径以便导入模块
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from database import SessionLocal
|
||||
from models import Lab
|
||||
|
||||
def fix_year_data_types():
|
||||
"""修复所有工程研究中心年度数据中的年份类型问题"""
|
||||
try:
|
||||
print("🔄 正在连接数据库...")
|
||||
db = SessionLocal()
|
||||
|
||||
print("🔄 正在查询所有工程研究中心...")
|
||||
labs = db.query(Lab).all()
|
||||
|
||||
print(f"✅ 找到 {len(labs)} 个工程研究中心")
|
||||
|
||||
fixed_count = 0
|
||||
error_count = 0
|
||||
|
||||
for lab in labs:
|
||||
try:
|
||||
if not lab.annual_data:
|
||||
continue
|
||||
|
||||
if isinstance(lab.annual_data, str):
|
||||
# 解析JSON数据
|
||||
data = json.loads(lab.annual_data)
|
||||
|
||||
# 检查是否需要修复
|
||||
needs_fix = False
|
||||
for year_data in data:
|
||||
year_field = year_data.get("归属年份")
|
||||
if isinstance(year_field, int):
|
||||
needs_fix = True
|
||||
break
|
||||
|
||||
if needs_fix:
|
||||
print(f"🔧 修复工程研究中心: {lab.name}")
|
||||
|
||||
# 修复所有年份字段
|
||||
for year_data in data:
|
||||
year_field = year_data.get("归属年份")
|
||||
if isinstance(year_field, int):
|
||||
year_data["归属年份"] = str(year_field)
|
||||
print(f" - 将年份 {year_field} 转换为字符串")
|
||||
|
||||
# 保存修复后的数据
|
||||
lab.annual_data = json.dumps(data, ensure_ascii=False)
|
||||
fixed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 处理工程研究中心 {lab.name} 时出错: {str(e)}")
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
# 提交更改
|
||||
if fixed_count > 0:
|
||||
print(f"\n💾 正在保存修复结果...")
|
||||
db.commit()
|
||||
print(f"✅ 修复完成!")
|
||||
else:
|
||||
print("ℹ️ 没有需要修复的数据")
|
||||
|
||||
print(f"📊 统计信息:")
|
||||
print(f" - 修复的工程研究中心: {fixed_count}")
|
||||
print(f" - 错误数量: {error_count}")
|
||||
print(f" - 总计处理: {len(labs)}")
|
||||
|
||||
db.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 修复失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def verify_fix():
|
||||
"""验证修复结果"""
|
||||
try:
|
||||
print("\n🔍 验证修复结果...")
|
||||
db = SessionLocal()
|
||||
|
||||
# 检查西部优势矿产资源高效利用工程研究中心
|
||||
lab = db.query(Lab).filter(Lab.name == "西部优势矿产资源高效利用").first()
|
||||
|
||||
if lab and lab.annual_data:
|
||||
data = json.loads(lab.annual_data)
|
||||
for i, year_data in enumerate(data):
|
||||
year_field = year_data.get("归属年份")
|
||||
print(f"📅 年度 {i+1}: {year_field} (类型: {type(year_field)})")
|
||||
|
||||
db.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 验证失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("🚀 开始修复年度数据类型...")
|
||||
print("=" * 50)
|
||||
|
||||
success = fix_year_data_types()
|
||||
|
||||
if success:
|
||||
verify_fix()
|
||||
|
||||
print("=" * 50)
|
||||
if success:
|
||||
print("🎉 修复完成!")
|
||||
else:
|
||||
print("💥 修复失败!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
270
backend/import_lab_data_full.py
Normal file
@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
工程研究中心数据完整导入脚本
|
||||
用于导入assets/工程研究中心.json中的所有工程研究中心数据到数据库
|
||||
|
||||
使用方法:
|
||||
1. 确保后端服务未运行
|
||||
2. 在backend目录下执行: python import_lab_data_full.py
|
||||
|
||||
主要功能:
|
||||
- 导入所有工程研究中心的基本信息
|
||||
- 导入多年度数据到annual_data JSON字段
|
||||
- 自动生成lab_id
|
||||
- 支持数据更新(如果lab已存在)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 添加父目录到路径以便导入模块
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from database import SessionLocal, engine
|
||||
from models import Lab
|
||||
import uuid
|
||||
|
||||
def safe_int(value):
|
||||
"""安全转换为整数"""
|
||||
if value is None or value == "":
|
||||
return 0
|
||||
if isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return int(float(value))
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
return 0
|
||||
|
||||
def safe_float(value):
|
||||
"""安全转换为浮点数"""
|
||||
if value is None or value == "":
|
||||
return 0.0
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return float(value)
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
return 0.0
|
||||
|
||||
def safe_str(value):
|
||||
"""安全转换为字符串"""
|
||||
if value is None:
|
||||
return ""
|
||||
return str(value)
|
||||
|
||||
def import_lab_data():
|
||||
"""导入工程研究中心数据"""
|
||||
# 检查JSON文件是否存在
|
||||
json_file = Path(__file__).parent.parent / "src" / "assets" / "工程研究中心.json"
|
||||
if not json_file.exists():
|
||||
print(f"❌ 错误:找不到数据文件 {json_file}")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 读取JSON数据
|
||||
print(f"📖 正在读取数据文件: {json_file}")
|
||||
with open(json_file, 'r', encoding='utf-8') as f:
|
||||
labs_data = json.load(f)
|
||||
|
||||
print(f"✅ 成功读取数据,共 {len(labs_data)} 个工程研究中心")
|
||||
|
||||
# 创建数据库会话
|
||||
db = SessionLocal()
|
||||
|
||||
imported_count = 0
|
||||
updated_count = 0
|
||||
error_count = 0
|
||||
|
||||
try:
|
||||
for lab_info in labs_data:
|
||||
try:
|
||||
center_name = lab_info.get("中心名称", "")
|
||||
center_number = lab_info.get("中心编号", "")
|
||||
annual_data = lab_info.get("年度数据", [])
|
||||
|
||||
if not center_name:
|
||||
print(f"⚠️ 跳过无名称的工程研究中心")
|
||||
continue
|
||||
|
||||
print(f"\n🔄 正在处理工程研究中心: {center_name} (编号: {center_number})")
|
||||
|
||||
# 检查是否已存在
|
||||
existing_lab = None
|
||||
if center_number:
|
||||
existing_lab = db.query(Lab).filter(Lab.center_number == center_number).first()
|
||||
|
||||
if not existing_lab and center_name:
|
||||
existing_lab = db.query(Lab).filter(Lab.name == center_name).first()
|
||||
|
||||
if existing_lab:
|
||||
print(f" 📝 工程研究中心已存在,更新数据...")
|
||||
lab = existing_lab
|
||||
updated_count += 1
|
||||
else:
|
||||
print(f" ➕ 创建新工程研究中心...")
|
||||
lab = Lab()
|
||||
lab.id = str(uuid.uuid4())
|
||||
imported_count += 1
|
||||
|
||||
# 设置基本信息
|
||||
lab.name = center_name
|
||||
lab.center_number = center_number
|
||||
lab.idcode = center_number # 用编号作为显示ID
|
||||
|
||||
# 处理年度数据
|
||||
if annual_data:
|
||||
# 提取基本信息(从最新年度数据)
|
||||
latest_data = max(annual_data, key=lambda x: x.get("归属年份", "0"))
|
||||
|
||||
lab.school = latest_data.get("所属学校", "")
|
||||
lab.department = latest_data.get("主管部门", "")
|
||||
lab.field = latest_data.get("所属领域", "")
|
||||
lab.current_year = latest_data.get("归属年份", "")
|
||||
|
||||
# 存储多年度数据到annual_data JSON字段
|
||||
lab.annual_data = json.dumps(annual_data, ensure_ascii=False)
|
||||
|
||||
# 从最新数据中提取主要信息字段
|
||||
lab.innovation_situation = latest_data.get("技术攻关与创新情况", "")
|
||||
lab.overall_situation = latest_data.get("1.总体情况", "")
|
||||
lab.engineering_cases = latest_data.get("2.工程化案例", "")
|
||||
lab.industry_service = latest_data.get("3.行业服务情况", "")
|
||||
lab.discipline_support = latest_data.get("1.学科发展支撑情况", "") or latest_data.get("1.支撑学科发展情况", "")
|
||||
lab.talent_cultivation = latest_data.get("2.人才培养情况", "")
|
||||
lab.team_building = latest_data.get("3.研究队伍建设情况", "")
|
||||
lab.department_support = latest_data.get("1.主管部门、依托单位支持情况", "")
|
||||
lab.equipment_sharing = latest_data.get("2.仪器设备开放共享情况", "")
|
||||
lab.academic_style = latest_data.get("3.学风建设情况", "")
|
||||
lab.technical_committee = latest_data.get("4.技术委员会工作情况", "")
|
||||
lab.next_year_plan = latest_data.get("下一年度工作计划", "")
|
||||
lab.problems_suggestions = latest_data.get("问题与建议", "")
|
||||
lab.director_opinion = latest_data.get("1.工程中心负责人意见", "")
|
||||
lab.institution_opinion = latest_data.get("2.依托单位意见", "")
|
||||
lab.research_directions = latest_data.get("研究方向/学术带头人", "")
|
||||
|
||||
# 统计数据字段
|
||||
lab.national_awards_first = safe_int(latest_data.get("国家级科技奖励一等奖(项)", 0))
|
||||
lab.national_awards_second = safe_int(latest_data.get("国家级科技奖励二等奖(项)", 0))
|
||||
lab.provincial_awards_first = safe_int(latest_data.get("省、部级科技奖励一等奖(项)", 0))
|
||||
lab.provincial_awards_second = safe_int(latest_data.get("省、部级科技奖励二等奖(项)", 0))
|
||||
lab.valid_patents = safe_int(latest_data.get("有效专利(项)", 0))
|
||||
lab.other_ip = safe_int(latest_data.get("其他知识产权(项)", 0))
|
||||
lab.international_standards = safe_int(latest_data.get("国际/国家标准(项)", 0))
|
||||
lab.industry_standards = safe_int(latest_data.get("行业/地方标准(项)", 0))
|
||||
|
||||
# 专利转化数据
|
||||
lab.patent_transfer_contracts = safe_int(latest_data.get("合同项数(项)", 0))
|
||||
lab.patent_transfer_amount = safe_float(latest_data.get("合同金额(万元)", 0))
|
||||
lab.patent_license_contracts = safe_int(latest_data.get("合同项数(项)_1", 0))
|
||||
lab.patent_license_amount = safe_float(latest_data.get("合同金额(万元)_1", 0))
|
||||
lab.patent_valuation_contracts = safe_int(latest_data.get("合同项数(项)_2", 0))
|
||||
lab.patent_valuation_amount = safe_float(latest_data.get("作价金额(万元)", 0))
|
||||
|
||||
# 项目合作
|
||||
lab.project_contracts = safe_int(latest_data.get("项目合同项数(项)", 0))
|
||||
lab.project_amount = safe_float(latest_data.get("项目合同金额(万元)", 0))
|
||||
|
||||
# 学科信息
|
||||
lab.discipline_1 = latest_data.get("依托学科1", "")
|
||||
lab.discipline_2 = latest_data.get("依托学科2", "")
|
||||
lab.discipline_3 = latest_data.get("依托学科3", "")
|
||||
|
||||
# 人才培养数据
|
||||
lab.doctoral_students = safe_int(latest_data.get("在读博士生", 0))
|
||||
lab.master_students = safe_int(latest_data.get("在读硕士生", 0))
|
||||
lab.graduated_doctoral = safe_int(latest_data.get("当年毕业博士", 0))
|
||||
lab.graduated_master = safe_int(latest_data.get("当年毕业硕士", 0))
|
||||
lab.undergraduate_courses = safe_int(latest_data.get("承担本科课程", 0))
|
||||
lab.graduate_courses = safe_int(latest_data.get("承担研究生课程", 0))
|
||||
lab.textbooks = safe_int(latest_data.get("大专院校教材", 0))
|
||||
|
||||
# 人员结构
|
||||
lab.professors = safe_int(latest_data.get("科技人才-教授(人)", 0))
|
||||
lab.associate_professors = safe_int(latest_data.get("科技人才-副教授(人)", 0))
|
||||
lab.lecturers = safe_int(latest_data.get("科技人才-讲师(人)", 0))
|
||||
lab.domestic_visitors = safe_int(latest_data.get("访问学者-国内(人)", 0))
|
||||
lab.foreign_visitors = safe_int(latest_data.get("访问学者-国外(人)", 0))
|
||||
lab.postdoc_in = safe_int(latest_data.get("本年度进站博士后(人)", 0))
|
||||
lab.postdoc_out = safe_int(latest_data.get("本年度出站博士后(人)", 0))
|
||||
|
||||
# 基础设施
|
||||
lab.center_area = safe_float(latest_data.get("工程中心面积(m²)", 0))
|
||||
lab.new_area = safe_float(latest_data.get("当年新增面积(m²)", 0))
|
||||
lab.fixed_personnel = safe_int(latest_data.get("固定人员(人)", 0))
|
||||
lab.mobile_personnel = safe_int(latest_data.get("流动人员(人)", 0))
|
||||
|
||||
# 经费情况
|
||||
lab.total_funding = safe_float(latest_data.get("当年项目到账总经费(万元)", 0))
|
||||
lab.vertical_funding = safe_float(latest_data.get("纵向经费(万元)", 0))
|
||||
lab.horizontal_funding = safe_float(latest_data.get("横向经费(万元)", 0))
|
||||
|
||||
# 服务情况
|
||||
lab.technical_consultations = safe_int(latest_data.get("技术咨询(次)", 0))
|
||||
lab.training_services = safe_int(latest_data.get("培训服务(人次)", 0))
|
||||
|
||||
# 设置兼容性字段
|
||||
lab.personnel = f"{lab.fixed_personnel + lab.mobile_personnel}人"
|
||||
lab.nationalProjects = str(lab.project_contracts)
|
||||
lab.otherProjects = "0"
|
||||
lab.achievements = str(lab.valid_patents)
|
||||
lab.image = "/image/实验室1.png" # 默认图片
|
||||
|
||||
print(f" ✅ 年度数据: {len(annual_data)} 年, 最新年份: {lab.current_year}")
|
||||
|
||||
# 添加到数据库(如果是新记录)
|
||||
if lab not in db.query(Lab).all():
|
||||
db.add(lab)
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ 处理工程研究中心 {center_name} 时出错: {str(e)}")
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
# 提交更改
|
||||
print(f"\n💾 正在保存到数据库...")
|
||||
db.commit()
|
||||
print(f"✅ 数据导入完成!")
|
||||
print(f"📊 统计信息:")
|
||||
print(f" - 新增工程研究中心: {imported_count}")
|
||||
print(f" - 更新工程研究中心: {updated_count}")
|
||||
print(f" - 错误数量: {error_count}")
|
||||
print(f" - 总计处理: {imported_count + updated_count}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 数据库操作失败: {str(e)}")
|
||||
db.rollback()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 导入失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("🚀 开始导入工程研究中心数据...")
|
||||
print("=" * 50)
|
||||
|
||||
success = import_lab_data()
|
||||
|
||||
print("=" * 50)
|
||||
if success:
|
||||
print("🎉 导入完成!")
|
||||
else:
|
||||
print("💥 导入失败!")
|
||||
|
||||
input("\n按回车键退出...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
226
backend/init_db.py
Normal file
@ -0,0 +1,226 @@
|
||||
import json
|
||||
from sqlalchemy.orm import Session
|
||||
from database import engine, SessionLocal, Base
|
||||
import models
|
||||
import main # 导入原有假数据
|
||||
import logging
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 默认仪表盘数据(如果main.py中没有定义)
|
||||
default_dashboard_data = {
|
||||
"paperCount": 3500,
|
||||
"patentCount": 2000,
|
||||
"highImpactPapers": 100,
|
||||
"keyProjects": 50,
|
||||
"fundingAmount": "500万元",
|
||||
"researcherStats": {
|
||||
"academician": 12,
|
||||
"chiefScientist": 28,
|
||||
"distinguishedProfessor": 56,
|
||||
"youngScientist": 120
|
||||
},
|
||||
"newsData": [
|
||||
{"title": "我校科研团队在量子计算领域取得重大突破", "date": "2023-05-15"},
|
||||
{"title": "张教授团队论文被Nature收录", "date": "2023-04-28"},
|
||||
{"title": "校长率团访问美国麻省理工学院商讨合作事宜", "date": "2023-04-15"},
|
||||
{"title": "我校获批3项国家重点研发计划", "date": "2023-03-20"},
|
||||
{"title": "2023年度国家自然科学基金申请工作启动", "date": "2023-02-10"}
|
||||
]
|
||||
}
|
||||
|
||||
# 创建所有表
|
||||
def create_tables():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
logger.info("数据库表已创建")
|
||||
|
||||
# 导入用户数据
|
||||
def import_users(db: Session):
|
||||
# 检查是否已存在用户
|
||||
existing_users = db.query(models.User).count()
|
||||
if existing_users == 0:
|
||||
for username, user_data in main.fake_users_db.items():
|
||||
db_user = models.User(
|
||||
username=user_data["username"],
|
||||
email=user_data.get("email"),
|
||||
full_name=user_data.get("full_name"),
|
||||
hashed_password=user_data["hashed_password"],
|
||||
disabled=user_data.get("disabled", False)
|
||||
)
|
||||
db.add(db_user)
|
||||
|
||||
db.commit()
|
||||
logger.info("用户数据已导入")
|
||||
else:
|
||||
logger.info("用户数据已存在,跳过导入")
|
||||
|
||||
# 导入人才数据
|
||||
def import_talents(db: Session):
|
||||
# 检查是否已存在人才数据
|
||||
existing_talents = db.query(models.Talent).count()
|
||||
if existing_talents == 0:
|
||||
for talent_data in main.talents:
|
||||
# 处理evaluationData字段 - 确保是JSON格式
|
||||
evaluation_data = talent_data.get("evaluationData")
|
||||
|
||||
db_talent = models.Talent(
|
||||
id=talent_data["id"],
|
||||
name=talent_data["name"],
|
||||
gender=talent_data.get("gender"),
|
||||
birthDate=talent_data.get("birthDate"),
|
||||
title=talent_data.get("title"),
|
||||
position=talent_data.get("position"),
|
||||
education=talent_data.get("education"),
|
||||
address=talent_data.get("address"),
|
||||
academicDirection=talent_data.get("academicDirection"),
|
||||
talentPlan=talent_data.get("talentPlan"),
|
||||
officeLocation=talent_data.get("officeLocation"),
|
||||
email=talent_data.get("email"),
|
||||
phone=talent_data.get("phone"),
|
||||
tutorType=talent_data.get("tutorType"),
|
||||
papers=talent_data.get("papers"),
|
||||
projects=talent_data.get("projects"),
|
||||
photo=talent_data.get("photo"),
|
||||
eduWorkHistory=talent_data.get("eduWorkHistory"),
|
||||
researchDirection=talent_data.get("researchDirection"),
|
||||
recentProjects=talent_data.get("recentProjects"),
|
||||
representativePapers=talent_data.get("representativePapers"),
|
||||
patents=talent_data.get("patents"),
|
||||
evaluationData=evaluation_data
|
||||
)
|
||||
db.add(db_talent)
|
||||
|
||||
db.commit()
|
||||
logger.info("人才数据已导入")
|
||||
else:
|
||||
logger.info("人才数据已存在,跳过导入")
|
||||
|
||||
# 导入工程研究中心数据
|
||||
def import_labs(db: Session):
|
||||
# 检查是否已存在工程研究中心数据
|
||||
existing_labs = db.query(models.Lab).count()
|
||||
if existing_labs == 0:
|
||||
for lab_data in main.labs:
|
||||
# 处理evaluationData字段 - 确保是JSON格式
|
||||
evaluation_data = lab_data.get("evaluationData")
|
||||
|
||||
db_lab = models.Lab(
|
||||
id=lab_data["id"],
|
||||
name=lab_data["name"],
|
||||
personnel=lab_data.get("personnel"),
|
||||
nationalProjects=lab_data.get("nationalProjects"),
|
||||
otherProjects=lab_data.get("otherProjects"),
|
||||
achievements=lab_data.get("achievements"),
|
||||
labAchievements=lab_data.get("labAchievements"),
|
||||
image=lab_data.get("image"),
|
||||
score=lab_data.get("score"),
|
||||
evaluationData=evaluation_data
|
||||
)
|
||||
db.add(db_lab)
|
||||
|
||||
db.commit()
|
||||
logger.info("工程研究中心数据已导入")
|
||||
else:
|
||||
logger.info("工程研究中心数据已存在,跳过导入")
|
||||
|
||||
# 导入仪表盘数据
|
||||
def import_dashboard(db: Session):
|
||||
# 检查是否已存在仪表盘数据
|
||||
existing_dashboard = db.query(models.DashboardData).count()
|
||||
if existing_dashboard == 0:
|
||||
# 优先使用main.py中的数据,如果不存在则使用默认数据
|
||||
dashboard_data = getattr(main, "dashboard_data", default_dashboard_data)
|
||||
|
||||
# 创建仪表盘记录
|
||||
db_dashboard = models.DashboardData(
|
||||
id=1, # 主键ID设为1
|
||||
paperCount=dashboard_data["paperCount"],
|
||||
patentCount=dashboard_data["patentCount"],
|
||||
highImpactPapers=dashboard_data["highImpactPapers"],
|
||||
keyProjects=dashboard_data["keyProjects"],
|
||||
fundingAmount=dashboard_data["fundingAmount"],
|
||||
researcherStats=dashboard_data["researcherStats"]
|
||||
)
|
||||
db.add(db_dashboard)
|
||||
db.flush() # 立即写入数据库,获取ID
|
||||
|
||||
# 创建新闻数据记录
|
||||
for news_item in dashboard_data["newsData"]:
|
||||
db_news = models.News(
|
||||
title=news_item["title"],
|
||||
date=news_item["date"],
|
||||
dashboard_id=db_dashboard.id
|
||||
)
|
||||
db.add(db_news)
|
||||
|
||||
db.commit()
|
||||
logger.info("仪表盘和新闻数据已导入")
|
||||
else:
|
||||
logger.info("仪表盘数据已存在,跳过导入")
|
||||
|
||||
# 导入默认维度
|
||||
def import_dimensions(db: Session):
|
||||
# 人才评估维度
|
||||
talent_dimensions = [
|
||||
{"name": "学术成果", "weight": 1.0, "category": "talent", "description": "包括论文发表、专著、专利等"},
|
||||
{"name": "科研项目", "weight": 1.0, "category": "talent", "description": "承担的科研项目数量和级别"},
|
||||
{"name": "人才引进", "weight": 1.0, "category": "talent", "description": "引进的人才数量和质量"},
|
||||
{"name": "学术影响力", "weight": 1.0, "category": "talent", "description": "学术引用和影响力指标"},
|
||||
{"name": "教学质量", "weight": 1.0, "category": "talent", "description": "教学评估和学生反馈"},
|
||||
{"name": "社会服务", "weight": 1.0, "category": "talent", "description": "社会服务和贡献"}
|
||||
]
|
||||
|
||||
# 工程研究中心评估维度
|
||||
lab_dimensions = [
|
||||
{"name": "科研产出", "weight": 1.0, "category": "lab", "description": "工程研究中心科研成果产出"},
|
||||
{"name": "人才培养", "weight": 1.0, "category": "lab", "description": "培养的研究生和博士后数量"},
|
||||
{"name": "项目承担", "weight": 1.0, "category": "lab", "description": "承担的科研项目数量和级别"},
|
||||
{"name": "设备利用", "weight": 1.0, "category": "lab", "description": "设备利用率和效益"},
|
||||
{"name": "学术交流", "weight": 1.0, "category": "lab", "description": "国内外学术交流和合作"},
|
||||
{"name": "社会服务", "weight": 1.0, "category": "lab", "description": "社会服务和贡献"}
|
||||
]
|
||||
|
||||
# 检查是否已存在维度
|
||||
existing_dimensions = db.query(models.Dimension).count()
|
||||
if existing_dimensions == 0:
|
||||
# 添加人才评估维度
|
||||
for dim in talent_dimensions:
|
||||
db_dimension = models.Dimension(**dim)
|
||||
db.add(db_dimension)
|
||||
|
||||
# 添加工程研究中心评估维度
|
||||
for dim in lab_dimensions:
|
||||
db_dimension = models.Dimension(**dim)
|
||||
db.add(db_dimension)
|
||||
|
||||
db.commit()
|
||||
logger.info("默认评估维度已导入")
|
||||
else:
|
||||
logger.info("评估维度已存在,跳过导入")
|
||||
|
||||
# 主函数
|
||||
def init_db():
|
||||
# 创建表结构
|
||||
create_tables()
|
||||
|
||||
# 获取数据库会话
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# 导入各类数据
|
||||
import_users(db)
|
||||
import_talents(db)
|
||||
import_labs(db)
|
||||
import_dashboard(db)
|
||||
import_dimensions(db)
|
||||
|
||||
logger.info("数据库初始化完成!")
|
||||
except Exception as e:
|
||||
logger.error(f"数据库初始化失败: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# 直接运行脚本时执行初始化
|
||||
if __name__ == "__main__":
|
||||
init_db()
|
68
backend/init_dimensions.py
Normal file
@ -0,0 +1,68 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from database import SessionLocal, engine, Base
|
||||
import models
|
||||
import crud
|
||||
|
||||
def init_dimensions():
|
||||
# 创建会话
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# 检查是否已经存在维度数据
|
||||
existing_dimensions = db.query(models.Dimension).count()
|
||||
|
||||
if existing_dimensions == 0:
|
||||
print("初始化维度数据...")
|
||||
|
||||
# 教师科研人才评估维度
|
||||
talent_dimensions = [
|
||||
{"name": "教育和工作经历", "weight": 10, "category": "talent", "description": "教育背景和工作经历评估"},
|
||||
{"name": "研究方向前沿性", "weight": 8, "category": "talent", "description": "研究是否处于学科前沿"},
|
||||
{"name": "主持科研项目情况", "weight": 12, "category": "talent", "description": "项目规模、数量及影响力"},
|
||||
{"name": "科研成果质量", "weight": 16, "category": "talent", "description": "论文、专利等成果的质量与影响"},
|
||||
{"name": "教学能力与效果", "weight": 14, "category": "talent", "description": "教学水平及学生评价"},
|
||||
{"name": "学术服务与影响力", "weight": 40, "category": "talent", "description": "学术服务与社会影响力"}
|
||||
]
|
||||
|
||||
# 工程研究中心评估维度
|
||||
lab_dimensions = [
|
||||
{"name": "工程技术研发能力与水平", "weight": 30, "category": "lab", "description": "工程研究中心整体工程技术研发水平"},
|
||||
{"name": "创新水平", "weight": 10, "category": "lab", "description": "工程研究中心科研创新程度"},
|
||||
{"name": "人才与队伍", "weight": 10, "category": "lab", "description": "工程研究中心人才梯队建设情况"},
|
||||
{"name": "装备与场地", "weight": 10, "category": "lab", "description": "工程研究中心设备和场地条件"},
|
||||
{"name": "成果转化与行业贡献", "weight": 30, "category": "lab", "description": "成果产业化情况与行业贡献"},
|
||||
{"name": "学科发展与人才培养", "weight": 20, "category": "lab", "description": "对学科发展与人才培养的贡献"},
|
||||
{"name": "开放与运行管理", "weight": 20, "category": "lab", "description": "工程研究中心开放程度与管理水平"}
|
||||
]
|
||||
|
||||
# 添加教师科研人才评估维度
|
||||
for dim in talent_dimensions:
|
||||
crud.create_dimension(
|
||||
db,
|
||||
name=dim["name"],
|
||||
weight=dim["weight"],
|
||||
category=dim["category"],
|
||||
description=dim["description"]
|
||||
)
|
||||
|
||||
# 添加工程研究中心评估维度
|
||||
for dim in lab_dimensions:
|
||||
crud.create_dimension(
|
||||
db,
|
||||
name=dim["name"],
|
||||
weight=dim["weight"],
|
||||
category=dim["category"],
|
||||
description=dim["description"]
|
||||
)
|
||||
|
||||
print("维度数据初始化完成!")
|
||||
else:
|
||||
print("数据库中已存在维度数据,跳过初始化。")
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 确保表已创建
|
||||
Base.metadata.create_all(bind=engine)
|
||||
# 初始化维度数据
|
||||
init_dimensions()
|
1556
backend/main.py
Normal file
221
backend/models.py
Normal file
@ -0,0 +1,221 @@
|
||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Float, Text, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from database import Base
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String, unique=True, index=True)
|
||||
email = Column(String, nullable=True)
|
||||
full_name = Column(String, nullable=True)
|
||||
hashed_password = Column(String)
|
||||
disabled = Column(Boolean, default=False)
|
||||
|
||||
class Talent(Base):
|
||||
__tablename__ = "talents"
|
||||
|
||||
id = Column(String, primary_key=True, index=True)
|
||||
idcode = Column(String, nullable=True, index=True)
|
||||
name = Column(String, index=True)
|
||||
gender = Column(String, nullable=True)
|
||||
birthDate = Column(String, nullable=True)
|
||||
title = Column(String, nullable=True)
|
||||
position = Column(String, nullable=True)
|
||||
education = Column(String, nullable=True)
|
||||
educationBackground = Column(String, nullable=True)
|
||||
address = Column(String, nullable=True)
|
||||
academicDirection = Column(String, nullable=True)
|
||||
talentPlan = Column(String, nullable=True)
|
||||
officeLocation = Column(String, nullable=True)
|
||||
email = Column(String, nullable=True)
|
||||
phone = Column(String, nullable=True)
|
||||
tutorType = Column(String, nullable=True)
|
||||
papers = Column(String, nullable=True)
|
||||
projects = Column(String, nullable=True)
|
||||
photo = Column(Text, nullable=True)
|
||||
eduWorkHistory = Column(Text, nullable=True)
|
||||
researchDirection = Column(Text, nullable=True)
|
||||
recentProjects = Column(Text, nullable=True)
|
||||
representativePapers = Column(Text, nullable=True)
|
||||
patents = Column(Text, nullable=True)
|
||||
evaluationData = Column(JSON, nullable=True)
|
||||
|
||||
class Lab(Base):
|
||||
__tablename__ = "labs"
|
||||
|
||||
id = Column(String, primary_key=True, index=True)
|
||||
idcode = Column(String, nullable=True, index=True)
|
||||
|
||||
# 基本信息
|
||||
name = Column(String, index=True) # 中心名称
|
||||
center_number = Column(String, nullable=True) # 中心编号
|
||||
school = Column(String, nullable=True) # 所属学校
|
||||
department = Column(String, nullable=True) # 主管部门
|
||||
field = Column(String, nullable=True) # 所属领域
|
||||
|
||||
# 年度数据 - 存储为JSON格式,包含多年数据
|
||||
annual_data = Column(JSON, nullable=True)
|
||||
|
||||
# 当前年度的主要信息(用于快速查询和显示)
|
||||
current_year = Column(String, nullable=True) # 当前评估年份
|
||||
|
||||
# 技术攻关与创新情况
|
||||
innovation_situation = Column(Text, nullable=True)
|
||||
|
||||
# 总体情况
|
||||
overall_situation = Column(Text, nullable=True)
|
||||
|
||||
# 工程化案例
|
||||
engineering_cases = Column(Text, nullable=True)
|
||||
|
||||
# 行业服务情况
|
||||
industry_service = Column(Text, nullable=True)
|
||||
|
||||
# 学科发展支撑情况
|
||||
discipline_support = Column(Text, nullable=True)
|
||||
|
||||
# 人才培养情况
|
||||
talent_cultivation = Column(Text, nullable=True)
|
||||
|
||||
# 研究队伍建设情况
|
||||
team_building = Column(Text, nullable=True)
|
||||
|
||||
# 主管部门、依托单位支持情况
|
||||
department_support = Column(Text, nullable=True)
|
||||
|
||||
# 仪器设备开放共享情况
|
||||
equipment_sharing = Column(Text, nullable=True)
|
||||
|
||||
# 学风建设情况
|
||||
academic_style = Column(Text, nullable=True)
|
||||
|
||||
# 技术委员会工作情况
|
||||
technical_committee = Column(Text, nullable=True)
|
||||
|
||||
# 下一年度工作计划
|
||||
next_year_plan = Column(Text, nullable=True)
|
||||
|
||||
# 问题与建议
|
||||
problems_suggestions = Column(Text, nullable=True)
|
||||
|
||||
# 工程中心负责人意见
|
||||
director_opinion = Column(Text, nullable=True)
|
||||
|
||||
# 依托单位意见
|
||||
institution_opinion = Column(Text, nullable=True)
|
||||
|
||||
# 研究方向/学术带头人
|
||||
research_directions = Column(Text, nullable=True)
|
||||
|
||||
# 统计数据字段
|
||||
national_awards_first = Column(Integer, default=0) # 国家级科技奖励一等奖
|
||||
national_awards_second = Column(Integer, default=0) # 国家级科技奖励二等奖
|
||||
provincial_awards_first = Column(Integer, default=0) # 省、部级科技奖励一等奖
|
||||
provincial_awards_second = Column(Integer, default=0) # 省、部级科技奖励二等奖
|
||||
valid_patents = Column(Integer, default=0) # 有效专利
|
||||
other_ip = Column(Integer, default=0) # 其他知识产权
|
||||
international_standards = Column(Integer, default=0) # 国际/国家标准
|
||||
industry_standards = Column(Integer, default=0) # 行业/地方标准
|
||||
|
||||
# 专利转化相关
|
||||
patent_transfer_contracts = Column(Integer, default=0) # 专利转让合同项数
|
||||
patent_transfer_amount = Column(Float, default=0.0) # 专利转让合同金额
|
||||
patent_license_contracts = Column(Integer, default=0) # 专利许可合同项数
|
||||
patent_license_amount = Column(Float, default=0.0) # 专利许可合同金额
|
||||
patent_valuation_contracts = Column(Integer, default=0) # 专利作价合同项数
|
||||
patent_valuation_amount = Column(Float, default=0.0) # 专利作价金额
|
||||
|
||||
# 项目合作
|
||||
project_contracts = Column(Integer, default=0) # 项目合同项数
|
||||
project_amount = Column(Float, default=0.0) # 项目合同金额
|
||||
|
||||
# 学科信息
|
||||
discipline_1 = Column(String, nullable=True) # 依托学科1
|
||||
discipline_2 = Column(String, nullable=True) # 依托学科2
|
||||
discipline_3 = Column(String, nullable=True) # 依托学科3
|
||||
|
||||
# 人才培养数据
|
||||
doctoral_students = Column(Integer, default=0) # 在读博士生
|
||||
master_students = Column(Integer, default=0) # 在读硕士生
|
||||
graduated_doctoral = Column(Integer, default=0) # 当年毕业博士
|
||||
graduated_master = Column(Integer, default=0) # 当年毕业硕士
|
||||
undergraduate_courses = Column(Integer, default=0) # 承担本科课程
|
||||
graduate_courses = Column(Integer, default=0) # 承担研究生课程
|
||||
textbooks = Column(Integer, default=0) # 大专院校教材
|
||||
|
||||
# 人员结构
|
||||
professors = Column(Integer, default=0) # 教授人数
|
||||
associate_professors = Column(Integer, default=0) # 副教授人数
|
||||
lecturers = Column(Integer, default=0) # 讲师人数
|
||||
domestic_visitors = Column(Integer, default=0) # 国内访问学者
|
||||
foreign_visitors = Column(Integer, default=0) # 国外访问学者
|
||||
postdoc_in = Column(Integer, default=0) # 本年度进站博士后
|
||||
postdoc_out = Column(Integer, default=0) # 本年度出站博士后
|
||||
|
||||
# 基础设施
|
||||
center_area = Column(Float, default=0.0) # 工程中心面积
|
||||
new_area = Column(Float, default=0.0) # 当年新增面积
|
||||
fixed_personnel = Column(Integer, default=0) # 固定人员
|
||||
mobile_personnel = Column(Integer, default=0) # 流动人员
|
||||
|
||||
# 经费情况
|
||||
total_funding = Column(Float, default=0.0) # 当年项目到账总经费
|
||||
vertical_funding = Column(Float, default=0.0) # 纵向经费
|
||||
horizontal_funding = Column(Float, default=0.0) # 横向经费
|
||||
|
||||
# 服务情况
|
||||
technical_consultations = Column(Integer, default=0) # 技术咨询次数
|
||||
training_services = Column(Integer, default=0) # 培训服务人次
|
||||
|
||||
# 原有字段保留兼容性
|
||||
personnel = Column(String, nullable=True)
|
||||
nationalProjects = Column(String, nullable=True)
|
||||
otherProjects = Column(String, nullable=True)
|
||||
achievements = Column(String, nullable=True)
|
||||
labAchievements = Column(Text, nullable=True)
|
||||
image = Column(Text, nullable=True)
|
||||
score = Column(Integer, nullable=True)
|
||||
evaluationData = Column(JSON, nullable=True)
|
||||
sub_dimension_evaluations = Column(JSON, nullable=True)
|
||||
|
||||
class DashboardData(Base):
|
||||
__tablename__ = "dashboard"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
paperCount = Column(Integer)
|
||||
patentCount = Column(Integer)
|
||||
highImpactPapers = Column(Integer)
|
||||
keyProjects = Column(Integer)
|
||||
fundingAmount = Column(String)
|
||||
researcherStats = Column(JSON)
|
||||
|
||||
class News(Base):
|
||||
__tablename__ = "news"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String)
|
||||
date = Column(String)
|
||||
dashboard_id = Column(Integer, ForeignKey("dashboard.id"))
|
||||
|
||||
dashboard = relationship("DashboardData", back_populates="news_items")
|
||||
|
||||
# 添加反向关系
|
||||
DashboardData.news_items = relationship("News", back_populates="dashboard")
|
||||
|
||||
class Dimension(Base):
|
||||
__tablename__ = "dimensions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True)
|
||||
weight = Column(Float, default=1.0)
|
||||
category = Column(String, nullable=True) # talent 或 lab
|
||||
description = Column(Text, nullable=True)
|
||||
parent_id = Column(Integer, ForeignKey("dimensions.id"), nullable=True)
|
||||
|
||||
# 自引用关系,用于树形结构
|
||||
children = relationship("Dimension", back_populates="parent", cascade="all, delete-orphan")
|
||||
parent = relationship("Dimension", back_populates="children", remote_side=[id])
|
||||
|
||||
# 存储子维度的JSON数据
|
||||
sub_dimensions = Column(JSON, nullable=True)
|
304
backend/network_diagnostic.py
Normal file
@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
网络诊断工具
|
||||
用于诊断服务器部署后访问外部网站时的网络连接问题
|
||||
"""
|
||||
|
||||
import socket
|
||||
import requests
|
||||
import time
|
||||
import urllib.parse
|
||||
import subprocess
|
||||
import platform
|
||||
from typing import Dict, List
|
||||
|
||||
def diagnose_network_connectivity(url: str) -> Dict:
|
||||
"""
|
||||
综合诊断网络连接问题
|
||||
"""
|
||||
print(f"开始诊断网络连接: {url}")
|
||||
results = {
|
||||
'url': url,
|
||||
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'tests': {}
|
||||
}
|
||||
|
||||
# 解析URL
|
||||
try:
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
hostname = parsed_url.hostname
|
||||
port = parsed_url.port or (443 if parsed_url.scheme == 'https' else 80)
|
||||
results['hostname'] = hostname
|
||||
results['port'] = port
|
||||
except Exception as e:
|
||||
results['error'] = f"URL解析失败: {e}"
|
||||
return results
|
||||
|
||||
# 1. DNS解析测试
|
||||
results['tests']['dns_resolution'] = test_dns_resolution(hostname)
|
||||
|
||||
# 2. TCP连接测试
|
||||
results['tests']['tcp_connection'] = test_tcp_connection(hostname, port)
|
||||
|
||||
# 3. HTTP请求测试
|
||||
results['tests']['http_request'] = test_http_request(url)
|
||||
|
||||
# 4. 系统网络配置检查
|
||||
results['tests']['system_info'] = get_system_network_info()
|
||||
|
||||
# 5. 建议解决方案
|
||||
results['suggestions'] = generate_suggestions(results['tests'])
|
||||
|
||||
return results
|
||||
|
||||
def test_dns_resolution(hostname: str) -> Dict:
|
||||
"""测试DNS解析"""
|
||||
print(f"测试DNS解析: {hostname}")
|
||||
test_result = {
|
||||
'status': 'unknown',
|
||||
'details': {}
|
||||
}
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
ip_address = socket.gethostbyname(hostname)
|
||||
dns_time = time.time() - start_time
|
||||
|
||||
test_result['status'] = 'success'
|
||||
test_result['details'] = {
|
||||
'ip_address': ip_address,
|
||||
'resolution_time': f"{dns_time:.3f}秒"
|
||||
}
|
||||
print(f"DNS解析成功: {hostname} -> {ip_address} ({dns_time:.3f}秒)")
|
||||
|
||||
except socket.gaierror as e:
|
||||
test_result['status'] = 'failed'
|
||||
test_result['details'] = {
|
||||
'error': str(e),
|
||||
'error_code': e.errno if hasattr(e, 'errno') else 'unknown'
|
||||
}
|
||||
print(f"DNS解析失败: {e}")
|
||||
|
||||
return test_result
|
||||
|
||||
def test_tcp_connection(hostname: str, port: int) -> Dict:
|
||||
"""测试TCP连接"""
|
||||
print(f"测试TCP连接: {hostname}:{port}")
|
||||
test_result = {
|
||||
'status': 'unknown',
|
||||
'details': {}
|
||||
}
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10) # 10秒超时
|
||||
|
||||
result = sock.connect_ex((hostname, port))
|
||||
connect_time = time.time() - start_time
|
||||
sock.close()
|
||||
|
||||
if result == 0:
|
||||
test_result['status'] = 'success'
|
||||
test_result['details'] = {
|
||||
'connection_time': f"{connect_time:.3f}秒",
|
||||
'port_open': True
|
||||
}
|
||||
print(f"TCP连接成功: {hostname}:{port} ({connect_time:.3f}秒)")
|
||||
else:
|
||||
test_result['status'] = 'failed'
|
||||
test_result['details'] = {
|
||||
'connection_time': f"{connect_time:.3f}秒",
|
||||
'port_open': False,
|
||||
'error_code': result
|
||||
}
|
||||
print(f"TCP连接失败: {hostname}:{port} (错误代码: {result})")
|
||||
|
||||
except Exception as e:
|
||||
test_result['status'] = 'failed'
|
||||
test_result['details'] = {
|
||||
'error': str(e)
|
||||
}
|
||||
print(f"TCP连接测试异常: {e}")
|
||||
|
||||
return test_result
|
||||
|
||||
def test_http_request(url: str) -> Dict:
|
||||
"""测试HTTP请求"""
|
||||
print(f"测试HTTP请求: {url}")
|
||||
test_result = {
|
||||
'status': 'unknown',
|
||||
'details': {}
|
||||
}
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
timeout=(10, 30), # (连接超时, 读取超时)
|
||||
verify=False,
|
||||
allow_redirects=True
|
||||
)
|
||||
request_time = time.time() - start_time
|
||||
|
||||
test_result['status'] = 'success'
|
||||
test_result['details'] = {
|
||||
'status_code': response.status_code,
|
||||
'request_time': f"{request_time:.3f}秒",
|
||||
'content_length': len(response.content),
|
||||
'encoding': response.encoding,
|
||||
'headers_count': len(response.headers)
|
||||
}
|
||||
print(f"HTTP请求成功: {response.status_code} ({request_time:.3f}秒)")
|
||||
|
||||
except requests.exceptions.Timeout as e:
|
||||
test_result['status'] = 'timeout'
|
||||
test_result['details'] = {
|
||||
'error': '请求超时',
|
||||
'timeout_type': str(type(e).__name__)
|
||||
}
|
||||
print(f"HTTP请求超时: {e}")
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
test_result['status'] = 'connection_error'
|
||||
test_result['details'] = {
|
||||
'error': '连接错误',
|
||||
'error_detail': str(e)
|
||||
}
|
||||
print(f"HTTP连接错误: {e}")
|
||||
|
||||
except Exception as e:
|
||||
test_result['status'] = 'failed'
|
||||
test_result['details'] = {
|
||||
'error': str(e)
|
||||
}
|
||||
print(f"HTTP请求失败: {e}")
|
||||
|
||||
return test_result
|
||||
|
||||
def get_system_network_info() -> Dict:
|
||||
"""获取系统网络信息"""
|
||||
print("收集系统网络信息...")
|
||||
info = {
|
||||
'platform': platform.system(),
|
||||
'platform_version': platform.version(),
|
||||
'python_version': platform.python_version()
|
||||
}
|
||||
|
||||
try:
|
||||
# 获取本机IP地址
|
||||
hostname = socket.gethostname()
|
||||
local_ip = socket.gethostbyname(hostname)
|
||||
info['hostname'] = hostname
|
||||
info['local_ip'] = local_ip
|
||||
except:
|
||||
info['hostname'] = 'unknown'
|
||||
info['local_ip'] = 'unknown'
|
||||
|
||||
# 检查网络配置(仅在Linux/Unix系统上)
|
||||
if platform.system() in ['Linux', 'Darwin']:
|
||||
try:
|
||||
# 检查DNS服务器
|
||||
with open('/etc/resolv.conf', 'r') as f:
|
||||
dns_servers = []
|
||||
for line in f:
|
||||
if line.startswith('nameserver'):
|
||||
dns_servers.append(line.split()[1])
|
||||
info['dns_servers'] = dns_servers
|
||||
except:
|
||||
info['dns_servers'] = 'unavailable'
|
||||
|
||||
try:
|
||||
# 检查默认网关
|
||||
result = subprocess.run(['ip', 'route', 'show', 'default'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
info['default_gateway'] = result.stdout.strip()
|
||||
else:
|
||||
info['default_gateway'] = 'unavailable'
|
||||
except:
|
||||
info['default_gateway'] = 'unavailable'
|
||||
|
||||
return info
|
||||
|
||||
def generate_suggestions(test_results: Dict) -> List[str]:
|
||||
"""根据测试结果生成建议"""
|
||||
suggestions = []
|
||||
|
||||
# DNS问题
|
||||
if test_results.get('dns_resolution', {}).get('status') == 'failed':
|
||||
suggestions.extend([
|
||||
"DNS解析失败,请检查服务器的DNS配置",
|
||||
"尝试使用公共DNS服务器 (8.8.8.8, 114.114.114.114)",
|
||||
"检查 /etc/resolv.conf 文件中的DNS服务器配置"
|
||||
])
|
||||
|
||||
# TCP连接问题
|
||||
if test_results.get('tcp_connection', {}).get('status') == 'failed':
|
||||
suggestions.extend([
|
||||
"TCP连接失败,可能是防火墙阻止了连接",
|
||||
"检查服务器的出站规则设置",
|
||||
"确认目标端口是否开放"
|
||||
])
|
||||
|
||||
# HTTP请求问题
|
||||
http_status = test_results.get('http_request', {}).get('status')
|
||||
if http_status == 'timeout':
|
||||
suggestions.extend([
|
||||
"HTTP请求超时,网络延迟较高",
|
||||
"增加超时时间设置",
|
||||
"考虑使用重试机制"
|
||||
])
|
||||
elif http_status == 'connection_error':
|
||||
suggestions.extend([
|
||||
"HTTP连接错误,可能是网络配置问题",
|
||||
"检查代理设置",
|
||||
"确认SSL/TLS证书配置"
|
||||
])
|
||||
|
||||
# 通用建议
|
||||
if not suggestions:
|
||||
suggestions.append("网络连接测试基本正常,问题可能在应用层面")
|
||||
|
||||
suggestions.extend([
|
||||
"联系系统管理员检查网络配置",
|
||||
"考虑使用VPN或代理服务器",
|
||||
"检查服务器所在云平台的安全组设置"
|
||||
])
|
||||
|
||||
return suggestions
|
||||
|
||||
def main():
|
||||
"""主函数,用于命令行测试"""
|
||||
test_url = "https://ac.bit.edu.cn"
|
||||
|
||||
print("=" * 60)
|
||||
print("网络连接诊断工具")
|
||||
print("=" * 60)
|
||||
|
||||
results = diagnose_network_connectivity(test_url)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("诊断结果")
|
||||
print("=" * 60)
|
||||
|
||||
for test_name, test_result in results['tests'].items():
|
||||
print(f"\n{test_name}: {test_result['status']}")
|
||||
if 'details' in test_result:
|
||||
for key, value in test_result['details'].items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("建议解决方案")
|
||||
print("=" * 60)
|
||||
|
||||
for i, suggestion in enumerate(results['suggestions'], 1):
|
||||
print(f"{i}. {suggestion}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
1
backend/quick_network_test.py
Normal file
@ -0,0 +1 @@
|
||||
|
46
backend/quick_test.py
Normal file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
快速网络连接测试脚本
|
||||
"""
|
||||
|
||||
import socket
|
||||
import time
|
||||
import requests
|
||||
|
||||
def quick_test():
|
||||
target_url = "https://ac.bit.edu.cn"
|
||||
target_host = "ac.bit.edu.cn"
|
||||
|
||||
print("快速网络连接测试")
|
||||
print("=" * 40)
|
||||
|
||||
# DNS测试
|
||||
print("1. DNS解析测试...")
|
||||
try:
|
||||
ip = socket.gethostbyname(target_host)
|
||||
print(f" 成功: {target_host} -> {ip}")
|
||||
except Exception as e:
|
||||
print(f" 失败: {e}")
|
||||
return
|
||||
|
||||
# HTTP测试
|
||||
print("\n2. HTTP请求测试...")
|
||||
try:
|
||||
start = time.time()
|
||||
response = requests.get(
|
||||
target_url,
|
||||
timeout=(30, 90),
|
||||
verify=False,
|
||||
headers={'User-Agent': 'Mozilla/5.0'}
|
||||
)
|
||||
duration = time.time() - start
|
||||
print(f" 成功: {response.status_code} ({duration:.2f}秒)")
|
||||
except requests.exceptions.Timeout:
|
||||
print(" 超时: 网络延迟过高")
|
||||
except Exception as e:
|
||||
print(f" 失败: {e}")
|
||||
|
||||
print("\n测试完成")
|
||||
|
||||
if __name__ == "__main__":
|
||||
quick_test()
|
13
backend/requirements.txt
Normal file
@ -0,0 +1,13 @@
|
||||
fastapi==0.109.2
|
||||
uvicorn==0.27.1
|
||||
pydantic==2.6.1
|
||||
python-multipart==0.0.9
|
||||
python-jose==3.3.0
|
||||
passlib==1.7.4
|
||||
bcrypt==4.1.2
|
||||
requests==2.31.0
|
||||
beautifulsoup4==4.12.2
|
||||
sqlalchemy==2.0.26
|
||||
aiosqlite==0.19.0
|
||||
alembic==1.13.1
|
||||
python-docx==0.8.11
|
246
backend/schemas.py
Normal file
@ -0,0 +1,246 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
# 用户模型
|
||||
class UserBase(BaseModel):
|
||||
username: str
|
||||
email: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
class User(UserBase):
|
||||
id: int
|
||||
disabled: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# 令牌模型
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: str
|
||||
|
||||
# 人才模型
|
||||
class TalentBase(BaseModel):
|
||||
name: str
|
||||
idcode: Optional[str] = None # 新增展示用ID字段
|
||||
gender: Optional[str] = None
|
||||
birthDate: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
position: Optional[str] = None
|
||||
education: Optional[str] = None
|
||||
educationBackground: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
academicDirection: Optional[str] = None
|
||||
talentPlan: Optional[str] = None
|
||||
officeLocation: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
tutorType: Optional[str] = None
|
||||
papers: Optional[str] = None
|
||||
projects: Optional[str] = None
|
||||
photo: Optional[str] = None
|
||||
eduWorkHistory: Optional[str] = None
|
||||
researchDirection: Optional[str] = None
|
||||
recentProjects: Optional[str] = None
|
||||
representativePapers: Optional[str] = None
|
||||
patents: Optional[str] = None
|
||||
evaluationData: Optional[List[float]] = None
|
||||
|
||||
class TalentCreate(TalentBase):
|
||||
id: str
|
||||
|
||||
class Talent(TalentBase):
|
||||
id: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# 工程研究中心模型
|
||||
class LabBase(BaseModel):
|
||||
name: str
|
||||
idcode: Optional[str] = None # 新增展示用ID字段
|
||||
|
||||
# 基本信息
|
||||
center_number: Optional[str] = None # 中心编号
|
||||
school: Optional[str] = None # 所属学校
|
||||
department: Optional[str] = None # 主管部门
|
||||
field: Optional[str] = None # 所属领域
|
||||
|
||||
# 年度数据 - 修改为可以接受字符串或字典列表
|
||||
annual_data: Optional[str] = None # 存储为JSON字符串
|
||||
current_year: Optional[str] = None # 当前评估年份
|
||||
|
||||
# 详细信息
|
||||
innovation_situation: Optional[str] = None # 技术攻关与创新情况
|
||||
overall_situation: Optional[str] = None # 总体情况
|
||||
engineering_cases: Optional[str] = None # 工程化案例
|
||||
industry_service: Optional[str] = None # 行业服务情况
|
||||
discipline_support: Optional[str] = None # 学科发展支撑情况
|
||||
talent_cultivation: Optional[str] = None # 人才培养情况
|
||||
team_building: Optional[str] = None # 研究队伍建设情况
|
||||
department_support: Optional[str] = None # 主管部门、依托单位支持情况
|
||||
equipment_sharing: Optional[str] = None # 仪器设备开放共享情况
|
||||
academic_style: Optional[str] = None # 学风建设情况
|
||||
technical_committee: Optional[str] = None # 技术委员会工作情况
|
||||
next_year_plan: Optional[str] = None # 下一年度工作计划
|
||||
problems_suggestions: Optional[str] = None # 问题与建议
|
||||
director_opinion: Optional[str] = None # 工程中心负责人意见
|
||||
institution_opinion: Optional[str] = None # 依托单位意见
|
||||
research_directions: Optional[str] = None # 研究方向/学术带头人
|
||||
|
||||
# 统计数据
|
||||
national_awards_first: Optional[int] = 0 # 国家级科技奖励一等奖
|
||||
national_awards_second: Optional[int] = 0 # 国家级科技奖励二等奖
|
||||
provincial_awards_first: Optional[int] = 0 # 省、部级科技奖励一等奖
|
||||
provincial_awards_second: Optional[int] = 0 # 省、部级科技奖励二等奖
|
||||
valid_patents: Optional[int] = 0 # 有效专利
|
||||
other_ip: Optional[int] = 0 # 其他知识产权
|
||||
international_standards: Optional[int] = 0 # 国际/国家标准
|
||||
industry_standards: Optional[int] = 0 # 行业/地方标准
|
||||
|
||||
# 专利转化相关
|
||||
patent_transfer_contracts: Optional[int] = 0 # 专利转让合同项数
|
||||
patent_transfer_amount: Optional[float] = 0.0 # 专利转让合同金额
|
||||
patent_license_contracts: Optional[int] = 0 # 专利许可合同项数
|
||||
patent_license_amount: Optional[float] = 0.0 # 专利许可合同金额
|
||||
patent_valuation_contracts: Optional[int] = 0 # 专利作价合同项数
|
||||
patent_valuation_amount: Optional[float] = 0.0 # 专利作价金额
|
||||
|
||||
# 项目合作
|
||||
project_contracts: Optional[int] = 0 # 项目合同项数
|
||||
project_amount: Optional[float] = 0.0 # 项目合同金额
|
||||
|
||||
# 学科信息
|
||||
discipline_1: Optional[str] = None # 依托学科1
|
||||
discipline_2: Optional[str] = None # 依托学科2
|
||||
discipline_3: Optional[str] = None # 依托学科3
|
||||
|
||||
# 人才培养数据
|
||||
doctoral_students: Optional[int] = 0 # 在读博士生
|
||||
master_students: Optional[int] = 0 # 在读硕士生
|
||||
graduated_doctoral: Optional[int] = 0 # 当年毕业博士
|
||||
graduated_master: Optional[int] = 0 # 当年毕业硕士
|
||||
undergraduate_courses: Optional[int] = 0 # 承担本科课程
|
||||
graduate_courses: Optional[int] = 0 # 承担研究生课程
|
||||
textbooks: Optional[int] = 0 # 大专院校教材
|
||||
|
||||
# 人员结构
|
||||
professors: Optional[int] = 0 # 教授人数
|
||||
associate_professors: Optional[int] = 0 # 副教授人数
|
||||
lecturers: Optional[int] = 0 # 讲师人数
|
||||
domestic_visitors: Optional[int] = 0 # 国内访问学者
|
||||
foreign_visitors: Optional[int] = 0 # 国外访问学者
|
||||
postdoc_in: Optional[int] = 0 # 本年度进站博士后
|
||||
postdoc_out: Optional[int] = 0 # 本年度出站博士后
|
||||
|
||||
# 基础设施
|
||||
center_area: Optional[float] = 0.0 # 工程中心面积
|
||||
new_area: Optional[float] = 0.0 # 当年新增面积
|
||||
fixed_personnel: Optional[int] = 0 # 固定人员
|
||||
mobile_personnel: Optional[int] = 0 # 流动人员
|
||||
|
||||
# 经费情况
|
||||
total_funding: Optional[float] = 0.0 # 当年项目到账总经费
|
||||
vertical_funding: Optional[float] = 0.0 # 纵向经费
|
||||
horizontal_funding: Optional[float] = 0.0 # 横向经费
|
||||
|
||||
# 服务情况
|
||||
technical_consultations: Optional[int] = 0 # 技术咨询次数
|
||||
training_services: Optional[int] = 0 # 培训服务人次
|
||||
|
||||
# 原有字段保留兼容性
|
||||
personnel: Optional[str] = None
|
||||
nationalProjects: Optional[str] = None
|
||||
otherProjects: Optional[str] = None
|
||||
achievements: Optional[str] = None
|
||||
labAchievements: Optional[str] = None
|
||||
image: Optional[str] = None
|
||||
score: Optional[int] = None
|
||||
evaluationData: Optional[List[float]] = None
|
||||
sub_dimension_evaluations: Optional[Dict[str, Any]] = None # 存储二级维度评估数据
|
||||
|
||||
class LabCreate(LabBase):
|
||||
id: str
|
||||
|
||||
class Lab(LabBase):
|
||||
id: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# 新闻模型
|
||||
class NewsBase(BaseModel):
|
||||
title: str
|
||||
date: str
|
||||
|
||||
class NewsCreate(NewsBase):
|
||||
dashboard_id: int
|
||||
|
||||
class News(NewsBase):
|
||||
id: int
|
||||
dashboard_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# 仪表盘数据模型
|
||||
class DashboardData(BaseModel):
|
||||
paperCount: int
|
||||
patentCount: int
|
||||
highImpactPapers: int
|
||||
keyProjects: int
|
||||
fundingAmount: str
|
||||
researcherStats: Dict[str, int]
|
||||
newsData: List[Dict[str, str]]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# URL抓取请求模型
|
||||
class ScrapeRequest(BaseModel):
|
||||
url: str
|
||||
|
||||
# 保存评估数据请求模型
|
||||
class SaveDataRequest(BaseModel):
|
||||
data_type: str # "talent" 或 "lab"
|
||||
data: Dict[str, Any]
|
||||
|
||||
# 维度模型
|
||||
class SubDimensionBase(BaseModel):
|
||||
name: str
|
||||
weight: float = 1.0
|
||||
description: Optional[str] = None
|
||||
|
||||
class SubDimension(SubDimensionBase):
|
||||
id: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class DimensionBase(BaseModel):
|
||||
name: str
|
||||
weight: float = 0.0 # 一级维度不需要权重
|
||||
category: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
sub_dimensions: Optional[List[SubDimension]] = None
|
||||
subDimensions: Optional[List[SubDimension]] = None # 添加subDimensions作为别名,兼容前端
|
||||
|
||||
class DimensionCreate(DimensionBase):
|
||||
pass
|
||||
|
||||
class Dimension(DimensionBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# 批量保存维度的请求模型
|
||||
class SaveDimensionsRequest(BaseModel):
|
||||
dimensions: List[DimensionBase]
|
||||
category: str # "talent" 或 "lab"
|
256
backend/scrape_url_improved.py
Normal file
@ -0,0 +1,256 @@
|
||||
import requests
|
||||
import socket
|
||||
import time
|
||||
import urllib.parse
|
||||
import random
|
||||
from bs4 import BeautifulSoup
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
async def scrape_url_improved(request):
|
||||
"""
|
||||
改进版本的URL抓取函数,解决服务器部署后的网络连接问题
|
||||
"""
|
||||
try:
|
||||
print(f"开始抓取URL: {request.url}")
|
||||
|
||||
# 设置请求头,模拟浏览器访问
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
# 添加DNS解析测试
|
||||
try:
|
||||
url_parts = urllib.parse.urlparse(request.url)
|
||||
hostname = url_parts.hostname
|
||||
print(f"正在解析域名: {hostname}")
|
||||
ip_address = socket.gethostbyname(hostname)
|
||||
print(f"域名解析成功: {hostname} -> {ip_address}")
|
||||
except socket.gaierror as dns_error:
|
||||
print(f"DNS解析失败: {dns_error}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": f"DNS解析失败,无法访问 {hostname}: {str(dns_error)}"},
|
||||
)
|
||||
|
||||
# 使用重试机制,增加连接和读取超时时间
|
||||
max_retries = 3
|
||||
timeout_settings = (30, 90) # (连接超时, 读取超时)
|
||||
|
||||
response = None
|
||||
last_error = None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
print(f"第 {attempt + 1} 次尝试连接...")
|
||||
start_time = time.time()
|
||||
|
||||
# 发送HTTP请求获取页面内容,增加超时时间和重试
|
||||
response = requests.get(
|
||||
request.url,
|
||||
headers=headers,
|
||||
timeout=timeout_settings,
|
||||
verify=False, # 临时禁用SSL验证,避免证书问题
|
||||
allow_redirects=True
|
||||
)
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
print(f"请求成功,耗时: {elapsed_time:.2f}秒")
|
||||
response.raise_for_status() # 如果请求失败,抛出异常
|
||||
break
|
||||
|
||||
except requests.exceptions.Timeout as timeout_error:
|
||||
last_error = timeout_error
|
||||
print(f"第 {attempt + 1} 次尝试超时: {timeout_error}")
|
||||
if attempt < max_retries - 1:
|
||||
wait_time = (attempt + 1) * 2 # 递增等待时间
|
||||
print(f"等待 {wait_time} 秒后重试...")
|
||||
time.sleep(wait_time)
|
||||
continue
|
||||
|
||||
except requests.exceptions.ConnectionError as conn_error:
|
||||
last_error = conn_error
|
||||
print(f"第 {attempt + 1} 次尝试连接错误: {conn_error}")
|
||||
if attempt < max_retries - 1:
|
||||
wait_time = (attempt + 1) * 2
|
||||
print(f"等待 {wait_time} 秒后重试...")
|
||||
time.sleep(wait_time)
|
||||
continue
|
||||
|
||||
except requests.exceptions.RequestException as req_error:
|
||||
last_error = req_error
|
||||
print(f"第 {attempt + 1} 次尝试请求错误: {req_error}")
|
||||
if attempt < max_retries - 1:
|
||||
wait_time = (attempt + 1) * 2
|
||||
print(f"等待 {wait_time} 秒后重试...")
|
||||
time.sleep(wait_time)
|
||||
continue
|
||||
|
||||
# 如果所有重试都失败了
|
||||
if response is None:
|
||||
error_msg = f"经过 {max_retries} 次重试后仍然无法连接到 {request.url}"
|
||||
if last_error:
|
||||
error_msg += f",最后错误: {str(last_error)}"
|
||||
print(error_msg)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"error": error_msg,
|
||||
"suggestions": [
|
||||
"检查服务器网络连接",
|
||||
"确认目标网站是否可访问",
|
||||
"检查防火墙设置",
|
||||
"考虑配置代理服务器",
|
||||
"联系系统管理员检查网络配置"
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# 设置编码以正确处理中文字符
|
||||
response.encoding = 'utf-8'
|
||||
|
||||
# 解析HTML
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
# 获取基础URL用于解析相对路径
|
||||
url_parts = urllib.parse.urlparse(request.url)
|
||||
base_url = f"{url_parts.scheme}://{url_parts.netloc}"
|
||||
|
||||
# 初始化数据字典
|
||||
teacher_data = {
|
||||
"id": f"BLG{random.randint(10000, 99999)}",
|
||||
"photo": f"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='120' viewBox='0 0 100 120'%3E%3Crect width='100' height='120' fill='%234986ff' opacity='0.3'/%3E%3Ccircle cx='50' cy='45' r='25' fill='%234986ff' opacity='0.6'/%3E%3Ccircle cx='50' cy='95' r='35' fill='%234986ff' opacity='0.6'/%3E%3C/svg%3E",
|
||||
"evaluationData": [
|
||||
round(min(100, max(60, 70 + 20 * (0.5 - random.random())))) for _ in range(6)
|
||||
]
|
||||
}
|
||||
|
||||
# 从教师信息表提取基本信息
|
||||
info_table = soup.find('div', class_='wz_teacher')
|
||||
if info_table:
|
||||
table = info_table.find('table')
|
||||
if table:
|
||||
rows = table.find_all('tr')
|
||||
|
||||
# 提取姓名、性别、出生年月
|
||||
if len(rows) > 0:
|
||||
cells = rows[0].find_all('td')
|
||||
if len(cells) >= 6:
|
||||
teacher_data["name"] = cells[1].text.strip()
|
||||
teacher_data["gender"] = cells[3].text.strip()
|
||||
teacher_data["birthDate"] = cells[5].text.strip()
|
||||
|
||||
# 提取职称、职务、最高学历
|
||||
if len(rows) > 1:
|
||||
cells = rows[1].find_all('td')
|
||||
if len(cells) >= 6:
|
||||
teacher_data["title"] = cells[1].text.strip()
|
||||
position = cells[3].text.strip()
|
||||
teacher_data["position"] = position if position else ""
|
||||
teacher_data["education"] = cells[5].text.strip()
|
||||
|
||||
# 提取学科方向
|
||||
if len(rows) > 2:
|
||||
cells = rows[2].find_all('td')
|
||||
if len(cells) >= 2:
|
||||
teacher_data["academicDirection"] = cells[1].text.strip()
|
||||
|
||||
# 提取人才计划和办公地点
|
||||
if len(rows) > 3:
|
||||
cells = rows[3].find_all('td')
|
||||
if len(cells) >= 6:
|
||||
talent_plan = cells[1].text.strip()
|
||||
teacher_data["talentPlan"] = talent_plan if talent_plan else ""
|
||||
teacher_data["officeLocation"] = cells[5].text.strip()
|
||||
|
||||
# 提取电子邮件和联系方式
|
||||
if len(rows) > 4:
|
||||
cells = rows[4].find_all('td')
|
||||
if len(cells) >= 6:
|
||||
email = cells[1].text.strip()
|
||||
teacher_data["email"] = email if email else ""
|
||||
phone = cells[5].text.strip()
|
||||
teacher_data["phone"] = phone if phone else ""
|
||||
|
||||
# 提取通讯地址
|
||||
if len(rows) > 5:
|
||||
cells = rows[5].find_all('td')
|
||||
if len(cells) >= 2:
|
||||
teacher_data["address"] = cells[1].text.strip()
|
||||
|
||||
# 提取导师类型
|
||||
if len(rows) > 6:
|
||||
cells = rows[6].find_all('td')
|
||||
if len(cells) >= 2:
|
||||
teacher_data["tutorType"] = cells[1].text.strip()
|
||||
|
||||
# 提取照片
|
||||
photo_element = soup.select_one('.teacherInfo .img img')
|
||||
if photo_element and photo_element.get('src'):
|
||||
img_src = photo_element['src']
|
||||
|
||||
# 处理相对路径,构建完整的图片URL
|
||||
if img_src.startswith('../../../'):
|
||||
# 从URL获取基础路径(移除文件名和最后两级目录)
|
||||
url_parts = request.url.split('/')
|
||||
if len(url_parts) >= 4:
|
||||
base_path = '/'.join(url_parts[:-3])
|
||||
img_url = f"{base_path}/{img_src[9:]}" # 移除 '../../../'
|
||||
else:
|
||||
img_url = urllib.parse.urljoin(base_url, img_src)
|
||||
else:
|
||||
img_url = urllib.parse.urljoin(base_url, img_src)
|
||||
|
||||
# 直接保存完整的图片URL,不下载到本地
|
||||
teacher_data["photo"] = img_url
|
||||
|
||||
# 提取详细信息部分
|
||||
content_divs = soup.select('.con01_t')
|
||||
for div in content_divs:
|
||||
heading = div.find('h3')
|
||||
if not heading:
|
||||
continue
|
||||
|
||||
heading_text = heading.text.strip()
|
||||
|
||||
# 获取该部分的所有段落文本
|
||||
paragraphs = [p.text.strip() for p in div.find_all('p') if p.text.strip()]
|
||||
section_content = '\n'.join(paragraphs)
|
||||
|
||||
# 根据标题将内容映射到相应字段
|
||||
if '教育与工作经历' in heading_text:
|
||||
teacher_data["eduWorkHistory"] = section_content
|
||||
elif '研究方向' in heading_text:
|
||||
teacher_data["researchDirection"] = section_content
|
||||
elif '近5年承担的科研项目' in heading_text or '近五年承担的科研项目' in heading_text:
|
||||
teacher_data["recentProjects"] = section_content
|
||||
# 计算项目数量
|
||||
project_count = len([p for p in paragraphs if p.strip().startswith(str(len(paragraphs) - paragraphs.index(p))+".")])
|
||||
if project_count > 0:
|
||||
teacher_data["projects"] = f"{project_count}项"
|
||||
else:
|
||||
teacher_data["projects"] = f"{len(paragraphs)}项"
|
||||
elif '代表性学术论文' in heading_text:
|
||||
teacher_data["representativePapers"] = section_content
|
||||
# 计算论文数量
|
||||
paper_count = len([p for p in paragraphs if p.strip().startswith("[")])
|
||||
if paper_count > 0:
|
||||
teacher_data["papers"] = f"{paper_count}篇"
|
||||
else:
|
||||
teacher_data["papers"] = f"{len(paragraphs)}篇"
|
||||
elif '授权国家发明专利' in heading_text or '专利' in heading_text:
|
||||
teacher_data["patents"] = section_content
|
||||
|
||||
print(f"抓取成功,提取到教师数据: {teacher_data.get('name', '未知')}")
|
||||
return teacher_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"抓取错误: {str(e)}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": f"抓取网页失败: {str(e)}"},
|
||||
)
|
BIN
backend/static/images/d5e83cac900c49478a8cd5130c8a2ad9.png
Normal file
After Width: | Height: | Size: 121 KiB |
28
docker-compose.yml
Normal file
@ -0,0 +1,28 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "48100:48100"
|
||||
restart: always
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "48996:48996"
|
||||
restart: always
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
22
nginx.conf
Normal file
@ -0,0 +1,22 @@
|
||||
server {
|
||||
listen 48100;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 如果有API请求,可以配置代理
|
||||
# location /api {
|
||||
# proxy_pass http://backend-service:8080;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# }
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "fast-dashboard",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:local": "cross-env NODE_ENV=development vite",
|
||||
"dev:prod": "cross-env NODE_ENV=production vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"backend": "cd backend && python app.py",
|
||||
"start": "concurrently \"npm run dev\" \"npm run backend\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.8.4",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.9.8",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.2",
|
||||
"concurrently": "^7.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"vite": "^6.3.1"
|
||||
}
|
||||
}
|
1425
pnpm-lock.yaml
generated
Normal file
BIN
public/image/人1.png
Normal file
After Width: | Height: | Size: 280 KiB |
BIN
public/image/人2.png
Normal file
After Width: | Height: | Size: 301 KiB |
BIN
public/image/人3.png
Normal file
After Width: | Height: | Size: 279 KiB |
BIN
public/image/人4.png
Normal file
After Width: | Height: | Size: 225 KiB |
BIN
public/image/人5.png
Normal file
After Width: | Height: | Size: 197 KiB |
BIN
public/image/人6.png
Normal file
After Width: | Height: | Size: 219 KiB |
BIN
public/image/人7.png
Normal file
After Width: | Height: | Size: 202 KiB |
BIN
public/image/人8.png
Normal file
After Width: | Height: | Size: 247 KiB |
BIN
public/image/实验室1.png
Normal file
After Width: | Height: | Size: 663 KiB |
BIN
public/image/实验室2.png
Normal file
After Width: | Height: | Size: 312 KiB |
BIN
public/image/实验室3.png
Normal file
After Width: | Height: | Size: 911 KiB |
BIN
public/image/实验室4.png
Normal file
After Width: | Height: | Size: 767 KiB |
BIN
public/image/实验室5.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
public/image/实验室6.png
Normal file
After Width: | Height: | Size: 670 KiB |
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/实验室简历.docx
Normal file
51
setup_firewall.bat
Normal file
@ -0,0 +1,51 @@
|
||||
@echo off
|
||||
echo ===========================================
|
||||
echo 配置 Fast Dashboard 防火墙规则
|
||||
echo ===========================================
|
||||
echo.
|
||||
|
||||
echo 此脚本将为 Fast Dashboard 项目配置防火墙规则
|
||||
echo 需要管理员权限来修改防火墙设置
|
||||
echo.
|
||||
pause
|
||||
|
||||
echo 正在添加防火墙规则...
|
||||
echo.
|
||||
|
||||
echo 添加前端端口 5173 的入站规则...
|
||||
netsh advfirewall firewall add rule name="Fast Dashboard Frontend (Port 5173)" dir=in action=allow protocol=TCP localport=5173
|
||||
if %errorlevel% equ 0 (
|
||||
echo ✓ 前端端口 5173 规则添加成功
|
||||
) else (
|
||||
echo ✗ 前端端口 5173 规则添加失败,请以管理员身份运行此脚本
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 添加后端端口 8000 的入站规则...
|
||||
netsh advfirewall firewall add rule name="Fast Dashboard Backend (Port 8000)" dir=in action=allow protocol=TCP localport=8000
|
||||
if %errorlevel% equ 0 (
|
||||
echo ✓ 后端端口 8000 规则添加成功
|
||||
) else (
|
||||
echo ✗ 后端端口 8000 规则添加失败,请以管理员身份运行此脚本
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ===========================================
|
||||
echo 防火墙配置完成!
|
||||
echo ===========================================
|
||||
echo.
|
||||
echo 已添加以下防火墙规则:
|
||||
echo - Fast Dashboard Frontend (Port 5173)
|
||||
echo - Fast Dashboard Backend (Port 8000)
|
||||
echo.
|
||||
echo 现在其他设备应该可以通过以下地址访问您的服务:
|
||||
echo - 前端: http://192.168.18.108:5173
|
||||
echo - 后端: http://192.168.18.108:8000
|
||||
echo.
|
||||
echo 如果仍然无法访问,请检查:
|
||||
echo 1. 路由器是否有端口转发设置
|
||||
echo 2. 网络是否在同一局域网内
|
||||
echo 3. 是否有其他安全软件阻止访问
|
||||
echo ===========================================
|
||||
echo.
|
||||
pause
|
23
setup_py_env.bat
Normal file
@ -0,0 +1,23 @@
|
||||
@echo off
|
||||
echo Setting up Python environment for fast-dashboard project...
|
||||
|
||||
REM Call conda activate in batch mode
|
||||
call conda activate fast-dashboard-env || (
|
||||
echo Failed to activate environment. Creating it...
|
||||
call conda create -n fast-dashboard-env python=3.11 -y
|
||||
call conda activate fast-dashboard-env
|
||||
)
|
||||
|
||||
echo Installing dependencies...
|
||||
pip install -r backend/requirements.txt
|
||||
|
||||
echo Environment setup complete!
|
||||
echo.
|
||||
echo To activate this environment in the future, run:
|
||||
echo conda activate fast-dashboard-env
|
||||
echo.
|
||||
echo To run the backend server, run:
|
||||
echo cd backend
|
||||
echo uvicorn main:app --reload
|
||||
echo.
|
||||
pause
|
183
src/App.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 登录页面 -->
|
||||
<Login v-if="!isLoggedIn" @login-success="loginSuccess" />
|
||||
|
||||
<!-- 显示Token测试页面的按钮 -->
|
||||
<!-- <div v-if="isLoggedIn" class="token-test-button">-->
|
||||
<!-- <el-button type="info" size="small" @click="toggleTokenTest">-->
|
||||
<!-- {{ showTokenTest ? '关闭测试工具' : '打开Token测试工具' }}-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- Token测试工具 -->
|
||||
<TokenTest v-if="isLoggedIn && showTokenTest" />
|
||||
|
||||
<!-- 主仪表盘 -->
|
||||
<Dashboard v-if="isLoggedIn && currentPage === 'dashboard' && !showTokenTest"
|
||||
@navigate="changePage"
|
||||
@logout="handleLogout" />
|
||||
|
||||
<!-- 科研成果详情页 -->
|
||||
<ResearchDetail v-if="isLoggedIn && currentPage === 'research' && !showTokenTest"
|
||||
@back-to-dashboard="backToDashboard"
|
||||
@logout="handleLogout" />
|
||||
|
||||
<!-- 教师科研人才页面 -->
|
||||
<TalentDetail v-if="isLoggedIn && currentPage === 'talent' && !showTokenTest"
|
||||
@back-to-dashboard="backToDashboard"
|
||||
@navigate="changePage"
|
||||
@logout="handleLogout" />
|
||||
|
||||
<!-- 工程研究中心页面 -->
|
||||
<LabDetail v-if="isLoggedIn && currentPage === 'lab' && !showTokenTest"
|
||||
@back-to-dashboard="backToDashboard"
|
||||
@navigate="changePage"
|
||||
@logout="handleLogout" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import Login from './components/login.vue'
|
||||
import Dashboard from './components/Dashboard.vue'
|
||||
import ResearchDetail from './components/ResearchDetail.vue'
|
||||
import TalentDetail from './components/TalentDetail.vue'
|
||||
import LabDetail from './components/LabDetail.vue'
|
||||
import TokenTest from './components/TokenTest.vue'
|
||||
import axios from 'axios'
|
||||
import { getApiBaseUrl } from './config'
|
||||
|
||||
// 登录状态
|
||||
const isLoggedIn = ref(false)
|
||||
|
||||
// 当前显示的页面
|
||||
const currentPage = ref('dashboard')
|
||||
|
||||
// 是否显示Token测试工具
|
||||
const showTokenTest = ref(false)
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
onMounted(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
const loginStatus = localStorage.getItem('isLoggedIn')
|
||||
if (token && loginStatus === 'true') {
|
||||
isLoggedIn.value = true
|
||||
}
|
||||
})
|
||||
|
||||
// 登录成功
|
||||
const loginSuccess = () => {
|
||||
isLoggedIn.value = true
|
||||
}
|
||||
|
||||
// 切换页面
|
||||
const changePage = (page) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
// 返回仪表盘
|
||||
const backToDashboard = () => {
|
||||
currentPage.value = 'dashboard'
|
||||
}
|
||||
|
||||
// 切换Token测试工具显示
|
||||
const toggleTokenTest = () => {
|
||||
showTokenTest.value = !showTokenTest.value
|
||||
}
|
||||
|
||||
// 处理登出
|
||||
const handleLogout = () => {
|
||||
ElMessageBox.confirm(
|
||||
'确定要退出登录吗?',
|
||||
'退出提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
customClass: 'custom-message-box',
|
||||
confirmButtonClass: 'custom-confirm-button',
|
||||
cancelButtonClass: 'custom-cancel-button',
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
// 获取token
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
// 调用清空数据API
|
||||
// await axios.post(
|
||||
// `${getApiBaseUrl()}/api/clear-all-data`,
|
||||
// {},
|
||||
// {
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${token}`
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清空数据失败:', error)
|
||||
} finally {
|
||||
// 无论清空数据成功或失败,都清除登录信息
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('isLoggedIn')
|
||||
isLoggedIn.value = false
|
||||
currentPage.value = 'dashboard'
|
||||
showTokenTest.value = false
|
||||
}
|
||||
}).catch(() => {
|
||||
// 用户取消操作
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Microsoft YaHei', 'PingFang SC', Arial, sans-serif;
|
||||
background-color: #0c1633;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.token-test-button {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* 自定义确认框样式 */
|
||||
.custom-message-box {
|
||||
background-color: #0c1633;
|
||||
border: 1px solid #1d3160;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.custom-message-box .el-message-box__title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.custom-message-box .el-message-box__content {
|
||||
color: #c0c5d6;
|
||||
}
|
||||
|
||||
.custom-confirm-button {
|
||||
background-color: #1989fa;
|
||||
border-color: #1989fa;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.custom-cancel-button {
|
||||
background-color: transparent;
|
||||
border-color: #3a5794;
|
||||
color: #c0c5d6;
|
||||
}
|
||||
</style>
|
BIN
src/assets/loginBackground.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
src/assets/logo1.png
Normal file
After Width: | Height: | Size: 207 KiB |
BIN
src/assets/logo2.png
Normal file
After Width: | Height: | Size: 23 KiB |
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
BIN
src/assets/人才详情贴图.png
Normal file
After Width: | Height: | Size: 822 KiB |
BIN
src/assets/人才贴图.png
Normal file
After Width: | Height: | Size: 3.1 MiB |
2979
src/assets/实验室.json
Normal file
494
src/assets/实验室_fixed.json
Normal file
BIN
src/assets/实验室详情贴图.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/实验室贴图.png
Normal file
After Width: | Height: | Size: 4.8 MiB |
1272
src/components/Dashboard.vue
Normal file
483
src/components/DimensionDrawer.vue
Normal file
@ -0,0 +1,483 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="props.visible"
|
||||
title=""
|
||||
direction="rtl"
|
||||
size="600px"
|
||||
:before-close="handleClose"
|
||||
custom-class="dimension-drawer"
|
||||
>
|
||||
<div class="drawer-content">
|
||||
<div class="dimension-section">
|
||||
<h2 class="section-title">评估维度设置</h2>
|
||||
|
||||
<!-- 维度列表 -->
|
||||
<div class="dimensions-list">
|
||||
<div v-for="(dim, index) in dimensions" :key="index" class="dimension-item">
|
||||
<div class="dimension-name">
|
||||
<el-input
|
||||
v-model="dim.name"
|
||||
placeholder="请输入维度名称"
|
||||
/>
|
||||
</div>
|
||||
<div class="dimension-weight">
|
||||
<div class="weight-input-group">
|
||||
<el-input-number
|
||||
v-model="dim.weight"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="weight-unit">%</span>
|
||||
</div>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="Delete"
|
||||
circle
|
||||
@click="removeDimension(index)"
|
||||
class="delete-btn"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增维度表单 -->
|
||||
<div class="new-dimension-form">
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<span class="label">新增维度名称</span>
|
||||
<el-input
|
||||
v-model="newDimension.name"
|
||||
placeholder="请输入维度名称"
|
||||
class="dimension-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item weight-item">
|
||||
<span class="label">维度权重</span>
|
||||
<div class="weight-input-container">
|
||||
<el-input-number
|
||||
v-model="newDimension.weight"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="1"
|
||||
class="weight-input"
|
||||
/>
|
||||
<span class="weight-unit">%</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="Plus"
|
||||
circle
|
||||
class="add-btn"
|
||||
@click="addDimension"
|
||||
:disabled="!newDimension.name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 权重总计信息 -->
|
||||
<div class="weight-summary">
|
||||
<div class="weight-total">
|
||||
<span>权重维度要求:</span>
|
||||
<span class="weight-value">100%</span>
|
||||
</div>
|
||||
<div class="weight-current" :class="{ 'weight-error': totalWeight !== 100 }">
|
||||
<span>当前权重维度总计:</span>
|
||||
<span class="weight-value">{{ totalWeight }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<button class="drawer-btn cancel-btn" @click="handleClose">取消</button>
|
||||
<button
|
||||
class="drawer-btn confirm-btn"
|
||||
@click="handleSave"
|
||||
>确认</button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import axios from 'axios';
|
||||
import { getApiBaseUrl } from '../config'; // 导入API基础URL函数
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
dimensions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
default: 'talent' // 'talent'或'lab'
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'save']);
|
||||
|
||||
// 用于visible属性的双向绑定
|
||||
const drawerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val)
|
||||
});
|
||||
|
||||
// 所有维度数据
|
||||
const dimensions = ref([]);
|
||||
|
||||
// 初始化维度数据
|
||||
watch(() => props.dimensions, (newDimensions) => {
|
||||
if (newDimensions && newDimensions.length > 0) {
|
||||
// 深拷贝维度数据,避免直接修改props
|
||||
dimensions.value = JSON.parse(JSON.stringify(newDimensions));
|
||||
} else {
|
||||
dimensions.value = [];
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 新维度数据
|
||||
const newDimension = reactive({
|
||||
name: '',
|
||||
weight: 5
|
||||
});
|
||||
|
||||
// 计算总权重
|
||||
const totalWeight = computed(() => {
|
||||
return dimensions.value.reduce((sum, dim) => sum + (dim.weight || 0), 0);
|
||||
});
|
||||
|
||||
// 添加新维度
|
||||
const addDimension = () => {
|
||||
if (!newDimension.name) {
|
||||
ElMessage.warning('请输入维度名称');
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加到维度列表
|
||||
dimensions.value.push({
|
||||
name: newDimension.name,
|
||||
weight: newDimension.weight
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
newDimension.name = '';
|
||||
newDimension.weight = 5;
|
||||
};
|
||||
|
||||
// 删除维度
|
||||
const removeDimension = (index) => {
|
||||
dimensions.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 验证总权重 - 不再每次变更时显示提示
|
||||
const validateTotalWeight = () => {
|
||||
// 移除提示消息,只在保存时验证
|
||||
};
|
||||
|
||||
// 处理关闭 - 修复后的函数
|
||||
const handleClose = (done) => {
|
||||
// 如果有 done 回调(来自 before-close),则调用它
|
||||
if (typeof done === 'function') {
|
||||
done();
|
||||
}
|
||||
// 无论如何都要更新状态
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
// 处理保存
|
||||
const handleSave = async () => {
|
||||
|
||||
|
||||
if (totalWeight.value !== 100) {
|
||||
ElMessage.error('权重总计必须为100%,请调整后再保存');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建要保存的维度数据
|
||||
const dimensionsData = dimensions.value.map(dim => ({
|
||||
name: dim.name,
|
||||
weight: dim.weight,
|
||||
category: props.category,
|
||||
id: dim.id
|
||||
}));
|
||||
|
||||
// 从localStorage获取JWT令牌
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
ElMessage.error('未登录或登录已过期,请重新登录');
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据类别(教师科研人才评估或工程研究中心评估)选择对应的API接口
|
||||
const response = await axios.post(`${getApiBaseUrl()}/api/save-dimensions`, {
|
||||
dimensions: dimensionsData,
|
||||
category: props.category
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success('维度设置保存成功');
|
||||
// 发送保存事件,通知父组件刷新数据
|
||||
emit('save', dimensions.value);
|
||||
// 关闭抽屉
|
||||
handleClose();
|
||||
} else {
|
||||
throw new Error(response.data.message || '保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存维度失败:', error);
|
||||
ElMessage.error('保存维度失败,请重试');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 确保引入common.css */
|
||||
@import './common.css';
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.dimension-drawer :deep(.el-drawer__header) {
|
||||
margin-bottom: 0;
|
||||
color: white;
|
||||
background-color: #0c1633;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.dimension-drawer :deep(.el-drawer__body) {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #0c1633;
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
padding: 20px;
|
||||
color: white;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #0c1633;
|
||||
}
|
||||
|
||||
.dimension-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 15px 0;
|
||||
font-size: 18px;
|
||||
color: rgba(255,255,255,0.9);
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.dimensions-list {
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
margin-bottom: 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1; /* 让这个元素占满可用空间 */
|
||||
}
|
||||
|
||||
.dimension-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.1);
|
||||
}
|
||||
|
||||
.dimension-name {
|
||||
flex: 1;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.dimension-weight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.weight-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
font-size: 12px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.new-dimension-form {
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.weight-item {
|
||||
flex: 0.7;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.weight-input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.weight-input {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.weight-unit {
|
||||
margin: 0 5px;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.weight-summary {
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.weight-total, .weight-current {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.weight-value {
|
||||
font-weight: bold;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.weight-error .weight-value {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 抽屉页脚 */
|
||||
.dimension-drawer :deep(.el-drawer__footer) {
|
||||
border-top: 1px solid rgba(73,134,255,0.3);
|
||||
padding: 10px 20px;
|
||||
background-color: #0c1633;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.drawer-btn {
|
||||
padding: 8px 15px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-regular;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: transparent;
|
||||
color: rgba(255,255,255,0.8);
|
||||
border: 1px solid rgba(73,134,255,0.5);
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background-color: rgba(14,62,167,1);
|
||||
color: rgba(255,255,255,1);
|
||||
border: 1px solid rgba(73,134,255,1);
|
||||
}
|
||||
|
||||
.confirm-btn:disabled {
|
||||
background-color: rgba(14,62,167,0.5);
|
||||
border: 1px solid rgba(73,134,255,0.5);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Element Plus组件的深色主题覆盖样式 */
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-textarea__wrapper) {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
box-shadow: 0 0 0 1px rgba(73,134,255,0.3) inset;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner),
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner::placeholder),
|
||||
:deep(.el-textarea__inner::placeholder) {
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
:deep(.el-input-number) {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
:deep(.el-input-number__decrease),
|
||||
:deep(.el-input-number__increase) {
|
||||
background-color: rgba(73,134,255,0.2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.drawer-content::-webkit-scrollbar,
|
||||
.dimensions-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.drawer-content::-webkit-scrollbar-track,
|
||||
.dimensions-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.drawer-content::-webkit-scrollbar-thumb,
|
||||
.dimensions-list::-webkit-scrollbar-thumb {
|
||||
background-color: #4986ff;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
886
src/components/LabDetail.vue
Normal file
@ -0,0 +1,886 @@
|
||||
<template>
|
||||
<div class="evaluation-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="dashboard-header">
|
||||
<div class="logo">
|
||||
<img src="../assets/logo1.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
|
||||
<img src="../assets/logo2.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
|
||||
<h1 class="main-title">
|
||||
<div class="title-line"></div>
|
||||
<span class="title-text">智慧科研评估系统</span>
|
||||
<div class="title-line"></div>
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="content-container">
|
||||
<!-- 左侧维度设置 -->
|
||||
<div class="dimension-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1 class="sidebar-title">
|
||||
<span class="home-link" @click="jumpToDashboard">首页</span> > 工程研究中心评估
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="dimension-content">
|
||||
<h2 class="dimension-section-title">评估维度设置</h2>
|
||||
|
||||
<div class="dimension-list custom-scrollbar">
|
||||
<div v-for="(dim, index) in dimensions" :key="index" class="dimension-item primary-dimension">
|
||||
<div class="dimension-header" @click="editDimension(dim, index)">
|
||||
<div class="dimension-name">
|
||||
<label>{{ dim.name }}</label>
|
||||
</div>
|
||||
<div class="dimension-expand">
|
||||
<span class="expand-icon">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 二级维度列表 -->
|
||||
<div class="sub-dimensions">
|
||||
<div v-for="(subDim, subIndex) in dim.subDimensions" :key="`${index}-${subIndex}`" class="dimension-item sub-dimension">
|
||||
<div class="dimension-name">
|
||||
<label>{{ subDim.name }}</label>
|
||||
</div>
|
||||
<div class="dimension-weight">
|
||||
<span class="weight-label">W:</span>
|
||||
<span class="weight-value">{{ subDim.weight }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dimension-add" @click="openDimensionDrawer">
|
||||
<span class="add-icon">+</span>
|
||||
<span class="add-text">添加自定义维度</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="action-bar">
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入工程研究中心名称或ID号"
|
||||
v-model="searchQuery"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
<button class="search-button">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||
<path fill="white" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="add-evaluation-btn" @click="openAddEvaluationDrawer">
|
||||
新增评估
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 工程研究中心卡片列表 -->
|
||||
<div class="lab-card-grid custom-scrollbar">
|
||||
<div v-for="(lab, index) in filteredLabs" :key="index" class="lab-card" style="height: 440px;" @click="openLabDetail(lab)">
|
||||
<div class="card-header">
|
||||
<span class="lab-id">ID: {{ lab.idcode || lab.id }}</span>
|
||||
<!-- <span class="total-score">综合评估分数: <span class="score-value">{{ lab.score }}分</span></span> -->
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="lab-image">
|
||||
<img :src="lab.image" alt="工程研究中心图片" />
|
||||
</div>
|
||||
|
||||
<div class="lab-info">
|
||||
<div class="info-item">
|
||||
<span class="info-label">研究中心名称:</span>
|
||||
<span class="info-value">{{ lab.name }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">所属领域:</span>
|
||||
<span class="info-value">{{ lab.field }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">所属学校:</span>
|
||||
<span class="info-value">{{ lab.school }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">主管部门:</span>
|
||||
<span class="info-value">{{ lab.department }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="evaluation-chart">
|
||||
<div :id="`lab-chart-${index}`" class="radar-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 维度设置抽屉 -->
|
||||
<LabDimensionDrawer
|
||||
v-model:visible="dimensionDrawerVisible"
|
||||
:dimensions="dimensions"
|
||||
@save="handleSaveDimensions"
|
||||
/>
|
||||
|
||||
<LabDrawerDetail
|
||||
v-model:visible="drawerVisible"
|
||||
:is-edit="isEditMode"
|
||||
:dimensions="dimensions"
|
||||
:lab-data="selectedLab"
|
||||
@save="handleSaveEvaluation"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { RadarChart } from 'echarts/charts';
|
||||
import LabDrawerDetail from './LabDrawerDetail.vue';
|
||||
import LabDimensionDrawer from './LabDimensionDrawer.vue';
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent
|
||||
} from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import axios from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getApiBaseUrl } from '../config'; // 导入API基础URL函数
|
||||
|
||||
// 注册必要的 echarts 组件
|
||||
echarts.use([
|
||||
RadarChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
CanvasRenderer
|
||||
]);
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
const isEditMode = ref(false);
|
||||
const selectedLab = ref(null);
|
||||
const dimensionDrawerVisible = ref(false);
|
||||
|
||||
// 打开添加评估抽屉
|
||||
const openAddEvaluationDrawer = () => {
|
||||
isEditMode.value = false;
|
||||
selectedLab.value = null;
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
// 打开维度设置抽屉
|
||||
const openDimensionDrawer = () => {
|
||||
dimensionDrawerVisible.value = true;
|
||||
};
|
||||
|
||||
// 处理保存评估
|
||||
const handleSaveEvaluation = async (data) => {
|
||||
// 接收到保存事件后,直接从服务器重新加载最新数据
|
||||
// 而不是手动更新本地数据
|
||||
await loadLabs();
|
||||
|
||||
// 关闭抽屉
|
||||
drawerVisible.value = false;
|
||||
|
||||
// 显示成功消息
|
||||
ElMessage.success(isEditMode.value ? '工程研究中心评估数据更新成功' : '工程研究中心评估数据新增成功');
|
||||
};
|
||||
|
||||
// 处理保存维度
|
||||
const handleSaveDimensions = (newDimensions) => {
|
||||
dimensions.value = newDimensions;
|
||||
// 更新所有雷达图
|
||||
updateAllRadarCharts();
|
||||
};
|
||||
|
||||
// 向父组件发送页面切换事件
|
||||
const emit = defineEmits(['navigate', 'back-to-dashboard', 'logout']);
|
||||
|
||||
// 跳转到仪表盘页面
|
||||
const jumpToDashboard = () => {
|
||||
emit('back-to-dashboard');
|
||||
};
|
||||
|
||||
// 处理Logo点击事件
|
||||
const handleLogoClick = () => {
|
||||
emit('logout');
|
||||
};
|
||||
|
||||
// 评估维度数据
|
||||
const dimensions = ref([]);
|
||||
|
||||
// 工程研究中心数据
|
||||
const labs = ref([]);
|
||||
|
||||
// 根据搜索过滤工程研究中心
|
||||
const filteredLabs = ref([]);
|
||||
|
||||
// 为每个工程研究中心分配唯一的图片
|
||||
function assignUniqueLabImages() {
|
||||
// 确保每个工程研究中心都有不同的图片
|
||||
labs.value.forEach((lab, index) => {
|
||||
// 计算1-6之间的序号,确保均匀分布
|
||||
const photoIndex = (index % 6) + 1;
|
||||
lab.image = `/image/实验室${photoIndex}.png`;
|
||||
});
|
||||
}
|
||||
|
||||
// 在组件挂载时获取维度数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 获取工程研究中心评估维度数据
|
||||
const response = await axios.get(`${getApiBaseUrl()}/dimensions/lab`);
|
||||
dimensions.value = response.data;
|
||||
// 加载工程研究中心数据
|
||||
await loadLabs();
|
||||
// 在此不需要调用handleSearch,因为loadLabs中已经调用了
|
||||
} catch (error) {
|
||||
console.error('获取维度数据失败:', error);
|
||||
ElMessage.error('获取维度数据失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 加载工程研究中心数据
|
||||
const loadLabs = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${getApiBaseUrl()}/labs`);
|
||||
labs.value = response.data;
|
||||
|
||||
// 为工程研究中心分配唯一的图片
|
||||
assignUniqueLabImages();
|
||||
|
||||
// 确保每个工程研究中心都有分数
|
||||
labs.value.forEach(lab => {
|
||||
// 确保有分数
|
||||
if (!lab.score && lab.evaluationData && lab.evaluationData.length > 0) {
|
||||
// 计算平均分作为综合得分
|
||||
lab.score = Math.round(lab.evaluationData.reduce((a, b) => a + b, 0) / lab.evaluationData.length);
|
||||
}
|
||||
});
|
||||
|
||||
// 先更新过滤的工程研究中心列表
|
||||
handleSearch();
|
||||
|
||||
// 给一个短暂的延时,确保DOM已经更新
|
||||
setTimeout(() => {
|
||||
updateAllRadarCharts();
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('获取工程研究中心数据失败:', error);
|
||||
ElMessage.error('获取工程研究中心数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑维度
|
||||
const editDimension = (dim, index) => {
|
||||
// 打开维度设置抽屉
|
||||
openDimensionDrawer();
|
||||
};
|
||||
|
||||
// 更新所有雷达图
|
||||
const updateAllRadarCharts = () => {
|
||||
nextTick(() => {
|
||||
filteredLabs.value.forEach((lab, index) => {
|
||||
const chartDom = document.getElementById(`lab-chart-${index}`);
|
||||
if (!chartDom) return;
|
||||
|
||||
// 先清空之前的图表实例,避免重复创建
|
||||
echarts.dispose(chartDom);
|
||||
const chart = echarts.init(chartDom);
|
||||
|
||||
// 从二级维度生成指标
|
||||
const indicators = [];
|
||||
if (dimensions.value && dimensions.value.length > 0) {
|
||||
dimensions.value.forEach(dim => {
|
||||
if (dim.subDimensions && dim.subDimensions.length > 0) {
|
||||
dim.subDimensions.forEach(subDim => {
|
||||
indicators.push({
|
||||
name: subDim.name,
|
||||
max: 100
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 如果没有维度,使用默认维度
|
||||
if (indicators.length === 0) {
|
||||
indicators.push(
|
||||
{ name: '创新水平', max: 100 },
|
||||
{ name: '研究能力', max: 100 },
|
||||
{ name: '成果转化', max: 100 },
|
||||
{ name: '学科建设', max: 100 },
|
||||
{ name: '行业贡献', max: 100 },
|
||||
{ name: '发展潜力', max: 100 }
|
||||
);
|
||||
}
|
||||
|
||||
// 准备雷达图数据
|
||||
let radarData = [];
|
||||
|
||||
// 检查是否有二级维度评估数据
|
||||
let hasSubDimensionData = false;
|
||||
if (lab.sub_dimension_evaluations && Object.keys(lab.sub_dimension_evaluations).length > 0) {
|
||||
dimensions.value.forEach(dim => {
|
||||
if (dim.subDimensions && dim.subDimensions.length > 0 &&
|
||||
lab.sub_dimension_evaluations[dim.name]) {
|
||||
dim.subDimensions.forEach(subDim => {
|
||||
if (lab.sub_dimension_evaluations[dim.name][subDim.name] !== undefined) {
|
||||
hasSubDimensionData = true;
|
||||
radarData.push(lab.sub_dimension_evaluations[dim.name][subDim.name]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 如果没有二级维度数据,使用传统的evaluationData
|
||||
if (!hasSubDimensionData) {
|
||||
// 确保评估数据与指标数量匹配
|
||||
if (!lab.evaluationData || lab.evaluationData.length !== indicators.length) {
|
||||
// 生成随机评估数据,范围在60-90之间
|
||||
lab.evaluationData = indicators.map(() => Math.floor(Math.random() * 31) + 60);
|
||||
}
|
||||
radarData = lab.evaluationData;
|
||||
}
|
||||
|
||||
chart.setOption({
|
||||
radar: {
|
||||
indicator: indicators,
|
||||
splitArea: {
|
||||
show: false
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(211, 253, 250, 0.8)'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgba(211, 253, 250, 0.8)'
|
||||
}
|
||||
},
|
||||
name: {
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: [
|
||||
{
|
||||
value: radarData,
|
||||
name: '评估结果',
|
||||
areaStyle: {
|
||||
color: 'rgba(255, 0, 255, 0.3)'
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'rgba(255, 0, 255, 0.8)',
|
||||
width: 1
|
||||
},
|
||||
itemStyle: {
|
||||
color: 'rgba(255, 0, 255, 0.8)'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 添加窗口大小变化时的图表调整
|
||||
window.addEventListener('resize', () => {
|
||||
chart && chart.resize();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 搜索功能
|
||||
const searchQuery = ref('');
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
if (searchQuery.value === '') {
|
||||
filteredLabs.value = labs.value;
|
||||
} else {
|
||||
filteredLabs.value = labs.value.filter(lab =>
|
||||
lab.name.includes(searchQuery.value) ||
|
||||
(lab.idcode && lab.idcode.includes(searchQuery.value))
|
||||
);
|
||||
}
|
||||
|
||||
// 在过滤后调用更新雷达图
|
||||
nextTick(() => {
|
||||
updateAllRadarCharts();
|
||||
});
|
||||
};
|
||||
|
||||
// 打开工程研究中心详情抽屉
|
||||
const openLabDetail = (lab) => {
|
||||
selectedLab.value = lab;
|
||||
isEditMode.value = true;
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './common.css';
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.evaluation-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #0c1633;
|
||||
color: white;
|
||||
overflow: hidden; /* 防止页面整体出现滚动条 */
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(73,134,255,0.5) rgba(38,47,80,0.3);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(38,47,80,0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(73,134,255,0.5);
|
||||
border-radius: 4px;
|
||||
border: 2px solid rgba(38,47,80,0.3);
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 28px;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.title-line {
|
||||
border: 2px solid rgba(73,134,255,1);
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
margin: 0 30px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 特定于LabDetail的样式 */
|
||||
.sidebar-header {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.home-link {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.dimension-sidebar {
|
||||
width: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 100px); /* 限制最大高度 */
|
||||
}
|
||||
|
||||
.dimension-content {
|
||||
flex: 1;
|
||||
background-color: #262F50;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* 加上这个防止内容溢出 */
|
||||
}
|
||||
|
||||
.dimension-section-title {
|
||||
margin: 15px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.dimension-list {
|
||||
padding: 0 15px 15px 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
overflow-y: auto; /* 允许列表滚动 */
|
||||
flex: 1; /* 让列表占满剩余空间 */
|
||||
}
|
||||
|
||||
.dimension-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
background-color: rgba(73,134,255,0.1);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #4986ff;
|
||||
cursor: pointer; /* 添加指针样式,提示可点击 */
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.dimension-item:hover {
|
||||
background-color: rgba(73,134,255,0.2);
|
||||
}
|
||||
|
||||
.dimension-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dimension-checkbox input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
accent-color: #4986ff;
|
||||
}
|
||||
|
||||
.dimension-weight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.weight-label {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.weight-value {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dimension-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
background-color: rgba(73,134,255,0.1);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.dimension-add:hover {
|
||||
background-color: rgba(73,134,255,0.2);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 18px;
|
||||
margin-right: 5px;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
color: #4986ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 右侧内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
max-height: calc(100vh - 100px); /* 限制最大高度 */
|
||||
}
|
||||
|
||||
/* 搜索和操作栏 */
|
||||
.action-bar {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 300px;
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 10px 15px;
|
||||
color: white;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.search-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.add-evaluation-btn {
|
||||
background-color: rgba(14,62,167,1);
|
||||
color: rgba(255,255,255,1);
|
||||
border: 1px solid rgba(73,134,255,1);
|
||||
border-radius: 10px;
|
||||
padding: 8px 15px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
font-family: PingFangSC-regular;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 工程研究中心卡片网格 */
|
||||
.lab-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
|
||||
gap: 20px;
|
||||
overflow-y: auto; /* 允许卡片区域滚动 */
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background-color: #262F50;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.lab-card {
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 350px; /* 调整最小高度,确保雷达图有足够空间 */
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.lab-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
background-color: rgba(73,134,255,0.1);
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.lab-id {
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.total-score {
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: rgb(63, 196, 15);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.lab-image {
|
||||
width: 40%;
|
||||
height: 120px;
|
||||
background-color: rgba(73,134,255,0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lab-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.lab-info {
|
||||
width: 60%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(255,255,255,0.7);
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.evaluation-chart {
|
||||
flex: 1; /* 让雷达图占据剩余空间 */
|
||||
min-height: 200px; /* 确保雷达图最小高度 */
|
||||
padding: 10px 15px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.radar-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
/* 二级维度样式 */
|
||||
.primary-dimension {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dimension-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dimension-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dimension-expand {
|
||||
color: #4986ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sub-dimensions {
|
||||
margin-top: 5px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.sub-dimension {
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
border-left: 2px solid rgba(73,134,255,0.6);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.lab-card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dimension-sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 300px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
height: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.lab-image, .lab-info {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
572
src/components/LabDimensionDrawer.vue
Normal file
@ -0,0 +1,572 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="props.visible"
|
||||
title=""
|
||||
direction="rtl"
|
||||
size="600px"
|
||||
:before-close="handleClose"
|
||||
custom-class="dimension-drawer"
|
||||
>
|
||||
<div class="drawer-content">
|
||||
<div class="dimension-section">
|
||||
<h2 class="section-title">工程研究中心评估维度设置</h2>
|
||||
|
||||
<!-- 维度列表 -->
|
||||
<div class="dimensions-list">
|
||||
<div v-for="(dim, index) in dimensions" :key="index" class="dimension-item primary-dimension">
|
||||
<div class="dimension-name">
|
||||
<el-input
|
||||
v-model="dim.name"
|
||||
placeholder="请输入一级维度名称"
|
||||
/>
|
||||
</div>
|
||||
<div class="dimension-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="Plus"
|
||||
circle
|
||||
@click="addSubDimension(index)"
|
||||
class="add-sub-btn"
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="Delete"
|
||||
circle
|
||||
@click="removeDimension(index)"
|
||||
class="delete-btn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 二级维度列表 -->
|
||||
<div class="sub-dimensions-list">
|
||||
<div v-for="(subDim, subIndex) in dim.subDimensions" :key="`${index}-${subIndex}`" class="dimension-item sub-dimension">
|
||||
<div class="dimension-name">
|
||||
<el-input
|
||||
v-model="subDim.name"
|
||||
placeholder="请输入二级维度名称"
|
||||
/>
|
||||
</div>
|
||||
<div class="dimension-weight">
|
||||
<div class="weight-input-group">
|
||||
<el-input-number
|
||||
v-model="subDim.weight"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="weight-unit">%</span>
|
||||
</div>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="Delete"
|
||||
circle
|
||||
@click="removeSubDimension(index, subIndex)"
|
||||
class="delete-btn"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增一级维度表单 -->
|
||||
<div class="new-dimension-form">
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<span class="label">新增一级维度</span>
|
||||
<el-input
|
||||
v-model="newDimension.name"
|
||||
placeholder="请输入一级维度名称"
|
||||
class="dimension-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item action-item">
|
||||
<span class="label"> </span>
|
||||
<div class="action-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="Plus"
|
||||
circle
|
||||
class="add-btn"
|
||||
@click="addDimension"
|
||||
:disabled="!newDimension.name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 权重总计信息 -->
|
||||
<div class="weight-summary">
|
||||
<div class="weight-total">
|
||||
<span>权重维度要求:</span>
|
||||
<span class="weight-value">100%</span>
|
||||
</div>
|
||||
<div class="weight-current" :class="{ 'weight-error': totalWeight !== 100 }">
|
||||
<span>当前权重维度总计:</span>
|
||||
<span class="weight-value">{{ totalWeight }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<button class="drawer-btn cancel-btn" @click="handleClose">取消</button>
|
||||
<button
|
||||
class="drawer-btn confirm-btn"
|
||||
@click="handleSave"
|
||||
:disabled="totalWeight !== 100"
|
||||
>确认</button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import axios from 'axios';
|
||||
import { getApiBaseUrl } from '../config'; // 导入API基础URL函数
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
dimensions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'save']);
|
||||
|
||||
// 用于visible属性的双向绑定
|
||||
const drawerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val)
|
||||
});
|
||||
|
||||
// 所有维度数据
|
||||
const dimensions = ref([]);
|
||||
|
||||
// 初始化维度数据
|
||||
watch(() => props.dimensions, (newDimensions) => {
|
||||
if (newDimensions && newDimensions.length > 0) {
|
||||
// 深拷贝维度数据,避免直接修改props
|
||||
const dimensionsCopy = JSON.parse(JSON.stringify(newDimensions));
|
||||
|
||||
// 确保每个维度都有subDimensions数组
|
||||
dimensionsCopy.forEach(dim => {
|
||||
if (!dim.subDimensions) {
|
||||
dim.subDimensions = [];
|
||||
}
|
||||
});
|
||||
|
||||
dimensions.value = dimensionsCopy;
|
||||
} else {
|
||||
dimensions.value = [];
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 新维度数据
|
||||
const newDimension = reactive({
|
||||
name: '',
|
||||
subDimensions: []
|
||||
});
|
||||
|
||||
// 计算所有二级维度的总权重
|
||||
const totalWeight = computed(() => {
|
||||
let total = 0;
|
||||
dimensions.value.forEach(dim => {
|
||||
dim.subDimensions.forEach(subDim => {
|
||||
total += (subDim.weight || 0);
|
||||
});
|
||||
});
|
||||
return total;
|
||||
});
|
||||
|
||||
// 添加新的一级维度
|
||||
const addDimension = () => {
|
||||
if (!newDimension.name) {
|
||||
ElMessage.warning('请输入维度名称');
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加到维度列表
|
||||
dimensions.value.push({
|
||||
name: newDimension.name,
|
||||
subDimensions: []
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
newDimension.name = '';
|
||||
};
|
||||
|
||||
// 添加二级维度
|
||||
const addSubDimension = (parentIndex) => {
|
||||
// 确保维度对象存在且有subDimensions属性
|
||||
if (!dimensions.value[parentIndex]) {
|
||||
console.error('维度不存在:', parentIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果subDimensions不存在,先初始化为空数组
|
||||
if (!dimensions.value[parentIndex].subDimensions) {
|
||||
dimensions.value[parentIndex].subDimensions = [];
|
||||
}
|
||||
|
||||
// 添加新的二级维度
|
||||
dimensions.value[parentIndex].subDimensions.push({
|
||||
name: '',
|
||||
weight: 5
|
||||
});
|
||||
};
|
||||
|
||||
// 删除一级维度
|
||||
const removeDimension = (index) => {
|
||||
dimensions.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 删除二级维度
|
||||
const removeSubDimension = (parentIndex, subIndex) => {
|
||||
dimensions.value[parentIndex].subDimensions.splice(subIndex, 1);
|
||||
};
|
||||
|
||||
// 处理关闭
|
||||
const handleClose = (done) => {
|
||||
// 如果有 done 回调(来自 before-close),则调用它
|
||||
if (typeof done === 'function') {
|
||||
done();
|
||||
}
|
||||
// 无论如何都要更新状态
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
// 处理保存
|
||||
const handleSave = async () => {
|
||||
|
||||
if (totalWeight.value !== 100) {
|
||||
ElMessage.error('权重总计必须为100%,请调整后再保存');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查每个二级维度是否都有名称
|
||||
let hasEmptyName = false;
|
||||
dimensions.value.forEach(dim => {
|
||||
if (!dim.name) {
|
||||
hasEmptyName = true;
|
||||
}
|
||||
dim.subDimensions.forEach(subDim => {
|
||||
if (!subDim.name) {
|
||||
hasEmptyName = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (hasEmptyName) {
|
||||
ElMessage.error('请为所有维度填写名称');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 从localStorage获取JWT令牌
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
ElMessage.error('未登录或登录已过期,请重新登录');
|
||||
return;
|
||||
}
|
||||
|
||||
// 将二级维度结构保存到后端
|
||||
const response = await axios.post(`${getApiBaseUrl()}/dimensions/save`, {
|
||||
dimensions: dimensions.value,
|
||||
category: 'lab'
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
// 发送保存事件
|
||||
emit('save', dimensions.value);
|
||||
|
||||
// 关闭抽屉
|
||||
handleClose();
|
||||
|
||||
ElMessage.success('维度设置保存成功');
|
||||
} else {
|
||||
throw new Error(response.data?.message || '保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存维度数据失败:', error);
|
||||
ElMessage.error(`保存维度数据失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 确保引入common.css */
|
||||
@import './common.css';
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.dimension-drawer :deep(.el-drawer__header) {
|
||||
margin-bottom: 0;
|
||||
color: white;
|
||||
background-color: #0c1633;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.dimension-drawer :deep(.el-drawer__body) {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #0c1633;
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
padding: 20px;
|
||||
color: white;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #0c1633;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dimension-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 15px 0;
|
||||
font-size: 18px;
|
||||
color: rgba(255,255,255,0.9);
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.dimensions-list {
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
margin-bottom: 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1; /* 让这个元素占满可用空间 */
|
||||
}
|
||||
|
||||
.dimension-item {
|
||||
padding: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.primary-dimension {
|
||||
background-color: rgba(73,134,255,0.1);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
border-left: 3px solid #4986ff;
|
||||
}
|
||||
|
||||
.dimension-name {
|
||||
flex: 1;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dimension-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sub-dimensions-list {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.sub-dimension {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.sub-dimension .dimension-name {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.dimension-weight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 120px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.weight-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.delete-btn, .add-sub-btn {
|
||||
font-size: 12px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.new-dimension-form {
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
flex: 0.3;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.weight-unit {
|
||||
margin: 0 5px;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.weight-summary {
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.weight-total, .weight-current {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.weight-value {
|
||||
font-weight: bold;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.weight-error .weight-value {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 抽屉页脚 */
|
||||
.dimension-drawer :deep(.el-drawer__footer) {
|
||||
border-top: 1px solid rgba(73,134,255,0.3);
|
||||
padding: 10px 20px;
|
||||
background-color: #0c1633;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.drawer-btn {
|
||||
padding: 8px 15px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-regular;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: transparent;
|
||||
color: rgba(255,255,255,0.8);
|
||||
border: 1px solid rgba(73,134,255,0.5);
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background-color: rgba(14,62,167,1);
|
||||
color: rgba(255,255,255,1);
|
||||
border: 1px solid rgba(73,134,255,1);
|
||||
}
|
||||
|
||||
.confirm-btn:disabled {
|
||||
background-color: rgba(14,62,167,0.5);
|
||||
border: 1px solid rgba(73,134,255,0.5);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Element Plus组件的深色主题覆盖样式 */
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-textarea__wrapper) {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
box-shadow: 0 0 0 1px rgba(73,134,255,0.3) inset;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner),
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner::placeholder),
|
||||
:deep(.el-textarea__inner::placeholder) {
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
:deep(.el-input-number) {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
:deep(.el-input-number__decrease),
|
||||
:deep(.el-input-number__increase) {
|
||||
background-color: rgba(73,134,255,0.2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.drawer-content::-webkit-scrollbar,
|
||||
.dimensions-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.drawer-content::-webkit-scrollbar-track,
|
||||
.dimensions-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.drawer-content::-webkit-scrollbar-thumb,
|
||||
.dimensions-list::-webkit-scrollbar-thumb {
|
||||
background-color: #4986ff;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
1494
src/components/LabDrawerDetail.vue
Normal file
315
src/components/ResearchDetail.vue
Normal file
@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<div class="detail-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="dashboard-header">
|
||||
<div class="logo">
|
||||
<img src="../assets/logo1.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
|
||||
<img src="../assets/logo2.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
|
||||
<h1 class="main-title">
|
||||
<div class="title-line"></div>
|
||||
<span class="title-text">智慧科研评估系统</span>
|
||||
<div class="title-line"></div>
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="page-header">
|
||||
<button class="back-button" @click="goBack">
|
||||
<el-icon><back /></el-icon> 返回仪表盘
|
||||
</button>
|
||||
<h1>科研成果详情</h1>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<!-- 筛选条件 -->
|
||||
<div class="filter-panel">
|
||||
<h2>筛选条件</h2>
|
||||
<div class="filter-form">
|
||||
<div class="filter-row">
|
||||
<div class="filter-item">
|
||||
<label>时间范围</label>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>成果类型</label>
|
||||
<el-select v-model="researchType">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="论文" value="paper" />
|
||||
<el-option label="专利" value="patent" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>学院</label>
|
||||
<el-select v-model="college">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="计算机学院" value="cs" />
|
||||
<el-option label="材料学院" value="material" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-buttons">
|
||||
<el-button type="primary">应用筛选</el-button>
|
||||
<el-button>重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-panel">
|
||||
<h2>研究成果列表</h2>
|
||||
<el-table :data="tableData" style="width: 100%" border>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="标题" min-width="250" />
|
||||
<el-table-column prop="author" label="作者" width="150" />
|
||||
<el-table-column prop="type" label="类型" width="100" />
|
||||
<el-table-column prop="journal" label="期刊/来源" width="180" />
|
||||
<el-table-column prop="date" label="发表日期" width="120" />
|
||||
<el-table-column prop="citations" label="引用次数" width="100" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default>
|
||||
<el-button size="small">查看</el-button>
|
||||
<el-button size="small" type="primary">导出</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:total="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 跳转回仪表盘
|
||||
const emit = defineEmits(['back-to-dashboard', 'logout'])
|
||||
const goBack = () => {
|
||||
emit('back-to-dashboard')
|
||||
}
|
||||
|
||||
// 处理Logo点击事件
|
||||
const handleLogoClick = () => {
|
||||
emit('logout')
|
||||
}
|
||||
|
||||
// 筛选条件
|
||||
const dateRange = ref([])
|
||||
const researchType = ref('')
|
||||
const college = ref('')
|
||||
|
||||
// 模拟表格数据
|
||||
const tableData = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '基于深度学习的图像识别算法研究',
|
||||
author: '张三, 李四',
|
||||
type: '论文',
|
||||
journal: 'IEEE Transactions on Image Processing',
|
||||
date: '2024-10-15',
|
||||
citations: 25
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '新型高效电池材料的合成与表征',
|
||||
author: '王五, 赵六',
|
||||
type: '论文',
|
||||
journal: 'Advanced Materials',
|
||||
date: '2024-09-22',
|
||||
citations: 18
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '一种新型智能机器人控制方法',
|
||||
author: '张三, 王五',
|
||||
type: '专利',
|
||||
journal: '国家知识产权局',
|
||||
date: '2024-08-10',
|
||||
citations: 0
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '高性能计算在流体力学中的应用',
|
||||
author: '李四, 赵六',
|
||||
type: '论文',
|
||||
journal: 'Journal of Computational Physics',
|
||||
date: '2024-07-05',
|
||||
citations: 12
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '纳米材料在催化领域的应用研究',
|
||||
author: '王五, 张三',
|
||||
type: '论文',
|
||||
journal: 'ACS Catalysis',
|
||||
date: '2024-06-18',
|
||||
citations: 30
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: #0c1633;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 60px;
|
||||
background-color: #1a2b56;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.title-line {
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: #fff;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background-color: #1a2b56;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #4080ff;
|
||||
cursor: pointer;
|
||||
margin-right: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-panel, .table-panel {
|
||||
background-color: #1a2b56;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-panel h2, .table-panel h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filter-item label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Element Plus 暗色主题定制 */
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-select .el-input__wrapper) {
|
||||
background-color: #2a3b66;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-table th.el-table__cell) {
|
||||
background-color: #2a3b66;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-table tr) {
|
||||
background-color: #1a2b56;
|
||||
}
|
||||
|
||||
:deep(.el-table td.el-table__cell) {
|
||||
color: white;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
border: 1px solid #4080ff;
|
||||
color: #4080ff;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.el-button--primary) {
|
||||
background-color: #4080ff;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
806
src/components/TalentDetail.vue
Normal file
@ -0,0 +1,806 @@
|
||||
<template>
|
||||
<div class="evaluation-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="dashboard-header">
|
||||
<div class="logo">
|
||||
<img src="../assets/logo1.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
|
||||
<img src="../assets/logo2.png" alt="北京理工大学" @click="handleLogoClick" style="cursor: pointer;" />
|
||||
<h1 class="main-title">
|
||||
<div class="title-line"></div>
|
||||
<span class="title-text">智慧科研评估系统</span>
|
||||
<div class="title-line"></div>
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="content-container">
|
||||
<!-- 左侧维度设置 -->
|
||||
<div class="dimension-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1 class="sidebar-title">
|
||||
<span class="home-link" @click="jumpToDashboard">首页</span> > 教师科研人才评估
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="dimension-content">
|
||||
<h2 class="dimension-section-title">评估维度设置</h2>
|
||||
|
||||
<div class="dimension-list">
|
||||
<div v-for="(dim, index) in dimensions" :key="index" class="dimension-item" >
|
||||
<div class="dimension-checkbox">
|
||||
<label :for="`dim-${index}`">{{ dim.name }}</label>
|
||||
</div>
|
||||
<div class="dimension-weight">
|
||||
<span class="weight-label">W:</span>
|
||||
<span class="weight-value">{{ dim.weight }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dimension-add" @click="showAddDimensionDialog">
|
||||
<span class="add-icon">+</span>
|
||||
<span class="add-text">添加自定义维度</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="action-bar">
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入教师姓名或ID"
|
||||
v-model="searchQuery"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
<button class="search-button">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||
<path fill="white" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="add-evaluation-btn" @click="openAddEvaluationDrawer">
|
||||
新增评估
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 教师卡片列表 -->
|
||||
<div class="teacher-card-grid custom-scrollbar">
|
||||
<div v-for="(teacher, index) in filteredTeachers" :key="index" class="teacher-card" style="height: 300px;" @click="openTeacherDetail(teacher)">
|
||||
<div class="card-top">
|
||||
<div class="teacher-left">
|
||||
<div class="teacher-photo">
|
||||
<img :src="teacher.photo" alt="教师照片" />
|
||||
</div>
|
||||
<div class="teacher-id">ID: {{ teacher.idcode || teacher.id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="teacher-info">
|
||||
<div class="info-row">
|
||||
<span class="info-label">姓名:</span>
|
||||
<span class="info-value">{{ teacher.name }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">教育背景:</span>
|
||||
|
||||
<span class="info-value">{{ teacher.education }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">论文:</span>
|
||||
<span class="info-value">{{ teacher.papers }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">项目:</span>
|
||||
<span class="info-value">{{ teacher.projects }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="evaluation-chart">
|
||||
<div :id="`chart-${index}`" class="radar-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义维度对话框 -->
|
||||
<el-dialog v-model="dimensionDialogVisible" :title="isEditingDimension ? '编辑维度' : '添加维度'" width="30%" custom-class="dimension-dialog">
|
||||
<el-form :model="dimensionForm" :rules="dimensionRules" ref="dimensionFormRef" label-position="top">
|
||||
<el-form-item label="维度名称" prop="name">
|
||||
<el-input v-model="dimensionForm.name" placeholder="请输入维度名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="权重(W)" prop="weight">
|
||||
<el-input-number v-model="dimensionForm.weight" :min="1" :max="100" :step="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dimensionDialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" v-if="isEditingDimension" @click="deleteDimension">删除</el-button>
|
||||
<el-button type="primary" @click="saveDimension">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<TalentDrawerDetail
|
||||
v-model:visible="drawerVisible"
|
||||
:is-edit="isEditMode"
|
||||
:dimensions="dimensions"
|
||||
:teacher-data="selectedTeacher"
|
||||
@save="handleSaveEvaluation"
|
||||
/>
|
||||
<DimensionDrawer
|
||||
v-model:visible="dimensionDrawerVisible"
|
||||
:dimensions="dimensions"
|
||||
category="talent"
|
||||
@save="handleSaveDimensions"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { RadarChart } from 'echarts/charts';
|
||||
// 引入新组件
|
||||
import DimensionDrawer from './DimensionDrawer.vue';
|
||||
import TalentDrawerDetail from './TalentDrawerDetail.vue';
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent
|
||||
} from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import axios from 'axios'; // 添加axios导入
|
||||
import { getApiBaseUrl } from '../config'; // 导入API基础URL函数
|
||||
import { ElMessage } from 'element-plus'; // 导入Element Plus的消息组件
|
||||
|
||||
// 注册必要的 echarts 组件
|
||||
echarts.use([
|
||||
RadarChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
CanvasRenderer
|
||||
]);
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
const isEditMode = ref(false);
|
||||
const selectedTeacher = ref(null);
|
||||
|
||||
|
||||
// 新增drawer可见性控制
|
||||
const dimensionDrawerVisible = ref(false);
|
||||
|
||||
// 修改显示添加维度方法
|
||||
const showAddDimensionDialog = () => {
|
||||
// 不再使用dialog
|
||||
// dimensionDialogVisible.value = true;
|
||||
|
||||
// 使用drawer
|
||||
dimensionDrawerVisible.value = true;
|
||||
};
|
||||
|
||||
// 处理保存维度
|
||||
const handleSaveDimensions = (updatedDimensions) => {
|
||||
// 更新维度列表
|
||||
dimensions.value = updatedDimensions;
|
||||
|
||||
// 重新初始化雷达图
|
||||
updateAllRadarCharts();
|
||||
|
||||
dimensionDrawerVisible.value = false;
|
||||
// 添加保存成功提示
|
||||
ElMessage.success('维度设置保存成功');
|
||||
};
|
||||
|
||||
|
||||
// Function to open the drawer for adding a new evaluation
|
||||
const openAddEvaluationDrawer = () => {
|
||||
isEditMode.value = false;
|
||||
selectedTeacher.value = null;
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
// Function to handle save from drawer
|
||||
const handleSaveEvaluation = (data) => {
|
||||
if (data._deleted) {
|
||||
// 处理删除操作
|
||||
const index = teachers.value.findIndex(t => t.id === data.id);
|
||||
if (index !== -1) {
|
||||
teachers.value.splice(index, 1);
|
||||
}
|
||||
} else if (isEditMode.value) {
|
||||
// Update existing teacher data
|
||||
const index = teachers.value.findIndex(t => t.id === data.id);
|
||||
if (index !== -1) {
|
||||
teachers.value[index] = { ...data };
|
||||
}
|
||||
} else {
|
||||
// Add new teacher
|
||||
teachers.value.push({ ...data });
|
||||
}
|
||||
|
||||
// Update filtered teachers
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
// 向父组件发送页面切换事件
|
||||
const emit = defineEmits(['navigate', 'back-to-dashboard', 'logout']);
|
||||
|
||||
// 处理Logo点击事件
|
||||
const handleLogoClick = () => {
|
||||
emit('logout');
|
||||
};
|
||||
|
||||
// Jump to dashboard page function
|
||||
const jumpToDashboard = () => {
|
||||
emit('back-to-dashboard');
|
||||
};
|
||||
|
||||
// 评估维度数据
|
||||
const dimensions = ref([]); // 改为空数组,从API获取数据
|
||||
|
||||
// 在组件挂载时获取维度数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 获取教师评估维度数据
|
||||
const response = await axios.get(`${getApiBaseUrl()}/dimensions/talent`);
|
||||
dimensions.value = response.data;
|
||||
// 加载教师数据
|
||||
await loadTeachers();
|
||||
// 初始化雷达图
|
||||
handleSearch();
|
||||
} catch (error) {
|
||||
console.error('获取维度数据失败:', error);
|
||||
ElMessage.error('获取维度数据失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 加载教师数据
|
||||
const loadTeachers = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${getApiBaseUrl()}/talents`);
|
||||
teachers.value = response.data;
|
||||
|
||||
// 确保每个教师都有照片和教育背景,并重新生成随机评估数据
|
||||
teachers.value.forEach(teacher => {
|
||||
// 确保每个教师都有ID
|
||||
if (!teacher.id) {
|
||||
teacher.id = 'T' + Math.floor(Math.random() * 10000).toString();
|
||||
}
|
||||
|
||||
// 使用默认头像,不再使用随机头像
|
||||
if (!teacher.photo) {
|
||||
teacher.photo = '/image/人1.png';
|
||||
}
|
||||
|
||||
// 如果没有教育背景,生成模拟数据
|
||||
if (!teacher.educationBackground) {
|
||||
const eduOptions = [
|
||||
"清华大学 博士",
|
||||
"北京大学 博士",
|
||||
"北京理工大学 博士",
|
||||
"上海交通大学 博士",
|
||||
"中国科学院 博士",
|
||||
"哈尔滨工业大学 博士",
|
||||
"华中科技大学 博士",
|
||||
"武汉大学 博士"
|
||||
];
|
||||
teacher.educationBackground = eduOptions[Math.floor(Math.random() * eduOptions.length)];
|
||||
}
|
||||
|
||||
// 每次加载都重新生成随机评估数据
|
||||
const baseValue = teacher.id ? parseInt(teacher.id.slice(-2)) : Math.floor(Math.random() * 30);
|
||||
|
||||
// 确保dimensions.value已加载
|
||||
if (dimensions.value.length > 0) {
|
||||
teacher.evaluationData = dimensions.value.map((_, dimIndex) => {
|
||||
const offset = (dimIndex * 7 + baseValue) % 35;
|
||||
return Math.min(95, Math.max(20, Math.floor(Math.random() * 40) + 30 + offset));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
handleSearch();
|
||||
} catch (error) {
|
||||
console.error('获取教师数据失败:', error);
|
||||
ElMessage.error('获取教师数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 维度编辑相关
|
||||
const dimensionDialogVisible = ref(false);
|
||||
const isEditingDimension = ref(false);
|
||||
const currentDimensionIndex = ref(-1);
|
||||
const dimensionForm = ref({
|
||||
name: '',
|
||||
weight: 10
|
||||
});
|
||||
const dimensionFormRef = ref(null);
|
||||
const dimensionRules = {
|
||||
name: [{ required: true, message: '请输入维度名称', trigger: 'blur' }],
|
||||
weight: [{ required: true, message: '请输入权重', trigger: 'blur' }]
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 编辑维度
|
||||
const editDimension = (dim, index) => {
|
||||
isEditingDimension.value = true;
|
||||
dimensionForm.value = { ...dim };
|
||||
currentDimensionIndex.value = index;
|
||||
dimensionDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 保存维度
|
||||
const saveDimension = async () => {
|
||||
if (!dimensionFormRef.value) return;
|
||||
|
||||
await dimensionFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
if (isEditingDimension.value && currentDimensionIndex.value >= 0) {
|
||||
// 更新现有维度
|
||||
dimensions.value[currentDimensionIndex.value] = { ...dimensionForm.value, enabled: true };
|
||||
} else {
|
||||
// 添加新维度
|
||||
dimensions.value.push({ ...dimensionForm.value, enabled: true });
|
||||
}
|
||||
dimensionDialogVisible.value = false;
|
||||
|
||||
// 重新初始化雷达图
|
||||
updateAllRadarCharts();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 删除维度
|
||||
const deleteDimension = () => {
|
||||
if (currentDimensionIndex.value >= 0) {
|
||||
dimensions.value.splice(currentDimensionIndex.value, 1);
|
||||
dimensionDialogVisible.value = false;
|
||||
|
||||
// 重新初始化雷达图
|
||||
updateAllRadarCharts();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新所有雷达图
|
||||
const updateAllRadarCharts = () => {
|
||||
// 在下一个 tick 后更新图表,确保 DOM 已更新
|
||||
nextTick(() => {
|
||||
initRadarCharts();
|
||||
});
|
||||
};
|
||||
|
||||
// 搜索功能
|
||||
const searchQuery = ref('');
|
||||
|
||||
// 教师数据
|
||||
const teachers = ref([]);
|
||||
|
||||
// 根据搜索过滤教师
|
||||
const filteredTeachers = ref([]);
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
if (searchQuery.value === '') {
|
||||
filteredTeachers.value = teachers.value;
|
||||
} else {
|
||||
filteredTeachers.value = teachers.value.filter(teacher =>
|
||||
teacher.name.includes(searchQuery.value) ||
|
||||
teacher.id.includes(searchQuery.value)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化雷达图
|
||||
const initRadarCharts = () => {
|
||||
filteredTeachers.value.forEach((teacher, index) => {
|
||||
const chartDom = document.getElementById(`chart-${index}`);
|
||||
if (!chartDom) return;
|
||||
|
||||
// 先清空已有的图表实例
|
||||
echarts.dispose(chartDom);
|
||||
const chart = echarts.init(chartDom);
|
||||
|
||||
// 生成雷达图所需的指标
|
||||
const indicators = dimensions.value.map(dim => ({
|
||||
name: dim.name,
|
||||
max: 100
|
||||
}));
|
||||
|
||||
// 为每个教师生成一个基准值,用于增加各教师数据的差异性
|
||||
const baseValue = teacher.id ? parseInt(teacher.id.slice(-2)) : Math.floor(Math.random() * 30);
|
||||
|
||||
// 确保评估数据的长度与维度数量匹配
|
||||
let evaluationData = teacher.evaluationData || [];
|
||||
if (evaluationData.length !== dimensions.value.length) {
|
||||
// 生成更加随机和独特的评估数据
|
||||
evaluationData = dimensions.value.map((_, dimIndex) => {
|
||||
// 使用教师ID、维度索引和基准值来生成差异更大的随机数
|
||||
const offset = (dimIndex * 7 + baseValue) % 35;
|
||||
// 生成20-95之间的随机数,不同教师、不同维度有不同的随机范围
|
||||
return Math.min(95, Math.max(20, Math.floor(Math.random() * 40) + 30 + offset));
|
||||
});
|
||||
// 更新教师的评估数据
|
||||
teacher.evaluationData = evaluationData;
|
||||
}
|
||||
|
||||
chart.setOption({
|
||||
radar: {
|
||||
indicator: indicators,
|
||||
splitArea: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
|
||||
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
|
||||
name: { textStyle: { color: '#fff', fontSize: 10 } },
|
||||
radius: '70%'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: [
|
||||
{
|
||||
value: evaluationData,
|
||||
name: '评估结果',
|
||||
areaStyle: { opacity: 0 }, // 移除半透明的底色
|
||||
lineStyle: { color: 'rgb(63, 196, 15)', width: 2 }, // 设置线段颜色为RGB(63, 196, 15)
|
||||
itemStyle: { color: 'rgb(63, 196, 15)' } // 设置点的颜色
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 添加窗口大小变化时的调整
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 监听过滤后的教师数据变化,重新初始化图表
|
||||
watch(filteredTeachers, () => {
|
||||
// 使用 nextTick 确保 DOM 已更新
|
||||
nextTick(() => {
|
||||
initRadarCharts();
|
||||
});
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时显示所有教师
|
||||
filteredTeachers.value = teachers.value;
|
||||
// 需要延迟一下,确保 DOM 已经渲染
|
||||
nextTick(() => {
|
||||
initRadarCharts();
|
||||
});
|
||||
});
|
||||
|
||||
// 打开教师详情抽屉
|
||||
const openTeacherDetail = (teacher) => {
|
||||
selectedTeacher.value = teacher;
|
||||
isEditMode.value = true;
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './common.css';
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.evaluation-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #0c1633;
|
||||
color: white;
|
||||
overflow: hidden; /* 防止页面整体出现滚动条 */
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 28px;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.title-line {
|
||||
border: 2px solid rgba(73,134,255,1);
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
margin: 0 30px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 特定于TalentDetail的样式 */
|
||||
.sidebar-header {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.home-link {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.dimension-sidebar {
|
||||
width: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 100px); /* 限制最大高度 */
|
||||
}
|
||||
|
||||
.dimension-content {
|
||||
flex: 1;
|
||||
background-color: #262F50;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* 加上这个防止内容溢出 */
|
||||
}
|
||||
|
||||
.dimension-section-title {
|
||||
margin: 15px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.dimension-list {
|
||||
padding: 0 15px 15px 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
overflow-y: auto; /* 允许列表滚动 */
|
||||
flex: 1; /* 让列表占满剩余空间 */
|
||||
}
|
||||
|
||||
.dimension-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
background-color: rgba(73,134,255,0.1);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #4986ff;
|
||||
cursor: pointer; /* 添加指针样式,提示可点击 */
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.dimension-item:hover {
|
||||
background-color: rgba(73,134,255,0.2);
|
||||
}
|
||||
|
||||
.dimension-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dimension-checkbox input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
accent-color: #4986ff;
|
||||
}
|
||||
|
||||
.dimension-weight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.weight-label {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.weight-value {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dimension-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
background-color: rgba(73,134,255,0.1);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.dimension-add:hover {
|
||||
background-color: rgba(73,134,255,0.2);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 18px;
|
||||
margin-right: 5px;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
color: #4986ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 右侧内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
max-height: calc(100vh - 100px); /* 限制最大高度 */
|
||||
}
|
||||
|
||||
/* 搜索和操作栏 */
|
||||
.action-bar {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 300px;
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 10px 15px;
|
||||
color: white;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.search-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.add-evaluation-btn {
|
||||
background-color: rgba(14,62,167,1);
|
||||
color: rgba(255,255,255,1);
|
||||
border: 1px solid rgba(73,134,255,1);
|
||||
border-radius: 10px;
|
||||
padding: 8px 15px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
font-family: PingFangSC-regular;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 教师卡片网格 */
|
||||
.teacher-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
overflow-y: auto; /* 允许卡片区域滚动 */
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background-color: #262F50;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* 维度对话框样式 */
|
||||
:deep(.dimension-dialog) {
|
||||
background-color: #1f3266;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
:deep(.dimension-dialog .el-dialog__header) {
|
||||
color: white;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
:deep(.dimension-dialog .el-dialog__body) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.dimension-dialog .el-input__inner),
|
||||
:deep(.dimension-dialog .el-input-number__decrease),
|
||||
:deep(.dimension-dialog .el-input-number__increase) {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
border-color: rgba(73,134,255,0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.dimension-dialog .el-form-item__label) {
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
:deep(.dimension-dialog .el-dialog__footer) {
|
||||
border-top: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.dimension-sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 300px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
height: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
1036
src/components/TalentDrawerDetail.vue
Normal file
197
src/components/TokenTest.vue
Normal file
@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div class="token-test">
|
||||
<h2>Token 测试工具</h2>
|
||||
<div class="token-info">
|
||||
<p>当前token信息:</p>
|
||||
<pre>{{ tokenInfo }}</pre>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<el-button type="primary" @click="testToken">测试Token</el-button>
|
||||
<el-button type="info" @click="refreshToken">重新登录刷新Token</el-button>
|
||||
<el-button type="warning" @click="checkUserInfo">获取用户信息</el-button>
|
||||
</div>
|
||||
<div v-if="apiTestResult" class="api-result">
|
||||
<h3>API测试结果</h3>
|
||||
<pre>{{ apiTestResult }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getApiBaseUrl } from '../config';
|
||||
|
||||
const tokenInfo = ref('未加载');
|
||||
const apiTestResult = ref('');
|
||||
|
||||
onMounted(() => {
|
||||
loadTokenInfo();
|
||||
});
|
||||
|
||||
const loadTokenInfo = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
const username = localStorage.getItem('username');
|
||||
const isLoggedIn = localStorage.getItem('isLoggedIn');
|
||||
|
||||
if (token) {
|
||||
const tokenParts = token.split('.');
|
||||
let payload = {};
|
||||
try {
|
||||
if (tokenParts.length >= 2) {
|
||||
payload = JSON.parse(atob(tokenParts[1]));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析token失败', e);
|
||||
}
|
||||
|
||||
tokenInfo.value = {
|
||||
token: token.substring(0, 15) + '...',
|
||||
username,
|
||||
isLoggedIn,
|
||||
expires: payload.exp ? new Date(payload.exp * 1000).toLocaleString() : '未知',
|
||||
payload
|
||||
};
|
||||
} else {
|
||||
tokenInfo.value = '未找到token';
|
||||
}
|
||||
};
|
||||
|
||||
const testToken = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
ElMessage.warning('未找到token,请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建一个测试API请求
|
||||
const response = await axios.get(`${getApiBaseUrl()}/health`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
apiTestResult.value = {
|
||||
status: response.status,
|
||||
data: response.data
|
||||
};
|
||||
|
||||
ElMessage.success('Token有效,API测试成功');
|
||||
} catch (error) {
|
||||
console.error('API测试失败', error);
|
||||
apiTestResult.value = {
|
||||
error: true,
|
||||
status: error.response?.status,
|
||||
message: error.response?.data?.detail || error.message
|
||||
};
|
||||
|
||||
ElMessage.error(`API测试失败: ${error.response?.data?.detail || error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const checkUserInfo = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
ElMessage.warning('未找到token,请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 测试用户信息API
|
||||
const response = await axios.get(`${getApiBaseUrl()}/users/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
apiTestResult.value = {
|
||||
status: response.status,
|
||||
data: response.data
|
||||
};
|
||||
|
||||
ElMessage.success('获取用户信息成功');
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败', error);
|
||||
apiTestResult.value = {
|
||||
error: true,
|
||||
status: error.response?.status,
|
||||
message: error.response?.data?.detail || error.message
|
||||
};
|
||||
|
||||
ElMessage.error(`获取用户信息失败: ${error.response?.data?.detail || error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshToken = async () => {
|
||||
try {
|
||||
// 使用默认账户重新获取token
|
||||
const response = await axios.post(`${getApiBaseUrl()}/token`,
|
||||
new URLSearchParams({
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 保存新的token
|
||||
localStorage.setItem('token', response.data.access_token);
|
||||
localStorage.setItem('username', 'admin');
|
||||
localStorage.setItem('isLoggedIn', 'true');
|
||||
|
||||
loadTokenInfo();
|
||||
ElMessage.success('Token已刷新');
|
||||
} catch (error) {
|
||||
console.error('刷新Token失败', error);
|
||||
ElMessage.error(`刷新Token失败: ${error.response?.data?.detail || error.message}`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.token-test {
|
||||
padding: 20px;
|
||||
background-color: #0c1633;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.token-info {
|
||||
background-color: #1f3266;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background-color: #121c38;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.api-result {
|
||||
background-color: #1f3266;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-top: 20px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
</style>
|
303
src/components/common.css
Normal file
@ -0,0 +1,303 @@
|
||||
/* ========================================
|
||||
共享样式文件 (common.css)
|
||||
为智慧科研评估系统提供的公共样式
|
||||
包含:基础色彩主题、布局结构、组件样式和响应式设计
|
||||
======================================== */
|
||||
|
||||
/* ========= 基础色彩变量 ========= */
|
||||
:root {
|
||||
/* 背景色系 */
|
||||
--primary-bg: #0c1633; /* 主背景色 - 深蓝 */
|
||||
--secondary-bg: #1a2b56; /* 次级背景色 - 面板背景 */
|
||||
--tertiary-bg: #262F50; /* 三级背景色 - 卡片背景 */
|
||||
--card-bg: #1f3266; /* 卡片背景色 */
|
||||
|
||||
/* 边框色系 */
|
||||
--primary-border: rgba(73,134,255,1); /* 主边框色 - 蓝色 */
|
||||
--secondary-border: rgba(73,134,255,0.3); /* 次级边框色 - 半透明蓝 */
|
||||
|
||||
/* 强调色系 */
|
||||
--accent-color: #4080ff; /* 主强调色 - 蓝色 */
|
||||
--accent-color-alt: #4986ff; /* 次级强调色 */
|
||||
--accent-color-success: rgb(63, 196, 15); /* 成功色 - 绿色 */
|
||||
|
||||
/* 文本色系 */
|
||||
--text-color: white; /* 主文本色 - 白色 */
|
||||
--text-color-dim: rgba(255,255,255,0.7); /* 次级文本色 - 半透明白 */
|
||||
--text-color-dimmer: rgba(255,255,255,0.5); /* 更弱的文本色 */
|
||||
|
||||
/* 按钮相关 */
|
||||
--button-primary-bg: rgba(14,62,167,1); /* 主按钮背景色 */
|
||||
--button-border: rgba(73,134,255,1); /* 按钮边框色 */
|
||||
--button-border-light: rgba(73,134,255,0.5); /* 次要按钮边框色 */
|
||||
}
|
||||
|
||||
/* ========= 基础布局和页面结构 ========= */
|
||||
/* 主容器样式 */
|
||||
.dashboard, .evaluation-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: var(--primary-bg);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ========= 头部导航栏样式 ========= */
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
/* Logo区域样式 */
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.main-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 32px;
|
||||
font-family: PingFangSC-bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.title-line {
|
||||
border: 2px solid var(--primary-border);
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
margin: 0 50px;
|
||||
}
|
||||
|
||||
/* ========= 内容区域布局 ========= */
|
||||
.dashboard-content, .content-container {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* ========= 面板和卡片样式 ========= */
|
||||
.dashboard-panel {
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dashboard-panel h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.panel-link {
|
||||
color: var(--accent-color);
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ========= 教师卡片和信息样式 ========= */
|
||||
.teacher-card {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--secondary-border);
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid var(--secondary-border);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.teacher-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.teacher-photo {
|
||||
width: 100px;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 5px;
|
||||
background-color: rgba(73,134,255,0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.teacher-photo img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.teacher-id {
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
.teacher-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 80px;
|
||||
color: var(--text-color-dim);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ========= 按钮样式 ========= */
|
||||
.add-evaluation-btn, .drawer-btn {
|
||||
padding: 8px 15px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
font-family: PingFangSC-regular;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-evaluation-btn, .confirm-btn {
|
||||
background-color: var(--button-primary-bg);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--button-border);
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: transparent;
|
||||
color: rgba(255,255,255,0.8);
|
||||
border: 1px solid var(--button-border-light);
|
||||
}
|
||||
|
||||
/* ========= 图表容器样式 ========= */
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.radar-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.evaluation-chart {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
/* ========= 自定义滚动条 ========= */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--accent-color-alt) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar,
|
||||
.drawer-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track,
|
||||
.drawer-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb,
|
||||
.drawer-content::-webkit-scrollbar-thumb {
|
||||
background-color: var(--accent-color-alt);
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ========= 响应式布局 ========= */
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard-content, .content-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.title-line {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.teacher-card-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.teacher-card {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
171
src/components/login.vue
Normal file
@ -0,0 +1,171 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { User, Lock } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getApiBaseUrl } from '../config';
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const emit = defineEmits(['login-success']);
|
||||
const loading = ref(false);
|
||||
|
||||
const login = async () => {
|
||||
if (!username.value || !password.value) {
|
||||
ElMessage.warning('请输入用户名和密码');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// 调用后端登录API
|
||||
const response = await fetch(`${getApiBaseUrl()}/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// 保存token到localStorage
|
||||
localStorage.setItem('token', data.access_token);
|
||||
// 保存用户名到localStorage,用于显示用户信息
|
||||
localStorage.setItem('username', username.value);
|
||||
// 保存登录状态
|
||||
localStorage.setItem('isLoggedIn', 'true');
|
||||
// 登录成功
|
||||
ElMessage.success('登录成功');
|
||||
emit('login-success');
|
||||
} else {
|
||||
// 登录失败
|
||||
ElMessage.error(data.detail || '用户名或密码错误');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录请求出错:', error);
|
||||
ElMessage.error('登录请求失败,请稍后再试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-left">
|
||||
<img src="../assets/loginBackground.png" alt="Background" class="login-background" />
|
||||
</div>
|
||||
<div class="login-right">
|
||||
<div class="login-header">
|
||||
<img src="../assets/logo1.png" alt="北京理工大学" class="bit-logo" />
|
||||
<h1 class="system-title">智慧科研评估系统</h1>
|
||||
</div>
|
||||
<div class="login-form">
|
||||
<div class="form-item">
|
||||
<el-input
|
||||
v-model="username"
|
||||
placeholder="用户名"
|
||||
prefix-icon="User"
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<el-input
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
prefix-icon="Lock"
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="login"
|
||||
class="login-button"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
>登录</el-button>
|
||||
<div class="forgot-password">
|
||||
忘记密码请联系系统管理员
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.login-left {
|
||||
flex: 1;
|
||||
background: linear-gradient(to bottom right, #e6f7ff, #f0f9ff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.login-right {
|
||||
width: 500px;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.bit-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.system-title {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
484
src/components/别动/LabDetail.vue
Normal file
@ -0,0 +1,484 @@
|
||||
<template>
|
||||
<div class="evaluation-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="dashboard-header">
|
||||
<div class="logo">
|
||||
<img src="../assets/logo1.png" alt="北京理工大学" @click="jumpToDashboard" style="cursor: pointer;" />
|
||||
<img src="../assets/logo2.png" alt="北京理工大学" @click="jumpToDashboard" style="cursor: pointer;" />
|
||||
<h1 class="main-title">
|
||||
<div class="title-line"></div>
|
||||
<span class="title-text">智慧科研评估系统</span>
|
||||
<div class="title-line"></div>
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="content-container">
|
||||
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="action-bar" style="margin-left: 170px;">
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入工程研究中心名称或ID号"
|
||||
v-model="searchQuery"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
<button class="search-button">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||
<path fill="white" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="add-evaluation-btn" @click="openAddEvaluationDrawer" style="margin-right: 170px;">
|
||||
新增评估
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 工程研究中心卡片列表 -->
|
||||
<div class="lab-card-grid custom-scrollbar">
|
||||
<img src="../assets/实验室贴图.png" alt="工程研究中心列表" class="lab-list-image" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LabDrawerDetail
|
||||
v-model:visible="drawerVisible"
|
||||
:is-edit="isEditMode"
|
||||
:dimensions="dimensions"
|
||||
:lab-data="selectedLab"
|
||||
@save="handleSaveEvaluation"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { RadarChart } from 'echarts/charts';
|
||||
import LabDrawerDetail from './LabDrawerDetail.vue';
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent
|
||||
} from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { getApiBaseUrl } from '../../config';
|
||||
|
||||
// 注册必要的 echarts 组件
|
||||
echarts.use([
|
||||
RadarChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
CanvasRenderer
|
||||
]);
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
const isEditMode = ref(false);
|
||||
const selectedLab = ref(null);
|
||||
|
||||
// 打开添加评估抽屉
|
||||
const openAddEvaluationDrawer = () => {
|
||||
isEditMode.value = false;
|
||||
selectedLab.value = null;
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
// 处理保存评估
|
||||
const handleSaveEvaluation = (data) => {
|
||||
if (isEditMode.value) {
|
||||
// 更新现有工程研究中心数据
|
||||
const index = labs.value.findIndex(l => l.id === data.id);
|
||||
if (index !== -1) {
|
||||
labs.value[index] = { ...data };
|
||||
}
|
||||
} else {
|
||||
// 添加新工程研究中心
|
||||
labs.value.push({ ...data });
|
||||
}
|
||||
|
||||
// 更新过滤的工程研究中心
|
||||
handleSearch();
|
||||
};
|
||||
// 向父组件发送页面切换事件
|
||||
const emit = defineEmits(['navigate']);
|
||||
|
||||
// 跳转到仪表盘页面
|
||||
const jumpToDashboard = () => {
|
||||
console.log('正在尝试跳转到仪表盘...');
|
||||
// 检查是否正确定义了emit
|
||||
if (typeof emit !== 'function') {
|
||||
console.error('emit 不是一个函数,无法触发事件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保事件名称正确
|
||||
emit('navigate', 'dashboard');
|
||||
|
||||
// 添加调试信息
|
||||
console.log('已触发 navigate 事件,参数为:', 'dashboard');
|
||||
|
||||
// 注意:确保父组件中有监听此事件的处理程序
|
||||
// 例如:<LabDetail @navigate="handleNavigate" />
|
||||
};
|
||||
|
||||
// 评估维度数据
|
||||
const dimensions = ref([
|
||||
{ name: '工程技术研发能力与水平 (30%)', weight: 30, enabled: true },
|
||||
{ name: '创新水平', weight: 10, enabled: true },
|
||||
{ name: '人才与队伍', weight: 10, enabled: true },
|
||||
{ name: '装备与场地', weight: 10, enabled: true },
|
||||
{ name: '成果转化与行业贡献 (30%)', weight: 30, enabled: true },
|
||||
{ name: '学科发展与人才培养 (20%)', weight: 20, enabled: true },
|
||||
{ name: '开放与运行管理 (20%)', weight: 20, enabled: true }
|
||||
]);
|
||||
|
||||
// 搜索功能
|
||||
const searchQuery = ref('');
|
||||
|
||||
// 获取token函数
|
||||
const getToken = () => {
|
||||
return localStorage.getItem('token');
|
||||
};
|
||||
|
||||
// 工程研究中心数据现在从API获取
|
||||
const labs = ref([]);
|
||||
const filteredLabs = ref([]);
|
||||
const loading = ref(true);
|
||||
|
||||
// 从API获取工程研究中心数据
|
||||
const fetchLabs = async () => {
|
||||
try {
|
||||
const response = await fetch(`${getApiBaseUrl()}/labs`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${getToken()}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
labs.value = data;
|
||||
handleSearch(); // 初始化filteredLabs
|
||||
} else {
|
||||
console.error('获取工程研究中心数据失败:', response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取工程研究中心数据出错:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期钩子 - 组件挂载后获取数据
|
||||
onMounted(() => {
|
||||
fetchLabs();
|
||||
});
|
||||
|
||||
// 搜索功能实现
|
||||
const handleSearch = () => {
|
||||
if (!searchQuery.value.trim()) {
|
||||
filteredLabs.value = [...labs.value];
|
||||
return;
|
||||
}
|
||||
|
||||
const query = searchQuery.value.toLowerCase().trim();
|
||||
filteredLabs.value = labs.value.filter(lab =>
|
||||
lab.name.toLowerCase().includes(query) ||
|
||||
lab.id.toLowerCase().includes(query)
|
||||
);
|
||||
};
|
||||
|
||||
// 生成随机的工程研究中心图片占位符
|
||||
function generateLabImagePlaceholder() {
|
||||
// 这里生成不同类型的工程研究中心图片占位符
|
||||
const imageTypes = [
|
||||
// 建筑类型图片
|
||||
`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%234986ff' opacity='0.2'/%3E%3Cpath d='M50,180 L50,80 L150,30 L250,80 L250,180 Z' fill='%234986ff' opacity='0.4'/%3E%3Crect x='90' y='120' width='40' height='60' fill='%234986ff' opacity='0.6'/%3E%3Crect x='170' y='120' width='40' height='60' fill='%234986ff' opacity='0.6'/%3E%3C/svg%3E`,
|
||||
// 工程研究中心内部类型图片
|
||||
`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%234986ff' opacity='0.2'/%3E%3Crect x='20' y='40' width='260' height='120' fill='%234986ff' opacity='0.3'/%3E%3Ccircle cx='70' cy='70' r='20' fill='%234986ff' opacity='0.5'/%3E%3Crect x='120' y='50' width='140' height='40' fill='%234986ff' opacity='0.4'/%3E%3Crect x='120' y='110' width='140' height='30' fill='%234986ff' opacity='0.4'/%3E%3C/svg%3E`,
|
||||
// 商业建筑类型图片
|
||||
`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%234986ff' opacity='0.2'/%3E%3Cpath d='M50,30 L250,30 L250,170 L50,170 Z' fill='%234986ff' opacity='0.3'/%3E%3Cpath d='M80,50 L220,50 L220,150 L80,150 Z' fill='%23fff' opacity='0.2'/%3E%3Cpath d='M90,60 L110,60 L110,80 L90,80 Z' fill='%234986ff' opacity='0.5'/%3E%3Cpath d='M130,60 L150,60 L150,80 L130,80 Z' fill='%234986ff' opacity='0.5'/%3E%3Cpath d='M170,60 L190,60 L190,80 L170,80 Z' fill='%234986ff' opacity='0.5'/%3E%3Cpath d='M90,100 L110,100 L110,120 L90,120 Z' fill='%234986ff' opacity='0.5'/%3E%3Cpath d='M130,100 L150,100 L150,120 L130,120 Z' fill='%234986ff' opacity='0.5'/%3E%3Cpath d='M170,100 L190,100 L190,120 L170,120 Z' fill='%234986ff' opacity='0.5'/%3E%3C/svg%3E`,
|
||||
// 高科技玻璃建筑风格
|
||||
`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%234986ff' opacity='0.1'/%3E%3Cpath d='M60,20 L240,20 L240,180 L60,180 Z' fill='%234986ff' opacity='0.2'/%3E%3Cpath d='M80,40 L140,40 L140,160 L80,160 Z' fill='%234986ff' opacity='0.3'/%3E%3Cpath d='M160,40 L220,40 L220,160 L160,160 Z' fill='%234986ff' opacity='0.3'/%3E%3Cline x1='80' y1='60' x2='140' y2='60' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='80' y1='80' x2='140' y2='80' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='80' y1='100' x2='140' y2='100' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='80' y1='120' x2='140' y2='120' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='80' y1='140' x2='140' y2='140' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='160' y1='60' x2='220' y2='60' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='160' y1='80' x2='220' y2='80' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='160' y1='100' x2='220' y2='100' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='160' y1='120' x2='220' y2='120' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3Cline x1='160' y1='140' x2='220' y2='140' stroke='%23fff' stroke-width='1' opacity='0.5' /%3E%3C/svg%3E`
|
||||
];
|
||||
|
||||
// 随机选择一种图片类型
|
||||
return imageTypes[Math.floor(Math.random() * imageTypes.length)];
|
||||
}
|
||||
|
||||
// 初始化雷达图
|
||||
const initRadarCharts = () => {
|
||||
filteredLabs.value.forEach((lab, index) => {
|
||||
const chartDom = document.getElementById(`lab-chart-${index}`);
|
||||
if (!chartDom) return;
|
||||
|
||||
// 先清空已有的图表实例
|
||||
echarts.dispose(chartDom);
|
||||
const chart = echarts.init(chartDom);
|
||||
|
||||
// 生成雷达图所需的指标
|
||||
const indicators = [
|
||||
{ name: '创新水平', max: 100 },
|
||||
{ name: '研究能力', max: 100 },
|
||||
{ name: '成果转化', max: 100 },
|
||||
{ name: '学科建设', max: 100 },
|
||||
{ name: '行业贡献', max: 100 },
|
||||
{ name: '发展潜力', max: 100 }
|
||||
];
|
||||
|
||||
chart.setOption({
|
||||
radar: {
|
||||
indicator: indicators,
|
||||
splitArea: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
|
||||
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
|
||||
name: { textStyle: { color: '#fff', fontSize: 10 } },
|
||||
radius: '70%'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: [
|
||||
{
|
||||
value: lab.evaluationData,
|
||||
name: '评估结果',
|
||||
areaStyle: { opacity: 0 },
|
||||
lineStyle: { color: 'rgb(238, 23, 143)', width: 2 }, // 玫红色线段
|
||||
itemStyle: { color: 'rgb(238, 23, 143)' } // 玫红色数据点
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 添加窗口大小变化时的调整
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 监听过滤后的工程研究中心数据变化,重新初始化图表
|
||||
watch(filteredLabs, () => {
|
||||
// 使用 setTimeout 确保 DOM 已更新
|
||||
setTimeout(() => {
|
||||
initRadarCharts();
|
||||
}, 100);
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时显示所有工程研究中心
|
||||
filteredLabs.value = labs.value;
|
||||
// 需要延迟一下,确保 DOM 已经渲染
|
||||
setTimeout(() => {
|
||||
initRadarCharts();
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './common.css';
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.evaluation-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* background-color: #0c1633; */
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 28px;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.title-line {
|
||||
border: 2px solid rgba(73,134,255,1);
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
margin: 0 30px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dimension-image, .lab-list-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 特定于LabDetail的样式 */
|
||||
.sidebar-header {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.home-link {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.dimension-sidebar {
|
||||
width: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dimension-content {
|
||||
flex: 1;
|
||||
/* background-color: #262F50; */
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 右侧内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 搜索和操作栏 */
|
||||
.action-bar {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 300px;
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 10px 15px;
|
||||
color: white;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.search-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.add-evaluation-btn {
|
||||
/* background-color: rgba(14,62,167,1); */
|
||||
color: rgba(255,255,255,1);
|
||||
border: 1px solid rgba(73,134,255,1);
|
||||
border-radius: 10px;
|
||||
padding: 8px 15px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
font-family: PingFangSC-regular;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 工程研究中心卡片网格 */
|
||||
.lab-card-grid {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
/* background-color: #262F50; */
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.dimension-sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 300px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
height: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
707
src/components/别动/LabDrawerDetail.vue
Normal file
@ -0,0 +1,707 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
:title="drawerTitle"
|
||||
direction="rtl"
|
||||
size="900px"
|
||||
:before-close="handleClose"
|
||||
custom-class="lab-drawer"
|
||||
>
|
||||
<div class="drawer-content">
|
||||
<!-- 标签导航 -->
|
||||
<div class="tab-navigation">
|
||||
<div
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
:class="['tab-item', { active: activeTab === tab.value }]"
|
||||
@click="activeTab = tab.value"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- URL输入标签页 -->
|
||||
<div v-if="activeTab === 'url'" class="tab-content">
|
||||
<div class="url-input-container">
|
||||
<el-input
|
||||
v-model="urlInput"
|
||||
placeholder="请输入URL"
|
||||
class="url-input"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="fetchDataFromUrl"
|
||||
class="send-button"
|
||||
>
|
||||
发送
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="detail-content-image">
|
||||
<img src="../assets/实验室详情贴图.png" alt="工程研究中心详情" class="lab-detail-image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传文档标签页 -->
|
||||
<div v-else-if="activeTab === 'upload'" class="tab-content">
|
||||
<el-upload
|
||||
class="upload-container"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<el-button type="primary">选择文件</el-button>
|
||||
<div class="el-upload__tip">请上传文档,支持PDF、DOC、DOCX格式</div>
|
||||
</el-upload>
|
||||
|
||||
<div class="detail-content-image">
|
||||
<img src="../assets/实验室详情贴图.png" alt="工程研究中心详情" class="lab-detail-image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手动输入标签页 -->
|
||||
<div v-else class="tab-content">
|
||||
<div class="manual-input-tip">请手动填写以下信息</div>
|
||||
|
||||
<div class="detail-content-image">
|
||||
<img src="../assets/实验室详情贴图.png" alt="工程研究中心详情" class="lab-detail-image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工程研究中心信息表单 - 仅在数据加载后显示 -->
|
||||
<div v-if="dataLoaded" class="lab-info-form">
|
||||
<div class="form-header">
|
||||
<div class="image-section">
|
||||
<div class="lab-image">
|
||||
<img :src="formData.image || defaultImage" alt="工程研究中心照片" />
|
||||
</div>
|
||||
<div class="lab-id">ID: {{ formData.id || 'BLG00000' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="basic-info">
|
||||
<div class="form-row">
|
||||
<div class="form-item full-width">
|
||||
<span class="label">工程研究中心名称:</span>
|
||||
<el-input v-model="formData.name" placeholder="请输入工程研究中心名称" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<span class="label">人员数量:</span>
|
||||
<el-input v-model="formData.personnel" placeholder="请输入人员数量" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="label">国家级项目:</span>
|
||||
<el-input v-model="formData.nationalProjects" placeholder="请输入国家级项目数" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="label">其它项目:</span>
|
||||
<el-input v-model="formData.otherProjects" placeholder="请输入其它项目数" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<span class="label">成果:</span>
|
||||
<el-input v-model="formData.achievements" placeholder="请输入成果数量" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工程研究中心成就信息 -->
|
||||
<div class="detail-sections">
|
||||
<h3 class="section-title">工程研究中心其他成就信息</h3>
|
||||
<div class="section-content">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="formData.labAchievements"
|
||||
:rows="8"
|
||||
placeholder="请输入工程研究中心其他成就信息"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 雷达图 -->
|
||||
<div class="evaluation-chart-section">
|
||||
<div id="lab-evaluation-radar-chart" class="radar-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<!-- <button class="drawer-btn cancel-btn" @click="handleClose">取消</button>
|
||||
<button class="drawer-btn confirm-btn" @click="handleSave">确定</button> -->
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { RadarChart } from 'echarts/charts';
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent
|
||||
} from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
// 注册必要的echarts组件
|
||||
echarts.use([
|
||||
RadarChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
CanvasRenderer
|
||||
]);
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
dimensions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
labData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'save']);
|
||||
|
||||
// 用于visible属性的双向绑定
|
||||
const drawerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val)
|
||||
});
|
||||
|
||||
// 默认工程研究中心图片占位符
|
||||
const defaultImage = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%234986ff' opacity='0.2'/%3E%3Crect x='20' y='40' width='260' height='120' fill='%234986ff' opacity='0.3'/%3E%3Ccircle cx='70' cy='70' r='20' fill='%234986ff' opacity='0.5'/%3E%3Crect x='120' y='50' width='140' height='40' fill='%234986ff' opacity='0.4'/%3E%3Crect x='120' y='110' width='140' height='30' fill='%234986ff' opacity='0.4'/%3E%3C/svg%3E`;
|
||||
|
||||
// 抽屉标题的计算属性
|
||||
const drawerTitle = computed(() => props.isEdit ? '详情' : '新增评估');
|
||||
|
||||
// 标签页配置
|
||||
const tabs = [
|
||||
{ label: 'URL输入', value: 'url' },
|
||||
{ label: '上传文档', value: 'upload' },
|
||||
{ label: '手动录入', value: 'manual' }
|
||||
];
|
||||
|
||||
// 激活的标签页
|
||||
const activeTab = ref('url');
|
||||
|
||||
// URL输入
|
||||
const urlInput = ref('');
|
||||
|
||||
// 数据是否已加载标志
|
||||
const dataLoaded = ref(false);
|
||||
|
||||
// 选择的上传文件
|
||||
const selectedFile = ref(null);
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: 'BLG45187',
|
||||
name: '',
|
||||
personnel: '',
|
||||
nationalProjects: '',
|
||||
otherProjects: '',
|
||||
achievements: '',
|
||||
image: '',
|
||||
labAchievements: '',
|
||||
evaluationData: [60, 60, 60, 60, 60, 60] // 默认评估数据
|
||||
});
|
||||
|
||||
// 雷达图实例
|
||||
let chart = null;
|
||||
|
||||
// 从props初始化表单数据
|
||||
watch(() => props.labData, (newValue) => {
|
||||
if (newValue && Object.keys(newValue).length > 0) {
|
||||
Object.assign(formData, newValue);
|
||||
if (props.isEdit) {
|
||||
dataLoaded.value = true;
|
||||
}
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 监听抽屉可见性变化
|
||||
watch(() => props.visible, (isVisible) => {
|
||||
if (isVisible && dataLoaded.value) {
|
||||
// 当抽屉变为可见时,初始化或更新图表
|
||||
nextTick(() => {
|
||||
initRadarChart();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
if (props.visible && dataLoaded.value) {
|
||||
initRadarChart();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化雷达图
|
||||
const initRadarChart = () => {
|
||||
const chartDom = document.getElementById('lab-evaluation-radar-chart');
|
||||
if (!chartDom) return;
|
||||
|
||||
// 清除任何现有图表
|
||||
if (chart) {
|
||||
chart.dispose();
|
||||
}
|
||||
|
||||
chart = echarts.init(chartDom);
|
||||
|
||||
// 从dimensions生成指标
|
||||
const indicators = [
|
||||
{ name: '创新水平', max: 100 },
|
||||
{ name: '研究能力', max: 100 },
|
||||
{ name: '成果转化', max: 100 },
|
||||
{ name: '学科建设', max: 100 },
|
||||
{ name: '行业贡献', max: 100 },
|
||||
{ name: '发展潜力', max: 100 }
|
||||
];
|
||||
|
||||
chart.setOption({
|
||||
radar: {
|
||||
indicator: indicators,
|
||||
splitArea: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
|
||||
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
|
||||
name: { textStyle: { color: '#fff', fontSize: 10 } },
|
||||
radius: '70%'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: [
|
||||
{
|
||||
value: formData.evaluationData,
|
||||
name: '评估结果',
|
||||
areaStyle: { opacity: 0 },
|
||||
lineStyle: { color: 'rgb(238, 23, 143)', width: 2 }, // 玫红色线段
|
||||
itemStyle: { color: 'rgb(238, 23, 143)' } // 玫红色数据点
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 添加窗口大小变化事件监听器
|
||||
window.addEventListener('resize', () => {
|
||||
chart && chart.resize();
|
||||
});
|
||||
};
|
||||
|
||||
// 处理文件变更
|
||||
const handleFileChange = (file) => {
|
||||
selectedFile.value = file;
|
||||
|
||||
// 文件上传后的模拟数据加载
|
||||
setTimeout(() => {
|
||||
fetchMockData();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// 从URL获取数据
|
||||
const fetchDataFromUrl = async () => {
|
||||
if (!urlInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 演示使用模拟数据
|
||||
fetchMockData();
|
||||
};
|
||||
|
||||
// 用于演示的模拟数据
|
||||
const fetchMockData = () => {
|
||||
// 模拟与图片中匹配的响应数据
|
||||
const response = {
|
||||
id: 'BLG45187',
|
||||
name: '基础力学教学实验中心',
|
||||
personnel: '30人',
|
||||
nationalProjects: '45项',
|
||||
otherProjects: '11项',
|
||||
achievements: '28项',
|
||||
image: defaultImage,
|
||||
labAchievements: `基础力学教学实验中心成立于2018年,是北京理工大学重点建设的工程研究中心之一。致力于力学基础理论与应用研究,为工业和国防科技提供科研支持。
|
||||
|
||||
工程研究中心拥有先进的力学测试设备和模拟系统,在材料力学、流体力学等领域具有领先优势。近年来承担多项国家重点研发计划项目。
|
||||
|
||||
工程研究中心现有研究人员26人,其中教授5人,副教授8人,讲师和工程技术人员13人。拥有博士生导师6人,硕士生导师10人。
|
||||
|
||||
近五年承担国家重点研发计划项目3项,国家自然科学基金项目12项,省部级科研项目25项,企业合作项目30余项。
|
||||
|
||||
近五年发表SCI论文120余篇,获得国家发明专利35项,省部级科技奖励5项。多项成果已在航空航天、高端装备制造等领域实现转化应用。`,
|
||||
evaluationData: [85, 78, 92, 76, 88, 80]
|
||||
};
|
||||
|
||||
// 使用响应更新表单数据
|
||||
Object.assign(formData, response);
|
||||
|
||||
// 设置数据已加载标志
|
||||
dataLoaded.value = true;
|
||||
|
||||
// 更新图表
|
||||
nextTick(() => {
|
||||
initRadarChart();
|
||||
});
|
||||
};
|
||||
|
||||
// 处理关闭
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
// 重置数据加载状态
|
||||
if (!props.isEdit) {
|
||||
dataLoaded.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取token函数
|
||||
const getToken = () => {
|
||||
return localStorage.getItem('token');
|
||||
};
|
||||
|
||||
// 处理保存
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// 准备请求体数据
|
||||
const labData = { ...formData.value };
|
||||
|
||||
// 发送POST请求到后端API
|
||||
const response = await fetch('http://localhost:8000/labs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${getToken()}`
|
||||
},
|
||||
body: JSON.stringify(labData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const savedData = await response.json();
|
||||
emit('save', savedData);
|
||||
ElMessage.success('保存成功');
|
||||
handleClose();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
ElMessage.error(`保存失败: ${error.detail || '未知错误'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存请求出错:', error);
|
||||
ElMessage.error('保存失败,请稍后再试');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './common.css';
|
||||
|
||||
.el-drawer__body{
|
||||
padding: 0px !important;
|
||||
}
|
||||
.el-drawer__header{
|
||||
background-color: #0c1633 !important;
|
||||
margin-bottom:0px !important;
|
||||
}
|
||||
.el-drawer__footer{
|
||||
background-color: #0c1633 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.lab-drawer :deep(.el-drawer__header) {
|
||||
margin-bottom: 0;
|
||||
color: white;
|
||||
background-color: #0c1633;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.lab-drawer :deep(.el-drawer__body) {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #0c1633;
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
padding: 20px;
|
||||
color: white;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: #0c1633;
|
||||
}
|
||||
|
||||
/* 标签导航 */
|
||||
.tab-navigation {
|
||||
display: flex;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #4986ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #4986ff;
|
||||
}
|
||||
|
||||
/* URL输入部分 */
|
||||
.url-input-container {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.url-input {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* 上传部分 */
|
||||
.upload-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
color: rgba(255,255,255,0.7);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 手动输入提示 */
|
||||
.manual-input-tip {
|
||||
margin-bottom: 20px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
/* 工程研究中心信息表单 */
|
||||
.form-header {
|
||||
display: flex;
|
||||
margin-bottom: 30px;
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.image-section {
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lab-image {
|
||||
width: 150px;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.lab-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.lab-id {
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.basic-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
flex: 1;
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.form-item.full-width {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
/* 详细信息部分 */
|
||||
.detail-sections {
|
||||
margin-bottom: 30px;
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 15px 0 10px;
|
||||
font-size: 16px;
|
||||
color: rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
.section-content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 雷达图部分 */
|
||||
.evaluation-chart-section {
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.radar-chart {
|
||||
width: 100%;
|
||||
height: 300px; /* 明确指定雷达图高度 */
|
||||
min-height: 300px; /* 确保最小高度 */
|
||||
}
|
||||
|
||||
/* 抽屉页脚 */
|
||||
.lab-drawer :deep(.el-drawer__footer) {
|
||||
border-top: 1px solid rgba(73,134,255,0.3);
|
||||
padding: 10px 20px;
|
||||
background-color: #0c1633;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.drawer-btn {
|
||||
padding: 8px 15px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-regular;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: transparent;
|
||||
color: rgba(255,255,255,0.8);
|
||||
border: 1px solid rgba(73,134,255,0.5);
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background-color: rgba(14,62,167,1);
|
||||
color: rgba(255,255,255,1);
|
||||
border: 1px solid rgba(73,134,255,1);
|
||||
}
|
||||
|
||||
/* Element Plus组件的深色主题覆盖样式 */
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-textarea__wrapper) {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
box-shadow: 0 0 0 1px rgba(73,134,255,0.3) inset;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner),
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner::placeholder),
|
||||
:deep(.el-textarea__inner::placeholder) {
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
:deep(.el-select .el-input__wrapper) {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
box-shadow: 0 0 0 1px rgba(73,134,255,0.3) inset;
|
||||
}
|
||||
|
||||
:deep(.el-select-dropdown__item) {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
:deep(.el-date-editor) {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
border-color: rgba(73,134,255,0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-upload),
|
||||
:deep(.el-upload-dragger) {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
border-color: rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.drawer-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.drawer-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.drawer-content::-webkit-scrollbar-thumb {
|
||||
background-color: #4986ff;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.detail-content-image {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lab-detail-image {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
455
src/components/别动/TalentDetail.vue
Normal file
@ -0,0 +1,455 @@
|
||||
<template>
|
||||
<div class="evaluation-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="dashboard-header">
|
||||
<div class="logo">
|
||||
<img src="../assets/logo1.png" alt="北京理工大学" @click="jumpToDashboard" style="cursor: pointer;" />
|
||||
<img src="../assets/logo2.png" alt="北京理工大学" @click="jumpToDashboard" style="cursor: pointer;" />
|
||||
<h1 class="main-title">
|
||||
<div class="title-line"></div>
|
||||
<span class="title-text">智慧科研评估系统</span>
|
||||
<div class="title-line"></div>
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="content-container">
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="action-bar">
|
||||
<div class="search-box" style="margin-left: 170px;">
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入教师姓名或ID"
|
||||
v-model="searchQuery"
|
||||
@input="handleSearch"
|
||||
|
||||
/>
|
||||
<button class="search-button">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||
<path fill="white" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="add-evaluation-btn" @click="openAddEvaluationDrawer" style="margin-right: 170px;">
|
||||
新增评估
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 教师卡片列表 -->
|
||||
<div class="teacher-card-grid custom-scrollbar">
|
||||
<img src="../assets/人才贴图.png" alt="人才列表" class="talent-list-image" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TalentDrawerDetail
|
||||
v-model:visible="drawerVisible"
|
||||
:is-edit="isEditMode"
|
||||
:dimensions="dimensions"
|
||||
:teacher-data="selectedTeacher"
|
||||
@save="handleSaveEvaluation"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { RadarChart } from 'echarts/charts';
|
||||
import TalentDrawerDetail from './TalentDrawerDetail.vue';
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent
|
||||
} from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { getApiBaseUrl } from '../../config';
|
||||
|
||||
// 注册必要的 echarts 组件
|
||||
echarts.use([
|
||||
RadarChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
CanvasRenderer
|
||||
]);
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
const isEditMode = ref(false);
|
||||
const selectedTeacher = ref(null);
|
||||
|
||||
// Function to open the drawer for adding a new evaluation
|
||||
const openAddEvaluationDrawer = () => {
|
||||
isEditMode.value = false;
|
||||
selectedTeacher.value = null;
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
|
||||
// Function to handle save from drawer
|
||||
const handleSaveEvaluation = (data) => {
|
||||
if (isEditMode.value) {
|
||||
// Update existing teacher data
|
||||
const index = teachers.value.findIndex(t => t.id === data.id);
|
||||
if (index !== -1) {
|
||||
teachers.value[index] = { ...data };
|
||||
}
|
||||
} else {
|
||||
// Add new teacher
|
||||
teachers.value.push({ ...data });
|
||||
}
|
||||
|
||||
// Update filtered teachers
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
// 向父组件发送页面切换事件
|
||||
const emit = defineEmits(['navigate']);
|
||||
|
||||
// 跳转到仪表盘页面
|
||||
const jumpToDashboard = () => {
|
||||
emit('navigate', 'dashboard');
|
||||
// 或者使用路由导航 (如果你使用的是 vue-router)
|
||||
// router.push('/dashboard');
|
||||
};
|
||||
|
||||
// 跳转到外部科研成果评估链接
|
||||
const redirectToResearchEvaluation = () => {
|
||||
window.open('http://82.156.236.221:10004/login', '_blank');
|
||||
};
|
||||
|
||||
// 评估维度数据
|
||||
const dimensions = ref([
|
||||
{ name: '教育和工作经历', weight: 10, enabled: true },
|
||||
{ name: '研究方向前沿性', weight: 8, enabled: true },
|
||||
{ name: '主持科研项目情况', weight: 12, enabled: true },
|
||||
{ name: '科研成果质量', weight: 16, enabled: true },
|
||||
{ name: '教学能力与效果', weight: 14, enabled: true },
|
||||
{ name: '学术服务与影响力', weight: 40, enabled: true }
|
||||
]);
|
||||
|
||||
// 搜索功能
|
||||
const searchQuery = ref('');
|
||||
|
||||
// 获取token函数
|
||||
const getToken = () => {
|
||||
return localStorage.getItem('token');
|
||||
};
|
||||
|
||||
// 教师数据现在从API获取
|
||||
const teachers = ref([]);
|
||||
const filteredTeachers = ref([]);
|
||||
const loading = ref(true);
|
||||
|
||||
// 从API获取人才数据
|
||||
const fetchTalents = async () => {
|
||||
try {
|
||||
const response = await fetch(`${getApiBaseUrl()}/talents`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${getToken()}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
teachers.value = data;
|
||||
handleSearch(); // 初始化filteredTeachers
|
||||
} else {
|
||||
console.error('获取人才数据失败:', response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取人才数据出错:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期钩子 - 组件挂载后获取数据
|
||||
onMounted(() => {
|
||||
fetchTalents();
|
||||
});
|
||||
|
||||
// 搜索功能实现
|
||||
const handleSearch = () => {
|
||||
if (!searchQuery.value.trim()) {
|
||||
filteredTeachers.value = [...teachers.value];
|
||||
return;
|
||||
}
|
||||
|
||||
const query = searchQuery.value.toLowerCase().trim();
|
||||
filteredTeachers.value = teachers.value.filter(teacher =>
|
||||
teacher.name.toLowerCase().includes(query) ||
|
||||
teacher.id.toLowerCase().includes(query)
|
||||
);
|
||||
};
|
||||
|
||||
// 初始化雷达图
|
||||
const initRadarCharts = () => {
|
||||
filteredTeachers.value.forEach((teacher, index) => {
|
||||
const chartDom = document.getElementById(`chart-${index}`);
|
||||
if (!chartDom) return;
|
||||
|
||||
// 先清空已有的图表实例
|
||||
echarts.dispose(chartDom);
|
||||
const chart = echarts.init(chartDom);
|
||||
|
||||
// 生成雷达图所需的指标
|
||||
const indicators = dimensions.value.map(dim => ({
|
||||
name: dim.name,
|
||||
max: 100
|
||||
}));
|
||||
|
||||
chart.setOption({
|
||||
radar: {
|
||||
indicator: indicators,
|
||||
splitArea: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
|
||||
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
|
||||
name: { textStyle: { color: '#fff', fontSize: 10 } },
|
||||
radius: '70%'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: [
|
||||
{
|
||||
value: teacher.evaluationData,
|
||||
name: '评估结果',
|
||||
areaStyle: { opacity: 0 }, // 移除半透明的底色
|
||||
lineStyle: { color: 'rgb(63, 196, 15)', width: 2 }, // 设置线段颜色为RGB(63, 196, 15)
|
||||
itemStyle: { color: 'rgb(63, 196, 15)' } // 设置点的颜色
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 添加窗口大小变化时的调整
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 监听过滤后的教师数据变化,重新初始化图表
|
||||
watch(filteredTeachers, () => {
|
||||
// 使用 nextTick 确保 DOM 已更新
|
||||
setTimeout(() => {
|
||||
initRadarCharts();
|
||||
}, 100);
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时显示所有教师
|
||||
filteredTeachers.value = teachers.value;
|
||||
// 需要延迟一下,确保 DOM 已经渲染
|
||||
setTimeout(() => {
|
||||
initRadarCharts();
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './common.css';
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.evaluation-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #0c1633;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
height: 60px;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 28px;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.title-line {
|
||||
border: 2px solid rgba(73,134,255,1);
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
margin: 0 30px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dimension-image, .talent-list-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 特定于TalentDetail的样式 */
|
||||
.sidebar-header {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.home-link {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
color: #4986ff;
|
||||
}
|
||||
|
||||
.dimension-sidebar {
|
||||
width: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dimension-content {
|
||||
flex: 1;
|
||||
background-color: #262F50;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 右侧内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 搜索和操作栏 */
|
||||
.action-bar {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 300px;
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 10px 15px;
|
||||
color: white;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.search-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.add-evaluation-btn {
|
||||
background-color: rgba(14,62,167,1);
|
||||
color: rgba(255,255,255,1);
|
||||
border: 1px solid rgba(73,134,255,1);
|
||||
border-radius: 10px;
|
||||
padding: 8px 15px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
font-family: PingFangSC-regular;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 教师卡片网格 */
|
||||
.teacher-card-grid {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
/* background-color: #262F50; */
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.dimension-sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 300px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
height: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
806
src/components/别动/TalentDrawerDetail.vue
Normal file
@ -0,0 +1,806 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
:title="drawerTitle"
|
||||
direction="rtl"
|
||||
size="900px"
|
||||
:before-close="handleClose"
|
||||
custom-class="talent-drawer"
|
||||
>
|
||||
<div class="drawer-content">
|
||||
<!-- 标签导航 -->
|
||||
<div class="tab-navigation">
|
||||
<div
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
:class="['tab-item', { active: activeTab === tab.value }]"
|
||||
@click="activeTab = tab.value"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- URL输入标签页 -->
|
||||
<div v-if="activeTab === 'url'" class="tab-content">
|
||||
<div class="url-input-container">
|
||||
<el-input
|
||||
v-model="urlInput"
|
||||
placeholder="请输入URL"
|
||||
class="url-input"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="fetchDataFromUrl"
|
||||
class="send-button"
|
||||
>
|
||||
发送
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="detail-content-image">
|
||||
<img src="../assets/人才详情贴图.png" alt="人才详情" class="talent-detail-image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传文档标签页 -->
|
||||
<div v-else-if="activeTab === 'upload'" class="tab-content">
|
||||
<el-upload
|
||||
class="upload-container"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<el-button type="primary">选择文件</el-button>
|
||||
<div class="el-upload__tip">请上传文档,支持PDF、DOC、DOCX格式</div>
|
||||
</el-upload>
|
||||
|
||||
<div class="detail-content-image">
|
||||
<img src="../assets/人才详情贴图.png" alt="人才详情" class="talent-detail-image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手动输入标签页 -->
|
||||
<div v-else class="tab-content">
|
||||
<div class="manual-input-tip">请手动填写以下信息</div>
|
||||
|
||||
<div class="detail-content-image">
|
||||
<img src="../assets/人才详情贴图.png" alt="人才详情" class="talent-detail-image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 个人信息表单 - 仅在数据加载后显示 -->
|
||||
<div v-if="dataLoaded" class="personal-info-form">
|
||||
<div class="form-header">
|
||||
<div class="photo-section">
|
||||
<div class="teacher-photo">
|
||||
<img :src="formData.photo || defaultPhoto" alt="教师照片" />
|
||||
</div>
|
||||
<div class="teacher-id">ID: {{ formData.id || 'BLG00000' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="basic-info">
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<span class="label">姓名:</span>
|
||||
<el-input v-model="formData.name" placeholder="请输入姓名" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="label">性别:</span>
|
||||
<el-input v-model="formData.gender" placeholder="请输入性别" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="label">出生年月:</span>
|
||||
<el-date-picker
|
||||
v-model="formData.birthDate"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
format="YYYY.MM.DD"
|
||||
value-format="YYYY.MM.DD"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<span class="label">职称:</span>
|
||||
<el-input v-model="formData.title" placeholder="请输入职称" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="label">职务:</span>
|
||||
<el-input v-model="formData.position" placeholder="请输入职务" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="label">最高学历:</span>
|
||||
<el-input v-model="formData.education" placeholder="请输入最高学历" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-item full-width">
|
||||
<span class="label">通讯地址:</span>
|
||||
<el-input v-model="formData.address" placeholder="请输入通讯地址" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<span class="label">学科方向:</span>
|
||||
<el-input v-model="formData.academicDirection" placeholder="请输入学科方向" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="label">人才计划:</span>
|
||||
<el-input v-model="formData.talentPlan" placeholder="请输入人才计划" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="label">办公地点:</span>
|
||||
<el-input v-model="formData.officeLocation" placeholder="请输入办公地点" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<span class="label">电子邮箱:</span>
|
||||
<el-input v-model="formData.email" placeholder="请输入电子邮箱" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="label">联系方式:</span>
|
||||
<el-input v-model="formData.phone" placeholder="请输入联系方式" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="label">导师类型:</span>
|
||||
<el-input v-model="formData.tutorType" placeholder="请输入导师类型" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<span class="label">论文:</span>
|
||||
<el-input v-model="formData.papers" placeholder="请输入论文数量" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="label">项目:</span>
|
||||
<el-input v-model="formData.projects" placeholder="请输入项目数量" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细信息部分 -->
|
||||
<div class="detail-sections">
|
||||
<h3 class="section-title">教育与工作经历</h3>
|
||||
<div class="section-content">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="formData.eduWorkHistory"
|
||||
:rows="4"
|
||||
placeholder="请输入教育与工作经历"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 class="section-title">研究方向</h3>
|
||||
<div class="section-content">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="formData.researchDirection"
|
||||
:rows="4"
|
||||
placeholder="请输入研究方向"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 class="section-title">近五年承担的科研项目</h3>
|
||||
<div class="section-content">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="formData.recentProjects"
|
||||
:rows="4"
|
||||
placeholder="请输入近五年承担的科研项目"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 class="section-title">代表性学术论文</h3>
|
||||
<div class="section-content">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="formData.representativePapers"
|
||||
:rows="4"
|
||||
placeholder="请输入代表性学术论文"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 class="section-title">授权国家发明专利及出版专著</h3>
|
||||
<div class="section-content">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="formData.patents"
|
||||
:rows="4"
|
||||
placeholder="请输入授权国家发明专利及出版专著"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 雷达图 -->
|
||||
<div class="evaluation-chart-section">
|
||||
<div id="evaluation-radar-chart" class="radar-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<!-- <button class="drawer-btn cancel-btn" @click="handleClose">取消</button>
|
||||
<button class="drawer-btn confirm-btn" @click="handleSave">确定</button> -->
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { RadarChart } from 'echarts/charts';
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent
|
||||
} from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
// 注册必要的echarts组件
|
||||
echarts.use([
|
||||
RadarChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
CanvasRenderer
|
||||
]);
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
dimensions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
teacherData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'save']);
|
||||
|
||||
// 用于visible属性的双向绑定
|
||||
const drawerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val)
|
||||
});
|
||||
|
||||
// 默认照片占位符
|
||||
const defaultPhoto = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='120' viewBox='0 0 100 120'%3E%3Crect width='100' height='120' fill='%234986ff' opacity='0.3'/%3E%3Ccircle cx='50' cy='45' r='25' fill='%234986ff' opacity='0.6'/%3E%3Ccircle cx='50' cy='95' r='35' fill='%234986ff' opacity='0.6'/%3E%3C/svg%3E`;
|
||||
|
||||
// 抽屉标题的计算属性
|
||||
const drawerTitle = computed(() => props.isEdit ? '详情' : '新增评估');
|
||||
|
||||
// 标签页配置
|
||||
const tabs = [
|
||||
{ label: 'URL输入', value: 'url' },
|
||||
{ label: '上传文档', value: 'upload' },
|
||||
{ label: '手动录入', value: 'manual' }
|
||||
];
|
||||
|
||||
// 激活的标签页
|
||||
const activeTab = ref('url');
|
||||
|
||||
// URL输入
|
||||
const urlInput = ref('');
|
||||
|
||||
// 数据是否已加载标志
|
||||
const dataLoaded = ref(false);
|
||||
|
||||
// 选择的上传文件
|
||||
const selectedFile = ref(null);
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: 'BLG45187',
|
||||
name: '',
|
||||
gender: '',
|
||||
birthDate: '',
|
||||
title: '',
|
||||
position: '',
|
||||
education: '',
|
||||
address: '',
|
||||
academicDirection: '',
|
||||
talentPlan: '',
|
||||
officeLocation: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
tutorType: '',
|
||||
papers: '',
|
||||
projects: '',
|
||||
photo: '',
|
||||
eduWorkHistory: '',
|
||||
researchDirection: '',
|
||||
recentProjects: '',
|
||||
representativePapers: '',
|
||||
patents: '',
|
||||
evaluationData: [60, 60, 60, 60, 60, 60] // 默认评估数据
|
||||
});
|
||||
|
||||
// 雷达图实例
|
||||
let chart = null;
|
||||
|
||||
// 从props初始化表单数据
|
||||
watch(() => props.teacherData, (newValue) => {
|
||||
if (newValue && Object.keys(newValue).length > 0) {
|
||||
Object.assign(formData, newValue);
|
||||
if (props.isEdit) {
|
||||
dataLoaded.value = true;
|
||||
}
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 监听抽屉可见性变化
|
||||
watch(() => props.visible, (isVisible) => {
|
||||
if (isVisible && dataLoaded.value) {
|
||||
// 当抽屉变为可见时,初始化或更新图表
|
||||
nextTick(() => {
|
||||
initRadarChart();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
if (props.visible && dataLoaded.value) {
|
||||
initRadarChart();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化雷达图
|
||||
const initRadarChart = () => {
|
||||
const chartDom = document.getElementById('evaluation-radar-chart');
|
||||
if (!chartDom) return;
|
||||
|
||||
// 清除任何现有图表
|
||||
if (chart) {
|
||||
chart.dispose();
|
||||
}
|
||||
|
||||
chart = echarts.init(chartDom);
|
||||
|
||||
// 从dimensions生成指标
|
||||
const indicators = props.dimensions.map(dim => ({
|
||||
name: dim.name,
|
||||
max: 100
|
||||
})) || [
|
||||
{ name: '工作经历', max: 100 },
|
||||
{ name: '研究方向', max: 100 },
|
||||
{ name: '科研项目', max: 100 },
|
||||
{ name: '学术论文', max: 100 },
|
||||
{ name: '专利专著', max: 100 },
|
||||
{ name: '学术影响', max: 100 }
|
||||
];
|
||||
|
||||
chart.setOption({
|
||||
radar: {
|
||||
indicator: indicators,
|
||||
splitArea: { show: false },
|
||||
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
|
||||
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
|
||||
name: { textStyle: { color: '#fff', fontSize: 10 } },
|
||||
radius: '70%'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: [
|
||||
{
|
||||
value: formData.evaluationData,
|
||||
name: '评估结果',
|
||||
areaStyle: { opacity: 0 },
|
||||
lineStyle: { color: 'rgb(63, 196, 15)', width: 2 },
|
||||
itemStyle: { color: 'rgb(63, 196, 15)' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 添加窗口大小变化事件监听器
|
||||
window.addEventListener('resize', () => {
|
||||
chart && chart.resize();
|
||||
});
|
||||
};
|
||||
|
||||
// 处理文件变更
|
||||
const handleFileChange = (file) => {
|
||||
selectedFile.value = file;
|
||||
|
||||
// 文件上传后的模拟数据加载
|
||||
setTimeout(() => {
|
||||
fetchMockData();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// 从URL获取数据
|
||||
const fetchDataFromUrl = async () => {
|
||||
if (!urlInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 演示使用模拟数据
|
||||
fetchMockData();
|
||||
};
|
||||
|
||||
// 用于演示的模拟数据
|
||||
const fetchMockData = () => {
|
||||
// 模拟与图片中匹配的响应数据
|
||||
const response = {
|
||||
id: 'BLG45187',
|
||||
name: '张三',
|
||||
gender: '男',
|
||||
birthDate: '1991.11.12',
|
||||
title: '预聘助理教授',
|
||||
position: '教授',
|
||||
education: '博士研究生',
|
||||
address: '北京市海淀区北京理工大学自动化学院',
|
||||
academicDirection: '控制理论与控制工程',
|
||||
talentPlan: '无',
|
||||
officeLocation: '6号楼509',
|
||||
email: 'zhangsan@bit.edu.cn',
|
||||
phone: '15487546574',
|
||||
tutorType: '硕士生导师',
|
||||
papers: '15篇',
|
||||
projects: '26项',
|
||||
photo: defaultPhoto,
|
||||
eduWorkHistory: '2023.12 至今北京理工大学, 自动化学院, 预聘助理教授\n2020.07-2023.12 北京理工大学 自动化学院 博士后(合作导师:张国良 教授)\n2016.09-2021.09 北京航空航天大学 自动化科学与电气工程学院 控制科学与工程 博士 联培\n2011.09-2015.06 中国石油大学(华东)机械设计制造及其自动化 本科',
|
||||
researchDirection: '伺服控制\n高天空地机器人协同',
|
||||
recentProjects: '项目1\n项目2\n项目3',
|
||||
representativePapers: '论文1\n论文2\n论文3',
|
||||
patents: '专利1\n专利2',
|
||||
evaluationData: [85, 90, 78, 82, 76, 88]
|
||||
};
|
||||
|
||||
// 使用响应更新表单数据
|
||||
Object.assign(formData, response);
|
||||
|
||||
// 设置数据已加载标志
|
||||
dataLoaded.value = true;
|
||||
|
||||
// 更新图表
|
||||
nextTick(() => {
|
||||
initRadarChart();
|
||||
});
|
||||
};
|
||||
|
||||
// 处理关闭
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false;
|
||||
// 重置数据加载状态
|
||||
if (!props.isEdit) {
|
||||
dataLoaded.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取token函数
|
||||
const getToken = () => {
|
||||
return localStorage.getItem('token');
|
||||
};
|
||||
|
||||
// 处理保存
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// 准备请求体数据
|
||||
const talentData = { ...formData.value };
|
||||
|
||||
// 发送POST请求到后端API
|
||||
const response = await fetch('http://localhost:8000/talents', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${getToken()}`
|
||||
},
|
||||
body: JSON.stringify(talentData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const savedData = await response.json();
|
||||
emit('save', savedData);
|
||||
ElMessage.success('保存成功');
|
||||
handleClose();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
ElMessage.error(`保存失败: ${error.detail || '未知错误'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存请求出错:', error);
|
||||
ElMessage.error('保存失败,请稍后再试');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './common.css';
|
||||
|
||||
.el-drawer__body{
|
||||
padding: 0px !important;
|
||||
}
|
||||
.el-drawer__header{
|
||||
background-color: #0c1633 !important;
|
||||
margin-bottom:0px !important;
|
||||
}
|
||||
.el-drawer__footer{
|
||||
background-color: #0c1633 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.talent-drawer :deep(.el-drawer__header) {
|
||||
margin-bottom: 0;
|
||||
color: white;
|
||||
background-color: #0c1633;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.talent-drawer :deep(.el-drawer__body) {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #0c1633;
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
padding: 20px;
|
||||
color: white;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: #0c1633;
|
||||
}
|
||||
|
||||
/* 标签导航 */
|
||||
.tab-navigation {
|
||||
display: flex;
|
||||
border-bottom: 1px solid rgba(73,134,255,0.3);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #4986ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #4986ff;
|
||||
}
|
||||
|
||||
/* URL输入部分 */
|
||||
.url-input-container {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.url-input {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* 上传部分 */
|
||||
.upload-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
color: rgba(255,255,255,0.7);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 手动输入提示 */
|
||||
.manual-input-tip {
|
||||
margin-bottom: 20px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
/* 个人信息表单 */
|
||||
.form-header {
|
||||
display: flex;
|
||||
margin-bottom: 30px;
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.photo-section {
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.basic-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
flex: 1;
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.form-item.full-width {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
/* 详细信息部分 */
|
||||
.detail-sections {
|
||||
margin-bottom: 30px;
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 15px 0 10px;
|
||||
font-size: 16px;
|
||||
color: rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
.section-content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 雷达图部分 */
|
||||
.evaluation-chart-section {
|
||||
background-color: #1f3266;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
.radar-chart {
|
||||
width: 100%;
|
||||
height: 300px; /* 明确指定雷达图高度 */
|
||||
min-height: 300px; /* 确保最小高度 */
|
||||
}
|
||||
|
||||
/* 抽屉页脚 */
|
||||
.talent-drawer :deep(.el-drawer__footer) {
|
||||
border-top: 1px solid rgba(73,134,255,0.3);
|
||||
padding: 10px 20px;
|
||||
background-color: #0c1633;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 按钮样式 - 现在与TalentDetail中的按钮一致 */
|
||||
.drawer-btn {
|
||||
padding: 8px 15px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-regular;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: transparent;
|
||||
color: rgba(255,255,255,0.8);
|
||||
border: 1px solid rgba(73,134,255,0.5);
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background-color: rgba(14,62,167,1);
|
||||
color: rgba(255,255,255,1);
|
||||
border: 1px solid rgba(73,134,255,1);
|
||||
}
|
||||
|
||||
/* Element Plus组件的深色主题覆盖样式 */
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-textarea__wrapper) {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
box-shadow: 0 0 0 1px rgba(73,134,255,0.3) inset;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner),
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner::placeholder),
|
||||
:deep(.el-textarea__inner::placeholder) {
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
:deep(.el-select .el-input__wrapper) {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
box-shadow: 0 0 0 1px rgba(73,134,255,0.3) inset;
|
||||
}
|
||||
|
||||
:deep(.el-select-dropdown__item) {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
:deep(.el-date-editor) {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
border-color: rgba(73,134,255,0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-upload),
|
||||
:deep(.el-upload-dragger) {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
border-color: rgba(73,134,255,0.3);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.drawer-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.drawer-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.drawer-content::-webkit-scrollbar-thumb {
|
||||
background-color: #4986ff;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.detail-content-image {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.talent-detail-image {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
17
src/config.js
Normal file
@ -0,0 +1,17 @@
|
||||
// API配置
|
||||
const env = import.meta.env.MODE || 'development';
|
||||
|
||||
const config = {
|
||||
development: {
|
||||
apiBaseUrl: 'http://127.0.0.1:48996',
|
||||
},
|
||||
production: {
|
||||
apiBaseUrl: 'http://36.103.199.107:48996',
|
||||
}
|
||||
};
|
||||
|
||||
export const getApiBaseUrl = () => {
|
||||
return config[env].apiBaseUrl;
|
||||
};
|
||||
|
||||
export default config;
|
17
src/main.js
Normal file
@ -0,0 +1,17 @@
|
||||
// src/main.js
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import App from './App.vue'
|
||||
|
||||
// 创建应用
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
79
src/style.css
Normal file
@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
58
start_with_ip.bat
Normal file
@ -0,0 +1,58 @@
|
||||
@echo off
|
||||
echo ===========================================
|
||||
echo Fast Dashboard IP 访问启动脚本
|
||||
echo ===========================================
|
||||
echo.
|
||||
|
||||
echo 正在获取本机IP地址...
|
||||
for /f "tokens=2 delims=:" %%i in ('ipconfig ^| findstr /c:"IPv4 地址"') do (
|
||||
for /f "tokens=1" %%j in ("%%i") do (
|
||||
echo 检测到IP地址: %%j
|
||||
set "LOCAL_IP=%%j"
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 激活Python环境...
|
||||
call conda activate fast-dashboard-env
|
||||
if errorlevel 1 (
|
||||
echo 错误: 无法激活conda环境,请确保已按照PYTHON_ENV_SETUP_README.md配置环境
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 启动后端服务器 (端口: 8000)...
|
||||
cd backend
|
||||
start "后端服务器" cmd /k "uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
|
||||
|
||||
echo 等待后端启动...
|
||||
timeout /t 3 /nobreak >nul
|
||||
|
||||
echo.
|
||||
echo 启动前端服务器 (端口: 5173)...
|
||||
cd ..
|
||||
start "前端服务器" cmd /k "npm run dev -- --host 0.0.0.0"
|
||||
|
||||
echo.
|
||||
echo ===========================================
|
||||
echo 服务启动完成!
|
||||
echo ===========================================
|
||||
echo.
|
||||
echo 本地访问地址:
|
||||
echo 前端: http://localhost:5173
|
||||
echo 后端: http://localhost:8000
|
||||
echo.
|
||||
echo 网络访问地址 (同局域网内其他设备可访问):
|
||||
if defined LOCAL_IP (
|
||||
echo 前端: http://%LOCAL_IP%:5173
|
||||
echo 后端: http://%LOCAL_IP%:8000
|
||||
) else (
|
||||
echo 前端: http://192.168.18.108:5173
|
||||
echo 后端: http://192.168.18.108:8000
|
||||
)
|
||||
echo.
|
||||
echo 注意: 确保防火墙允许端口 5173 和 8000 的访问
|
||||
echo ===========================================
|
||||
echo.
|
||||
pause
|
15
vite.config.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
define: {
|
||||
'import.meta.env.MODE': JSON.stringify(process.env.NODE_ENV || 'development')
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0', // 允许从任何IP访问
|
||||
port: 5173, // 前端端口
|
||||
strictPort: true
|
||||
}
|
||||
})
|