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