Flask入门学习教程,从入门到精通,Flask智能租房——首页 知识点详解(6)
·
Flask智能租房——首页 知识点详解
一、房源总数展示
1.1 Flask路由与视图函数基础
# ==================== app.py ====================
# 导入Flask框架核心模块
from flask import Flask, render_template, jsonify, request
# 导入SQLAlchemy用于数据库ORM操作
from flask_sqlalchemy import SQLAlchemy
# 创建Flask应用实例,__name__用于确定应用根路径
app = Flask(__name__)
# 配置数据库连接URI,格式:数据库类型://用户名:密码@主机:端口/数据库名
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/smart_rent'
# 关闭SQLAlchemy的修改追踪功能,减少内存开销
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# 创建数据库实例,绑定到Flask应用
db = SQLAlchemy(app)
1.2 数据库模型定义
# ==================== models.py ====================
# 定义房源信息模型类,继承自db.Model基类
class House(db.Model):
# 指定数据库中的表名
__tablename__ = 'house'
# 定义主键字段,类型为整数,自动递增
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
# 定义房源标题字段,类型为字符串,最大长度200,不允许为空
title = db.Column(db.String(200), nullable=False)
# 定义租金字段,类型为浮点数,默认值为0
price = db.Column(db.Float, default=0)
# 定义房屋面积字段,类型为浮点数
area = db.Column(db.Float)
# 定义户型字段,如"2室1厅1卫",最大长度50
house_type = db.Column(db.String(50))
# 定义地址字段,最大长度300
address = db.Column(db.String(300))
# 定义描述字段,类型为文本(长文本)
description = db.Column(db.Text)
# 定义发布时间字段,类型为日期时间,默认为当前时间
publish_time = db.Column(db.DateTime, default=datetime.now)
# 定义浏览次数字段,整数类型,默认值为0
view_count = db.Column(db.Integer, default=0)
# 定义是否推荐字段,布尔类型,默认为False
is_hot = db.Column(db.Boolean, default=False)
# 定义外键字段,关联到用户表的id,表示该房源的发布者
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
# 定义与Image模型的一对多关系,backref提供反向引用,lazy=True表示懒加载
images = db.relationship('Image', backref='house', lazy=True)
# 定义对象的字符串表示,便于调试
def __repr__(self):
return f'<House {self.title}>'
# 定义图片模型类
class Image(db.Model):
# 指定表名为image
__tablename__ = 'image'
# 主键id
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
# 图片URL路径
image_url = db.Column(db.String(500))
# 外键,关联house表
house_id = db.Column(db.Integer, db.ForeignKey('house.id'))
1.3 查询房源总数的视图函数
# ==================== views.py ====================
from flask import Blueprint, render_template, jsonify
from models import db, House
# 创建蓝图对象,用于模块化组织路由,'main'是蓝图名称
main = Blueprint('main', __name__)
# 定义首页路由,接受GET请求
@main.route('/')
def index():
"""
首页视图函数
功能:获取房源总数并渲染首页模板
"""
# 使用SQLAlchemy的query.count()方法查询房源总数
# count()直接在数据库层面执行COUNT(*),效率高
total_houses = House.query.count()
# render_template渲染模板,将total_houses变量传递给模板
# 模板中可通过{{ total_houses }}访问该变量
return render_template('index.html', total_houses=total_houses)
# 定义API接口,返回房源总数(用于前端AJAX异步请求)
@main.route('/api/house/total')
def get_house_total():
"""
获取房源总数的API接口
返回JSON格式数据
"""
# 查询房源总数
total = House.query.count()
# 使用jsonify将Python字典转换为JSON响应
# 返回格式:{"code": 200, "total": 数字}
return jsonify({
'code': 200, # 状态码,200表示成功
'total': total # 房源总数
})
1.4 Jinja2模板语法——变量渲染与过滤器
<!-- ==================== templates/index.html ==================== -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>智能租房 - 首页</title>
<!-- url_for根据蓝图名称和函数名生成静态资源路径 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<!-- ============ 房源总数展示区 ============ -->
<section class="stats-section">
<!--
Jinja2模板变量语法:{{ 变量名 }}
渲染时会被替换为Python传递过来的实际值
-->
<h2>平台房源总数</h2>
<!-- 直接渲染Python传递的total_houses变量 -->
<span class="total-count">{{ total_houses }}</span>
<span>套</span>
<!--
Jinja2过滤器语法:{{ 变量 | 过滤器名 }}
number_format:格式化数字(添加千位分隔符)
注意:Flask默认不带number_format,需要自定义
-->
<p>已有 <strong>{{ total_houses | format_number }}</strong> 套优质房源</p>
</section>
</body>
</html>
1.5 自定义Jinja2过滤器
# ==================== filters.py ====================
# 自定义Jinja2过滤器模块
def format_number(value):
"""
自定义数字格式化过滤器
功能:将数字转换为带千位分隔符的字符串
例如:12345 -> "12,345"
参数:value - 要格式化的数字
返回:格式化后的字符串
"""
try:
# 使用Python内置的format函数进行千位分隔符格式化
# {:,} 表示用逗号作为千位分隔符
return format(value, ',')
except (ValueError, TypeError):
# 如果value不是数字类型,直接返回原值
return value
# ==================== app.py 中注册过滤器 ====================
# 在创建app之后注册自定义过滤器
# 第一个参数'format_number'是模板中使用的过滤器名
# 第二个参数是对应的Python函数
app.jinja_env.filters['format_number'] = format_number
二、最新房源数据展示
2.1 数据库查询——排序与分页
# ==================== views.py ====================
from flask import Blueprint, render_template
from models import db, House
from datetime import datetime
@main.route('/')
def index():
"""首页视图函数——展示最新房源"""
# 获取房源总数
total_houses = House.query.count()
# -------- 查询最新房源 --------
# .query - 创建查询对象
# .order_by() - 排序,House.publish_time.desc()表示按发布时间降序
# .limit(6) - 限制返回6条记录
# .all() - 将查询结果转为列表
latest_houses = House.query.order_by(
House.publish_time.desc() # desc()降序排列,最新的在前
).limit(6).all()
# -------- 分页查询示例 --------
# request.args.get获取URL查询参数,如?page=2
# 第一个参数是参数名,第二个是默认值,第三个是类型转换
page = request.args.get('page', 1, type=int)
# paginate()分页查询方法
# page: 当前页码
# per_page: 每页显示条数
# error_out: 页码超出范围时是否报错
pagination = House.query.order_by(
House.publish_time.desc()
).paginate(
page=page, # 当前页码
per_page=10, # 每页10条
error_out=False # 页码无效时不报错,返回空结果
)
# pagination.items - 当前页的数据列表
# pagination.page - 当前页码
# pagination.pages - 总页数
# pagination.total - 总记录数
# pagination.has_prev - 是否有上一页(布尔值)
# pagination.has_next - 是否有下一页(布尔值)
# pagination.prev_num - 上一页页码
# pagination.next_num - 下一页页码
return render_template(
'index.html',
total_houses=total_houses,
latest_houses=latest_houses,
pagination=pagination
)
2.2 Jinja2模板——循环遍历与条件判断
<!-- ==================== templates/index.html ==================== -->
<!-- ============ 最新房源展示区 ============ -->
<section class="latest-section">
<h2>最新房源</h2>
<!--
Jinja2 for循环语法:
{% for 变量 in 可迭代对象 %}
循环体
{% endfor %}
latest_houses是从视图函数传递过来的房源列表
house是每次循环的当前房源对象
-->
<div class="house-list">
{% for house in latest_houses %}
<!--
循环变量loop是Jinja2内置的循环信息对象
loop.index - 当前循环次数(从1开始)
loop.index0 - 当前循环次数(从0开始)
loop.first - 是否是第一次循环(布尔值)
loop.last - 是否是最后一次循环(布尔值)
loop.length - 总循环次数
-->
<div class="house-card {% if loop.first %}first-card{% endif %}">
<!-- 模板条件判断:{% if 条件 %} ... {% endif %} -->
{% if house.images %}
<!-- 如果房源有图片,显示第一张 -->
<!-- house.images[0]获取images列表的第一个元素 -->
<img src="{{ house.images[0].image_url }}"
alt="{{ house.title }}"
class="house-img">
{% else %}
<!-- 否则显示默认占位图 -->
<img src="{{ url_for('static', filename='images/default.jpg') }}"
alt="暂无图片"
class="house-img">
{% endif %}
<div class="house-info">
<!-- 渲染房源标题 -->
<h3 class="house-title">{{ house.title }}</h3>
<!--
使用过滤器对价格进行格式化
round(2)保留两位小数
-->
<p class="price">
¥{{ house.price | round(2) }}元/月
</p>
<!-- 渲染房源基本信息 -->
<p class="detail">
<span>{{ house.house_type }}</span>
<span>|</span>
<span>{{ house.area }}㎡</span>
</p>
<!-- 渲染地址信息,truncate过滤器截断过长文本 -->
<p class="address">{{ house.address | truncate(30) }}</p>
<!--
格式化日期显示
strftime过滤器需要自定义或使用default方式
-->
<p class="time">发布时间:{{ house.publish_time.strftime('%Y-%m-%d') }}</p>
</div>
</div>
<!--
如果列表为空,for循环中的else分支会被执行
类似于Python的 for...else 语法
-->
{% else %}
<div class="empty-message">
<p>暂无最新房源信息</p>
</div>
{% endfor %}
</div>
<!-- ============ 分页导航 ============ -->
{% if pagination %}
<div class="pagination">
<!-- 上一页按钮 -->
{% if pagination.has_prev %}
<!-- url_for生成带page参数的URL -->
<a href="{{ url_for('main.index', page=pagination.prev_num) }}">
« 上一页
</a>
{% else %}
<span class="disabled">« 上一页</span>
{% endif %}
<!-- 页码列表 -->
{% for p in pagination.iter_pages() %}
{% if p %}
{% if p == pagination.page %}
<!-- 当前页码高亮显示 -->
<span class="current-page">{{ p }}</span>
{% else %}
<a href="{{ url_for('main.index', page=p) }}">{{ p }}</a>
{% endif %}
{% else %}
<!-- iter_pages()返回None表示中间有省略的页码 -->
<span class="ellipsis">...</span>
{% endif %}
{% endfor %}
<!-- 下一页按钮 -->
{% if pagination.has_next %}
<a href="{{ url_for('main.index', page=pagination.next_num) }}">
下一页 »
</a>
{% else %}
<span class="disabled">下一页 »</span>
{% endif %}
</div>
{% endif %}
</section>
2.3 SQLAlchemy高级查询操作
# ==================== 查询知识点汇总 ====================
# ----- 基础查询 -----
# 查询所有房源
all_houses = House.query.all() # 返回列表
# 查询第一条记录
first_house = House.query.first() # 返回对象或None
# 按主键查询,等价于 WHERE id = 1
house = House.query.get(1) # 返回对象或None
# ----- 条件过滤 -----
# filter_by使用关键字参数,等价于 WHERE price = 2000
houses = House.query.filter_by(price=2000).all()
# filter使用表达式,支持更复杂的条件
# 等价于 WHERE price > 1000
houses = House.query.filter(House.price > 1000).all()
# 多条件查询(AND关系)
# 等价于 WHERE price > 1000 AND area > 50
houses = House.query.filter(
House.price > 1000,
House.area > 50
).all()
# ----- 模糊查询 -----
# 等价于 WHERE title LIKE '%两室%'
houses = House.query.filter(
House.title.like('%两室%')
).all()
# 等价于 WHERE title LIKE '%朝阳%' OR title LIKE '%海淀%'
houses = House.query.filter(
db.or_(
House.title.like('%朝阳%'),
House.title.like('%海淀%')
)
).all()
# ----- 范围查询 -----
# 等价于 WHERE price BETWEEN 1000 AND 3000
houses = House.query.filter(
House.price.between(1000, 3000)
).all()
# 等价于 WHERE id IN (1, 3, 5, 7)
houses = House.query.filter(
House.id.in_([1, 3, 5, 7])
).all()
# ----- 排序 -----
# 按价格升序排列
houses = House.query.order_by(House.price.asc()).all()
# 按价格降序排列
houses = House.query.order_by(House.price.desc()).all()
# 多字段排序:先按价格降序,再按面积升序
houses = House.query.order_by(
House.price.desc(),
House.area.asc()
).all()
# ----- 聚合函数 -----
from sqlalchemy import func
# 统计总数
total = db.session.query(func.count(House.id)).scalar()
# 最高租金
max_price = db.session.query(func.max(House.price)).scalar()
# 最低租金
min_price = db.session.query(func.min(House.price)).scalar()
# 平均租金
avg_price = db.session.query(func.avg(House.price)).scalar()
# 租金总和
sum_price = db.session.query(func.sum(House.price)).scalar()
# 分组统计:按house_type分组,统计每种户型的数量
# 等价于 SELECT house_type, COUNT(*) FROM house GROUP BY house_type
result = db.session.query(
House.house_type,
func.count(House.id).label('count') # label给聚合列起别名
).group_by(House.house_type).all()
三、热点房源数据展示
3.1 多条件查询与排序策略
# ==================== views.py ====================
@main.route('/')
def index():
"""首页视图函数——展示热点房源"""
# -------- 方式一:按浏览量排序获取热点房源 --------
# 查询浏览量最高的6条房源
# ORDER BY view_count DESC LIMIT 6
hot_houses = House.query.order_by(
House.view_count.desc()
).limit(6).all()
# -------- 方式二:查询is_hot为True的推荐房源 --------
# WHERE is_hot = 1 ORDER BY publish_time DESC LIMIT 6
recommended_houses = House.query.filter_by(
is_hot=True
).order_by(
House.publish_time.desc()
).limit(6).all()
# -------- 方式三:综合热度评分查询 --------
# 使用SQLAlchemy的func构建复杂计算表达式
# 热度 = 浏览量 * 0.6 + 是否推荐 * 100 * 0.4
from sqlalchemy import case
hot_score = (
House.view_count * 0.6 +
case(
# case类似于SQL的CASE WHEN语句
# 条件为True时值为100,否则为0
(House.is_hot == True, 100),
else_=0
) * 0.4
)
# 按热度评分降序排列
top_houses = House.query.order_by(
hot_score.desc()
).limit(6).all()
return render_template(
'index.html',
total_houses=total_houses,
latest_houses=latest_houses,
hot_houses=hot_houses,
recommended_houses=recommended_houses,
top_houses=top_houses
)
3.2 热点房源模板展示
<!-- ============ 热点房源展示区 ============ -->
<section class="hot-section">
<h2>热点房源</h2>
<!-- 使用网格布局展示热点房源 -->
<div class="house-grid">
{% for house in hot_houses %}
<!--
data-* 自定义数据属性,用于前端JavaScript获取数据
无需额外请求即可在前端使用这些数据
-->
<div class="house-card hot-card"
data-id="{{ house.id }}"
data-price="{{ house.price }}">
<!-- 图片区域 -->
<div class="card-img-wrapper">
{% if house.images %}
<img src="{{ house.images[0].image_url }}" alt="{{ house.title }}">
{% else %}
<img src="{{ url_for('static', filename='images/default.jpg') }}" alt="暂无图片">
{% endif %}
<!-- 热点标签 -->
<span class="hot-tag">热门</span>
<!-- 浏览量显示 -->
<span class="view-count">
<!-- 使用Jinja2的length过滤器统计图片数量 -->
浏览:{{ house.view_count }}
</span>
</div>
<!-- 信息区域 -->
<div class="card-body">
<h4>{{ house.title | truncate(20) }}</h4>
<p class="price-tag">
<em>¥{{ house.price | round(0, 'floor') | int }}</em>元/月
</p>
<p class="meta">
{{ house.house_type }} | {{ house.area }}㎡ | {{ house.address | truncate(15) }}
</p>
</div>
</div>
{% endfor %}
</div>
</section>
3.3 常用Jinja2过滤器汇总
# ==================== Jinja2内置过滤器示例 ====================
# --- 字符串过滤器 ---
# {{ "hello world" | capitalize }} → "Hello world" 首字母大写
# {{ "HELLO" | lower }} → "hello" 全部转小写
# {{ "hello" | upper }} → "HELLO" 全部转大写
# {{ "hello world" | title }} → "Hello World" 每个单词首字母大写
# {{ " hello " | trim }} → "hello" 去除首尾空格
# {{ "hello" | reverse }} → "olleh" 字符串反转
# {{ "hello world" | truncate(5) }} → "hello..." 截断文本(默认255字符)
# {{ "<b>bold</b>" | striptags }} → "bold" 去除HTML标签
# {{ "hello" | length }} → 5 字符串长度
# {{ ["a","b","c"] | join(",") }} → "a,b,c" 列表连接
# --- 数字过滤器 ---
# {{ 3.14159 | round(2) }} → 3.14 四舍五入保留2位
# {{ 3.7 | int }} → 3 转为整数
# {{ 1234 | abs }} → 1234 绝对值
# {{ 4 | pow(2) }} → 16 幂运算
# --- 列表过滤器 ---
# {{ [3,1,2] | sort }} → [1, 2, 3] 排序
# {{ [3,1,2] | reverse }} → [2, 1, 3] 反转
# {{ [1,2,3] | first }} → 1 第一个元素
# {{ [1,2,3] | last }} → 3 最后一个元素
# {{ [1,2,3,4] | batch(2) }} → [[1,2], [3,4]] 分批
# {{ [1,2,3] | sum }} → 6 求和
# {{ [1,2,3,4,5] | random }} → 随机元素
# --- 默认值过滤器 ---
# {{ None | default('N/A') }} → "N/A" 空值默认
# {{ "" | default('N/A', boolean=True) }}→ "N/A" 空字符串也使用默认值
四、智能搜索功能
4.1 搜索表单设计(前端)
<!-- ============ templates/index.html 搜索区域 ============ -->
<section class="search-section">
<h2>智能搜索</h2>
<!--
form表单属性说明:
action: 提交到的URL地址
method: HTTP请求方法,GET方式会将参数拼接到URL上
id: 唯一标识符,供JavaScript获取元素
-->
<form id="searchForm" action="{{ url_for('main.search') }}" method="GET">
<!-- 关键词搜索 -->
<div class="form-group">
<label for="keyword">关键词:</label>
<!--
input属性说明:
type="text" - 文本输入框
name="keyword" - 表单字段名,后端通过此名称获取值
id="keyword" - 唯一标识符
placeholder - 占位提示文字
class - CSS类名,用于样式定义
required - HTML5原生验证,表示必填
-->
<input type="text"
name="keyword"
id="keyword"
placeholder="输入房源标题、地址等关键词"
class="form-control"
required>
</div>
<!-- 租金范围 -->
<div class="form-group">
<label>租金范围:</label>
<input type="number"
name="price_min"
id="priceMin"
placeholder="最低价格"
min="0"
class="form-control price-input">
<span>—</span>
<input type="number"
name="price_max"
id="priceMax"
placeholder="最高价格"
min="0"
class="form-control price-input">
</div>
<!-- 户型选择 -->
<div class="form-group">
<label>户型:</label>
<!--
select下拉选择框
name: 表单字段名
option: 选项列表
value: 提交到后端的值
-->
<select name="house_type" id="houseType" class="form-control">
<!-- value="" 表示不限制 -->
<option value="">不限</option>
<option value="1室1厅1卫">一室</option>
<option value="2室1厅1卫">两室</option>
<option value="3室1厅1卫">三室</option>
<option value="4室及以上">四室及以上</option>
</select>
</div>
<!-- 面积范围 -->
<div class="form-group">
<label>面积范围:</label>
<select name="area_range" id="areaRange" class="form-control">
<option value="">不限</option>
<!-- value格式:"最小值-最大值",后端解析此格式 -->
<option value="0-50">50㎡以下</option>
<option value="50-80">50-80㎡</option>
<option value="80-120">80-120㎡</option>
<option value="120-99999">120㎡以上</option>
</select>
</div>
<!-- 排序方式 -->
<div class="form-group">
<label>排序:</label>
<select name="sort" id="sort" class="form-control">
<option value="newest">最新发布</option>
<option value="price_asc">价格从低到高</option>
<option value="price_desc">价格从高到低</option>
<option value="area_desc">面积从大到小</option>
<option value="popular">最受欢迎</option>
</select>
</div>
<!-- 提交按钮 -->
<div class="form-group">
<!-- type="submit" 点击时触发表单提交 -->
<button type="submit" class="btn-search">搜索房源</button>
<!-- type="reset" 点击时重置表单所有字段为初始值 -->
<button type="reset" class="btn-reset">重置条件</button>
</div>
</form>
</section>
4.2 搜索后端逻辑实现
# ==================== views.py 搜索视图 ====================
from flask import request, render_template, redirect, url_for
from sqlalchemy import or_, and_
@main.route('/search')
def search():
"""
搜索视图函数
接收GET请求的查询参数,构建动态查询条件
"""
# ---- 1. 获取搜索参数 ----
# request.args 是一个ImmutableMultiDict,包含URL中?后的所有参数
# .get()方法第一个参数是键名,第二个是默认值
keyword = request.args.get('keyword', '', type=str).strip()
price_min = request.args.get('price_min', 0, type=float)
price_max = request.args.get('price_max', 0, type=float)
house_type = request.args.get('house_type', '', type=str)
area_range = request.args.get('area_range', '', type=str)
sort_by = request.args.get('sort', 'newest', type=str)
page = request.args.get('page', 1, type=int)
# ---- 2. 构建基础查询 ----
# 开始构建查询,不立即执行
# House.query返回一个BaseQuery对象,支持链式调用
query = House.query
# ---- 3. 关键词搜索(模糊匹配) ----
if keyword:
# 使用or_实现多字段模糊搜索
# 等价于:WHERE title LIKE '%keyword%' OR address LIKE '%keyword%'
query = query.filter(
or_(
House.title.like(f'%{keyword}%'),
House.address.like(f'%{keyword}%'),
House.description.like(f'%{keyword}%')
)
)
# ---- 4. 价格范围筛选 ----
if price_min > 0:
# 等价于:WHERE price >= price_min
query = query.filter(House.price >= price_min)
if price_max > 0:
# 等价于:WHERE price <= price_max
query = query.filter(House.price <= price_max)
# ---- 5. 户型筛选 ----
if house_type:
# 等价于:WHERE house_type = '具体户型'
query = query.filter(House.house_type == house_type)
# ---- 6. 面积范围筛选 ----
if area_range:
# 解析面积范围字符串,如"50-80"→[50, 80]
area_parts = area_range.split('-')
if len(area_parts) == 2:
area_min = float(area_parts[0]) # 最小面积
area_max = float(area_parts[1]) # 最大面积
query = query.filter(
and_(
House.area >= area_min,
House.area <= area_max
)
)
# ---- 7. 排序处理 ----
# 使用字典映射实现排序策略的选择
sort_map = {
'newest': House.publish_time.desc(), # 最新发布
'price_asc': House.price.asc(), # 价格升序
'price_desc': House.price.desc(), # 价格降序
'area_desc': House.area.desc(), # 面积降序
'popular': House.view_count.desc(), # 按浏览量排序
}
# .get()获取排序方式,如果sort_by不在字典中,默认按最新发布排序
query = query.order_by(sort_map.get(sort_by, House.publish_time.desc()))
# ---- 8. 分页执行查询 ----
pagination = query.paginate(
page=page,
per_page=10,
error_out=False
)
# ---- 9. 获取搜索结果 ----
houses = pagination.items
# ---- 10. 记录搜索历史(可选功能) ----
# 将搜索参数保存,用于在模板中回显
search_params = {
'keyword': keyword,
'price_min': price_min if price_min > 0 else '',
'price_max': price_max if price_max > 0 else '',
'house_type': house_type,
'area_range': area_range,
'sort': sort_by
}
return render_template(
'search_result.html',
houses=houses,
pagination=pagination,
search_params=search_params,
total=pagination.total # 搜索结果总数
)
4.3 搜索结果模板
<!-- ==================== templates/search_result.html ==================== -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>搜索结果 - 智能租房</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<!-- 搜索栏(保留搜索条件回显) -->
<section class="search-bar">
<form action="{{ url_for('main.search') }}" method="GET">
<!--
value="{{ search_params.keyword }}"
将之前的搜索关键词回填到输入框中
-->
<input type="text" name="keyword"
value="{{ search_params.keyword }}"
placeholder="搜索房源...">
<!--
使用循环生成选项并自动选中之前的选择
selected属性使option在页面加载时被选中
-->
<select name="house_type">
<option value="">不限</option>
{% for type in ['1室1厅1卫', '2室1厅1卫', '3室1厅1卫'] %}
<!--
Jinja2条件表达式语法:
如果当前户型与搜索参数匹配,则添加selected属性
-->
<option value="{{ type }}"
{% if type == search_params.house_type %}selected{% endif %}>
{{ type }}
</option>
{% endfor %}
</select>
<button type="submit">搜索</button>
</form>
</section>
<!-- 搜索结果统计 -->
<section class="result-info">
<p>
共找到 <strong>{{ total }}</strong> 套房源
{% if search_params.keyword %}
,关键词:<em>"{{ search_params.keyword }}"</em>
{% endif %}
</p>
</section>
<!-- 搜索结果列表 -->
<section class="result-list">
{% if houses %}
{% for house in houses %}
<div class="result-item">
<div class="item-img">
{% if house.images %}
<img src="{{ house.images[0].image_url }}" alt="">
{% else %}
<img src="{{ url_for('static', filename='images/default.jpg') }}" alt="">
{% endif %}
</div>
<div class="item-info">
<h3>{{ house.title }}</h3>
<p>{{ house.house_type }} | {{ house.area }}㎡ | {{ house.address }}</p>
<p class="price">¥{{ house.price }}/月</p>
<!--
linebreaksbr过滤器将换行符转换为HTML的<br>标签
保留用户输入的换行格式
-->
<p class="desc">{{ house.description | truncate(100) }}</p>
</div>
</div>
{% endfor %}
{% else %}
<!--
如果没有搜索结果,显示空状态提示
可以加入推荐内容
-->
<div class="empty-result">
<h3>未找到符合条件的房源</h3>
<p>请尝试修改搜索条件</p>
</div>
{% endif %}
</section>
<!-- 分页(同上面的分页模板) -->
{% if pagination and pagination.pages > 1 %}
<div class="pagination">
{% for p in pagination.iter_pages() %}
{% if p %}
<a href="{{ url_for('main.search', page=p, keyword=search_params.keyword, house_type=search_params.house_type) }}"
class="{% if p == pagination.page %}active{% endif %}">
{{ p }}
</a>
{% else %}
<span>...</span>
{% endif %}
{% endfor %}
</div>
{% endif %}
</body>
</html>
五、AJAX异步请求与智能搜索前端逻辑
5.1 原生JavaScript AJAX实现
// ==================== static/js/search.js ====================
/**
* 使用原生XMLHttpRequest实现AJAX请求
* XMLHttpRequest是浏览器内置的HTTP请求对象
*/
function searchByAJAX() {
// ---- 1. 获取表单元素的值 ----
// document.getElementById()通过id获取DOM元素
// .value获取输入框/选择框的当前值
var keyword = document.getElementById('keyword').value;
var priceMin = document.getElementById('priceMin').value;
var priceMax = document.getElementById('priceMax').value;
var houseType = document.getElementById('houseType').value;
// ---- 2. 创建XMLHttpRequest对象 ----
// XMLHttpRequest是AJAX的核心对象,用于在后台与服务器交换数据
var xhr = new XMLHttpRequest();
// ---- 3. 构建查询参数字符串 ----
// encodeURIComponent()对特殊字符进行URL编码
// 防止中文、空格、&等特殊字符破坏URL格式
var params = 'keyword=' + encodeURIComponent(keyword)
+ '&price_min=' + encodeURIComponent(priceMin)
+ '&price_max=' + encodeURIComponent(priceMax)
+ '&house_type=' + encodeURIComponent(houseType);
// ---- 4. 配置请求 ----
// xhr.open(方法, URL, 是否异步)
// 'GET'请求方法
// '/api/search'请求的API地址
// true表示异步请求(不阻塞页面)
xhr.open('GET', '/api/search?' + params, true);
// ---- 5. 设置回调函数 ----
// onreadystatechange事件在请求状态改变时触发
xhr.onreadystatechange = function() {
// readyState状态值说明:
// 0: UNSENT - 请求未初始化(open()还未调用)
// 1: OPENED - 已调用open(),尚未调用send()
// 2: HEADERS_RECEIVED - 已接收响应头
// 3: LOADING - 正在接收响应体
// 4: DONE - 请求完成
if (xhr.readyState === 4) {
// HTTP状态码200表示请求成功
if (xhr.status === 200) {
// JSON.parse()将JSON字符串解析为JavaScript对象
var data = JSON.parse(xhr.responseText);
// 调用渲染函数,将数据渲染到页面
renderSearchResults(data.houses);
} else {
// 请求失败时的错误处理
console.error('请求失败,状态码:' + xhr.status);
}
}
};
// ---- 6. 发送请求 ----
// GET请求的参数已经拼接在URL中,所以send()无需传参
xhr.send();
}
/**
* 渲染搜索结果到页面
* @param {Array} houses - 房源数据数组
*/
function renderSearchResults(houses) {
// 获取结果容器元素
var container = document.getElementById('searchResults');
// 如果没有结果,显示提示信息
if (!houses || houses.length === 0) {
container.innerHTML = '<p class="no-result">未找到匹配房源</p>';
return; // 提前结束函数
}
// 构建HTML字符串
var html = '';
// 遍历房源数组,为每条房源构建卡片HTML
// for循环遍历数组
for (var i = 0; i < houses.length; i++) {
var house = houses[i]; // 获取当前房源对象
// 字符串拼接构建HTML
// 使用模板字符串(ES6反引号)更方便
html += '<div class="house-card" data-id="' + house.id + '">';
html += ' <img src="' + (house.image || '/static/images/default.jpg') + '">';
html += ' <h3>' + house.title + '</h3>';
html += ' <p>¥' + house.price + '元/月</p>';
html += ' <p>' + house.house_type + ' | ' + house.area + '㎡</p>';
html += '</div>';
}
// innerHTML设置元素的HTML内容
container.innerHTML = html;
}
5.2 jQuery AJAX实现(推荐)
// ==================== static/js/search_jquery.js ====================
/**
* 使用jQuery实现AJAX搜索
* jQuery封装了XMLHttpRequest,使用更简洁
* 需要先引入jQuery:<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
*/
$(document).ready(function() {
// ---- 页面加载完成后执行 ----
// $(document).ready()确保DOM已完全加载
/**
* 表单提交事件绑定
* #searchForm是CSS选择器,选择id为searchForm的元素
* .submit()绑定表单提交事件
* event.preventDefault()阻止表单的默认提交行为(防止页面刷新)
*/
$('#searchForm').submit(function(event) {
// 阻止表单默认提交(页面刷新)
event.preventDefault();
// 调用搜索函数
doSearch();
});
/**
* 关键词输入框实时搜索(防抖处理)
* .on('input', callback) 在输入内容变化时触发
*/
var searchTimer = null; // 定时器变量,用于防抖
$('#keyword').on('input', function() {
// 防抖逻辑:清除之前的定时器
// 用户停止输入500毫秒后才执行搜索
if (searchTimer) {
clearTimeout(searchTimer); // 清除之前的定时器
}
// 设置新的定时器,500毫秒后执行
searchTimer = setTimeout(function() {
doSearch(); // 执行搜索
}, 500); // 延迟500毫秒
});
});
/**
* 执行AJAX搜索请求
*/
function doSearch() {
// ---- 1. 使用jQuery的serialize()自动序列化表单数据 ----
// 将表单中所有有name属性的字段拼接为URL查询字符串
// 如:keyword=两室&price_min=1000&price_max=3000&house_type=2室1厅1卫
var formData = $('#searchForm').serialize();
// ---- 2. 发送AJAX GET请求 ----
$.ajax({
url: '/api/search', // 请求地址
type: 'GET', // 请求方法
data: formData, // 请求参数(自动拼接到URL)
dataType: 'json', // 期望返回的数据类型
// 请求成功时的回调函数
// data: 服务器返回的已解析的JSON对象
// textStatus: 请求状态文本(如"success")
// jqXHR: jQuery封装的XMLHttpRequest对象
success: function(data, textStatus, jqXHR) {
if (data.code === 200) {
// 请求成功,渲染结果
renderResults(data.houses);
// 更新结果计数
$('#resultCount').text(data.total);
} else {
// 业务逻辑错误
alert('搜索出错:' + data.message);
}
},
// 请求失败时的回调函数
error: function(jqXHR, textStatus, errorThrown) {
console.error('AJAX请求失败:', textStatus, errorThrown);
alert('网络请求失败,请稍后重试');
},
// 请求完成时的回调(无论成功或失败都会执行)
complete: function(jqXHR, textStatus) {
// 隐藏加载动画
$('#loadingSpinner').hide();
},
// 请求发送前的回调
beforeSend: function(jqXHR) {
// 显示加载动画
$('#loadingSpinner').show();
}
});
}
/**
* 渲染搜索结果(使用jQuery方法)
* @param {Array} houses - 房源数组
*/
function renderResults(houses) {
var $container = $('#searchResults');
// 清空容器内容
$container.empty();
// 判断是否有结果
if (!houses || houses.length === 0) {
// .append()向元素内部末尾追加内容
$container.append(
'<div class="empty-result"><p>暂无匹配房源</p></div>'
);
return;
}
// 遍历房源数据
// $.each()是jQuery的遍历方法
// index: 当前索引,house: 当前元素
$.each(houses, function(index, house) {
// 构建房源卡片HTML
var cardHtml = `
<div class="house-card" data-id="${house.id}">
<div class="card-img">
<img src="${house.image || '/static/images/default.jpg'}"
alt="${house.title}">
</div>
<div class="card-info">
<h3>${house.title}</h3>
<p class="price">¥${house.price}元/月</p>
<p class="meta">${house.house_type} | ${house.area}㎡</p>
<p class="addr">${house.address}</p>
</div>
</div>
`;
// 将卡片追加到容器中
$container.append(cardHtml);
});
}
5.3 AJAX搜索API后端实现
# ==================== views.py AJAX搜索API ====================
from flask import request, jsonify
@main.route('/api/search')
def api_search():
"""
AJAX搜索API接口
返回JSON格式的搜索结果
"""
# ---- 获取参数 ----
keyword = request.args.get('keyword', '', type=str).strip()
price_min = request.args.get('price_min', 0, type=float)
price_max = request.args.get('price_max', 0, type=float)
house_type = request.args.get('house_type', '', type=str)
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
# ---- 参数验证 ----
# 确保每页数量在合理范围内
if per_page > 50:
per_page = 50 # 限制最大每页50条
try:
# ---- 构建查询 ----
query = House.query
if keyword:
query = query.filter(
or_(
House.title.like(f'%{keyword}%'),
House.address.like(f'%{keyword}%')
)
)
if price_min > 0:
query = query.filter(House.price >= price_min)
if price_max > 0:
query = query.filter(House.price <= price_max)
if house_type:
query = query.filter(House.house_type == house_type)
# 分页查询
pagination = query.order_by(
House.publish_time.desc()
).paginate(page=page, per_page=per_page, error_out=False)
# ---- 序列化查询结果 ----
# 将ORM对象转换为字典列表,便于JSON序列化
houses_data = []
for house in pagination.items:
house_dict = {
'id': house.id, # 房源ID
'title': house.title, # 标题
'price': house.price, # 价格
'area': house.area, # 面积
'house_type': house.house_type, # 户型
'address': house.address, # 地址
'view_count': house.view_count, # 浏览量
'publish_time': house.publish_time.strftime('%Y-%m-%d'), # 格式化时间
'image': house.images[0].image_url if house.images else None # 三元表达式
}
houses_data.append(house_dict)
# ---- 构建响应 ----
response_data = {
'code': 200,
'message': 'success',
'total': pagination.total, # 总记录数
'pages': pagination.pages, # 总页数
'current_page': pagination.page, # 当前页码
'houses': houses_data # 房源数据列表
}
return jsonify(response_data)
except Exception as e:
# 异常处理,捕获所有错误
# 返回错误信息,前端根据code判断是否成功
return jsonify({
'code': 500,
'message': f'服务器内部错误:{str(e)}',
'houses': []
}), 500 # 第二个参数设置HTTP状态码
六、Flask蓝图与项目结构
6.1 蓝图注册与应用工厂模式
# ==================== app.py ====================
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS # 跨域资源共享
# 创建数据库实例(不在全局绑定app)
db = SQLAlchemy()
def create_app(config_name='default'):
"""
应用工厂函数
功能:创建并配置Flask应用实例
优势:便于测试、可以创建多个不同配置的实例
参数:config_name - 配置名称,如'default'、'testing'、'production'
返回:配置完成的Flask应用实例
"""
# 创建Flask应用实例
app = Flask(__name__)
# ---- 加载配置 ----
from config import config
app.config.from_object(config[config_name])
# ---- 初始化扩展 ----
db.init_app(app) # 初始化数据库
CORS(app) # 初始化跨域支持
# ---- 注册蓝图 ----
# register_blueprint()将蓝图注册到应用上
# url_prefix为该蓝图下所有路由添加统一前缀
from views import main
app.register_blueprint(main, url_prefix='')
# 注册用户模块蓝图
from user_views import user_bp
app.register_blueprint(user_bp, url_prefix='/user')
# ---- 注册自定义过滤器 ----
from filters import format_number
app.jinja_env.filters['format_number'] = format_number
# ---- 错误处理 ----
# @app.errorhandler装饰器注册错误处理函数
# 404是HTTP状态码,表示页面未找到
@app.errorhandler(404)
def page_not_found(error):
"""
404错误处理函数
当用户访问不存在的页面时显示
"""
return render_template('errors/404.html'), 404
# 500是服务器内部错误
@app.errorhandler(500)
def internal_error(error):
"""
500错误处理函数
当服务器发生未捕获的异常时显示
"""
# 回滚数据库事务
db.session.rollback()
return render_template('errors/500.html'), 500
return app
# ==================== 启动入口 ====================
if __name__ == '__main__':
# 创建应用实例
app = create_app()
# 创建数据库表(如果不存在)
with app.app_context():
db.create_all()
# 启动开发服务器
# debug=True开启调试模式(代码修改后自动重启,显示详细错误信息)
# host='0.0.0.0'允许外部网络访问
# port=5000指定端口号
app.run(debug=True, host='0.0.0.0', port=5000)
6.2 配置文件分离
# ==================== config.py ====================
import os
class BaseConfig:
"""基础配置类,包含所有环境通用的配置"""
# SECRET_KEY用于session加密、CSRF保护等安全功能
SECRET_KEY = os.environ.get('SECRET_KEY') or 'my-secret-key-123'
# 数据库配置
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@localhost:3306/smart_rent'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 分页配置
PER_PAGE = 10
class DevelopmentConfig(BaseConfig):
"""开发环境配置"""
DEBUG = True
class ProductionConfig(BaseConfig):
"""生产环境配置"""
DEBUG = False
class TestingConfig(BaseConfig):
"""测试环境配置"""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' # 使用内存数据库
# 配置字典,通过名称选择配置
config = {
'default': DevelopmentConfig, # 默认使用开发配置
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig
}
七、前端页面完整样式与动画
7.1 CSS样式实现
/* ==================== static/css/style.css ==================== */
/* ---- CSS自定义属性(变量)定义 ---- */
:root {
/* 在:root中定义CSS变量,整个文档都可以使用 */
--primary-color: #2563eb; /* 主色调:蓝色 */
--secondary-color: #f59e0b; /* 次要色:橙色 */
--bg-color: #f8fafc; /* 背景色:浅灰 */
--card-bg: #ffffff; /* 卡片背景:白色 */
--text-primary: #1e293b; /* 主要文字颜色:深灰 */
--text-secondary: #64748b; /* 次要文字颜色:灰色 */
--border-color: #e2e8f0; /* 边框颜色:浅灰 */
--shadow: 0 4px 6px rgba(0,0,0,0.1); /* 阴影 */
--radius: 8px; /* 圆角 */
--transition: all 0.3s ease; /* 过渡动画 */
}
/* ---- 全局重置 ---- */
* {
margin: 0; /* 外边距归零 */
padding: 0; /* 内边距归零 */
box-sizing: border-box; /* 盒模型:宽高包含padding和border */
}
body {
font-family: 'Microsoft YaHei', sans-serif;
background-color: var(--bg-color); /* 使用CSS变量 */
color: var(--text-primary);
line-height: 1.6; /* 行高 */
}
/* ---- 搜索区域样式 ---- */
.search-section {
max-width: 1200px; /* 最大宽度 */
margin: 30px auto; /* 上下30px,左右自动(居中) */
padding: 30px;
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.search-section h2 {
font-size: 24px;
margin-bottom: 20px;
color: var(--text-primary);
/* border-left: 左边框 */
border-left: 4px solid var(--primary-color);
padding-left: 12px;
}
/* ---- 表单样式 ---- */
.form-group {
display: flex; /* 弹性盒布局 */
align-items: center; /* 垂直居中 */
margin-bottom: 15px;
gap: 10px; /* 子元素间距 */
}
.form-group label {
min-width: 80px; /* 最小宽度,保证标签对齐 */
font-weight: 600;
color: var(--text-secondary);
}
.form-control {
padding: 10px 14px;
border: 1px solid var(--border-color);
border-radius: var(--radius);
font-size: 14px;
transition: var(--transition);
outline: none; /* 去除默认聚焦轮廓 */
}
/* :focus伪类,元素获得焦点时的样式 */
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
}
.price-input {
width: 120px; /* 价格输入框固定宽度 */
}
/* ---- 房源卡片网格布局 ---- */
.house-grid {
display: grid; /* 网格布局 */
/* 自动填充列,每列最小280px,最大1fr(等分剩余空间) */
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px; /* 网格间距 */
padding: 20px 0;
}
/* ---- 房源卡片样式 ---- */
.house-card {
background: var(--card-bg);
border-radius: var(--radius);
overflow: hidden; /* 隐藏超出的内容(图片圆角效果) */
box-shadow: var(--shadow);
transition: var(--transition);
/* 初始状态:透明+下移 */
opacity: 0;
transform: translateY(20px);
/* 动画:名称 持续时间 缓动函数 填充模式 */
animation: fadeInUp 0.6s ease forwards;
}
/* nth-child(n)选择第n个子元素,实现交错动画 */
.house-card:nth-child(2) { animation-delay: 0.1s; }
.house-card:nth-child(3) { animation-delay: 0.2s; }
.house-card:nth-child(4) { animation-delay: 0.3s; }
.house-card:nth-child(5) { animation-delay: 0.4s; }
.house-card:nth-child(6) { animation-delay: 0.5s; }
/* @keyframes定义动画关键帧 */
@keyframes fadeInUp {
from {
opacity: 0; /* 起始状态:完全透明 */
transform: translateY(20px); /* 起始位置:向下偏移20px */
}
to {
opacity: 1; /* 结束状态:完全可见 */
transform: translateY(0); /* 结束位置:原始位置 */
}
}
/* :hover伪类,鼠标悬停时的样式 */
.house-card:hover {
transform: translateY(-5px); /* 上移5px */
box-shadow: 0 12px 24px rgba(0,0,0,0.15); /* 加深阴影 */
}
.card-img-wrapper {
position: relative; /* 相对定位,作为子元素绝对定位的参照 */
height: 200px;
overflow: hidden;
}
.card-img-wrapper img {
width: 100%;
height: 100%;
object-fit: cover; /* 图片裁剪填充,保持比例 */
transition: transform 0.3s ease;
}
.house-card:hover .card-img-wrapper img {
transform: scale(1.05); /* 鼠标悬停时图片略微放大 */
}
/* 热门标签 */
.hot-tag {
position: absolute; /* 绝对定位,相对于最近的positioned祖先 */
top: 10px;
right: 10px;
background: var(--secondary-color);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.card-body {
padding: 15px;
}
.card-body h4 {
font-size: 16px;
margin-bottom: 8px;
/* 文字溢出时显示省略号 */
white-space: nowrap; /* 不换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 显示省略号 */
}
.price-tag em {
font-style: normal; /* 取消斜体 */
font-size: 22px;
font-weight: 700;
color: #e74c3c; /* 价格用红色突出 */
}
/* ---- 按钮样式 ---- */
.btn-search {
padding: 10px 30px;
background: var(--primary-color);
color: white;
border: none;
border-radius: var(--radius);
font-size: 16px;
cursor: pointer; /* 鼠标指针变为手形 */
transition: var(--transition);
letter-spacing: 0; /* 初始字间距 */
}
.btn-search:hover {
background: #1d4ed8;
letter-spacing: 2px; /* 悬停时字间距增大 */
}
/* ---- 分页样式 ---- */
.pagination {
display: flex;
justify-content: center; /* 水平居中 */
gap: 8px;
margin: 30px 0;
}
.pagination a,
.pagination span {
display: inline-block;
padding: 8px 14px;
border: 1px solid var(--border-color);
border-radius: var(--radius);
text-decoration: none; /* 去除下划线 */
color: var(--text-primary);
transition: var(--transition);
}
.pagination a:hover {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.pagination .current-page {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
/* ---- 加载动画 ---- */
.loading-spinner {
display: none; /* 默认隐藏 */
text-align: center;
padding: 20px;
}
.loading-spinner.show {
display: block; /* 显示加载动画 */
}
/* CSS纯动画实现的加载旋转效果 */
.spinner {
width: 40px;
height: 40px;
border: 4px solid var(--border-color);
border-top: 4px solid var(--primary-color); /* 顶部边框不同颜色 */
border-radius: 50%; /* 圆形 */
animation: spin 1s linear infinite; /* 无限旋转 */
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
八、会话管理与用户状态
8.1 Flask Session使用
# ==================== 会话管理 ====================
from flask import session, redirect, url_for, flash
import os
# 在配置中设置SECRET_KEY(Session加密必需)
app.config['SECRET_KEY'] = os.urandom(24) # 生成24字节随机密钥
@main.route('/')
def index():
"""首页——检查用户登录状态"""
# session是基于Cookie的会话对象
# 数据存储在服务器端(加密后发送Cookie到客户端)
# 获取session中的用户信息
# .get()方法如果键不存在返回None,不会报错
username = session.get('username')
user_id = session.get('user_id')
is_logged_in = session.get('logged_in', False)
return render_template(
'index.html',
username=username,
user_id=user_id,
is_logged_in=is_logged_in
)
@main.route('/login', methods=['GET', 'POST'])
def login():
"""
登录视图
GET请求:显示登录页面
POST请求:处理登录表单提交
"""
if request.method == 'POST':
# 获取表单数据
username = request.form.get('username')
password = request.form.get('password')
# 查询用户
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
# 密码正确,设置session
session['user_id'] = user.id # 存储用户ID
session['username'] = user.username # 存储用户名
session['logged_in'] = True # 登录标记
# flash()发送一次性消息,下次读取后自动删除
# 第二个参数是消息类别:'success', 'error', 'warning', 'info'
flash('登录成功!', 'success')
# redirect()重定向到指定URL
# url_for()通过函数名生成URL
return redirect(url_for('main.index'))
else:
flash('用户名或密码错误', 'error')
return render_template('login.html')
@main.route('/logout')
def logout():
"""退出登录"""
# session.clear()清除所有session数据
session.clear()
# 也可以选择性删除
# session.pop('user_id', None) # 删除指定键,None是默认值
flash('已成功退出', 'info')
return redirect(url_for('main.index'))
# 在模板中使用session
# Jinja2模板可以直接访问session对象
"""
{% if session.get('logged_in') %}
<p>欢迎回来,{{ session.username }}!</p>
<a href="{{ url_for('main.logout') }}">退出</a>
{% else %}
<a href="{{ url_for('main.login') }}">登录</a>
{% endif %}
"""
九、装饰器与中间件
9.1 登录验证装饰器
# ==================== decorators.py ====================
from functools import wraps # 保留原函数的元信息
from flask import session, redirect, url_for, flash, request
def login_required(f):
"""
登录验证装饰器
功能:检查用户是否已登录,未登录则重定向到登录页
参数:f - 被装饰的视图函数
返回:包装后的函数
工作原理:
1. 用户访问被@login_required装饰的路由
2. wrapper函数先执行,检查session
3. 已登录→执行原视图函数;未登录→重定向
"""
@wraps(f) # 保留原函数的名称、文档字符串等信息
def wrapper(*args, **kwargs):
# *args接收位置参数,**kwargs接收关键字参数
# 检查session中是否有登录标记
if not session.get('logged_in'):
# 记录用户原本想访问的URL
# 用户登录后可以重定向回该页面
next_url = request.url
flash('请先登录', 'warning')
# 重定向到登录页,并带上next参数
return redirect(url_for('main.login', next=next_url))
# 已登录,执行原函数
return f(*args, **kwargs)
return wrapper
# ==================== 使用装饰器 ====================
@main.route('/publish')
@login_required # 装饰器放在路由装饰器下面
def publish_house():
"""发布房源——需要登录才能访问"""
# 到这里说明用户已登录
return render_template('publish.html')
9.2 请求钩子(before_request/after_request)
# ==================== 请求钩子 ====================
# before_request:在每次请求之前执行
# 可用于记录日志、权限检查、数据库连接等
@main.before_request
def before_request_func():
"""
请求前钩子
在每个请求到达视图函数之前执行
"""
# 记录请求信息
print(f'请求方法: {request.method}')
print(f'请求路径: {request.path}')
print(f'请求IP: {request.remote_addr}')
print(f'用户代理: {request.user_agent}')
# 示例:检查是否在维护模式
# 如果应用处于维护状态,所有请求返回维护页面
if app.config.get('MAINTENANCE_MODE'):
return render_template('maintenance.html'), 503
# after_request:在每次请求之后执行
# 可用于添加响应头、日志记录等
@main.after_request
def after_request_func(response):
"""
请求后钩子
在视图函数返回响应之后执行
参数:response - 视图函数返回的响应对象
返回:修改后的响应对象(必须返回response)
"""
# 添加安全相关的响应头
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
return response # 必须返回response
# teardown_request:在请求结束时执行(无论是否有错误)
# 用于资源清理
@main.teardown_request
def teardown_request_func(exception):
"""
请求结束钩子
即使发生异常也会执行
常用于关闭数据库连接等清理工作
"""
if exception:
print(f'请求发生异常: {exception}')
# 某些情况下可能需要回滚
# db.session.rollback()
十、综合案例——完整首页实现
# ==================== views.py 完整首页视图 ====================
from flask import Blueprint, render_template, request, jsonify, session
from models import db, House, Image
from sqlalchemy import func, or_
main = Blueprint('main', __name__)
@main.route('/')
def index():
"""
首页完整视图函数
包含:房源总数、最新房源、热点房源
"""
# ===== 1. 获取房源总数 =====
total_houses = House.query.count()
# ===== 2. 获取最新房源(按发布时间倒序,取6条) =====
latest_houses = House.query.order_by(
House.publish_time.desc()
).limit(6).all()
# ===== 3. 获取热点房源(按浏览量倒序,取6条) =====
hot_houses = House.query.order_by(
House.view_count.desc()
).limit(6).all()
# ===== 4. 获取统计数据 =====
# 查询最低租金
min_price = db.session.query(func.min(House.price)).scalar()
# 查询最高租金
max_price = db.session.query(func.max(House.price)).scalar()
# 查询户型列表(去重)
house_types = db.session.query(
House.house_type.distinct() # distinct()去重
).all()
# 将查询结果转为纯字符串列表
house_types = [t[0] for t in house_types if t[0]]
# ===== 5. 获取用户登录状态 =====
is_logged_in = session.get('logged_in', False)
username = session.get('username', '')
# ===== 6. 渲染模板并传递所有数据 =====
return render_template(
'index.html',
total_houses=total_houses, # 房源总数
latest_houses=latest_houses, # 最新房源列表
hot_houses=hot_houses, # 热点房源列表
min_price=min_price, # 最低租金
max_price=max_price, # 最高租金
house_types=house_types, # 户型列表
is_logged_in=is_logged_in, # 登录状态
username=username # 用户名
)
@main.route('/api/search')
def api_search():
"""智能搜索API"""
keyword = request.args.get('keyword', '').strip()
price_min = request.args.get('price_min', 0, type=float)
price_max = request.args.get('price_max', 0, type=float)
house_type = request.args.get('house_type', '')
sort_by = request.args.get('sort', 'newest')
page = request.args.get('page', 1, type=int)
try:
query = House.query
# 关键词模糊搜索
if keyword:
query = query.filter(or_(
House.title.like(f'%{keyword}%'),
House.address.like(f'%{keyword}%')
))
# 价格筛选
if price_min > 0:
query = query.filter(House.price >= price_min)
if price_max > 0:
query = query.filter(House.price <= price_max)
# 户型筛选
if house_type:
query = query.filter(House.house_type == house_type)
# 排序
sort_map = {
'newest': House.publish_time.desc(),
'price_asc': House.price.asc(),
'price_desc': House.price.desc(),
'popular': House.view_count.desc()
}
query = query.order_by(sort_map.get(sort_by, House.publish_time.desc()))
# 分页
pagination = query.paginate(page=page, per_page=10, error_out=False)
# 序列化
houses = [{
'id': h.id,
'title': h.title,
'price': h.price,
'area': h.area,
'house_type': h.house_type,
'address': h.address,
'view_count': h.view_count,
'image': h.images[0].image_url if h.images else None,
'publish_time': h.publish_time.strftime('%Y-%m-%d')
} for h in pagination.items]
return jsonify({
'code': 200,
'total': pagination.total,
'pages': pagination.pages,
'current_page': page,
'houses': houses
})
except Exception as e:
return jsonify({'code': 500, 'message': str(e)}), 500
本章知识点总结
| 知识分类 | 具体知识点 | 关键语法/方法 |
|---|---|---|
| Flask基础 | 路由定义、视图函数、蓝图 | @app.route()、Blueprint() |
| 模板引擎 | 变量渲染、循环、条件、过滤器 | {{ }}、{% for %}、{% if %}、| filter |
| 数据库ORM | 模型定义、CRUD操作、复杂查询 | db.Model、query.filter()、func.* |
| AJAX通信 | 前后端异步数据交互 | $.ajax()、XMLHttpRequest、jsonify() |
| 会话管理 | 用户登录状态维护 | session、flash() |
| 表单处理 | GET/POST请求、参数获取 | request.args、request.form |
| 装饰器 | 登录验证、权限控制 | @wraps、*args/**kwargs |
| 请求钩子 | 请求前后的处理逻辑 | before_request、after_request |
| 前端技术 | CSS布局、动画、JavaScript | Grid、Flexbox、@keyframes |
| 项目结构 | 应用工厂、配置分离 | create_app()、config.py |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)