Files
nexus/Project/fonrey/UI_DESIGN/房源管理/房源列表_UI.md
2026-06-04 14:34:32 +08:00

68 KiB
Raw Blame History

房源列表 UI 设计文档

版本v1.0 · 日期2026-04-26
依赖规范UI_SYSTEM.md v1.2 · 组件规范设计.md v1.0
PRD 来源Project/fonrey/PRD/房源管理/房源管理模块PRD.md §5.1 房源列表
优先级P0 功能在本文档中用 🔴 标注P1 用 🟡P2 用


目录

  1. 模块概述
    • 1.1 功能范围
    • 1.2 页面清单
    • 1.3 用户角色与权限差异
  2. 页面设计规范
    • 2.1 房源列表主页
  3. Data Table 规范
    • 3.1 列定义
    • 3.2 列状态变体
    • 3.3 操作列
    • 3.4 表格交互状态
  4. 弹窗设计规范(列表页)
    • 4.1 自定义列弹窗
  5. 交互状态规范
    • 5.1 房源状态机
    • 5.2 权限控制矩阵
    • 5.3 HTMX 请求规范
  6. 关键数据字段说明
  7. 竞品截图对应关系
  8. 实现优先级与工期估算
  9. 开放问题(待决策)

1. 模块概述

1.1 功能范围

P0 功能MVP 必须实现)🔴

  • 房源列表展示(出售 / 出租 / 未挂牌 / 成交房源 / 全部房源 五个一级 Tab
  • 关键词搜索(房源编号、小区/学校名称、地址、业主主姓名、电话、钥匙编号等)
  • 楼栋 / 单元 / 房号独立精确输入
  • 多维度组合筛选(范围、区域、价格、面积、户型、楼层、标签、筛选、维护)
  • 列表数据展示(含交易类型标签、状态 Badge、价格趋势箭头
  • 批量操作(批量收藏、取消收藏、设置保护房、修改相关方、删除)
  • 分页与每页条数控制(每页 20 条,可跳页)
  • 新增房源主 CTA 按钮

P1 功能(第一迭代)🟡

  • 已存搜索条件保存与快速调用
  • 导出当前筛选结果Excel
  • 自定义列表显示字段
  • 智能排序(系统推荐)
  • 海报视图切换
  • 关注小区配置提示
  • 重复房源检测
  • 疑似问题号码房源查询

P2 功能(路线图)

  • 地图找房视图
  • 全部商铺列表 Tab
  • 全部写字楼列表 Tab

1.2 页面清单

页面名称 URL 模式建议 优先级 对应 PRD 章节
房源列表(出售) /properties/?tab=for_sale P0 🔴 §5.1, Story 2
房源列表(出租) /properties/?tab=for_rent P0 🔴 §5.1, Story 2
房源列表(未挂牌) /properties/?tab=unlisted P0 🔴 §5.1, Story 2
房源列表(成交房源) /properties/?tab=sold P0 🔴 §5.1, Story 2
房源列表(全部房源) /properties/ P0 🔴 §5.1, Story 2

1.3 用户角色与权限差异

差异点 经纪人 店长 管理员
默认数据范围 仅自己名下(seller_agent_id = me 本门店全部(org_unit_id IN my_stores 全司所有
「与我相关」快捷筛选 可用(默认可能已激活) 可用 可用
「我部门相关」快捷筛选 仅限自己所属部门 本门店 全司
批量删除 仅限自己名下房源 本门店房源 所有房源
修改相关方 不可操作他人房源 本门店范围 所有
重复房源检测链接 可见 可见 可见
「+ 新增房源」按钮 可见 可见 可见
导出按钮 仅自己数据 本门店数据 全量
业主电话 打码显示 打码显示(可申请查看) 明文

2. 页面设计规范

2.1 房源列表主页P0 🔴

2.1.1 页面概述

  • URL/properties/query paramstab, status, grade, cursor, q, sort, order 等;分页采用 Keyset详见 ADR-20260604-001
  • 访问入口:顶部全局导航栏「房源」菜单 → 默认进入全部房源列表
  • 页面职责:展示经纪人名下(或门店/全司)的房源列表,支持多维度搜索筛选、批量操作、状态快览
  • 竞品参考截图
    • Project/fonrey/screenshots/房源/房源列表.png(出售 Tab 视图,主参考)
    • Project/fonrey/screenshots/房源/全部房源.png(全部房源 Tab 视图)

2.1.2 布局结构

┌──────────────────────────────────────────────────────────────────────┐
│  一级 Tab 导航(出售 / 出租 / 未挂牌 / 成交房源 / 全部房源)              │
│  右侧:更多 ▾ | 重复房源 | 疑似问题号码房源 | + 新增房源 按钮             │
├──────────────────────────────────────────────────────────────────────┤
│  搜索区域(关键词搜索 + 楼栋/单元/房号精确输入 + 地图找房入口)             │
│  关注小区配置提示条(可关闭)                                             │
├──────────────────────────────────────────────────────────────────────┤
│  快捷筛选行(范围:最新挂牌 / 最新降价 / 与我相关 / 我部门相关 / 收藏房源 / 超时未跟进)│
│  区域筛选行(区域按钮组 + 地铁选项)                                      │
│  价格筛选行(售价/单价 + 预设区间 + 自定义区间)                           │
│  面积筛选行(预设区间 + 自定义区间)                                       │
│  房型筛选行(室数 + 卫生间数量)                                          │
│  楼层筛选行(低/中/高/顶/底 + 自定义区间)                                │
│  标签筛选行(速销/独家/有钥匙/电梯等)                                    │
│  筛选行(相关方/维护人/房屋现状/状态属性/装修朝向等下拉)                  │
│  维护行(发布/实勘/核验/跟进带看/钥匙委托/维护完成度)                     │
│  底部操作行(查询 / 重置 / 已存搜索条件 ▾ / 收起更多 ∧)                  │
├──────────────────────────────────────────────────────────────────────┤
│  工具栏(房源海报 | 批量操作按钮 | 更多 ▾ | 共N条 | 导出 | 自定义列表 | 智能排序)│
├──────────────────────────────────────────────────────────────────────┤
│  数据表格主体                                                           │
├──────────────────────────────────────────────────────────────────────┤
│  分页栏共N条 | 上一页 / 页码 / 下一页 / 每页20条 / 跳页)              │
└──────────────────────────────────────────────────────────────────────┘

整体页面背景:bg-neutral-50
主内容区外层容器:max-w-[1600px] mx-auto px-6 py-4
各区块背景:bg-white rounded-lg border border-neutral-200

2.1.3 区域详细规范


[一级 Tab 导航区]

属性 说明
组件 Tab Navigation§10 Tab Navigationunderline 变体
位置 页面最顶部,紧贴全局顶导下方,无额外卡片容器
Tab 项 出售(含数量 Badge/ 出租(含数量 Badge/ 未挂牌 / 成交房源 / 全部房源
激活样式 border-b-2 border-primary-600 text-primary-600 font-medium
非激活样式 text-neutral-500 hover:text-neutral-700
右侧内容 「更多 ▾」下拉菜单 + 「重复房源」链接 + 「疑似问题号码房源」链接 + 「+ 新增房源」主按钮

右侧操作区Tab 栏右侧,绝对定位)

<div class="flex items-center gap-3 text-sm">
  <!-- 更多下拉 -->
  <div x-data="{ open: false }" class="relative">
    <button @click="open = !open"
            class="flex items-center gap-1 text-sm text-neutral-600 
                   border border-neutral-300 rounded-lg px-3 py-1.5 
                   hover:bg-neutral-50 transition-colors">
      更多
      <svg class="w-4 h-4"><!-- heroicon: chevron-down --></svg>
    </button>
    <div x-show="open" x-cloak
         class="absolute right-0 top-full mt-1 w-40 bg-white shadow-lg 
                border border-neutral-200 rounded-lg z-50 py-1">
      <a href="#" class="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-50">
        海报视图
      </a>
    </div>
  </div>
  
  <!-- 重复房源P1 🟡) -->
  <a href="/properties/duplicates/"
     class="text-neutral-500 hover:text-neutral-700 text-sm">
    重复房源
  </a>
  
  <!-- 疑似问题号码P1 🟡) -->
  <a href="/properties/suspect-numbers/"
     class="text-neutral-500 hover:text-neutral-700 text-sm">
    疑似问题号码房源
  </a>
  
  <!-- 新增房源主 CTA -->
  <a href="/properties/create/"
     class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-primary-600 
            hover:bg-primary-700 text-white text-sm font-medium rounded-lg 
            transition-colors">
    <svg class="w-4 h-4"><!-- heroicon: plus --></svg>
    新增房源
  </a>
</div>

截图差异说明:竞品截图中「+ 新增房源」为橙色按钮Fonrey 使用主色 Tealbg-primary-600),品牌差异,无需对齐竞品色。


[搜索区域]

属性 说明
组件 搜索输入框组合
容器 bg-white rounded-lg border border-neutral-200 px-4 py-3 mt-3
关键词搜索框 宽约 flex-1 max-w-lg,占位符「房源编号/小区/学校名称/地址/业主姓名/电话/钥匙编号等」
楼栋输入框 标签「楼栋」+ 独立 Inputw-24,精确匹配 block_no
单元输入框 标签「单元」+ 独立 Inputw-24,精确匹配 unit_no
房号输入框 标签「房号」+ 独立 Input + Heroicon information-circle 提示,w-24,精确匹配 room_no
搜索按钮 bg-primary-600 text-white Heroicon magnifying-glassw-9 h-9 rounded-lg
地图找房P2 右侧浅色按钮「🗺 地图找房 →」P2 阶段置灰不可点击
<div class="bg-white rounded-lg border border-neutral-200 px-4 py-3 mt-3"
     x-data="{ showFilters: true, showMoreFilters: false }">
  
  <!-- 搜索行 -->
  <div class="flex items-center gap-2 flex-wrap">
    <!-- 关键词搜索框 -->
    <div class="relative flex-1 min-w-[280px] max-w-lg">
      <input type="search"
             name="q"
             placeholder="房源编号/小区/学校名称/地址/业主姓名/电话/钥匙编号等"
             class="w-full pl-3 pr-10 py-2 border border-neutral-300 rounded-lg 
                    text-sm focus-visible:outline-none focus-visible:ring-2 
                    focus-visible:ring-primary-600/40"
             hx-get="/properties/"
             hx-trigger="keyup changed delay:300ms, search"
             hx-target="#property-list-container"
             hx-swap="innerHTML"
             hx-include="[name='tab'],[name='status'],[name='page_size']">
    </div>
    
    <!-- 楼栋输入 -->
    <div class="flex items-center gap-1.5">
      <span class="text-sm text-neutral-500 whitespace-nowrap">楼栋</span>
      <input type="text" name="block_no" placeholder="请输入"
             class="w-20 px-2 py-2 border border-neutral-300 rounded-lg text-sm
                    focus-visible:ring-2 focus-visible:ring-primary-600/40">
    </div>
    
    <!-- 单元输入 -->
    <div class="flex items-center gap-1.5">
      <span class="text-sm text-neutral-500 whitespace-nowrap">单元</span>
      <input type="text" name="unit_no" placeholder="请输入"
             class="w-20 px-2 py-2 border border-neutral-300 rounded-lg text-sm
                    focus-visible:ring-2 focus-visible:ring-primary-600/40">
    </div>
    
    <!-- 房号输入 -->
    <div class="flex items-center gap-1.5">
      <span class="text-sm text-neutral-500 whitespace-nowrap">房号</span>
      <div class="relative">
        <input type="text" name="room_no" placeholder="请输入"
               class="w-20 px-2 pr-6 py-2 border border-neutral-300 rounded-lg text-sm
                      focus-visible:ring-2 focus-visible:ring-primary-600/40">
        <svg class="absolute right-1.5 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-400 cursor-pointer"
             title="支持精确匹配房间号码"><!-- heroicon: information-circle --></svg>
      </div>
    </div>
    
    <!-- 搜索按钮 -->
    <button type="submit"
            class="bg-primary-600 hover:bg-primary-700 text-white 
                   w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0
                   transition-colors">
      <svg class="w-4 h-4"><!-- heroicon: magnifying-glass --></svg>
    </button>
    
    <!-- 地图找房P2置灰 -->
    <button disabled
            class="ml-auto flex items-center gap-1.5 px-3 py-2 text-sm 
                   text-neutral-400 border border-neutral-200 rounded-lg 
                   cursor-not-allowed bg-neutral-50">
      <svg class="w-4 h-4"><!-- heroicon: map --></svg>
      地图找房
      <svg class="w-4 h-4"><!-- heroicon: arrow-right --></svg>
    </button>
  </div>
  
  <!-- 搜索历史提示(仅非首次使用时展示) -->
  <p class="mt-1.5 text-xs text-neutral-400">搜索历史在后台记录,请勿违规!</p>
  
  <!-- 关注小区配置提示条P1 🟡,可关闭) -->
  <div x-data="{ show: true }" x-show="show"
       class="mt-2 flex items-center gap-2 px-3 py-2 bg-warning-50 
              border border-warning-200 rounded-lg text-sm">
    <svg class="w-4 h-4 text-warning-600 flex-shrink-0"><!-- heroicon: information-circle --></svg>
    <span class="text-warning-700">
      <a href="/settings/watch-complexes/" 
         class="font-medium text-warning-700 hover:underline">配置关注小区</a>
      (关注小区后,当该小区产生对应交易类型下的新上房源、降价房源时,系统将第一时间通知您,提升您的作业效率哦!)
    </span>
    <button @click="show = false" class="ml-auto text-neutral-400 hover:text-neutral-600">
      <svg class="w-4 h-4"><!-- heroicon: x-mark --></svg>
    </button>
  </div>
  
  <!-- 筛选区(可折叠) -->
  <div x-show="showFilters"
       x-transition:enter="transition ease-out duration-150"
       x-transition:enter-start="opacity-0 -translate-y-2"
       x-transition:enter-end="opacity-100 translate-y-0"
       class="mt-3 space-y-2">
    <!-- 各筛选行,见下方 -->
  </div>
