Files
nexus/Project/fonrey/UI_DESIGN/客源_UI设计.md

58 KiB
Raw Blame History

客源列表 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.2 页面清单
    • 1.3 用户角色与权限差异
  2. 页面设计规范
    • 2.1 客源列表主页(私客视图)
  3. 弹窗/抽屉设计规范
    • 3.1 改等级弹窗
    • 3.2 改状态弹窗
    • 3.3 转公客弹窗
    • 3.4 转成交弹窗
    • 3.5 转无效弹窗
    • 3.6 收藏夹选择弹窗
  4. 交互状态规范
    • 4.1 客源状态机
    • 4.2 权限控制矩阵
    • 4.3 HTMX 请求规范
  5. 关键数据字段说明
  6. 竞品截图对应关系
  7. 实现优先级与工期估算
  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 paramstab, 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 栏右侧)

<div class="flex items-center gap-4 text-sm">
  <span class="text-neutral-500">
    私客与成交客重复:
    <a href="/clients/duplicates/transacted/" 
       class="text-info-600 hover:underline font-medium">{{ dup_transacted_count }}</a>
  </span>
  <span class="text-neutral-500">
    私客与公客重复:
    <a href="/clients/duplicates/public/"
       class="text-info-600 hover:underline font-medium">{{ dup_public_count }}</a>
  </span>
  <a href="/clients/deleted/" class="text-neutral-500 hover:text-neutral-700">
    已删客源
  </a>
  <a href="/clients/private/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>

