feat: 添加工作任务管理功能

- 新增 Task 模型(状态、优先级、进展百分比)
- 任务 API(列表、统计、进展更新、完成标记)
- 前端任务板块(统计卡片 + 任务列表)
- 进展可视化(进度条 + 进展记录)
This commit is contained in:
maoshen
2026-04-14 10:16:14 +00:00
parent b273789ae8
commit b680c573db
7 changed files with 292 additions and 17 deletions

View File

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

View File

@@ -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()

View File

@@ -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']

View File

@@ -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)),

View File

@@ -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)

View File

@@ -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;
}
</style>
</head>
<body>
@@ -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 = `
<div class="error">加载失败:${error.message}</div>
@@ -211,7 +289,7 @@
}
}
function render(stats, entries, expStats, experiences) {
function render(stats, entries, expStats, experiences, taskStats, tasks) {
const app = document.getElementById('app');
app.innerHTML = `
@@ -220,18 +298,45 @@
<h3>${stats.total_entries || 0}</h3>
<p>总日记数</p>
</div>
<div class="stat-card">
<h3>${taskStats.total || 0}</h3>
<p>总任务</p>
</div>
<div class="stat-card">
<h3>${taskStats.in_progress || 0}</h3>
<p>进行中</p>
</div>
<div class="stat-card">
<h3>${taskStats.completed || 0}</h3>
<p>已完成</p>
</div>
<div class="stat-card">
<h3>${taskStats.completion_rate || 0}%</h3>
<p>完成率</p>
</div>
<div class="stat-card">
<h3>${expStats.total_experiences || 0}</h3>
<p>经验总结</p>
</div>
<div class="stat-card">
<h3>${stats.first_entry || '-'}</h3>
<p>第一天</p>
</div>
<div class="stat-card">
<h3>${stats.latest_entry || '-'}</h3>
<p>最新日记</p>
</div>
</div>
<div class="section-box" style="margin-bottom: 30px;">
<h2>📋 工作任务</h2>
${tasks.length === 0 ? '<p>暂无活跃任务</p>' : tasks.map(task => `
<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">${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>
${task.progress_notes ? `<div class="progress-notes"><strong>进展记录:</strong><br>${task.progress_notes}</div>` : ''}
</div>
`).join('')}
</div>
<div class="grid-2">

13
makemigrations_tasks.py Normal file
View File

@@ -0,0 +1,13 @@
#!/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
call_command('makemigrations', 'diary')
call_command('migrate')
print("✅ 数据库迁移完成")