# 客源列表 UI 设计文档 > **版本**:v1.0 · **日期**:2026-04-25 > **依赖规范**:UI_SYSTEM.md v1.1 · 组件规范设计.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. [弹窗/抽屉设计规范](#3-弹窗抽屉设计规范) - 3.1 改等级弹窗 - 3.2 改状态弹窗 - 3.3 转公客弹窗 - 3.4 转成交弹窗 - 3.5 转无效弹窗 - 3.6 收藏夹选择弹窗 4. [交互状态规范](#4-交互状态规范) - 4.1 客源状态机 - 4.2 权限控制矩阵 - 4.3 HTMX 请求规范 5. [关键数据字段说明](#5-关键数据字段说明) 6. [竞品截图对应关系](#6-竞品截图对应关系) 7. [实现优先级与工期估算](#7-实现优先级与工期估算) 8. [开放问题(待决策)](#8-开放问题待决策) --- ## 1. 模块概述 ### 1.1 功能范围 **P0 功能(MVP 必须实现)🔴** - 私客列表展示(全部私客 / 求购 / 求租 / 暂缓 四个二级 Tab) - 关键词搜索(姓名/号码/号码后4位/客源编号/备注) - 多维度筛选(状态、等级、需求类型、位置、价格区间、房室) - 展开更多筛选(相关方、委托日期、来源、购房目的等) - 列表数据展示(含活跃度标签) - 批量操作(修改相关方、修改来源、删除客源、合并客源) - 分页与每页条数控制 - 重复客源提示(私客与成交客重复、私客与公客重复、已删客源) - 常用快捷筛选(即将掉公、录入时间、与我相关、我部门相关) **P1 功能(第一迭代)🟡** - 已存搜索条件保存与快速调用 - 导出当前筛选结果(Excel) - 自定义列表显示字段 **P2 功能(路线图)⚫** - 公客列表 Tab - 成交客列表 Tab - 暂缓私客高级操作 ### 1.2 页面清单 | 页面名称 | URL 模式建议 | 优先级 | 对应 PRD 章节 | |---|---|---|---| | 客源列表(私客·全部) | `/clients/private/` | P0 🔴 | §5.1, Story 2 | | 客源列表(私客·求购) | `/clients/private/?tab=buying` | P0 🔴 | §5.1, Story 3 | | 客源列表(私客·求租) | `/clients/private/?tab=renting` | P0 🔴 | §5.1, Story 4 | | 客源列表(私客·暂缓) | `/clients/private/?tab=suspended` | P2 ⚫ | §5.1, Story 5 | | 客源列表(公客) | `/clients/public/` | P2 ⚫ | Story 12 | | 客源列表(成交客) | `/clients/transacted/` | P2 ⚫ | Story 13 | ### 1.3 用户角色与权限差异 | 差异点 | 经纪人 | 店长 | 管理员 | |---|---|---|---| | 默认数据范围 | 仅自己名下(`owner_id = me`) | 本门店全部(`org_unit_id IN my_stores`) | 全司所有 | | 「与我相关」快捷筛选 | 可用(默认可能已激活) | 可用 | 可用 | | 「我部门相关」快捷筛选 | 仅限自己所属部门 | 本门店 | 全司 | | 批量删除 | 仅限自己名下客源 | 本门店客源 | 所有客源 | | 合并客源 | 不可操作 | 可操作 | 可操作 | | 重复客源提示数字链接 | 可见 | 可见 | 可见 | | 「+ 新增私客」按钮 | 可见 | 可见 | 可见 | | 导出按钮 | 仅自己数据 | 本门店数据 | 全量 | --- ## 2. 页面设计规范 ### 2.1 客源列表主页(私客视图)(P0 🔴) #### 2.1.1 页面概述 - **URL**:`/clients/private/`(query params:`tab`, `status`, `grade`, `page`, `q` 等) - **访问入口**:顶部全局导航栏「客源」菜单 → 默认进入私客列表 - **页面职责**:展示经纪人名下(或门店/全司)的私客列表,支持多维度搜索筛选、批量操作、状态快览 - **竞品参考截图**: - `Project/fonrey/screenshots/客源/全部私客.png`(主参考) - `Project/fonrey/screenshots/客源/求购私客.png` - `Project/fonrey/screenshots/客源/求租私客.png` #### 2.1.2 布局结构 ``` ┌──────────────────────────────────────────────────────────────────────┐ │ 一级 Tab 导航(私客 / 资料客 / 营销客 / 成交客 / 公客) │ │ 右侧:重复检测提示 | 已删客源 | + 新增私客 按钮 │ ├──────────────────────────────────────────────────────────────────────┤ │ 二级 Tab 导航(求购 / 求租 / 暂缓 / 全部私客) │ ├──────────────────────────────────────────────────────────────────────┤ │ 搜索区域(关键词搜索 + 已存搜索快速调用 + 收起/展开筛选) │ ├──────────────────────────────────────────────────────────────────────┤ │ 快捷筛选行(即将掉公 / 录入时间 / 与我相关 / 我部门相关) │ ├──────────────────────────────────────────────────────────────────────┤ │ 分组筛选区(状态 / 需求 / 等级 / 位置 / 购价或租价 / 房室 / 更多筛选) │ ├──────────────────────────────────────────────────────────────────────┤ │ 工具栏(批量操作 | 总条数显示 | 导出 | 自定义列表) │ ├──────────────────────────────────────────────────────────────────────┤ │ 数据表格主体 │ ├──────────────────────────────────────────────────────────────────────┤ │ 分页栏(上一页 / 页码 / 下一页 / 每页条数 / 跳页) │ └──────────────────────────────────────────────────────────────────────┘ ``` 整体页面背景:`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) | | 位置 | 页面最顶部,紧贴全局顶导下方,无额外卡片容器,直接贴页面背景 | | Tab 项 | 私客(激活)/ 资料客 / 营销客 / 成交客 / 公客 | | 激活样式 | `border-b-2 border-primary-600 text-primary-600 font-medium` | | 非激活样式 | `text-neutral-500 hover:text-neutral-700` | | 右侧内容(绝对定位至 Tab 栏右侧) | 重复检测提示 + 「已删客源」链接 + 「+ 新增私客」按钮 | **重复检测提示区(Tab 栏右侧)**: ```html
私客与成交客重复: {{ dup_transacted_count }} 私客与公客重复: {{ dup_public_count }} 已删客源 新增私客
``` > **截图差异说明**:竞品截图中「+ 新增私客」为橙色按钮(`#F97316`),Fonrey 使用主色 Teal(`bg-primary-600`),这是品牌差异,无需对齐竞品色。 --- **[二级 Tab 导航区]** | 属性 | 说明 | |---|---| | 组件 | Tab Navigation(§10 Tab Navigation),Pill 变体 | | Tab 项 | 求购(含数量 Badge)/ 求租(含数量 Badge)/ 暂缓 / 全部私客 | | 激活样式 | `bg-primary-50 text-primary-700 font-semibold rounded-md px-4 py-1.5` | | 非激活样式 | `text-neutral-600 hover:bg-neutral-100 rounded-md px-4 py-1.5` | | 数量 Badge | `ml-1.5 bg-neutral-200 text-neutral-600 text-xs px-1.5 py-0.5 rounded-full` | | 切换行为 | HTMX `hx-get` 刷新整个筛选区+表格区,URL 更新 query param `tab` | > **截图说明**:竞品中激活 Tab 为橙色底色(「求购」橙色高亮),Fonrey 遵循系统主色 Teal,以 `bg-primary-50 text-primary-700` 替代橙色激活态。「全部私客」Tab 在截图中有橙色边框高亮——映射为 Fonrey 的 `bg-primary-600 text-white`(当激活)。 ```html
``` --- **[搜索区域]** | 属性 | 说明 | |---|---| | 组件 | 搜索输入框 + 下拉范围选择 | | 容器 | `bg-white rounded-lg border border-neutral-200 px-4 py-3 mt-3` | | 搜索框左侧 | 下拉范围选择器「客户信息 ▾」(`select` 元素,`text-sm text-neutral-600`),选项:客户信息(默认)/ 客源编号 | | 搜索输入框 | `w-80 pl-3 pr-10 py-2 border border-neutral-300 rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-600/40` | | 搜索触发 | 输入后 300ms debounce + Enter 键 + 搜索按钮点击;HTMX `hx-trigger="keyup changed delay:300ms, search"` | | 搜索图标按钮 | Heroicon `magnifying-glass`,`bg-primary-600 text-white w-9 h-9 rounded-lg` | | 已存搜索 | 搜索框右侧:「✦ N条已存搜索 ▾」下拉展开历史搜索条件列表 | | 收起/展开筛选 | 最右侧文字链接:「收起筛选 ∧」/ 「展开筛选 ∨」,Alpine.js 控制筛选区展开状态 | ```html
``` --- **[快捷筛选行]** | 属性 | 说明 | |---|---| | 位置 | 筛选区第一行,标签「常用」 | | 筛选项 | 即将掉公(Checkbox)/ 录入时间(下拉)/ 与我相关(Checkbox)/ 我部门相关(Checkbox) | ```html
常用
录入时间
``` --- **[分组筛选区]** 每个筛选组为一行,格式:`[标签] [选项1] [选项2] ... [自定义输入(如有)]` 整体容器:`space-y-2`,每行:`flex items-center flex-wrap gap-x-3 gap-y-1.5 text-sm` | 筛选组 | 标签宽 | 选项形式 | 特殊说明 | |---|---|---|---| | 状态 | `w-6 text-xs text-neutral-400` | 单选 Tag 按钮组 | Tab 决定显示哪些选项;激活为 `bg-primary-600 text-white`,默认为「不限」 | | 需求 | 同上 | 单选 Tag 按钮组 | 二手/新房(求购);租房(求租) | | 等级 | 同上 | 单选/多选 Tag 按钮组 | A急迫/A/B较强/C一般/D较弱/E暂不关注/未填写 | | 位置 | 同上 | 多选 Tag 按钮组(可滚动) | 宝山/崇明/...共19个区 | | 购价(求购Tab) | 同上 | 单选 Tag + 自定义区间输入 | 预设区间 + 「最小值」「~」「最大值」万 | | 租价(求租Tab) | 同上 | 单选 Tag + 自定义区间输入 | 预设区间 + 「最小值」「~」「最大值」元 | | 房室 | 同上 | 单选 Tag + 「是大价值」复选框(求购) | 不限/1居/2居/3居/4居/5居及以上 | | 筛选(展开) | 同上 | 各类下拉/DateRange | 含「展开 ∨」按钮;相关方/委托日期/来源/购房目的/带看进度/活跃情况/是否有效/面积/跟进时间/带看时间/审批中/审批驳回/收藏客源 | | 筛选(收起后隐藏) | 同上 | 各类 Checkbox/下拉 | 保护客/合作者/偏好新房/巧客力访客/置顶/用途/带看次数 | **Tag 按钮通用样式**: ```html ``` **价格自定义区间**: ```html
~
``` **展开更多筛选(下方收起区)**: - 点击「展开 ∨」,Alpine.js `showMoreFilters` 切换 - 展开后显示额外筛选行:相关方(`MultiSelect`)、委托日期(Date Range Picker §9)、来源(下拉)、购房目的(多选 Tag)等 - 「收起 ∧」收回 HTMX 行为:所有筛选变更统一通过 `hx-get="/clients/private/" hx-trigger="change" hx-target="#client-list-container" hx-swap="innerHTML" hx-include="closest form"`,由 Django 视图根据所有 query params 返回完整刷新片段。 --- **[工具栏区]** | 属性 | 说明 | |---|---| | 组件 | Toolbar(§4 Toolbar) | | 容器 | `flex items-center justify-between px-4 py-2.5 bg-white border-b border-neutral-100` | | 左侧 | 批量操作按钮组(勾选时激活)+ 总条数文字 | | 右侧 | 导出按钮 + 自定义列表按钮 | **批量操作按钮(勾选 ≥1 条后变为可点击)**: ```html
{{ total_count }} 已选
``` --- **[数据表格]** | 属性 | 说明 | |---|---| | 组件 | Data Table(§1 Data Table) | | 容器 id | `client-list-container`(HTMX swap 目标),内嵌 `client-table-body` | | 行高 | 56px(`h-14`),含活跃度标签时允许 auto 高度,最小 56px | | 横向滚动 | `overflow-x-auto` 包裹 ``,宽屏 ≥1280px 可全列展示 | **列规范(全部私客 / 求购私客视图)**: | 列名 | 数据字段 | 宽度 | 排序 | 特殊渲染 | |---|---|---|---|---| | 复选框 | — | `w-10 px-4` | 否 | `` 全选/单选,Alpine.js `selected[]` 数组 | | 姓名 | `contact_name`(主联系人)+ `grade` + 活跃度标签 | `min-w-[160px]` | 否 | 蓝色链接跳转详情;姓名下方渲染活跃度 Tag(多个可并排);名字过长截断 `truncate max-w-[140px]` | | 状态 | `status` | `80px` | 否 | Status Badge(见下方说明) | | 需求类型 | `requirement_type`(主需求) | `80px` | 否 | 文字:二手 / 新房 / 租房 | | 需求/解读 | `budget_min~budget_max` + `area_min~area_max` | `min-w-[180px]` | 否 | 格式:`550-600万,100㎡-110㎡...`;截断 `truncate`;点击可展开 Tooltip | | 智能配房 | `match_count` | `90px` | 否 | 数字 + Heroicon `information-circle`(`w-4 h-4 text-neutral-400`);点击 ⓘ 弹出配房预览 | | 意向商圈/小区 | `intent_business_area` / `intent_complex_names` | `min-w-[120px]` | 否 | 多值逗号分隔,截断显示,`-` 表示未填 | | 归属人 | `owner_name` + `org_unit_name` | `min-w-[140px]` | 否 | 格式:`雷威-都市港湾店一组`;`text-sm text-neutral-700` | | 带看进度 | `viewing_progress_label` | `80px` | 否 | 文字标签:未带看 / 一看 / 二看 / 复看;「一看」用 `bg-warning-50 text-warning-600 px-2 py-0.5 rounded-full text-xs` | | 带看次数 | `viewing_count` | `70px` | 是 | `N次`;排序时显示箭头 | | 委托日期 | `commission_date` | `90px` | 是 | `YYYY-MM-DD` | | 最近时间 | `last_follow_at` 距今天数 | `90px` | 是(默认降序) | `N天前` / `今天`;超过30天用 `text-danger-600` | | 操作 | — | `60px` | 否 | 「拨号」按钮(Heroicon `phone`,`text-primary-600`),点击触发拨号弹层 | **活跃度标签渲染规则**: | 标签值 | 文字 | Tailwind 样式 | |---|---|---| | `new_matched` | 新配偶 | `bg-info-50 text-info-600` | | `active_7d` | 7日活跃 | `bg-success-50 text-success-600` | | `active_30d` | 30日活跃 | `bg-green-50 text-green-500` | | `expiring` | 即将过期 | `bg-warning-50 text-warning-600` | | `frozen` | 暂缓 | `bg-neutral-100 text-neutral-500` | | `invalid` | 无效 | `bg-danger-50 text-danger-600` | | 来源为营销 | 营销客 | `bg-purple-50 text-purple-600` | | 来源为销售 | 销售客 | `bg-orange-50 text-orange-600` | | 来源为访客 | 访客 | `bg-neutral-100 text-neutral-500` | 标签 HTML: ```html 新配偶 ``` **状态 Badge 渲染**: | status 值 | 显示文字 | 样式 | |---|---|---| | `buying` | 求购 | `bg-primary-50 text-primary-700 text-xs px-2 py-0.5 rounded-full` | | `renting` | 求租 | `bg-info-50 text-info-600 text-xs px-2 py-0.5 rounded-full` | | `buy_or_rent` | 租购 | `bg-warning-50 text-warning-600 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` | **行点击行为**:点击姓名链接跳转至 `/clients/private/{id}/`(全页跳转,非 HTMX);点击行其他区域不跳转(避免误操作)。 **表格 HTML 结构(关键片段)**: ```html
{% for client in clients %} {% endfor %}
姓名 带看次数
{{ client.contact_name }}
{% if client.grade %} {{ client.grade_display }} {% endif %} {% if client.activity_level %} {{ client.activity_level_display }} {% endif %}
``` **求租 Tab 差异**: - 需求/解读列:展示租价区间(元/月)+ 面积区间,如「4000-5000元,45㎡-60㎡...」 - 价格筛选行标签改为「租价」,区间单位为「元」 - 状态筛选选项改为:不限 / 求租 / 租购 - 需求筛选改为:租房(仅此一项) **暂缓 Tab 差异**: - 状态筛选:不限 / 暂缓(租购) / 暂缓(求租) / 暂缓(求购) - 无「是大价值」复选框 --- **[分页栏]** | 属性 | 说明 | |---|---| | 组件 | Pagination(§2 Pagination) | | 位置 | 表格下方 `mt-4 flex items-center justify-between px-1` | | 左侧 | 总条数文字「共 N 条」 | | 中间 | 页码导航:`← 上一页 [1] [2] [3] [4] [5] … [196] 下一页 →` | | 右侧 | 每页条数选择「20条/页 ▾」(选项:20/50/100)+ 跳页输入框「跳至」+ 输入框 + 「页」+ 「确定」 | | HTMX | 页码按钮 `hx-get="/clients/private/" hx-vals='{"page": N}' hx-target="#client-list-container" hx-swap="innerHTML" hx-include="closest form"` | | 当前页样式 | `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 | 客源数据主体展示 | 行高兼容双行(姓名+活跃度标签),允许 `min-h-14 h-auto` | | Pagination | §2 Pagination | 底部分页控件 | 新增跳页输入框,与标准实现一致 | | Column Visibility Panel | §3 Column Visibility Panel | 自定义列表字段选择 | 触发按钮为「自定义列表」文字 + 图标 | | Toolbar | §4 Toolbar | 批量操作 + 导出 + 统计 | 批量按钮默认 disabled,selectedCount > 0 激活 | | Export Button | §5 Export Button | 导出 Excel | HTMX `hx-post` 异步触发 Celery 任务 | | Tab Navigation | §10 Tab Navigation | 一级/二级 Tab 切换 | 一级用 underline 样式,二级用 pill 样式 | | Date Range Picker | §9 Date Range Picker | 「录入时间」「委托日期」「跟进时间」筛选 | 集成 Flatpickr,range 模式 | | Multi-select Tag Input | §17 Multi-select Tag Input | 位置(多选区)、等级(多选)筛选 | 无需 Tag Input 样式,直接用按钮多选组 | | Modal Dialog | §7 Modal Dialog | 改等级/改状态/转公客/转成交/转无效弹窗 | 见第3章各弹窗规范 | #### 2.1.5 空状态设计 **筛选无结果(最常见场景)**: ```html

