feat(家庭事项): 按到期时间分类显示,一个月内显示详情,超过一个月显示数量

- 修改视图逻辑,将未完成事项分为即将到期(一个月内)和远期事项
- 即将到期的事项显示完整详情表格
- 超过一个月的远期事项只显示数量,不显示详情
- 支持显示已过期的事项(红色标记)
- 合并远程更新
This commit is contained in:
2026-03-16 18:27:52 +08:00
11 changed files with 466 additions and 15 deletions

View File

@@ -13,6 +13,8 @@ from .models import (
FamilyTask, FamilyTask,
TodayPlan, TodayPlan,
SystemConfig, SystemConfig,
PublicContentType,
PublicContent,
) )
@@ -96,3 +98,17 @@ class TodayPlanAdmin(admin.ModelAdmin):
@admin.register(SystemConfig) @admin.register(SystemConfig)
class SystemConfigAdmin(admin.ModelAdmin): class SystemConfigAdmin(admin.ModelAdmin):
list_display = ('smtp_server', 'smtp_port', 'smtp_username', 'recipient_email', 'send_time', 'created_at') list_display = ('smtp_server', 'smtp_port', 'smtp_username', 'recipient_email', 'send_time', 'created_at')
@admin.register(PublicContentType)
class PublicContentTypeAdmin(admin.ModelAdmin):
list_display = ('name', 'created_at', 'updated_at')
search_fields = ('name',)
@admin.register(PublicContent)
class PublicContentAdmin(admin.ModelAdmin):
list_display = ('title', 'type', 'is_published', 'sort_order', 'created_at')
list_filter = ('type', 'is_published')
search_fields = ('title', 'content')
ordering = ('sort_order', '-created_at')

View File

@@ -8,7 +8,8 @@ from .models import (
FamilyTask, FamilyTask,
TodayPlan, TodayPlan,
SystemConfig, SystemConfig,
FamilyMember FamilyMember,
PublicContent
) )
class ReadingRecordForm(forms.ModelForm): class ReadingRecordForm(forms.ModelForm):
@@ -109,4 +110,19 @@ class SystemConfigForm(forms.ModelForm):
if not re.match(email_pattern, recipient_email): if not re.match(email_pattern, recipient_email):
logger.warning(f"收件人邮箱格式不正确: {recipient_email}") logger.warning(f"收件人邮箱格式不正确: {recipient_email}")
raise forms.ValidationError("请输入有效的邮箱地址格式如example@domain.com") raise forms.ValidationError("请输入有效的邮箱地址格式如example@domain.com")
return recipient_email return recipient_email
class PublicContentForm(forms.ModelForm):
"""公开内容表单"""
class Meta:
model = PublicContent
fields = ['type', 'title', 'content', 'file', 'url', 'sort_order', 'is_published']
widgets = {
'type': forms.Select(attrs={'class': 'form-select'}),
'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入标题'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 5, 'placeholder': '请输入内容'}),
'file': forms.FileInput(attrs={'class': 'form-control'}),
'url': forms.URLInput(attrs={'class': 'form-control', 'placeholder': '请输入链接地址'}),
'sort_order': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '请输入排序值'}),
}

View File

@@ -216,3 +216,37 @@ class SystemConfig(models.Model):
"""获取系统配置,单例模式""" """获取系统配置,单例模式"""
config, created = cls.objects.get_or_create(pk=1) config, created = cls.objects.get_or_create(pk=1)
return config return config
class PublicContentType(models.Model):
"""公开内容类型"""
name = models.CharField(max_length=20, unique=True, verbose_name="名称")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
verbose_name = "公开内容类型"
verbose_name_plural = "公开内容类型"
ordering = ['name']
def __str__(self):
return self.name
class PublicContent(models.Model):
"""公开内容表"""
type = models.ForeignKey(PublicContentType, on_delete=models.CASCADE, verbose_name="类型")
title = models.CharField(max_length=200, verbose_name="标题")
content = models.TextField(blank=True, null=True, verbose_name="内容")
file = models.FileField(upload_to='public_files/', blank=True, null=True, verbose_name="上传文件")
url = models.URLField(blank=True, null=True, verbose_name="链接地址")
sort_order = models.IntegerField(default=0, verbose_name="排序")
is_published = models.BooleanField(default=True, verbose_name="是否发布")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
verbose_name = "公开内容"
verbose_name_plural = "公开内容"
ordering = ['sort_order', '-created_at']
def __str__(self):
return f"{self.type.name} - {self.title}"

