一个家庭日报系统

This commit is contained in:
2026-01-04 19:17:33 +08:00
commit eaee38d658
57 changed files with 2756 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
# 家庭日报系统设计与实现Django 5.1.4
## 系统概述
一个轻量化的家庭日报系统用于自动汇总昨日信息阅读、收获生成今日计划注重生活化、个性化和低门槛使用。支持上传文件或视频作为附加内容并可生成PDF报告定时发送至邮箱。
## 技术栈
- **Python 3.9+**:核心开发语言
- **Django 5.1.4**Web框架
- **SQLite**:轻量化数据库
- **loguru**:日志记录
- **Bootstrap 5**:前端样式
- **Chart.js**:数据可视化
- **WeasyPrint**PDF生成
- **Celery**:定时任务
- **Redis**消息队列用于Celery
- **django-celery-beat**:定时任务管理
## 项目结构
```
diary-family/
├── diary_family/ # Django项目目录
│ ├── settings.py # 项目配置
│ ├── urls.py # 路由配置
│ └── wsgi.py # WSGI配置
├── core/ # 核心应用
│ ├── migrations/ # 数据库迁移
│ ├── models.py # 数据模型
│ ├── views.py # 视图函数
│ ├── forms.py # 表单定义
│ ├── templates/ # 模板文件
│ ├── static/ # 静态文件
│ ├── tasks.py # Celery任务
│ └── apps.py # 应用配置
├── media/ # 媒体文件存储
├── logs/ # 日志目录
├── reports/ # PDF报告存储
├── manage.py # Django管理脚本
└── requirements.txt # 依赖文件
```
## 核心功能模块
### 1. 数据模型设计
#### 1.1 阅读记录表 (ReadingRecord)
- 日期、类型(书籍/文章/视频)、标题、来源、进度、上传文件
#### 1.2 感悟记录表 (InsightRecord)
- 日期、内容、上传文件
#### 1.3 家庭事项表 (FamilyTask)
- 类型(采购/家务)、内容、优先级、状态、截止日期
#### 1.4 今日计划表 (TodayPlan)
- 日期、内容、优先级、类型、状态
#### 1.5 系统配置表 (SystemConfig)
- 邮箱设置SMTP服务器、端口、用户名、密码
- 报告发送时间
- 收件人邮箱
### 2. 核心功能
#### 2.1 昨日记录
- 阅读记录(支持多类型)
- 感悟记录
- 支持文件/视频上传
#### 2.2 家庭事项管理
- 事项添加、编辑、删除
- 按类型、状态筛选
#### 2.3 今日计划生成
- 基于昨日状态和待处理事项自动生成
- 支持手动调整
- 完成状态标记
#### 2.4 报告生成
- 整合昨日阅读、感悟和今日计划
- 可视化统计
- 支持生成PDF格式
- 支持查看历史报告
#### 2.5 定时邮件发送
- 配置每日发送时间
- 自动生成并发送PDF报告到指定邮箱
- 支持手动触发发送
### 3. 文件上传功能
- 支持上传图片、文档、视频等多种格式
- 自动生成访问链接
- 支持预览和下载
## 实现步骤
### 1. 项目初始化
- 创建Django项目
- 创建核心应用
- 配置数据库和媒体文件路径
- 安装依赖包
### 2. 数据模型实现
- 定义模型类
- 生成并执行数据库迁移
- 创建超级用户
### 3. 功能实现
- 实现昨日记录表单和视图
- 实现家庭事项管理功能
- 实现今日计划生成和管理
- 实现报告生成功能
- 实现PDF报告生成
- 实现文件上传功能
- 配置Celery和定时任务
- 实现邮件发送功能
### 4. 模板设计
- 基础模板(导航栏、页脚)
- 昨日记录页面
- 家庭事项页面
- 今日计划页面
- 报告页面
- 系统配置页面
- PDF报告模板
### 5. 日志配置
- 使用loguru配置日志
- 记录关键操作和错误信息
## 运行方式
1. **安装依赖**
```bash
pip install -r requirements.txt
```
2. **启动Redis服务**
```bash
redis-server
```
3. **数据库迁移**
```bash
python manage.py migrate
```
4. **启动Django开发服务器**
```bash
python manage.py runserver
```
5. **启动Celery worker**
```bash
celery -A diary_family worker --loglevel=info
```
6. **启动Celery beat**
```bash
celery -A diary_family beat --loglevel=info
```
7. **访问系统**
- 浏览器打开 http://localhost:8000
- 支持移动端访问
## 系统特点
- **轻量化**使用SQLite部署简单
- **低门槛**:界面简洁,易于使用
- **生活化**:专注家庭场景
- **个性化**:支持自定义记录和计划
- **文件支持**:支持上传多种格式文件
- **可视化**:提供数据统计图表
- **响应式**:适配移动端和桌面端
- **PDF报告**支持生成PDF格式报告
- **定时邮件**:自动发送报告到指定邮箱
## 预期效果
用户可以通过手机或电脑轻松完成:
1. 记录昨日阅读内容和感悟,可上传相关文件
2. 管理家庭事项
3. 生成并查看今日计划
4. 查看自动生成的可视化报告支持PDF下载
5. 配置系统参数,设置邮件发送
6. 每日自动收到包含PDF报告的邮件
系统设计简洁易用,适合家庭日常使用,注重生活化和个性化体验。

0
core/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
core/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
core/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core'

84
core/forms.py Normal file
View File

@@ -0,0 +1,84 @@
from django import forms
from django.utils import timezone
from .models import (
ReadingRecord,
InsightRecord,
FamilyTask,
TodayPlan,
SystemConfig
)
class ReadingRecordForm(forms.ModelForm):
"""阅读记录表单"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 设置日期字段默认值为当日
self.fields['date'].initial = timezone.now().date()
class Meta:
model = ReadingRecord
fields = ['date', 'type', 'title', 'source', 'progress', 'file']
widgets = {
'date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control', 'readonly': 'readonly'}),
'type': forms.Select(attrs={'class': 'form-select'}),
'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入标题'}),
'source': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入来源'}),
'progress': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入进度'}),
'file': forms.FileInput(attrs={'class': 'form-control'}),
}
class InsightRecordForm(forms.ModelForm):
"""感悟记录表单"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 设置日期字段默认值为当日
self.fields['date'].initial = timezone.now().date()
class Meta:
model = InsightRecord
fields = ['date', 'content', 'file']
widgets = {
'date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control', 'readonly': 'readonly'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 5, 'placeholder': '请输入今日感悟'}),
'file': forms.FileInput(attrs={'class': 'form-control'}),
}
class FamilyTaskForm(forms.ModelForm):
"""家庭事项表单"""
class Meta:
model = FamilyTask
fields = ['type', 'content', 'priority', 'status', 'deadline']
widgets = {
'type': forms.Select(attrs={'class': 'form-select'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': '请输入事项内容'}),
'priority': forms.Select(attrs={'class': 'form-select'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'deadline': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
}
class TodayPlanForm(forms.ModelForm):
"""今日计划表单"""
class Meta:
model = TodayPlan
fields = ['date', 'content', 'priority', 'type', 'status']
widgets = {
'date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': '请输入计划内容'}),
'priority': forms.Select(attrs={'class': 'form-select'}),
'type': forms.Select(attrs={'class': 'form-select'}),
'status': forms.Select(attrs={'class': 'form-select'}),
}
class SystemConfigForm(forms.ModelForm):
"""系统配置表单"""
class Meta:
model = SystemConfig
fields = ['smtp_server', 'smtp_port', 'smtp_username', 'smtp_password', 'send_time', 'recipient_email']
widgets = {
'smtp_server': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入SMTP服务器'}),
'smtp_port': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '请输入SMTP端口'}),
'smtp_username': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入SMTP用户名'}),
'smtp_password': forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': '请输入SMTP密码'}),
'send_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
'recipient_email': forms.EmailInput(attrs={'class': 'form-control', 'placeholder': '请输入收件人邮箱'}),
}

