diff --git a/backend/diary/migrations/0003_task.py b/backend/diary/migrations/0003_task.py new file mode 100644 index 0000000..1d8e3a1 --- /dev/null +++ b/backend/diary/migrations/0003_task.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.11 on 2026-04-14 10:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('diary', '0002_experience'), + ] + + operations = [ + migrations.CreateModel( + name='Task', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200, verbose_name='任务标题')), + ('description', models.TextField(blank=True, default='', verbose_name='任务描述')), + ('status', models.CharField(choices=[('pending', '⏳ 待开始'), ('in_progress', '🔄 进行中'), ('blocked', '🚧 已阻塞'), ('completed', '✅ 已完成'), ('cancelled', '❌ 已取消')], default='pending', max_length=20, verbose_name='状态')), + ('priority', models.CharField(choices=[('low', '低'), ('medium', '中'), ('high', '高'), ('critical', '紧急')], default='medium', max_length=20, verbose_name='优先级')), + ('progress_percent', models.IntegerField(default=0, verbose_name='进展百分比')), + ('progress_notes', models.TextField(blank=True, default='', verbose_name='进展记录')), + ('assigned_to', models.CharField(blank=True, default='码神', max_length=100, verbose_name='负责人')), + ('due_date', models.DateField(blank=True, null=True, verbose_name='截止日期')), + ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '任务', + 'verbose_name_plural': '任务', + 'ordering': ['-priority', 'created_at'], + }, + ), + ] diff --git a/backend/diary/models.py b/backend/diary/models.py index 8b0e97d..bdac689 100755 --- a/backend/diary/models.py +++ b/backend/diary/models.py @@ -64,3 +64,59 @@ class DailyProgress(models.Model): def __str__(self): return f"{self.entry.date} - {self.category}: {self.progress_percent}%" + + +class Task(models.Model): + """工作任务 - 跟踪任务和进展""" + STATUS_CHOICES = [ + ('pending', '⏳ 待开始'), + ('in_progress', '🔄 进行中'), + ('blocked', '🚧 已阻塞'), + ('completed', '✅ 已完成'), + ('cancelled', '❌ 已取消'), + ] + + PRIORITY_CHOICES = [ + ('low', '低'), + ('medium', '中'), + ('high', '高'), + ('critical', '紧急'), + ] + + title = models.CharField('任务标题', max_length=200) + description = models.TextField('任务描述', blank=True, default='') + status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='pending') + 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='码神') + due_date = models.DateField('截止日期', null=True, blank=True) + completed_at = models.DateTimeField('完成时间', null=True, blank=True) + created_at = models.DateTimeField('创建时间', auto_now_add=True) + updated_at = models.DateTimeField('更新时间', auto_now=True) + + class Meta: + ordering = ['-priority', 'created_at'] + verbose_name = '任务' + verbose_name_plural = '任务' + + def __str__(self): + return f"{self.title} ({self.get_status_display()})" + + def mark_completed(self): + """标记任务为已完成""" + self.status = 'completed' + self.progress_percent = 100 + self.completed_at = timezone.now() + self.save() + + def update_progress(self, percent, notes=''): + """更新任务进展""" + self.progress_percent = percent + if percent > 0 and self.status == 'pending': + self.status = 'in_progress' + if percent == 100: + self.mark_completed() + if notes: + self.progress_notes = f"[{timezone.now().strftime('%Y-%m-%d %H:%M')}] {notes}\n" + self.progress_notes + self.save() diff --git a/backend/diary/serializers.py b/backend/diary/serializers.py index f78fd2f..daa8a54 100755 --- a/backend/diary/serializers.py +++ b/backend/diary/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import DiaryEntry, DailyProgress, Experience +from .models import DiaryEntry, DailyProgress, Experience, Task class ExperienceSerializer(serializers.ModelSerializer): class Meta: @@ -17,3 +17,12 @@ class DiaryEntrySerializer(serializers.ModelSerializer): class Meta: model = DiaryEntry fields = '__all__' + +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) + + class Meta: + model = Task + fields = '__all__' + read_only_fields = ['completed_at', 'created_at', 'updated_at'] diff --git a/backend/diary/urls.py b/backend/diary/urls.py index e42d716..693d353 100755 --- a/backend/diary/urls.py +++ b/backend/diary/urls.py @@ -1,11 +1,12 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import DiaryEntryViewSet, DailyProgressViewSet, ExperienceViewSet +from .views import DiaryEntryViewSet, DailyProgressViewSet, ExperienceViewSet, TaskViewSet router = DefaultRouter() router.register(r'entries', DiaryEntryViewSet) router.register(r'progress', DailyProgressViewSet) router.register(r'experiences', ExperienceViewSet) +router.register(r'tasks', TaskViewSet) urlpatterns = [ path('', include(router.urls)), diff --git a/backend/diary/views.py b/backend/diary/views.py index bd0a05f..d21c707 100755 --- a/backend/diary/views.py +++ b/backend/diary/views.py @@ -2,8 +2,8 @@ from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response from django.utils import timezone -from .models import DiaryEntry, DailyProgress, Experience -from .serializers import DiaryEntrySerializer, DailyProgressSerializer, ExperienceSerializer +from .models import DiaryEntry, DailyProgress, Experience, Task +from .serializers import DiaryEntrySerializer, DailyProgressSerializer, ExperienceSerializer, TaskSerializer class DiaryEntryViewSet(viewsets.ModelViewSet): queryset = DiaryEntry.objects.all() @@ -60,3 +60,59 @@ class ExperienceViewSet(viewsets.ModelViewSet): experiences = Experience.objects.order_by('-date', '-created_at')[:10] serializer = self.get_serializer(experiences, many=True) return Response(serializer.data) + + +class TaskViewSet(viewsets.ModelViewSet): + queryset = Task.objects.all() + serializer_class = TaskSerializer + + @action(detail=False, methods=['get']) + def by_status(self, request): + """按状态分组获取任务""" + statuses = {} + for task in Task.objects.all(): + status = task.get_status_display() + if status not in statuses: + statuses[status] = [] + statuses[status].append(TaskSerializer(task).data) + return Response(statuses) + + @action(detail=False, methods=['get']) + def active(self, request): + """获取活跃任务(未完成和未取消)""" + tasks = Task.objects.exclude(status__in=['completed', 'cancelled']) + serializer = self.get_serializer(tasks, many=True) + return Response(serializer.data) + + @action(detail=False, methods=['get']) + def stats(self, request): + """获取任务统计""" + total = Task.objects.count() + completed = Task.objects.filter(status='completed').count() + in_progress = Task.objects.filter(status='in_progress').count() + pending = Task.objects.filter(status='pending').count() + blocked = Task.objects.filter(status='blocked').count() + return Response({ + 'total': total, + 'completed': completed, + 'in_progress': in_progress, + 'pending': pending, + 'blocked': blocked, + 'completion_rate': round(completed / total * 100, 1) if total > 0 else 0 + }) + + @action(detail=True, methods=['post']) + def update_progress(self, request, pk=None): + """更新任务进展""" + task = self.get_object() + percent = request.data.get('percent', task.progress_percent) + notes = request.data.get('notes', '') + task.update_progress(int(percent), notes) + return Response(TaskSerializer(task).data) + + @action(detail=True, methods=['post']) + def complete(self, request, pk=None): + """标记任务为完成""" + task = self.get_object() + task.mark_completed() + return Response(TaskSerializer(task).data) diff --git a/frontend/index.html b/frontend/index.html index 30b1d2e..9456db6 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -167,11 +167,85 @@ grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)); gap: 30px; } + .grid-3 { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 30px; + } @media (max-width: 768px) { - .grid-2 { + .grid-2, .grid-3 { grid-template-columns: 1fr; } } + .task-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.status-cancelled { border-left-color: #ef4444; opacity: 0.6; } + .task-item .header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + } + .task-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; + } + .task-item .description { + color: #666; + margin: 10px 0; + white-space: pre-wrap; + } + .task-item .progress-notes { + background: #fef3c7; + padding: 10px; + border-radius: 5px; + font-size: 0.85em; + color: #92400e; + white-space: pre-wrap; + }
@@ -191,19 +265,23 @@ async function loadDiary() { try { - const [statsRes, entriesRes, expStatsRes, experiencesRes] = await Promise.all([ + const [statsRes, entriesRes, expStatsRes, experiencesRes, taskStatsRes, tasksRes] = await Promise.all([ fetch(`${API_BASE}/entries/stats/`), fetch(`${API_BASE}/entries/recent/`), fetch(`${API_BASE}/experiences/stats/`), - fetch(`${API_BASE}/experiences/recent/`) + fetch(`${API_BASE}/experiences/recent/`), + fetch(`${API_BASE}/tasks/stats/`), + fetch(`${API_BASE}/tasks/active/`) ]); const stats = await statsRes.json(); const entries = await entriesRes.json(); const expStats = await expStatsRes.json(); const experiences = await experiencesRes.json(); + const taskStats = await taskStatsRes.json(); + const tasks = await tasksRes.json(); - render(stats, entries, expStats, experiences); + render(stats, entries, expStats, experiences, taskStats, tasks); } catch (error) { document.getElementById('app').innerHTML = `总日记数
+总任务
+进行中
+已完成
+完成率
+经验总结
第一天
-最新日记
-暂无活跃任务
' : tasks.map(task => ` +