截图差异说明:竞品截图中「+ 新增私客」为橙色按钮(#F97316Fonrey 使用主色 Tealbg-primary-600),这是品牌差异,无需对齐竞品色。


[二级 Tab 导航区]

属性 说明
组件 Tab Navigation§10 Tab NavigationPill 变体
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 遵循系统主色 Tealbg-primary-50 text-primary-700 替代橙色激活态。「全部私客」Tab 在截图中有橙色边框高亮——映射为 Fonrey 的 bg-primary-600 text-white(当激活)。

<!-- 二级 Tab 容器Alpine.js 管理本地激活 tabHTMX 负责数据刷新 -->
<div x-data="{ activeTab: '{{ active_tab|default:\"all\" }}' }"
     class="flex items-center gap-1 p-1 bg-neutral-100 rounded-lg w-fit">
  <template x-for="tab in tabs" :key="tab.key">
    <button
      :class="activeTab === tab.key 
        ? 'bg-white text-primary-700 shadow-sm font-semibold' 
        : 'text-neutral-600 hover:bg-white/60'"
      class="px-4 py-1.5 text-sm rounded-md transition-all"
      @click="activeTab = tab.key"
      hx-get="/clients/private/"
      :hx-vals="JSON.stringify({tab: tab.key})"
      hx-target="#client-list-container"
      hx-swap="innerHTML"
      hx-push-url="true">
      <span x-text="tab.label"></span>
      <span x-show="tab.count !== null"
            class="ml-1.5 bg-neutral-200 text-neutral-600 text-xs 
                   px-1.5 py-0.5 rounded-full"
            x-text="tab.count"></span>
    </button>
  </template>
</div>

[搜索区域]

属性 说明
组件 搜索输入框 + 下拉范围选择
容器 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-glassbg-primary-600 text-white w-9 h-9 rounded-lg
已存搜索 搜索框右侧:「✦ N条已存搜索 ▾」下拉展开历史搜索条件列表
收起/展开筛选 最右侧文字链接:「收起筛选 ∧」/ 「展开筛选 Alpine.js 控制筛选区展开状态
<div class="bg-white rounded-lg border border-neutral-200 px-4 py-3 mt-3"
     x-data="{ showFilters: true }">
  
  <!-- 搜索行 -->
  <div class="flex items-center gap-3">
    <!-- 范围选择器 -->
    <select class="text-sm text-neutral-600 border border-neutral-300 
                   rounded-lg px-3 py-2 bg-white focus-visible:ring-2 
                   focus-visible:ring-primary-600/40">
      <option value="all">客户信息</option>
      <option value="client_no">客源编号</option>
    </select>
    
    <!-- 搜索框 -->
    <div class="relative flex-1 max-w-md">
      <input type="search" 
             name="q"
             placeholder="输入客源姓名/号码/号码后4位/客源编号/备注信息"
             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="/clients/private/"
             hx-trigger="keyup changed delay:300ms, search"
             hx-target="#client-table-body"
             hx-swap="innerHTML"
             hx-include="[name='tab'],[name='status'],[name='grade'],[name='page_size']">
      <button type="submit" 
              class="absolute right-1 top-1/2 -translate-y-1/2 
                     bg-primary-600 hover:bg-primary-700 text-white 
                     w-8 h-8 rounded-md flex items-center justify-center">
        <svg class="w-4 h-4"><!-- magnifying-glass --></svg>
      </button>
    </div>
    
    <!-- 已存搜索 -->
    <div x-data="{ open: false }" class="relative">
      <button @click="open = !open"
              class="text-sm text-neutral-500 hover:text-neutral-700 
                     flex items-center gap-1">
        <svg class="w-4 h-4"><!-- bookmark --></svg>
        <span>{{ saved_search_count }}条已存搜索</span>
        <svg class="w-3 h-3"><!-- chevron-down --></svg>
      </button>
      <!-- 下拉内容saved searches list -->
      <div x-show="open" x-cloak
           class="absolute left-0 top-full mt-1 w-64 bg-white shadow-lg 
                  border border-neutral-200 rounded-lg z-50 py-1">
        <!-- 历史搜索条目循环 -->
      </div>
    </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>
      <svg class="w-4 h-4 transition-transform" 
           :class="showFilters ? 'rotate-180' : ''"><!-- chevron-down --></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.5">
    <!-- 快捷筛选行 / 分组筛选行... 见下方 -->
  </div>
</div>

[快捷筛选行]

属性 说明
位置 筛选区第一行,标签「常用」
筛选项 即将掉公Checkbox/ 录入时间(下拉)/ 与我相关Checkbox/ 我部门相关Checkbox
<div class="flex items-center gap-4 text-sm">
  <span class="text-neutral-400 text-xs w-6 shrink-0">常用</span>
  
  <!-- 即将掉公 -->
  <label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600">
    <input type="checkbox" name="expiring_soon" value="1"
           class="w-4 h-4 rounded accent-primary-600"
           hx-get="/clients/private/"
           hx-trigger="change"
           hx-target="#client-list-container"
           hx-swap="innerHTML"
           hx-include="closest form">
    即将掉公
  </label>
  
  <!-- 录入时间 -->
  <div class="flex items-center gap-1.5">
    <span class="text-neutral-600">录入时间</span>
    <select name="created_period"
            class="text-sm border-0 text-neutral-600 bg-transparent cursor-pointer
                   focus-visible:ring-0 hover:text-primary-600"
            hx-get="/clients/private/"
            hx-trigger="change"
            hx-target="#client-list-container"
            hx-swap="innerHTML"
            hx-include="closest form">
      <option value="">不限</option>
      <option value="today">今天</option>
      <option value="7d">最近7天</option>
      <option value="30d">最近30天</option>
      <option value="custom">自定义</option>
    </select>
  </div>
  
  <!-- 与我相关 -->
  <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="/clients/private/"
           hx-trigger="change"
           hx-target="#client-list-container"
           hx-swap="innerHTML"
           hx-include="closest form">
    与我相关
    <svg class="w-3.5 h-3.5 text-neutral-400"><!-- 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="/clients/private/"
           hx-trigger="change"
           hx-target="#client-list-container"
           hx-swap="innerHTML"
           hx-include="closest form">
    我部门相关
    <svg class="w-3.5 h-3.5 text-neutral-400"><!-- information-circle --></svg>
  </label>
</div>

[分组筛选区]

每个筛选组为一行,格式:[标签] [选项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 按钮通用样式

<!-- 非激活 -->
<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">
  选项文字
</button>

<!-- 激活橙色在竞品中Fonrey 用 primary-600 -->
<button class="px-3 py-1 text-xs bg-primary-600 text-white rounded-md 
               border border-primary-600">
  不限
</button>

价格自定义区间

<div class="flex items-center gap-1.5 mt-1">
  <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>

展开更多筛选(下方收起区)

  • 点击「展开 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 条后变为可点击)

<div x-data="{ selectedCount: 0 }" 
     class="flex items-center gap-2">
  
  <!-- 批量操作按钮:仅 selectedCount > 0 时高亮 -->
  <button :disabled="selectedCount === 0"
          :class="selectedCount > 0 
            ? 'text-neutral-700 hover:bg-neutral-100 border-neutral-300' 
            : '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-related-modal')">
    修改相关方
  </button>
  
  <button :disabled="selectedCount === 0" ...>修改来源</button>
  <button :disabled="selectedCount === 0" ...>删除客源</button>
  <button :disabled="selectedCount === 0" ...>合并客客</button>
  
  <!-- 总条数 -->
  <span class="text-sm text-neutral-500 ml-2"><strong class="text-neutral-800">{{ total_count }}</strong></span>
  
  <!-- 已选提示 -->
  <span x-show="selectedCount > 0" class="text-sm text-primary-600">
    已选 <span x-text="selectedCount"></span></span>