</div>

[筛选区 - 详细规范]

筛选区按行组织,每行格式:[标签(w-8)] [选项组],标签使用 text-xs text-neutral-400 whitespace-nowrap w-8

范围筛选行

筛选项 类型 说明
最新挂牌 Checkbox 筛选最近挂牌房源(listed_at 降序默认)
最新降价 Checkbox 最近有调价记录且降价
与我相关 Checkbox + ⓘ seller_agent_id = me OR first_recorder_id = me
我部门相关 Checkbox + ⓘ 本门店员工相关房源
收藏房源 下拉 ▾ 展开选择收藏集合(来自 property_favorites
超时未跟进房源 Checkbox last_followed_at < NOW() - 7d(具体阈值待产品确认)
<div class="flex items-center gap-4 text-sm flex-wrap">
  <span class="text-neutral-400 text-xs w-8 shrink-0">范围</span>
  
  <label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600">
    <input type="checkbox" name="scope_latest_listed" value="1"
           class="w-4 h-4 rounded accent-primary-600"
           hx-get="/properties/" hx-trigger="change"
           hx-target="#property-list-container" hx-swap="innerHTML"
           hx-include="closest form">
    最新挂牌
  </label>
  
  <label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600">
    <input type="checkbox" name="scope_latest_reduced" value="1"
           class="w-4 h-4 rounded accent-primary-600"
           hx-get="/properties/" hx-trigger="change"
           hx-target="#property-list-container" hx-swap="innerHTML"
           hx-include="closest form">
    最新降价
  </label>
  
  <label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600">
    <input type="checkbox" name="related_to_me" value="1"
           class="w-4 h-4 rounded accent-primary-600"
           hx-get="/properties/" hx-trigger="change"
           hx-target="#property-list-container" hx-swap="innerHTML"
           hx-include="closest form">
    与我相关
    <svg class="w-3.5 h-3.5 text-neutral-400"><!-- heroicon: information-circle --></svg>
  </label>
  
  <label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600">
    <input type="checkbox" name="my_dept" value="1"
           class="w-4 h-4 rounded accent-primary-600"
           hx-get="/properties/" hx-trigger="change"
           hx-target="#property-list-container" hx-swap="innerHTML"
           hx-include="closest form">
    我部门相关
    <svg class="w-3.5 h-3.5 text-neutral-400"><!-- heroicon: information-circle --></svg>
  </label>
  
  <!-- 收藏房源下拉 -->
  <div x-data="{ open: false }" class="relative">
    <button @click="open = !open"
            class="flex items-center gap-1 text-sm text-neutral-600 hover:text-primary-600">
      收藏房源
      <svg class="w-3 h-3"><!-- heroicon: chevron-down --></svg>
    </button>
    <div x-show="open" x-cloak
         class="absolute left-0 top-full mt-1 w-40 bg-white shadow-lg 
                border border-neutral-200 rounded-lg z-50 py-1">
      <label class="flex items-center gap-2 px-4 py-2 text-sm text-neutral-600 hover:bg-neutral-50 cursor-pointer">
        <input type="checkbox" name="favorited" value="1" class="w-4 h-4 rounded accent-primary-600">
        我的收藏
      </label>
    </div>
  </div>
  
  <label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600">
    <input type="checkbox" name="overdue_follow" value="1"
           class="w-4 h-4 rounded accent-primary-600"
           hx-get="/properties/" hx-trigger="change"
           hx-target="#property-list-container" hx-swap="innerHTML"
           hx-include="closest form">
    超时未跟进房源
  </label>
</div>

区域筛选行

筛选项 类型 说明
地区(行政区) Tag 按钮多选 宝山/嘉定/静安/闵行/普陀/松江/长宁等,多选,来自 complexes.district
地铁 下拉 ▾ 选择地铁线 + 站点(二级联动),来自楼盘数据
<div class="flex items-start gap-2 text-sm flex-wrap">
  <span class="text-neutral-400 text-xs w-8 shrink-0 mt-1">区域</span>
  <div class="flex items-center gap-1.5 flex-wrap flex-1">
    <!-- 地区 Tag 多选 -->
    {% for district in districts %}
    <button type="button"
            :class="selectedDistricts.includes('{{ district.value }}') 
              ? 'bg-primary-600 text-white border-primary-600' 
              : 'text-neutral-600 border-neutral-200 hover:border-primary-400 hover:text-primary-600'"
            class="px-3 py-1 text-xs border rounded-md transition-colors"
            @click="toggleDistrict('{{ district.value }}')">
      {{ district.label }}
    </button>
    {% endfor %}
    <!-- 地铁 -->
    <div class="relative ml-2" x-data="{ open: false }">
      <button @click="open = !open"
              class="flex items-center gap-1 px-3 py-1 text-xs border border-neutral-200 
                     rounded-md text-neutral-600 hover:border-primary-400 hover:text-primary-600">
        地铁
        <svg class="w-3 h-3"><!-- heroicon: chevron-down --></svg>
      </button>
      <!-- 地铁线 + 站点联动面板 -->
    </div>
  </div>
</div>

价格筛选行

筛选项 类型 说明
售价/单价 切换 单选 Tab 出售 Tab 默认显示;出租 Tab 替换为「租价」
价格预设区间 Tag 单选 200万以下 / 250-300万 / 300-400万 / 400-500万 / 500-700万 / 700-1000万 / 1000-1500万 / 1500-2000万 / 2000万以上
自定义区间 数字输入框 × 2 + 单位 最小值 ~ 最大值 万元
收起/展开 文字链接 Alpine.js 控制显示部分预设区间
<div class="flex items-start gap-2 text-sm flex-wrap">
  <span class="text-neutral-400 text-xs w-8 shrink-0 mt-1">价格</span>
  <div class="flex-1 space-y-1.5">
    <!-- 售价/单价 切换(仅出售 Tab 显示) -->
    <div class="flex items-center gap-1 mb-1"
         x-show="activeTab !== 'for_rent'">
      <button :class="priceMode === 'sale' ? 'text-primary-600 font-medium' : 'text-neutral-500'"
              class="text-xs hover:text-primary-600 transition-colors"
              @click="priceMode = 'sale'">售价</button>
      <span class="text-neutral-300">|</span>
      <button :class="priceMode === 'unit' ? 'text-primary-600 font-medium' : 'text-neutral-500'"
              class="text-xs hover:text-primary-600 transition-colors"
              @click="priceMode = 'unit'">单价</button>
    </div>
    
    <!-- 预设区间 Tag -->
    <div class="flex items-center gap-1.5 flex-wrap">
      <button class="px-3 py-1 text-xs bg-primary-600 text-white rounded-md border border-primary-600">
        不限
      </button>
      <button class="px-3 py-1 text-xs border border-neutral-200 rounded-md text-neutral-600 
                     hover:border-primary-400 hover:text-primary-600 transition-colors">
        200万以下
      </button>
      <button class="px-3 py-1 text-xs border border-neutral-200 rounded-md text-neutral-600 
                     hover:border-primary-400 hover:text-primary-600 transition-colors">
        250-300万
      </button>
      <!-- ... 其他预设区间 ... -->
      <button class="text-xs text-primary-600 hover:underline">收起 ∧</button>
    </div>
    
    <!-- 自定义区间 -->
    <div class="flex items-center gap-1.5">
      <input type="number" name="price_min" placeholder="最小值"
             class="w-20 px-2 py-1 text-xs border border-neutral-300 rounded-md 
                    focus-visible:ring-2 focus-visible:ring-primary-600/40">
      <span class="text-neutral-400">~</span>
      <input type="number" name="price_max" placeholder="最大值"
             class="w-20 px-2 py-1 text-xs border border-neutral-300 rounded-md 
                    focus-visible:ring-2 focus-visible:ring-primary-600/40">
      <span class="text-xs text-neutral-500">万元</span>
    </div>
  </div>
</div>

面积筛选行

筛选项 类型 说明
面积预设区间 Tag 单选 50m²以下 / 50-70m² / 70-90m² / 90-110m² / 110-130m² / 130-150m² / 150m²以上
自定义区间 数字输入框 × 2 + 单位「m²」 精确区间输入

房型筛选行

筛选项 类型 说明
室数 Tag 多选 1室 / 2室 / 3室 / 4室 / 5室及以上对应 bedroom_count
卫生间 Tag 多选 1卫 / 2卫 / 3卫 / 4卫对应 bathroom_count
收起/展开 文字链接 控制卫生间行显示

楼层筛选行

筛选项 类型 说明
楼层段 Tag 多选 低层1-3层 / 中层4-9层 / 高层10层+ / 顶楼 / 底楼
自定义区间 数字输入框 × 2 + 单位「层」 精确楼层范围

标签筛选行

筛选项 类型 说明
标签 Checkbox 多选 速销、独家、有钥匙、电梯、唯一、有照片、贷款、视频、AI视频、有VR、3D
一键装换 Checkbox 快捷筛选
一般委托 Checkbox 快捷筛选

筛选行(展开更多)

下拉筛选项,使用 <select> 或 Multi-select每项触发 hx-get="/properties/":

筛选项 组件类型 对应字段
相关方 Multi-select人员选择器 seller_agent_id / first_recorder_id
维护人 Multi-select人员选择器
房屋现状 下拉多选 house_status
状态/属性 下拉多选 status / attribute
装修/朝向 下拉多选 decoration / orientation
学校 下拉多选
等级 Tag 多选 gradeA急迫/B较强/C一般
用途 下拉多选 usage_type
房本年限 下拉多选 ownership_years
唯一住房 单选 is_only_house
税费/贷款 下拉多选 tax_included / has_loan
建成年代 区间输入 built_year
产权性质 下拉多选 ownership_nature
挂牌类型 下拉多选 listing_type(来源)
来源 下拉多选 source
看房时间 下拉多选 viewing_time
审核 下拉单选 审核状态
保护房 Checkbox property_protections

维护行

筛选项 组件类型 说明
发布 下拉 ▾ 发布状态(已发布/未发布)
实勘 下拉 ▾ 有/无实勘记录
核验 下拉 ▾ 核验状态
跟进/带看 下拉 ▾ 最近跟进时间段筛选
钥匙/委托 下拉 ▾ 有/无钥匙,有/无委托
维护完成度 下拉 ▾ 完成度区间筛选(completeness_score

底部操作行

<div class="flex items-center gap-3 mt-3 pt-3 border-t border-neutral-100">
  <!-- 查询按钮 -->
  <button type="submit"
          class="px-5 py-2 bg-primary-600 hover:bg-primary-700 text-white 
                 text-sm font-medium rounded-lg transition-colors"
          hx-get="/properties/"
          hx-target="#property-list-container"
          hx-swap="innerHTML"
          hx-include="closest form">
    查询
  </button>
  
  <!-- 重置 -->
  <button type="reset"
          class="px-5 py-2 text-sm text-neutral-600 border border-neutral-300 
                 rounded-lg hover:bg-neutral-50 transition-colors"
          hx-get="/properties/"
          hx-target="#property-list-container"
          hx-swap="innerHTML">
    重置
  </button>
  
  <!-- 已存搜索条件P1 🟡) -->
  <div x-data="{ open: false }" class="relative">
    <button @click="open = !open"
            class="flex items-center gap-1 text-sm text-neutral-500 hover:text-neutral-700">
      <svg class="w-4 h-4"><!-- heroicon: bookmark --></svg>
      已存搜索条件
      <svg class="w-3 h-3"><!-- heroicon: chevron-down --></svg>
    </button>
    <!-- 已存搜索下拉列表 -->
  </div>
  
  <!-- 收起更多 -->
  <button @click="showFilters = !showFilters"
          class="ml-auto text-sm text-neutral-500 hover:text-primary-600 
                 flex items-center gap-1">
    <span x-text="showFilters ? '收起更多 ∧' : '展开更多 '"></span>
  </button>
</div>

[工具栏区]

属性 说明
组件 Toolbar§4 Toolbar
容器 flex items-center gap-2 px-4 py-2.5 bg-white border-b border-neutral-100 mt-3 rounded-t-lg
左侧 「房源海报」视图切换按钮 + 批量操作按钮组(勾选时激活)+ 更多 ▾ + 总条数文字
右侧 导出按钮 + 自定义列表按钮 + 智能排序按钮P1 🟡
<div class="flex items-center gap-2 px-4 py-2.5 bg-white border border-neutral-200 
            rounded-t-lg mt-3"
     x-data="{ selectedCount: 0 }">
  
  <!-- 房源海报切换P1 🟡) -->
  <button class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-neutral-600 
                 border border-neutral-300 rounded-md hover:bg-neutral-50">
    <svg class="w-4 h-4"><!-- heroicon: squares-2x2 --></svg>
    房源海报
  </button>
  
  <!-- 批量操作(勾选 ≥1 条后激活) -->
  <button :disabled="selectedCount === 0"
          :class="selectedCount > 0 
            ? 'text-neutral-700 hover:bg-neutral-100 border-neutral-300 cursor-pointer' 
            : 'text-neutral-300 border-neutral-200 cursor-not-allowed'"
          class="px-3 py-1.5 text-sm border rounded-md transition-colors"
          @click="selectedCount > 0 && $dispatch('open-batch-favorite-modal')">
    批量收藏
  </button>
  
  <button :disabled="selectedCount === 0"
          :class="selectedCount > 0 
            ? 'text-neutral-700 hover:bg-neutral-100 border-neutral-300 cursor-pointer' 
            : 'text-neutral-300 border-neutral-200 cursor-not-allowed'"
          class="px-3 py-1.5 text-sm border rounded-md transition-colors">
    取消收藏
  </button>
  
  <button :disabled="selectedCount === 0"
          :class="selectedCount > 0 
            ? 'text-neutral-700 hover:bg-neutral-100 border-neutral-300 cursor-pointer' 
            : 'text-neutral-300 border-neutral-200 cursor-not-allowed'"
          class="px-3 py-1.5 text-sm border rounded-md transition-colors">
    设置保护房
  </button>
  
  <button :disabled="selectedCount === 0"
          :class="selectedCount > 0 
            ? 'text-neutral-700 hover:bg-neutral-100 border-neutral-300 cursor-pointer' 
            : 'text-neutral-300 border-neutral-200 cursor-not-allowed'"
          class="px-3 py-1.5 text-sm border rounded-md transition-colors">
    修改相关方
  </button>
  
  <!-- 更多批量操作 ▾ -->
  <div x-data="{ open: false }" class="relative">
    <button @click="open = !open"
            :disabled="selectedCount === 0"
            class="px-3 py-1.5 text-sm border border-neutral-300 rounded-md 
                   text-neutral-700 hover:bg-neutral-100 flex items-center gap-1">
      更多
      <svg class="w-3 h-3"><!-- heroicon: chevron-down --></svg>
    </button>
    <div x-show="open" x-cloak
         class="absolute left-0 top-full mt-1 w-32 bg-white shadow-lg 
                border border-neutral-200 rounded-lg z-50 py-1">
      <button class="w-full text-left px-4 py-2 text-sm text-danger-600 hover:bg-danger-50">
        删除
      </button>
    </div>
  </div>
  
  <!-- 总条数 -->
  <span class="text-sm text-neutral-500 ml-1"><strong class="text-neutral-800">{{ total_count }}</strong></span>
  
  <!-- 已选提示 -->
  <span x-show="selectedCount > 0" class="text-sm text-primary-600 ml-1">
    已选 <span x-text="selectedCount"></span></span>
  
  <!-- 右侧工具 -->
  <div class="flex items-center gap-2 ml-auto">
    <!-- 导出P1 🟡) -->
    <button hx-post="/properties/export/"
            hx-trigger="click"
            hx-vals='{"format": "excel"}'
            class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-neutral-600 
                   border border-neutral-300 rounded-md hover:bg-neutral-50">
      <svg class="w-4 h-4"><!-- heroicon: arrow-down-tray --></svg>
      导出
    </button>
    
    <!-- 自定义列表P1 🟡) -->
    <button @click="$dispatch('open-column-settings')"
            class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-neutral-600 
                   border border-neutral-300 rounded-md hover:bg-neutral-50">
      <svg class="w-4 h-4"><!-- heroicon: adjustments-horizontal --></svg>
      自定义列表
    </button>
    
    <!-- 智能排序P1 🟡) -->
    <button class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-neutral-600 
                   border border-neutral-300 rounded-md hover:bg-neutral-50">
      <svg class="w-4 h-4"><!-- heroicon: sparkles --></svg>
      智能排序
    </button>
  </div>
