├── README.md ├── README.zh.md ├── app ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-312.pyc │ ├── models.cpython-312.pyc │ └── routes.cpython-312.pyc ├── controllers │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-312.pyc │ │ ├── idea_controller.cpython-312.pyc │ │ ├── main_controller.cpython-312.pyc │ │ ├── notes_controller.cpython-312.pyc │ │ ├── papers_controller.cpython-312.pyc │ │ ├── plans_controller.cpython-312.pyc │ │ └── projects_controller.cpython-312.pyc │ ├── idea_controller.py │ ├── main_controller.py │ ├── notes_controller.py │ ├── papers_controller.py │ ├── plans_controller.py │ └── projects_controller.py ├── models.py ├── routes.py ├── static │ ├── css │ │ ├── ideas.css │ │ ├── index.css │ │ ├── notes.css │ │ ├── papers.css │ │ ├── plans.css │ │ ├── projects.css │ │ ├── styles.css │ │ ├── view_idea.css │ │ ├── view_note.css │ │ └── view_project.css │ ├── dist │ │ ├── fullcalendar │ │ │ ├── index.global.js │ │ │ └── index.global.min.js │ │ ├── highlight │ │ │ ├── default.min.css │ │ │ └── highlight.min.js │ │ ├── html2pdf │ │ │ ├── es6-promise.auto.min.js │ │ │ ├── html2canvas.min.js │ │ │ ├── html2pdf.bundle.min.js │ │ │ └── jspdf.umd.min.js │ │ ├── timelineJS │ │ │ └── timeline.js │ │ └── turndown │ │ │ └── turndown.js │ ├── images │ │ ├── Raphael.ico │ │ └── Raphael.png │ └── js │ │ ├── ideas.js │ │ ├── index.js │ │ ├── notes.js │ │ ├── papers.js │ │ ├── plans.js │ │ ├── projects.js │ │ ├── scripts.js │ │ ├── view_idea.js │ │ ├── view_note.js │ │ └── view_project.js ├── templates │ ├── base.html │ ├── ideas.html │ ├── index.html │ ├── notes.html │ ├── papers.html │ ├── plans.html │ ├── projects.html │ ├── view_idea.html │ ├── view_note.html │ └── view_project.html └── utils │ ├── __pycache__ │ └── helper.cpython-312.pyc │ └── helper.py ├── config.py ├── init_db.py ├── requirements.txt ├── run.py ├── run.spec └── screenshots ├── ideas.jpg ├── index.jpg ├── notes.jpg ├── notes2.jpg ├── overview.jpg ├── papers.jpg ├── plans.jpg └── projects.jpg /README.md: -------------------------------------------------------------------------------- 1 | # Raphael - Your Personal Research Assistant 2 | 3 | [中文版](README.zh.md) 4 | 5 | ![Raphael Overview](screenshots/overview.jpg) 6 | 7 | ## Overview 8 | 9 | **Raphael** is a comprehensive academic project management tool designed to streamline your research workflow. Whether you are managing projects, organizing ideas, writing papers, or keeping detailed notes, Raphael provides all the essential features to enhance your productivity and efficiency. 10 | 11 | ## Features 12 | 13 | - **Project Management**: Easily create, edit, and delete projects to organize your research effectively. 14 | - **Plan Management**: Develop detailed plans, set research goals, and track milestones. 15 | - **Idea Library**: Capture and organize your research ideas in a centralized location. 16 | - **Paper Management**: Seamlessly write, edit, and manage your academic papers. 17 | - **Note-Taking**: Keep comprehensive notes that are easy to access and search. 18 | - **File Management**: Upload, categorize, search, and manage files related to your projects. 19 | - **Task Management**: Create, assign, and track tasks to ensure timely completion of project goals. 20 | - **Milestone Tracking**: Visualize project progress through an interactive timeline showcasing key milestones. 21 | - **Frontend Libraries**: 22 | - **FullCalendar**: For interactive calendars and scheduling features. 23 | - **html2pdf**: Converts HTML content into PDF documents. 24 | - **MathJax**: Beautifully renders mathematical formulas. 25 | - **TimelineJS**: Creates dynamic and visually appealing timelines. 26 | - **Markdown-it**: Provides a Markdown compiler for creating and editing notes. 27 | 28 | ## Benefits 29 | 30 | - **Extreme Lightweight and Small**: Thanks to browser-based rendering, the entire software is optimized to a file size of only **22MB**, ensuring smooth operation even on low-spec hardware. For comparison, the popular paper management software **Zotero** has an installer size of **85MB**. 31 | - **Integrated with Many Features**: Combines multiple project management functions into one unified tool. 32 | - **Low Performance Overhead**: Runs efficiently with minimal resource usage, suitable for various hardware configurations. 33 | - **Cross-Platform Convenience**: Designed without dependence on a specific GUI system, ensuring seamless operation across different operating systems. 34 | - **Minimal Dependencies**: No reliance on heavy external UI systems, making maintenance and deployment easier. 35 | - **Simplified and Beautiful Design**: A clean and intuitive user interface to enhance user experience without unnecessary complexity. 36 | 37 | ## Known Issues 38 | 39 | - **CSS Management**: Since this is my first time writing a large frontend project, the current CSS structure is somewhat messy. Future updates will focus on improving and modularizing the CSS for better maintainability. 40 | - **Feature Enhancements**: While Raphael covers a broad range of project management needs, there is always room for more features based on user feedback. 41 | 42 | Contributions and improvements are welcome. Feel free to fork the repository and submit a Pull Request! 43 | 44 | ## Installation 45 | 46 | ### Prerequisites 47 | 48 | - **Python 3.7+** 49 | - **pip** 50 | - **Git** 51 | 52 | ### Steps 53 | 54 | 1. **Clone the Repository** 55 | 56 | ```bash 57 | git clone https://github.com/574118090/Raphael-Assistant.git 58 | cd Raphael 59 | ``` 60 | 61 | 2. **Install Dependencies** 62 | 63 | ```bash 64 | pip install -r requirements.txt 65 | ``` 66 | 67 | 3. **Initialize the Database** 68 | 69 | ```bash 70 | python init_db.py 71 | ``` 72 | 73 | 4. **Run the Application** 74 | 75 | ```bash 76 | python run.py 77 | ``` 78 | 79 | 5. **Alternatively, download the release executable** 80 | 81 | - Visit the [Releases](https://github.com/yourusername/Raphael/releases) page. 82 | - Download the latest `Raphael.exe`. 83 | - Run the executable to start Raphael without setting up a Python environment. 84 | 85 | 6. **Access Raphael** 86 | 87 | - **If running from source**: 88 | - Open a browser and visit [http://127.0.0.1:21823](http://127.0.0.1:21823). Raphael will launch and show up in the system tray. **Access the app via the tray icon or the shortcut Alt+R.** 89 | - **If running the `.exe` file**: 90 | - Raphael will launch and show up in the system tray. Access the app via the tray icon or the shortcut Alt+R. 91 | 92 | ## Packaging the Application 93 | 94 | To create a standalone executable file (`.exe`) using PyInstaller, follow these steps: 95 | 96 | 1. **Make sure all dependencies are installed** 97 | 98 | ```bash 99 | pip install -r requirements.txt 100 | ``` 101 | 102 | 2. **Run PyInstaller using the provided Spec file** 103 | 104 | ```bash 105 | pyinstaller run.spec 106 | ``` 107 | 108 | - This will generate a `dist` folder containing the `Raphael.exe` executable file. 109 | 110 | ## Usage 111 | 112 | ### Creating a Project 113 | 114 | 1. Click the **Create Project** button. 115 | 2. Fill in project details including the name, description, client, and type. 116 | 3. Submit the form to create a new project. 117 | 118 | ### Managing Projects 119 | 120 | - **Edit**: Click the edit icon on the project card to modify project details. 121 | - **Delete**: Click the trash can icon to delete a project. 122 | 123 | ### System Tray Features 124 | 125 | - **Access Raphael**: Double-click the Raphael icon in the system tray to open the app. 126 | - **Open Webpage**: Click the "Open Webpage" option to open Raphael in your browser. 127 | - **Exit**: Close Raphael via the system tray. 128 | 129 | ### Navigation Features 130 | 131 | Use the navigation bar to switch between plans, projects, ideas, papers, and notes. 132 | 133 | ### File Management 134 | 135 | - Upload and categorize files within each project. 136 | - Efficiently search and manage your files. 137 | 138 | ### Task and Milestone Management 139 | 140 | - Create and track tasks related to your projects. 141 | - Visualize project progress using the interactive timeline. 142 | 143 | ## Tech Stack 144 | 145 | - **Backend**: 146 | - **Flask**: A lightweight WSGI web application framework. 147 | - **SQLAlchemy**: ORM for database interaction. 148 | - **Frontend**: 149 | - **FullCalendar**: For interactive calendar and scheduling features. 150 | - **html2pdf**: Converts HTML content into PDF documents. 151 | - **MathJax**: Renders mathematical formulas. 152 | - **TimelineJS**: Creates dynamic timelines. 153 | - **Markdown-it**: Provides a Markdown compiler for creating and editing notes. 154 | - **Other Libraries**: 155 | - **pystray**: For system tray icon integration. 156 | - **PIL (Pillow)**: Image processing. 157 | - **pyinstaller**: For packaging Python applications. 158 | 159 | ## Screenshots 160 | 161 | ### Overview 162 | 163 | ### Project Dashboard 164 | 165 | ![Project Dashboard](screenshots/projects.jpg) 166 | *Shows the project dashboard.* 167 | 168 | ### Plan Calendar 169 | 170 | ![Plan Calendar](screenshots/plans.jpg) 171 | *Interactive calendar for managing plans.* 172 | 173 | ### Idea Library 174 | 175 | ![Idea Library](screenshots/ideas.jpg) 176 | *Interface for managing ideas in a centralized location.* 177 | 178 | ### Paper Management 179 | 180 | ![Paper Management](screenshots/papers.jpg) 181 | *Interface for managing academic papers.* 182 | 183 | ### Notes Section 184 | 185 | ![Notes Section](screenshots/notes.jpg) 186 | *Comprehensive note-taking interface.* 187 | 188 | ![Notes Section 2](screenshots/notes2.jpg) 189 | *Note editing interface.* 190 | 191 | ## Contributing 192 | 193 | Contributions are welcome! Please follow these steps: 194 | 195 | 1. **Fork the repository** 196 | 197 | 2. **Create a new branch** 198 | 199 | ```bash 200 | git checkout -b feature/YourFeature 201 | ``` 202 | 203 | 3. **Commit changes** 204 | 205 | ```bash 206 | git commit -m "Add YourFeature" 207 | ``` 208 | 209 | 4. **Push to the branch** 210 | 211 | ```bash 212 | git push origin feature/YourFeature 213 | ``` 214 | 215 | 5. **Open a Pull Request** 216 | 217 | ## License 218 | 219 | This project is licensed under the [MIT License](LICENSE). 220 | 221 | ## Contact 222 | 223 | If you have any questions or need support, please contact [chenlizheme@gmail.com](mailto:chenlizheme@gmail.com). -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # Raphael - 您的私人研究助理 2 | 3 | [English Version](README.md) 4 | 5 | ![Raphael 概述](screenshots/overview.jpg) 6 | 7 | ## 概述 8 | 9 | **Raphael** 是一款全面的学术项目管理工具,旨在简化您的研究工作流程。无论您是在管理项目、组织想法、撰写论文还是记录详细笔记,Raphael 都提供了所有必要的功能,以提升您的生产力和效率。 10 | 11 | ## 功能 12 | 13 | - **项目管理**:轻松创建、编辑和删除项目,有效组织您的研究工作。 14 | - **计划管理**:制定详细的计划,规划您的研究目标和里程碑。 15 | - **想法库**:在一个集中位置捕捉和组织您的研究想法。 16 | - **论文管理**:无缝撰写、编辑和管理您的学术论文。 17 | - **笔记记录**:保持全面的笔记,易于访问和搜索。 18 | - **文件处理**:轻松上传、分类、搜索和管理与项目相关的文件。 19 | - **任务管理**:创建、分配和跟踪任务,确保项目目标的及时完成。 20 | - **里程碑跟踪**:通过互动时间线可视化项目进展,展示重要里程碑。 21 | - **前端库**: 22 | - **FullCalendar**:用于交互式日历和调度功能。 23 | - **html2pdf**:将 HTML 内容转换为 PDF 文档。 24 | - **MathJax**:美观地渲染数学公式。 25 | - **TimelineJS**:创建动态且视觉吸引力的时间线。 26 | - **Markdown-it**:提供 Markdown 编译器,用于创建和编辑笔记。 27 | 28 | ## 优势 29 | 30 | - **极致轻量化和小型化**:由于依赖浏览器网页渲染,整个软件的大小控制在了只有 **20MB**,确保在低配置的硬件上也能流畅运行。与之形成对比的是流行的论文管理软件**Zotero**,仅安装包就达到了**85MB**。 31 | - **集成了大量功能**:将多个项目管理功能整合到一个统一的工具中。 32 | - **性能开销低**:优化效率高,Raphael 以低性能开销运行,适用于各种硬件配置。 33 | - **跨平台便利性**:设计无需依赖特定的 GUI 系统,确保在不同操作系统上无缝运行。 34 | - **最小化依赖**:不依赖于繁重的外部 UI 系统,便于维护和部署。 35 | - **简约美观的设计**:简洁直观的用户界面,提升用户体验,避免不必要的复杂性。 36 | 37 | ## 已知问题 38 | 39 | - **CSS 管理**:由于这是我第一次编写大型前端项目,目前的 CSS 结构有些混乱。未来的更新将致力于改进和模块化 CSS 以提高可维护性。 40 | - **功能增强**:虽然 Raphael 覆盖了广泛的项目管理需求,但基于用户反馈,总有机会添加更多功能。 41 | 42 | 欢迎贡献和改进。请随时 Fork 仓库并提交 Pull Request! 43 | 44 | ## 安装 45 | 46 | ### 前提条件 47 | 48 | - **Python 3.7+** 49 | - **pip** 50 | - **Git** 51 | 52 | ### 步骤 53 | 54 | 1. **克隆仓库** 55 | 56 | ```bash 57 | git clone https://github.com/574118090/Raphael-Assistant.git 58 | cd Raphael 59 | ``` 60 | 61 | 2. **安装依赖** 62 | 63 | ```bash 64 | pip install -r requirements.txt 65 | ``` 66 | 67 | 3. **初始化数据库** 68 | 69 | ```bash 70 | python init_db.py 71 | ``` 72 | 73 | 4. **运行应用** 74 | 75 | ```bash 76 | python run.py 77 | ``` 78 | 79 | 5. **或者,下载发布版可执行文件** 80 | 81 | - 访问 [Releases](https://github.com/yourusername/Raphael/releases) 页面。 82 | - 下载最新的 `Raphael.exe`。 83 | - 运行可执行文件以启动 Raphael,无需设置 Python 环境。 84 | 85 | 6. **访问 Raphael** 86 | 87 | - **如果通过源码运行**: 88 | - 打开浏览器,访问 [http://127.0.0.1:21823](http://127.0.0.1:21823)。Raphael 将启动并显示在系统托盘中,使用托盘图标或是快捷键Alt+R访问应用。 89 | - **如果运行 `.exe` 文件**: 90 | - Raphael 将启动并显示在系统托盘中。使用托盘图标或是快捷键Alt+R访问应用。 91 | 92 | ## 打包应用 93 | 94 | 要使用 PyInstaller 创建独立的可执行文件(`.exe`),请按照以下步骤操作: 95 | 96 | 1. **确保已安装所有依赖** 97 | 98 | ```bash 99 | pip install -r requirements.txt 100 | ``` 101 | 102 | 2. **使用提供的 Spec 文件运行 PyInstaller** 103 | 104 | ```bash 105 | pyinstaller run.spec 106 | ``` 107 | 108 | - 这将生成一个 `dist` 文件夹,包含 `Raphael.exe` 可执行文件。 109 | 110 | ## 使用 111 | 112 | ### 创建项目 113 | 114 | 1. 点击 **创建项目** 按钮。 115 | 2. 填写项目详情,包括名称、简介、甲方和类型。 116 | 3. 提交表单以创建新项目。 117 | 118 | ### 管理项目 119 | 120 | - **编辑**:点击项目卡片上的编辑图标以修改项目详情。 121 | - **删除**:点击垃圾桶图标以删除项目。 122 | 123 | ### 系统托盘功能 124 | 125 | - **访问 Raphael**:双击系统托盘中的 Raphael 图标以打开应用。 126 | - **打开网页**:点击“打开网页”选项以在浏览器中打开 Raphael。 127 | - **退出**:通过系统托盘关闭 Raphael。 128 | 129 | ### 导航功能 130 | 131 | 使用导航栏在计划、项目、想法、论文和笔记之间切换。 132 | 133 | ### 文件管理 134 | 135 | - 在每个项目内上传和分类文件。 136 | - 高效搜索和管理您的文件。 137 | 138 | ### 任务与里程碑管理 139 | 140 | - 创建和跟踪与项目相关的任务。 141 | - 使用互动时间线可视化项目进展。 142 | 143 | ## 技术栈 144 | 145 | - **后端**: 146 | - **Flask**:轻量级的 WSGI Web 应用框架。 147 | - **SQLAlchemy**:用于数据库交互的 ORM。 148 | - **前端**: 149 | - **FullCalendar**:用于交互式日历和调度。 150 | - **html2pdf**:将 HTML 内容转换为 PDF。 151 | - **MathJax**:渲染数学表达式。 152 | - **TimelineJS**:创建动态时间线。 153 | - **Markdown-it**:提供 Markdown 编辑器,用于创建和编辑笔记。 154 | - **其他库**: 155 | - **pystray**:系统托盘图标集成。 156 | - **PIL (Pillow)**:图像处理。 157 | - **pyinstaller**:打包 Python 应用。 158 | 159 | ## 截图 160 | 161 | ### 概述 162 | 163 | ### 项目仪表盘 164 | 165 | ![项目仪表盘](screenshots/projects.jpg) 166 | *展示项目的项目仪表盘。* 167 | 168 | ### 计划日历 169 | 170 | ![计划日历](screenshots/plans.jpg) 171 | *用于管理计划的交互式日历。* 172 | 173 | ### 想法库 174 | 175 | ![想法库](screenshots/ideas.jpg) 176 | *集中管理想法的界面。* 177 | 178 | ### 论文管理 179 | 180 | ![论文管理](screenshots/papers.jpg) 181 | *管理学术论文的界面。* 182 | 183 | ### 笔记部分 184 | 185 | ![笔记部分](screenshots/notes.jpg) 186 | *全面的笔记记录界面。* 187 | 188 | ![笔记部分2](screenshots/notes2.jpg) 189 | *笔记编辑界面。* 190 | 191 | ## 贡献 192 | 193 | 欢迎贡献!请按照以下步骤操作: 194 | 195 | 1. **Fork 仓库** 196 | 197 | 2. **创建新分支** 198 | 199 | ```bash 200 | git checkout -b feature/YourFeature 201 | ``` 202 | 203 | 3. **提交更改** 204 | 205 | ```bash 206 | git commit -m "添加 YourFeature" 207 | ``` 208 | 209 | 4. **推送到分支** 210 | 211 | ```bash 212 | git push origin feature/YourFeature 213 | ``` 214 | 215 | 5. **打开 Pull Request** 216 | 217 | ## 许可证 218 | 219 | 本项目基于 [MIT 许可证](LICENSE) 许可。 220 | 221 | ## 联系方式 222 | 223 | 如有任何疑问或需要支持,请联系 [chenlizheme@gmail.com](mailto:chenlizheme@gmail.com)。 224 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Flask 3 | from flask_cors import CORS 4 | from flask_sqlalchemy import SQLAlchemy 5 | from config import Config 6 | 7 | db = SQLAlchemy() 8 | 9 | def create_app(): 10 | app = Flask(__name__) 11 | app.config.from_object(Config) 12 | 13 | CORS(app) 14 | db.init_app(app) 15 | 16 | from .routes import main_bp 17 | app.register_blueprint(main_bp) 18 | 19 | return app 20 | -------------------------------------------------------------------------------- /app/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /app/__pycache__/models.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/__pycache__/models.cpython-312.pyc -------------------------------------------------------------------------------- /app/__pycache__/routes.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/__pycache__/routes.cpython-312.pyc -------------------------------------------------------------------------------- /app/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/controllers/__init__.py -------------------------------------------------------------------------------- /app/controllers/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/controllers/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /app/controllers/__pycache__/idea_controller.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/controllers/__pycache__/idea_controller.cpython-312.pyc -------------------------------------------------------------------------------- /app/controllers/__pycache__/main_controller.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/controllers/__pycache__/main_controller.cpython-312.pyc -------------------------------------------------------------------------------- /app/controllers/__pycache__/notes_controller.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/controllers/__pycache__/notes_controller.cpython-312.pyc -------------------------------------------------------------------------------- /app/controllers/__pycache__/papers_controller.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/controllers/__pycache__/papers_controller.cpython-312.pyc -------------------------------------------------------------------------------- /app/controllers/__pycache__/plans_controller.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/controllers/__pycache__/plans_controller.cpython-312.pyc -------------------------------------------------------------------------------- /app/controllers/__pycache__/projects_controller.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/controllers/__pycache__/projects_controller.cpython-312.pyc -------------------------------------------------------------------------------- /app/controllers/idea_controller.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from ..models import Idea, RelatedPaper 4 | from .. import db 5 | from ..utils.helper import format_response 6 | 7 | idea_api = Blueprint('idea_api', __name__, url_prefix='/api/ideas') 8 | 9 | @idea_api.route('', methods=['GET']) 10 | def get_ideas(): 11 | ideas = Idea.query.all() 12 | ideas_list = [{'id': idea.id, 'title': idea.title, 'description': idea.description} for idea in ideas] 13 | return jsonify(format_response(ideas_list)), 200 14 | 15 | @idea_api.route('', methods=['POST']) 16 | def add_idea(): 17 | data = request.get_json() 18 | title = data.get('title') 19 | description = data.get('description', '') 20 | if not title or not description: 21 | return jsonify(format_response({'error': '标题和描述是必填项'}, status=400)), 400 22 | new_idea = Idea(title=title, description=description) 23 | db.session.add(new_idea) 24 | db.session.commit() 25 | return jsonify(format_response({'message': '想法添加成功', 'id': new_idea.id})), 201 26 | 27 | @idea_api.route('/', methods=['GET']) 28 | def get_idea_detail(idea_id): 29 | idea = Idea.query.get_or_404(idea_id) 30 | idea_detail = { 31 | 'id': idea.id, 32 | 'title': idea.title, 33 | 'description': idea.description, 34 | 'background': idea.background, 35 | 'motivation': idea.motivation, 36 | 'challenge': idea.challenge, 37 | 'method': idea.method, 38 | 'experiment': idea.experiment, 39 | 'innovation': idea.innovation, 40 | 'papers': idea.papers, 41 | 'related_papers': [{'id': rp.id, 'title': rp.title, 'content': rp.content, 'link': rp.link} for rp in idea.related_papers] 42 | } 43 | return jsonify(format_response(idea_detail)), 200 44 | 45 | @idea_api.route('/', methods=['PUT']) 46 | def update_idea_detail(idea_id): 47 | idea = Idea.query.get_or_404(idea_id) 48 | data = request.get_json() 49 | idea.title = data.get('title', idea.title) 50 | idea.description = data.get('description', idea.description) 51 | idea.background = data.get('background', idea.background) 52 | idea.motivation = data.get('motivation', idea.motivation) 53 | idea.challenge = data.get('challenge', idea.challenge) 54 | idea.method = data.get('method', idea.method) 55 | idea.experiment = data.get('experiment', idea.experiment) 56 | idea.innovation = data.get('innovation', idea.innovation) 57 | idea.papers = data.get('papers', idea.papers) 58 | db.session.commit() 59 | return jsonify(format_response({'message': '想法更新成功'})), 200 60 | 61 | @idea_api.route('/', methods=['DELETE']) 62 | def delete_idea(idea_id): 63 | idea = Idea.query.get_or_404(idea_id) 64 | db.session.delete(idea) 65 | db.session.commit() 66 | return jsonify(format_response({'message': '想法删除成功'})), 200 67 | 68 | @idea_api.route('//related_papers', methods=['POST']) 69 | def add_related_paper(idea_id): 70 | idea = Idea.query.get_or_404(idea_id) 71 | data = request.get_json() 72 | title = data.get('title') 73 | content = data.get('content', '') 74 | link = data.get('link', '') 75 | if not title: 76 | return jsonify(format_response({'error': '论文标题是必填项'}, status=400)), 400 77 | new_paper = RelatedPaper(title=title, content=content, link=link, idea=idea) 78 | db.session.add(new_paper) 79 | db.session.commit() 80 | return jsonify(format_response({'message': '关联论文添加成功', 'id': new_paper.id})), 201 81 | 82 | @idea_api.route('//related_papers/', methods=['PUT']) 83 | def update_related_paper(idea_id, paper_id): 84 | idea = Idea.query.get_or_404(idea_id) 85 | paper = RelatedPaper.query.filter_by(id=paper_id, idea_id=idea_id).first_or_404() 86 | data = request.get_json() 87 | paper.title = data.get('title', paper.title) 88 | paper.content = data.get('content', paper.content) 89 | paper.link = data.get('link', paper.link) 90 | db.session.commit() 91 | return jsonify(format_response({'message': '关联论文更新成功'})), 200 92 | 93 | @idea_api.route('//related_papers/', methods=['DELETE']) 94 | def delete_related_paper(idea_id, paper_id): 95 | idea = Idea.query.get_or_404(idea_id) 96 | paper = RelatedPaper.query.filter_by(id=paper_id, idea_id=idea_id).first_or_404() 97 | db.session.delete(paper) 98 | db.session.commit() 99 | return jsonify(format_response({'message': '关联论文删除成功'})), 200 100 | -------------------------------------------------------------------------------- /app/controllers/main_controller.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from ..models import Plan, Project, Idea, Paper, Note 4 | from .. import db 5 | from ..utils.helper import format_response 6 | from datetime import datetime 7 | from config import Config 8 | 9 | main_api = Blueprint('api', __name__, url_prefix='/api') 10 | 11 | @main_api.route('/plans', methods=['GET']) 12 | def get_plans(): 13 | plans = Plan.query.all() 14 | plans_list = [ 15 | { 16 | 'id': plan.id, 17 | 'title': plan.title, 18 | 'description': plan.description, 19 | 'date': plan.date.strftime('%Y-%m-%d'), 20 | 'time': plan.time.strftime('%H:%M') 21 | } 22 | for plan in plans 23 | ] 24 | return jsonify(format_response(plans_list)) 25 | 26 | @main_api.route('/plans/', methods=['GET']) 27 | def get_plans_by_date(date): 28 | try: 29 | target_date = datetime.strptime(date, '%Y-%m-%d').date() 30 | except ValueError: 31 | return jsonify(format_response({'error': 'Invalid date format. Use YYYY-MM-DD.'}, status=400)), 400 32 | 33 | plans = Plan.query.filter_by(date=target_date).order_by(Plan.time).all() 34 | plans_list = [ 35 | { 36 | 'id': plan.id, 37 | 'title': plan.title, 38 | 'description': plan.description, 39 | 'time': plan.time.strftime('%H:%M') 40 | } 41 | for plan in plans 42 | ] 43 | return jsonify(format_response(plans_list)) 44 | 45 | @main_api.route('/plans', methods=['POST']) 46 | def add_plan(): 47 | data = request.get_json() 48 | title = data.get('title') 49 | description = data.get('description', '') 50 | date_str = data.get('date') 51 | time_str = data.get('time') 52 | 53 | if not title or not date_str or not time_str: 54 | return jsonify(format_response({'error': 'Title, date, and time are required'}, status=400)), 400 55 | 56 | try: 57 | plan_date = datetime.strptime(date_str, '%Y-%m-%d').date() 58 | plan_time = datetime.strptime(time_str, '%H:%M').time() 59 | except ValueError: 60 | return jsonify(format_response({'error': 'Invalid date or time format.'}, status=400)), 400 61 | 62 | new_plan = Plan(title=title, description=description, date=plan_date, time=plan_time) 63 | db.session.add(new_plan) 64 | db.session.commit() 65 | return jsonify(format_response({'message': 'Plan added successfully'})), 201 66 | 67 | @main_api.route('/projects', methods=['GET']) 68 | def get_projects(): 69 | projects = Project.query.all() 70 | projects_list = [{'id': project.id, 'name': project.name, 'description': project.description} for project in projects] 71 | return jsonify(format_response(projects_list)) 72 | 73 | @main_api.route('/projects', methods=['POST']) 74 | def add_project(): 75 | data = request.get_json() 76 | name = data.get('name') 77 | description = data.get('description', '') 78 | if not name: 79 | return jsonify(format_response({'error': 'Name is required'}, status=400)), 400 80 | new_project = Project(name=name, description=description) 81 | db.session.add(new_project) 82 | db.session.commit() 83 | return jsonify(format_response({'message': 'Project added successfully'})), 201 84 | 85 | @main_api.route('/ideas', methods=['GET']) 86 | def get_ideas(): 87 | ideas = Idea.query.all() 88 | ideas_list = [{'id': idea.id, 'title': idea.title, 'description': idea.description} for idea in ideas] 89 | return jsonify(format_response(ideas_list)) 90 | 91 | @main_api.route('/ideas', methods=['POST']) 92 | def add_idea(): 93 | data = request.get_json() 94 | title = data.get('title') 95 | description = data.get('description', '') 96 | if not title: 97 | return jsonify(format_response({'error': 'Title is required'}, status=400)), 400 98 | new_idea = Idea(title=title, description=description) 99 | db.session.add(new_idea) 100 | db.session.commit() 101 | return jsonify(format_response({'message': 'Idea added successfully'})), 201 102 | 103 | @main_api.route('/papers', methods=['GET']) 104 | def get_papers(): 105 | papers = Paper.query.all() 106 | papers_list = [{'id': paper.id, 'title': paper.title, 'abstract': paper.abstract} for paper in papers] 107 | return jsonify(format_response(papers_list)) 108 | 109 | @main_api.route('/papers', methods=['POST']) 110 | def add_paper(): 111 | data = request.get_json() 112 | title = data.get('title') 113 | abstract = data.get('abstract', '') 114 | if not title: 115 | return jsonify(format_response({'error': 'Title is required'}, status=400)), 400 116 | new_paper = Paper(title=title, abstract=abstract) 117 | db.session.add(new_paper) 118 | db.session.commit() 119 | return jsonify(format_response({'message': 'Paper added successfully'})), 201 120 | 121 | @main_api.route('/notes', methods=['GET']) 122 | def get_notes(): 123 | notes = Note.query.all() 124 | notes_list = [{'id': note.id, 'content': note.content} for note in notes] 125 | return jsonify(format_response(notes_list)) 126 | 127 | @main_api.route('/notes', methods=['POST']) 128 | def add_note(): 129 | data = request.get_json() 130 | content = data.get('content') 131 | if not content: 132 | return jsonify(format_response({'error': 'Content is required'}, status=400)), 400 133 | new_note = Note(content=content) 134 | db.session.add(new_note) 135 | db.session.commit() 136 | return jsonify(format_response({'message': 'Note added successfully'})), 201 137 | 138 | 139 | from flask import Blueprint, request, jsonify, send_from_directory, abort 140 | from ..models import Plan, Project, Idea, Paper, Note 141 | from .. import db 142 | from ..utils.helper import format_response 143 | from pathlib import Path 144 | 145 | main_api = Blueprint('api', __name__, url_prefix='/api') -------------------------------------------------------------------------------- /app/controllers/notes_controller.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, jsonify, request, current_app, render_template 3 | import os 4 | 5 | notes_api = Blueprint('notes_api', __name__, url_prefix='/api/notes') 6 | 7 | @notes_api.route('/list', methods=['GET']) 8 | def list_notes(): 9 | notes_folder = current_app.config['NOTES_FOLDER'] 10 | requested_path = request.args.get('path', '') # 获取请求的路径 11 | 12 | # 构造绝对路径 13 | abs_path = os.path.abspath(os.path.join(notes_folder, requested_path)) 14 | 15 | # 确保请求的路径在 NOTES_FOLDER 内 16 | if not abs_path.startswith(os.path.abspath(notes_folder)): 17 | return jsonify({ 18 | 'status': 'error', 19 | 'message': 'Invalid path.' 20 | }), 400 21 | 22 | if not os.path.exists(abs_path): 23 | return jsonify({ 24 | 'status': 'error', 25 | 'message': 'Path does not exist.' 26 | }), 404 27 | 28 | items = os.listdir(abs_path) 29 | notes = [] 30 | for item in items: 31 | item_path = os.path.join(abs_path, item) 32 | if os.path.isdir(item_path): 33 | notes.append({ 34 | 'type': 'folder', 35 | 'name': item, 36 | 'path': os.path.join(requested_path, item).replace('\\', '/') 37 | }) 38 | elif item.endswith('.md'): 39 | notes.append({ 40 | 'type': 'file', 41 | 'name': item, 42 | 'path': os.path.join(requested_path, item).replace('\\', '/') 43 | }) 44 | 45 | # 获取上级目录路径 46 | parent_path = os.path.dirname(requested_path) 47 | if parent_path == requested_path: 48 | parent_path = '' 49 | 50 | return jsonify({ 51 | 'status': 'success', 52 | 'data': notes, 53 | 'current_path': requested_path, 54 | 'parent_path': parent_path 55 | }) 56 | 57 | @notes_api.route('/create_folder', methods=['POST']) 58 | def create_folder(): 59 | data = request.get_json() 60 | path = data.get('path', '') 61 | folder_name = data.get('folder_name', '新文件夹') 62 | 63 | # Sanitize folder name 64 | folder_name = os.path.basename(folder_name) 65 | 66 | notes_folder = current_app.config['NOTES_FOLDER'] 67 | abs_path = os.path.abspath(os.path.join(notes_folder, path, folder_name)) 68 | 69 | # Ensure the path is within NOTES_FOLDER 70 | if not abs_path.startswith(os.path.abspath(notes_folder)): 71 | return jsonify({'status': 'error', 'message': 'Invalid path.'}), 400 72 | 73 | if os.path.exists(abs_path): 74 | return jsonify({'status': 'error', 'message': '文件夹已存在。'}), 400 75 | 76 | try: 77 | os.makedirs(abs_path) 78 | return jsonify({'status': 'success', 'message': '文件夹创建成功。'}), 201 79 | except Exception as e: 80 | return jsonify({'status': 'error', 'message': str(e)}), 500 81 | 82 | @notes_api.route('/create_file', methods=['POST']) 83 | def create_file(): 84 | data = request.get_json() 85 | path = data.get('path', '') 86 | file_name = data.get('file_name', '新笔记.md') 87 | 88 | # Sanitize file name 89 | file_name = os.path.basename(file_name) 90 | if not file_name.endswith('.md'): 91 | file_name += '.md' 92 | 93 | notes_folder = current_app.config['NOTES_FOLDER'] 94 | abs_path = os.path.abspath(os.path.join(notes_folder, path, file_name)) 95 | 96 | # Ensure the path is within NOTES_FOLDER 97 | if not abs_path.startswith(os.path.abspath(notes_folder)): 98 | return jsonify({'status': 'error', 'message': 'Invalid path.'}), 400 99 | 100 | if os.path.exists(abs_path): 101 | return jsonify({'status': 'error', 'message': '文件已存在。'}), 400 102 | 103 | try: 104 | with open(abs_path, 'w', encoding='utf-8') as f: 105 | f.write('') # 创建空的 markdown 文件 106 | return jsonify({'status': 'success', 'message': '文件创建成功。'}), 201 107 | except Exception as e: 108 | return jsonify({'status': 'error', 'message': str(e)}), 500 109 | 110 | @notes_api.route('/rename', methods=['POST']) 111 | def rename_item(): 112 | data = request.get_json() 113 | path = data.get('path', '') 114 | old_name = data.get('old_name') 115 | new_name = data.get('new_name') 116 | 117 | if not old_name or not new_name: 118 | return jsonify({'status': 'error', 'message': '旧名称和新名称是必需的。'}), 400 119 | 120 | # Sanitize new name 121 | new_name = os.path.basename(new_name) 122 | 123 | notes_folder = current_app.config['NOTES_FOLDER'] 124 | old_path = os.path.abspath(os.path.join(notes_folder, path, old_name)) 125 | new_path = os.path.abspath(os.path.join(notes_folder, path, new_name)) 126 | 127 | # Ensure the paths are within NOTES_FOLDER 128 | if not old_path.startswith(os.path.abspath(notes_folder)) or not new_path.startswith(os.path.abspath(notes_folder)): 129 | return jsonify({'status': 'error', 'message': 'Invalid path.'}), 400 130 | 131 | if not os.path.exists(old_path): 132 | return jsonify({'status': 'error', 'message': '原文件或文件夹不存在。'}), 404 133 | 134 | if os.path.exists(new_path): 135 | return jsonify({'status': 'error', 'message': '新名称已存在。'}), 400 136 | 137 | try: 138 | os.rename(old_path, new_path) 139 | return jsonify({'status': 'success', 'message': '重命名成功。'}), 200 140 | except Exception as e: 141 | return jsonify({'status': 'error', 'message': str(e)}), 500 142 | 143 | import os 144 | import shutil 145 | from flask import request, jsonify, current_app 146 | 147 | import os 148 | import shutil 149 | from flask import request, jsonify, current_app 150 | 151 | @notes_api.route('/delete', methods=['POST']) 152 | def delete_item(): 153 | data = request.get_json() 154 | path = data.get('path', '') 155 | name = data.get('name') 156 | force_delete = data.get('forceDelete', False) # 获取是否强制删除的标志 157 | 158 | if not name: 159 | return jsonify({'status': 'error', 'message': '名称是必需的。'}), 400 160 | 161 | notes_folder = current_app.config['NOTES_FOLDER'] 162 | abs_path = os.path.abspath(os.path.join(notes_folder, path, name)) 163 | 164 | # Ensure the path is within NOTES_FOLDER 165 | if not abs_path.startswith(os.path.abspath(notes_folder)): 166 | return jsonify({'status': 'error', 'message': 'Invalid path.'}), 400 167 | 168 | if not os.path.exists(abs_path): 169 | return jsonify({'status': 'error', 'message': '文件或文件夹不存在。'}), 404 170 | 171 | try: 172 | if os.path.isdir(abs_path): 173 | shutil.rmtree(abs_path) 174 | else: 175 | # 删除文件 176 | os.remove(abs_path) 177 | 178 | return jsonify({'status': 'success', 'message': '删除成功。'}), 200 179 | except OSError as e: 180 | return jsonify({'status': 'error', 'message': '目录不为空或其他错误。'}), 400 181 | except Exception as e: 182 | return jsonify({'status': 'error', 'message': str(e)}), 500 183 | 184 | 185 | 186 | @notes_api.route('/get_file', methods=['GET']) 187 | def get_file(): 188 | path = request.args.get('path', '') 189 | if not path.endswith('.md'): 190 | return jsonify({'status': 'error', 'message': '仅支持 Markdown 文件。'}), 400 191 | 192 | notes_folder = current_app.config['NOTES_FOLDER'] 193 | abs_path = os.path.abspath(os.path.join(notes_folder, path)) 194 | 195 | # Ensure the path is within NOTES_FOLDER 196 | if not abs_path.startswith(os.path.abspath(notes_folder)): 197 | return jsonify({'status': 'error', 'message': 'Invalid path.'}), 400 198 | 199 | if not os.path.exists(abs_path): 200 | return jsonify({'status': 'error', 'message': '文件不存在。'}), 404 201 | 202 | try: 203 | with open(abs_path, 'r', encoding='utf-8') as f: 204 | content = f.read() 205 | return jsonify({'status': 'success', 'content': content}), 200 206 | except Exception as e: 207 | return jsonify({'status': 'error', 'message': str(e)}), 500 208 | 209 | @notes_api.route('/update_file', methods=['POST']) 210 | def update_file(): 211 | data = request.get_json() 212 | path = data.get('path', '') 213 | content = data.get('content', '') 214 | 215 | if not path.endswith('.md'): 216 | return jsonify({'status': 'error', 'message': '仅支持 Markdown 文件。'}), 400 217 | 218 | notes_folder = current_app.config['NOTES_FOLDER'] 219 | abs_path = os.path.abspath(os.path.join(notes_folder, path)) 220 | 221 | # Ensure the path is within NOTES_FOLDER 222 | if not abs_path.startswith(os.path.abspath(notes_folder)): 223 | return jsonify({'status': 'error', 'message': 'Invalid path.'}), 400 224 | 225 | if not os.path.exists(abs_path): 226 | return jsonify({'status': 'error', 'message': '文件不存在。'}), 404 227 | 228 | try: 229 | with open(abs_path, 'w', encoding='utf-8') as f: 230 | f.write(content) 231 | return jsonify({'status': 'success', 'message': '文件保存成功。'}), 200 232 | except Exception as e: 233 | return jsonify({'status': 'error', 'message': str(e)}), 500 234 | 235 | @notes_api.route('/get_content', methods=['GET']) 236 | def get_content(): 237 | path = request.args.get('path', '') 238 | notes_folder = current_app.config['NOTES_FOLDER'] 239 | abs_path = os.path.abspath(os.path.join(notes_folder, path)) 240 | 241 | # 确保路径在 NOTES_FOLDER 内 242 | if not abs_path.startswith(os.path.abspath(notes_folder)): 243 | return jsonify({'status': 'error', 'message': 'Invalid path.'}), 400 244 | 245 | if not os.path.isfile(abs_path): 246 | return jsonify({'status': 'error', 'message': 'File does not exist.'}), 404 247 | 248 | try: 249 | with open(abs_path, 'r', encoding='utf-8') as f: 250 | content = f.read() 251 | return jsonify({'status': 'success', 'content': content}), 200 252 | except Exception as e: 253 | return jsonify({'status': 'error', 'message': str(e)}), 500 254 | 255 | @notes_api.route('/save_content', methods=['POST']) 256 | def save_content(): 257 | data = request.get_json() 258 | path = data.get('path', '') 259 | content = data.get('content', '') 260 | 261 | notes_folder = current_app.config['NOTES_FOLDER'] 262 | abs_path = os.path.abspath(os.path.join(notes_folder, path)) 263 | 264 | # 确保路径在 NOTES_FOLDER 内 265 | if not abs_path.startswith(os.path.abspath(notes_folder)): 266 | return jsonify({'status': 'error', 'message': 'Invalid path.'}), 400 267 | 268 | if not os.path.isfile(abs_path): 269 | return jsonify({'status': 'error', 'message': 'File does not exist.'}), 404 270 | 271 | try: 272 | with open(abs_path, 'w', encoding='utf-8') as f: 273 | f.write(content) 274 | return jsonify({'status': 'success', 'message': 'Content saved successfully.'}), 200 275 | except Exception as e: 276 | return jsonify({'status': 'error', 'message': str(e)}), 500 277 | 278 | @notes_api.route('/view', methods=['GET']) 279 | def view_note(): 280 | path = request.args.get('path', '') 281 | notes_folder = current_app.config['NOTES_FOLDER'] 282 | abs_path = os.path.abspath(os.path.join(notes_folder, path)) 283 | 284 | # 确保路径在 NOTES_FOLDER 内 285 | if not abs_path.startswith(os.path.abspath(notes_folder)): 286 | return "Invalid path.", 400 287 | 288 | if not os.path.isfile(abs_path): 289 | return "File does not exist.", 404 290 | 291 | return render_template('view_note.html', path=path) 292 | 293 | -------------------------------------------------------------------------------- /app/controllers/papers_controller.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import uuid 4 | import requests 5 | import shutil 6 | import logging 7 | from flask import Blueprint, request, jsonify, send_from_directory 8 | from werkzeug.utils import secure_filename 9 | from sqlalchemy.exc import SQLAlchemyError 10 | from .. import db 11 | from ..models import Paper # 确保 Paper 模型已定义 12 | from ..utils.helper import format_response 13 | from config import Config 14 | 15 | logging.basicConfig(level=logging.DEBUG) 16 | 17 | paper_api = Blueprint('paper_api', __name__, url_prefix='/api/papers') 18 | 19 | ALLOWED_EXTENSIONS = {'pdf', 'txt', 'doc', 'docx', 'md'} 20 | 21 | def allowed_file(filename): 22 | return '.' in filename and \ 23 | filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS 24 | 25 | @paper_api.route('', methods=['GET']) 26 | def get_papers(): 27 | category_filter = request.args.get('category', None) # 按分类过滤 28 | 29 | query = Paper.query 30 | 31 | if category_filter: 32 | # 实现模糊搜索,使用 LIKE 查询 33 | query = query.filter( 34 | (Paper.category.ilike(f'%{category_filter}%')) | 35 | (Paper.title.ilike(f'%{category_filter}%')) 36 | ) 37 | 38 | # 使用默认排序方式(例如按创建时间降序) 39 | query = query.order_by(Paper.created_at.desc()) 40 | 41 | try: 42 | papers = query.all() 43 | except SQLAlchemyError as e: 44 | logging.error(f"数据库查询错误:{str(e)}") 45 | return jsonify(format_response({'error': '无法获取论文列表。'}, status=500)), 500 46 | 47 | papers_list = [] 48 | for paper in papers: 49 | folder_path = os.path.join(Config.PAPERS_FOLDER, secure_filename(paper.id)) 50 | if os.path.exists(folder_path): 51 | files = os.listdir(folder_path) 52 | else: 53 | files = [] 54 | papers_list.append({ 55 | 'id': paper.id, 56 | 'title': paper.title, 57 | 'category': paper.category, 58 | 'starred': paper.starred, 59 | 'files': files, 60 | 'created_at': paper.created_at.strftime('%Y-%m-%d %H:%M:%S') 61 | }) 62 | return jsonify(format_response(papers_list)), 200 63 | 64 | @paper_api.route('', methods=['POST']) 65 | def create_paper(): 66 | title = request.form.get('title') 67 | pdf_url = request.form.get('pdf_url') 68 | category = request.form.get('category') # 获取分类 69 | # 已移除文件上传部分 70 | # files = request.files.getlist('files') 71 | 72 | if not title: 73 | return jsonify(format_response({'error': '论文标题是必填项。'}, status=400)), 400 74 | 75 | if not pdf_url: 76 | return jsonify(format_response({'error': 'PDF 地址是必填项。'}, status=400)), 400 77 | 78 | # 生成唯一的论文ID 79 | paper_id = str(uuid.uuid4()) 80 | paper_folder = os.path.join(Config.PAPERS_FOLDER, secure_filename(paper_id)) 81 | 82 | try: 83 | os.makedirs(paper_folder, exist_ok=True) 84 | logging.debug(f"创建论文文件夹:{paper_folder}") 85 | except Exception as e: 86 | logging.error(f"无法创建论文文件夹:{str(e)}") 87 | return jsonify(format_response({'error': f'无法创建论文文件夹:{str(e)}'}, status=500)), 500 88 | 89 | # 如果提供了 PDF URL,下载 PDF 90 | try: 91 | response = requests.get(pdf_url, timeout=10) # 添加超时限制 92 | response.raise_for_status() 93 | pdf_filename = secure_filename(os.path.basename(pdf_url)) 94 | if not allowed_file(pdf_filename): 95 | pdf_filename += '.pdf' # 默认扩展名 96 | pdf_path = os.path.join(paper_folder, pdf_filename) 97 | with open(pdf_path, 'wb') as f: 98 | f.write(response.content) 99 | logging.debug(f"下载并保存 PDF 文件:{pdf_path}") 100 | except Exception as e: 101 | logging.error(f"无法下载 PDF 文件:{str(e)}") 102 | shutil.rmtree(paper_folder, ignore_errors=True) # 清理已创建的文件夹 103 | return jsonify(format_response({'error': f'无法下载 PDF 文件:{str(e)}'}, status=400)), 400 104 | 105 | # 在数据库中记录论文信息 106 | new_paper = Paper(id=paper_id, title=title, folder=paper_folder, category=category) 107 | try: 108 | db.session.add(new_paper) 109 | db.session.commit() 110 | logging.debug(f"论文记录已保存到数据库:{new_paper}") 111 | except SQLAlchemyError as e: 112 | # 如果数据库操作失败,删除已创建的文件夹 113 | shutil.rmtree(paper_folder, ignore_errors=True) 114 | logging.error(f"无法保存论文信息到数据库:{str(e)}") 115 | return jsonify(format_response({'error': f'无法保存论文信息到数据库:{str(e)}'}, status=500)), 500 116 | 117 | return jsonify(format_response({'message': '论文创建成功。', 'id': paper_id})), 201 118 | 119 | @paper_api.route('//notes', methods=['POST']) 120 | def upload_notes(paper_id): 121 | files = request.files.getlist('notes_files') 122 | paper_folder = os.path.join(Config.PAPERS_FOLDER, secure_filename(paper_id)) 123 | 124 | if not os.path.exists(paper_folder): 125 | return jsonify(format_response({'error': '论文不存在。'}, status=404)), 404 126 | 127 | for file in files: 128 | if file and allowed_file(file.filename): 129 | filename = secure_filename(file.filename) 130 | file.save(os.path.join(paper_folder, filename)) 131 | 132 | return jsonify(format_response({'message': '笔记上传成功。'})), 200 133 | 134 | @paper_api.route('//download/', methods=['GET']) 135 | def download_file(paper_id, filename): 136 | paper_folder = os.path.join(Config.PAPERS_FOLDER, secure_filename(paper_id)) 137 | print(paper_folder) 138 | if not os.path.exists(paper_folder): 139 | return jsonify(format_response({'error': '论文不存在。'}, status=404)), 404 140 | if not os.path.exists(os.path.join(paper_folder, filename)): 141 | return jsonify(format_response({'error': '文件不存在。'}, status=404)), 404 142 | return send_from_directory(paper_folder, filename, as_attachment=False) 143 | 144 | @paper_api.route('/view//', methods=['GET']) 145 | def view_pdf(paper_id, filename): 146 | safe_paper_id = secure_filename(paper_id) 147 | paper_folder = os.path.join(Config.PAPERS_FOLDER, safe_paper_id) 148 | print(paper_folder) 149 | try: 150 | return send_from_directory( 151 | directory=paper_folder, 152 | path=filename, 153 | mimetype='application/pdf', 154 | as_attachment=False # 设置为 False 以允许浏览器内联显示 155 | ) 156 | except Exception as e: 157 | # 记录异常日志(可选) 158 | # app.logger.error(f"无法发送文件 {file_path}: {str(e)}") 159 | print(e) 160 | return jsonify(format_response({'error': '无法发送文件。'}, status=500)), 500 161 | 162 | @paper_api.route('/', methods=['DELETE']) 163 | def delete_paper(paper_id): 164 | paper = Paper.query.get(paper_id) 165 | if not paper: 166 | return jsonify(format_response({'error': '论文不存在。'}, status=404)), 404 167 | 168 | paper_folder = os.path.join(Config.PAPERS_FOLDER, secure_filename(paper.id)) 169 | try: 170 | shutil.rmtree(paper_folder) 171 | logging.debug(f"删除论文文件夹:{paper_folder}") 172 | except Exception as e: 173 | logging.error(f"无法删除论文文件夹:{str(e)}") 174 | return jsonify(format_response({'error': f'无法删除论文文件夹:{str(e)}'}, status=500)), 500 175 | 176 | try: 177 | db.session.delete(paper) 178 | db.session.commit() 179 | logging.debug(f"论文记录已从数据库删除:{paper}") 180 | except SQLAlchemyError as e: 181 | logging.error(f"无法删除论文记录:{str(e)}") 182 | return jsonify(format_response({'error': f'无法删除论文记录:{str(e)}'}, status=500)), 500 183 | 184 | return jsonify(format_response({'message': '论文删除成功。'})), 200 185 | 186 | @paper_api.route('//star', methods=['PUT']) 187 | def star_paper(paper_id): 188 | paper = Paper.query.get(paper_id) 189 | if not paper: 190 | return jsonify(format_response({'error': '论文不存在。'}, status=404)), 404 191 | 192 | data = request.get_json() 193 | if not data or 'starred' not in data: 194 | return jsonify(format_response({'error': '缺少 "starred" 字段。'}, status=400)), 400 195 | 196 | paper.starred = data['starred'] 197 | try: 198 | db.session.commit() 199 | return jsonify(format_response({'message': '论文星标状态更新成功。'})), 200 200 | except SQLAlchemyError as e: 201 | logging.error(f"无法更新星标状态:{str(e)}") 202 | return jsonify(format_response({'error': f'无法更新星标状态:{str(e)}'}, status=500)), 500 203 | -------------------------------------------------------------------------------- /app/controllers/plans_controller.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from ..models import Plan 4 | from .. import db 5 | from datetime import datetime 6 | 7 | import random 8 | 9 | plans_api = Blueprint('plans_api', __name__, url_prefix='/api/plans') 10 | 11 | COLOR_PALETTE = [ 12 | '#FF5733', # 红色 13 | '#33FF57', # 绿色 14 | '#3357FF', # 蓝色 15 | '#F1C40F', # 黄色 16 | '#9B59B6', # 紫色 17 | '#E67E22', # 橙色 18 | '#1ABC9C', # 青色 19 | '#2ECC71', # 浅绿色 20 | '#3498DB', # 天蓝色 21 | '#E74C3C' # 深红色 22 | ] 23 | 24 | def get_color(plan_id): 25 | """基于 plan_id 从 COLOR_PALETTE 中选择颜色""" 26 | random.seed(plan_id) # 确保颜色一致性 27 | return random.choice(COLOR_PALETTE) 28 | 29 | @plans_api.route('/', methods=['GET']) 30 | def get_plans(): 31 | date_str = request.args.get('date') 32 | if date_str: 33 | try: 34 | selected_date = datetime.strptime(date_str, '%Y-%m-%d').date() 35 | plans = Plan.query.filter_by(date=selected_date).all() 36 | except ValueError: 37 | return jsonify({'error': 'Invalid date format. Use YYYY-MM-DD.'}), 400 38 | else: 39 | plans = Plan.query.all() 40 | # 返回 FullCalendar 事件格式,包括随机颜色 41 | events = [{ 42 | 'title': plan.title, 43 | 'start': plan.date.isoformat(), 44 | 'id': plan.id, 45 | 'description': plan.description, 46 | 'color': get_color(plan.id) # 分配颜色 47 | } for plan in plans] 48 | return jsonify(events) 49 | 50 | @plans_api.route('/', methods=['POST']) 51 | def add_plan(): 52 | data = request.get_json() 53 | title = data.get('title') 54 | description = data.get('description', '') 55 | date_str = data.get('date') 56 | if not title or not date_str: 57 | return jsonify({'error': 'Title and date are required'}), 400 58 | try: 59 | plan_date = datetime.strptime(date_str, '%Y-%m-%d').date() 60 | except ValueError: 61 | return jsonify({'error': 'Invalid date format. Use YYYY-MM-DD.'}), 400 62 | new_plan = Plan(title=title, description=description, date=plan_date) 63 | db.session.add(new_plan) 64 | db.session.commit() 65 | return jsonify({'message': 'Plan added successfully'}), 201 66 | 67 | @plans_api.route('/', methods=['DELETE']) 68 | def delete_plan(plan_id): 69 | plan = Plan.query.get_or_404(plan_id) 70 | db.session.delete(plan) 71 | db.session.commit() 72 | return jsonify({'message': 'Plan deleted successfully'}), 200 73 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | 2 | from . import db 3 | from datetime import date 4 | 5 | class Plan(db.Model): 6 | id = db.Column(db.Integer, primary_key=True) 7 | title = db.Column(db.String(100), nullable=False) 8 | description = db.Column(db.Text, nullable=True) 9 | date = db.Column(db.Date, nullable=False, default=date.today) # 新增日期字段 10 | 11 | def __repr__(self): 12 | return f"" 13 | 14 | class Project(db.Model): 15 | id = db.Column(db.Integer, primary_key=True) 16 | name = db.Column(db.String(100), unique=True, nullable=False) 17 | description = db.Column(db.Text, nullable=True) 18 | client = db.Column(db.String(100), nullable=False) 19 | type = db.Column(db.String(50), nullable=False) 20 | 21 | tasks = db.relationship('Task', backref='project', cascade="all, delete-orphan", lazy=True) 22 | milestones = db.relationship('Milestone', backref='project', cascade="all, delete-orphan", lazy=True) 23 | 24 | def __repr__(self): 25 | return f"" 26 | 27 | class Idea(db.Model): 28 | id = db.Column(db.Integer, primary_key=True) 29 | title = db.Column(db.String(100), nullable=False) 30 | description = db.Column(db.String(255), nullable=False) 31 | background = db.Column(db.String(255), nullable=True) 32 | motivation = db.Column(db.String(255), nullable=True) 33 | challenge = db.Column(db.String(255), nullable=True) 34 | method = db.Column(db.String(255), nullable=True) 35 | experiment = db.Column(db.String(255), nullable=True) 36 | innovation = db.Column(db.String(255), nullable=True) 37 | papers = db.Column(db.String(255), nullable=True) 38 | 39 | # 一对多关系:一个Idea有多个RelatedPapers 40 | related_papers = db.relationship('RelatedPaper', backref='idea', cascade="all, delete-orphan", lazy=True) 41 | 42 | def __repr__(self): 43 | return f"" 44 | 45 | class Task(db.Model): 46 | id = db.Column(db.Integer, primary_key=True) 47 | title = db.Column(db.String(100), nullable=False) 48 | description = db.Column(db.Text, nullable=True) 49 | status = db.Column(db.String(20), nullable=False, default='Pending') # e.g., Pending, In Progress, Completed 50 | project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False) 51 | 52 | def __repr__(self): 53 | return f"" 54 | 55 | class Milestone(db.Model): 56 | id = db.Column(db.Integer, primary_key=True) 57 | title = db.Column(db.String(100), nullable=False) 58 | description = db.Column(db.Text, nullable=True) 59 | date = db.Column(db.Date, nullable=False) 60 | project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False) 61 | 62 | def __repr__(self): 63 | return f"" 64 | 65 | class File(db.Model): 66 | id = db.Column(db.Integer, primary_key=True) 67 | filename = db.Column(db.String(200), nullable=False) 68 | category = db.Column(db.String(100), nullable=True) # 分类或标签 69 | project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False) 70 | 71 | def __repr__(self): 72 | return f"" 73 | 74 | class RelatedPaper(db.Model): 75 | id = db.Column(db.Integer, primary_key=True) 76 | title = db.Column(db.String(150), nullable=False) 77 | content = db.Column(db.Text, nullable=True) 78 | link = db.Column(db.String(255), nullable=True) 79 | 80 | # 外键关联到Idea 81 | idea_id = db.Column(db.Integer, db.ForeignKey('idea.id'), nullable=False) 82 | 83 | def __repr__(self): 84 | return f"" 85 | 86 | class Paper(db.Model): 87 | id = db.Column(db.String(36), primary_key=True) # UUID 88 | title = db.Column(db.String(255), nullable=False) 89 | folder = db.Column(db.String(500), nullable=False) # 文件夹路径 90 | category = db.Column(db.String(100), nullable=True) # 论文分类 91 | starred = db.Column(db.Boolean, default=False) # 标星状态 92 | created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) 93 | 94 | def __repr__(self): 95 | return f"" 96 | 97 | class Note(db.Model): 98 | id = db.Column(db.Integer, primary_key=True) 99 | content = db.Column(db.Text, nullable=False) 100 | 101 | def __repr__(self): 102 | return f"" 103 | -------------------------------------------------------------------------------- /app/routes.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, jsonify, render_template, request 3 | 4 | from app.utils.helper import notify 5 | from config import Config 6 | from .controllers.main_controller import main_api 7 | from .controllers.plans_controller import plans_api 8 | from .controllers.notes_controller import notes_api 9 | from .controllers.idea_controller import idea_api 10 | from .controllers.papers_controller import paper_api 11 | from .controllers.projects_controller import project_api 12 | 13 | main_bp = Blueprint('main', __name__) 14 | 15 | @main_bp.route('/') 16 | def home(): 17 | return render_template('index.html') 18 | 19 | @main_bp.route('/plans') 20 | def plans(): 21 | return render_template('plans.html') 22 | 23 | @main_bp.route('/projects') 24 | def projects(): 25 | return render_template('projects.html') 26 | 27 | @main_bp.route('/projects/') 28 | def view_project(project_name): 29 | return render_template('view_project.html', project_name=project_name) 30 | 31 | @main_bp.route('/ideas') 32 | def ideas(): 33 | return render_template('ideas.html') 34 | 35 | @main_bp.route('/ideas/') 36 | def view_idea(idea_id): 37 | return render_template('view_idea.html', idea_id=idea_id) 38 | 39 | @main_bp.route('/papers') 40 | def papers(): 41 | return render_template('papers.html') 42 | 43 | @main_bp.route('/notes') 44 | def notes(): 45 | return render_template('notes.html') 46 | 47 | @main_bp.route('/send_notification', methods=['POST']) 48 | def send_notification(): 49 | data = request.get_json() 50 | api_key = data.get('api_key') 51 | 52 | # 验证 API 密钥 53 | if api_key != Config.NOTIFICATION_API_KEY: 54 | return jsonify({'status': 'error', 'message': '无效的 API 密钥。'}), 403 55 | 56 | title = data.get('title', '默认标题') 57 | message = data.get('message', '这是一个通知消息。') 58 | 59 | try: 60 | notify(title, message) 61 | return jsonify({'status': 'success', 'message': '通知已发送。'}), 200 62 | except Exception as e: 63 | return jsonify({'status': 'error', 'message': f'发送通知失败: {str(e)}'}), 500 64 | 65 | main_bp.register_blueprint(main_api) 66 | main_bp.register_blueprint(plans_api) 67 | main_bp.register_blueprint(notes_api) 68 | main_bp.register_blueprint(idea_api) 69 | main_bp.register_blueprint(paper_api) 70 | main_bp.register_blueprint(project_api) 71 | -------------------------------------------------------------------------------- /app/static/css/ideas.css: -------------------------------------------------------------------------------- 1 | 2 | .create-idea-button { 3 | padding: 10px 20px; 4 | background-color: #333333; 5 | color: #ffffff; 6 | border: none; 7 | border-radius: 4px; 8 | font-size: 16px; 9 | cursor: pointer; 10 | transition: background-color 0.3s, transform 0.3s; 11 | margin-bottom: 20px; 12 | } 13 | 14 | .create-idea-button:hover { 15 | background-color: #555555; 16 | transform: scale(1.05); 17 | } 18 | 19 | .modal { 20 | display: none; position: fixed; z-index: 1000; left: 0; 21 | top: 0; 22 | width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.5); animation: fadeIn 0.3s; 23 | } 24 | 25 | .modal-content { 26 | background-color: #ffffff; 27 | margin: 5% auto; padding: 20px; 28 | border: 1px solid #888; 29 | width: 80%; max-width: 600px; 30 | border-radius: 8px; 31 | position: relative; 32 | animation: slideIn 0.3s; 33 | } 34 | 35 | .close-button { 36 | color: #aaaaaa; 37 | float: right; 38 | font-size: 28px; 39 | font-weight: bold; 40 | position: absolute; 41 | top: 10px; 42 | right: 20px; 43 | cursor: pointer; 44 | transition: color 0.3s; 45 | } 46 | 47 | .close-button:hover, 48 | .close-button:focus { 49 | color: #000000; 50 | text-decoration: none; 51 | } 52 | 53 | .modal-content h2 { 54 | margin-top: 0; 55 | color: #333333; 56 | } 57 | 58 | .modal-content form { 59 | display: flex; 60 | flex-direction: column; 61 | } 62 | 63 | .modal-content form input { 64 | padding: 10px; 65 | margin-bottom: 15px; 66 | border: 1px solid #cccccc; 67 | border-radius: 4px; 68 | font-size: 16px; 69 | } 70 | 71 | .modal-content form label { 72 | margin-bottom: 5px; 73 | color: #333333; 74 | font-weight: bold; 75 | } 76 | 77 | .modal-content form button { 78 | padding: 10px; 79 | background-color: #333333; 80 | color: #ffffff; 81 | border: none; 82 | border-radius: 4px; 83 | font-size: 16px; 84 | cursor: pointer; 85 | transition: background-color 0.3s; 86 | } 87 | 88 | .modal-content form button:hover { 89 | background-color: #555555; 90 | } 91 | 92 | @keyframes fadeIn { 93 | from { opacity: 0; } 94 | to { opacity: 1; } 95 | } 96 | 97 | @keyframes slideIn { 98 | from { transform: translateY(-50px); } 99 | to { transform: translateY(0); } 100 | } 101 | 102 | #ideas-container { 103 | list-style-type: none; 104 | padding: 0; 105 | } 106 | 107 | .idea-item { 108 | background-color: #ffffff; 109 | padding: 15px; 110 | margin-bottom: 15px; 111 | border-left: 5px solid #333333; 112 | border-radius: 4px; 113 | position: relative; 114 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 115 | animation: slideInRight 0.5s ease forwards; 116 | cursor: pointer; transition: background-color 0.3s, transform 0.2s; 117 | display: flex; 118 | justify-content: space-between; 119 | align-items: center; 120 | } 121 | 122 | .idea-item:hover { 123 | background-color: #ebebeb; 124 | transform: translateX(5px); 125 | } 126 | 127 | .idea-item h3 { 128 | margin: 0; 129 | color: #333333; 130 | flex-grow: 1; 131 | } 132 | 133 | .idea-item p { 134 | margin: 0 10px 0 0; 135 | color: #666666; 136 | flex-grow: 2; 137 | } 138 | 139 | .idea-item .delete-button { 140 | background: none; 141 | border: none; 142 | color: #2f2f2f; 143 | font-size: 18px; 144 | cursor: pointer; 145 | transition: color 0.3s; 146 | } 147 | 148 | .idea-item .delete-button:hover { 149 | color: #2f2f2f; 150 | } 151 | 152 | @keyframes slideInRight { 153 | from { 154 | opacity: 0; 155 | transform: translateX(50px); 156 | } 157 | to { 158 | opacity: 1; 159 | transform: translateX(0); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /app/static/css/index.css: -------------------------------------------------------------------------------- 1 | 2 | @import url('https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css'); 3 | 4 | @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css'); 5 | 6 | @font-face { 7 | font-family: 'PingFang-Bold'; 8 | src: url('/static/font/PingFang-Bold.ttf') format('ttf'); 9 | font-weight: bold; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: 'PingFang-Heavy'; 15 | src: url('/static/font/PingFang-Heavy.ttf') format('ttf'); 16 | font-weight: bolder; 17 | font-style: normal; 18 | } 19 | 20 | .grid-container { 21 | display: grid; 22 | grid-template-rows: auto auto auto; grid-gap: 20px; max-width: 1200px; 23 | margin: 20px auto; 24 | padding: 10px; 25 | } 26 | 27 | .quote { 28 | background-color: rgba(255, 255, 255, 0.8); 29 | border-radius: 15px; 30 | padding: 30px; 31 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 32 | text-align: center; 33 | font-family: "PingFang-Bold", "PingFang-Heavy", "黑体", "宋体", "Open Sans", "Helvetica Neue", "Helvetica", "Arial", "나눔바른고딕", 34 | "Nanum Barun Gothic", "맑은고딕", "Malgun Gothic", sans-serif; 35 | } 36 | 37 | .time-weather { 38 | display: grid; 39 | grid-template-columns: 1fr 1fr; grid-gap: 20px; } 40 | 41 | .weather{ 42 | display: flex; 43 | flex-direction: column; 44 | font-size: 80px; 45 | } 46 | 47 | .time-container, .weather-container { 48 | background-color: rgba(255, 255, 255, 0.8); 49 | border-radius: 15px; 50 | padding: 20px; 51 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 52 | text-align: center; 53 | font-family: "PingFang-Bold", "PingFang-Heavy", "黑体", "宋体", "Open Sans", "Helvetica Neue", "Helvetica", "Arial", "나눔바른고딕", 54 | "Nanum Barun Gothic", "맑은고딕", "Malgun Gothic", sans-serif; 55 | } 56 | 57 | .time-suggestion, .weather-suggestion { 58 | margin-top: 15px; 59 | padding: 10px; 60 | background-color: rgba(240, 240, 240, 0.9); 61 | border-radius: 10px; 62 | font-size: 14px; 63 | color: #333; 64 | box-shadow: 0 2px 5px rgba(0,0,0,0.05); 65 | text-align: left; 66 | } 67 | 68 | .links { 69 | background-color: rgba(255, 255, 255, 0.8); 70 | border-radius: 15px; 71 | padding: 20px; 72 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 73 | font-family: "PingFang-Bold", "PingFang-Heavy", "黑体", "宋体", "Open Sans", "Helvetica Neue", "Helvetica", "Arial", "나눔바른고딕", 74 | "Nanum Barun Gothic", "맑은고딕", "Malgun Gothic", sans-serif; 75 | } 76 | 77 | .links-container { 78 | display: grid; 79 | grid-template-columns: repeat(4, 1fr); grid-gap: 15px; } 80 | 81 | .links-container a { 82 | display: flex; 83 | align-items: center; 84 | justify-content: center; 85 | background-color: #f0f0f0; 86 | color: #333; 87 | text-decoration: none; 88 | padding: 15px; 89 | border-radius: 10px; 90 | box-shadow: 0 2px 5px rgba(0,0,0,0.1); 91 | transition: background-color 0.3s; 92 | } 93 | 94 | .links-container a:hover { 95 | background-color: #e0e0e0; 96 | } 97 | 98 | .links-container a i { 99 | margin-right: 8px; 100 | } 101 | 102 | .modal { 103 | display: none; position: fixed; z-index: 1000; left: 0; 104 | top: 0; 105 | width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4); } 106 | 107 | .modal-content { 108 | background-color: #fefefe; 109 | margin: 15% auto; padding: 20px; 110 | border: 1px solid #888; 111 | border-radius: 15px; 112 | width: 80%; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); 113 | } 114 | 115 | .close-button { 116 | color: #aaa; 117 | float: right; 118 | font-size: 28px; 119 | font-weight: bold; 120 | cursor: pointer; 121 | } 122 | 123 | .date-time{ 124 | font-size: 80px; 125 | display: flex; 126 | flex-direction: column; 127 | } 128 | 129 | .footer { 130 | text-align: center; 131 | padding: 10px; 132 | left: 0; 133 | bottom: -10px; 134 | width: 90%; 135 | color: #555; 136 | } 137 | 138 | 139 | .close-button:hover, 140 | .close-button:focus { 141 | color: black; 142 | text-decoration: none; 143 | } 144 | 145 | .modal-button { 146 | padding: 10px 20px; 147 | margin: 10px; 148 | border: none; 149 | border-radius: 8px; 150 | cursor: pointer; 151 | font-size: 16px; 152 | } 153 | 154 | .modal-button.confirm { 155 | background-color: #4CAF50; 156 | color: white; 157 | } 158 | 159 | .modal-button.cancel { 160 | background-color: #f44336; 161 | color: white; 162 | } 163 | 164 | @media (max-width: 768px) { 165 | .links-container { 166 | grid-template-columns: repeat(2, 1fr); } 167 | 168 | .time-weather { 169 | grid-template-columns: 1fr; } 170 | } 171 | 172 | 173 | .memo-container { 174 | padding: 20px; 175 | background-color: #ffffff; 176 | border-radius: 8px; 177 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 178 | } 179 | 180 | .memo-container h2 { 181 | margin-bottom: 15px; 182 | font-size: 22px; 183 | color: #333; 184 | } 185 | 186 | .memo-wrapper { 187 | display: flex; 188 | height: 100%; height: 250px; 189 | max-height: 250px; 190 | width: 100%; 191 | } 192 | 193 | .memo-list { 194 | width: 30%; 195 | border-right: 1px solid #ddd; 196 | padding-right: 10px; 197 | overflow-y: auto; 198 | position: relative; 199 | } 200 | 201 | .memo-list ul { 202 | list-style-type: none; 203 | padding: 0; 204 | margin: 0; 205 | } 206 | 207 | .memo-item { 208 | display: flex; 209 | flex-direction: column; 210 | justify-content: space-between; 211 | align-items: flex-start; 212 | padding: 10px; 213 | margin-left: 10px; 214 | margin-right: 10px; 215 | margin-bottom: 8px; 216 | background-color: #fff; 217 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 218 | border-radius: 4px; 219 | cursor: pointer; 220 | transition: background-color 0.3s, transform 0.3s, opacity 0.3s; 221 | opacity: 0; 222 | transform: translateX(-20px); 223 | position: relative; 224 | } 225 | 226 | .memo-item span{ 227 | font-size: 16px; 228 | width: 90%; 229 | text-overflow:clip; 230 | } 231 | 232 | .memo-item.fade-in { 233 | animation: fadeIn 0.5s forwards; 234 | } 235 | 236 | .memo-item.fade-out { 237 | animation: fadeOut 0.5s forwards; 238 | } 239 | 240 | .memo-item:hover { 241 | background-color: #f0f0f0; 242 | } 243 | 244 | .memo-item.active { 245 | background-color: #f0f0f0; 246 | } 247 | 248 | .memo-item span { 249 | flex-grow: 1; 250 | margin-right: 10px; 251 | overflow: hidden; 252 | text-overflow: ellipsis; 253 | white-space: nowrap; 254 | font-weight: bold; 255 | } 256 | 257 | .memo-datetime { 258 | font-size: 12px; 259 | color: #666; 260 | margin-top: 5px; 261 | } 262 | 263 | .delete-memo { 264 | position: absolute; 265 | top: 10px; 266 | right: 10px; 267 | background-color: transparent; 268 | border: none; 269 | color: #dc3545; 270 | font-size: 16px; 271 | cursor: pointer; 272 | transition: color 0.3s; 273 | } 274 | 275 | .delete-memo:hover { 276 | color: #a71d2a; 277 | } 278 | 279 | .add-memo-button { 280 | cursor: pointer; 281 | background-color: #2f2f2f; 282 | color: #ffffff; 283 | height: 60px; 284 | width: 95%; 285 | border: none; 286 | padding-right: 10px; 287 | overflow-y: auto; 288 | position: relative; 289 | border-radius: 8px; 290 | margin-left: 8px; 291 | margin-top: 10px; 292 | transition: background-color 0.3s; 293 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 294 | } 295 | 296 | .add-memo-button:hover { 297 | background-color: #929292; 298 | } 299 | 300 | .memo-detail { 301 | width: 70%; 302 | padding-left: 20px; 303 | overflow-y: auto; 304 | } 305 | 306 | .memo-detail h3 { 307 | margin-bottom: 10px; 308 | font-size: 18px; 309 | color: #555; 310 | } 311 | 312 | .editable-content { 313 | font-size: 16px; 314 | color: #333; 315 | padding: 10px; 316 | border: 1px solid transparent; 317 | border-radius: 4px; 318 | min-height: 200px; 319 | background-color: #fff; 320 | transition: background-color 0.3s, border 0.3s; 321 | } 322 | 323 | .editable-content:focus { 324 | outline: none; 325 | border: 1px solid #007bff; 326 | background-color: #e9f5ff; 327 | } 328 | 329 | .fade-in { 330 | animation: fadeIn 0.5s forwards; 331 | } 332 | 333 | .fade-out { 334 | animation: fadeOut 0.5s forwards; 335 | } 336 | 337 | @keyframes fadeIn { 338 | from { 339 | opacity: 0; 340 | transform: translateX(-20px); 341 | } 342 | to { 343 | opacity: 1; 344 | transform: translateX(0); 345 | } 346 | } 347 | 348 | @keyframes fadeOut { 349 | from { 350 | opacity: 1; 351 | transform: translateX(0); 352 | } 353 | to { 354 | opacity: 0; 355 | transform: translateX(20px); 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /app/static/css/notes.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .notes-header { 4 | display: flex; 5 | align-items: center; 6 | gap: 10px; 7 | margin-bottom: 20px; 8 | } 9 | 10 | .current-path { 11 | margin-left: auto; 12 | font-size: 16px; 13 | color: #555; 14 | } 15 | 16 | .back-button { 17 | background-color: #2f2f2f; 18 | color: #ffffff; 19 | border: none; 20 | padding: 10px 20px; 21 | border-radius: 4px; 22 | cursor: pointer; 23 | font-size: 16px; 24 | display: flex; 25 | align-items: center; 26 | transition: background-color 0.3s, transform 0.2s; 27 | } 28 | 29 | .back-button i { 30 | margin-right: 8px; 31 | } 32 | 33 | .back-button:hover:not(:disabled) { 34 | background-color: #808080; 35 | transform: translateY(-2px); 36 | } 37 | 38 | .back-button:disabled { 39 | opacity: 0.5; 40 | cursor: not-allowed; 41 | } 42 | 43 | .notes-container { 44 | display: grid; 45 | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); 46 | gap: 20px; 47 | margin-top: 20px; 48 | } 49 | 50 | .note-item { 51 | width: 100%; 52 | padding-top: 100%; 53 | position: relative; 54 | background-color: #ebebeb; 55 | border-radius: 12px; 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | cursor: pointer; 60 | transition: transform 0.2s, background-color 0.3s, box-shadow 0.3s; 61 | overflow: hidden; 62 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 63 | } 64 | 65 | .note-item:hover { 66 | transform: scale(1.05); 67 | background-color: #f5f5f5; 68 | box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); 69 | } 70 | 71 | .note-item i { 72 | font-size: 48px; 73 | color: #333333; 74 | position: absolute; 75 | top: 50%; 76 | left: 50%; 77 | transform: translate(-50%, -50%); 78 | } 79 | 80 | .note-item span { 81 | position: absolute; 82 | bottom: 10px; 83 | left: 10px; 84 | right: 10px; 85 | text-align: center; 86 | font-size: 14px; 87 | color: #333333; 88 | white-space: nowrap; 89 | overflow: hidden; 90 | text-overflow: ellipsis; 91 | padding: 0 5px; 92 | background: rgba(255, 255, 255, 0.8); 93 | border-radius: 4px; 94 | } 95 | 96 | 97 | .rename-input { 98 | width: 90%; 99 | padding: 5px; 100 | font-size: 14px; 101 | border: 1px solid #ccc; 102 | border-radius: 4px; 103 | position: absolute; 104 | bottom: 10px; 105 | left: 10px; 106 | right: 10px; 107 | box-sizing: border-box; 108 | } 109 | 110 | 111 | .context-menu { 112 | position: absolute; 113 | background-color: #ffffff; 114 | border: 1px solid #ccc; 115 | box-shadow: 2px 2px 10px rgba(0,0,0,0.2); 116 | display: none; 117 | z-index: 1000; 118 | border-radius: 4px; 119 | } 120 | 121 | .context-menu ul { 122 | list-style: none; 123 | margin: 0; 124 | padding: 5px 0; 125 | } 126 | 127 | .context-menu ul li { 128 | padding: 8px 20px; 129 | cursor: pointer; 130 | transition: background-color 0.2s; 131 | } 132 | 133 | .context-menu ul li:hover { 134 | background-color: #f5f5f5; 135 | } 136 | 137 | 138 | .modal { 139 | display: none; 140 | position: fixed; 141 | z-index: 2000; 142 | left: 0; 143 | top: 0; 144 | width: 100%; 145 | height: 100%; 146 | overflow: auto; 147 | background-color: rgba(0,0,0,0.4); 148 | } 149 | 150 | .modal-content { 151 | background-color: #fefefe; 152 | margin: 15% auto; 153 | padding: 20px; 154 | border: 1px solid #888; 155 | width: 300px; 156 | border-radius: 8px; 157 | text-align: center; 158 | } 159 | 160 | .close-button { 161 | color: #aaa; 162 | float: right; 163 | font-size: 28px; 164 | font-weight: bold; 165 | cursor: pointer; 166 | } 167 | 168 | .close-button:hover, 169 | .close-button:focus { 170 | color: black; 171 | text-decoration: none; 172 | } 173 | 174 | .modal-button { 175 | padding: 10px 20px; 176 | margin: 10px; 177 | border: none; 178 | border-radius: 4px; 179 | cursor: pointer; 180 | font-size: 16px; 181 | } 182 | 183 | .modal-button.confirm { 184 | background-color: #d9534f; 185 | color: white; 186 | } 187 | 188 | .modal-button.confirm:hover { 189 | background-color: #c9302c; 190 | } 191 | 192 | .modal-button.cancel { 193 | background-color: #5bc0de; 194 | color: white; 195 | } 196 | 197 | .modal-button.cancel:hover { 198 | background-color: #31b0d5; 199 | } 200 | .markdown-view { 201 | border: 1px solid #ccc; 202 | padding: 10px; 203 | height: 400px; 204 | overflow-y: auto; 205 | background-color: #f9f9f9; 206 | } 207 | 208 | .markdown-edit textarea { 209 | border: 1px solid #ccc; 210 | padding: 10px; 211 | resize: vertical; 212 | font-family: monospace; 213 | background-color: #fff; 214 | } 215 | -------------------------------------------------------------------------------- /app/static/css/papers.css: -------------------------------------------------------------------------------- 1 | 2 | .papers-container { 3 | padding: 30px; margin: 0 auto; 4 | box-sizing: border-box; 5 | background-color: #f5f5f5; 6 | } 7 | 8 | .header-section { 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-start; gap: 20px; margin-bottom: 20px; } 12 | 13 | .papers-container h1 { 14 | color: #2f2f2f; font-size: 24px; margin: 0; 15 | } 16 | 17 | .sort-filter-container { 18 | position: fixed; bottom: 0; left: 50%; transform: translateX(-50%); background-color: rgba(255, 255, 255, 0.95); 19 | padding: 20px; 20 | border-radius: 8px; 21 | box-shadow: 0 -4px 12px rgba(0,0,0,0.2); 22 | z-index: 100; width: 80%; max-width: 800px; display: flex; 23 | flex-direction: row; 24 | align-items: center; 25 | gap: 10px; 26 | bottom: 5%; 27 | } 28 | 29 | .sort-filter-container h2 { 30 | margin: 0; 31 | font-size: 18px; 32 | color: #333333; 33 | flex: 1; 34 | } 35 | 36 | .sort-filter-container input { 37 | padding: 6px 10px; border: 1px solid #cccccc; 38 | border-radius: 4px; 39 | font-size: 16px; color: #2f2f2f; width: 88%; 40 | background-color: #ffffff; } 41 | 42 | .create-paper-button { 43 | padding: 10px 20px; background-color: #2f2f2f; color: #ffffff; 44 | border: none; 45 | border-radius: 6px; 46 | font-size: 14px; cursor: pointer; 47 | transition: background-color 0.3s, transform 0.2s; 48 | } 49 | 50 | .create-paper-button:hover { 51 | background-color: #1e1e1e; transform: scale(1.05); 52 | } 53 | 54 | .modal { 55 | display: none; position: fixed; z-index: 1000; left: 0; 56 | top: 0; 57 | width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.5); animation: fadeIn 0.15s; } 58 | 59 | .modal-content { 60 | background-color: #ffffff; 61 | margin: 5% auto; padding: 25px; border: 1px solid #2f2f2f; width: 90%; max-width: 500px; border-radius: 8px; 62 | position: relative; 63 | animation: slideIn 0.15s; } 64 | 65 | .close-button { 66 | color: #2f2f2f; float: right; 67 | font-size: 24px; font-weight: bold; 68 | position: absolute; 69 | top: 10px; 70 | right: 15px; 71 | cursor: pointer; 72 | transition: color 0.3s; 73 | } 74 | 75 | .close-button:hover, 76 | .close-button:focus { 77 | color: #000000; 78 | text-decoration: none; 79 | } 80 | 81 | .modal-content h2 { 82 | margin-top: 0; 83 | color: #2f2f2f; text-align: center; 84 | margin-bottom: 15px; font-size: 20px; } 85 | 86 | .modal-content form { 87 | display: flex; 88 | flex-direction: column; 89 | } 90 | 91 | .modal-content form label { 92 | margin-top: 8px; 93 | margin-bottom: 4px; 94 | color: #2f2f2f; font-weight: bold; 95 | font-size: 14px; } 96 | 97 | .modal-content form input[type="text"], 98 | .modal-content form input[type="url"] { 99 | padding: 8px; 100 | border: 1px solid #cccccc; 101 | border-radius: 4px; 102 | font-size: 12px; color: #2f2f2f; background-color: #ffffff; 103 | } 104 | 105 | 106 | .modal-content form button { 107 | margin-top: 15px; padding: 10px; 108 | background-color: #2f2f2f; color: #ffffff; 109 | border: none; 110 | border-radius: 6px; 111 | font-size: 14px; cursor: pointer; 112 | transition: background-color 0.3s; 113 | } 114 | 115 | .modal-content form button:hover { 116 | background-color: #1e1e1e; } 117 | 118 | @keyframes fadeIn { 119 | from { opacity: 0; } 120 | to { opacity: 1; } 121 | } 122 | 123 | @keyframes slideIn { 124 | from { transform: translateY(-50px); } 125 | to { transform: translateY(0); } 126 | } 127 | 128 | .papers-list { 129 | display: flex; 130 | flex-wrap: wrap; 131 | gap: 15px; justify-content: flex-start; } 132 | 133 | #papers-container { 134 | list-style-type: none; 135 | padding: 0; 136 | display: flex; 137 | flex-wrap: wrap; 138 | gap: 15px; justify-content: flex-start; opacity: 1; 139 | transition: opacity 0.15s ease-in-out; } 140 | 141 | #papers-container.fade-out { 142 | opacity: 0; 143 | } 144 | 145 | #papers-container.fade-in { 146 | opacity: 1; 147 | } 148 | 149 | .paper-item { 150 | height: 140px; background-color: #ffffff; 151 | padding: 10px; border-left: 4px solid #2f2f2f; border-radius: 6px; 152 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); width: 330px; position: relative; 153 | cursor: pointer; 154 | transition: transform 0.2s, box-shadow 0.15s; display: flex; 155 | flex-direction: column; 156 | gap: 3px; opacity: 1; 157 | overflow: hidden; opacity: 0; transition: transform 0.2s, box-shadow 0.15s, opacity 0.3s; 158 | } 159 | 160 | .paper-item.fade-in { 161 | opacity: 1; } 162 | 163 | 164 | .paper-item:hover { 165 | transform: translateY(-3px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } 166 | 167 | .paper-item h3 { 168 | margin-top: 0; 169 | height: 100px; 170 | width: 90%; 171 | color: #2f2f2f; font-size: 16px; white-space: normal; 172 | overflow: hidden; 173 | text-overflow: ellipsis; } 174 | 175 | .paper-item p { 176 | color: #666666; 177 | font-size: 14px; margin-bottom: 3px; white-space: nowrap; 178 | overflow: hidden; 179 | text-overflow: ellipsis; } 180 | 181 | .paper-item .files-list { 182 | list-style-type: none; 183 | padding: 0; 184 | margin: 0; 185 | } 186 | 187 | .paper-item .files-list li { 188 | margin-bottom: 2px; } 189 | 190 | .paper-item .files-list li a { 191 | color: #2f2f2f; text-decoration: none; 192 | font-size: 12px; } 193 | 194 | .paper-item .files-list li a:hover { 195 | text-decoration: underline; 196 | } 197 | 198 | .paper-item .delete-button { 199 | position: absolute; 200 | top: 8px; 201 | right: 8px; 202 | background: none; 203 | border: none; 204 | color: #2f2f2f; 205 | font-size: 20px; cursor: pointer; 206 | transition: color 0.3s; 207 | } 208 | 209 | .paper-item .delete-button:hover { 210 | color: #2f2f2f; 211 | } 212 | 213 | .paper-item .star-button { 214 | position: absolute; 215 | bottom: 8px; 216 | right: 8px; 217 | background: none; 218 | border: none; 219 | color: #ffc107; 220 | font-size: 20px; cursor: pointer; 221 | transition: color 0.3s; 222 | } 223 | 224 | .paper-item .star-button.unstarred { 225 | color: #e4e5e9; 226 | } 227 | 228 | .paper-item .star-button:hover { 229 | color: #ffc107; 230 | } -------------------------------------------------------------------------------- /app/static/css/plans.css: -------------------------------------------------------------------------------- 1 | 2 | @import url('styles.css'); 3 | 4 | 5 | .relative-container { 6 | position: relative; 7 | } 8 | 9 | .calendar-container { 10 | margin-bottom: 30px; 11 | max-width: 1400px; 12 | width: 100%; 13 | margin-left: auto; 14 | margin-right: auto; 15 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 16 | border-radius: 8px; 17 | } 18 | 19 | .plans-list { 20 | background-color: #ffffff; 21 | padding: 20px; 22 | border-radius: 8px; 23 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 24 | transition: all 0.3s ease; 25 | max-width: 1360px; 26 | margin: 20px auto; 27 | } 28 | 29 | .plans-list h2 { 30 | margin-top: 0; 31 | color: #333333; 32 | } 33 | 34 | .plan-item { 35 | padding: 10px; 36 | border-bottom: 1px solid #ebebeb; 37 | display: flex; 38 | justify-content: space-between; 39 | align-items: center; 40 | transition: background-color 0.3s, transform 0.2s; 41 | } 42 | 43 | .plan-item:last-child { 44 | border-bottom: none; 45 | } 46 | 47 | .plan-item:hover { 48 | background-color: #f5f5f5; 49 | transform: translateY(-2px); 50 | } 51 | 52 | .plan-details { 53 | flex-grow: 1; 54 | } 55 | 56 | .plan-details h3 { 57 | margin: 0 0 5px 0; 58 | font-size: 16px; 59 | color: #2c3e50; 60 | } 61 | 62 | .plan-details p { 63 | margin: 0; 64 | color: #7f8c8d; 65 | } 66 | 67 | .delete-button { 68 | background-color: #2f2f2f; 69 | border: none; 70 | color: white; 71 | padding: 5px 10px; 72 | border-radius: 4px; 73 | cursor: pointer; 74 | transition: background-color 0.3s, transform 0.2s; 75 | } 76 | 77 | .delete-button:hover { 78 | background-color: #2f2f2f; 79 | transform: scale(1.1); 80 | } 81 | 82 | .floating-add-plan { 83 | position: fixed; bottom: 0; left: 50%; transform: translateX(-50%); background-color: rgba(255, 255, 255, 0.95); 84 | padding: 20px; 85 | border-radius: 8px; 86 | box-shadow: 0 -4px 12px rgba(0,0,0,0.2); 87 | z-index: 100; width: 80%; max-width: 800px; display: flex; 88 | flex-direction: row; 89 | align-items: center; 90 | gap: 10px; 91 | bottom: 5%; 92 | } 93 | 94 | .floating-add-plan h2 { 95 | margin: 0; 96 | font-size: 18px; 97 | color: #333333; 98 | flex: 1; 99 | } 100 | 101 | .floating-add-plan form { 102 | display: flex; 103 | flex-direction: row; 104 | align-items: center; 105 | flex: 3; 106 | gap: 10px; 107 | width: 100%; 108 | } 109 | 110 | .floating-add-plan input { 111 | padding: 8px; 112 | border: 1px solid #ccc; 113 | border-radius: 4px; 114 | transition: border-color 0.3s; 115 | flex: 2; 116 | font-size: 14px; 117 | } 118 | 119 | .floating-add-plan input:focus { 120 | border-color: #333333; 121 | outline: none; 122 | } 123 | 124 | .floating-add-plan button { 125 | padding: 8px 16px; 126 | background-color: #2f2f2f; 127 | color: #ffffff; 128 | border: none; 129 | border-radius: 4px; 130 | cursor: pointer; 131 | transition: background-color 0.3s, transform 0.2s; 132 | flex: 1; 133 | font-size: 14px; 134 | } 135 | 136 | .floating-add-plan button:hover { 137 | background-color: #6d6d6d; 138 | transform: scale(1.05); 139 | } 140 | 141 | .fc .fc-button-primary { 142 | background-color: #2f2f2f; 143 | border-color: var(--fc-button-border-color); 144 | color: var(--fc-button-text-color); 145 | } 146 | 147 | 148 | @media (max-width: 768px) { 149 | .calendar-container { 150 | max-width: 100%; 151 | padding: 0 10px; 152 | } 153 | 154 | .plans-list { 155 | max-width: 100%; 156 | padding: 10px; 157 | } 158 | 159 | .floating-add-plan { 160 | width: 95%; 161 | left: 50%; 162 | transform: translateX(-50%); 163 | padding: 15px; 164 | flex-direction: column; 165 | align-items: stretch; 166 | } 167 | 168 | .floating-add-plan form { 169 | flex-direction: column; 170 | gap: 10px; 171 | } 172 | 173 | .floating-add-plan input { 174 | flex: unset; 175 | width: 100%; 176 | } 177 | 178 | .floating-add-plan button { 179 | flex: unset; 180 | width: 100%; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /app/static/css/projects.css: -------------------------------------------------------------------------------- 1 | /* static/css/projects.css */ 2 | 3 | h1 { 4 | text-align: center; 5 | margin-bottom: 20px; 6 | } 7 | 8 | h2 { 9 | font-size: 20px; 10 | color: #333333; 11 | margin-bottom: 10px; 12 | } 13 | 14 | /* Project Actions */ 15 | .project-actions { 16 | margin-bottom: 20px; 17 | display: flex; 18 | gap: 10px; 19 | align-items: center; 20 | } 21 | 22 | .project-actions .btn { 23 | padding: 10px 20px; 24 | font-size: 16px; 25 | background-color: #333333; 26 | color: #ffffff; 27 | border: none; 28 | border-radius: 4px; 29 | cursor: pointer; 30 | transition: background-color 0.3s, transform 0.2s; 31 | } 32 | 33 | .project-actions .btn:hover { 34 | background-color: #555555; 35 | transform: scale(1.05); 36 | } 37 | 38 | #search-projects { 39 | padding: 10px; 40 | font-size: 16px; 41 | border: 1px solid #ccc; 42 | border-radius: 4px; 43 | flex: 1; 44 | transition: border-color 0.3s; 45 | } 46 | 47 | #search-projects:focus { 48 | border-color: #333333; 49 | } 50 | 51 | /* 项目列表 */ 52 | .project-list { 53 | display: flex; 54 | flex-wrap: wrap; 55 | gap: 20px; 56 | } 57 | 58 | /* 项目卡牌 */ 59 | .project-card { 60 | cursor: pointer; 61 | background-color: #ffffff; 62 | border: 1px solid #ebebeb; 63 | border-radius: 8px; 64 | padding: 20px; 65 | width: 220px; 66 | height: 320px; 67 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 68 | position: relative; 69 | transition: transform 0.3s, box-shadow 0.3s; 70 | display: flex; 71 | flex-direction: column; 72 | justify-content: space-between; 73 | } 74 | 75 | .project-card:hover { 76 | transform: translateY(-5px); 77 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); 78 | } 79 | 80 | .project-card h3 { 81 | width: 80%; 82 | height: 120px; 83 | margin: 0 0 10px 0; 84 | font-size: 20px; 85 | color: #333333; 86 | overflow: hidden; 87 | text-overflow: ellipsis; 88 | } 89 | 90 | .project-card p { 91 | height: 100px; 92 | font-size: 12px; 93 | color: #666666; 94 | overflow: hidden; 95 | text-overflow: ellipsis; 96 | } 97 | 98 | .project-card .client, 99 | .project-card .type { 100 | height: 50px; 101 | font-size: 12px; 102 | color: #999999; 103 | } 104 | 105 | /* Action Buttons Container */ 106 | .action-buttons { 107 | position: absolute; 108 | top: 10px; 109 | right: 10px; 110 | display: flex; 111 | flex-direction: column; 112 | gap: 5px; 113 | } 114 | 115 | /* 编辑项目按钮 */ 116 | .edit-project { 117 | background: none; 118 | border: none; 119 | color: #333333; 120 | font-size: 16px; 121 | cursor: pointer; 122 | transition: color 0.3s; 123 | } 124 | 125 | .edit-project:hover { 126 | color: #555555; 127 | } 128 | 129 | /* 删除项目按钮 */ 130 | .delete-project { 131 | background: none; 132 | border: none; 133 | color: #ff4d4d; 134 | font-size: 16px; 135 | cursor: pointer; 136 | transition: color 0.3s; 137 | } 138 | 139 | .delete-project:hover { 140 | color: #ff1a1a; 141 | } 142 | 143 | /* 弹出窗口样式 */ 144 | .modal { 145 | display: none; /* 默认隐藏 */ 146 | position: fixed; 147 | z-index: 1000; 148 | left: 0; 149 | top: 0; 150 | width: 100%; 151 | height: 100%; 152 | overflow: auto; 153 | background-color: rgba(0,0,0,0.5); 154 | animation: fadeIn 0.3s; 155 | } 156 | 157 | .modal-content { 158 | background-color: #ffffff; 159 | margin: 10% auto; 160 | padding: 20px; 161 | border: 1px solid #888; 162 | width: 400px; 163 | border-radius: 8px; 164 | position: relative; 165 | animation: slideIn 0.3s; 166 | } 167 | 168 | .close-button { 169 | color: #aaa; 170 | position: absolute; 171 | top: 10px; 172 | right: 15px; 173 | font-size: 28px; 174 | font-weight: bold; 175 | cursor: pointer; 176 | } 177 | 178 | .close-button:hover, 179 | .close-button:focus { 180 | color: #000; 181 | } 182 | 183 | .modal-content h2 { 184 | margin-top: 0; 185 | color: #333333; 186 | } 187 | 188 | .modal-content form { 189 | display: flex; 190 | flex-direction: column; 191 | } 192 | 193 | .modal-content form label { 194 | margin-top: 10px; 195 | margin-bottom: 5px; 196 | color: #333333; 197 | } 198 | 199 | .modal-content form input, 200 | .modal-content form textarea { 201 | padding: 10px; 202 | font-size: 16px; 203 | border: 1px solid #ccc; 204 | border-radius: 4px; 205 | transition: border-color 0.3s; 206 | } 207 | 208 | .modal-content form input:focus, 209 | .modal-content form textarea:focus { 210 | border-color: #333333; 211 | } 212 | 213 | .modal-content form button { 214 | margin-top: 20px; 215 | padding: 10px; 216 | font-size: 16px; 217 | background-color: #333333; 218 | color: #ffffff; 219 | border: none; 220 | border-radius: 4px; 221 | cursor: pointer; 222 | transition: background-color 0.3s, transform 0.2s; 223 | } 224 | 225 | .modal-content form button:hover { 226 | background-color: #555555; 227 | transform: scale(1.05); 228 | } 229 | 230 | /* 搜索栏动效 */ 231 | #search-projects { 232 | animation: fadeInRight 0.5s ease; 233 | } 234 | 235 | /* 弹出窗口动画 */ 236 | @keyframes fadeIn { 237 | from {opacity: 0;} 238 | to {opacity: 1;} 239 | } 240 | 241 | @keyframes slideIn { 242 | from {transform: translateY(-50px);} 243 | to {transform: translateY(0);} 244 | } 245 | 246 | @keyframes fadeInRight { 247 | from {opacity: 0; transform: translateX(20px);} 248 | to {opacity: 1; transform: translateX(0);} 249 | } 250 | -------------------------------------------------------------------------------- /app/static/css/styles.css: -------------------------------------------------------------------------------- 1 | @import url('notes.css'); 2 | .animate__animated { 3 | --animate-duration: 0.2s; } 4 | 5 | body { 6 | margin: 0; 7 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 8 | background-color: #f5f5f5; 9 | color: #333333; 10 | transition: background-color 0.3s, color 0.3s; 11 | } 12 | 13 | .container { 14 | display: flex; 15 | height: 100vh; 16 | overflow: hidden; 17 | } 18 | 19 | .sidebar { 20 | width: 220px; 21 | background-color: #ffffff; 22 | box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); 23 | padding: 20px; 24 | transition: all 0.3s ease; 25 | } 26 | 27 | .sidebar.collapsed { 28 | width: 60px; 29 | } 30 | 31 | .sidebar h2 { 32 | text-align: center; 33 | margin-bottom: 30px; 34 | color: #333333; 35 | font-size: 24px; 36 | } 37 | 38 | .sidebar ul { 39 | list-style-type: none; 40 | padding: 0; 41 | } 42 | 43 | .sidebar ul li { 44 | margin: 20px 0; 45 | } 46 | 47 | .sidebar ul li a { 48 | color: #333333; 49 | text-decoration: none; 50 | font-size: 18px; 51 | display: flex; 52 | align-items: center; 53 | padding: 10px; 54 | border-radius: 4px; 55 | transition: background-color 0.3s, color 0.3s, transform 0.2s; 56 | } 57 | 58 | .sidebar ul li a i { 59 | margin-right: 10px; 60 | } 61 | 62 | .sidebar.collapsed ul li a span { 63 | display: none; 64 | } 65 | 66 | .sidebar ul li a:hover { 67 | background-color: #ebebeb; 68 | color: #000000; 69 | transform: translateX(5px); 70 | } 71 | 72 | .content { 73 | flex: 1; 74 | padding: 40px; 75 | overflow-y: auto; 76 | background-color: #f5f5f5; 77 | transition: background-color 0.3s, color 0.3s; 78 | } 79 | 80 | h1 { 81 | color: #333333; 82 | margin-bottom: 20px; 83 | font-size: 32px; 84 | animation: none; 85 | } 86 | 87 | p { 88 | font-size: 16px; 89 | line-height: 1.6; 90 | animation: none; 91 | } 92 | 93 | .feature-icons { 94 | margin-top: 30px; 95 | display: flex; 96 | gap: 20px; 97 | justify-content: center; 98 | } 99 | 100 | .feature-icons i { 101 | font-size: 40px; 102 | color: #333333; 103 | transition: transform 0.3s, color 0.3s; 104 | } 105 | 106 | .logo-container { 107 | text-align: center; 108 | margin-bottom: 20px; 109 | filter: grayscale(100%); 110 | } 111 | 112 | .logo { 113 | max-width: 150px; 114 | height: auto; 115 | } 116 | 117 | .feature-icons i:hover { 118 | transform: scale(1.2); 119 | color: #000000; 120 | } 121 | 122 | @keyframes fadeInDown { 123 | from { 124 | opacity: 0; 125 | transform: translateY(-20px); 126 | } 127 | to { 128 | opacity: 1; 129 | transform: translateY(0); 130 | } 131 | } 132 | 133 | @keyframes fadeInUp { 134 | from { 135 | opacity: 0; 136 | transform: translateY(20px); 137 | } 138 | to { 139 | opacity: 1; 140 | transform: translateY(0); 141 | } 142 | } 143 | 144 | @media (max-width: 768px) { 145 | .container { 146 | flex-direction: column; 147 | } 148 | 149 | .sidebar { 150 | width: 100%; 151 | box-shadow: none; 152 | display: flex; 153 | justify-content: space-around; 154 | } 155 | 156 | .sidebar h2 { 157 | display: none; 158 | } 159 | 160 | .sidebar ul { 161 | display: flex; 162 | width: 100%; 163 | justify-content: space-around; 164 | } 165 | 166 | .sidebar ul li { 167 | margin: 0; 168 | } 169 | 170 | .sidebar ul li a { 171 | padding: 15px; 172 | font-size: 16px; 173 | } 174 | 175 | .content { 176 | padding: 20px; 177 | } 178 | } 179 | 180 | .toggle-button { 181 | background: none; 182 | border: none; 183 | font-size: 24px; 184 | cursor: pointer; 185 | color: #333333; 186 | margin-bottom: 20px; 187 | display: none; } 188 | 189 | @media (max-width: 768px) { 190 | .toggle-button { 191 | display: block; 192 | } 193 | 194 | .sidebar.collapsed { 195 | height: auto; 196 | } 197 | 198 | .sidebar.collapsed ul { 199 | display: none; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /app/static/css/view_idea.css: -------------------------------------------------------------------------------- 1 | 2 | .view-idea-container { 3 | background-size: cover; 4 | background-position: center; 5 | padding: 40px; 6 | min-height: 100vh; 7 | box-sizing: border-box; 8 | } 9 | 10 | .back-button { 11 | display: inline-flex; 12 | padding: 8px 12px; 13 | background-color: #2f2f2f; 14 | color: #ffffff; 15 | text-decoration: none; 16 | border-radius: 4px; 17 | transition: background-color 0.3s; 18 | margin-bottom: 20px; 19 | } 20 | 21 | .back-button:hover { 22 | background-color: #333333; 23 | } 24 | 25 | .idea-detail { 26 | padding: 30px; 27 | border-radius: 8px; 28 | } 29 | 30 | .idea-detail form { 31 | display: flex; 32 | flex-direction: column; 33 | } 34 | 35 | .idea-detail form label { 36 | margin-top: 15px; 37 | font-weight: bold; 38 | color: #333333; 39 | } 40 | 41 | .idea-detail form input { 42 | padding: 12px; 43 | margin-top: 5px; 44 | border: 1px solid #cccccc; 45 | border-radius: 4px; 46 | font-size: 16px; 47 | } 48 | 49 | .idea-detail form button { 50 | margin-top: 25px; 51 | padding: 9px; 52 | background-color: #2f2f2f; 53 | color: #ffffff; 54 | border: none; 55 | border-radius: 4px; 56 | cursor: pointer; 57 | width: 10%; 58 | transition: background-color 0.3s; 59 | font-size: 16px; 60 | } 61 | 62 | .idea-detail form button:hover { 63 | background-color: #218838; 64 | } 65 | 66 | .related-papers { 67 | margin-top: 40px; 68 | } 69 | 70 | .related-papers h4 { 71 | margin-bottom: 15px; 72 | color: #333333; 73 | } 74 | 75 | .related-papers ul { 76 | list-style-type: none; 77 | padding: 0; 78 | } 79 | 80 | .related-papers li { 81 | background-color: #f8f9fa; 82 | padding: 20px; 83 | margin-bottom: 15px; 84 | border-radius: 4px; 85 | position: relative; 86 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 87 | } 88 | 89 | .related-papers li .delete-related-paper { 90 | position: absolute; 91 | top: 15px; 92 | right: 15px; 93 | background: none; 94 | border: none; 95 | color: #2f2f2f; 96 | font-size: 16px; 97 | cursor: pointer; 98 | transition: color 0.3s; 99 | } 100 | 101 | .related-papers li .delete-related-paper:hover { 102 | color: #2f2f2f; 103 | } 104 | 105 | #add-related-paper-form { 106 | display: flex; 107 | flex-direction: column; 108 | margin-top: 20px; 109 | } 110 | 111 | #add-related-paper-form input { 112 | padding: 10px; 113 | margin-bottom: 12px; 114 | border: 1px solid #cccccc; 115 | border-radius: 4px; 116 | font-size: 14px; 117 | } 118 | 119 | #add-related-paper-form button { 120 | padding: 10px; 121 | background-color: #2f2f2f; 122 | color: #ffffff; 123 | border: none; 124 | border-radius: 4px; 125 | cursor: pointer; 126 | transition: background-color 0.3s; 127 | font-size: 14px; 128 | } 129 | 130 | #add-related-paper-form button:hover { 131 | background-color: #138496; 132 | } 133 | 134 | .input-text{ 135 | font-family: Arial, Helvetica, sans-serif; 136 | font-size: 16px; 137 | background-color: transparent; 138 | border: transparent; 139 | resize: none; 140 | } 141 | 142 | .idea-label{ 143 | margin-top: 100px; 144 | font-size: 24px; 145 | margin-bottom: 10px; 146 | } 147 | 148 | #related-papers-container{ 149 | gap:20px; 150 | } 151 | 152 | .related-paper-div{ 153 | width: 100%; 154 | } -------------------------------------------------------------------------------- /app/static/css/view_note.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'PingFang-Bold'; 3 | src: url('{{ url_for(\'static\', filename=\'font/PingFang-Bold.ttf\') }}') format('truetype'); 4 | font-weight: bold; 5 | font-style: normal; 6 | } 7 | 8 | .view-note-container { 9 | padding: 20px; 10 | max-width: 90%; 11 | height: 82vh; 12 | margin: auto; 13 | font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif; /* 更新字体 */ 14 | } 15 | 16 | .view-note-header { 17 | display: flex; 18 | align-items: center; 19 | gap: 10px; 20 | margin-bottom: 20px; 21 | } 22 | 23 | .current-path { 24 | flex-grow: 1; 25 | font-size: 18px; 26 | color: #555; 27 | } 28 | 29 | /* 按钮样式 */ 30 | .back-button, .save-button, .export-button { 31 | background-color: #2f2f2f; 32 | color: #ffffff; 33 | border: none; 34 | padding: 10px 20px; 35 | border-radius: 8px; /* 增加圆角 */ 36 | cursor: pointer; 37 | font-size: 16px; 38 | display: flex; 39 | align-items: center; 40 | transition: background-color 0.3s, transform 0.2s; 41 | } 42 | 43 | .back-button i, .save-button i { 44 | margin-right: 8px; 45 | } 46 | 47 | .back-button:hover:not(:disabled), .save-button:hover, .export-button:hover { 48 | background-color: #555555; /* 更柔和的悬停颜色 */ 49 | transform: translateY(-2px); 50 | } 51 | 52 | .back-button:disabled { 53 | opacity: 0.5; 54 | cursor: not-allowed; 55 | } 56 | 57 | /* 模态窗口样式 */ 58 | .modal { 59 | display: none; 60 | position: fixed; 61 | z-index: 3000; 62 | left: 0; 63 | top: 0; 64 | width: 100%; 65 | height: 100%; 66 | overflow: auto; 67 | background-color: rgba(0,0,0,0.4); 68 | } 69 | 70 | .modal-content { 71 | background-color: #fefefe; 72 | margin: 15% auto; 73 | padding: 20px; 74 | border: 1px solid #ddd; /* 更浅的边框颜色 */ 75 | width: 400px; 76 | border-radius: 12px; /* 增加圆角 */ 77 | text-align: center; 78 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 增加阴影 */ 79 | animation: fadeInZoom 0.3s; /* 延长动画时间 */ 80 | } 81 | 82 | .export-dropdown { 83 | position: relative; 84 | display: inline-block; 85 | } 86 | 87 | .export-menu { 88 | display: none; 89 | position: absolute; 90 | right: 0; 91 | background-color: #f9f9f9; 92 | min-width: 160px; 93 | box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); 94 | z-index: 1000; 95 | border-radius: 8px; /* 增加圆角 */ 96 | } 97 | 98 | .export-option { 99 | background-color: #f9f9f9; 100 | color: black; 101 | padding: 12px 16px; 102 | text-decoration: none; 103 | display: block; 104 | width: 100%; 105 | border: none; 106 | text-align: left; 107 | cursor: pointer; 108 | font-size: 14px; 109 | border-radius: 0; /* 保持选项矩形 */ 110 | } 111 | 112 | .export-option:hover { 113 | background-color: #e0e0e0; /* 更柔和的悬停颜色 */ 114 | } 115 | 116 | .export-dropdown:hover .export-menu { 117 | display: block; 118 | } 119 | 120 | @keyframes fadeInZoom { 121 | from { opacity: 0; transform: scale(0.8); } 122 | to { opacity: 1; transform: scale(1); } 123 | } 124 | 125 | .close-button { 126 | color: #aaa; 127 | float: right; 128 | font-size: 28px; 129 | font-weight: bold; 130 | cursor: pointer; 131 | } 132 | 133 | .close-button:hover, 134 | .close-button:focus { 135 | color: black; 136 | text-decoration: none; 137 | } 138 | 139 | .modal-button { 140 | padding: 10px 20px; 141 | margin: 10px; 142 | border: none; 143 | border-radius: 8px; /* 增加圆角 */ 144 | cursor: pointer; 145 | font-size: 16px; 146 | } 147 | 148 | .modal-button.confirm { 149 | background-color: #5cb85c; 150 | color: white; 151 | } 152 | 153 | .modal-button.confirm:hover { 154 | background-color: #4cae4c; 155 | } 156 | 157 | .modal-button.cancel { 158 | background-color: #d9534f; 159 | color: white; 160 | } 161 | 162 | .modal-button.cancel:hover { 163 | background-color: #c9302c; 164 | } 165 | 166 | .view-note-content { 167 | display: flex; 168 | flex-direction: column; 169 | height: 100%; 170 | padding: 10px; 171 | overflow: hidden; 172 | } 173 | 174 | /* 合并 .viewer 类 */ 175 | .viewer { 176 | width: 100%; 177 | display: flex; 178 | flex-direction: column; 179 | flex: 1; 180 | overflow-y: auto; 181 | padding: 20px; 182 | background-color: #ffffff; /* 更接近 Notion 的白色背景 */ 183 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); /* 更柔和的阴影 */ 184 | white-space: pre-wrap; 185 | word-wrap: break-word; 186 | font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif; /* 使用 Inter 字体 */ 187 | font-size: 16px; 188 | color: #333333; 189 | border-radius: 8px; /* 增加圆角 */ 190 | margin: 0 auto; /* 居中 */ 191 | max-width: 900px; /* 设置最大宽度 */ 192 | line-height: 1.6; /* 增加行高 */ 193 | outline: none; 194 | } 195 | 196 | /* 新增:为可写的 div 添加鼠标悬停效果 */ 197 | .viewer > div, 198 | .viewer > p, 199 | .viewer > h1, 200 | .viewer > h2, 201 | .viewer > h3, 202 | .viewer > blockquote, 203 | .viewer > pre, 204 | .viewer > li { 205 | transition: background-color 0.3s ease; /* 添加平滑过渡 */ 206 | } 207 | 208 | .viewer > div:hover, 209 | .viewer > p:hover, 210 | .viewer > h1:hover, 211 | .viewer > h2:hover, 212 | .viewer > h3:hover, 213 | .viewer > blockquote:hover, 214 | .viewer > pre:hover, 215 | .viewer > li:hover { 216 | background-color: #f0f0f0; /* 悬停时的背景颜色 */ 217 | } 218 | 219 | /* 标题样式 */ 220 | .viewer h1 { 221 | font-size: 32px; /* 调整标题大小 */ 222 | font-weight: 700; /* 加粗 */ 223 | color: #212121; 224 | text-align: left; /* 左对齐 */ 225 | margin-bottom: 20px; /* 增加下边距 */ 226 | } 227 | 228 | .viewer h2 { 229 | font-size: 24px; /* 调整标题大小 */ 230 | font-weight: 600; 231 | margin-bottom: 15px; 232 | color: #212121; 233 | } 234 | 235 | .viewer h3 { 236 | font-size: 20px; 237 | font-weight: 500; 238 | color: #212121; 239 | margin-bottom: 10px; 240 | } 241 | 242 | .viewer p { 243 | margin-top: 0; 244 | margin-bottom: 16px; /* 增加段落下边距 */ 245 | color: #555555; /* 更柔和的文本颜色 */ 246 | } 247 | 248 | /* 代码块样式 */ 249 | .hljs { 250 | padding: 15px; 251 | background-color: #f4f6f9; 252 | border-radius: 8px; 253 | border: 1px solid #e0e0e0; 254 | font-family: 'Courier New', monospace; 255 | font-size: 16px; 256 | color: #3e3e3e; 257 | overflow-x: auto; 258 | margin-bottom: 20px; /* 增加下边距 */ 259 | } 260 | 261 | /* 引用样式 */ 262 | .viewer blockquote { 263 | border-left: 4px solid #2f2f2f; 264 | padding-left: 20px; /* 增加内边距 */ 265 | margin: 20px 0; 266 | color: #666; 267 | background-color: #f4f6f9; 268 | border-radius: 4px; /* 增加圆角 */ 269 | font-style: italic; /* 斜体 */ 270 | } 271 | 272 | /* 列表样式 */ 273 | .viewer ol, .viewer ul { 274 | padding-left: 25px; /* 增加左填充 */ 275 | margin-bottom: 16px; /* 增加下边距 */ 276 | } 277 | 278 | .viewer li { 279 | margin-bottom: 8px; /* 增加列表项间距 */ 280 | } 281 | 282 | /* 链接样式 */ 283 | .viewer a { 284 | color: #007bff; 285 | text-decoration: none; 286 | transition: color 0.2s ease-in-out; 287 | } 288 | 289 | .viewer a:hover { 290 | text-decoration: underline; 291 | color: #0056b3; 292 | } 293 | 294 | /* 表格样式 */ 295 | .viewer table { 296 | width: 100%; 297 | border-collapse: collapse; 298 | margin-top: 20px; 299 | margin-bottom: 20px; 300 | border: 1px solid #e0e0e0; /* 增加表格边框 */ 301 | border-radius: 8px; /* 增加圆角 */ 302 | overflow: hidden; /* 隐藏溢出部分 */ 303 | } 304 | 305 | .viewer th, .viewer td { 306 | padding: 12px; 307 | text-align: left; 308 | border-bottom: 1px solid #e0e0e0; 309 | } 310 | 311 | .viewer th { 312 | background-color: #f4f6f9; 313 | color: #212121; 314 | font-weight: 600; 315 | } 316 | 317 | .viewer tr:nth-child(even) { 318 | background-color: #fafbfc; 319 | } 320 | 321 | /* 代码行内样式 */ 322 | .viewer code { 323 | background-color: #f4f6f9; 324 | padding: 2px 5px; 325 | border-radius: 4px; 326 | font-family: 'Courier New', monospace !important; 327 | font-size: 1em; 328 | color: #d9534f; 329 | } 330 | 331 | /* 强调文本样式 */ 332 | .viewer strong { 333 | font-weight: 600; 334 | color: #007bff; 335 | } 336 | 337 | .viewer em { 338 | font-style: italic; 339 | color: #666; 340 | } 341 | 342 | /* 分隔线 */ 343 | .viewer hr { 344 | border: 0; 345 | border-top: 1px solid #e0e0e0; 346 | margin: 30px 0; 347 | } 348 | 349 | /* 调整中文输入的字体和行高 */ 350 | .viewer p, .viewer li, .viewer h1, .viewer h2, .viewer h3, .viewer blockquote, .viewer table { 351 | font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; 352 | } 353 | 354 | /* 代码块样式 (中文友好) */ 355 | .viewer pre { 356 | font-family: 'Courier New', monospace; 357 | font-size: 16px; 358 | } 359 | 360 | /* 标题字体适应中文 */ 361 | .viewer h1, .viewer h2, .viewer h3 { 362 | font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; 363 | font-weight: bold; 364 | } 365 | 366 | /* MathJax 样式调整 */ 367 | .MathJax_Display { 368 | text-align: center; 369 | margin: 1em 0; 370 | font-size: 1.2em; /* 增大数学公式字体 */ 371 | } 372 | 373 | /* 代码块的滚动条样式(可选) */ 374 | .viewer pre::-webkit-scrollbar { 375 | width: 8px; 376 | } 377 | 378 | .viewer pre::-webkit-scrollbar-track { 379 | background: #f1f1f1; 380 | border-radius: 8px; 381 | } 382 | 383 | .viewer pre::-webkit-scrollbar-thumb { 384 | background: #c1c1c1; 385 | border-radius: 8px; 386 | } 387 | 388 | .viewer pre::-webkit-scrollbar-thumb:hover { 389 | background: #a8a8a8; 390 | } 391 | -------------------------------------------------------------------------------- /app/static/css/view_project.css: -------------------------------------------------------------------------------- 1 | /* static/css/view_project.css */ 2 | 3 | /* General Styles */ 4 | body, .animate__animated, .animate__fadeIn { 5 | font-family: Arial, sans-serif; 6 | } 7 | 8 | h1 { 9 | text-align: center; 10 | margin-bottom: 20px; 11 | } 12 | 13 | h2 { 14 | font-size: 20px; 15 | color: #333333; 16 | margin-bottom: 10px; 17 | } 18 | 19 | /* Management Container */ 20 | .management-container { 21 | display: flex; 22 | gap: 20px; 23 | flex-wrap: wrap; 24 | margin-bottom: 20px; 25 | } 26 | 27 | /* File Management Section */ 28 | .file-management { 29 | display: block; 30 | flex: 1; 31 | min-width: 300px; 32 | background-color: #ffffff; 33 | padding: 20px; 34 | border-radius: 8px; 35 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 36 | display: flex; 37 | flex-direction: column; 38 | gap: 10px; 39 | } 40 | 41 | #upload-file-form { 42 | display: flex; 43 | flex-direction: column; 44 | gap: 10px; 45 | } 46 | 47 | .custom-file-upload { 48 | display: inline-block; 49 | padding: 10px 20px; 50 | cursor: pointer; 51 | background-color: #333333; 52 | color: #ffffff; 53 | border-radius: 4px; 54 | transition: background-color 0.3s, transform 0.2s; 55 | text-align: center; 56 | } 57 | 58 | .custom-file-upload:hover { 59 | background-color: #555555; 60 | transform: scale(1.05); 61 | } 62 | 63 | #upload-file-form input[type="file"] { 64 | display: none; 65 | } 66 | 67 | #upload-file-form input[type="text"] { 68 | padding: 10px; 69 | font-size: 14px; 70 | width: 80%; 71 | border: 1px solid #ccc; 72 | border-radius: 4px; 73 | } 74 | 75 | #upload-file-form button { 76 | padding: 13px; 77 | font-size: 16px; 78 | width: 100%; 79 | background-color: #333333; 80 | color: #ffffff; 81 | border: none; 82 | border-radius: 4px; 83 | cursor: pointer; 84 | transition: background-color 0.3s, transform 0.2s; 85 | } 86 | 87 | #upload-file-form button:hover { 88 | background-color: #555555; 89 | transform: scale(1.05); 90 | } 91 | 92 | #file-search { 93 | padding: 10px; 94 | font-size: 14px; 95 | border: 1px solid #ccc; 96 | border-radius: 4px; 97 | } 98 | 99 | .file-list-container { 100 | max-height: 300px; 101 | overflow-y: auto; 102 | border: 1px solid #ebebeb; 103 | border-radius: 4px; 104 | padding: 10px; 105 | background-color: #f9f9f9; 106 | } 107 | 108 | #file-list { 109 | display: flex; 110 | flex-wrap: wrap; 111 | gap: 10px; 112 | list-style-type: none; 113 | padding: 0; 114 | } 115 | 116 | #file-list li { 117 | width: 100px; 118 | height: 100px; 119 | background-color: #ffffff; 120 | border: 1px solid #ccc; 121 | border-radius: 8px; 122 | position: relative; 123 | display: flex; 124 | align-items: center; 125 | justify-content: center; 126 | flex-direction: column; 127 | transition: box-shadow 0.3s, transform 0.3s; 128 | } 129 | 130 | #file-list li:hover { 131 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); 132 | transform: scale(1.05); 133 | } 134 | 135 | .file-item i { 136 | display: flex; 137 | justify-content: center; 138 | font-size: 24px; 139 | color: #333333; 140 | margin-bottom: 5px; 141 | } 142 | 143 | .file-item a { 144 | font-size: 12px; 145 | color: #333333; 146 | text-decoration: none; 147 | word-wrap: break-word; 148 | text-align: center; 149 | transition: color 0.3s; 150 | } 151 | 152 | .file-item a:hover { 153 | color: #000000; 154 | } 155 | 156 | .delete-file { 157 | position: absolute; 158 | top: 2px; 159 | right: 2px; 160 | background: none; 161 | border: none; 162 | color: #ff4d4d; 163 | font-size: 14px; 164 | cursor: pointer; 165 | transition: color 0.3s; 166 | } 167 | 168 | .delete-file:hover { 169 | color: #ff1a1a; 170 | } 171 | 172 | /* Task Management Section */ 173 | .task-management { 174 | flex: 1; 175 | min-width: 300px; 176 | background-color: #ffffff; 177 | padding: 20px; 178 | border-radius: 8px; 179 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 180 | display: flex; 181 | flex-direction: column; 182 | gap: 10px; 183 | } 184 | 185 | #add-task-form { 186 | display: flex; 187 | flex-direction: column; 188 | gap: 10px; 189 | } 190 | 191 | #add-task-form input, 192 | #add-task-form textarea, 193 | #add-task-form select { 194 | padding: 10px; 195 | font-size: 14px; 196 | border: 1px solid #ccc; 197 | border-radius: 4px; 198 | } 199 | 200 | #add-task-form textarea { 201 | resize: vertical; 202 | } 203 | 204 | #add-task-form button { 205 | padding: 10px; 206 | font-size: 16px; 207 | background-color: #333333; 208 | color: #ffffff; 209 | border: none; 210 | border-radius: 4px; 211 | cursor: pointer; 212 | transition: background-color 0.3s, transform 0.2s; 213 | } 214 | 215 | #add-task-form button:hover { 216 | background-color: #555555; 217 | transform: scale(1.05); 218 | } 219 | 220 | .task-list-container { 221 | display: flex; 222 | flex-direction: column; 223 | gap: 10px; 224 | } 225 | 226 | #task-cards { 227 | display: grid; 228 | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); 229 | gap: 10px; 230 | padding: 10px; 231 | height: 150px; 232 | overflow-y: auto; 233 | } 234 | 235 | .task-card { 236 | background-color: #f9f9f9; 237 | border: 1px solid #ebebeb; 238 | border-radius: 8px; 239 | padding: 10px; 240 | position: relative; 241 | transition: transform 0.3s, box-shadow 0.3s; 242 | } 243 | 244 | .task-card:hover { 245 | transform: translateY(-5px); 246 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); 247 | } 248 | 249 | .task-title { 250 | font-size: 12px; 251 | font-weight: bold; 252 | color: #333333; 253 | margin-bottom: 5px; 254 | border: none; 255 | background: none; 256 | width: 100%; 257 | text-align: left; 258 | } 259 | 260 | .task-description { 261 | font-size: 12px; 262 | color: #666666; 263 | resize: none; 264 | border: none; 265 | background: none; 266 | width: 100%; 267 | height: 50px; 268 | } 269 | 270 | .task-status { 271 | width: 100%; 272 | padding: 5px; 273 | font-size: 14px; 274 | border: 1px solid #ccc; 275 | border-radius: 4px; 276 | } 277 | 278 | .delete-task { 279 | position: absolute; 280 | bottom: 5px; 281 | right: 5px; 282 | background: none; 283 | border: none; 284 | color: #ff4d4d; 285 | font-size: 14px; 286 | cursor: pointer; 287 | transition: color 0.3s; 288 | } 289 | 290 | .delete-task:hover { 291 | color: #ff1a1a; 292 | } 293 | 294 | .pagination { 295 | display: flex; 296 | justify-content: center; 297 | align-items: center; 298 | gap: 10px; 299 | } 300 | 301 | .page-btn { 302 | padding: 5px 10px; 303 | font-size: 14px; 304 | background-color: #333333; 305 | color: #ffffff; 306 | border: none; 307 | border-radius: 4px; 308 | cursor: pointer; 309 | transition: background-color 0.3s, transform 0.2s; 310 | } 311 | 312 | .page-btn:hover { 313 | background-color: #555555; 314 | transform: scale(1.05); 315 | } 316 | 317 | /* Milestone Management Section */ 318 | .milestone-management { 319 | background-color: #ffffff; 320 | padding: 20px; 321 | border-radius: 8px; 322 | margin-bottom: 20px; 323 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 324 | } 325 | 326 | .milestone-container { 327 | display: flex; 328 | gap: 20px; 329 | flex-wrap: wrap; 330 | } 331 | 332 | .milestone-controls { 333 | flex: 1; 334 | min-width: 300px; 335 | display: flex; 336 | flex-direction: column; 337 | gap: 10px; 338 | } 339 | 340 | #add-milestone-form { 341 | display: flex; 342 | flex-direction: column; 343 | gap: 10px; 344 | margin-bottom: 10px; 345 | } 346 | 347 | #add-milestone-form input[type="text"], 348 | #add-milestone-form textarea, 349 | #add-milestone-form input[type="date"] { 350 | padding: 10px; 351 | font-size: 14px; 352 | border: 1px solid #ccc; 353 | border-radius: 4px; 354 | } 355 | 356 | #add-milestone-form button { 357 | padding: 10px; 358 | font-size: 14px; 359 | background-color: #333333; 360 | color: #ffffff; 361 | border: none; 362 | border-radius: 4px; 363 | cursor: pointer; 364 | transition: background-color 0.3s, transform 0.2s; 365 | } 366 | 367 | #add-milestone-form button:hover { 368 | background-color: #555555; 369 | transform: scale(1.05); 370 | } 371 | 372 | #milestone-list-container { 373 | height: 170px; 374 | overflow-y: auto; 375 | border: 1px solid #ebebeb; 376 | border-radius: 4px; 377 | padding: 10px; 378 | background-color: #f9f9f9; 379 | } 380 | 381 | #milestone-list { 382 | list-style-type: none; 383 | padding: 0; 384 | } 385 | 386 | #milestone-list li { 387 | padding: 10px; 388 | border-bottom: 1px solid #ebebeb; 389 | display: flex; 390 | justify-content: space-between; 391 | align-items: center; 392 | transition: background-color 0.3s; 393 | } 394 | 395 | #milestone-list li:last-child { 396 | border-bottom: none; 397 | } 398 | 399 | #milestone-list li:hover { 400 | background-color: #bcbcbc; 401 | } 402 | 403 | .milestone-item { 404 | display: flex; 405 | flex-direction: column; 406 | gap: 5px; 407 | } 408 | 409 | .milestone-title { 410 | font-size: 16px; 411 | font-weight: bold; 412 | color: #333333; 413 | } 414 | 415 | .milestone-date { 416 | font-size: 14px; 417 | color: #666666; 418 | } 419 | 420 | .milestone-actions { 421 | display: flex; 422 | gap: 10px; 423 | } 424 | 425 | .edit-milestone, 426 | .delete-milestone { 427 | background: none; 428 | border: none; 429 | color: #333333; 430 | cursor: pointer; 431 | font-size: 14px; 432 | transition: color 0.3s; 433 | } 434 | 435 | .edit-milestone:hover, 436 | .delete-milestone:hover { 437 | color: #555555; 438 | } 439 | 440 | /* TimelineJS Container */ 441 | .milestone-timeline { 442 | flex: 2; 443 | width: 300px; 444 | height: 410px; /* Adjust as needed */ 445 | } 446 | 447 | /* Override TimelineJS Fonts */ 448 | #timeline { 449 | font-family: Arial, sans-serif !important; 450 | } 451 | 452 | /* Back Button */ 453 | #back-btn { 454 | display: block; 455 | margin: 0 auto; 456 | padding: 10px 20px; 457 | font-size: 16px; 458 | background-color: #333333; 459 | color: #ffffff; 460 | border: none; 461 | border-radius: 4px; 462 | cursor: pointer; 463 | transition: background-color 0.3s, transform 0.2s; 464 | } 465 | 466 | #back-btn:hover { 467 | background-color: #555555; 468 | transform: scale(1.05); 469 | } 470 | 471 | /* Responsive Design */ 472 | @media (max-width: 1024px) { 473 | .management-container { 474 | flex-direction: column; 475 | } 476 | } 477 | 478 | @media (max-width: 768px) { 479 | .management-container { 480 | flex-direction: column; 481 | } 482 | 483 | .file-management, 484 | .task-management { 485 | min-width: 100%; 486 | } 487 | 488 | .milestone-container { 489 | flex-direction: column; 490 | } 491 | 492 | .milestone-timeline { 493 | height: 300px; /* Adjust for smaller screens */ 494 | } 495 | } 496 | 497 | .tl-storyslider{ 498 | height: 150px; 499 | } 500 | 501 | #milestone-description { 502 | resize: none; 503 | } -------------------------------------------------------------------------------- /app/static/dist/highlight/default.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Theme: Default 3 | Description: Original highlight.js style 4 | Author: (c) Ivan Sagalaev 5 | Maintainer: @highlightjs/core-team 6 | Website: https://highlightjs.org/ 7 | License: see project LICENSE 8 | Touched: 2021 9 | */pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} -------------------------------------------------------------------------------- /app/static/dist/html2pdf/es6-promise.auto.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.ES6Promise=e()}(this,function(){"use strict";function t(t){var e=typeof t;return null!==t&&("object"===e||"function"===e)}function e(t){return"function"==typeof t}function n(t){W=t}function r(t){z=t}function o(){return function(){return process.nextTick(a)}}function i(){return"undefined"!=typeof U?function(){U(a)}:c()}function s(){var t=0,e=new H(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){return t.port2.postMessage(0)}}function c(){var t=setTimeout;return function(){return t(a,1)}}function a(){for(var t=0;t { 3 | const createForm = document.getElementById('create-idea-form'); 4 | const titleInput = document.getElementById('idea-title'); 5 | const descriptionInput = document.getElementById('idea-description'); 6 | const ideasContainer = document.getElementById('ideas-container'); 7 | 8 | const createModal = document.getElementById('create-idea-modal'); 9 | const openCreateModalButton = document.getElementById('open-create-idea'); 10 | const closeCreateModalButton = createModal.querySelector('.close-button'); 11 | 12 | const openModal = (modal) => { 13 | modal.style.display = 'block'; 14 | }; 15 | 16 | const closeModal = (modal) => { 17 | modal.style.display = 'none'; 18 | }; 19 | 20 | openCreateModalButton.addEventListener('click', () => { 21 | openModal(createModal); 22 | }); 23 | 24 | closeCreateModalButton.addEventListener('click', () => { 25 | closeModal(createModal); 26 | }); 27 | 28 | window.addEventListener('click', (event) => { 29 | if (event.target == createModal) { 30 | closeModal(createModal); 31 | } 32 | }); 33 | 34 | const fetchIdeas = () => { 35 | fetch('/api/ideas') 36 | .then(response => response.json()) 37 | .then(data => { 38 | if (data.status === 200) { 39 | ideasContainer.innerHTML = ''; 40 | data.data.forEach(idea => { 41 | const li = document.createElement('li'); 42 | li.classList.add('idea-item'); 43 | li.setAttribute('data-id', idea.id); 44 | li.innerHTML = ` 45 |
46 |