</div>

<!-- 右侧工具 -->
<div class="flex items-center gap-2">
  <!-- 导出§5 Export Button -->
  <button hx-post="/clients/private/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"><!-- arrow-down-tray --></svg>
    导出
  </button>
  
  <!-- 自定义列表§3 Column Visibility Panel -->
  <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"><!-- adjustments-horizontal --></svg>
    自定义列表
  </button>
</div>

[数据表格]

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

列规范(全部私客 / 求购私客视图)

列名 数据字段 宽度 排序 特殊渲染
复选框 w-10 px-4 <input type="checkbox"> 全选/单选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-circlew-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 phonetext-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

<span class="text-[11px] px-1.5 py-0.5 rounded-full font-medium bg-info-50 text-info-600">
  新配偶
</span>

状态 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 结构(关键片段)

<div id="client-list-container">
  <div class="rounded-lg border border-neutral-200 overflow-hidden bg-white mt-3">
    <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">
              姓名
            </th>
            <!-- 其他列头... 带排序的列头包含 hx-get 触发排序 -->
            <th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide cursor-pointer"
                hx-get="/clients/private/"
                hx-vals='{"sort": "viewing_count", "order": "{{ sort_order_toggle }}"}'
                hx-target="#client-list-container"
                hx-swap="innerHTML"
                hx-include="closest form">
              带看次数
              <svg class="inline w-4 h-4 text-neutral-300 ml-0.5"><!-- chevron-up-down --></svg>
            </th>
          </tr>
        </thead>
        <tbody id="client-table-body" class="divide-y divide-neutral-100 bg-white">
          {% for client in clients %}
          <tr class="hover:bg-neutral-50 transition-colors h-14"
              :class="selected.includes('{{ client.id }}') ? 'bg-primary-50' : ''">
            <td class="w-10 px-4">
              <input type="checkbox" :value="'{{ client.id }}'"
                     class="w-4 h-4 rounded accent-primary-600"
                     x-model="selected">
            </td>
            <td class="px-4 py-2">
              <div class="flex flex-col gap-0.5">
                <a href="/clients/private/{{ client.id }}/"
                   class="text-info-600 hover:underline font-medium text-sm truncate max-w-[160px]">
                  {{ client.contact_name }}
                </a>
                <!-- 活跃度标签行 -->
                <div class="flex items-center gap-1 flex-wrap">
                  {% if client.grade %}
                  <span class="text-[11px] text-neutral-500">{{ client.grade_display }}</span>
                  {% endif %}
                  {% if client.activity_level %}
                  <span class="text-[11px] px-1.5 py-0.5 rounded-full font-medium 
                               {{ client.activity_level_class }}">
                    {{ client.activity_level_display }}
                  </span>
                  {% endif %}
                </div>
              </div>
            </td>
            <!-- 其他列... -->
            <td class="px-4 py-2">
              <button class="text-primary-600 hover:text-primary-700 p-1.5 rounded-md hover:bg-primary-50"
                      title="拨号">
                <svg class="w-5 h-5"><!-- phone --></svg>
              </button>
            </td>
          </tr>
          {% endfor %}
        </tbody>
      </table>
    </div>
  </div>