</div>

[数据表格]

属性 说明
组件 Data Table§1 Data Table
容器 id property-list-containerHTMX swap 目标),内嵌 property-table-body
行高 56pxh-14),含多行标签时允许 auto 高度,最小 56px
横向滚动 overflow-x-auto 包裹 <table>,宽屏 ≥1280px 尽量全列展示

详细列规范见 §3.1。


[分页栏]

属性 说明
组件 Pagination§2 Pagination
位置 表格下方 mt-0 flex items-center justify-between px-4 py-3 border-t border-neutral-100
左侧 总条数「共 N 条」
中间 页码导航:← 上一页 [1] [2] [3] [4] [5] … [N] 下一页 →
右侧 每页条数「20条/页 ▾」选项20/50/100+ 跳页「跳至」+ 输入框 + 「页」
HTMX hx-get="/properties/" hx-vals='{"cursor": LAST_ID}' hx-target="#property-list-container" hx-swap="innerHTML" hx-include="closest form"Keyset 分页,LAST_ID 为当前页最后一条 id;详见 ADR-20260604-001。MVP 过渡期允许保留页码 UI 表现,但 cursor 是后端主参数)
当前页 bg-primary-600 text-white rounded-md w-8 h-8 font-medium
非当前页 text-neutral-600 hover:bg-neutral-100 rounded-md w-8 h-8

