初始化技术博客项目
功能特性: - 前端文章展示 (Markdown渲染、代码高亮) - 后台管理系统 (文章、分类、标签、作者管理) - 图片上传 (点击、拖拽、粘贴) - 网站设置 (Logo、底部宣传图片上传) - 访问统计 (IP记录、趋势图表) - 参考来源、附件支持 - 单端口部署 (16012) 技术栈: - Flask 3.0 + SQLAlchemy - SQLite数据库 - marked.js + highlight.js (Markdown渲染) - Chart.js (统计图表)
This commit is contained in:
13
app/templates/frontend/404.html
Normal file
13
app/templates/frontend/404.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}404 - 页面未找到{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="error-page">
|
||||
<div class="container">
|
||||
<h1>404</h1>
|
||||
<p>页面未找到</p>
|
||||
<a href="{{ url_for('frontend.index') }}" class="btn">返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
13
app/templates/frontend/500.html
Normal file
13
app/templates/frontend/500.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}500 - 服务器错误{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="error-page">
|
||||
<div class="container">
|
||||
<h1>500</h1>
|
||||
<p>服务器内部错误</p>
|
||||
<a href="{{ url_for('frontend.index') }}" class="btn">返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
157
app/templates/frontend/article.html
Normal file
157
app/templates/frontend/article.html
Normal file
@@ -0,0 +1,157 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
|
||||
{% block title %}{{ article.title }} - {{ g.site_config.site_name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github.min.css">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/markdown.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/core.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/python.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/javascript.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/bash.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/sql.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/json.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/yaml.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/dockerfile.min.js"></script>
|
||||
<script>
|
||||
// 配置 marked
|
||||
marked.setOptions({
|
||||
highlight: function(code, lang) {
|
||||
if (lang && hlse.getLanguage(lang)) {
|
||||
try {
|
||||
return hlse.highlight(code, { language: lang }).value;
|
||||
} catch (e) {}
|
||||
}
|
||||
return hlse.highlightAuto(code).value;
|
||||
},
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
|
||||
// 渲染 Markdown
|
||||
const content = document.getElementById('markdown-content');
|
||||
if (content) {
|
||||
const markdownText = content.getAttribute('data-content');
|
||||
content.innerHTML = marked.parse(markdownText);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article class="article-detail">
|
||||
<header class="article-header">
|
||||
<h1 class="article-title">{{ article.title }}</h1>
|
||||
|
||||
<div class="article-meta">
|
||||
{% if article.author %}
|
||||
<span class="author">
|
||||
{% if article.author.avatar %}
|
||||
<img src="{{ article.author.avatar }}" alt="{{ article.author.name }}" class="author-avatar">
|
||||
{% endif %}
|
||||
{{ article.author.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if article.category %}
|
||||
<span class="category">
|
||||
<a href="{{ url_for('frontend.category', category_id=article.category.id) }}">
|
||||
{{ article.category.name }}
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="date">{{ article.published_at|datetime_format('%Y-%m-%d %H:%M') }}</span>
|
||||
<span class="views">{{ article.view_count }} 阅读</span>
|
||||
</div>
|
||||
|
||||
{% if article.tags %}
|
||||
<div class="article-tags">
|
||||
{% for tag in article.tags %}
|
||||
<a href="{{ url_for('frontend.tag', tag_id=tag.id) }}"
|
||||
class="tag" style="--tag-color: {{ tag.color }}">
|
||||
{{ tag.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if article.summary %}
|
||||
<div class="article-summary">
|
||||
<strong>摘要:</strong>{{ article.summary }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if article.cover_image %}
|
||||
<div class="article-cover">
|
||||
<img src="{{ article.cover_image }}" alt="{{ article.title }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="markdown-content" class="article-content markdown-body" data-content="{{ article.content | e }}">
|
||||
<!-- Markdown 将在这里渲染 -->
|
||||
</div>
|
||||
|
||||
{% if article.author and article.author.bio %}
|
||||
<div class="author-card">
|
||||
{% if article.author.avatar %}
|
||||
<img src="{{ article.author.avatar }}" alt="{{ article.author.name }}" class="author-avatar-lg">
|
||||
{% endif %}
|
||||
<div class="author-info">
|
||||
<h4>{{ article.author.name }}</h4>
|
||||
<p>{{ article.author.bio }}</p>
|
||||
{% if article.author.website %}
|
||||
<a href="{{ article.author.website }}" target="_blank">个人主页</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if article.references.all() %}
|
||||
<div class="references-section">
|
||||
<h3>📚 参考来源</h3>
|
||||
<ul>
|
||||
{% for ref in article.references %}
|
||||
<li>
|
||||
{% if ref.url %}
|
||||
<a href="{{ ref.url }}" target="_blank">{{ ref.title }}</a>
|
||||
{% else %}
|
||||
{{ ref.title }}
|
||||
{% endif %}
|
||||
{% if ref.description %}
|
||||
<span class="ref-desc">{{ ref.description }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if article.attachments.all() %}
|
||||
<div class="attachments-section">
|
||||
<h3>📎 相关附件</h3>
|
||||
<ul>
|
||||
{% for att in article.attachments %}
|
||||
<li>
|
||||
<a href="/uploads/{{ att.filename }}" target="blank">
|
||||
{{ att.filename }}
|
||||
</a>
|
||||
<span class="filesize">{{ att.get_filesize_display() }}</span>
|
||||
{% if att.description %}
|
||||
<span class="att-desc">{{ att.description }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<footer class="article-footer">
|
||||
<div class="article-nav">
|
||||
<a href="{{ url_for('frontend.index') }}">← 返回首页</a>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
{% endblock %}
|
||||
163
app/templates/frontend/base.html
Normal file
163
app/templates/frontend/base.html
Normal file
@@ -0,0 +1,163 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block body %}
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="{{ url_for('frontend.index') }}" class="logo">
|
||||
{% if g.site_config.site_logo %}
|
||||
<img src="{{ g.site_config.site_logo }}" alt="{{ g.site_config.site_name }}">
|
||||
{% else %}
|
||||
{{ g.site_config.site_name }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% if g.site_config.site_subtitle %}
|
||||
<span class="site-subtitle">{{ g.site_config.site_subtitle }}</span>
|
||||
{% endif %}
|
||||
<div class="nav-links">
|
||||
<a href="{{ url_for('frontend.index') }}">首页</a>
|
||||
{% for cat in categories %}
|
||||
<a href="{{ url_for('frontend.category', category_id=cat.id) }}"
|
||||
class="{% if current_category == cat.id %}active{% endif %}">{{ cat.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="container">
|
||||
<div class="content-wrapper">
|
||||
<div class="content-area">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<aside class="sidebar">
|
||||
<!-- 搜索 -->
|
||||
<div class="sidebar-section">
|
||||
<h3>搜索</h3>
|
||||
<form class="search-form">
|
||||
<input type="text" placeholder="搜索文章..." name="q">
|
||||
<button type="submit">🔍</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 最新文章 -->
|
||||
<div class="sidebar-section">
|
||||
<h3>最新文章</h3>
|
||||
<ul class="recent-list">
|
||||
{% for article in recent_articles %}
|
||||
<li>
|
||||
<a href="{{ url_for('frontend.article', slug=article.slug) }}">
|
||||
{{ article.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 热门文章 -->
|
||||
<div class="sidebar-section">
|
||||
<h3>热门文章</h3>
|
||||
<ul class="hot-list">
|
||||
{% for article in hot_articles %}
|
||||
<li>
|
||||
<a href="{{ url_for('frontend.article', slug=article.slug) }}">
|
||||
<span class="rank">{{ loop.index }}</span>
|
||||
{{ article.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 标签云 -->
|
||||
<div class="sidebar-section">
|
||||
<h3>标签云</h3>
|
||||
<div class="tag-cloud">
|
||||
{% for tag in tags %}
|
||||
<a href="{{ url_for('frontend.tag', tag_id=tag.id) }}"
|
||||
class="tag {% if current_tag == tag.id %}active{% endif %}"
|
||||
style="--tag-color: {{ tag.color }}">
|
||||
{{ tag.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-main">
|
||||
<p>{{ g.site_config.footer_text }}</p>
|
||||
{% if g.site_config.footer_copyright %}
|
||||
<p class="copyright">{{ g.site_config.footer_copyright }}</p>
|
||||
{% endif %}
|
||||
{% if g.site_config.footer_technology %}
|
||||
<p class="technology">{{ g.site_config.footer_technology }}</p>
|
||||
{% endif %}
|
||||
{% if g.site_config.contact_email or g.site_config.contact_github %}
|
||||
<div class="contact-links">
|
||||
{% if g.site_config.contact_email %}
|
||||
<a href="mailto:{{ g.site_config.contact_email }}">📧 {{ g.site_config.contact_email }}</a>
|
||||
{% endif %}
|
||||
{% if g.site_config.contact_github %}
|
||||
<a href="{{ g.site_config.contact_github }}" target="_blank">🔗 GitHub</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if g.site_config.promo_image_1 or g.site_config.promo_image_2 or g.site_config.promo_image_3 %}
|
||||
<div class="footer-promo">
|
||||
{% if g.site_config.promo_image_1 %}
|
||||
<div class="promo-item">
|
||||
{% if g.site_config.promo_image_1_link %}
|
||||
<a href="{{ g.site_config.promo_image_1_link }}" target="_blank" class="promo-link">
|
||||
<img src="{{ g.site_config.promo_image_1 }}" alt="">
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="promo-link">
|
||||
<img src="{{ g.site_config.promo_image_1 }}" alt="">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if g.site_config.promo_image_2 %}
|
||||
<div class="promo-item">
|
||||
{% if g.site_config.promo_image_2_link %}
|
||||
<a href="{{ g.site_config.promo_image_2_link }}" target="_blank" class="promo-link">
|
||||
<img src="{{ g.site_config.promo_image_2 }}" alt="">
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="promo-link">
|
||||
<img src="{{ g.site_config.promo_image_2 }}" alt="">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if g.site_config.promo_image_3 %}
|
||||
<div class="promo-item">
|
||||
<div class="promo-link">
|
||||
<img src="{{ g.site_config.promo_image_3 }}" alt="">
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
{% endblock %}
|
||||
97
app/templates/frontend/index.html
Normal file
97
app/templates/frontend/index.html
Normal file
@@ -0,0 +1,97 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
|
||||
{% block title %}
|
||||
{% if category_title %}{{ category_title }} - 技术博客
|
||||
{% elif tag_title %}{{ tag_title }} - 技术博客
|
||||
{% else %}首页 - 技术博客{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{% if category_title %}
|
||||
{{ category_title }}
|
||||
{% elif tag_title %}
|
||||
标签: {{ tag_title }}
|
||||
{% else %}
|
||||
技术文章
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{% if articles.items %}
|
||||
<div class="article-list">
|
||||
{% for article in articles.items %}
|
||||
<article class="article-card">
|
||||
{% if article.cover_image %}
|
||||
<div class="article-cover">
|
||||
<img src="{{ article.cover_image }}" alt="{{ article.title }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="article-body">
|
||||
<h2 class="article-title">
|
||||
<a href="{{ url_for('frontend.article', slug=article.slug) }}">
|
||||
{{ article.title }}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<div class="article-meta">
|
||||
{% if article.category %}
|
||||
<span class="category">{{ article.category.name }}</span>
|
||||
{% endif %}
|
||||
<span class="date">{{ article.published_at|datetime_format('%Y-%m-%d') }}</span>
|
||||
<span class="views">{{ article.view_count }} 阅读</span>
|
||||
</div>
|
||||
|
||||
<p class="article-summary">{{ article.summary|truncate_content(200) }}</p>
|
||||
|
||||
{% if article.tags %}
|
||||
<div class="article-tags">
|
||||
{% for tag in article.tags %}
|
||||
<a href="{{ url_for('frontend.tag', tag_id=tag.id) }}"
|
||||
class="tag" style="--tag-color: {{ tag.color }}">
|
||||
{{ tag.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
{% if articles.pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if articles.has_prev %}
|
||||
<a href="{{ url_for('frontend.index', page=articles.prev_num, category=current_category, tag=current_tag) }}"
|
||||
class="page-link">上一页</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page in articles.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if page %}
|
||||
{% if page == articles.page %}
|
||||
<span class="page-link active">{{ page }}</span>
|
||||
{% else %}
|
||||
<a href="{{ url_for('frontend.index', page=page, category=current_category, tag=current_tag) }}"
|
||||
class="page-link">{{ page }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="page-link ellipsis">...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if articles.has_next %}
|
||||
<a href="{{ url_for('frontend.index', page=articles.next_num, category=current_category, tag=current_tag) }}"
|
||||
class="page-link">下一页</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<p>暂无文章</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user