</div>

求租 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 批量操作 + 导出 + 统计 批量按钮默认 disabledselectedCount > 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 「录入时间」「委托日期」「跟进时间」筛选 集成 Flatpickrrange 模式
Multi-select Tag Input §17 Multi-select Tag Input 位置(多选区)、等级(多选)筛选 无需 Tag Input 样式,直接用按钮多选组
Modal Dialog §7 Modal Dialog 改等级/改状态/转公客/转成交/转无效弹窗 见第3章各弹窗规范

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"><!-- user-group --></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="/clients/private/"
          hx-target="#client-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"><!-- user-plus --></svg>
  <p class="text-neutral-600 text-base font-medium">还没有私客</p>
  <p class="text-neutral-400 text-sm mt-1.5">开始录入您的第一位私客</p>
  <a href="/clients/private/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"><!-- plus --></svg>
    新增私客
  </a>
</div>

2.1.6 Loading 状态

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

<!-- 骨架屏5行占位 -->
<div class="htmx-indicator animate-pulse space-y-0 
            rounded-lg border border-neutral-200 overflow-hidden">
  {% for i in "12345" %}
  <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"></div>
    <div class="h-4 bg-neutral-200 rounded w-32"></div>
    <div class="h-4 bg-neutral-200 rounded w-16 ml-8"></div>
    <div class="h-4 bg-neutral-200 rounded w-48 ml-4"></div>
    <div class="h-4 bg-neutral-200 rounded w-16"></div>
    <div class="ml-auto h-4 bg-neutral-200 rounded w-20"></div>
  </div>
  {% endfor %}
</div>

HTMX loading 触发:在 <div id="client-list-container"> 添加 hx-indicator="#client-skeleton";骨架屏默认隐藏,请求期间通过 htmx-request class 显示。


3. 弹窗/抽屉设计规范

3.1 改等级弹窗P0 🔴

3.1.1 触发方式

  • 触发位置:客源详情页右侧信息概览面板「改等级」快捷按钮(本文档中列表页行内操作暂无此入口,详情页触发)
  • 组件类型Modal Dialog选择理由操作简单2个字段Drawer 过重)
  • 尺寸max-w-sm384px
  • 竞品截图:竞品中改等级为小弹窗形式,与 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 属性
<form hx-post="/clients/private/{{ client.id }}/grade/"
      hx-target="#client-info-panel"
      hx-swap="innerHTML"
      hx-on::after-request="if(event.detail.successful) { $dispatch('close-modal'); $dispatch('show-toast', {message: '等级已更新', type: 'success'}); }">
  ...
  <button type="submit" :disabled="!newGrade"
          :class="newGrade ? 'bg-primary-600 hover:bg-primary-700' : 'bg-neutral-200 cursor-not-allowed'"
          class="px-4 py-2 text-white text-sm font-medium rounded-lg">
    确定
  </button>
</form>

3.1.4 使用的特殊组件

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