${idea.title}

47 |

${idea.description}

48 |
49 | 50 | `; 51 | ideasContainer.appendChild(li); 52 | }); 53 | } else { 54 | console.error('获取想法失败:', data); 55 | } 56 | }) 57 | .catch(error => { 58 | console.error('获取想法出错:', error); 59 | }); 60 | }; 61 | 62 | const createIdea = (title, description) => { 63 | fetch('/api/ideas', { 64 | method: 'POST', 65 | headers: { 66 | 'Content-Type': 'application/json' 67 | }, 68 | body: JSON.stringify({ title, description }) 69 | }) 70 | .then(response => response.json()) 71 | .then(data => { 72 | if (data.status === 200) { 73 | fetchIdeas(); 74 | closeModal(createModal); 75 | createForm.reset(); 76 | } else { 77 | alert(data.data.error || '添加想法失败。'); 78 | } 79 | }) 80 | .catch(error => { 81 | console.error('创建想法出错:', error); 82 | }); 83 | }; 84 | 85 | const deleteIdea = (id) => { 86 | fetch(`/api/ideas/${id}`, { 87 | method: 'DELETE' 88 | }) 89 | .then(response => response.json()) 90 | .then(data => { 91 | if (data.status === 200) { 92 | fetchIdeas(); 93 | } else { 94 | alert(data.data.error || '删除想法失败。'); 95 | } 96 | }) 97 | .catch(error => { 98 | console.error('删除想法出错:', error); 99 | }); 100 | }; 101 | 102 | createForm.addEventListener('submit', (e) => { 103 | e.preventDefault(); 104 | const title = titleInput.value.trim(); 105 | const description = descriptionInput.value.trim(); 106 | if (title && description) { 107 | createIdea(title, description); 108 | } else { 109 | alert('标题和描述均为必填项。'); 110 | } 111 | }); 112 | 113 | ideasContainer.addEventListener('click', (e) => { 114 | if (e.target.closest('.delete-button')) { 115 | e.stopPropagation(); const id = e.target.closest('.delete-button').dataset.id; 116 | if (confirm('确定要删除这个想法吗?')) { 117 | deleteIdea(id); 118 | } 119 | } 120 | }); 121 | 122 | ideasContainer.addEventListener('click', (e) => { 123 | const ideaItem = e.target.closest('.idea-item'); 124 | if (ideaItem && !e.target.closest('.delete-button')) { 125 | const id = ideaItem.dataset.id; 126 | window.location.href = `/ideas/${id}`; 127 | } 128 | }); 129 | 130 | fetchIdeas(); 131 | }); 132 | -------------------------------------------------------------------------------- /app/static/js/papers.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | document.addEventListener('DOMContentLoaded', () => { 4 | const createModal = document.getElementById('create-paper-modal'); 5 | const openCreateModalButton = document.getElementById('open-create-paper'); 6 | const closeCreateModalButton = createModal.querySelector('.close-button'); 7 | const createForm = document.getElementById('create-paper-form'); 8 | const papersContainer = document.getElementById('papers-container'); 9 | 10 | const filterCategoryInput = document.getElementById('filter-category'); 11 | const loadingIndicator = document.getElementById('loading-indicator'); 12 | 13 | 14 | let papersCache = []; 15 | let isFetching = false; 16 | 17 | 18 | const debounce = (func, delay) => { 19 | let debounceTimer; 20 | return function() { 21 | const context = this; 22 | const args = arguments; 23 | clearTimeout(debounceTimer); 24 | debounceTimer = setTimeout(() => func.apply(context, args), delay); 25 | } 26 | }; 27 | 28 | 29 | const openModal = (modal) => { 30 | modal.style.display = 'block'; 31 | }; 32 | 33 | 34 | const closeModal = (modal) => { 35 | modal.style.display = 'none'; 36 | }; 37 | 38 | 39 | openCreateModalButton.addEventListener('click', () => { 40 | openModal(createModal); 41 | }); 42 | 43 | 44 | closeCreateModalButton.addEventListener('click', () => { 45 | closeModal(createModal); 46 | }); 47 | 48 | 49 | window.addEventListener('click', (event) => { 50 | if (event.target == createModal) { 51 | closeModal(createModal); 52 | } 53 | }); 54 | 55 | 56 | const showError = (message) => { 57 | alert(message); 58 | }; 59 | 60 | const openPDF = (url) => { 61 | console.log(url) 62 | window.open(url, '_blank'); 63 | }; 64 | 65 | 66 | 67 | const renderPapers = async (papers) => { 68 | 69 | papersContainer.innerHTML = ''; 70 | 71 | 72 | for (const paper of papers) { 73 | const li = document.createElement('li'); 74 | li.classList.add('paper-item'); 75 | li.setAttribute('data-id', paper.id); 76 | 77 | try { 78 | const firstFile = paper.files.length > 0 ? paper.files[0] : null; 79 | const viewUrl = firstFile ? `/api/papers/view/${encodeURIComponent(paper.id)}/${encodeURIComponent(firstFile)}` : '#'; 80 | 81 | li.innerHTML = ` 82 | 85 |
86 |