View File

@@ -0,0 +1,18 @@
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
class Command(BaseCommand):
"""非交互式创建超级用户"""
help = 'Create a superuser non-interactively'
def handle(self, *args, **options):
User = get_user_model()
if not User.objects.filter(username='admin').exists():
User.objects.create_superuser(
username='admin',
email='admin@example.com',
password='admin123'
)
self.stdout.write(self.style.SUCCESS('Superuser created successfully!'))
else:
self.stdout.write(self.style.WARNING('Superuser already exists.'))

View File

@@ -0,0 +1,104 @@
# Generated by Django 5.1.4 on 2026-01-04 11:06
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='FamilyTask',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('purchase', '采购'), ('housework', '家务')], max_length=20, verbose_name='类型')),
('content', models.TextField(verbose_name='内容')),
('priority', models.CharField(choices=[('low', ''), ('medium', ''), ('high', '')], default='medium', max_length=10, verbose_name='优先级')),
('status', models.CharField(choices=[('pending', '待处理'), ('completed', '已完成')], default='pending', max_length=20, verbose_name='状态')),
('deadline', models.DateField(blank=True, null=True, verbose_name='截止日期')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '家庭事项',
'verbose_name_plural': '家庭事项',
'ordering': ['-priority', '-deadline', '-created_at'],
},
),
migrations.CreateModel(
name='InsightRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=django.utils.timezone.now, verbose_name='日期')),
('content', models.TextField(verbose_name='内容')),
('file', models.FileField(blank=True, null=True, upload_to='insight_files/', verbose_name='上传文件')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '感悟记录',
'verbose_name_plural': '感悟记录',
'ordering': ['-date', '-created_at'],
},
),
migrations.CreateModel(
name='ReadingRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=django.utils.timezone.now, verbose_name='日期')),
('type', models.CharField(choices=[('book', '书籍'), ('article', '文章'), ('video', '视频')], max_length=20, verbose_name='类型')),
('title', models.CharField(max_length=200, verbose_name='标题')),
('source', models.CharField(blank=True, max_length=200, null=True, verbose_name='来源')),
('progress', models.CharField(blank=True, max_length=100, null=True, verbose_name='进度')),
('file', models.FileField(blank=True, null=True, upload_to='reading_files/', verbose_name='上传文件')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '阅读记录',
'verbose_name_plural': '阅读记录',
'ordering': ['-date', '-created_at'],
},
),
migrations.CreateModel(
name='SystemConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('smtp_server', models.CharField(blank=True, max_length=100, null=True, verbose_name='SMTP服务器')),
('smtp_port', models.IntegerField(default=587, verbose_name='SMTP端口')),
('smtp_username', models.CharField(blank=True, max_length=100, null=True, verbose_name='SMTP用户名')),
('smtp_password', models.CharField(blank=True, max_length=100, null=True, verbose_name='SMTP密码')),
('send_time', models.TimeField(default='08:00', verbose_name='每日发送时间')),
('recipient_email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='收件人邮箱')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '系统配置',
'verbose_name_plural': '系统配置',
},
),
migrations.CreateModel(
name='TodayPlan',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=django.utils.timezone.now, verbose_name='日期')),
('content', models.TextField(verbose_name='内容')),
('priority', models.CharField(choices=[('low', ''), ('medium', ''), ('high', '')], default='medium', max_length=10, verbose_name='优先级')),
('type', models.CharField(choices=[('reading', '阅读'), ('insight', '感悟'), ('family', '家庭事项'), ('other', '其他')], default='other', max_length=20, verbose_name='类型')),
('status', models.CharField(choices=[('pending', '待处理'), ('completed', '已完成')], default='pending', max_length=20, verbose_name='状态')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '今日计划',
'verbose_name_plural': '今日计划',
'ordering': ['-priority', '-created_at'],
},
),
]

View File

Binary file not shown.

131
core/models.py Normal file
View File

