commit a3eb5c7b2dc3a33c36e2bbdbca47374c421bd965 Author: xiaopeng <1509442308@qq.com> Date: Sun Jun 14 01:07:19 2026 +0800 init: TEALab 2025 实验室网站项目 从百度网盘 /apps/bypy/dofile/ 下载的完整项目结构,包含: - dofile/: Flask 后端(端口 5000)+ 静态前端(端口 6002) - officefile/: 项目相关文档 - .gitignore: Python venv / 日志 / 缓存 Co-Authored-By: Claude Opus 4.7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5b11d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Python +.venv/ +venv/ +__pycache__/ +*.py[cod] +*$py.class +*.egg-info/ +.pytest_cache/ + +# Logs +*.log + +# Env +.env +.env.local + +# OS +.DS_Store +Thumbs.db + +# Editor +.vscode/ +.idea/ +*.swp +*.swo + +# Build +build/ +dist/ + +# Flask session +instance/ diff --git a/dofile/BACKEND_SETUP.md b/dofile/BACKEND_SETUP.md new file mode 100644 index 0000000..abe08ae --- /dev/null +++ b/dofile/BACKEND_SETUP.md @@ -0,0 +1,171 @@ +# 后端系统设置指南 + +## 登录前后的功能差别 + +### 登录前 +- 只能查看公开内容 +- 受保护内容显示"登录后查看"遮罩 +- 无法下载受保护资源 +- 无法查看详细统计数据 +- 导航栏显示"登录"按钮 + +### 登录后 +- 可以查看所有内容,包括受保护内容 +- 可以下载受保护资源 +- 可以查看详细统计数据(文章数、资源数、下载量等) +- 导航栏显示用户名和"退出"按钮 +- 可以访问内部文章和资料 + +## 快速开始 + +### 1. 安装Python依赖 + +```bash +cd backend +pip install -r requirements.txt +``` + +或使用虚拟环境: + +```bash +cd backend +python3 -m venv venv +source venv/bin/activate # Linux/Mac +# 或 venv\Scripts\activate # Windows +pip install -r requirements.txt +``` + +### 2. 初始化数据库 + +```bash +cd backend +python init_data.py +``` + +这将创建: +- 数据库表 +- 默认管理员账户:`admin` / `admin123` +- 测试用户账户:`user` / `user123` +- 示例受保护内容 + +### 3. 启动后端服务器 + +```bash +cd backend +python app.py +``` + +或使用启动脚本: + +```bash +./start_backend.sh +``` + +服务器将在 `http://localhost:5000` 启动。 + +### 4. 启动前端服务器 + +在另一个终端: + +```bash +cd dofile +python3 -m http.server 6002 --bind 0.0.0.0 +``` + +## 测试登录功能 + +1. 访问 `http://localhost:6002/website.html` +2. 点击导航栏的"登录"按钮 +3. 使用以下账户登录: + - 管理员:`admin` / `admin123` + - 普通用户:`user` / `user123` +4. 登录后,页面会显示受保护内容区块 + +## API端点说明 + +### 认证相关 +- `POST /api/auth/login` - 用户登录 +- `POST /api/auth/logout` - 用户登出 +- `GET /api/auth/check` - 检查登录状态 + +### 内容管理 +- `GET /api/cms/articles` - 获取文章列表 +- `GET /api/cms/resources` - 获取资源列表 +- `GET /api/cms/team` - 获取团队成员 + +### 受保护内容 +- `GET /api/protected-content` - 获取受保护内容(需登录) +- `GET /api/stats` - 获取统计数据(需登录) +- `GET /api/download/` - 下载资源文件 + +## 内容管理系统(CMS) + +管理员可以通过API管理内容: + +1. **文章管理** + - 创建、编辑、删除文章 + - 设置文章为受保护内容 + - 控制文章发布状态 + +2. **资源管理** + - 上传文件资源 + - 设置资源为受保护 + - 跟踪下载次数 + +3. **团队管理** + - 添加、编辑团队成员信息 + - 管理成员角色和排序 + +## 数据库 + +- 开发环境使用SQLite:`data/dael.db` +- 生产环境建议使用PostgreSQL +- 数据库模型定义在 `backend/models.py` + +## 文件结构 + +``` +backend/ +├── app.py # Flask主应用 +├── models.py # 数据库模型 +├── init_data.py # 初始化脚本 +├── requirements.txt # Python依赖 +├── routes/ # API路由 +│ ├── auth.py # 认证路由 +│ ├── cms.py # 内容管理路由 +│ └── api.py # 其他API路由 +└── README.md # 详细文档 + +data/ +└── dael.db # SQLite数据库 + +uploads/ # 上传文件存储 +``` + +## 注意事项 + +1. **安全性** + - 生产环境必须更改默认密码 + - 使用强SECRET_KEY + - 启用HTTPS + - 配置CORS策略 + +2. **性能** + - 生产环境使用PostgreSQL + - 配置文件上传大小限制 + - 使用CDN存储静态文件 + +3. **扩展性** + - 可以添加更多用户角色 + - 可以实现更细粒度的权限控制 + - 可以集成第三方认证服务 + + + + + + + + + + diff --git a/dofile/MEETINGS_RESEARCH_GUIDE.md b/dofile/MEETINGS_RESEARCH_GUIDE.md new file mode 100644 index 0000000..3e429f4 --- /dev/null +++ b/dofile/MEETINGS_RESEARCH_GUIDE.md @@ -0,0 +1,222 @@ +# 组会管理和研究管理系统使用指南 + +## 功能概述 + +系统已成功实现组会管理和个人研究管理两个核心功能模块,允许每个用户创建和管理自己的内容,同时支持实验室成员之间的内容共享。 + +## 组会管理系统 + +### 功能特性 + +#### 基础功能 +- ✅ 创建组会(标题、描述、日期、时间、地点) +- ✅ 查看所有组会列表(实验室成员可查看) +- ✅ 编辑自己的组会 +- ✅ 删除自己的组会 +- ✅ 组会状态管理(计划中/进行中/已结束/已取消) + +#### 高级功能 +- ✅ 组会报名(实验室成员可报名参会) +- ✅ 签到功能(参会人员可签到) +- ✅ 会议记录(组织者可添加会议记录) +- ✅ 文件共享(组织者可上传会议相关文件) +- ✅ 参会人员列表查看 + +### 访问方式 + +1. **登录后访问** + - 登录账户后,导航栏会显示"组会管理"链接 + - 或直接访问:`http://localhost:6002/meetings.html` + +2. **创建组会** + - 点击"创建组会"按钮 + - 填写组会信息(标题、日期、时间、地点等) + - 选择组会类型(周会/月会/专题会) + - 保存 + +3. **管理组会** + - 查看:点击"查看"按钮查看详情 + - 编辑:只有创建者可以编辑(显示"编辑"按钮) + - 删除:只有创建者可以删除(显示"删除"按钮) + +4. **报名参会** + - 在组会详情页面,点击"报名参会"按钮 + - 报名后状态显示为"已报名" + - 会议开始后可进行签到 + +## 个人研究管理系统 + +### 功能特性 + +#### 基础功能 +- ✅ 创建研究项目(标题、描述、日期、进度、标签) +- ✅ 查看所有研究项目(实验室成员可查看) +- ✅ 编辑自己的项目 +- ✅ 删除自己的项目 +- ✅ 项目进度跟踪(0-100%) + +#### 高级功能 +- ✅ 研究笔记管理(创建、编辑、删除笔记) +- ✅ 实验记录(笔记类型:笔记/实验记录/文献笔记/想法) +- ✅ 任务管理(创建任务、分配任务、跟踪任务状态) +- ✅ 文件管理(上传、下载、删除研究文件) +- ✅ 项目时间线(开始日期、结束日期) + +### 访问方式 + +1. **登录后访问** + - 登录账户后,导航栏会显示"研究管理"链接 + - 或直接访问:`http://localhost:6002/research.html` + +2. **创建研究项目** + - 点击"新建项目"按钮 + - 填写项目信息(名称、描述、日期、进度、标签) + - 选择项目状态(进行中/已完成/已暂停/已取消) + - 保存 + +3. **管理项目内容** + - **笔记管理**: + - 在项目详情页的"笔记"标签下 + - 点击"+ 新建笔记"创建笔记 + - 可以编辑和删除自己的笔记 + - 支持多种笔记类型(笔记/实验记录/文献笔记/想法) + + - **任务管理**: + - 在项目详情页的"任务"标签下 + - 项目创建者可以创建任务 + - 可以分配任务给其他成员 + - 设置任务优先级(高/中/低)和截止日期 + - 可以勾选完成任务 + + - **文件管理**: + - 在项目详情页的"文件"标签下 + - 点击"+ 上传文件"上传研究文件 + - 支持多种文件格式(PDF、文档、代码、数据文件等) + - 可以下载和删除文件(只有上传者可以删除) + +## 权限说明 + +### 访问权限 +- **实验室共享模式**:所有登录的实验室成员可以查看所有内容 +- **编辑权限**:只有创建者可以编辑和删除自己创建的内容 +- **查看权限**:所有实验室成员可以查看所有组会和项目 + +### 具体权限规则 + +#### 组会管理 +- 创建:所有登录用户 +- 查看:所有登录用户 +- 编辑:只有组织者(创建者) +- 删除:只有组织者 +- 报名:所有登录用户 +- 上传文件:只有组织者 + +#### 研究管理 +- 创建项目:所有登录用户 +- 查看项目:所有登录用户 +- 编辑项目:只有项目所有者 +- 删除项目:只有项目所有者 +- 创建笔记:所有登录用户(在自己的项目或查看的项目中) +- 编辑笔记:只有笔记作者 +- 创建任务:只有项目所有者 +- 编辑任务:项目所有者或任务分配者 +- 上传文件:所有登录用户 +- 删除文件:只有文件上传者 + +## 使用流程示例 + +### 示例1:创建和管理组会 + +1. 登录系统(使用 `admin` / `admin123` 或 `user` / `user123`) +2. 点击导航栏的"组会管理" +3. 点击"+ 创建组会" +4. 填写组会信息: + - 标题:周会 - AI算法讨论 + - 日期:选择下周的日期 + - 时间:14:00 + - 地点:会议室A + - 类型:周会 +5. 保存后,组会出现在列表中 +6. 其他成员可以查看并报名参会 +7. 会议开始后,可以添加会议记录和上传相关文件 + +### 示例2:创建研究项目并管理 + +1. 登录系统 +2. 点击导航栏的"研究管理" +3. 点击"+ 新建项目" +4. 填写项目信息: + - 项目名称:生成式生态学算法研究 + - 描述:研究基于GAN的生态系统设计算法 + - 开始日期:2024-01-01 + - 进度:30% + - 标签:AI, 生态, 算法 +5. 保存后,点击"查看"进入项目详情 +6. 在"笔记"标签下创建研究笔记 +7. 在"任务"标签下创建研究任务 +8. 在"文件"标签下上传研究资料 + +## API端点 + +### 组会管理API +- `GET /api/meetings/meetings` - 获取组会列表 +- `GET /api/meetings/meetings/` - 获取组会详情 +- `POST /api/meetings/meetings` - 创建组会 +- `PUT /api/meetings/meetings/` - 更新组会 +- `DELETE /api/meetings/meetings/` - 删除组会 +- `POST /api/meetings/meetings//register` - 报名参会 +- `POST /api/meetings/meetings//checkin` - 签到 +- `POST /api/meetings/meetings//files` - 上传文件 + +### 研究管理API +- `GET /api/research/projects` - 获取项目列表 +- `GET /api/research/projects/` - 获取项目详情 +- `POST /api/research/projects` - 创建项目 +- `PUT /api/research/projects/` - 更新项目 +- `DELETE /api/research/projects/` - 删除项目 +- `GET /api/research/projects//notes` - 获取笔记列表 +- `POST /api/research/projects//notes` - 创建笔记 +- `PUT /api/research/notes/` - 更新笔记 +- `DELETE /api/research/notes/` - 删除笔记 +- `GET /api/research/projects//tasks` - 获取任务列表 +- `POST /api/research/projects//tasks` - 创建任务 +- `PUT /api/research/tasks/` - 更新任务 +- `DELETE /api/research/tasks/` - 删除任务 +- `POST /api/research/projects//files` - 上传文件 +- `GET /api/research/projects//files/` - 下载文件 +- `DELETE /api/research/projects//files/` - 删除文件 + +## 数据库表结构 + +新增的数据表: +- `meetings` - 组会表 +- `meeting_registrations` - 组会报名表 +- `meeting_files` - 组会文件表 +- `research_projects` - 研究项目表 +- `research_notes` - 研究笔记表 +- `research_tasks` - 研究任务表 +- `research_files` - 研究文件表 + +## 注意事项 + +1. **数据库迁移**:首次使用需要运行数据库初始化,新表会自动创建 +2. **文件存储**:上传的文件存储在 `uploads/meetings/` 和 `uploads/research/` 目录 +3. **权限控制**:所有API都要求用户登录,编辑和删除操作会检查创建者权限 +4. **数据共享**:所有实验室成员可以查看所有内容,但只有创建者可以编辑 + +## 下一步改进建议 + +1. 添加富文本编辑器(用于笔记和会议记录) +2. 添加邮件通知功能(组会提醒、任务到期提醒) +3. 添加数据导出功能(导出项目报告、会议记录) +4. 添加搜索和筛选功能(按标签、日期等筛选) +5. 添加数据可视化(项目进度图表、任务统计) + + + + + + + + + diff --git a/dofile/academic.html b/dofile/academic.html new file mode 100644 index 0000000..109de19 --- /dev/null +++ b/dofile/academic.html @@ -0,0 +1,189 @@ + + + + + + 学术资料 | Design AI & Ecology Lab + + + + + + + + + + +
+
+

学术资料

+

软件使用教程、研究方法和学术资源

+
+
+ + +
+ +
+ + + + + + + + + + + + diff --git a/dofile/admin.html b/dofile/admin.html new file mode 100644 index 0000000..6e915da --- /dev/null +++ b/dofile/admin.html @@ -0,0 +1,239 @@ + + + + + + 内容管理 | Design AI & Ecology Lab + + + + + + + + + + +
+
+

内容管理系统

+ + +
+ + + +
+ + +
+
+

文章列表

