Compare commits

...

11 Commits

Author SHA1 Message Date
maoshen
6b31779ab4 feat: 完善日记系统前端(创建/编辑功能)
新增功能:
- ✏️ DiaryEditor 组件 - 日记创建/编辑器
- 💡 ExperienceEditor 组件 - 经验总结创建/编辑器
- 🔄 DiaryDetail 组件 - 支持查看/编辑切换
- 🔄 ExperienceList 组件 - 支持创建/编辑经验

改进:
- 📱 优化 UI 样式和交互体验
- 🔐 完善认证流程
- 📅 日历组件保持不变(核心功能)

技术栈:
- React 18 + Axios
- MobX (状态管理)
- 原生 CSS
2026-04-15 07:22:11 +00:00
maoshen
8aa7a34895 feat: 多用户系统发布(登录/注册 + 数据隔离 + 简化前端) 2026-04-15 03:16:38 +00:00
maoshen
ae2a6d912f feat: 完成多用户系统(登录/注册界面 + 用户隔离) 2026-04-15 03:02:54 +00:00
maoshen
e6aecd2752 feat: 多用户系统改造(数据模型 + 认证 API) 2026-04-15 02:59:36 +00:00
maoshen
75423d4e0e feat: React 重构前端(组件化,清晰逻辑) 2026-04-15 02:39:18 +00:00
maoshen
c8178ce98f docs: 添加日历功能警告注释(防止误删) 2026-04-15 02:16:19 +00:00
maoshen
e91b58b079 docs: 创建分模块需求文档(每个模块独立说明) 2026-04-15 01:54:13 +00:00
maoshen
d95174a0c4 refactor: 测试脚本支持针对性测试(修改什么测什么) 2026-04-15 01:38:19 +00:00
maoshen
af4c4826ff docs: 精简文档,只保留核心测试和详细需求 2026-04-15 01:35:49 +00:00
maoshen
418104aed1 docs: 添加详细需求说明和前端检查清单(防止丢失功能的强制机制) 2026-04-15 01:29:46 +00:00
maoshen
35664f9d56 docs: 添加 2026-04-15 教训日的记录脚本 2026-04-15 01:25:46 +00:00
51 changed files with 20454 additions and 1017 deletions

2
.gitignore vendored
View File

@@ -11,3 +11,5 @@ media/
*.swp
*.swo
*~
frontend-react/node_modules/
frontend-react/build/

View File

@@ -1,112 +0,0 @@
# 日记系统 - 开发规范
## ⚠️ 血泪教训2026-04-15
**问题**:修改前端时丢失了已有的日历功能
**原因**
1. 没有先查看现有代码
2. 没有功能清单对照
3. 直接在 main 分支修改
4. 修改后没有验证所有功能
---
## 📋 修改代码前的必须步骤
### 1. 查看当前功能清单
```bash
# 查看 git 历史了解功能演进
git log --oneline -20
# 查看当前有哪些文件
git ls-files
# 查看最近的改动
git diff HEAD~5
```
### 2. 备份当前版本
```bash
# 创建备份分支
git branch backup-$(date +%Y%m%d-%H%M)
# 或者至少复制关键文件
cp frontend/index.html frontend/index.html.bak
```
### 3. 使用分支开发
```bash
# 创建功能分支
git checkout -b feature/xxx
# 开发完成后合并
git checkout main
git merge feature/xxx
```
### 4. 修改后验证清单
- [ ] 日历组件是否正常
- [ ] 统计面板数据是否正确
- [ ] Tab 切换是否正常
- [ ] API 接口是否可用
- [ ] 云服务器是否同步
---
## 📁 必须维护的文档
### 1. FEATURES.md - 功能清单
记录所有已实现的功能,每次修改前对照检查。
### 2. CHANGELOG.md - 变更日志
记录每次修改的内容,便于回滚。
### 3. API.md - 接口文档
记录所有 API 接口,避免删除后端接口。
---
## 🔄 标准开发流程
```
1. 查看功能清单 (FEATURES.md)
2. 创建备份分支
3. 创建功能分支开发
4. 本地测试所有功能
5. 同步到测试环境验证
6. 合并到 main
7. 部署到生产环境
8. 更新功能清单
```
---
## 🛑 禁止操作
- ❌ 直接覆盖 `index.html` 而不查看原有内容
- ❌ 在 main 分支直接开发新功能
- ❌ 修改后不验证就部署
- ❌ 删除文件前不确认是否还在使用
---
## ✅ 推荐操作
- ✅ 小步迭代,每次只改一个功能
- ✅ 使用 `git diff` 查看改动
- ✅ 修改前后截图对比
- ✅ 保持向后兼容
- ✅ 不确定时先问用户
---
_最后更新2026-04-15_
_创建原因:防止再次丢失已有功能_