${paper.title}

87 |

分类:${paper.category.toLowerCase() || '未分类'}

88 |
89 | 90 | `; 91 | 92 | if (firstFile) { 93 | const paperDetails = li.querySelector('.paper-details'); 94 | paperDetails.addEventListener('click', () => { 95 | openPDF(viewUrl); 96 | }); 97 | } 98 | 99 | papersContainer.appendChild(li); 100 | 101 | setTimeout(() => { 102 | li.classList.add('fade-in'); 103 | }, 10); 104 | } catch (error) { 105 | console.error(`获取论文ID: ${paper.id} 的文件夹路径时出错:`, error); 106 | 107 | li.innerHTML = ` 108 | 111 |
112 |

${paper.title}

113 |

分类:${paper.category || '未分类'}

114 |

无法获取文件夹路径

115 |
116 | 117 | `; 118 | papersContainer.appendChild(li); 119 | } 120 | } 121 | }; 122 | 123 | 124 | 125 | const updateCache = async () => { 126 | if (isFetching) return; 127 | isFetching = true; 128 | loadingIndicator.style.display = 'block'; 129 | 130 | try { 131 | const response = await fetch('/api/papers'); 132 | const data = await response.json(); 133 | if (data.status === 200) { 134 | papersCache = data.data; 135 | renderPapers(papersCache); 136 | } else { 137 | console.error('获取论文失败:', data); 138 | showError('无法获取论文列表。请稍后再试。'); 139 | } 140 | } catch (error) { 141 | console.error('获取论文出错:', error); 142 | showError('获取论文时发生错误。请检查网络连接并重试。'); 143 | } finally { 144 | isFetching = false; 145 | loadingIndicator.style.display = 'none'; 146 | } 147 | }; 148 | 149 | 150 | const refreshCache = async () => { 151 | await updateCache(); 152 | }; 153 | 154 | 155 | const filterAndRenderPapers = () => { 156 | const filter = filterCategoryInput.value.trim().toLowerCase(); 157 | if (filter === '') { 158 | renderPapers(papersCache); 159 | } else { 160 | const filteredPapers = papersCache.filter(paper => 161 | paper.category && paper.category.toLowerCase().includes(filter) || 162 | paper.title && paper.title.toLowerCase().includes(filter) 163 | ); 164 | renderPapers(filteredPapers); 165 | } 166 | }; 167 | 168 | 169 | const createPaper = async (formData) => { 170 | try { 171 | const response = await fetch('/api/papers', { 172 | method: 'POST', 173 | body: formData 174 | }); 175 | const data = await response.json(); 176 | if (data.status === 200) { 177 | await refreshCache(); 178 | closeModal(createModal); 179 | createForm.reset(); 180 | } else { 181 | console.error('创建论文失败:', data); 182 | showError('创建论文失败:' + (data.data.error || '未知错误')); 183 | } 184 | } catch (error) { 185 | console.error('创建论文出错:', error); 186 | showError('创建论文时发生错误。请检查网络连接并重试。'); 187 | } 188 | }; 189 | 190 | 191 | const deletePaper = async (paperId) => { 192 | if (confirm('确定要删除这篇论文吗?这将删除所有相关文件。')) { 193 | try { 194 | const response = await fetch(`/api/papers/${paperId}`, { 195 | method: 'DELETE' 196 | }); 197 | const data = await response.json(); 198 | if (data.status === 200) { 199 | await refreshCache(); 200 | } else { 201 | console.error('删除论文失败:', data); 202 | showError('删除论文失败:' + (data.data.error || '未知错误')); 203 | } 204 | } catch (error) { 205 | console.error('删除论文出错:', error); 206 | showError('删除论文时发生错误。请检查网络连接并重试。'); 207 | } 208 | } 209 | }; 210 | 211 | 212 | const toggleStarPaper = async (paperId, currentStarred) => { 213 | try { 214 | const response = await fetch(`/api/papers/${paperId}/star`, { 215 | method: 'PUT', 216 | headers: { 217 | 'Content-Type': 'application/json' 218 | }, 219 | body: JSON.stringify({ starred: !currentStarred }) 220 | }); 221 | const data = await response.json(); 222 | if (data.status === 200) { 223 | await refreshCache(); 224 | } else { 225 | console.error('更新星标状态失败:', data); 226 | showError('更新星标状态失败:' + (data.data.error || '未知错误')); 227 | } 228 | } catch (error) { 229 | console.error('更新星标状态出错:', error); 230 | showError('更新星标状态时发生错误。请检查网络连接并重试。'); 231 | } 232 | }; 233 | 234 | 235 | createForm.addEventListener('submit', (e) => { 236 | e.preventDefault(); 237 | const formData = new FormData(createForm); 238 | 239 | 240 | formData.delete('files'); 241 | 242 | createPaper(formData); 243 | }); 244 | 245 | 246 | papersContainer.addEventListener('click', (e) => { 247 | if (e.target.closest('.delete-button')) { 248 | const paperItem = e.target.closest('.paper-item'); 249 | const paperId = paperItem.getAttribute('data-id'); 250 | deletePaper(paperId); 251 | } 252 | 253 | if (e.target.closest('.star-button')) { 254 | const paperItem = e.target.closest('.paper-item'); 255 | const paperId = paperItem.getAttribute('data-id'); 256 | const isStarred = !paperItem.querySelector('.star-button').classList.contains('unstarred'); 257 | toggleStarPaper(paperId, isStarred); 258 | } 259 | }); 260 | 261 | 262 | filterCategoryInput.addEventListener('input', debounce(() => { 263 | filterAndRenderPapers(); 264 | }, 300)); 265 | 266 | 267 | updateCache(); 268 | }); 269 | -------------------------------------------------------------------------------- /app/static/js/plans.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | document.addEventListener('DOMContentLoaded', () => { 4 | const calendarEl = document.getElementById('calendar'); 5 | const plansList = document.getElementById('plans-list'); 6 | const addPlanForm = document.getElementById('add-plan-form'); 7 | let selectedDate = new Date().toISOString().split('T')[0]; 8 | 9 | 10 | const calendar = new FullCalendar.Calendar(calendarEl, { 11 | initialView: 'dayGridMonth', 12 | selectable: true, 13 | select: function(info) { 14 | selectedDate = info.startStr; 15 | fetchPlans(selectedDate); 16 | }, 17 | dateClick: function(info) { 18 | selectedDate = info.dateStr; 19 | fetchPlans(selectedDate); 20 | }, 21 | headerToolbar: { 22 | left: 'prev,next today', 23 | center: 'title', 24 | right: 'dayGridMonth,timeGridWeek,timeGridDay' 25 | }, 26 | height: 'auto', 27 | themeSystem: 'standard', 28 | 29 | 30 | }); 31 | 32 | calendar.render(); 33 | 34 | 35 | function fetchPlans(date = selectedDate) { 36 | fetch(`/api/plans/?date=${date}`) 37 | .then(response => response.json()) 38 | .then(data => { 39 | if (data.status === 200) { 40 | displayPlans(data.data); 41 | } else { 42 | console.error(data.data.error); 43 | } 44 | }) 45 | .catch(error => console.error('Error fetching plans:', error)); 46 | } 47 | 48 | 49 | function displayPlans(plans) { 50 | plansList.innerHTML = ''; 51 | if (plans.length === 0) { 52 | plansList.innerHTML = '

当天没有计划。

'; 53 | return; 54 | } 55 | plans.forEach(plan => { 56 | const planDiv = document.createElement('div'); 57 | planDiv.classList.add('plan-item', 'animate__animated', 'animate__fadeInUp'); 58 | 59 | const detailsDiv = document.createElement('div'); 60 | detailsDiv.classList.add('plan-details'); 61 | detailsDiv.innerHTML = `