+ +
+
+ +
+
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dofile/backend/README.md b/dofile/backend/README.md new file mode 100644 index 0000000..c04655f --- /dev/null +++ b/dofile/backend/README.md @@ -0,0 +1,88 @@ +# DAEL Lab Backend API + +Flask后端API服务器,提供用户认证、内容管理和数据API功能。 + +## 安装和设置 + +### 1. 安装依赖 + +```bash +cd backend +pip install -r requirements.txt +``` + +### 2. 配置环境变量 + +复制 `.env.example` 为 `.env` 并修改配置: + +```bash +cp .env.example .env +``` + +### 3. 初始化数据库 + +```bash +python init_data.py +``` + +这将创建数据库表和默认账户: +- 管理员:`admin` / `admin123` +- 测试用户:`user` / `user123` + +### 4. 启动服务器 + +```bash +python app.py +``` + +服务器将在 `http://localhost:5000` 启动。 + +## API端点 + +### 认证 +- `POST /api/auth/register` - 用户注册 +- `POST /api/auth/login` - 用户登录 +- `POST /api/auth/logout` - 用户登出 +- `GET /api/auth/check` - 检查认证状态 +- `GET /api/auth/me` - 获取当前用户信息 + +### 内容管理 +- `GET /api/cms/articles` - 获取文章列表 +- `GET /api/cms/articles/` - 获取单篇文章 +- `POST /api/cms/articles` - 创建文章(管理员) +- `PUT /api/cms/articles/` - 更新文章(管理员) +- `DELETE /api/cms/articles/` - 删除文章(管理员) + +- `GET /api/cms/resources` - 获取资源列表 +- `POST /api/cms/resources` - 上传资源(管理员) + +- `GET /api/cms/team` - 获取团队成员列表 +- `POST /api/cms/team` - 创建团队成员(管理员) + +### 其他API +- `GET /api/download/` - 下载资源文件 +- `GET /api/stats` - 获取统计数据(需登录) +- `GET /api/protected-content` - 获取受保护内容(需登录) + +## 数据库模型 + +- **User**: 用户模型 +- **Article**: 文章/新闻模型 +- **Resource**: 资源文件模型 +- **TeamMember**: 团队成员模型 + +## 开发说明 + +- 数据库文件存储在 `../data/dael.db` +- 上传文件存储在 `../uploads/` +- 使用SQLite作为开发数据库,生产环境建议使用PostgreSQL + + + + + + + + + + diff --git a/dofile/backend/app.py b/dofile/backend/app.py new file mode 100644 index 0000000..e542148 --- /dev/null +++ b/dofile/backend/app.py @@ -0,0 +1,76 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +from flask_login import LoginManager +from models import db, User +import os +from dotenv import load_dotenv + +load_dotenv() + +app = Flask(__name__) +app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') + +# 获取项目根目录(backend的父目录) +base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +data_dir = os.path.join(base_dir, 'data') +db_path = os.path.join(data_dir, 'dael.db') + +# 确保data目录存在 +os.makedirs(data_dir, exist_ok=True) + +# 配置数据库URI(使用绝对路径) +app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', f'sqlite:///{db_path}') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['UPLOAD_FOLDER'] = os.path.join(base_dir, 'uploads') +app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB max file size + +# 确保上传目录存在 +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + +db.init_app(app) +# 配置CORS以支持跨域请求和cookies +CORS(app, + origins=['http://localhost:6002', 'http://127.0.0.1:6002', 'http://8.152.100.83:6002'], + supports_credentials=True, + allow_headers=['Content-Type', 'Authorization']) + +# Flask-Login配置 +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = 'login' + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + +# 导入路由 +from routes import auth, cms, api, meetings, research + +app.register_blueprint(auth.bp, url_prefix='/api/auth') +app.register_blueprint(cms.bp, url_prefix='/api/cms') +app.register_blueprint(api.bp, url_prefix='/api') +app.register_blueprint(meetings.bp, url_prefix='/api/meetings') +app.register_blueprint(research.bp, url_prefix='/api/research') + +@app.route('/api/health', methods=['GET']) +def health_check(): + """健康检查""" + return jsonify({'status': 'ok', 'message': 'DAEL Lab API is running'}) + +def init_db(): + """初始化数据库""" + with app.app_context(): + db.create_all() + + # 创建默认管理员账户(如果不存在) + if not User.query.filter_by(username='admin').first(): + admin = User(username='admin', email='admin@dael.edu.cn', is_admin=True) + admin.set_password('admin123') # 默认密码,生产环境应更改 + db.session.add(admin) + db.session.commit() + print("Default admin user created: admin/admin123") + +if __name__ == '__main__': + init_db() + app.run(debug=True, host='0.0.0.0', port=5000) + diff --git a/dofile/backend/init_data.py b/dofile/backend/init_data.py new file mode 100644 index 0000000..43092e7 --- /dev/null +++ b/dofile/backend/init_data.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""初始化数据库并添加示例数据""" + +import os +import sys + +# 确保可以导入app +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from app import app, db +from models import User, Article, Resource, TeamMember +from datetime import datetime + +def init_data(): + # 确保data目录存在 + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + data_dir = os.path.join(base_dir, 'data') + os.makedirs(data_dir, exist_ok=True) + + with app.app_context(): + # 创建数据库表 + db.create_all() + + # 创建默认管理员 + if not User.query.filter_by(username='admin').first(): + admin = User(username='admin', email='admin@dael.edu.cn', is_admin=True) + admin.set_password('admin123') + db.session.add(admin) + print("Created admin user: admin/admin123") + + # 创建示例用户 + if not User.query.filter_by(username='user').first(): + user = User(username='user', email='user@dael.edu.cn', is_admin=False) + user.set_password('user123') + db.session.add(user) + print("Created test user: user/user123") + + # 创建示例文章(受保护) + if Article.query.count() == 0: + articles = [ + Article( + title='实验室内部研究进展报告', + content='这是实验室的内部研究进展报告,包含详细的研究数据和成果分析...', + summary='2024年度实验室内部研究进展总结', + category='news', + is_protected=True, + author_id=1, + published=True + ), + Article( + title='内部技术文档:生成式生态学算法详解', + content='本文档详细介绍了生成式生态学算法的实现原理和应用案例...', + summary='内部技术文档,仅供实验室成员查阅', + category='publication', + is_protected=True, + author_id=1, + published=True + ) + ] + for article in articles: + db.session.add(article) + print("Created sample protected articles") + + # 创建示例资源(受保护) + if Resource.query.count() == 0: + resources = [ + Resource( + name='内部数据集 v2.0', + description='实验室内部使用的完整数据集,包含所有研究数据', + file_path='/uploads/internal_dataset.zip', + file_size=1024 * 1024 * 500, # 500MB + file_type='zip', + category='dataset', + is_protected=True + ), + Resource( + name='内部研究报告模板', + description='实验室内部研究报告的标准模板', + file_path='/uploads/report_template.docx', + file_size=1024 * 50, # 50KB + file_type='docx', + category='document', + is_protected=True + ) + ] + for resource in resources: + db.session.add(resource) + print("Created sample protected resources") + + # 创建团队成员 + if TeamMember.query.count() == 0: + members = [ + TeamMember( + name='Dr. Lin Chen', + title='Principal Investigator / 实验室主任', + title_en='Principal Investigator', + bio='博士毕业于MIT Media Lab,专注于计算设计与人机交互领域的研究。', + bio_en='PhD from MIT Media Lab, focusing on computational design and HCI.', + role='professor', + email='lchen@dael.edu.cn', + order=1 + ), + TeamMember( + name='Sarah Wu', + title='Lead Ecologist / 生态学顾问', + title_en='Lead Ecologist', + bio='城市生态学专家,研究重点为城市微气候与生物多样性修复。', + bio_en='Urban ecology expert, focusing on microclimate and biodiversity restoration.', + role='faculty', + email='swu@dael.edu.cn', + order=2 + ) + ] + for member in members: + db.session.add(member) + print("Created sample team members") + + db.session.commit() + print("\nDatabase initialized successfully!") + print("\nDefault accounts:") + print(" Admin: admin / admin123") + print(" User: user / user123") + +if __name__ == '__main__': + init_data() + diff --git a/dofile/backend/init_data.sh b/dofile/backend/init_data.sh new file mode 100644 index 0000000..4de42f7 --- /dev/null +++ b/dofile/backend/init_data.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# 初始化数据库脚本 + +cd "$(dirname "$0")" + +# 检查虚拟环境 +if [ -d ".venv" ]; then + source .venv/bin/activate + python init_data.py +else + echo "Virtual environment not found. Please run: python3 -m venv .venv" + echo "Then install dependencies: pip install -r requirements.txt" + exit 1 +fi + + + + + + + + + + diff --git a/dofile/backend/models.py b/dofile/backend/models.py new file mode 100644 index 0000000..6f285ec --- /dev/null +++ b/dofile/backend/models.py @@ -0,0 +1,342 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +from datetime import datetime + +db = SQLAlchemy() + +class User(UserMixin, db.Model): + """用户模型""" + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(255), nullable=False) + is_admin = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + def to_dict(self): + return { + 'id': self.id, + 'username': self.username, + 'email': self.email, + 'is_admin': self.is_admin, + 'created_at': self.created_at.isoformat() + } + +class Article(db.Model): + """文章/新闻模型""" + __tablename__ = 'articles' + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(200), nullable=False) + content = db.Column(db.Text, nullable=False) + summary = db.Column(db.Text) + category = db.Column(db.String(50), default='news') # news, publication, etc. + is_protected = db.Column(db.Boolean, default=False) # 是否需要登录查看 + author_id = db.Column(db.Integer, db.ForeignKey('users.id')) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + published = db.Column(db.Boolean, default=True) + + author = db.relationship('User', backref='articles') + + def to_dict(self): + return { + 'id': self.id, + 'title': self.title, + 'content': self.content, + 'summary': self.summary, + 'category': self.category, + 'is_protected': self.is_protected, + 'author_id': self.author_id, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat(), + 'published': self.published + } + +class Resource(db.Model): + """资源文件模型""" + __tablename__ = 'resources' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text) + file_path = db.Column(db.String(500), nullable=False) + file_size = db.Column(db.Integer) # 文件大小(字节) + file_type = db.Column(db.String(50)) # 文件类型 + category = db.Column(db.String(50)) # software, dataset, document, etc. + is_protected = db.Column(db.Boolean, default=False) # 是否需要登录下载 + download_count = db.Column(db.Integer, default=0) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'file_path': self.file_path, + 'file_size': self.file_size, + 'file_type': self.file_type, + 'category': self.category, + 'is_protected': self.is_protected, + 'download_count': self.download_count, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat() + } + +class TeamMember(db.Model): + """团队成员模型""" + __tablename__ = 'team_members' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + title = db.Column(db.String(200)) + title_en = db.Column(db.String(200)) + bio = db.Column(db.Text) + bio_en = db.Column(db.Text) + role = db.Column(db.String(50)) # professor, student, alumni + email = db.Column(db.String(120)) + website = db.Column(db.String(500)) + avatar_path = db.Column(db.String(500)) + order = db.Column(db.Integer, default=0) # 排序 + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'title': self.title, + 'title_en': self.title_en, + 'bio': self.bio, + 'bio_en': self.bio_en, + 'role': self.role, + 'email': self.email, + 'website': self.website, + 'avatar_path': self.avatar_path, + 'order': self.order, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat() + } + +class Meeting(db.Model): + """组会模型""" + __tablename__ = 'meetings' + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text) + date = db.Column(db.Date, nullable=False) + time = db.Column(db.Time, nullable=False) + location = db.Column(db.String(200)) + organizer_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + status = db.Column(db.String(50), default='planned') # planned, ongoing, completed, cancelled + meeting_type = db.Column(db.String(50), default='weekly') # weekly, monthly, special + meeting_notes = db.Column(db.Text) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + organizer = db.relationship('User', backref='organized_meetings') + registrations = db.relationship('MeetingRegistration', backref='meeting', cascade='all, delete-orphan') + files = db.relationship('MeetingFile', backref='meeting', cascade='all, delete-orphan') + + def to_dict(self): + return { + 'id': self.id, + 'title': self.title, + 'description': self.description, + 'date': self.date.isoformat() if self.date else None, + 'time': self.time.strftime('%H:%M') if self.time else None, + 'location': self.location, + 'organizer_id': self.organizer_id, + 'organizer_name': self.organizer.username if self.organizer else None, + 'status': self.status, + 'meeting_type': self.meeting_type, + 'meeting_notes': self.meeting_notes, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat(), + 'attendee_count': len(self.registrations) if self.registrations else 0 + } + +class MeetingRegistration(db.Model): + """组会报名模型""" + __tablename__ = 'meeting_registrations' + + id = db.Column(db.Integer, primary_key=True) + meeting_id = db.Column(db.Integer, db.ForeignKey('meetings.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + registered_at = db.Column(db.DateTime, default=datetime.utcnow) + status = db.Column(db.String(50), default='registered') # registered, checked_in, absent + + user = db.relationship('User', backref='meeting_registrations') + + def to_dict(self): + return { + 'id': self.id, + 'meeting_id': self.meeting_id, + 'user_id': self.user_id, + 'username': self.user.username if self.user else None, + 'registered_at': self.registered_at.isoformat(), + 'status': self.status + } + +class MeetingFile(db.Model): + """组会文件模型""" + __tablename__ = 'meeting_files' + + id = db.Column(db.Integer, primary_key=True) + meeting_id = db.Column(db.Integer, db.ForeignKey('meetings.id'), nullable=False) + name = db.Column(db.String(200), nullable=False) + file_path = db.Column(db.String(500), nullable=False) + file_size = db.Column(db.Integer) + file_type = db.Column(db.String(50)) + uploader_id = db.Column(db.Integer, db.ForeignKey('users.id')) + uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) + + uploader = db.relationship('User', backref='uploaded_meeting_files') + + def to_dict(self): + return { + 'id': self.id, + 'meeting_id': self.meeting_id, + 'name': self.name, + 'file_path': self.file_path, + 'file_size': self.file_size, + 'file_type': self.file_type, + 'uploader_id': self.uploader_id, + 'uploaded_at': self.uploaded_at.isoformat() + } + +class ResearchProject(db.Model): + """研究项目模型""" + __tablename__ = 'research_projects' + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text) + start_date = db.Column(db.Date) + end_date = db.Column(db.Date) + owner_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + status = db.Column(db.String(50), default='active') # active, completed, paused, cancelled + progress = db.Column(db.Integer, default=0) # 0-100 + tags = db.Column(db.String(500)) # 逗号分隔的标签 + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + owner = db.relationship('User', backref='research_projects') + notes = db.relationship('ResearchNote', backref='project', cascade='all, delete-orphan') + tasks = db.relationship('ResearchTask', backref='project', cascade='all, delete-orphan') + files = db.relationship('ResearchFile', backref='project', cascade='all, delete-orphan') + + def to_dict(self): + return { + 'id': self.id, + 'title': self.title, + 'description': self.description, + 'start_date': self.start_date.isoformat() if self.start_date else None, + 'end_date': self.end_date.isoformat() if self.end_date else None, + 'owner_id': self.owner_id, + 'owner_name': self.owner.username if self.owner else None, + 'status': self.status, + 'progress': self.progress, + 'tags': self.tags.split(',') if self.tags else [], + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat() + } + +class ResearchNote(db.Model): + """研究笔记模型""" + __tablename__ = 'research_notes' + + id = db.Column(db.Integer, primary_key=True) + project_id = db.Column(db.Integer, db.ForeignKey('research_projects.id'), nullable=False) + title = db.Column(db.String(200), nullable=False) + content = db.Column(db.Text, nullable=False) + author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + note_type = db.Column(db.String(50), default='note') # note, experiment, literature, idea + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + author = db.relationship('User', backref='research_notes') + + def to_dict(self): + return { + 'id': self.id, + 'project_id': self.project_id, + 'title': self.title, + 'content': self.content, + 'author_id': self.author_id, + 'author_name': self.author.username if self.author else None, + 'note_type': self.note_type, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat() + } + +class ResearchTask(db.Model): + """研究任务模型""" + __tablename__ = 'research_tasks' + + id = db.Column(db.Integer, primary_key=True) + project_id = db.Column(db.Integer, db.ForeignKey('research_projects.id'), nullable=False) + title = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text) + assignee_id = db.Column(db.Integer, db.ForeignKey('users.id')) + due_date = db.Column(db.Date) + status = db.Column(db.String(50), default='todo') # todo, in_progress, completed, cancelled + priority = db.Column(db.String(50), default='medium') # high, medium, low + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + assignee = db.relationship('User', backref='assigned_tasks') + + def to_dict(self): + return { + 'id': self.id, + 'project_id': self.project_id, + 'title': self.title, + 'description': self.description, + 'assignee_id': self.assignee_id, + 'assignee_name': self.assignee.username if self.assignee else None, + 'due_date': self.due_date.isoformat() if self.due_date else None, + 'status': self.status, + 'priority': self.priority, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat() + } + +class ResearchFile(db.Model): + """研究文件模型""" + __tablename__ = 'research_files' + + id = db.Column(db.Integer, primary_key=True) + project_id = db.Column(db.Integer, db.ForeignKey('research_projects.id'), nullable=False) + name = db.Column(db.String(200), nullable=False) + file_path = db.Column(db.String(500), nullable=False) + file_size = db.Column(db.Integer) + file_type = db.Column(db.String(50)) + uploader_id = db.Column(db.Integer, db.ForeignKey('users.id')) + uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) + + uploader = db.relationship('User', backref='uploaded_research_files') + + def to_dict(self): + return { + 'id': self.id, + 'project_id': self.project_id, + 'name': self.name, + 'file_path': self.file_path, + 'file_size': self.file_size, + 'file_type': self.file_type, + 'uploader_id': self.uploader_id, + 'uploaded_at': self.uploaded_at.isoformat() + } + diff --git a/dofile/backend/requirements.txt b/dofile/backend/requirements.txt new file mode 100644 index 0000000..411c9c8 --- /dev/null +++ b/dofile/backend/requirements.txt @@ -0,0 +1,17 @@ +Flask==3.0.0 +Flask-SQLAlchemy==3.1.1 +Flask-Login==0.6.3 +Flask-CORS==4.0.0 +Werkzeug==3.0.1 +bcrypt==4.1.2 +python-dotenv==1.0.0 + + + + + + + + + + diff --git a/dofile/backend/routes/__init__.py b/dofile/backend/routes/__init__.py new file mode 100644 index 0000000..12800b8 --- /dev/null +++ b/dofile/backend/routes/__init__.py @@ -0,0 +1,11 @@ +# Routes package + + + + + + + + + + diff --git a/dofile/backend/routes/api.py b/dofile/backend/routes/api.py new file mode 100644 index 0000000..4db2c15 --- /dev/null +++ b/dofile/backend/routes/api.py @@ -0,0 +1,59 @@ +from flask import Blueprint, request, jsonify, send_from_directory +from flask_login import login_required, current_user +import sys +import os + +# 添加父目录到路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from models import db, Resource + +bp = Blueprint('api', __name__) + +@bp.route('/download/', methods=['GET']) +def download_resource(resource_id): + """下载资源文件""" + resource = Resource.query.get_or_404(resource_id) + + # 检查是否需要登录 + if resource.is_protected and not current_user.is_authenticated: + return jsonify({'error': 'Authentication required'}), 401 + + # 增加下载计数 + resource.download_count += 1 + db.session.commit() + + # 返回文件 + uploads_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'uploads') + filename = os.path.basename(resource.file_path) + + return send_from_directory(uploads_dir, filename, as_attachment=True) + +@bp.route('/stats', methods=['GET']) +@login_required +def get_stats(): + """获取统计数据(需要登录)""" + from models import Article, Resource, TeamMember + + stats = { + 'articles': Article.query.count(), + 'resources': Resource.query.count(), + 'team_members': TeamMember.query.count(), + 'total_downloads': db.session.query(db.func.sum(Resource.download_count)).scalar() or 0 + } + + return jsonify({'stats': stats}), 200 + +@bp.route('/protected-content', methods=['GET']) +@login_required +def get_protected_content(): + """获取受保护内容(需要登录)""" + from models import Article, Resource + + protected_articles = Article.query.filter_by(is_protected=True, published=True).all() + protected_resources = Resource.query.filter_by(is_protected=True).all() + + return jsonify({ + 'articles': [article.to_dict() for article in protected_articles], + 'resources': [resource.to_dict() for resource in protected_resources] + }), 200 + diff --git a/dofile/backend/routes/auth.py b/dofile/backend/routes/auth.py new file mode 100644 index 0000000..aaf3225 --- /dev/null +++ b/dofile/backend/routes/auth.py @@ -0,0 +1,87 @@ +from flask import Blueprint, request, jsonify +from flask_login import login_user, logout_user, login_required, current_user +import sys +import os + +# 添加父目录到路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from models import db, User +from werkzeug.security import check_password_hash + +bp = Blueprint('auth', __name__) + +@bp.route('/register', methods=['POST']) +def register(): + """用户注册""" + data = request.get_json() + + if not data or not data.get('username') or not data.get('email') or not data.get('password'): + return jsonify({'error': 'Missing required fields'}), 400 + + # 检查用户名是否已存在 + if User.query.filter_by(username=data['username']).first(): + return jsonify({'error': 'Username already exists'}), 400 + + # 检查邮箱是否已存在 + if User.query.filter_by(email=data['email']).first(): + return jsonify({'error': 'Email already exists'}), 400 + + # 创建新用户 + user = User( + username=data['username'], + email=data['email'], + is_admin=data.get('is_admin', False) + ) + user.set_password(data['password']) + + db.session.add(user) + db.session.commit() + + return jsonify({ + 'message': 'User registered successfully', + 'user': user.to_dict() + }), 201 + +@bp.route('/login', methods=['POST']) +def login(): + """用户登录""" + data = request.get_json() + + if not data or not data.get('username') or not data.get('password'): + return jsonify({'error': 'Username and password required'}), 400 + + user = User.query.filter_by(username=data['username']).first() + + if user and user.check_password(data['password']): + login_user(user, remember=data.get('remember', False)) + return jsonify({ + 'message': 'Login successful', + 'user': user.to_dict() + }), 200 + else: + return jsonify({'error': 'Invalid username or password'}), 401 + +@bp.route('/logout', methods=['POST']) +@login_required +def logout(): + """用户登出""" + logout_user() + return jsonify({'message': 'Logout successful'}), 200 + +@bp.route('/me', methods=['GET']) +@login_required +def get_current_user(): + """获取当前登录用户信息""" + return jsonify({'user': current_user.to_dict()}), 200 + +@bp.route('/check', methods=['GET']) +def check_auth(): + """检查认证状态""" + if current_user.is_authenticated: + return jsonify({ + 'authenticated': True, + 'user': current_user.to_dict() + }), 200 + else: + return jsonify({'authenticated': False}), 200 + diff --git a/dofile/backend/routes/cms.py b/dofile/backend/routes/cms.py new file mode 100644 index 0000000..c7bc832 --- /dev/null +++ b/dofile/backend/routes/cms.py @@ -0,0 +1,227 @@ +from flask import Blueprint, request, jsonify, send_file, current_app +from flask_login import login_required, current_user +from werkzeug.utils import secure_filename +import sys +import os + +# 添加父目录到路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from models import db, Article, Resource, TeamMember +from datetime import datetime + +bp = Blueprint('cms', __name__) + +def allowed_file(filename, allowed_extensions): + """检查文件扩展名是否允许""" + return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions + +# 文章管理 +@bp.route('/articles', methods=['GET']) +def get_articles(): + """获取文章列表""" + category = request.args.get('category', 'news') + published_only = request.args.get('published_only', 'true').lower() == 'true' + + query = Article.query.filter_by(category=category) + + if published_only: + query = query.filter_by(published=True) + + # 如果未登录,过滤受保护内容 + if not current_user.is_authenticated: + query = query.filter_by(is_protected=False) + + articles = query.order_by(Article.created_at.desc()).all() + + return jsonify({ + 'articles': [article.to_dict() for article in articles] + }), 200 + +@bp.route('/articles/', methods=['GET']) +def get_article(article_id): + """获取单篇文章""" + article = Article.query.get_or_404(article_id) + + # 检查是否需要登录 + if article.is_protected and not current_user.is_authenticated: + return jsonify({'error': 'Authentication required'}), 401 + + if not article.published: + if not current_user.is_authenticated or not current_user.is_admin: + return jsonify({'error': 'Article not found'}), 404 + + return jsonify({'article': article.to_dict()}), 200 + +@bp.route('/articles', methods=['POST']) +@login_required +def create_article(): + """创建文章(需要管理员权限)""" + if not current_user.is_admin: + return jsonify({'error': 'Admin access required'}), 403 + + data = request.get_json() + + article = Article( + title=data.get('title'), + content=data.get('content'), + summary=data.get('summary'), + category=data.get('category', 'news'), + is_protected=data.get('is_protected', False), + author_id=current_user.id, + published=data.get('published', True) + ) + + db.session.add(article) + db.session.commit() + + return jsonify({ + 'message': 'Article created successfully', + 'article': article.to_dict() + }), 201 + +@bp.route('/articles/', methods=['PUT']) +@login_required +def update_article(article_id): + """更新文章(需要管理员权限)""" + if not current_user.is_admin: + return jsonify({'error': 'Admin access required'}), 403 + + article = Article.query.get_or_404(article_id) + data = request.get_json() + + article.title = data.get('title', article.title) + article.content = data.get('content', article.content) + article.summary = data.get('summary', article.summary) + article.category = data.get('category', article.category) + article.is_protected = data.get('is_protected', article.is_protected) + article.published = data.get('published', article.published) + article.updated_at = datetime.utcnow() + + db.session.commit() + + return jsonify({ + 'message': 'Article updated successfully', + 'article': article.to_dict() + }), 200 + +@bp.route('/articles/', methods=['DELETE']) +@login_required +def delete_article(article_id): + """删除文章(需要管理员权限)""" + if not current_user.is_admin: + return jsonify({'error': 'Admin access required'}), 403 + + article = Article.query.get_or_404(article_id) + db.session.delete(article) + db.session.commit() + + return jsonify({'message': 'Article deleted successfully'}), 200 + +# 资源管理 +@bp.route('/resources', methods=['GET']) +def get_resources(): + """获取资源列表""" + category = request.args.get('category') + + query = Resource.query + + if category: + query = query.filter_by(category=category) + + # 如果未登录,过滤受保护资源 + if not current_user.is_authenticated: + query = query.filter_by(is_protected=False) + + resources = query.order_by(Resource.created_at.desc()).all() + + return jsonify({ + 'resources': [resource.to_dict() for resource in resources] + }), 200 + +@bp.route('/resources', methods=['POST']) +@login_required +def upload_resource(): + """上传资源文件(需要管理员权限)""" + if not current_user.is_admin: + return jsonify({'error': 'Admin access required'}), 403 + + if 'file' not in request.files: + return jsonify({'error': 'No file provided'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'error': 'No file selected'}), 400 + + if file and allowed_file(file.filename, {'pdf', 'zip', 'rar', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'}): + filename = secure_filename(file.filename) + uploads_dir = current_app.config['UPLOAD_FOLDER'] + os.makedirs(uploads_dir, exist_ok=True) + filepath = os.path.join(uploads_dir, filename) + file.save(filepath) + + resource = Resource( + name=request.form.get('name', filename), + description=request.form.get('description', ''), + file_path=f'/uploads/{filename}', + file_size=os.path.getsize(filepath), + file_type=filename.rsplit('.', 1)[1].lower(), + category=request.form.get('category', 'document'), + is_protected=request.form.get('is_protected', 'false').lower() == 'true' + ) + + db.session.add(resource) + db.session.commit() + + return jsonify({ + 'message': 'Resource uploaded successfully', + 'resource': resource.to_dict() + }), 201 + + return jsonify({'error': 'Invalid file type'}), 400 + +# 团队成员管理 +@bp.route('/team', methods=['GET']) +def get_team(): + """获取团队成员列表""" + role = request.args.get('role') + + query = TeamMember.query + + if role: + query = query.filter_by(role=role) + + members = query.order_by(TeamMember.order, TeamMember.created_at).all() + + return jsonify({ + 'members': [member.to_dict() for member in members] + }), 200 + +@bp.route('/team', methods=['POST']) +@login_required +def create_team_member(): + """创建团队成员(需要管理员权限)""" + if not current_user.is_admin: + return jsonify({'error': 'Admin access required'}), 403 + + data = request.get_json() + + member = TeamMember( + name=data.get('name'), + title=data.get('title'), + title_en=data.get('title_en'), + bio=data.get('bio'), + bio_en=data.get('bio_en'), + role=data.get('role', 'student'), + email=data.get('email'), + website=data.get('website'), + order=data.get('order', 0) + ) + + db.session.add(member) + db.session.commit() + + return jsonify({ + 'message': 'Team member created successfully', + 'member': member.to_dict() + }), 201 + diff --git a/dofile/backend/routes/meetings.py b/dofile/backend/routes/meetings.py new file mode 100644 index 0000000..3db9b9f --- /dev/null +++ b/dofile/backend/routes/meetings.py @@ -0,0 +1,256 @@ +from flask import Blueprint, request, jsonify, send_from_directory, current_app +from flask_login import login_required, current_user +from werkzeug.utils import secure_filename +import sys +import os +from datetime import datetime, date, time + +# 添加父目录到路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from models import db, Meeting, MeetingRegistration, MeetingFile, User + +bp = Blueprint('meetings', __name__) + +def allowed_file(filename, allowed_extensions): + """检查文件扩展名是否允许""" + return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions + +@bp.route('/meetings', methods=['GET']) +@login_required +def get_meetings(): + """获取组会列表(实验室成员可查看所有)""" + status = request.args.get('status') + meeting_type = request.args.get('type') + + query = Meeting.query + + if status: + query = query.filter_by(status=status) + if meeting_type: + query = query.filter_by(meeting_type=meeting_type) + + meetings = query.order_by(Meeting.date.desc(), Meeting.time.desc()).all() + + return jsonify({ + 'meetings': [meeting.to_dict() for meeting in meetings] + }), 200 + +@bp.route('/meetings/', methods=['GET']) +@login_required +def get_meeting(meeting_id): + """获取单个组会详情""" + meeting = Meeting.query.get_or_404(meeting_id) + + # 获取参会人员列表 + registrations = MeetingRegistration.query.filter_by(meeting_id=meeting_id).all() + attendees = [reg.to_dict() for reg in registrations] + + # 获取文件列表 + files = MeetingFile.query.filter_by(meeting_id=meeting_id).all() + file_list = [f.to_dict() for f in files] + + result = meeting.to_dict() + result['attendees'] = attendees + result['files'] = file_list + + return jsonify({'meeting': result}), 200 + +@bp.route('/meetings', methods=['POST']) +@login_required +def create_meeting(): + """创建组会(需要登录)""" + data = request.get_json() + + # 解析日期和时间 + meeting_date = datetime.strptime(data['date'], '%Y-%m-%d').date() if data.get('date') else None + meeting_time = datetime.strptime(data['time'], '%H:%M').time() if data.get('time') else None + + meeting = Meeting( + title=data.get('title'), + description=data.get('description'), + date=meeting_date, + time=meeting_time, + location=data.get('location'), + organizer_id=current_user.id, + status=data.get('status', 'planned'), + meeting_type=data.get('meeting_type', 'weekly') + ) + + db.session.add(meeting) + db.session.commit() + + return jsonify({ + 'message': 'Meeting created successfully', + 'meeting': meeting.to_dict() + }), 201 + +@bp.route('/meetings/', methods=['PUT']) +@login_required +def update_meeting(meeting_id): + """更新组会(只有创建者可以编辑)""" + meeting = Meeting.query.get_or_404(meeting_id) + + # 检查权限:只有创建者可以编辑 + if meeting.organizer_id != current_user.id: + return jsonify({'error': 'Only the organizer can edit this meeting'}), 403 + + data = request.get_json() + + if 'title' in data: + meeting.title = data['title'] + if 'description' in data: + meeting.description = data['description'] + if 'date' in data: + meeting.date = datetime.strptime(data['date'], '%Y-%m-%d').date() + if 'time' in data: + meeting.time = datetime.strptime(data['time'], '%H:%M').time() + if 'location' in data: + meeting.location = data['location'] + if 'status' in data: + meeting.status = data['status'] + if 'meeting_type' in data: + meeting.meeting_type = data['meeting_type'] + if 'meeting_notes' in data: + meeting.meeting_notes = data['meeting_notes'] + + meeting.updated_at = datetime.utcnow() + db.session.commit() + + return jsonify({ + 'message': 'Meeting updated successfully', + 'meeting': meeting.to_dict() + }), 200 + +@bp.route('/meetings/', methods=['DELETE']) +@login_required +def delete_meeting(meeting_id): + """删除组会(只有创建者可以删除)""" + meeting = Meeting.query.get_or_404(meeting_id) + + # 检查权限 + if meeting.organizer_id != current_user.id: + return jsonify({'error': 'Only the organizer can delete this meeting'}), 403 + + db.session.delete(meeting) + db.session.commit() + + return jsonify({'message': 'Meeting deleted successfully'}), 200 + +@bp.route('/meetings//register', methods=['POST']) +@login_required +def register_meeting(meeting_id): + """报名参加组会""" + meeting = Meeting.query.get_or_404(meeting_id) + + # 检查是否已报名 + existing = MeetingRegistration.query.filter_by( + meeting_id=meeting_id, + user_id=current_user.id + ).first() + + if existing: + return jsonify({'error': 'Already registered'}), 400 + + registration = MeetingRegistration( + meeting_id=meeting_id, + user_id=current_user.id, + status='registered' + ) + + db.session.add(registration) + db.session.commit() + + return jsonify({ + 'message': 'Registered successfully', + 'registration': registration.to_dict() + }), 201 + +@bp.route('/meetings//checkin', methods=['POST']) +@login_required +def checkin_meeting(meeting_id): + """签到组会""" + registration = MeetingRegistration.query.filter_by( + meeting_id=meeting_id, + user_id=current_user.id + ).first_or_404() + + registration.status = 'checked_in' + db.session.commit() + + return jsonify({ + 'message': 'Checked in successfully', + 'registration': registration.to_dict() + }), 200 + +@bp.route('/meetings//files', methods=['POST']) +@login_required +def upload_meeting_file(meeting_id): + """上传组会文件(只有创建者可以上传)""" + meeting = Meeting.query.get_or_404(meeting_id) + + # 检查权限 + if meeting.organizer_id != current_user.id: + return jsonify({'error': 'Only the organizer can upload files'}), 403 + + if 'file' not in request.files: + return jsonify({'error': 'No file provided'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'error': 'No file selected'}), 400 + + if file and allowed_file(file.filename, {'pdf', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'zip', 'rar'}): + filename = secure_filename(file.filename) + uploads_dir = current_app.config['UPLOAD_FOLDER'] + meeting_dir = os.path.join(uploads_dir, 'meetings', str(meeting_id)) + os.makedirs(meeting_dir, exist_ok=True) + + filepath = os.path.join(meeting_dir, filename) + file.save(filepath) + + meeting_file = MeetingFile( + meeting_id=meeting_id, + name=request.form.get('name', filename), + file_path=f'/uploads/meetings/{meeting_id}/{filename}', + file_size=os.path.getsize(filepath), + file_type=filename.rsplit('.', 1)[1].lower(), + uploader_id=current_user.id + ) + + db.session.add(meeting_file) + db.session.commit() + + return jsonify({ + 'message': 'File uploaded successfully', + 'file': meeting_file.to_dict() + }), 201 + + return jsonify({'error': 'Invalid file type'}), 400 + +@bp.route('/meetings//files/', methods=['GET']) +@login_required +def download_meeting_file(meeting_id, file_id): + """下载组会文件""" + meeting_file = MeetingFile.query.filter_by( + meeting_id=meeting_id, + id=file_id + ).first_or_404() + + uploads_dir = current_app.config['UPLOAD_FOLDER'] + file_path = os.path.join(uploads_dir, 'meetings', str(meeting_id), os.path.basename(meeting_file.file_path)) + + return send_from_directory( + os.path.dirname(file_path), + os.path.basename(file_path), + as_attachment=True + ) + + + + + + + + + + diff --git a/dofile/backend/routes/research.py b/dofile/backend/routes/research.py new file mode 100644 index 0000000..ce1fffa --- /dev/null +++ b/dofile/backend/routes/research.py @@ -0,0 +1,410 @@ +from flask import Blueprint, request, jsonify, send_from_directory, current_app +from flask_login import login_required, current_user +from werkzeug.utils import secure_filename +import sys +import os +from datetime import datetime, date + +# 添加父目录到路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from models import db, ResearchProject, ResearchNote, ResearchTask, ResearchFile, User + +bp = Blueprint('research', __name__) + +def allowed_file(filename, allowed_extensions): + """检查文件扩展名是否允许""" + return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions + +# 研究项目管理 +@bp.route('/projects', methods=['GET']) +@login_required +def get_projects(): + """获取研究项目列表(实验室成员可查看所有)""" + status = request.args.get('status') + owner_id = request.args.get('owner_id') # 可选:筛选特定用户的项目 + + query = ResearchProject.query + + if status: + query = query.filter_by(status=status) + if owner_id: + query = query.filter_by(owner_id=owner_id) + + projects = query.order_by(ResearchProject.created_at.desc()).all() + + return jsonify({ + 'projects': [project.to_dict() for project in projects] + }), 200 + +@bp.route('/projects/', methods=['GET']) +@login_required +def get_project(project_id): + """获取单个研究项目详情""" + project = ResearchProject.query.get_or_404(project_id) + + # 获取笔记、任务、文件 + notes = ResearchNote.query.filter_by(project_id=project_id).order_by(ResearchNote.created_at.desc()).all() + tasks = ResearchTask.query.filter_by(project_id=project_id).order_by(ResearchTask.created_at.desc()).all() + files = ResearchFile.query.filter_by(project_id=project_id).order_by(ResearchFile.uploaded_at.desc()).all() + + result = project.to_dict() + result['notes'] = [note.to_dict() for note in notes] + result['tasks'] = [task.to_dict() for task in tasks] + result['files'] = [f.to_dict() for f in files] + + return jsonify({'project': result}), 200 + +@bp.route('/projects', methods=['POST']) +@login_required +def create_project(): + """创建研究项目(需要登录)""" + data = request.get_json() + + start_date = datetime.strptime(data['start_date'], '%Y-%m-%d').date() if data.get('start_date') else None + end_date = datetime.strptime(data['end_date'], '%Y-%m-%d').date() if data.get('end_date') else None + tags_str = ','.join(data.get('tags', [])) if isinstance(data.get('tags'), list) else data.get('tags', '') + + project = ResearchProject( + title=data.get('title'), + description=data.get('description'), + start_date=start_date, + end_date=end_date, + owner_id=current_user.id, + status=data.get('status', 'active'), + progress=data.get('progress', 0), + tags=tags_str + ) + + db.session.add(project) + db.session.commit() + + return jsonify({ + 'message': 'Project created successfully', + 'project': project.to_dict() + }), 201 + +@bp.route('/projects/', methods=['PUT']) +@login_required +def update_project(project_id): + """更新研究项目(只有创建者可以编辑)""" + project = ResearchProject.query.get_or_404(project_id) + + # 检查权限 + if project.owner_id != current_user.id: + return jsonify({'error': 'Only the owner can edit this project'}), 403 + + data = request.get_json() + + if 'title' in data: + project.title = data['title'] + if 'description' in data: + project.description = data['description'] + if 'start_date' in data: + project.start_date = datetime.strptime(data['start_date'], '%Y-%m-%d').date() if data['start_date'] else None + if 'end_date' in data: + project.end_date = datetime.strptime(data['end_date'], '%Y-%m-%d').date() if data['end_date'] else None + if 'status' in data: + project.status = data['status'] + if 'progress' in data: + project.progress = data['progress'] + if 'tags' in data: + tags_str = ','.join(data['tags']) if isinstance(data['tags'], list) else data['tags'] + project.tags = tags_str + + project.updated_at = datetime.utcnow() + db.session.commit() + + return jsonify({ + 'message': 'Project updated successfully', + 'project': project.to_dict() + }), 200 + +@bp.route('/projects/', methods=['DELETE']) +@login_required +def delete_project(project_id): + """删除研究项目(只有创建者可以删除)""" + project = ResearchProject.query.get_or_404(project_id) + + # 检查权限 + if project.owner_id != current_user.id: + return jsonify({'error': 'Only the owner can delete this project'}), 403 + + db.session.delete(project) + db.session.commit() + + return jsonify({'message': 'Project deleted successfully'}), 200 + +# 研究笔记管理 +@bp.route('/projects//notes', methods=['GET']) +@login_required +def get_notes(project_id): + """获取项目笔记列表""" + note_type = request.args.get('type') + + query = ResearchNote.query.filter_by(project_id=project_id) + + if note_type: + query = query.filter_by(note_type=note_type) + + notes = query.order_by(ResearchNote.created_at.desc()).all() + + return jsonify({ + 'notes': [note.to_dict() for note in notes] + }), 200 + +@bp.route('/projects//notes', methods=['POST']) +@login_required +def create_note(project_id): + """创建研究笔记(需要登录)""" + project = ResearchProject.query.get_or_404(project_id) + + data = request.get_json() + + note = ResearchNote( + project_id=project_id, + title=data.get('title'), + content=data.get('content'), + author_id=current_user.id, + note_type=data.get('note_type', 'note') + ) + + db.session.add(note) + db.session.commit() + + return jsonify({ + 'message': 'Note created successfully', + 'note': note.to_dict() + }), 201 + +@bp.route('/notes/', methods=['PUT']) +@login_required +def update_note(note_id): + """更新研究笔记(只有创建者可以编辑)""" + note = ResearchNote.query.get_or_404(note_id) + + # 检查权限 + if note.author_id != current_user.id: + return jsonify({'error': 'Only the author can edit this note'}), 403 + + data = request.get_json() + + if 'title' in data: + note.title = data['title'] + if 'content' in data: + note.content = data['content'] + if 'note_type' in data: + note.note_type = data['note_type'] + + note.updated_at = datetime.utcnow() + db.session.commit() + + return jsonify({ + 'message': 'Note updated successfully', + 'note': note.to_dict() + }), 200 + +@bp.route('/notes/', methods=['DELETE']) +@login_required +def delete_note(note_id): + """删除研究笔记(只有创建者可以删除)""" + note = ResearchNote.query.get_or_404(note_id) + + # 检查权限 + if note.author_id != current_user.id: + return jsonify({'error': 'Only the author can delete this note'}), 403 + + db.session.delete(note) + db.session.commit() + + return jsonify({'message': 'Note deleted successfully'}), 200 + +# 研究任务管理 +@bp.route('/projects//tasks', methods=['GET']) +@login_required +def get_tasks(project_id): + """获取项目任务列表""" + status = request.args.get('status') + assignee_id = request.args.get('assignee_id') + + query = ResearchTask.query.filter_by(project_id=project_id) + + if status: + query = query.filter_by(status=status) + if assignee_id: + query = query.filter_by(assignee_id=assignee_id) + + tasks = query.order_by(ResearchTask.due_date, ResearchTask.priority).all() + + return jsonify({ + 'tasks': [task.to_dict() for task in tasks] + }), 200 + +@bp.route('/projects//tasks', methods=['POST']) +@login_required +def create_task(project_id): + """创建研究任务(项目创建者可以创建)""" + project = ResearchProject.query.get_or_404(project_id) + + # 只有项目创建者可以创建任务 + if project.owner_id != current_user.id: + return jsonify({'error': 'Only the project owner can create tasks'}), 403 + + data = request.get_json() + + due_date = datetime.strptime(data['due_date'], '%Y-%m-%d').date() if data.get('due_date') else None + + task = ResearchTask( + project_id=project_id, + title=data.get('title'), + description=data.get('description'), + assignee_id=data.get('assignee_id'), + due_date=due_date, + status=data.get('status', 'todo'), + priority=data.get('priority', 'medium') + ) + + db.session.add(task) + db.session.commit() + + return jsonify({ + 'message': 'Task created successfully', + 'task': task.to_dict() + }), 201 + +@bp.route('/tasks/', methods=['PUT']) +@login_required +def update_task(task_id): + """更新研究任务(项目创建者或任务分配者可以编辑)""" + task = ResearchTask.query.get_or_404(task_id) + project = ResearchProject.query.get(task.project_id) + + # 检查权限:项目创建者或任务分配者 + if project.owner_id != current_user.id and task.assignee_id != current_user.id: + return jsonify({'error': 'Only the project owner or assignee can edit this task'}), 403 + + data = request.get_json() + + if 'title' in data: + task.title = data['title'] + if 'description' in data: + task.description = data['description'] + if 'assignee_id' in data: + task.assignee_id = data['assignee_id'] + if 'due_date' in data: + task.due_date = datetime.strptime(data['due_date'], '%Y-%m-%d').date() if data['due_date'] else None + if 'status' in data: + task.status = data['status'] + if 'priority' in data: + task.priority = data['priority'] + + task.updated_at = datetime.utcnow() + db.session.commit() + + return jsonify({ + 'message': 'Task updated successfully', + 'task': task.to_dict() + }), 200 + +@bp.route('/tasks/', methods=['DELETE']) +@login_required +def delete_task(task_id): + """删除研究任务(只有项目创建者可以删除)""" + task = ResearchTask.query.get_or_404(task_id) + project = ResearchProject.query.get(task.project_id) + + # 检查权限 + if project.owner_id != current_user.id: + return jsonify({'error': 'Only the project owner can delete this task'}), 403 + + db.session.delete(task) + db.session.commit() + + return jsonify({'message': 'Task deleted successfully'}), 200 + +# 研究文件管理 +@bp.route('/projects//files', methods=['POST']) +@login_required +def upload_research_file(project_id): + """上传研究文件(需要登录)""" + project = ResearchProject.query.get_or_404(project_id) + + if 'file' not in request.files: + return jsonify({'error': 'No file provided'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'error': 'No file selected'}), 400 + + if file and allowed_file(file.filename, {'pdf', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'zip', 'rar', 'txt', 'csv', 'json', 'py', 'ipynb'}): + filename = secure_filename(file.filename) + uploads_dir = current_app.config['UPLOAD_FOLDER'] + project_dir = os.path.join(uploads_dir, 'research', str(project_id)) + os.makedirs(project_dir, exist_ok=True) + + filepath = os.path.join(project_dir, filename) + file.save(filepath) + + research_file = ResearchFile( + project_id=project_id, + name=request.form.get('name', filename), + file_path=f'/uploads/research/{project_id}/{filename}', + file_size=os.path.getsize(filepath), + file_type=filename.rsplit('.', 1)[1].lower(), + uploader_id=current_user.id + ) + + db.session.add(research_file) + db.session.commit() + + return jsonify({ + 'message': 'File uploaded successfully', + 'file': research_file.to_dict() + }), 201 + + return jsonify({'error': 'Invalid file type'}), 400 + +@bp.route('/projects//files/', methods=['GET']) +@login_required +def download_research_file(project_id, file_id): + """下载研究文件""" + research_file = ResearchFile.query.filter_by( + project_id=project_id, + id=file_id + ).first_or_404() + + uploads_dir = current_app.config['UPLOAD_FOLDER'] + file_path = os.path.join(uploads_dir, 'research', str(project_id), os.path.basename(research_file.file_path)) + + return send_from_directory( + os.path.dirname(file_path), + os.path.basename(file_path), + as_attachment=True + ) + +@bp.route('/projects//files/', methods=['DELETE']) +@login_required +def delete_research_file(project_id, file_id): + """删除研究文件(只有上传者可以删除)""" + research_file = ResearchFile.query.filter_by( + project_id=project_id, + id=file_id + ).first_or_404() + + # 检查权限 + if research_file.uploader_id != current_user.id: + return jsonify({'error': 'Only the uploader can delete this file'}), 403 + + db.session.delete(research_file) + db.session.commit() + + return jsonify({'message': 'File deleted successfully'}), 200 + + + + + + + + + + diff --git a/dofile/css/common.css b/dofile/css/common.css new file mode 100644 index 0000000..60e4142 --- /dev/null +++ b/dofile/css/common.css @@ -0,0 +1,196 @@ +/* Common Styles for DAEL Lab Website */ + +body { + font-family: 'Inter', 'Noto Sans SC', sans-serif; + background-color: #ffffff; + color: #1a1a1a; + overflow-x: hidden; +} + +/* Custom Scrollbar */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* Navigation Link Animation */ +.nav-link { + position: relative; +} + +.nav-link::after { + content: ''; + position: absolute; + width: 0; + height: 1px; + bottom: -2px; + left: 0; + background-color: #000; + transition: width 0.3s ease; +} + +.nav-link:hover::after { + width: 100%; +} + +/* Fade In Up Animation */ +.fade-in-up { + animation: fadeInUp 0.8s ease-out forwards; + opacity: 0; + transform: translateY(20px); +} + +@keyframes fadeInUp { + to { + opacity: 1; + transform: translateY(0); + } +} + +.delay-100 { animation-delay: 0.1s; } +.delay-200 { animation-delay: 0.2s; } +.delay-300 { animation-delay: 0.3s; } +.delay-400 { animation-delay: 0.4s; } + +/* Canvas container */ +#canvas-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + opacity: 0.6; + pointer-events: none; +} + +/* News Card Styles */ +.news-card { + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.news-card:hover { + transform: translateY(-4px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); +} + +/* Search Box */ +.search-box { + position: relative; +} + +.search-results { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + max-height: 400px; + overflow-y: auto; + z-index: 100; + display: none; +} + +.search-results.active { + display: block; +} + +.search-result-item { + padding: 12px 16px; + border-bottom: 1px solid #f3f4f6; + cursor: pointer; + transition: background-color 0.2s; +} + +.search-result-item:hover { + background-color: #f9fafb; +} + +.search-result-item:last-child { + border-bottom: none; +} + +/* Login Form */ +.login-form { + max-width: 400px; + margin: 0 auto; +} + +.form-input { + width: 100%; + padding: 12px 16px; + border: 1px solid #e5e7eb; + border-radius: 4px; + font-size: 14px; + transition: border-color 0.3s; +} + +.form-input:focus { + outline: none; + border-color: #000; +} + +/* Protected Content */ +.protected-content { + opacity: 0.5; + pointer-events: none; + position: relative; +} + +.protected-content::after { + content: '登录后查看'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 8px 16px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; +} + +/* Page Transition */ +.page-transition { + animation: fadeIn 0.3s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* Stats Counter */ +.stats-counter { + font-variant-numeric: tabular-nums; +} + + + + + + + + + + diff --git a/dofile/data/dael.db b/dofile/data/dael.db new file mode 100644 index 0000000..0432ae3 Binary files /dev/null and b/dofile/data/dael.db differ diff --git a/dofile/js/admin.js b/dofile/js/admin.js new file mode 100644 index 0000000..58b39fe --- /dev/null +++ b/dofile/js/admin.js @@ -0,0 +1,390 @@ +// Admin Panel JavaScript + +let currentArticles = []; +let currentResources = []; +let currentTeamMembers = []; + +// 检查管理员权限 +async function checkAdminAccess() { + try { + const response = await window.DAELAPI.auth.checkAuth(); + if (!response.authenticated || !response.user.is_admin) { + alert('需要管理员权限才能访问此页面'); + window.location.href = 'website.html'; + return false; + } + document.getElementById('admin-user').textContent = `管理员: ${response.user.username}`; + return true; + } catch (error) { + alert('无法验证权限,请先登录'); + window.location.href = 'login.html'; + return false; + } +} + +// Tab切换 +document.querySelectorAll('.admin-tab').forEach(tab => { + tab.addEventListener('click', () => { + const tabName = tab.dataset.tab; + + // 更新tab样式 + document.querySelectorAll('.admin-tab').forEach(t => { + t.classList.remove('active', 'border-black', 'text-black'); + t.classList.add('text-gray-500'); + }); + tab.classList.add('active', 'border-black', 'text-black'); + tab.classList.remove('text-gray-500'); + + // 更新内容 + document.querySelectorAll('.admin-tab-content').forEach(content => { + content.classList.add('hidden'); + }); + document.getElementById(`tab-${tabName}`).classList.remove('hidden'); + + // 加载对应内容 + if (tabName === 'articles') loadArticles(); + else if (tabName === 'resources') loadResources(); + else if (tabName === 'team') loadTeamMembers(); + }); +}); + +// 加载文章列表 +async function loadArticles() { + try { + const response = await window.DAELAPI.content.getArticles('news', false); + currentArticles = response.articles; + displayArticles(currentArticles); + } catch (error) { + console.error('Failed to load articles:', error); + document.getElementById('articles-list').innerHTML = '