Alpine.js 管理:弹窗开关(opennewGrade 绑定,确定按钮 disabled 状态。


3.2 改状态弹窗P0 🔴

3.2.1 触发方式

  • 触发位置:客源详情页信息概览面板「改状态」按钮
  • 组件类型Modal Dialog
  • 尺寸max-w-sm384px

3.2.2 表单字段规范

字段名 组件类型 必填 校验规则 默认值/预填值
原状态(只读) 只读文本 当前状态,如「求购」
新状态 Select 必选;不能与原状态相同 无默认值
等级 Select 当前等级值(可修改)
更改理由 Textarea 最少 1 字,最多 200 字

新状态选项:求购 / 求租 / 租购

3.2.3 提交行为

  • 提交方式hx-post="/clients/private/{id}/status/"
  • 成功响应:关闭弹窗 + Toast「状态已更新」+ 刷新客源信息概览面板
  • 失败响应422字段级红字提示Textarea 下方显示「请填写更改理由」)
  • HTMX 属性
<form hx-post="/clients/private/{{ client.id }}/status/"
      hx-target="#client-info-panel"
      hx-swap="innerHTML"
      hx-on::after-request="if(event.detail.successful) closeModalAndToast('状态已更新')">

「确定」按钮: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 必选 当前等级值

弹窗标题区下方显示橙色警告提示:

<div class="bg-warning-50 border border-warning-200 rounded-md px-3 py-2 text-sm text-warning-600 mb-4">
  转为公客后将无法撤销,该客源将进入公共客源池,全员可见可跟进。
</div>

3.3.3 提交行为

  • 提交方式hx-post="/clients/private/{id}/to-public/"
  • 成功响应:关闭弹窗 + Toast「已转为公客」+ 跳转至私客列表页(window.location.href = '/clients/private/'
  • HTMX 属性
<form hx-post="/clients/private/{{ client.id }}/to-public/"
      hx-on::after-request="if(event.detail.successful){ showToast('已转为公客'); window.location.href='/clients/private/'; }">

3.3.4 使用的特殊组件

组件名 来源 用途
Modal Dialog §7 Modal Dialog 弹窗容器,含不可逆警告

3.4 转成交弹窗P0 🔴

3.4.1 触发方式

  • 触发位置:客源详情页信息概览面板「转成交」按钮
  • 组件类型Modal Dialog需录入成交信息字段较多
  • 尺寸max-w-lg512px

3.4.2 表单字段规范

字段名 组件类型 必填 校验规则 默认值/预填值
状态 Radio我购 / 我租) 我购
房源类型 Radio二手 / 新房) 二手
成交房源 房源选择器(链接按钮 → 弹出选择浮层) 必选1套
成交日期 Date Picker 不能晚于今日 当日
成交价格 Number Input单位万元 > 0
成交方 人员选择器 当前登录用户所属门店

成交房源选择器:点击「+ 选择成交房源」按钮,弹出独立 Modalmax-w-4xl),包含:

  • 搜索框(房源编号/楼盘地址/业主姓名/电话)
  • 筛选栏(区域/状态/相关方/部门)
  • 表格(房源名称、交易类型、状态、用途、城区商圈、房型、楼层、面积)
  • 单选Radio per row
  • 分页50条/页共89704条
  • 底部「已选(0)」+ 「确定」按钮

3.4.3 提交行为

  • 提交方式hx-post="/clients/private/{id}/to-transacted/"
  • 成功响应:关闭弹窗 + Toast「已标记为成交」+ 跳转详情页刷新状态
  • 失败响应422:字段级错误提示
  • HTMX 属性
<form hx-post="/clients/private/{{ client.id }}/to-transacted/"
      hx-target="body"
      hx-swap="none"
      hx-on::after-request="handleTransactedResponse(event)">

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 表单字段规范

弹窗顶部蓝色信息提示框:

<div class="bg-info-50 border border-info-200 rounded-md px-3 py-2 text-sm text-info-600 mb-4">
  该功能为整体客转无效,将把所有电话标记无效
</div>
字段名 组件类型 必填 校验规则 默认值/预填值
无效原因 Radio List单选 必选1项 号码无效(第一项)

Radio 选项:

  • ⦿ 号码无效(默认)
  • ○ 同行中介
  • ○ 广告推销
  • ○ 客户无意向
  • ○ 其他

Radio HTML