2.1.4 使用的特殊组件

组件名 来源(§章节) 用途 自定义说明
Data Table §1 Data Table 房源数据主体展示 房源名称列含交易类型 Badge + 速卖标签,行高 56px
Pagination §2 Pagination 底部分页控件 含跳页输入框,与标准实现一致
Column Visibility Panel §3 Column Visibility Panel 自定义列表字段选择P1 触发按钮为「自定义列表」文字 + 图标
Toolbar §4 Toolbar 批量操作 + 导出 + 统计 批量按钮默认 disabledselectedCount > 0 激活
Export Button §5 Export Button 导出 ExcelP1 HTMX hx-post 异步触发 Celery 任务
Tab Navigation §10 Tab Navigation 一级 Tab出售/出租等)切换 Underline 变体,右侧含操作区
Date Range Picker §9 Date Range Picker 挂牌日期、跟进时间筛选 集成 Flatpickrrange 模式
Multi-select Tag Input §17 Multi-select Tag Input 相关方、学校等多选筛选 展开更多筛选行内使用
Modal Dialog §7 Modal Dialog 自定义列弹窗 见 §4.1 规范

2.1.5 空状态设计

筛选无结果(最常见场景)

<div class="flex flex-col items-center justify-center py-20 text-center">
  <svg class="w-12 h-12 text-neutral-300 mb-4"><!-- heroicon: home --></svg>
  <p class="text-neutral-500 text-sm font-medium">暂无房源</p>
  <p class="text-neutral-400 text-xs mt-1">当前筛选条件下没有房源,尝试调整筛选条件</p>
  <button class="mt-4 text-sm text-primary-600 hover:underline"
          hx-get="/properties/"
          hx-target="#property-list-container"
          hx-swap="innerHTML">
    清空筛选条件
  </button>