加载失败

'; + } +} + +// 显示文章列表 +function displayArticles(articles) { + const container = document.getElementById('articles-list'); + if (articles.length === 0) { + container.innerHTML = '

暂无文章

'; + return; + } + + container.innerHTML = articles.map(article => ` +
+
+
+

${article.title}

+

${article.summary || article.content.substring(0, 100)}...

+
+ 分类: ${article.category} + ${article.is_protected ? '🔒 受保护' : '公开'} + ${article.published ? '已发布' : '草稿'} + ${new Date(article.created_at).toLocaleDateString()} +
+
+
+ + +
+
+
+ `).join(''); +} + +// 显示文章表单 +function showArticleForm(articleId = null) { + const modal = document.getElementById('article-modal'); + const form = document.getElementById('article-form'); + const title = document.getElementById('article-modal-title'); + + if (articleId) { + const article = currentArticles.find(a => a.id === articleId); + if (article) { + title.textContent = '编辑文章'; + document.getElementById('article-id').value = article.id; + document.getElementById('article-title').value = article.title; + document.getElementById('article-summary').value = article.summary || ''; + document.getElementById('article-content').value = article.content; + document.getElementById('article-category').value = article.category; + document.getElementById('article-published').value = article.published.toString(); + document.getElementById('article-protected').checked = article.is_protected; + } + } else { + title.textContent = '新建文章'; + form.reset(); + document.getElementById('article-id').value = ''; + } + + modal.classList.remove('hidden'); +} + +// 关闭文章表单 +function closeArticleForm() { + document.getElementById('article-modal').classList.add('hidden'); +} + +// 保存文章 +async function saveArticle(event) { + event.preventDefault(); + + const articleId = document.getElementById('article-id').value; + const articleData = { + title: document.getElementById('article-title').value, + summary: document.getElementById('article-summary').value, + content: document.getElementById('article-content').value, + category: document.getElementById('article-category').value, + published: document.getElementById('article-published').value === 'true', + is_protected: document.getElementById('article-protected').checked + }; + + try { + if (articleId) { + await window.DAELAPI.content.updateArticle(articleId, articleData); + } else { + await window.DAELAPI.content.createArticle(articleData); + } + closeArticleForm(); + loadArticles(); + } catch (error) { + alert('保存失败: ' + error.message); + } +} + +// 编辑文章 +function editArticle(articleId) { + showArticleForm(articleId); +} + +// 删除文章 +async function deleteArticle(articleId) { + if (!confirm('确定要删除这篇文章吗?')) return; + + try { + await window.DAELAPI.content.deleteArticle(articleId); + loadArticles(); + } catch (error) { + alert('删除失败: ' + error.message); + } +} + +// 加载资源列表 +async function loadResources() { + try { + const response = await window.DAELAPI.content.getResources(); + currentResources = response.resources; + displayResources(currentResources); + } catch (error) { + console.error('Failed to load resources:', error); + document.getElementById('resources-list').innerHTML = '

加载失败

'; + } +} + +// 显示资源列表 +function displayResources(resources) { + const container = document.getElementById('resources-list'); + if (resources.length === 0) { + container.innerHTML = '

暂无资源

'; + return; + } + + container.innerHTML = resources.map(resource => ` +
+
+
+