<fieldset class="space-y-2">
  {% for reason in invalid_reasons %}
  <label class="flex items-center gap-3 p-3 border border-neutral-200 rounded-lg 
                cursor-pointer hover:bg-neutral-50"
         :class="selectedReason === '{{ reason.value }}' ? 'border-primary-400 bg-primary-50' : ''">
    <input type="radio" name="invalid_reason" value="{{ reason.value }}"
           x-model="selectedReason"
           class="w-4 h-4 accent-primary-600">
    <span class="text-sm text-neutral-700">{{ reason.label }}</span>
  </label>
  {% endfor %}
</fieldset>

3.5.3 提交行为

  • 提交方式hx-post="/clients/private/{id}/invalidate/"
  • 成功响应:关闭弹窗 + Toast「已标记为无效」+ 从私客列表移除该行(列表局部刷新)
  • HTMX 属性
<form hx-post="/clients/private/{{ client.id }}/invalidate/"
      hx-target="#client-list-container"
      hx-swap="innerHTML"
      hx-on::after-request="if(event.detail.successful) closeModal()">

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字

收藏夹列表:

<div class="space-y-1 max-h-48 overflow-y-auto">
  {% for folder in folders %}
  <label class="flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer 
                hover:bg-neutral-50">
    <input type="radio" name="folder_id" value="{{ folder.id }}"
           x-model="selectedFolder"
           class="w-4 h-4 accent-primary-600">
    <span class="text-sm text-neutral-700">{{ folder.name }}</span>
    {% if folder.is_default %}
    <span class="text-xs text-neutral-400">默认</span>
    {% endif %}
  </label>
  {% endfor %}
</div>

<!-- 新建收藏夹(内联交互) -->
<div class="mt-3 pt-3 border-t border-neutral-100">
  <template x-if="!creatingFolder">
    <button @click="creatingFolder = true"
            class="text-sm text-info-600 hover:underline">
      + 创建收藏夹
    </button>
  </template>
  <template x-if="creatingFolder">
    <div class="flex items-center gap-2">
      <input type="text" x-model="newFolderName" maxlength="10"
             placeholder="请输入名称最多10个字"
             class="flex-1 px-3 py-1.5 text-sm border border-neutral-300 
                    rounded-md focus-visible:ring-2 focus-visible:ring-primary-600/40">
      <button :disabled="!newFolderName || newFolderName.length > 10"
              :class="newFolderName && newFolderName.length <= 10 
                ? 'bg-primary-600 text-white' : 'bg-neutral-200 text-neutral-400'"
              class="px-3 py-1.5 text-sm rounded-md"
              hx-post="/clients/folders/create/"
              hx-target="#folder-list"
              hx-swap="innerHTML"
              hx-vals="js:{name: newFolderName}"
              hx-on::after-request="creatingFolder = false; newFolderName = ''">
        创建
      </button>
      <button @click="creatingFolder = false" class="text-sm text-neutral-500">取消</button>
    </div>
  </template>
</div>

3.6.3 提交行为

  • 提交方式hx-post="/clients/private/{id}/favorite/"
  • 成功响应:关闭弹窗 + 信息面板「收藏」图标变为实心星 ★(text-warning-600
  • HTMX 属性
<form hx-post="/clients/private/{{ client.id }}/favorite/"
      hx-target="#favorite-icon"
      hx-swap="outerHTML"
      hx-on::after-request="if(event.detail.successful) closeModal()">

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 切换 clickAlpine.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
批量修改相关方 submitModal内 PATCH /clients/private/batch/related/ #client-list-container innerHTML 按钮 loading

按钮 Loading 实现(用于提交按钮):

<button type="submit"
        class="..."
        hx-disabled-elt="this"
        _="on htmx:beforeRequest add .opacity-75 to me
           on htmx:afterRequest remove .opacity-75 from me">
  <span class="htmx-indicator">
    <svg class="animate-spin w-4 h-4"><!-- spinner --></svg>
  </span>
  确定
</button>

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-400Fonrey 对应为 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 天
导出 ExcelCelery 异步) 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 后端