feat(历史记录): 添加历史记录查询功能及PDF导出
新增历史记录查询页面,支持按时间范围筛选阅读记录、感悟记录、汇总记录、家庭事项和今日计划 添加历史记录PDF导出功能,生成包含所有记录的格式化PDF报告
This commit is contained in:
150
core/history_views.py
Normal file
150
core/history_views.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.conf import settings
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from loguru import logger
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
ReadingRecord,
|
||||||
|
InsightRecord,
|
||||||
|
Summary,
|
||||||
|
FamilyTask,
|
||||||
|
TodayPlan,
|
||||||
|
)
|
||||||
|
from .views import is_weasyprint_available
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def history_records(request):
|
||||||
|
"""历史记录查询页面"""
|
||||||
|
logger.info("用户访问历史记录查询页面")
|
||||||
|
|
||||||
|
start_date_str = request.GET.get('start_date')
|
||||||
|
end_date_str = request.GET.get('end_date')
|
||||||
|
|
||||||
|
if not start_date_str or not end_date_str:
|
||||||
|
today = timezone.now().date()
|
||||||
|
start_date = datetime(today.year, 1, 1).date()
|
||||||
|
end_date = today
|
||||||
|
else:
|
||||||
|
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
|
||||||
|
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
|
||||||
|
|
||||||
|
# 查询各个类型的记录
|
||||||
|
reading_records = ReadingRecord.objects.filter(
|
||||||
|
date__range=[start_date, end_date]
|
||||||
|
).order_by('-date')
|
||||||
|
|
||||||
|
insight_records = InsightRecord.objects.filter(
|
||||||
|
date__range=[start_date, end_date]
|
||||||
|
).order_by('-date')
|
||||||
|
|
||||||
|
summary_records = Summary.objects.filter(
|
||||||
|
date__range=[start_date, end_date]
|
||||||
|
).order_by('-date')
|
||||||
|
|
||||||
|
family_tasks = FamilyTask.objects.filter(
|
||||||
|
created_at__date__range=[start_date, end_date]
|
||||||
|
).order_by('-created_at')
|
||||||
|
|
||||||
|
today_plans = TodayPlan.objects.filter(
|
||||||
|
date__range=[start_date, end_date]
|
||||||
|
).order_by('-date')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'start_date': start_date,
|
||||||
|
'end_date': end_date,
|
||||||
|
'reading_records': reading_records,
|
||||||
|
'insight_records': insight_records,
|
||||||
|
'summary_records': summary_records,
|
||||||
|
'family_tasks': family_tasks,
|
||||||
|
'today_plans': today_plans,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'core/history_records.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def history_pdf(request):
|
||||||
|
"""导出历史记录PDF"""
|
||||||
|
if not is_weasyprint_available():
|
||||||
|
logger.error("WeasyPrint库不可用,无法生成PDF报告")
|
||||||
|
return HttpResponse("PDF功能不可用,请检查WeasyPrint库是否正确安装", status=500)
|
||||||
|
|
||||||
|
start_date_str = request.GET.get('start_date')
|
||||||
|
end_date_str = request.GET.get('end_date')
|
||||||
|
|
||||||
|
if not start_date_str or not end_date_str:
|
||||||
|
return HttpResponse("请提供开始时间和结束时间", status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
|
||||||
|
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponse("日期格式错误", status=400)
|
||||||
|
|
||||||
|
if start_date > end_date:
|
||||||
|
return HttpResponse("开始时间不能晚于结束时间", status=400)
|
||||||
|
|
||||||
|
logger.info(f"用户导出历史记录PDF: {start_date} 至 {end_date}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 查询各个类型的记录
|
||||||
|
reading_records = ReadingRecord.objects.filter(
|
||||||
|
date__range=[start_date, end_date]
|
||||||
|
).order_by('-date')
|
||||||
|
|
||||||
|
insight_records = InsightRecord.objects.filter(
|
||||||
|
date__range=[start_date, end_date]
|
||||||
|
).order_by('-date')
|
||||||
|
|
||||||
|
summary_records = Summary.objects.filter(
|
||||||
|
date__range=[start_date, end_date]
|
||||||
|
).order_by('-date')
|
||||||
|
|
||||||
|
family_tasks = FamilyTask.objects.filter(
|
||||||
|
created_at__date__range=[start_date, end_date]
|
||||||
|
).order_by('-created_at')
|
||||||
|
|
||||||
|
today_plans = TodayPlan.objects.filter(
|
||||||
|
date__range=[start_date, end_date]
|
||||||
|
).order_by('-date')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'start_date': start_date,
|
||||||
|
'end_date': end_date,
|
||||||
|
'reading_records': reading_records,
|
||||||
|
'insight_records': insight_records,
|
||||||
|
'summary_records': summary_records,
|
||||||
|
'family_tasks': family_tasks,
|
||||||
|
'today_plans': today_plans,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 渲染HTML模板
|
||||||
|
html_string = render(request, 'core/history_pdf.html', context).content.decode('utf-8')
|
||||||
|
|
||||||
|
# 生成PDF
|
||||||
|
pdf_file = f"history_{start_date_str}_to_{end_date_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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"生成历史记录PDF失败: {str(e)}")
|
||||||
|
return HttpResponse(f"生成PDF失败: {str(e)}", status=500)
|
||||||
344
core/templates/core/history_pdf.html
Normal file
344
core/templates/core/history_pdf.html
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>历史记录 - {{ start_date|date:"Y-m-d" }} 至 {{ end_date|date:"Y-m-d" }}</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 1.5cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Microsoft YaHei", "SimHei", sans-serif;
|
||||||
|
font-size: 12pt;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 3px solid #2c3e50;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 24pt;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .date-range {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title.reading { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
||||||
|
.section-title.insight { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
|
||||||
|
.section-title.summary { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
|
||||||
|
.section-title.task { background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); }
|
||||||
|
.section-title.plan { background: linear-gradient(135deg, #8e9eab 0%, #eef2f3 100%); color: #333; }
|
||||||
|
|
||||||
|
.record-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-radius: 0 5px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-date {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-meta {
|
||||||
|
color: #666;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-content {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-content p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 9pt;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-primary { background: #667eea; color: white; }
|
||||||
|
.badge-secondary { background: #6c757d; color: white; }
|
||||||
|
.badge-success { background: #28a745; color: white; }
|
||||||
|
.badge-warning { background: #ffc107; color: #333; }
|
||||||
|
.badge-info { background: #17a2b8; color: white; }
|
||||||
|
.badge-danger { background: #dc3545; color: white; }
|
||||||
|
|
||||||
|
.empty-notice {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 8px;
|
||||||
|
width: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
background: #667eea;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>历史记录汇总</h1>
|
||||||
|
<div class="date-range">
|
||||||
|
{{ start_date|date:"Y年m月d日" }} 至 {{ end_date|date:"Y年m月d日" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 阅读记录 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title reading">阅读记录 ({{ reading_records|length }} 条)</div>
|
||||||
|
{% if reading_records %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>日期</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>标题</th>
|
||||||
|
<th>来源</th>
|
||||||
|
<th>进度</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for record in reading_records %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ record.date|date:"Y-m-d" }}</td>
|
||||||
|
<td><span class="badge badge-info">{{ record.type.name }}</span></td>
|
||||||
|
<td>{{ record.title }}</td>
|
||||||
|
<td>{{ record.source|default:"-" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if record.progress %}
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: {{ record.progress }}%"></div>
|
||||||
|
</div>
|
||||||
|
<small>{{ record.progress }}%</small>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-notice">该时间段没有阅读记录</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 感悟记录 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title insight">感悟记录 ({{ insight_records|length }} 条)</div>
|
||||||
|
{% if insight_records %}
|
||||||
|
{% for record in insight_records %}
|
||||||
|
<div class="record-item">
|
||||||
|
<div class="record-header">
|
||||||
|
<span class="record-date">{{ record.date|date:"Y-m-d" }}</span>
|
||||||
|
<span class="record-meta">发言人: {{ record.speaker.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="record-content">
|
||||||
|
{{ record.content|linebreaks }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-notice">该时间段没有感悟记录</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 汇总记录 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title summary">汇总记录 ({{ summary_records|length }} 条)</div>
|
||||||
|
{% if summary_records %}
|
||||||
|
{% for record in summary_records %}
|
||||||
|
<div class="record-item">
|
||||||
|
<div class="record-header">
|
||||||
|
<span class="record-date">{{ record.date|date:"Y-m-d" }}</span>
|
||||||
|
<span class="record-meta">
|
||||||
|
<span class="badge badge-secondary">{{ record.category.name }}</span>
|
||||||
|
发言人: {{ record.speaker.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="record-content">
|
||||||
|
{{ record.content|linebreaks }}
|
||||||
|
{% if record.source %}
|
||||||
|
<p><small>来源: {{ record.source }}</small></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-notice">该时间段没有汇总记录</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 家庭事项 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title task">家庭事项 ({{ family_tasks|length }} 条)</div>
|
||||||
|
{% if family_tasks %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>内容</th>
|
||||||
|
<th>优先级</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>截止日期</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for task in family_tasks %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ task.created_at|date:"Y-m-d H:i" }}</td>
|
||||||
|
<td><span class="badge badge-secondary">{{ task.type.name }}</span></td>
|
||||||
|
<td>{{ task.content|truncatechars:50 }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if task.priority.name == '高' %}badge-danger{% elif task.priority.name == '中' %}badge-warning{% else %}badge-info{% endif %}">
|
||||||
|
{{ task.priority.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if task.status.name == 'completed' %}badge-success{% else %}badge-warning{% endif %}">
|
||||||
|
{{ task.status.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ task.deadline|date:"Y-m-d"|default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-notice">该时间段没有家庭事项</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 今日计划 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title plan">今日计划 ({{ today_plans|length }} 条)</div>
|
||||||
|
{% if today_plans %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>日期</th>
|
||||||
|
<th>发言人</th>
|
||||||
|
<th>内容</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>优先级</th>
|
||||||
|
<th>状态</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for plan in today_plans %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ plan.date|date:"Y-m-d" }}</td>
|
||||||
|
<td>{{ plan.speaker.name }}</td>
|
||||||
|
<td>{{ plan.content|truncatechars:50 }}</td>
|
||||||
|
<td><span class="badge badge-info">{{ plan.type.name }}</span></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if plan.priority.name == '高' %}badge-danger{% elif plan.priority.name == '中' %}badge-warning{% else %}badge-info{% endif %}">
|
||||||
|
{{ plan.priority.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if plan.status.name == 'completed' %}badge-success{% else %}badge-warning{% endif %}">
|
||||||
|
{{ plan.status.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-notice">该时间段没有今日计划</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>家庭日报系统 - 历史记录导出</p>
|
||||||
|
<p>生成时间: {% now "Y-m-d H:i:s" %}</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
293
core/templates/core/history_records.html
Normal file
293
core/templates/core/history_records.html
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
{% extends 'core/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card" style="background: linear-gradient(135deg, #2c3e50 0%, #4ca1af 100%); color: white;">
|
||||||
|
<div class="card-body d-flex align-items-center justify-content-between py-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-1"><i class="bi bi-clock-history me-2"></i>历史记录查询</h3>
|
||||||
|
<p class="mb-0 opacity-75">
|
||||||
|
<i class="bi bi-calendar-range me-2"></i>
|
||||||
|
{{ start_date|date:"Y年m月d日" }} 至 {{ end_date|date:"Y年m月d日" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-end d-none d-md-block">
|
||||||
|
<i class="bi bi-archive" style="font-size: 4rem; opacity: 0.3;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 阅读记录 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0"><i class="bi bi-book me-2"></i>阅读记录</h5>
|
||||||
|
<span class="badge bg-light text-primary">{{ reading_records|length }} 条</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if reading_records %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>日期</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>标题</th>
|
||||||
|
<th>来源</th>
|
||||||
|
<th>进度</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for record in reading_records %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ record.date|date:"Y-m-d" }}</td>
|
||||||
|
<td><span class="badge bg-info">{{ record.type.name }}</span></td>
|
||||||
|
<td>{{ record.title }}</td>
|
||||||
|
<td>{{ record.source|default:"-" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if record.progress %}
|
||||||
|
<div class="progress" style="height: 6px; width: 100px;">
|
||||||
|
<div class="progress-bar bg-primary" role="progressbar" style="width: {{ record.progress }}%"></div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ record.progress }}%</small>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-2">该时间段没有阅读记录</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 感悟记录 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0"><i class="bi bi-lightbulb me-2"></i>感悟记录</h5>
|
||||||
|
<span class="badge bg-light text-success">{{ insight_records|length }} 条</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if insight_records %}
|
||||||
|
<div class="list-group">
|
||||||
|
{% for record in insight_records %}
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-success me-2">{{ record.date|date:"Y-m-d" }}</span>
|
||||||
|
<span class="text-muted"><i class="bi bi-person me-1"></i>{{ record.speaker.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mb-2">{{ record.content|linebreaks }}</p>
|
||||||
|
{% if record.file %}
|
||||||
|
<a href="{{ record.file.url }}" class="file-link small" target="_blank">
|
||||||
|
<i class="bi bi-paperclip me-1"></i>查看附件
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-2">该时间段没有感悟记录</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 汇总记录 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0"><i class="bi bi-collection me-2"></i>汇总记录</h5>
|
||||||
|
<span class="badge bg-light text-info">{{ summary_records|length }} 条</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if summary_records %}
|
||||||
|
<div class="list-group">
|
||||||
|
{% for record in summary_records %}
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-primary me-2">{{ record.date|date:"Y-m-d" }}</span>
|
||||||
|
<span class="badge bg-secondary me-2">{{ record.category.name }}</span>
|
||||||
|
<span class="text-muted"><i class="bi bi-person me-1"></i>{{ record.speaker.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mb-2">{{ record.content|linebreaks }}</p>
|
||||||
|
{% if record.source %}
|
||||||
|
<div class="small text-muted mb-2">
|
||||||
|
<i class="bi bi-link-45deg me-1"></i>{{ record.source }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if record.file %}
|
||||||
|
<a href="{{ record.file.url }}" class="file-link small" target="_blank">
|
||||||
|
<i class="bi bi-paperclip me-1"></i>查看附件
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-2">该时间段没有汇总记录</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 家庭事项 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-warning text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0"><i class="bi bi-list-check me-2"></i>家庭事项</h5>
|
||||||
|
<span class="badge bg-light text-warning">{{ family_tasks|length }} 条</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if family_tasks %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>内容</th>
|
||||||
|
<th>优先级</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>截止日期</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for task in family_tasks %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ task.created_at|date:"Y-m-d H:i" }}</td>
|
||||||
|
<td><span class="badge bg-secondary">{{ task.type.name }}</span></td>
|
||||||
|
<td>{{ task.content|truncatechars:50 }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if task.priority.name == '高' %}bg-danger{% elif task.priority.name == '中' %}bg-warning{% else %}bg-info{% endif %}">
|
||||||
|
{{ task.priority.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if task.status.name == 'completed' %}bg-success{% else %}bg-warning{% endif %}">
|
||||||
|
{{ task.status.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if task.deadline %}
|
||||||
|
{% if task.is_overdue %}
|
||||||
|
<span class="text-danger">{{ task.deadline|date:"Y-m-d" }} (已逾期)</span>
|
||||||
|
{% else %}
|
||||||
|
{{ task.deadline|date:"Y-m-d" }}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-2">该时间段没有家庭事项</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 今日计划 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0"><i class="bi bi-calendar-check me-2"></i>今日计划</h5>
|
||||||
|
<span class="badge bg-light text-secondary">{{ today_plans|length }} 条</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if today_plans %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>日期</th>
|
||||||
|
<th>发言人</th>
|
||||||
|
<th>内容</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>优先级</th>
|
||||||
|
<th>状态</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for plan in today_plans %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ plan.date|date:"Y-m-d" }}</td>
|
||||||
|
<td>{{ plan.speaker.name }}</td>
|
||||||
|
<td>{{ plan.content|truncatechars:50 }}</td>
|
||||||
|
<td><span class="badge bg-info">{{ plan.type.name }}</span></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if plan.priority.name == '高' %}bg-danger{% elif plan.priority.name == '中' %}bg-warning{% else %}bg-info{% endif %}">
|
||||||
|
{{ plan.priority.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if plan.status.name == 'completed' %}bg-success{% else %}bg-warning{% endif %}">
|
||||||
|
{{ plan.status.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-2">该时间段没有今日计划</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<a href="{% url 'index' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i>返回首页
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'history_pdf' %}?start_date={{ start_date|date:'Y-m-d' }}&end_date={{ end_date|date:'Y-m-d' }}" class="btn btn-success">
|
||||||
|
<i class="bi bi-file-earmark-pdf me-1"></i>导出PDF
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -279,4 +279,97 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 历史记录查询 -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0"><i class="bi bi-clock-history me-2"></i>历史记录查询</h5>
|
||||||
|
<span class="badge bg-light text-dark">导出PDF</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="historyForm" method="GET" action="{% url 'history_records' %}">
|
||||||
|
<div class="row align-items-end">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label"><i class="bi bi-calendar-minus me-1"></i>开始时间</label>
|
||||||
|
<input type="date" class="form-control" id="start_date" name="start_date" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label"><i class="bi bi-calendar-plus me-1"></i>结束时间</label>
|
||||||
|
<input type="date" class="form-control" id="end_date" name="end_date" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setCurrentQuarter()">
|
||||||
|
<i class="bi bi-calendar3 me-1"></i>当季
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setCurrentYear()">
|
||||||
|
<i class="bi bi-calendar-year me-1"></i>本年度
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-search me-1"></i>查询记录
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-success" onclick="exportHistoryPDF()">
|
||||||
|
<i class="bi bi-file-earmark-pdf me-1"></i>导出PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 设置当季时间范围(最近3个月)
|
||||||
|
function setCurrentQuarter() {
|
||||||
|
const today = new Date();
|
||||||
|
const threeMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 2, 1);
|
||||||
|
|
||||||
|
document.getElementById('start_date').value = formatDate(threeMonthsAgo);
|
||||||
|
document.getElementById('end_date').value = formatDate(today);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置本年度时间范围
|
||||||
|
function setCurrentYear() {
|
||||||
|
const today = new Date();
|
||||||
|
const yearStart = new Date(today.getFullYear(), 0, 1);
|
||||||
|
|
||||||
|
document.getElementById('start_date').value = formatDate(yearStart);
|
||||||
|
document.getElementById('end_date').value = formatDate(today);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期为 YYYY-MM-DD
|
||||||
|
function formatDate(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出历史记录PDF
|
||||||
|
function exportHistoryPDF() {
|
||||||
|
const startDate = document.getElementById('start_date').value;
|
||||||
|
const endDate = document.getElementById('end_date').value;
|
||||||
|
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
alert('请先选择开始时间和结束时间');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new Date(startDate) > new Date(endDate)) {
|
||||||
|
alert('开始时间不能晚于结束时间');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = `{% url 'history_pdf' %}?start_date=${startDate}&end_date=${endDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时默认设置为本年度
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
setCurrentYear();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from . import views
|
from . import views
|
||||||
|
from . import history_views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# 登录和注销
|
# 登录和注销
|
||||||
@@ -63,4 +64,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
# PDF文件列表
|
# PDF文件列表
|
||||||
path('pdf-list/', views.pdf_list, name='pdf_list'),
|
path('pdf-list/', views.pdf_list, name='pdf_list'),
|
||||||
|
|
||||||
|
# 历史记录查询
|
||||||
|
path('history/', history_views.history_records, name='history_records'),
|
||||||
|
path('history/pdf/', history_views.history_pdf, name='history_pdf'),
|
||||||
]
|
]
|
||||||
Reference in New Issue
Block a user