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

75
add_comment_demo.py Normal file
View File

@@ -0,0 +1,75 @@
#!/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 diary.models import Comment, DiaryEntry, Task, Experience
def add_comment(content_type, object_id, content, created_by='北极星'):
"""添加批注"""
comment = Comment.objects.create(
content_type=content_type,
object_id=object_id,
content=content,
created_by=created_by
)
print(f"✅ 批注已添加:{content_type} #{object_id}")
print(f" 内容:{content[:50]}...")
return comment
def show_comments(content_type, object_id):
"""查看批注"""
comments = Comment.objects.filter(
content_type=content_type,
object_id=object_id
)
print(f"\n📝 {content_type} #{object_id} 的批注:\n")
if not comments:
print(" 暂无批注")
return
for comment in comments:
print(f" [{comment.created_at.strftime('%Y-%m-%d %H:%M')}] {comment.created_by}:")
print(f" {comment.content}")
print()
if __name__ == '__main__':
# 示例:查看今天的日记
print("📖 批注功能演示\n")
print("=" * 60)
# 获取今天的日记
from django.utils import timezone
today = timezone.now().date()
entry = DiaryEntry.objects.filter(date=today).first()
if entry:
print(f"\n今天的日记:{entry.title}")
print(f"ID: {entry.id}")
# 查看批注
show_comments('diary', entry.id)
# 添加示例批注
print("\n添加示例批注...")
add_comment('diary', entry.id, '今天的日记内容很丰富!继续保持!')
# 再次查看
show_comments('diary', entry.id)
else:
print("今天还没有日记")
print("\n" + "=" * 60)
print("\n💡 使用方法:")
print(" 1. 查看批注show_comments('diary', 1)")
print(" 2. 添加批注add_comment('diary', 1, '你的批注内容')")
print("\n 支持的内容类型diary, task, experience")

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}%" 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): class Task(models.Model):
"""工作任务 - 跟踪任务和进展""" """工作任务 - 跟踪任务和进展"""
STATUS_CHOICES = [ STATUS_CHOICES = [

View File

@@ -1,8 +1,15 @@
from rest_framework import serializers 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): class ExperienceSerializer(serializers.ModelSerializer):
category_display = serializers.CharField(source='get_category_display', read_only=True) category_display = serializers.CharField(source='get_category_display', read_only=True)
comments = CommentSerializer(many=True, read_only=True, source='comment_set')
class Meta: class Meta:
model = Experience model = Experience
@@ -24,6 +31,7 @@ class DiaryEntrySerializer(serializers.ModelSerializer):
progresses = DailyProgressSerializer(many=True, read_only=True) progresses = DailyProgressSerializer(many=True, read_only=True)
linked_tasks = TaskSimpleSerializer(many=True, read_only=True) linked_tasks = TaskSimpleSerializer(many=True, read_only=True)
experiences = ExperienceSerializer(many=True, read_only=True) experiences = ExperienceSerializer(many=True, read_only=True)
comments = CommentSerializer(many=True, read_only=True, source='comment_set')
class Meta: class Meta:
model = DiaryEntry model = DiaryEntry
@@ -33,9 +41,10 @@ class TaskSerializer(serializers.ModelSerializer):
status_display = serializers.CharField(source='get_status_display', read_only=True) status_display = serializers.CharField(source='get_status_display', read_only=True)
priority_display = serializers.CharField(source='get_priority_display', read_only=True) priority_display = serializers.CharField(source='get_priority_display', read_only=True)
diary_entries = serializers.SerializerMethodField() diary_entries = serializers.SerializerMethodField()
comments = CommentSerializer(many=True, read_only=True, source='comment_set')
def get_diary_entries(self, obj): 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 return DiaryEntrySerializer(entries, many=True).data
class Meta: class Meta:

View File

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

View File

@@ -2,8 +2,11 @@ from rest_framework import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from django.utils import timezone from django.utils import timezone
from .models import DiaryEntry, DailyProgress, Experience, Task from .models import DiaryEntry, DailyProgress, Experience, Task, Comment
from .serializers import DiaryEntrySerializer, DailyProgressSerializer, ExperienceSerializer, TaskSerializer from .serializers import (
DiaryEntrySerializer, DailyProgressSerializer, ExperienceSerializer,
TaskSerializer, CommentSerializer
)
class DiaryEntryViewSet(viewsets.ModelViewSet): class DiaryEntryViewSet(viewsets.ModelViewSet):
queryset = DiaryEntry.objects.all() queryset = DiaryEntry.objects.all()
@@ -146,3 +149,24 @@ class TaskViewSet(viewsets.ModelViewSet):
task = self.get_object() task = self.get_object()
task.mark_completed() task.mark_completed()
return Response(TaskSerializer(task).data) 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([])