${plan.title}

${plan.description}

`; 62 | 63 | const deleteBtn = document.createElement('button'); 64 | deleteBtn.classList.add('delete-button'); 65 | deleteBtn.innerHTML = '删除'; 66 | deleteBtn.addEventListener('click', () => deletePlan(plan.id)); 67 | 68 | planDiv.appendChild(detailsDiv); 69 | planDiv.appendChild(deleteBtn); 70 | plansList.appendChild(planDiv); 71 | }); 72 | } 73 | 74 | 75 | addPlanForm.addEventListener('submit', (e) => { 76 | e.preventDefault(); 77 | const title = document.getElementById('plan-title').value.trim(); 78 | const description = document.getElementById('plan-description').value.trim(); 79 | const date = selectedDate; 80 | 81 | if (!title || !date) { 82 | alert('标题和日期是必填项。'); 83 | return; 84 | } 85 | 86 | fetch('/api/plans/', { 87 | method: 'POST', 88 | headers: { 89 | 'Content-Type': 'application/json' 90 | }, 91 | body: JSON.stringify({ title, description, date }) 92 | }) 93 | .then(response => response.json()) 94 | .then(data => { 95 | if (data.status === 201) { 96 | alert('计划添加成功!'); 97 | fetchPlans(date); 98 | addPlanForm.reset(); 99 | } else { 100 | alert(`错误: ${data.data.error}`); 101 | } 102 | }) 103 | .catch(error => console.error('Error adding plan:', error)); 104 | }); 105 | 106 | 107 | function deletePlan(planId) { 108 | if (!confirm('确定要删除这个计划吗?')) return; 109 | 110 | fetch(`/api/plans/${planId}`, { 111 | method: 'DELETE' 112 | }) 113 | .then(response => response.json()) 114 | .then(data => { 115 | if (data.status === 200) { 116 | alert('计划删除成功!'); 117 | fetchPlans(selectedDate); 118 | } else { 119 | alert(`错误: ${data.data.error}`); 120 | } 121 | }) 122 | .catch(error => console.error('Error deleting plan:', error)); 123 | } 124 | 125 | 126 | fetchPlans(selectedDate); 127 | }); 128 | -------------------------------------------------------------------------------- /app/static/js/projects.js: -------------------------------------------------------------------------------- 1 | // static/js/projects.js 2 | 3 | document.addEventListener('DOMContentLoaded', () => { 4 | const projectList = document.querySelector('.project-list'); 5 | const openCreateProjectBtn = document.getElementById('open-create-project-btn'); 6 | const createModal = document.getElementById('create-project-modal'); 7 | const editModal = document.getElementById('edit-project-modal'); 8 | const closeButtons = document.querySelectorAll('.close-button'); 9 | const createProjectForm = document.getElementById('create-project-form'); 10 | const editProjectForm = document.getElementById('edit-project-form'); 11 | const searchInput = document.getElementById('search-projects'); 12 | 13 | // Utility function to generate a consistent random color based on a string 14 | function getRandomColor(str) { 15 | let hash = 0; 16 | for (let i = 0; i < str.length; i++) { 17 | hash = str.charCodeAt(i) + ((hash << 5) - hash); 18 | } 19 | const c = (hash & 0x00FFFFFF) 20 | .toString(16) 21 | .toUpperCase(); 22 | 23 | return "#" + "00000".substring(0, 6 - c.length) + c; 24 | } 25 | 26 | // 打开创建项目弹出窗口 27 | openCreateProjectBtn.addEventListener('click', () => { 28 | createModal.style.display = 'block'; 29 | }); 30 | 31 | // 打开编辑项目弹出窗口 32 | function openEditModal(project) { 33 | editModal.style.display = 'block'; 34 | document.getElementById('edit-project-original-name').value = project.name; 35 | document.getElementById('edit-project-name').value = project.name; 36 | document.getElementById('edit-project-description').value = project.description || ''; 37 | document.getElementById('edit-project-client').value = project.client; 38 | document.getElementById('edit-project-type').value = project.type; 39 | } 40 | 41 | // 关闭弹出窗口 42 | closeButtons.forEach(button => { 43 | button.addEventListener('click', () => { 44 | createModal.style.display = 'none'; 45 | editModal.style.display = 'none'; 46 | }); 47 | }); 48 | 49 | // 点击窗口外部关闭弹出窗口 50 | window.addEventListener('click', (event) => { 51 | if (event.target == createModal) { 52 | createModal.style.display = 'none'; 53 | } 54 | if (event.target == editModal) { 55 | editModal.style.display = 'none'; 56 | } 57 | }); 58 | 59 | // 加载项目列表,支持过滤 60 | function loadProjects(filter = '') { 61 | fetch('/api/projects/') 62 | .then(response => response.json()) 63 | .then(data => { 64 | if (data.status === 200) { 65 | displayProjects(data.data, filter); 66 | } else { 67 | console.error('获取项目列表时出错:', data.data); 68 | } 69 | }) 70 | .catch(error => console.error('错误:', error)); 71 | } 72 | 73 | // 显示项目卡牌 74 | function displayProjects(projects, filter) { 75 | projectList.innerHTML = ''; 76 | projects.forEach(project => { 77 | // 过滤项目 78 | if (filter) { 79 | const filterLower = filter.toLowerCase(); 80 | const matches = ( 81 | project.name.toLowerCase().includes(filterLower) || 82 | (project.description && project.description.toLowerCase().includes(filterLower)) || 83 | project.client.toLowerCase().includes(filterLower) || 84 | project.type.toLowerCase().includes(filterLower) 85 | ); 86 | if (!matches) { 87 | return; 88 | } 89 | } 90 | 91 | const card = document.createElement('div'); 92 | card.classList.add('project-card', 'animate__animated', 'animate__fadeInUp'); 93 | 94 | // Assign a random color based on project name as top-border 95 | const color = getRandomColor(project.name); 96 | card.style.borderTop = `5px solid ${color}`; 97 | 98 | // 点击项目卡牌跳转到项目详情页 99 | card.addEventListener('click', (e) => { 100 | // 如果点击的是编辑或删除按钮,则不跳转 101 | if (e.target.closest('.edit-project') || e.target.closest('.delete-project')) { 102 | return; 103 | } 104 | window.location.href = `/projects/${encodeURIComponent(project.name)}`; 105 | }); 106 | 107 | // 创建一个容器来放置编辑和删除按钮 108 | const actionButtons = document.createElement('div'); 109 | actionButtons.classList.add('action-buttons'); 110 | 111 | // 编辑按钮 112 | const editBtn = document.createElement('button'); 113 | editBtn.classList.add('edit-project'); 114 | editBtn.innerHTML = ''; 115 | editBtn.addEventListener('click', (e) => { 116 | e.stopPropagation(); // 阻止卡牌点击事件 117 | openEditModal(project); 118 | }); 119 | 120 | // 删除按钮 121 | const deleteBtn = document.createElement('button'); 122 | deleteBtn.classList.add('delete-project'); 123 | deleteBtn.innerHTML = ''; 124 | deleteBtn.addEventListener('click', (e) => { 125 | e.stopPropagation(); // 阻止卡牌点击事件 126 | deleteProject(project.name); 127 | }); 128 | 129 | actionButtons.appendChild(editBtn); 130 | actionButtons.appendChild(deleteBtn); 131 | 132 | const title = document.createElement('h3'); 133 | title.textContent = project.name; 134 | 135 | const description = document.createElement('p'); 136 | description.textContent = project.description || '暂无简介'; 137 | 138 | const client = document.createElement('p'); 139 | client.classList.add('client'); 140 | client.textContent = `甲方: ${project.client}`; 141 | 142 | const type = document.createElement('p'); 143 | type.classList.add('type'); 144 | type.textContent = `类型: ${project.type}`; 145 | 146 | card.appendChild(actionButtons); 147 | card.appendChild(title); 148 | card.appendChild(description); 149 | card.appendChild(client); 150 | card.appendChild(type); 151 | 152 | projectList.appendChild(card); 153 | }); 154 | } 155 | 156 | // 创建新项目 157 | createProjectForm.addEventListener('submit', (e) => { 158 | e.preventDefault(); 159 | const projectName = document.getElementById('project-name').value.trim(); 160 | const projectDescription = document.getElementById('project-description').value.trim(); 161 | const projectClient = document.getElementById('project-client').value.trim(); 162 | const projectType = document.getElementById('project-type').value.trim(); 163 | 164 | if (projectName === '' || projectClient === '' || projectType === '') { 165 | alert('请输入所有必填字段'); 166 | return; 167 | } 168 | 169 | fetch('/api/projects/', { 170 | method: 'POST', 171 | headers: { 172 | 'Content-Type': 'application/json' 173 | }, 174 | body: JSON.stringify({ 175 | name: projectName, 176 | description: projectDescription, 177 | client: projectClient, 178 | type: projectType 179 | }) 180 | }) 181 | .then(response => response.json()) 182 | .then(data => { 183 | if (data.status === 200) { 184 | createModal.style.display = 'none'; 185 | createProjectForm.reset(); 186 | loadProjects(searchInput.value.trim()); 187 | } else { 188 | alert(data.data.error || '创建项目失败'); 189 | } 190 | }) 191 | .catch(error => console.error('错误:', error)); 192 | }); 193 | 194 | // 编辑项目 195 | editProjectForm.addEventListener('submit', (e) => { 196 | e.preventDefault(); 197 | const originalName = document.getElementById('edit-project-original-name').value.trim(); 198 | const projectName = document.getElementById('edit-project-name').value.trim(); 199 | const projectDescription = document.getElementById('edit-project-description').value.trim(); 200 | const projectClient = document.getElementById('edit-project-client').value.trim(); 201 | const projectType = document.getElementById('edit-project-type').value.trim(); 202 | 203 | if (projectName === '' || projectClient === '' || projectType === '') { 204 | alert('请输入所有必填字段'); 205 | return; 206 | } 207 | 208 | fetch(`/api/projects/${encodeURIComponent(originalName)}`, { 209 | method: 'PUT', 210 | headers: { 211 | 'Content-Type': 'application/json' 212 | }, 213 | body: JSON.stringify({ 214 | name: projectName, 215 | description: projectDescription, 216 | client: projectClient, 217 | type: projectType 218 | }) 219 | }) 220 | .then(response => response.json()) 221 | .then(data => { 222 | if (data.status === 200) { 223 | editModal.style.display = 'none'; 224 | editProjectForm.reset(); 225 | loadProjects(searchInput.value.trim()); 226 | } else { 227 | alert(data.data.error || '编辑项目失败'); 228 | } 229 | }) 230 | .catch(error => console.error('错误:', error)); 231 | }); 232 | 233 | // 删除项目 234 | function deleteProject(projectName) { 235 | if (!confirm(`确定要删除项目 "${projectName}" 吗?`)) { 236 | return; 237 | } 238 | 239 | fetch(`/api/projects/${encodeURIComponent(projectName)}`, { 240 | method: 'DELETE' 241 | }) 242 | .then(response => response.json()) 243 | .then(data => { 244 | if (data.status === 200) { 245 | loadProjects(searchInput.value.trim()); 246 | } else { 247 | alert(data.data.error || '删除项目失败'); 248 | } 249 | }) 250 | .catch(error => console.error('错误:', error)); 251 | } 252 | 253 | // 搜索项目 254 | searchInput.addEventListener('input', () => { 255 | const filter = searchInput.value.trim(); 256 | loadProjects(filter); 257 | }); 258 | 259 | // 初始化加载 260 | loadProjects(); 261 | }); 262 | -------------------------------------------------------------------------------- /app/static/js/scripts.js: -------------------------------------------------------------------------------- 1 | 2 | document.addEventListener('DOMContentLoaded', () => { 3 | const sidebar = document.querySelector('.sidebar'); 4 | const toggleButton = document.createElement('button'); 5 | 6 | toggleButton.innerHTML = ''; 7 | toggleButton.classList.add('toggle-button'); 8 | sidebar.insertBefore(toggleButton, sidebar.firstChild); 9 | 10 | toggleButton.addEventListener('click', () => { 11 | sidebar.classList.toggle('collapsed'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /app/static/js/view_idea.js: -------------------------------------------------------------------------------- 1 | 2 | document.addEventListener('DOMContentLoaded', () => { 3 | const ideaId = window.location.pathname.split('/').pop(); 4 | const editForm = document.getElementById('edit-idea-form'); 5 | const relatedPapersContainer = document.getElementById('related-papers-container'); 6 | const addRelatedPaperForm = document.getElementById('add-related-paper-form'); 7 | 8 | const fetchIdeaDetail = () => { 9 | fetch(`/api/ideas/${ideaId}`) 10 | .then(response => response.json()) 11 | .then(data => { 12 | if (data.status === 200) { 13 | const idea = data.data; 14 | document.getElementById('edit-title').value = idea.title; 15 | document.getElementById('edit-description').value = idea.description; 16 | document.getElementById('edit-background').value = idea.background || ''; 17 | document.getElementById('edit-motivation').value = idea.motivation || ''; 18 | document.getElementById('edit-challenge').value = idea.challenge || ''; 19 | document.getElementById('edit-method').value = idea.method || ''; 20 | document.getElementById('edit-experiment').value = idea.experiment || ''; 21 | document.getElementById('edit-innovation').value = idea.innovation || ''; 22 | 23 | const colors = [ 24 | '#FF5733', '#33FF57', '#3357FF', '#FF33A1', '#FFD700', '#9B59B6', '#1F77B4', '#E74C3C', '#2ECC71', '#F39C12' ]; 25 | 26 | function getColorByTitle(title) { 27 | let hash = 0; 28 | for (let i = 0; i < title.length; i++) { 29 | hash = (hash << 5) - hash + title.charCodeAt(i); 30 | hash = hash & hash; } 31 | 32 | const index = Math.abs(hash) % colors.length; 33 | return colors[index]; 34 | } 35 | 36 | relatedPapersContainer.innerHTML = ''; 37 | idea.related_papers.forEach(paper => { 38 | const li = document.createElement('li'); 39 | 40 | let link = paper.link; 41 | 42 | if (!link.startsWith('http://') && !link.startsWith('https://')) { 43 | link = 'https://' + link; 44 | } 45 | 46 | li.innerHTML = ` 47 | 53 | `; 54 | const color = getColorByTitle(paper.title); 55 | li.style.borderLeft = `15px solid ${color}`; 56 | 57 | relatedPapersContainer.appendChild(li); 58 | }); 59 | } else { 60 | console.error('获取想法详情失败:', data); 61 | alert('获取想法详情失败。'); 62 | } 63 | }) 64 | .catch(error => { 65 | console.error('获取想法详情出错:', error); 66 | alert('获取想法详情出错。'); 67 | }); 68 | }; 69 | 70 | const updateIdeaDetail = (details) => { 71 | fetch(`/api/ideas/${ideaId}`, { 72 | method: 'PUT', 73 | headers: { 74 | 'Content-Type': 'application/json' 75 | }, 76 | body: JSON.stringify(details) 77 | }) 78 | .then(response => response.json()) 79 | .then(data => { 80 | if (data.status === 200) { 81 | alert('想法更新成功。'); 82 | } else { 83 | alert(data.data.error || '更新想法失败。'); 84 | } 85 | }) 86 | .catch(error => { 87 | console.error('更新想法出错:', error); 88 | alert('更新想法出错。'); 89 | }); 90 | }; 91 | 92 | const addRelatedPaper = (paperData) => { 93 | fetch(`/api/ideas/${ideaId}/related_papers`, { 94 | method: 'POST', 95 | headers: { 96 | 'Content-Type': 'application/json' 97 | }, 98 | body: JSON.stringify(paperData) 99 | }) 100 | .then(response => response.json()) 101 | .then(data => { 102 | fetchIdeaDetail(); 103 | addRelatedPaperForm.reset(); 104 | }) 105 | .catch(error => { 106 | console.error('添加关联论文出错:', error); 107 | alert('添加关联论文出错。'); 108 | }); 109 | }; 110 | 111 | const deleteRelatedPaper = (paperId) => { 112 | fetch(`/api/ideas/${ideaId}/related_papers/${paperId}`, { 113 | method: 'DELETE' 114 | }) 115 | .then(response => response.json()) 116 | .then(data => { 117 | if (data.status === 200) { 118 | fetchIdeaDetail(); 119 | } else { 120 | alert(data.data.error || '删除关联论文失败。'); 121 | } 122 | }) 123 | .catch(error => { 124 | console.error('删除关联论文出错:', error); 125 | alert('删除关联论文出错。'); 126 | }); 127 | }; 128 | 129 | editForm.addEventListener('submit', (e) => { 130 | e.preventDefault(); 131 | const details = { 132 | title: document.getElementById('edit-title').value.trim(), 133 | description: document.getElementById('edit-description').value.trim(), 134 | background: document.getElementById('edit-background').value.trim(), 135 | motivation: document.getElementById('edit-motivation').value.trim(), 136 | challenge: document.getElementById('edit-challenge').value.trim(), 137 | method: document.getElementById('edit-method').value.trim(), 138 | experiment: document.getElementById('edit-experiment').value.trim(), 139 | innovation: document.getElementById('edit-innovation').value.trim(), 140 | }; 141 | if (details.title && details.description) { 142 | updateIdeaDetail(details); 143 | } else { 144 | alert('标题和描述均为必填项。'); 145 | } 146 | }); 147 | 148 | addRelatedPaperForm.addEventListener('submit', (e) => { 149 | e.preventDefault(); 150 | const paperData = { 151 | title: addRelatedPaperForm.querySelector('input[name="title"]').value.trim(), 152 | content: addRelatedPaperForm.querySelector('input[name="content"]').value.trim(), 153 | link: addRelatedPaperForm.querySelector('input[name="link"]').value.trim(), 154 | }; 155 | if (paperData.title) { 156 | addRelatedPaper(paperData); 157 | } else { 158 | alert('关联论文标题为必填项。'); 159 | } 160 | }); 161 | 162 | relatedPapersContainer.addEventListener('click', (e) => { 163 | if (e.target.closest('.delete-related-paper')) { 164 | const paperId = e.target.closest('.delete-related-paper').dataset.paperId; 165 | if (confirm('确定要删除这个关联论文吗?')) { 166 | deleteRelatedPaper(paperId); 167 | } 168 | } 169 | }); 170 | 171 | fetchIdeaDetail(); 172 | }); 173 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 拉斐尔 - {% block title %}{% endblock %} 8 | 9 | 10 | 11 | 12 | 13 | {% block extra_css %}{% endblock %} 14 | 15 | 16 |
17 | 32 |
33 | {% block content %}{% endblock %} 34 |
35 |
36 | 37 | {% block extra_js %}{% endblock %} 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/templates/ideas.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block title %}想法{% endblock %} 5 | 6 | {% block extra_css %} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |

想法

13 | 14 | 15 | 26 | 27 |
28 |
    29 |
30 |
31 |
32 | {% endblock %} 33 | 34 | {% block extra_js %} 35 | 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block title %}主页{% endblock %} 5 | 6 | {% block extra_css %} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 | 13 |
14 |
15 |

拉斐尔:

16 |
17 |
18 |

“您好,主人。愿您今日有所收获。”

19 |
20 |
21 | 22 |
23 |
24 |
25 | --:--:-- 26 | --/--/---- 27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 | 加载中... 35 | 加载中... 36 |
37 |
38 |
39 |
40 |
41 | 42 | 50 | 51 |
52 |
53 |
54 |
55 |
    56 |
57 | 58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 66 |
67 | 68 | 78 | 79 | 80 | 81 |
82 |

© 2024 Lizhe Chen. Licensed under the MIT License. Developed by Lizhe Chen, Tsinghua University.

83 |
84 | {% endblock %} 85 | -------------------------------------------------------------------------------- /app/templates/notes.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block title %}笔记{% endblock %} 5 | 6 | {% block content %} 7 |
8 |

笔记

9 |
10 | 11 | 12 | 13 |
当前目录: /
14 |
15 | 22 |
23 |
24 |
25 | 26 |
27 |
    28 |
  • 重命名
  • 29 |
  • 删除
  • 30 |
31 |
32 | 33 | 42 | 43 | 44 | 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /app/templates/papers.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block title %}论文管理{% endblock %} 5 | 6 | {% block extra_css %} 7 | 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

论文

13 | 14 | 15 |
16 |

筛选:

17 | 18 |
19 | 20 |
21 |
22 |
    23 |
24 |
25 | 26 | 29 |
30 | 31 | 50 | {% endblock %} 51 | 52 | {% block extra_js %} 53 | 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /app/templates/plans.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | 5 | {% block title %}计划{% endblock %} 6 | 7 | {% block extra_css %} 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 |

计划

15 |
16 |
17 | 18 |

当天的计划

19 |
20 |

请选择一个日期查看计划。

21 |
22 |
23 | 24 |
25 |

添加计划

26 |
27 | 28 | 29 | 30 |
31 |
32 |
33 | {% endblock %} 34 | 35 | {% block extra_js %} 36 | 37 | 38 | 39 | 185 | {% endblock %} 186 | -------------------------------------------------------------------------------- /app/templates/projects.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | 5 | {% block title %}项目{% endblock %} 6 | 7 | {% block extra_css %} 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 |
15 |

项目

16 |
17 | 18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 | 47 | 48 | 49 | 72 | {% endblock %} 73 | 74 | {% block extra_js %} 75 | 76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /app/templates/view_idea.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block title %}查看想法{% endblock %} 5 | 6 | {% block extra_css %} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |
13 |
14 |
15 | 返回想法列表 16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 | 47 | 48 |
49 | 55 |
56 |
57 |
58 | {% endblock %} 59 | 60 | {% block extra_js %} 61 | 101 | 102 | {% endblock %} 103 | -------------------------------------------------------------------------------- /app/templates/view_note.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}查看笔记{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 |
当前笔记: /
17 | 18 |
19 | 20 |
21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 | 35 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /app/templates/view_project.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | 5 | {% block title %}项目详情 - {{ project_name }}{% endblock %} 6 | 7 | {% block extra_css %} 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 |

项目: {{ project_name }}

15 | 16 |
17 |
18 |
19 |
20 |
21 | 25 | 26 |
27 | 28 | 29 |
30 |
31 | 32 |
33 |
    34 | 35 |
36 |
37 |
38 | 39 | 40 |
41 |
42 |
43 | 44 | 45 |
46 | 51 | 52 |
53 |
54 |
55 | 56 |
57 | 62 |
63 |
64 |
65 | 66 | 67 |
68 |
69 | 70 |
71 |
72 | 73 | 74 | 75 | 76 |
77 |
78 |
    79 | 80 |
81 |
82 |
83 | 84 | 85 |
86 |
87 |
88 |
89 |
90 |
91 | {% endblock %} 92 | 93 | {% block extra_js %} 94 | 95 | 96 | 97 | {% endblock %} 98 | -------------------------------------------------------------------------------- /app/utils/__pycache__/helper.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/app/utils/__pycache__/helper.cpython-312.pyc -------------------------------------------------------------------------------- /app/utils/helper.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import subprocess 3 | from plyer import notification 4 | import logging 5 | import os 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | def format_response(data, status=200): 10 | return { 11 | 'status': status, 12 | 'data': data 13 | } 14 | 15 | def notify(title, message): 16 | current_os = platform.system() 17 | try: 18 | if current_os == "Windows": 19 | # 使用 plyer 进行 Windows 通知 20 | notification.notify( 21 | title=title, 22 | message=message, 23 | app_name='Raphael', 24 | # 可选:添加图标路径 25 | # app_icon=os.path.join('app', 'static', 'images', 'Raphael.ico') 26 | ) 27 | elif current_os == "Darwin": 28 | # 使用 AppleScript 进行 macOS 通知 29 | script = f'display notification "{message}" with title "{title}"' 30 | subprocess.run(["osascript", "-e", script]) 31 | else: 32 | # 使用 plyer 作为其他操作系统的备用 33 | notification.notify( 34 | title=title, 35 | message=message, 36 | app_name='Raphael' 37 | ) 38 | except Exception as e: 39 | logger.error(f"Failed to send notification: {e}") -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | 7 | class Config: 8 | SECRET_KEY = os.environ.get('SECRET_KEY', 'your_secret_key') 9 | PORT = 21823 10 | DEBUG = False 11 | 12 | REMOTE_DB = { 13 | 'ADDRESS': os.environ.get('DB_ADDRESS'), 14 | 'USERNAME': os.environ.get('DB_USERNAME'), 15 | 'PASSWORD': os.environ.get('DB_PASSWORD'), 16 | 'DB_NAME': os.environ.get('DB_NAME'), 17 | } 18 | 19 | USE_REMOTE_DB = False 20 | NOTIFICATION_API_KEY = "your-secure-api-key" 21 | 22 | # 获取操作系统平台 23 | platform = sys.platform 24 | 25 | # 获取"我的文档"目录路径,适配不同平台 26 | if platform == "win32": # Windows 系统 27 | DOCUMENTS_DIR = str(Path.home() / "Documents") 28 | elif platform == "darwin": # macOS 系统 29 | DOCUMENTS_DIR = str(Path.home() / "Documents") 30 | else: # Linux 系统 31 | DOCUMENTS_DIR = str(Path.home() / "Documents") 32 | 33 | # 创建 Raphael 文件夹路径 34 | RAPHAEL_DIR = os.path.join(DOCUMENTS_DIR, "Raphael") 35 | 36 | BASE_DIR = RAPHAEL_DIR 37 | 38 | # 确保 Raphael 文件夹存在 39 | os.makedirs(BASE_DIR, exist_ok=True) 40 | 41 | # 创建数据文件夹 42 | NOTES_FOLDER = os.path.join(BASE_DIR, 'notes') 43 | PAPERS_FOLDER = os.path.join(BASE_DIR, 'papers') 44 | PROJECTS_FOLDER = os.path.join(BASE_DIR, 'projects') 45 | 46 | os.makedirs(NOTES_FOLDER, exist_ok=True) 47 | os.makedirs(PAPERS_FOLDER, exist_ok=True) 48 | os.makedirs(PROJECTS_FOLDER, exist_ok=True) 49 | 50 | # 本地数据库路径应该指向"我的文档"下的 Raphael 文件夹 51 | LOCAL_DB_URI = f"sqlite:///{os.path.join(BASE_DIR, 'database.db')}" 52 | 53 | # 配置数据库 URI 54 | if USE_REMOTE_DB and all(REMOTE_DB.values()): 55 | SQLALCHEMY_DATABASE_URI = f"postgresql://{REMOTE_DB['USERNAME']}:{REMOTE_DB['PASSWORD']}@{REMOTE_DB['ADDRESS']}/{REMOTE_DB['DB_NAME']}" 56 | else: 57 | SQLALCHEMY_DATABASE_URI = LOCAL_DB_URI 58 | 59 | SQLALCHEMY_TRACK_MODIFICATIONS = False 60 | 61 | # 快捷键配置 62 | SHORTCUT_KEY = os.environ.get('SHORTCUT_KEY', 'alt+r') # 默认快捷键为 Alt + R 63 | -------------------------------------------------------------------------------- /init_db.py: -------------------------------------------------------------------------------- 1 | # init_db.py 2 | 3 | from app import create_app, db 4 | 5 | app = create_app() 6 | 7 | with app.app_context(): 8 | db.create_all() 9 | print("数据库初始化完成。") 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/requirements.txt -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import os 3 | import sys 4 | import logging 5 | from config import Config # 提前导入 Config 以访问 Config.debug 6 | 7 | # Conditionally import winreg for Windows 8 | if platform.system() == "Windows": 9 | import winreg 10 | 11 | import threading 12 | import pystray 13 | from app.utils.helper import notify 14 | from pystray import MenuItem as item 15 | from PIL import Image 16 | import webbrowser 17 | 18 | from app import create_app, db 19 | from plyer import notification 20 | 21 | # For keyboard events 22 | import keyboard # Consider using pynput for cross-platform support 23 | 24 | # 根据 Config.debug 配置日志记录 25 | if Config.DEBUG: 26 | logging.basicConfig( 27 | level=logging.INFO, 28 | format='%(asctime)s [%(levelname)s] %(message)s', 29 | handlers=[ 30 | logging.FileHandler("raphael.log"), 31 | logging.StreamHandler() 32 | ] 33 | ) 34 | else: 35 | # 禁用所有日志记录 36 | logging.basicConfig(level=logging.CRITICAL) 37 | logging.disable(logging.CRITICAL) 38 | 39 | logger = logging.getLogger(__name__) 40 | 41 | app = create_app() 42 | 43 | def create_image(): 44 | current_os = platform.system() 45 | if current_os == "Windows": 46 | icon_filename = 'Raphael.ico' 47 | elif current_os == "Darwin": 48 | icon_filename = 'Raphael.png' 49 | else: 50 | icon_filename = 'Raphael.png' 51 | 52 | icon_path = os.path.join(os.path.dirname(__file__), 'app', 'static', 'images', icon_filename) 53 | if not os.path.exists(icon_path): 54 | logger.warning(f"Icon file not found: {icon_path}. Using default icon.") 55 | image = Image.new('RGB', (64, 64), color='blue') 56 | else: 57 | image = Image.open(icon_path) 58 | return image 59 | 60 | def on_quit(icon, item): 61 | logger.info("Quitting Raphael.") 62 | icon.stop() 63 | exit_event.set() 64 | sys.exit() 65 | 66 | def open_webpage(icon, item): 67 | logger.info("Opening Raphael webpage.") 68 | webbrowser.open(f"http://127.0.0.1:{Config.PORT}") 69 | 70 | def on_double_click(icon, mouse_event): 71 | logger.info("Icon double-clicked, opening webpage.") 72 | webbrowser.open(f"http://127.0.0.1:{Config.PORT}") 73 | 74 | def initialize_registry(): 75 | current_os = platform.system() 76 | if current_os != "Windows": 77 | logger.info("Registry initialization skipped on non-Windows OS.") 78 | return 79 | try: 80 | registry = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) 81 | key_path = r"Software\LizheChen\Raphael" 82 | 83 | # 检查键是否存在 84 | try: 85 | key = winreg.OpenKey(registry, key_path, 0, winreg.KEY_READ) 86 | winreg.CloseKey(key) 87 | except FileNotFoundError: 88 | # 如果不存在,创建键并设置默认值 89 | key = winreg.CreateKey(registry, key_path) 90 | winreg.SetValueEx(key, "EnableShortcuts", 0, winreg.REG_SZ, "True") 91 | winreg.CloseKey(key) 92 | logger.info("Registry key created with default settings.") 93 | except Exception as e: 94 | logger.error(f"Error initializing registry: {e}") 95 | 96 | def get_shortcut_status(): 97 | current_os = platform.system() 98 | if current_os != "Windows": 99 | logger.info("Shortcut status retrieval skipped on non-Windows OS.") 100 | return True 101 | try: 102 | registry = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) 103 | key = winreg.OpenKey(registry, r"Software\LizheChen\Raphael", 0, winreg.KEY_READ) 104 | value, _ = winreg.QueryValueEx(key, "EnableShortcuts") 105 | winreg.CloseKey(key) 106 | return value == "True" 107 | except (FileNotFoundError, OSError) as e: 108 | logger.warning(f"Failed to get shortcut status: {e}. Defaulting to True.") 109 | return True 110 | 111 | def set_shortcut_status(enable=True): 112 | current_os = platform.system() 113 | if current_os != "Windows": 114 | logger.info("Setting shortcut status skipped on non-Windows OS.") 115 | return 116 | try: 117 | registry = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) 118 | key = winreg.OpenKey(registry, r"Software\LizheChen\Raphael", 0, winreg.KEY_SET_VALUE) 119 | winreg.SetValueEx(key, "EnableShortcuts", 0, winreg.REG_SZ, "True" if enable else "False") 120 | winreg.CloseKey(key) 121 | logger.info(f"Shortcut status set to {'enabled' if enable else 'disabled'}.") 122 | except Exception as e: 123 | logger.error(f"Failed to set shortcut status: {e}") 124 | 125 | def toggle_shortcut(icon, item): 126 | current_status = get_shortcut_status() 127 | set_shortcut_status(not current_status) 128 | notify("Raphael", f"Shortcuts {'Enabled' if not current_status else 'Disabled'}.") 129 | icon.update_menu() 130 | 131 | def create_tray(): 132 | icon = pystray.Icon("Raphael", create_image(), menu=( 133 | item("访问", open_webpage), 134 | item("启用快捷键", toggle_shortcut, checked=lambda item: get_shortcut_status()), 135 | item("退出", on_quit), 136 | )) 137 | 138 | # 设置双击事件处理器 139 | icon.on_double_click = on_double_click 140 | 141 | # 运行托盘图标 142 | icon.run() 143 | 144 | def run_flask(): 145 | try: 146 | # 初始化数据库(创建表) 147 | with app.app_context(): 148 | db.create_all() 149 | logger.info("Database initialized.") 150 | 151 | notify("Raphael", "Raphael is running.") 152 | app.run(host="localhost", port=Config.PORT, debug=False) 153 | except Exception as e: 154 | logger.error(f"Flask application failed to start: {e}") 155 | notify("Raphael Error", "Failed to start the Flask application.") 156 | exit_event.set() 157 | 158 | pressed_keys = set() 159 | has_opened_webpage = False 160 | 161 | def on_key_event(keyboard_event): 162 | global has_opened_webpage 163 | 164 | if get_shortcut_status(): 165 | shortcut_keys = Config.SHORTCUT_KEY.split('+') 166 | if len(shortcut_keys) == 2: 167 | modifier, key = shortcut_keys 168 | if (keyboard_event.name == key.lower() and keyboard.is_pressed(modifier.lower())) and not has_opened_webpage: 169 | logger.info(f"Pressed {Config.SHORTCUT_KEY}, opening webpage.") 170 | webbrowser.open(f"http://127.0.0.1:{Config.PORT}") 171 | has_opened_webpage = True 172 | 173 | if keyboard_event.event_type == keyboard.KEY_UP: 174 | if keyboard_event.name in [mod.lower() for mod in Config.SHORTCUT_KEY.split('+')]: 175 | has_opened_webpage = False 176 | 177 | def start_key_listener(): 178 | try: 179 | keyboard.hook(on_key_event) 180 | keyboard.wait() 181 | except Exception as e: 182 | logger.error(f"Key listener encountered an error: {e}") 183 | notify("Raphael Error", "Key listener encountered an error.") 184 | exit_event.set() 185 | 186 | exit_event = threading.Event() 187 | 188 | def safe_run(func): 189 | def wrapper(*args, **kwargs): 190 | try: 191 | func(*args, **kwargs) 192 | except Exception as e: 193 | logger.error(f"Exception in {func.__name__}: {e}") 194 | notify("Raphael Error", f"Exception in {func.__name__}: {e}") 195 | exit_event.set() 196 | return wrapper 197 | 198 | @safe_run 199 | def run_flask_safe(): 200 | run_flask() 201 | 202 | @safe_run 203 | def start_key_listener_safe(): 204 | start_key_listener() 205 | 206 | if __name__ == '__main__': 207 | # 初始化注册表 208 | initialize_registry() 209 | 210 | # 启动 Flask 应用线程 211 | flask_thread = threading.Thread(target=run_flask_safe) 212 | flask_thread.daemon = True 213 | flask_thread.start() 214 | 215 | # 启动快捷键监听线程 216 | key_listener_thread = threading.Thread(target=start_key_listener_safe) 217 | key_listener_thread.daemon = True 218 | key_listener_thread.start() 219 | 220 | # 启动系统托盘 221 | try: 222 | create_tray() 223 | except KeyboardInterrupt: 224 | logger.info("Shutting down Raphael.") 225 | exit_event.set() 226 | sys.exit() 227 | -------------------------------------------------------------------------------- /run.spec: -------------------------------------------------------------------------------- 1 | # run.spec 2 | 3 | from PyInstaller.utils.hooks import collect_data_files, collect_submodules 4 | import os 5 | 6 | # Collect all data files in the 'app' package 7 | app_datas = collect_data_files('app') 8 | 9 | # Manually specify additional data files not covered by collect_data_files 10 | additional_datas = [ 11 | ('app/static', 'app/static'), 12 | ('app/templates', 'app/templates'), 13 | # You can omit ('app/__init__.py', 'app/__init__.py') since it's part of the package 14 | # Similarly, run.py and config.py are already included as scripts or modules 15 | ] 16 | 17 | # Combine all data files 18 | all_datas = app_datas + additional_datas 19 | 20 | # Collect all submodules of 'plyer.platforms' to ensure dynamic imports are included 21 | plyer_submodules = collect_submodules('plyer.platforms') 22 | 23 | # Collect all submodules of 'pystray' to ensure platform-specific backends are included 24 | pystray_submodules = collect_submodules('pystray') 25 | 26 | # Define hidden imports 27 | hidden_imports = [ 28 | 'plyer.platforms.win.notification', # Ensure Windows notification backend is included 29 | 'pystray._win32', # pystray Windows backend 30 | 'pystray._darwin', # pystray macOS backend (if needed) 31 | 'pystray._xorg', # pystray X11 backend (if needed) 32 | # Add any other hidden imports if necessary 33 | ] 34 | 35 | # Alternatively, include all submodules collected above 36 | # hidden_imports = plyer_submodules + pystray_submodules 37 | 38 | # Initialize PyInstaller Analysis 39 | a = Analysis( 40 | ['run.py'], # Your main script 41 | pathex=['.'], # Path to search for imports 42 | binaries=[], 43 | datas=all_datas, # Data files to include 44 | hiddenimports=hidden_imports, # Hidden imports to include 45 | hookspath=[], # Custom hook paths 46 | runtime_hooks=[], # Runtime hooks 47 | excludes=[] # Exclude any unnecessary modules 48 | ) 49 | 50 | # Create the PYZ archive 51 | pyz = PYZ(a.pure, a.zipped_data) 52 | 53 | # Define the EXE 54 | exe = EXE( 55 | pyz, 56 | a.scripts, 57 | a.binaries, 58 | a.zipfiles, 59 | a.datas, 60 | [], 61 | name='run', 62 | debug=False, 63 | bootloader_ignore_signals=False, 64 | strip=True, 65 | upx=True, 66 | console=False, # Hide the console window 67 | icon=os.path.join('app', 'static', 'images', 'Raphael.ico') # Use .ico for Windows compatibility 68 | ) 69 | 70 | # Note: 71 | # - Ensure that 'Raphael.ico' exists in 'app/static/images/'. PyInstaller prefers .ico files for Windows icons. 72 | # - If you're packaging for macOS, you might want to use a .icns file instead and adjust the icon path accordingly. 73 | -------------------------------------------------------------------------------- /screenshots/ideas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/screenshots/ideas.jpg -------------------------------------------------------------------------------- /screenshots/index.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/screenshots/index.jpg -------------------------------------------------------------------------------- /screenshots/notes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/screenshots/notes.jpg -------------------------------------------------------------------------------- /screenshots/notes2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/screenshots/notes2.jpg -------------------------------------------------------------------------------- /screenshots/overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/screenshots/overview.jpg -------------------------------------------------------------------------------- /screenshots/papers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/screenshots/papers.jpg -------------------------------------------------------------------------------- /screenshots/plans.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/screenshots/plans.jpg -------------------------------------------------------------------------------- /screenshots/projects.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChenlizheMe/Raphael-Assistant/1c07cd51bcfa69a906ac588d74c7a5f50712d142/screenshots/projects.jpg --------------------------------------------------------------------------------