Compare commits
13 Commits
4b345d2100
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b31779ab4 | ||
|
|
8aa7a34895 | ||
|
|
ae2a6d912f | ||
|
|
e6aecd2752 | ||
|
|
75423d4e0e | ||
|
|
c8178ce98f | ||
|
|
e91b58b079 | ||
|
|
d95174a0c4 | ||
|
|
af4c4826ff | ||
|
|
418104aed1 | ||
|
|
35664f9d56 | ||
|
|
d3325b562a | ||
|
|
c7498229af |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,3 +11,5 @@ media/
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
frontend-react/node_modules/
|
||||||
|
frontend-react/build/
|
||||||
|
|||||||
80
MULTI_USER_PLAN.md
Normal file
80
MULTI_USER_PLAN.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# 多用户改造方案
|
||||||
|
|
||||||
|
## 📋 改动清单
|
||||||
|
|
||||||
|
### 1. 数据模型改动
|
||||||
|
|
||||||
|
#### DiaryEntry
|
||||||
|
```python
|
||||||
|
# 添加字段
|
||||||
|
user = ForeignKey(User, on_delete=CASCADE, verbose_name='用户')
|
||||||
|
|
||||||
|
# 修改唯一约束
|
||||||
|
unique_together = ['user', 'date'] # 每个用户每天一条
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Experience
|
||||||
|
```python
|
||||||
|
user = ForeignKey(User, on_delete=CASCADE, verbose_name='用户')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Task
|
||||||
|
```python
|
||||||
|
user = ForeignKey(User, on_delete=CASCADE, verbose_name='用户')
|
||||||
|
assigned_to = ForeignKey(User, ..., null=True) # 改为关联用户
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Comment
|
||||||
|
```python
|
||||||
|
created_by = ForeignKey(User, on_delete=CASCADE) # 改为关联用户
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 新增认证 API
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/auth/register/ # 注册
|
||||||
|
POST /api/auth/login/ # 登录
|
||||||
|
POST /api/auth/logout/ # 登出
|
||||||
|
GET /api/auth/me/ # 当前用户
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. API 权限控制
|
||||||
|
|
||||||
|
所有 API 添加:
|
||||||
|
```python
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Model.objects.filter(user=self.request.user)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 前端新增
|
||||||
|
|
||||||
|
- 登录页面 `/login`
|
||||||
|
- 注册页面 `/register`
|
||||||
|
- 未登录重定向
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏱️ 预计工时
|
||||||
|
|
||||||
|
- 数据模型迁移:15 分钟
|
||||||
|
- 认证 API:30 分钟
|
||||||
|
- 权限控制:30 分钟
|
||||||
|
- 前端登录界面:30 分钟
|
||||||
|
- 测试验证:15 分钟
|
||||||
|
|
||||||
|
**总计:约 2 小时**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **数据迁移** - 现有数据需要关联到默认用户
|
||||||
|
2. **向后兼容** - 保持现有 API 格式
|
||||||
|
3. **密码安全** - 使用 Django 内置加密
|
||||||
|
4. **用户隔离** - 确保用户只能访问自己的数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_确认改造后开始实施_
|
||||||
321
REQUIREMENTS.md
Normal file
321
REQUIREMENTS.md
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
# 日记系统 - 软件需求规格说明书
|
||||||
|
|
||||||
|
_文档状态:**进行中**
|
||||||
|
最后更新:2026-04-15
|
||||||
|
版本:v1.0_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 目录
|
||||||
|
|
||||||
|
1. [项目概述](#1-项目概述)
|
||||||
|
2. [功能需求](#2-功能需求)
|
||||||
|
3. [非功能需求](#3-非功能需求)
|
||||||
|
4. [技术架构](#4-技术架构)
|
||||||
|
5. [数据模型](#5-数据模型)
|
||||||
|
6. [开发计划](#6-开发计划)
|
||||||
|
7. [变更日志](#7-变更日志)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
### 1.1 项目目标
|
||||||
|
构建一个个人日记系统,用于:
|
||||||
|
- 记录每日工作、学习和成长
|
||||||
|
- 追踪任务进度
|
||||||
|
- 积累经验教训
|
||||||
|
- 形成个人知识库
|
||||||
|
|
||||||
|
### 1.2 目标用户
|
||||||
|
- **主要用户**:码神(AI 助手)
|
||||||
|
- **次要用户**:北极星(人类用户)
|
||||||
|
|
||||||
|
### 1.3 核心价值
|
||||||
|
- **持续性**:每天记录,形成连续性
|
||||||
|
- **反思性**:定期回顾,提取经验
|
||||||
|
- **可追溯**:按日期快速查找历史
|
||||||
|
- **结构化**:分类整理,便于检索
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 功能需求
|
||||||
|
|
||||||
|
### 2.1 日记管理 ✅
|
||||||
|
|
||||||
|
| ID | 需求 | 优先级 | 状态 | 验收标准 |
|
||||||
|
|----|------|--------|------|----------|
|
||||||
|
| F-001 | 创建日记条目 | P0 | ✅ 已实现 | 能创建包含日期和内容的日记 |
|
||||||
|
| F-002 | 编辑日记内容 | P0 | ✅ 已实现 | 能修改已有日记 |
|
||||||
|
| F-003 | 删除日记 | P1 | ⏳ 待实现 | - |
|
||||||
|
| F-004 | 查看今日日记 | P0 | ✅ 已实现 | API `/api/entries/today/` 返回今天日记 |
|
||||||
|
| F-005 | 查看历史日记 | P0 | ✅ 已实现 | API `/api/entries/recent/` 返回最近 7 天 |
|
||||||
|
| F-006 | **日历视图展示** | P0 | ✅ 已实现 | **详见 2.1.1 日历组件详细需求** |
|
||||||
|
| F-007 | 日记内容字段 | P0 | ✅ 已实现 | 包含完成任务、学到的东西、问题、反思、进步、计划 |
|
||||||
|
|
||||||
|
#### 2.1.1 日历组件详细需求 ⭐ **核心功能,必须测试**
|
||||||
|
|
||||||
|
| ID | 功能点 | 详细描述 | 验收方法 |
|
||||||
|
|----|--------|----------|----------|
|
||||||
|
| F-006-01 | 月历视图 | 显示完整的月历,包含上月/下月的部分日期 | 页面加载后显示当前月份的日历 |
|
||||||
|
| F-006-02 | 星期标题 | 日历顶部显示 日、一、二、三、四、五、六 | 第一行显示 7 个星期标题 |
|
||||||
|
| F-006-03 | 日期高亮 - 今天 | 今天的日期用特殊样式高亮(背景色) | 今天的格子有 `.today` 类 |
|
||||||
|
| F-006-04 | 日期标记 - 有日记 | 有日记的日期显示 📝 图标 | 调用 API 获取所有日记日期,有日记的格子有 `.has-diary` 类 |
|
||||||
|
| F-006-05 | 点击日期 | 点击任意日期,下方显示当天的日记详情 | 点击后触发 `selectDate()` 函数,显示日记内容 |
|
||||||
|
| F-006-06 | 日记详情展示 | 显示选中日期的所有字段(完成任务、学到的东西等) | 详情区域显示完整的日记内容 |
|
||||||
|
| F-006-07 | 无日记提示 | 点击没有日记的日期,显示"这一天还没有日记" | 显示友好的空状态提示 |
|
||||||
|
| F-006-08 | 上月切换 | 点击"上月"按钮,显示上个月的日历 | 调用 `prevMonth()`,日历刷新 |
|
||||||
|
| F-006-09 | 下月切换 | 点击"下月"按钮,显示下个月的日历 | 调用 `nextMonth()`,日历刷新 |
|
||||||
|
| F-006-10 | 非当月日期 | 上月/下月的日期用灰色显示,不可点击 | 有 `.other-month` 类,样式灰色 |
|
||||||
|
|
||||||
|
**⚠️ 此组件为日记系统的核心功能,任何前端修改后必须手动验证以上 10 个功能点!**
|
||||||
|
|
||||||
|
### 2.2 经验总结 ✅
|
||||||
|
|
||||||
|
| ID | 需求 | 优先级 | 状态 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| F-010 | 创建经验总结 | P0 | ✅ 已实现 |
|
||||||
|
| F-011 | 经验分类(部署/开发/数据库/权限/网络/其他) | P0 | ✅ 已实现 |
|
||||||
|
| F-012 | 记录问题描述 | P0 | ✅ 已实现 |
|
||||||
|
| F-013 | 记录解决方案 | P0 | ✅ 已实现 |
|
||||||
|
| F-014 | 记录经验教训 | P1 | ✅ 已实现 |
|
||||||
|
| F-015 | 按类别查看经验 | P1 | ✅ 已实现 |
|
||||||
|
| F-016 | 经验总结搜索 | P2 | ⏳ 待实现 |
|
||||||
|
|
||||||
|
### 2.3 任务管理 ✅
|
||||||
|
|
||||||
|
| ID | 需求 | 优先级 | 状态 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| F-020 | 创建任务 | P0 | ✅ 已实现 |
|
||||||
|
| F-021 | 任务关联到日记 | P0 | ✅ 已实现 |
|
||||||
|
| F-022 | 任务状态追踪(进行中/已完成) | P0 | ✅ 已实现 |
|
||||||
|
| F-023 | 任务完成率统计 | P0 | ✅ 已实现 |
|
||||||
|
| F-024 | 任务优先级 | P2 | ⏳ 待实现 |
|
||||||
|
|
||||||
|
### 2.4 统计面板 ✅
|
||||||
|
|
||||||
|
| ID | 需求 | 优先级 | 状态 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| F-030 | 总日记数统计 | P0 | ✅ 已实现 |
|
||||||
|
| F-031 | 总任务数统计 | P0 | ✅ 已实现 |
|
||||||
|
| F-032 | 进行中任务数 | P0 | ✅ 已实现 |
|
||||||
|
| F-033 | 已完成任务数 | P0 | ✅ 已实现 |
|
||||||
|
| F-034 | 任务完成率 | P0 | ✅ 已实现 |
|
||||||
|
| F-035 | 经验总结数 | P0 | ✅ 已实现 |
|
||||||
|
|
||||||
|
### 2.5 搜索与筛选 ⏳
|
||||||
|
|
||||||
|
| ID | 需求 | 优先级 | 状态 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| F-040 | 按日期范围筛选日记 | P1 | ⏳ 待实现 |
|
||||||
|
| F-041 | 按关键词搜索日记内容 | P1 | ⏳ 待实现 |
|
||||||
|
| F-042 | 按类别筛选经验总结 | P1 | ⏳ 待实现 |
|
||||||
|
| F-043 | 高级搜索(组合条件) | P2 | ⏳ 待实现 |
|
||||||
|
|
||||||
|
### 2.6 数据导出 ⏳
|
||||||
|
|
||||||
|
| ID | 需求 | 优先级 | 状态 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| F-050 | 导出日记为 Markdown | P1 | ⏳ 待实现 |
|
||||||
|
| F-051 | 导出经验总结 | P1 | ⏳ 待实现 |
|
||||||
|
| F-052 | 全量数据备份 | P0 | ⏳ 待实现 |
|
||||||
|
| F-053 | 定期自动备份 | P1 | ⏳ 待实现 |
|
||||||
|
|
||||||
|
### 2.7 用户认证 ⏳
|
||||||
|
|
||||||
|
| ID | 需求 | 优先级 | 状态 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| F-060 | 用户登录 | P2 | ⏳ 待实现 |
|
||||||
|
| F-061 | 权限控制 | P2 | ⏳ 待实现 |
|
||||||
|
| F-062 | 多用户支持 | P3 | ⏳ 待实现 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 非功能需求
|
||||||
|
|
||||||
|
### 3.1 性能
|
||||||
|
| ID | 需求 | 优先级 | 状态 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| N-001 | 页面加载时间 < 2 秒 | P0 | ✅ 已满足 |
|
||||||
|
| N-002 | API 响应时间 < 500ms | P0 | ✅ 已满足 |
|
||||||
|
| N-003 | 支持 1000+ 日记条目 | P1 | ✅ 已满足 |
|
||||||
|
|
||||||
|
### 3.2 可用性
|
||||||
|
| ID | 需求 | 优先级 | 状态 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| N-010 | 7x24 小时可用 | P0 | ✅ 已满足 |
|
||||||
|
| N-011 | 数据持久化 | P0 | ✅ 已满足 |
|
||||||
|
| N-012 | 支持离线访问 | P2 | ⏳ 待实现 |
|
||||||
|
|
||||||
|
### 3.3 安全性
|
||||||
|
| ID | 需求 | 优先级 | 状态 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| N-020 | 防止 SQL 注入 | P0 | ✅ 已满足 |
|
||||||
|
| N-021 | 防止 XSS 攻击 | P0 | ✅ 已满足 |
|
||||||
|
| N-022 | 敏感数据加密 | P1 | ⏳ 待实现 |
|
||||||
|
|
||||||
|
### 3.4 可维护性
|
||||||
|
| ID | 需求 | 优先级 | 状态 |
|
||||||
|
|----|------|--------|------|
|
||||||
|
| N-030 | 代码有文档注释 | P0 | ✅ 已满足 |
|
||||||
|
| N-031 | 有功能清单文档 | P0 | ✅ 已满足 |
|
||||||
|
| N-032 | 有开发规范文档 | P0 | ✅ 已满足 |
|
||||||
|
| N-033 | 有 Git 版本控制 | P0 | ✅ 已满足 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 技术架构
|
||||||
|
|
||||||
|
### 4.1 技术栈
|
||||||
|
|
||||||
|
| 层级 | 技术 | 版本 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 后端 | Django | 4.2+ | Web 框架 |
|
||||||
|
| API | Django REST Framework | 3.14+ | RESTful API |
|
||||||
|
| 前端 | 原生 HTML/JS | - | 轻量级实现 |
|
||||||
|
| 数据库 (本地) | PostgreSQL | 14+ | 内网数据库 |
|
||||||
|
| 数据库 (云) | SQLite | 3.x | 本地文件 |
|
||||||
|
| Web 服务器 | Nginx | 1.24+ | 反向代理 |
|
||||||
|
| 应用服务器 | Gunicorn | 25.x | WSGI 服务器 |
|
||||||
|
|
||||||
|
### 4.2 部署架构
|
||||||
|
|
||||||
|
```
|
||||||
|
用户 → Nginx(8001) → Gunicorn(8002) → Django → 数据库
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
diary-system/
|
||||||
|
├── backend/ # Django 后端
|
||||||
|
│ ├── diary/ # 日记应用
|
||||||
|
│ │ ├── models.py # 数据模型
|
||||||
|
│ │ ├── views.py # API 视图
|
||||||
|
│ │ ├── serializers.py
|
||||||
|
│ │ └── urls.py
|
||||||
|
│ ├── diary_system/ # 项目配置
|
||||||
|
│ └── manage.py
|
||||||
|
├── frontend/ # 前端页面
|
||||||
|
│ └── index.html
|
||||||
|
├── FEATURES.md # 功能清单
|
||||||
|
├── DEV_GUIDE.md # 开发规范
|
||||||
|
├── REQUIREMENTS.md # 本文档
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 数据模型
|
||||||
|
|
||||||
|
### 5.1 DiaryEntry(日记条目)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DiaryEntry(models.Model):
|
||||||
|
date = DateField(unique=True) # 日期
|
||||||
|
title = CharField(max_length=200) # 标题
|
||||||
|
completed_tasks = TextField() # 完成的任务
|
||||||
|
learned = TextField() # 学到的东西
|
||||||
|
problems = TextField() # 遇到的问题
|
||||||
|
reflections = TextField() # 想法和反思
|
||||||
|
improvements = TextField() # 进步点
|
||||||
|
plans = TextField() # 明日计划
|
||||||
|
created_at = DateTimeField() # 创建时间
|
||||||
|
updated_at = DateTimeField() # 更新时间
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Experience(经验总结)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Experience(models.Model):
|
||||||
|
title = CharField(max_length=200) # 标题
|
||||||
|
category = CharField(choices=CATEGORY_CHOICES) # 类别
|
||||||
|
problem = TextField() # 问题描述
|
||||||
|
solution = TextField() # 解决方案
|
||||||
|
lesson_learned = TextField() # 经验教训
|
||||||
|
date = DateField() # 日期
|
||||||
|
created_at = DateTimeField() # 创建时间
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 DailyProgress(任务进度)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DailyProgress(models.Model):
|
||||||
|
entry = ForeignKey(DiaryEntry) # 关联日记
|
||||||
|
category = CharField(max_length=50) # 类别
|
||||||
|
description = TextField() # 描述
|
||||||
|
progress_percent = IntegerField() # 进度百分比
|
||||||
|
created_at = DateTimeField() # 创建时间
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 开发计划
|
||||||
|
|
||||||
|
### 6.1 已完成 (v1.0) ✅
|
||||||
|
|
||||||
|
- [x] 日记 CRUD 功能
|
||||||
|
- [x] 经验总结功能
|
||||||
|
- [x] 任务管理功能
|
||||||
|
- [x] 统计面板
|
||||||
|
- [x] 日历组件
|
||||||
|
- [x] 云服务器部署
|
||||||
|
- [x] 文档完善(功能清单、开发规范)
|
||||||
|
|
||||||
|
### 6.2 进行中 (v1.1) 🚧
|
||||||
|
|
||||||
|
- [ ] 搜索功能
|
||||||
|
- [ ] 数据导出
|
||||||
|
- [ ] 自动备份
|
||||||
|
|
||||||
|
### 6.3 计划中 (v2.0) 📅
|
||||||
|
|
||||||
|
- [ ] 用户认证
|
||||||
|
- [ ] 多用户支持
|
||||||
|
- [ ] 数据可视化(图表)
|
||||||
|
- [ ] 移动端优化
|
||||||
|
|
||||||
|
### 6.4 未来规划 (v3.0) 🔮
|
||||||
|
|
||||||
|
- [ ] AI 辅助写日记
|
||||||
|
- [ ] 智能标签
|
||||||
|
- [ ] 情感分析
|
||||||
|
- [ ] 年度报告生成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 变更日志
|
||||||
|
|
||||||
|
### v1.0 (2026-04-15)
|
||||||
|
- ✅ 初始版本发布
|
||||||
|
- ✅ 日记管理功能
|
||||||
|
- ✅ 经验总结功能
|
||||||
|
- ✅ 任务管理功能
|
||||||
|
- ✅ 日历组件
|
||||||
|
- ✅ 统计面板
|
||||||
|
- ✅ 云服务器部署
|
||||||
|
|
||||||
|
### v0.2 (2026-04-14)
|
||||||
|
- ✅ 添加经验总结模块
|
||||||
|
- ✅ 添加分类管理
|
||||||
|
- ✅ 前端展示优化
|
||||||
|
|
||||||
|
### v0.1 (2026-04-14)
|
||||||
|
- ✅ 项目初始化
|
||||||
|
- ✅ 基础日记功能
|
||||||
|
- ✅ Django 后端搭建
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 需求变更流程
|
||||||
|
|
||||||
|
1. **提出需求** - 在 GitHub Issues 或对话中提出
|
||||||
|
2. **评估优先级** - 确定 P0/P1/P2/P3
|
||||||
|
3. **更新文档** - 修改本需求文档
|
||||||
|
4. **开发实现** - 按开发规范执行
|
||||||
|
5. **测试验证** - 验证功能正常
|
||||||
|
6. **更新状态** - 标记为"已实现"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_本文档由码神维护,每次需求变更后必须更新并提交 git_
|
||||||
34
backend/authentication/serializers.py
Normal file
34
backend/authentication/serializers.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['id', 'username', 'email', 'date_joined']
|
||||||
|
read_only_fields = ['date_joined']
|
||||||
|
|
||||||
|
class RegisterSerializer(serializers.ModelSerializer):
|
||||||
|
password = serializers.CharField(write_only=True, min_length=6)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['username', 'email', 'password']
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username=validated_data['username'],
|
||||||
|
email=validated_data.get('email', ''),
|
||||||
|
password=validated_data['password']
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
class LoginSerializer(serializers.Serializer):
|
||||||
|
username = serializers.CharField()
|
||||||
|
password = serializers.CharField(write_only=True)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
user = authenticate(**data)
|
||||||
|
if user and user.is_active:
|
||||||
|
return user
|
||||||
|
raise serializers.ValidationError("用户名或密码错误")
|
||||||
9
backend/authentication/urls.py
Normal file
9
backend/authentication/urls.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import RegisterView, LoginView, LogoutView, CurrentUserView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('register/', RegisterView.as_view(), name='register'),
|
||||||
|
path('login/', LoginView.as_view(), name='login'),
|
||||||
|
path('logout/', LogoutView.as_view(), name='logout'),
|
||||||
|
path('me/', CurrentUserView.as_view(), name='current-user'),
|
||||||
|
]
|
||||||
54
backend/authentication/views.py
Normal file
54
backend/authentication/views.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from rest_framework import generics, permissions, status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||||
|
from django.contrib.auth import login, logout
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from .serializers import UserSerializer, RegisterSerializer, LoginSerializer
|
||||||
|
|
||||||
|
class RegisterView(generics.CreateAPIView):
|
||||||
|
"""用户注册"""
|
||||||
|
serializer_class = RegisterSerializer
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
user = serializer.save()
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'user': UserSerializer(user).data,
|
||||||
|
'message': '注册成功'
|
||||||
|
}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
class LoginView(generics.GenericAPIView):
|
||||||
|
"""用户登录"""
|
||||||
|
serializer_class = LoginSerializer
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
user = serializer.validated_data
|
||||||
|
|
||||||
|
login(request, user)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'user': UserSerializer(user).data,
|
||||||
|
'message': '登录成功'
|
||||||
|
})
|
||||||
|
|
||||||
|
class LogoutView(generics.GenericAPIView):
|
||||||
|
"""用户登出"""
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
logout(request)
|
||||||
|
return Response({'message': '登出成功'})
|
||||||
|
|
||||||
|
class CurrentUserView(generics.RetrieveAPIView):
|
||||||
|
"""当前用户信息"""
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.request.user
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2026-04-15 02:59
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('diary', '0007_comment_creativity_comment_efficiency_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='comment',
|
||||||
|
name='created_by',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='comment',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL, verbose_name='创建者'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='diaryentry',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='diary_entries', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='experience',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='experiences', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='task',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to=settings.AUTH_USER_MODEL, verbose_name='创建者'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='diaryentry',
|
||||||
|
name='date',
|
||||||
|
field=models.DateField(verbose_name='日期'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='task',
|
||||||
|
name='assigned_to',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_tasks', to=settings.AUTH_USER_MODEL, verbose_name='负责人'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='diaryentry',
|
||||||
|
unique_together={('user', 'date')},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,9 +1,24 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
class DiaryEntry(models.Model):
|
class DiaryEntry(models.Model):
|
||||||
"""日记条目 - 作为所有记录的主入口"""
|
"""
|
||||||
date = models.DateField('日期', unique=True)
|
⚠️ 核心模型:日记条目
|
||||||
|
与日历组件关联 - 修改此模型会影响日历功能
|
||||||
|
|
||||||
|
日历功能依赖:
|
||||||
|
- date 字段(唯一)- 用于日历标记
|
||||||
|
- linked_tasks - 关联任务
|
||||||
|
- extract_experience() - 提炼经验
|
||||||
|
|
||||||
|
修改前必须:
|
||||||
|
1. 阅读 docs/CALENDAR.md
|
||||||
|
2. 确认不影响日历显示
|
||||||
|
3. 运行 test_frontend.py diary 验证
|
||||||
|
"""
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户', related_name='diary_entries', null=True, blank=True)
|
||||||
|
date = models.DateField('日期')
|
||||||
title = models.CharField('标题', max_length=200, default='每日日记')
|
title = models.CharField('标题', max_length=200, default='每日日记')
|
||||||
content = models.TextField('日记内容', blank=True, default='')
|
content = models.TextField('日记内容', blank=True, default='')
|
||||||
completed_tasks = models.TextField('完成的任务', blank=True, default='')
|
completed_tasks = models.TextField('完成的任务', blank=True, default='')
|
||||||
@@ -22,6 +37,7 @@ class DiaryEntry(models.Model):
|
|||||||
ordering = ['-date']
|
ordering = ['-date']
|
||||||
verbose_name = '日记'
|
verbose_name = '日记'
|
||||||
verbose_name_plural = '日记'
|
verbose_name_plural = '日记'
|
||||||
|
unique_together = ['user', 'date'] # 每个用户每天一条日记
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.date} - {self.title}"
|
return f"{self.date} - {self.title}"
|
||||||
@@ -47,7 +63,17 @@ class DiaryEntry(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Experience(models.Model):
|
class Experience(models.Model):
|
||||||
"""经验总结 - 记录遇到的问题和解决方法"""
|
"""
|
||||||
|
⚠️ 核心模型:经验总结
|
||||||
|
从日记提炼的经验教训
|
||||||
|
|
||||||
|
关联:
|
||||||
|
- extracted_from - 可选,关联到日记
|
||||||
|
- category - 分类显示
|
||||||
|
|
||||||
|
修改前阅读 docs/EXPERIENCE.md
|
||||||
|
"""
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户', related_name='experiences', null=True, blank=True)
|
||||||
title = models.CharField('标题', max_length=200)
|
title = models.CharField('标题', max_length=200)
|
||||||
category = models.CharField('类别', max_length=50, choices=[
|
category = models.CharField('类别', max_length=50, choices=[
|
||||||
('deployment', '📦 部署'),
|
('deployment', '📦 部署'),
|
||||||
@@ -112,7 +138,7 @@ class Comment(models.Model):
|
|||||||
creativity = models.IntegerField('创新性', null=True, blank=True, help_text='1-10 分')
|
creativity = models.IntegerField('创新性', null=True, blank=True, help_text='1-10 分')
|
||||||
learning = models.IntegerField('学习价值', null=True, blank=True, help_text='1-10 分')
|
learning = models.IntegerField('学习价值', null=True, blank=True, help_text='1-10 分')
|
||||||
|
|
||||||
created_by = models.CharField('创建者', max_length=100, default='北极星')
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='创建者', related_name='comments', null=True, blank=True)
|
||||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -147,7 +173,8 @@ class Task(models.Model):
|
|||||||
priority = models.CharField('优先级', max_length=20, choices=PRIORITY_CHOICES, default='medium')
|
priority = models.CharField('优先级', max_length=20, choices=PRIORITY_CHOICES, default='medium')
|
||||||
progress_percent = models.IntegerField('进展百分比', default=0)
|
progress_percent = models.IntegerField('进展百分比', default=0)
|
||||||
progress_notes = models.TextField('进展记录', blank=True, default='')
|
progress_notes = models.TextField('进展记录', blank=True, default='')
|
||||||
assigned_to = models.CharField('负责人', max_length=100, blank=True, default='码神')
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='创建者', related_name='tasks', null=True, blank=True)
|
||||||
|
assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='负责人', related_name='assigned_tasks')
|
||||||
due_date = models.DateField('截止日期', null=True, blank=True)
|
due_date = models.DateField('截止日期', null=True, blank=True)
|
||||||
completed_at = models.DateTimeField('完成时间', null=True, blank=True)
|
completed_at = models.DateTimeField('完成时间', null=True, blank=True)
|
||||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ from rest_framework.routers import DefaultRouter
|
|||||||
from .views import DiaryEntryViewSet, DailyProgressViewSet, ExperienceViewSet, TaskViewSet, CommentViewSet
|
from .views import DiaryEntryViewSet, DailyProgressViewSet, ExperienceViewSet, TaskViewSet, CommentViewSet
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'entries', DiaryEntryViewSet)
|
router.register(r'entries', DiaryEntryViewSet, basename='diaryentry')
|
||||||
router.register(r'progress', DailyProgressViewSet)
|
router.register(r'progress', DailyProgressViewSet, basename='dailyprogress')
|
||||||
router.register(r'experiences', ExperienceViewSet)
|
router.register(r'experiences', ExperienceViewSet, basename='experience')
|
||||||
router.register(r'tasks', TaskViewSet)
|
router.register(r'tasks', TaskViewSet, basename='task')
|
||||||
router.register(r'comments', CommentViewSet)
|
router.register(r'comments', CommentViewSet, basename='comment')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import permissions
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from .models import DiaryEntry, DailyProgress, Experience, Task, Comment
|
from .models import DiaryEntry, DailyProgress, Experience, Task, Comment
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
@@ -9,21 +10,24 @@ from .serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
class DiaryEntryViewSet(viewsets.ModelViewSet):
|
class DiaryEntryViewSet(viewsets.ModelViewSet):
|
||||||
queryset = DiaryEntry.objects.all()
|
|
||||||
serializer_class = DiaryEntrySerializer
|
serializer_class = DiaryEntrySerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return DiaryEntry.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def today(self, request):
|
def today(self, request):
|
||||||
"""获取今天的日记"""
|
"""获取今天的日记"""
|
||||||
today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
entry, created = DiaryEntry.objects.get_or_create(date=today)
|
entry, created = DiaryEntry.objects.get_or_create(user=request.user, date=today)
|
||||||
serializer = self.get_serializer(entry)
|
serializer = self.get_serializer(entry)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def recent(self, request):
|
def recent(self, request):
|
||||||
"""获取最近 7 天的日记"""
|
"""获取最近 7 天的日记"""
|
||||||
entries = DiaryEntry.objects.order_by('-date')[:7]
|
entries = DiaryEntry.objects.filter(user=request.user).order_by('-date')[:7]
|
||||||
serializer = self.get_serializer(entries, many=True)
|
serializer = self.get_serializer(entries, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@@ -73,8 +77,11 @@ class DailyProgressViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ExperienceViewSet(viewsets.ModelViewSet):
|
class ExperienceViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Experience.objects.all()
|
|
||||||
serializer_class = ExperienceSerializer
|
serializer_class = ExperienceSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Experience.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def by_category(self, request):
|
def by_category(self, request):
|
||||||
@@ -96,8 +103,11 @@ class ExperienceViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class TaskViewSet(viewsets.ModelViewSet):
|
class TaskViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Task.objects.all()
|
|
||||||
serializer_class = TaskSerializer
|
serializer_class = TaskSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Task.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def by_status(self, request):
|
def by_status(self, request):
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
'rest_framework.authtoken',
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
'diary',
|
'diary',
|
||||||
|
'authentication',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ from django.urls import path, include
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('api/auth/', include('authentication.urls')),
|
||||||
path('api/', include('diary.urls')),
|
path('api/', include('diary.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
7
create_auth_app.py
Normal file
7
create_auth_app.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os, sys
|
||||||
|
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'diary_system.settings'
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
sys.argv = ['manage.py', 'startapp', 'authentication']
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
56
deploy_multiuser.sh
Normal file
56
deploy_multiuser.sh
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 多用户系统 - 部署脚本
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "======================================"
|
||||||
|
echo "🚀 部署多用户日记系统"
|
||||||
|
echo "======================================"
|
||||||
|
|
||||||
|
cd /home/ubuntu/diary-system
|
||||||
|
|
||||||
|
# 1. 安装依赖
|
||||||
|
echo "📦 安装 Python 依赖..."
|
||||||
|
cd backend
|
||||||
|
pip3 install -r requirements.txt -q 2>/dev/null || pip3 install -r requirements.txt --break-system-packages -q
|
||||||
|
|
||||||
|
# 2. 数据库迁移
|
||||||
|
echo "🗄️ 执行数据库迁移..."
|
||||||
|
python3 manage.py migrate --run-syncdb
|
||||||
|
|
||||||
|
# 3. 迁移现有数据到默认用户
|
||||||
|
echo "📦 迁移现有数据..."
|
||||||
|
python3 ../migrate_data.py
|
||||||
|
|
||||||
|
# 4. 重启 Gunicorn
|
||||||
|
echo "⚙️ 重启 Gunicorn..."
|
||||||
|
sudo systemctl restart diary-system
|
||||||
|
|
||||||
|
# 5. 检查服务状态
|
||||||
|
echo ""
|
||||||
|
echo "🔍 检查服务状态..."
|
||||||
|
sudo systemctl is-active diary-system > /dev/null && echo " ✅ Gunicorn 运行中" || echo " ❌ Gunicorn 未运行"
|
||||||
|
|
||||||
|
# 6. 测试访问
|
||||||
|
echo ""
|
||||||
|
echo "🧪 测试访问..."
|
||||||
|
sleep 2
|
||||||
|
if curl -s http://127.0.0.1:8002/api/auth/me/ > /dev/null 2>&1; then
|
||||||
|
echo " ✅ 认证 API 正常"
|
||||||
|
else
|
||||||
|
echo " ⚠️ 认证 API 可能有问题"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "======================================"
|
||||||
|
echo "✅ 部署完成!"
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
echo "📍 访问地址:http://cssc.datalibstar.com:8001/"
|
||||||
|
echo ""
|
||||||
|
echo "📝 默认用户:"
|
||||||
|
echo " 用户名:beijixing"
|
||||||
|
echo " 密码:beijixing123"
|
||||||
|
echo ""
|
||||||
|
echo "💡 其他用户可以注册新账号使用"
|
||||||
|
echo ""
|
||||||
86
docs/CALENDAR.md
Normal file
86
docs/CALENDAR.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# 日历组件需求说明
|
||||||
|
|
||||||
|
_位置:`frontend/index.html`
|
||||||
|
核心功能 ⭐ - 修改前必须阅读_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 功能列表
|
||||||
|
|
||||||
|
| ID | 功能 | 描述 | 代码位置 |
|
||||||
|
|----|------|------|----------|
|
||||||
|
| C-01 | 月历视图 | 显示完整月历 | `renderCalendar()` |
|
||||||
|
| C-02 | 星期标题 | 日一二三四五六 | `.calendar-day-header` |
|
||||||
|
| C-03 | 今天高亮 | 当前日期特殊样式 | `.today` 类 |
|
||||||
|
| C-04 | 日记标记 | 有日记的日期显示 📝 | `.has-diary` 类 |
|
||||||
|
| C-05 | 点击日期 | 显示当天日记详情 | `selectDate()` |
|
||||||
|
| C-06 | 日记详情 | 显示完整日记内容 | `#selected-diary` |
|
||||||
|
| C-07 | 上月切换 | 显示上个月 | `prevMonth()` |
|
||||||
|
| C-08 | 下月切换 | 显示下个月 | `nextMonth()` |
|
||||||
|
| C-09 | 非当月日期 | 灰色显示 | `.other-month` 类 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 UI 结构
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="calendar-container">
|
||||||
|
<div class="calendar-header">
|
||||||
|
<button onclick="prevMonth()">上月</button>
|
||||||
|
<h2 id="calendar-title">2026 年 4 月</h2>
|
||||||
|
<button onclick="nextMonth()">下月</button>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-grid" id="calendar-grid">
|
||||||
|
<!-- 7 个星期标题 -->
|
||||||
|
<div class="calendar-day-header">日</div>
|
||||||
|
...
|
||||||
|
<!-- 日期格子 -->
|
||||||
|
<div class="calendar-day today has-diary">15</div>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="selected-diary">点击日期后显示详情</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 修改指南
|
||||||
|
|
||||||
|
### 可以修改的
|
||||||
|
- ✅ 样式(颜色、大小、间距)
|
||||||
|
- ✅ 按钮文字
|
||||||
|
- ✅ 日期格式
|
||||||
|
- ✅ 图标样式
|
||||||
|
|
||||||
|
### 不能删除的
|
||||||
|
- ❌ `renderCalendar()` 函数
|
||||||
|
- ❌ `selectDate()` 函数
|
||||||
|
- ❌ `prevMonth()` / `nextMonth()` 函数
|
||||||
|
- ❌ `.calendar-day` 相关样式
|
||||||
|
- ❌ `#calendar-grid` 元素
|
||||||
|
- ❌ `#selected-diary` 元素
|
||||||
|
|
||||||
|
### 添加新功能时
|
||||||
|
1. 先查看现有函数
|
||||||
|
2. 不要删除现有 DOM 元素
|
||||||
|
3. 可以在现有结构上扩展
|
||||||
|
4. 修改后运行 `test_frontend.py diary`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试清单
|
||||||
|
|
||||||
|
修改后必须验证:
|
||||||
|
- [ ] 日历正常显示
|
||||||
|
- [ ] 今天高亮
|
||||||
|
- [ ] 有日记的日期有 📝 标记
|
||||||
|
- [ ] 点击日期显示详情
|
||||||
|
- [ ] 上月/下月切换正常
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_frontend.py diary
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_此文档必须与代码一起维护,修改日历组件时先阅读_
|
||||||
93
docs/DIARY.md
Normal file
93
docs/DIARY.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# 日记模块需求说明
|
||||||
|
|
||||||
|
_位置:`backend/diary/models.py`, `frontend/index.html`
|
||||||
|
核心功能 ⭐_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 后端模型
|
||||||
|
|
||||||
|
### DiaryEntry 模型
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DiaryEntry(models.Model):
|
||||||
|
date = DateField(unique=True) # 日期(唯一)
|
||||||
|
title = CharField(max_length=200) # 标题
|
||||||
|
completed_tasks = TextField() # 完成的任务
|
||||||
|
learned = TextField() # 学到的东西
|
||||||
|
problems = TextField() # 遇到的问题
|
||||||
|
reflections = TextField() # 想法和反思
|
||||||
|
improvements = TextField() # 进步点
|
||||||
|
plans = TextField() # 明日计划
|
||||||
|
created_at = DateTimeField() # 创建时间
|
||||||
|
updated_at = DateTimeField() # 更新时间
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 API 接口
|
||||||
|
|
||||||
|
| 接口 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/entries/` | GET | 获取所有日记 |
|
||||||
|
| `/api/entries/{id}/` | GET | 获取单条日记 |
|
||||||
|
| `/api/entries/` | POST | 创建日记 |
|
||||||
|
| `/api/entries/{id}/` | PUT | 更新日记 |
|
||||||
|
| `/api/entries/today/` | GET | 获取今天日记 |
|
||||||
|
| `/api/entries/recent/` | GET | 最近 7 天日记 |
|
||||||
|
| `/api/entries/stats/` | GET | 统计信息 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 前端展示
|
||||||
|
|
||||||
|
### 日历视图(主要)
|
||||||
|
- 有日记的日期显示 📝 标记
|
||||||
|
- 点击日期显示详情
|
||||||
|
- 详情包含所有字段
|
||||||
|
|
||||||
|
### 日记列表
|
||||||
|
- 按日期倒序
|
||||||
|
- 显示完整内容
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 修改指南
|
||||||
|
|
||||||
|
### 可以修改的
|
||||||
|
- ✅ 添加新字段
|
||||||
|
- ✅ 修改样式
|
||||||
|
- ✅ 修改 API 返回格式
|
||||||
|
- ✅ 添加新的统计维度
|
||||||
|
|
||||||
|
### 不能删除的
|
||||||
|
- ❌ DiaryEntry 模型
|
||||||
|
- ❌ `/api/entries/` 相关 API
|
||||||
|
- ❌ 日历组件
|
||||||
|
- ❌ 日记详情展示
|
||||||
|
|
||||||
|
### 添加新字段时
|
||||||
|
1. 修改 `models.py` 添加字段
|
||||||
|
2. 运行 `makemigrations diary`
|
||||||
|
3. 运行 `migrate`
|
||||||
|
4. 更新 `serializers.py`
|
||||||
|
5. 更新前端展示
|
||||||
|
6. 运行 `test_frontend.py diary`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试清单
|
||||||
|
|
||||||
|
修改后必须验证:
|
||||||
|
- [ ] 日历正常显示
|
||||||
|
- [ ] 有日记的日期有 📝 标记
|
||||||
|
- [ ] 点击日期显示详情
|
||||||
|
- [ ] 所有字段正确显示
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_frontend.py diary
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_此文档必须与代码一起维护_
|
||||||
108
docs/EXPERIENCE.md
Normal file
108
docs/EXPERIENCE.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# 经验总结模块需求说明
|
||||||
|
|
||||||
|
_位置:`backend/diary/models.py`, `frontend/index.html`_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 后端模型
|
||||||
|
|
||||||
|
### Experience 模型
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Experience(models.Model):
|
||||||
|
title = CharField(max_length=200) # 标题
|
||||||
|
category = CharField(choices=CATEGORY_CHOICES) # 类别
|
||||||
|
problem = TextField() # 问题描述
|
||||||
|
solution = TextField() # 解决方案
|
||||||
|
lesson_learned = TextField() # 经验教训
|
||||||
|
date = DateField() # 日期
|
||||||
|
created_at = DateTimeField() # 创建时间
|
||||||
|
```
|
||||||
|
|
||||||
|
### 类别选项
|
||||||
|
- `deployment` - 📦 部署
|
||||||
|
- `development` - 💻 开发
|
||||||
|
- `database` - 🗄️ 数据库
|
||||||
|
- `permission` - 🔐 权限
|
||||||
|
- `network` - 🌐 网络
|
||||||
|
- `other` - 其他
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 API 接口
|
||||||
|
|
||||||
|
| 接口 | 方法 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/experiences/` | GET | 获取所有经验 |
|
||||||
|
| `/api/experiences/{id}/` | GET | 获取单条经验 |
|
||||||
|
| `/api/experiences/` | POST | 创建经验 |
|
||||||
|
| `/api/experiences/{id}/` | PUT | 更新经验 |
|
||||||
|
| `/api/experiences/recent/` | GET | 最近 10 条 |
|
||||||
|
| `/api/experiences/by_category/` | GET | 按类别分组 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 前端展示
|
||||||
|
|
||||||
|
### UI 结构
|
||||||
|
```html
|
||||||
|
<div class="experience-item">
|
||||||
|
<div class="experience-header">
|
||||||
|
<span class="experience-title">标题</span>
|
||||||
|
<span class="experience-category">类别</span>
|
||||||
|
</div>
|
||||||
|
<div class="experience-problem">
|
||||||
|
<div class="experience-problem-title">🐛 问题</div>
|
||||||
|
<div>问题描述</div>
|
||||||
|
</div>
|
||||||
|
<div class="experience-solution">
|
||||||
|
<div class="experience-solution-title">✅ 解决方案</div>
|
||||||
|
<div>解决方案</div>
|
||||||
|
</div>
|
||||||
|
<div class="experience-lesson">
|
||||||
|
<div class="experience-lesson-title">📌 经验教训</div>
|
||||||
|
<div>经验教训</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 修改指南
|
||||||
|
|
||||||
|
### 可以修改的
|
||||||
|
- ✅ 添加新的类别选项
|
||||||
|
- ✅ 修改样式
|
||||||
|
- ✅ 添加新字段
|
||||||
|
- ✅ 修改展示格式
|
||||||
|
|
||||||
|
### 不能删除的
|
||||||
|
- ❌ Experience 模型
|
||||||
|
- ❌ `/api/experiences/` 相关 API
|
||||||
|
- ❌ 经验总结 Tab
|
||||||
|
- ❌ `.experience-item` 样式
|
||||||
|
|
||||||
|
### 添加新功能时
|
||||||
|
1. 修改 `models.py` 添加字段
|
||||||
|
2. 运行 `makemigrations` 和 `migrate`
|
||||||
|
3. 更新 `serializers.py`
|
||||||
|
4. 更新前端展示
|
||||||
|
5. 运行 `test_frontend.py experience`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试清单
|
||||||
|
|
||||||
|
修改后必须验证:
|
||||||
|
- [ ] 经验列表正常显示
|
||||||
|
- [ ] 分类标签正确
|
||||||
|
- [ ] 问题/解决方案格式正确
|
||||||
|
- [ ] 经验教训高亮显示
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_frontend.py experience
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_此文档必须与代码一起维护_
|
||||||
70
docs/README.md
Normal file
70
docs/README.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# 日记系统 - 模块文档索引
|
||||||
|
|
||||||
|
_每个模块都有独立的需求说明,修改前先阅读对应文档_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 文档结构
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── README.md # 本文档(索引)
|
||||||
|
├── CALENDAR.md # 日历组件 ⭐ 核心功能
|
||||||
|
├── DIARY.md # 日记模块 ⭐ 核心功能
|
||||||
|
├── EXPERIENCE.md # 经验总结模块
|
||||||
|
├── STATS.md # 统计面板
|
||||||
|
└── TASKS.md # 任务管理(待创建)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 修改代码时的流程
|
||||||
|
|
||||||
|
### 1. 确定修改的模块
|
||||||
|
|
||||||
|
| 修改内容 | 阅读文档 | 测试命令 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| 日历样式 | `CALENDAR.md` | `test_frontend.py diary` |
|
||||||
|
| 日记字段 | `DIARY.md` | `test_frontend.py diary` |
|
||||||
|
| 经验总结 | `EXPERIENCE.md` | `test_frontend.py experience` |
|
||||||
|
| 统计面板 | `STATS.md` | `test_frontend.py` |
|
||||||
|
|
||||||
|
### 2. 阅读文档
|
||||||
|
|
||||||
|
- 查看功能列表
|
||||||
|
- 确认**不能删除**的部分
|
||||||
|
- 了解修改指南
|
||||||
|
|
||||||
|
### 3. 增量修改
|
||||||
|
|
||||||
|
- 只添加,不覆盖
|
||||||
|
- 只修改相关模块
|
||||||
|
- 保持其他功能不变
|
||||||
|
|
||||||
|
### 4. 针对性测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_frontend.py <模块名>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 核心功能(除非用户明确要求,否则不动)
|
||||||
|
|
||||||
|
1. **日历组件** (`CALENDAR.md`)
|
||||||
|
- 月历视图
|
||||||
|
- 点击日期显示日记
|
||||||
|
- 上月/下月切换
|
||||||
|
|
||||||
|
2. **日记模块** (`DIARY.md`)
|
||||||
|
- 日记 CRUD
|
||||||
|
- 日历集成
|
||||||
|
|
||||||
|
3. **Tab 切换**
|
||||||
|
- 工作任务
|
||||||
|
- 日记
|
||||||
|
- 经验总结
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_修改任何代码前,先阅读对应模块文档_
|
||||||
57
docs/STATS.md
Normal file
57
docs/STATS.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# 统计面板需求说明
|
||||||
|
|
||||||
|
_位置:`frontend/index.html`_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 统计卡片
|
||||||
|
|
||||||
|
| 卡片 | 数据来源 | API |
|
||||||
|
|------|---------|-----|
|
||||||
|
| 总日记数 | `stats.total_entries` | `/api/entries/stats/` |
|
||||||
|
| 总任务数 | `stats.total_tasks` | `/api/entries/stats/` |
|
||||||
|
| 进行中 | `stats.progressing` | `/api/entries/stats/` |
|
||||||
|
| 已完成 | `stats.completed` | `/api/entries/stats/` |
|
||||||
|
| 完成率 | `stats.completion_rate` | `/api/entries/stats/` |
|
||||||
|
| 经验数 | `experiences.length` | `/api/experiences/recent/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 UI 结构
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3 id="stat-total-diaries">-</h3>
|
||||||
|
<p>总日记</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3 id="stat-total-tasks">-</h3>
|
||||||
|
<p>总任务</p>
|
||||||
|
</div>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 修改指南
|
||||||
|
|
||||||
|
### 可以修改的
|
||||||
|
- ✅ 添加新的统计卡片
|
||||||
|
- ✅ 修改样式
|
||||||
|
- ✅ 修改统计维度
|
||||||
|
|
||||||
|
### 不能删除的
|
||||||
|
- ❌ `.stats` 容器
|
||||||
|
- ❌ `.stat-card` 样式
|
||||||
|
- ❌ 统计 API 调用
|
||||||
|
|
||||||
|
### 添加新统计时
|
||||||
|
1. 后端 API 添加统计字段
|
||||||
|
2. 前端添加新的统计卡片
|
||||||
|
3. 更新数据绑定
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_此文档必须与代码一起维护_
|
||||||
12
fix_migrate.py
Normal file
12
fix_migrate.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os, sys
|
||||||
|
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'diary_system.settings'
|
||||||
|
|
||||||
|
# 先删除有问题的迁移
|
||||||
|
import subprocess
|
||||||
|
subprocess.run(['rm', '-f', 'diary/migrations/0008_*.py'], cwd='/root/.openclaw/workspace/diary-system/backend')
|
||||||
|
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
sys.argv = ['manage.py', 'migrate']
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
76
frontend-react/README.md
Normal file
76
frontend-react/README.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# React 前端重构说明
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend-react/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── Calendar.js # ⭐ 日历组件(核心)
|
||||||
|
│ │ ├── DiaryDetail.js # 日记详情
|
||||||
|
│ │ ├── ExperienceList.js # 经验总结列表
|
||||||
|
│ │ └── StatsPanel.js # 统计面板
|
||||||
|
│ ├── App.js # 主应用
|
||||||
|
│ ├── index.js # 入口
|
||||||
|
│ └── index.css # 样式
|
||||||
|
├── package.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 组件化优势
|
||||||
|
|
||||||
|
### 之前的 HTML/JS 大杂烩
|
||||||
|
- ❌ 所有逻辑混在一个文件
|
||||||
|
- ❌ 难以维护
|
||||||
|
- ❌ 看不清功能边界
|
||||||
|
- ❌ 容易误删核心功能
|
||||||
|
|
||||||
|
### 现在的 React 组件
|
||||||
|
- ✅ 每个功能独立组件
|
||||||
|
- ✅ 清晰的依赖关系
|
||||||
|
- ✅ 易于测试和维护
|
||||||
|
- ✅ 组件级别注释保护
|
||||||
|
|
||||||
|
## 🔧 开发指南
|
||||||
|
|
||||||
|
### 修改日历功能
|
||||||
|
```bash
|
||||||
|
# 只需修改这个文件
|
||||||
|
src/components/Calendar.js
|
||||||
|
|
||||||
|
# 阅读文档
|
||||||
|
docs/CALENDAR.md
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新功能
|
||||||
|
1. 在 `components/` 创建新组件
|
||||||
|
2. 在 `App.js` 中引入
|
||||||
|
3. 运行测试
|
||||||
|
|
||||||
|
## 🚀 构建部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 开发模式
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# 生产构建
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 部署构建产物
|
||||||
|
cp -r build/* ../frontend/
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ 核心组件
|
||||||
|
|
||||||
|
**不可删除的组件:**
|
||||||
|
- `Calendar.js` - 日历组件 ⭐⭐⭐
|
||||||
|
- `App.js` - 主应用 ⭐⭐
|
||||||
|
- `StatsPanel.js` - 统计面板 ⭐
|
||||||
|
|
||||||
|
修改前必须阅读 `docs/` 对应文档。
|
||||||
13
frontend-react/build/asset-manifest.json
Normal file
13
frontend-react/build/asset-manifest.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"files": {
|
||||||
|
"main.css": "/static/css/main.358dee66.css",
|
||||||
|
"main.js": "/static/js/main.7934ac80.js",
|
||||||
|
"index.html": "/index.html",
|
||||||
|
"main.358dee66.css.map": "/static/css/main.358dee66.css.map",
|
||||||
|
"main.7934ac80.js.map": "/static/js/main.7934ac80.js.map"
|
||||||
|
},
|
||||||
|
"entrypoints": [
|
||||||
|
"static/css/main.358dee66.css",
|
||||||
|
"static/js/main.7934ac80.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
frontend-react/build/index.html
Normal file
1
frontend-react/build/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#667eea"/><meta name="description" content="码神的日记系统 - 记录每天的进步与成长"/><title>码神的日记系统</title><script defer="defer" src="/static/js/main.7934ac80.js"></script><link href="/static/css/main.358dee66.css" rel="stylesheet"></head><body><noscript>需要启用 JavaScript 才能运行此应用。</noscript><div id="root"></div></body></html>
|
||||||
17130
frontend-react/package-lock.json
generated
Normal file
17130
frontend-react/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
frontend-react/package.json
Normal file
29
frontend-react/package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "diary-system-react",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"axios": "^1.6.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
14
frontend-react/public/index.html
Normal file
14
frontend-react/public/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#667eea" />
|
||||||
|
<meta name="description" content="码神的日记系统 - 记录每天的进步与成长" />
|
||||||
|
<title>码神的日记系统</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>需要启用 JavaScript 才能运行此应用。</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
238
frontend-react/src/App.js
Normal file
238
frontend-react/src/App.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import StatsPanel from './components/StatsPanel';
|
||||||
|
import Calendar from './components/Calendar';
|
||||||
|
import DiaryDetail from './components/DiaryDetail';
|
||||||
|
import ExperienceList from './components/ExperienceList';
|
||||||
|
import Login from './components/Login';
|
||||||
|
import Register from './components/Register';
|
||||||
|
|
||||||
|
const API_BASE = '/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ 主应用组件
|
||||||
|
*
|
||||||
|
* 核心功能:
|
||||||
|
* - 用户认证
|
||||||
|
* - 统计面板
|
||||||
|
* - 日历组件 ⭐
|
||||||
|
* - 日记详情(支持创建/编辑)
|
||||||
|
* - 经验总结(支持创建/编辑)
|
||||||
|
*
|
||||||
|
* ⚠️ 修改前阅读 docs/README.md
|
||||||
|
*/
|
||||||
|
function App() {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [authView, setAuthView] = useState('login'); // login | register
|
||||||
|
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
|
||||||
|
const [entries, setEntries] = useState([]);
|
||||||
|
const [experiences, setExperiences] = useState([]);
|
||||||
|
const [stats, setStats] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 检查是否已登录
|
||||||
|
checkAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkAuth = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(`${API_BASE}/auth/me/`);
|
||||||
|
setUser(res.data);
|
||||||
|
loadData();
|
||||||
|
} catch (err) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = (userData, view) => {
|
||||||
|
if (view) {
|
||||||
|
setAuthView(view);
|
||||||
|
} else if (userData) {
|
||||||
|
setUser(userData);
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegister = (userData, view) => {
|
||||||
|
if (view) {
|
||||||
|
setAuthView(view);
|
||||||
|
} else if (userData) {
|
||||||
|
setUser(userData);
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await axios.post(`${API_BASE}/auth/logout/`);
|
||||||
|
setUser(null);
|
||||||
|
setEntries([]);
|
||||||
|
setExperiences([]);
|
||||||
|
setStats({});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('登出失败:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
const [statsRes, entriesRes, expRes] = await Promise.all([
|
||||||
|
axios.get(`${API_BASE}/entries/stats/`),
|
||||||
|
axios.get(`${API_BASE}/entries/`),
|
||||||
|
axios.get(`${API_BASE}/experiences/recent/`)
|
||||||
|
]);
|
||||||
|
|
||||||
|
setStats(statsRes.data);
|
||||||
|
setEntries(entriesRes.data);
|
||||||
|
setExperiences(expRes.data);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
setError(`加载失败:${err.message}`);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateSelect = (date) => {
|
||||||
|
setSelectedDate(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDiaryUpdate = async () => {
|
||||||
|
// 日记更新后重新加载数据
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExperienceRefresh = () => {
|
||||||
|
// 经验更新后重新加载经验列表
|
||||||
|
axios.get(`${API_BASE}/experiences/recent/`)
|
||||||
|
.then(res => setExperiences(res.data))
|
||||||
|
.catch(err => console.error('加载经验失败:', err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查找选中日期的日记
|
||||||
|
const selectedEntry = entries.find(e => e.date === selectedDate);
|
||||||
|
|
||||||
|
// 未登录显示登录/注册界面
|
||||||
|
if (!user) {
|
||||||
|
if (authView === 'register') {
|
||||||
|
return <Register onRegister={handleRegister} />;
|
||||||
|
}
|
||||||
|
return <Login onLogin={handleLogin} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '100vh',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'white',
|
||||||
|
padding: '40px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '48px', marginBottom: '20px' }}>⚡</div>
|
||||||
|
<div style={{ color: '#666' }}>加载中...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '100vh',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'white',
|
||||||
|
padding: '40px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '48px', marginBottom: '20px' }}>❌</div>
|
||||||
|
<div style={{ color: '#c00', marginBottom: '20px' }}>{error}</div>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: '#667eea',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
刷新页面
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<header>
|
||||||
|
<div className="header-content">
|
||||||
|
<div>
|
||||||
|
<h1>⚡ 码神的日记系统</h1>
|
||||||
|
<p>记录每天的进步与成长</p>
|
||||||
|
</div>
|
||||||
|
<div className="user-info">
|
||||||
|
<span>👤 {user.username}</span>
|
||||||
|
<button onClick={handleLogout} className="btn-logout">登出</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<StatsPanel stats={stats} />
|
||||||
|
|
||||||
|
<div className="grid-2">
|
||||||
|
<div className="section-box">
|
||||||
|
<h2>📅 日历</h2>
|
||||||
|
<Calendar
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
onDateSelect={handleDateSelect}
|
||||||
|
diaryDates={entries.map(e => e.date)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="section-box">
|
||||||
|
<h2>📝 {selectedDate} 的日记</h2>
|
||||||
|
<DiaryDetail
|
||||||
|
entry={selectedEntry}
|
||||||
|
date={selectedDate}
|
||||||
|
onUpdate={handleDiaryUpdate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="section-box">
|
||||||
|
<ExperienceList
|
||||||
|
experiences={experiences}
|
||||||
|
onRefresh={handleExperienceRefresh}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '20px',
|
||||||
|
color: '#999',
|
||||||
|
fontSize: '14px',
|
||||||
|
marginTop: '30px',
|
||||||
|
}}>
|
||||||
|
<p>© 2026 码神的日记系统 · 记录每一天的成长</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
84
frontend-react/src/components/Calendar.js
Normal file
84
frontend-react/src/components/Calendar.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ 核心组件:日历组件
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* - 显示月历视图
|
||||||
|
* - 标记有日记的日期
|
||||||
|
* - 点击日期选择
|
||||||
|
* - 上月/下月切换
|
||||||
|
*
|
||||||
|
* ⚠️ 不可删除此组件,修改前阅读 docs/CALENDAR.md
|
||||||
|
*/
|
||||||
|
function Calendar({ selectedDate, onDateSelect, diaryDates }) {
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
|
||||||
|
const year = currentDate.getFullYear();
|
||||||
|
const month = currentDate.getMonth();
|
||||||
|
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
const startDay = firstDay.getDay();
|
||||||
|
const totalDays = lastDay.getDate();
|
||||||
|
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
|
||||||
|
const dayNames = ['日', '一', '二', '三', '四', '五', '六'];
|
||||||
|
|
||||||
|
const prevMonth = () => {
|
||||||
|
setCurrentDate(new Date(year, month - 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextMonth = () => {
|
||||||
|
setCurrentDate(new Date(year, month + 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDays = () => {
|
||||||
|
let days = [];
|
||||||
|
|
||||||
|
// 上个月的日期
|
||||||
|
for (let i = 0; i < startDay; i++) {
|
||||||
|
days.push(<div key={`empty-${i}`} className="calendar-day empty"></div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当月日期
|
||||||
|
for (let day = 1; day <= totalDays; day++) {
|
||||||
|
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
const isToday = dateStr === today;
|
||||||
|
const isSelected = dateStr === selectedDate;
|
||||||
|
const hasDiary = diaryDates.includes(dateStr);
|
||||||
|
|
||||||
|
days.push(
|
||||||
|
<div
|
||||||
|
key={day}
|
||||||
|
className={`calendar-day ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''} ${hasDiary ? 'has-diary' : ''}`}
|
||||||
|
onClick={() => onDateSelect(dateStr)}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
{hasDiary && <span className="diary-marker">📝</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="calendar">
|
||||||
|
<div className="calendar-header">
|
||||||
|
<button onClick={prevMonth}>←</button>
|
||||||
|
<span>{year}年 {monthNames[month]}</span>
|
||||||
|
<button onClick={nextMonth}>→</button>
|
||||||
|
</div>
|
||||||
|
<div className="calendar-grid">
|
||||||
|
{dayNames.map(day => (
|
||||||
|
<div key={day} className="calendar-day-header">{day}</div>
|
||||||
|
))}
|
||||||
|
{renderDays()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Calendar;
|
||||||
183
frontend-react/src/components/DiaryDetail.js
Normal file
183
frontend-react/src/components/DiaryDetail.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import DiaryEditor from './DiaryEditor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日记详情组件
|
||||||
|
* 显示日记内容,支持编辑
|
||||||
|
*/
|
||||||
|
function DiaryDetail({ entry, date, onUpdate }) {
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
|
const handleSave = (savedEntry) => {
|
||||||
|
setEditing(false);
|
||||||
|
if (onUpdate) {
|
||||||
|
onUpdate(savedEntry);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
setEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const boxStyle = {
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '20px',
|
||||||
|
minHeight: '300px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelStyle = {
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#667eea',
|
||||||
|
marginBottom: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentStyle = {
|
||||||
|
background: '#f8f9fa',
|
||||||
|
padding: '15px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
lineHeight: '1.6',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyStyle = {
|
||||||
|
color: '#999',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '40px 20px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonStyle = {
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: '#667eea',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
marginTop: '15px',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑模式
|
||||||
|
if (editing) {
|
||||||
|
return (
|
||||||
|
<div style={boxStyle}>
|
||||||
|
<DiaryEditor
|
||||||
|
entry={entry}
|
||||||
|
date={date}
|
||||||
|
onSave={handleSave}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看模式
|
||||||
|
if (!entry) {
|
||||||
|
return (
|
||||||
|
<div style={boxStyle}>
|
||||||
|
<div style={emptyStyle}>
|
||||||
|
<p style={{ fontSize: '48px', marginBottom: '15px' }}>📝</p>
|
||||||
|
<p>这一天还没有日记</p>
|
||||||
|
<button
|
||||||
|
onClick={handleEdit}
|
||||||
|
style={buttonStyle}
|
||||||
|
>
|
||||||
|
✏️ 写一篇
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasContent = entry.completed_tasks || entry.learned || entry.problems ||
|
||||||
|
entry.reflections || entry.improvements || entry.plans;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={boxStyle}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||||
|
<h3 style={{ margin: 0, color: '#333' }}>{entry.title}</h3>
|
||||||
|
<button
|
||||||
|
onClick={handleEdit}
|
||||||
|
style={{
|
||||||
|
...buttonStyle,
|
||||||
|
marginTop: 0,
|
||||||
|
background: '#f0f0f0',
|
||||||
|
color: '#666',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✏️ 编辑
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!hasContent ? (
|
||||||
|
<div style={emptyStyle}>
|
||||||
|
<p>这篇日记还没有内容</p>
|
||||||
|
<button onClick={handleEdit} style={buttonStyle}>
|
||||||
|
✏️ 编辑
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{entry.completed_tasks && (
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<div style={labelStyle}>✅ 完成的任务</div>
|
||||||
|
<div style={contentStyle}>{entry.completed_tasks}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entry.learned && (
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<div style={labelStyle}>💡 学到的东西</div>
|
||||||
|
<div style={contentStyle}>{entry.learned}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entry.problems && (
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<div style={labelStyle}>🚧 遇到的问题</div>
|
||||||
|
<div style={contentStyle}>{entry.problems}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entry.reflections && (
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<div style={labelStyle}>💭 反思和想法</div>
|
||||||
|
<div style={contentStyle}>{entry.reflections}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entry.improvements && (
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<div style={labelStyle}>✨ 进步点</div>
|
||||||
|
<div style={contentStyle}>{entry.improvements}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entry.plans && (
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<div style={labelStyle}>🎯 明日计划</div>
|
||||||
|
<div style={contentStyle}>{entry.plans}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ marginTop: '20px', paddingTop: '15px', borderTop: '1px solid #eee', color: '#999', fontSize: '12px' }}>
|
||||||
|
创建于 {new Date(entry.created_at).toLocaleString('zh-CN')}
|
||||||
|
{entry.updated_at !== entry.created_at && ` · 更新于 ${new Date(entry.updated_at).toLocaleString('zh-CN')}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DiaryDetail;
|
||||||
233
frontend-react/src/components/DiaryEditor.js
Normal file
233
frontend-react/src/components/DiaryEditor.js
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日记编辑器组件
|
||||||
|
* 用于创建和编辑日记
|
||||||
|
*/
|
||||||
|
function DiaryEditor({ entry, date, onSave, onCancel }) {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
completed_tasks: '',
|
||||||
|
learned: '',
|
||||||
|
problems: '',
|
||||||
|
reflections: '',
|
||||||
|
improvements: '',
|
||||||
|
plans: '',
|
||||||
|
});
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (entry) {
|
||||||
|
setFormData({
|
||||||
|
title: entry.title || `${entry.date} 的日记`,
|
||||||
|
content: entry.content || '',
|
||||||
|
completed_tasks: entry.completed_tasks || '',
|
||||||
|
learned: entry.learned || '',
|
||||||
|
problems: entry.problems || '',
|
||||||
|
reflections: entry.reflections || '',
|
||||||
|
improvements: entry.improvements || '',
|
||||||
|
plans: entry.plans || '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
title: `${date} 的日记`,
|
||||||
|
content: '',
|
||||||
|
completed_tasks: '',
|
||||||
|
learned: '',
|
||||||
|
problems: '',
|
||||||
|
reflections: '',
|
||||||
|
improvements: '',
|
||||||
|
plans: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [entry, date]);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
date: entry?.date || date,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response;
|
||||||
|
if (entry && entry.id) {
|
||||||
|
// 更新已有日记
|
||||||
|
response = await axios.put(`/api/entries/${entry.id}/`, payload);
|
||||||
|
} else {
|
||||||
|
// 创建新日记
|
||||||
|
response = await axios.post('/api/entries/', payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || '保存失败,请重试');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputStyle = {
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
resize: 'vertical',
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelStyle = {
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '8px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#555',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sectionStyle = {
|
||||||
|
marginBottom: '20px',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{error && (
|
||||||
|
<div style={{
|
||||||
|
background: '#fee',
|
||||||
|
color: '#c00',
|
||||||
|
padding: '10px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
marginBottom: '15px',
|
||||||
|
}}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}>📝 标题</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, fontWeight: '600' }}
|
||||||
|
placeholder="给今天的日记起个标题"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}>✅ 完成的任务</label>
|
||||||
|
<textarea
|
||||||
|
name="completed_tasks"
|
||||||
|
value={formData.completed_tasks}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, minHeight: '80px' }}
|
||||||
|
placeholder="今天完成了哪些任务?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}>💡 学到的东西</label>
|
||||||
|
<textarea
|
||||||
|
name="learned"
|
||||||
|
value={formData.learned}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, minHeight: '80px' }}
|
||||||
|
placeholder="今天学到了什么新知识或技能?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}>🚧 遇到的问题</label>
|
||||||
|
<textarea
|
||||||
|
name="problems"
|
||||||
|
value={formData.problems}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, minHeight: '80px' }}
|
||||||
|
placeholder="遇到了什么问题?如何解决的?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}>💭 反思和想法</label>
|
||||||
|
<textarea
|
||||||
|
name="reflections"
|
||||||
|
value={formData.reflections}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, minHeight: '80px' }}
|
||||||
|
placeholder="有什么感悟、反思或想法?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}> 进步点</label>
|
||||||
|
<textarea
|
||||||
|
name="improvements"
|
||||||
|
value={formData.improvements}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, minHeight: '80px' }}
|
||||||
|
placeholder="今天有哪些进步?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}>🎯 明日计划</label>
|
||||||
|
<textarea
|
||||||
|
name="plans"
|
||||||
|
value={formData.plans}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, minHeight: '80px' }}
|
||||||
|
placeholder="明天计划做什么?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '10px', marginTop: '25px' }}>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
style={{
|
||||||
|
padding: '12px 24px',
|
||||||
|
background: '#667eea',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: saving ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: saving ? 0.7 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{saving ? '保存中...' : '💾 保存'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
style={{
|
||||||
|
padding: '12px 24px',
|
||||||
|
background: '#f0f0f0',
|
||||||
|
color: '#666',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✖️ 取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DiaryEditor;
|
||||||
210
frontend-react/src/components/ExperienceEditor.js
Normal file
210
frontend-react/src/components/ExperienceEditor.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 经验总结编辑器组件
|
||||||
|
* 用于创建和编辑经验总结
|
||||||
|
*/
|
||||||
|
function ExperienceEditor({ experience, onSave, onCancel }) {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
title: '',
|
||||||
|
category: 'development',
|
||||||
|
problem: '',
|
||||||
|
solution: '',
|
||||||
|
lesson_learned: '',
|
||||||
|
date: new Date().toISOString().split('T')[0],
|
||||||
|
});
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (experience) {
|
||||||
|
setFormData({
|
||||||
|
title: experience.title || '',
|
||||||
|
category: experience.category || 'development',
|
||||||
|
problem: experience.problem || '',
|
||||||
|
solution: experience.solution || '',
|
||||||
|
lesson_learned: experience.lesson_learned || '',
|
||||||
|
date: experience.date || new Date().toISOString().split('T')[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [experience]);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
if (experience && experience.id) {
|
||||||
|
response = await axios.put(`/api/experiences/${experience.id}/`, formData);
|
||||||
|
} else {
|
||||||
|
response = await axios.post('/api/experiences/', formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || '保存失败,请重试');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputStyle = {
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
resize: 'vertical',
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelStyle = {
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '8px',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#555',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sectionStyle = {
|
||||||
|
marginBottom: '20px',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{error && (
|
||||||
|
<div style={{
|
||||||
|
background: '#fee',
|
||||||
|
color: '#c00',
|
||||||
|
padding: '10px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
marginBottom: '15px',
|
||||||
|
}}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}>💡 标题</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, fontWeight: '600' }}
|
||||||
|
placeholder="给这个经验起个标题"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}>📦 类别</label>
|
||||||
|
<select
|
||||||
|
name="category"
|
||||||
|
value={formData.category}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<option value="deployment">📦 部署</option>
|
||||||
|
<option value="development">💻 开发</option>
|
||||||
|
<option value="database">🗄️ 数据库</option>
|
||||||
|
<option value="permission">🔐 权限</option>
|
||||||
|
<option value="network">🌐 网络</option>
|
||||||
|
<option value="other">其他</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}> 日期</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="date"
|
||||||
|
value={formData.date}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={inputStyle}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}>🚨 问题描述</label>
|
||||||
|
<textarea
|
||||||
|
name="problem"
|
||||||
|
value={formData.problem}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, minHeight: '100px' }}
|
||||||
|
placeholder="遇到了什么问题?详细描述一下"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}>✅ 解决方案</label>
|
||||||
|
<textarea
|
||||||
|
name="solution"
|
||||||
|
value={formData.solution}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, minHeight: '100px' }}
|
||||||
|
placeholder="如何解决的?步骤和方法"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<label style={labelStyle}>💎 经验教训</label>
|
||||||
|
<textarea
|
||||||
|
name="lesson_learned"
|
||||||
|
value={formData.lesson_learned}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ ...inputStyle, minHeight: '80px' }}
|
||||||
|
placeholder="从中学到了什么?有什么经验可以分享?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '10px', marginTop: '25px' }}>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
style={{
|
||||||
|
padding: '12px 24px',
|
||||||
|
background: '#667eea',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: saving ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: saving ? 0.7 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{saving ? '保存中...' : '💾 保存'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
style={{
|
||||||
|
padding: '12px 24px',
|
||||||
|
background: '#f0f0f0',
|
||||||
|
color: '#666',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✖️ 取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExperienceEditor;
|
||||||
147
frontend-react/src/components/ExperienceList.js
Normal file
147
frontend-react/src/components/ExperienceList.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import ExperienceEditor from './ExperienceEditor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 经验总结列表组件
|
||||||
|
* 显示经验列表,支持创建新经验
|
||||||
|
*/
|
||||||
|
function ExperienceList({ experiences, onRefresh }) {
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [selectedExp, setSelectedExp] = useState(null);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
setCreating(false);
|
||||||
|
setSelectedExp(null);
|
||||||
|
if (onRefresh) {
|
||||||
|
onRefresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setCreating(false);
|
||||||
|
setSelectedExp(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (exp) => {
|
||||||
|
setSelectedExp(exp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryIcon = (category) => {
|
||||||
|
const icons = {
|
||||||
|
deployment: '📦',
|
||||||
|
development: '💻',
|
||||||
|
database: '🗄️',
|
||||||
|
permission: '🔐',
|
||||||
|
network: '🌐',
|
||||||
|
other: '📝',
|
||||||
|
};
|
||||||
|
return icons[category] || '📝';
|
||||||
|
};
|
||||||
|
|
||||||
|
const boxStyle = {
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '20px',
|
||||||
|
minHeight: '200px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemStyle = {
|
||||||
|
padding: '15px',
|
||||||
|
background: '#f8f9fa',
|
||||||
|
borderRadius: '6px',
|
||||||
|
marginBottom: '12px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonStyle = {
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: '#667eea',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建/编辑模式
|
||||||
|
if (creating || selectedExp) {
|
||||||
|
return (
|
||||||
|
<div style={boxStyle}>
|
||||||
|
<ExperienceEditor
|
||||||
|
experience={selectedExp}
|
||||||
|
onSave={handleSave}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表模式
|
||||||
|
return (
|
||||||
|
<div style={boxStyle}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||||
|
<h3 style={{ margin: 0, color: '#333' }}>💡 经验总结</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setCreating(true)}
|
||||||
|
style={buttonStyle}
|
||||||
|
>
|
||||||
|
➕ 新建
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!experiences || experiences.length === 0 ? (
|
||||||
|
<div style={{
|
||||||
|
color: '#999',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '40px 20px',
|
||||||
|
}}>
|
||||||
|
<p style={{ fontSize: '48px', marginBottom: '15px' }}>💡</p>
|
||||||
|
<p>还没有经验总结</p>
|
||||||
|
<button onClick={() => setCreating(true)} style={{ ...buttonStyle, marginTop: '15px' }}>
|
||||||
|
➕ 创建第一条
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{experiences.map((exp) => (
|
||||||
|
<div
|
||||||
|
key={exp.id}
|
||||||
|
style={itemStyle}
|
||||||
|
onClick={() => handleEdit(exp)}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#e9ecef';
|
||||||
|
e.currentTarget.style.transform = 'translateX(5px)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = '#f8f9fa';
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '10px' }}>
|
||||||
|
<span style={{ fontSize: '24px' }}>{getCategoryIcon(exp.category)}</span>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontWeight: '600', color: '#333', marginBottom: '5px' }}>
|
||||||
|
{exp.title}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '13px', color: '#666', marginBottom: '8px' }}>
|
||||||
|
{exp.problem?.substring(0, 100)}{exp.problem?.length > 100 ? '...' : ''}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||||
|
📅 {new Date(exp.date).toLocaleDateString('zh-CN')} ·
|
||||||
|
📝 {exp.get_category_display || exp.category}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '18px', color: '#999' }}>›</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExperienceList;
|
||||||
77
frontend-react/src/components/Login.js
Normal file
77
frontend-react/src/components/Login.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_BASE = '/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录组件
|
||||||
|
*/
|
||||||
|
function Login({ onLogin }) {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.post(`${API_BASE}/auth/login/`, {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
onLogin(res.data.user);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || '登录失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="auth-container">
|
||||||
|
<div className="auth-card">
|
||||||
|
<h1>⚡ 码神日记系统</h1>
|
||||||
|
<h2>登录</h2>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>用户名</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>密码</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="btn-primary" disabled={loading}>
|
||||||
|
{loading ? '登录中...' : '登录'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p className="auth-tip">
|
||||||
|
还没有账号?<a href="#register" onClick={(e) => { e.preventDefault(); onLogin(null, 'register'); }}>立即注册</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
112
frontend-react/src/components/Register.js
Normal file
112
frontend-react/src/components/Register.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_BASE = '/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册组件
|
||||||
|
*/
|
||||||
|
function Register({ onRegister }) {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError('两次输入的密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
setError('密码至少 6 位');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.post(`${API_BASE}/auth/register/`, {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
onRegister(res.data.user);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.username?.[0] || err.response?.data?.message || '注册失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="auth-container">
|
||||||
|
<div className="auth-card">
|
||||||
|
<h1>⚡ 码神日记系统</h1>
|
||||||
|
<h2>注册</h2>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>用户名</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>邮箱(可选)</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>密码</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="至少 6 位"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>确认密码</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="再次输入密码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="btn-primary" disabled={loading}>
|
||||||
|
{loading ? '注册中...' : '注册'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p className="auth-tip">
|
||||||
|
已有账号?<a href="#login" onClick={(e) => { e.preventDefault(); onRegister(null, 'login'); }}>立即登录</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Register;
|
||||||
38
frontend-react/src/components/StatsPanel.js
Normal file
38
frontend-react/src/components/StatsPanel.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计面板组件
|
||||||
|
* 显示系统统计数据
|
||||||
|
*/
|
||||||
|
function StatsPanel({ stats }) {
|
||||||
|
return (
|
||||||
|
<div className="stats">
|
||||||
|
<div className="stat-card">
|
||||||
|
<h3>{stats.total_entries || 0}</h3>
|
||||||
|
<p>总日记</p>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<h3>{stats.total_tasks || 0}</h3>
|
||||||
|
<p>总任务</p>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<h3>{stats.progressing || 0}</h3>
|
||||||
|
<p>进行中</p>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<h3>{stats.completed || 0}</h3>
|
||||||
|
<p>已完成</p>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<h3>{stats.completion_rate || 0}%</h3>
|
||||||
|
<p>完成率</p>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<h3>{stats.total_experiences || 0}</h3>
|
||||||
|
<p>经验</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatsPanel;
|
||||||
392
frontend-react/src/index.css
Normal file
392
frontend-react/src/index.css
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计面板 */
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h3 {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 1.8em;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区域 */
|
||||||
|
.grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-box {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-box h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日历组件 */
|
||||||
|
.calendar {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header button {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header button:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day-header {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day:hover {
|
||||||
|
background: #eef1f5;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.today {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.selected {
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.has-diary::after {
|
||||||
|
content: '📝';
|
||||||
|
font-size: 0.7em;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.empty {
|
||||||
|
cursor: default;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日记详情 */
|
||||||
|
.diary-detail {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-section {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 经验总结 */
|
||||||
|
.experience-item {
|
||||||
|
padding: 15px;
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
background: #fffbeb;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-category {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-problem, .experience-solution {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-problem-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #dc2626;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-solution-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #059669;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-lesson {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: #fef3c7;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-lesson-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #92400e;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态 */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #fee;
|
||||||
|
color: #c00;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 认证界面 */
|
||||||
|
.auth-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card h1 {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 2em;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card h2 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 1em;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tip {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tip a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tip a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息 */
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255,255,255,0.5);
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout:hover {
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.grid-2 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
frontend-react/src/index.js
Normal file
11
frontend-react/src/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
13
frontend/asset-manifest.json
Normal file
13
frontend/asset-manifest.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"files": {
|
||||||
|
"main.css": "/static/css/main.358dee66.css",
|
||||||
|
"main.js": "/static/js/main.7934ac80.js",
|
||||||
|
"index.html": "/index.html",
|
||||||
|
"main.358dee66.css.map": "/static/css/main.358dee66.css.map",
|
||||||
|
"main.7934ac80.js.map": "/static/js/main.7934ac80.js.map"
|
||||||
|
},
|
||||||
|
"entrypoints": [
|
||||||
|
"static/css/main.358dee66.css",
|
||||||
|
"static/js/main.7934ac80.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
125
frontend/dashboard.html
Normal file
125
frontend/dashboard.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>码神的日记系统 - 仪表盘</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
header h1 { font-size: 2.5em; margin-bottom: 10px; }
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.logout-btn {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.logout-btn:hover { background: #5568d3; }
|
||||||
|
.content {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.content h2 { color: #333; margin-bottom: 20px; }
|
||||||
|
.content p { color: #666; margin-bottom: 30px; }
|
||||||
|
.feature-list {
|
||||||
|
text-align: left;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.feature-item {
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 5px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>⚡ 码神的日记系统</h1>
|
||||||
|
<p>记录每天的进步与成长</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="user-info">
|
||||||
|
<span>👤 欢迎,<strong id="username">用户</strong></span>
|
||||||
|
<button class="logout-btn" onclick="logout()">登出</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<h2>🎉 多用户系统已上线!</h2>
|
||||||
|
<p>你现在可以:</p>
|
||||||
|
<div class="feature-list">
|
||||||
|
<div class="feature-item">
|
||||||
|
<strong>📝 写日记</strong> - 记录每天的任务、学到的东西、反思和进步
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<strong>📅 日历视图</strong> - 通过日历快速查看历史日记
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<strong>💡 经验总结</strong> - 记录遇到的问题和解决方案
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<strong>📊 任务管理</strong> - 追踪任务进展和完成率
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<strong>🔒 数据隔离</strong> - 每个用户的数据完全独立
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 30px; color: #999;">
|
||||||
|
React 前端正在构建中,当前使用简化版界面<br>
|
||||||
|
完整功能即将上线...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 检查登录状态
|
||||||
|
fetch('/api/auth/me/')
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('username').textContent = data.username;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
window.location.href = '/';
|
||||||
|
});
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
fetch('/api/auth/logout/', { method: 'POST' })
|
||||||
|
.then(() => {
|
||||||
|
window.location.href = '/';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,739 +1 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#667eea"/><meta name="description" content="码神的日记系统 - 记录每天的进步与成长"/><title>码神的日记系统</title><script defer="defer" src="/static/js/main.7934ac80.js"></script><link href="/static/css/main.358dee66.css" rel="stylesheet"></head><body><noscript>需要启用 JavaScript 才能运行此应用。</noscript><div id="root"></div></body></html>
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>码神的日记系统</title>
|
|
||||||
<style>
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.container { max-width: 1400px; margin: 0 auto; }
|
|
||||||
header { text-align: center; color: white; margin-bottom: 30px; }
|
|
||||||
header h1 { font-size: 2.5em; margin-bottom: 10px; }
|
|
||||||
header p { opacity: 0.9; }
|
|
||||||
.stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.rating-stats {
|
|
||||||
background: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.rating-stats h3 {
|
|
||||||
color: #333;
|
|
||||||
font-size: 1.1em;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.rating-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
.rating-item {
|
|
||||||
background: linear-gradient(135deg, #fafaff, #f0f0ff);
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-left: 4px solid #667eea;
|
|
||||||
}
|
|
||||||
.rating-item .label {
|
|
||||||
font-size: 0.85em;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.rating-item .value {
|
|
||||||
font-size: 1.5em;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
.rating-item .value.quality { color: #10b981; }
|
|
||||||
.rating-item .value.efficiency { color: #3b82f6; }
|
|
||||||
.rating-item .value.creativity { color: #8b5cf6; }
|
|
||||||
.rating-item .value.learning { color: #f59e0b; }
|
|
||||||
.rating-item .count {
|
|
||||||
font-size: 0.8em;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
.stat-card {
|
|
||||||
background: white;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 10px;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.stat-card h3 { color: #667eea; font-size: 1.8em; margin-bottom: 5px; }
|
|
||||||
.stat-card p { color: #666; font-size: 0.9em; }
|
|
||||||
.tabs { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
|
|
||||||
.tab-btn {
|
|
||||||
padding: 12px 24px;
|
|
||||||
background: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #666;
|
|
||||||
transition: all 0.3s;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.tab-btn:hover { background: #f8f9fa; transform: translateY(-2px); }
|
|
||||||
.tab-btn.active { background: linear-gradient(135deg, #667eea, #764ba2); color: white; }
|
|
||||||
.section-box {
|
|
||||||
background: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.section-box h2 {
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 2px solid #667eea;
|
|
||||||
}
|
|
||||||
.task-item, .diary-item, .experience-item {
|
|
||||||
padding: 15px;
|
|
||||||
border-left: 4px solid #667eea;
|
|
||||||
background: #f8f9fa;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.task-item.status-pending { border-left-color: #6b7280; }
|
|
||||||
.task-item.status-in_progress { border-left-color: #3b82f6; }
|
|
||||||
.task-item.status-blocked { border-left-color: #f59e0b; }
|
|
||||||
.task-item.status-completed { border-left-color: #10b981; }
|
|
||||||
.task-item .header, .diary-item .header, .experience-item .header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.task-item .title, .diary-item .title, .experience-item .title {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
.task-item .status, .experience-item .category {
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.task-item .status { background: #6b7280; }
|
|
||||||
.task-item.status-completed .status { background: #10b981; }
|
|
||||||
.experience-item .category { background: #f59e0b; }
|
|
||||||
.task-item .priority { font-size: 0.8em; color: #666; margin-bottom: 8px; }
|
|
||||||
.task-item .progress-bar {
|
|
||||||
height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; margin: 10px 0;
|
|
||||||
}
|
|
||||||
.task-item .progress-fill {
|
|
||||||
height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); transition: width 0.3s;
|
|
||||||
}
|
|
||||||
.task-item .progress-text { font-size: 0.85em; color: #666; text-align: right; }
|
|
||||||
.diary-item .date { color: #667eea; font-weight: bold; margin-bottom: 10px; }
|
|
||||||
.diary-item .section { margin: 10px 0; }
|
|
||||||
.diary-item .section-title { font-weight: bold; color: #555; }
|
|
||||||
.diary-item .section-content { color: #666; margin-left: 20px; white-space: pre-wrap; }
|
|
||||||
.experience-item .problem, .experience-item .solution, .experience-item .lesson { margin: 10px 0; }
|
|
||||||
.experience-item .problem-title { font-weight: bold; color: #dc2626; margin-bottom: 5px; }
|
|
||||||
.experience-item .solution-title { font-weight: bold; color: #059669; margin-bottom: 5px; }
|
|
||||||
.experience-item .lesson { padding: 10px; background: #fef3c7; border-radius: 5px; }
|
|
||||||
.experience-item .lesson-title { font-weight: bold; color: #92400e; margin-bottom: 5px; }
|
|
||||||
|
|
||||||
/* 批注样式 - 直接附加到内容尾部 */
|
|
||||||
.embedded-comments {
|
|
||||||
margin-top: 15px;
|
|
||||||
padding-top: 15px;
|
|
||||||
border-top: 2px dashed #e0e7ff;
|
|
||||||
}
|
|
||||||
.embedded-comment {
|
|
||||||
background: linear-gradient(135deg, #fafaff, #f0f0ff);
|
|
||||||
padding: 12px 15px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-top: 10px;
|
|
||||||
border-left: 3px solid #667eea;
|
|
||||||
}
|
|
||||||
.embedded-comment .meta {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
.embedded-comment .author {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
.embedded-comment .time {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
.embedded-comment .scores {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.embedded-comment .score-badge {
|
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
||||||
color: white;
|
|
||||||
padding: 3px 10px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.embedded-comment .score-badge.quality { background: linear-gradient(135deg, #10b981, #059669); }
|
|
||||||
.embedded-comment .score-badge.efficiency { background: linear-gradient(135deg, #3b82f6, #2563eb); }
|
|
||||||
.embedded-comment .score-badge.creativity { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
|
|
||||||
.embedded-comment .score-badge.learning { background: linear-gradient(135deg, #f59e0b, #d97706); }
|
|
||||||
.embedded-comment .content {
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.6;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-size: 0.95em;
|
|
||||||
}
|
|
||||||
.embedded-comment .label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #667eea;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-comment-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #667eea;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9em;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 10px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
.add-comment-btn:hover { background: #e0e7ff; }
|
|
||||||
.comment-form {
|
|
||||||
margin-top: 15px;
|
|
||||||
padding: 15px;
|
|
||||||
background: linear-gradient(135deg, #fafaff, #f0f0ff);
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 2px solid #e0e7ff;
|
|
||||||
}
|
|
||||||
.comment-form .dimension-scores {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
.comment-form .dimension {
|
|
||||||
background: white;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #e0e7ff;
|
|
||||||
}
|
|
||||||
.comment-form .dimension label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.85em;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #555;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.comment-form .dimension select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 6px;
|
|
||||||
border: 2px solid #e0e7ff;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
.comment-form textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
border: 2px solid #e0e7ff;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.95em;
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 80px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.comment-form textarea:focus { outline: none; border-color: #667eea; }
|
|
||||||
.comment-form .btn-group { display: flex; gap: 10px; }
|
|
||||||
.comment-form .btn {
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.95em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.comment-form .btn-cancel { background: #f1f5f9; color: #666; }
|
|
||||||
.loading { text-align: center; padding: 40px; color: white; font-size: 1.2em; }
|
|
||||||
.error { background: #fee; color: #c00; padding: 20px; border-radius: 10px; margin-bottom: 20px; }
|
|
||||||
.empty-state { text-align: center; padding: 40px; color: #666; }
|
|
||||||
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 30px; }
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
body { padding: 10px; }
|
|
||||||
header h1 { font-size: 1.8em; }
|
|
||||||
.stats { grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
|
||||||
.stat-card { padding: 10px; }
|
|
||||||
.stat-card h3 { font-size: 1.4em; }
|
|
||||||
.tabs { gap: 8px; }
|
|
||||||
.tab-btn { padding: 10px 16px; font-size: 0.9em; flex: 1; text-align: center; }
|
|
||||||
.section-box { padding: 15px; }
|
|
||||||
.grid-2 { grid-template-columns: 1fr; }
|
|
||||||
.comment-form .dimension-scores { grid-template-columns: repeat(2, 1fr); }
|
|
||||||
}
|
|
||||||
@media (max-width: 400px) {
|
|
||||||
.stats { grid-template-columns: repeat(2, 1fr); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header>
|
|
||||||
<h1>⚡ 码神的日记系统</h1>
|
|
||||||
<p>记录每天的进步与成长</p>
|
|
||||||
</header>
|
|
||||||
<div id="app"><div class="loading">加载中...</div></div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const API_BASE = '/api';
|
|
||||||
let state = {
|
|
||||||
currentTab: 'tasks',
|
|
||||||
selectedDate: new Date().toISOString().split('T')[0],
|
|
||||||
selectedTask: null,
|
|
||||||
allTasks: [],
|
|
||||||
allEntries: [],
|
|
||||||
allExperiences: [],
|
|
||||||
taskStats: {},
|
|
||||||
ratingStats: {},
|
|
||||||
showCommentForm: null,
|
|
||||||
commentContent: '',
|
|
||||||
scores: { quality: '', efficiency: '', creativity: '', learning: '' }
|
|
||||||
};
|
|
||||||
|
|
||||||
async function loadComments(contentType, objectId) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_BASE}/comments/by_content/?content_type=${contentType}&object_id=${objectId}`);
|
|
||||||
return await res.json();
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadData() {
|
|
||||||
try {
|
|
||||||
const [taskStatsRes, tasksRes, diaryStatsRes, entriesRes, expStatsRes, experiencesRes] = await Promise.all([
|
|
||||||
fetch(`${API_BASE}/tasks/stats/`),
|
|
||||||
fetch(`${API_BASE}/tasks/`),
|
|
||||||
fetch(`${API_BASE}/entries/stats/`),
|
|
||||||
fetch(`${API_BASE}/entries/`),
|
|
||||||
fetch(`${API_BASE}/experiences/stats/`),
|
|
||||||
fetch(`${API_BASE}/experiences/`)
|
|
||||||
]);
|
|
||||||
state.taskStats = await taskStatsRes.json();
|
|
||||||
state.allTasks = await tasksRes.json();
|
|
||||||
state.diaryStats = await diaryStatsRes.json();
|
|
||||||
state.allEntries = await entriesRes.json();
|
|
||||||
state.expStats = await expStatsRes.json();
|
|
||||||
state.allExperiences = await experiencesRes.json();
|
|
||||||
|
|
||||||
// 加载所有批注
|
|
||||||
for (const entry of state.allEntries) {
|
|
||||||
entry.comments = await loadComments('diary', entry.id);
|
|
||||||
}
|
|
||||||
for (const task of state.allTasks) {
|
|
||||||
task.comments = await loadComments('task', task.id);
|
|
||||||
}
|
|
||||||
for (const exp of state.allExperiences) {
|
|
||||||
exp.comments = await loadComments('experience', exp.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算评分统计
|
|
||||||
calculateRatingStats();
|
|
||||||
|
|
||||||
render();
|
|
||||||
} catch (error) {
|
|
||||||
document.getElementById('app').innerHTML = `<div class="error">加载失败:${error.message}</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEntryByDate(date) {
|
|
||||||
return state.allEntries.find(entry => entry.date === date);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitComment(contentType, objectId) {
|
|
||||||
const hasScore = Object.values(state.scores).some(s => s !== '');
|
|
||||||
if (!state.commentContent.trim() && !hasScore) {
|
|
||||||
alert('请填写批注内容或选择评分');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
content_type: contentType,
|
|
||||||
object_id: objectId,
|
|
||||||
created_by: '北极星',
|
|
||||||
content: state.commentContent.trim() || '(无文字批注)'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果有维度评分,计算平均分作为总评分
|
|
||||||
const scoreValues = Object.values(state.scores).filter(s => s !== '').map(Number);
|
|
||||||
if (scoreValues.length > 0) {
|
|
||||||
payload.score = Math.round(scoreValues.reduce((a, b) => a + b, 0) / scoreValues.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetch(`${API_BASE}/comments/`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
|
|
||||||
state.showCommentForm = null;
|
|
||||||
state.commentContent = '';
|
|
||||||
state.scores = { quality: '', efficiency: '', creativity: '', learning: '' };
|
|
||||||
loadData();
|
|
||||||
} catch (error) {
|
|
||||||
alert('保存失败:' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderItemRatingStats(comments) {
|
|
||||||
if (!comments || comments.length === 0) return '';
|
|
||||||
|
|
||||||
const stats = { quality: [], efficiency: [], creativity: [], learning: [] };
|
|
||||||
comments.forEach(c => {
|
|
||||||
if (c.quality) stats.quality.push(c.quality);
|
|
||||||
if (c.efficiency) stats.efficiency.push(c.efficiency);
|
|
||||||
if (c.creativity) stats.creativity.push(c.creativity);
|
|
||||||
if (c.learning) stats.learning.push(c.learning);
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasAnyScore = stats.quality.length > 0 || stats.efficiency.length > 0 ||
|
|
||||||
stats.creativity.length > 0 || stats.learning.length > 0;
|
|
||||||
|
|
||||||
if (!hasAnyScore) return '';
|
|
||||||
|
|
||||||
const avg = (arr) => arr.length > 0 ? (arr.reduce((a,b) => a+b, 0) / arr.length).toFixed(1) : '-';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="rating-stats" style="margin-top: 20px; padding: 12px;">
|
|
||||||
<h3 style="font-size: 0.95em; margin-bottom: 10px;">📊 本项评分统计</h3>
|
|
||||||
<div class="rating-grid" style="grid-template-columns: repeat(4, 1fr); gap: 10px;">
|
|
||||||
${stats.quality.length > 0 ? `
|
|
||||||
<div class="rating-item" style="padding: 8px;">
|
|
||||||
<div class="label" style="font-size: 0.75em;">📐 质量</div>
|
|
||||||
<div class="value quality" style="font-size: 1.2em;">${avg(stats.quality)}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${stats.efficiency.length > 0 ? `
|
|
||||||
<div class="rating-item" style="padding: 8px;">
|
|
||||||
<div class="label" style="font-size: 0.75em;">⚡ 效率</div>
|
|
||||||
<div class="value efficiency" style="font-size: 1.2em;">${avg(stats.efficiency)}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${stats.creativity.length > 0 ? `
|
|
||||||
<div class="rating-item" style="padding: 8px;">
|
|
||||||
<div class="label" style="font-size: 0.75em;">💡 创新</div>
|
|
||||||
<div class="value creativity" style="font-size: 1.2em;">${avg(stats.creativity)}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${stats.learning.length > 0 ? `
|
|
||||||
<div class="rating-item" style="padding: 8px;">
|
|
||||||
<div class="label" style="font-size: 0.75em;">📚 学习</div>
|
|
||||||
<div class="value learning" style="font-size: 1.2em;">${avg(stats.learning)}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEmbeddedComments(contentType, objectId, comments) {
|
|
||||||
if (!comments || comments.length === 0) return '';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="embedded-comments">
|
|
||||||
<div class="label">📝 批注与反馈 (${comments.length})</div>
|
|
||||||
${comments.map(c => {
|
|
||||||
const scoreBadges = [];
|
|
||||||
if (c.quality) scoreBadges.push(`<span class="score-badge quality">质量 ${c.quality}</span>`);
|
|
||||||
if (c.efficiency) scoreBadges.push(`<span class="score-badge efficiency">效率 ${c.efficiency}</span>`);
|
|
||||||
if (c.creativity) scoreBadges.push(`<span class="score-badge creativity">创新 ${c.creativity}</span>`);
|
|
||||||
if (c.learning) scoreBadges.push(`<span class="score-badge learning">学习 ${c.learning}</span>`);
|
|
||||||
if (c.score && !scoreBadges.length) scoreBadges.push(`<span class="score-badge">综合 ${c.score}</span>`);
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="embedded-comment">
|
|
||||||
<div class="meta">
|
|
||||||
<span class="author">${c.created_by}</span>
|
|
||||||
<span class="time">${c.created_at.split('T')[0]}</span>
|
|
||||||
</div>
|
|
||||||
${scoreBadges.length > 0 ? `<div class="scores">${scoreBadges.join('')}</div>` : ''}
|
|
||||||
${c.content && c.content !== '(无文字批注)' ? `<div class="content">${c.content}</div>` : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('')}
|
|
||||||
${renderItemRatingStats(comments)}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCommentForm(contentType, objectId) {
|
|
||||||
return `
|
|
||||||
<div class="comment-form">
|
|
||||||
<div class="dimension-scores">
|
|
||||||
<div class="dimension">
|
|
||||||
<label>📐 完成质量</label>
|
|
||||||
<select onchange="state.scores.quality = this.value">
|
|
||||||
<option value="">-</option>
|
|
||||||
${[10,9,8,7,6,5,4,3,2,1].map(n => `<option value="${n}" ${state.scores.quality === String(n) ? 'selected' : ''}>${n}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="dimension">
|
|
||||||
<label>⚡ 效率</label>
|
|
||||||
<select onchange="state.scores.efficiency = this.value">
|
|
||||||
<option value="">-</option>
|
|
||||||
${[10,9,8,7,6,5,4,3,2,1].map(n => `<option value="${n}" ${state.scores.efficiency === String(n) ? 'selected' : ''}>${n}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="dimension">
|
|
||||||
<label>💡 创新性</label>
|
|
||||||
<select onchange="state.scores.creativity = this.value">
|
|
||||||
<option value="">-</option>
|
|
||||||
${[10,9,8,7,6,5,4,3,2,1].map(n => `<option value="${n}" ${state.scores.creativity === String(n) ? 'selected' : ''}>${n}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="dimension">
|
|
||||||
<label>📚 学习价值</label>
|
|
||||||
<select onchange="state.scores.learning = this.value">
|
|
||||||
<option value="">-</option>
|
|
||||||
${[10,9,8,7,6,5,4,3,2,1].map(n => `<option value="${n}" ${state.scores.learning === String(n) ? 'selected' : ''}>${n}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<textarea placeholder="写下你的批注、反馈或指示..." oninput="state.commentContent = this.value">${state.commentContent}</textarea>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn" onclick="submitComment('${contentType}', ${objectId})">💾 保存</button>
|
|
||||||
<button class="btn btn-cancel" onclick="state.showCommentForm = null; state.commentContent = ''; state.scores = {quality:'',efficiency:'',creativity:'',learning:''}; render()">取消</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTasksView() {
|
|
||||||
if (!state.selectedTask) {
|
|
||||||
return `
|
|
||||||
<div class="tabs">
|
|
||||||
<button class="tab-btn active" onclick="switchTab('tasks')">📋 工作任务</button>
|
|
||||||
<button class="tab-btn" onclick="switchTab('diary')">📝 日记</button>
|
|
||||||
<button class="tab-btn" onclick="switchTab('experiences')">💡 经验总结</button>
|
|
||||||
</div>
|
|
||||||
<div class="section-box">
|
|
||||||
<h2>📋 所有任务</h2>
|
|
||||||
${state.allTasks.length === 0 ? '<div class="empty-state">暂无任务</div>' : state.allTasks.map(task => `
|
|
||||||
<div class="task-item status-${task.status}" onclick="selectTask(${task.id})">
|
|
||||||
<div class="header">
|
|
||||||
<span class="title">${task.title}</span>
|
|
||||||
<span class="status">${task.status_display}</span>
|
|
||||||
</div>
|
|
||||||
<div class="priority">优先级:${task.priority_display} | 创建:${task.created_at.split('T')[0]}</div>
|
|
||||||
${task.description ? `<div class="description" style="color:#666;margin:10px0;">${task.description}</div>` : ''}
|
|
||||||
<div class="progress-bar"><div class="progress-fill" style="width: ${task.progress_percent}%"></div></div>
|
|
||||||
<div class="progress-text">进展:${task.progress_percent}%</div>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const task = state.selectedTask;
|
|
||||||
return `
|
|
||||||
<div class="tabs">
|
|
||||||
<button class="tab-btn" onclick="switchTab('tasks')">📋 工作任务</button>
|
|
||||||
<button class="tab-btn" onclick="switchTab('diary')">📝 日记</button>
|
|
||||||
<button class="tab-btn" onclick="switchTab('experiences')">💡 经验总结</button>
|
|
||||||
</div>
|
|
||||||
<div class="section-box">
|
|
||||||
<h2>📋 任务详情</h2>
|
|
||||||
<button class="tab-btn" onclick="state.selectedTask = null; render();" style="margin-bottom:20px;">← 返回列表</button>
|
|
||||||
<div class="task-item status-${task.status}">
|
|
||||||
<div class="header">
|
|
||||||
<span class="title">${task.title}</span>
|
|
||||||
<span class="status">${task.status_display}</span>
|
|
||||||
</div>
|
|
||||||
<div class="priority">优先级:${task.priority_display} | 负责人:${task.assigned_to || '码神'}</div>
|
|
||||||
${task.description ? `<div class="description" style="margin:15px0;color:#666;white-space:pre-wrap;">${task.description}</div>` : ''}
|
|
||||||
<div class="progress-bar"><div class="progress-fill" style="width: ${task.progress_percent}%"></div></div>
|
|
||||||
<div class="progress-text">进展:${task.progress_percent}%</div>
|
|
||||||
</div>
|
|
||||||
${renderEmbeddedComments('task', task.id, task.comments || [])}
|
|
||||||
${state.showCommentForm === `task-${task.id}` ? renderCommentForm('task', task.id) : `
|
|
||||||
<button class="add-comment-btn" onclick="state.showCommentForm = 'task-${task.id}'; render()">📝 添加批注 / 评分</button>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDiaryView() {
|
|
||||||
const entry = getEntryByDate(state.selectedDate);
|
|
||||||
return `
|
|
||||||
<div class="tabs">
|
|
||||||
<button class="tab-btn" onclick="switchTab('tasks')">📋 工作任务</button>
|
|
||||||
<button class="tab-btn active" onclick="switchTab('diary')">📝 日记</button>
|
|
||||||
<button class="tab-btn" onclick="switchTab('experiences')">💡 经验总结</button>
|
|
||||||
</div>
|
|
||||||
<div class="section-box">
|
|
||||||
<h2>📝 ${state.selectedDate} 的日记</h2>
|
|
||||||
${!entry ? '<div class="empty-state">📅 这一天没有日记</div>' : `
|
|
||||||
<div class="diary-item">
|
|
||||||
<div class="header">
|
|
||||||
<span class="title">${entry.title || '每日日记'}</span>
|
|
||||||
<span class="date">${entry.date}</span>
|
|
||||||
</div>
|
|
||||||
${entry.completed_tasks ? `<div class="section"><span class="section-title">✅ 完成的任务</span><div class="section-content">${entry.completed_tasks}</div></div>` : ''}
|
|
||||||
${entry.learned ? `<div class="section"><span class="section-title">📚 学到的东西</span><div class="section-content">${entry.learned}</div></div>` : ''}
|
|
||||||
${entry.reflections ? `<div class="section"><span class="section-title">💡 想法和反思</span><div class="section-content">${entry.reflections}</div></div>` : ''}
|
|
||||||
${entry.improvements ? `<div class="section"><span class="section-title">📈 进步点</span><div class="section-content">${entry.improvements}</div></div>` : ''}
|
|
||||||
</div>
|
|
||||||
${renderEmbeddedComments('diary', entry.id, entry.comments || [])}
|
|
||||||
${state.showCommentForm === `diary-${entry.id}` ? renderCommentForm('diary', entry.id) : `
|
|
||||||
<button class="add-comment-btn" onclick="state.showCommentForm = 'diary-${entry.id}'; render()">📝 添加批注 / 评分</button>
|
|
||||||
`}
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderExperiencesView() {
|
|
||||||
return `
|
|
||||||
<div class="tabs">
|
|
||||||
<button class="tab-btn" onclick="switchTab('tasks')">📋 工作任务</button>
|
|
||||||
<button class="tab-btn" onclick="switchTab('diary')">📝 日记</button>
|
|
||||||
<button class="tab-btn active" onclick="switchTab('experiences')">💡 经验总结</button>
|
|
||||||
</div>
|
|
||||||
<div class="section-box">
|
|
||||||
<h2>💡 经验总结</h2>
|
|
||||||
${state.allExperiences.length === 0 ? '<div class="empty-state">暂无经验总结</div>' : state.allExperiences.map(exp => `
|
|
||||||
<div class="experience-item">
|
|
||||||
<div class="header">
|
|
||||||
<span class="title">${exp.title}</span>
|
|
||||||
<span class="category">${exp.category_display}</span>
|
|
||||||
</div>
|
|
||||||
<div class="problem"><div class="problem-title">🐛 问题</div><div class="section-content">${exp.problem}</div></div>
|
|
||||||
<div class="solution"><div class="solution-title">✅ 解决方案</div><div class="section-content">${exp.solution}</div></div>
|
|
||||||
${exp.lesson_learned ? `<div class="lesson"><div class="lesson-title">📌 经验教训</div><div class="section-content">${exp.lesson_learned}</div></div>` : ''}
|
|
||||||
${renderEmbeddedComments('experience', exp.id, exp.comments || [])}
|
|
||||||
${state.showCommentForm === `experience-${exp.id}` ? renderCommentForm('experience', exp.id) : `
|
|
||||||
<button class="add-comment-btn" onclick="state.showCommentForm = 'experience-${exp.id}'; render()">📝 添加批注 / 评分</button>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
const app = document.getElementById('app');
|
|
||||||
let content = '';
|
|
||||||
if (state.currentTab === 'tasks') content = renderTasksView();
|
|
||||||
else if (state.currentTab === 'diary') content = renderDiaryView();
|
|
||||||
else content = renderExperiencesView();
|
|
||||||
|
|
||||||
app.innerHTML = `
|
|
||||||
<div class="stats">
|
|
||||||
<div class="stat-card"><h3>${state.diaryStats.total_entries || 0}</h3><p>总日记</p></div>
|
|
||||||
<div class="stat-card"><h3>${state.taskStats.total || 0}</h3><p>总任务</p></div>
|
|
||||||
<div class="stat-card"><h3>${state.taskStats.in_progress || 0}</h3><p>进行中</p></div>
|
|
||||||
<div class="stat-card"><h3>${state.taskStats.completed || 0}</h3><p>已完成</p></div>
|
|
||||||
<div class="stat-card"><h3>${state.taskStats.completion_rate || 0}%</h3><p>完成率</p></div>
|
|
||||||
<div class="stat-card"><h3>${state.expStats.total_experiences || 0}</h3><p>经验</p></div>
|
|
||||||
</div>
|
|
||||||
${content}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateRatingStats() {
|
|
||||||
const allComments = [
|
|
||||||
...state.allEntries.flatMap(e => e.comments || []),
|
|
||||||
...state.allTasks.flatMap(t => t.comments || []),
|
|
||||||
...state.allExperiences.flatMap(e => e.comments || [])
|
|
||||||
];
|
|
||||||
|
|
||||||
const stats = {
|
|
||||||
quality: { sum: 0, count: 0 },
|
|
||||||
efficiency: { sum: 0, count: 0 },
|
|
||||||
creativity: { sum: 0, count: 0 },
|
|
||||||
learning: { sum: 0, count: 0 }
|
|
||||||
};
|
|
||||||
|
|
||||||
allComments.forEach(c => {
|
|
||||||
if (c.quality) { stats.quality.sum += c.quality; stats.quality.count++; }
|
|
||||||
if (c.efficiency) { stats.efficiency.sum += c.efficiency; stats.efficiency.count++; }
|
|
||||||
if (c.creativity) { stats.creativity.sum += c.creativity; stats.creativity.count++; }
|
|
||||||
if (c.learning) { stats.learning.sum += c.learning; stats.learning.count++; }
|
|
||||||
});
|
|
||||||
|
|
||||||
state.ratingStats = {
|
|
||||||
quality: stats.quality.count > 0 ? (stats.quality.sum / stats.quality.count).toFixed(1) : '-',
|
|
||||||
efficiency: stats.efficiency.count > 0 ? (stats.efficiency.sum / stats.efficiency.count).toFixed(1) : '-',
|
|
||||||
creativity: stats.creativity.count > 0 ? (stats.creativity.sum / stats.creativity.count).toFixed(1) : '-',
|
|
||||||
learning: stats.learning.count > 0 ? (stats.learning.sum / stats.learning.count).toFixed(1) : '-',
|
|
||||||
totalComments: allComments.length
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRatingStats() {
|
|
||||||
if (state.ratingStats.totalComments === 0) return '';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="rating-stats">
|
|
||||||
<h3>📊 多维度评分统计</h3>
|
|
||||||
<div class="rating-grid">
|
|
||||||
<div class="rating-item">
|
|
||||||
<div class="label">📐 完成质量</div>
|
|
||||||
<div class="value quality">${state.ratingStats.quality}</div>
|
|
||||||
<div class="count">${state.ratingStats.totalComments} 条评价</div>
|
|
||||||
</div>
|
|
||||||
<div class="rating-item">
|
|
||||||
<div class="label">⚡ 效率</div>
|
|
||||||
<div class="value efficiency">${state.ratingStats.efficiency}</div>
|
|
||||||
<div class="count">${state.ratingStats.totalComments} 条评价</div>
|
|
||||||
</div>
|
|
||||||
<div class="rating-item">
|
|
||||||
<div class="label">💡 创新性</div>
|
|
||||||
<div class="value creativity">${state.ratingStats.creativity}</div>
|
|
||||||
<div class="count">${state.ratingStats.totalComments} 条评价</div>
|
|
||||||
</div>
|
|
||||||
<div class="rating-item">
|
|
||||||
<div class="label">📚 学习价值</div>
|
|
||||||
<div class="value learning">${state.ratingStats.learning}</div>
|
|
||||||
<div class="count">${state.ratingStats.totalComments} 条评价</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchTab(tab) { state.currentTab = tab; state.selectedTask = null; render(); }
|
|
||||||
function selectTask(taskId) { state.selectedTask = state.allTasks.find(t => t.id === taskId); render(); }
|
|
||||||
|
|
||||||
loadData();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
149
frontend/login.html
Normal file
149
frontend/login.html
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>码神的日记系统 - 登录</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.auth-card {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.auth-card h1 {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 2em;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.auth-card h2 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background: #fee;
|
||||||
|
color: #c00;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
background: #efe;
|
||||||
|
color: #0a0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="auth-card">
|
||||||
|
<h1>⚡ 码神的日记系统</h1>
|
||||||
|
<h2>登录</h2>
|
||||||
|
|
||||||
|
<div id="message"></div>
|
||||||
|
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>用户名</label>
|
||||||
|
<input type="text" id="username" required placeholder="请输入用户名" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>密码</label>
|
||||||
|
<input type="password" id="password" required placeholder="请输入密码" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-primary">登录</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p style="text-align: center; margin-top: 20px; color: #666;">
|
||||||
|
默认账号:beijixing / beijixing123
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = '/api';
|
||||||
|
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const messageDiv = document.getElementById('message');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/auth/login/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
messageDiv.innerHTML = `<div class="success">✅ 登录成功!正在跳转...</div>`;
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/dashboard.html';
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
messageDiv.innerHTML = `<div class="error">❌ ${data.message || '登录失败'}</div>`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
messageDiv.innerHTML = `<div class="error">❌ 网络错误:${err.message}</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
makemigrations_multiuser.py
Normal file
7
makemigrations_multiuser.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os, sys
|
||||||
|
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'diary_system.settings'
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
sys.argv = ['manage.py', 'makemigrations']
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
53
migrate_data.py
Normal file
53
migrate_data.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
创建默认用户并迁移现有数据
|
||||||
|
"""
|
||||||
|
import os, sys
|
||||||
|
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'diary_system.settings'
|
||||||
|
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from diary.models import DiaryEntry, Experience, Task, Comment
|
||||||
|
|
||||||
|
# 1. 创建默认用户(北极星)
|
||||||
|
print("📝 创建默认用户...")
|
||||||
|
default_user, created = User.objects.get_or_create(
|
||||||
|
username='beijixing',
|
||||||
|
defaults={'email': 'beijixing@example.com'}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
default_user.set_password('beijixing123')
|
||||||
|
default_user.save()
|
||||||
|
print(f"✅ 创建默认用户:beijixing / beijixing123")
|
||||||
|
else:
|
||||||
|
print(f"✅ 默认用户已存在:beijixing")
|
||||||
|
|
||||||
|
# 2. 迁移现有数据到默认用户
|
||||||
|
print("\n📦 迁移现有数据...")
|
||||||
|
|
||||||
|
# 迁移日记
|
||||||
|
diary_count = DiaryEntry.objects.filter(user__isnull=True).count()
|
||||||
|
DiaryEntry.objects.filter(user__isnull=True).update(user=default_user)
|
||||||
|
print(f" 日记:{diary_count} 条")
|
||||||
|
|
||||||
|
# 迁移经验
|
||||||
|
exp_count = Experience.objects.filter(user__isnull=True).count()
|
||||||
|
Experience.objects.filter(user__isnull=True).update(user=default_user)
|
||||||
|
print(f" 经验:{exp_count} 条")
|
||||||
|
|
||||||
|
# 迁移任务
|
||||||
|
task_count = Task.objects.filter(user__isnull=True).count()
|
||||||
|
Task.objects.filter(user__isnull=True).update(user=default_user)
|
||||||
|
print(f" 任务:{task_count} 条")
|
||||||
|
|
||||||
|
# 迁移评论
|
||||||
|
comment_count = Comment.objects.filter(user__isnull=True).count()
|
||||||
|
Comment.objects.filter(user__isnull=True).update(user=default_user)
|
||||||
|
print(f" 评论:{comment_count} 条")
|
||||||
|
|
||||||
|
print("\n✅ 数据迁移完成!")
|
||||||
|
print(f"\n📝 默认用户:beijixing")
|
||||||
|
print(f"🔑 密码:beijixing123")
|
||||||
7
migrate_multiuser.py
Normal file
7
migrate_multiuser.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os, sys
|
||||||
|
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'diary_system.settings'
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
sys.argv = ['manage.py', 'migrate']
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
7
run_syncdb.py
Normal file
7
run_syncdb.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os, sys
|
||||||
|
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'diary_system.settings'
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
sys.argv = ['manage.py', 'migrate', '--run-syncdb']
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
31
save_today.py
Normal file
31
save_today.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os, sys
|
||||||
|
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'diary_system.settings'
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from diary.models import DiaryEntry, Experience
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
entry, _ = DiaryEntry.objects.get_or_create(date=today)
|
||||||
|
entry.title = "教训深刻的一天 - 丢失功能的警示"
|
||||||
|
entry.completed_tasks = "- 开发日记系统 Web 版本\n- 添加经验总结模块\n- 部署到云服务器\n- 创建软件需求文档\n- 配置双 git 仓库同步"
|
||||||
|
entry.learned = "- Git 版本控制的重要性\n- 修改前必须先备份\n- 功能清单必须维护\n- 小步迭代,多次验证\n- 不确定时先问用户"
|
||||||
|
entry.problems = "- 修改前端时覆盖了原有的日历组件\n- 没有查看现有代码就直接修改\n- 没有功能清单对照\n- 修改后没有验证所有功能\n- 导致用户多次要求回退版本"
|
||||||
|
entry.reflections = "这是一个严重的工程习惯问题。作为开发者,应该:1.敬畏现有代码 2.先理解再修改 3.小步快跑 4.文档先行 5.用户沟通。今天的错误虽然造成了时间浪费,但换来了深刻的教训,值得记录。"
|
||||||
|
entry.improvements = "1.创建了 FEATURES.md 功能清单 2.创建了 DEV_GUIDE.md 开发规范 3.创建了 REQUIREMENTS.md 需求文档 4.建立了修改前的备份流程 5.建立了修改后的验证清单"
|
||||||
|
entry.plans = "- 严格执行开发规范 - 每次修改前查看功能清单 - 使用 git 分支开发 - 修改后对照清单验证 - 定期回顾今天的教训"
|
||||||
|
entry.save()
|
||||||
|
print(f"✅ 日记已保存:{entry.date}")
|
||||||
|
|
||||||
|
exp = Experience.objects.create(
|
||||||
|
title="修改前端时丢失日历功能的教训",
|
||||||
|
category="development",
|
||||||
|
problem="在添加经验总结板块时,直接覆盖了 frontend/index.html,导致已有的日历组件丢失。用户发现后要求回退版本,来回折腾了 3 次才恢复到正确的版本。具体问题:1.没有先查看现有代码 2.没有功能清单对照 3.没有使用 git 分支开发 4.修改后没有验证所有功能 5.自作主张添加了不需要的功能",
|
||||||
|
solution="1.立即使用 git restore 恢复文件 2.回退到正确的 commit 3.同步到云服务器 4.创建功能清单和开发规范 5.建立修改前后的检查流程。长期解决方案:维护 FEATURES.md 功能清单、维护 DEV_GUIDE.md 开发规范、修改前创建 git 备份分支、使用功能分支开发、修改后对照清单验证",
|
||||||
|
lesson_learned="📌 核心教训:1.敬畏现有代码 - 每行代码都有它的价值,不要随意覆盖 2.先理解再修改 - 修改前必须先查看现有代码和功能 3.小步迭代 - 每次只改一个功能,立即验证 4.文档先行 - 功能清单、需求文档必须在开发前维护 5.用户沟通 - 不确定时先问用户,不要自作主张 6.版本控制 - 善用 git 分支和回滚功能。💡 这个教训的价值远超今天浪费的时间,它会成为以后开发中的警示灯。",
|
||||||
|
date=today
|
||||||
|
)
|
||||||
|
print(f"✅ 经验总结已保存:{exp.title}")
|
||||||
27
setup_auth_tables.py
Normal file
27
setup_auth_tables.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
手动创建 auth 相关表
|
||||||
|
"""
|
||||||
|
import os, sys
|
||||||
|
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'diary_system.settings'
|
||||||
|
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
# 先迁移 auth 和 contenttypes
|
||||||
|
print("📦 创建 auth 相关表...")
|
||||||
|
call_command('migrate', 'contenttypes', '--run-syncdb')
|
||||||
|
call_command('migrate', 'auth', '--run-syncdb')
|
||||||
|
call_command('migrate', 'admin', '--run-syncdb')
|
||||||
|
call_command('migrate', 'sessions', '--run-syncdb')
|
||||||
|
|
||||||
|
print("✅ auth 表创建完成")
|
||||||
|
|
||||||
|
# 再运行 diary 迁移
|
||||||
|
print("\n📦 运行 diary 迁移...")
|
||||||
|
call_command('migrate', 'diary', '--run-syncdb')
|
||||||
|
print("✅ diary 迁移完成")
|
||||||
145
test_frontend.py
Normal file
145
test_frontend.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
日记系统前端功能自动化测试
|
||||||
|
支持针对性测试:修改什么测什么
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python3 test_frontend.py # 全量测试
|
||||||
|
python3 test_frontend.py diary # 只测日记相关
|
||||||
|
python3 test_frontend.py experience # 只测经验总结
|
||||||
|
python3 test_frontend.py api # 只测 API
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
|
||||||
|
BASE_URL = 'http://127.0.0.1:8001'
|
||||||
|
API_BASE = f'{BASE_URL}/api'
|
||||||
|
|
||||||
|
def test_diary_api():
|
||||||
|
"""测试日记 API"""
|
||||||
|
print("📡 测试日记 API...")
|
||||||
|
tests = [
|
||||||
|
('日记统计', f'{API_BASE}/entries/stats/'),
|
||||||
|
('日记列表', f'{API_BASE}/entries/'),
|
||||||
|
('今日日记', f'{API_BASE}/entries/today/'),
|
||||||
|
('最近日记', f'{API_BASE}/entries/recent/'),
|
||||||
|
]
|
||||||
|
return run_tests(tests)
|
||||||
|
|
||||||
|
def test_experience_api():
|
||||||
|
"""测试经验总结 API"""
|
||||||
|
print("📡 测试经验总结 API...")
|
||||||
|
tests = [
|
||||||
|
('经验列表', f'{API_BASE}/experiences/'),
|
||||||
|
('最近经验', f'{API_BASE}/experiences/recent/'),
|
||||||
|
]
|
||||||
|
return run_tests(tests)
|
||||||
|
|
||||||
|
def test_frontend_core():
|
||||||
|
"""测试前端核心功能(日历组件)"""
|
||||||
|
print("\n🌐 测试前端核心功能...")
|
||||||
|
try:
|
||||||
|
resp = requests.get(BASE_URL, timeout=5)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
print(f" ❌ 页面访问失败:{resp.status_code}")
|
||||||
|
return 0, 1
|
||||||
|
|
||||||
|
content = resp.text
|
||||||
|
checks = [
|
||||||
|
('日历组件', 'calendar'),
|
||||||
|
('选择日期函数', 'selectDate'),
|
||||||
|
('渲染日历函数', 'renderCalendar'),
|
||||||
|
]
|
||||||
|
return check_content(content, checks)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ 页面访问错误:{str(e)}")
|
||||||
|
return 0, 1
|
||||||
|
|
||||||
|
def test_frontend_experience():
|
||||||
|
"""测试经验总结前端"""
|
||||||
|
print("\n🌐 测试经验总结前端...")
|
||||||
|
try:
|
||||||
|
resp = requests.get(BASE_URL, timeout=5)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
return 0, 1
|
||||||
|
|
||||||
|
content = resp.text
|
||||||
|
checks = [
|
||||||
|
('经验总结 Tab', 'experience'),
|
||||||
|
('经验分类', 'category'),
|
||||||
|
]
|
||||||
|
return check_content(content, checks)
|
||||||
|
except Exception as e:
|
||||||
|
return 0, 1
|
||||||
|
|
||||||
|
def run_tests(tests):
|
||||||
|
"""运行测试用例"""
|
||||||
|
passed = failed = 0
|
||||||
|
for name, url in tests:
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
print(f" ✅ {name}")
|
||||||
|
passed += 1
|
||||||
|
else:
|
||||||
|
print(f" ❌ {name}: {resp.status_code}")
|
||||||
|
failed += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ {name}: {str(e)}")
|
||||||
|
failed += 1
|
||||||
|
return passed, failed
|
||||||
|
|
||||||
|
def check_content(content, checks):
|
||||||
|
"""检查页面内容"""
|
||||||
|
passed = failed = 0
|
||||||
|
for name, keyword in checks:
|
||||||
|
if keyword in content:
|
||||||
|
print(f" ✅ {name}")
|
||||||
|
passed += 1
|
||||||
|
else:
|
||||||
|
print(f" ❌ {name}: 缺失")
|
||||||
|
failed += 1
|
||||||
|
return passed, failed
|
||||||
|
|
||||||
|
def main():
|
||||||
|
scope = sys.argv[1] if len(sys.argv) > 1 else 'all'
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"🧪 日记系统测试 [{' + '.join(sys.argv[1:]) if len(sys.argv) > 1 else '全量'}]")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
total_passed = total_failed = 0
|
||||||
|
|
||||||
|
if scope in ['all', 'api', 'diary']:
|
||||||
|
p, f = test_diary_api()
|
||||||
|
total_passed += p
|
||||||
|
total_failed += f
|
||||||
|
|
||||||
|
if scope in ['all', 'api', 'experience']:
|
||||||
|
p, f = test_experience_api()
|
||||||
|
total_passed += p
|
||||||
|
total_failed += f
|
||||||
|
|
||||||
|
if scope in ['all', 'diary']:
|
||||||
|
p, f = test_frontend_core()
|
||||||
|
total_passed += p
|
||||||
|
total_failed += f
|
||||||
|
|
||||||
|
if scope == 'experience':
|
||||||
|
p, f = test_frontend_experience()
|
||||||
|
total_passed += p
|
||||||
|
total_failed += f
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"📊 测试结果:{total_passed} 通过,{total_failed} 失败")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if total_failed > 0:
|
||||||
|
print("\n⚠️ 测试失败,请检查!")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("\n✅ 测试通过!")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
98
today_record.py
Normal file
98
today_record.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""记录今天的日记和经验总结"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'diary_system.settings')
|
||||||
|
sys.path.insert(0, '/root/.openclaw/workspace/diary-system/backend')
|
||||||
|
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from diary.models import DiaryEntry, Experience
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
# ============ 创建日记 ============
|
||||||
|
today = date.today()
|
||||||
|
entry, created = DiaryEntry.objects.get_or_create(date=today)
|
||||||
|
|
||||||
|
entry.title = "教训深刻的一天 - 丢失功能的警示"
|
||||||
|
entry.completed_tasks = """- 开发日记系统 Web 版本
|
||||||
|
- 添加经验总结模块
|
||||||
|
- 部署到云服务器
|
||||||
|
- 创建软件需求文档
|
||||||
|
- 配置双 git 仓库同步"""
|
||||||
|
|
||||||
|
entry.learned = """- Git 版本控制的重要性
|
||||||
|
- 修改前必须先备份
|
||||||
|
- 功能清单必须维护
|
||||||
|
- 小步迭代,多次验证
|
||||||
|
- 不确定时先问用户"""
|
||||||
|
|
||||||
|
entry.problems = """- 修改前端时覆盖了原有的日历组件
|
||||||
|
- 没有查看现有代码就直接修改
|
||||||
|
- 没有功能清单对照
|
||||||
|
- 修改后没有验证所有功能
|
||||||
|
- 导致用户多次要求回退版本"""
|
||||||
|
|
||||||
|
entry.reflections = """这是一个严重的工程习惯问题。作为开发者,应该:
|
||||||
|
1. 敬畏现有代码 - 每行代码都有价值
|
||||||
|
2. 先理解再修改 - 不要盲目覆盖
|
||||||
|
3. 小步快跑 - 每次只改一点,立即验证
|
||||||
|
4. 文档先行 - 功能清单、需求文档必须维护
|
||||||
|
5. 用户沟通 - 不确定时先问,不要自作主张
|
||||||
|
|
||||||
|
今天的错误虽然造成了时间浪费,但换来了深刻的教训,值得记录。"""
|
||||||
|
|
||||||
|
entry.improvements = """1. 创建了 FEATURES.md 功能清单
|
||||||
|
2. 创建了 DEV_GUIDE.md 开发规范
|
||||||
|
3. 创建了 REQUIREMENTS.md 需求文档
|
||||||
|
4. 建立了修改前的备份流程
|
||||||
|
5. 建立了修改后的验证清单"""
|
||||||
|
|
||||||
|
entry.plans = """- 严格执行开发规范
|
||||||
|
- 每次修改前查看功能清单
|
||||||
|
- 使用 git 分支开发
|
||||||
|
- 修改后对照清单验证
|
||||||
|
- 定期回顾今天的教训"""
|
||||||
|
|
||||||
|
entry.save()
|
||||||
|
print(f"✅ 日记已保存:{entry.date}")
|
||||||
|
|
||||||
|
# ============ 创建经验总结 ============
|
||||||
|
exp = Experience.objects.create(
|
||||||
|
title="修改前端时丢失日历功能的教训",
|
||||||
|
category="development",
|
||||||
|
problem="""在添加经验总结板块时,直接覆盖了 frontend/index.html,导致已有的日历组件丢失。
|
||||||
|
用户发现后要求回退版本,来回折腾了 3 次才恢复到正确的版本。
|
||||||
|
|
||||||
|
具体问题:
|
||||||
|
1. 没有先查看现有代码
|
||||||
|
2. 没有功能清单对照
|
||||||
|
3. 没有使用 git 分支开发
|
||||||
|
4. 修改后没有验证所有功能
|
||||||
|
5. 自作主张添加了不需要的功能""",
|
||||||
|
solution="""1. 立即使用 git restore 恢复文件
|
||||||
|
2. 回退到正确的 commit (4aeb21c)
|
||||||
|
3. 同步到云服务器
|
||||||
|
4. 创建功能清单和开发规范
|
||||||
|
5. 建立修改前后的检查流程
|
||||||
|
|
||||||
|
长期解决方案:
|
||||||
|
- 维护 FEATURES.md 功能清单
|
||||||
|
- 维护 DEV_GUIDE.md 开发规范
|
||||||
|
- 修改前创建 git 备份分支
|
||||||
|
- 使用功能分支开发
|
||||||
|
- 修改后对照清单验证""",
|
||||||
|
lesson_learned="""📌 核心教训:
|
||||||
|
1. **敬畏现有代码** - 每行代码都有它的价值,不要随意覆盖
|
||||||
|
2. **先理解再修改** - 修改前必须先查看现有代码和功能
|
||||||
|
3. **小步迭代** - 每次只改一个功能,立即验证
|
||||||
|
4. **文档先行** - 功能清单、需求文档必须在开发前维护
|
||||||
|
5. **用户沟通** - 不确定时先问用户,不要自作主张
|
||||||
|
6. **版本控制** - 善用 git 分支和回滚功能
|
||||||
|
|
||||||
|
💡 这个教训的价值远超今天浪费的时间,它会成为以后开发中的警示灯。"""
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✅ 经验总结已保存:{exp.title}")
|
||||||
|
print(f" 类别:{exp.get_category_display()}")
|
||||||
Reference in New Issue
Block a user