修改文档
This commit is contained in:
733
Project/fonrey/UI_DESIGN/客源管理/编辑客源_UI.md
Normal file
733
Project/fonrey/UI_DESIGN/客源管理/编辑客源_UI.md
Normal file
@@ -0,0 +1,733 @@
|
||||
# 编辑客源 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 页面清单 | 前端 + 后端工程师 |
|
||||
643
Project/fonrey/UI_DESIGN/新增客源_UI.html
Normal file
643
Project/fonrey/UI_DESIGN/新增客源_UI.html
Normal file
@@ -0,0 +1,643 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=1280">
|
||||
<title>Fonrey 新增客源 · 静态原型</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#F0FDFA',
|
||||
100: '#CCFBF1',
|
||||
200: '#99F6E4',
|
||||
500: '#14B8A6',
|
||||
600: '#0F766E',
|
||||
700: '#115E59',
|
||||
800: '#134E4A'
|
||||
},
|
||||
neutral: {
|
||||
50: '#F8FAFC',
|
||||
100: '#F1F5F9',
|
||||
200: '#E2E8F0',
|
||||
300: '#CBD5E1',
|
||||
400: '#94A3B8',
|
||||
500: '#64748B',
|
||||
600: '#475569',
|
||||
700: '#334155',
|
||||
800: '#1E293B',
|
||||
900: '#0F172A'
|
||||
},
|
||||
success: { 50: '#F0FDF4', 600: '#16A34A' },
|
||||
warning: { 50: '#FFFBEB', 600: '#D97706' },
|
||||
danger: { 50: '#FEF2F2', 600: '#DC2626' },
|
||||
info: { 50: '#EFF6FF', 600: '#2563EB' }
|
||||
},
|
||||
boxShadow: {
|
||||
xs: '0 1px 2px rgba(15,23,42,0.04)'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'PingFang SC', 'Microsoft YaHei', 'sans-serif']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
html { scroll-behavior: smooth; }
|
||||
[x-cloak] { display: none !important; }
|
||||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-neutral-50 text-sm text-neutral-700 antialiased" x-data="createClientPage()">
|
||||
<header class="fixed top-0 left-0 right-0 h-14 z-20 bg-primary-800 flex items-center justify-between">
|
||||
<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 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 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>
|
||||
<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 text-primary-100 hover:bg-primary-700 hover:text-white">交易</a>
|
||||
<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 text-primary-100 hover:bg-primary-700 hover:text-white">人事</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">系统</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="消息">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022 23.848 23.848 0 0 0 5.454 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
||||
</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>
|
||||
|
||||
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-neutral-200 bg-white">
|
||||
<nav class="p-3 space-y-0.5">
|
||||
<div class="px-2 pt-2 pb-1 text-xs font-medium text-neutral-500 uppercase tracking-wide">客源管理</div>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md bg-primary-50 text-primary-700 font-medium">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>
|
||||
私客列表
|
||||
</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">公客池</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">成交客</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">已删客源</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="ml-60 pt-[72px] min-h-screen bg-neutral-50 px-6 py-5">
|
||||
<div class="mx-auto max-w-5xl">
|
||||
<div class="mb-6">
|
||||
<nav class="flex items-center gap-1 text-xs text-neutral-500 mb-2" aria-label="面包屑">
|
||||
<a href="/clients/" class="hover:text-neutral-700">客源</a>
|
||||
<span>/</span>
|
||||
<span class="text-neutral-700">录入私客</span>
|
||||
</nav>
|
||||
<h1 class="text-xl font-semibold text-neutral-800">录入私客</h1>
|
||||
</div>
|
||||
|
||||
<form id="create-client-form" @submit.prevent="submitForm" class="space-y-4">
|
||||
<section class="bg-white rounded-lg border border-neutral-200 p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-base font-semibold text-neutral-800">联系人</h2>
|
||||
<span class="text-xs text-neutral-500">最多添加 5 位联系人</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<template x-for="(contact, idx) in form.contacts" :key="contact.id">
|
||||
<div class="border border-neutral-200 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-semibold text-neutral-700" x-text="'联系人' + (idx + 1)"></h3>
|
||||
<button
|
||||
type="button"
|
||||
x-show="idx > 0"
|
||||
@click="removeContact(idx)"
|
||||
class="text-xs text-danger-600 hover:text-danger-600 hover:underline"
|
||||
>删除</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">
|
||||
姓名 <span class="text-danger-600">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model.trim="contact.name"
|
||||
:data-field-key="fieldKey('contacts', idx, 'name')"
|
||||
placeholder="请输入"
|
||||
class="block w-full px-3 py-2 text-sm rounded-md border placeholder:text-neutral-400 focus:outline-none focus:ring-2"
|
||||
:class="inputClass(fieldKey('contacts', idx, 'name'))"
|
||||
>
|
||||
<p class="text-xs text-danger-600" x-show="errors[fieldKey('contacts', idx, 'name')]" x-text="errors[fieldKey('contacts', idx, 'name')]"></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">
|
||||
性别 <span class="text-danger-600">*</span>
|
||||
</label>
|
||||
<div :data-field-key="fieldKey('contacts', idx, 'gender')" class="flex items-center gap-6 rounded-md border px-3 py-2 min-h-[42px]"
|
||||
:class="groupClass(fieldKey('contacts', idx, 'gender'))">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" x-model="contact.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" x-model="contact.gender" value="female" class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700">女士</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-danger-600" x-show="errors[fieldKey('contacts', idx, 'gender')]" x-text="errors[fieldKey('contacts', idx, 'gender')]"></p>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">
|
||||
电话1 <span class="text-danger-600">*</span>
|
||||
</label>
|
||||
<div
|
||||
:data-field-key="fieldKey('contacts', idx, 'phone')"
|
||||
class="flex rounded-md border overflow-hidden focus-within:ring-2"
|
||||
:class="phoneWrapClass(fieldKey('contacts', idx, 'phone'))"
|
||||
>
|
||||
<select x-model="contact.phoneCountryCode" class="w-24 px-2 py-2 text-sm bg-neutral-50 border-r border-neutral-200 text-neutral-700 focus:outline-none">
|
||||
<template x-for="code in countryCodes" :key="code">
|
||||
<option :value="code" x-text="code"></option>
|
||||
</template>
|
||||
</select>
|
||||
<input
|
||||
type="tel"
|
||||
x-model.trim="contact.phone"
|
||||
@blur="idx === 0 ? checkDuplicatePhone() : null"
|
||||
placeholder="输入手机号"
|
||||
class="flex-1 px-3 py-2 text-sm bg-white focus:outline-none"
|
||||
>
|
||||
</div>
|
||||
<p class="text-xs text-danger-600" x-show="errors[fieldKey('contacts', idx, 'phone')]" x-text="errors[fieldKey('contacts', idx, 'phone')]"></p>
|
||||
|
||||
<template x-if="idx === 0">
|
||||
<div class="mt-1 min-h-[20px]">
|
||||
<template x-if="checkingDuplicate">
|
||||
<div class="animate-pulse h-4 bg-neutral-200 rounded w-56"></div>
|
||||
</template>
|
||||
<p
|
||||
x-show="duplicateHint"
|
||||
x-text="duplicateHint"
|
||||
class="text-xs text-warning-600"
|
||||
></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="contact.expanded = !contact.expanded"
|
||||
class="flex items-center gap-1 text-sm text-neutral-500 hover:text-neutral-700"
|
||||
>
|
||||
<span>电话2、微信、QQ等</span>
|
||||
<svg class="w-4 h-4 transition-transform" :class="contact.expanded ? 'rotate-180' : ''" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
|
||||
<div x-show="contact.expanded" x-transition class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">电话2</label>
|
||||
<div class="flex rounded-md border border-neutral-300 overflow-hidden focus-within:border-primary-600 focus-within:ring-2 focus-within:ring-primary-600/20">
|
||||
<select x-model="contact.phone2CountryCode" class="w-24 px-2 py-2 text-sm bg-neutral-50 border-r border-neutral-200 text-neutral-700 focus:outline-none">
|
||||
<template x-for="code in countryCodes" :key="'p2-' + code">
|
||||
<option :value="code" x-text="code"></option>
|
||||
</template>
|
||||
</select>
|
||||
<input type="tel" x-model.trim="contact.phone2" placeholder="输入手机号" class="flex-1 px-3 py-2 text-sm bg-white focus:outline-none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">微信</label>
|
||||
<input type="text" x-model.trim="contact.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>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">QQ</label>
|
||||
<input type="text" x-model.trim="contact.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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
x-show="form.contacts.length < 5"
|
||||
@click="addContact"
|
||||
class="flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||
新增联系人
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section 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="space-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">状态 <span class="text-danger-600">*</span></label>
|
||||
<div data-field-key="status" class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]" :class="groupClass('status')">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" x-model="form.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" x-model="form.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" x-model="form.status" value="buy_or_rent" class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700">租购</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-danger-600" x-show="errors.status" x-text="errors.status"></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">用途 <span class="text-danger-600">*</span></label>
|
||||
<div data-field-key="property_usage" class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]" :class="groupClass('property_usage')">
|
||||
<template x-for="item in usageOptions" :key="item.value">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" x-model="form.propertyUsage" :value="item.value" class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700" x-text="item.label"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<p class="text-xs text-danger-600" x-show="errors.property_usage" x-text="errors.property_usage"></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">等级 <span class="text-danger-600">*</span></label>
|
||||
<div data-field-key="grade" class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]" :class="groupClass('grade')">
|
||||
<template x-for="item in gradeOptions" :key="item.value">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" x-model="form.grade" :value="item.value" class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700" x-text="item.label"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<p class="text-xs text-danger-600" x-show="errors.grade" x-text="errors.grade"></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">来源 <span class="text-danger-600">*</span></label>
|
||||
<select
|
||||
data-field-key="source"
|
||||
x-model="form.source"
|
||||
:disabled="sourceLoading"
|
||||
class="block w-full max-w-xs px-3 py-2 text-sm rounded-md border bg-white focus:outline-none focus:ring-2 disabled:bg-neutral-100 disabled:text-neutral-400"
|
||||
:class="inputClass('source')"
|
||||
>
|
||||
<option value="" x-text="sourceLoading ? '加载中...' : '请选择'"></option>
|
||||
<template x-for="item in sourceOptions" :key="item.value">
|
||||
<option :value="item.value" x-text="item.label"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p class="text-xs text-danger-600" x-show="errors.source" x-text="errors.source"></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button" @click="infoExpanded = !infoExpanded" class="flex items-center gap-1 text-sm text-neutral-500 hover:text-neutral-700">
|
||||
<span>证件类型、证件号码、意向学校等</span>
|
||||
<svg class="w-4 h-4 transition-transform" :class="infoExpanded ? 'rotate-180' : ''" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
|
||||
<div x-show="infoExpanded" x-transition class="mt-3 space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">证件类型</label>
|
||||
<select x-model="form.idType" class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 bg-white focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20">
|
||||
<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 class="block text-sm font-medium text-neutral-700">证件号码</label>
|
||||
<input type="text" x-model.trim="form.idNumber" 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-2">
|
||||
<label class="block text-sm font-medium text-neutral-700">意向学校</label>
|
||||
<template x-for="(school, sIdx) in form.schools" :key="'school-' + sIdx">
|
||||
<div class="flex items-center gap-2 max-w-md">
|
||||
<input type="text" x-model.trim="form.schools[sIdx]" 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">
|
||||
<button type="button" @click="removeSchool(sIdx)" x-show="form.schools.length > 1" class="px-2 py-2 text-xs text-danger-600 hover:bg-danger-50 rounded-md">删除</button>
|
||||
</div>
|
||||
</template>
|
||||
<button type="button" @click="addSchool" class="flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||
添加学校
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section 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-1 md: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="魏深 - 都市港湾店一组" 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" x-model="form.firstRecorderId">
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">归属人</label>
|
||||
<input type="text" value="魏深 - 都市港湾店一组" 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" x-model="form.ownerId">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex items-center gap-3 mt-2 pb-8">
|
||||
<button
|
||||
type="submit"
|
||||
: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"
|
||||
>
|
||||
<svg x-show="submitting" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-opacity=".25" stroke-width="4"></circle>
|
||||
<path d="M22 12a10 10 0 0 1-10 10" stroke="currentColor" stroke-width="4"></path>
|
||||
</svg>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div x-show="toast.show" x-transition.opacity class="fixed bottom-6 right-6 z-[70]">
|
||||
<div class="w-80 bg-white rounded-lg shadow-lg border border-neutral-200 flex items-start gap-3 p-3">
|
||||
<svg class="w-5 h-5 text-success-600 shrink-0 mt-0.5" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>
|
||||
<div class="flex-1 text-sm">
|
||||
<p class="font-medium text-neutral-800" x-text="toast.message"></p>
|
||||
<p class="text-xs text-neutral-500">客源信息已保存</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function createClientPage() {
|
||||
return {
|
||||
countryCodes: ['+86', '+852', '+853', '+886', '+1'],
|
||||
usageOptions: [
|
||||
{ value: 'residential', label: '住宅' },
|
||||
{ value: 'villa', label: '别墅' },
|
||||
{ value: 'commercial_residential', label: '商住' },
|
||||
{ value: 'shop', label: '商铺' },
|
||||
{ value: 'office', label: '写字楼' },
|
||||
{ value: 'other', label: '其他' }
|
||||
],
|
||||
gradeOptions: [
|
||||
{ value: 'A_urgent', label: 'A(急迫)' },
|
||||
{ value: 'A', label: 'A' },
|
||||
{ value: 'B', label: 'B(较强)' },
|
||||
{ value: 'C', label: 'C(一般)' },
|
||||
{ value: 'D', label: 'D(较弱)' },
|
||||
{ value: 'E', label: 'E(暂不关注)' }
|
||||
],
|
||||
sourceLoading: true,
|
||||
sourceOptions: [],
|
||||
infoExpanded: false,
|
||||
checkingDuplicate: false,
|
||||
duplicateHint: '',
|
||||
submitting: false,
|
||||
redirectHint: '',
|
||||
errors: {},
|
||||
toast: {
|
||||
show: false,
|
||||
message: ''
|
||||
},
|
||||
form: {
|
||||
contacts: [],
|
||||
status: '',
|
||||
propertyUsage: 'residential',
|
||||
grade: '',
|
||||
source: '',
|
||||
idType: '',
|
||||
idNumber: '',
|
||||
schools: [''],
|
||||
firstRecorderId: 'staff-1001',
|
||||
ownerId: 'staff-1001'
|
||||
},
|
||||
|
||||
init() {
|
||||
this.form.contacts = [this.newContact()];
|
||||
this.simulateSourceLoading();
|
||||
},
|
||||
|
||||
newContact() {
|
||||
return {
|
||||
id: 'c-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
|
||||
name: '',
|
||||
gender: '',
|
||||
phoneCountryCode: '+86',
|
||||
phone: '',
|
||||
expanded: false,
|
||||
phone2CountryCode: '+86',
|
||||
phone2: '',
|
||||
wechat: '',
|
||||
qq: ''
|
||||
};
|
||||
},
|
||||
|
||||
fieldKey(group, idx, field) {
|
||||
return group + '_' + idx + '_' + field;
|
||||
},
|
||||
|
||||
inputClass(key) {
|
||||
return this.errors[key]
|
||||
? 'border-danger-600 ring-danger-600/20 focus:border-danger-600 focus:ring-danger-600/20'
|
||||
: 'border-neutral-300 focus:border-primary-600 focus:ring-primary-600/20';
|
||||
},
|
||||
|
||||
groupClass(key) {
|
||||
return this.errors[key]
|
||||
? 'border-danger-600 ring-2 ring-danger-600/20'
|
||||
: 'border-neutral-300';
|
||||
},
|
||||
|
||||
phoneWrapClass(key) {
|
||||
return this.errors[key]
|
||||
? 'border-danger-600 ring-danger-600/20 focus-within:border-danger-600 focus-within:ring-danger-600/20'
|
||||
: 'border-neutral-300 focus-within:border-primary-600 focus-within:ring-primary-600/20';
|
||||
},
|
||||
|
||||
addContact() {
|
||||
if (this.form.contacts.length >= 5) return;
|
||||
this.form.contacts.push(this.newContact());
|
||||
},
|
||||
|
||||
removeContact(idx) {
|
||||
if (idx === 0) return;
|
||||
this.form.contacts.splice(idx, 1);
|
||||
},
|
||||
|
||||
addSchool() {
|
||||
this.form.schools.push('');
|
||||
},
|
||||
|
||||
removeSchool(idx) {
|
||||
if (this.form.schools.length <= 1) return;
|
||||
this.form.schools.splice(idx, 1);
|
||||
},
|
||||
|
||||
simulateSourceLoading() {
|
||||
setTimeout(() => {
|
||||
this.sourceOptions = [
|
||||
{ value: 'store_visit', label: '线下丨门店接待' },
|
||||
{ value: 'old_client_referral', label: '老客户转介绍' },
|
||||
{ value: 'online_form', label: '线上丨留资表单' },
|
||||
{ value: 'community_push', label: '社群活动' }
|
||||
];
|
||||
this.sourceLoading = false;
|
||||
}, 900);
|
||||
},
|
||||
|
||||
validatePhone(phone, countryCode) {
|
||||
if (!phone) return false;
|
||||
if (countryCode === '+86') return /^1\d{10}$/.test(phone);
|
||||
return /^[0-9]{5,20}$/.test(phone);
|
||||
},
|
||||
|
||||
validateForm() {
|
||||
this.errors = {};
|
||||
let firstErrorKey = '';
|
||||
|
||||
this.form.contacts.forEach((contact, idx) => {
|
||||
const nameKey = this.fieldKey('contacts', idx, 'name');
|
||||
const genderKey = this.fieldKey('contacts', idx, 'gender');
|
||||
const phoneKey = this.fieldKey('contacts', idx, 'phone');
|
||||
|
||||
if (!contact.name) {
|
||||
this.errors[nameKey] = '姓名不能为空';
|
||||
if (!firstErrorKey) firstErrorKey = nameKey;
|
||||
}
|
||||
|
||||
if (!contact.gender) {
|
||||
this.errors[genderKey] = '请选择性别';
|
||||
if (!firstErrorKey) firstErrorKey = genderKey;
|
||||
}
|
||||
|
||||
if (!contact.phone) {
|
||||
this.errors[phoneKey] = '请输入手机号';
|
||||
if (!firstErrorKey) firstErrorKey = phoneKey;
|
||||
} else if (!this.validatePhone(contact.phone, contact.phoneCountryCode)) {
|
||||
this.errors[phoneKey] = contact.phoneCountryCode === '+86' ? '请输入有效的手机号码' : '请输入有效的电话号码';
|
||||
if (!firstErrorKey) firstErrorKey = phoneKey;
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.form.status) {
|
||||
this.errors.status = '请选择客源状态';
|
||||
if (!firstErrorKey) firstErrorKey = 'status';
|
||||
}
|
||||
|
||||
if (!this.form.propertyUsage) {
|
||||
this.errors.property_usage = '请选择用途';
|
||||
if (!firstErrorKey) firstErrorKey = 'property_usage';
|
||||
}
|
||||
|
||||
if (!this.form.grade) {
|
||||
this.errors.grade = '请选择客源等级';
|
||||
if (!firstErrorKey) firstErrorKey = 'grade';
|
||||
}
|
||||
|
||||
if (!this.form.source) {
|
||||
this.errors.source = '请选择客户来源';
|
||||
if (!firstErrorKey) firstErrorKey = 'source';
|
||||
}
|
||||
|
||||
if (firstErrorKey) {
|
||||
this.$nextTick(() => {
|
||||
const target = document.querySelector('[data-field-key="' + firstErrorKey + '"]');
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
if (typeof target.focus === 'function') target.focus({ preventScroll: true });
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
checkDuplicatePhone() {
|
||||
const c = this.form.contacts[0];
|
||||
this.duplicateHint = '';
|
||||
if (!c.phone || !this.validatePhone(c.phone, c.phoneCountryCode)) return;
|
||||
|
||||
this.checkingDuplicate = true;
|
||||
setTimeout(() => {
|
||||
const duplicatedPhones = ['13800138000', '13911112222', '13700009999'];
|
||||
if (c.phoneCountryCode === '+86' && duplicatedPhones.includes(c.phone)) {
|
||||
this.duplicateHint = '存在重复客源:张三(可前往详情查看),可继续提交但建议先核对。';
|
||||
}
|
||||
this.checkingDuplicate = false;
|
||||
}, 600);
|
||||
},
|
||||
|
||||
showToast(message) {
|
||||
this.toast.message = message;
|
||||
this.toast.show = true;
|
||||
setTimeout(() => {
|
||||
this.toast.show = false;
|
||||
}, 2200);
|
||||
},
|
||||
|
||||
submitForm() {
|
||||
if (this.submitting) return;
|
||||
if (!this.validateForm()) return;
|
||||
|
||||
this.submitting = true;
|
||||
this.redirectHint = '';
|
||||
|
||||
setTimeout(() => {
|
||||
this.submitting = false;
|
||||
this.showToast('保存成功');
|
||||
this.redirectHint = '模拟跳转中:即将进入客源详情页 /clients/CL20260426001/';
|
||||
}, 1200);
|
||||
},
|
||||
|
||||
cancelForm() {
|
||||
this.redirectHint = '已取消录入,模拟返回客源列表 /clients/';
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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,影响全模块提交按钮 | 产品/设计 |
|
||||
| 2 | **联系人上限**:PRD §5.2.2 写「理论上不限联系人数量(建议上限5个)」,本文档取5个上限。是否采用5个或其他数量? | §2.1.3 区域B | 产品 |
|
||||
| 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规范 | 产品 |
|
||||
1082
Project/fonrey/UI_DESIGN/编辑客源_UI.html
Normal file
1082
Project/fonrey/UI_DESIGN/编辑客源_UI.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,5 @@
|
||||
# Fonrey 模块 UI 设计文档生成提示词
|
||||
|
||||
> **用途**:每次针对一个具体业务模块,填入变量后直接发给 AI,输出该模块的标准化 UI 设计文档。
|
||||
> **输出文件**:`Project/fonrey/UI_DESIGN/{模块名}_UI.md`
|
||||
|
||||
---
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 复制下方「---PROMPT START---」到「---PROMPT END---」之间的全部内容
|
||||
2. 将所有 `{{变量}}` 替换为本次模块的实际值
|
||||
3. 把替换后的提示词发给 AI
|
||||
|
||||
---PROMPT START---
|
||||
|
||||
# 任务:为 {{模块名称}} 生成模块 UI 设计文档
|
||||
# 任务:为 新增客源生成模块 UI 设计文档
|
||||
|
||||
## 你的角色
|
||||
|
||||
@@ -45,36 +31,30 @@
|
||||
## 本次任务输入
|
||||
|
||||
### 1. 目标模块
|
||||
**模块名称**:{{模块名称}}
|
||||
**模块描述**:{{一句话描述模块核心功能}}
|
||||
**模块名称**:新增客源
|
||||
**模块描述**:新增客源
|
||||
|
||||
### 2. PRD 功能文档路径
|
||||
```
|
||||
{{PRD文件路径,如:Project/fonrey/PRD/房源管理/房源管理模块PRD.md}}
|
||||
`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` 章节5.2 录入私客
|
||||
```
|
||||
请读取该文件,理解每个功能点的业务逻辑和验收标准。
|
||||
|
||||
### 3. DATA_MODEL 数据模型文档路径
|
||||
```
|
||||
{{DATA_MODEL文件路径,如:Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md}}
|
||||
```
|
||||
请读取该文件,理解该模块的数据模型以及字段命名。
|
||||
|
||||
### 4. 竞品参考截图
|
||||
### 3. 竞品参考截图
|
||||
请读取以下截图文件作为视觉参考(所有截图均在 `Project/fonrey/screenshots/` 目录下):
|
||||
|
||||
{{截图列表,格式如下,每行一张:
|
||||
- 功能名称:`Project/fonrey/screenshots/模块/截图名.png`
|
||||
}}
|
||||
- 客源列表:
|
||||
- 录入客源:`Project/fonrey/screenshots/客源/录入客源.png`
|
||||
- 编辑客源: `Project/fonrey/screenshots/客源/编辑客源.png`
|
||||
|
||||
### 5. MVP 优先级参考
|
||||
### 4. MVP 优先级参考
|
||||
请参考 `Project/fonrey/PRD/PRD_MVP.md`,在设计文档中标注每个页面/功能的优先级(P0/P1/P2)。
|
||||
|
||||
---
|
||||
|
||||
## 输出格式要求
|
||||
|
||||
输出一份完整的 Markdown 文档,文件名为 `{{模块名称}}_UI.md`,结构如下:
|
||||
输出一份完整的 Markdown 文档,文件名为 `新增客源_UI.md`,结构如下:
|
||||
|
||||
---
|
||||
|
||||
@@ -331,7 +311,7 @@
|
||||
3. 【本次模块UI设计文档】本次需要实现的模块设计说明
|
||||
- `Project/fonrey/UI_DESIGN/新增客源_UI.md`
|
||||
4. 【本次模块UI输出静态原型文件】
|
||||
- `Project/fonrey/UI_DESIGN/新增客源_UI.html`**
|
||||
- `Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
|
||||
### 强制约束(不可违反)
|
||||
|
||||
307
Project/fonrey/prompt/为 编辑客源生成模块 UI 设计文档.md
Normal file
307
Project/fonrey/prompt/为 编辑客源生成模块 UI 设计文档.md
Normal file
@@ -0,0 +1,307 @@
|
||||
|
||||
# 任务:为 编辑客源生成模块 UI 设计文档
|
||||
|
||||
## 你的角色
|
||||
|
||||
你是 Fonrey 房产经纪管理系统的 **UI/UX 架构师**,负责根据竞品截图和 PRD 功能描述,产出一份标准化的模块级 UI 设计文档。该文档将直接交给 AI Engineer 用于编码实现,必须包含足够的细节,Engineer 无需再问任何问题。
|
||||
|
||||
**注意**
|
||||
以下所有的文档或图片是基于文档库的相对路径。
|
||||
文档库的根路径为:`/mnt/d/Workspace/nexus`
|
||||
|
||||
---
|
||||
|
||||
## 全局设计约束(必须严格遵守)
|
||||
|
||||
> 所有设计决策必须符合 `Project/fonrey/UI_SYSTEM/UI_SYSTEM.md` 中的设计规范。核心约束如下:
|
||||
|
||||
- **技术栈**:Tailwind CSS + HTMX + Alpine.js + Django HTML 模板(非 React/Vue/JSX)
|
||||
- **图标库**:Heroicons v2(Outline 24px 默认,Solid 20px 强调,Mini 16px 极密场景)
|
||||
- **主色**:Teal `#0F766E`(`primary-600`),所有颜色引用 Token,禁止硬编码 Hex
|
||||
- **圆角**:`rounded-lg`(8px)为默认,表格行/小组件用 `rounded-md`(6px)
|
||||
- **表格行高**:56px(`h-14`)
|
||||
- **字体**:Inter + PingFang SC,正文 `text-sm`(14px)
|
||||
- **焦点环**:`focus-visible:ring-2 focus-visible:ring-primary-600/40`
|
||||
- **桌面优先**:≥1280px,不做移动端适配
|
||||
- **禁止独立 CSS 文件或 CSS-in-JS**:所有样式用 Tailwind utility class(少量例外如 Flatpickr 覆盖样式)
|
||||
- **组件实现参考**:`Project/fonrey/UI_SYSTEM/组件规范设计.md`(含 20 个特殊组件的完整 HTML + Alpine.js 实现)
|
||||
|
||||
---
|
||||
|
||||
## 本次任务输入
|
||||
|
||||
### 1. 目标模块
|
||||
**模块名称**:编辑客源
|
||||
**模块描述**:编辑客源
|
||||
|
||||
### 2. PRD 功能文档路径
|
||||
```
|
||||
`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` 章节5.15 编辑客源
|
||||
```
|
||||
请读取该文件,理解每个功能点的业务逻辑和验收标准。
|
||||
|
||||
### 3. DATA_MODEL 数据模型文档路径
|
||||
```
|
||||
DATA_MODEL文件路径,如:Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md
|
||||
```
|
||||
请读取该文件,理解该模块的数据模型以及字段命名。
|
||||
|
||||
### 4. 竞品参考截图
|
||||
请读取以下截图文件作为视觉参考(所有截图均在 `Project/fonrey/screenshots/` 目录下):
|
||||
|
||||
- 客源列表:
|
||||
- 编辑客源: `Project/fonrey/screenshots/客源/编辑客源.png`
|
||||
|
||||
### 5. MVP 优先级参考
|
||||
请参考 `Project/fonrey/PRD/PRD_MVP.md`,在设计文档中标注每个页面/功能的优先级(P0/P1/P2)。
|
||||
|
||||
---
|
||||
|
||||
## 输出格式要求
|
||||
|
||||
输出一份完整的 Markdown 文档,文件名为 `编辑客源_UI.md`,结构如下:
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# {{模块名称}} UI 设计文档
|
||||
|
||||
> **版本**:v1.0 · **日期**:{今日日期}
|
||||
> **依赖规范**:UI_SYSTEM.md v1.1 · 组件规范设计.md v1.0
|
||||
> **PRD 来源**:{PRD文件路径}
|
||||
> **优先级**:P0 功能在本文档中用 🔴 标注,P1 用 🟡,P2 用 ⚫
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
(列出本文档所有章节)
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
### 1.1 功能范围
|
||||
(从 PRD 提取本模块包含的所有功能,按优先级分组列表)
|
||||
|
||||
### 1.2 页面清单
|
||||
(列出本模块所有页面/视图,每行包含:页面名称 | URL 模式建议 | 优先级 | 对应 PRD 章节)
|
||||
|
||||
### 1.3 用户角色与权限差异
|
||||
(说明不同角色(经纪人/店长/管理员)在本模块的视图差异,如哪些字段/按钮对特定角色隐藏)
|
||||
|
||||
---
|
||||
|
||||
## 2. 页面设计规范
|
||||
|
||||
> 每个页面单独一节,按以下子结构输出。
|
||||
|
||||
### 2.N {页面名称}({优先级} 🔴/🟡/⚫)
|
||||
|
||||
#### 2.N.1 页面概述
|
||||
- **URL**:`/模块/页面/`
|
||||
- **访问入口**:(从哪里进入此页面)
|
||||
- **页面职责**:(一句话)
|
||||
- **竞品参考截图**:`{截图路径}`
|
||||
|
||||
#### 2.N.2 布局结构
|
||||
(用文字描述页面整体布局,如:三栏布局、左侧边栏+右侧主内容区、全宽列表等)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ 顶部区域(Breadcrumb / 页面标题 / 主操作按钮) │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ 筛选区域(可折叠) │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ 工具栏(批量操作 / 排序切换 / 列设置 / 导出) │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ 主内容区(表格 / 详情卡片 / 表单) │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ 分页栏 │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
(根据实际页面调整此 ASCII 图)
|
||||
|
||||
#### 2.N.3 区域详细规范
|
||||
|
||||
> 每个区域独立描述,包含:组件类型、字段/按钮清单、交互逻辑
|
||||
|
||||
**[区域名称,如:搜索筛选区]**
|
||||
|
||||
| 属性 | 说明 |
|
||||
|---|---|
|
||||
| 组件 | (引用组件规范设计.md 中的组件名,如:Date Range Picker) |
|
||||
| 展开/收起 | (是否支持折叠,默认状态) |
|
||||
| 筛选字段 | (列出所有筛选字段及输入类型) |
|
||||
| 联动逻辑 | (字段间的联动关系) |
|
||||
| HTMX 行为 | (如:`hx-get="/api/xxx/" hx-trigger="change" hx-target="#table-body"`) |
|
||||
|
||||
**[区域名称,如:数据表格]**
|
||||
|
||||
| 列名 | 数据类型 | 宽度 | 排序 | 特殊渲染 |
|
||||
|---|---|---|---|---|
|
||||
| (列名) | (string/number/date/badge/...) | (fixed px 或 auto) | (是/否) | (如:Tag、趋势箭头、行内按钮) |
|
||||
|
||||
(补充表格交互说明:行点击跳转、批量选择、列固定等)
|
||||
|
||||
#### 2.N.4 使用的特殊组件
|
||||
|
||||
| 组件名 | 来源(组件规范设计.md 章节) | 用途 | 自定义说明 |
|
||||
|---|---|---|---|
|
||||
| (组件名) | (如:§1 Data Table) | (用于展示xxx列表) | (如果有与标准实现不同的地方,详细说明) |
|
||||
|
||||
#### 2.N.5 空状态设计
|
||||
(描述列表/表格无数据时的展示方式,参考 UI_SYSTEM.md §6.3 空状态设计)
|
||||
|
||||
#### 2.N.6 Loading 状态
|
||||
(描述数据加载中的骨架屏或加载指示方案)
|
||||
|
||||
---
|
||||
|
||||
## 3. 弹窗/抽屉设计规范
|
||||
|
||||
> 每个弹窗/抽屉独立一节,按以下结构输出。
|
||||
|
||||
### 3.N {弹窗/抽屉名称}({触发入口})
|
||||
|
||||
#### 3.N.1 触发方式
|
||||
- **触发位置**:(如:房源详情页-调价链接)
|
||||
- **组件类型**:Modal Dialog / Drawer(选一个,说明选择理由)
|
||||
- **尺寸**:(Modal: max-w-sm/md/lg/xl/2xl;Drawer: w-[480px]/w-[640px])
|
||||
- **竞品截图**:`{截图路径}`
|
||||
|
||||
#### 3.N.2 表单字段规范
|
||||
|
||||
| 字段名 | 组件类型 | 必填 | 校验规则 | 默认值/预填值 |
|
||||
|---|---|---|---|---|
|
||||
| (字段名) | (Input/Select/Textarea/DatePicker/Toggle/TreeSelect/MultiTag/...) | (是/否) | (规则描述) | (如有) |
|
||||
|
||||
#### 3.N.3 提交行为
|
||||
- **提交方式**:HTMX `hx-post` / `hx-put` / `hx-patch`
|
||||
- **成功响应**:(如:关闭弹窗 + Toast "保存成功" + 刷新目标区域)
|
||||
- **失败响应(422)**:(字段级错误提示)
|
||||
- **HTMX 属性**:(完整写出 hx-post/hx-target/hx-swap/hx-on 等)
|
||||
|
||||
#### 3.N.4 使用的特殊组件
|
||||
|
||||
| 组件名 | 来源 | 用途 |
|
||||
|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 4. 交互状态规范
|
||||
|
||||
### 4.1 全局状态机(如有)
|
||||
(如房源状态机:在售 → 暂缓 → 成交 → 下架,用状态流转图描述)
|
||||
|
||||
### 4.2 权限控制矩阵
|
||||
(描述不同角色对本模块各操作的权限,如:删除只有管理员可见)
|
||||
|
||||
| 操作 | 经纪人 | 店长 | 管理员 |
|
||||
|---|---|---|---|
|
||||
|
||||
### 4.3 HTMX 请求规范
|
||||
(列出本模块所有 HTMX 请求,包含:触发事件、URL、target、swap 方式、Loading 行为)
|
||||
|
||||
| 操作 | hx-trigger | hx-get/post/... | hx-target | hx-swap | Loading |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键数据字段说明
|
||||
|
||||
(列出本模块所有需要后端支持的数据字段,便于 Engineer 与后端联调)
|
||||
|
||||
| 字段名(英文) | 显示名 | 数据类型 | 说明 |
|
||||
|---|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 6. 竞品截图对应关系
|
||||
|
||||
(将本模块所有参考截图按功能分类整理,说明截图对应设计文档的哪个章节)
|
||||
|
||||
| 截图路径 | 对应功能 | 对应文档章节 | 采纳的设计要点 |
|
||||
|---|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 7. 实现优先级与工期估算
|
||||
|
||||
| 页面/功能 | 优先级 | 特殊组件复杂度 | 工期估算(前端) |
|
||||
|---|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 8. 开放问题(待决策)
|
||||
|
||||
(列出设计过程中发现的、需要产品/后端确认的问题)
|
||||
|
||||
| # | 问题 | 影响范围 | 待确认方 |
|
||||
|---|---|---|---|
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 额外要求
|
||||
|
||||
1. **截图优先**:有截图的功能,以截图呈现的 UI 为主要参考,PRD 文字为补充说明;截图和 PRD 有冲突时,以截图为准,并在文档中注明差异。
|
||||
|
||||
2. **组件引用**:每次使用特殊组件(如 Data Table、Tree Select、Drawer 等),必须在"使用的特殊组件"表格中引用组件规范设计.md 的对应章节编号,并说明如有差异的自定义部分。
|
||||
|
||||
3. **HTMX 落地**:每个需要异步更新的交互(筛选、分页、弹窗提交)必须写出完整的 HTMX 属性,Engineer 可以直接复制使用。
|
||||
|
||||
4. **Alpine.js 分工**:说明哪些状态由 Alpine.js 管理(弹窗开关、选中状态、表单联动),哪些交互走 HTMX(数据加载、表单提交)。
|
||||
|
||||
5. **禁止设计移动端**:所有布局仅针对 ≥1280px 桌面端。
|
||||
|
||||
6. **优先级标注**:P0 功能用 🔴,P1 用 🟡,P2 用 ⚫,确保 Engineer 知道实现顺序。
|
||||
|
||||
7. **不要遗漏边界状态**:每个列表页必须包含空状态设计;每个表单必须包含校验失败状态;每个异步操作必须包含 Loading 状态。
|
||||
|
||||
---PROMPT END---
|
||||
|
||||
---
|
||||
|
||||
## 已生成的模块 UI 设计文档
|
||||
|
||||
| 模块 | 文件路径 | 生成日期 | 覆盖 PRD 版本 |
|
||||
|---|---|---|---|
|
||||
| (待填入) | | | |
|
||||
|
||||
---
|
||||
|
||||
## 变量填写示例(房源列表页)
|
||||
|
||||
```
|
||||
{{模块名称}} → 房源管理
|
||||
{{一句话描述模块核心功能}} → 管理房产经纪公司的二手房/租赁房源,支持录入、筛选、跟进、状态变更
|
||||
{{PRD文件路径}} → Project/fonrey/PRD/房源管理/房源管理模块PRD.md
|
||||
{{截图列表}} →
|
||||
- 房源列表(二手&租赁):`Project/fonrey/screenshots/房源/全部房源.png`
|
||||
- 房源列表(全部商铺):`Project/fonrey/screenshots/房源/全部商铺.png`
|
||||
- 房源详情(第1屏):`Project/fonrey/screenshots/房源/房源详情1.png`
|
||||
- 房源详情(第2屏):`Project/fonrey/screenshots/房源/房源详情2.png`
|
||||
- 房源详情(第3屏):`Project/fonrey/screenshots/房源/房源详情3.png`
|
||||
- 新增住宅表单:`Project/fonrey/screenshots/房源/增房/新增住宅.png`
|
||||
- 调价弹窗:`Project/fonrey/screenshots/房源/调价.png`
|
||||
- 调价记录弹窗:`Project/fonrey/screenshots/房源/调价记录.png`
|
||||
- 房源状态变更:`Project/fonrey/screenshots/房源/房源状态变更.png`
|
||||
- 跟进管理-全部:`Project/fonrey/screenshots/房源/跟进管理/全部.png`
|
||||
- 跟进管理-写入跟进:`Project/fonrey/screenshots/房源/跟进管理/写入跟进.png`
|
||||
- 相册管理:`Project/fonrey/screenshots/房源/增房/上传图片.png`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 单次提示词只针对**一个模块**,不要同时处理多个模块
|
||||
- 对于同一模块内页面较多的情况(如房源管理有列表、详情、新增、跟进等多个页面),**全部包含在同一份文档中**,通过 `§2.N` 分节区分
|
||||
- 弹窗数量较多时(如房源详情有 10+ 个编辑弹窗),可以将**结构相似的弹窗合并为一个通用弹窗规范**,仅列出字段差异表
|
||||
- 生成完成后,将文档路径更新到上方「已生成的模块 UI 设计文档」表格中
|
||||
|
||||
|
||||
310
Project/fonrey/prompt/提示词模板/模块UI设计文档生成提示词_v1.md
Normal file
310
Project/fonrey/prompt/提示词模板/模块UI设计文档生成提示词_v1.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# Fonrey 模块 UI 设计文档生成提示词
|
||||
|
||||
> **用途**:每次针对一个具体业务模块,填入变量后直接发给 AI,输出该模块的标准化 UI 设计文档。
|
||||
> **输出文件**:`Project/fonrey/UI_DESIGN/{模块名}_UI.md`
|
||||
|
||||
---
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 复制下方「---PROMPT START---」到「---PROMPT END---」之间的全部内容
|
||||
2. 将所有 `{{变量}}` 替换为本次模块的实际值
|
||||
3. 把替换后的提示词发给 AI
|
||||
|
||||
---PROMPT START---
|
||||
|
||||
# 任务:为 {{模块名称}} 生成模块 UI 设计文档
|
||||
|
||||
## 你的角色
|
||||
|
||||
你是 Fonrey 房产经纪管理系统的 **UI/UX 架构师**,负责根据竞品截图和 PRD 功能描述,产出一份标准化的模块级 UI 设计文档。该文档将直接交给 AI Engineer 用于编码实现,必须包含足够的细节,Engineer 无需再问任何问题。
|
||||
|
||||
**注意**
|
||||
以下所有的文档或图片是基于文档库的相对路径。
|
||||
文档库的根路径为:`/mnt/d/Workspace/nexus`
|
||||
|
||||
---
|
||||
|
||||
## 全局设计约束(必须严格遵守)
|
||||
|
||||
> 所有设计决策必须符合 `Project/fonrey/UI_SYSTEM/UI_SYSTEM.md` 中的设计规范。核心约束如下:
|
||||
|
||||
- **技术栈**:Tailwind CSS + HTMX + Alpine.js + Django HTML 模板(非 React/Vue/JSX)
|
||||
- **图标库**:Heroicons v2(Outline 24px 默认,Solid 20px 强调,Mini 16px 极密场景)
|
||||
- **主色**:Teal `#0F766E`(`primary-600`),所有颜色引用 Token,禁止硬编码 Hex
|
||||
- **圆角**:`rounded-lg`(8px)为默认,表格行/小组件用 `rounded-md`(6px)
|
||||
- **表格行高**:56px(`h-14`)
|
||||
- **字体**:Inter + PingFang SC,正文 `text-sm`(14px)
|
||||
- **焦点环**:`focus-visible:ring-2 focus-visible:ring-primary-600/40`
|
||||
- **桌面优先**:≥1280px,不做移动端适配
|
||||
- **禁止独立 CSS 文件或 CSS-in-JS**:所有样式用 Tailwind utility class(少量例外如 Flatpickr 覆盖样式)
|
||||
- **组件实现参考**:`Project/fonrey/UI_SYSTEM/组件规范设计.md`(含 20 个特殊组件的完整 HTML + Alpine.js 实现)
|
||||
|
||||
---
|
||||
|
||||
## 本次任务输入
|
||||
|
||||
### 1. 目标模块
|
||||
**模块名称**:{{模块名称}}
|
||||
**模块描述**:{{一句话描述模块核心功能}}
|
||||
|
||||
### 2. PRD 功能文档路径
|
||||
```
|
||||
{{PRD文件路径,如:Project/fonrey/PRD/房源管理/房源管理模块PRD.md}}
|
||||
```
|
||||
请读取该文件,理解每个功能点的业务逻辑和验收标准。
|
||||
|
||||
### 3. DATA_MODEL 数据模型文档路径
|
||||
```
|
||||
{{DATA_MODEL文件路径,如:Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md}}
|
||||
```
|
||||
请读取该文件,理解该模块的数据模型以及字段命名。
|
||||
|
||||
### 4. 竞品参考截图
|
||||
请读取以下截图文件作为视觉参考(所有截图均在 `Project/fonrey/screenshots/` 目录下):
|
||||
|
||||
{{截图列表,格式如下,每行一张:
|
||||
- 功能名称:`Project/fonrey/screenshots/模块/截图名.png`
|
||||
}}
|
||||
|
||||
### 5. MVP 优先级参考
|
||||
请参考 `Project/fonrey/PRD/PRD_MVP.md`,在设计文档中标注每个页面/功能的优先级(P0/P1/P2)。
|
||||
|
||||
---
|
||||
|
||||
## 输出格式要求
|
||||
|
||||
输出一份完整的 Markdown 文档,`Project/fonrey/UI_DESIGN/{模块名}_UI.md`,结构如下:
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# {{模块名称}} UI 设计文档
|
||||
|
||||
> **版本**:v1.0 · **日期**:{今日日期}
|
||||
> **依赖规范**:UI_SYSTEM.md v1.1 · 组件规范设计.md v1.0
|
||||
> **PRD 来源**:{PRD文件路径}
|
||||
> **优先级**:P0 功能在本文档中用 🔴 标注,P1 用 🟡,P2 用 ⚫
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
(列出本文档所有章节)
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
### 1.1 功能范围
|
||||
(从 PRD 提取本模块包含的所有功能,按优先级分组列表)
|
||||
|
||||
### 1.2 页面清单
|
||||
(列出本模块所有页面/视图,每行包含:页面名称 | URL 模式建议 | 优先级 | 对应 PRD 章节)
|
||||
|
||||
### 1.3 用户角色与权限差异
|
||||
(说明不同角色(经纪人/店长/管理员)在本模块的视图差异,如哪些字段/按钮对特定角色隐藏)
|
||||
|
||||
---
|
||||
|
||||
## 2. 页面设计规范
|
||||
|
||||
> 每个页面单独一节,按以下子结构输出。
|
||||
|
||||
### 2.N {页面名称}({优先级} 🔴/🟡/⚫)
|
||||
|
||||
#### 2.N.1 页面概述
|
||||
- **URL**:`/模块/页面/`
|
||||
- **访问入口**:(从哪里进入此页面)
|
||||
- **页面职责**:(一句话)
|
||||
- **竞品参考截图**:`{截图路径}`
|
||||
|
||||
#### 2.N.2 布局结构
|
||||
(用文字描述页面整体布局,如:三栏布局、左侧边栏+右侧主内容区、全宽列表等)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ 顶部区域(Breadcrumb / 页面标题 / 主操作按钮) │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ 筛选区域(可折叠) │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ 工具栏(批量操作 / 排序切换 / 列设置 / 导出) │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ 主内容区(表格 / 详情卡片 / 表单) │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ 分页栏 │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
(根据实际页面调整此 ASCII 图)
|
||||
|
||||
#### 2.N.3 区域详细规范
|
||||
|
||||
> 每个区域独立描述,包含:组件类型、字段/按钮清单、交互逻辑
|
||||
|
||||
**[区域名称,如:搜索筛选区]**
|
||||
|
||||
| 属性 | 说明 |
|
||||
|---|---|
|
||||
| 组件 | (引用组件规范设计.md 中的组件名,如:Date Range Picker) |
|
||||
| 展开/收起 | (是否支持折叠,默认状态) |
|
||||
| 筛选字段 | (列出所有筛选字段及输入类型) |
|
||||
| 联动逻辑 | (字段间的联动关系) |
|
||||
| HTMX 行为 | (如:`hx-get="/api/xxx/" hx-trigger="change" hx-target="#table-body"`) |
|
||||
|
||||
**[区域名称,如:数据表格]**
|
||||
|
||||
| 列名 | 数据类型 | 宽度 | 排序 | 特殊渲染 |
|
||||
|---|---|---|---|---|
|
||||
| (列名) | (string/number/date/badge/...) | (fixed px 或 auto) | (是/否) | (如:Tag、趋势箭头、行内按钮) |
|
||||
|
||||
(补充表格交互说明:行点击跳转、批量选择、列固定等)
|
||||
|
||||
#### 2.N.4 使用的特殊组件
|
||||
|
||||
| 组件名 | 来源(组件规范设计.md 章节) | 用途 | 自定义说明 |
|
||||
|---|---|---|---|
|
||||
| (组件名) | (如:§1 Data Table) | (用于展示xxx列表) | (如果有与标准实现不同的地方,详细说明) |
|
||||
|
||||
#### 2.N.5 空状态设计
|
||||
(描述列表/表格无数据时的展示方式,参考 UI_SYSTEM.md §6.3 空状态设计)
|
||||
|
||||
#### 2.N.6 Loading 状态
|
||||
(描述数据加载中的骨架屏或加载指示方案)
|
||||
|
||||
---
|
||||
|
||||
## 3. 弹窗/抽屉设计规范
|
||||
|
||||
> 每个弹窗/抽屉独立一节,按以下结构输出。
|
||||
|
||||
### 3.N {弹窗/抽屉名称}({触发入口})
|
||||
|
||||
#### 3.N.1 触发方式
|
||||
- **触发位置**:(如:房源详情页-调价链接)
|
||||
- **组件类型**:Modal Dialog / Drawer(选一个,说明选择理由)
|
||||
- **尺寸**:(Modal: max-w-sm/md/lg/xl/2xl;Drawer: w-[480px]/w-[640px])
|
||||
- **竞品截图**:`{截图路径}`
|
||||
|
||||
#### 3.N.2 表单字段规范
|
||||
|
||||
| 字段名 | 组件类型 | 必填 | 校验规则 | 默认值/预填值 |
|
||||
|---|---|---|---|---|
|
||||
| (字段名) | (Input/Select/Textarea/DatePicker/Toggle/TreeSelect/MultiTag/...) | (是/否) | (规则描述) | (如有) |
|
||||
|
||||
#### 3.N.3 提交行为
|
||||
- **提交方式**:HTMX `hx-post` / `hx-put` / `hx-patch`
|
||||
- **成功响应**:(如:关闭弹窗 + Toast "保存成功" + 刷新目标区域)
|
||||
- **失败响应(422)**:(字段级错误提示)
|
||||
- **HTMX 属性**:(完整写出 hx-post/hx-target/hx-swap/hx-on 等)
|
||||
|
||||
#### 3.N.4 使用的特殊组件
|
||||
|
||||
| 组件名 | 来源 | 用途 |
|
||||
|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 4. 交互状态规范
|
||||
|
||||
### 4.1 全局状态机(如有)
|
||||
(如房源状态机:在售 → 暂缓 → 成交 → 下架,用状态流转图描述)
|
||||
|
||||
### 4.2 权限控制矩阵
|
||||
(描述不同角色对本模块各操作的权限,如:删除只有管理员可见)
|
||||
|
||||
| 操作 | 经纪人 | 店长 | 管理员 |
|
||||
|---|---|---|---|
|
||||
|
||||
### 4.3 HTMX 请求规范
|
||||
(列出本模块所有 HTMX 请求,包含:触发事件、URL、target、swap 方式、Loading 行为)
|
||||
|
||||
| 操作 | hx-trigger | hx-get/post/... | hx-target | hx-swap | Loading |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键数据字段说明
|
||||
|
||||
(列出本模块所有需要后端支持的数据字段,便于 Engineer 与后端联调)
|
||||
|
||||
| 字段名(英文) | 显示名 | 数据类型 | 说明 |
|
||||
|---|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 6. 竞品截图对应关系
|
||||
|
||||
(将本模块所有参考截图按功能分类整理,说明截图对应设计文档的哪个章节)
|
||||
|
||||
| 截图路径 | 对应功能 | 对应文档章节 | 采纳的设计要点 |
|
||||
|---|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 7. 实现优先级与工期估算
|
||||
|
||||
| 页面/功能 | 优先级 | 特殊组件复杂度 | 工期估算(前端) |
|
||||
|---|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 8. 开放问题(待决策)
|
||||
|
||||
(列出设计过程中发现的、需要产品/后端确认的问题)
|
||||
|
||||
| # | 问题 | 影响范围 | 待确认方 |
|
||||
|---|---|---|---|
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 额外要求
|
||||
|
||||
1. **PRD优先**:有截图的功能,以截图呈现的 UI 为补充说明,以PRD 文字为主要参考;截图和 PRD 有冲突时,以PRD为准,并在文档中注明差异。
|
||||
2. **组件引用**:每次使用特殊组件(如 Data Table、Tree Select、Drawer 等),必须在"使用的特殊组件"表格中引用组件规范设计.md 的对应章节编号,并说明如有差异的自定义部分。
|
||||
3. **HTMX 落地**:每个需要异步更新的交互(筛选、分页、弹窗提交)必须写出完整的 HTMX 属性,Engineer 可以直接复制使用。
|
||||
4. **Alpine.js 分工**:说明哪些状态由 Alpine.js 管理(弹窗开关、选中状态、表单联动),哪些交互走 HTMX(数据加载、表单提交)。
|
||||
5. **禁止设计移动端**:所有布局仅针对 ≥1280px 桌面端。
|
||||
6. **优先级标注**:P0 功能用 🔴,P1 用 🟡,P2 用 ⚫,确保 Engineer 知道实现顺序。
|
||||
7. **不要遗漏边界状态**:每个列表页必须包含空状态设计;每个表单必须包含校验失败状态;每个异步操作必须包含 Loading 状态。
|
||||
|
||||
---PROMPT END---
|
||||
|
||||
---
|
||||
## 已生成的模块 UI 设计文档
|
||||
|
||||
| 模块 | 文件路径 | 生成日期 | 覆盖 PRD 版本 |
|
||||
|---|---|---|---|
|
||||
| (待填入) | | | |
|
||||
|
||||
---
|
||||
|
||||
## 变量填写示例(房源列表页)
|
||||
|
||||
```
|
||||
{{模块名称}} → 房源管理
|
||||
{{一句话描述模块核心功能}} → 管理房产经纪公司的二手房/租赁房源,支持录入、筛选、跟进、状态变更
|
||||
{{PRD文件路径}} → Project/fonrey/PRD/房源管理/房源管理模块PRD.md
|
||||
{{截图列表}} →
|
||||
- 房源列表(二手&租赁):`Project/fonrey/screenshots/房源/全部房源.png`
|
||||
- 房源列表(全部商铺):`Project/fonrey/screenshots/房源/全部商铺.png`
|
||||
- 房源详情(第1屏):`Project/fonrey/screenshots/房源/房源详情1.png`
|
||||
- 房源详情(第2屏):`Project/fonrey/screenshots/房源/房源详情2.png`
|
||||
- 房源详情(第3屏):`Project/fonrey/screenshots/房源/房源详情3.png`
|
||||
- 新增住宅表单:`Project/fonrey/screenshots/房源/增房/新增住宅.png`
|
||||
- 调价弹窗:`Project/fonrey/screenshots/房源/调价.png`
|
||||
- 调价记录弹窗:`Project/fonrey/screenshots/房源/调价记录.png`
|
||||
- 房源状态变更:`Project/fonrey/screenshots/房源/房源状态变更.png`
|
||||
- 跟进管理-全部:`Project/fonrey/screenshots/房源/跟进管理/全部.png`
|
||||
- 跟进管理-写入跟进:`Project/fonrey/screenshots/房源/跟进管理/写入跟进.png`
|
||||
- 相册管理:`Project/fonrey/screenshots/房源/增房/上传图片.png`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 单次提示词只针对**一个模块**,不要同时处理多个模块
|
||||
- 对于同一模块内页面较多的情况(如房源管理有列表、详情、新增、跟进等多个页面),**全部包含在同一份文档中**,通过 `§2.N` 分节区分
|
||||
- 弹窗数量较多时(如房源详情有 10+ 个编辑弹窗),可以将**结构相似的弹窗合并为一个通用弹窗规范**,仅列出字段差异表
|
||||
- 生成完成后,将文档路径更新到上方「已生成的模块 UI 设计文档」表格中
|
||||
72
Project/fonrey/prompt/提示词模板/模块UI静态原型页面生成提示词_v1.md
Normal file
72
Project/fonrey/prompt/提示词模板/模块UI静态原型页面生成提示词_v1.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 任务:为 {{模块}}生成模块 UI 静态原型
|
||||
|
||||
## 你的角色
|
||||
|
||||
你是 Fonrey 房产经纪管理系统的 **UI/UX 架构师**,负责根据竞品截图和 PRD 功能描述,产出一份标准化的模块级 UI 设计文档。该文档将直接交给 AI Engineer 用于编码实现,必须包含足够的细节,Engineer 无需再问任何问题。
|
||||
|
||||
**注意**
|
||||
以下所有的文档或图片是基于文档库的相对路径。
|
||||
文档库的根路径为:`/mnt/d/Workspace/nexus`
|
||||
|
||||
---
|
||||
|
||||
## 全局设计约束(必须严格遵守)
|
||||
|
||||
> 所有设计决策必须符合 `Project/fonrey/UI_SYSTEM/UI_SYSTEM.md` 中的设计规范。核心约束如下:
|
||||
|
||||
- **技术栈**:Tailwind CSS + HTMX + Alpine.js + Django HTML 模板(非 React/Vue/JSX)
|
||||
- **图标库**:Heroicons v2(Outline 24px 默认,Solid 20px 强调,Mini 16px 极密场景)
|
||||
- **主色**:Teal `#0F766E`(`primary-600`),所有颜色引用 Token,禁止硬编码 Hex
|
||||
- **圆角**:`rounded-lg`(8px)为默认,表格行/小组件用 `rounded-md`(6px)
|
||||
- **表格行高**:56px(`h-14`)
|
||||
- **字体**:Inter + PingFang SC,正文 `text-sm`(14px)
|
||||
- **焦点环**:`focus-visible:ring-2 focus-visible:ring-primary-600/40`
|
||||
- **桌面优先**:≥1280px,不做移动端适配
|
||||
- **禁止独立 CSS 文件或 CSS-in-JS**:所有样式用 Tailwind utility class(少量例外如 Flatpickr 覆盖样式)
|
||||
- **组件实现参考**:`Project/fonrey/UI_SYSTEM/组件规范设计.md`(含 20 个特殊组件的完整 HTML + Alpine.js 实现)
|
||||
|
||||
|
||||
**输入文件**
|
||||
1. 【UI_SYSTEM】全局UI设计规范文档 `Project/fonrey/UI_SYSTEM/UI_SYSTEM.md`
|
||||
2. 【现有原型页面】已完成的HTML页面(作为视觉和代码参考基准)
|
||||
- `Project/fonrey/UI_SYSTEM/preview.html`
|
||||
- `Project/fonrey/UI_DESIGN/客源列表_UI.html`
|
||||
- `Project/fonrey/UI_DESIGN/客源详情_UI.html`
|
||||
- `Project/fonrey/UI_DESIGN/新增客源_UI.html`
|
||||
3. 【本次模块UI设计文档】本次需要实现的模块设计说明
|
||||
- `Project/fonrey/UI_DESIGN/编辑客源_UI.md`
|
||||
**输出文件**
|
||||
- 【本次模块UI输出静态原型文件】
|
||||
- `Project/fonrey/UI_DESIGN/编辑客源_UI.html`
|
||||
|
||||
### 强制约束(不可违反)
|
||||
|
||||
#### 一致性约束
|
||||
- 颜色、字体、字号、圆角、阴影、间距等视觉变量,必须与 UI_SYSTEM 保持完全一致,不得自行创造新的变量
|
||||
- 公共组件(导航栏、侧边栏、顶部栏、按钮、表单、卡片、标签等)的样式和结构,必须与现有原型页面中的实现保持一致
|
||||
- 如果现有页面使用了 CSS 变量或特定 class 命名规范,本次输出必须沿用相同的规范
|
||||
|
||||
#### 布局约束
|
||||
- 整体页面框架(如侧边栏宽度、顶栏高度、内容区边距)必须与现有原型页面保持一致
|
||||
- 响应式断点策略(如有)需与已有页面对齐
|
||||
#### 代码约束
|
||||
- 输出单一 HTML 文件,CSS 写在 `<style>` 标签内,JS 写在 `<script>` 标签内
|
||||
- 不引入任何外部依赖,除非现有原型页面已经使用了该依赖
|
||||
- 类名、变量名的命名风格与现有代码保持一致
|
||||
|
||||
#### 执行步骤(按顺序执行)
|
||||
1. 通读所有输入材料,识别 UI_SYSTEM 中的核心设计 token
|
||||
2. 分析现有原型页面,提取公共组件的 HTML 结构和 CSS 实现
|
||||
3. 阅读本次模块设计文档,理解页面结构、交互状态和内容层级
|
||||
4. 以现有页面为外壳,将本次模块内容填入正确的内容区域
|
||||
5. 对照设计文档逐项检查还原度,确认无遗漏后输出
|
||||
|
||||
#### 输出要求
|
||||
- 直接输出完整可运行的 HTML 文件内容
|
||||
- 页面中需要数据的地方使用合理的占位内容(不要留空)
|
||||
- 交互状态(hover、active、selected、disabled)需在 CSS 中体现
|
||||
- 输出完成后,列出你在本次实现中做出的所有设计假设或补充决策
|
||||
|
||||
#### 注意事项
|
||||
- 如果设计文档与 UI_SYSTEM 存在冲突,以 UI_SYSTEM 为准,并告知我冲突点
|
||||
- 如果设计文档描述不清晰,不要自行猜测,先列出疑问再继续
|
||||
42
Project/fonrey/prompt/文档模板/TASK.md
Normal file
42
Project/fonrey/prompt/文档模板/TASK.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Project Task Board
|
||||
|
||||
## 项目状态总览
|
||||
- 当前阶段:MVP Phase 1
|
||||
- 技术栈:[你的 tech stack 简述]
|
||||
- 最后更新:YYYY-MM-DD
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 - MVP(P0,必须完成)
|
||||
|
||||
### [模块名,如:用户认证]
|
||||
- [ ] US-001 用户可以用邮箱注册账号
|
||||
- 涉及文件:`/auth/register.tsx`, `userModel`
|
||||
- 验收标准:表单校验通过 → 写入 DB → 跳转登录页
|
||||
- [ ] US-002 用户可以登录并保持会话
|
||||
- 涉及文件:`/auth/login.tsx`, `sessionModel`
|
||||
- 验收标准:JWT 生成 → localStorage 存储 → 保护路由生效
|
||||
|
||||
### [模块名,如:仪表盘]
|
||||
- [ ] US-005 用户可以看到核心数据概览
|
||||
- 涉及文件:`/dashboard/index.tsx`
|
||||
- 验收标准:数据从 API 读取,与 UI 原型视觉一致
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 - 增强功能(P1,MVP 后实现)
|
||||
|
||||
- [ ] US-010 用户可以导出数据为 CSV
|
||||
- [ ] US-011 支持第三方登录(Google OAuth)
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 - 锦上添花(P2,有时间再做)
|
||||
|
||||
- [ ] US-020 动画和过渡效果优化
|
||||
- [ ] US-021 深色模式支持
|
||||
|
||||
---
|
||||
|
||||
## 已完成
|
||||
- [x] US-XXX xxxxxxx(完成日期)
|
||||
Reference in New Issue
Block a user