功能特性: - 前端文章展示 (Markdown渲染、代码高亮) - 后台管理系统 (文章、分类、标签、作者管理) - 图片上传 (点击、拖拽、粘贴) - 网站设置 (Logo、底部宣传图片上传) - 访问统计 (IP记录、趋势图表) - 参考来源、附件支持 - 单端口部署 (16012) 技术栈: - Flask 3.0 + SQLAlchemy - SQLite数据库 - marked.js + highlight.js (Markdown渲染) - Chart.js (统计图表)
172 lines
4.6 KiB
HTML
172 lines
4.6 KiB
HTML
{% extends 'admin/base.html' %}
|
|
|
|
{% block title %}访问日志{% endblock %}
|
|
{% block page_title %}访问日志{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="stats-tabs">
|
|
<a href="{{ url_for('admin.stats') }}" class="tab">概览</a>
|
|
<a href="{{ url_for('admin.stats_charts') }}" class="tab">趋势图表</a>
|
|
<a href="{{ url_for('admin.stats_logs') }}" class="tab active">访问日志</a>
|
|
</div>
|
|
|
|
<div class="toolbar">
|
|
<form method="GET" class="search-form">
|
|
<input type="text" name="path" value="{{ path_filter }}"
|
|
placeholder="筛选路径...">
|
|
<button type="submit" class="btn btn-secondary">筛选</button>
|
|
{% if path_filter %}
|
|
<a href="{{ url_for('admin.stats_logs') }}" class="btn btn-secondary">清除</a>
|
|
{% endif %}
|
|
</form>
|
|
|
|
<form action="{{ url_for('admin.stats_clear') }}" method="POST" class="clear-form">
|
|
<input type="number" name="days" value="90" min="30" max="365" style="width: 80px;">
|
|
<button type="submit" class="btn btn-danger"
|
|
onclick="return confirm('确定要清理旧日志吗?')">清理旧日志</button>
|
|
</form>
|
|
</div>
|
|
|
|
{% if logs.items %}
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th width="15%">IP地址</th>
|
|
<th width="25%">访问路径</th>
|
|
<th width="15%">User Agent</th>
|
|
<th width="15%">来源页面</th>
|
|
<th width="20%">访问时间</th>
|
|
<th width="10%">文章</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for log in logs.items %}
|
|
<tr>
|
|
<td>{{ log.ip_address }}</td>
|
|
<td>{{ log.path }}</td>
|
|
<td>
|
|
<span class="user-agent" title="{{ log.user_agent }}">
|
|
{{ (log.user_agent or '-')[:50] }}{% if log.user_agent and len(log.user_agent) > 50 %}...{% endif %}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
{% if log.referer %}
|
|
<a href="{{ log.referer }}" target="_blank" title="{{ log.referer }}">
|
|
{{ log.referer[:30] }}...
|
|
</a>
|
|
{% else %}
|
|
直接访问
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ log.visited_at|datetime_format('%Y-%m-%d %H:%M:%S') }}</td>
|
|
<td>
|
|
{% if log.article %}
|
|
<a href="{{ url_for('admin.article_edit', article_id=log.article.id) }}">
|
|
{{ log.article.title[:20] }}...
|
|
</a>
|
|
{% else %}
|
|
-
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- 分页 -->
|
|
{% if logs.pages > 1 %}
|
|
<div class="pagination">
|
|
{% if logs.has_prev %}
|
|
<a href="{{ url_for('admin.stats_logs', page=logs.prev_num, path=path_filter) }}"
|
|
class="page-link">上一页</a>
|
|
{% endif %}
|
|
|
|
{% for page in logs.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
|
{% if page %}
|
|
{% if page == logs.page %}
|
|
<span class="page-link active">{{ page }}</span>
|
|
{% else %}
|
|
<a href="{{ url_for('admin.stats_logs', page=page, path=path_filter) }}"
|
|
class="page-link">{{ page }}</a>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="page-link ellipsis">...</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if logs.has_next %}
|
|
<a href="{{ url_for('admin.stats_logs', page=logs.next_num, path=path_filter) }}"
|
|
class="page-link">下一页</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>暂无访问日志</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<style>
|
|
.stats-tabs {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.tab {
|
|
padding: 10px 20px;
|
|
background: var(--admin-card-bg);
|
|
border-radius: 5px;
|
|
color: var(--admin-text);
|
|
border: 1px solid var(--admin-border);
|
|
}
|
|
|
|
.tab:hover {
|
|
text-decoration: none;
|
|
background: var(--admin-primary);
|
|
color: #fff;
|
|
}
|
|
|
|
.tab.active {
|
|
background: var(--admin-primary);
|
|
color: #fff;
|
|
border-color: var(--admin-primary);
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.search-form {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.search-form input {
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--admin-border);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.clear-form {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.clear-form input {
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--admin-border);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.user-agent {
|
|
color: var(--admin-text-light);
|
|
font-size: 0.85rem;
|
|
}
|
|
</style>
|
|
{% endblock %} |