1104 lines
61 KiB
Markdown
1104 lines
61 KiB
Markdown
# 客源列表 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. [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)
|
||
- 关键词搜索(姓名/号码/号码后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
|
||
<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>
|
||
```
|
||
|
||
> **截图差异说明**:竞品截图中「+ 新增私客」为橙色按钮(`#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
|
||
<!-- 二级 Tab 容器,Alpine.js 管理本地激活 tab,HTMX 负责数据刷新 -->
|
||
<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-glass`,`bg-primary-600 text-white w-9 h-9 rounded-lg` |
|
||
| 已存搜索 | 搜索框右侧:「✦ N条已存搜索 ▾」下拉展开历史搜索条件列表 |
|
||
| 收起/展开筛选 | 最右侧文字链接:「收起筛选 ∧」/ 「展开筛选 ∨」,Alpine.js 控制筛选区展开状态 |
|
||
|
||
```html
|
||
<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) |
|
||
|
||
```html
|
||
<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急迫/B较强/C一般/D较弱/E暂不关注/未填写 |
|
||
| 位置 | 同上 | 单选Tag 按钮组 | 宝山/崇明/...共19个区, |
|
||
| 区域 | 同上 | 单选Tag按钮组 | 位置选择后出现二级区域选择 |
|
||
| 购价(求购Tab) | 同上 | 单选 Tag + 自定义区间输入 | 预设区间 + 「最小值」「~」「最大值」万 |
|
||
| 租价(求租Tab) | 同上 | 单选 Tag + 自定义区间输入 | 预设区间 + 「最小值」「~」「最大值」元 |
|
||
| 居室 | 同上 | 单选 Tag + 「是大价值」复选框(求购) | 不限/1居/2居/3居/4居/5居及以上 |
|
||
| 筛选(展开) | 同上 | 各类下拉/DateRange | 含「展开 ∨」按钮;相关方/委托日期/来源/购房目的/带看进度/活跃情况/是否有效/面积/跟进时间/带看时间/审批中/审批驳回/收藏客源 |
|
||
| 筛选(收起后隐藏) | 同上 | 各类 Checkbox/下拉 | 保护客/合作者/偏好新房/巧客力访客/置顶/用途/带看次数 |
|
||
|
||
**Tag 按钮通用样式**:
|
||
```html
|
||
<!-- 非激活 -->
|
||
<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>
|
||
```
|
||
|
||
**价格自定义区间**:
|
||
```html
|
||
<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 条后变为可点击)**:
|
||
|
||
```html
|
||
<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-container`(HTMX swap 目标),内嵌 `client-table-body` |
|
||
| 行高 | 56px(`h-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-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
|
||
<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 结构(关键片段)**:
|
||
```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 | 批量操作 + 导出 + 统计 | 批量按钮默认 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 | 自定义列弹窗 | 见§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"><!-- 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>
|
||
```
|
||
|
||
**首次进入无数据(无任何客源)**:
|
||
|
||
```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"><!-- 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` 内显示骨架屏:
|
||
|
||
```html
|
||
<!-- 骨架屏: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. 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" id="select-all">`;单选:行内 `<input type="checkbox" x-model="selected" :value="client.id">` |
|
||
| 2 | 姓名 | `contact_name` + `grade_display` + 活跃度标签 | `min-w-[160px] max-w-[200px]` | 左对齐 | 否 | 蓝色链接 `text-info-600 hover:underline`;下方渲染 Grade Badge + 活跃度 Tag(见 §3.2) |
|
||
| 3 | 状态 | `status_display` | `w-20`(80px) | 左对齐 | 否 | Status Badge(见 §3.2) |
|
||
| 4 | 需求类型 | `requirement_type_display` | `w-20`(80px) | 左对齐 | 否 | 纯文字:二手 / 新房 / 租房 |
|
||
| 5 | 需求/解读 | `budget_area_display` | `min-w-[180px]` | 左对齐 | 否 | 截断 `truncate`;Tooltip 展示完整内容(`title` 属性) |
|
||
| 6 | 智能配房 | `match_count` | `w-24`(96px) | 左对齐 | 否 | `N套` + Heroicon `information-circle`(`w-4 h-4 text-neutral-400 ml-1 cursor-pointer`);点击弹出配房预览 Popover |
|
||
| 7 | 意向商圈/小区 | `intent_location_display` | `min-w-[120px]` | 左对齐 | 否 | 多值逗号分隔;`-` 表示未填;截断 `truncate max-w-[160px]` |
|
||
| 8 | 归属人 | `owner_display` | `min-w-[140px]` | 左对齐 | 否 | 格式:`姓名-门店组`;`text-sm text-neutral-700` |
|
||
| 9 | 带看进度 | `viewing_progress_display` | `w-20`(80px) | 左对齐 | 否 | 「未带看」灰色文字;「一看」`bg-warning-50 text-warning-600 px-2 py-0.5 rounded-full text-xs`;「二看」`bg-info-50 text-info-600`;「复看」`bg-success-50 text-success-600` |
|
||
| 10 | 带看次数 | `viewing_count` | `w-[72px]` | 左对齐 | 是 | `N次`;点击列头触发 HTMX 排序 |
|
||
| 11 | 委托日期 | `commission_date` | `w-24`(96px) | 左对齐 | 是 | `YYYY-MM-DD`;未填显示 `-` |
|
||
| 12 | 最近时间 | `last_follow_display` / `last_contact_display` | `w-24`(96px) | 左对齐 | 是(默认降序) | `N天前` / `今天`;超过 30 天字色 `text-danger-600` |
|
||
| 13 | 操作 | — | `w-16`(64px) | 居中 | 否 | 见 §3.3 |
|
||
|
||
> **自定义列**(P1 🟡):用户通过「自定义列表」弹窗(§4.1)选择显示字段后,后端将用户配置存入 `UserColumnPreference`,Django 模板根据配置动态渲染列头和列单元格。可选字段见截图 `客源列表-自定义字段.png`:录入日期、最近通话日期、用途、来源、客源编号、首录人、成交人等。
|
||
|
||
---
|
||
|
||
### 3.2 列状态变体
|
||
|
||
**活跃度标签(姓名列下方,行内渲染,多个并排)**
|
||
|
||
| 标签值 | 显示文字 | 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
|
||
<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` |
|
||
|
||
**排序列头**(带看次数 / 委托日期 / 最近时间):
|
||
```html
|
||
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide
|
||
cursor-pointer hover:bg-neutral-100 select-none whitespace-nowrap"
|
||
hx-get="/clients/private/"
|
||
:hx-vals="JSON.stringify({sort: 'viewing_count', order: currentOrder})"
|
||
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>
|
||
<!-- 升序激活:向上箭头 text-primary-600 -->
|
||
<!-- 降序激活:向下箭头 text-primary-600 -->
|
||
</th>
|
||
```
|
||
|
||
**行选中态**:
|
||
```html
|
||
<tr class="hover:bg-neutral-50 transition-colors h-14"
|
||
:class="selected.includes(client.id) ? 'bg-primary-50 hover:bg-primary-100' : ''">
|
||
```
|
||
|
||
---
|
||
|
||
### 3.3 操作列
|
||
|
||
操作列宽 `w-16`(64px),居中对齐,固定在表格最右侧。
|
||
|
||
MVP 阶段操作列仅包含一个「拨号」图标按钮:
|
||
|
||
```html
|
||
<td class="px-3 py-2 text-center">
|
||
<button class="inline-flex items-center justify-center w-8 h-8 rounded-md
|
||
text-primary-600 hover:bg-primary-50 hover:text-primary-700
|
||
transition-colors"
|
||
title="拨号"
|
||
@click="$dispatch('open-dial-modal', { clientId: '{{ client.id }}', phone: '{{ client.contact_phone_masked }}' })">
|
||
<svg class="w-5 h-5"><!-- heroicon: phone --></svg>
|
||
</button>
|
||
</td>
|
||
```
|
||
|
||
> 后续迭代可在操作列增加「更多操作」下拉菜单(`...` 图标),包含跟进记录、收藏、转无效等快捷操作,但 MVP 阶段不实现,避免列表行交互过重。
|
||
|
||
---
|
||
|
||
### 3.4 表格交互状态
|
||
|
||
| 状态 | 触发场景 | 视觉表现 |
|
||
|---|---|---|
|
||
| 默认(无选中) | 页面加载完毕 | 所有行 `bg-white`,hover 时 `bg-neutral-50` |
|
||
| 行选中 | 勾选复选框 | `bg-primary-50 hover:bg-primary-100`;工具栏批量操作按钮激活 |
|
||
| 全选 | 点击表头复选框 | 当前页所有行选中;表头 checkbox `indeterminate` 或 `checked` |
|
||
| Loading(HTMX 请求中) | 筛选/分页/排序触发 | 骨架屏覆盖 `#client-list-container`(见 §2.1.6) |
|
||
| 空状态(无数据) | 筛选无结果 / 首次进入 | 见 §2.1.5 空状态设计 |
|
||
| 置顶行 | `is_pinned = true` | 行头部显示 `⬆` 或 `bg-warning-50/30` 浅黄底色(产品待确认)|
|
||
|
||
---
|
||
|
||
## 4. 弹窗设计规范(列表页)
|
||
|
||
> **范围说明**:本章仅包含从**客源列表页直接触发**的弹窗。改等级、改状态、转公客、转成交、转无效、收藏夹等操作弹窗从**客源详情页**触发,应记录于详情页 UI 设计文档。
|
||
|
||
### 4.1 自定义列弹窗(P1 🟡)
|
||
|
||
#### 4.1.1 触发方式
|
||
|
||
- **触发位置**:工具栏右侧「自定义列表」按钮(Heroicon `adjustments-horizontal` + 文字)
|
||
- **组件类型**:Modal Dialog(`组件规范设计.md` §7)
|
||
- **尺寸**:`max-w-2xl`(640px)
|
||
- **竞品参考截图**:`Project/fonrey/screenshots/客源/客源列表-自定义字段.png`
|
||
|
||
#### 4.1.2 弹窗布局
|
||
|
||
弹窗分为左右两栏:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ 标题:自定义信息 [×] │
|
||
├────────────────────────┬────────────────────────────┤
|
||
│ 未选信息 │ 已选信息 │
|
||
│ (可勾选字段列表) │ (已选字段,拖拽排序) │
|
||
│ │ ┌─────────────────────┐ │
|
||
│ □ 录入日期 │ │ ⋮⋮ 姓名 [删] │ │
|
||
│ □ 最近通话日期 │ │ ⋮⋮ 状态 [删] │ │
|
||
│ □ 用途 │ │ ⋮⋮ 需求类型 [删] │ │
|
||
│ □ 来源 │ │ ⋮⋮ 需求/解读 [删] │ │
|
||
│ □ 客源编号 │ │ ... │ │
|
||
│ □ 首录人 │ └─────────────────────┘ │
|
||
│ □ 成交人 │ 提示:拖拽可调整展示顺序 │
|
||
├────────────────────────┴────────────────────────────┤
|
||
│ [恢复默认] [取消] [确定] │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### 4.1.3 字段说明
|
||
|
||
**未选信息区(左栏)**:
|
||
- 显示所有可用但当前未选中的字段
|
||
- 每项为 Checkbox + 字段名,点击勾选后字段移至右栏「已选信息」
|
||
- 可用字段(参考截图):录入日期、最近通话日期、用途、来源、客源编号、首录人、成交人
|
||
|
||
**已选信息区(右栏)**:
|
||
- 显示当前已选中的字段列表,带序号/排序手柄(`⋮⋮` Heroicon `bars-2`)
|
||
- 拖拽排序(SortableJS 或原生 HTML5 drag-and-drop)
|
||
- 每行末尾「删除」图标(Heroicon `x-mark`,`text-neutral-400 hover:text-danger-600`)点击将字段移回左栏
|
||
- 固定字段(如「姓名」)不可删除(无删除图标,显示锁形图标 `lock-closed`)
|
||
|
||
#### 4.1.4 底部操作
|
||
|
||
| 按钮 | 位置 | 行为 |
|
||
|---|---|---|
|
||
| 恢复默认 | 底部左侧 | 重置为系统默认列配置;Alpine.js 本地状态重置,不立即提交 |
|
||
| 取消 | 底部右侧次按钮 | 关闭弹窗,不保存 |
|
||
| 确定 | 底部右侧主按钮 | `hx-post="/clients/column-preferences/"` 保存配置 → 关闭弹窗 → HTMX 刷新 `#client-list-container` 以重新渲染列 |
|
||
|
||
#### 4.1.5 提交行为
|
||
|
||
```html
|
||
<form hx-post="/clients/column-preferences/"
|
||
hx-target="#client-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)">
|
||
...
|
||
<button type="submit"
|
||
class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white
|
||
text-sm font-medium rounded-lg transition-colors">
|
||
确定
|
||
</button>
|
||
</form>
|
||
```
|
||
|
||
#### 4.1.6 使用的特殊组件
|
||
|
||
| 组件名 | 来源 | 用途 |
|
||
|---|---|---|
|
||
| Modal Dialog | §7 Modal Dialog | 弹窗容器,`max-w-2xl` |
|
||
|
||
Alpine.js 管理:`selectedColumns`(有序数组)、`availableColumns`(剩余可选)、`dragging` 状态;拖拽排序通过 Alpine.js + SortableJS CDN 实现。
|
||
|
||
## 5. 交互状态规范
|
||
|
||
### 5.1 全局状态机
|
||
|
||
**客源状态流转**(适用于列表页行内操作和详情页操作):
|
||
|
||
```
|
||
buying(求购) ←→ suspended(暂缓)
|
||
renting(求租) ↘
|
||
buy_or_rent(租购)→ public(公客)[不可逆]
|
||
→ bought(已购)[不可逆]
|
||
→ rented_done(已租)[不可逆]
|
||
→ invalid(无效)[需审批可恢复]
|
||
```
|
||
|
||
视觉呈现:在列表状态列、详情页标题区用 Badge 展示;状态变更后相关 Badge 实时 HTMX 局部刷新。
|
||
|
||
### 5.2 权限控制矩阵
|
||
|
||
| 操作 | 经纪人(自己的客源) | 店长 | 管理员 |
|
||
|---|---|---|---|
|
||
| 查看列表 | 仅自己名下 | 本门店 | 全司 |
|
||
| 新增私客 | ✅ | ✅ | ✅ |
|
||
| 删除私客(批量) | ✅(仅自己) | ✅(本门店) | ✅ |
|
||
| 合并客源 | ❌ | ✅ | ✅ |
|
||
| 改等级 | ✅(自己的) | ✅ | ✅ |
|
||
| 改状态 | ✅(自己的) | ✅ | ✅ |
|
||
| 转公客 | ✅(归属人/首录人) | ✅ | ✅ |
|
||
| 转成交 | ✅(归属人) | ✅ | ✅ |
|
||
| 转无效 | ✅(归属人/首录人) | ✅ | ✅ |
|
||
| 导出 | 仅自己数据 | 本门店 | 全量 |
|
||
| 查看号码 | ✅(需审计日志) | ✅ | ✅ |
|
||
| 修改相关员工(跨店) | ❌ | ✅ | ✅ |
|
||
|
||
权限控制实现:Django 视图层通过 `request.user` 的 role 和 org_unit 过滤 QuerySet;前端通过 Django 模板 `{% if user.role == 'manager' %}` 条件渲染隐藏不可用按钮(双重防护)。
|
||
|
||
### 5.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` | 骨架屏 |
|
||
| 导出 | `click` | `POST /clients/private/export/` | `body` | `none` | Toast 提示「正在生成,完成后下载」 |
|
||
| 批量删除 | `click` | `DELETE /clients/private/batch/` | `#client-list-container` | `innerHTML` | 按钮 loading |
|
||
| 批量修改相关方 | `submit`(Modal内) | `PATCH /clients/private/batch/related/` | `#client-list-container` | `innerHTML` | 按钮 loading |
|
||
| 保存自定义列 | `submit`(§4.1 弹窗内) | `POST /clients/column-preferences/` | `#client-list-container` | `innerHTML` | 按钮 loading spinner |
|
||
|
||
**按钮 Loading 实现**(用于提交按钮):
|
||
```html
|
||
<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>
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 关键数据字段说明
|
||
|
||
以下字段为客源列表页后端 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 | |
|
||
|
||
---
|
||
|
||
## 7. 竞品截图对应关系
|
||
|
||
| 截图路径 | 对应功能 | 对应文档章节 | 采纳的设计要点 |
|
||
|---|---|---|---|
|
||
| `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`。
|
||
|
||
---
|
||
|
||
## 8. 实现优先级与工期估算
|
||
|
||
| 页面/功能 | 优先级 | 特殊组件复杂度 | 工期估算(前端) |
|
||
|---|---|---|---|
|
||
| 一级 Tab + 二级 Tab 导航框架 | P0 🔴 | 低(Tab Navigation §10) | 0.5 天 |
|
||
| 搜索框 + 已存搜索 | P0 🔴 | 中(下拉交互) | 0.5 天 |
|
||
| 快捷筛选行 + 分组筛选条(含展开/收起) | P0 🔴 | 中(Alpine.js 管理展开状态 + HTMX) | 1.5 天 |
|
||
| 价格区间筛选(预设+自定义) | P0 🔴 | 低 | 0.5 天 |
|
||
| 工具栏(批量操作 + 总条数 + 导出) | P0 🔴 | 低(Toolbar §4) | 0.5 天 |
|
||
| Data Table 主体(列定义 + 活跃度标签渲染) | P0 🔴 | 高(§3 Data Table,多行单元格) | 2 天 |
|
||
| 分页栏(含跳页) | P0 🔴 | 低(Pagination §2) | 0.5 天 |
|
||
| 重复检测提示区 | P0 🔴 | 低 | 0.25 天 |
|
||
| 空状态设计 | P0 🔴 | 低 | 0.25 天 |
|
||
| 骨架屏 Loading | P0 🔴 | 低 | 0.25 天 |
|
||
| 已存搜索保存与调用 | P1 🟡 | 中 | 1 天 |
|
||
| 导出 Excel(Celery 异步) | P1 🟡 | 中(后端为主) | 0.5 天前端 |
|
||
| 自定义列弹窗(§4.1 Column Visibility) | P1 🟡 | 中(Modal + 拖拽排序) | 1.5 天 |
|
||
| **合计 P0** | | | **约 6.25 天** |
|
||
| **合计 P1** | | | **约 3 天** |
|
||
|
||
---
|
||
|
||
## 9. 开放问题(待决策)
|
||
|
||
| # | 问题 | 影响范围 | 待确认方 | 回答 |
|
||
| --- | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------- | ----------- |
|
||
| 1 | 「资料客」「营销客」Tab 在 MVP 阶段是否展示为灰色禁用 Tab,还是直接不显示? | 一级 Tab 导航 §2.1.3 | 产品经理 | 展示为灰色禁用 |
|
||
| 2 | 二级 Tab 上的客源数量 Badge(如「求购 913」)是否实时计数?若是,是否有性能开销?建议改为后端分 Tab 预聚合或缓存 | 二级 Tab §2.1.3 | 后端 + 产品 | 改为后端分Tab预聚合 |
|
||
| 3 | 「与我相关」和「我部门相关」的精确业务定义:经纪人同时是首录人和归属人时,「与我相关」指 `owner_id=me OR first_recorder_id=me`?还是仅 `owner_id=me`? | 快捷筛选行 §2.1.3 | 产品经理 | owner_id=me |
|
||
| 4 | 「即将掉公」筛选的时间阈值(距自动转公还有多少天开始提示)是运营后台可配置项还是硬编码?需要前端在筛选行旁边展示剩余天数吗? | 快捷筛选行 | 产品 + 后端 | 后台可配置 |
|
||
| 5 | 价格筛选的自定义区间输入:用户手动输入后是否需要点击「搜索」按钮才触发,还是 blur 后自动 HTMX?(与其他 Tag 筛选项行为需统一) | 价格筛选 §2.1.3 | 产品经理 | blur后自动HTMX |
|
||
| 6 | 表格「最近时间」列:PRD 写的是「最近时间」(最近跟进或带看的距今天数),截图中显示「N天前」+ 日期(如`2026-04-19`)两行,是否需要双行展示? | 表格列定义 §2.1.3 | 产品经理(截图已有双行,建议对齐截图) | |
|
||
| 7 | 导出功能:Celery 异步生成后如何通知用户下载?WebSocket Push / 轮询 / 下载中心页?MVP 阶段建议使用轮询+下载链接 Toast | 导出按钮 §2.1.3 | 后端 + 产品 | 可在通知中心里显示消息 |
|
||
| 8 | 批量合并客源:需要独立的合并规则弹窗(选择主记录 + 字段合并规则),复杂度高,是否降级到 P2? | 工具栏批量操作 | 产品经理 | P0不做 |
|
||
| 9 | 转成交弹窗中「成交方」人员选择器:默认带入当前用户所属门店,支持修改的范围是全司还是当前用户权限内? | 详情页转成交弹窗(待详情页文档) | 产品 + 后端 | 全司 |
|
||
| 10 | 活跃度标签「营销客」「销售客」「访客」的触发条件(截图可见但 DATA_MODEL_CLIENT.md 中的 `activity_level` 枚举不含这三项):这些是 `source` 字段衍生的展示标签,还是独立的 `activity_level` 值?需后端澄清 | 活跃度标签渲染 §2.1.3 | 后端 | 后台可配置 |
|