${resource.name}

+

${resource.description || ''}

+
+ 分类: ${resource.category} + ${resource.is_protected ? '🔒 受保护' : '公开'} + 大小: ${(resource.file_size / 1024 / 1024).toFixed(2)} MB + 下载: ${resource.download_count} 次 +
+
+
+ + +
+
+
+ `).join(''); +} + +// 显示资源表单 +function showResourceForm() { + document.getElementById('resource-modal').classList.remove('hidden'); +} + +// 关闭资源表单 +function closeResourceForm() { + document.getElementById('resource-modal').classList.add('hidden'); + document.getElementById('resource-form').reset(); +} + +// 保存资源 +async function saveResource(event) { + event.preventDefault(); + + const fileInput = document.getElementById('resource-file'); + if (!fileInput.files[0]) { + alert('请选择文件'); + return; + } + + try { + await window.DAELAPI.content.uploadResource( + fileInput.files[0], + document.getElementById('resource-name').value, + document.getElementById('resource-description').value, + document.getElementById('resource-category').value, + document.getElementById('resource-protected').checked + ); + closeResourceForm(); + loadResources(); + } catch (error) { + alert('上传失败: ' + error.message); + } +} + +// 删除资源 +async function deleteResource(resourceId) { + if (!confirm('确定要删除这个资源吗?')) return; + // 注意:需要添加删除资源的API端点 + alert('删除功能需要后端支持'); +} + +// 加载团队成员 +async function loadTeamMembers() { + try { + const response = await window.DAELAPI.content.getTeam(); + currentTeamMembers = response.members; + displayTeamMembers(currentTeamMembers); + } catch (error) { + console.error('Failed to load team members:', error); + document.getElementById('team-list').innerHTML = '

加载失败

'; + } +} + +// 显示团队成员 +function displayTeamMembers(members) { + const container = document.getElementById('team-list'); + if (members.length === 0) { + container.innerHTML = '

暂无成员

'; + return; + } + + container.innerHTML = members.map(member => ` +
+
+
+