View File

@@ -0,0 +1,76 @@
{% extends 'core/base.html' %}
{% block content %}
<!-- 页面标题 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">
<i class="bi bi-globe me-2 text-info"></i>添加公开内容
</h2>
<a href="{% url 'public_content' %}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>返回
</a>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header bg-info text-dark">
<h5 class="card-title mb-0">
<i class="bi bi-plus-circle me-2"></i>填写公开内容信息
</h5>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for field in form %}
<div class="mb-4">
<label for="{{ field.id_for_label }}" class="form-label">
{% if field.name == 'type' %}
<i class="bi bi-folder me-1 text-info"></i>
{% elif field.name == 'title' %}
<i class="bi bi-fonts me-1 text-info"></i>
{% elif field.name == 'content' %}
<i class="bi bi-text-paragraph me-1 text-info"></i>
{% elif field.name == 'file' %}
<i class="bi bi-file-earmark me-1 text-info"></i>
{% elif field.name == 'url' %}
<i class="bi bi-link-45deg me-1 text-info"></i>
{% elif field.name == 'sort_order' %}
<i class="bi bi-sort-numeric-down me-1 text-info"></i>
{% elif field.name == 'is_published' %}
<i class="bi bi-eye me-1 text-info"></i>
{% else %}
<i class="bi bi-circle me-1 text-info"></i>
{% endif %}
{{ field.label }}
{% if field.field.required %}
<span class="text-danger">*</span>
{% endif %}
</label>
{{ field }}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
{% for error in field.errors %}
<div class="invalid-feedback d-block">
<i class="bi bi-exclamation-circle me-1"></i>{{ error }}
</div>
{% endfor %}
</div>
{% endfor %}
<div class="d-flex gap-2 justify-content-center mt-4">
<button type="submit" class="btn btn-info px-4 text-dark">
<i class="bi bi-check-lg me-1"></i>保存
</button>
<a href="{% url 'public_content' %}" class="btn btn-secondary px-4">
<i class="bi bi-x-lg me-1"></i>取消
</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -449,6 +449,12 @@
<i class="bi bi-speedometer2"></i><span>首页</span> <i class="bi bi-speedometer2"></i><span>首页</span>
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'public_content' %}active{% endif %}" href="{% url 'public_content' %}">
<i class="bi bi-globe"></i><span>公开内容</span>
</a>
</li>
{% if user.is_authenticated %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'yesterday_records' or 'add_reading' in request.path or 'edit_reading' in request.path or 'add_insight' in request.path or 'edit_insight' in request.path %}active{% endif %}" href="{% url 'yesterday_records' %}"> <a class="nav-link {% if request.resolver_match.url_name == 'yesterday_records' or 'add_reading' in request.path or 'edit_reading' in request.path or 'add_insight' in request.path or 'edit_insight' in request.path %}active{% endif %}" href="{% url 'yesterday_records' %}">
<i class="bi bi-journal-text"></i><span>昨日记录</span> <i class="bi bi-journal-text"></i><span>昨日记录</span>
@@ -484,6 +490,7 @@
<i class="bi bi-gear"></i><span>系统配置</span> <i class="bi bi-gear"></i><span>系统配置</span>
</a> </a>
</li> </li>
{% endif %}
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">
{% if user.is_authenticated %} {% if user.is_authenticated %}
@@ -504,11 +511,13 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if user.is_authenticated %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/houtai/"> <a class="nav-link" href="/houtai/">
<i class="bi bi-shield-lock"></i><span>后台管理</span> <i class="bi bi-shield-lock"></i><span>后台管理</span>
</a> </a>
</li> </li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
@@ -538,7 +547,7 @@
</p> </p>
<p class="mb-0 mt-2 small text-white-50"> <p class="mb-0 mt-2 small text-white-50">
<i class="bi bi-clock-history me-1"></i> <i class="bi bi-clock-history me-1"></i>
代码最后更新:{% git_last_commit_time %} 更新时间{% git_last_commit_time %}
</p> </p>
</div> </div>
</footer> </footer>

View File

