734 lines
42 KiB
Markdown
734 lines
42 KiB
Markdown
# 编辑客源 UI 设计文档
|
||
|
||
> **版本**:v1.0 · **日期**:2026-04-26
|
||
> **依赖规范**:UI_SYSTEM.md v1.2 · 组件规范设计.md v1.0
|
||
> **PRD 来源**:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` §5.15 + Story 14
|
||
> **优先级**:P0 功能在本文档中用 🔴 标注,P1 用 🟡,P2 用 ⚫
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [模块概述](#1-模块概述)
|
||
- 1.1 功能范围
|
||
- 1.2 页面清单
|
||
- 1.3 用户角色与权限差异
|
||
2. [页面设计规范](#2-页面设计规范)
|
||
- 2.1 编辑客源主页面(三 Tab 表单)
|
||
3. [弹窗/抽屉设计规范](#3-弹窗抽屉设计规范)
|
||
- 3.1 编辑基础信息弹窗(快捷入口,来自信息概览面板)
|
||
4. [交互状态规范](#4-交互状态规范)
|
||
- 4.1 全局状态说明
|
||
- 4.2 权限控制矩阵
|
||
- 4.3 HTMX 请求规范
|
||
5. [关键数据字段说明](#5-关键数据字段说明)
|
||
6. [竞品截图对应关系](#6-竞品截图对应关系)
|
||
7. [实现优先级与工期估算](#7-实现优先级与工期估算)
|
||
8. [开放问题(待决策)](#8-开放问题待决策)
|
||
|
||
---
|
||
|
||
## 1. 模块概述
|
||
|
||
### 1.1 功能范围
|
||
|
||
编辑客源模块包含以下功能,按优先级分组:
|
||
|
||
**P0 — MVP 上线必须实现** 🔴
|
||
|
||
| 功能 | PRD 来源 |
|
||
|------|---------|
|
||
| 联系人 Tab:编辑主联系人及多联系人信息(姓名/称呼/电话1/电话2/微信/QQ/备注) | Story 14 §5.15.2 |
|
||
| 联系人 Tab:查看号码权限验证(点击「查看号码」后才能编辑电话) | §5.15.2 |
|
||
| 联系人 Tab:「标记无效」号码操作 | §5.15.2 |
|
||
| 联系人 Tab:「+ 添加联系人」追加联系人区块 | §5.15.2 |
|
||
| 基础信息 Tab:必填字段编辑(需求类型/用途/等级/来源) | §5.15.3 |
|
||
| 基础信息 Tab:选填字段编辑(购房目的/付款方式/名下房产/贷款记录/证件/意向学校/入学时间) | §5.15.3 |
|
||
| 二手 Tab:全量需求字段可编辑(总价/面积/居室/楼层/朝向/装修/楼龄/意向商圈/意向小区/交通/备注) | §5.15.4 |
|
||
| 全局保存/取消操作(表单底部内联按钮,保存成功返回详情页) | §5.15.5 |
|
||
| Tab 切换时保持各 Tab 表单数据不丢失 | §5.15.5 |
|
||
| 表单校验:必填字段红框高亮 + 滚动定位到首个错误 | §5.15.5 |
|
||
|
||
**P1 — 首迭代实现** 🟡
|
||
|
||
| 功能 | PRD 来源 |
|
||
|------|---------|
|
||
| 新房 Tab:新房需求字段编辑(待截图补充确认字段) | §5.15.4 注 |
|
||
| 租房 Tab:租房需求字段编辑(待截图补充确认字段) | §5.15.4 注 |
|
||
| 编辑基础信息弹窗(从信息概览面板「编辑客源」快捷入口触发,Story 22) | §5.23 |
|
||
|
||
### 1.2 页面清单
|
||
|
||
| 页面名称 | URL 模式建议 | 优先级 | 对应 PRD 章节 |
|
||
|---------|------------|--------|--------------|
|
||
| 编辑客源主页面 | `/clients/<client_id>/edit/` | P0 🔴 | §5.15 / Story 14 |
|
||
| 编辑基础信息弹窗 | 无独立 URL,HTMX 局部渲染 | P1 🟡 | §5.23 / Story 22 |
|
||
|
||
### 1.3 用户角色与权限差异
|
||
|
||
| 功能 | 经纪人(归属人/首录人) | 经纪人(非归属人) | 店长/管理员 |
|
||
|------|-------------------|-----------------|-----------|
|
||
| 进入编辑客源页 | ✅ 可进入 | ❌ 无权限 | ✅ 可进入 |
|
||
| 查看并编辑电话号码 | 需点击「查看号码」验证,通过后可编辑 | ❌ | ✅ |
|
||
| 「标记无效」号码 | ✅ | ❌ | ✅ |
|
||
| 编辑基础信息/需求信息 | ✅ | ❌ | ✅ |
|
||
| 添加/删除联系人 | ✅ | ❌ | ✅ |
|
||
| 「+ 添加联系人」按钮 | ✅ | ❌ | ✅ |
|
||
|
||
> **说明**:
|
||
> - 电话1 默认打码显示(`137****1234`),需权限验证后才能查看完整号码并进入编辑态。
|
||
> - 非归属人/非首录人访问 `/clients/<client_id>/edit/` 时,后端返回 403,前端重定向至详情页并展示 Toast "无权限编辑该客源"。
|
||
|
||
---
|
||
|
||
## 2. 页面设计规范
|
||
|
||
### 2.1 编辑客源主页面(P0 🔴)
|
||
|
||
#### 2.1.1 页面概述
|
||
|
||
- **URL**:`/clients/<client_id>/edit/`
|
||
- **访问入口**:
|
||
- 私客详情页右侧信息概览面板「编辑」文字按钮
|
||
- 私客详情页需求信息 Tab 右上角「编辑」蓝色文字链接(跳转到编辑页的「二手/新房/租房」Tab 激活态)
|
||
- **页面职责**:允许经纪人修改客源联系人信息、基础属性、购房/租房需求的全量字段
|
||
- **竞品参考截图**:`Project/fonrey/screenshots/客源/编辑客源.png`
|
||
|
||
#### 2.1.2 布局结构
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ 面包屑导航:客源 / 客源管理 / 编辑客源 │
|
||
│ 页面标题:编辑客源(text-xl font-semibold) │
|
||
├──────────────────────────────────────────────────────────┤
|
||
│ Tab 导航栏:[联系人] [基础信息] [二手/新房/租房] │
|
||
│ (Tab 下划线橙色激活 #F97316,对应竞品,非主色 Teal) │
|
||
├──────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 联系人 Tab 内容区 │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ [区块标题:联系人] [右上角:查看后可编辑号码 查看号码] [+添加联系人] │
|
||
│ │ │ │
|
||
│ │ 联系人 1 区块(白色卡片 rounded-lg) │ │
|
||
│ │ ● 姓名 [input] ● 电话1 [***打码] [标记无效] │ │
|
||
│ │ ● 称呼 ○先生 ○女士 电话2 [-] 微信 [-] │ │
|
||
│ │ QQ [-] │ │
|
||
│ │ 备注 [textarea] │ │
|
||
│ │ │ │
|
||
│ │ 联系人 2 区块(同上,右上角有「删除」红色链接) │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ─ ─ ─ ─ 以下为基础信息区块(同页,分 Section 渲染)─ ─ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ 基础信息 │ │
|
||
│ │ ● 需求类型 ☑二手 □新房 │ │
|
||
│ │ ● 用途 ○住宅 ○别墅 ○商住 ○商铺 ○写字楼 ○其他 │ │
|
||
│ │ ● 等级 ○A ○B ●C ○D ○E │ │
|
||
│ │ ● 来源 [下拉] │ │
|
||
│ │ 购房目的 □刚需 □投资 □学区 □改善 □商用 □其他 │ │
|
||
│ │ 付款方式 ○全额 ○商业贷款 ○商贷+公积金 ○公积金 │ │
|
||
│ │ 名下房产 ○无 ○本地无外地有 ○本地有房 │ │
|
||
│ │ 贷款记录 ○有 ○无 │ │
|
||
│ │ 证件类型 [下拉] 证件号码 [input] │ │
|
||
│ │ 意向学校1 [input] + 添加学校 │ │
|
||
│ │ 入学时间 [月份选择器] │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ 二手(需求信息区块,仅在「二手」Tab 或同页展示) │ │
|
||
│ │ ● 总价 [数字输入] - [数字输入] 万元 │ │
|
||
│ │ ● 面积 [数字输入] - [数字输入] m² │ │
|
||
│ │ ● 居室 □1居 □2居 □3居 □4居 □5居及以上 │ │
|
||
│ │ 楼层 □不要一层 □低楼层 □中楼层 □高楼层 □不要顶层│ │
|
||
│ │ 朝向 □东 □南 □西 □北 │ │
|
||
│ │ 装修 □毛坯 □清水 □简装 □中装 □精装 □豪装 │ │
|
||
│ │ 楼龄 □5年以内 □5-10年 □10-15年 □15-20年 □20年以上│ │
|
||
│ │ 意向商圈 [下拉多选] │ │
|
||
│ │ 意向小区1 [input] + 添加小区 │ │
|
||
│ │ 交通 [input, max 50字] │ │
|
||
│ │ 备注 [textarea, max 200字] │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
├──────────────────────────────────────────────────────────┤
|
||
│ 表单底部内联操作区:[保存(Teal 主按钮)] [取消(白色边框)] │
|
||
└──────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
> **截图与文档差异说明**:竞品截图(`编辑客源.png`)显示三个 Tab(联系人/基础信息/二手)**在同一页面内纵向分 Section 展示**,并非切换 Tab 才能看到内容——联系人、基础信息、二手三个区块都在一个页面滚动展示。顶部 Tab 的功能是**快速定位锚点**(点击 Tab 滚动到对应区块),而非隐藏其他区块。以截图为准实现。
|
||
|
||
#### 2.1.3 区域详细规范
|
||
|
||
---
|
||
|
||
**[顶部面包屑 + 标题区]**
|
||
|
||
| 属性 | 说明 |
|
||
|------|------|
|
||
| 面包屑 | `客源 / 客源管理 / 编辑客源`,使用 `text-sm text-neutral-500`,分隔符 `/`,最后一级用 `text-neutral-700` |
|
||
| 页面标题 | `编辑客源`,`text-xl font-semibold text-neutral-800` |
|
||
| 布局 | 面包屑在上,标题在下,左对齐,顶部 padding `pt-6 pb-4` |
|
||
|
||
---
|
||
|
||
**[Tab 导航栏]**
|
||
|
||
| 属性 | 说明 |
|
||
|------|------|
|
||
| 组件 | §10 Tab Navigation(组件规范设计.md) |
|
||
| Tab 项 | 「联系人」「基础信息」「二手/新房/租房」(第三项 label 根据客源需求类型动态确定) |
|
||
| 激活样式 | **注意:竞品使用橙色下划线** `border-b-2 border-orange-500 text-orange-500`,与系统主色 Teal 不同。为保持一致性,**本模块 Tab 激活色统一使用橙色** `#F97316`(`text-orange-500 border-orange-500`),以匹配竞品客源模块整体视觉风格 |
|
||
| 非激活样式 | `text-neutral-500 hover:text-neutral-700 border-b-2 border-transparent` |
|
||
| 行为 | 点击 Tab 触发 Alpine.js 平滑滚动到对应 Section 的锚点(`scrollIntoView({ behavior: 'smooth' })`),不做 HTMX 请求;Tab 同时高亮当前可见区域(Intersection Observer 驱动) |
|
||
| 粘性 | Tab 栏 `sticky top-0 z-10 bg-white border-b border-neutral-200` |
|
||
|
||
```html
|
||
<!-- Tab 导航栏示例 -->
|
||
<div class="sticky top-0 z-10 bg-white border-b border-neutral-200"
|
||
x-data="{ activeTab: 'contacts' }">
|
||
<nav class="flex gap-0 px-6">
|
||
<button class="px-4 py-3 text-sm font-medium border-b-2 transition-colors"
|
||
:class="activeTab === 'contacts'
|
||
? 'border-orange-500 text-orange-500'
|
||
: 'border-transparent text-neutral-500 hover:text-neutral-700'"
|
||
@click="activeTab = 'contacts'; document.getElementById('section-contacts').scrollIntoView({behavior:'smooth'})">
|
||
联系人
|
||
</button>
|
||
<button class="px-4 py-3 text-sm font-medium border-b-2 transition-colors"
|
||
:class="activeTab === 'basic'
|
||
? 'border-orange-500 text-orange-500'
|
||
: 'border-transparent text-neutral-500 hover:text-neutral-700'"
|
||
@click="activeTab = 'basic'; document.getElementById('section-basic').scrollIntoView({behavior:'smooth'})">
|
||
基础信息
|
||
</button>
|
||
<button class="px-4 py-3 text-sm font-medium border-b-2 transition-colors"
|
||
:class="activeTab === 'requirement'
|
||
? 'border-orange-500 text-orange-500'
|
||
: 'border-transparent text-neutral-500 hover:text-neutral-700'"
|
||
@click="activeTab = 'requirement'; document.getElementById('section-requirement').scrollIntoView({behavior:'smooth'})">
|
||
{{ requirement_tab_label }} {# 二手 / 新房 / 租房 #}
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
**[联系人 Section]**(锚点 `id="section-contacts"`)
|
||
|
||
区块标题行:
|
||
|
||
| 元素 | 说明 |
|
||
|------|------|
|
||
| 标题 | `联系人`,`text-base font-semibold text-neutral-800` |
|
||
| 右侧辅助文字 | `查看后可编辑号码`,`text-sm text-neutral-400` |
|
||
| 「查看号码」按钮 | `text-sm text-info-600 hover:underline cursor-pointer`,点击后触发查看号码确认流程(见下方) |
|
||
| 「+ 添加联系人」按钮 | `btn-secondary text-sm`,位于标题行右侧最末,点击后 Alpine.js 动态追加联系人区块 |
|
||
|
||
**联系人区块(每个联系人独立卡片)**:
|
||
|
||
```
|
||
bg-white rounded-lg border border-neutral-200 p-5 mb-4
|
||
```
|
||
|
||
区块内字段布局为**网格布局**,参考截图:3列网格,每列包含"字段名 + 输入组件"。
|
||
|
||
| 字段 | 必填 | 组件类型 | 校验 | 备注 |
|
||
|------|------|---------|------|------|
|
||
| 姓名 | ✅ | `<input type="text">` | 不可为空,最多50字 | 与录入一致 |
|
||
| 称呼 | ✅ | 单选 Radio | 选一 | `○ 先生 ○ 女士` |
|
||
| 电话1 | ✅ | 区号下拉 + 手机号 input | 手机号格式 | 默认打码,需「查看号码」后才能编辑(见下方交互) |
|
||
| 标记无效 | — | 蓝色文字链接 | — | 显示在电话1 旁,点击 HTMX PATCH 标记 `phone_is_invalid=True` |
|
||
| 微信 | 否 | `<input type="text">` | — | 显示 `-` 时可直接编辑 |
|
||
| 电话2 | 否 | 区号下拉 + 手机号 input | 手机号格式(若填写) | 同电话1,但无打码逻辑 |
|
||
| QQ | 否 | `<input type="text">` | — | |
|
||
| 备注 | 否 | `<textarea>` | 最多200字,字数计数器 | 新增字段(录入时无) |
|
||
|
||
**电话1 查看号码交互流程**:
|
||
|
||
1. 页面加载时,`phone_enc` 解密后以打码形式(`137****1234`)展示在只读 `<span>` 中,输入框 `disabled`
|
||
2. 用户点击「查看号码」按钮 → HTMX GET 请求验证权限 → 后端返回:
|
||
- **成功**:返回明文号码,前端 Alpine.js 将 `<span>` 替换为可编辑 `<input>`,同时在 `client_follow_logs` 插入一条 `log_type='sensitive_view'` 记录
|
||
- **失败(403/权限不足)**:Toast 提示「无查看号码权限」
|
||
3. 「查看号码」按钮变为已点击态(灰色,禁用),显示文字「已查看」
|
||
4. 「标记无效」链接始终可见(无需查看号码),但触发时需后端权限校验
|
||
|
||
```html
|
||
<!-- 电话1 打码展示 + 查看号码按钮 -->
|
||
<div x-data="{ revealed: false, phoneValue: '' }">
|
||
<!-- 打码显示态 -->
|
||
<template x-if="!revealed">
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-sm text-neutral-700">{{ contact.phone_masked }}</span>
|
||
<button type="button"
|
||
class="text-sm text-info-600 hover:underline"
|
||
hx-get="/api/clients/{{ client.id }}/contacts/{{ contact.id }}/reveal-phone/"
|
||
hx-target="#phone-reveal-{{ contact.id }}"
|
||
hx-swap="outerHTML"
|
||
@htmx:after-request="if(event.detail.successful) revealed = true">
|
||
查看号码
|
||
</button>
|
||
<a href="#" class="text-sm text-info-600 hover:underline"
|
||
hx-patch="/api/clients/{{ client.id }}/contacts/{{ contact.id }}/mark-invalid/"
|
||
hx-confirm="确认将该号码标记为无效?"
|
||
hx-swap="none"
|
||
@htmx:after-request="/* 处理成功/失败 */">
|
||
标记无效
|
||
</a>
|
||
</div>
|
||
</template>
|
||
<!-- 已解码可编辑态 -->
|
||
<div id="phone-reveal-{{ contact.id }}">
|
||
<template x-if="revealed">
|
||
<div class="flex items-center gap-2">
|
||
<input type="tel" name="contacts[{{ forloop.counter0 }}][phone]"
|
||
:value="phoneValue"
|
||
class="input-base w-40"
|
||
placeholder="请输入手机号">
|
||
<span class="text-xs text-neutral-400">已查看</span>
|
||
<a href="#" class="text-sm text-info-600 hover:underline"
|
||
hx-patch="/api/clients/{{ client.id }}/contacts/{{ contact.id }}/mark-invalid/"
|
||
hx-confirm="确认将该号码标记为无效?"
|
||
hx-swap="none">
|
||
标记无效
|
||
</a>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
**联系人区块的添加/删除逻辑**(Alpine.js 驱动):
|
||
|
||
```html
|
||
<div x-data="{
|
||
contacts: [
|
||
{ id: '{{ c.id }}', name: '{{ c.name }}', ... }
|
||
// 由 Django 模板初始化
|
||
],
|
||
addContact() {
|
||
this.contacts.push({ id: null, name: '', gender: 'male', phone: '', ... });
|
||
},
|
||
removeContact(index) {
|
||
if (this.contacts.length > 1) this.contacts.splice(index, 1);
|
||
}
|
||
}">
|
||
<template x-for="(contact, index) in contacts" :key="index">
|
||
<div class="bg-white rounded-lg border border-neutral-200 p-5 mb-4">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<span class="text-sm font-medium text-neutral-700" x-text="'联系人 ' + (index+1)"></span>
|
||
<button x-show="index > 0" type="button"
|
||
class="text-sm text-danger-600 hover:underline"
|
||
@click="removeContact(index)">删除</button>
|
||
</div>
|
||
<!-- 字段区域 -->
|
||
</div>
|
||
</template>
|
||
<button type="button" @click="addContact()"
|
||
class="flex items-center gap-1 text-sm text-info-600 hover:underline mt-2">
|
||
<svg class="w-4 h-4"><!-- Heroicons plus --></svg>
|
||
添加联系人
|
||
</button>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
**[基础信息 Section]**(锚点 `id="section-basic"`)
|
||
|
||
Section 容器:`bg-white rounded-lg border border-neutral-200 p-5 mb-4 mt-6`
|
||
|
||
字段布局:竞品截图显示为**标签在左(约100px 固定宽度)、控件在右**的两列表单布局,行间距 `space-y-4`。
|
||
|
||
| 字段 | 必填 | 组件类型 | 枚举值 | 后端字段 |
|
||
|------|------|---------|-------|---------|
|
||
| 需求类型 | ✅(*) | 复选框 | ☑ 二手 / □ 新房 | `clients.status` 间接关联;需求类型影响第三个 Tab |
|
||
| 用途 | ✅(*) | Radio 单选(横排) | 住宅 / 别墅 / 商住 / 商铺 / 写字楼 / 其他 | `clients.property_usage` |
|
||
| 等级 | ✅(*) | Radio 单选(横排) | A(急迫) / B(较强) / C(一般) / D(较弱) / E(暂不关注) | `clients.grade` |
|
||
| 来源 | ✅(*) | `<select>` 下拉 | 由运营维护,如「线下\|门店接待」 | `clients.source` |
|
||
| 购房目的 | 否 | 多选复选框(横排) | 刚需 / 投资 / 学区 / 改善 / 商用 / 其他 | `clients.buying_purpose[]` |
|
||
| 付款方式 | 否 | Radio 单选(横排) | 全额 / 商业贷款 / 商业贷款+公积金 / 公积金 | `clients.payment_method` |
|
||
| 名下房产 | 否 | Radio 单选(横排) | 无 / 本地无房外地有房 / 本地有房 | `clients.properties_owned` |
|
||
| 贷款记录 | 否 | Radio 单选(横排) | 有 / 无 | `clients.has_loan_record` |
|
||
| 证件类型 | 否 | `<select>` 下拉 | 身份证(默认)/ 护照 / 港澳通行证 / 其他 | `clients.id_type` |
|
||
| 证件号码 | 否 | `<input type="text">` | placeholder「请输入证件号码」;选择身份证时校验18位 | `clients.id_number_enc`(AES 加密存储) |
|
||
| 意向学校 | 否 | `<input type="text">` + 「+ 添加学校」链接 | 支持多所,动态追加输入行 | `client_school_preferences.school_name` |
|
||
| 入学时间 | 否 | 月份选择器(年月,如「2027-09」) | placeholder「请选择年月」,Flatpickr `mode:'single', showMonths:1` | `client_requirements.school_enrollment_date` |
|
||
|
||
**意向学校多输入行(Alpine.js 驱动)**:
|
||
|
||
```html
|
||
<div x-data="{ schools: {{ schools_json }}, addSchool() { this.schools.push('') } }">
|
||
<template x-for="(school, i) in schools" :key="i">
|
||
<div class="flex items-center gap-2 mb-2">
|
||
<input type="text" :name="`school_names[${i}]`" x-model="schools[i]"
|
||
class="input-base w-48" placeholder="请输入学校名称">
|
||
<button type="button" @click="schools.splice(i,1)" x-show="schools.length > 1"
|
||
class="text-danger-600 text-sm hover:underline">删除</button>
|
||
</div>
|
||
</template>
|
||
<button type="button" @click="addSchool()"
|
||
class="text-sm text-info-600 hover:underline">+ 添加学校</button>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
**[需求信息 Section(二手)]**(锚点 `id="section-requirement"`)
|
||
|
||
Section 容器同上,Section 标题为「二手」(或「新房」/「租房」),`text-base font-semibold text-neutral-800`。
|
||
|
||
字段布局:与竞品截图一致,**标签在左(约100px)、控件在右**,行间距 `space-y-4`。
|
||
|
||
| 字段 | 必填 | 组件类型 | 约束 | 后端字段 |
|
||
|------|------|---------|------|---------|
|
||
| 总价 | ✅(*) | 两个 `<input type="number">` + 文字「-」+ 「万元」 | 正数;最小值 ≤ 最大值 | `client_requirements.budget_min` / `budget_max` |
|
||
| 面积 | ✅(*) | 两个 `<input type="number">` + 文字「-」+ 「m²」 | 正数;最小值 ≤ 最大值 | `area_min` / `area_max` |
|
||
| 居室 | ✅(*) | 多选复选框(横排) | 至少选一项 | □1居 □2居 □3居 □4居 □5居及以上 | `bedroom_counts[]` |
|
||
| 楼层 | 否 | 多选复选框(横排) | — | □不要一层 □低楼层 □中楼层 □高楼层 □不要顶层 | `floor_preferences[]` |
|
||
| 朝向 | 否 | 多选复选框(横排) | — | □东 □南 □西 □北 | `orientations[]` |
|
||
| 装修 | 否 | 多选复选框(横排) | — | □毛坯 □清水 □简装 □中装 □精装 □豪装 | `decorations[]` |
|
||
| 楼龄 | 否 | 多选复选框(横排) | — | □5年以内 □5-10年 □10-15年 □15-20年 □20年以上 | `building_age_ranges[]` |
|
||
| 意向商圈 | 否 | `<select multiple>` 下拉多选 | placeholder「请选择商圈」 | `intent_business_area_ids[]` |
|
||
| 意向小区 | 否 | `<input type="text">` + 「+ 添加小区」 | 支持多个,动态追加 | `intent_complex_names`(逗号拼接) |
|
||
| 交通 | 否 | `<input type="text">` | 最多50字,实时计数显示 `0/50` | `transportation` |
|
||
| 备注 | 否 | `<textarea>` | 最多200字,实时计数显示 `0/200` | `requirement_notes` |
|
||
|
||
**总价/面积区间双输入框**:
|
||
|
||
```html
|
||
<div class="flex items-center gap-2">
|
||
<input type="number" name="budget_min" value="{{ req.budget_min }}"
|
||
min="0" step="0.01"
|
||
class="input-base w-24"
|
||
placeholder="最小值">
|
||
<span class="text-neutral-400">-</span>
|
||
<input type="number" name="budget_max" value="{{ req.budget_max }}"
|
||
min="0" step="0.01"
|
||
class="input-base w-24"
|
||
placeholder="最大值">
|
||
<span class="text-sm text-neutral-500">万元</span>
|
||
</div>
|
||
```
|
||
|
||
**字数计数器(Alpine.js)**:
|
||
|
||
```html
|
||
<div x-data="{ count: {{ req.transportation|length }} }">
|
||
<input type="text" name="transportation" maxlength="50"
|
||
x-model.debounce="value" @input="count = $event.target.value.length"
|
||
class="input-base w-full">
|
||
<span class="text-xs text-neutral-400 text-right block mt-1"
|
||
x-text="count + ' / 50'"></span>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
**[底部操作按钮区]**
|
||
|
||
```html
|
||
<div class="flex items-center gap-3 mt-2 pb-8">
|
||
<button type="submit" form="form-edit-client"
|
||
:disabled="submitting"
|
||
class="inline-flex items-center gap-1.5 px-6 py-2 text-sm font-medium bg-primary-600 text-white rounded-md hover:bg-primary-700 active:bg-primary-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 disabled:opacity-70 disabled:cursor-wait">
|
||
<!-- spinner,提交中显示 -->
|
||
<span x-text="submitting ? '保存中...' : '保存'"></span>
|
||
</button>
|
||
<button type="button" @click="cancelForm" :disabled="submitting"
|
||
class="inline-flex items-center px-4 py-2 text-sm font-medium bg-white border border-neutral-300 text-neutral-700 rounded-md hover:bg-neutral-50 hover:border-neutral-400 disabled:opacity-60">
|
||
取消
|
||
</button>
|
||
<p x-show="redirectHint" x-text="redirectHint" class="text-xs text-success-600"></p>
|
||
</div>
|
||
```
|
||
|
||
> **说明**:按钮区为表单内部的普通流式布局(`mt-2 pb-8`),与「新增客源」页保持一致。保存按钮使用系统主色 Teal(`bg-primary-600`),取消按钮为白底边框风格 `<button>`,无需为固定底栏预留额外底部间距。
|
||
|
||
#### 2.1.4 使用的特殊组件
|
||
|
||
| 组件名 | 来源(组件规范设计.md 章节) | 用途 | 自定义说明 |
|
||
|--------|--------------------------|------|-----------|
|
||
| Tab Navigation | §10 Tab Navigation | 联系人/基础信息/二手 三 Tab | Tab 激活色使用橙色(`text-orange-500 border-orange-500`)而非系统主色 Teal,与竞品保持一致 |
|
||
| Modal Dialog | §7 Modal Dialog | 查看号码时如需二次确认弹窗(权限验证提示) | max-w-sm 尺寸 |
|
||
| Date Range Picker (Flatpickr) | §9 Date Range Picker | 入学时间的月份选择器 | 模式:`plugins: [new monthSelectPlugin()]`,只选年月 |
|
||
| Multi-select Tag Input | §17 Multi-select Tag Input | 意向商圈下拉多选 | 可选参考 Choices.js,或用原生 `<select multiple>` + 自定义样式 |
|
||
|
||
#### 2.1.5 空状态设计
|
||
|
||
**联系人列表无联系人**:理论上不存在(保存时必须有至少一个联系人)。如因异常加载失败,展示错误提示区块而非空状态。
|
||
|
||
**基础信息字段未填写**:在只读详情中显示「-」,在编辑态中显示为空输入框(含 placeholder)。
|
||
|
||
**意向学校/意向小区无条目时**:动态列表为空时显示「+ 添加学校」/「+ 添加小区」入口,不显示空状态图案。
|
||
|
||
#### 2.1.6 Loading 状态
|
||
|
||
| 场景 | 方案 |
|
||
|------|------|
|
||
| 页面初始加载 | Django 服务端渲染,无需骨架屏,直接输出完整表单 |
|
||
| 点击「查看号码」 | 按钮显示 loading spinner(`htmx-request` 状态),按钮文字临时变为「查看中...」|
|
||
| 点击「标记无效」 | 按钮 loading,完成后 Toast 提示 |
|
||
| 保存提交中 | 「保存」按钮变为 disabled + spinner,文字「保存中...」;通过 `hx-indicator` 控制 |
|
||
| 意向商圈下拉加载选项 | 如为异步加载,Select 容器内显示 `<option>加载中...</option>` 再替换 |
|
||
|
||
---
|
||
|
||
## 3. 弹窗/抽屉设计规范
|
||
|
||
### 3.1 编辑基础信息弹窗(P1 🟡)
|
||
|
||
> 此弹窗由信息概览面板「编辑客源」快捷图标触发(Story 22 §5.23)。功能是快捷入口,字段是主编辑页基础信息 Tab 的子集。
|
||
|
||
#### 3.1.1 触发方式
|
||
|
||
- **触发位置**:私客详情页右侧信息概览面板 → 快捷操作区2 → 「编辑客源」图标按钮
|
||
- **竞品截图**:`Project/fonrey/screenshots/客源/编辑基础信息.png`
|
||
- **组件类型**:Modal Dialog(选择理由:字段数量中等,约10个字段,Modal 比 Drawer 更紧凑;操作完成后不需要保留背景页面的上下文)
|
||
- **尺寸**:`max-w-lg`(512px),竖向滚动支持
|
||
|
||
#### 3.1.2 表单字段规范
|
||
|
||
| 字段名 | 组件类型 | 必填 | 校验规则 | 默认值/预填值 |
|
||
|--------|---------|------|---------|------------|
|
||
| 需求类型 | 复选框(横排) | ✅ | 至少选一项 | 回显 `clients.status` 对应的类型 |
|
||
| 用途 | `<select>` 下拉 | ✅ | 必选 | 回显 `clients.property_usage` |
|
||
| 来源 | `<select>` 下拉 | ✅ | 必选 | 回显 `clients.source` |
|
||
| 购房目的 | 多选复选框(横排) | 否 | — | 回显 `clients.buying_purpose[]` |
|
||
| 付款方式 | `<select>` 下拉 | 否 | — | 回显 `clients.payment_method` |
|
||
| 名下房产 | `<select>` 下拉 | 否 | — | 回显 `clients.properties_owned` |
|
||
| 贷款记录 | Radio 单选(横排) | 否 | — | 回显 `clients.has_loan_record` |
|
||
| 证件类型 | `<select>` 下拉 | 否 | — | 回显 `clients.id_type` |
|
||
| 证件号码 | `<input type="text">` | 否 | 身份证18位格式(当证件类型为身份证时) | 回显解密后的证件号码 |
|
||
| 意向学校 | `<input>` + 「+ 添加学校」 | 否 | — | 回显已有学校列表 |
|
||
|
||
#### 3.1.3 提交行为
|
||
|
||
- **提交方式**:`hx-patch="/api/clients/{{ client.id }}/basic-info/"`
|
||
- **成功响应**:
|
||
- 关闭 Modal(Alpine.js `open = false`)
|
||
- 触发 Toast:「保存成功」(`success-600` 绿色,3秒自动消失)
|
||
- 刷新信息概览面板相关字段区域(`hx-target="#info-panel-basic-fields"` `hx-swap="innerHTML"`)
|
||
- **失败响应(422)**:
|
||
- 弹窗内字段级错误提示(输入框下方 `text-danger-600 text-xs`)
|
||
- 必填字段边框变 `border-danger-600`
|
||
- 不关闭弹窗
|
||
|
||
```html
|
||
<!-- 编辑基础信息 Modal 触发 -->
|
||
<button type="button"
|
||
@click="$dispatch('open-modal', 'edit-basic-info')"
|
||
class="flex flex-col items-center gap-1 text-xs text-neutral-600 hover:text-primary-600">
|
||
<svg class="w-5 h-5"><!-- Heroicons pencil-square --></svg>
|
||
编辑客源
|
||
</button>
|
||
|
||
<!-- Modal 内 form -->
|
||
<form id="form-edit-basic-info"
|
||
hx-patch="/api/clients/{{ client.id }}/basic-info/"
|
||
hx-target="#info-panel-basic-fields"
|
||
hx-swap="innerHTML"
|
||
hx-on::after-request="
|
||
if(event.detail.successful) {
|
||
$dispatch('close-modal');
|
||
$dispatch('show-toast', {message:'保存成功', type:'success'});
|
||
}
|
||
">
|
||
<!-- 字段区域 -->
|
||
<div class="px-6 py-4 space-y-4">
|
||
<!-- 需求类型 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-neutral-700 mb-1">
|
||
需求类型 <span class="text-danger-600">*</span>
|
||
</label>
|
||
<div class="flex gap-4">
|
||
<label class="flex items-center gap-2 text-sm">
|
||
<input type="checkbox" name="requirement_type[]" value="second_hand"
|
||
{{ 'checked' if 'second_hand' in requirement_types }} class="checkbox-base">
|
||
二手
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm">
|
||
<input type="checkbox" name="requirement_type[]" value="new_house"
|
||
{{ 'checked' if 'new_house' in requirement_types }} class="checkbox-base">
|
||
新房
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<!-- 其他字段省略... -->
|
||
</div>
|
||
<div class="px-6 py-4 border-t border-neutral-200 flex justify-end gap-3">
|
||
<button type="button" @click="$dispatch('close-modal')"
|
||
class="btn-secondary px-4 py-2 text-sm rounded-lg">取消</button>
|
||
<button type="submit"
|
||
class="btn-primary bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 text-sm rounded-lg">
|
||
确定
|
||
</button>
|
||
</div>
|
||
</form>
|
||
```
|
||
|
||
#### 3.1.4 使用的特殊组件
|
||
|
||
| 组件名 | 来源 | 用途 |
|
||
|--------|------|------|
|
||
| Modal Dialog | §7 Modal Dialog | 编辑基础信息弹窗容器 |
|
||
|
||
---
|
||
|
||
## 4. 交互状态规范
|
||
|
||
### 4.1 全局状态说明
|
||
|
||
**Alpine.js 管理的状态**:
|
||
|
||
| 状态 | 位置 | 说明 |
|
||
|------|------|------|
|
||
| `contacts[]` | 联系人区块 x-data | 联系人列表,支持动态追加/删除 |
|
||
| `schools[]` | 基础信息区块 x-data | 意向学校列表,支持动态追加/删除 |
|
||
| `complexes[]` | 需求信息区块 x-data | 意向小区列表,支持动态追加/删除 |
|
||
| `activeTab` | Tab 导航 x-data | 当前高亮的 Tab(联动 Intersection Observer) |
|
||
| `phoneRevealed[contactId]` | 每个联系人区块 x-data | 该联系人号码是否已解码可编辑 |
|
||
| `editBasicInfoOpen` | 页面级 / 信息概览面板 | 编辑基础信息 Modal 的开关状态 |
|
||
|
||
**HTMX 管理的数据流**:
|
||
|
||
| 操作 | 说明 |
|
||
|------|------|
|
||
| 查看号码 | `hx-get` 获取明文号码,局部替换号码区域 HTML |
|
||
| 标记无效 | `hx-patch` 更新 `phone_is_invalid`,无需刷新页面(`hx-swap="none"`),Toast 提示 |
|
||
| 整体表单保存 | `<form hx-post>` 提交,成功后 `HX-Redirect` 跳转到详情页 |
|
||
| 编辑基础信息弹窗提交 | `hx-patch` 局部更新面板字段区域 |
|
||
|
||
### 4.2 权限控制矩阵
|
||
|
||
| 操作 | 经纪人(归属人) | 经纪人(非归属人) | 店长 | 管理员 |
|
||
|------|--------------|-----------------|------|--------|
|
||
| 进入编辑页 | ✅ | ❌(后端403) | ✅ | ✅ |
|
||
| 查看电话号码明文 | ✅(需点击验证,留痕) | ❌ | ✅ | ✅ |
|
||
| 标记号码无效 | ✅ | ❌ | ✅ | ✅ |
|
||
| 编辑联系人信息 | ✅ | ❌ | ✅ | ✅ |
|
||
| 添加/删除联系人 | ✅ | ❌ | ✅ | ✅ |
|
||
| 编辑基础信息 | ✅ | ❌ | ✅ | ✅ |
|
||
| 编辑需求信息 | ✅ | ❌ | ✅ | ✅ |
|
||
| 使用编辑基础信息快捷弹窗 | ✅ | ❌ | ✅ | ✅ |
|
||
|
||
### 4.3 HTMX 请求规范
|
||
|
||
| 操作 | hx-trigger | hx-method + URL | hx-target | hx-swap | Loading 行为 |
|
||
|------|-----------|----------------|-----------|---------|-------------|
|
||
| 查看电话1明文 | `click` | `hx-get="/api/clients/{id}/contacts/{cid}/reveal-phone/"` | `#phone-reveal-{cid}` | `outerHTML` | 按钮内 spinner,`hx-indicator="#reveal-btn-{cid}"` |
|
||
| 标记号码无效 | `click` + 确认对话框 | `hx-patch="/api/clients/{id}/contacts/{cid}/mark-invalid/"` | `this`(hx-swap="none") | `none` | 按钮 disabled + spinner |
|
||
| 保存整体表单 | `submit` | `hx-post="/clients/{id}/edit/"` | `body`(后端返回 `HX-Redirect`) | — | 「保存」按钮 disabled + `保存中...` |
|
||
| 编辑基础信息弹窗提交 | `submit` | `hx-patch="/api/clients/{id}/basic-info/"` | `#info-panel-basic-fields` | `innerHTML` | 「确定」按钮 disabled + spinner |
|
||
| 意向商圈选项懒加载(如需) | `revealed`(下拉打开) | `hx-get="/api/business-areas/?format=options"` | `#business-area-select` | `innerHTML` | Select 内显示「加载中」option |
|
||
|
||
---
|
||
|
||
## 5. 关键数据字段说明
|
||
|
||
### 5.1 主表字段(clients)
|
||
|
||
| 字段名(英文) | 显示名 | 数据类型 | 说明 |
|
||
|--------------|--------|---------|------|
|
||
| `id` | — | UUID | 主键 |
|
||
| `status` | 需求状态 | VARCHAR(20) | `buying`=求购 / `renting`=求租 / `buy_or_rent`=租购,编辑页不直接修改此字段,通过「改状态」弹窗操作 |
|
||
| `grade` | 等级 | VARCHAR(5) | `A_urgent`/`A`/`B`/`C`/`D`/`E`,编辑页可直接修改 |
|
||
| `property_usage` | 用途 | VARCHAR(30) | `residential`/`villa`/`commercial_residential`/`shop`/`office`/`other` |
|
||
| `buying_purpose` | 购房目的 | VARCHAR(20)[] | 多选数组,如 `['rigid', 'school_district']` |
|
||
| `payment_method` | 付款方式 | VARCHAR(30) | `full`/`mortgage`/`mortgage_fund`/`fund` |
|
||
| `properties_owned` | 名下房产 | VARCHAR(20) | `none`/`local_none`/`local_has` |
|
||
| `has_loan_record` | 贷款记录 | BOOLEAN | `True`=有 / `False`=无 |
|
||
| `id_type` | 证件类型 | VARCHAR(20) | `id_card`/`passport`/`hk_macao`/`other` |
|
||
| `id_number_enc` | 证件号码 | BYTEA | AES 加密,后端解密后传前端,前端提交后后端加密存储 |
|
||
| `source` | 客户来源 | VARCHAR(50) | lookup_items 维护 |
|
||
|
||
### 5.2 联系人表字段(client_contacts)
|
||
|
||
| 字段名(英文) | 显示名 | 数据类型 | 说明 |
|
||
|--------------|--------|---------|------|
|
||
| `id` | — | UUID | 联系人主键 |
|
||
| `client_id` | — | UUID | 外键→clients |
|
||
| `sort_order` | 排序 | SMALLINT | 0=联系人1(主联系人)|
|
||
| `name` | 姓名 | VARCHAR(50) | 必填 |
|
||
| `gender` | 称呼 | VARCHAR(10) | `male`=先生 / `female`=女士 |
|
||
| `phone_enc` | 电话1 | BYTEA | AES 加密,前端默认显示打码格式 |
|
||
| `phone_hash` | 电话1哈希 | VARCHAR(64) | 后端维护,用于重复检测 |
|
||
| `phone_country_code` | 区号 | VARCHAR(10) | 默认 `+86` |
|
||
| `phone_is_invalid` | 号码是否无效 | BOOLEAN | `True`=已标记无效 |
|
||
| `phone2_enc` | 电话2 | BYTEA | 选填 |
|
||
| `wechat` | 微信 | VARCHAR(100) | 选填 |
|
||
| `qq` | QQ | VARCHAR(20) | 选填 |
|
||
| `remarks` | 备注 | VARCHAR(200) | 最多200字 |
|
||
|
||
### 5.3 需求信息表字段(client_requirements)
|
||
|
||
| 字段名(英文) | 显示名 | 数据类型 | 说明 |
|
||
|--------------|--------|---------|------|
|
||
| `requirement_type` | 需求类型 | VARCHAR(20) | `second_hand`/`new_house`/`rental` |
|
||
| `budget_min` | 总价最小值 | NUMERIC(12,2) | 万元 |
|
||
| `budget_max` | 总价最大值 | NUMERIC(12,2) | 万元 |
|
||
| `area_min` | 面积最小值 | NUMERIC(8,2) | ㎡ |
|
||
| `area_max` | 面积最大值 | NUMERIC(8,2) | ㎡ |
|
||
| `bedroom_counts` | 居室 | SMALLINT[] | 如 `[2, 3]` |
|
||
| `floor_preferences` | 楼层偏好 | VARCHAR(20)[] | `no_first`/`low`/`mid`/`high`/`no_top` |
|
||
| `orientations` | 朝向 | VARCHAR(10)[] | `east`/`south`/`west`/`north` |
|
||
| `decorations` | 装修 | VARCHAR(10)[] | 枚举同 properties |
|
||
| `building_age_ranges` | 楼龄 | VARCHAR(20)[] | `within_5y`/`5_10y`/`10_15y`/`15_20y`/`over_20y` |
|
||
| `intent_business_area_ids` | 意向商圈 | UUID[] | 商圈 ID 数组 |
|
||
| `intent_complex_names` | 意向小区 | TEXT | 逗号分隔 |
|
||
| `transportation` | 交通要求 | VARCHAR(50) | 最多50字 |
|
||
| `requirement_notes` | 备注 | VARCHAR(200) | 最多200字 |
|
||
| `school_enrollment_date` | 入学时间 | DATE | 月份精度(存储为该月1日) |
|
||
|
||
### 5.4 意向学校表字段(client_school_preferences)
|
||
|
||
| 字段名(英文) | 显示名 | 数据类型 | 说明 |
|
||
|--------------|--------|---------|------|
|
||
| `requirement_id` | — | UUID | 外键→client_requirements |
|
||
| `school_id` | — | UUID | 外键→schools,允许NULL(自由输入时) |
|
||
| `school_name` | 学校名称 | VARCHAR(100) | 当 school_id 为 NULL 时为手动输入的名称 |
|
||
|
||
---
|
||
|
||
## 6. 竞品截图对应关系
|
||
|
||
| 截图路径 | 对应功能 | 对应文档章节 | 采纳的设计要点 |
|
||
|---------|---------|-----------|-------------|
|
||
| `Project/fonrey/screenshots/客源/编辑客源.png` | 编辑客源主页(三区块滚动页) | §2.1 | 1. 顶部 Tab(联系人/基础信息/二手)为锚点快速定位,非 Tab 切换隐藏;2. 三区块纵向排列在同一页面;3. Tab 激活使用橙色下划线;4. 联系人区块右上角有「查看后可编辑号码」说明 + 橙色「查看号码」按钮 + 「+ 添加联系人」;5. 电话1 旁有「标记无效」蓝色链接;6. 基础信息区块所有字段(含购房目的/付款方式/名下房产/贷款记录/证件/意向学校/入学时间)全部展开(无折叠);7. 需求信息(二手)区块紧跟其后,字段全展开;8. 底部操作按钮为表单内联布局,保存按钮使用系统主色 Teal,与「新增客源」页保持一致 |
|
||
| `Project/fonrey/screenshots/客源/编辑基础信息.png` | 编辑基础信息快捷弹窗 | §3.1 | Modal 弹窗,含「确定」橙色按钮 + 「取消」按钮 |
|
||
|
||
---
|
||
|
||
## 7. 实现优先级与工期估算
|
||
|
||
| 页面/功能 | 优先级 | 特殊组件复杂度 | 工期估算(前端) |
|
||
|----------|--------|--------------|---------------|
|
||
| 编辑客源主页面基础框架(Tab 导航 + 三区块布局) | P0 🔴 | 低 | 0.5 天 |
|
||
| 联系人 Tab:表单字段渲染(姓名/称呼/电话/微信/QQ/备注) | P0 🔴 | 低 | 0.5 天 |
|
||
| 联系人 Tab:查看号码 HTMX 交互(打码→明文→可编辑) | P0 🔴 | 中 | 1 天 |
|
||
| 联系人 Tab:标记无效 HTMX 操作 | P0 🔴 | 低 | 0.5 天 |
|
||
| 联系人 Tab:动态追加/删除联系人(Alpine.js) | P0 🔴 | 中 | 0.5 天 |
|
||
| 基础信息区块:必填字段渲染(需求类型/用途/等级/来源) | P0 🔴 | 低 | 0.5 天 |
|
||
| 基础信息区块:选填字段(购房目的/付款方式/名下房产/贷款记录/证件/意向学校/入学时间) | P0 🔴 | 中(月份选择器) | 1 天 |
|
||
| 需求信息区块(二手):全量字段表单渲染 | P0 🔴 | 低 | 0.5 天 |
|
||
| 需求信息区块(二手):意向商圈下拉多选 | P0 🔴 | 中(多选组件) | 0.5 天 |
|
||
| 需求信息区块(二手):意向小区动态追加(Alpine.js) | P0 🔴 | 低 | 0.25 天 |
|
||
| 表单保存提交(HTMX + 后端 HX-Redirect) | P0 🔴 | 低 | 0.5 天 |
|
||
| 表单校验(必填高亮 + 滚动定位) | P0 🔴 | 中 | 0.5 天 |
|
||
| 编辑基础信息弹窗(快捷入口 Modal) | P1 🟡 | 中(Modal + HTMX 局部刷新) | 1 天 |
|
||
| 新房 Tab / 租房 Tab 需求字段 | P1 🟡 | 低(字段待确认) | 1 天 |
|
||
| **总计** | — | — | **约 8 天** |
|
||
|
||
---
|
||
|
||
## 8. 开放问题(待决策)
|
||
|
||
| # | 问题 | 影响范围 | 待确认方 |
|
||
|---|------|---------|---------|
|
||
| 1 | 新房 Tab 和租房 Tab 的具体字段清单?截图中无对应截图,PRD §5.15.4 仅说明「待新房/租房截图补充确认」 | §2.1.3 需求信息区块、§7 工期 | 产品经理 |
|
||
| 2 | 意向商圈的数据来源:是后端 API 返回商圈列表,还是 Django 模板预渲染 `<option>`?数据量大时是否需要异步加载 + 搜索过滤? | §4.3 HTMX 请求规范 | 后端工程师 |
|
||
| 3 | 「查看号码」验证流程:是否需要密码二次确认,还是仅凭当前 Session 权限直接返回明文?具体的验证逻辑由后端定义 | §2.1.3 联系人区块 | 后端工程师 + 产品经理 |
|
||
| 4 | 编辑页保存时,如果同时修改了联系人电话(可能造成与已有私客重复),是否需要与「录入私客」相同的重复检测提示?若需要,是保存时拦截还是仅提示 Warning? | §2.1.3 联系人区块 | 产品经理 |
|
||
| 5 | 入学时间的月份选择器:Flatpickr `monthSelectPlugin` 是否已在项目中引入?或使用其他方案(如原生 `<input type="month">`)? | §2.1.4 特殊组件 | 前端工程师 |
|
||
| 6 | 竞品截图中「基础信息」区块字段均已展开显示(无折叠)。PRD Story 14 §5.14 描述「录入时默认折叠,编辑时全部展开」,确认编辑态下所有字段一律展开(不需要点击「展开更多」)? | §2.1.3 基础信息区块 | 产品经理(以截图为准,当前设计已按全展开处理) |
|
||
| 7 | 编辑页面 URL 方案:是否为 `/clients/<client_id>/edit/`?还是 `/clients/<client_id>/edit/?tab=contacts`(支持直接定位到特定 Tab)?从需求信息 Tab「编辑」链接进入时希望直接滚动到需求区块 | §1.2 页面清单 | 前端 + 后端工程师 |
|