暂无客源

当前筛选条件下没有客源,尝试调整筛选条件

``` **首次进入无数据(无任何客源)**: ```html

还没有私客

开始录入您的第一位私客

新增私客
``` #### 2.1.6 Loading 状态 **筛选/分页触发 HTMX 请求期间**,`#client-list-container` 内显示骨架屏: ```html
{% for i in "12345" %}
{% endfor %}
``` HTMX loading 触发:在 `
` 添加 `hx-indicator="#client-skeleton"`;骨架屏默认隐藏,请求期间通过 `htmx-request` class 显示。 --- ## 3. 弹窗/抽屉设计规范 ### 3.1 改等级弹窗(P0 🔴) #### 3.1.1 触发方式 - **触发位置**:客源详情页右侧信息概览面板「改等级」快捷按钮(本文档中列表页行内操作暂无此入口,详情页触发) - **组件类型**:Modal Dialog(选择理由:操作简单,2个字段,Drawer 过重) - **尺寸**:`max-w-sm`(384px) - **竞品截图**:竞品中改等级为小弹窗形式,与 Modal sm 对应 #### 3.1.2 表单字段规范 | 字段名 | 组件类型 | 必填 | 校验规则 | 默认值/预填值 | |---|---|---|---|---| | 原等级(只读) | 只读文本展示 | — | — | 当前等级值,如「C(一般)」 | | 新等级 | Select 下拉 | 是 | 必选 | 无默认值 | 新等级选项:A急迫 / A / B较强 / C一般 / D较弱 / E暂不关注 #### 3.1.3 提交行为 - **提交方式**:HTMX `hx-post="/clients/private/{id}/grade/"` - **成功响应**:关闭弹窗 + Toast「等级已更新」+ HTMX 局部刷新客源信息概览面板(`hx-target="#client-info-panel" hx-swap="innerHTML"`) - **失败响应(422)**:在「新等级」下方显示行内错误文字 `text-danger-600 text-xs` - **HTMX 属性**: ```html
...
``` #### 3.1.4 使用的特殊组件 | 组件名 | 来源 | 用途 | |---|---|---| | Modal Dialog | §7 Modal Dialog | 弹窗容器,`max-w-sm` | Alpine.js 管理:弹窗开关(`open`),`newGrade` 绑定,确定按钮 disabled 状态。 --- ### 3.2 改状态弹窗(P0 🔴) #### 3.2.1 触发方式 - **触发位置**:客源详情页信息概览面板「改状态」按钮 - **组件类型**:Modal Dialog - **尺寸**:`max-w-sm`(384px) #### 3.2.2 表单字段规范 | 字段名 | 组件类型 | 必填 | 校验规则 | 默认值/预填值 | |---|---|---|---|---| | 原状态(只读) | 只读文本 | — | — | 当前状态,如「求购」 | | 新状态 | Select | 是 | 必选;不能与原状态相同 | 无默认值 | | 等级 | Select | 否 | — | 当前等级值(可修改) | | 更改理由 | Textarea | 是 | 最少 1 字,最多 200 字 | 空 | 新状态选项:求购 / 求租 / 租购 #### 3.2.3 提交行为 - **提交方式**:`hx-post="/clients/private/{id}/status/"` - **成功响应**:关闭弹窗 + Toast「状态已更新」+ 刷新客源信息概览面板 - **失败响应(422)**:字段级红字提示(Textarea 下方显示「请填写更改理由」) - **HTMX 属性**: ```html
``` 「确定」按钮:`newStatus` 未选或 `reason` 为空时 disabled。 #### 3.2.4 使用的特殊组件 | 组件名 | 来源 | 用途 | |---|---|---| | Modal Dialog | §7 Modal Dialog | 弹窗容器 | --- ### 3.3 转公客弹窗(P0 🔴) #### 3.3.1 触发方式 - **触发位置**:客源详情页信息概览面板「转公客」按钮 - **组件类型**:Modal Dialog(操作不可逆,需明确确认) - **尺寸**:`max-w-sm` #### 3.3.2 表单字段规范 | 字段名 | 组件类型 | 必填 | 校验规则 | 默认值/预填值 | |---|---|---|---|---| | 状态 | Select | 是 | 必选 | 当前状态值 | | 等级 | Select | 是 | 必选 | 当前等级值 | 弹窗标题区下方显示橙色警告提示: ```html
转为公客后将无法撤销,该客源将进入公共客源池,全员可见可跟进。
``` #### 3.3.3 提交行为 - **提交方式**:`hx-post="/clients/private/{id}/to-public/"` - **成功响应**:关闭弹窗 + Toast「已转为公客」+ 跳转至私客列表页(`window.location.href = '/clients/private/'`) - **HTMX 属性**: ```html ``` #### 3.3.4 使用的特殊组件 | 组件名 | 来源 | 用途 | |---|---|---| | Modal Dialog | §7 Modal Dialog | 弹窗容器,含不可逆警告 | --- ### 3.4 转成交弹窗(P0 🔴) #### 3.4.1 触发方式 - **触发位置**:客源详情页信息概览面板「转成交」按钮 - **组件类型**:Modal Dialog(需录入成交信息,字段较多) - **尺寸**:`max-w-lg`(512px) #### 3.4.2 表单字段规范 | 字段名 | 组件类型 | 必填 | 校验规则 | 默认值/预填值 | |---|---|---|---|---| | 状态 | Radio(我购 / 我租) | 是 | — | 我购 | | 房源类型 | Radio(二手 / 新房) | 是 | — | 二手 | | 成交房源 | 房源选择器(链接按钮 → 弹出选择浮层) | 是 | 必选1套 | 空 | | 成交日期 | Date Picker | 是 | 不能晚于今日 | 当日 | | 成交价格 | Number Input(单位:万元) | 是 | > 0 | 空 | | 成交方 | 人员选择器 | 是 | — | 当前登录用户所属门店 | **成交房源选择器**:点击「+ 选择成交房源」按钮,弹出独立 Modal(`max-w-4xl`),包含: - 搜索框(房源编号/楼盘地址/业主姓名/电话) - 筛选栏(区域/状态/相关方/部门) - 表格(房源名称、交易类型、状态、用途、城区商圈、房型、楼层、面积) - 单选(Radio per row) - 分页(50条/页,共89704条) - 底部「已选(0)」+ 「确定」按钮 #### 3.4.3 提交行为 - **提交方式**:`hx-post="/clients/private/{id}/to-transacted/"` - **成功响应**:关闭弹窗 + Toast「已标记为成交」+ 跳转详情页刷新状态 - **失败响应(422)**:字段级错误提示 - **HTMX 属性**: ```html ``` #### 3.4.4 使用的特殊组件 | 组件名 | 来源 | 用途 | |---|---|---| | Modal Dialog | §7 Modal Dialog | 主弹窗容器(`max-w-lg`) | | Modal Dialog(嵌套) | §7 Modal Dialog | 成交房源选择浮层(`max-w-4xl`) | | Date Range Picker | §9 Date Range Picker | 成交日期单选(single mode) | | Data Table | §1 Data Table | 房源选择列表 | | Pagination | §2 Pagination | 房源选择分页 | --- ### 3.5 转无效弹窗(P0 🔴) #### 3.5.1 触发方式 - **触发位置**:客源详情页信息概览面板「转无效」按钮 - **组件类型**:Modal Dialog(操作相对不可逆) - **尺寸**:`max-w-sm` #### 3.5.2 表单字段规范 弹窗顶部蓝色信息提示框: ```html
该功能为整体客转无效,将把所有电话标记无效
``` | 字段名 | 组件类型 | 必填 | 校验规则 | 默认值/预填值 | |---|---|---|---|---| | 无效原因 | Radio List(单选) | 是 | 必选1项 | 号码无效(第一项) | Radio 选项: - ⦿ 号码无效(默认) - ○ 同行中介 - ○ 广告推销 - ○ 客户无意向 - ○ 其他 Radio HTML: ```html
{% for reason in invalid_reasons %} {% endfor %}
``` #### 3.5.3 提交行为 - **提交方式**:`hx-post="/clients/private/{id}/invalidate/"` - **成功响应**:关闭弹窗 + Toast「已标记为无效」+ 从私客列表移除该行(列表局部刷新) - **HTMX 属性**: ```html ``` #### 3.5.4 使用的特殊组件 | 组件名 | 来源 | 用途 | |---|---|---| | Modal Dialog | §7 Modal Dialog | 弹窗容器,含信息提示 | --- ### 3.6 收藏夹选择弹窗(P1 🟡) #### 3.6.1 触发方式 - **触发位置**:客源详情页信息概览面板「☆ 收藏」按钮 - **组件类型**:Modal Dialog(轻量操作,非抽屉) - **尺寸**:`max-w-sm` #### 3.6.2 表单字段规范 弹窗标题:「选择私客收藏夹」 副标题:`text-neutral-400 text-xs`「可在私客列表筛选收藏的客户」 | 字段名 | 组件类型 | 必填 | 校验规则 | 默认值 | |---|---|---|---|---| | 选择收藏夹 | Radio List | 是 | 必选 | 默认收藏夹 | | 新建收藏夹名称(内联出现) | Text Input | — | 最多10字 | 空 | 收藏夹列表: ```html
{% for folder in folders %} {% endfor %}
``` #### 3.6.3 提交行为 - **提交方式**:`hx-post="/clients/private/{id}/favorite/"` - **成功响应**:关闭弹窗 + 信息面板「收藏」图标变为实心星 ★(`text-warning-600`) - **HTMX 属性**: ```html ``` #### 3.6.4 使用的特殊组件 | 组件名 | 来源 | 用途 | |---|---|---| | Modal Dialog | §7 Modal Dialog | 弹窗容器 | --- ## 4. 交互状态规范 ### 4.1 全局状态机 **客源状态流转**(适用于列表页行内操作和详情页操作): ``` buying(求购) ←→ suspended(暂缓) renting(求租) ↘ buy_or_rent(租购)→ public(公客)[不可逆] → bought(已购)[不可逆] → rented_done(已租)[不可逆] → invalid(无效)[需审批可恢复] ``` 视觉呈现:在列表状态列、详情页标题区用 Badge 展示;状态变更后相关 Badge 实时 HTMX 局部刷新。 ### 4.2 权限控制矩阵 | 操作 | 经纪人(自己的客源) | 店长 | 管理员 | |---|---|---|---| | 查看列表 | 仅自己名下 | 本门店 | 全司 | | 新增私客 | ✅ | ✅ | ✅ | | 删除私客(批量) | ✅(仅自己) | ✅(本门店) | ✅ | | 合并客源 | ❌ | ✅ | ✅ | | 改等级 | ✅(自己的) | ✅ | ✅ | | 改状态 | ✅(自己的) | ✅ | ✅ | | 转公客 | ✅(归属人/首录人) | ✅ | ✅ | | 转成交 | ✅(归属人) | ✅ | ✅ | | 转无效 | ✅(归属人/首录人) | ✅ | ✅ | | 导出 | 仅自己数据 | 本门店 | 全量 | | 查看号码 | ✅(需审计日志) | ✅ | ✅ | | 修改相关员工(跨店) | ❌ | ✅ | ✅ | 权限控制实现:Django 视图层通过 `request.user` 的 role 和 org_unit 过滤 QuerySet;前端通过 Django 模板 `{% if user.role == 'manager' %}` 条件渲染隐藏不可用按钮(双重防护)。 ### 4.3 HTMX 请求规范 | 操作 | hx-trigger | hx-method + URL | hx-target | hx-swap | Loading 行为 | |---|---|---|---|---|---| | 关键词搜索 | `keyup changed delay:300ms, search` | `GET /clients/private/` | `#client-list-container` | `innerHTML` | 骨架屏覆盖表格区 | | 筛选条件变更 | `change` | `GET /clients/private/` | `#client-list-container` | `innerHTML` | 骨架屏 | | 二级 Tab 切换 | `click`(Alpine.js 配合) | `GET /clients/private/?tab=X` | `#client-list-container` | `innerHTML` | 骨架屏 | | 分页跳转 | `click` | `GET /clients/private/?page=N` | `#client-list-container` | `innerHTML` | 骨架屏 | | 每页条数变更 | `change` | `GET /clients/private/?page_size=N` | `#client-list-container` | `innerHTML` | 骨架屏 | | 列排序 | `click`(表头) | `GET /clients/private/?sort=field&order=asc\|desc` | `#client-list-container` | `innerHTML` | 骨架屏 | | 改等级提交 | `submit` | `POST /clients/private/{id}/grade/` | `#client-info-panel` | `innerHTML` | 按钮 loading spinner | | 改状态提交 | `submit` | `POST /clients/private/{id}/status/` | `#client-info-panel` | `innerHTML` | 按钮 loading spinner | | 转公客提交 | `submit` | `POST /clients/private/{id}/to-public/` | `body` | `none` | 按钮 loading spinner | | 转成交提交 | `submit` | `POST /clients/private/{id}/to-transacted/` | `body` | `none` | 按钮 loading spinner | | 转无效提交 | `submit` | `POST /clients/private/{id}/invalidate/` | `#client-list-container` | `innerHTML` | 按钮 loading spinner | | 收藏/取消收藏 | `submit` | `POST /clients/private/{id}/favorite/` | `#favorite-icon` | `outerHTML` | 图标 spinning | | 导出 | `click` | `POST /clients/private/export/` | `body` | `none` | Toast 提示「正在生成,完成后下载」 | | 创建收藏夹 | `click` | `POST /clients/folders/create/` | `#folder-list` | `innerHTML` | 按钮 loading | | 批量删除 | `click` | `DELETE /clients/private/batch/` | `#client-list-container` | `innerHTML` | 按钮 loading | | 批量修改相关方 | `submit`(Modal内) | `PATCH /clients/private/batch/related/` | `#client-list-container` | `innerHTML` | 按钮 loading | **按钮 Loading 实现**(用于提交按钮): ```html ``` --- ## 5. 关键数据字段说明 以下字段为客源列表页后端 API 需返回的完整字段集: | 字段名(英文) | 显示名 | 数据类型 | 说明 | |---|---|---|---| | `id` | 客源ID | UUID | 路由参数 | | `client_no` | 客源编号 | string | 如 KY20260424001 | | `client_type` | 客源类型 | enum | private/public/transacted | | `status` | 状态 | enum | buying/renting/buy_or_rent/suspended/... | | `status_display` | 状态显示名 | string | 求购/求租/租购/暂缓 | | `grade` | 等级 | enum | A_urgent/A/B/C/D/E | | `grade_display` | 等级显示名 | string | A(急迫)/B(较强)/C(一般) 等 | | `activity_level` | 活跃度 | enum | new_matched/active_7d/active_30d/expiring/frozen/invalid | | `activity_level_display` | 活跃度显示名 | string | 新配偶/7日活跃 等 | | `activity_level_class` | 活跃度 CSS 类 | string | Django 模板方法生成对应 Tailwind class 字符串 | | `contact_name` | 主联系人姓名 | string | client_contacts.sort_order=0 的 name | | `contact_phone_masked` | 主联系人手机(打码) | string | `135****6789` | | `requirement_type` | 主需求类型 | enum | second_hand/new_house/rental | | `requirement_type_display` | 需求类型显示名 | string | 二手/新房/租房 | | `budget_min` | 预算下限 | decimal | 万元(购房)或元/月(租房) | | `budget_max` | 预算上限 | decimal | | | `area_min` | 面积下限 | decimal | ㎡ | | `area_max` | 面积上限 | decimal | ㎡ | | `budget_area_display` | 需求/解读列显示文本 | string | 如「550-600万,100㎡-110㎡...」,后端格式化 | | `match_count` | 智能配房套数 | int | 未反馈不合适的配房数 | | `intent_location_display` | 意向商圈/小区 | string | 后端聚合格式化 | | `owner_name` | 归属人姓名 | string | staff.name | | `org_unit_name` | 归属门店-组 | string | 格式:`都市港湾店一组` | | `owner_display` | 归属人+门店(合并) | string | `雷威-都市港湾店一组` | | `viewing_progress` | 带看进度编号 | int | 0=未带看,1=一看,2=二看... | | `viewing_progress_display` | 带看进度显示名 | string | 未带看/一看/二看/复看 | | `viewing_count` | 带看次数 | int | | | `commission_date` | 委托日期 | date | YYYY-MM-DD | | `last_follow_display` | 最近时间显示 | string | `3天前` / `今天` / `8天前` | | `last_follow_at` | 最近跟进时间 | datetime | 排序用原始值 | | `is_starred` | 是否收藏 | bool | 影响收藏图标状态 | | `is_pinned` | 是否置顶 | bool | 置顶的客源排在最前 | | `is_big_value` | 是否大价值 | bool | 用于「是大价值」筛选 | | `is_protected` | 是否保护客 | bool | 保护客不会自动转公 | | `source` | 客户来源 | string | lookup_items 维护 | | `dup_transacted_count` | 与成交客重复数量 | int | 顶部提示用 | | `dup_public_count` | 与公客重复数量 | int | 顶部提示用 | | `total_count` | 当前筛选总条数 | int | 工具栏「共N条」 | | `page` | 当前页码 | int | | | `page_size` | 每页条数 | int | 默认20 | | `total_pages` | 总页数 | int | | --- ## 6. 竞品截图对应关系 | 截图路径 | 对应功能 | 对应文档章节 | 采纳的设计要点 | |---|---|---|---| | `screenshots/客源/全部私客.png` | 全部私客 Tab 完整视图 | §2.1 全部 | ① 二级 Tab 全部私客样式;② 筛选区「常用」快捷行布局;③ 状态/需求/等级/位置/购价/房室分组筛选行布局;④ 工具栏批量按钮 + 总条数 + 导出 + 自定义列表按钮排列;⑤ 表格12列完整列定义(含活跃度标签双行渲染);⑥ 分页栏「20条/页」跳页格式 | | `screenshots/客源/求购私客.png` | 求购 Tab 专属视图 | §2.1(求购差异) | ① 购价筛选行展开样式(含「收起」链接);② 「展开」更多筛选行含保护客/合作者/偏好新房等;③ 求购 Tab 激活态(橙色→Fonrey 用 primary-600);④ 「是大价值」复选框在房室行末尾 | | `screenshots/客源/求租私客.png` | 求租 Tab 专属视图 | §2.1(求租差异) | ① 租价筛选行单位为「元」;② 租价预设区间(2000元以下~10000以上);③ 状态筛选改为「不限/求租/租购」;④ 需求类型仅「租房」;⑤ 列表行「需求类型」列显示「租房」;⑥ 需求/解读列格式为租价+面积 | **截图与 PRD 差异说明**: 1. 竞品截图中一级 Tab 包含「资料客」「营销客」,PRD §5.1 亦提及,但 PRD_MVP.md 未将这两个 Tab 列为 P0,本文档将其作为空 Tab 占位展示(不可点击或点击提示「即将上线」),避免 MVP 实现复杂度。 2. 竞品右上角有「商城」入口和用户头像/姓名,属于全局顶导,不在本文档范围。 3. 竞品颜色为橙色系,Fonrey 统一使用 Teal 主色,所有橙色激活态映射为 `bg-primary-600` / `text-primary-600`。 4. 竞品截图中「全部私客」Tab 有橙色边框高亮(`border border-orange-400`),Fonrey 对应为 pill 激活态 `bg-white text-primary-700 shadow-sm`。 --- ## 7. 实现优先级与工期估算 | 页面/功能 | 优先级 | 特殊组件复杂度 | 工期估算(前端) | |---|---|---|---| | 一级 Tab + 二级 Tab 导航框架 | P0 🔴 | 低(Tab Navigation §10) | 0.5 天 | | 搜索框 + 已存搜索 | P0 🔴 | 中(下拉交互) | 0.5 天 | | 快捷筛选行 + 分组筛选条(含展开/收起) | P0 🔴 | 中(Alpine.js 管理展开状态 + HTMX) | 1.5 天 | | 价格区间筛选(预设+自定义) | P0 🔴 | 低 | 0.5 天 | | 工具栏(批量操作 + 总条数 + 导出 + 自定义列) | P0 🔴 | 中(Column Visibility Panel §3) | 1 天 | | 数据表格主体(12列 + 活跃度标签渲染) | P0 🔴 | 高(Data Table §1,多行单元格) | 2 天 | | 分页栏(含跳页) | P0 🔴 | 低(Pagination §2) | 0.5 天 | | 重复检测提示区 | P0 🔴 | 低 | 0.25 天 | | 空状态设计 | P0 🔴 | 低 | 0.25 天 | | 骨架屏 Loading | P0 🔴 | 低 | 0.25 天 | | 改等级弹窗 | P0 🔴 | 低(Modal §7) | 0.5 天 | | 改状态弹窗 | P0 🔴 | 低(Modal §7) | 0.5 天 | | 转公客弹窗 | P0 🔴 | 低(Modal §7) | 0.5 天 | | 转成交弹窗(含房源选择器) | P0 🔴 | 高(嵌套 Modal + 房源搜索列表) | 2 天 | | 转无效弹窗 | P0 🔴 | 低(Modal §7) | 0.5 天 | | 已存搜索保存与调用 | P1 🟡 | 中 | 1 天 | | 导出 Excel(Celery 异步) | P1 🟡 | 中(后端为主) | 0.5 天前端 | | 自定义列表字段(Column Visibility) | P1 🟡 | 中(§3 Column Visibility) | 1 天 | | 收藏夹选择弹窗(含创建收藏夹) | P1 🟡 | 中(Modal + 内联交互) | 1 天 | | **合计 P0** | | | **约 10.75 天** | | **合计 P1** | | | **约 3.5 天** | --- ## 8. 开放问题(待决策) | # | 问题 | 影响范围 | 待确认方 | |---|---|---|---| | 1 | 「资料客」「营销客」Tab 在 MVP 阶段是否展示为灰色禁用 Tab,还是直接不显示? | 一级 Tab 导航 §2.1.3 | 产品经理 | | 2 | 二级 Tab 上的客源数量 Badge(如「求购 913」)是否实时计数?若是,是否有性能开销?建议改为后端分 Tab 预聚合或缓存 | 二级 Tab §2.1.3 | 后端 + 产品 | | 3 | 「与我相关」和「我部门相关」的精确业务定义:经纪人同时是首录人和归属人时,「与我相关」指 `owner_id=me OR first_recorder_id=me`?还是仅 `owner_id=me`? | 快捷筛选行 §2.1.3 | 产品经理 | | 4 | 「即将掉公」筛选的时间阈值(距自动转公还有多少天开始提示)是运营后台可配置项还是硬编码?需要前端在筛选行旁边展示剩余天数吗? | 快捷筛选行 | 产品 + 后端 | | 5 | 价格筛选的自定义区间输入:用户手动输入后是否需要点击「搜索」按钮才触发,还是 blur 后自动 HTMX?(与其他 Tag 筛选项行为需统一) | 价格筛选 §2.1.3 | 产品经理 | | 6 | 表格「最近时间」列:PRD 写的是「最近时间」(最近跟进或带看的距今天数),截图中显示「N天前」+ 日期(如`2026-04-19`)两行,是否需要双行展示? | 表格列定义 §2.1.3 | 产品经理(截图已有双行,建议对齐截图) | | 7 | 导出功能:Celery 异步生成后如何通知用户下载?WebSocket Push / 轮询 / 下载中心页?MVP 阶段建议使用轮询+下载链接 Toast | 导出按钮 §2.1.3 | 后端 + 产品 | | 8 | 批量合并客源:需要独立的合并规则弹窗(选择主记录 + 字段合并规则),复杂度高,是否降级到 P2? | 工具栏批量操作 | 产品经理 | | 9 | 转成交弹窗中「成交方」人员选择器:默认带入当前用户所属门店,支持修改的范围是全司还是当前用户权限内? | 转成交弹窗 §3.4 | 产品 + 后端 | | 10 | 活跃度标签「营销客」「销售客」「访客」的触发条件(截图可见但 DATA_MODEL_CLIENT.md 中的 `activity_level` 枚举不含这三项):这些是 `source` 字段衍生的展示标签,还是独立的 `activity_level` 值?需后端澄清 | 活跃度标签渲染 §2.1.3 | 后端 |