1380 lines
68 KiB
Markdown
1380 lines
68 KiB
Markdown
# 房源列表 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 功能范围
|
||
- 1.2 页面清单
|
||
- 1.3 用户角色与权限差异
|
||
2. [页面设计规范](#2-页面设计规范)
|
||
- 2.1 房源列表主页
|
||
3. [Data Table 规范](#3-data-table-规范)
|
||
- 3.1 列定义
|
||
- 3.2 列状态变体
|
||
- 3.3 操作列
|
||
- 3.4 表格交互状态
|
||
4. [弹窗设计规范(列表页)](#4-弹窗设计规范列表页)
|
||
- 4.1 自定义列弹窗
|
||
5. [交互状态规范](#5-交互状态规范)
|
||
- 5.1 房源状态机
|
||
- 5.2 权限控制矩阵
|
||
- 5.3 HTMX 请求规范
|
||
6. [关键数据字段说明](#6-关键数据字段说明)
|
||
7. [竞品截图对应关系](#7-竞品截图对应关系)
|
||
8. [实现优先级与工期估算](#8-实现优先级与工期估算)
|
||
9. [开放问题(待决策)](#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 params:`tab`, `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 Navigation),underline 变体 |
|
||
| 位置 | 页面最顶部,紧贴全局顶导下方,无额外卡片容器 |
|
||
| Tab 项 | 出售(含数量 Badge)/ 出租(含数量 Badge)/ 未挂牌 / 成交房源 / 全部房源 |
|
||
| 激活样式 | `border-b-2 border-primary-600 text-primary-600 font-medium` |
|
||
| 非激活样式 | `text-neutral-500 hover:text-neutral-700` |
|
||
| 右侧内容 | 「更多 ▾」下拉菜单 + 「重复房源」链接 + 「疑似问题号码房源」链接 + 「+ 新增房源」主按钮 |
|
||
|
||
**右侧操作区(Tab 栏右侧,绝对定位)**:
|
||
|
||
```html
|
||
<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 使用主色 Teal(`bg-primary-600`),品牌差异,无需对齐竞品色。
|
||
|
||
---
|
||
|
||
**[搜索区域]**
|
||
|
||
| 属性 | 说明 |
|
||
|---|---|
|
||
| 组件 | 搜索输入框组合 |
|
||
| 容器 | `bg-white rounded-lg border border-neutral-200 px-4 py-3 mt-3` |
|
||
| 关键词搜索框 | 宽约 `flex-1 max-w-lg`,占位符「房源编号/小区/学校名称/地址/业主姓名/电话/钥匙编号等」 |
|
||
| 楼栋输入框 | 标签「楼栋」+ 独立 Input,`w-24`,精确匹配 `block_no` |
|
||
| 单元输入框 | 标签「单元」+ 独立 Input,`w-24`,精确匹配 `unit_no` |
|
||
| 房号输入框 | 标签「房号」+ 独立 Input + Heroicon `information-circle` 提示,`w-24`,精确匹配 `room_no` |
|
||
| 搜索按钮 | `bg-primary-600 text-white` Heroicon `magnifying-glass`,`w-9 h-9 rounded-lg` |
|
||
| 地图找房(P2 ⚫)| 右侧浅色按钮「🗺 地图找房 →」,P2 阶段置灰不可点击 |
|
||
|
||
```html
|
||
<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`(具体阈值待产品确认) |
|
||
|
||
```html
|
||
<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` |
|
||
| 地铁 | 下拉 ▾ | 选择地铁线 + 站点(二级联动),来自楼盘数据 |
|
||
|
||
```html
|
||
<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 控制显示部分预设区间 |
|
||
|
||
```html
|
||
<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 多选 | `grade`:A急迫/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`) |
|
||
|
||
**底部操作行**:
|
||
|
||
```html
|
||
<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 🟡) |
|
||
|
||
```html
|
||
<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-container`(HTMX swap 目标),内嵌 `property-table-body` |
|
||
| 行高 | 56px(`h-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 | 批量操作 + 导出 + 统计 | 批量按钮默认 disabled,selectedCount > 0 激活 |
|
||
| Export Button | §5 Export Button | 导出 Excel(P1) | HTMX `hx-post` 异步触发 Celery 任务 |
|
||
| Tab Navigation | §10 Tab Navigation | 一级 Tab(出售/出租等)切换 | Underline 变体,右侧含操作区 |
|
||
| Date Range Picker | §9 Date Range Picker | 挂牌日期、跟进时间筛选 | 集成 Flatpickr,range 模式 |
|
||
| Multi-select Tag Input | §17 Multi-select Tag Input | 相关方、学校等多选筛选 | 展开更多筛选行内使用 |
|
||
| Modal Dialog | §7 Modal Dialog | 自定义列弹窗 | 见 §4.1 规范 |
|
||
|
||
#### 2.1.5 空状态设计
|
||
|
||
**筛选无结果(最常见场景)**:
|
||
|
||
```html
|
||
<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>
|
||
```
|
||
|
||
**首次进入无数据(无任何房源)**:
|
||
|
||
```html
|
||
<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` 内显示骨架屏:
|
||
|
||
```html
|
||
<!-- 骨架屏: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-10`(40px)`px-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-16`(64px) | 左对齐 | 否 | 纯文字;无数据显示 `-` |
|
||
| 4 | 单元 | `unit_no` | `w-16`(64px) | 左对齐 | 否 | 纯文字;无数据显示 `-` |
|
||
| 5 | 房号 | `room_no` | `w-16`(64px) | 左对齐 | 否 | 纯文字;无数据显示 `-` |
|
||
| 6 | 区域板块 | `district` + `business_area` | `min-w-[100px]` | 左对齐 | 否 | 格式:`嘉定 丰庄`,两行或空格分隔 |
|
||
| 7 | 状态 | `status` | `w-16`(64px) | 左对齐 | 否 | Status Badge(见 §3.2) |
|
||
| 8 | 售价(万) | `sale_price` | `w-24`(96px) | 右对齐 | **是** | 价格数字 + 价格变动趋势箭头(见 §3.2);出租 Tab 此列隐藏 |
|
||
| 9 | 单价(元/m²) | `sale_unit_price` | `w-28`(112px) | 右对齐 | **是** | 数字,格式化千分位;出租 Tab 此列隐藏 |
|
||
| 10 | 租价(元/月) | `rent_price` | `w-28`(112px) | 右对齐 | **是** | 数字,格式化千分位;出售 Tab 此列隐藏 |
|
||
| 11 | 面积(m²) | `area` | `w-20`(80px) | 右对齐 | **是** | 保留1位小数,如 `81.3` |
|
||
| 12 | 户型 | `bedroom_count` + `living_room_count` | `w-16`(64px) | 左对齐 | 否 | 格式:`3/1/1`(室/厅/卫);简显为 `X室X厅` |
|
||
| 13 | 楼层 | `floor` + `total_floors` | `w-16`(64px) | 左对齐 | 否 | 格式:`4/6`;无数据显示 `-` |
|
||
| 14 | 朝向 | `orientation` | `w-12`(48px) | 左对齐 | 否 | 中文显示:南北/东南等;无数据显示 `-` |
|
||
| 15 | 挂牌日期 | `listed_at` | `w-28`(112px) | 左对齐 | **是** | 格式:`YYYY-MM-DD` |
|
||
| 16 | 房源最后跟进日 | `last_followed_at` | `w-28`(112px) | 左对齐 | **是**(默认降序) | 格式:`YYYY-MM-DD`;超过 30 天字色 `text-danger-600` |
|
||
|
||
> **全部房源 Tab** 与**出售/出租 Tab** 的差异:全部房源 Tab 同时显示「售价」和「租价」列,部分行会有一列为 `-`,属正常显示。
|
||
|
||
> **`whitespace-nowrap` 要求**:以下列的数据单元格(`<td>`)必须加 `whitespace-nowrap`,防止内容折行:状态、售价、单价、租价、户型、楼层、朝向、挂牌日期、最后跟进日期。
|
||
|
||
> **自定义列(P1 🟡)**:用户通过「自定义列表」弹窗(§4.1)选择显示字段,可选字段包括:单价、等级、属性、装修、建成年代、来源、相关方、维护完成度等。
|
||
|
||
**表格 HTML 结构(关键片段)**:
|
||
|
||
```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` |
|
||
|
||
**价格趋势箭头(售价列)**:
|
||
|
||
```html
|
||
<!-- 价格下降(最近一次调价为降价) -->
|
||
<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` 上箭头;未变化 → 不显示箭头。
|
||
|
||
**排序列头(售价/单价/面积/挂牌日期/最后跟进日)**:
|
||
|
||
```html
|
||
<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>
|
||
```
|
||
|
||
**行选中态**:
|
||
|
||
```html
|
||
<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-white`,hover 时 `bg-neutral-50` |
|
||
| 行选中 | 勾选复选框 | `bg-primary-50 hover:bg-primary-100`;工具栏批量操作按钮激活 |
|
||
| 全选 | 点击表头复选框 | 当前页所有行选中;表头 checkbox `indeterminate` 或 `checked` |
|
||
| Loading(HTMX 请求中) | 筛选/分页/排序触发 | 骨架屏覆盖 `#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-2xl`(640px)
|
||
|
||
#### 4.1.2 弹窗布局
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ 标题:自定义列表信息 [×] │
|
||
├────────────────────────┬────────────────────────────┤
|
||
│ 未选信息 │ 已选信息 │
|
||
│ (可勾选字段列表) │ (已选字段,拖拽排序) │
|
||
│ │ ┌─────────────────────┐ │
|
||
│ □ 等级 │ │ ⋮⋮ 房源名称 [🔒] │ │
|
||
│ □ 属性 │ │ ⋮⋮ 楼栋 [删] │ │
|
||
│ □ 装修 │ │ ⋮⋮ 单元 [删] │ │
|
||
│ □ 建成年代 │ │ ⋮⋮ 房号 [删] │ │
|
||
│ □ 来源 │ │ ⋮⋮ 区域板块 [删] │ │
|
||
│ □ 完成度 │ │ ... │ │
|
||
│ □ 相关方 │ └─────────────────────┘ │
|
||
├────────────────────────┴────────────────────────────┤
|
||
│ [恢复默认] [取消] [确定] │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### 4.1.3 字段说明
|
||
|
||
**固定不可隐藏列**:房源名称(锁定,无删除按钮)
|
||
|
||
**可选字段(完整清单)**:楼栋、单元、房号、区域板块、状态、售价(万)、单价(元/m²)、租价(元/月)、面积(m²)、户型、楼层、朝向、挂牌日期、最后跟进日、等级、属性、装修、建成年代、来源、维护完成度、出售方、首录方
|
||
|
||
#### 4.1.4 提交行为
|
||
|
||
- **提交方式**:`hx-post="/properties/column-preferences/"`
|
||
- **成功响应**:关闭弹窗 + HTMX 刷新 `#property-list-container` 以重新渲染列
|
||
- **HTMX 属性**:
|
||
```html
|
||
<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 按钮点击 | `click`(Alpine.js 更新 hidden input)→ `change` | `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) | m²,保留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-100,Celery 异步计算 |
|
||
| `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 | 地铁筛选是否需要在 MVP(P0)阶段实现?PRD 中列出但 MVP 优先级矩阵中未单独标注 | 区域筛选行地铁入口 | 产品经理 |
|
||
| 7 | 「关注小区配置提示条」是否在列表页默认显示?关闭后是否持久化(即刷新后不再显示)? | 搜索区关注小区提示条 | 产品经理 |
|
||
| 8 | 全部房源 Tab 下,出售价和租价同时展示时,对于只有租价的房源,售价列显示 `-` 还是直接隐藏该列? | §3.1 全部房源 Tab 列定义 | 产品经理 |
|
||
| 9 | 「一键装换」和「一般委托」标签筛选的具体业务含义?在数据模型中对应哪个字段或条件? | 标签筛选行 | 产品经理 + 后端 |
|
||
| 10 | 列表默认展示的 Tab 是「出售」还是「全部房源」?用户未操作时哪个 Tab 默认激活? | 一级 Tab 初始状态 | 产品经理 |
|