${member.name}

+

${member.title || ''}

+

${member.bio || ''}

+
+ 角色: ${member.role} + 排序: ${member.order} +
+
+
+ + +
+
+
+ `).join(''); +} + +// 显示团队成员表单 +function showTeamForm(memberId = null) { + const modal = document.getElementById('team-modal'); + const form = document.getElementById('team-form'); + + if (memberId) { + const member = currentTeamMembers.find(m => m.id === memberId); + if (member) { + document.getElementById('team-id').value = member.id; + document.getElementById('team-name').value = member.name; + document.getElementById('team-title').value = member.title || ''; + document.getElementById('team-title-en').value = member.title_en || ''; + document.getElementById('team-bio').value = member.bio || ''; + document.getElementById('team-bio-en').value = member.bio_en || ''; + document.getElementById('team-role').value = member.role; + document.getElementById('team-order').value = member.order; + document.getElementById('team-email').value = member.email || ''; + document.getElementById('team-website').value = member.website || ''; + } + } else { + form.reset(); + document.getElementById('team-id').value = ''; + } + + modal.classList.remove('hidden'); +} + +// 关闭团队成员表单 +function closeTeamForm() { + document.getElementById('team-modal').classList.add('hidden'); +} + +// 保存团队成员 +async function saveTeamMember(event) { + event.preventDefault(); + + const memberId = document.getElementById('team-id').value; + const memberData = { + name: document.getElementById('team-name').value, + title: document.getElementById('team-title').value, + title_en: document.getElementById('team-title-en').value, + bio: document.getElementById('team-bio').value, + bio_en: document.getElementById('team-bio-en').value, + role: document.getElementById('team-role').value, + order: parseInt(document.getElementById('team-order').value) || 0, + email: document.getElementById('team-email').value, + website: document.getElementById('team-website').value + }; + + try { + if (memberId) { + // 更新功能需要后端支持 + alert('更新功能需要后端支持'); + } else { + await window.DAELAPI.content.createTeamMember(memberData); + } + closeTeamForm(); + loadTeamMembers(); + } catch (error) { + alert('保存失败: ' + error.message); + } +} + +// 编辑团队成员 +function editTeamMember(memberId) { + showTeamForm(memberId); +} + +// 删除团队成员 +async function deleteTeamMember(memberId) { + if (!confirm('确定要删除这个成员吗?')) return; + // 注意:需要添加删除成员的API端点 + alert('删除功能需要后端支持'); +} + +// 初始化 +document.addEventListener('DOMContentLoaded', async () => { + if (await checkAdminAccess()) { + loadArticles(); + + // 退出按钮 + document.getElementById('logout-btn').addEventListener('click', async () => { + await logout(); + }); + } +}); + + + + + + + + + + diff --git a/dofile/js/api.js b/dofile/js/api.js new file mode 100644 index 0000000..c0a53c8 --- /dev/null +++ b/dofile/js/api.js @@ -0,0 +1,362 @@ +// API Client for DAEL Lab Backend + +const API_BASE_URL = 'http://localhost:5000/api'; + +// 辅助函数:发送请求 +async function apiRequest(endpoint, options = {}) { + const url = `${API_BASE_URL}${endpoint}`; + const config = { + credentials: 'include', // 包含cookies + headers: { + 'Content-Type': 'application/json', + ...options.headers + }, + ...options + }; + + if (config.body && typeof config.body === 'object' && !(config.body instanceof FormData)) { + config.body = JSON.stringify(config.body); + } + + try { + const response = await fetch(url, config); + + // 检查响应内容类型 + const contentType = response.headers.get('content-type'); + let data; + + if (contentType && contentType.includes('application/json')) { + data = await response.json(); + } else { + const text = await response.text(); + try { + data = JSON.parse(text); + } catch { + throw new Error(text || 'Invalid response'); + } + } + + if (!response.ok) { + throw new Error(data.error || `Request failed with status ${response.status}`); + } + + return data; + } catch (error) { + console.error('API request failed:', error); + console.error('URL:', url); + console.error('Config:', config); + throw error; + } +} + +// 认证API +const authAPI = { + async login(username, password, remember = false) { + return apiRequest('/auth/login', { + method: 'POST', + body: { username, password, remember } + }); + }, + + async logout() { + return apiRequest('/auth/logout', { + method: 'POST' + }); + }, + + async register(username, email, password) { + return apiRequest('/auth/register', { + method: 'POST', + body: { username, email, password } + }); + }, + + async checkAuth() { + return apiRequest('/auth/check', { + method: 'GET' + }); + }, + + async getCurrentUser() { + return apiRequest('/auth/me', { + method: 'GET' + }); + } +}; + +// 内容API +const contentAPI = { + async getArticles(category = 'news', publishedOnly = true) { + return apiRequest(`/cms/articles?category=${category}&published_only=${publishedOnly}`, { + method: 'GET' + }); + }, + + async getArticle(articleId) { + return apiRequest(`/cms/articles/${articleId}`, { + method: 'GET' + }); + }, + + async createArticle(articleData) { + return apiRequest('/cms/articles', { + method: 'POST', + body: articleData + }); + }, + + async updateArticle(articleId, articleData) { + return apiRequest(`/cms/articles/${articleId}`, { + method: 'PUT', + body: articleData + }); + }, + + async deleteArticle(articleId) { + return apiRequest(`/cms/articles/${articleId}`, { + method: 'DELETE' + }); + }, + + async getResources(category = null) { + const url = category ? `/cms/resources?category=${category}` : '/cms/resources'; + return apiRequest(url, { + method: 'GET' + }); + }, + + async uploadResource(file, name, description, category, isProtected) { + const formData = new FormData(); + formData.append('file', file); + formData.append('name', name); + formData.append('description', description || ''); + formData.append('category', category || 'document'); + formData.append('is_protected', isProtected ? 'true' : 'false'); + + return apiRequest('/cms/resources', { + method: 'POST', + headers: {}, // 让浏览器设置Content-Type + body: formData + }); + }, + + async getTeam(role = null) { + const url = role ? `/cms/team?role=${role}` : '/cms/team'; + return apiRequest(url, { + method: 'GET' + }); + }, + + async createTeamMember(memberData) { + return apiRequest('/cms/team', { + method: 'POST', + body: memberData + }); + } +}; + +// 其他API +const api = { + async downloadResource(resourceId) { + window.location.href = `${API_BASE_URL}/download/${resourceId}`; + }, + + async getStats() { + return apiRequest('/stats', { + method: 'GET' + }); + }, + + async getProtectedContent() { + return apiRequest('/protected-content', { + method: 'GET' + }); + } +}; + +// 组会管理API +const meetingsAPI = { + async getMeetings(status = null, type = null) { + let url = '/meetings/meetings'; + const params = []; + if (status) params.push(`status=${status}`); + if (type) params.push(`type=${type}`); + if (params.length > 0) url += '?' + params.join('&'); + return apiRequest(url, { method: 'GET' }); + }, + + async getMeeting(meetingId) { + return apiRequest(`/meetings/meetings/${meetingId}`, { method: 'GET' }); + }, + + async createMeeting(meetingData) { + return apiRequest('/meetings/meetings', { + method: 'POST', + body: meetingData + }); + }, + + async updateMeeting(meetingId, meetingData) { + return apiRequest(`/meetings/meetings/${meetingId}`, { + method: 'PUT', + body: meetingData + }); + }, + + async deleteMeeting(meetingId) { + return apiRequest(`/meetings/meetings/${meetingId}`, { + method: 'DELETE' + }); + }, + + async registerMeeting(meetingId) { + return apiRequest(`/meetings/meetings/${meetingId}/register`, { + method: 'POST' + }); + }, + + async checkinMeeting(meetingId) { + return apiRequest(`/meetings/meetings/${meetingId}/checkin`, { + method: 'POST' + }); + }, + + async uploadMeetingFile(meetingId, file, name) { + const formData = new FormData(); + formData.append('file', file); + formData.append('name', name || file.name); + + return apiRequest(`/meetings/meetings/${meetingId}/files`, { + method: 'POST', + headers: {}, + body: formData + }); + }, + + async downloadMeetingFile(meetingId, fileId) { + window.location.href = `${API_BASE_URL}/meetings/meetings/${meetingId}/files/${fileId}`; + } +}; + +// 研究管理API +const researchAPI = { + async getProjects(status = null, ownerId = null) { + let url = '/research/projects'; + const params = []; + if (status) params.push(`status=${status}`); + if (ownerId) params.push(`owner_id=${ownerId}`); + if (params.length > 0) url += '?' + params.join('&'); + return apiRequest(url, { method: 'GET' }); + }, + + async getProject(projectId) { + return apiRequest(`/research/projects/${projectId}`, { method: 'GET' }); + }, + + async createProject(projectData) { + return apiRequest('/research/projects', { + method: 'POST', + body: projectData + }); + }, + + async updateProject(projectId, projectData) { + return apiRequest(`/research/projects/${projectId}`, { + method: 'PUT', + body: projectData + }); + }, + + async deleteProject(projectId) { + return apiRequest(`/research/projects/${projectId}`, { + method: 'DELETE' + }); + }, + + async getNotes(projectId, noteType = null) { + let url = `/research/projects/${projectId}/notes`; + if (noteType) url += `?type=${noteType}`; + return apiRequest(url, { method: 'GET' }); + }, + + async createNote(projectId, noteData) { + return apiRequest(`/research/projects/${projectId}/notes`, { + method: 'POST', + body: noteData + }); + }, + + async updateNote(noteId, noteData) { + return apiRequest(`/research/notes/${noteId}`, { + method: 'PUT', + body: noteData + }); + }, + + async deleteNote(noteId) { + return apiRequest(`/research/notes/${noteId}`, { + method: 'DELETE' + }); + }, + + async getTasks(projectId, status = null, assigneeId = null) { + let url = `/research/projects/${projectId}/tasks`; + const params = []; + if (status) params.push(`status=${status}`); + if (assigneeId) params.push(`assignee_id=${assigneeId}`); + if (params.length > 0) url += '?' + params.join('&'); + return apiRequest(url, { method: 'GET' }); + }, + + async createTask(projectId, taskData) { + return apiRequest(`/research/projects/${projectId}/tasks`, { + method: 'POST', + body: taskData + }); + }, + + async updateTask(taskId, taskData) { + return apiRequest(`/research/tasks/${taskId}`, { + method: 'PUT', + body: taskData + }); + }, + + async deleteTask(taskId) { + return apiRequest(`/research/tasks/${taskId}`, { + method: 'DELETE' + }); + }, + + async uploadResearchFile(projectId, file, name) { + const formData = new FormData(); + formData.append('file', file); + formData.append('name', name || file.name); + + return apiRequest(`/research/projects/${projectId}/files`, { + method: 'POST', + headers: {}, + body: formData + }); + }, + + async downloadResearchFile(projectId, fileId) { + window.location.href = `${API_BASE_URL}/research/projects/${projectId}/files/${fileId}`; + }, + + async deleteResearchFile(projectId, fileId) { + return apiRequest(`/research/projects/${projectId}/files/${fileId}`, { + method: 'DELETE' + }); + } +}; + +// 导出API +window.DAELAPI = { + auth: authAPI, + content: contentAPI, + api: api, + meetings: meetingsAPI, + research: researchAPI +}; + diff --git a/dofile/js/auth.js b/dofile/js/auth.js new file mode 100644 index 0000000..821f1bc --- /dev/null +++ b/dofile/js/auth.js @@ -0,0 +1,208 @@ +// User Authentication Functions + +let currentUser = null; + +// Check if user is logged in +async function isLoggedIn() { + try { + const response = await window.DAELAPI.auth.checkAuth(); + if (response.authenticated) { + currentUser = response.user; + return true; + } + return false; + } catch (error) { + return false; + } +} + +// Get current user +function getCurrentUser() { + return currentUser; +} + +// Login function +async function login(username, password, remember = false) { + try { + const response = await window.DAELAPI.auth.login(username, password, remember); + currentUser = response.user; + return true; + } catch (error) { + console.error('Login failed:', error); + return false; + } +} + +// Logout function +async function logout() { + try { + await window.DAELAPI.auth.logout(); + currentUser = null; + window.location.href = 'website.html'; + } catch (error) { + console.error('Logout failed:', error); + // 即使API失败也清除本地状态 + currentUser = null; + window.location.href = 'website.html'; + } +} + +// Check protected content access +async function checkProtectedContent() { + const loggedIn = await isLoggedIn(); + const protectedElements = document.querySelectorAll('.protected-content'); + + if (loggedIn) { + protectedElements.forEach(el => { + el.classList.remove('protected-content'); + }); + + // 加载受保护内容 + try { + const response = await window.DAELAPI.api.getProtectedContent(); + displayProtectedContent(response); + } catch (error) { + console.error('Failed to load protected content:', error); + } + } else { + protectedElements.forEach(el => { + el.classList.add('protected-content'); + }); + } +} + +// 显示受保护内容 +async function displayProtectedContent(data) { + // 更新受保护的文章 + const protectedArticlesContainer = document.getElementById('protected-articles'); + if (protectedArticlesContainer && data.articles && data.articles.length > 0) { + protectedArticlesContainer.innerHTML = data.articles.map(article => ` +
+