@@ -0,0 +1,48 @@
{% extends 'core/base.html' %}
{% block content %}
<!-- 页面标题 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">
<i class="bi bi-trash me-2 text-danger"></i>删除公开内容
</h2>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-danger">
<div class="card-header bg-danger text-white text-center py-4">
<i class="bi bi-exclamation-triangle" style="font-size: 3rem;"></i>
<h5 class="card-title mb-0 mt-2">确认删除</h5>
</div>
<div class="card-body text-center">
<p class="mb-4">您确定要删除以下公开内容吗?</p>
<div class="alert alert-light border">
<div class="mb-2">
<span class="badge bg-info me-2">{{ content.get_type_display }}</span>
</div>
<h6 class="mb-0">{{ content.title }}</h6>
{% if content.content %}
<p class="mb-0 mt-2 text-muted small">{{ content.content|truncatechars:100 }}</p>
{% endif %}
</div>
<p class="text-danger small">
<i class="bi bi-exclamation-circle me-1"></i>
此操作不可撤销!
</p>
<form method="post" class="mt-4">
{% csrf_token %}
<div class="d-flex gap-2 justify-content-center">
<button type="submit" class="btn btn-danger px-4">
<i class="bi bi-trash me-1"></i>确认删除
</button>
<a href="{% url 'public_content' %}" class="btn btn-secondary px-4">
<i class="bi bi-x-lg me-1"></i>取消
</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,76 @@
{% extends 'core/base.html' %}
{% block content %}
<!-- 页面标题 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">
<i class="bi bi-pencil-square me-2 text-info"></i>编辑公开内容
</h2>
<a href="{% url 'public_content' %}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>返回
</a>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header bg-info text-dark">
<h5 class="card-title mb-0">
<i class="bi bi-pencil me-2"></i>修改公开内容信息
</h5>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for field in form %}
<div class="mb-4">
<label for="{{ field.id_for_label }}" class="form-label">
{% if field.name == 'type' %}
<i class="bi bi-folder me-1 text-info"></i>
{% elif field.name == 'title' %}
<i class="bi bi-fonts me-1 text-info"></i>
{% elif field.name == 'content' %}
<i class="bi bi-text-paragraph me-1 text-info"></i>
{% elif field.name == 'file' %}
<i class="bi bi-file-earmark me-1 text-info"></i>
{% elif field.name == 'url' %}
<i class="bi bi-link-45deg me-1 text-info"></i>
{% elif field.name == 'sort_order' %}
<i class="bi bi-sort-numeric-down me-1 text-info"></i>
{% elif field.name == 'is_published' %}
<i class="bi bi-eye me-1 text-info"></i>
{% else %}
<i class="bi bi-circle me-1 text-info"></i>
{% endif %}
{{ field.label }}
{% if field.field.required %}
<span class="text-danger">*</span>
{% endif %}
</label>
{{ field }}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
{% for error in field.errors %}
<div class="invalid-feedback d-block">
<i class="bi bi-exclamation-circle me-1"></i>{{ error }}
</div>
{% endfor %}
</div>
{% endfor %}
<div class="d-flex gap-2 justify-content-center mt-4">
<button type="submit" class="btn btn-info px-4 text-dark">
<i class="bi bi-check-lg me-1"></i>保存修改
</button>
<a href="{% url 'public_content' %}" class="btn btn-secondary px-4">
<i class="bi bi-x-lg me-1"></i>取消
</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,7 @@
{% extends 'core/base.html' %} {% extends 'core/base.html' %}
{% block content %} {% block content %}
{% if user.is_authenticated %}
<!-- 欢迎区域 --> <!-- 欢迎区域 -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
@@ -372,4 +373,21 @@
setCurrentYear(); setCurrentYear();
}); });
</script> </script>
{% else %}
<!-- 未登录欢迎区域 -->
<div class="row">
<div class="col-12">
<div class="card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<div class="card-body text-center py-5">
<i class="bi bi-house-heart" style="font-size: 5rem; opacity: 0.8;"></i>
<h2 class="mt-4 mb-3">欢迎使用家庭日报系统</h2>
<p class="mb-4 opacity-90">请先登录以使用系统功能</p>
<a href="{% url 'login' %}" class="btn btn-light btn-lg px-5">
<i class="bi bi-box-arrow-in-right me-2"></i>登录系统
</a>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,80 @@
{% extends 'core/base.html' %}
{% block content %}
<!-- 页面标题 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">
<i class="bi bi-globe me-2 text-info"></i>公开内容
</h2>
{% if user.is_authenticated %}
<div>
<a href="{% url 'add_public_content' %}" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>添加内容
</a>
</div>
{% endif %}
</div>
{% if content_by_type %}
{% for type_name, contents in content_by_type.items %}
<div class="card mb-4">
<div class="card-header bg-info text-dark d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="bi bi-folder me-2"></i>{{ type_name }}
</h5>
<span class="badge bg-light text-info">{{ contents|length }} 项</span>
</div>
<div class="card-body">
<div class="list-group">
{% for content in contents %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-2">
{% if content.url %}
<a href="{{ content.url }}" target="_blank" class="text-decoration-none">
<i class="bi bi-link-45deg me-1"></i>{{ content.title }}
</a>
{% else %}
<i class="bi bi-file-earmark-text me-1"></i>{{ content.title }}
{% endif %}
</h6>
{% if content.content %}
<p class="text-muted small mb-2">{{ content.content|truncatechars:200 }}</p>
{% endif %}
{% if content.file %}
<a href="{{ content.file.url }}" class="btn btn-sm btn-outline-primary me-2" target="_blank">
<i class="bi bi-download me-1"></i>下载文件
</a>
{% endif %}
</div>
{% if user.is_authenticated %}
<div class="btn-group ms-3">
<a href="{% url 'edit_public_content' content.id %}" class="btn btn-sm btn-warning" title="编辑">
<i class="bi bi-pencil"></i>
</a>
<a href="{% url 'delete_public_content' content.id %}" class="btn btn-sm btn-danger" title="删除">
<i class="bi bi-trash"></i>
</a>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-5">
<i class="bi bi-inbox text-muted" style="font-size: 5rem;"></i>
<h5 class="text-muted mt-3">暂无公开内容</h5>
<p class="text-muted">请稍后再来查看</p>
{% if user.is_authenticated %}
<a href="{% url 'add_public_content' %}" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i>添加内容
</a>
{% endif %}
</div>
{% endif %}
{% endblock %}

View File

@@ -68,4 +68,10 @@ urlpatterns = [
# 历史记录查询 # 历史记录查询
path('history/', history_views.history_records, name='history_records'), path('history/', history_views.history_records, name='history_records'),
path('history/pdf/', history_views.history_pdf, name='history_pdf'), path('history/pdf/', history_views.history_pdf, name='history_pdf'),
# 公开内容
path('public/', views.public_content, name='public_content'),
path('public/add/', views.add_public_content, name='add_public_content'),
path('public/<int:pk>/edit/', views.edit_public_content, name='edit_public_content'),
path('public/<int:pk>/delete/', views.delete_public_content, name='delete_public_content'),
] ]