View File

@@ -1,142 +0,0 @@
# 日记系统 - 功能清单
_最后更新2026-04-15_
---
## ✅ 已实现功能
### 后端 API
#### 日记模块 (`/api/entries/`)
- [x] `GET /` - 获取所有日记
- [x] `GET /{id}/` - 获取单条日记
- [x] `POST /` - 创建日记
- [x] `PUT /{id}/` - 更新日记
- [x] `DELETE /{id}/` - 删除日记
- [x] `GET /today/` - 获取今天的日记
- [x] `GET /recent/` - 获取最近 7 天的日记
- [x] `GET /stats/` - 获取统计信息
#### 经验总结模块 (`/api/experiences/`)
- [x] `GET /` - 获取所有经验
- [x] `GET /{id}/` - 获取单条经验
- [x] `POST /` - 创建经验
- [x] `PUT /{id}/` - 更新经验
- [x] `DELETE /{id}/` - 删除经验
- [x] `GET /recent/` - 获取最近 10 条经验
- [x] `GET /by_category/` - 按类别分组
#### 进度追踪模块 (`/api/progress/`)
- [x] `GET /` - 获取所有进度
- [x] `GET /{id}/` - 获取单条进度
- [x] `POST /` - 创建进度
- [x] `PUT /{id}/` - 更新进度
- [x] `DELETE /{id}/` - 删除进度
### 前端页面
#### 统计面板
- [x] 总日记数
- [x] 总任务数
- [x] 进行中任务
- [x] 已完成任务
- [x] 完成率
- [x] 经验总结数
#### 日历组件 ⭐
- [x] 月历视图
- [x] 有日记的日期标记 📝
- [x] 今天高亮显示
- [x] 点击日期查看详情
- [x] 上月/下月切换
- [x] 星期标题
#### Tab 切换
- [x] 工作任务 Tab
- [x] 日记 Tab
- [x] 经验总结 Tab
#### 日记展示
- [x] 日历视图(主视图)
- [x] 列表视图
- [x] 详情展示
- [x] 日期选择
#### 经验总结展示
- [x] 列表展示
- [x] 分类标签
- [x] 问题/解决方案格式
- [x] 经验教训高亮
### 数据模型
#### DiaryEntry
- [x] date - 日期
- [x] title - 标题
- [x] completed_tasks - 完成的任务
- [x] learned - 学到的东西
- [x] problems - 遇到的问题
- [x] reflections - 想法和反思
- [x] improvements - 进步点
- [x] plans - 明日计划
- [x] created_at - 创建时间
- [x] updated_at - 更新时间
#### Experience
- [x] title - 标题
- [x] category - 类别deployment/development/database/permission/network/other
- [x] problem - 问题描述
- [x] solution - 解决方案
- [x] lesson_learned - 经验教训
- [x] date - 日期
- [x] created_at - 创建时间
#### DailyProgress
- [x] entry - 关联日记
- [x] category - 类别
- [x] description - 描述
- [x] progress_percent - 进度百分比
- [x] created_at - 创建时间
### 部署
#### 本地部署
- [x] Gunicorn 服务(端口 8002
- [x] Nginx 反向代理(端口 8001
- [x] PostgreSQL 数据库
#### 云服务器部署
- [x] Gunicorn systemd 服务
- [x] Nginx 反向代理
- [x] SQLite 数据库
- [x] 访问地址http://cssc.datalibstar.com:8001/
---
## 🚧 计划功能
- [ ] 日记创建/编辑表单
- [ ] 经验总结创建表单
- [ ] 搜索功能
- [ ] 数据导出Markdown
- [ ] 用户认证
- [ ] 数据备份
---
## 📝 修改检查清单
每次修改代码前,对照此清单确保不丢失功能:
- [ ] 日历组件是否正常
- [ ] 统计面板数据是否正确
- [ ] Tab 切换是否正常
- [ ] 日记列表是否显示
- [ ] 经验总结是否显示
- [ ] API 接口是否可用
- [ ] 云服务器是否同步更新
---
_此文档必须在每次功能变更后更新_

80
MULTI_USER_PLAN.md Normal file
View 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. **用户隔离** - 确保用户只能访问自己的数据
---
_确认改造后开始实施_

View File

@@ -43,15 +43,32 @@ _文档状态**进行中**
### 2.1 日记管理 ✅
| ID | 需求 | 优先级 | 状态 |
|----|------|--------|------|
| F-001 | 创建日记条目 | P0 | ✅ 已实现 |
| F-002 | 编辑日记内容 | P0 | ✅ 已实现 |
| F-003 | 删除日记 | P1 | ⏳ 待实现 |
| F-004 | 查看今日日记 | P0 | ✅ 已实现 |
| F-005 | 查看历史日记 | P0 | ✅ 已实现 |
| F-006 | 日历视图展示 | P0 | ✅ 已实现 |
| F-007 | 日记内容包含完成任务、学到的东西、问题、反思、进步、计划 | P0 | ✅ 已实现 |
| 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 经验总结 ✅

View 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("用户名或密码错误")

View 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'),
]

View 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

View File

@@ -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')},
),
]