${article.title}

+

${article.summary || article.content.substring(0, 100) + '...'}

+
${new Date(article.created_at).toLocaleDateString()}
+
+ `).join(''); + protectedArticlesContainer.classList.remove('hidden'); + } + + // 更新受保护的资源 + const protectedResourcesContainer = document.getElementById('protected-resources'); + if (protectedResourcesContainer && data.resources && data.resources.length > 0) { + protectedResourcesContainer.innerHTML = data.resources.map(resource => ` +
+

${resource.name}

+

${resource.description || ''}

+

大小: ${(resource.file_size / 1024 / 1024).toFixed(2)} MB

+ +
+ `).join(''); + protectedResourcesContainer.classList.remove('hidden'); + } + + // 更新统计信息 + try { + const statsResponse = await window.DAELAPI.api.getStats(); + const stats = statsResponse.stats; + const statsContainer = document.getElementById('protected-stats'); + if (statsContainer) { + const statElements = statsContainer.querySelectorAll('.text-3xl'); + if (statElements.length >= 4) { + statElements[0].textContent = stats.articles || '0'; + statElements[1].textContent = stats.resources || '0'; + statElements[2].textContent = stats.team_members || '0'; + statElements[3].textContent = stats.total_downloads || '0'; + } + } + } catch (error) { + console.error('Failed to load stats:', error); + } +} + +// Initialize auth on page load +document.addEventListener('DOMContentLoaded', async () => { + // 等待API加载 + if (typeof window.DAELAPI === 'undefined') { + console.warn('API not loaded, using fallback authentication'); + // 使用本地存储作为后备 + const localAuth = localStorage.getItem('dael_user_logged_in') === 'true'; + updateUI(localAuth); + return; + } + + await checkProtectedContent(); + await updateAuthUI(); + + // Logout button handler + const logoutBtn = document.getElementById('logout-btn'); + if (logoutBtn) { + logoutBtn.addEventListener('click', async (e) => { + e.preventDefault(); + if (confirm('确定要退出登录吗?')) { + await logout(); + } + }); + } +}); + +// 更新认证UI +async function updateAuthUI() { + const loggedIn = await isLoggedIn(); + const loginBtn = document.getElementById('login-btn'); + const logoutBtn = document.getElementById('logout-btn'); + const adminBtn = document.getElementById('admin-btn'); + const meetingsLink = document.getElementById('meetings-link'); + const researchLink = document.getElementById('research-link'); + const userInfo = document.getElementById('user-info'); + + if (loggedIn) { + const user = getCurrentUser(); + if (loginBtn) loginBtn.classList.add('hidden'); + if (logoutBtn) logoutBtn.classList.remove('hidden'); + if (adminBtn && user && user.is_admin) { + adminBtn.classList.remove('hidden'); + } else if (adminBtn) { + adminBtn.classList.add('hidden'); + } + // 显示组会和研究管理链接(所有登录用户) + if (meetingsLink) meetingsLink.classList.remove('hidden'); + if (researchLink) researchLink.classList.remove('hidden'); + const mobileMeetingsLink = document.getElementById('mobile-meetings-link'); + const mobileResearchLink = document.getElementById('mobile-research-link'); + if (mobileMeetingsLink) mobileMeetingsLink.classList.remove('hidden'); + if (mobileResearchLink) mobileResearchLink.classList.remove('hidden'); + if (userInfo && user) { + userInfo.textContent = user.username; + userInfo.classList.remove('hidden'); + } + } else { + if (loginBtn) loginBtn.classList.remove('hidden'); + if (logoutBtn) logoutBtn.classList.add('hidden'); + if (adminBtn) adminBtn.classList.add('hidden'); + if (meetingsLink) meetingsLink.classList.add('hidden'); + if (researchLink) researchLink.classList.add('hidden'); + const mobileMeetingsLink = document.getElementById('mobile-meetings-link'); + const mobileResearchLink = document.getElementById('mobile-research-link'); + if (mobileMeetingsLink) mobileMeetingsLink.classList.add('hidden'); + if (mobileResearchLink) mobileResearchLink.classList.add('hidden'); + if (userInfo) userInfo.classList.add('hidden'); + } +} + +// 后备UI更新函数 +function updateUI(loggedIn) { + const loginBtn = document.getElementById('login-btn'); + const logoutBtn = document.getElementById('logout-btn'); + + if (loggedIn) { + if (loginBtn) loginBtn.classList.add('hidden'); + if (logoutBtn) logoutBtn.classList.remove('hidden'); + } else { + if (loginBtn) loginBtn.classList.remove('hidden'); + if (logoutBtn) logoutBtn.classList.add('hidden'); + } +} + diff --git a/dofile/js/common.js b/dofile/js/common.js new file mode 100644 index 0000000..9b6bda9 --- /dev/null +++ b/dofile/js/common.js @@ -0,0 +1,156 @@ +// Common JavaScript Functions for DAEL Lab Website + +// Initialize Lucide Icons +function initIcons() { + if (window.lucide) { + lucide.createIcons(); + } else { + console.warn("Lucide icons failed to load."); + } +} + +// Mobile Menu Toggle +function initMobileMenu() { + const menuBtn = document.getElementById('mobile-menu-btn'); + const mobileMenu = document.getElementById('mobile-menu'); + + if (menuBtn && mobileMenu) { + menuBtn.addEventListener('click', () => { + mobileMenu.classList.toggle('hidden'); + }); + } +} + +// Navbar scroll effect +function initNavbarScroll() { + window.addEventListener('scroll', () => { + const nav = document.getElementById('navbar'); + if (nav) { + if (window.scrollY > 50) { + nav.classList.add('shadow-sm'); + if (nav.classList.contains('h-20')) { + nav.classList.replace('h-20', 'h-16'); + } + } else { + nav.classList.remove('shadow-sm'); + if (nav.classList.contains('h-16')) { + nav.classList.replace('h-16', 'h-20'); + } + } + } + }); +} + +// Canvas Animation: Organic Connection Nodes +function initCanvasAnimation() { + const canvas = document.getElementById('heroCanvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + let width, height; + let particles = []; + + function resize() { + width = window.innerWidth; + height = window.innerHeight; + canvas.width = width; + canvas.height = height; + } + + class Particle { + constructor() { + this.x = Math.random() * width; + this.y = Math.random() * height; + this.vx = (Math.random() - 0.5) * 0.5; + this.vy = (Math.random() - 0.5) * 0.5; + this.size = Math.random() * 2 + 1; + } + + update() { + this.x += this.vx; + this.y += this.vy; + + if (this.x < 0 || this.x > width) this.vx *= -1; + if (this.y < 0 || this.y > height) this.vy *= -1; + } + + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + ctx.fill(); + } + } + + function initParticles() { + particles = []; + const particleCount = Math.min(window.innerWidth / 10, 100); + for (let i = 0; i < particleCount; i++) { + particles.push(new Particle()); + } + } + + function animate() { + ctx.clearRect(0, 0, width, height); + + for (let i = 0; i < particles.length; i++) { + particles[i].update(); + particles[i].draw(); + + for (let j = i + 1; j < particles.length; j++) { + const dx = particles[i].x - particles[j].x; + const dy = particles[i].y - particles[j].y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 150) { + ctx.beginPath(); + ctx.strokeStyle = `rgba(0, 0, 0, ${0.05 - dist/3000})`; + ctx.lineWidth = 0.5; + ctx.moveTo(particles[i].x, particles[i].y); + ctx.lineTo(particles[j].x, particles[j].y); + ctx.stroke(); + } + } + } + requestAnimationFrame(animate); + } + + window.addEventListener('resize', () => { + resize(); + initParticles(); + }); + + resize(); + initParticles(); + animate(); +} + +// Visit Counter +function initVisitCounter() { + const counterElement = document.getElementById('visit-counter'); + if (!counterElement) return; + + let count = parseInt(localStorage.getItem('dael_visit_count') || '0'); + count++; + localStorage.setItem('dael_visit_count', count.toString()); + counterElement.textContent = count.toLocaleString(); +} + +// Initialize all common functions +document.addEventListener('DOMContentLoaded', () => { + initIcons(); + initMobileMenu(); + initNavbarScroll(); + initCanvasAnimation(); + initVisitCounter(); +}); + + + + + + + + + + diff --git a/dofile/js/meetings.js b/dofile/js/meetings.js new file mode 100644 index 0000000..e475b57 --- /dev/null +++ b/dofile/js/meetings.js @@ -0,0 +1,369 @@ +// Meetings Management JavaScript + +let currentMeetings = []; +let currentFilter = 'all'; + +// 检查登录状态 +async function checkLogin() { + try { + const response = await window.DAELAPI.auth.checkAuth(); + if (!response.authenticated) { + window.location.href = 'login.html'; + return false; + } + return true; + } catch (error) { + window.location.href = 'login.html'; + return false; + } +} + +// 加载组会列表 +async function loadMeetings() { + try { + const response = await window.DAELAPI.meetings.getMeetings(); + currentMeetings = response.meetings; + displayMeetings(currentMeetings); + } catch (error) { + console.error('Failed to load meetings:', error); + document.getElementById('meetings-list').innerHTML = '

加载失败

'; + } +} + +// 显示组会列表 +function displayMeetings(meetings) { + const container = document.getElementById('meetings-list'); + + // 根据筛选器过滤 + let filteredMeetings = meetings; + if (currentFilter !== 'all') { + filteredMeetings = meetings.filter(m => m.status === currentFilter); + } + + if (filteredMeetings.length === 0) { + container.innerHTML = '

暂无组会

'; + return; + } + + container.innerHTML = filteredMeetings.map(meeting => { + const date = new Date(meeting.date); + const isOwner = meeting.organizer_id === (window.currentUser?.id); + + return ` +
+
+
+
+

${meeting.title}

+ ${getStatusText(meeting.status)} + ${getTypeText(meeting.meeting_type)} +
+

${meeting.description || ''}

+
+ 📅 ${date.toLocaleDateString('zh-CN')} + 🕐 ${meeting.time || ''} + 📍 ${meeting.location || '未指定'} + 👤 组织者: ${meeting.organizer_name || ''} + 👥 报名: ${meeting.attendee_count || 0} 人 +
+
+
+ + ${isOwner ? ` + + + ` : ''} +
+
+
+ `; + }).join(''); +} + +// 状态颜色 +function getStatusColor(status) { + const colors = { + 'planned': 'bg-blue-100 text-blue-800', + 'ongoing': 'bg-green-100 text-green-800', + 'completed': 'bg-gray-100 text-gray-800', + 'cancelled': 'bg-red-100 text-red-800' + }; + return colors[status] || 'bg-gray-100 text-gray-800'; +} + +// 状态文本 +function getStatusText(status) { + const texts = { + 'planned': '计划中', + 'ongoing': '进行中', + 'completed': '已结束', + 'cancelled': '已取消' + }; + return texts[status] || status; +} + +// 类型文本 +function getTypeText(type) { + const texts = { + 'weekly': '周会', + 'monthly': '月会', + 'special': '专题会' + }; + return texts[type] || type; +} + +// 显示组会表单 +function showMeetingForm(meetingId = null) { + const modal = document.getElementById('meeting-modal'); + const form = document.getElementById('meeting-form'); + const title = document.getElementById('meeting-modal-title'); + + if (meetingId) { + const meeting = currentMeetings.find(m => m.id === meetingId); + if (meeting) { + title.textContent = '编辑组会'; + document.getElementById('meeting-id').value = meeting.id; + document.getElementById('meeting-title').value = meeting.title; + document.getElementById('meeting-description').value = meeting.description || ''; + document.getElementById('meeting-date').value = meeting.date; + document.getElementById('meeting-time').value = meeting.time || ''; + document.getElementById('meeting-location').value = meeting.location || ''; + document.getElementById('meeting-type').value = meeting.meeting_type; + document.getElementById('meeting-status').value = meeting.status; + } + } else { + title.textContent = '创建组会'; + form.reset(); + document.getElementById('meeting-id').value = ''; + // 设置默认日期为今天 + const today = new Date().toISOString().split('T')[0]; + document.getElementById('meeting-date').value = today; + } + + modal.classList.remove('hidden'); +} + +// 关闭组会表单 +function closeMeetingForm() { + document.getElementById('meeting-modal').classList.add('hidden'); +} + +// 保存组会 +async function saveMeeting(event) { + event.preventDefault(); + + const meetingId = document.getElementById('meeting-id').value; + const meetingData = { + title: document.getElementById('meeting-title').value, + description: document.getElementById('meeting-description').value, + date: document.getElementById('meeting-date').value, + time: document.getElementById('meeting-time').value, + location: document.getElementById('meeting-location').value, + meeting_type: document.getElementById('meeting-type').value, + status: document.getElementById('meeting-status').value + }; + + try { + if (meetingId) { + await window.DAELAPI.meetings.updateMeeting(meetingId, meetingData); + } else { + await window.DAELAPI.meetings.createMeeting(meetingData); + } + closeMeetingForm(); + loadMeetings(); + } catch (error) { + alert('保存失败: ' + error.message); + } +} + +// 查看组会详情 +async function viewMeeting(meetingId) { + try { + const response = await window.DAELAPI.meetings.getMeeting(meetingId); + const meeting = response.meeting; + displayMeetingDetail(meeting); + } catch (error) { + alert('加载失败: ' + error.message); + } +} + +// 显示组会详情 +function displayMeetingDetail(meeting) { + const modal = document.getElementById('meeting-detail-modal'); + const container = document.getElementById('meeting-detail-content'); + const date = new Date(meeting.date); + const isOwner = meeting.organizer_id === (window.currentUser?.id); + + container.innerHTML = ` +
+