View File

@@ -2,7 +2,7 @@ from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
from django.utils import timezone from django.utils import timezone
from django.db import models from django.db import models
from django.db.models import Count from django.db.models import Count, Q
from django.core.mail import send_mail, EmailMessage from django.core.mail import send_mail, EmailMessage
from django.conf import settings from django.conf import settings
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@@ -48,7 +48,9 @@ from .models import (
TodayPlan, TodayPlan,
SystemConfig, SystemConfig,
FamilyMember, FamilyMember,
SummaryCategory SummaryCategory,
PublicContentType,
PublicContent
) )
from .forms import ( from .forms import (
ReadingRecordForm, ReadingRecordForm,
@@ -56,7 +58,8 @@ from .forms import (
SummaryForm, SummaryForm,
FamilyTaskForm, FamilyTaskForm,
TodayPlanForm, TodayPlanForm,
SystemConfigForm SystemConfigForm,
PublicContentForm
) )
# 首页视图 # 首页视图
@@ -75,8 +78,10 @@ def index(request):
today_plan = TodayPlan.objects.filter(date=today) today_plan = TodayPlan.objects.filter(date=today)
# 获取未完成的家庭事项(排除已完成状态) # 获取未完成的家庭事项(排除已完成状态和已截止的事项
pending_family_tasks = FamilyTask.objects.exclude(status__name='completed') pending_family_tasks = FamilyTask.objects.exclude(status__name='completed')
# 过滤掉截止日期早于今天的事项(如果设置了截止日期)
pending_family_tasks = pending_family_tasks.filter(Q(deadline__gte=today) | Q(deadline__isnull=True))
context = { context = {
'yesterday': yesterday, 'yesterday': yesterday,
@@ -400,30 +405,30 @@ def delete_summary(request, pk):
def family_tasks(request): def family_tasks(request):
"""家庭事项 - 显示未完成的事项,一个月内显示详情,超过一个月显示数量""" """家庭事项 - 显示未完成的事项,一个月内显示详情,超过一个月显示数量"""
logger.info("用户访问家庭事项页面") logger.info("用户访问家庭事项页面")
today = timezone.now().date() today = timezone.now().date()
one_month_later = today + timedelta(days=30) one_month_later = today + timedelta(days=30)
# 获取所有未完成的事项 # 获取所有未完成的事项(排除已完成状态)
all_pending_tasks = FamilyTask.objects.exclude(status__name='completed') all_pending_tasks = FamilyTask.objects.exclude(status__name='completed')
# 一个月内到期的事项(显示详情) # 一个月内到期的事项(显示详情)
# 包括:有截止日期且在一个月内,或者没有截止日期的事项 # 包括:有截止日期且在一个月内,或者没有截止日期的事项
upcoming_tasks = all_pending_tasks.filter( upcoming_tasks = all_pending_tasks.filter(
models.Q(deadline__isnull=True) | models.Q(deadline__lte=one_month_later) Q(deadline__isnull=True) | Q(deadline__lte=one_month_later)
) )
# 超过一个月到期的事项(只显示数量) # 超过一个月到期的事项(只显示数量)
future_tasks = all_pending_tasks.filter(deadline__gt=one_month_later) future_tasks = all_pending_tasks.filter(deadline__gt=one_month_later)
future_tasks_count = future_tasks.count() future_tasks_count = future_tasks.count()
context = { context = {
'upcoming_tasks': upcoming_tasks, 'upcoming_tasks': upcoming_tasks,
'future_tasks_count': future_tasks_count, 'future_tasks_count': future_tasks_count,
'total_pending_count': all_pending_tasks.count(), 'total_pending_count': all_pending_tasks.count(),
'today': today, 'today': today,
} }
return render(request, 'core/family_tasks.html', context) return render(request, 'core/family_tasks.html', context)
# 添加家庭事项 # 添加家庭事项
@@ -969,3 +974,70 @@ def user_logout(request):
messages.success(request, '已成功注销!') messages.success(request, '已成功注销!')
return redirect('login') return redirect('login')
# 公开内容视图
def public_content(request):
"""公开内容页面 - 无需登录"""
logger.info("用户访问公开内容页面")
# 获取所有已发布的公开内容
public_contents = PublicContent.objects.filter(is_published=True)
# 按类型分组
content_by_type = {}
for content in public_contents:
type_name = content.type.name
if type_name not in content_by_type:
content_by_type[type_name] = []
content_by_type[type_name].append(content)
context = {
'content_by_type': content_by_type,
}
return render(request, 'core/public_content.html', context)
# 添加公开内容
@login_required
def add_public_content(request):
"""添加公开内容"""
if request.method == 'POST':
form = PublicContentForm(request.POST, request.FILES)
if form.is_valid():
form.save()
logger.info(f"添加公开内容: {form.cleaned_data['title']}")
return redirect('public_content')
else:
form = PublicContentForm()
context = {'form': form}
return render(request, 'core/add_public_content.html', context)
# 编辑公开内容
@login_required
def edit_public_content(request, pk):
"""编辑公开内容"""
content = get_object_or_404(PublicContent, pk=pk)
if request.method == 'POST':
form = PublicContentForm(request.POST, request.FILES, instance=content)
if form.is_valid():
form.save()
logger.info(f"编辑公开内容: {form.cleaned_data['title']}")
return redirect('public_content')
else:
form = PublicContentForm(instance=content)
context = {'form': form, 'content': content}
return render(request, 'core/edit_public_content.html', context)
# 删除公开内容
@login_required
def delete_public_content(request, pk):
"""删除公开内容"""
content = get_object_or_404(PublicContent, pk=pk)
if request.method == 'POST':
content.delete()
logger.info(f"删除公开内容: {content.title}")
return redirect('public_content')
context = {'content': content}
return render(request, 'core/delete_public_content.html', context)