</div>

首次进入无数据(无任何房源)

<div class="flex flex-col items-center justify-center py-24 text-center">
  <svg class="w-16 h-16 text-neutral-200 mb-4"><!-- heroicon: building-office --></svg>
  <p class="text-neutral-600 text-base font-medium">还没有房源</p>
  <p class="text-neutral-400 text-sm mt-1.5">开始录入第一套房源</p>
  <a href="/properties/create/"
     class="mt-5 inline-flex items-center gap-1.5 px-4 py-2 
            bg-primary-600 hover:bg-primary-700 text-white text-sm 
            font-medium rounded-lg transition-colors">
    <svg class="w-4 h-4"><!-- heroicon: plus --></svg>
    新增房源
  </a>
</div>

2.1.6 Loading 状态

筛选/分页触发 HTMX 请求期间#property-list-container 内显示骨架屏:

<!-- 骨架屏8 行占位 -->
<div class="htmx-indicator animate-pulse space-y-0 
            rounded-b-lg border border-t-0 border-neutral-200 overflow-hidden bg-white">
  {% for i in "12345678" %}
  <div class="flex items-center gap-4 px-4 h-14 border-b border-neutral-100 last:border-0">
    <div class="w-4 h-4 bg-neutral-200 rounded flex-shrink-0"></div>
    <div class="h-4 bg-neutral-200 rounded w-40"></div>
    <div class="h-4 bg-neutral-200 rounded w-8 ml-2"></div>
    <div class="h-4 bg-neutral-200 rounded w-8 ml-2"></div>
    <div class="h-4 bg-neutral-200 rounded w-20 ml-4"></div>
    <div class="h-4 bg-neutral-200 rounded w-16 ml-2"></div>
    <div class="h-4 bg-neutral-200 rounded w-16 ml-2"></div>
    <div class="h-4 bg-neutral-200 rounded w-12 ml-2"></div>
    <div class="h-4 bg-neutral-200 rounded w-12 ml-2"></div>
    <div class="ml-auto h-4 bg-neutral-200 rounded w-24"></div>
  </div>
  {% endfor %}
</div>

3. Data Table 规范

引用基础规范:组件规范设计.md §1 Data Table
本章描述房源列表页 Data Table 的具体实例化配置

3.1 列定义(全部房源视图,默认显示列)

表格容器:<table class="min-w-full divide-y divide-neutral-200">
表头行:<thead class="bg-neutral-50">,列头单元格统一样式:px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap
数据行:<tr class="hover:bg-neutral-50 transition-colors" style="height:56px">

