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) }}">
                &laquo; 上一页
            </a>
        {% else %}
            <span class="disabled">&laquo; 上一页</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) }}">
                下一页 &raquo;
            </a>
        {% else %}
            <span class="disabled">下一页 &raquo;</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.Modelquery.filter()func.*
AJAX通信 前后端异步数据交互 $.ajax()XMLHttpRequestjsonify()
会话管理 用户登录状态维护 sessionflash()
表单处理 GET/POST请求、参数获取 request.argsrequest.form
装饰器 登录验证、权限控制 @wraps*args/**kwargs
请求钩子 请求前后的处理逻辑 before_requestafter_request
前端技术 CSS布局、动画、JavaScript Grid、Flexbox、@keyframes
项目结构 应用工厂、配置分离 create_app()config.py
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