feat: 添加批注功能

后端:
- Comment 模型(支持日记/任务/经验三种内容类型)
- CommentSerializer 和 CommentViewSet
- API: /api/comments/ - 批注 CRUD
- API: /api/comments/by_content/?content_type=diary&object_id=1 - 按内容获取批注
- 日记/任务/经验序列化器嵌套显示批注

前端:
- 批注样式(comments-section, comment-item)
- 添加批注输入框

使用方式:
- 北极星可以在任何日记/任务/经验下添加批注
- 批注会显示在内容下方
- 支持查看历史批注
This commit is contained in:
maoshen
2026-04-14 11:46:52 +00:00
parent 4aeb21c2d7
commit 00a6aef16b
6 changed files with 166 additions and 5 deletions

View File

@@ -0,0 +1,29 @@
# Generated by Django 4.2.11 on 2026-04-14 11:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('diary', '0004_diaryentry_content_diaryentry_linked_tasks_and_more'),
]
operations = [
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content_type', models.CharField(choices=[('diary', '日记'), ('task', '任务'), ('experience', '经验')], max_length=20, verbose_name='内容类型')),
('object_id', models.IntegerField(verbose_name='内容 ID')),
('content', models.TextField(verbose_name='批注内容')),
('created_by', models.CharField(default='北极星', max_length=100, verbose_name='创建者')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
],
options={
'verbose_name': '批注',
'verbose_name_plural': '批注',
'ordering': ['-created_at'],
},
),
]

View File

@@ -93,6 +93,29 @@ class DailyProgress(models.Model):
return f"{self.entry.date} - {self.category}: {self.progress_percent}%"
class Comment(models.Model):
"""批注 - 用户可以对日记/任务/经验添加评论"""
CONTENT_TYPE_CHOICES = [
('diary', '日记'),
('task', '任务'),
('experience', '经验'),
]
content_type = models.CharField('内容类型', max_length=20, choices=CONTENT_TYPE_CHOICES)
object_id = models.IntegerField('内容 ID')
content = models.TextField('批注内容')
created_by = models.CharField('创建者', max_length=100, default='北极星')
created_at = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
ordering = ['-created_at']
verbose_name = '批注'
verbose_name_plural = '批注'
def __str__(self):
return f"{self.content_type} #{self.object_id} - {self.created_by}"
class Task(models.Model):
"""工作任务 - 跟踪任务和进展"""
STATUS_CHOICES = [

View File

@@ -1,8 +1,15 @@
from rest_framework import serializers
from .models import DiaryEntry, DailyProgress, Experience, Task
from .models import DiaryEntry, DailyProgress, Experience, Task, Comment
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
read_only_fields = ['created_at']
class ExperienceSerializer(serializers.ModelSerializer):
category_display = serializers.CharField(source='get_category_display', read_only=True)
comments = CommentSerializer(many=True, read_only=True, source='comment_set')
class Meta:
model = Experience
@@ -24,6 +31,7 @@ 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)
comments = CommentSerializer(many=True, read_only=True, source='comment_set')
class Meta:
model = DiaryEntry
@@ -33,9 +41,10 @@ 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()
comments = CommentSerializer(many=True, read_only=True, source='comment_set')
def get_diary_entries(self, obj):
entries = obj.diary_entries.all()[:5] # 最近 5 条关联日记
entries = obj.diary_entries.all()[:5]
return DiaryEntrySerializer(entries, many=True).data
class Meta:

View File

@@ -1,12 +1,13 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import DiaryEntryViewSet, DailyProgressViewSet, ExperienceViewSet, TaskViewSet
from .views import DiaryEntryViewSet, DailyProgressViewSet, ExperienceViewSet, TaskViewSet, CommentViewSet
router = DefaultRouter()
router.register(r'entries', DiaryEntryViewSet)
router.register(r'progress', DailyProgressViewSet)
router.register(r'experiences', ExperienceViewSet)
router.register(r'tasks', TaskViewSet)
router.register(r'comments', CommentViewSet)
urlpatterns = [
path('', include(router.urls)),

View File

@@ -2,8 +2,11 @@ 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, Task
from .serializers import DiaryEntrySerializer, DailyProgressSerializer, ExperienceSerializer, TaskSerializer
from .models import DiaryEntry, DailyProgress, Experience, Task, Comment
from .serializers import (
DiaryEntrySerializer, DailyProgressSerializer, ExperienceSerializer,
TaskSerializer, CommentSerializer
)
class DiaryEntryViewSet(viewsets.ModelViewSet):
queryset = DiaryEntry.objects.all()
@@ -146,3 +149,24 @@ class TaskViewSet(viewsets.ModelViewSet):
task = self.get_object()
task.mark_completed()
return Response(TaskSerializer(task).data)
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
@action(detail=False, methods=['get'])
def by_content(self, request):
"""按内容类型和 ID 获取批注"""
content_type = request.query_params.get('content_type')
object_id = request.query_params.get('object_id')
if content_type and object_id:
comments = Comment.objects.filter(
content_type=content_type,
object_id=object_id
)
serializer = self.get_serializer(comments, many=True)
return Response(serializer.data)
return Response([])