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:
75
add_comment_demo.py
Normal file
75
add_comment_demo.py
Normal 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")
|
||||||
29
backend/diary/migrations/0005_comment.py
Normal file
29
backend/diary/migrations/0005_comment.py
Normal 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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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([])
|
||||||
|
|||||||
Reference in New Issue
Block a user