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,
TodayPlan,
SystemConfig,
PublicContentType,
PublicContent,
)
@@ -96,3 +98,17 @@ class TodayPlanAdmin(admin.ModelAdmin):
@admin.register(SystemConfig)
class SystemConfigAdmin(admin.ModelAdmin):
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,
TodayPlan,
SystemConfig,
FamilyMember
FamilyMember,
PublicContent
)
class ReadingRecordForm(forms.ModelForm):
@@ -109,4 +110,19 @@ class SystemConfigForm(forms.ModelForm):
if not re.match(email_pattern, recipient_email):
logger.warning(f"收件人邮箱格式不正确: {recipient_email}")
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)
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>
</a>
</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">
<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>
@@ -484,6 +490,7 @@
<i class="bi bi-gear"></i><span>系统配置</span>
</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
{% if user.is_authenticated %}
@@ -504,11 +511,13 @@
</a>
</li>
{% endif %}
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="/houtai/">
<i class="bi bi-shield-lock"></i><span>后台管理</span>
</a>
</li>
{% endif %}
</ul>
</div>
</div>
@@ -538,7 +547,7 @@
</p>
<p class="mb-0 mt-2 small text-white-50">
<i class="bi bi-clock-history me-1"></i>
代码最后更新:{% git_last_commit_time %}
更新时间{% git_last_commit_time %}
</p>
</div>
</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' %}
{% block content %}
{% if user.is_authenticated %}
<!-- 欢迎区域 -->
<div class="row mb-4">
<div class="col-12">
@@ -372,4 +373,21 @@
setCurrentYear();
});
</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 %}

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/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.utils import timezone
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.conf import settings
from django.views.decorators.csrf import csrf_exempt
@@ -48,7 +48,9 @@ from .models import (
TodayPlan,
SystemConfig,
FamilyMember,
SummaryCategory
SummaryCategory,
PublicContentType,
PublicContent
)
from .forms import (
ReadingRecordForm,
@@ -56,7 +58,8 @@ from .forms import (
SummaryForm,
FamilyTaskForm,
TodayPlanForm,
SystemConfigForm
SystemConfigForm,
PublicContentForm
)
# 首页视图
@@ -75,8 +78,10 @@ def index(request):
today_plan = TodayPlan.objects.filter(date=today)
# 获取未完成的家庭事项(排除已完成状态)
# 获取未完成的家庭事项(排除已完成状态和已截止的事项
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 = {
'yesterday': yesterday,
@@ -400,30 +405,30 @@ def delete_summary(request, pk):
def family_tasks(request):
"""家庭事项 - 显示未完成的事项,一个月内显示详情,超过一个月显示数量"""
logger.info("用户访问家庭事项页面")
today = timezone.now().date()
one_month_later = today + timedelta(days=30)
# 获取所有未完成的事项
# 获取所有未完成的事项(排除已完成状态)
all_pending_tasks = FamilyTask.objects.exclude(status__name='completed')
# 一个月内到期的事项(显示详情)
# 包括:有截止日期且在一个月内,或者没有截止日期的事项
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_count = future_tasks.count()
context = {
'upcoming_tasks': upcoming_tasks,
'future_tasks_count': future_tasks_count,
'total_pending_count': all_pending_tasks.count(),
'today': today,
}
return render(request, 'core/family_tasks.html', context)
# 添加家庭事项
@@ -969,3 +974,70 @@ def user_logout(request):
messages.success(request, '已成功注销!')
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)