View File

@@ -1,9 +1,24 @@
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
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='每日日记')
content = models.TextField('日记内容', blank=True, default='')
completed_tasks = models.TextField('完成的任务', blank=True, default='')
@@ -22,6 +37,7 @@ class DiaryEntry(models.Model):
ordering = ['-date']
verbose_name = '日记'
verbose_name_plural = '日记'
unique_together = ['user', 'date'] # 每个用户每天一条日记
def __str__(self):
return f"{self.date} - {self.title}"
@@ -47,7 +63,17 @@ class DiaryEntry(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)
category = models.CharField('类别', max_length=50, choices=[
('deployment', '📦 部署'),
@@ -112,7 +138,7 @@ class Comment(models.Model):
creativity = 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)
class Meta:
@@ -147,7 +173,8 @@ class Task(models.Model):
priority = models.CharField('优先级', max_length=20, choices=PRIORITY_CHOICES, default='medium')
progress_percent = models.IntegerField('进展百分比', default=0)
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)
completed_at = models.DateTimeField('完成时间', null=True, blank=True)
created_at = models.DateTimeField('创建时间', auto_now_add=True)

View File

@@ -3,11 +3,11 @@ from rest_framework.routers import DefaultRouter
from .views import DiaryEntryViewSet, DailyProgressViewSet, ExperienceViewSet, TaskViewSet, CommentViewSet
router = DefaultRouter()
router.register(r'entries', DiaryEntryViewSet)
router.register(r'progress', DailyProgressViewSet)
router.register(r'experiences', ExperienceViewSet)
router.register(r'tasks', TaskViewSet)
router.register(r'comments', CommentViewSet)
router.register(r'entries', DiaryEntryViewSet, basename='diaryentry')
router.register(r'progress', DailyProgressViewSet, basename='dailyprogress')
router.register(r'experiences', ExperienceViewSet, basename='experience')
router.register(r'tasks', TaskViewSet, basename='task')
router.register(r'comments', CommentViewSet, basename='comment')
urlpatterns = [
path('', include(router.urls)),

View File

@@ -1,6 +1,7 @@
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import permissions
from django.utils import timezone
from .models import DiaryEntry, DailyProgress, Experience, Task, Comment
from .serializers import (
@@ -9,21 +10,24 @@ from .serializers import (
)
class DiaryEntryViewSet(viewsets.ModelViewSet):
queryset = DiaryEntry.objects.all()
serializer_class = DiaryEntrySerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return DiaryEntry.objects.filter(user=self.request.user)
@action(detail=False, methods=['get'])
def today(self, request):
"""获取今天的日记"""
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)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def recent(self, request):
"""获取最近 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)
return Response(serializer.data)
@@ -73,8 +77,11 @@ class DailyProgressViewSet(viewsets.ModelViewSet):
class ExperienceViewSet(viewsets.ModelViewSet):
queryset = Experience.objects.all()
serializer_class = ExperienceSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Experience.objects.filter(user=self.request.user)
@action(detail=False, methods=['get'])
def by_category(self, request):
@@ -96,8 +103,11 @@ class ExperienceViewSet(viewsets.ModelViewSet):
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Task.objects.filter(user=self.request.user)
@action(detail=False, methods=['get'])
def by_status(self, request):

View File

@@ -21,8 +21,10 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
'diary',
'authentication',
]
MIDDLEWARE = [

View File

@@ -6,5 +6,6 @@ from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/auth/', include('authentication.urls')),
path('api/', include('diary.urls')),
]

7
create_auth_app.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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/` 对应文档。

View 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"
]
}

View 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

File diff suppressed because it is too large Load Diff

View 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"
]
}
}

View 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
View 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;

View 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}>&#8592;</button>
<span>{year} {monthNames[month]}</span>
<button onClick={nextMonth}>&#8594;</button>
</div>
<div className="calendar-grid">
{dayNames.map(day => (
<div key={day} className="calendar-day-header">{day}</div>
))}
{renderDays()}
</div>
</div>
);
}
export default Calendar;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;
}
}

View 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>
);

View 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
View 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>

View File

@@ -1,739 +1 @@
<!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: 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>
<!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>

149
frontend/login.html Normal file
View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()}")