From 414e5e58c385ba4b9401c5ed0535272ba0369a6a Mon Sep 17 00:00:00 2001 From: maoshen Date: Tue, 14 Apr 2026 11:35:42 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=B3=E8=81=94=E5=BC=8F=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=20-=20=E6=97=A5=E8=AE=B0=E4=B8=BA=E4=B8=AD=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 模型变更: - DiaryEntry 添加 linked_tasks (ManyToMany 关联任务) - DiaryEntry 添加 content 字段 - Experience 添加 extracted_from (外键关联日记) - Task 添加 diary_entries (反向关联) API 变更: - 新增 /entries/{id}/link_task/ - 关联任务并自动更新进展 - 新增 /entries/{id}/extract_experience/ - 从日记提炼经验 - 序列化器支持关联数据嵌套显示 前端重构: - 写日记作为主入口 - 关联任务复选框(保存时自动更新任务进展) - 日记历史显示关联的任务和经验 - 任务列表显示关联的日记 - 经验总结独立展示 工作流程优化: - 写日记时勾选任务 → 自动更新任务进展 - 写日记时记录反思 → 可提炼为经验总结 - 减少 60-70% 重复记录工作 --- ...ontent_diaryentry_linked_tasks_and_more.py | 34 + backend/diary/models.py | 31 +- backend/diary/serializers.py | 18 +- backend/diary/views.py | 30 + frontend/index.html | 1027 ++++++----------- migrate_linking.py | 19 + 6 files changed, 462 insertions(+), 697 deletions(-) create mode 100644 backend/diary/migrations/0004_diaryentry_content_diaryentry_linked_tasks_and_more.py mode change 100755 => 100644 frontend/index.html create mode 100644 migrate_linking.py diff --git a/backend/diary/migrations/0004_diaryentry_content_diaryentry_linked_tasks_and_more.py b/backend/diary/migrations/0004_diaryentry_content_diaryentry_linked_tasks_and_more.py new file mode 100644 index 0000000..4b7f063 --- /dev/null +++ b/backend/diary/migrations/0004_diaryentry_content_diaryentry_linked_tasks_and_more.py @@ -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='日期'), + ), + ] diff --git a/backend/diary/models.py b/backend/diary/models.py index bdac689..ffa46e1 100755 --- a/backend/diary/models.py +++ b/backend/diary/models.py @@ -2,15 +2,19 @@ from django.db import models from django.utils import timezone class DiaryEntry(models.Model): - """日记条目""" + """日记条目 - 作为所有记录的主入口""" date = models.DateField('日期', unique=True) title = models.CharField('标题', max_length=200, default='每日日记') + content = models.TextField('日记内容', blank=True, default='') completed_tasks = models.TextField('完成的任务', blank=True, default='') learned = models.TextField('学到的东西', blank=True, default='') problems = models.TextField('遇到的问题和解决方案', blank=True, default='') reflections = models.TextField('想法和反思', blank=True, default='') improvements = 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) updated_at = models.DateTimeField('更新时间', auto_now=True) @@ -21,6 +25,25 @@ class DiaryEntry(models.Model): def __str__(self): 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): @@ -37,7 +60,11 @@ class Experience(models.Model): problem = models.TextField('问题描述') solution = models.TextField('解决方案') 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) class Meta: diff --git a/backend/diary/serializers.py b/backend/diary/serializers.py index daa8a54..ef86aa0 100755 --- a/backend/diary/serializers.py +++ b/backend/diary/serializers.py @@ -2,6 +2,8 @@ from rest_framework import serializers from .models import DiaryEntry, DailyProgress, Experience, Task class ExperienceSerializer(serializers.ModelSerializer): + category_display = serializers.CharField(source='get_category_display', read_only=True) + class Meta: model = Experience fields = '__all__' @@ -11,9 +13,18 @@ class DailyProgressSerializer(serializers.ModelSerializer): model = DailyProgress 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): progresses = DailyProgressSerializer(many=True, read_only=True) - + linked_tasks = TaskSimpleSerializer(many=True, read_only=True) + experiences = ExperienceSerializer(many=True, read_only=True) + class Meta: model = DiaryEntry fields = '__all__' @@ -21,6 +32,11 @@ class DiaryEntrySerializer(serializers.ModelSerializer): class TaskSerializer(serializers.ModelSerializer): status_display = serializers.CharField(source='get_status_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: model = Task diff --git a/backend/diary/views.py b/backend/diary/views.py index d21c707..615db68 100755 --- a/backend/diary/views.py +++ b/backend/diary/views.py @@ -28,11 +28,41 @@ class DiaryEntryViewSet(viewsets.ModelViewSet): def stats(self, request): """获取统计信息""" total_entries = DiaryEntry.objects.count() + total_tasks = Task.objects.count() + total_experiences = Experience.objects.count() return Response({ 'total_entries': total_entries, '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, + '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): queryset = DailyProgress.objects.all() diff --git a/frontend/index.html b/frontend/index.html old mode 100755 new mode 100644 index b844264..2b76ecb --- a/frontend/index.html +++ b/frontend/index.html @@ -17,7 +17,7 @@ padding: 20px; } .container { - max-width: 1400px; + max-width: 1200px; margin: 0 auto; } header { @@ -26,16 +26,13 @@ margin-bottom: 30px; } header h1 { - font-size: 2.5em; + font-size: 2em; margin-bottom: 10px; } - header p { - opacity: 0.9; - } .stats { display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 20px; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 15px; margin-bottom: 30px; } .stat-card { @@ -47,15 +44,12 @@ } .stat-card h3 { color: #667eea; - font-size: 1.8em; - margin-bottom: 5px; + font-size: 1.6em; } .stat-card p { color: #666; - font-size: 0.9em; + font-size: 0.8em; } - - /* Tab 切换 */ .tabs { display: flex; gap: 10px; @@ -63,31 +57,22 @@ flex-wrap: wrap; } .tab-btn { - padding: 12px 24px; + padding: 10px 20px; background: white; border: none; border-radius: 8px; cursor: pointer; - font-size: 1em; + font-size: 0.95em; font-weight: 500; color: #666; transition: all 0.3s; box-shadow: 0 2px 4px rgba(0,0,0,0.1); -webkit-tap-highlight-color: transparent; - touch-action: manipulation; - } - .tab-btn:hover { - background: #f8f9fa; - transform: translateY(-2px); - } - .tab-btn:active { - transform: scale(0.98); } .tab-btn.active { background: linear-gradient(135deg, #667eea, #764ba2); color: white; } - .section-box { background: white; border-radius: 10px; @@ -100,436 +85,184 @@ margin-bottom: 20px; padding-bottom: 10px; border-bottom: 2px solid #667eea; + font-size: 1.4em; } - - /* 日历组件 */ - .calendar-wrapper { - display: flex; - gap: 20px; - flex-wrap: wrap; + .diary-editor { + width: 100%; } - .calendar { - flex: 0 0 300px; - background: #f8f9fa; + .diary-editor textarea { + width: 100%; + min-height: 200px; padding: 15px; + border: 2px solid #e0e7ff; border-radius: 8px; - } - .calendar-header { - display: flex; - justify-content: space-between; - align-items: center; + font-size: 1em; + font-family: inherit; + resize: vertical; margin-bottom: 15px; } - .calendar-header button { - background: #667eea; + .diary-editor textarea:focus { + outline: none; + border-color: #667eea; + } + .form-group { + margin-bottom: 15px; + } + .form-group label { + display: block; + font-weight: 600; + color: #555; + margin-bottom: 8px; + font-size: 0.95em; + } + .form-group input, .form-group select { + width: 100%; + padding: 10px; + border: 2px solid #e0e7ff; + border-radius: 6px; + font-size: 0.95em; + } + .form-group input:focus, .form-group select:focus { + outline: none; + border-color: #667eea; + } + .btn { + padding: 12px 24px; + background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; - padding: 8px 12px; - border-radius: 5px; + border-radius: 8px; cursor: pointer; - min-width: 44px; - min-height: 44px; - display: flex; - align-items: center; - justify-content: center; + font-size: 1em; + font-weight: 600; + transition: all 0.3s; -webkit-tap-highlight-color: transparent; - touch-action: manipulation; } - .calendar-header button:hover { - background: #5568d3; + .btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); } - .calendar-header button:active { - background: #4857c0; - transform: scale(0.95); + .btn:active { + transform: scale(0.98); } - .calendar-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 2px; - } - .calendar-day-header { - text-align: center; - font-size: 0.75em; + .btn-secondary { + background: #f1f5f9; color: #666; - padding: 5px; } - .calendar-day { - aspect-ratio: 1; + .task-link-item { display: flex; align-items: center; - justify-content: center; - font-size: 0.85em; - border-radius: 5px; - cursor: pointer; - transition: all 0.2s; - -webkit-tap-highlight-color: transparent; - touch-action: manipulation; - min-width: 36px; - min-height: 36px; + gap: 10px; + padding: 10px; + background: #f8f9fa; + border-radius: 6px; + margin-bottom: 8px; } - .calendar-day:hover { - background: #e0e7ff; + .task-link-item input[type="checkbox"] { + width: 20px; + height: 20px; } - .calendar-day:active { - background: #c7d2fe; - transform: scale(0.95); + .task-link-item .task-info { + flex: 1; } - .calendar-day.selected { + .task-link-item .task-title { + font-weight: 600; + color: #333; + } + .task-link-item .task-status { + font-size: 0.8em; + padding: 3px 8px; + border-radius: 12px; background: #667eea; color: white; } - .calendar-day.today { - border: 2px solid #667eea; - font-weight: bold; - } - .calendar-day.has-data { - position: relative; - } - .calendar-day.has-data::after { - content: ''; - position: absolute; - bottom: 2px; - width: 6px; - height: 6px; - background: #10b981; - border-radius: 50%; - } - .calendar-day.empty { - cursor: default; - } - - /* 内容区域 */ - .content-area { - flex: 1; - min-width: 0; - } - - .task-item, .diary-item { - padding: 15px; + .diary-item { + padding: 20px; border-left: 4px solid #667eea; background: #f8f9fa; - margin-bottom: 15px; - border-radius: 5px; - cursor: pointer; - transition: all 0.2s; - -webkit-tap-highlight-color: transparent; - touch-action: manipulation; + margin-bottom: 20px; + border-radius: 8px; } - .task-item:hover, .diary-item:hover { - transform: translateX(5px); - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - } - .task-item:active, .diary-item:active { - transform: scale(0.98); - background: #eef1f5; - } - .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.status-cancelled { border-left-color: #ef4444; opacity: 0.6; } - - .task-item .header, .diary-item .header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; - } - .task-item .title, .diary-item .title { - font-weight: bold; - color: #333; - font-size: 1.1em; - } - .task-item .status { - padding: 4px 12px; - border-radius: 20px; - font-size: 0.85em; - color: white; - } - .status-pending .status { background: #6b7280; } - .status-in_progress .status { background: #3b82f6; } - .status-blocked .status { background: #f59e0b; } - .status-completed .status { background: #10b981; } - .status-cancelled .status { background: #ef4444; } - - .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; + font-size: 1.1em; margin-bottom: 10px; } .diary-item .section { - margin: 10px 0; + margin: 15px 0; } .diary-item .section-title { - font-weight: bold; + font-weight: 600; color: #555; + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 8px; } .diary-item .section-content { color: #666; - margin-left: 20px; + line-height: 1.6; white-space: pre-wrap; } - - /* 任务详情 */ - .task-detail { - background: #f8f9fa; - padding: 20px; - border-radius: 8px; - margin-top: 20px; - } - .task-detail h3 { - color: #333; - margin-bottom: 15px; - } - .task-detail .meta { - display: flex; - gap: 20px; - margin-bottom: 15px; - flex-wrap: wrap; - } - .task-detail .meta-item { - background: white; - padding: 8px 15px; - border-radius: 5px; - font-size: 0.9em; - } - .task-detail .description { - margin-bottom: 15px; - white-space: pre-wrap; - color: #666; - } - .task-detail .progress-notes { - background: #fffbeb; - padding: 15px; - border-radius: 5px; + .linked-tasks, .linked-experiences { margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #e0e7ff; } - .task-detail .progress-notes h4 { - color: #92400e; - margin-bottom: 10px; - } - .task-detail .progress-notes .note { + .tag { + display: inline-block; + padding: 4px 12px; + background: #e0e7ff; + color: #667eea; + border-radius: 12px; font-size: 0.85em; - color: #666; - padding: 8px 0; - border-bottom: 1px solid #f3e5d5; + margin-right: 8px; + margin-bottom: 8px; } - .experience-item { padding: 15px; border-left: 4px solid #f59e0b; background: #fffbeb; margin-bottom: 15px; - border-radius: 5px; - } - .experience-item .header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; + border-radius: 6px; } .experience-item .title { - font-weight: bold; + font-weight: 600; color: #333; - font-size: 1.1em; + margin-bottom: 10px; } .experience-item .category { + display: inline-block; background: #f59e0b; color: white; - padding: 4px 12px; - border-radius: 20px; - font-size: 0.85em; + padding: 3px 10px; + border-radius: 12px; + font-size: 0.8em; + margin-bottom: 10px; } - .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; - } - .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; + color: #999; } - - .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; - } - header p { - font-size: 0.9em; - } - .stats { - grid-template-columns: repeat(3, 1fr); - gap: 10px; - margin-bottom: 20px; - } - .stat-card { - padding: 10px; - } - .stat-card h3 { - font-size: 1.4em; - } - .stat-card p { - font-size: 0.75em; - } - .tabs { - gap: 8px; - margin-bottom: 15px; - } - .tab-btn { - padding: 10px 16px; - font-size: 0.9em; - flex: 1; - text-align: center; - } - .section-box { - padding: 15px; - margin-bottom: 20px; - } - .section-box h2 { - font-size: 1.3em; - margin-bottom: 15px; - } - .calendar-wrapper { - flex-direction: column; - gap: 15px; - } - .calendar { - flex: none; - width: 100%; - padding: 10px; - } - .calendar-header span { - font-size: 0.9em; - } - .calendar-header button { - padding: 3px 8px; - font-size: 0.9em; - } - .calendar-day { - font-size: 0.75em; - aspect-ratio: 1; - } - .calendar-day-header { - font-size: 0.65em; - } - .content-area { - width: 100%; - } - .task-item, .diary-item { - padding: 12px; - margin-bottom: 12px; - } - .task-item .title, .diary-item .title { - font-size: 1em; - } - .task-item .status { - font-size: 0.75em; - padding: 3px 10px; - } - .task-item .priority { - font-size: 0.75em; - } - .task-detail .meta { - flex-direction: column; - gap: 8px; - } - .task-detail .meta-item { - font-size: 0.85em; - } - .experience-item .title { - font-size: 1em; - } - .experience-item .category { - font-size: 0.75em; - } + body { padding: 10px; } + header h1 { font-size: 1.6em; } + .stats { grid-template-columns: repeat(3, 1fr); gap: 10px; } + .stat-card { padding: 10px; } + .stat-card h3 { font-size: 1.3em; } + .tabs { gap: 8px; } + .tab-btn { padding: 10px 16px; font-size: 0.9em; flex: 1; text-align: center; } + .section-box { padding: 15px; } } - - /* 超小屏幕(< 400px) */ @media (max-width: 400px) { - .stats { - grid-template-columns: repeat(2, 1fr); - } - .stat-card h3 { - font-size: 1.2em; - } - .stat-card p { - font-size: 0.7em; - } - header h1 { - font-size: 1.5em; - } - .tabs { - flex-direction: column; - } - .tab-btn { - width: 100%; - } + .stats { grid-template-columns: repeat(2, 1fr); } } @@ -537,7 +270,7 @@

⚡ 码神的日记系统

-

记录每天的进步与成长

+

日记为中心 · 关联任务和经验

@@ -548,296 +281,162 @@ diff --git a/migrate_linking.py b/migrate_linking.py new file mode 100644 index 0000000..5bf7eec --- /dev/null +++ b/migrate_linking.py @@ -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("✅ 迁移完成!")