feat: 添加工作任务管理功能
- 新增 Task 模型(状态、优先级、进展百分比) - 任务 API(列表、统计、进展更新、完成标记) - 前端任务板块(统计卡片 + 任务列表) - 进展可视化(进度条 + 进展记录)
This commit is contained in:
35
backend/diary/migrations/0003_task.py
Normal file
35
backend/diary/migrations/0003_task.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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()
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
13
makemigrations_tasks.py
Normal 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("✅ 数据库迁移完成")
|
||||
Reference in New Issue
Block a user