@@ -0,0 +1,131 @@
from django.db import models
from django.utils import timezone
# 阅读记录类型
READING_TYPE_CHOICES = [
('book', '书籍'),
('article', '文章'),
('video', '视频'),
]
# 家庭事项类型
FAMILY_TASK_TYPE_CHOICES = [
('purchase', '采购'),
('housework', '家务'),
]
# 优先级
PRIORITY_CHOICES = [
('low', ''),
('medium', ''),
('high', ''),
]
# 状态
STATUS_CHOICES = [
('pending', '待处理'),
('completed', '已完成'),
]
# 今日计划类型
PLAN_TYPE_CHOICES = [
('reading', '阅读'),
('insight', '感悟'),
('family', '家庭事项'),
('other', '其他'),
]
class ReadingRecord(models.Model):
"""阅读记录表"""
date = models.DateField(default=timezone.now, verbose_name="日期")
type = models.CharField(max_length=20, choices=READING_TYPE_CHOICES, verbose_name="类型")
title = models.CharField(max_length=200, verbose_name="标题")
source = models.CharField(max_length=200, blank=True, null=True, verbose_name="来源")
progress = models.CharField(max_length=100, blank=True, null=True, verbose_name="进度")
file = models.FileField(upload_to='reading_files/', blank=True, null=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 = ['-date', '-created_at']
def __str__(self):
return f"{self.title} ({self.get_type_display()})"
class InsightRecord(models.Model):
"""感悟记录表"""
date = models.DateField(default=timezone.now, verbose_name="日期")
content = models.TextField(verbose_name="内容")
file = models.FileField(upload_to='insight_files/', blank=True, null=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 = ['-date', '-created_at']
def __str__(self):
return f"感悟记录 - {self.date}"
class FamilyTask(models.Model):
"""家庭事项表"""
type = models.CharField(max_length=20, choices=FAMILY_TASK_TYPE_CHOICES, verbose_name="类型")
content = models.TextField(verbose_name="内容")
priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default='medium', verbose_name="优先级")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="状态")
deadline = models.DateField(blank=True, null=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 = ['-priority', '-deadline', '-created_at']
def __str__(self):
return f"{self.get_type_display()} - {self.content[:20]}..."
class TodayPlan(models.Model):
"""今日计划表"""
date = models.DateField(default=timezone.now, verbose_name="日期")
content = models.TextField(verbose_name="内容")
priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default='medium', verbose_name="优先级")
type = models.CharField(max_length=20, choices=PLAN_TYPE_CHOICES, default='other', verbose_name="类型")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', 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 = ['-priority', '-created_at']
def __str__(self):
return f"{self.date} - {self.content[:20]}..."
class SystemConfig(models.Model):
"""系统配置表"""
smtp_server = models.CharField(max_length=100, blank=True, null=True, verbose_name="SMTP服务器")
smtp_port = models.IntegerField(default=587, verbose_name="SMTP端口")
smtp_username = models.CharField(max_length=100, blank=True, null=True, verbose_name="SMTP用户名")
smtp_password = models.CharField(max_length=100, blank=True, null=True, verbose_name="SMTP密码")
send_time = models.TimeField(default='08:00', verbose_name="每日发送时间")
recipient_email = models.EmailField(blank=True, null=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 = "系统配置"
def __str__(self):
return "系统配置"
@classmethod
def get_config(cls):
"""获取系统配置,单例模式"""
config, created = cls.objects.get_or_create(pk=1)
return config

123
core/tasks.py Normal file
View File

@@ -0,0 +1,123 @@
from celery import shared_task
from django.utils import timezone
from datetime import timedelta
from django.core.mail import EmailMessage
from django.conf import settings
from django.shortcuts import render
import os
from loguru import logger
# WeasyPrint可用性标记初始为None
WEASYPRINT_AVAILABLE = None
# 获取WeasyPrint可用性
def is_weasyprint_available():
"""检查WeasyPrint是否可用"""
global WEASYPRINT_AVAILABLE
if WEASYPRINT_AVAILABLE is None:
try:
from weasyprint import HTML
WEASYPRINT_AVAILABLE = True
except ImportError:
logger.warning("WeasyPrint库无法导入PDF功能将不可用")
WEASYPRINT_AVAILABLE = False
return WEASYPRINT_AVAILABLE
from .models import (
ReadingRecord,
InsightRecord,
FamilyTask,
TodayPlan,
SystemConfig
)
@shared_task
def send_daily_report():
"""发送每日报告"""
logger.info("开始执行每日报告发送任务")
# 检查WeasyPrint是否可用
if not is_weasyprint_available():
logger.error("WeasyPrint库不可用无法生成PDF报告")
return False
today = timezone.now().date()
today_str = today.strftime('%Y-%m-%d')
# 获取系统配置
config = SystemConfig.get_config()
# 检查邮件配置是否完整
if not all([config.smtp_server, config.smtp_username, config.smtp_password, config.recipient_email]):
logger.error("邮件配置不完整,无法发送邮件")
return False
# 生成报告数据
report_date = today
yesterday = report_date - timedelta(days=1)
# 获取昨日记录
yesterday_reading = ReadingRecord.objects.filter(date=yesterday)
yesterday_insight = InsightRecord.objects.filter(date=yesterday)
# 获取今日计划
today_plan = TodayPlan.objects.filter(date=report_date)
# 获取家庭事项统计
from django.db.models import Count
family_task_stats = FamilyTask.objects.values('type').annotate(count=Count('id'))
# 准备上下文
context = {
'today': report_date,
'yesterday': yesterday,
'yesterday_reading': yesterday_reading,
'yesterday_insight': yesterday_insight,
'today_plan': today_plan,
'family_task_stats': family_task_stats,
}
# 渲染HTML模板
html_string = render(None, 'core/report_pdf.html', context).content.decode('utf-8')
# 生成PDF报告
pdf_file = f"report_{today_str}.pdf"
pdf_path = os.path.join(settings.REPORTS_ROOT, pdf_file)
# 确保报告目录存在
os.makedirs(settings.REPORTS_ROOT, exist_ok=True)
# 生成PDF - 动态导入WeasyPrint
try:
from weasyprint import HTML
HTML(string=html_string).write_pdf(pdf_path)
logger.info(f"PDF报告生成成功: {pdf_path}")
except Exception as e:
logger.error(f"PDF报告生成失败: {str(e)}")
return False
# 发送邮件
subject = f"家庭日报 - {today_str}"
message = f"这是您的家庭日报,日期:{today_str}"
from_email = config.smtp_username
recipient_list = [config.recipient_email]
try:
email = EmailMessage(
subject=subject,
body=message,
from_email=from_email,
to=recipient_list,
)
# 添加附件
with open(pdf_path, 'rb') as f:
email.attach(pdf_file, f.read(), 'application/pdf')
# 发送邮件
email.send()
logger.info(f"邮件发送成功,收件人:{config.recipient_email}")
return True
except Exception as e:
logger.error(f"邮件发送失败:{str(e)}")
return False

View File

@@ -0,0 +1,27 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>添加家庭事项</h2>
<form method="post" class="mt-4">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</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">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<div class="mt-4">
<button type="submit" class="btn btn-primary">保存</button>
<a href="{% url 'family_tasks' %}" class="btn btn-secondary">取消</a>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>添加感悟记录</h2>
<form method="post" enctype="multipart/form-data" class="mt-4">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</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">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<div class="mt-4">
<button type="submit" class="btn btn-primary">保存</button>
<a href="{% url 'yesterday_records' %}" class="btn btn-secondary">取消</a>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>添加阅读记录</h2>
<form method="post" enctype="multipart/form-data" class="mt-4">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</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">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<div class="mt-4">
<button type="submit" class="btn btn-primary">保存</button>
<a href="{% url 'yesterday_records' %}" class="btn btn-secondary">取消</a>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>添加今日计划</h2>
<form method="post" class="mt-4">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</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">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<div class="mt-4">
<button type="submit" class="btn btn-primary">保存</button>
<a href="{% url 'today_plan' %}" class="btn btn-secondary">取消</a>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>家庭日报系统</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
/* 自定义样式 */
.nav-item.active .nav-link {
font-weight: bold;
color: #0d6efd !important;
}
.card {
margin-bottom: 1.5rem;
}
.btn-sm {
margin: 0 0.25rem;
}
.file-link {
font-size: 0.875rem;
color: #6c757d;
}
.status-badge {
font-size: 0.75rem;
}
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="{% url 'index' %}">家庭日报系统</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'index' %}active{% endif %}" href="{% url 'index' %}">首页</a>
</li>
<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>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'family_tasks' or 'add_family_task' in request.path or 'edit_family_task' in request.path %}active{% endif %}" href="{% url 'family_tasks' %}">家庭事项</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'today_plan' or 'add_today_plan' in request.path or 'edit_today_plan' in request.path %}active{% endif %}" href="{% url 'today_plan' %}">今日计划</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'generate_report' or 'view_report' in request.path or 'generate_pdf_report' in request.path %}active{% endif %}" href="{% url 'generate_report' %}">报告生成</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'system_settings' %}active{% endif %}" href="{% url 'system_settings' %}">系统配置</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- 主要内容区域 -->
<main class="container mt-4">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% block content %}
{% endblock %}
</main>
<!-- 页脚 -->
<footer class="bg-light text-center text-lg-start mt-5 py-3">
<div class="container">
<p class="text-muted">© 2024 家庭日报系统 - 专注于家庭生活的轻量级日报系统</p>
</div>
</footer>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- 自定义JS -->
<script>
// 切换计划状态
document.addEventListener('DOMContentLoaded', function() {
const toggleButtons = document.querySelectorAll('.toggle-status-btn');
toggleButtons.forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const url = this.getAttribute('href');
const statusBadge = this.closest('tr').querySelector('.status-badge');
fetch(url, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'completed') {
statusBadge.className = 'badge bg-success status-badge';
statusBadge.textContent = '已完成';
this.innerHTML = '<i class="bi bi-check2-square"></i> 已完成';
} else {
statusBadge.className = 'badge bg-warning status-badge';
statusBadge.textContent = '待处理';
this.innerHTML = '<i class="bi bi-square"></i> 标记完成';
}
})
.catch(error => {
console.error('Error:', error);
});
});
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,18 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>删除家庭事项</h2>
<div class="alert alert-danger mt-4">
<p>您确定要删除这条家庭事项吗?</p>
<p><strong>{{ task.content }}</strong> ({{ task.get_type_display }})</p>
<p>此操作不可恢复。</p>
</div>
<form method="post" class="mt-4">
{% csrf_token %}
<button type="submit" class="btn btn-danger">确认删除</button>
<a href="{% url 'family_tasks' %}" class="btn btn-secondary">取消</a>
</form>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>删除感悟记录</h2>
<div class="alert alert-danger mt-4">
<p>您确定要删除这条感悟记录吗?</p>
<p><strong>{{ insight.content|truncatechars:50 }}</strong></p>
<p>此操作不可恢复。</p>
</div>
<form method="post" class="mt-4">
{% csrf_token %}
<button type="submit" class="btn btn-danger">确认删除</button>
<a href="{% url 'yesterday_records' %}" class="btn btn-secondary">取消</a>
</form>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>删除阅读记录</h2>
<div class="alert alert-danger mt-4">
<p>您确定要删除这条阅读记录吗?</p>
<p><strong>{{ reading.title }}</strong> ({{ reading.get_type_display }})</p>
<p>此操作不可恢复。</p>
</div>
<form method="post" class="mt-4">
{% csrf_token %}
<button type="submit" class="btn btn-danger">确认删除</button>
<a href="{% url 'yesterday_records' %}" class="btn btn-secondary">取消</a>
</form>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>删除今日计划</h2>
<div class="alert alert-danger mt-4">
<p>您确定要删除这条今日计划吗?</p>
<p><strong>{{ plan.content }}</strong> ({{ plan.get_type_display }})</p>
<p>此操作不可恢复。</p>
</div>
<form method="post" class="mt-4">
{% csrf_token %}
<button type="submit" class="btn btn-danger">确认删除</button>
<a href="{% url 'today_plan' %}" class="btn btn-secondary">取消</a>
</form>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>编辑家庭事项</h2>
<form method="post" class="mt-4">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</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">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<div class="mt-4">
<button type="submit" class="btn btn-primary">保存</button>
<a href="{% url 'family_tasks' %}" class="btn btn-secondary">取消</a>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>编辑感悟记录</h2>
<form method="post" enctype="multipart/form-data" class="mt-4">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</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">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<div class="mt-4">
<button type="submit" class="btn btn-primary">保存</button>
<a href="{% url 'yesterday_records' %}" class="btn btn-secondary">取消</a>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>编辑阅读记录</h2>
<form method="post" enctype="multipart/form-data" class="mt-4">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</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">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<div class="mt-4">
<button type="submit" class="btn btn-primary">保存</button>
<a href="{% url 'yesterday_records' %}" class="btn btn-secondary">取消</a>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>编辑今日计划</h2>
<form method="post" class="mt-4">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</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">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<div class="mt-4">
<button type="submit" class="btn btn-primary">保存</button>
<a href="{% url 'today_plan' %}" class="btn btn-secondary">取消</a>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,61 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>家庭事项</h2>
<!-- 添加事项按钮 -->
<div class="mb-4">
<a href="{% url 'add_family_task' %}" class="btn btn-primary">添加家庭事项</a>
</div>
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">家庭事项列表</h5>
</div>
<div class="card-body">
{% if tasks %}
<table class="table table-striped">
<thead>
<tr>
<th>类型</th>
<th>内容</th>
<th>优先级</th>
<th>状态</th>
<th>截止日期</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for task in tasks %}
<tr>
<td>{{ task.get_type_display }}</td>
<td>{{ task.content }}</td>
<td>
<span class="badge {% if task.priority == 'high' %}bg-danger{% elif task.priority == 'medium' %}bg-warning{% else %}bg-info{% endif %}">
{{ task.get_priority_display }}
</span>
</td>
<td>
<span class="badge {% if task.status == 'completed' %}bg-success{% else %}bg-warning{% endif %}">
{{ task.get_status_display }}
</span>
</td>
<td>{{ task.deadline|default:"-" }}</td>
<td>
<a href="{% url 'edit_family_task' task.id %}" class="btn btn-sm btn-warning" title="编辑">
<i class="bi bi-pencil"></i>
</a>
<a href="{% url 'delete_family_task' task.id %}" class="btn btn-sm btn-danger" title="删除">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">还没有家庭事项,点击上方按钮添加</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,141 @@
{% extends 'core/base.html' %}
{% block content %}
<div class="row">
<!-- 今日概览 -->
<div class="col-md-8">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">今日概览</h5>
</div>
<div class="card-body">
<h6>日期:{{ today }}</h6>
<div class="mt-4">
<h5>今日计划</h5>
{% if today_plan %}
<ul class="list-group">
{% for plan in today_plan %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>{{ plan.content }}</strong>
<small class="text-muted"> - {{ plan.get_type_display }} - {{ plan.get_priority_display }}</small>
</div>
<span class="badge {% if plan.status == 'completed' %}bg-success{% else %}bg-warning{% endif %} status-badge">
{{ plan.get_status_display }}
</span>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">今天还没有计划,快去添加吧!</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 待处理事项 -->
<div class="col-md-4">
<div class="card">
<div class="card-header bg-warning text-dark">
<h5 class="card-title mb-0">待处理事项</h5>
</div>
<div class="card-body">
{% if pending_family_tasks %}
<ul class="list-group">
{% for task in pending_family_tasks %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>{{ task.content }}</strong>
<small class="text-muted"> - {{ task.get_type_display }}</small>
</div>
<span class="badge bg-info status-badge">{{ task.get_priority_display }}</span>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">没有待处理的家庭事项</p>
{% endif %}
<a href="{% url 'family_tasks' %}" class="btn btn-sm btn-outline-warning mt-3">查看所有家庭事项</a>
</div>
</div>
<!-- 快捷操作 -->
<div class="card mt-4">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0">快捷操作</h5>
</div>
<div class="card-body">
<div class="btn-group-vertical w-100">
<a href="{% url 'yesterday_records' %}" class="btn btn-outline-primary mb-2">记录昨日信息</a>
<a href="{% url 'today_plan' %}" class="btn btn-outline-primary mb-2">管理今日计划</a>
<a href="{% url 'generate_report' %}" class="btn btn-outline-primary mb-2">查看今日报告</a>
<a href="{% url 'send_email' %}" class="btn btn-outline-success">发送今日邮件</a>
</div>
</div>
</div>
</div>
</div>
<!-- 昨日记录 -->
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header bg-secondary text-white">
<h5 class="card-title mb-0">昨日记录 ({{ yesterday }})</h5>
</div>
<div class="card-body">
<div class="row">
<!-- 昨日阅读 -->
<div class="col-md-6">
<h5>阅读记录</h5>
{% if yesterday_reading %}
<ul class="list-group">
{% for reading in yesterday_reading %}
<li class="list-group-item">
<strong>{{ reading.title }}</strong> ({{ reading.get_type_display }})
{% if reading.source %}
<small class="text-muted"> - {{ reading.source }}</small>
{% endif %}
{% if reading.progress %}
<div class="mt-1"><small>进度:{{ reading.progress }}</small></div>
{% endif %}
{% if reading.file %}
<div class="mt-1">
<a href="{{ reading.file.url }}" class="file-link" target="_blank"><i class="bi bi-file-earmark"></i> 查看附件</a>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">昨日没有阅读记录</p>
{% endif %}
</div>
<!-- 昨日感悟 -->
<div class="col-md-6">
<h5>感悟记录</h5>
{% if yesterday_insight %}
<ul class="list-group">
{% for insight in yesterday_insight %}
<li class="list-group-item">
{{ insight.content }}
{% if insight.file %}
<div class="mt-1">
<a href="{{ insight.file.url }}" class="file-link" target="_blank"><i class="bi bi-file-earmark"></i> 查看附件</a>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">昨日没有感悟记录</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,171 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>家庭日报报告</h2>
<div class="mb-4">
<a href="{% url 'generate_pdf_report' today|date:'Y-m-d' %}" class="btn btn-primary">
<i class="bi bi-file-pdf"></i> 生成PDF报告
</a>
<a href="{% url 'send_email' %}" class="btn btn-success">
<i class="bi bi-envelope"></i> 发送邮件
</a>
</div>
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">报告概览</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>报告日期:{{ today }}</h6>
<h6>昨日日期:{{ yesterday }}</h6>
</div>
<div class="col-md-6">
<div class="chart-container" style="height: 200px;">
<canvas id="familyTaskChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- 昨日阅读记录 -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">昨日阅读记录</h5>
</div>
<div class="card-body">
{% if yesterday_reading %}
<table class="table table-striped">
<thead>
<tr>
<th>类型</th>
<th>标题</th>
<th>来源</th>
<th>进度</th>
</tr>
</thead>
<tbody>
{% for reading in yesterday_reading %}
<tr>
<td>{{ reading.get_type_display }}</td>
<td>{{ reading.title }}</td>
<td>{{ reading.source|default:"-" }}</td>
<td>{{ reading.progress|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">昨日没有阅读记录</p>
{% endif %}
</div>
</div>
</div>
<!-- 昨日感悟记录 -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0">昨日感悟记录</h5>
</div>
<div class="card-body">
{% if yesterday_insight %}
<ul class="list-group">
{% for insight in yesterday_insight %}
<li class="list-group-item">{{ insight.content }}</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">昨日没有感悟记录</p>
{% endif %}
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header bg-warning text-dark">
<h5 class="card-title mb-0">今日计划</h5>
</div>
<div class="card-body">
{% if today_plan %}
<table class="table table-striped">
<thead>
<tr>
<th>类型</th>
<th>内容</th>
<th>优先级</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{% for plan in today_plan %}
<tr>
<td>{{ plan.get_type_display }}</td>
<td>{{ plan.content }}</td>
<td>
<span class="badge {% if plan.priority == 'high' %}bg-danger{% elif plan.priority == 'medium' %}bg-warning{% else %}bg-info{% endif %}">
{{ plan.get_priority_display }}
</span>
</td>
<td>
<span class="badge {% if plan.status == 'completed' %}bg-success{% else %}bg-warning{% endif %}">
{{ plan.get_status_display }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">今天还没有计划</p>
{% endif %}
</div>
</div>
<script>
// 家庭事项统计图表
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('familyTaskChart').getContext('2d');
const familyTaskStats = {% if family_task_stats %}{{ family_task_stats|safe }}{% else %}[]{% endif %};
const labels = familyTaskStats.map(stat => {
return stat.type === 'purchase' ? '采购' : '家务';
});
const data = familyTaskStats.map(stat => stat.count);
new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: [
'#0d6efd',
'#198754'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '家庭事项统计'
},
legend: {
position: 'bottom'
}
}
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,203 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>家庭日报 - {{ today }}</title>
<style>
/* PDF样式 */
body {
font-family: SimHei, Arial, sans-serif;
margin: 20px;
font-size: 14px;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #000;
padding-bottom: 10px;
}
h2 {
color: #555;
font-size: 18px;
margin-top: 25px;
margin-bottom: 15px;
border-left: 4px solid #007bff;
padding-left: 10px;
}
h3 {
color: #666;
font-size: 16px;
margin-top: 20px;
margin-bottom: 10px;
}
.summary {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
table, th, td {
border: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
padding: 8px;
text-align: left;
}
td {
padding: 8px;
}
.section {
margin-bottom: 30px;
}
.no-data {
text-align: center;
color: #999;
padding: 20px;
}
ul {
margin: 0;
padding-left: 20px;
}
li {
margin-bottom: 8px;
}
.footer {
margin-top: 40px;
text-align: center;
color: #999;
font-size: 12px;
}
</style>
</head>
<body>
<h1>家庭日报</h1>
<div class="summary">
<h3>报告概览</h3>
<p>报告日期:{{ today }}</p>
<p>昨日日期:{{ yesterday }}</p>
</div>
<!-- 昨日阅读记录 -->
<div class="section">
<h2>昨日阅读记录</h2>
{% if yesterday_reading %}
<table>
<thead>
<tr>
<th>类型</th>
<th>标题</th>
<th>来源</th>
<th>进度</th>
</tr>
</thead>
<tbody>
{% for reading in yesterday_reading %}
<tr>
<td>{{ reading.get_type_display }}</td>
<td>{{ reading.title }}</td>
<td>{{ reading.source|default:"-" }}</td>
<td>{{ reading.progress|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="no-data">昨日没有阅读记录</div>
{% endif %}
</div>
<!-- 昨日感悟记录 -->
<div class="section">
<h2>昨日感悟记录</h2>
{% if yesterday_insight %}
<ul>
{% for insight in yesterday_insight %}
<li>{{ insight.content }}</li>
{% endfor %}
</ul>
{% else %}
<div class="no-data">昨日没有感悟记录</div>
{% endif %}
</div>
<!-- 今日计划 -->
<div class="section">
<h2>今日计划</h2>
{% if today_plan %}
<table>
<thead>
<tr>
<th>类型</th>
<th>内容</th>
<th>优先级</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{% for plan in today_plan %}
<tr>
<td>{{ plan.get_type_display }}</td>
<td>{{ plan.content }}</td>
<td>{{ plan.get_priority_display }}</td>
<td>{{ plan.get_status_display }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="no-data">今天还没有计划</div>
{% endif %}
</div>
<!-- 家庭事项统计 -->
<div class="section">
<h2>家庭事项统计</h2>
{% if family_task_stats %}
<table>
<thead>
<tr>
<th>类型</th>
<th>数量</th>
</tr>
</thead>
<tbody>
{% for stat in family_task_stats %}
<tr>
<td>{{ stat.type|yesno:"采购,家务" }}</td>
<td>{{ stat.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="no-data">没有家庭事项统计数据</div>
{% endif %}
</div>
<div class="footer">
<p>此报告由家庭日报系统自动生成</p>
<p>生成时间:{{ today|date:"Y年m月d日 H:i:s" }}</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,95 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>系统配置</h2>
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">系统配置设置</h5>
</div>
<div class="card-body">
<form method="post" class="mt-4">
{% csrf_token %}
<div class="row">
<div class="col-md-6">
<h6>邮件配置</h6>
<div class="mb-3">
<label for="{{ form.smtp_server.id_for_label }}" class="form-label">{{ form.smtp_server.label }}</label>
{{ form.smtp_server }}
{% if form.smtp_server.help_text %}
<div class="form-text">{{ form.smtp_server.help_text }}</div>
{% endif %}
{% for error in form.smtp_server.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ form.smtp_port.id_for_label }}" class="form-label">{{ form.smtp_port.label }}</label>
{{ form.smtp_port }}
{% if form.smtp_port.help_text %}
<div class="form-text">{{ form.smtp_port.help_text }}</div>
{% endif %}
{% for error in form.smtp_port.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ form.smtp_username.id_for_label }}" class="form-label">{{ form.smtp_username.label }}</label>
{{ form.smtp_username }}
{% if form.smtp_username.help_text %}
<div class="form-text">{{ form.smtp_username.help_text }}</div>
{% endif %}
{% for error in form.smtp_username.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ form.smtp_password.id_for_label }}" class="form-label">{{ form.smtp_password.label }}</label>
{{ form.smtp_password }}
{% if form.smtp_password.help_text %}
<div class="form-text">{{ form.smtp_password.help_text }}</div>
{% endif %}
{% for error in form.smtp_password.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="col-md-6">
<h6>发送配置</h6>
<div class="mb-3">
<label for="{{ form.send_time.id_for_label }}" class="form-label">{{ form.send_time.label }}</label>
{{ form.send_time }}
{% if form.send_time.help_text %}
<div class="form-text">{{ form.send_time.help_text }}</div>
{% endif %}
{% for error in form.send_time.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ form.recipient_email.id_for_label }}" class="form-label">{{ form.recipient_email.label }}</label>
{{ form.recipient_email }}
{% if form.recipient_email.help_text %}
<div class="form-text">{{ form.recipient_email.help_text }}</div>
{% endif %}
{% for error in form.recipient_email.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary">保存配置</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,66 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>今日计划</h2>
<!-- 添加计划按钮 -->
<div class="mb-4">
<a href="{% url 'add_today_plan' %}" class="btn btn-primary">添加今日计划</a>
</div>
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">今日计划列表</h5>
</div>
<div class="card-body">
{% if plans %}
<table class="table table-striped">
<thead>
<tr>
<th>类型</th>
<th>内容</th>
<th>优先级</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for plan in plans %}
<tr>
<td>{{ plan.get_type_display }}</td>
<td>{{ plan.content }}</td>
<td>
<span class="badge {% if plan.priority == 'high' %}bg-danger{% elif plan.priority == 'medium' %}bg-warning{% else %}bg-info{% endif %}">
{{ plan.get_priority_display }}
</span>
</td>
<td>
<span class="badge {% if plan.status == 'completed' %}bg-success{% else %}bg-warning{% endif %} status-badge">
{{ plan.get_status_display }}
</span>
</td>
<td>
<a href="{% url 'toggle_today_plan' plan.id %}" class="btn btn-sm btn-success toggle-status-btn">
{% if plan.status == 'completed' %}
<i class="bi bi-check2-square"></i> 已完成
{% else %}
<i class="bi bi-square"></i> 标记完成
{% endif %}
</a>
<a href="{% url 'edit_today_plan' plan.id %}" class="btn btn-sm btn-warning" title="编辑">
<i class="bi bi-pencil"></i>
</a>
<a href="{% url 'delete_today_plan' plan.id %}" class="btn btn-sm btn-danger" title="删除">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">今天还没有计划,快去添加吧!</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,105 @@
{% extends 'core/base.html' %}
{% block content %}
<h2>昨日记录 ({{ yesterday }})</h2>
<!-- 添加记录按钮 -->
<div class="mb-4">
<a href="{% url 'add_reading' %}" class="btn btn-primary">添加阅读记录</a>
<a href="{% url 'add_insight' %}" class="btn btn-success">添加感悟记录</a>
</div>
<div class="row">
<!-- 阅读记录 -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">阅读记录</h5>
</div>
<div class="card-body">
{% if reading_records %}
<table class="table table-striped">
<thead>
<tr>
<th>类型</th>
<th>标题</th>
<th>来源</th>
<th>进度</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for reading in reading_records %}
<tr>
<td>{{ reading.get_type_display }}</td>
<td>{{ reading.title }}</td>
<td>{{ reading.source|default:"-" }}</td>
<td>{{ reading.progress|default:"-" }}</td>
<td>
{% if reading.file %}
<a href="{{ reading.file.url }}" class="btn btn-sm btn-info" title="查看附件" target="_blank">
<i class="bi bi-file-earmark"></i>
</a>
{% endif %}
<a href="{% url 'edit_reading' reading.id %}" class="btn btn-sm btn-warning" title="编辑">
<i class="bi bi-pencil"></i>
</a>
<a href="{% url 'delete_reading' reading.id %}" class="btn btn-sm btn-danger" title="删除">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">昨日没有阅读记录,点击上方按钮添加</p>
{% endif %}
</div>
</div>
</div>
<!-- 感悟记录 -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0">感悟记录</h5>
</div>
<div class="card-body">
{% if insight_records %}
<table class="table table-striped">
<thead>
<tr>
<th>内容</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for insight in insight_records %}
<tr>
<td>{{ insight.content }}</td>
<td>
{% if insight.file %}
<a href="{{ insight.file.url }}" class="btn btn-sm btn-info" title="查看附件" target="_blank">
<i class="bi bi-file-earmark"></i>
</a>
{% endif %}
<a href="{% url 'edit_insight' insight.id %}" class="btn btn-sm btn-warning" title="编辑">
<i class="bi bi-pencil"></i>
</a>
<a href="{% url 'delete_insight' insight.id %}" class="btn btn-sm btn-danger" title="删除">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">昨日没有感悟记录,点击上方按钮添加</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

3
core/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

40
core/urls.py Normal file
View File

@@ -0,0 +1,40 @@
from django.urls import path
from . import views
urlpatterns = [
# 首页
path('', views.index, name='index'),
# 昨日记录
path('yesterday/', views.yesterday_records, name='yesterday_records'),
path('yesterday/reading/add/', views.add_reading, name='add_reading'),
path('yesterday/reading/<int:pk>/edit/', views.edit_reading, name='edit_reading'),
path('yesterday/reading/<int:pk>/delete/', views.delete_reading, name='delete_reading'),
path('yesterday/insight/add/', views.add_insight, name='add_insight'),
path('yesterday/insight/<int:pk>/edit/', views.edit_insight, name='edit_insight'),
path('yesterday/insight/<int:pk>/delete/', views.delete_insight, name='delete_insight'),
# 家庭事项
path('family-tasks/', views.family_tasks, name='family_tasks'),
path('family-tasks/add/', views.add_family_task, name='add_family_task'),
path('family-tasks/<int:pk>/edit/', views.edit_family_task, name='edit_family_task'),
path('family-tasks/<int:pk>/delete/', views.delete_family_task, name='delete_family_task'),
# 今日计划
path('today-plan/', views.today_plan, name='today_plan'),
path('today-plan/add/', views.add_today_plan, name='add_today_plan'),
path('today-plan/<int:pk>/edit/', views.edit_today_plan, name='edit_today_plan'),
path('today-plan/<int:pk>/delete/', views.delete_today_plan, name='delete_today_plan'),
path('today-plan/<int:pk>/toggle/', views.toggle_today_plan, name='toggle_today_plan'),
# 报告生成
path('report/', views.generate_report, name='generate_report'),
path('report/<str:date>/', views.view_report, name='view_report'),
path('report/<str:date>/pdf/', views.generate_pdf_report, name='generate_pdf_report'),
# 系统配置
path('settings/', views.system_settings, name='system_settings'),
# 手动发送邮件
path('send-email/', views.send_email, name='send_email'),
]

503
core/views.py Normal file
View File

@@ -0,0 +1,503 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse, JsonResponse
from django.utils import timezone
from django.db.models import Count
from django.core.mail import send_mail, EmailMessage
from django.conf import settings
from datetime import timedelta, datetime
import os
from loguru import logger
# WeasyPrint可用性标记初始为None
WEASYPRINT_AVAILABLE = None
# 获取WeasyPrint可用性
def is_weasyprint_available():
"""检查WeasyPrint是否可用"""
global WEASYPRINT_AVAILABLE
if WEASYPRINT_AVAILABLE is None:
try:
from weasyprint import HTML
WEASYPRINT_AVAILABLE = True
except ImportError:
logger.warning("WeasyPrint库无法导入PDF功能将不可用")
WEASYPRINT_AVAILABLE = False
return WEASYPRINT_AVAILABLE
from .models import (
ReadingRecord,
InsightRecord,
FamilyTask,
TodayPlan,
SystemConfig
)
from .forms import (
ReadingRecordForm,
InsightRecordForm,
FamilyTaskForm,
TodayPlanForm,
SystemConfigForm
)
# 首页视图
def index(request):
"""首页"""
logger.info("用户访问首页")
today = timezone.now().date()
yesterday = today - timedelta(days=1)
# 获取昨日记录
yesterday_reading = ReadingRecord.objects.filter(date=yesterday)
yesterday_insight = InsightRecord.objects.filter(date=yesterday)
# 获取今日计划
today_plan = TodayPlan.objects.filter(date=today)
# 获取待处理的家庭事项
pending_family_tasks = FamilyTask.objects.filter(status='pending')
context = {
'yesterday': yesterday,
'today': today,
'yesterday_reading': yesterday_reading,
'yesterday_insight': yesterday_insight,
'today_plan': today_plan,
'pending_family_tasks': pending_family_tasks,
}
return render(request, 'core/index.html', context)
# 昨日记录视图
def yesterday_records(request):
"""昨日记录"""
logger.info("用户访问昨日记录页面")
today = timezone.now().date()
yesterday = today - timedelta(days=1)
# 获取昨日阅读记录
reading_records = ReadingRecord.objects.filter(date=yesterday)
# 获取昨日感悟记录
insight_records = InsightRecord.objects.filter(date=yesterday)
context = {
'yesterday': yesterday,
'reading_records': reading_records,
'insight_records': insight_records,
}
return render(request, 'core/yesterday_records.html', context)
# 添加阅读记录
def add_reading(request):
"""添加阅读记录"""
if request.method == 'POST':
form = ReadingRecordForm(request.POST, request.FILES)
if form.is_valid():
form.save()
logger.info(f"添加阅读记录: {form.cleaned_data['title']}")
return redirect('yesterday_records')
else:
form = ReadingRecordForm()
context = {'form': form}
return render(request, 'core/add_reading.html', context)
# 编辑阅读记录
def edit_reading(request, pk):
"""编辑阅读记录"""
reading = get_object_or_404(ReadingRecord, pk=pk)
if request.method == 'POST':
form = ReadingRecordForm(request.POST, request.FILES, instance=reading)
if form.is_valid():
form.save()
logger.info(f"编辑阅读记录: {form.cleaned_data['title']}")
return redirect('yesterday_records')
else:
form = ReadingRecordForm(instance=reading)
context = {'form': form, 'reading': reading}
return render(request, 'core/edit_reading.html', context)
# 删除阅读记录
def delete_reading(request, pk):
"""删除阅读记录"""
reading = get_object_or_404(ReadingRecord, pk=pk)
if request.method == 'POST':
reading.delete()
logger.info(f"删除阅读记录: {reading.title}")
return redirect('yesterday_records')
context = {'reading': reading}
return render(request, 'core/delete_reading.html', context)
# 添加感悟记录
def add_insight(request):
"""添加感悟记录"""
if request.method == 'POST':
form = InsightRecordForm(request.POST, request.FILES)
if form.is_valid():
form.save()
logger.info("添加感悟记录")
return redirect('yesterday_records')
else:
form = InsightRecordForm()
context = {'form': form}
return render(request, 'core/add_insight.html', context)
# 编辑感悟记录
def edit_insight(request, pk):
"""编辑感悟记录"""
insight = get_object_or_404(InsightRecord, pk=pk)
if request.method == 'POST':
form = InsightRecordForm(request.POST, request.FILES, instance=insight)
if form.is_valid():
form.save()
logger.info("编辑感悟记录")
return redirect('yesterday_records')
else:
form = InsightRecordForm(instance=insight)
context = {'form': form, 'insight': insight}
return render(request, 'core/edit_insight.html', context)
# 删除感悟记录
def delete_insight(request, pk):
"""删除感悟记录"""
insight = get_object_or_404(InsightRecord, pk=pk)
if request.method == 'POST':
insight.delete()
logger.info("删除感悟记录")
return redirect('yesterday_records')
context = {'insight': insight}
return render(request, 'core/delete_insight.html', context)
# 家庭事项视图
def family_tasks(request):
"""家庭事项"""
logger.info("用户访问家庭事项页面")
tasks = FamilyTask.objects.all()
context = {
'tasks': tasks,
}
return render(request, 'core/family_tasks.html', context)
# 添加家庭事项
def add_family_task(request):
"""添加家庭事项"""
if request.method == 'POST':
form = FamilyTaskForm(request.POST)
if form.is_valid():
form.save()
logger.info(f"添加家庭事项: {form.cleaned_data['content'][:20]}...")
return redirect('family_tasks')
else:
form = FamilyTaskForm()
context = {'form': form}
return render(request, 'core/add_family_task.html', context)
# 编辑家庭事项
def edit_family_task(request, pk):
"""编辑家庭事项"""
task = get_object_or_404(FamilyTask, pk=pk)
if request.method == 'POST':
form = FamilyTaskForm(request.POST, instance=task)
if form.is_valid():
form.save()
logger.info(f"编辑家庭事项: {form.cleaned_data['content'][:20]}...")
return redirect('family_tasks')
else:
form = FamilyTaskForm(instance=task)
context = {'form': form, 'task': task}
return render(request, 'core/edit_family_task.html', context)
# 删除家庭事项
def delete_family_task(request, pk):
"""删除家庭事项"""
task = get_object_or_404(FamilyTask, pk=pk)
if request.method == 'POST':
task.delete()
logger.info(f"删除家庭事项: {task.content[:20]}...")
return redirect('family_tasks')
context = {'task': task}
return render(request, 'core/delete_family_task.html', context)
# 今日计划视图
def today_plan(request):
"""今日计划"""
logger.info("用户访问今日计划页面")
today = timezone.now().date()
# 获取今日计划
plans = TodayPlan.objects.filter(date=today)
context = {
'today': today,
'plans': plans,
}
return render(request, 'core/today_plan.html', context)
# 添加今日计划
def add_today_plan(request):
"""添加今日计划"""
if request.method == 'POST':
form = TodayPlanForm(request.POST)
if form.is_valid():
form.save()
logger.info(f"添加今日计划: {form.cleaned_data['content'][:20]}...")
return redirect('today_plan')
else:
form = TodayPlanForm()
context = {'form': form}
return render(request, 'core/add_today_plan.html', context)
# 编辑今日计划
def edit_today_plan(request, pk):
"""编辑今日计划"""
plan = get_object_or_404(TodayPlan, pk=pk)
if request.method == 'POST':
form = TodayPlanForm(request.POST, instance=plan)
if form.is_valid():
form.save()
logger.info(f"编辑今日计划: {form.cleaned_data['content'][:20]}...")
return redirect('today_plan')
else:
form = TodayPlanForm(instance=plan)
context = {'form': form, 'plan': plan}
return render(request, 'core/edit_today_plan.html', context)
# 删除今日计划
def delete_today_plan(request, pk):
"""删除今日计划"""
plan = get_object_or_404(TodayPlan, pk=pk)
if request.method == 'POST':
plan.delete()
logger.info(f"删除今日计划: {plan.content[:20]}...")
return redirect('today_plan')
context = {'plan': plan}
return render(request, 'core/delete_today_plan.html', context)
# 切换今日计划状态
def toggle_today_plan(request, pk):
"""切换今日计划状态"""
plan = get_object_or_404(TodayPlan, pk=pk)
plan.status = 'completed' if plan.status == 'pending' else 'pending'
plan.save()
logger.info(f"切换今日计划状态: {plan.content[:20]}... -> {plan.get_status_display()}")
if request.headers.get('x-requested-with') == 'XMLHttpRequest':
return JsonResponse({'status': plan.status, 'status_text': plan.get_status_display()})
return redirect('today_plan')
# 生成报告
def generate_report(request):
"""生成报告"""
logger.info("用户访问报告生成页面")
today = timezone.now().date()
yesterday = today - timedelta(days=1)
# 获取昨日记录
yesterday_reading = ReadingRecord.objects.filter(date=yesterday)
yesterday_insight = InsightRecord.objects.filter(date=yesterday)
# 获取今日计划
today_plan = TodayPlan.objects.filter(date=today)
# 获取家庭事项统计
family_task_stats = FamilyTask.objects.values('type').annotate(count=Count('id'))
context = {
'today': today,
'yesterday': yesterday,
'yesterday_reading': yesterday_reading,
'yesterday_insight': yesterday_insight,
'today_plan': today_plan,
'family_task_stats': family_task_stats,
}
return render(request, 'core/report.html', context)
# 查看报告
def view_report(request, date):
"""查看指定日期的报告"""
logger.info(f"用户查看报告: {date}")
report_date = datetime.strptime(date, '%Y-%m-%d').date()
yesterday = report_date - timedelta(days=1)
# 获取指定日期的记录
yesterday_reading = ReadingRecord.objects.filter(date=yesterday)
yesterday_insight = InsightRecord.objects.filter(date=yesterday)
today_plan = TodayPlan.objects.filter(date=report_date)
# 获取家庭事项统计
family_task_stats = FamilyTask.objects.values('type').annotate(count=Count('id'))
context = {
'today': report_date,
'yesterday': yesterday,
'yesterday_reading': yesterday_reading,
'yesterday_insight': yesterday_insight,
'today_plan': today_plan,
'family_task_stats': family_task_stats,
}
return render(request, 'core/report.html', context)
# 生成PDF报告
def generate_pdf_report(request, date):
"""生成PDF报告"""
if not is_weasyprint_available():
logger.error("WeasyPrint库不可用无法生成PDF报告")
return HttpResponse("PDF功能不可用请检查WeasyPrint库是否正确安装", status=500)
logger.info(f"用户生成PDF报告: {date}")
report_date = datetime.strptime(date, '%Y-%m-%d').date()
yesterday = report_date - timedelta(days=1)
# 获取指定日期的记录
yesterday_reading = ReadingRecord.objects.filter(date=yesterday)
yesterday_insight = InsightRecord.objects.filter(date=yesterday)
today_plan = TodayPlan.objects.filter(date=report_date)
# 获取家庭事项统计
family_task_stats = FamilyTask.objects.values('type').annotate(count=Count('id'))
context = {
'today': report_date,
'yesterday': yesterday,
'yesterday_reading': yesterday_reading,
'yesterday_insight': yesterday_insight,
'today_plan': today_plan,
'family_task_stats': family_task_stats,
}
# 渲染HTML模板
html_string = render(request, 'core/report_pdf.html', context).content.decode('utf-8')
# 生成PDF
pdf_file = f"report_{date}.pdf"
pdf_path = os.path.join(settings.REPORTS_ROOT, pdf_file)
# 确保报告目录存在
os.makedirs(settings.REPORTS_ROOT, exist_ok=True)
# 动态导入WeasyPrint
from weasyprint import HTML
HTML(string=html_string).write_pdf(pdf_path)
logger.info(f"PDF报告生成成功: {pdf_path}")
# 返回PDF文件
with open(pdf_path, 'rb') as f:
response = HttpResponse(f.read(), content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="{pdf_file}"'
return response
# 系统配置
def system_settings(request):
"""系统配置"""
logger.info("用户访问系统配置页面")
config = SystemConfig.get_config()
if request.method == 'POST':
form = SystemConfigForm(request.POST, instance=config)
if form.is_valid():
form.save()
logger.info("系统配置更新成功")
return redirect('system_settings')
else:
form = SystemConfigForm(instance=config)
context = {'form': form}
return render(request, 'core/system_settings.html', context)
# 发送邮件
def send_email_view(request):
"""手动发送邮件"""
if not is_weasyprint_available():
logger.error("WeasyPrint库不可用无法生成PDF报告")
return HttpResponse("PDF功能不可用无法发送邮件", status=500)
logger.info("用户手动发送邮件")
today = timezone.now().date()
today_str = today.strftime('%Y-%m-%d')
# 获取系统配置
config = SystemConfig.get_config()
if not all([config.smtp_server, config.smtp_username, config.smtp_password, config.recipient_email]):
logger.error("邮件配置不完整")
return redirect('system_settings')
# 生成PDF报告
report_date = today
yesterday = report_date - timedelta(days=1)
yesterday_reading = ReadingRecord.objects.filter(date=yesterday)
yesterday_insight = InsightRecord.objects.filter(date=yesterday)
today_plan = TodayPlan.objects.filter(date=report_date)
family_task_stats = FamilyTask.objects.values('type').annotate(count=Count('id'))
context = {
'today': report_date,
'yesterday': yesterday,
'yesterday_reading': yesterday_reading,
'yesterday_insight': yesterday_insight,
'today_plan': today_plan,
'family_task_stats': family_task_stats,
}
# 渲染HTML模板
html_string = render(request, 'core/report_pdf.html', context).content.decode('utf-8')
# 生成PDF
pdf_file = f"report_{today_str}.pdf"
pdf_path = os.path.join(settings.REPORTS_ROOT, pdf_file)
os.makedirs(settings.REPORTS_ROOT, exist_ok=True)
# 动态导入WeasyPrint
from weasyprint import HTML
HTML(string=html_string).write_pdf(pdf_path)
# 发送邮件
subject = f"家庭日报 - {today_str}"
message = f"这是您的家庭日报,日期:{today_str}"
from_email = config.smtp_username
recipient_list = [config.recipient_email]
email = EmailMessage(
subject=subject,
body=message,
from_email=from_email,
to=recipient_list,
)
# 添加附件
with open(pdf_path, 'rb') as f:
email.attach(pdf_file, f.read(), 'application/pdf')
# 发送邮件
try:
email.send()
logger.info(f"邮件发送成功,收件人:{config.recipient_email}")
return redirect('index')
except Exception as e:
logger.error(f"邮件发送失败:{str(e)}")
return redirect('system_settings')
# 别名保持URL配置一致
def send_email(request):
"""发送邮件别名"""
return send_email_view(request)

BIN
db.sqlite3 Normal file

Binary file not shown.

4
diary_family/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
# 确保Celery应用在Django启动时被加载
from .celery import app as celery_app
__all__ = ('celery_app',)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
diary_family/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for diary_family project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'diary_family.settings')
application = get_asgi_application()

20
diary_family/celery.py Normal file
View File

@@ -0,0 +1,20 @@
import os
from celery import Celery
from django.conf import settings
# 设置默认的Django设置模块
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'diary_family.settings')
# 创建Celery应用实例
app = Celery('diary_family')
# 从Django设置中读取配置
app.config_from_object('django.conf:settings', namespace='CELERY')
# 自动发现任务
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
@app.task(bind=True)
def debug_task(self):
"""调试任务"""
print(f'Request: {self.request!r}')

171
diary_family/settings.py Normal file
View File

@@ -0,0 +1,171 @@
"""
Django settings for diary_family project.
Generated by 'django-admin startproject' using Django 5.1.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-x^5b(t-qcpahyz+l^@3)lg_1d5@ks@jk*bqi042i7sle#vtmt('
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core',
'django_celery_beat',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'diary_family.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'diary_family.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Media files configuration
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Reports files configuration
REPORTS_URL = '/reports/'
REPORTS_ROOT = BASE_DIR / 'reports'
# Celery configuration
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
CELERY_TIMEZONE = 'Asia/Shanghai'
CELERY_ENABLE_UTC = True
# Celery Beat configuration
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
# Logging configuration
import os
from loguru import logger
LOG_DIR = BASE_DIR / 'logs'
LOG_DIR.mkdir(exist_ok=True)
# Configure loguru
logger.add(
LOG_DIR / 'app.log',
rotation='1 day',
retention='7 days',
compression='zip',
level='INFO'
)
# Internationalization
LANGUAGE_CODE = 'zh-Hans'
TIME_ZONE = 'Asia/Shanghai'
# Allow all hosts for development
ALLOWED_HOSTS = ['*']
# Template directories
TEMPLATES[0]['DIRS'] = [BASE_DIR / 'templates']
# Static files configuration
STATICFILES_DIRS = [BASE_DIR / 'static']

30
diary_family/urls.py Normal file
View File

@@ -0,0 +1,30 @@
"""
URL configuration for diary_family project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')),
]
# Static files for development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.REPORTS_URL, document_root=settings.REPORTS_ROOT)

16
diary_family/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for diary_family project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'diary_family.settings')
application = get_wsgi_application()

5
logs/app.log Normal file
View File

@@ -0,0 +1,5 @@
2026-01-04 19:09:19.202 | INFO | core.views:index:45 - 用户访问首页
2026-01-04 19:09:30.615 | INFO | core.views:generate_report:307 - 用户访问报告生成页面
2026-01-04 19:09:41.595 | INFO | core.views:system_settings:411 - 用户访问系统配置页面
2026-01-04 19:10:41.211 | INFO | core.views:index:45 - 用户访问首页
2026-01-04 19:11:02.642 | INFO | core.views:yesterday_records:73 - 用户访问昨日记录页面

22
manage.py Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'diary_family.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()