第九章:实战案例
9.1 案例一:任务管理系统
一个完整的 CRUD 应用,展示 HTMX 的核心功能。
目录结构
task-manager/
├── app.py # Flask 后端
├── templates/
│ ├── base.html
│ ├── index.html
│ └── partials/
│ ├── _task_item.html
│ ├── _task_list.html
│ └── _task_form.html
└── static/
└── style.css
后端代码
# app.py
from flask import Flask, render_template, request, redirect, jsonify
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db'
db = SQLAlchemy(app)
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
status = db.Column(db.String(20), default='pending') # pending, active, done
priority = db.Column(db.String(10), default='medium') # low, medium, high
created_at = db.Column(db.DateTime, default=db.func.now())
with app.app_context():
db.create_all()
@app.route('/')
def index():
return render_template('index.html')
@app.route('/tasks')
def task_list():
status = request.args.get('status', 'all')
priority = request.args.get('priority', 'all')
query = Task.query
if status != 'all':
query = query.filter_by(status=status)
if priority != 'all':
query = query.filter_by(priority=priority)
tasks = query.order_by(Task.created_at.desc()).all()
return render_template('partials/_task_list.html', tasks=tasks)
@app.route('/tasks', methods=['POST'])
def task_create():
task = Task(
title=request.form['title'],
description=request.form.get('description', ''),
priority=request.form.get('priority', 'medium')
)
db.session.add(task)
db.session.commit()
return render_template('partials/_task_item.html', task=task)
@app.route('/tasks/<int:id>/edit', methods=['GET'])
def task_edit_form(id):
task = Task.query.get_or_404(id)
return render_template('partials/_task_form.html', task=task)
@app.route('/tasks/<int:id>', methods=['PUT'])
def task_update(id):
task = Task.query.get_or_404(id)
task.title = request.form['title']
task.description = request.form.get('description', '')
task.priority = request.form.get('priority', 'medium')
db.session.commit()
return render_template('partials/_task_item.html', task=task)
@app.route('/tasks/<int:id>/status', methods=['POST'])
def task_status_update(id):
task = Task.query.get_or_404(id)
task.status = request.form['status']
db.session.commit()
return render_template('partials/_task_item.html', task=task)
@app.route('/tasks/<int:id>', methods=['DELETE'])
def task_delete(id):
task = Task.query.get_or_404(id)
db.session.delete(task)
db.session.commit()
return ''
@app.route('/tasks/stats')
def task_stats():
stats = {
'total': Task.query.count(),
'pending': Task.query.filter_by(status='pending').count(),
'active': Task.query.filter_by(status='active').count(),
'done': Task.query.filter_by(status='done').count()
}
return render_template('partials/_stats.html', stats=stats)
if __name__ == '__main__':
app.run(debug=True)
前端模板
<!-- templates/index.html -->
{% extends "base.html" %}
{% block content %}
<div class="container">
<h1>📋 任务管理系统</h1>
<!-- 统计面板 -->
<div id="stats-panel" hx-get="/tasks/stats" hx-trigger="load, taskChanged from:body">
加载统计...
</div>
<!-- 筛选器 -->
<div class="filters">
<select name="status"
hx-get="/tasks"
hx-target="#task-list"
hx-trigger="change"
003e
<option value="all">全部状态</option>
<option value="pending">待办</option>
<option value="active">进行中</option>
<option value="done">已完成</option>
</select>
<select name="priority"
hx-get="/tasks"
hx-target="#task-list"
hx-trigger="change"
hx-include="[name='status']"
003e
<option value="all">全部优先级</option>
<option value="high">高</option>
<option value="medium">中</option>
<option value="low">低</option>
</select>
</div>
<!-- 添加表单 -->
<form class="add-form"
hx-post="/tasks"
hx-target="#task-list"
hx-swap="afterbegin"
hx-on::after-request="this.reset(); document.getElementById('task-list').dispatchEvent(new Event('taskChanged'))"
003e
<input type="text" name="title" placeholder="任务标题" required>
<input type="text" name="description" placeholder="描述">
<select name="priority">
<option value="low">低优先级</option>
<option value="medium" selected>中优先级</option>
<option value="high">高优先级</option>
</select>
<button type="submit">➕ 添加任务</button>
</form>
<!-- 任务列表 -->
<div id="task-list" hx-get="/tasks" hx-trigger="load">
加载中...
</div>
</div>
{% endblock %}
<!-- templates/partials/_task_list.html -->
{% if tasks %}
<div class="task-list">
{% for task in tasks %}
{% include 'partials/_task_item.html' %}
{% endfor %}
</div>
{% else %}
<p class="empty">暂无任务,添加一个吧!</p>
{% endif %}
<!-- templates/partials/_task_item.html -->
<div id="task-{{ task.id }}" class="task-item {{ task.status }} priority-{{ task.priority }}">
<div class="task-header">
<span class="task-title">{{ task.title }}</span>
<span class="task-badge priority-{{ task.priority }}">{{ task.priority }}</span>
</div>
{% if task.description %}
<p class="task-desc">{{ task.description }}</p>
{% endif %}
<div class="task-actions">
<!-- 状态切换 -->
<div class="status-buttons"
003e
<button class="{% if task.status == 'pending' %}active{% endif %}"
hx-post="/tasks/{{ task.id }}/status"
hx-vals='{"status": "pending"}'
hx-target="#task-{{ task.id }}"
hx-swap="outerHTML"
hx-on::after-request="document.body.dispatchEvent(new Event('taskChanged'))"
003e
待办
</button>
<button class="{% if task.status == 'active' %}active{% endif %}"
hx-post="/tasks/{{ task.id }}/status"
hx-vals='{"status": "active"}'
hx-target="#task-{{ task.id }}"
hx-swap="outerHTML"
hx-on::after-request="document.body.dispatchEvent(new Event('taskChanged'))"
003e
进行中
</button>
<button class="{% if task.status == 'done' %}active{% endif %}"
hx-post="/tasks/{{ task.id }}/status"
hx-vals='{"status": "done"}'
hx-target="#task-{{ task.id }}"
hx-swap="outerHTML"
hx-on::after-request="document.body.dispatchEvent(new Event('taskChanged'))"
003e
完成
</button>
</div>
<div class="action-buttons">
<button class="edit-btn"
hx-get="/tasks/{{ task.id }}/edit"
hx-target="#task-{{ task.id }}"
hx-swap="outerHTML"
003e
编辑
</button>
<button class="delete-btn"
hx-delete="/tasks/{{ task.id }}"
hx-target="#task-{{ task.id }}"
hx-swap="outerHTML swap:300ms"
hx-confirm="确定删除此任务?"
hx-on::after-request="document.body.dispatchEvent(new Event('taskChanged'))"
003e
删除
</button>
</div>
</div>
</div>
<!-- templates/partials/_task_form.html -->
<form id="task-{{ task.id }}" class="task-form"
hx-put="/tasks/{{ task.id }}"
hx-target="#task-{{ task.id }}"
hx-swap="outerHTML"
hx-on::after-request="document.body.dispatchEvent(new Event('taskChanged'))"
003e
<input type="text" name="title" value="{{ task.title }}" required>
<input type="text" name="description" value="{{ task.description or '' }}">
<select name="priority">
<option value="low" {% if task.priority == 'low' %}selected{% endif %}>低</option>
<option value="medium" {% if task.priority == 'medium' %}selected{% endif %}>中</option>
<option value="high" {% if task.priority == 'high' %}selected{% endif %}>高</option>
</select>
<button type="submit">保存</button>
<button type="button"
hx-get="/tasks"
hx-target="#task-list"
hx-trigger="click"
003e
取消
</button>
</form>
<!-- templates/partials/_stats.html -->
<div class="stats-panel">
<div class="stat-item">
<span class="stat-value">{{ stats.total }}</span>
<span class="stat-label">总计</span>
</div>
<div class="stat-item pending">
<span class="stat-value">{{ stats.pending }}</span>
<span class="stat-label">待办</span>
</div>
<div class="stat-item active">
<span class="stat-value">{{ stats.active }}</span>
<span class="stat-label">进行中</span>
</div>
<div class="stat-item done">
<span class="stat-value">{{ stats.done }}</span>
<span class="stat-label">已完成</span>
</div>
</div>
9.2 案例二:无限滚动新闻列表
<!-- news-feed.html -->
<div class="news-feed"
hx-get="/api/news?page=1"
hx-trigger="load"
hx-target="this"
hx-swap="innerHTML"
003e
<div class="loading">加载中...</div>
</div>
<!-- 后端返回 -->
<!-- templates/partials/_news_list.html -->
{% for article in articles %}
<article class="news-item">
<img src="{{ article.image }}" alt="" loading="lazy">
<h2>{{ article.title }}</h2>
<p>{{ article.summary }}</p>
<time>{{ article.published_at }}</time>
</article>
{% endfor %}
{% if has_more %}
<div class="load-more-trigger"
hx-get="/api/news?page={{ next_page }}"
hx-trigger="revealed"
hx-target="this"
hx-swap="outerHTML"
003e
<span class="loading-text">加载更多...</span>
</div>
{% endif %}
9.3 案例三:实时通知中心
<!-- notification-center.html -->
<div class="notification-center"
hx-sse="connect:/sse/notifications"
003e
<div class="notification-header">
<h3>🔔 通知中心 <span id="unread-count">0</span></h3>
<button hx-post="/notifications/read-all"
hx-target="#notification-list"
hx-swap="outerHTML"
003e
全部已读
</button>
</div>
<div id="notification-list"
hx-sse="swap:beforeend"
class="notification-list"
003e
{% for notification in notifications %}
{% include 'partials/_notification.html' %}
{% endfor %}
</div>
</div>
下一章预告:第十章将对比 HTMX 与其他方案,并给出总结建议。
第九章完