${meeting.title}

+ +
+ +
+
+ 日期时间: + ${date.toLocaleDateString('zh-CN')} ${meeting.time || ''} +
+
+ 地点: + ${meeting.location || '未指定'} +
+
+ 组织者: + ${meeting.organizer_name || ''} +
+
+ 类型: + ${getTypeText(meeting.meeting_type)} +
+
+ 状态: + ${getStatusText(meeting.status)} +
+ ${meeting.description ? ` +
+ 描述: +

${meeting.description}

+
+ ` : ''} +
+ +
+

参会人员 (${meeting.attendees?.length || 0})

+
+ ${meeting.attendees && meeting.attendees.length > 0 ? + meeting.attendees.map(attendee => ` +
+ ${attendee.username} + ${getStatusText(attendee.status)} +
+ `).join('') : + '

暂无报名

' + } +
+ ${!isOwner && meeting.status === 'planned' ? ` + + ` : ''} +
+ + ${meeting.meeting_notes ? ` +
+

会议记录

+
${meeting.meeting_notes}
+
+ ` : ''} + + ${meeting.files && meeting.files.length > 0 ? ` +
+

附件 (${meeting.files.length})

+
+ ${meeting.files.map(file => ` +
+ ${file.name} + +
+ `).join('')} +
+
+ ` : ''} + + ${isOwner ? ` +
+ + +
+ ` : ''} + `; + + modal.classList.remove('hidden'); +} + +// 关闭组会详情 +function closeMeetingDetail() { + document.getElementById('meeting-detail-modal').classList.add('hidden'); +} + +// 编辑组会 +function editMeeting(meetingId) { + closeMeetingDetail(); + showMeetingForm(meetingId); +} + +// 删除组会 +async function deleteMeeting(meetingId) { + if (!confirm('确定要删除这个组会吗?')) return; + + try { + await window.DAELAPI.meetings.deleteMeeting(meetingId); + closeMeetingDetail(); + loadMeetings(); + } catch (error) { + alert('删除失败: ' + error.message); + } +} + +// 报名参会 +async function registerForMeeting(meetingId) { + try { + await window.DAELAPI.meetings.registerMeeting(meetingId); + alert('报名成功!'); + viewMeeting(meetingId); // 刷新详情 + } catch (error) { + alert('报名失败: ' + error.message); + } +} + +// 筛选器切换 +document.querySelectorAll('.filter-tab').forEach(tab => { + tab.addEventListener('click', () => { + document.querySelectorAll('.filter-tab').forEach(t => { + t.classList.remove('active', 'border-black', 'text-black'); + t.classList.add('text-gray-500'); + }); + tab.classList.add('active', 'border-black', 'text-black'); + tab.classList.remove('text-gray-500'); + + currentFilter = tab.dataset.filter; + displayMeetings(currentMeetings); + }); +}); + +// 初始化 +document.addEventListener('DOMContentLoaded', async () => { + if (await checkLogin()) { + // 获取当前用户信息 + try { + const authResponse = await window.DAELAPI.auth.checkAuth(); + window.currentUser = authResponse.user; + } catch (error) { + console.error('Failed to get user info:', error); + } + + loadMeetings(); + + // 退出按钮 + document.getElementById('logout-btn').addEventListener('click', async () => { + await logout(); + }); + } +}); + + + + + + + + + + diff --git a/dofile/js/research.js b/dofile/js/research.js new file mode 100644 index 0000000..28a4b50 --- /dev/null +++ b/dofile/js/research.js @@ -0,0 +1,573 @@ +// Research Management JavaScript + +let currentProjects = []; +let currentFilter = 'all'; +let currentProjectDetail = null; + +// 检查登录状态 +async function checkLogin() { + try { + const response = await window.DAELAPI.auth.checkAuth(); + if (!response.authenticated) { + window.location.href = 'login.html'; + return false; + } + return true; + } catch (error) { + window.location.href = 'login.html'; + return false; + } +} + +// 加载项目列表 +async function loadProjects() { + try { + const response = await window.DAELAPI.research.getProjects(); + currentProjects = response.projects; + displayProjects(currentProjects); + } catch (error) { + console.error('Failed to load projects:', error); + document.getElementById('projects-list').innerHTML = '

加载失败

'; + } +} + +// 显示项目列表 +function displayProjects(projects) { + const container = document.getElementById('projects-list'); + + // 根据筛选器过滤 + let filteredProjects = projects; + if (currentFilter !== 'all') { + filteredProjects = projects.filter(p => p.status === currentFilter); + } + + if (filteredProjects.length === 0) { + container.innerHTML = '

暂无项目

'; + return; + } + + container.innerHTML = filteredProjects.map(project => { + const startDate = project.start_date ? new Date(project.start_date).toLocaleDateString('zh-CN') : '未设置'; + const endDate = project.end_date ? new Date(project.end_date).toLocaleDateString('zh-CN') : '未设置'; + const isOwner = project.owner_id === (window.currentUser?.id); + + return ` +
+
+
+
+

${project.title}

+ ${getStatusText(project.status)} +
+

${project.description || ''}

+
+ 📅 ${startDate} - ${endDate} + 👤 ${project.owner_name || ''} + 📊 进度: ${project.progress}% +
+ ${project.tags && project.tags.length > 0 ? ` +
+ ${project.tags.map(tag => `${tag}`).join('')} +
+ ` : ''} +
+
+ + ${isOwner ? ` + + + ` : ''} +
+
+
+
+
+
+
+
+ `; + }).join(''); +} + +// 状态颜色 +function getStatusColor(status) { + const colors = { + 'active': 'bg-green-100 text-green-800', + 'completed': 'bg-gray-100 text-gray-800', + 'paused': 'bg-yellow-100 text-yellow-800', + 'cancelled': 'bg-red-100 text-red-800' + }; + return colors[status] || 'bg-gray-100 text-gray-800'; +} + +// 状态文本 +function getStatusText(status) { + const texts = { + 'active': '进行中', + 'completed': '已完成', + 'paused': '已暂停', + 'cancelled': '已取消' + }; + return texts[status] || status; +} + +// 显示项目表单 +function showProjectForm(projectId = null) { + const modal = document.getElementById('project-modal'); + const form = document.getElementById('project-form'); + const title = document.getElementById('project-modal-title'); + + if (projectId) { + const project = currentProjects.find(p => p.id === projectId); + if (project) { + title.textContent = '编辑项目'; + document.getElementById('project-id').value = project.id; + document.getElementById('project-title').value = project.title; + document.getElementById('project-description').value = project.description || ''; + document.getElementById('project-start-date').value = project.start_date || ''; + document.getElementById('project-end-date').value = project.end_date || ''; + document.getElementById('project-status').value = project.status; + document.getElementById('project-progress').value = project.progress; + document.getElementById('project-tags').value = project.tags ? project.tags.join(', ') : ''; + } + } else { + title.textContent = '新建项目'; + form.reset(); + document.getElementById('project-id').value = ''; + document.getElementById('project-progress').value = 0; + } + + modal.classList.remove('hidden'); +} + +// 关闭项目表单 +function closeProjectForm() { + document.getElementById('project-modal').classList.add('hidden'); +} + +// 保存项目 +async function saveProject(event) { + event.preventDefault(); + + const projectId = document.getElementById('project-id').value; + const tagsInput = document.getElementById('project-tags').value; + const tags = tagsInput ? tagsInput.split(',').map(t => t.trim()).filter(t => t) : []; + + const projectData = { + title: document.getElementById('project-title').value, + description: document.getElementById('project-description').value, + start_date: document.getElementById('project-start-date').value || null, + end_date: document.getElementById('project-end-date').value || null, + status: document.getElementById('project-status').value, + progress: parseInt(document.getElementById('project-progress').value) || 0, + tags: tags + }; + + try { + if (projectId) { + await window.DAELAPI.research.updateProject(projectId, projectData); + } else { + await window.DAELAPI.research.createProject(projectData); + } + closeProjectForm(); + loadProjects(); + } catch (error) { + alert('保存失败: ' + error.message); + } +} + +// 查看项目详情 +async function viewProject(projectId) { + try { + const response = await window.DAELAPI.research.getProject(projectId); + currentProjectDetail = response.project; + displayProjectDetail(currentProjectDetail); + } catch (error) { + alert('加载失败: ' + error.message); + } +} + +// 显示项目详情 +function displayProjectDetail(project) { + const modal = document.getElementById('project-detail-modal'); + const container = document.getElementById('project-detail-content'); + const isOwner = project.owner_id === (window.currentUser?.id); + + container.innerHTML = ` +
+
+

${project.title}

+
+ ${getStatusText(project.status)} + 进度: ${project.progress}% +
+
+ +
+ +
+ ${project.description ? ` +
+

项目描述

+

${project.description}

+
+ ` : ''} +
+
+ 开始日期: + ${project.start_date ? new Date(project.start_date).toLocaleDateString('zh-CN') : '未设置'} +
+
+ 结束日期: + ${project.end_date ? new Date(project.end_date).toLocaleDateString('zh-CN') : '未设置'} +
+
+ 负责人: + ${project.owner_name || ''} +
+
+ ${project.tags && project.tags.length > 0 ? ` +
+ 标签: +
+ ${project.tags.map(tag => `${tag}`).join('')} +
+
+ ` : ''} +
+ + +
+ + + +
+ +
+ ${isOwner ? ` + + ` : ''} +
+ ${project.notes && project.notes.length > 0 ? + project.notes.map(note => ` +
+
+
${note.title}
+ ${isOwner && note.author_id === window.currentUser?.id ? ` +
+ + +
+ ` : ''} +
+

${note.content.substring(0, 200)}${note.content.length > 200 ? '...' : ''}

+
+ ${note.author_name} · ${new Date(note.created_at).toLocaleDateString('zh-CN')} +
+
+ `).join('') : + '

暂无笔记

' + } +
+
+ + + + + + ${isOwner ? ` +
+ + +
+ ` : ''} + `; + + // 初始化标签页切换 + initDetailTabs(); + modal.classList.remove('hidden'); +} + +// 初始化详情页标签 +function initDetailTabs() { + document.querySelectorAll('#project-detail-modal .tab-btn').forEach(btn => { + btn.addEventListener('click', () => { + const tabName = btn.dataset.tab; + + // 更新按钮样式 + document.querySelectorAll('#project-detail-modal .tab-btn').forEach(b => { + b.classList.remove('active', 'border-black', 'text-black'); + b.classList.add('text-gray-500'); + }); + btn.classList.add('active', 'border-black', 'text-black'); + btn.classList.remove('text-gray-500'); + + // 更新内容 + document.querySelectorAll('#project-detail-modal .tab-content').forEach(content => { + content.classList.add('hidden'); + }); + document.getElementById(`tab-content-${tabName}`).classList.remove('hidden'); + }); + }); +} + +// 关闭项目详情 +function closeProjectDetail() { + document.getElementById('project-detail-modal').classList.add('hidden'); + currentProjectDetail = null; +} + +// 编辑项目 +function editProject(projectId) { + closeProjectDetail(); + showProjectForm(projectId); +} + +// 删除项目 +async function deleteProject(projectId) { + if (!confirm('确定要删除这个项目吗?所有相关的笔记、任务和文件也将被删除。')) return; + + try { + await window.DAELAPI.research.deleteProject(projectId); + closeProjectDetail(); + loadProjects(); + } catch (error) { + alert('删除失败: ' + error.message); + } +} + +// 笔记管理 +function showNoteForm(noteId = null) { + // 简化版:使用prompt创建笔记 + const title = prompt('笔记标题:'); + if (!title) return; + + const content = prompt('笔记内容:'); + if (!content) return; + + createNote(title, content); +} + +async function createNote(title, content) { + try { + await window.DAELAPI.research.createNote(currentProjectDetail.id, { + title: title, + content: content, + note_type: 'note' + }); + viewProject(currentProjectDetail.id); // 刷新详情 + } catch (error) { + alert('创建失败: ' + error.message); + } +} + +async function deleteNote(noteId) { + if (!confirm('确定要删除这条笔记吗?')) return; + + try { + await window.DAELAPI.research.deleteNote(noteId); + viewProject(currentProjectDetail.id); // 刷新详情 + } catch (error) { + alert('删除失败: ' + error.message); + } +} + +// 任务管理 +function showTaskForm() { + const title = prompt('任务标题:'); + if (!title) return; + + const description = prompt('任务描述:'); + const dueDate = prompt('截止日期 (YYYY-MM-DD):'); + + createTask(title, description, dueDate); +} + +async function createTask(title, description, dueDate) { + try { + await window.DAELAPI.research.createTask(currentProjectDetail.id, { + title: title, + description: description || '', + due_date: dueDate || null, + status: 'todo', + priority: 'medium' + }); + viewProject(currentProjectDetail.id); // 刷新详情 + } catch (error) { + alert('创建失败: ' + error.message); + } +} + +async function toggleTask(taskId, completed) { + try { + await window.DAELAPI.research.updateTask(taskId, { + status: completed ? 'completed' : 'todo' + }); + viewProject(currentProjectDetail.id); // 刷新详情 + } catch (error) { + alert('更新失败: ' + error.message); + } +} + +async function deleteTask(taskId) { + if (!confirm('确定要删除这个任务吗?')) return; + + try { + await window.DAELAPI.research.deleteTask(taskId); + viewProject(currentProjectDetail.id); // 刷新详情 + } catch (error) { + alert('删除失败: ' + error.message); + } +} + +// 文件管理 +function showFileUpload() { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = false; + input.onchange = async (e) => { + const file = e.target.files[0]; + if (!file) return; + + try { + await window.DAELAPI.research.uploadResearchFile(currentProjectDetail.id, file, file.name); + viewProject(currentProjectDetail.id); // 刷新详情 + } catch (error) { + alert('上传失败: ' + error.message); + } + }; + input.click(); +} + +async function deleteFile(projectId, fileId) { + if (!confirm('确定要删除这个文件吗?')) return; + + try { + await window.DAELAPI.research.deleteResearchFile(projectId, fileId); + viewProject(projectId); // 刷新详情 + } catch (error) { + alert('删除失败: ' + error.message); + } +} + +// 优先级颜色 +function getPriorityColor(priority) { + const colors = { + 'high': 'bg-red-100 text-red-800', + 'medium': 'bg-yellow-100 text-yellow-800', + 'low': 'bg-blue-100 text-blue-800' + }; + return colors[priority] || 'bg-gray-100 text-gray-800'; +} + +// 优先级文本 +function getPriorityText(priority) { + const texts = { + 'high': '高', + 'medium': '中', + 'low': '低' + }; + return texts[priority] || priority; +} + +// 筛选器切换 +document.querySelectorAll('.filter-tab').forEach(tab => { + tab.addEventListener('click', () => { + document.querySelectorAll('.filter-tab').forEach(t => { + t.classList.remove('active', 'border-black', 'text-black'); + t.classList.add('text-gray-500'); + }); + tab.classList.add('active', 'border-black', 'text-black'); + tab.classList.remove('text-gray-500'); + + currentFilter = tab.dataset.filter; + displayProjects(currentProjects); + }); +}); + +// 初始化 +document.addEventListener('DOMContentLoaded', async () => { + if (await checkLogin()) { + // 获取当前用户信息 + try { + const authResponse = await window.DAELAPI.auth.checkAuth(); + window.currentUser = authResponse.user; + } catch (error) { + console.error('Failed to get user info:', error); + } + + loadProjects(); + + // 退出按钮 + document.getElementById('logout-btn').addEventListener('click', async () => { + await logout(); + }); + } +}); \ No newline at end of file diff --git a/dofile/js/search.js b/dofile/js/search.js new file mode 100644 index 0000000..e777571 --- /dev/null +++ b/dofile/js/search.js @@ -0,0 +1,144 @@ +// Search Functionality + +// Search data structure (in production, this would come from a server) +const searchData = { + pages: [ + { title: '关于实验室', url: 'website.html#about', content: 'DAEL Design AI Ecology Lab 跨学科研究平台' }, + { title: '研究方向', url: 'website.html#research', content: '生成式生态学 生物启发式智能 数字可持续性' }, + { title: '研究团队', url: 'website.html#team', content: 'Dr. Lin Chen Sarah Wu James Zhao Yu Zhang' }, + { title: '新闻资讯', url: 'news.html', content: '最新资讯 最新文章 热点文章' }, + { title: '成果展示', url: 'publications.html', content: '专著教材 文章目录 软件开发 学位论文' }, + { title: '资源下载', url: 'resources.html', content: '软件下载 资料下载 开源代码' }, + { title: '学术资料', url: 'academic.html', content: '软件使用 教程 文档' } + ], + news: [], + publications: [], + resources: [] +}; + +// Simple search function +function performSearch(query) { + if (!query || query.trim().length === 0) { + return []; + } + + const lowerQuery = query.toLowerCase().trim(); + const results = []; + + // Search in pages + searchData.pages.forEach(page => { + const titleMatch = page.title.toLowerCase().includes(lowerQuery); + const contentMatch = page.content.toLowerCase().includes(lowerQuery); + + if (titleMatch || contentMatch) { + results.push({ + type: 'page', + title: page.title, + url: page.url, + snippet: page.content.substring(0, 100) + '...' + }); + } + }); + + return results; +} + +// Initialize search +function initSearch() { + const searchInput = document.getElementById('search-input'); + const searchResults = document.getElementById('search-results'); + const searchForm = document.getElementById('search-form'); + + if (!searchInput || !searchResults) return; + + // Real-time search + let searchTimeout; + searchInput.addEventListener('input', (e) => { + clearTimeout(searchTimeout); + const query = e.target.value.trim(); + + if (query.length === 0) { + searchResults.classList.remove('active'); + searchResults.innerHTML = ''; + return; + } + + searchTimeout = setTimeout(() => { + const results = performSearch(query); + displaySearchResults(results, searchResults); + }, 300); + }); + + // Handle form submission + if (searchForm) { + searchForm.addEventListener('submit', (e) => { + e.preventDefault(); + const query = searchInput.value.trim(); + if (query) { + window.location.href = `search.html?q=${encodeURIComponent(query)}`; + } + }); + } + + // Close search results when clicking outside + document.addEventListener('click', (e) => { + if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) { + searchResults.classList.remove('active'); + } + }); +} + +// Display search results +function displaySearchResults(results, container) { + if (results.length === 0) { + container.innerHTML = '
未找到相关结果
'; + container.classList.add('active'); + return; + } + + container.innerHTML = results.map(result => ` + +
${result.title}
+
${result.snippet}
+
+ `).join(''); + + container.classList.add('active'); +} + +// Get search query from URL +function getSearchQuery() { + const params = new URLSearchParams(window.location.search); + return params.get('q') || ''; +} + +// Initialize on page load +document.addEventListener('DOMContentLoaded', () => { + initSearch(); + + // If on search page, perform search + if (window.location.pathname.includes('search.html')) { + const query = getSearchQuery(); + const searchInput = document.getElementById('search-input'); + const resultsContainer = document.getElementById('search-results-container'); + + if (query && searchInput) { + searchInput.value = query; + } + + if (query && resultsContainer) { + const results = performSearch(query); + displaySearchResults(results, resultsContainer); + } + } +}); + + + + + + + + + + diff --git a/dofile/login.html b/dofile/login.html new file mode 100644 index 0000000..2fd6741 --- /dev/null +++ b/dofile/login.html @@ -0,0 +1,184 @@ + + + + + + 用户登录 | Design AI & Ecology Lab + + + + + + + + + + +
+
+
+

登录

+

登录以访问更多内容

+ + + +
+

演示模式:任意用户名和密码即可登录

+
+
+
+
+ + + + + + + + + + + + + diff --git a/dofile/meetings.html b/dofile/meetings.html new file mode 100644 index 0000000..a13d2a5 --- /dev/null +++ b/dofile/meetings.html @@ -0,0 +1,136 @@ + + + + + + 组会管理 | Design AI & Ecology Lab + + + + + + + + + + +
+
+
+
+

组会管理

+

创建和管理实验室组会,报名参会,记录会议内容

+
+ +
+ + +
+ + + + +
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dofile/news.html b/dofile/news.html new file mode 100644 index 0000000..e15016e --- /dev/null +++ b/dofile/news.html @@ -0,0 +1,196 @@ + + + + + + 新闻资讯 | Design AI & Ecology Lab + + + + + + + + + + +
+
+

新闻资讯

+

了解实验室的最新动态、研究成果和活动信息

+
+
+ + +
+
+
+ +
+

最新资讯

+
+
+
+
2024.12.15
+
+

实验室新项目启动:AI驱动的城市生态规划

+

+ 我们很高兴地宣布启动新的研究项目,该项目将利用生成式AI技术优化城市生态系统的规划与设计。项目将结合机器学习算法和生态学原理,为城市可持续发展提供创新解决方案。 +

+ 阅读更多 -> +
+
+
+ +
+
+
2024.11.20
+
+

研究成果在顶级期刊发表

+

+ 实验室关于生物启发式智能算法的研究成果在Nature子刊发表。该研究提出了一种新的群体智能算法,可用于优化分布式系统的控制策略。 +

+ 阅读更多 -> +
+
+
+ +
+
+
2024.10.10
+
+

欢迎新成员加入实验室

+

+ 我们很高兴地宣布,三位新的博士生和两位研究助理加入了我们的团队。新成员将在生成式生态学和数字可持续性等领域开展研究。 +

+ 阅读更多 -> +
+
+
+
+
+ + +
+

热点文章

+
+
+

设计智能与生态系统的未来

+

2024.09.15

+
+
+

AI在可持续建筑中的应用

+

2024.08.20

+
+
+

生物启发算法的创新突破

+

2024.07.10

+
+
+
+
+
+
+ + + + + + + + + + + + diff --git a/dofile/publications.html b/dofile/publications.html new file mode 100644 index 0000000..6b7b555 --- /dev/null +++ b/dofile/publications.html @@ -0,0 +1,239 @@ + + + + + + 成果展示 | Design AI & Ecology Lab + + + + + + + + + + +
+
+

成果展示

+

实验室的研究成果、出版物和软件工具

+
+
+ + +
+
+ +
+ + + + +
+ + +
+
+
+

《设计智能与生态计算》

+

2024年出版 | 科学出版社

+

+ 本书系统介绍了设计智能与生态计算的基本理论和方法,涵盖了生成式生态学、生物启发式智能和数字可持续性等核心内容。 +

+
+
+

《AI驱动的可持续设计》

+

2023年出版 | 清华大学出版社

+

+ 探讨人工智能技术在可持续设计中的应用,包括能源优化、材料选择和生命周期评估等方面。 +

+
+
+
+ + + + + + + + + +
+
+ + + + + + + + + + + + + diff --git a/dofile/research.html b/dofile/research.html new file mode 100644 index 0000000..c923c50 --- /dev/null +++ b/dofile/research.html @@ -0,0 +1,132 @@ + + + + + + 研究管理 | Design AI & Ecology Lab + + + + + + + + + + +
+
+
+
+

研究管理

+

管理您的研究项目、笔记、任务和文件

+
+ +
+ + +
+ + + + +
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dofile/resources.html b/dofile/resources.html new file mode 100644 index 0000000..bf4bd8c --- /dev/null +++ b/dofile/resources.html @@ -0,0 +1,214 @@ + + + + + + 资源下载 | Design AI & Ecology Lab + + + + + + + + + + +
+
+

资源下载

+

下载实验室开发的软件、数据集和研究资料

+
+
+ + +
+
+
+ +
+

软件下载

+
+
+
+
+

EcoGen AI v2.0

+

生成式生态系统设计工具

+

更新日期: 2024.12.01 | 大小: 125 MB

+
+ +
+ 下载 -> +
+ +
+
+
+

BioSwarm Simulator v1.5

+

群体智能仿真平台

+

更新日期: 2024.11.15 | 大小: 89 MB

+
+ +
+ 下载 -> +
+
+
+ + +
+

数据集与资料

+
+
+
+
+

城市生态系统数据集

+

包含50个城市的生态数据

+

更新日期: 2024.10.20 | 大小: 2.3 GB

+
+ +
+ 下载 -> +
+ +
+
+
+

研究资料包

+

实验室研究方法和工具文档

+

更新日期: 2024.09.10 | 大小: 45 MB

+
+ +
+ 下载 -> +
+
+
+
+ + + +
+
+ + + + + + + + + + + + diff --git a/dofile/search.html b/dofile/search.html new file mode 100644 index 0000000..066c044 --- /dev/null +++ b/dofile/search.html @@ -0,0 +1,174 @@ + + + + + + 搜索结果 | Design AI & Ecology Lab + + + + + + + + + + +
+
+
+

搜索结果

+

搜索关键词:

+
+ +
+ +
+ + +
+
+ + + + + + + + + + + + + diff --git a/dofile/start_backend.sh b/dofile/start_backend.sh new file mode 100644 index 0000000..40c595f --- /dev/null +++ b/dofile/start_backend.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# 启动后端服务器 + +cd "$(dirname "$0")/backend" + +# 检查虚拟环境 +if [ ! -d "venv" ]; then + echo "Creating virtual environment..." + python3 -m venv venv +fi + +# 激活虚拟环境 +source venv/bin/activate + +# 安装依赖 +echo "Installing dependencies..." +pip install -r requirements.txt + +# 初始化数据库 +echo "Initializing database..." +python init_data.py + +# 启动服务器 +echo "Starting Flask server..." +python app.py + + + + + + + + + + diff --git a/dofile/team.html b/dofile/team.html new file mode 100644 index 0000000..8c82d93 --- /dev/null +++ b/dofile/team.html @@ -0,0 +1,223 @@ + + + + + + 研究团队 | Design AI & Ecology Lab + + + + + + + + + + +
+
+

研究团队

+

了解实验室的成员和研究背景

+
+
+ + +
+
+ +
+

Principal Investigators / 实验室主任

+
+
+

Dr. Lin Chen

+

Principal Investigator / 实验室主任

+

+ 博士毕业于MIT Media Lab,专注于计算设计与人机交互领域的研究。在生成式设计和AI辅助设计方面有丰富的研究经验。 +

+ +
+
+
+ + +
+

Faculty / 教师

+
+
+

Sarah Wu

+

Lead Ecologist / 生态学顾问

+

+ 城市生态学专家,研究重点为城市微气候与生物多样性修复。 +

+
+
+

James Zhao

+

AI Researcher / 算法工程师

+

+ 专攻深度强化学习在复杂系统模拟中的应用。 +

+
+
+
+ + +
+

Current Students / 在读学生

+
+
+
+

Wang Li

+

PhD Student

+

+ 研究方向:生成式生态学 +

+
+
+
+

Liu Ming

+

PhD Student

+

+ 研究方向:生物启发式智能 +

+
+
+
+

Zhang Wei

+

Master Student

+

+ 研究方向:数字可持续性 +

+
+
+
+ + +
+

Alumni / 毕业学生

+
+
+
+

Chen Xia

+

PhD, 2023

+

+ 现就职于:XX科技公司 +

+
+
+
+

Zhou Fang

+

Master, 2023

+

+ 现就职于:XX设计院 +

+
+
+
+
+
+ + + + + + + + + + + + diff --git a/dofile/test_login.html b/dofile/test_login.html new file mode 100644 index 0000000..ee36105 --- /dev/null +++ b/dofile/test_login.html @@ -0,0 +1,50 @@ + + + + Test Login + + +

Login Test

+ +
+ + + + + + + + + + + + + + diff --git a/dofile/uploads/251130_LAF_en_f.docx b/dofile/uploads/251130_LAF_en_f.docx new file mode 100644 index 0000000..852ee03 Binary files /dev/null and b/dofile/uploads/251130_LAF_en_f.docx differ diff --git a/dofile/website.html b/dofile/website.html new file mode 100644 index 0000000..9951e2e --- /dev/null +++ b/dofile/website.html @@ -0,0 +1,540 @@ + + + + + + + Design AI & Ecology Lab | 设计智能与生态设计实验室 + + + + + + + + + +
+ +
+ + + + + +
+
+
+

+ Design Intelligence
+ & Ecology. +

+

+ 设计智能与生态设计实验室致力于探索人工智能技术与生态系统的共生关系。我们通过数据驱动的设计方法,重塑人造环境与自然环境的边界。 +

+ +
+
+
+ + +
+
+
+
+

Mission / 使命

+
+
+

+ 我们相信,未来的设计不应仅服务于人类的需求,更应回应地球生态的呼唤。 +

+
+

+ DAEL (Design AI & Ecology Lab) 是一个跨学科的研究平台。我们融合计算机科学、生态学、建筑学与交互设计,利用机器学习算法模拟自然形态的演变,寻求可持续发展的最优解。 +

+

+ 实验室致力于培养新一代具备计算思维与生态伦理的设计师,通过与产业界、学术界的紧密合作,推动从“以人为本”向“生态为本”的设计范式转移。 +

+
+
+
+
+
+ + +
+
+
+
+

Research Areas

+

研究方向

+
+ +
+ +
+ +
+
+ +
+
+ +
+
+

Generative Ecology
生成式生态学

+

+ 利用生成对抗网络(GANs)与进化算法,模拟复杂生态系统的演替过程,辅助城市绿地与生物多样性规划。 +

+
+ + +
+
+
+
+ +
+
+

Bio-inspired AI
生物启发式智能

+

+ 从自然界的群体智能(如蚁群、蜂群)中汲取灵感,开发适用于分布式建筑机器人与自适应材料的控制算法。 +

+
+ + +
+
+
+
+ +
+
+

Digital Sustainability
数字可持续性

+

+ 量化数字基础设施的碳足迹,并开发基于AI的能源优化模型,用于智慧城市与低碳建筑的运营管理。 +

+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+

+ "Data is the soil, Algorithms are the seeds, Design is the fruit." +

+
+
+ 20+ + Projects +
+
+ 15 + Partners +
+
+ 50+ + Publications +
+
+
+
+ + +
+
+
+

Internal Resources

+

内部资源

+

登录后查看实验室内部资料和详细数据

+
+ + + + + + + + +
+

详细统计

+
+
+
--
+
总文章数
+
+
+
--
+
总资源数
+
+
+
--
+
团队成员
+
+
+
--
+
总下载量
+
+
+
+
+
+ + +
+
+
+
+

Team / 团队

+

+ 我们是一个多元化的团队,成员背景涵盖计算机科学、建筑学、生物学及社会学。 +

+ + 查看全体成员名单 + +
+
+
+ +
+
+

Dr. Lin Chen

+

Principal Investigator / 实验室主任

+

+ 博士毕业于MIT Media Lab,专注于计算设计与人机交互领域的研究。 +

+
+ +
+
+

Sarah Wu

+

Lead Ecologist / 生态学顾问

+

+ 城市生态学专家,研究重点为城市微气候与生物多样性修复。 +

+
+ +
+
+

James Zhao

+

AI Researcher / 算法工程师

+

+ 专攻深度强化学习在复杂系统模拟中的应用。 +

+
+ +
+
+

Yu Zhang

+

Design Lead / 设计主创

+

+ 探索参数化设计与数字建造的结合。 +

+
+
+
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/officefile/prompts.md b/officefile/prompts.md new file mode 100644 index 0000000..b4be064 --- /dev/null +++ b/officefile/prompts.md @@ -0,0 +1,13 @@ +# 后台运行前后端命令 +## 启动后端服务器 +cd /home/admin305/Projects/2025_DAEL/dofile/backend +source .venv/bin/activate +nohup python app.py > app.log 2>&1 & + +## 启动前端服务器 +cd /home/admin305/Projects/2025_DAEL/dofile +nohup python3 -m http.server 6002 --bind 0.0.0.0 > http.log 2>&1 & + +- 端口号 +[1] 1998431 +[2] 1998432