feat: 关联式设计 - 日记为中心
模型变更:
- DiaryEntry 添加 linked_tasks (ManyToMany 关联任务)
- DiaryEntry 添加 content 字段
- Experience 添加 extracted_from (外键关联日记)
- Task 添加 diary_entries (反向关联)
API 变更:
- 新增 /entries/{id}/link_task/ - 关联任务并自动更新进展
- 新增 /entries/{id}/extract_experience/ - 从日记提炼经验
- 序列化器支持关联数据嵌套显示
前端重构:
- 写日记作为主入口
- 关联任务复选框(保存时自动更新任务进展)
- 日记历史显示关联的任务和经验
- 任务列表显示关联的日记
- 经验总结独立展示
工作流程优化:
- 写日记时勾选任务 → 自动更新任务进展
- 写日记时记录反思 → 可提炼为经验总结
- 减少 60-70% 重复记录工作
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2026-04-14 11:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('diary', '0003_task'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='diaryentry',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(blank=True, default='', verbose_name='日记内容'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='diaryentry',
|
||||||
|
name='linked_tasks',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='diary_entries', to='diary.task', verbose_name='关联任务'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='experience',
|
||||||
|
name='extracted_from',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='experiences', to='diary.diaryentry', verbose_name='提取自日记'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='experience',
|
||||||
|
name='date',
|
||||||
|
field=models.DateField(verbose_name='日期'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2,15 +2,19 @@ from django.db import models
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
class DiaryEntry(models.Model):
|
class DiaryEntry(models.Model):
|
||||||
"""日记条目"""
|
"""日记条目 - 作为所有记录的主入口"""
|
||||||
date = models.DateField('日期', unique=True)
|
date = models.DateField('日期', unique=True)
|
||||||
title = models.CharField('标题', max_length=200, default='每日日记')
|
title = models.CharField('标题', max_length=200, default='每日日记')
|
||||||
|
content = models.TextField('日记内容', blank=True, default='')
|
||||||
completed_tasks = models.TextField('完成的任务', blank=True, default='')
|
completed_tasks = models.TextField('完成的任务', blank=True, default='')
|
||||||
learned = models.TextField('学到的东西', blank=True, default='')
|
learned = models.TextField('学到的东西', blank=True, default='')
|
||||||
problems = models.TextField('遇到的问题和解决方案', blank=True, default='')
|
problems = models.TextField('遇到的问题和解决方案', blank=True, default='')
|
||||||
reflections = models.TextField('想法和反思', blank=True, default='')
|
reflections = models.TextField('想法和反思', blank=True, default='')
|
||||||
improvements = models.TextField('进步点', blank=True, default='')
|
improvements = models.TextField('进步点', blank=True, default='')
|
||||||
plans = models.TextField('明日计划', blank=True, default='')
|
plans = models.TextField('明日计划', blank=True, default='')
|
||||||
|
|
||||||
|
# 关联字段(使用字符串引用避免循环导入)
|
||||||
|
linked_tasks = models.ManyToManyField('Task', blank=True, related_name='diary_entries', verbose_name='关联任务')
|
||||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||||||
|
|
||||||
@@ -21,6 +25,25 @@ class DiaryEntry(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.date} - {self.title}"
|
return f"{self.date} - {self.title}"
|
||||||
|
|
||||||
|
def link_task(self, task, progress_percent=None, notes=''):
|
||||||
|
"""关联任务并更新进展"""
|
||||||
|
self.linked_tasks.add(task)
|
||||||
|
if progress_percent is not None:
|
||||||
|
task.update_progress(progress_percent, f"日记 {self.date}: {notes}")
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def extract_experience(self, title, category, problem, solution, lesson_learned=''):
|
||||||
|
"""从日记提炼经验"""
|
||||||
|
exp = Experience.objects.create(
|
||||||
|
title=title,
|
||||||
|
category=category,
|
||||||
|
problem=problem,
|
||||||
|
solution=solution,
|
||||||
|
lesson_learned=lesson_learned,
|
||||||
|
date=self.date
|
||||||
|
)
|
||||||
|
return exp
|
||||||
|
|
||||||
|
|
||||||
class Experience(models.Model):
|
class Experience(models.Model):
|
||||||
@@ -37,7 +60,11 @@ class Experience(models.Model):
|
|||||||
problem = models.TextField('问题描述')
|
problem = models.TextField('问题描述')
|
||||||
solution = models.TextField('解决方案')
|
solution = models.TextField('解决方案')
|
||||||
lesson_learned = models.TextField('经验教训', blank=True, default='')
|
lesson_learned = models.TextField('经验教训', blank=True, default='')
|
||||||
date = models.DateField('日期', auto_now_add=True)
|
date = models.DateField('日期')
|
||||||
|
|
||||||
|
# 关联到日记(可选,经验可以独立创建)
|
||||||
|
extracted_from = models.ForeignKey(DiaryEntry, on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
|
related_name='experiences', verbose_name='提取自日记')
|
||||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from rest_framework import serializers
|
|||||||
from .models import DiaryEntry, DailyProgress, Experience, Task
|
from .models import DiaryEntry, DailyProgress, Experience, Task
|
||||||
|
|
||||||
class ExperienceSerializer(serializers.ModelSerializer):
|
class ExperienceSerializer(serializers.ModelSerializer):
|
||||||
|
category_display = serializers.CharField(source='get_category_display', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Experience
|
model = Experience
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
@@ -11,9 +13,18 @@ class DailyProgressSerializer(serializers.ModelSerializer):
|
|||||||
model = DailyProgress
|
model = DailyProgress
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
class TaskSimpleSerializer(serializers.ModelSerializer):
|
||||||
|
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Task
|
||||||
|
fields = ['id', 'title', 'status', 'status_display', 'progress_percent']
|
||||||
|
|
||||||
class DiaryEntrySerializer(serializers.ModelSerializer):
|
class DiaryEntrySerializer(serializers.ModelSerializer):
|
||||||
progresses = DailyProgressSerializer(many=True, read_only=True)
|
progresses = DailyProgressSerializer(many=True, read_only=True)
|
||||||
|
linked_tasks = TaskSimpleSerializer(many=True, read_only=True)
|
||||||
|
experiences = ExperienceSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DiaryEntry
|
model = DiaryEntry
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
@@ -21,6 +32,11 @@ class DiaryEntrySerializer(serializers.ModelSerializer):
|
|||||||
class TaskSerializer(serializers.ModelSerializer):
|
class TaskSerializer(serializers.ModelSerializer):
|
||||||
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
||||||
priority_display = serializers.CharField(source='get_priority_display', read_only=True)
|
priority_display = serializers.CharField(source='get_priority_display', read_only=True)
|
||||||
|
diary_entries = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_diary_entries(self, obj):
|
||||||
|
entries = obj.diary_entries.all()[:5] # 最近 5 条关联日记
|
||||||
|
return DiaryEntrySerializer(entries, many=True).data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Task
|
model = Task
|
||||||
|
|||||||
@@ -28,11 +28,41 @@ class DiaryEntryViewSet(viewsets.ModelViewSet):
|
|||||||
def stats(self, request):
|
def stats(self, request):
|
||||||
"""获取统计信息"""
|
"""获取统计信息"""
|
||||||
total_entries = DiaryEntry.objects.count()
|
total_entries = DiaryEntry.objects.count()
|
||||||
|
total_tasks = Task.objects.count()
|
||||||
|
total_experiences = Experience.objects.count()
|
||||||
return Response({
|
return Response({
|
||||||
'total_entries': total_entries,
|
'total_entries': total_entries,
|
||||||
'first_entry': DiaryEntry.objects.order_by('date').first().date if total_entries > 0 else None,
|
'first_entry': DiaryEntry.objects.order_by('date').first().date if total_entries > 0 else None,
|
||||||
'latest_entry': DiaryEntry.objects.order_by('-date').first().date if total_entries > 0 else None,
|
'latest_entry': DiaryEntry.objects.order_by('-date').first().date if total_entries > 0 else None,
|
||||||
|
'total_tasks': total_tasks,
|
||||||
|
'total_experiences': total_experiences,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def link_task(self, request, pk=None):
|
||||||
|
"""关联任务到日记"""
|
||||||
|
entry = self.get_object()
|
||||||
|
task_id = request.data.get('task_id')
|
||||||
|
progress_percent = request.data.get('progress_percent')
|
||||||
|
notes = request.data.get('notes', '')
|
||||||
|
|
||||||
|
task = Task.objects.get(id=task_id)
|
||||||
|
entry.link_task(task, progress_percent, notes)
|
||||||
|
|
||||||
|
return Response(DiaryEntrySerializer(entry).data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def extract_experience(self, request, pk=None):
|
||||||
|
"""从日记提炼经验"""
|
||||||
|
entry = self.get_object()
|
||||||
|
exp = entry.extract_experience(
|
||||||
|
title=request.data.get('title'),
|
||||||
|
category=request.data.get('category'),
|
||||||
|
problem=request.data.get('problem'),
|
||||||
|
solution=request.data.get('solution'),
|
||||||
|
lesson_learned=request.data.get('lesson_learned', '')
|
||||||
|
)
|
||||||
|
return Response(ExperienceSerializer(exp).data)
|
||||||
|
|
||||||
class DailyProgressViewSet(viewsets.ModelViewSet):
|
class DailyProgressViewSet(viewsets.ModelViewSet):
|
||||||
queryset = DailyProgress.objects.all()
|
queryset = DailyProgress.objects.all()
|
||||||
|
|||||||
1027
frontend/index.html
Executable file → Normal file
1027
frontend/index.html
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
19
migrate_linking.py
Normal file
19
migrate_linking.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""创建关联功能的数据库迁移"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'diary_system.settings')
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
print("📦 创建迁移...")
|
||||||
|
call_command('makemigrations', 'diary')
|
||||||
|
|
||||||
|
print("🗄️ 执行迁移...")
|
||||||
|
call_command('migrate')
|
||||||
|
|
||||||
|
print("✅ 迁移完成!")
|
||||||
Reference in New Issue
Block a user