# 列名 数据字段 列宽 对齐 可排序 特殊渲染说明
1 (复选框) w-1040pxpx-4 居中 全选/单选 <input type="checkbox">Alpine.js selected[] 数组
2 房源名称 complex_name + block_no + unit_no + room_no + 交易类型标签 + 速卖/电梯等标签 min-w-[200px] max-w-[280px] 左对齐 蓝色链接跳详情;名称行下方渲染标签组(见 §3.2「切换展示」链接P1 🟡
3 楼栋 block_no w-1664px 左对齐 纯文字;无数据显示 -
4 单元 unit_no w-1664px 左对齐 纯文字;无数据显示 -
5 房号 room_no w-1664px 左对齐 纯文字;无数据显示 -
6 区域板块 district + business_area min-w-[100px] 左对齐 格式:嘉定 丰庄,两行或空格分隔
7 状态 status w-1664px 左对齐 Status Badge见 §3.2
8 售价(万) sale_price w-2496px 右对齐 价格数字 + 价格变动趋势箭头(见 §3.2);出租 Tab 此列隐藏
9 单价(元/m² sale_unit_price w-28112px 右对齐 数字,格式化千分位;出租 Tab 此列隐藏
10 租价(元/月) rent_price w-28112px 右对齐 数字,格式化千分位;出售 Tab 此列隐藏
11 面积 area w-2080px 右对齐 保留1位小数81.3
12 户型 bedroom_count + living_room_count w-1664px 左对齐 格式:3/1/1(室/厅/卫);简显为 X室X厅
13 楼层 floor + total_floors w-1664px 左对齐 格式:4/6;无数据显示 -
14 朝向 orientation w-1248px 左对齐 中文显示:南北/东南等;无数据显示 -
15 挂牌日期 listed_at w-28112px 左对齐 格式:YYYY-MM-DD
16 房源最后跟进日 last_followed_at w-28112px 左对齐 (默认降序) 格式:YYYY-MM-DD;超过 30 天字色 text-danger-600

全部房源 Tab出售/出租 Tab 的差异:全部房源 Tab 同时显示「售价」和「租价」列,部分行会有一列为 -,属正常显示。

whitespace-nowrap 要求:以下列的数据单元格(<td>)必须加 whitespace-nowrap,防止内容折行:状态、售价、单价、租价、户型、楼层、朝向、挂牌日期、最后跟进日期。

自定义列P1 🟡用户通过「自定义列表」弹窗§4.1)选择显示字段,可选字段包括:单价、等级、属性、装修、建成年代、来源、相关方、维护完成度等。

表格 HTML 结构(关键片段)

<div id="property-list-container">
  <div class="rounded-b-lg border border-t-0 border-neutral-200 overflow-hidden bg-white">
    <div class="overflow-x-auto">
      <table class="min-w-full divide-y divide-neutral-200">
        <thead class="bg-neutral-50">
          <tr>
            <th class="w-10 px-4 py-3">
              <input type="checkbox" id="select-all"
                     class="w-4 h-4 rounded accent-primary-600"
                     @change="toggleAll($event)">
            </th>
            <th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 
                       uppercase tracking-wide whitespace-nowrap min-w-[200px]">
              房源名称
            </th>
            <th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 
                       uppercase tracking-wide whitespace-nowrap w-16">
              楼栋
            </th>
            <th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 
                       uppercase tracking-wide whitespace-nowrap w-16">
              单元
            </th>
            <th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 
                       uppercase tracking-wide whitespace-nowrap w-16">
              房号
            </th>
            <th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 
                       uppercase tracking-wide whitespace-nowrap min-w-[100px]">
              区域板块
            </th>
            <th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 
                       uppercase tracking-wide whitespace-nowrap w-16">
              状态
            </th>
            <!-- 售价列(含排序,出租 Tab 隐藏) -->
            <th class="px-4 py-3 text-right text-xs font-semibold text-neutral-500 
                       uppercase tracking-wide whitespace-nowrap w-24 cursor-pointer 
                       hover:bg-neutral-100 select-none"
                x-show="activeTab !== 'for_rent'"
                hx-get="/properties/"
                hx-vals='{"sort": "sale_price", "order": "{{ sort_order_toggle }}"}'
                hx-target="#property-list-container"
                hx-swap="innerHTML"
                hx-include="closest form">
              售价(万)
              <svg class="inline w-4 h-4 text-neutral-300 ml-0.5"><!-- heroicon: chevron-up-down --></svg>
            </th>
            <!-- 其他列头... -->
            <th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 
                       uppercase tracking-wide whitespace-nowrap w-28 cursor-pointer 
                       hover:bg-neutral-100 select-none"
                hx-get="/properties/"
                hx-vals='{"sort": "last_followed_at", "order": "{{ sort_order_toggle }}"}'
                hx-target="#property-list-container"
                hx-swap="innerHTML"
                hx-include="closest form">
              房源最后跟进日
              <svg class="inline w-4 h-4 text-primary-600 ml-0.5"><!-- 当前排序列chevron-down --></svg>
            </th>
          </tr>
        </thead>
        <tbody id="property-table-body" class="divide-y divide-neutral-100 bg-white">
          {% for prop in properties %}
          <tr class="hover:bg-neutral-50 transition-colors"
              style="height: 56px"
              :class="selected.includes('{{ prop.id }}') ? 'bg-primary-50 hover:bg-primary-100' : ''">
            <!-- 复选框 -->
            <td class="w-10 px-4">
              <input type="checkbox" :value="'{{ prop.id }}'"
                     class="w-4 h-4 rounded accent-primary-600"
                     x-model="selected">
            </td>
            <!-- 房源名称 -->
            <td class="px-4 py-2 min-w-[200px]">
              <div class="flex flex-col gap-0.5">
                <!-- 交易类型标签 + 名称 -->
                <div class="flex items-center gap-1.5 flex-wrap">
                  <!-- 交易类型 Badge -->
                  <span class="{{ prop.transaction_type_class }} text-[10px] px-1.5 py-0.5 rounded font-medium flex-shrink-0">
                    {{ prop.transaction_type_display }}
                  </span>
                  <!-- 房源名称链接 -->
                  <a href="/properties/{{ prop.id }}/"
                     class="text-info-600 hover:underline font-medium text-sm truncate max-w-[180px]">
                    {{ prop.complex_name }}{{ prop.room_display }}
                  </a>
                </div>
                <!-- 速卖/电梯/视频等标签行 -->
                <div class="flex items-center gap-1 flex-wrap">
                  {% for tag in prop.display_tags %}
                  <span class="text-[10px] px-1.5 py-0.5 rounded-sm {{ tag.style_class }}">
                    {{ tag.name }}
                  </span>
                  {% endfor %}
                  <!-- 满五等特殊标签 -->
                  {% if prop.ownership_label %}
                  <span class="text-[10px] px-1.5 py-0.5 text-success-600 bg-success-50 rounded-sm font-medium">
                    {{ prop.ownership_label }}
                  </span>
                  {% endif %}
                </div>
              </div>
            </td>
            <!-- 楼栋 -->
            <td class="px-4 py-2 text-sm text-neutral-700 w-16">{{ prop.block_no|default:"-" }}</td>
            <!-- 单元 -->
            <td class="px-4 py-2 text-sm text-neutral-700 w-16">{{ prop.unit_no|default:"-" }}</td>
            <!-- 房号 -->
            <td class="px-4 py-2 text-sm text-neutral-700 w-16">{{ prop.room_no|default:"-" }}</td>
            <!-- 区域板块 -->
            <td class="px-4 py-2 text-sm text-neutral-700 min-w-[100px]">
              <div class="flex flex-col">
                <span>{{ prop.district }}</span>
                <span class="text-neutral-400 text-xs">{{ prop.business_area }}</span>
              </div>
            </td>
            <!-- 状态 -->
            <td class="px-4 py-2 w-16">
              <span class="text-xs px-2 py-0.5 rounded-full font-medium {{ prop.status_badge_class }}">
                {{ prop.status_display }}
              </span>
            </td>
            <!-- 售价(含价格趋势箭头) -->
            <td class="px-4 py-2 text-right w-24" x-show="activeTab !== 'for_rent'">
              {% if prop.sale_price %}
              <div class="flex flex-col items-end gap-0.5">
                <span class="text-sm font-medium text-neutral-800">{{ prop.sale_price }}</span>
                {% if prop.price_change_direction == 'down' %}
                <span class="text-[10px] text-danger-600">
                  ↓ {{ prop.price_change_display }}
                </span>
                {% elif prop.price_change_direction == 'up' %}
                <span class="text-[10px] text-success-600">
                  ↑ {{ prop.price_change_display }}
                </span>
                {% endif %}
              </div>
              {% else %}-{% endif %}
            </td>
            <!-- 单价 -->
            <td class="px-4 py-2 text-right w-28" x-show="activeTab !== 'for_rent'">
              <span class="text-sm text-neutral-700">{{ prop.sale_unit_price|default:"-" }}</span>
            </td>
            <!-- 租价 -->
            <td class="px-4 py-2 text-right w-28" x-show="activeTab !== 'for_sale'">
              <span class="text-sm text-neutral-700">
                {% if prop.rent_price %}{{ prop.rent_price }}{% else %}-{% endif %}
              </span>
            </td>
            <!-- 面积 -->
            <td class="px-4 py-2 text-right w-20">
              <span class="text-sm text-neutral-700">{{ prop.area }}</span>
            </td>
            <!-- 户型 -->
            <td class="px-4 py-2 w-16">
              <span class="text-sm text-neutral-700">{{ prop.layout_display }}</span>
            </td>
            <!-- 楼层 -->
            <td class="px-4 py-2 w-16">
              <span class="text-sm text-neutral-700">
                {% if prop.floor %}{{ prop.floor }}/{{ prop.total_floors }}{% else %}-{% endif %}
              </span>
            </td>
            <!-- 朝向 -->
            <td class="px-4 py-2 w-12">
              <span class="text-sm text-neutral-700">{{ prop.orientation_display|default:"-" }}</span>
            </td>
            <!-- 挂牌日期 -->
            <td class="px-4 py-2 w-28">
              <span class="text-sm text-neutral-700">{{ prop.listed_at|date:"Y-m-d"|default:"-" }}</span>
            </td>
            <!-- 最后跟进日 -->
            <td class="px-4 py-2 w-28">
              <span class="text-sm {% if prop.follow_overdue %}text-danger-600{% else %}text-neutral-700{% endif %}">
                {{ prop.last_followed_at|date:"Y-m-d"|default:"-" }}
              </span>
            </td>
          </tr>
          {% endfor %}
        </tbody>
      </table>
    </div>
  </div>
</div>

3.2 列状态变体

交易类型标签(房源名称列内,蓝/橙色 Badge

property_type + status 场景 显示文字 样式
出售中(for_sale 买卖 bg-danger-600 text-white text-[10px] px-1.5 py-0.5 rounded
出租中(for_rent 租赁 bg-info-600 text-white text-[10px] px-1.5 py-0.5 rounded
出售+出租(for_sale_rent 租售 同时展示「买卖」和「租赁」两个 Badge

房源附加标签(名称行下方标签组)

标签类型 来源 样式
满五 ownership_years 含「满五」 bg-success-50 text-success-600 text-[10px] px-1.5 py-0.5 rounded-sm font-medium
电梯 has_elevator = true 且有电梯 bg-neutral-100 text-neutral-600 text-[10px] px-1.5 py-0.5 rounded-sm
视频 property_tags 含视频标签 bg-neutral-100 text-neutral-600 text-[10px] px-1.5 py-0.5 rounded-sm
私(私盘) attribute = 'private' bg-warning-50 text-warning-700 text-[10px] px-1.5 py-0.5 rounded-sm font-medium
速销 property_tags 含速销标签 bg-danger-50 text-danger-600 text-[10px] px-1.5 py-0.5 rounded-sm
独家 property_tags 含独家标签 bg-primary-50 text-primary-600 text-[10px] px-1.5 py-0.5 rounded-sm

状态 Badge状态列

status 显示文字 样式
for_sale 出售 bg-success-50 text-success-700 text-xs px-2 py-0.5 rounded-full
for_rent 出租 bg-info-50 text-info-600 text-xs px-2 py-0.5 rounded-full
for_sale_rent 租售 bg-primary-50 text-primary-700 text-xs px-2 py-0.5 rounded-full
suspended 暂缓 bg-neutral-100 text-neutral-500 text-xs px-2 py-0.5 rounded-full
sold_elsewhere 他售 bg-warning-50 text-warning-600 text-xs px-2 py-0.5 rounded-full
rented_elsewhere 他租 bg-warning-50 text-warning-600 text-xs px-2 py-0.5 rounded-full
sold 成交 bg-neutral-200 text-neutral-600 text-xs px-2 py-0.5 rounded-full
unlisted 未挂牌 bg-neutral-100 text-neutral-400 text-xs px-2 py-0.5 rounded-full

价格趋势箭头(售价列)

<!-- 价格下降(最近一次调价为降价) -->
<div class="flex flex-col items-end">
  <span class="text-sm font-medium text-neutral-800">275</span>
  <span class="text-[10px] text-danger-600 flex items-center gap-0.5">
    <svg class="w-3 h-3"><!-- heroicon: arrow-down --></svg>
  </span>
</div>

<!-- 价格上涨 -->
<div class="flex flex-col items-end">
  <span class="text-sm font-medium text-neutral-800">650</span>
  <span class="text-[10px] text-success-600 flex items-center gap-0.5">
    <svg class="w-3 h-3"><!-- heroicon: arrow-up --></svg>
  </span>
</div>

PRD 补充说明竞品截图中「全部房源」Tab 显示的价格趋势为小型下箭头()贴在价格数字下方。设计为:降价→ text-danger-600 下箭头;涨价 → text-success-600 上箭头;未变化 → 不显示箭头。

排序列头(售价/单价/面积/挂牌日期/最后跟进日)

<th class="px-4 py-3 text-right text-xs font-semibold text-neutral-500 uppercase 
           tracking-wide cursor-pointer hover:bg-neutral-100 select-none whitespace-nowrap"
    hx-get="/properties/"
    :hx-vals="JSON.stringify({sort: 'sale_price', order: currentSalePriceOrder})"
    hx-target="#property-list-container"
    hx-swap="innerHTML"
    hx-include="closest form">
  售价(万)
  <!-- 未排序 -->
  <svg class="inline w-4 h-4 text-neutral-300 ml-0.5"><!-- heroicon: chevron-up-down --></svg>
  <!-- 升序激活(隐藏上方) -->
  <!-- 降序激活(隐藏下方) -->
</th>

行选中态

<tr class="hover:bg-neutral-50 transition-colors" style="height: 56px"
    :class="selected.includes(prop.id) ? 'bg-primary-50 hover:bg-primary-100' : ''">

3.3 操作列

MVP 阶段房源列表无独立操作列(行点击跳转到详情页即可);点击「房源名称」链接跳转。

:竞品截图中房源列表最右侧无固定操作列按钮,操作通过 checkbox 批量选中后在工具栏操作,或直接点击行跳详情。本设计遵循此交互模式,不添加操作列,降低信息密度。

3.4 表格交互状态

状态 触发场景 视觉表现
默认(无选中) 页面加载完毕 所有行 bg-whitehover 时 bg-neutral-50
行选中 勾选复选框 bg-primary-50 hover:bg-primary-100;工具栏批量操作按钮激活
全选 点击表头复选框 当前页所有行选中;表头 checkbox indeterminatechecked
LoadingHTMX 请求中) 筛选/分页/排序触发 骨架屏覆盖 #property-list-container(见 §2.1.6
空状态(无数据) 筛选无结果 / 首次进入 见 §2.1.5 空状态设计

4. 弹窗设计规范(列表页)

范围说明:本章仅包含从房源列表页直接触发的弹窗。调价、改状态、改等级等操作弹窗从房源详情页触发,记录于详情页 UI 设计文档。

4.1 自定义列弹窗P1 🟡

4.1.1 触发方式

  • 触发位置工具栏右侧「自定义列表」按钮Heroicon adjustments-horizontal + 文字)
  • 组件类型Modal Dialog组件规范设计.md §7
  • 尺寸max-w-2xl640px

4.1.2 弹窗布局

┌─────────────────────────────────────────────────────┐
│  标题:自定义列表信息                         [×]    │
├────────────────────────┬────────────────────────────┤
│  未选信息              │  已选信息                   │
│  (可勾选字段列表)     │  (已选字段,拖拽排序)      │
│                        │  ┌─────────────────────┐   │
│  □ 等级               │  │ ⋮⋮ 房源名称     [🔒] │   │
│  □ 属性               │  │ ⋮⋮ 楼栋         [删] │   │
│  □ 装修               │  │ ⋮⋮ 单元         [删] │   │
│  □ 建成年代            │  │ ⋮⋮ 房号         [删] │   │
│  □ 来源               │  │ ⋮⋮ 区域板块     [删] │   │
│  □ 完成度              │  │ ...                  │   │
│  □ 相关方              │  └─────────────────────┘   │
├────────────────────────┴────────────────────────────┤
│  [恢复默认]                       [取消]  [确定]     │
└─────────────────────────────────────────────────────┘

4.1.3 字段说明

固定不可隐藏列:房源名称(锁定,无删除按钮)

可选字段(完整清单):楼栋、单元、房号、区域板块、状态、售价(万)、单价(元/m²、租价元/月、面积、户型、楼层、朝向、挂牌日期、最后跟进日、等级、属性、装修、建成年代、来源、维护完成度、出售方、首录方

4.1.4 提交行为

  • 提交方式hx-post="/properties/column-preferences/"
  • 成功响应:关闭弹窗 + HTMX 刷新 #property-list-container 以重新渲染列
  • HTMX 属性
    <form hx-post="/properties/column-preferences/"
          hx-target="#property-list-container"
          hx-swap="innerHTML"
          hx-on::after-request="if(event.detail.successful){ $dispatch('close-modal'); }">
      <input type="hidden" name="columns" :value="JSON.stringify(selectedColumns)">
    </form>
    

4.1.5 使用的特殊组件

组件名 来源 用途
Modal Dialog §7 Modal Dialog 弹窗容器,max-w-2xl

Alpine.js 管理:selectedColumns(有序数组)、availableColumns(剩余可选)、dragging 状态;拖拽通过 Alpine.js + SortableJS 实现。


5. 交互状态规范

5.1 全局状态机

房源状态流转

for_sale出售  ──→  suspended暂缓  ──→  for_sale重新挂牌
                  ──→  sold_elsewhere他售
                  ──→  sold成交
                  
for_rent出租  ──→  suspended暂缓  ──→  for_rent重新挂牌
                  ──→  rented_elsewhere他租
                  ──→  sold成交
                  
unlisted未挂牌──→  for_sale / for_rent挂牌

suspended暂缓 ──→  for_sale / for_rent恢复挂牌

sold_elsewhere / rented_elsewhere ──→  for_sale / for_rent重新挂牌

状态在列表页的视觉标记

  • for_sale / for_rent:正常行背景
  • suspended / sold_elsewhere / rented_elsewhere:正常行背景,状态 Badge 有区分
  • sold:行文字颜色轻微降低(text-neutral-500),提示该房源已成交

5.2 权限控制矩阵

操作 经纪人 店长 管理员
查看房源列表 (自己名下 + 公盘) (本门店 + 公盘) (全部)
新增房源
批量收藏/取消收藏
设置保护房 (需权限)
批量修改相关方 (需权限) (本门店)
批量删除 (自己名下) (本门店)
导出 (自己数据) (本门店) (全量)
查看业主电话 (打码,需点击查看) 同经纪人
重复房源检测

5.3 HTMX 请求规范

操作 hx-trigger hx-get/post hx-target hx-swap Loading
Tab 切换(出售/出租等) click hx-get="/properties/" #property-list-container innerHTML 骨架屏
关键词搜索输入 keyup changed delay:300ms, search hx-get="/properties/" #property-list-container innerHTML 骨架屏
筛选 Checkbox 变更 change hx-get="/properties/" #property-list-container innerHTML 骨架屏
筛选 Tag 按钮点击 clickAlpine.js 更新 hidden inputchange hx-get="/properties/" #property-list-container innerHTML 骨架屏
查询按钮点击 click hx-get="/properties/" #property-list-container innerHTML 骨架屏
重置按钮 click hx-get="/properties/" #property-list-container innerHTML 骨架屏
排序列头点击 click hx-get="/properties/" #property-list-container innerHTML 骨架屏
分页页码点击 click hx-get="/properties/" #property-list-container innerHTML 骨架屏
每页条数变更 change hx-get="/properties/" #property-list-container innerHTML 骨架屏
导出按钮 click hx-post="/properties/export/" #export-status-area innerHTML Toast 提示「导出任务已提交」
自定义列保存 submit hx-post="/properties/column-preferences/" #property-list-container innerHTML Modal 内 Loading Spinner

Alpine.js 状态管理分工

状态/行为 管理方式 说明
一级 Tab 激活状态(activeTab Alpine.js 用于控制售价/租价列的显示/隐藏
筛选区展开/收起(showFilters Alpine.js 控制筛选区域整体折叠
展开更多筛选(showMoreFilters Alpine.js 控制下方展开筛选行的显示
价格模式切换(priceMode Alpine.js 售价/单价 Tab 切换
区域多选(selectedDistricts[] Alpine.js 维护已选行政区数组
表格行选中(selected[] Alpine.js 维护已勾选行的 ID 数组,驱动批量操作按钮激活
已选条数(selectedCount Alpine.js 计算属性 selected.length
自定义列弹窗开关(columnSettingsOpen Alpine.js 弹窗的 open/close 状态
数据加载/筛选/分页/排序 HTMX 所有后端数据请求
表单提交(批量操作/导出/保存列设置) HTMX 后端交互

6. 关键数据字段说明

字段名(英文) 显示名 数据类型 说明
id 房源 ID UUID 主键,用于行跳转链接
property_type 房源类型 VARCHAR(20) residential / villa / shop 等
status 状态 VARCHAR(20) for_sale / for_rent / suspended 等
attribute 属性 VARCHAR(20) public / private / special / sealed
complex_name 小区名称 关联查询 来自 complexes.name
block_no 楼栋号 VARCHAR(30) 栋/幢/弄号
unit_no 单元号 VARCHAR(30)
room_no 房号 VARCHAR(30) 门牌号
floor 所在楼层 SMALLINT
total_floors 总楼层 SMALLINT
bedroom_count 室数 SMALLINT 户型组合
living_room_count 厅数 SMALLINT 户型组合
bathroom_count 卫数 SMALLINT 户型组合
area 建筑面积 NUMERIC(8,2) 保留1位小数显示
sale_price 挂牌售价 NUMERIC(12,2) 万元
sale_unit_price 单价 计算字段 sale_price * 10000 / area,元/m²
rent_price 挂牌租价 NUMERIC(10,2) 元/月
orientation 朝向 VARCHAR(10) east / south 等,需翻译
decoration 装修 VARCHAR(10) rough / fine 等,需翻译
has_elevator 是否有电梯 BOOLEAN 用于标签显示
ownership_years 房本年限 VARCHAR(30) 满2年/满5年等用于「满五」标签
grade 等级 VARCHAR(10) A_urgent / A / B / C / D
listed_at 最近挂牌时间 TIMESTAMPTZ 显示为 YYYY-MM-DD
last_followed_at 最后跟进时间 TIMESTAMPTZ 超30天标红显示为 YYYY-MM-DD
completeness_score 维护完成度 SMALLINT 0-100Celery 异步计算
seller_agent_name 出售方经纪人 关联查询 来自 staff.name
first_recorder_name 首录方 关联查询 来自 staff.name
district 行政区 关联查询 来自 complexes → districts
business_area 商圈 关联查询 来自 complexes → business_areas
tags 标签列表 关联查询 来自 property_tag_relations → property_tags
is_favorited 是否已收藏 BOOLEAN 当前用户收藏状态,来自 property_favorites
latest_price_direction 最近调价方向 计算字段 up / down / null,来自最近一条 price_changes 记录

7. 竞品截图对应关系

截图路径 对应功能 对应文档章节 采纳的设计要点
Project/fonrey/screenshots/房源/房源列表.png 出售 Tab 列表视图(主参考) §2.1.2 布局、§3.1 列定义、§3.2 状态变体 列布局(楼栋/单元/房号/区域板块/状态/售价/单价/面积/户型/楼层/朝向/挂牌/跟进日);交易类型 Badge橙色买卖/蓝色租赁);多维度筛选区展开方式;工具栏布局(批量操作+导出+自定义分页样式每页20条+跳页)
Project/fonrey/screenshots/房源/全部房源.png 全部房源 Tab 视图 §2.1.3 区域详细规范、§3.1 列定义 全部房源同时含售价/租价/面积列;出售+出租混合显示方式;房源名称行显示多个附加标签(满五/电梯/视频/私盘等);价格列下方显示趋势小箭头

与竞品截图差异说明PRD 优先)

  1. 主色差异:竞品使用橙色(#F97316)作为主按钮色和 Tab 激活色Fonrey 使用 Teal#0F766E = primary-600)。所有激活状态、主按钮均使用 Teal。
  2. 按钮颜色竞品「新增房源」为橙色大按钮Fonrey 改为 bg-primary-600 Teal 色。
  3. 区域筛选位置:竞品截图中区域筛选(宝山/嘉定等)放在「区域」行独立显示,与 PRD 一致,本文档采纳。
  4. 右侧悬浮栏:竞品截图中页面右侧有悬浮操作栏(增客/增房/发审批等);该功能属于全局导航组件,不在本列表页 UI 文档范围内,由全局布局文档单独定义。

8. 实现优先级与工期估算

页面/功能 优先级 特殊组件复杂度 工期估算(前端)
一级 Tab 导航 + 页面框架 P0 🔴 0.5 天
搜索区域(关键词 + 楼栋/单元/房号) P0 🔴 0.5 天
范围/区域/价格/面积/房型筛选行 P0 🔴 中(多 Tag 按钮联动) 1.5 天
楼层/标签/筛选/维护筛选行 P0 🔴 中(多下拉联动) 1 天
工具栏(批量操作 + 总条数) P0 🔴 0.5 天
数据表格(核心列定义 + 状态 Badge + 价格箭头) P0 🔴 中(多列状态变体) 1.5 天
分页栏 P0 🔴 0.25 天
空状态 + Loading 骨架屏 P0 🔴 0.25 天
HTMX 请求整合(所有筛选联动) P0 🔴 1 天
Alpine.js 状态管理整合 P0 🔴 0.5 天
重复房源检测 + 疑似号码入口 P1 🟡 0.25 天
已存搜索条件保存/调用 P1 🟡 1 天
导出功能Celery 异步) P1 🟡 低(复用客源导出) 0.5 天
自定义列表弹窗(含拖拽排序) P1 🟡 SortableJS 集成) 1.5 天
智能排序P1 P1 🟡 0.25 天
海报视图切换P1 P1 🟡 高(卡片布局全新设计) 2 天
地图找房P2 P2 高(地图组件集成)

P0 总估算:约 7.5 天前端工时
P1 总估算:约 5.5 天前端工时


9. 开放问题(待决策)

# 问题 影响范围 待确认方
1 「超时未跟进房源」的超时阈值是多少天PRD 未明确指定具体天数 范围筛选行、超时标红逻辑 产品经理
2 「与我相关」的精确范围定义:是仅 seller_agent_id = me,还是包含 first_recorder_id / number_holder_id 筛选逻辑、后端查询 产品经理 + 后端
3 价格趋势箭头是显示具体降幅数字如「↓5万」还是仅显示方向箭头竞品截图中仅有箭头图标 §3.2 价格趋势箭头渲染 产品经理
4 房源名称列下方标签组的完整枚举:「满五/电梯/视频/私盘/速销/独家」之外还有哪些?是否全部来自 property_tags 还是有些来自字段推导? §3.2 房源附加标签 产品经理 + 后端
5 「海报视图」的具体布局设计尚未定义,仅知道是列表/海报切换;海报单卡片展示哪些字段? 工具栏切换按钮、海报卡片组件 产品经理
6 地铁筛选是否需要在 MVPP0阶段实现PRD 中列出但 MVP 优先级矩阵中未单独标注 区域筛选行地铁入口 产品经理
7 「关注小区配置提示条」是否在列表页默认显示?关闭后是否持久化(即刷新后不再显示)? 搜索区关注小区提示条 产品经理
8 全部房源 Tab 下,出售价和租价同时展示时,对于只有租价的房源,售价列显示 - 还是直接隐藏该列? §3.1 全部房源 Tab 列定义 产品经理
9 「一键装换」和「一般委托」标签筛选的具体业务含义?在数据模型中对应哪个字段或条件? 标签筛选行 产品经理 + 后端
10 列表默认展示的 Tab 是「出售」还是「全部房源」?用户未操作时哪个 Tab 默认激活? 一级 Tab 初始状态 产品经理