修改文档
This commit is contained in:
1103
Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md
Normal file
1103
Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md
Normal file
File diff suppressed because it is too large
Load Diff
468
Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md
Normal file
468
Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md
Normal file
@@ -0,0 +1,468 @@
|
||||
# 客源详情页 UI 设计文档
|
||||
|
||||
> **版本**:v1.2 · **日期**:2026-04-25
|
||||
> **依赖规范**:`Project/fonrey/UI_SYSTEM/UI_SYSTEM.md` v1.2、`Project/fonrey/UI_SYSTEM/组件规范设计.md`
|
||||
> **视觉参考**:`Project/fonrey/UI_SYSTEM/preview.html`(页面骨架、卡片层级、工具栏密度)
|
||||
> **PRD 来源**:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` §5.7 私客详情页
|
||||
> **静态原型**:`Project/fonrey/客源详情_静态原型.html`(以此为视觉真相来源)
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
### 1.1 页面目标
|
||||
|
||||
- 在一个页面内完成私客详情查看、跟进、带看、状态流转、联系人管理、相关员工查看。
|
||||
- 桌面优先(`>=1280px`),不做移动端适配。
|
||||
- 技术栈固定:Tailwind CSS + HTMX + Alpine.js + Django Template。
|
||||
|
||||
### 1.2 本版关键改动(相对 v1.1)
|
||||
|
||||
- 修正 Topbar 配色:`bg-white border-b` → `bg-primary-800`,与 UI_SYSTEM v1.2 对齐。
|
||||
- 修正主内容区 padding:`pt-14 py-4` → `pt-[72px] py-5`。
|
||||
- 修正 Section 锚点导航 sticky 位置:`top-20` → `top-16`。
|
||||
- 修正右侧面板 sticky:`top-20` → `top-16`,并增加 `max-h-[calc(100vh-80px)] overflow-y-auto`。
|
||||
- 修正客源概览卡片顶部标识区:`bg-primary-600`(非 `bg-primary-800`)。
|
||||
- 补充客源解读 Section 实际字段和数据展示规范。
|
||||
- 补充 Alpine.js `clientDetailPage()` 状态机完整定义。
|
||||
|
||||
### 1.3 URL 与入口
|
||||
|
||||
- 详情页主路由:`/clients/<client_id>/`
|
||||
- 入口:客源列表点击姓名或详情操作。
|
||||
- 所有左侧 Section 默认随页面 SSR 输出,首屏即可看到首个 Section,向下滚动查看其余 Section。
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计基线(必须遵守)
|
||||
|
||||
### 2.1 视觉与 Token
|
||||
|
||||
- 主色:`primary-600`,禁用硬编码 Hex。
|
||||
- Topbar 背景:`bg-primary-800`(深青绿,区别于内容区)。
|
||||
- 页面底色:`bg-neutral-50`,内容卡片:`bg-white border border-neutral-200 rounded-lg`。
|
||||
- 正文字号:`text-sm`;辅助:`text-xs text-neutral-500`;关键数字加 `tabular-nums`。
|
||||
- 圆角:卡片 `rounded-lg`,输入/按钮 `rounded-md`,标签 `rounded` 或 `rounded-full`。
|
||||
- 焦点环统一:`focus-visible:ring-2 focus-visible:ring-primary-600/40`。
|
||||
|
||||
### 2.2 组件与图标
|
||||
|
||||
- 图标库:Heroicons v2。
|
||||
- 默认:Outline 24px(`w-5 h-5`)
|
||||
- 高密:Mini 16px(`w-4 h-4`)
|
||||
- 禁止独立 CSS 文件;样式全部使用 Tailwind utility class。
|
||||
|
||||
### 2.3 页面骨架
|
||||
|
||||
- Topbar:`fixed top-0 left-0 right-0 h-14 z-20 bg-primary-800 flex items-center justify-between`
|
||||
- Sidebar:`fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-neutral-200 bg-white`
|
||||
- 主内容区:`ml-60 pt-[72px] min-h-screen bg-neutral-50 px-6 py-5`
|
||||
- 区块垂直节奏:`space-y-4`(外层)、`space-y-6`(左侧 Section 间距)
|
||||
|
||||
---
|
||||
|
||||
## 3. 页面信息架构
|
||||
|
||||
### 3.1 整体结构
|
||||
|
||||
```
|
||||
Topbar (h-14, bg-primary-800)
|
||||
-> Sidebar (w-60, fixed left)
|
||||
-> Content Area (ml-60, pt-[72px], bg-neutral-50, px-6 py-5)
|
||||
-> Breadcrumb + Page Title
|
||||
-> Main Grid (12 cols, gap-6)
|
||||
- Left (col-span-8)
|
||||
- Sticky Section Anchor Nav (top-16, z-30)
|
||||
- Section 1: 需求信息
|
||||
- Section 2: 跟进记录
|
||||
- Section 3: 带看记录
|
||||
- Section 4: 客源解读
|
||||
- Section 5: 二手配房
|
||||
- Right Sidebar (col-span-4, sticky top-16)
|
||||
- 客源信息概览(含快捷操作)
|
||||
- 联系人
|
||||
- 相关员工
|
||||
```
|
||||
|
||||
### 3.2 主体布局骨架
|
||||
|
||||
```html
|
||||
<main class="ml-60 pt-[72px] min-h-screen bg-neutral-50 px-6 py-5">
|
||||
<div class="mx-auto max-w-[1600px] space-y-4">
|
||||
<!-- 面包屑 + 页头 -->
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<nav class="flex items-center gap-1 text-xs text-neutral-500 mb-1" aria-label="面包屑">...</nav>
|
||||
<h1 class="text-xl font-semibold text-neutral-800">客源详情</h1>
|
||||
<p class="text-xs text-neutral-500 mt-0.5">按 Section 连续展示,点击导航锚点快速定位</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-12 gap-6 items-start">
|
||||
<section class="col-span-8 min-w-0 space-y-6">
|
||||
<!-- Anchor Nav + All Sections -->
|
||||
</section>
|
||||
<aside class="col-span-4 min-w-0 space-y-3 sticky top-16 max-h-[calc(100vh-80px)] overflow-y-auto">
|
||||
<!-- Right panels -->
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Topbar
|
||||
|
||||
结构同 UI_SYSTEM §5.2,客源详情页激活「客源」导航项。
|
||||
|
||||
```html
|
||||
<header class="fixed top-0 left-0 right-0 h-14 z-20 bg-primary-800 flex items-center justify-between">
|
||||
<!-- 左区:Logo -->
|
||||
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
|
||||
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
|
||||
<span class="text-base font-semibold text-white">Fonrey</span>
|
||||
</div>
|
||||
<!-- 中区:主导航(nav flex-1,无搜索框) -->
|
||||
<nav class="flex items-center gap-1 flex-1 px-2" aria-label="主导航">
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">主页</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md bg-primary-600 text-white font-medium">客源</a><!-- 激活态 -->
|
||||
<!-- …其余项 -->
|
||||
</nav>
|
||||
<!-- 右区 -->
|
||||
<div class="flex items-center gap-1 px-4 shrink-0">
|
||||
<button class="p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md" aria-label="消息"><!-- BellIcon --></button>
|
||||
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
|
||||
<div class="w-8 h-8 rounded-full bg-primary-600 text-white flex items-center justify-center text-sm font-semibold">魏</div>
|
||||
<span class="text-sm font-medium text-primary-100">魏深</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
```
|
||||
|
||||
> 注:详情页 Topbar 右区只有消息通知 + 头像姓名,省略设置按钮(与 preview.html 保持一致)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 左侧主内容区(全量 Section + 锚点导航)
|
||||
|
||||
### 5.1 Section 导航(替代 Tab)
|
||||
|
||||
#### 5.1.1 交互定义
|
||||
|
||||
- 导航仅用于锚点跳转,不切换内容,不销毁 DOM。
|
||||
- 点击导航项:`scrollToSection(id)` 平滑滚动到目标 Section。
|
||||
- 当前高亮:`IntersectionObserver`(rootMargin `-140px 0px -55% 0px`,threshold `0.01`)驱动 `activeSection`。
|
||||
- 导航条 sticky:`sticky top-16 z-30`,滚动时固定在左栏顶部。
|
||||
|
||||
#### 5.1.2 导航样式
|
||||
|
||||
- 容器:`bg-white border border-neutral-200 rounded-lg px-3 py-2 sticky top-16 z-30 shadow-xs`
|
||||
- 项默认:`px-3 py-1.5 text-sm rounded-md text-neutral-600 hover:bg-neutral-100 hover:text-neutral-800`
|
||||
- 项激活:`bg-primary-50 text-primary-700 font-medium`
|
||||
- 焦点:`focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40`
|
||||
- `aria-current="true"` 标注当前激活项
|
||||
|
||||
#### 5.1.3 Alpine.js 数据结构
|
||||
|
||||
```js
|
||||
navItems: [
|
||||
{ id: 'section-requirements', label: '需求信息' },
|
||||
{ id: 'section-follow', label: '跟进记录' },
|
||||
{ id: 'section-viewings', label: '带看记录' },
|
||||
{ id: 'section-insights', label: '客源解读' },
|
||||
{ id: 'section-matches', label: '二手配房' }
|
||||
],
|
||||
activeSection: 'section-requirements'
|
||||
```
|
||||
|
||||
#### 5.1.4 代码骨架
|
||||
|
||||
```html
|
||||
<div class="bg-white border border-neutral-200 rounded-lg px-3 py-2 sticky top-16 z-30 shadow-xs">
|
||||
<nav class="flex items-center gap-1 overflow-x-auto whitespace-nowrap" aria-label="详情分区导航">
|
||||
<template x-for="item in navItems" :key="item.id">
|
||||
<a :href="'#' + item.id"
|
||||
@click.prevent="scrollToSection(item.id)"
|
||||
:aria-current="activeSection === item.id ? 'true' : 'false'"
|
||||
:class="activeSection === item.id
|
||||
? 'bg-primary-50 text-primary-700 font-medium'
|
||||
: 'text-neutral-600 hover:bg-neutral-100 hover:text-neutral-800'"
|
||||
class="px-3 py-1.5 text-sm rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40"
|
||||
x-text="item.label"></a>
|
||||
</template>
|
||||
</nav>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Section 1:需求信息(P0)
|
||||
|
||||
- ID:`section-requirements`,class 含 `section-anchor scroll-mt-24`
|
||||
- 容器:`bg-white rounded-lg border border-neutral-200 p-4 space-y-4`
|
||||
- Header:标题 `text-base font-semibold text-neutral-800` + 右侧文字链「编辑」`text-sm text-primary-600 hover:text-primary-700 hover:underline underline-offset-2`
|
||||
- 字段网格:`grid grid-cols-3 gap-x-6 gap-y-4`
|
||||
- 字段项:`<dt class="text-xs text-neutral-500">` + `<dd class="text-sm text-neutral-900">`,数字型加 `tabular-nums`
|
||||
- 备注字段:`col-span-3`(跨满三列)
|
||||
- 空值:统一显示 `-`
|
||||
|
||||
字段列表:总价、面积、居室、装修、朝向、楼层、楼龄、意向商圈、意向小区、交通情况、备注(col-span-3)
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Section 2:跟进记录(P0)
|
||||
|
||||
- ID:`section-follow`,class 含 `section-anchor scroll-mt-24`
|
||||
- 容器:`bg-white rounded-lg border border-neutral-200 p-4 space-y-4`
|
||||
- Header 右侧:「写跟进」Primary 按钮,含铅笔图标 `w-4 h-4`,点击触发写跟进 Drawer(`drawerOpen=true`)
|
||||
|
||||
**类型筛选条**(`flex items-center gap-2 flex-wrap`):
|
||||
- 激活项:`px-3 py-1 text-xs rounded-full bg-primary-600 text-white`
|
||||
- 默认项:`px-3 py-1 text-xs rounded-full bg-neutral-100 text-neutral-600 hover:bg-neutral-200`
|
||||
- 选项:全部 / 写入跟进 / 敏感信息跟进 / 修改跟进 / 其他跟进
|
||||
|
||||
**日期筛选栏**:`border border-neutral-200 rounded-md p-3 bg-neutral-50 flex items-center gap-3 flex-wrap text-xs`,含开始/结束日期输入框、有录音/有图片 checkbox。
|
||||
|
||||
**时间线**:`relative border-l-2 border-neutral-200 ml-3 space-y-4 pl-5`
|
||||
- 普通记录:圆点 `bg-primary-600`,卡片 `rounded-md border border-neutral-200 p-3 bg-white space-y-1`
|
||||
- 敏感记录:圆点 `bg-warning-600`,卡片 `rounded-md border border-warning-600/20 bg-warning-50 p-3 space-y-1`
|
||||
- 类型标签:`inline-flex items-center px-2 py-0.5 rounded-full bg-info-50 text-info-600 font-medium`(电话),`bg-warning-50 text-warning-600`(敏感查看)
|
||||
|
||||
**底部**:「查看全部跟进」文字链,`text-sm text-primary-600 hover:underline underline-offset-2`。
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Section 3:带看记录(P0)
|
||||
|
||||
- ID:`section-viewings`,class 含 `section-anchor scroll-mt-24`
|
||||
- Header 右侧:两个 Secondary 按钮(`新增预约` / `新增带看`)
|
||||
- 筛选栏:同跟进记录日期筛选样式,含「归属人带看」「其他人带看」checkbox
|
||||
- 时间线结构同跟进记录,房源名称用 `text-primary-600 hover:underline` 链接
|
||||
- 带看次数标签:`inline-flex items-center px-2 py-0.5 rounded-full bg-primary-50 text-primary-700 font-medium`(如「一看」)
|
||||
|
||||
---
|
||||
|
||||
### 5.5 Section 4:客源解读(P1)
|
||||
|
||||
- ID:`section-insights`,class 含 `section-anchor scroll-mt-24`
|
||||
- Header 右侧:更新时间文字 `text-xs text-neutral-500`
|
||||
|
||||
**行为指标行**(`grid grid-cols-3 gap-3`):
|
||||
- 卡片:`border border-neutral-200 rounded-md p-3 bg-white`
|
||||
- 标签 `text-xs text-neutral-500`,值 `text-xl font-semibold text-neutral-900 tabular-nums`
|
||||
- 字段:活跃行为 / 工作日活跃 / 周末活跃
|
||||
|
||||
**偏好占比行**(`grid grid-cols-3 gap-3`):
|
||||
- 卡片:`border border-neutral-200 rounded-md p-3 text-center`
|
||||
- 标签 `text-xs text-neutral-500`,占比值 `text-2xl font-semibold text-primary-600 tabular-nums`
|
||||
- 字段:价格偏好 / 户型偏好 / 面积偏好
|
||||
|
||||
---
|
||||
|
||||
### 5.6 Section 5:二手配房(P1)
|
||||
|
||||
- ID:`section-matches`,class 含 `section-anchor scroll-mt-24`
|
||||
- Header 右侧:「批量分享」Secondary 按钮
|
||||
|
||||
**分组标题**:`text-sm font-semibold text-neutral-700`(如「优质户型」)
|
||||
|
||||
**房源卡片**(`border border-neutral-200 rounded-md p-3`):
|
||||
- 缩略图:`w-20 h-14 rounded bg-neutral-100`(占位)
|
||||
- 房源名:`text-sm font-medium text-primary-600 hover:underline`
|
||||
- 描述:`text-xs text-neutral-500`(户型·面积·区域)
|
||||
- 标签行:`inline-flex items-center px-1.5 py-0.5 text-[11px] rounded`,朝向 `bg-warning-50 text-warning-600`,私盘 `bg-info-50 text-info-600`
|
||||
- 价格:`text-sm font-medium text-neutral-900 tabular-nums`,降价说明 `text-xs font-normal text-neutral-500`
|
||||
|
||||
---
|
||||
|
||||
## 6. 右侧信息面板
|
||||
|
||||
右侧 `col-span-4`,`sticky top-16 max-h-[calc(100vh-80px)] overflow-y-auto space-y-3`。
|
||||
|
||||
### 6.1 客源信息概览(P0)
|
||||
|
||||
容器:`bg-white rounded-lg border border-neutral-200 overflow-hidden`
|
||||
|
||||
**顶部标识区**:`bg-primary-600 px-4 py-3`
|
||||
- 求购 Badge:`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-white/20 text-white`
|
||||
- 客户姓名:`text-sm font-semibold text-white truncate`
|
||||
- 带看进度副文字:`text-xs text-white/80 mt-0.5`
|
||||
|
||||
**标签行**(`flex items-center gap-1.5 flex-wrap`):
|
||||
- 私客:`bg-neutral-100 text-neutral-600`
|
||||
- 带看进度(如「一看」):`bg-primary-50 text-primary-700`
|
||||
- 等级(如「C(一般)」):`bg-warning-50 text-warning-600`
|
||||
- 统一尺寸:`inline-flex items-center px-1.5 py-0.5 text-xs rounded`
|
||||
- 右侧文字链「编辑」`text-sm text-primary-600 hover:text-primary-700 hover:underline underline-offset-2`
|
||||
|
||||
**字段列表**(`dl space-y-1.5`):
|
||||
- 每行:`grid grid-cols-[72px_1fr] gap-2`
|
||||
- `dt`:`text-xs text-neutral-500`;`dd`:`text-xs text-right text-neutral-800`
|
||||
- 数字/日期加 `tabular-nums`,编号加 `font-mono`
|
||||
- 字段:最近跟进 / 客户编号 / 委托日期 / 需求类型 / 房源用途 / 客户来源
|
||||
|
||||
### 6.2 快捷操作区(P0)
|
||||
|
||||
**三主按钮**(`grid grid-cols-3 gap-2`):
|
||||
- 样式:`flex flex-col items-center gap-1 py-2 text-xs font-medium bg-primary-600 text-white rounded-md hover:bg-primary-700`
|
||||
- 按钮:打电话 / 写跟进(触发 `drawerOpen=true`)/ 报备/常看
|
||||
|
||||
**操作网格**(`grid grid-cols-2 gap-1`):
|
||||
- 默认:`px-2 py-2 text-xs text-left rounded-md text-neutral-600 hover:bg-neutral-100`
|
||||
- 危险项(转无效):`text-danger-600 hover:bg-danger-50`
|
||||
- 按钮:收藏 / 不置顶 / 改等级(`modal='grade'`)/ 改状态(`modal='status'`)/ 转公客 / 转成交(`modal='deal'`)/ 转无效 / 编辑客源(`modal='edit'`)
|
||||
|
||||
### 6.3 联系人面板(P0)
|
||||
|
||||
容器:`bg-white rounded-lg border border-neutral-200 p-3`
|
||||
|
||||
- Header 操作:`查看号码` / `新增联系人`,`text-xs text-primary-600 hover:underline space-x-2`
|
||||
- 姓名:`text-sm font-medium text-neutral-900`
|
||||
- 号码(脱敏):`text-xs text-neutral-600 tabular-nums`
|
||||
- 拨打统计:`text-xs text-neutral-500`
|
||||
|
||||
### 6.4 相关员工面板(P0)
|
||||
|
||||
容器:`bg-white rounded-lg border border-neutral-200 p-3`
|
||||
|
||||
- Header 操作:`编辑`,`text-xs text-primary-600 hover:underline`
|
||||
- 每个员工块(`space-y-3`):
|
||||
- 角色标签:`text-xs font-medium text-neutral-700`(如「【首录人】」)
|
||||
- 姓名:`text-sm text-neutral-900`
|
||||
- 参与时间:`text-xs text-neutral-500 tabular-nums`
|
||||
|
||||
---
|
||||
|
||||
## 7. 弹窗与抽屉
|
||||
|
||||
### 7.1 统一规范
|
||||
|
||||
- 遮罩:`fixed inset-0 z-50 bg-neutral-900/40`,点击关闭
|
||||
- 弹窗层:`z-60 fixed inset-0 flex items-center justify-center p-4 pointer-events-none`
|
||||
- 弹窗体:`pointer-events-auto bg-white rounded-xl shadow-lg border border-neutral-200 flex flex-col`
|
||||
- Header:`flex items-center justify-between px-5 py-4 border-b border-neutral-200`,关闭按钮 `p-1 text-neutral-500 hover:bg-neutral-100 rounded-md`
|
||||
- Footer:`flex items-center justify-end gap-2 px-5 py-3 border-t border-neutral-200 bg-neutral-50`,取消 Secondary + 确认 Primary
|
||||
|
||||
### 7.2 弹窗清单
|
||||
|
||||
| 弹窗 | 触发 | 宽度 | 内容要点 |
|
||||
|---|---|---|---|
|
||||
| 改等级 | `modal='grade'` | `max-w-sm` | 原等级展示 + 下拉选择新等级 |
|
||||
| 改状态 | `modal='status'` | `max-w-md` | 原状态展示 + 下拉新状态 + 理由 textarea |
|
||||
| 转成交 | `modal='deal'` | `max-w-lg` | 状态/房源类型下拉 + 选择成交房源按钮 |
|
||||
| 编辑基础信息 | `modal='edit'` | `max-w-2xl max-h-[85vh] overflow-y-auto` | 需求类型/来源/用途/付款方式/证件号码 |
|
||||
|
||||
### 7.3 写跟进 Drawer
|
||||
|
||||
- 触发:`drawerOpen=true`
|
||||
- 宽度:`w-[480px]`,从右侧滑入(`translate-x-full` → `translate-x-0`)
|
||||
- 字段:跟进方式(select)/ 跟进内容(textarea,至少 6 字)/ 跟进时间(input)/ 附件(file)/ 是否开放给同事查看(checkbox,默认勾选)
|
||||
|
||||
---
|
||||
|
||||
## 8. Alpine.js 状态机
|
||||
|
||||
```js
|
||||
function clientDetailPage() {
|
||||
return {
|
||||
modal: null, // 'grade' | 'status' | 'deal' | 'edit' | null
|
||||
drawerOpen: false,
|
||||
navItems: [
|
||||
{ id: 'section-requirements', label: '需求信息' },
|
||||
{ id: 'section-follow', label: '跟进记录' },
|
||||
{ id: 'section-viewings', label: '带看记录' },
|
||||
{ id: 'section-insights', label: '客源解读' },
|
||||
{ id: 'section-matches', label: '二手配房' }
|
||||
],
|
||||
activeSection: 'section-requirements',
|
||||
observer: null,
|
||||
scrollToSection(id) {
|
||||
const el = document.getElementById(id)
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
},
|
||||
init() {
|
||||
const sections = Array.from(document.querySelectorAll('.section-anchor'))
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) this.activeSection = entry.target.id
|
||||
})
|
||||
}, { root: null, rootMargin: '-140px 0px -55% 0px', threshold: 0.01 })
|
||||
sections.forEach((s) => this.observer.observe(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. HTMX 交互规范
|
||||
|
||||
### 9.1 原则
|
||||
|
||||
- 页面首次渲染直接 SSR 输出完整 5 个 Section。
|
||||
- 每个 Section 内部筛选/分页独立请求,仅刷新本 Section 容器。
|
||||
- 弹窗/Drawer 提交后定向刷新对应 Section 或右侧面板。
|
||||
|
||||
### 9.2 请求映射
|
||||
|
||||
| 操作 | URL | Target | Swap |
|
||||
|---|---|---|---|
|
||||
| 跟进类型筛选 | `/clients/{id}/follow-logs/partial` | `#follow-section-body` | `innerHTML` |
|
||||
| 跟进加载更多 | `/clients/{id}/follow-logs/partial?page=N` | `#follow-timeline-list` | `beforeend` |
|
||||
| 带看筛选 | `/clients/{id}/viewings/partial` | `#viewings-section-body` | `innerHTML` |
|
||||
| 客源解读刷新 | `/clients/{id}/insights/partial` | `#insights-section-body` | `innerHTML` |
|
||||
| 配房筛选/分页 | `/clients/{id}/matches/partial` | `#matches-section-body` | `innerHTML` |
|
||||
| 查看号码 | `/clients/{id}/contacts/{cid}/reveal-phone/` | `#phone-{cid}` | `innerHTML` |
|
||||
|
||||
---
|
||||
|
||||
## 10. 状态与可用性规范
|
||||
|
||||
### 10.1 Loading
|
||||
|
||||
- 每个 Section 内独立 `htmx-indicator` 骨架。
|
||||
- 按钮提交中显示 Spinner + 进行时文案(如 `保存中...`)。
|
||||
|
||||
### 10.2 Empty
|
||||
|
||||
- 跟进为空:`暂无跟进`
|
||||
- 带看为空:`暂无带看记录` + `新增带看` 按钮
|
||||
- 配房为空:`暂无匹配房源`
|
||||
|
||||
### 10.3 Error
|
||||
|
||||
- `htmx:responseError` 保留旧内容 + 右下角 Error Toast。
|
||||
|
||||
### 10.4 A11y
|
||||
|
||||
- 可点击项支持键盘 Tab 聚焦。
|
||||
- 所有交互控件保留 `focus-visible` 样式。
|
||||
- 锚点导航当前项 `aria-current="true"`。
|
||||
- Modal 打开时 `role="dialog"` + `aria-modal="true"`。
|
||||
|
||||
---
|
||||
|
||||
## 11. 工程落地清单
|
||||
|
||||
1. `body` 挂载 `x-data="clientDetailPage()"`,包含完整状态机(见第 8 节)。
|
||||
2. Topbar 使用 `bg-primary-800`,激活项 `bg-primary-600 text-white`。
|
||||
3. 主内容区:`ml-60 pt-[72px] min-h-screen bg-neutral-50 px-6 py-5`。
|
||||
4. Section 锚点导航 sticky `top-16 z-30`;右侧面板 sticky `top-16 max-h-[calc(100vh-80px)] overflow-y-auto`。
|
||||
5. 所有 Section 添加 class `section-anchor scroll-mt-24`,供 IntersectionObserver 监听。
|
||||
6. 右侧客源概览顶部标识区用 `bg-primary-600`(非 `bg-primary-800`)。
|
||||
7. 每个 Section 设置独立 HTMX target,避免全页刷新。
|
||||
8. Modal 遮罩 `z-50`,弹窗体 `z-60`;Drawer 遮罩 `z-50`,Drawer 面板 `z-60`。
|
||||
9. 全量检查 class 是否符合 token(颜色、圆角、焦点环、表格密度)。
|
||||
|
||||
---
|
||||
|
||||
## 12. 验收标准
|
||||
|
||||
- Topbar 为深青绿色 `bg-primary-800`,与内容区有明显层次区分。
|
||||
- 左侧主区无 Tab 切换行为,所有内容可连续滚动查看。
|
||||
- 点击 Section 导航仅发生锚点滚动,不触发内容隐藏/显示。
|
||||
- 锚点导航随滚动自动高亮当前 Section。
|
||||
- 页面视觉与 `客源详情_静态原型.html` 一致:层级、卡片密度、按钮和输入风格一致。
|
||||
- 颜色、字号、圆角、焦点环全部使用系统 token 与规范类名。
|
||||
- 关键路径(写跟进、改状态、查看号码)可在单页完成并有明确反馈(loading/toast/error)。
|
||||
747
Project/fonrey/UI_DESIGN/客源管理/新增客源_UI.md
Normal file
747
Project/fonrey/UI_DESIGN/客源管理/新增客源_UI.md
Normal file
@@ -0,0 +1,747 @@
|
||||
# 新增客源 UI 设计文档
|
||||
|
||||
> **版本**:v1.0 · **日期**:2026-04-26
|
||||
> **依赖规范**:UI_SYSTEM.md v1.2 · 组件规范设计.md v1.0
|
||||
> **PRD 来源**:`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` §5.2 录入私客 / Story 1
|
||||
> **优先级**:P0 功能在本文档中用 🔴 标注,P1 用 🟡,P2 用 ⚫
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [模块概述](#1-模块概述)
|
||||
- 1.1 功能范围
|
||||
- 1.2 页面清单
|
||||
- 1.3 用户角色与权限差异
|
||||
2. [页面设计规范](#2-页面设计规范)
|
||||
- 2.1 录入私客页面(P0 🔴)
|
||||
3. [弹窗/抽屉设计规范](#3-弹窗抽屉设计规范)
|
||||
- 3.1 扩展联系人字段展开区(内联展开)
|
||||
- 3.2 扩展基础信息字段展开区(内联展开)
|
||||
4. [交互状态规范](#4-交互状态规范)
|
||||
- 4.1 表单校验状态机
|
||||
- 4.2 权限控制矩阵
|
||||
- 4.3 HTMX 请求规范
|
||||
5. [关键数据字段说明](#5-关键数据字段说明)
|
||||
6. [竞品截图对应关系](#6-竞品截图对应关系)
|
||||
7. [实现优先级与工期估算](#7-实现优先级与工期估算)
|
||||
8. [开放问题(待决策)](#8-开放问题待决策)
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
### 1.1 功能范围
|
||||
|
||||
本文档聚焦 **新增客源(录入私客)** 单一页面的 UI 设计。该页面是客源管理模块的核心入口,对应 PRD Story 1。
|
||||
|
||||
| 功能 | 优先级 | PRD 章节 |
|
||||
|------|--------|---------|
|
||||
| 联系人区块录入(姓名/性别/电话1) | P0 🔴 | §5.2.2 |
|
||||
| 联系人扩展字段(电话2/微信/QQ) | P0 🔴 | §5.2.2 |
|
||||
| 多联系人支持(新增/删除) | P0 🔴 | §5.2.2 |
|
||||
| 基础信息录入(状态/用途/等级/来源) | P0 🔴 | §5.2.3 |
|
||||
| 基础信息扩展字段(证件/意向学校) | P1 🟡 | §5.2.3 |
|
||||
| 相关员工区块(首录人/归属人只读) | P0 🔴 | §5.2.4 |
|
||||
| 表单提交校验与成功跳转 | P0 🔴 | §5.2.5 |
|
||||
| 电话1 实时重复检测 | P0 🔴 | §5.2.5 / §5.4 |
|
||||
|
||||
### 1.2 页面清单
|
||||
|
||||
| 页面名称 | URL 模式建议 | 优先级 | 对应 PRD 章节 |
|
||||
|---------|------------|--------|--------------|
|
||||
| 录入私客 | `/clients/create/` | P0 🔴 | §5.2 Story 1 |
|
||||
|
||||
### 1.3 用户角色与权限差异
|
||||
|
||||
| 差异点 | 经纪人 | 店长 | 管理员 |
|
||||
|--------|--------|------|--------|
|
||||
| 访问入口 | 顶部导航「客源」→「+ 新增私客」 / 右侧浮动「增客」 | 同左 | 同左 |
|
||||
| 首录人字段 | 只读(自动填充自身) | 只读(自动填充自身) | 只读(自动填充自身) |
|
||||
| 归属人字段 | 只读(与首录人一致) | 只读(默认自身,可在详情页修改) | 只读(可在详情页修改) |
|
||||
| 来源选项 | 使用运营维护的枚举值,不可新增 | 同左 | 可在系统设置中维护来源枚举 |
|
||||
|
||||
> **说明**:录入页面本身无角色差异(字段完全相同),权限差异体现在录入后的归属人修改权限。
|
||||
|
||||
---
|
||||
|
||||
## 2. 页面设计规范
|
||||
|
||||
### 2.1 录入私客页面(P0 🔴)
|
||||
|
||||
#### 2.1.1 页面概述
|
||||
|
||||
- **URL**:`/clients/create/`
|
||||
- **访问入口**:
|
||||
- 顶部导航 「客源」 → 右上角「+ 新增私客」按钮
|
||||
- 右侧浮动快捷入口「增客」图标按钮
|
||||
- **页面职责**:录入新私客联系人信息、购房需求及相关员工,提交后跳转至客源详情页
|
||||
- **竞品参考截图**:`Project/fonrey/screenshots/客源/录入客源.png`
|
||||
|
||||
#### 2.1.2 布局结构
|
||||
|
||||
页面采用**居中单栏表单**布局,内容区水平居中显示,两侧留有 padding,无侧边栏(独立页面)。
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 顶部导航栏(Topbar,`bg-primary-800`,56px) │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ 面包屑 + 页面标题 H1「录入私客」 │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ 联系人1 区块 │ │
|
||||
│ │ (姓名 | 性别 | 电话1 + [电话2、微信、QQ等 ▾]) │ │
|
||||
│ ├────────────────────────────────────────────────────────┤ │
|
||||
│ │ 联系人2 区块(若已新增)[删除] │ │
|
||||
│ │ … │ │
|
||||
│ │ [+ 新增联系人] │ │
|
||||
│ ├────────────────────────────────────────────────────────┤ │
|
||||
│ │ 基础信息区块 │ │
|
||||
│ │ (状态 | 用途 | 等级 | 来源 + [证件、学校等 ▾]) │ │
|
||||
│ ├────────────────────────────────────────────────────────┤ │
|
||||
│ │ 相关员工区块 │ │
|
||||
│ │ (首录人 [只读] | 归属人 [只读]) │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [确定(橙色主按钮)] [取消] │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**布局核心参数:**
|
||||
|
||||
| 参数 | 值 |
|
||||
|------|-----|
|
||||
| 内容区最大宽度 | `max-w-5xl`(1024px),水平居中 `mx-auto` |
|
||||
| 内容区内边距 | `px-6 py-6` |
|
||||
| 页面背景 | `bg-neutral-50` |
|
||||
| 各区块容器 | `bg-white rounded-lg border border-neutral-200 p-6 mb-4` |
|
||||
| 区块间距 | `space-y-4` |
|
||||
|
||||
> **竞品截图对比**:竞品(巧房 V2.0)使用居中略窄的单栏卡片布局,表单内容在白色卡片区域内分区呈现。Fonrey 遵循同样模式,但改用 Teal 主色系,底部固定操作按钮与页面流同步(非 Sticky fixed)。
|
||||
|
||||
> **新增/编辑页面统一宽度约定(全局)**:从本页面起,客源模块后续所有「新增/编辑」类页面的内容区默认使用 `max-w-5xl`(1024px)+ `mx-auto`。若某页面字段更少可在局部收窄,但默认实现不得低于该基准,以避免右侧内容区留白过多与字段扩展空间不足。
|
||||
|
||||
#### 2.1.3 区域详细规范
|
||||
|
||||
---
|
||||
|
||||
**[区域 A:页面头部]**
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| 面包屑 | `客源 / 录入私客`,使用 `text-xs text-neutral-500`,分隔符 `/` |
|
||||
| 页面标题 | `录入私客`,`text-xl font-semibold text-neutral-800` |
|
||||
| 顶部无操作按钮 | 本页为纯录入表单,顶部无次级按钮 |
|
||||
|
||||
```html
|
||||
<!-- 页面头部 -->
|
||||
<div class="mb-6">
|
||||
<nav class="flex items-center gap-1 text-xs text-neutral-400 mb-2" aria-label="面包屑">
|
||||
<a href="/clients/" class="hover:text-neutral-600 hover:underline">客源</a>
|
||||
<span>/</span>
|
||||
<span class="text-neutral-600">录入私客</span>
|
||||
</nav>
|
||||
<h1 class="text-xl font-semibold text-neutral-800">录入私客</h1>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**[区域 B:联系人区块]**
|
||||
|
||||
联系人区块支持动态增减,默认展示联系人1(主联系人,不可删除),通过「+ 新增联系人」按钮追加更多联系人,上限5个。
|
||||
|
||||
**联系人1(主联系人,不可删除):**
|
||||
|
||||
| 字段 | 组件类型 | 必填 | 校验规则 | 默认值 |
|
||||
|------|---------|------|---------|--------|
|
||||
| 区块标题 | 静态文本「联系人1」 | — | — | — |
|
||||
| 姓名 | `<input type="text">` | 必填 ✱ | 不能为空 | `placeholder="请输入"` |
|
||||
| 性别 | Radio Group | 必填 ✱ | 必须选一 | 无默认选中 |
|
||||
| 电话1 | 区号下拉 + 手机号输入 | 必填 ✱ | 手机号格式(11位数字,`+86` 时) | 区号默认 `+86` |
|
||||
|
||||
> **截图依据**:竞品截图中,联系人1 无「删除」链接(仅联系人2起才有),姓名和电话1旁均有红色 `*` 标记,性别为「先生」/「女士」并排单选,电话1 左侧区号默认 `+86` 并可下拉修改。本设计与截图一致。
|
||||
|
||||
**性别单选按钮规范:**
|
||||
|
||||
```html
|
||||
<!-- 性别单选组 -->
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" name="contact_1_gender" value="male"
|
||||
class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700">先生</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" name="contact_1_gender" value="female"
|
||||
class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700">女士</span>
|
||||
</label>
|
||||
</div>
|
||||
```
|
||||
|
||||
**电话1(区号 + 手机号)规范:**
|
||||
|
||||
```html
|
||||
<!-- 电话1:分体输入框(区号下拉 + 手机号输入) -->
|
||||
<div class="flex gap-0 rounded-md border border-neutral-300 focus-within:border-primary-600 focus-within:ring-2 focus-within:ring-primary-600/20 overflow-hidden">
|
||||
<!-- 区号下拉 -->
|
||||
<select name="contact_1_phone_country_code"
|
||||
class="w-20 px-2 py-2 text-sm bg-neutral-50 border-r border-neutral-200 text-neutral-700
|
||||
focus:outline-none appearance-none">
|
||||
<option value="+86">+86</option>
|
||||
<option value="+852">+852</option>
|
||||
<option value="+853">+853</option>
|
||||
<option value="+886">+886</option>
|
||||
<option value="+1">+1</option>
|
||||
<!-- 更多国际区号 -->
|
||||
</select>
|
||||
<!-- 手机号输入 -->
|
||||
<input type="tel" name="contact_1_phone"
|
||||
placeholder="输入手机号"
|
||||
class="flex-1 px-3 py-2 text-sm bg-white focus:outline-none"
|
||||
hx-post="/clients/check-duplicate-phone/"
|
||||
hx-trigger="blur"
|
||||
hx-target="#phone1-duplicate-hint"
|
||||
hx-include="[name='contact_1_phone'],[name='contact_1_phone_country_code']">
|
||||
</div>
|
||||
<!-- 重复检测提示区域 -->
|
||||
<div id="phone1-duplicate-hint" class="mt-1"></div>
|
||||
```
|
||||
|
||||
> **重复检测提示(来自后端 HTMX 响应)**:
|
||||
> - 无重复:返回空 HTML,提示区域为空
|
||||
> - 有重复:返回橙色警告提示 `<p class="text-xs text-warning-600 flex items-center gap-1"><svg><!-- ExclamationTriangleIcon --></svg>存在重复客源:<a href="/clients/123/" class="underline hover:text-warning-700">张三</a>(可点击查看)</p>`
|
||||
|
||||
**扩展字段折叠区(「电话2、微信、QQ等 ▾」):**
|
||||
|
||||
Alpine.js 控制展开/收起,初始折叠。
|
||||
|
||||
| 字段 | 组件类型 | 必填 | 说明 |
|
||||
|------|---------|------|------|
|
||||
| 电话2 | 区号下拉 + 手机号输入 | 否 | 格式同电话1 |
|
||||
| 微信 | `<input type="text">` | 否 | `placeholder="请输入"` |
|
||||
| QQ | `<input type="text">` | 否 | `placeholder="请输入"` |
|
||||
|
||||
```html
|
||||
<!-- 扩展字段折叠触发器 -->
|
||||
<div x-data="{ expanded: false }">
|
||||
<button type="button" @click="expanded = !expanded"
|
||||
class="flex items-center gap-1 text-sm text-neutral-500 hover:text-neutral-700 mt-2 mb-1">
|
||||
<span>电话2、微信、QQ等</span>
|
||||
<svg class="w-4 h-4 transition-transform" :class="expanded ? 'rotate-180' : ''" aria-hidden="true">
|
||||
<!-- ChevronDownIcon -->
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 扩展字段区域 -->
|
||||
<div x-show="expanded" x-collapse class="space-y-3 mt-2">
|
||||
<!-- 电话2 -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-neutral-700">电话2</label>
|
||||
<!-- 同电话1区号+手机号组件,字段名 contact_1_phone2 / contact_1_phone2_country_code -->
|
||||
</div>
|
||||
<!-- 微信 -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-neutral-700">微信</label>
|
||||
<input type="text" name="contact_1_wechat" placeholder="请输入"
|
||||
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400
|
||||
focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20">
|
||||
</div>
|
||||
<!-- QQ -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-neutral-700">QQ</label>
|
||||
<input type="text" name="contact_1_qq" placeholder="请输入"
|
||||
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400
|
||||
focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**联系人2起的区块标题行(含「删除」):**
|
||||
|
||||
```html
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-semibold text-neutral-700">联系人2</h3>
|
||||
<button type="button" @click="removeContact(2)"
|
||||
class="text-xs text-danger-600 hover:text-danger-700 hover:underline">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**「+ 新增联系人」按钮:**
|
||||
|
||||
```html
|
||||
<button type="button" @click="addContact()"
|
||||
x-show="contacts.length < 5"
|
||||
class="flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700 mt-3">
|
||||
<svg class="w-4 h-4" aria-hidden="true"><!-- PlusIcon --></svg>
|
||||
新增联系人
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**[区域 C:基础信息区块]**
|
||||
|
||||
| 字段 | 组件类型 | 必填 | 枚举值 / 说明 | 默认值 |
|
||||
|------|---------|------|--------------|--------|
|
||||
| 状态(`status`) | Radio Group(横向排列) | 必填 ✱ | `buying`=求购 / `renting`=求租 / `buy_or_rent`=租购 | 无默认 |
|
||||
| 用途(`property_usage`) | Radio Group(横向排列) | 必填 ✱ | `residential`=住宅 / `villa`=别墅 / `commercial_residential`=商住 / `shop`=商铺 / `office`=写字楼 / `other`=其他 | 默认选中 `residential`(住宅) |
|
||||
| 等级(`grade`) | Radio Group(横向排列) | 必填 ✱ | `A_urgent`=A(急迫) / `A`=A / `B`=B(较强) / `C`=C(一般) / `D`=D(较弱) / `E`=E(暂不关注) | 无默认,截图中「A(急迫)」与「A」是两个独立选项 |
|
||||
| 来源(`source`) | `<select>` 下拉 | 必填 ✱ | `lookup_items` 维护的来源标识,非外键 | `placeholder="请选择"` |
|
||||
|
||||
> **截图依据**:竞品截图中,状态/用途/等级均以横向 Radio Group 形式展示,用途默认选中「住宅」(橙色圆点)。来源为下拉选择框。本设计与截图完全一致。
|
||||
|
||||
**单选按钮(Radio Group)统一规范:**
|
||||
|
||||
```html
|
||||
<!-- 示例:状态字段 -->
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">
|
||||
状态 <span class="text-danger-600">*</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap items-center gap-x-6 gap-y-2">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" name="status" value="buying"
|
||||
class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700">求购</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" name="status" value="renting"
|
||||
class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700">求租</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" name="status" value="buy_or_rent"
|
||||
class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700">租购</span>
|
||||
</label>
|
||||
</div>
|
||||
<!-- 校验错误提示(统一校验时注入) -->
|
||||
<p id="status-error" class="hidden text-xs text-danger-600">请选择客源状态</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**来源下拉选择:**
|
||||
|
||||
```html
|
||||
<div class="space-y-1.5">
|
||||
<label for="source" class="block text-sm font-medium text-neutral-700">
|
||||
来源 <span class="text-danger-600">*</span>
|
||||
</label>
|
||||
<select id="source" name="source"
|
||||
class="block w-full max-w-xs px-3 py-2 text-sm rounded-md border border-neutral-300
|
||||
focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20
|
||||
bg-white">
|
||||
<option value="">请选择</option>
|
||||
{% for item in source_options %}
|
||||
<option value="{{ item.value }}">{{ item.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p id="source-error" class="hidden text-xs text-danger-600">请选择客户来源</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**扩展字段折叠区(「证件类型、证件号码、意向学校等 ▾」):**
|
||||
|
||||
| 字段 | 组件类型 | 必填 | 说明 |
|
||||
|------|---------|------|------|
|
||||
| 证件类型 | `<select>` 下拉 | 否 | 身份证 / 护照 / 港澳通行证 / 其他 |
|
||||
| 证件号码 | `<input type="text">` | 否 | 选择「身份证」时校验18位格式 |
|
||||
| 意向学校 | `<input type="text">` (可多条) | 否 | 支持「+ 添加学校」追加输入框 |
|
||||
|
||||
> **注意**:PRD §5.2.3 扩展字段中「意向学校」在录入页为文本输入,编辑页(Story 14)有入学时间字段。本录入页**不包含入学时间**(仅编辑页有,与截图一致)。
|
||||
|
||||
```html
|
||||
<!-- 扩展字段折叠触发器 -->
|
||||
<div x-data="{ expanded: false, schools: [''] }">
|
||||
<button type="button" @click="expanded = !expanded"
|
||||
class="flex items-center gap-1 text-sm text-neutral-500 hover:text-neutral-700 mt-2 mb-1">
|
||||
<span>证件类型、证件号码、意向学校等</span>
|
||||
<svg class="w-4 h-4 transition-transform" :class="expanded ? 'rotate-180' : ''" aria-hidden="true">
|
||||
<!-- ChevronDownIcon -->
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="expanded" x-collapse class="space-y-3 mt-2">
|
||||
<!-- 证件类型 + 证件号码(并排双列) -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<label for="id_type" class="block text-sm font-medium text-neutral-700">证件类型</label>
|
||||
<select id="id_type" name="id_type"
|
||||
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300
|
||||
focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20 bg-white"
|
||||
x-model="idType">
|
||||
<option value="">请选择</option>
|
||||
<option value="id_card">身份证</option>
|
||||
<option value="passport">护照</option>
|
||||
<option value="hk_macao">港澳通行证</option>
|
||||
<option value="other">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label for="id_number" class="block text-sm font-medium text-neutral-700">证件号码</label>
|
||||
<input id="id_number" type="text" name="id_number" placeholder="请输入证件号码"
|
||||
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400
|
||||
focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20">
|
||||
</div>
|
||||
</div>
|
||||
<!-- 意向学校(多条动态) -->
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">意向学校</label>
|
||||
<template x-for="(school, idx) in schools" :key="idx">
|
||||
<input type="text" :name="'school_' + idx" x-model="schools[idx]"
|
||||
placeholder="请输入学校名称"
|
||||
class="block w-full max-w-xs px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400
|
||||
focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20 mb-2">
|
||||
</template>
|
||||
<button type="button" @click="schools.push('')"
|
||||
class="flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700">
|
||||
<svg class="w-4 h-4"><!-- PlusIcon --></svg>
|
||||
添加学校
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**[区域 D:相关员工区块]**
|
||||
|
||||
| 字段 | 组件类型 | 必填 | 说明 |
|
||||
|------|---------|------|------|
|
||||
| 首录人 | 只读文本输入框(`disabled`) | — | 自动填充当前登录用户「姓名 - 门店-组别」 |
|
||||
| 归属人 | 只读文本输入框(`disabled`) | — | 默认与首录人一致 |
|
||||
|
||||
```html
|
||||
<!-- 相关员工区块 -->
|
||||
<div class="bg-white rounded-lg border border-neutral-200 p-6">
|
||||
<h2 class="text-base font-semibold text-neutral-800 mb-4">相关员工</h2>
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">首录人</label>
|
||||
<input type="text" value="{{ request.user.display_name }}" disabled
|
||||
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-200
|
||||
bg-neutral-100 text-neutral-500 cursor-not-allowed">
|
||||
<input type="hidden" name="first_recorder_id" value="{{ request.user.id }}">
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">归属人</label>
|
||||
<input type="text" value="{{ request.user.display_name }}" disabled
|
||||
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-200
|
||||
bg-neutral-100 text-neutral-500 cursor-not-allowed">
|
||||
<input type="hidden" name="owner_id" value="{{ request.user.id }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
> **格式说明**:`display_name` 格式为「姓名 - 门店-组别」,如「杜利强 - 系统管理组」(竞品截图展示)。
|
||||
|
||||
---
|
||||
|
||||
**[区域 E:底部操作按钮]**
|
||||
|
||||
按钮组固定在表单内容底部(非 sticky fixed,随页面流):
|
||||
|
||||
```html
|
||||
<!-- 底部操作按钮组 -->
|
||||
<div class="flex items-center gap-3 mt-6 pb-8">
|
||||
<button type="submit" id="submit-btn"
|
||||
class="inline-flex items-center gap-1.5 px-6 py-2 text-sm font-medium
|
||||
bg-warning-600 text-white rounded-md
|
||||
hover:bg-warning-700 active:bg-warning-800
|
||||
focus:outline-none focus-visible:ring-2 focus-visible:ring-warning-600/40
|
||||
disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
确定
|
||||
</button>
|
||||
<a href="/clients/"
|
||||
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">
|
||||
取消
|
||||
</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
> **颜色说明**:竞品截图中「确定」按钮为橙色(竞品主色),对应 Fonrey 的 `warning-600`(`#D97706`)。根据截图优先原则,本模块的主提交按钮使用 `bg-warning-600`,而非 Fonrey 全局主色 `primary-600`,因为客源模块整体以橙色作为主操作色(与房源模块 Teal 色区分)。**Engineer 注意**:如产品决策统一为 Teal,改为 `bg-primary-600` 即可(见第8章开放问题)。
|
||||
|
||||
**Loading 态:**
|
||||
|
||||
```html
|
||||
<!-- 提交中 Loading 态 -->
|
||||
<button disabled
|
||||
class="inline-flex items-center gap-1.5 px-6 py-2 text-sm font-medium
|
||||
bg-warning-600 text-white rounded-md opacity-70 cursor-wait">
|
||||
<svg class="w-4 h-4 animate-spin"><!-- Spinner --></svg>
|
||||
保存中…
|
||||
</button>
|
||||
```
|
||||
|
||||
#### 2.1.4 使用的特殊组件
|
||||
|
||||
| 组件名 | 来源(组件规范设计.md 章节) | 用途 | 自定义说明 |
|
||||
|--------|--------------------------|------|-----------|
|
||||
| Modal Dialog | §7 Modal Dialog | 重复检测结果提示(若需二次确认) | 本模块不使用弹窗型重复确认,改为输入框下方内联提示 |
|
||||
| Collapsible(折叠展开) | UI_SYSTEM.md §3.11 Accordion | 联系人扩展字段 / 基础信息扩展字段的展开/收起 | 使用 `x-collapse` Alpine 插件,触发器为蓝色文字链接而非卡片标题 |
|
||||
| Multi-select Tag Input | §17 Multi-select Tag Input | 意向学校多条输入 | 简化为动态多个独立 input,非标准 Tag Input |
|
||||
|
||||
#### 2.1.5 空状态设计
|
||||
|
||||
本页为纯表单录入页,无列表空状态场景。以下为相关字段的空/初始状态:
|
||||
|
||||
| 场景 | 展示方式 |
|
||||
|------|---------|
|
||||
| 来源下拉无数据 | 显示「暂无可选来源,请联系管理员配置」,文字灰色禁用提示 |
|
||||
| 意向学校扩展默认 | 折叠收起,展开后默认一个空输入框 |
|
||||
| 重复检测提示区域初始 | 为空(不显示任何内容) |
|
||||
|
||||
#### 2.1.6 Loading 状态
|
||||
|
||||
| 场景 | 实现方式 |
|
||||
|------|---------|
|
||||
| 页面首次加载(来源枚举加载) | 来源 `<select>` 内 `<option disabled>加载中…</option>`,HTMX 加载完后 `hx-swap="outerHTML"` 替换 |
|
||||
| 提交表单 | 「确定」按钮 `disabled` + 内嵌 Spinner + 文案「保存中…」 |
|
||||
| 电话1 失焦重复检测 | 输入框下方出现 `animate-pulse` 单行灰色占位条,检测返回后替换 |
|
||||
|
||||
```html
|
||||
<!-- 重复检测 loading 占位 -->
|
||||
<div class="mt-1 animate-pulse">
|
||||
<div class="h-4 bg-neutral-200 rounded w-48"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 弹窗/抽屉设计规范
|
||||
|
||||
本模块(新增客源)无独立弹窗或抽屉组件,所有扩展内容均通过**内联折叠展开**实现。以下记录两个折叠区域的完整规范。
|
||||
|
||||
### 3.1 联系人扩展字段展开区(内联展开)
|
||||
|
||||
#### 3.1.1 触发方式
|
||||
|
||||
- **触发位置**:联系人区块内,电话1 输入框下方「电话2、微信、QQ等 ▾」文字链接
|
||||
- **组件类型**:内联 Collapsible(非 Modal/Drawer)
|
||||
- **尺寸**:随内容自适应高度
|
||||
|
||||
#### 3.1.2 内容字段
|
||||
|
||||
| 字段名 | 组件类型 | 必填 | 校验规则 | 默认值 |
|
||||
|--------|---------|------|---------|--------|
|
||||
| 电话2 | 区号下拉 + 手机号输入 | 否 | 格式可选(非必填时不校验) | 空 |
|
||||
| 微信 | `<input type="text">` | 否 | 无格式校验 | `placeholder="请输入"` |
|
||||
| QQ | `<input type="text">` | 否 | 数字格式(可选) | `placeholder="请输入"` |
|
||||
|
||||
#### 3.1.3 交互行为
|
||||
|
||||
- 点击「电话2、微信、QQ等 ▾」展开,箭头旋转 180°
|
||||
- 再次点击收起(已填内容不丢失)
|
||||
- `x-collapse` 插件控制高度动画
|
||||
|
||||
### 3.2 基础信息扩展字段展开区(内联展开)
|
||||
|
||||
#### 3.2.1 触发方式
|
||||
|
||||
- **触发位置**:基础信息区块内,来源字段下方「证件类型、证件号码、意向学校等 ▾」文字链接
|
||||
- **组件类型**:内联 Collapsible
|
||||
|
||||
#### 3.2.2 内容字段
|
||||
|
||||
| 字段名 | 组件类型 | 必填 | 校验规则 | 默认值 |
|
||||
|--------|---------|------|---------|--------|
|
||||
| 证件类型 | `<select>` 下拉 | 否 | — | 空(无默认选中) |
|
||||
| 证件号码 | `<input type="text">` | 否 | 若选「身份证」则校验18位格式 | `placeholder="请输入证件号码"` |
|
||||
| 意向学校 | 动态多 `<input>` | 否 | — | 1个空输入框;后端存入 `client_school_preferences.school_name`(`school_id=NULL` 自由输入模式) |
|
||||
|
||||
#### 3.2.3 交互行为
|
||||
|
||||
- 证件类型选择「身份证」时,证件号码输入框 blur 触发格式校验
|
||||
- 「+ 添加学校」追加一个新的学校输入框,Alpine.js 管理 `schools` 数组
|
||||
- 学校输入框可留空(提交时后端忽略空值)
|
||||
|
||||
---
|
||||
|
||||
## 4. 交互状态规范
|
||||
|
||||
### 4.1 表单校验状态机
|
||||
|
||||
```
|
||||
[初始态] → 用户填写字段 → [中间态]
|
||||
↓ 点击「确定」
|
||||
[统一校验]
|
||||
├── 所有必填字段已填 → [提交中] → 成功 → 跳转至客源详情页 + Toast「保存成功」
|
||||
│ 失败(5xx)→ Toast「保存失败,请重试」
|
||||
└── 存在未填必填字段 → [校验失败态]
|
||||
- 未填字段的输入框边框变红 `border-danger-600 ring-2 ring-danger-600/20`
|
||||
- 输入框下方显示红色提示文字
|
||||
- 页面自动滚动至第一个错误字段(`scrollIntoView`)
|
||||
```
|
||||
|
||||
**校验失败态输入框示例:**
|
||||
|
||||
```html
|
||||
<!-- 校验失败的字段 -->
|
||||
<input type="text" name="contact_1_name"
|
||||
class="block w-full px-3 py-2 text-sm rounded-md
|
||||
border border-danger-600 ring-2 ring-danger-600/20
|
||||
focus:outline-none focus:border-danger-600">
|
||||
<p class="text-xs text-danger-600 mt-1">姓名不能为空</p>
|
||||
```
|
||||
|
||||
**Radio Group 校验失败(无选中):**
|
||||
|
||||
```html
|
||||
<!-- 错误提示直接展示在 Radio Group 下方 -->
|
||||
<p id="status-error" class="text-xs text-danger-600 mt-1">请选择客源状态</p>
|
||||
```
|
||||
|
||||
**电话1 格式错误:**
|
||||
|
||||
```html
|
||||
<p class="text-xs text-danger-600 mt-1">请输入有效的手机号码</p>
|
||||
```
|
||||
|
||||
### 4.2 权限控制矩阵
|
||||
|
||||
| 操作 | 经纪人 | 店长 | 管理员 |
|
||||
|------|--------|------|--------|
|
||||
| 访问录入私客页面 | ✅ | ✅ | ✅ |
|
||||
| 提交录入表单 | ✅ | ✅ | ✅ |
|
||||
| 修改首录人字段 | ❌(只读) | ❌(只读) | ❌(只读,详情页才可改) |
|
||||
| 修改归属人字段 | ❌(只读) | ❌(只读,详情页才可改) | ❌(只读,详情页才可改) |
|
||||
| 查看来源枚举 | ✅ | ✅ | ✅(可在设置中维护) |
|
||||
|
||||
### 4.3 HTMX 请求规范
|
||||
|
||||
| 操作 | hx-trigger | hx-get/post/... | hx-target | hx-swap | Loading |
|
||||
|------|-----------|----------------|-----------|---------|---------|
|
||||
| 电话1 失焦重复检测 | `blur` | `hx-post="/clients/check-phone/"` | `#phone1-duplicate-hint` | `innerHTML` | 目标区出现骨架屏占位 |
|
||||
| 来源枚举动态加载(页面初始化) | `load` | `hx-get="/api/client-sources/"` | `#source-select` | `outerHTML` | select 内 `<option>加载中…</option>` |
|
||||
| 表单提交 | `submit`(原生 form) | `hx-post="/clients/create/"` | `body`(跳转)或 `#form-errors`(错误) | 成功:后端重定向;422:`innerHTML` | 「确定」按钮 Loading 态 |
|
||||
|
||||
**表单 HTMX 属性(完整):**
|
||||
|
||||
```html
|
||||
<form id="create-client-form"
|
||||
hx-post="/clients/create/"
|
||||
hx-target="#form-feedback"
|
||||
hx-swap="innerHTML"
|
||||
hx-on:htmx:before-request="document.getElementById('submit-btn').disabled=true"
|
||||
hx-on:htmx:after-settle="document.getElementById('submit-btn').disabled=false"
|
||||
class="space-y-4">
|
||||
<!-- 表单内容 -->
|
||||
<div id="form-feedback"></div>
|
||||
</form>
|
||||
```
|
||||
|
||||
> **成功跳转方案**:后端提交成功时,返回 `HX-Redirect: /clients/{id}/` header,HTMX 自动执行跳转。同时返回 `HX-Trigger: {"fonrey:toast": {"type": "success", "message": "保存成功"}}` 触发 Toast。
|
||||
|
||||
**电话1 重复检测完整 HTMX:**
|
||||
|
||||
```html
|
||||
<input type="tel" name="contact_1_phone"
|
||||
placeholder="输入手机号"
|
||||
class="..."
|
||||
hx-post="/clients/check-phone/"
|
||||
hx-trigger="blur"
|
||||
hx-target="#phone1-dup-hint"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="[name='contact_1_phone'],[name='contact_1_phone_country_code']"
|
||||
hx-indicator="#phone1-loading">
|
||||
<!-- Loading 指示器(隐藏,HTMX 自动显隐) -->
|
||||
<span id="phone1-loading" class="htmx-indicator">
|
||||
<div class="mt-1 animate-pulse h-4 bg-neutral-200 rounded w-48"></div>
|
||||
</span>
|
||||
<!-- 检测结果注入区 -->
|
||||
<div id="phone1-dup-hint"></div>
|
||||
```
|
||||
|
||||
**Alpine.js 管理的状态:**
|
||||
|
||||
| 状态 | 变量名 | 说明 |
|
||||
|------|--------|------|
|
||||
| 联系人列表 | `contacts` (Array) | 动态联系人区块,初始1个,最多5个 |
|
||||
| 每个联系人的扩展字段展开状态 | `contacts[i].expanded` (Boolean) | 控制「电话2/微信/QQ」区域展开 |
|
||||
| 基础信息扩展字段展开状态 | `infoExpanded` (Boolean) | 控制「证件/学校」区域展开 |
|
||||
| 意向学校列表 | `schools` (Array) | 动态学校输入框列表 |
|
||||
| 证件类型(联动校验) | `idType` (String) | 决定证件号码校验规则 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键数据字段说明
|
||||
|
||||
> **说明**:下表列出表单字段与数据库字段的对应关系。联系人字段来自 `client_contacts` 表,客源主字段来自 `clients` 表,意向学校通过 `client_school_preferences` 关联 `client_requirements` 存储。
|
||||
|
||||
| 字段名(表单 name) | 映射数据库字段 | 所在表 | 数据类型 | 说明 |
|
||||
|------------------|-------------|-------|---------|------|
|
||||
| `contacts[i][name]` | `name` | `client_contacts` | VARCHAR(50) | 必填,不限字数 |
|
||||
| `contacts[i][gender]` | `gender` | `client_contacts` | Enum | `male`=先生 / `female`=女士 |
|
||||
| `contacts[i][phone_country_code]` | `phone_country_code` | `client_contacts` | VARCHAR(10) | 如 `+86`,默认 `+86` |
|
||||
| `contacts[i][phone]` | `phone_enc`(AES加密存储)+ `phone_hash`(SHA-256,重复检测) | `client_contacts` | BYTEA + VARCHAR(64) | 必填;**明文仅在请求传输,后端加密存储,严禁明文落库** |
|
||||
| `contacts[i][phone2]` | `phone2_enc` + `phone2_hash` | `client_contacts` | BYTEA + VARCHAR(64) | 选填,同电话1加密规则 |
|
||||
| `contacts[i][wechat]` | `wechat` | `client_contacts` | VARCHAR(100) | 选填 |
|
||||
| `contacts[i][qq]` | `qq` | `client_contacts` | VARCHAR(20) | 选填 |
|
||||
| `status` | `status` | `clients` | Enum | `buying`=求购 / `renting`=求租 / `buy_or_rent`=租购 |
|
||||
| `property_usage` | `property_usage` | `clients` | Enum | `residential`=住宅 / `villa`=别墅 / `commercial_residential`=商住 / `shop`=商铺 / `office`=写字楼 / `other`=其他 |
|
||||
| `grade` | `grade` | `clients` | Enum | `A_urgent`=A(急迫) / `A`=A / `B`=B(较强) / `C`=C(一般) / `D`=D(较弱) / `E`=E(暂不关注) |
|
||||
| `source` | `source` | `clients` | VARCHAR(50) | 由 `lookup_items` 维护的来源标识,非外键 |
|
||||
| `id_type` | `id_type` | `clients` | Enum | `id_card`=身份证 / `passport`=护照 / `hk_macao`=港澳通行证 / `other`=其他(选填) |
|
||||
| `id_number` | `id_number_enc`(AES加密存储) | `clients` | BYTEA | 选填;**后端加密存储,严禁明文落库**;身份证格式前端校验18位 |
|
||||
| `schools[i]` | `school_name` | `client_school_preferences` | VARCHAR(100) | 选填,多条;通过 `client_requirements` 关联;`school_id` 为 NULL(自由输入模式) |
|
||||
| `first_recorder_id` | `first_recorder_id` | `clients` | UUID FK→staff | 自动填充当前登录用户,不可用户修改 |
|
||||
| `owner_id` | `owner_id` | `clients` | UUID FK→staff | 默认等于首录人,管理员可在详情页修改 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 竞品截图对应关系
|
||||
|
||||
| 截图路径 | 对应功能 | 对应文档章节 | 采纳的设计要点 |
|
||||
|---------|---------|------------|--------------|
|
||||
| `Project/fonrey/screenshots/客源/录入客源.png` | 录入私客完整表单 | §2.1 整体布局 | 居中单栏布局;三区块结构(联系人/基础信息/相关员工);「电话2、微信、QQ等 ▾」折叠文字链接;联系人2 标题旁红色「删除」;「+ 新增联系人」蓝色文字链接;「确定」橙色主按钮;相关员工两字段灰色禁用输入框 |
|
||||
| `Project/fonrey/screenshots/客源/录入客源.png` | 基础信息单选组布局 | §2.1.3 区域C | 状态/用途/等级 均横向 Radio Group;用途默认选中「住宅」(圆点显示);来源为独立下拉框;证件/学校字段以「证件类型、证件号码、意向学校等 ▾」折叠隐藏 |
|
||||
| `Project/fonrey/screenshots/客源/编辑客源.png` | 编辑客源三Tab布局参考 | §2.1.2 布局(参考) | 本文档仅为新增页面,但从编辑截图可见基础信息和二手 Tab 的完整字段(入学时间、意向商圈等);新增页面**不含二手Tab**,仅录入基础联系信息和意向等级 |
|
||||
| `Project/fonrey/screenshots/客源/编辑客源.png` | 联系人编辑字段参考 | §2.1.3 区域B(字段定义) | 编辑截图展示联系人1 完整字段布局:姓名+电话1(含「标记无效」)+微信 单行三列;称呼 Radio;电话2;QQ;备注。录入页简化版与此一致(缺「标记无效」和「备注」字段,与录入截图一致) |
|
||||
|
||||
**截图与 PRD 差异说明:**
|
||||
|
||||
| 差异点 | 截图呈现 | PRD 描述 | 采纳方案 |
|
||||
|--------|---------|---------|---------|
|
||||
| 联系人备注字段 | 编辑截图有「备注」,录入截图无 | Story 1 未提联系人备注 | **以录入截图为准,录入页不含联系人备注**(仅编辑页有) |
|
||||
| 入学时间 | 仅出现在编辑截图(基础信息Tab) | Story 14 编辑功能描述 | **录入页不含入学时间**(仅编辑页有) |
|
||||
| 按钮颜色 | 竞品使用橙色(`#FF6B00` 系) | PRD 描述「橙色主按钮」 | 使用 `warning-600`(`#D97706`),详见第8章开放问题 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 实现优先级与工期估算
|
||||
|
||||
| 页面/功能 | 优先级 | 特殊组件复杂度 | 工期估算(前端) |
|
||||
|---------|--------|--------------|--------------|
|
||||
| 录入私客页面整体骨架 | P0 🔴 | 低 | 0.5 天 |
|
||||
| 联系人区块(必填字段) | P0 🔴 | 低(Radio + Input) | 0.5 天 |
|
||||
| 电话1 区号+手机号分体输入框 | P0 🔴 | 中(自定义分体组件) | 0.5 天 |
|
||||
| 联系人动态增删(Alpine.js) | P0 🔴 | 中(动态渲染) | 0.5 天 |
|
||||
| 联系人扩展字段折叠展开 | P0 🔴 | 低(x-collapse) | 0.25 天 |
|
||||
| 基础信息区块(必填字段) | P0 🔴 | 低(Radio + Select) | 0.5 天 |
|
||||
| 基础信息扩展字段折叠展开 | P1 🟡 | 低(x-collapse) | 0.25 天 |
|
||||
| 意向学校动态多条输入 | P1 🟡 | 低(Alpine.js 数组) | 0.25 天 |
|
||||
| 相关员工只读区块 | P0 🔴 | 低(禁用 Input) | 0.25 天 |
|
||||
| 表单统一校验 + 滚动定位 | P0 🔴 | 中(JS 校验逻辑) | 0.5 天 |
|
||||
| 电话1 实时重复检测(HTMX) | P0 🔴 | 中(HTMX + 后端接口) | 0.5 天 |
|
||||
| 提交成功跳转 + Toast | P0 🔴 | 低 | 0.25 天 |
|
||||
| **合计** | — | — | **约 4.25 天** |
|
||||
|
||||
---
|
||||
|
||||
## 8. 开放问题(待决策)
|
||||
|
||||
| # | 问题 | 影响范围 | 待确认方 | 回答问题 |
|
||||
| --- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----- | ------------------ |
|
||||
| 1 | **主操作按钮颜色**:竞品截图和 PRD 均描述「确定」为橙色,但 Fonrey 设计规范主色为 Teal(`primary-600`)。应使用 `warning-600`(与竞品一致,橙色)还是 `primary-600`(与全局规范一致,Teal)? | §2.1.3 区域E,影响全模块提交按钮 | 产品/设计 | `primary-600`和全局一致 |
|
||||
| 2 | **联系人上限**:PRD §5.2.2 写「理论上不限联系人数量(建议上限5个)」,本文档取5个上限。是否采用5个或其他数量? | §2.1.3 区域B | 产品 | 3个 |
|
||||
| 3 | **来源枚举加载时机**:来源下拉是页面加载时随 Django 模板渲染(同步),还是通过 HTMX 异步加载?若系统配置中来源枚举较少(<50条),推荐同步渲染 | §2.1.3 区域C,§4.3 HTMX规范 | 后端/产品 | |
|
||||
| 4 | **电话重复检测后是否阻断提交**:当前方案为「警告不阻断」(允许继续提交),与 PRD §5.2.5 一致。是否在产品层面确认不需要强制阻断(即两人同一号码可同时保存)? | §4.3 电话检测行为 | 产品 | |
|
||||
| 5 | **意向学校数据源**:PRD §5.2.3 录入页为「下拉多选」,但编辑页截图显示为「文本输入 + 添加学校」。本文档以截图为准取文本输入。若产品希望关联楼盘数据库中的学校,需改为带搜索的下拉选择器 | §3.2.2 意向学校字段 | 产品/数据 | 改为带搜索的下拉选择器 |
|
||||
| 6 | **区号选择器完整枚举**:目前仅列出常用5个区号(+86/+852/+853/+886/+1)。是否需要完整的国际区号列表(约240+)?若是,建议实现带搜索的 Alpine.js 自定义下拉,而非原生 `<select>` | §2.1.3 电话1规范 | 产品 | 只保留+86 |
|
||||
Reference in New Issue
Block a user