文档修改

This commit is contained in:
Shen Wei
2026-04-26 19:50:01 +08:00
parent d2ae5b3948
commit d42bc16120
28 changed files with 6697 additions and 2545 deletions

834
Project/fonrey/PRD/TASK.md Normal file
View File

@@ -0,0 +1,834 @@
## Project Task Board
### 项目状态总览
- 产品名称Fonrey 房睿
- 当前阶段MVP Phase 1
- 技术栈Django 4.x + HTMX + Alpine.js + Tailwind CSS + PostgreSQL 16 + Redis + Celery + Cloudflare R2
- 最后更新2026-04-26
---
### Task 索引总览
> 点击 US 编号可直接跳转到对应 Task 详情。
#### Phase 1 — MVPP0
| US 编号 | 模块 | Task 描述 | 状态 |
|---|---|---|---|
| [US-ACCOUNT-001](#US-ACCOUNT-001-经纪人管理员使用账号密码登录系统) | 用户登录 | 经纪人/管理员使用账号密码登录系统 | [ ] |
| [US-ACCOUNT-002](#US-ACCOUNT-002-系统识别多租户子域名域名路由) | 用户登录 | 系统识别多租户(子域名/域名路由) | [ ] |
| [US-ACCOUNT-003](#US-ACCOUNT-003-系统管理-Token-与会话超时) | 用户登录 | 系统管理 Token 与会话超时 | [ ] |
| [US-COMPLEX-001](#US-COMPLEX-001-管理员录入与维护楼盘基础信息) | 楼盘管理 | 管理员录入与维护楼盘基础信息 | [ ] |
| [US-COMPLEX-002](#US-COMPLEX-002-经纪人查看楼盘列表与详情) | 楼盘管理 | 经纪人查看楼盘列表与详情 | [ ] |
| [US-COMPLEX-003](#US-COMPLEX-003-管理员维护区域管理城区商圈) | 楼盘管理 | 管理员维护区域管理(城区/商圈) | [ ] |
| [US-PROPERTY-001](#US-PROPERTY-001-经纪人录入二手住宅出售出租) | 房源管理 | 经纪人录入二手住宅(出售/出租) | [ ] |
| [US-PROPERTY-002](#US-PROPERTY-002-经纪人查看与筛选房源列表) | 房源管理 | 经纪人查看与筛选房源列表 | [ ] |
| [US-PROPERTY-003](#US-PROPERTY-003-经纪人查看房源详情页) | 房源管理 | 经纪人查看房源详情页 | [ ] |
| [US-PROPERTY-004](#US-PROPERTY-004-经纪人写入与查看房源跟进记录) | 房源管理 | 经纪人写入与查看房源跟进记录 | [ ] |
| [US-PROPERTY-005](#US-PROPERTY-005-经纪人管理房源图片上传分类排序) | 房源管理 | 经纪人管理房源图片(上传/分类/排序) | [ ] |
| [US-PROPERTY-006](#US-PROPERTY-006-经纪人管理业主联系人) | 房源管理 | 经纪人管理业主联系人 | [ ] |
| [US-PROPERTY-007](#US-PROPERTY-007-经纪人调整房源价格) | 房源管理 | 经纪人调整房源价格 | [ ] |
| [US-PROPERTY-008](#US-PROPERTY-008-经纪人变更房源状态) | 房源管理 | 经纪人变更房源状态 | [ ] |
| [US-CLIENT-001](#US-CLIENT-001-经纪人录入新私客) | 客源管理 | 经纪人录入新私客 | [ ] |
| [US-CLIENT-002](#US-CLIENT-002-经纪人查看与筛选私客列表全部求购求租) | 客源管理 | 经纪人查看与筛选私客列表(全部/求购/求租) | [ ] |
| [US-CLIENT-003](#US-CLIENT-003-经纪人批量操作私客列表) | 客源管理 | 经纪人批量操作私客列表 | [ ] |
| [US-CLIENT-004](#US-CLIENT-004-经纪人查看私客详情页) | 客源管理 | 经纪人查看私客详情页 | [ ] |
| [US-CLIENT-005](#US-CLIENT-005-经纪人查看与编辑需求信息) | 客源管理 | 经纪人查看与编辑需求信息 | [ ] |
| [US-CLIENT-006](#US-CLIENT-006-经纪人写入与查看跟进记录) | 客源管理 | 经纪人写入与查看跟进记录 | [ ] |
| [US-CLIENT-007](#US-CLIENT-007-经纪人管理带看记录预约带看新增带看) | 客源管理 | 经纪人管理带看记录(预约带看/新增带看) | [ ] |
| [US-CLIENT-008](#US-CLIENT-008-经纪人管理客源联系人查看新增编辑) | 客源管理 | 经纪人管理客源联系人(查看/新增/编辑) | [ ] |
| [US-CLIENT-009](#US-CLIENT-009-经纪人修改客源等级) | 客源管理 | 经纪人修改客源等级 | [ ] |
| [US-CLIENT-010](#US-CLIENT-010-经纪人修改客源状态) | 客源管理 | 经纪人修改客源状态 | [ ] |
| [US-CLIENT-011](#US-CLIENT-011-经纪人手动将私客转为公客) | 客源管理 | 经纪人手动将私客转为公客 | [ ] |
| [US-CLIENT-012](#US-CLIENT-012-经纪人将私客转为成交客) | 客源管理 | 经纪人将私客转为成交客 | [ ] |
| [US-CLIENT-013](#US-CLIENT-013-经纪人将客源标记为无效) | 客源管理 | 经纪人将客源标记为无效 | [ ] |
| [US-CLIENT-014](#US-CLIENT-014-经纪人编辑客源完整信息联系人基础信息需求) | 客源管理 | 经纪人编辑客源完整信息(联系人/基础信息/需求) | [ ] |
| [US-CLIENT-015](#US-CLIENT-015-经纪人管理客源相关员工查看编辑归属人首录人) | 客源管理 | 经纪人管理客源相关员工(查看/编辑归属人/首录人) | [ ] |
| [US-CLIENT-016](#US-CLIENT-016-系统自动将超时无跟进的私客转为公客) | 客源管理 | 系统自动将超时无跟进的私客转为公客 | [ ] |
| [US-CLIENT-017](#US-CLIENT-017-系统自动检测重复客源并提示) | 客源管理 | 系统自动检测重复客源并提示 | [ ] |
| [US-ORG-001](#US-ORG-001-管理员维护公司组织结构部门门店树) | 组织人事 | 管理员维护公司组织结构(部门/门店树) | [ ] |
| [US-ORG-002](#US-ORG-002-管理员查看与维护员工列表) | 组织人事 | 管理员查看与维护员工列表 | [ ] |
| [US-ORG-003](#US-ORG-003-管理员办理员工入职并创建系统账号) | 组织人事 | 管理员办理员工入职并创建系统账号 | [ ] |
| [US-PERMISSION-001](#US-PERMISSION-001-管理员配置角色预设角色自定义角色) | 权限管理 | 管理员配置角色(预设角色/自定义角色) | [ ] |
| [US-PERMISSION-002](#US-PERMISSION-002-管理员查看与管理人员权限列表) | 权限管理 | 管理员查看与管理人员权限列表 | [ ] |
| [US-PERMISSION-003](#US-PERMISSION-003-管理员批量为员工分配角色) | 权限管理 | 管理员批量为员工分配角色 | [ ] |
| [US-PERMISSION-004](#US-PERMISSION-004-系统执行功能权限控制菜单级) | 权限管理 | 系统执行功能权限控制(菜单级) | [ ] |
| [US-PERMISSION-005](#US-PERMISSION-005-系统执行数据权限控制部门个人全司) | 权限管理 | 系统执行数据权限控制(部门/个人/全司) | [ ] |
| [US-SETTING-001](#US-SETTING-001-管理员配置房源相关设置字段必填自定义字段标签) | 系统配置 | 管理员配置房源相关设置(字段必填/自定义字段/标签) | [ ] |
#### Phase 2 — 增强功能P1
| US 编号 | 模块 | Task 描述 | 状态 |
|---|---|---|---|
| [US-ACCOUNT-010](#US-ACCOUNT-010-经纪人使用短信验证码登录) | 用户登录 | 经纪人使用短信验证码登录 | [ ] |
| [US-ACCOUNT-011](#US-ACCOUNT-011-经纪人重置账号密码) | 用户登录 | 经纪人重置账号密码 | [ ] |
| [US-ACCOUNT-012](#US-ACCOUNT-012-系统记住用户登录状态) | 用户登录 | 系统记住用户登录状态 | [ ] |
| [US-COMPLEX-010](#US-COMPLEX-010-管理员管理楼盘照片) | 楼盘管理 | 管理员管理楼盘照片 | [ ] |
| [US-COMPLEX-011](#US-COMPLEX-011-管理员维护楼盘价格走势) | 楼盘管理 | 管理员维护楼盘价格走势 | [ ] |
| [US-COMPLEX-012](#US-COMPLEX-012-管理员维护周边配套学校管理) | 楼盘管理 | 管理员维护周边配套(学校管理) | [ ] |
| [US-PROPERTY-010](#US-PROPERTY-010-经纪人查看房源维护完成度诊断面板) | 房源管理 | 经纪人查看房源维护完成度(诊断面板) | [ ] |
| [US-PROPERTY-011](#US-PROPERTY-011-经纪人管理敏感信息跟进权限控制) | 房源管理 | 经纪人管理敏感信息跟进(权限控制) | [ ] |
| [US-PROPERTY-012](#US-PROPERTY-012-经纪人管理房源附件) | 房源管理 | 经纪人管理房源附件 | [ ] |
| [US-PROPERTY-013](#US-PROPERTY-013-经纪人查看房源市场报盘) | 房源管理 | 经纪人查看房源市场报盘 | [ ] |
| [US-PROPERTY-014](#US-PROPERTY-014-经纪人查看房源价格解读) | 房源管理 | 经纪人查看房源价格解读 | [ ] |
| [US-CLIENT-020](#US-CLIENT-020-经纪人使用二手配房功能查看匹配房源) | 客源管理 | 经纪人使用二手配房功能查看匹配房源 | [ ] |
| [US-CLIENT-021](#US-CLIENT-021-经纪人查看客源解读AI行为分析) | 客源管理 | 经纪人查看客源解读AI行为分析 | [ ] |
| [US-CLIENT-022](#US-CLIENT-022-经纪人将重点客源收藏至收藏夹) | 客源管理 | 经纪人将重点客源收藏至收藏夹 | [ ] |
| [US-CLIENT-023](#US-CLIENT-023-经纪人通过快捷入口编辑客源基础信息) | 客源管理 | 经纪人通过快捷入口编辑客源基础信息 | [ ] |
| [US-CLIENT-024](#US-CLIENT-024-经纪人查看客源操作日志) | 客源管理 | 经纪人查看客源操作日志 | [ ] |
| [US-ORG-010](#US-ORG-010-管理员办理员工离职与调动) | 组织人事 | 管理员办理员工离职与调动 | [ ] |
| [US-ORG-011](#US-ORG-011-管理员维护员工通讯录) | 组织人事 | 管理员维护员工通讯录 | [ ] |
| [US-ORG-012](#US-ORG-012-管理员管理员工职务) | 组织人事 | 管理员管理员工职务 | [ ] |
| [US-PERMISSION-010](#US-PERMISSION-010-管理员配置字段级权限敏感字段可见性) | 权限管理 | 管理员配置字段级权限(敏感字段可见性) | [ ] |
| [US-PERMISSION-011](#US-PERMISSION-011-管理员配置个人特定权限覆盖) | 权限管理 | 管理员配置个人特定权限覆盖 | [ ] |
| [US-SETTING-010](#US-SETTING-010-管理员配置首页展示内容) | 系统配置 | 管理员配置首页展示内容 | [ ] |
| [US-SETTING-011](#US-SETTING-011-管理员配置相关方规则) | 系统配置 | 管理员配置相关方规则 | [ ] |
| [US-SETTING-012](#US-SETTING-012-管理员配置客源相关参数) | 系统配置 | 管理员配置客源相关参数 | [ ] |
| [US-SYSTEM-010](#US-SYSTEM-010-平台管理员管理租户开通暂停配置) | 系统管理 | 平台管理员管理租户(开通/暂停/配置) | [ ] |
| [US-SYSTEM-011](#US-SYSTEM-011-平台管理员监控系统健康状态) | 系统管理 | 平台管理员监控系统健康状态 | [ ] |
| [US-RELEASE-010](#US-RELEASE-010-系统发布Windows桌面客户端安装包) | 客户端发布 | 系统发布Windows桌面客户端安装包 | [ ] |
| [US-RELEASE-011](#US-RELEASE-011-客户端自动检测并更新至最新版本) | 客户端发布 | 客户端自动检测并更新至最新版本 | [ ] |
#### Phase 3 — 路线图功能P2
| US 编号 | 模块 | Task 描述 | 状态 |
|---|---|---|---|
| [US-PROPERTY-020](#US-PROPERTY-020-经纪人录入别墅商铺商住写字楼其他类型房源) | 房源管理 | 经纪人录入别墅/商铺/商住/写字楼/其他类型房源 | [ ] |
| [US-PROPERTY-021](#US-PROPERTY-021-经纪人查看全部商铺写字楼列表) | 房源管理 | 经纪人查看全部商铺/写字楼列表 | [ ] |
| [US-COMPLEX-020](#US-COMPLEX-020-管理员使用应用数据标准功能) | 楼盘管理 | 管理员使用应用数据标准功能 | [ ] |
| [US-CLIENT-030](#US-CLIENT-030-经纪人查看与管理公客列表) | 客源管理 | 经纪人查看与管理公客列表 | [ ] |
| [US-CLIENT-031](#US-CLIENT-031-经纪人查看与管理成交客列表) | 客源管理 | 经纪人查看与管理成交客列表 | [ ] |
| [US-CLIENT-032](#US-CLIENT-032-经纪人管理暂缓私客) | 客源管理 | 经纪人管理暂缓私客 | [ ] |
| [US-ORG-020](#US-ORG-020-管理员查看员工异动记录) | 组织人事 | 管理员查看员工异动记录 | [ ] |
| [US-ORG-021](#US-ORG-021-管理员管理员工奖惩记录) | 组织人事 | 管理员管理员工奖惩记录 | [ ] |
| [US-ORG-022](#US-ORG-022-管理员查看门店分布地图) | 组织人事 | 管理员查看门店分布地图 | [ ] |
| [US-SETTING-020](#US-SETTING-020-管理员配置人事OA相关参数) | 系统配置 | 管理员配置人事OA相关参数 | [ ] |
| [US-SETTING-021](#US-SETTING-021-管理员配置交易规则) | 系统配置 | 管理员配置交易规则 | [ ] |
| [US-SETTING-022](#US-SETTING-022-管理员配置财务规则) | 系统配置 | 管理员配置财务规则 | [ ] |
| [US-SETTING-023](#US-SETTING-023-管理员配置合同模板) | 系统配置 | 管理员配置合同模板 | [ ] |
| [US-SYSTEM-020](#US-SYSTEM-020-平台管理员查看操作审计日志) | 系统管理 | 平台管理员查看操作审计日志 | [ ] |
| [US-SYSTEM-021](#US-SYSTEM-021-平台管理员管理灰度发布滚动升级) | 系统管理 | 平台管理员管理灰度发布/滚动升级 | [ ] |
---
## Phase 1 - MVPP0上线前必须完成
---
### 用户登录
##### US-ACCOUNT-001 经纪人管理员使用账号密码登录系统
- 参考PRD文档`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 账号密码登录
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/登录管理/登录_UI.md`
- 状态:[ ]
- 验收标准:输入正确账号密码后跳转首页;密码错误时展示"账号或密码错误"提示连续错误5次后账号锁定提示登录成功后 Token 写入 Cookie
##### US-ACCOUNT-002 系统识别多租户子域名域名路由
- 参考PRD文档`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 多租户识别
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
- 状态:[ ]
- 验收标准:访问不同子域名时系统自动切换对应租户 Schema非法/不存在子域名返回404页面跨租户请求被拦截并返回403
##### US-ACCOUNT-003 系统管理 Token 与会话超时
- 参考PRD文档`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - Token 管理/会话超时
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
- 状态:[ ]
- 验收标准会话超时后自动跳转登录页Token 刷新机制正常工作;登出后 Token 立即失效,再次请求跳转登录页
---
### 楼盘管理
##### US-COMPLEX-001 管理员录入与维护楼盘基础信息
- 参考PRD文档`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘信息管理
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/楼盘管理/楼盘详情_UI.md`
- 状态:[ ]
- 验收标准:可录入楼盘名称、地址、楼栋、结构等基础字段;保存成功后楼盘出现在楼盘列表;必填字段未填时高亮错误提示;楼盘编号系统自动生成且唯一
##### US-COMPLEX-002 经纪人查看楼盘列表与详情
- 参考PRD文档`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘列表/楼盘详情
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/楼盘管理/楼盘列表_UI.md`
- 状态:[ ]
- 验收标准:楼盘列表支持按名称/地址关键词搜索支持分页默认20条/页);点击楼盘名称跳转详情页;详情页展示楼盘基本信息、楼栋结构信息
##### US-COMPLEX-003 管理员维护区域管理城区商圈
- 参考PRD文档`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 区域管理
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
- 状态:[ ]
- 验收标准:可新增/编辑/删除城区和商圈;商圈必须归属于城区;区域数据被房源和客源模块正确关联引用;删除有关联数据的区域时系统给出警告
---
### 房源管理
##### US-PROPERTY-001 经纪人录入二手住宅出售出租
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 录入住宅(二手出售/出租)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/房源管理/新增房源_UI.md`
- 状态:[ ]
- 验收标准可在3分钟内完成住宅基本信息录入必填字段楼盘/楼层/面积/价格)未填时高亮错误提示;保存成功后跳转房源详情页并显示"保存成功";录入后即刻出现在房源列表
##### US-PROPERTY-002 经纪人查看与筛选房源列表
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源列表(二手&租赁)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/房源管理/房源列表_UI.md`
- 状态:[ ]
- 验收标准:列表支持按楼盘名/业主姓名/电话/房源编号关键词搜索;支持状态/区域/价格/房型多维度组合筛选列表分页默认20条/页且89000条数据下查询响应<2秒支持导出当前筛选结果为Excel
##### US-PROPERTY-003 经纪人查看房源详情页
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源详情页
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/房源管理/房源详情_UI.md`
- 状态:[ ]
- 验收标准:详情页完整展示基本信息、产证信息、交易信息;号码默认打码,点击"查看号码"后解密展示并记录审计日志;页面各功能 Tab 可正常切换
##### US-PROPERTY-004 经纪人写入与查看房源跟进记录
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 跟进记录(全部/写入/修改/其他)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 状态:[ ]
- 验收标准:跟进记录按时间线倒序展示;支持写入跟进/修改跟进/其他跟进(钥匙/委托/实勘子Tab切换跟进内容最少6字校验写入成功后记录实时出现在列表顶部
##### US-PROPERTY-005 经纪人管理房源图片上传分类排序
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 图片管理(相册上传/分类/排序)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 状态:[ ]
- 验收标准:支持上传 bmp/jpg/png/gif 格式图片单文件最大20MB上传成功后自动按分类展示支持拖拽排序封面图可手动指定
##### US-PROPERTY-006 经纪人管理业主联系人
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 业主联系人管理
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 状态:[ ]
- 验收标准:支持新增/编辑业主联系人(姓名/电话/微信);手机号加密存储,展示时默认打码;可查看同业主名下其他房源;至少保留一个联系人
##### US-PROPERTY-007 经纪人调整房源价格
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 价格调整(调价/调价记录)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 状态:[ ]
- 验收标准:调价弹窗需填写新价格和调价原因;调价成功后房源列表和详情页价格实时更新;调价记录以时间线形式留存且不可删除;调价幅度在列表中以"降价XX万"标签展示
##### US-PROPERTY-008 经纪人变更房源状态
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源状态变更(在售/暂缓/成交/下架)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 状态:[ ]
- 验收标准:状态变更严格遵循状态机流转规则(在售→暂缓/成交/下架);每次状态变更需填写原因;状态变更后列表状态标签实时更新;状态变更记录写入跟进日志
---
### 客源管理
##### US-CLIENT-001 经纪人录入新私客
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 1经纪人录入新私客5.2 录入私客
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/新增客源_UI.md`
- 状态:[ ]
- 验收标准:录入页面可通过顶部导航「客源」→「+新增私客」或右侧快捷入口「增客」触达联系人1必填姓名/性别/电话1联系人2起可增加删除基础信息必填字段状态/用途/等级/来源)缺填时高亮错误并定位到第一个错误处;保存成功后跳转该客源详情页并显示"保存成功"提示
##### US-CLIENT-002 经纪人查看与筛选私客列表全部求购求租
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2/3/4经纪人查看与筛选私客列表5.1 客源列表
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md`
- 状态:[ ]
- 验收标准顶部Tab导航私客/资料客/营销客/成交客/公客及二级Tab求购/求租/暂缓/全部私客)可正常切换;搜索框支持按客源姓名/号码/号码后4位/客源编号/备注检索;筛选栏支持状态/等级/位置/价格/房室等多维度组合筛选列表底部实时显示当前筛选总条数分页默认20条/页89000条数据量下查询<2秒
##### US-CLIENT-003 经纪人批量操作私客列表
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2列表批量操作5.1.3 批量操作
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md`
- 状态:[ ]
- 验收标准:勾选客源后激活批量操作按钮(修改相关方/修改来源/删除客源/合并客户);批量删除执行软删除,可在"已删客源"中查看批量修改相关方成功后列表归属人字段实时刷新列表支持导出当前筛选结果为ExcelCelery异步处理
##### US-CLIENT-004 经纪人查看私客详情页
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 6经纪人查看私客详情页Story 15经纪人查看客源信息概览面板
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准:详情页顶部展示需求标题+联系人姓名+带看进度标签;右侧固定信息概览面板展示客户编号/委托日期/需求类型等字段且不随页面滚动消失主内容区Tab导航需求信息/跟进记录/带看/客源解读/智能配房)默认激活"需求信息"Tab右侧面板三个主操作按钮打电话/写跟进/报备带看)可正常触发对应流程
##### US-CLIENT-005 经纪人查看与编辑需求信息
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 7经纪人查看与编辑需求信息
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准需求信息Tab展示总价/面积/居室/装修/朝向/楼层/楼龄/意向商圈/意向小区等字段(三栏布局);字段值为空时显示"-"占位符;右上角「编辑」链接点击后字段转为输入框/选择器;保存成功后返回详情页并刷新需求信息区块
##### US-CLIENT-006 经纪人写入与查看跟进记录
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 8经纪人写入与查看跟进记录
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准跟进记录分5个子Tab全部/写入跟进/敏感信息跟进/修改跟进/其他跟进全部Tab支持时间范围筛选及「有录音」「有图片」快速过滤跟进记录以时间线形式按日期分组倒序展示写入跟进时跟进目的支持23项多选跟进内容最少6字校验系统自动生成的操作日志如新增私客/状态变更)出现在"其他跟进"Tab
##### US-CLIENT-007 经纪人管理带看记录预约带看新增带看
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 9经纪人管理带看记录
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准带看Tab分"预约"和"带看"两个子Tab新增带看表单必填字段带看时间/带看情况≥6字/带看房源≥1套校验通过才可提交带看记录以时间线展示含带看房源蓝色可点击链接和带看进度标签员工选择器弹层支持组织树搜索和多选带看房源选择器支持按编号/楼盘/业主关键词搜索
##### US-CLIENT-008 经纪人管理客源联系人查看新增编辑
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 23经纪人管理客源联系人
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准:联系人面板默认打码显示电话("+86 135\*\*\*\*\*\*\*\*"),点击「查看号码」后展示完整号码并写入敏感信息跟进日志;新增联系人必填字段(姓名/称呼/电话1校验编辑联系人时电话1需点击「查看号码」后才可编辑保存成功后联系人面板实时刷新
##### US-CLIENT-009 经纪人修改客源等级
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 17经纪人修改客源等级
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准点击「改等级」触发弹窗展示当前等级只读和新等级下拉选择器A急迫/B较强/C一般/D较弱/E暂不关注新等级未选择时「确定」按钮置灰保存成功后信息概览面板等级标签实时更新操作日志中自动新增"改等级"记录
##### US-CLIENT-010 经纪人修改客源状态
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 18经纪人修改客源状态
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准:点击「改状态」触发弹窗,展示当前状态(只读)、新状态下拉(求购/求租/租购)、等级下拉和必填更改理由文本框;新状态未选或理由未填时「确定」按钮置灰;保存成功后信息概览面板状态标签实时更新;操作日志新增"改状态"记录(含更改理由)
##### US-CLIENT-011 经纪人手动将私客转为公客
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 19经纪人手动将私客转为公客
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准点击「转公客」触发弹窗状态和等级均为必填确认后客源从私客列表移除并进入公客池client_type='public'transfer_to_public_type='manual');操作日志新增"转公客"记录(含操作人/操作时间);权限控制:仅归属人、首录人或有管理权限的店长/经理可操作
##### US-CLIENT-012 经纪人将私客转为成交客
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 20经纪人将私客转为成交客
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准:点击「转成交」触发"客户成交"弹窗,必填字段(状态/房源类型/成交房源/成交日期/成交价格/成交方)全部填写后「确定」按钮高亮;成交房源选择浮层支持关键词搜索和区域/状态筛选;提交后客源移入成交客列表,状态更新为"成交";操作日志新增"转成交"记录(含成交信息摘要)
##### US-CLIENT-013 经纪人将客源标记为无效
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 21经纪人将客源标记为无效
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准:点击「转无效」触发弹窗,展示蓝色提示框说明"将把所有电话标记无效",无效原因单选(号码无效/同行中介/广告推销/客户无意向/其他)默认选中"号码无效";确认后所有联系人电话标记为无效,客源从私客活跃列表移除;操作日志新增"转无效"记录(含无效原因);权限控制:仅归属人、首录人或有管理权限者可操作
##### US-CLIENT-014 经纪人编辑客源完整信息联系人基础信息需求
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 14经纪人编辑客源信息
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/编辑客源_UI.md`
- 状态:[ ]
- 验收标准编辑页面分三个Tab联系人/基础信息/二手或新房或租房联系人Tab的电话1需点击「查看号码」后才可编辑旁边提供「标记无效」链接基础信息Tab包含需求类型/购房目的/付款方式/名下房产/贷款记录等扩展字段;保存时校验所有必填字段,成功后返回详情页并刷新需求信息区块
##### US-CLIENT-015 经纪人管理客源相关员工查看编辑归属人首录人
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 24经纪人管理客源相关员工
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准:相关员工面板展示首录人/归属人的所属门店+小组+姓名及参与时间;点击「编辑」触发弹窗,首录人和归属人均为必填下拉选择器(支持搜索姓名);保存后面板实时更新,操作日志新增"修改相关员工"记录;权限控制:跨团队/跨店修改需店长及以上权限
##### US-CLIENT-016 系统自动将超时无跟进的私客转为公客
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - 关键业务规则:私客自动转公
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 状态:[ ]
- 验收标准Celery Beat 定时任务每日凌晨执行超过运营配置天数如30天无跟进且非保护客的私客自动转入公客池transfer_to_public_type='auto');自动转公后 client_status_logs 生成一条 to_public 记录;即将过期的私客在列表中显示"即将掉公"提示标签
##### US-CLIENT-017 系统自动检测重复客源并提示
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 2顶部重复检测提示关键业务规则私客手机号唯一性
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 状态:[ ]
- 验收标准:录入/编辑联系人手机号时实时通过phone_hash检测与现有私客/成交客/公客的重复;客源列表顶部实时显示"私客与成交客重复XX"和"私客与公客重复XX"蓝色可点击链接;点击重复数字链接可查看重复名单
---
### 组织人事
##### US-ORG-001 管理员维护公司组织结构部门门店树
- 参考PRD文档`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 公司组织结构(部门/门店树)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md`
- 状态:[ ]
- 验收标准:支持新增/编辑/删除部门和门店节点;组织树以层级结构展示(公司→区域→门店→小组);删除有员工的部门时系统提示并阻止操作;组织结构变更实时反映在员工选择器弹层中
##### US-ORG-002 管理员查看与维护员工列表
- 参考PRD文档`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 员工列表/员工详情
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md`
- 状态:[ ]
- 验收标准:员工列表支持按姓名/手机号关键词搜索;支持按部门/状态筛选;列表展示员工姓名/所属门店/职位/状态等字段;点击员工姓名跳转员工详情页
##### US-ORG-003 管理员办理员工入职并创建系统账号
- 参考PRD文档`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 员工入职/账号创建
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md`
- 状态:[ ]
- 验收标准:入职表单必填字段(姓名/手机号/所属门店/职位)校验;创建账号后系统自动生成登录密码并可发送给员工;新员工账号立即可登录系统;员工账号与组织树节点正确关联
---
### 权限管理
##### US-PERMISSION-001 管理员配置角色预设角色自定义角色
- 参考PRD文档`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 角色管理(预设角色+自定义角色)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md`
- 状态:[ ]
- 验收标准:系统预设角色(经纪人/店长/管理员等)不可删除但可复制;支持创建自定义角色并配置功能权限;角色名称在同租户内唯一;删除自定义角色前需解除所有人员绑定
##### US-PERMISSION-002 管理员查看与管理人员权限列表
- 参考PRD文档`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 人员权限列表
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md`
- 状态:[ ]
- 验收标准:人员权限列表展示员工姓名/所属部门/当前角色;支持按部门/角色筛选;列表支持分页;点击员工行可查看详细权限配置
##### US-PERMISSION-003 管理员批量为员工分配角色
- 参考PRD文档`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 角色批量分配
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md`
- 状态:[ ]
- 验收标准勾选多名员工后可批量指定角色批量分配成功后所有选中员工角色立即生效分配后员工下次登录或刷新页面权限即更新Redis权限快照失效重载
##### US-PERMISSION-004 系统执行功能权限控制菜单级
- 参考PRD文档`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 功能权限(菜单级)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md`
- 状态:[ ]
- 验收标准无权限的菜单在导航中不展示直接访问无权限URL返回403页面权限变更后Redis缓存自动失效用户下一次请求即应用新权限经纪人无法访问管理员专属功能页
##### US-PERMISSION-005 系统执行数据权限控制部门个人全司
- 参考PRD文档`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 数据权限(部门/个人/全司)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md`
- 状态:[ ]
- 验收标准经纪人只能看到自己名下的房源和客源数据店长可见本门店所有员工的房源和客源管理员可见全司数据不同数据权限级别的用户查询结果严格隔离不可通过URL参数绕过
---
### 系统配置
##### US-SETTING-001 管理员配置房源相关设置字段必填自定义字段标签
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置.md` - 房源设置(字段必填/自定义字段/标签)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md`
- 状态:[ ]
- 验收标准:可配置房源录入表单中哪些字段为必填;可新增自定义字段并在房源表单中展示;标签配置后可在房源筛选中使用;配置变更后房源录入表单实时生效
---
## Phase 2 - 增强功能P1MVP 后第一迭代)
---
### 用户登录
##### US-ACCOUNT-010 经纪人使用短信验证码登录
- 参考PRD文档`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 短信验证码登录
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
- 状态:[ ]
- 验收标准:点击"发送验证码"后60秒内不可重复点击验证码5分钟内有效输入正确验证码后登录成功跳转首页错误验证码给出明确提示
##### US-ACCOUNT-011 经纪人重置账号密码
- 参考PRD文档`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 密码重置
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
- 状态:[ ]
- 验收标准通过手机号验证码或邮箱链接验证身份新密码需二次确认且满足强度要求8位以上含字母+数字);重置成功后原会话立即失效需重新登录;重置操作写入审计日志
##### US-ACCOUNT-012 系统记住用户登录状态
- 参考PRD文档`Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` - 记住登录状态
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
- 状态:[ ]
- 验收标准:勾选"记住我"后关闭浏览器重新打开仍处于登录态记住登录的有效期为7天超过有效期自动跳转登录页管理员可在后台强制让指定账号的所有会话失效
---
### 楼盘管理
##### US-COMPLEX-010 管理员管理楼盘照片
- 参考PRD文档`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘照片管理
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
- 状态:[ ]
- 验收标准支持上传楼盘封面图和相册图片格式jpg/png/gif单文件≤20MB上传成功后图片在楼盘详情页展示支持删除图片楼盘封面图可手动设置
##### US-COMPLEX-011 管理员维护楼盘价格走势
- 参考PRD文档`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 楼盘价格走势
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
- 状态:[ ]
- 验收标准:楼盘详情页可查看历史价格走势折线图;价格走势数据按月维度展示;支持手动录入或批量导入历史均价数据
##### US-COMPLEX-012 管理员维护周边配套学校管理
- 参考PRD文档`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 周边配套(学校管理)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md`
- 状态:[ ]
- 验收标准:可为楼盘关联周边学校(从学校库选择);学校库支持新增/编辑/删除;楼盘详情页展示关联学校列表;学校数据可在客源意向学校字段中搜索引用
---
### 房源管理
##### US-PROPERTY-010 经纪人查看房源维护完成度诊断面板
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 房源维护完成度(诊断面板)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 状态:[ ]
- 验收标准房源详情页展示数据完整度百分比及未填字段提示列表点击未填字段提示可直接跳转对应编辑区域完整度≥80%时展示绿色状态,<60%时展示红色警告
##### US-PROPERTY-011 经纪人管理敏感信息跟进权限控制
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 敏感信息跟进(查看权限控制)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 状态:[ ]
- 验收标准敏感信息跟进Tab仅对有权限的角色可见查看敏感信息自动写入审计日志不可删除无权限用户看到Tab时显示"无权限查看"提示而非隐藏Tab
##### US-PROPERTY-012 经纪人管理房源附件
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 附件管理
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 状态:[ ]
- 验收标准支持上传PDF/Word/Excel等文档类附件单文件≤50MB附件按上传时间倒序展示支持下载和删除附件附件上传通过Celery异步处理不阻塞主线程
##### US-PROPERTY-013 经纪人查看房源市场报盘
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 市场报盘
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 状态:[ ]
- 验收标准:房源详情页可查看同楼盘其他在售房源的报价分布;展示该楼盘近期成交均价和当前挂牌均价;数据按成交日期倒序展示
##### US-PROPERTY-014 经纪人查看房源价格解读
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 价格解读
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 状态:[ ]
- 验收标准:房源详情页展示基于同楼盘/同商圈数据的价格合理性分析;展示当前报价与参考价的偏差百分比;无足够数据时展示"暂无参考数据"
---
### 客源管理
##### US-CLIENT-020 经纪人使用二手配房功能查看匹配房源
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 11经纪人使用二手配房功能推荐房源
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准智能配房Tab分"录客配房"和"系统配房"两个子Tab录客配房按优质户型/降价/热门/新上四个分组展示房源卡片;每套房源卡片展示封面图/小区名/户型/面积/售价/标签;点击「分享房源」触发房源分享流程;支持「批量分享」多套房源
##### US-CLIENT-021 经纪人查看客源解读AI行为分析
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 10经纪人查看客源解读
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准客源解读Tab展示活跃行为/活跃时间/购房偏好三个模块购房偏好支持近7日/近30日/近90日三个时间维度切换切换后数据联动刷新价格/户型/面积偏好以圆环图+图例形式展示;无数据时展示"暂无数据"而非报错
##### US-CLIENT-022 经纪人将重点客源收藏至收藏夹
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 16经纪人收藏客源至私客收藏夹
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准:点击「☆收藏」触发"选择私客收藏夹"浮层,默认选中"默认收藏夹"支持在浮层内创建新收藏夹名称最多10字超出「创建」按钮置灰收藏成功后图标变为实心★橙色私客列表支持按收藏夹筛选展示
##### US-CLIENT-023 经纪人通过快捷入口编辑客源基础信息
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 22经纪人编辑客源基础信息快捷入口
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- 状态:[ ]
- 验收标准:点击信息概览面板「编辑客源」入口触发"编辑基础信息"抽屉浮层;浮层包含需求类型/用途/来源(必填)及购房目的/付款方式/名下房产等选填字段;点击「确定」校验必填字段,保存成功后面板相关字段实时更新;点击「取消」或×关闭不保存
##### US-CLIENT-024 经纪人查看客源操作日志
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 25经纪人查看客源操作日志
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 状态:[ ]
- 验收标准:点击「查看操作日志」跳转独立日志列表页(面包屑:客源/客源详情/客源操作日志);支持按日期范围/操作人/操作类型筛选;日志列表按操作时间倒序排列,展示操作时间/操作人/操作类型/操作内容日志为只读不支持编辑或删除支持分页默认20条/页)
---
### 组织人事
##### US-ORG-010 管理员办理员工离职与调动
- 参考PRD文档`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 员工离职/调动
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md`
- 状态:[ ]
- 验收标准:员工离职后账号立即禁用无法登录;离职员工名下房源和客源可批量转移给其他员工;员工调动后所属部门/组织信息实时更新;调动/离职操作写入异动记录
##### US-ORG-011 管理员维护员工通讯录
- 参考PRD文档`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 员工通讯录
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md`
- 状态:[ ]
- 验收标准:通讯录支持按姓名/部门搜索;展示员工姓名/职位/手机号/所属门店;手机号默认打码,有权限才可查看完整号码;支持按部门分组展示
##### US-ORG-012 管理员管理员工职务
- 参考PRD文档`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 职务管理
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md`
- 状态:[ ]
- 验收标准:支持新增/编辑/删除职务(如经纪人/店长/区域经理);职务可关联权限角色;删除有在职员工绑定的职务时系统提示并阻止
---
### 权限管理
##### US-PERMISSION-010 管理员配置字段级权限敏感字段可见性
- 参考PRD文档`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 字段级权限(敏感字段可见性)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md`
- 状态:[ ]
- 验收标准:可为不同角色配置联系人手机号/证件号码等敏感字段的可见性无查看权限的角色访问时号码始终打码无法点击查看查看敏感字段操作记录在审计日志中字段级权限配置变更后Redis缓存立即失效
##### US-PERMISSION-011 管理员配置个人特定权限覆盖
- 参考PRD文档`Project/fonrey/PRD/权限管理/权限管理模块PRD.md` - 个人特定权限覆盖
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md`
- 状态:[ ]
- 验收标准:支持在角色权限基础上为特定员工单独开启或关闭特定权限点;个人覆盖权限优先级高于角色权限;个人权限覆盖配置记录可查看和删除
---
### 系统配置
##### US-SETTING-010 管理员配置首页展示内容
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置.md` - 首页设置
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md`
- 状态:[ ]
- 验收标准:可配置首页展示的统计数据卡片(如今日新增房源/客源数量);配置变更后首页实时生效;不同角色可配置不同的首页视图
##### US-SETTING-011 管理员配置相关方规则
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置.md` - 相关方设置
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md`
- 状态:[ ]
- 验收标准:可配置房源/客源的相关方角色(如协作人/跟进人)及其权限范围;相关方配置影响房源/客源详情页的相关员工区块展示
##### US-SETTING-012 管理员配置客源相关参数
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置.md` - 客源设置(基本配置/参数配置)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md`
- 状态:[ ]
- 验收标准可配置私客自动转公客的天数阈值可配置客源来源枚举值lookup_items可配置活跃度计算的各阈值天数配置变更后Celery定时任务下次执行时使用新配置
---
### 系统管理(运营后台)
##### US-SYSTEM-010 平台管理员管理租户开通暂停配置
- 参考PRD文档`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 租户管理(开通/暂停/配置)
- 状态:[ ]
- 验收标准可在运营后台新开通租户自动创建独立PostgreSQL Schema可暂停租户暂停后租户用户无法登录可为租户配置域名/子域名;租户操作记录写入平台操作日志
##### US-SYSTEM-011 平台管理员监控系统健康状态
- 参考PRD文档`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 系统健康监控
- 状态:[ ]
- 验收标准运营后台展示系统核心指标API响应时间/错误率/Celery队列积压Sentry错误告警正常接收Grafana面板可查看历史监控数据关键指标超阈值时触发告警通知
---
### 客户端发布
##### US-RELEASE-010 系统发布Windows桌面客户端安装包
- 参考PRD文档`Project/fonrey/PRD/发布管理/客户端发布管理模块PRD.md` - Windows桌面客户端
- 状态:[ ]
- 验收标准electron-builder 输出 NSIS .exe 安装包和便携版 .zip安装包经EV证书签名安装时无SmartScreen警告安装包上传至Cloudflare R2并通过CDN分发后端 ClientRelease 表新增一条版本记录
##### US-RELEASE-011 客户端自动检测并更新至最新版本
- 参考PRD文档`Project/fonrey/PRD/发布管理/客户端发布管理模块PRD.md` - 自动更新机制
- 状态:[ ]
- 验收标准客户端启动时及每4小时自动检测 GET /api/client/updates/latest/ 接口有新版本时后台静默下载下载完成后提示用户重启下载完成后校验SHA256与服务端返回一致才允许安装强制更新标记时用户无法跳过更新
---
## Phase 3 - 路线图功能P2已规划未排期
---
### 房源管理
##### US-PROPERTY-020 经纪人录入别墅商铺商住写字楼其他类型房源
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 录入别墅/商铺/商住/写字楼/其他
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
##### US-PROPERTY-021 经纪人查看全部商铺写字楼列表
- 参考PRD文档`Project/fonrey/PRD/房源管理/房源管理模块PRD.md` - 全部商铺列表/全部写字楼列表
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
---
### 楼盘管理
##### US-COMPLEX-020 管理员使用应用数据标准功能
- 参考PRD文档`Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md` - 应用数据标准
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
---
### 客源管理
##### US-CLIENT-030 经纪人查看与管理公客列表
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 12经纪人查看与筛选公客列表
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
##### US-CLIENT-031 经纪人查看与管理成交客列表
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 13经纪人查看成交客列表
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
##### US-CLIENT-032 经纪人管理暂缓私客
- 参考PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md` - Story 5经纪人管理暂缓私客
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
---
### 组织人事
##### US-ORG-020 管理员查看员工异动记录
- 参考PRD文档`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 异动记录
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md`
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
##### US-ORG-021 管理员管理员工奖惩记录
- 参考PRD文档`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 奖惩记录
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md`
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
##### US-ORG-022 管理员查看门店分布地图
- 参考PRD文档`Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md` - 门店分布地图
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md`
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
---
### 系统配置
##### US-SETTING-020 管理员配置人事OA相关参数
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置.md` - 人事OA设置
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
##### US-SETTING-021 管理员配置交易规则
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置.md` - 交易设置
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
##### US-SETTING-022 管理员配置财务规则
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置.md` - 财务设置
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
##### US-SETTING-023 管理员配置合同模板
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置.md` - 合同设置
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
---
### 系统管理(运营后台)
##### US-SYSTEM-020 平台管理员查看操作审计日志
- 参考PRD文档`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 操作审计日志
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
##### US-SYSTEM-021 平台管理员管理灰度发布滚动升级
- 参考PRD文档`Project/fonrey/PRD/系统管理/系统管理模块PRD.md` - 灰度发布/滚动升级
- 状态:[ ]
- 验收标准规划中详细验收标准待PRD细化后补充
---
## Phase 4 - 明确不做Out of Scope
> 以下功能在 MVP 阶段及近期版本路线图中明确不实现,仅作备忘。
<!-- OUT OF SCOPE: 移动端适配 - v2 规划 -->
<!-- OUT OF SCOPE: 新房模块(新房管理/新房设置)- 独立模块,后续版本 -->
<!-- OUT OF SCOPE: 合同管理模块 - 独立模块,后续版本 -->
<!-- OUT OF SCOPE: 财务管理/提成结算 - 独立模块,后续版本 -->
<!-- OUT OF SCOPE: 三网发布(安居客/链家/贝壳对接)- 独立模块,后续版本 -->
<!-- OUT OF SCOPE: 数据报表/行程量化 - 独立模块,后续版本 -->
<!-- OUT OF SCOPE: 在线充值/增值服务 - 独立模块,后续版本 -->
<!-- OUT OF SCOPE: 任务管理OA任务/入职祝福)- 低优先 -->
<!-- OUT OF SCOPE: 考勤管理 - 独立 HR 模块 -->
<!-- OUT OF SCOPE: 审批流程 - 独立 OA 模块 -->
<!-- OUT OF SCOPE: 智慧大屏 / VR换装 - 增值产品 -->
<!-- OUT OF SCOPE: 房源广场(跨租户公共池)- 多租户复杂场景 -->
<!-- OUT OF SCOPE: 资料客详细功能 - 后续版本规划 -->
<!-- OUT OF SCOPE: 营销客详细功能(含端口发布/朋友圈分享)- 营销模块另行规划 -->
<!-- OUT OF SCOPE: AI 智能推荐引擎自研 - 基础版基于规则匹配实现 -->
<!-- OUT OF SCOPE: 客源数据对外 API 开放 - 接口规范另行设计 -->
---
### 已完成
(暂无)

File diff suppressed because it is too large Load Diff

View File

@@ -54,15 +54,15 @@
```
fonrey/
├── apps/
│ ├── tenants/ # django-tenants 配置shared_apps
│ ├── accounts/ # 登录认证(详见 登录管理技术方案.md
│ ├── permissions/ # 权限管理(详见 权限管理系统技术方案.md
│ ├── tenant/ # django-tenants 配置shared_apps
│ ├── account/ # 登录认证(详见 登录管理技术方案.md
│ ├── permission/ # 权限管理(详见 权限管理系统技术方案.md
│ ├── org/ # 组织人事org_units, staff
│ ├── region/ # 区域管理districts, business_areas, metro
│ ├── complex/ # 楼盘管理complexes, buildings, schools
│ ├── property/ # 房源核心(含 models/services/tasks 三层)
│ ├── client/ # 客源管理
│ ├── settings/ # 系统设置lookup, tags
│ ├── setting/ # 系统设置lookup, tags
│ └── release/ # 客户端发布管理shared_apps
├── shared/ # 公共 Schema App
└── core/
@@ -144,10 +144,51 @@ apps/property/
---
## 9. 文档维护原则
## 9. 测试策略
> **完整测试规范**见:[`测试规范.md`](./测试规范.md)。本节仅列关键结论。
Fonrey 采用 AI vibe coding 模式开发,测试是保证每日迭代质量的唯一安全网。**每个 P0 User Story 完成后,对应测试必须同步产出,不允许欠测试债。**
### 测试分层
| 层级 | 工具 | 覆盖目标 | 运行频率 |
|------|------|---------|---------|
| **单元测试** | `pytest-django` + `factory_boy` | `core/``services/``tasks.py` | 每次 push |
| **集成测试** | `pytest-django` TenantClient | 所有 P0 User Story 的 HTTP 接口 | 每次 push |
| **E2E 测试** | `playwright` (Python) | 5 条核心用户旅程 | 每日定时 |
### 关键约定
- 所有集成测试必须使用 `django-tenants``TenantClient`,禁止使用 Django 原生 `Client()`
- HTMX 局部请求测试须携带 `HTTP_HX_REQUEST: true` header并验证返回局部 HTML 而非完整页面
- Celery 任务测试使用 `CELERY_TASK_ALWAYS_EAGER = True` 同步执行
- 外部服务R2、Redis、邮件在测试中全部 Mock禁止真实调用
- 每个受权限保护的 View必须覆盖有权限200、无权限403、未登录302三个场景
### 覆盖率基准
| 模块 | 最低目标 |
|------|---------|
| `core/` 核心基础模块 | ≥ 90% |
| `apps/*/services/` 业务逻辑层 | ≥ 80% |
| `apps/*/views.py` 视图层 | ≥ 70% |
| E2E 核心用户旅程5 条) | 100% 通过 |
### CI 自动化
- 每次 push 到 `main` / `develop` 自动运行单元测试 + 集成测试
- 每日北京时间凌晨 2 点自动运行全量套件(含 E2E
- 配置文件:`.github/workflows/daily-test.yml`
---
## 10. 文档维护原则
- 本文档仅记录**跨模块共识**与**模块索引**,不展开模块细节
- 模块技术方案在子文档中维护,并通过 §8 表格回链
- 任何技术栈变更(替换组件、升级主版本、新增外部服务)须同步更新本文档 §2、§5、§6
- 新增模块时,先在 §4 目录结构补位,再在 §8 索引登记子文档
- 测试规范变更须同步更新 §9 关键结论,完整细节在 [`测试规范.md`](./测试规范.md) 中维护

View File

@@ -0,0 +1,611 @@
# Fonrey 测试规范TEST_SPEC
> **For AI assistants**: Read this entire file before writing any test code. All decisions here are final. Do not suggest alternatives unless asked. Every new feature or User Story implementation must be accompanied by corresponding tests as defined in this document.
**版本**: 1.0 **最后更新**: 2026-04-26
**定位**: 本文档定义 Fonrey 项目的完整测试策略包含测试分层、工具选型、目录结构、多租户测试约定、HTMX 测试约定、CI 自动化配置及 AI 辅助编码时的测试要求。
---
## 1. 测试目标
Fonrey 采用 AI vibe coding 模式开发AI 负责生成功能代码,**测试是保证每日迭代质量的唯一安全网**。测试体系须满足:
- 每个 P0 User Story 完成后,对应测试同步产出
- 每日自动运行全量测试套件,输出可读报告
- 测试失败时AI 可根据报告自主定位并修复问题
- 测试环境与生产环境技术栈完全一致(同样使用 PostgreSQL + django-tenants
**覆盖率基准目标**
| 层级 | 最低目标 |
|------|---------|
| `core/` 核心基础模块 | ≥ 90% |
| `apps/*/services/` 业务逻辑层 | ≥ 80% |
| `apps/*/views.py` 视图层 | ≥ 70% |
| `apps/*/tasks.py` 异步任务 | ≥ 70% |
| E2E 核心用户旅程 | 5 条必须全部通过 |
---
## 2. 测试分层架构
Fonrey 采用三层测试体系,从底层向上覆盖:
```
┌─────────────────────────────────────────┐
│ E2E 测试(用户行为模拟) │ ← Playwright
│ 覆盖5 条核心用户旅程 │
├─────────────────────────────────────────┤
│ 集成测试API / View 层) │ ← pytest-django TenantClient
│ 覆盖:所有 P0 User Story 的 HTTP 接口 │
├─────────────────────────────────────────┤
│ 单元测试(逻辑单元) │ ← pytest-django + factory_boy
│ 覆盖core/、services/、tasks.py │
└─────────────────────────────────────────┘
```
**三层分工原则**
- **单元测试**:不启动 HTTP server不依赖浏览器速度最快测试单一函数/类的逻辑正确性
- **集成测试**:使用 Django 测试客户端,验证完整请求-响应链路View → Service → DB不启动真实 HTTP server
- **E2E 测试**:启动真实 Django dev server用浏览器驱动验证真实用户操作流程速度最慢只覆盖核心旅程
---
## 3. 工具选型
### 3.1 工具清单
| 类型 | 工具 | 版本 | 用途 |
|------|------|------|------|
| 测试框架 | `pytest` | ≥ 8.x | 统一测试运行器 |
| Django 集成 | `pytest-django` | ≥ 4.x | Django 数据库、Client、设置管理 |
| 测试数据工厂 | `factory_boy` | ≥ 3.x | 创建测试用 Model 实例,避免手写 fixture |
| 假数据生成 | `Faker` | ≥ 25.x | 生成中文姓名、手机号、地址等假数据 |
| Mock 工具 | `pytest-mock` | ≥ 3.x | Mock 外部依赖R2、Redis、邮件服务 |
| HTTP Mock | `responses` | ≥ 0.25.x | Mock 第三方 HTTP 请求Cloudflare API 等) |
| E2E 测试 | `playwright` (Python) | ≥ 1.44.x | 浏览器自动化 |
| E2E 集成 | `pytest-playwright` | ≥ 0.5.x | Playwright 的 pytest 插件 |
| 覆盖率 | `pytest-cov` | ≥ 5.x | 生成代码覆盖率报告 |
| 并行加速 | `pytest-xdist` | ≥ 3.x | 多进程并行运行单元/集成测试 |
### 3.2 安装依赖
所有测试依赖统一放在 `requirements/test.txt`
```
pytest>=8.0
pytest-django>=4.8
pytest-mock>=3.12
pytest-cov>=5.0
pytest-xdist>=3.5
pytest-playwright>=0.5
factory_boy>=3.3
Faker>=25.0
responses>=0.25
```
安装命令:
```bash
pip install -r requirements/test.txt
playwright install chromium
```
---
## 4. 目录结构
```
fonrey/
└── tests/
├── conftest.py # 全局 fixtures租户、用户、客户端
├── settings_test.py # 测试专用 Django settings
├── factories/ # factory_boy 工厂
│ ├── __init__.py
│ ├── tenant_factory.py # Tenant、域名
│ ├── account_factory.py # Staff、Account
│ ├── org_factory.py # OrgUnit
│ ├── permission_factory.py # Role、Permission
│ ├── complex_factory.py # Complex、Building
│ ├── property_factory.py # Property、FollowUpLog
│ └── client_factory.py # Client、ClientFollowUp
├── unit/ # 单元测试
│ ├── test_encryption.py # PII 加密/解密
│ ├── test_soft_delete.py # 软删除 Manager
│ ├── test_permission_service.py
│ ├── test_property_service.py
│ ├── test_client_service.py
│ └── test_celery_tasks.py # Celery 任务(同步模式)
├── integration/ # 集成测试(按 User Story 分文件)
│ ├── account/
│ │ └── test_us_account.py # US-ACCOUNT-001~003
│ ├── permission/
│ │ └── test_us_permission.py # US-PERMISSION-001~005
│ ├── complex/
│ │ └── test_us_complex.py
│ ├── property/
│ │ └── test_us_property.py # US-PROPERTY-001~008
│ ├── client/
│ │ └── test_us_client.py # US-CLIENT-001~017
│ ├── org/
│ │ └── test_us_org.py
│ └── setting/
│ └── test_us_setting.py
└── e2e/ # E2E 测试(核心用户旅程)
├── conftest.py # E2E 专用 fixtureslive_server、page
├── test_journey_login.py
├── test_journey_property.py
├── test_journey_client.py
├── test_journey_permission.py
└── test_journey_onboarding.py
```
---
## 5. 多租户测试约定
这是 Fonrey 测试中最重要的约定。`django-tenants` 的 Schema 隔离在测试中必须正确处理,否则测试结果不可信。
### 5.1 核心原则
- **所有集成测试和单元测试**(涉及数据库的)必须在租户 Schema 上下文中执行
- 严禁在 `public` Schema 下直接操作业务数据
- 每个测试函数执行后,数据库状态自动回滚(`pytest-django``db` fixture 保证事务隔离)
- 禁止测试之间共享可变状态(禁止 `module``session` 级别的数据库 fixtures除非明确只读
### 5.2 租户 Fixture 规范
全局 `conftest.py` 必须提供以下标准 fixtures
```python
# tests/conftest.py规范示意非最终代码
@pytest.fixture(scope="session")
def tenant():
"""
创建一个测试租户session 级别,全程复用同一个 Schema
使用 django_tenants.test.client.TenantClient 配套使用。
"""
@pytest.fixture
def tenant_client(tenant):
"""
返回绑定到测试租户的 TenantClient 实例。
等价于 Django 的 Client(),但自动切换到租户 Schema。
所有集成测试的 HTTP 请求必须通过此 client 发出。
"""
@pytest.fixture
def staff_user(tenant):
"""普通员工用户,已完成登录态(含 Cookie/Session"""
@pytest.fixture
def admin_user(tenant):
"""系统管理员用户"""
@pytest.fixture
def authenticated_client(tenant_client, staff_user):
"""已登录状态的 TenantClient"""
```
### 5.3 禁止事项
- ❌ 禁止在测试中使用 Django 原生 `Client()`,必须使用 `TenantClient`
- ❌ 禁止在测试中手动 `SET search_path`,由 fixtures 统一管理
- ❌ 禁止跨租户数据访问断言(每个测试只能操作自己的租户数据)
---
## 6. 单元测试规范
### 6.1 适用范围
单元测试覆盖以下代码,**不依赖 HTTP 请求,速度要求 < 100ms/个**
| 目标代码 | 测试文件位置 |
|---------|------------|
| `core/encryption.py` | `tests/unit/test_encryption.py` |
| `core/models/base.py`软删除、ActiveManager | `tests/unit/test_soft_delete.py` |
| `apps/*/services/` 所有 service 函数 | `tests/unit/test_*_service.py` |
| `apps/*/tasks.py` Celery 任务 | `tests/unit/test_celery_tasks.py` |
| `core/cache.py` Redis key 工具函数 | `tests/unit/test_cache.py` |
### 6.2 factory_boy 规范
每个 Django Model 必须有对应的 Factory集中放在 `tests/factories/` 下:
- Factory 类名统一为 `{ModelName}Factory`
- 使用 `faker` 生成中文假数据(姓名、手机号、地址)
- 手机号字段必须使用未加密的明文值传入 factoryfactory 内部触发 Model 的加密逻辑)
- Factory 之间通过 `SubFactory` 表达依赖关系,禁止在 Factory 内部硬编码 ID
### 6.3 Celery 任务测试规范
所有 Celery 任务测试必须在同步模式下运行,在 `settings_test.py` 中配置:
```python
# tests/settings_test.py
CELERY_TASK_ALWAYS_EAGER = True # 任务同步执行
CELERY_TASK_EAGER_PROPAGATES = True # 同步模式下抛出真实异常
```
调用方式统一使用 `.apply()` 而非 `.delay()``.apply_async()`
```python
# 正确
result = my_task.apply(args=[...])
# 禁止在测试中使用
my_task.delay(...)
my_task.apply_async(...)
```
### 6.4 PII 加密测试要求
`test_encryption.py` 必须覆盖以下场景:
- 加密后的密文与明文不同
- 相同明文每次加密产生不同密文GCM nonce 随机性)
- 解密后的明文与原始明文完全一致
- 加密字段的 SHA-256 hash 索引值具有确定性(相同明文产生相同 hash
- 解密错误(篡改密文)抛出可识别异常
---
## 7. 集成测试规范
### 7.1 适用范围
集成测试覆盖完整的 HTTP 请求-响应链路,每个 P0 User Story 至少对应一个集成测试文件。
### 7.2 HTMX 请求约定
Fonrey 的 View 层分为两种响应模式,测试必须对应覆盖:
| 请求类型 | Header | 预期响应 |
|---------|--------|---------|
| 普通页面请求 | 无 | 完整 HTML`<html>`, `<head>`, `<body>` |
| HTMX 局部请求 | `HTTP_HX_REQUEST: true` | 局部 HTML 片段(不含完整页面结构) |
HTMX 请求在 `TenantClient` 中发送方式:
```python
# HTMX 局部请求(规范示意)
response = authenticated_client.get(
'/properties/',
HTTP_HX_REQUEST='true'
)
# 验证返回局部 HTML不含完整页面标签
assert '<html' not in response.content.decode()
assert response.status_code == 200
```
### 7.3 权限验证覆盖要求
每个受权限保护的 View集成测试必须覆盖以下场景缺一不可
1. **有权限用户**:返回 200响应内容符合预期
2. **无权限用户**:返回 403
3. **未登录用户**:返回 302 重定向到登录页
4. **数据域隔离**(如适用):只能看到自己权限范围内的数据
### 7.4 P0 User Story 测试覆盖映射
每个 User Story 的集成测试须覆盖其 **验收标准Acceptance Criteria** 中的所有条目:
| User Story 文件 | 集成测试文件 |
|----------------|------------|
| US-ACCOUNT-001~003 | `tests/integration/account/test_us_account.py` |
| US-PERMISSION-001~005 | `tests/integration/permission/test_us_permission.py` |
| US-COMPLEX-001~003 | `tests/integration/complex/test_us_complex.py` |
| US-PROPERTY-001~008 | `tests/integration/property/test_us_property.py` |
| US-CLIENT-001~017 | `tests/integration/client/test_us_client.py` |
| US-ORG-001~003 | `tests/integration/org/test_us_org.py` |
| US-SETTING-001 | `tests/integration/setting/test_us_setting.py` |
### 7.5 外部服务 Mock 规范
集成测试中必须 Mock 所有外部 I/O禁止真实调用
| 外部依赖 | Mock 方式 |
|---------|---------|
| Cloudflare R2 文件上传 | `pytest-mock` mock `boto3.client` |
| Redis | 使用 `fakeredis` 替代真实 Redis |
| 邮件发送 | Django `django.test.utils.override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')` |
| Celery 任务 | `CELERY_TASK_ALWAYS_EAGER=True`(见 §6.3 |
---
## 8. E2E 测试规范
### 8.1 适用范围与原则
E2E 测试成本高、速度慢,**只覆盖核心用户旅程,不追求全覆盖**。以下 5 条旅程为必须通过项,任意一条失败即视为阻塞级问题:
| # | 旅程名称 | 对应模块 |
|---|---------|---------|
| J-01 | 登录 → 进入首页 | 登录管理 |
| J-02 | 录入房源 → 上传照片 → 查看列表 | 房源管理 |
| J-03 | 录入客源 → 添加跟进记录 | 客源管理 |
| J-04 | 无权限员工访问受限页面 → 看到 403 提示 | 权限管理 |
| J-05 | 管理员创建员工 → 分配角色 → 新员工登录验证 | 组织人事 + 权限 |
### 8.2 Playwright 技术约定
- 浏览器:仅使用 Chromium与 Electron 内核一致)
- 运行模式CI 环境用 `headless=True`;本地调试可用 `headless=False`
- 等待策略:**禁止使用 `page.wait_for_timeout()`(固定等待)**,必须使用语义等待:
- `page.wait_for_url(pattern)` — 等待导航完成
- `expect(locator).to_be_visible()` — 等待元素出现
- `page.wait_for_load_state('networkidle')` — 等待 HTMX 请求完成
- 选择器优先级:`role` > `text` > `placeholder` > `data-testid` > CSS 选择器(可维护性从高到低)
- 断言使用 `expect()` 而非原生 `assert`,获得更清晰的错误输出
### 8.3 HTMX 页面的 E2E 注意事项
HTMX 局部更新后DOM 发生变化但页面 URL 可能不变。等待策略:
```python
# 触发 HTMX 请求后等待网络空闲HTMX 请求完成)
page.click('button:has-text("筛选")')
page.wait_for_load_state('networkidle')
# 再断言 DOM 内容
expect(page.locator('.property-list')).to_contain_text('...')
```
### 8.4 E2E 测试数据管理
- E2E 测试使用独立的测试租户(在 `tests/e2e/conftest.py` 中创建)
- 每次 E2E 测试套件运行前,重置测试租户数据至初始种子状态
- 禁止 E2E 测试依赖其他 E2E 测试的产出数据(每条旅程测试自行准备数据)
---
## 9. 测试配置文件
### 9.1 pytest.ini
```ini
[pytest]
DJANGO_SETTINGS_MODULE = tests.settings_test
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--tb=short
--strict-markers
-q
markers =
unit: 单元测试(不访问数据库)
integration: 集成测试(访问数据库,使用 TenantClient
e2e: E2E 测试(启动真实服务,需要浏览器)
slow: 耗时超过 5 秒的测试
```
### 9.2 tests/settings_test.py 关键配置
```python
# 继承主 settings覆盖以下配置
DATABASES = {
# 使用独立的测试数据库CI 中由环境变量注入)
}
# Celery 同步模式
CELERY_TASK_ALWAYS_EAGER = True
CELERY_TASK_EAGER_PROPAGATES = True
# 使用内存缓存(避免依赖真实 Redis
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
# 邮件使用内存后端
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
# 文件存储使用本地临时目录(非 R2
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
MEDIA_ROOT = '/tmp/fonrey_test_media/'
# 关闭密码哈希加速(测试中不需要高强度 hash
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
# 禁用 DEBUG贴近生产环境
DEBUG = False
```
---
## 10. CI 自动化运行
### 10.1 GitHub Actions 配置
每日凌晨 2 点自动运行全量测试套件,并在每次 push 到 `main` / `develop` 分支时触发:
```yaml
# .github/workflows/daily-test.yml
name: Daily Test Suite
on:
schedule:
- cron: '0 18 * * *' # UTC 18:00 = 北京时间次日 02:00
push:
branches: [main, develop]
jobs:
unit-and-integration:
name: Unit & Integration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: fonrey_test
POSTGRES_USER: fonrey
POSTGRES_PASSWORD: test_password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- run: pip install -r requirements/test.txt
- name: Run unit tests
run: pytest tests/unit -m unit --cov=core --cov=apps -q
- name: Run integration tests
run: pytest tests/integration -m integration -q
- name: Upload coverage report
uses: codecov/codecov-action@v4
e2e:
name: E2E Tests (Core Journeys)
runs-on: ubuntu-latest
needs: unit-and-integration
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- run: pip install -r requirements/test.txt
- run: playwright install chromium --with-deps
- name: Run E2E tests
run: pytest tests/e2e -m e2e -q
- name: Upload E2E screenshots on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-screenshots
path: tests/e2e/screenshots/
```
### 10.2 本地每日运行
开发机本地运行全量测试:
```bash
# scripts/daily_test.sh
#!/bin/bash
set -e
echo "=============================="
echo " Fonrey Daily Test Runner"
echo "=============================="
echo ""
echo "[1/3] 单元测试..."
pytest tests/unit -m unit -q --tb=short
echo ""
echo "[2/3] 集成测试..."
pytest tests/integration -m integration -q --tb=short
echo ""
echo "[3/3] E2E 核心旅程测试..."
pytest tests/e2e -m e2e -q --tb=short
echo ""
echo "[覆盖率报告]"
pytest tests/unit tests/integration \
--cov=core --cov=apps \
--cov-report=term-missing \
--cov-fail-under=70 \
-q --tb=no
echo ""
echo "=============================="
echo " All tests passed."
echo "=============================="
```
---
## 11. AI 辅助编码时的测试要求
在 Fonrey vibe coding 流程中,每次 AI 完成一个 User Story 的功能代码后,**必须同步产出对应测试**,不允许欠测试债。
### 11.1 每个 User Story 的测试产出清单
AI 完成功能代码后,须产出以下内容(缺一不可):
- [ ] 相关 Model 的 `factory_boy` Factory若尚未存在
- [ ] Service 层的单元测试(正常路径 + 至少 2 个边界/异常场景)
- [ ] View 层的集成测试(覆盖 PRD 验收标准中所有 AC 条目)
- [ ] 权限场景覆盖(有权限 / 无权限 / 未登录,见 §7.3
- [ ] HTMX 局部请求与完整页面请求分别测试(见 §7.2
### 11.2 触发 AI 生成测试的标准 Prompt 模板
```
基于刚才实现的 [US-XXX-NNN],请为其生成完整测试:
1. factory_boy 工厂(如尚未存在)
- 文件位置tests/factories/{app}_factory.py
- 使用 Faker 生成中文假数据
2. Service 层单元测试
- 文件位置tests/unit/test_{app}_service.py
- 覆盖正常路径 + 至少 2 个边界/异常场景
- 使用 pytest-mock mock 外部依赖
3. View 层集成测试
- 文件位置tests/integration/{app}/test_us_{module}.py
- 覆盖 PRD 中该 US 的所有 AC 条目
- 使用 TenantClient 发送请求
- HTMX 请求和普通请求分别覆盖
- 权限场景:有权限 / 无权限403 / 未登录302
所有测试须遵循 TECH_STACK/测试规范.md 中的约定。
租户 fixtures 从 tests/conftest.py 导入,不要重复定义。
```
### 11.3 测试失败时的修复流程
CI 测试失败后AI 修复流程:
1. 读取失败的测试输出(`--tb=short` 格式)
2. 定位失败原因(逻辑错误 / 数据错误 / 环境依赖问题)
3. **优先修复功能代码**(测试是需求的正式表达,不轻易修改测试)
4. 仅当测试本身有误(如 AC 理解错误)时才修改测试,并注明修改原因
5. 修复后本地重跑对应测试套件确认通过,再提交
---
## 12. 禁止项Do NOT
- ❌ 禁止使用 Django 原生 `Client()`,必须使用 `TenantClient`
- ❌ 禁止使用固定等待 `time.sleep()``page.wait_for_timeout()`
- ❌ 禁止测试直接调用真实外部服务R2、邮件、第三方 API
- ❌ 禁止测试之间共享可变数据(避免测试顺序依赖)
- ❌ 禁止在测试中硬编码 Tenant ID、UUID、时间戳
- ❌ 禁止 E2E 测试依赖其他 E2E 测试产出的数据
- ❌ 禁止跳过权限验证场景(无权限 / 未登录场景必须覆盖)
- ❌ 禁止在功能代码未完成时先写空测试(`pass` 占位)后忘记补全
---
## 13. 文档索引
| 文档 | 说明 |
|------|------|
| [`TECH_STACK.md`](./TECH_STACK.md) | 技术栈总纲 |
| [`登录管理技术方案.md`](./登录管理技术方案.md) | 登录模块技术细节 |
| [`权限管理系统技术方案.md`](./权限管理系统技术方案.md) | 权限模块技术细节 |
| [`../PRD/TASK.md`](../PRD/TASK.md) | P0 User Story 清单(测试覆盖基准) |
| [`../DATA_MODEL/DATA_MODEL.md`](../DATA_MODEL/DATA_MODEL.md) | 数据模型总览factory 设计参考) |

File diff suppressed because it is too large Load Diff

View File

@@ -873,17 +873,17 @@
</td>
<!-- 姓名 + 等级 + 活跃度 -->
<td class="px-4 py-2 align-middle min-w-[160px] max-w-[200px]">
<div class="flex flex-col gap-0.5">
<td class="px-4 py-2 align-middle min-w-[160px] max-w-[220px]">
<div class="flex flex-col gap-1">
<!-- 第一行:置顶标记 + 姓名 + 等级 -->
<div class="flex items-center gap-1.5">
<!-- 置顶标记 -->
<svg x-show="client.pinned" class="w-3 h-3 text-warning-600 shrink-0" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 2ZM10 15a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 15Z"/></svg>
<a href="#"
class="text-info-600 hover:underline font-medium text-sm truncate max-w-[160px]"
class="text-primary-600 hover:underline font-semibold text-base truncate max-w-[140px]"
x-text="client.name"></a>
<span class="text-[11px] text-neutral-400 font-medium shrink-0" x-text="client.gradeLabel"></span>
</div>
<!-- 活跃度标签 -->
<!-- 第二行:活跃度标签(强制独占一行) -->
<div class="flex items-center gap-1 flex-wrap">
<template x-for="tag in client.activityTags" :key="tag.key">
<span class="text-[11px] px-1.5 py-0.5 rounded-full font-medium"

View File

@@ -1,981 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>客源列表 · 私客 · Fonrey 房睿</title>
<meta name="viewport" content="width=1280">
<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'},
},
fontFamily: {
sans:['Inter','PingFang SC','Microsoft YaHei','sans-serif'],
},
}
}
}
</script>
<style>
body { font-family: 'Inter','PingFang SC','Microsoft YaHei',sans-serif; }
.tabular-nums { font-variant-numeric: tabular-nums; }
[x-cloak] { display: none !important; }
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-thumb{background:#CBD5E1;border-radius:4px}
::-webkit-scrollbar-thumb:hover{background:#94A3B8}
</style>
</head>
<body class="bg-neutral-50 text-neutral-700 text-sm antialiased">
<!-- ============ 顶部导航 ============ -->
<header class="fixed top-0 left-0 right-0 h-14 z-30 bg-primary-800 flex items-center justify-between">
<!-- 左区Logo -->
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white font-semibold text-sm">F</div>
<span class="font-semibold text-white text-base">Fonrey · 房睿</span>
</div>
<!-- 中区:主导航 + 搜索 -->
<div class="flex items-center gap-4 flex-1 px-2">
<nav class="flex items-center gap-1 text-sm">
<a href="#" class="px-3 py-1.5 rounded-md text-primary-100 hover:bg-primary-700 hover:text-white transition-colors">工作台</a>
<a href="#" class="px-3 py-1.5 rounded-md text-primary-100 hover:bg-primary-700 hover:text-white transition-colors">房源</a>
<a href="#" class="px-3 py-1.5 rounded-md bg-primary-600 text-white font-medium">客源</a>
<a href="#" class="px-3 py-1.5 rounded-md text-primary-100 hover:bg-primary-700 hover:text-white transition-colors">营销</a>
<a href="#" class="px-3 py-1.5 rounded-md text-primary-100 hover:bg-primary-700 hover:text-white transition-colors">交易</a>
<a href="#" class="px-3 py-1.5 rounded-md text-primary-100 hover:bg-primary-700 hover:text-white transition-colors">数据</a>
<a href="#" class="px-3 py-1.5 rounded-md text-primary-100 hover:bg-primary-700 hover:text-white transition-colors">人事</a>
<a href="#" class="px-3 py-1.5 rounded-md text-primary-100 hover:bg-primary-700 hover:text-white transition-colors">系统</a>
</nav>
<!-- 全局搜索 -->
<div class="max-w-xs w-full relative">
<svg class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-primary-300" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-4.3-4.3M11 19a8 8 0 1 1 0-16 8 8 0 0 1 0 16Z"/></svg>
<input type="text" placeholder="搜索房源 / 客户 / 楼盘 ⌘K"
class="w-full pl-9 pr-3 py-1.5 text-sm rounded-md bg-primary-700/60 border border-transparent text-white placeholder:text-primary-300 hover:bg-primary-700 focus:bg-white focus:text-neutral-700 focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20 focus:outline-none focus:placeholder:text-neutral-400 transition-all">
</div>
</div>
<!-- 右区:工具 -->
<div class="flex items-center gap-1 px-4 shrink-0">
<button class="relative p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md transition-colors">
<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>
<span class="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-danger-600"></span>
</button>
<button class="p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md transition-colors">
<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="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/></svg>
</button>
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
<div class="w-7 h-7 rounded-full bg-primary-600 text-white flex items-center justify-center text-xs font-semibold">WS</div>
<span class="text-sm font-medium text-primary-100">魏深</span>
</div>
</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 href="#" 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 href="#" class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">公客池</a>
<a href="#" class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">成交客</a>
<a href="#" class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">已删客源</a>
</nav>
</aside>
<!-- ============ 主内容区 ============ -->
<main x-data="clientListApp()" class="ml-60 pt-[72px] min-h-screen bg-neutral-50">
<div class="px-6 py-4">
<!-- ======== 一级 Tab 导航 ======== -->
<div class="flex items-center justify-between border-b border-neutral-200 bg-neutral-50 sticky top-14 z-20 -mx-6 px-6 pt-1">
<div class="flex items-center gap-0">
<a href="#" class="px-4 py-3 text-sm border-b-2 border-primary-600 text-primary-600 font-medium whitespace-nowrap">私客</a>
<a href="#" class="px-4 py-3 text-sm border-b-2 border-transparent text-neutral-500 hover:text-neutral-700 whitespace-nowrap transition-colors">资料客</a>
<a href="#" class="px-4 py-3 text-sm border-b-2 border-transparent text-neutral-500 hover:text-neutral-700 whitespace-nowrap transition-colors">营销客</a>
<a href="#" class="px-4 py-3 text-sm border-b-2 border-transparent text-neutral-500 hover:text-neutral-700 whitespace-nowrap transition-colors">成交客</a>
<a href="#" class="px-4 py-3 text-sm border-b-2 border-transparent text-neutral-500 hover:text-neutral-700 whitespace-nowrap transition-colors">公客</a>
</div>
<!-- 右侧操作区 -->
<div class="flex items-center gap-4 text-sm pb-1">
<span class="text-neutral-500">
私客与成交客重复:
<a href="#" class="text-info-600 hover:underline font-medium">3</a>
</span>
<span class="text-neutral-500">
私客与公客重复:
<a href="#" class="text-info-600 hover:underline font-medium">7</a>
</span>
<a href="#" class="text-neutral-500 hover:text-neutral-700 transition-colors">已删客源</a>
<a href="/clients/private/create/"
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg transition-colors">
<svg class="w-4 h-4" 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>
新增私客
</a>
</div>
</div>
<!-- ======== 二级 Tab 导航 ======== -->
<div class="mt-3 flex items-center gap-1 p-1 bg-neutral-100 rounded-lg w-fit">
<template x-for="tab in tabs" :key="tab.key">
<button
:class="activeTab === tab.key
? 'bg-white text-primary-700 shadow-sm font-semibold'
: 'text-neutral-600 hover:bg-white/60'"
class="px-4 py-1.5 text-sm rounded-md transition-all flex items-center gap-1"
@click="activeTab = tab.key">
<span x-text="tab.label"></span>
<span x-show="tab.count !== null"
class="bg-neutral-200 text-neutral-600 text-xs px-1.5 py-0.5 rounded-full tabular-nums"
x-text="tab.count"></span>
</button>
</template>
</div>
<!-- ======== 搜索 + 筛选区 ======== -->
<div class="bg-white rounded-lg border border-neutral-200 px-4 py-3 mt-3">
<!-- 搜索行 -->
<div class="flex items-center gap-3">
<!-- 范围选择器 -->
<select class="text-sm text-neutral-600 border border-neutral-300 rounded-lg px-3 py-2 bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 shrink-0">
<option>客户信息</option>
<option>客源编号</option>
<option>小区名称</option>
</select>
<!-- 搜索框 -->
<div class="relative flex-1 max-w-lg">
<svg class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-4.3-4.3M11 19a8 8 0 1 1 0-16 8 8 0 0 1 0 16Z"/></svg>
<input type="search"
placeholder="输入客源姓名 / 号码 / 号码后4位 / 客源编号 / 备注信息"
class="w-full pl-9 pr-10 py-2 border border-neutral-300 rounded-lg text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40">
<button class="absolute right-1 top-1/2 -translate-y-1/2 bg-primary-600 hover:bg-primary-700 text-white w-8 h-8 rounded-md flex items-center justify-center transition-colors">
<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="m21 21-4.3-4.3M11 19a8 8 0 1 1 0-16 8 8 0 0 1 0 16Z"/></svg>
</button>
</div>
<!-- 已存搜索 -->
<div x-data="{ open: false }" class="relative shrink-0">
<button @click="open = !open"
class="text-sm text-neutral-500 hover:text-neutral-700 flex items-center gap-1 transition-colors">
<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="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z"/></svg>
<span>3条已存搜索</span>
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg>
</button>
<div x-show="open" x-cloak @click.outside="open = false"
class="absolute left-0 top-full mt-1 w-64 bg-white shadow-lg border border-neutral-200 rounded-lg z-50 py-1">
<div class="px-3 py-2 text-xs text-neutral-400 border-b border-neutral-100">已存搜索条件</div>
<a href="#" class="flex items-center justify-between px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-50">
<span>求购·A级·宝山</span>
<button class="text-neutral-400 hover:text-danger-600 ml-2">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</a>
<a href="#" class="flex items-center justify-between px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-50">
<span>7日活跃·与我相关</span>
<button class="text-neutral-400 hover:text-danger-600 ml-2">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</a>
<a href="#" class="flex items-center justify-between px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-50">
<span>即将掉公·近30天录入</span>
<button class="text-neutral-400 hover:text-danger-600 ml-2">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</a>
<div class="px-3 py-2 border-t border-neutral-100">
<button class="text-xs text-primary-600 hover:underline">保存当前筛选条件</button>
</div>
</div>
</div>
<!-- 收起/展开筛选 -->
<button @click="showFilters = !showFilters"
class="ml-auto text-sm text-neutral-500 hover:text-primary-600 flex items-center gap-1 shrink-0 transition-colors">
<span x-text="showFilters ? '收起筛选' : '展开筛选'"></span>
<svg class="w-4 h-4 transition-transform" :class="showFilters ? 'rotate-180' : ''" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg>
</button>
</div>
<!-- 筛选区(可折叠) -->
<div x-show="showFilters"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 -translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-1"
class="mt-3 space-y-2.5 border-t border-neutral-100 pt-3">
<!-- 快捷筛选行 -->
<div class="flex items-center gap-5 text-sm">
<span class="text-neutral-400 text-xs w-6 shrink-0">常用</span>
<label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600 transition-colors">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> 即将掉公
</label>
<div class="flex items-center gap-1.5">
<span class="text-neutral-600">录入时间</span>
<select class="text-sm border-0 text-neutral-600 bg-transparent cursor-pointer focus:outline-none hover:text-primary-600 pr-4">
<option value="">不限</option>
<option>今天</option>
<option>最近7天</option>
<option>最近30天</option>
<option>自定义</option>
</select>
</div>
<label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600 transition-colors">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> 与我相关
<svg class="w-3.5 h-3.5 text-neutral-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"/></svg>
</label>
<label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600 transition-colors">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> 我部门相关
<svg class="w-3.5 h-3.5 text-neutral-400" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"/></svg>
</label>
</div>
<!-- 状态筛选行 -->
<div class="flex items-center flex-wrap gap-x-2 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-6 shrink-0">状态</span>
<template x-for="opt in statusOptions" :key="opt.value">
<button
:class="activeStatus === opt.value
? 'bg-primary-600 text-white border-primary-600'
: 'border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600'"
class="px-3 py-1 text-xs border rounded-md transition-colors"
@click="activeStatus = opt.value"
x-text="opt.label">
</button>
</template>
</div>
<!-- 需求类型筛选行 -->
<div class="flex items-center flex-wrap gap-x-2 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-6 shrink-0">需求</span>
<button class="px-3 py-1 text-xs bg-primary-600 text-white border border-primary-600 rounded-md">不限</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">二手</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">新房</button>
</div>
<!-- 等级筛选行 -->
<div class="flex items-center flex-wrap gap-x-2 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-6 shrink-0">等级</span>
<button class="px-3 py-1 text-xs bg-primary-600 text-white border border-primary-600 rounded-md">不限</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">A(急迫)</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">B(较强)</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">C(一般)</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">D(较弱)</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">E(暂不)</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">未填写</button>
</div>
<!-- 位置筛选行 -->
<div class="flex items-center flex-wrap gap-x-2 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-6 shrink-0">位置</span>
<button class="px-3 py-1 text-xs bg-primary-600 text-white border border-primary-600 rounded-md">不限</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">宝山</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">崇明</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">奉贤</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">虹口</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">黄浦</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">嘉定</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">金山</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">静安</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">闵行</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">浦东</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">普陀</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">青浦</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">松江</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">徐汇</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">杨浦</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">长宁</button>
</div>
<!-- 购价筛选行 -->
<div class="flex items-center flex-wrap gap-x-2 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-6 shrink-0">购价</span>
<button class="px-3 py-1 text-xs bg-primary-600 text-white border border-primary-600 rounded-md">不限</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">200万以下</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">200-300万</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">300-500万</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">500-800万</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">800-1000万</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">1000万以上</button>
<div class="flex items-center gap-1.5 ml-1">
<input type="number" placeholder="最小值" class="w-20 px-2 py-1 text-xs border border-neutral-300 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40">
<span class="text-neutral-400 text-xs">~</span>
<input type="number" placeholder="最大值" class="w-20 px-2 py-1 text-xs border border-neutral-300 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40">
<span class="text-xs text-neutral-500"></span>
</div>
</div>
<!-- 居室筛选行 -->
<div class="flex items-center flex-wrap gap-x-2 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-6 shrink-0">居室</span>
<button class="px-3 py-1 text-xs bg-primary-600 text-white border border-primary-600 rounded-md">不限</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">1居</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">2居</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">3居</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">4居</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">5居及以上</button>
<label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600 transition-colors ml-2">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> 是大价值
</label>
</div>
<!-- 更多筛选(展开/收起) -->
<div x-data="{ showMore: false }">
<button @click="showMore = !showMore"
class="flex items-center gap-1 text-xs text-neutral-500 hover:text-primary-600 transition-colors py-0.5">
<svg class="w-3.5 h-3.5 transition-transform" :class="showMore ? 'rotate-180' : ''" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg>
<span x-text="showMore ? '收起筛选' : '展开更多筛选'"></span>
</button>
<div x-show="showMore" x-cloak class="mt-2.5 space-y-2.5">
<!-- 相关方 -->
<div class="flex items-center flex-wrap gap-x-3 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-10 shrink-0">相关方</span>
<select class="text-sm border border-neutral-300 rounded-md px-2 py-1 bg-white focus:outline-none text-neutral-600">
<option>不限</option>
<option>首录人:我</option>
<option>归属人:我</option>
</select>
</div>
<!-- 委托日期 -->
<div class="flex items-center flex-wrap gap-x-3 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-10 shrink-0">委托日期</span>
<input type="date" class="text-sm border border-neutral-300 rounded-md px-2 py-1 bg-white focus:outline-none text-neutral-600">
<span class="text-neutral-400 text-xs">~</span>
<input type="date" class="text-sm border border-neutral-300 rounded-md px-2 py-1 bg-white focus:outline-none text-neutral-600">
</div>
<!-- 来源 -->
<div class="flex items-center flex-wrap gap-x-3 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-10 shrink-0">来源</span>
<select class="text-sm border border-neutral-300 rounded-md px-2 py-1 bg-white focus:outline-none text-neutral-600">
<option>不限</option>
<option>自然到访</option>
<option>网络推广</option>
<option>老客介绍</option>
<option>巧客力</option>
</select>
</div>
<!-- 活跃情况 -->
<div class="flex items-center flex-wrap gap-x-2 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-10 shrink-0">活跃</span>
<button class="px-3 py-1 text-xs bg-primary-600 text-white border border-primary-600 rounded-md">不限</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">新配房</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">7日活跃</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">30日活跃</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">即将过期</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">已暂缓</button>
</div>
<!-- 带看进度 -->
<div class="flex items-center flex-wrap gap-x-2 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-10 shrink-0">带看</span>
<button class="px-3 py-1 text-xs bg-primary-600 text-white border border-primary-600 rounded-md">不限</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">未带看</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">一看</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">二看</button>
<button class="px-3 py-1 text-xs border border-neutral-200 text-neutral-600 hover:border-primary-400 hover:text-primary-600 rounded-md transition-colors">复看</button>
</div>
<!-- 保护客 -->
<div class="flex items-center flex-wrap gap-x-5 gap-y-1.5 text-sm">
<span class="text-neutral-400 text-xs w-10 shrink-0">其他</span>
<label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600 transition-colors">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> 保护客
</label>
<label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600 transition-colors">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> 偏好新房
</label>
<label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600 transition-colors">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> 收藏客源
</label>
<label class="flex items-center gap-1.5 cursor-pointer text-neutral-600 hover:text-primary-600 transition-colors">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> 置顶客源
</label>
</div>
</div>
</div>
</div>
</div>
<!-- ======== 工具栏 ======== -->
<div class="flex items-center justify-between px-4 py-2.5 bg-white border border-neutral-200 rounded-lg mt-3">
<!-- 左侧:批量操作 + 计数 -->
<div class="flex items-center gap-2">
<button :disabled="selected.length === 0"
:class="selected.length > 0
? 'text-neutral-700 hover:bg-neutral-100 border-neutral-300 cursor-pointer'
: 'text-neutral-300 border-neutral-200 cursor-not-allowed'"
class="px-3 py-1.5 text-sm border rounded-md transition-colors">
修改相关方
</button>
<button :disabled="selected.length === 0"
:class="selected.length > 0
? 'text-neutral-700 hover:bg-neutral-100 border-neutral-300 cursor-pointer'
: 'text-neutral-300 border-neutral-200 cursor-not-allowed'"
class="px-3 py-1.5 text-sm border rounded-md transition-colors">
修改来源
</button>
<button :disabled="selected.length === 0"
:class="selected.length > 0
? 'text-danger-600 hover:bg-danger-50 border-danger-200 cursor-pointer'
: 'text-neutral-300 border-neutral-200 cursor-not-allowed'"
class="px-3 py-1.5 text-sm border rounded-md transition-colors">
删除客源
</button>
<button :disabled="selected.length === 0"
:class="selected.length > 0
? 'text-neutral-700 hover:bg-neutral-100 border-neutral-300 cursor-pointer'
: 'text-neutral-300 border-neutral-200 cursor-not-allowed'"
class="px-3 py-1.5 text-sm border rounded-md transition-colors">
合并客源
</button>
<span class="text-sm text-neutral-500 ml-2">
<strong class="text-neutral-800 tabular-nums">1,248</strong>
</span>
<span x-show="selected.length > 0" class="text-sm text-primary-600">
已选 <span x-text="selected.length" class="tabular-nums"></span>
</span>
</div>
<!-- 右侧:导出 + 自定义列 -->
<div class="flex items-center gap-2">
<button class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-neutral-600 border border-neutral-300 rounded-md hover:bg-neutral-50 transition-colors">
<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 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
导出
</button>
<button @click="showColumnModal = true"
class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-neutral-600 border border-neutral-300 rounded-md hover:bg-neutral-50 transition-colors">
<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="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"/></svg>
自定义列表
</button>
</div>
</div>
<!-- ======== 数据表格 ======== -->
<div id="client-list-container" class="rounded-lg border border-neutral-200 overflow-hidden bg-white mt-3">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-neutral-200">
<thead class="bg-neutral-50">
<tr>
<th class="w-10 px-4 py-3">
<input type="checkbox" id="select-all"
class="w-4 h-4 rounded accent-primary-600"
@change="toggleAll($event)">
</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap min-w-[160px]">姓名</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap w-20">状态</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap w-20">需求类型</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap min-w-[180px]">需求/解读</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap w-24">智能配房</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap min-w-[120px]">意向商圈/小区</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap min-w-[140px]">归属人</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap w-20">带看进度</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap w-[72px] cursor-pointer hover:bg-neutral-100 select-none">
带看次数
<svg class="inline w-3.5 h-3.5 text-neutral-300 ml-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"/></svg>
</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap w-24 cursor-pointer hover:bg-neutral-100 select-none">
委托日期
<svg class="inline w-3.5 h-3.5 text-neutral-300 ml-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"/></svg>
</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap w-24 cursor-pointer hover:bg-neutral-100 select-none text-primary-600">
最近时间
<svg class="inline w-3.5 h-3.5 text-primary-400 ml-0.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25 12 15.75 4.5 8.25"/></svg>
</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap w-16">操作</th>
</tr>
</thead>
<tbody id="client-table-body" class="divide-y divide-neutral-100 bg-white">
<!-- 数据行 1 -->
<tr class="hover:bg-neutral-50 transition-colors"
:class="selected.includes('c001') ? 'bg-primary-50 hover:bg-primary-100' : ''">
<td class="w-10 px-4 py-2">
<input type="checkbox" value="c001" class="w-4 h-4 rounded accent-primary-600" x-model="selected">
</td>
<td class="px-4 py-2 min-w-[160px]">
<div class="flex flex-col gap-0.5">
<a href="/clients/private/c001/" class="text-info-600 hover:underline font-medium text-sm truncate max-w-[160px]">王建国</a>
<div class="flex items-center gap-1 flex-wrap">
<span class="text-[11px] text-neutral-500">A(急迫)</span>
<span class="text-[11px] px-1.5 py-0.5 rounded-full font-medium bg-success-50 text-success-600">7日活跃</span>
<span class="text-[11px] px-1.5 py-0.5 rounded-full font-medium bg-info-50 text-info-600">新配房</span>
</div>
</div>
</td>
<td class="px-4 py-2">
<span class="bg-primary-50 text-primary-700 text-xs px-2 py-0.5 rounded-full">求购</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700">二手</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700 truncate block max-w-[200px]" title="550-600万100㎡-110㎡3居宝山">550-600万100㎡-110㎡3居宝山</span>
</td>
<td class="px-4 py-2">
<div class="flex items-center gap-1">
<span class="text-sm text-neutral-700 font-medium">8套</span>
<button class="text-neutral-400 hover:text-info-600 transition-colors" title="查看配房详情">
<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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"/></svg>
</button>
</div>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700 truncate block max-w-[160px]" title="宝山·顾村,大华锦绣华城">宝山·顾村,大华锦绣华城</span>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700">张伟-都市港湾店一组</span>
</td>
<td class="px-4 py-2">
<span class="bg-warning-50 text-warning-600 px-2 py-0.5 rounded-full text-xs">一看</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700 tabular-nums">3次</td>
<td class="px-4 py-2 text-sm text-neutral-600 tabular-nums">2026-03-15</td>
<td class="px-4 py-2 text-sm text-neutral-700 tabular-nums">今天</td>
<td class="px-3 py-2 text-center">
<button class="inline-flex items-center justify-center w-8 h-8 rounded-md text-primary-600 hover:bg-primary-50 hover:text-primary-700 transition-colors" title="拨号">
<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="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 0 1-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z"/></svg>
</button>
</td>
</tr>
<!-- 数据行 2 -->
<tr class="hover:bg-neutral-50 transition-colors"
:class="selected.includes('c002') ? 'bg-primary-50 hover:bg-primary-100' : ''">
<td class="w-10 px-4 py-2">
<input type="checkbox" value="c002" class="w-4 h-4 rounded accent-primary-600" x-model="selected">
</td>
<td class="px-4 py-2 min-w-[160px]">
<div class="flex flex-col gap-0.5">
<a href="/clients/private/c002/" class="text-info-600 hover:underline font-medium text-sm truncate max-w-[160px]">李晓敏</a>
<div class="flex items-center gap-1 flex-wrap">
<span class="text-[11px] text-neutral-500">B(较强)</span>
<span class="text-[11px] px-1.5 py-0.5 rounded-full font-medium bg-purple-50 text-purple-600">营销客</span>
</div>
</div>
</td>
<td class="px-4 py-2">
<span class="bg-info-50 text-info-600 text-xs px-2 py-0.5 rounded-full">求租</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700">租房</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700 truncate block max-w-[200px]" title="4000-6000元/月60㎡-80㎡2居静安">4000-6000元/月60㎡-80㎡2居静安</span>
</td>
<td class="px-4 py-2">
<div class="flex items-center gap-1">
<span class="text-sm text-neutral-700 font-medium">12套</span>
<button class="text-neutral-400 hover:text-info-600 transition-colors">
<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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"/></svg>
</button>
</div>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700 truncate block max-w-[160px]">静安·南京西路</span>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700">陈丽-静安旗舰店二组</span>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-500">未带看</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700 tabular-nums">0次</td>
<td class="px-4 py-2 text-sm text-neutral-600 tabular-nums">2026-04-10</td>
<td class="px-4 py-2 text-sm text-neutral-700 tabular-nums">3天前</td>
<td class="px-3 py-2 text-center">
<button class="inline-flex items-center justify-center w-8 h-8 rounded-md text-primary-600 hover:bg-primary-50 hover:text-primary-700 transition-colors" title="拨号">
<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="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 0 1-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z"/></svg>
</button>
</td>
</tr>
<!-- 数据行 3 -->
<tr class="hover:bg-neutral-50 transition-colors"
:class="selected.includes('c003') ? 'bg-primary-50 hover:bg-primary-100' : ''">
<td class="w-10 px-4 py-2">
<input type="checkbox" value="c003" class="w-4 h-4 rounded accent-primary-600" x-model="selected">
</td>
<td class="px-4 py-2 min-w-[160px]">
<div class="flex flex-col gap-0.5">
<a href="/clients/private/c003/" class="text-info-600 hover:underline font-medium text-sm truncate max-w-[160px]">赵志远</a>
<div class="flex items-center gap-1 flex-wrap">
<span class="text-[11px] text-neutral-500">A(急迫)</span>
<span class="text-[11px] px-1.5 py-0.5 rounded-full font-medium bg-warning-50 text-warning-600">即将过期</span>
</div>
</div>
</td>
<td class="px-4 py-2">
<span class="bg-warning-50 text-warning-600 text-xs px-2 py-0.5 rounded-full">租购</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700">二手</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700 truncate block max-w-[200px]" title="800-1000万120㎡以上4居浦东">800-1000万120㎡以上4居浦东</span>
</td>
<td class="px-4 py-2">
<div class="flex items-center gap-1">
<span class="text-sm text-neutral-700 font-medium">5套</span>
<button class="text-neutral-400 hover:text-info-600 transition-colors">
<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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"/></svg>
</button>
</div>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700 truncate block max-w-[160px]">浦东·陆家嘴,世纪公园</span>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700">刘洋-浦东总店三组</span>
</td>
<td class="px-4 py-2">
<span class="bg-info-50 text-info-600 px-2 py-0.5 rounded-full text-xs">二看</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700 tabular-nums">5次</td>
<td class="px-4 py-2 text-sm text-neutral-600 tabular-nums">2026-01-20</td>
<td class="px-4 py-2 text-sm text-neutral-700 tabular-nums">8天前</td>
<td class="px-3 py-2 text-center">
<button class="inline-flex items-center justify-center w-8 h-8 rounded-md text-primary-600 hover:bg-primary-50 hover:text-primary-700 transition-colors" title="拨号">
<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="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 0 1-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z"/></svg>
</button>
</td>
</tr>
<!-- 数据行 4 -->
<tr class="hover:bg-neutral-50 transition-colors"
:class="selected.includes('c004') ? 'bg-primary-50 hover:bg-primary-100' : ''">
<td class="w-10 px-4 py-2">
<input type="checkbox" value="c004" class="w-4 h-4 rounded accent-primary-600" x-model="selected">
</td>
<td class="px-4 py-2 min-w-[160px]">
<div class="flex flex-col gap-0.5">
<a href="/clients/private/c004/" class="text-info-600 hover:underline font-medium text-sm truncate max-w-[160px]">孙美玲</a>
<div class="flex items-center gap-1 flex-wrap">
<span class="text-[11px] text-neutral-500">C(一般)</span>
<span class="text-[11px] px-1.5 py-0.5 rounded-full font-medium bg-neutral-100 text-neutral-500">暂缓</span>
</div>
</div>
</td>
<td class="px-4 py-2">
<span class="bg-neutral-100 text-neutral-500 text-xs px-2 py-0.5 rounded-full">暂缓</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700">二手</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-500 truncate block max-w-[200px]">-</span>
</td>
<td class="px-4 py-2">
<div class="flex items-center gap-1">
<span class="text-sm text-neutral-500">0套</span>
</div>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-500">-</span>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700">魏深-都市港湾店一组</span>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-500">未带看</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700 tabular-nums">0次</td>
<td class="px-4 py-2 text-sm text-neutral-600 tabular-nums">2026-04-01</td>
<td class="px-4 py-2 text-sm text-danger-600 tabular-nums">32天前</td>
<td class="px-3 py-2 text-center">
<button class="inline-flex items-center justify-center w-8 h-8 rounded-md text-primary-600 hover:bg-primary-50 hover:text-primary-700 transition-colors" title="拨号">
<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="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 0 1-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z"/></svg>
</button>
</td>
</tr>
<!-- 数据行 5 -->
<tr class="hover:bg-neutral-50 transition-colors"
:class="selected.includes('c005') ? 'bg-primary-50 hover:bg-primary-100' : ''">
<td class="w-10 px-4 py-2">
<input type="checkbox" value="c005" class="w-4 h-4 rounded accent-primary-600" x-model="selected">
</td>
<td class="px-4 py-2 min-w-[160px]">
<div class="flex flex-col gap-0.5">
<a href="/clients/private/c005/" class="text-info-600 hover:underline font-medium text-sm truncate max-w-[160px]">陈建华</a>
<div class="flex items-center gap-1 flex-wrap">
<span class="text-[11px] text-neutral-500">B(较强)</span>
<span class="text-[11px] px-1.5 py-0.5 rounded-full font-medium bg-orange-50 text-orange-600">销售客</span>
</div>
</div>
</td>
<td class="px-4 py-2">
<span class="bg-primary-50 text-primary-700 text-xs px-2 py-0.5 rounded-full">求购</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700">新房</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700 truncate block max-w-[200px]" title="300-400万90㎡-110㎡3居嘉定/青浦">300-400万90㎡-110㎡3居嘉定/青浦</span>
</td>
<td class="px-4 py-2">
<div class="flex items-center gap-1">
<span class="text-sm text-neutral-700 font-medium">23套</span>
<button class="text-neutral-400 hover:text-info-600 transition-colors">
<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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"/></svg>
</button>
</div>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700 truncate block max-w-[160px]">嘉定·新城,远香湖</span>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700">魏深-都市港湾店一组</span>
</td>
<td class="px-4 py-2">
<span class="bg-success-50 text-success-600 px-2 py-0.5 rounded-full text-xs">复看</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700 tabular-nums">7次</td>
<td class="px-4 py-2 text-sm text-neutral-600 tabular-nums">2026-02-28</td>
<td class="px-4 py-2 text-sm text-neutral-700 tabular-nums">1天前</td>
<td class="px-3 py-2 text-center">
<button class="inline-flex items-center justify-center w-8 h-8 rounded-md text-primary-600 hover:bg-primary-50 hover:text-primary-700 transition-colors" title="拨号">
<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="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 0 1-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z"/></svg>
</button>
</td>
</tr>
<!-- 数据行 6 -->
<tr class="hover:bg-neutral-50 transition-colors"
:class="selected.includes('c006') ? 'bg-primary-50 hover:bg-primary-100' : ''">
<td class="w-10 px-4 py-2">
<input type="checkbox" value="c006" class="w-4 h-4 rounded accent-primary-600" x-model="selected">
</td>
<td class="px-4 py-2 min-w-[160px]">
<div class="flex flex-col gap-0.5">
<a href="/clients/private/c006/" class="text-info-600 hover:underline font-medium text-sm truncate max-w-[160px]">周小燕</a>
<div class="flex items-center gap-1 flex-wrap">
<span class="text-[11px] text-neutral-500">D(较弱)</span>
<span class="text-[11px] px-1.5 py-0.5 rounded-full font-medium bg-danger-50 text-danger-600">无效</span>
</div>
</div>
</td>
<td class="px-4 py-2">
<span class="bg-primary-50 text-primary-700 text-xs px-2 py-0.5 rounded-full">求购</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700">二手</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700 truncate block max-w-[200px]" title="200-300万80㎡-100㎡2居普陀/长宁">200-300万80㎡-100㎡2居普陀/长宁</span>
</td>
<td class="px-4 py-2">
<div class="flex items-center gap-1">
<span class="text-sm text-neutral-700 font-medium">2套</span>
<button class="text-neutral-400 hover:text-info-600 transition-colors">
<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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"/></svg>
</button>
</div>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700 truncate block max-w-[160px]">普陀·长风</span>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-700">王芳-普陀新村店</span>
</td>
<td class="px-4 py-2">
<span class="text-sm text-neutral-500">未带看</span>
</td>
<td class="px-4 py-2 text-sm text-neutral-700 tabular-nums">1次</td>
<td class="px-4 py-2 text-sm text-neutral-600 tabular-nums">2025-12-01</td>
<td class="px-4 py-2 text-sm text-danger-600 tabular-nums">45天前</td>
<td class="px-3 py-2 text-center">
<button class="inline-flex items-center justify-center w-8 h-8 rounded-md text-primary-600 hover:bg-primary-50 hover:text-primary-700 transition-colors" title="拨号">
<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="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 0 1-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z"/></svg>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ======== 分页栏 ======== -->
<div class="mt-4 flex items-center justify-between px-1">
<!-- 左侧:总条数 -->
<div class="text-sm text-neutral-500">
<span class="font-medium text-neutral-800 tabular-nums">1,248</span>
</div>
<!-- 中间:页码 -->
<div class="flex items-center gap-1">
<button class="flex items-center gap-1 px-2.5 py-1.5 text-sm text-neutral-600 hover:bg-neutral-100 rounded-md transition-colors disabled:opacity-40 disabled:cursor-not-allowed">
<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="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
上一页
</button>
<button class="w-8 h-8 flex items-center justify-center text-sm bg-primary-600 text-white rounded-md font-medium tabular-nums">1</button>
<button class="w-8 h-8 flex items-center justify-center text-sm text-neutral-600 hover:bg-neutral-100 rounded-md transition-colors tabular-nums">2</button>
<button class="w-8 h-8 flex items-center justify-center text-sm text-neutral-600 hover:bg-neutral-100 rounded-md transition-colors tabular-nums">3</button>
<button class="w-8 h-8 flex items-center justify-center text-sm text-neutral-600 hover:bg-neutral-100 rounded-md transition-colors tabular-nums">4</button>
<button class="w-8 h-8 flex items-center justify-center text-sm text-neutral-600 hover:bg-neutral-100 rounded-md transition-colors tabular-nums">5</button>
<span class="w-8 h-8 flex items-center justify-center text-sm text-neutral-400"></span>
<button class="w-8 h-8 flex items-center justify-center text-sm text-neutral-600 hover:bg-neutral-100 rounded-md transition-colors tabular-nums">63</button>
<button class="flex items-center gap-1 px-2.5 py-1.5 text-sm text-neutral-600 hover:bg-neutral-100 rounded-md transition-colors">
下一页
<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="m8.25 4.5 7.5 7.5-7.5 7.5"/></svg>
</button>
</div>
<!-- 右侧:每页条数 + 跳页 -->
<div class="flex items-center gap-3 text-sm">
<select class="border border-neutral-300 rounded-md px-2 py-1.5 text-sm text-neutral-600 bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40">
<option>20条/页</option>
<option>50条/页</option>
<option>100条/页</option>
</select>
<div class="flex items-center gap-1.5 text-neutral-500">
跳至
<input type="number" min="1" max="63"
class="w-14 px-2 py-1.5 border border-neutral-300 rounded-md text-center text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 tabular-nums">
<button class="px-2.5 py-1.5 text-sm border border-neutral-300 rounded-md text-neutral-600 hover:bg-neutral-50 transition-colors">确定</button>
</div>
</div>
</div>
</div>
</div><!-- /px-6 py-4 -->
<!-- ======== 自定义列弹窗 ======== -->
<div x-show="showColumnModal" x-cloak
class="fixed inset-0 z-50 flex items-center justify-center p-4"
x-transition:enter="ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
<!-- Overlay -->
<div class="absolute inset-0 bg-neutral-900/40 backdrop-blur-sm" @click="showColumnModal = false"></div>
<!-- Modal -->
<div class="relative bg-white rounded-xl shadow-xl w-full max-w-2xl flex flex-col max-h-[80vh]"
x-transition:enter="ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95 translate-y-2"
x-transition:enter-end="opacity-100 scale-100 translate-y-0">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200">
<h2 class="text-base font-semibold text-neutral-800">自定义信息</h2>
<button @click="showColumnModal = false" class="text-neutral-400 hover:text-neutral-600 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
<!-- Body -->
<div class="flex flex-1 overflow-hidden">
<!-- 左栏:未选信息 -->
<div class="w-1/2 border-r border-neutral-200 overflow-y-auto">
<div class="px-4 py-3 text-xs font-semibold text-neutral-500 uppercase tracking-wide border-b border-neutral-100">未选信息</div>
<div class="p-3 space-y-1">
<label class="flex items-center gap-2.5 px-2 py-2 rounded-md hover:bg-neutral-50 cursor-pointer">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> <span class="text-sm text-neutral-700">录入日期</span>
</label>
<label class="flex items-center gap-2.5 px-2 py-2 rounded-md hover:bg-neutral-50 cursor-pointer">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> <span class="text-sm text-neutral-700">最近通话日期</span>
</label>
<label class="flex items-center gap-2.5 px-2 py-2 rounded-md hover:bg-neutral-50 cursor-pointer">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> <span class="text-sm text-neutral-700">用途</span>
</label>
<label class="flex items-center gap-2.5 px-2 py-2 rounded-md hover:bg-neutral-50 cursor-pointer">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> <span class="text-sm text-neutral-700">来源</span>
</label>
<label class="flex items-center gap-2.5 px-2 py-2 rounded-md hover:bg-neutral-50 cursor-pointer">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> <span class="text-sm text-neutral-700">客源编号</span>
</label>
<label class="flex items-center gap-2.5 px-2 py-2 rounded-md hover:bg-neutral-50 cursor-pointer">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> <span class="text-sm text-neutral-700">首录人</span>
</label>
<label class="flex items-center gap-2.5 px-2 py-2 rounded-md hover:bg-neutral-50 cursor-pointer">
<input type="checkbox" class="w-4 h-4 rounded accent-primary-600"> <span class="text-sm text-neutral-700">成交人</span>
</label>
</div>
</div>
<!-- 右栏:已选信息 -->
<div class="w-1/2 overflow-y-auto">
<div class="px-4 py-3 text-xs font-semibold text-neutral-500 uppercase tracking-wide border-b border-neutral-100">已选信息</div>
<div class="p-3 space-y-1">
<!-- 固定字段(不可删) -->
<div class="flex items-center gap-2 px-2 py-2 rounded-md bg-neutral-50">
<svg class="w-4 h-4 text-neutral-300 cursor-grab" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 5.25h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5"/></svg>
<span class="text-sm text-neutral-700 flex-1">姓名</span>
<svg class="w-3.5 h-3.5 text-neutral-300" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"/></svg>
</div>
<!-- 可删字段 -->
<template x-for="(col, idx) in selectedColumns" :key="col">
<div class="flex items-center gap-2 px-2 py-2 rounded-md hover:bg-neutral-50 group">
<svg class="w-4 h-4 text-neutral-300 cursor-grab group-hover:text-neutral-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 5.25h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5"/></svg>
<span class="text-sm text-neutral-700 flex-1" x-text="col"></span>
<button @click="selectedColumns.splice(idx, 1)" class="text-neutral-400 hover:text-danger-600 transition-colors opacity-0 group-hover:opacity-100">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
</template>
</div>
<p class="px-5 pb-3 text-xs text-neutral-400">提示:拖拽可调整展示顺序</p>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-between px-6 py-4 border-t border-neutral-200">
<button class="text-sm text-neutral-500 hover:text-neutral-700 transition-colors">恢复默认</button>
<div class="flex items-center gap-3">
<button @click="showColumnModal = false" class="px-4 py-2 text-sm text-neutral-600 border border-neutral-300 rounded-lg hover:bg-neutral-50 transition-colors">取消</button>
<button @click="showColumnModal = false" class="px-4 py-2 text-sm font-medium bg-primary-600 hover:bg-primary-700 text-white rounded-lg transition-colors">确定</button>
</div>
</div>
</div>
</div><!-- /modal -->
</main><!-- /clientListApp -->
<script>
function clientListApp() {
return {
// 二级 Tab
activeTab: 'all',
tabs: [
{ key: 'buying', label: '求购', count: 913 },
{ key: 'renting', label: '求租', count: 187 },
{ key: 'suspended', label: '暂缓', count: null },
{ key: 'all', label: '全部私客', count: null },
],
// 筛选区展开
showFilters: true,
// 状态筛选选项
activeStatus: '',
statusOptions: [
{ value: '', label: '不限' },
{ value: 'buying', label: '求购' },
{ value: 'buy_or_rent', label: '租购' },
],
// 表格勾选
selected: [],
toggleAll(event) {
if (event.target.checked) {
this.selected = ['c001', 'c002', 'c003', 'c004', 'c005', 'c006'];
} else {
this.selected = [];
}
},
// 自定义列弹窗
showColumnModal: false,
selectedColumns: ['状态', '需求类型', '需求/解读', '智能配房', '意向商圈/小区', '归属人', '带看进度', '带看次数', '委托日期', '最近时间'],
}
}
</script>
</body>
</html>

View File

@@ -737,21 +737,21 @@ HTMX loading 触发:在 `<div id="client-list-container">` 添加 `hx-indicato
表头行:`<thead class="bg-neutral-50">`,列头单元格统一样式:`px-4 py-3 text-left text-xs font-semibold text-neutral-500 uppercase tracking-wide whitespace-nowrap`
数据行:`<tr class="hover:bg-neutral-50 transition-colors" style="height:56px">`
| # | 列名 | 数据字段 | 列宽 | 对齐 | 可排序 | 特殊渲染说明 |
|---|---|---|---|---|---|---|
| 1 | (复选框) | — | `w-10`40px`px-4` | 居中 | 否 | 全选:表头 `<input type="checkbox" id="select-all">`;单选:行内 `<input type="checkbox" x-model="selected" :value="client.id">` |
| 2 | 姓名 | `contact_name` + `grade_display` + 活跃度标签 | `min-w-[160px] max-w-[200px]` | 左对齐 | 否 | 蓝色链接 `text-info-600 hover:underline`;下方渲染 Grade Badge + 活跃度 Tag见 §3.2 |
| 3 | 状态 | `status_display` | `w-20`80px | 左对齐 | 否 | Status Badge见 §3.2 |
| 4 | 需求类型 | `requirement_type_display` | `w-20`80px | 左对齐 | 否 | 纯文字:二手 / 新房 / 租房 |
| 5 | 需求/解读 | `budget_area_display` | `min-w-[180px]` | 左对齐 | 否 | 截断 `truncate`Tooltip 展示完整内容(`title` 属性) |
| 6 | 智能配房 | `match_count` | `w-24`96px | 左对齐 | 否 | `N套` + Heroicon `information-circle``w-4 h-4 text-neutral-400 ml-1 cursor-pointer`);点击弹出配房预览 Popover |
| 7 | 意向商圈/小区 | `intent_location_display` | `min-w-[120px]` | 左对齐 | 否 | 多值逗号分隔;`-` 表示未填;截断 `truncate max-w-[160px]` |
| 8 | 归属人 | `owner_display` | `min-w-[140px]` | 左对齐 | 否 | 格式:`姓名-门店组``text-sm text-neutral-700` |
| 9 | 带看进度 | `viewing_progress_display` | `w-20`80px | 左对齐 | 否 | 「未带看」灰色文字;「一看」`bg-warning-50 text-warning-600 px-2 py-0.5 rounded-full text-xs`;「二看」`bg-info-50 text-info-600`;「复看」`bg-success-50 text-success-600` |
| 10 | 带看次数 | `viewing_count` | `w-[72px]` | 左对齐 | 是 | `N次`;点击列头触发 HTMX 排序 |
| 11 | 委托日期 | `commission_date` | `w-24`96px | 左对齐 | 是 | `YYYY-MM-DD`;未填显示 `-` |
| 12 | 最近时间 | `last_follow_display` / `last_contact_display` | `w-24`96px | 左对齐 | 是(默认降序) | `N天前` / `今天`;超过 30 天字色 `text-danger-600` |
| 13 | 操作 | — | `w-16`64px | 居中 | 否 | 见 §3.3 |
| # | 列名 | 数据字段 | 列宽 | 对齐 | 可排序 | 特殊渲染说明 |
| --- | ------- | ---------------------------------------------- | ----------------------------- | --- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | (复选框) | — | `w-10`40px`px-4` | 居中 | 否 | 全选:表头 `<input type="checkbox" id="select-all">`;单选:行内 `<input type="checkbox" x-model="selected" :value="client.id">` |
| 2 | 姓名 | `contact_name` + `grade_display` + 活跃度标签 | `min-w-[160px] max-w-[200px]` | 左对齐 | 否 | 蓝色链接 `text-primary-600 hover:underline`;下方渲染 Grade Badge + 活跃度 Tag见 §3.2 |
| 3 | 状态 | `status_display` | `w-20`80px | 左对齐 | 否 | Status Badge见 §3.2 |
| 4 | 需求类型 | `requirement_type_display` | `w-20`80px | 左对齐 | 否 | 纯文字:二手 / 新房 / 租房 |
| 5 | 需求/解读 | `budget_area_display` | `min-w-[180px]` | 左对齐 | 否 | 截断 `truncate`Tooltip 展示完整内容(`title` 属性) |
| 6 | 智能配房 | `match_count` | `w-24`96px | 左对齐 | 否 | `N套` + Heroicon `information-circle``w-4 h-4 text-neutral-400 ml-1 cursor-pointer`);点击弹出配房预览 Popover |
| 7 | 意向商圈/小区 | `intent_location_display` | `min-w-[120px]` | 左对齐 | 否 | 多值逗号分隔;`-` 表示未填;截断 `truncate max-w-[160px]` |
| 8 | 归属人 | `owner_display` | `min-w-[140px]` | 左对齐 | 否 | 格式:`姓名-门店组``text-sm text-neutral-700` |
| 9 | 带看进度 | `viewing_progress_display` | `w-20`80px | 左对齐 | 否 | 「未带看」灰色文字;「一看」`bg-warning-50 text-warning-600 px-2 py-0.5 rounded-full text-xs`;「二看」`bg-info-50 text-info-600`;「复看」`bg-success-50 text-success-600` |
| 10 | 带看次数 | `viewing_count` | `w-[72px]` | 左对齐 | 是 | `N次`;点击列头触发 HTMX 排序 |
| 11 | 委托日期 | `commission_date` | `w-24`96px | 左对齐 | 是 | `YYYY-MM-DD`;未填显示 `-` |
| 12 | 最近时间 | `last_follow_display` / `last_contact_display` | `w-24`96px | 左对齐 | 是(默认降序) | `N天前` / `今天`;超过 30 天字色 `text-danger-600` |
| 13 | 操作 | — | `w-16`64px | 居中 | 否 | 见 §3.3 |
> **自定义列**P1 🟡用户通过「自定义列表」弹窗§4.1)选择显示字段后,后端将用户配置存入 `UserColumnPreference`Django 模板根据配置动态渲染列头和列单元格。可选字段见截图 `客源列表-自定义字段.png`:录入日期、最近通话日期、用途、来源、客源编号、首录人、成交人等。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -622,6 +622,22 @@ Fonrey 的核心场景,规范必须严格执行。
| 逾期 / 紧急 / 冻结 | danger | `bg-danger-50 text-danger-600` |
| 暂缓 | 浅灰 + 斜体 | `bg-neutral-100 text-neutral-600 italic` |
#### 3.7.2 交易类型标签色板
交易类型标签(买卖 / 租赁 / 租售)属于**分类标签**不表达状态语义禁止使用语义色danger / info solid
**视觉层级要求**:交易类型标签须比同行副标签(满五、独家等)**更大、更醒目**
- 交易类型标签:`text-xs`12px+ `px-2 py-0.5` + `font-semibold` + 较深底色
- 副标签(房源属性 Tag`text-[10px]` + `px-1.5 py-0.5` + `font-medium` + 极淡底色
| 交易类型 | 色系 | Tailwind 类 | 设计理由 |
|---|---|---|---|
| 买卖 | primaryTeal | `bg-primary-200 text-primary-800` | 核心出售业务,用品牌主色强调 |
| 租赁 | warningAmber | `bg-warning-200 text-warning-800` | 与买卖形成色相区分,暖色低调 |
| 租售 | neutral | `bg-neutral-300 text-neutral-800` | 兼含两种类型,中性色避免歧义 |
> **禁止**对交易类型标签使用 Solid 样式(`bg-{color}-600 text-white`),该样式仅用于极少数强调性状态(如"紧急")。
#### 3.7.2 标准片段
```html

View File

@@ -42,6 +42,7 @@
- TECH_STACK`Project/fonrey/TECH_STACK/TECH_STACK.md`
- 登录管理技术方案:`Project/fonrey/TECH_STACK/登录管理技术方案.md`
- 权限管理技术方案:`Project/fonrey/TECH_STACK/登录管理技术方案.md`
- 测试规范:`Project/fonrey/TECH_STACK/测试规范`
**数据模型**
- DATA_MODEL`Project/fonrey/DATA_MODEL/DATA_MODEL.md`
@@ -58,11 +59,21 @@
- 模块UI Design:
- `Project/fonrey/UI_DESIGN/客源列表_UI.md`
- `Project/fonrey/UI_DESIGN/客源详情_UI.md`
- `Project/fonrey/UI_DESIGN/客源管理/新增客源_UI`
- `Project/fonrey/UI_DESIGN/客源管理/编辑客源_UI`
- `Project/fonrey/UI_DESIGN/房源管理/房源列表_UI`
**UI 原型页面**
- UI SYSTEM: `Project/fonrey/UI_SYSTEM/preview`
- UI SYSTEM: `Project/fonrey/UI_DESIGN/preview.html`
- 客源列表:`Project/fonrey/UI_DESIGN/客源列表_UI.html`
- 客源详情:`Project/fonrey/UI_DESIGN/客源详情_UI.html`
- 编辑客源:`Project/fonrey/UI_DESIGN/编辑客源_UI.html`
- 新增客源:`Project/fonrey/UI_DESIGN/新增客源_UI.html`
- 房源列表:`Project/fonrey/UI_DESIGN/房源列表_UI.html`
**任务汇总**
- TASK`Project/fonrey/PRD/TASK.md`
**其他参考文档**

View File

@@ -0,0 +1,115 @@
你是一个 B2B SaaS 项目的产品/技术项目经理。你的任务是根据下方的 PRD.md及PRD_MPV.md 内容,生成一份完整的 TASK.md 项目任务看板文件。
## 任务说明:
根据客源的PRD文档生成具体能落地的项目实现TASK列表
## 输入材料
- 模块PRD文档`Project/fonrey/PRD/客源管理/客源管理模块PRD.md`
- 模块DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- TECH_STACK文档`Project/fonrey/TECH_STACK/TECH_STACK.md`
- UI_SYSTEM文档`Project/fonrey/UI_SYSTEM/UI_SYSTEM.md`
- `Project/fonrey/UI_DESIGN/客源管理/新增客源_UI.md`
- `Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md`
- `Project/fonrey/UI_DESIGN/客源管理/编辑客源_UI.md`
- `Project/fonrey/UI_DESIGN/客源管理/客源详情_UI.md`
- - 模块PRD)MPV文档`Project/fonrey/PRD/PRD_MVP.md`
## 输出要求
**输出文档**`Project/fonrey/PRD/TASK.md`
### 文件结构规则
按以下 Phase 划分,严格对应 PRD 优先级:
- **Phase 1 - MVPP0上线前必须完成** → PRD 中标注 P0 的功能
- **Phase 2 - 增强功能P1MVP 后第一迭代)** → PRD 中标注 P1 的功能
- **Phase 3 - 路线图功能P2已规划未排期** → PRD 中标注 P2 的功能
- **Phase 4 - 明确不做Out of Scope** → PRD 第 3 节"非目标"中的功能,仅列出条目作为备忘,不写 task 详情
### 模块顺序(按 PRD 顺序)
1. 用户登录
2. 楼盘管理
3. 房源管理
4. 客源管理
5. 组织人事
6. 权限管理
7. 系统配置
8. 系统管理(运营后台)
9. 客户端发布
**请在项目目录里寻找匹配的文档填入下面具体的任务列表**
### 每条 Task 格式(严格遵守)
```
#### [模块中文名]
- [ ] US-XXX [动词开头的功能描述,主语为"经纪人/店长/管理员/系统"]
- 参考PRD文档`Project/fonrey/PRD/[模块名]/[模块名]模块PRD.md` - [对应 Story 或功能点名称]
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_[模块英文名].md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/[模块名]/[功能名]_UI.md`
- 参考UI静态原型页面`Project/fonrey/UI_DESIGN/[模块名]/[功能名]_UI.html`
- 验收标准:[从 PRD 功能描述提炼 2-4 条可测试的验收标准,用分号分隔]
```
### US 编号规则
- US 编号全局唯一,按模块分段:
- 用户登录US-ACCOUNT-001 US-ACCOUNT-100
- 楼盘管理US-COMPLEX-001 US-COMPLEX-100
- 房源管理US-PROPERTY-001 US-PROPERTY-100
- 客源管理US-CLIENT-001 US-CLIENT-100
- 组织人事US-ORG-001 US-ORG-100
- 权限管理US-PERMISSION-001 US-PERMISSION-100
- 系统配置US-SETTING-001 US-SETTING-100
- 系统管理US-SYSTEM-001 US-SYSTEM-100
- 客户端发布US-RELEASE-001 US-RELEASE-100
### 文件头部格式
```
## Project Task Board
### 项目状态总览
- 产品名称Fonrey 房睿
- 当前阶段MVP Phase 1
- 技术栈:[待填写]
- 最后更新2026-04-24
---
```
### 文件尾部格式
```
---
### 已完成
(暂无)
```
## 生成规则与约束
1. **PRD 里每一行功能条目都必须生成至少一条 Task**,不得遗漏
2. **Phase 4Out of Scope** 只用注释格式列出,不写 US 编号和子项:
```
<!-- OUT OF SCOPE: 移动端适配 - v2 规划 -->
```
3. **验收标准**必须是可测试的具体行为,禁止写"功能正常"等模糊描述
4. 文档路径中的模块英文名对照:
- 房源管理 → PROPERTY
- 客源管理 → CLIENT
- 楼盘管理 → COMPLEX
- 组织人事 → ORG
- 权限管理 → PERMISSION
- 用户登录 → ACCOUNT
- 系统配置 → SETTING
- 系统管理 → SYSTEM
3. 一个 PRD 功能条目如果包含多个子操作(如"新增/编辑/查看"),可以拆成多条 Task也可以合并为一条根据复杂度判断
4. 输出纯 Markdown 格式,不加任何解释说明,直接输出文件内容

View File

@@ -1,42 +0,0 @@
# Project Task Board
## 项目状态总览
- 当前阶段MVP Phase 1
- 技术栈:[你的 tech stack 简述]
- 最后更新YYYY-MM-DD
---
## Phase 1 - MVPP0必须完成
### [模块名,如:用户认证]
- [ ] US-001 用户可以用邮箱注册账号
- 涉及文件:`/auth/register.tsx`, `userModel`
- 验收标准:表单校验通过 → 写入 DB → 跳转登录页
- [ ] US-002 用户可以登录并保持会话
- 涉及文件:`/auth/login.tsx`, `sessionModel`
- 验收标准JWT 生成 → localStorage 存储 → 保护路由生效
### [模块名,如:仪表盘]
- [ ] US-005 用户可以看到核心数据概览
- 涉及文件:`/dashboard/index.tsx`
- 验收标准:数据从 API 读取,与 UI 原型视觉一致
---
## Phase 2 - 增强功能P1MVP 后实现)
- [ ] US-010 用户可以导出数据为 CSV
- [ ] US-011 支持第三方登录Google OAuth
---
## Phase 3 - 锦上添花P2有时间再做
- [ ] US-020 动画和过渡效果优化
- [ ] US-021 深色模式支持
---
## 已完成
- [x] US-XXX xxxxxxx完成日期

View File

@@ -0,0 +1,340 @@
# 任务:为房源列表 生成模块 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 v2Outline 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`
- `Project/fonrey/screenshots/房源/全部房源.png`
### 5. 参考已完成类似页面设计
请读取以下已完成和当前模块页面设计类似的文档和页面:
- 客源列表UI文档设计`Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.md`
- 客源泪飙UI静态原型`Project/fonrey/UI_DESIGN/客源管理/客源列表_UI.html`
### 6. 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/2xlDrawer: 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设计有限** 如有已完成的类似页面设计,请作为主要参考,设计页面布局,内容布局等。
2. **PRD优先**:有截图的功能,以截图呈现的 UI 为补充说明以PRD 文字为主要参考;截图和 PRD 有冲突时以PRD为准并在文档中注明差异。
3. **组件引用**:每次使用特殊组件(如 Data Table、Tree Select、Drawer 等),必须在"使用的特殊组件"表格中引用组件规范设计.md 的对应章节编号,并说明如有差异的自定义部分。
4. **HTMX 落地**:每个需要异步更新的交互(筛选、分页、弹窗提交)必须写出完整的 HTMX 属性Engineer 可以直接复制使用。
5. **Alpine.js 分工**:说明哪些状态由 Alpine.js 管理(弹窗开关、选中状态、表单联动),哪些交互走 HTMX数据加载、表单提交
6. **禁止设计移动端**:所有布局仅针对 ≥1280px 桌面端。
7. **优先级标注**P0 功能用 🔴P1 用 🟡P2 用 ⚫,确保 Engineer 知道实现顺序。
8. **不要遗漏边界状态**:每个列表页必须包含空状态设计;每个表单必须包含校验失败状态;每个异步操作必须包含 Loading 状态。
# 任务:为房源列表生成模块 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 v2Outline 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`
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 为准,并告知我冲突点
- 如果设计文档描述不清晰,不要自行猜测,先列出疑问再继续

View File

@@ -40,6 +40,7 @@
- 架构师方法论:`Project/fonrey/prompt/engineering-backend-architect.md`
- 现有技术栈草案:`Project/fonrey/TECH_STACK/TECH_STACK.md`
- 现有数据模型草案:`Project/fonrey/DATA_MODEL/DATA_MODEL.md`
- 现有UI总体设计方案`Project/fonrey/UI_SYSTEM/UI_SYSTEM.md`
---

View File

@@ -0,0 +1,603 @@
# Fonrey 项目骨架搭建 — 工程执行提示词
## 你的角色与约束
你是一名资深 Django 后端工程师。你的任务是**严格按照规范**搭建 Fonrey 项目骨架,不得自行发明技术方案,不得引入文档未授权的第三方库。每一步操作后必须验证结果。
**项目工作目录**`/mnt/c/Project/`(在此目录下创建 `fonrey/` 子目录)
**执行方式**:逐步创建,每创建一个文件/目录后立即验证,遇到冲突停下来询问而不是自行决策。
---
## 一、技术栈约束(必读,不得违反)
| 层级 | 技术 | 版本约束 |
|------|------|----------|
| Backend | Django | 4.2 LTSASGI 模式) |
| Multi-tenant | django-tenants | latest stable |
| Database | PostgreSQL | 16 |
| Cache | Redis | latest stable |
| Tasks | Celery + Celery Beat | latest stable |
| Storage | django-storages + boto3 | Cloudflare R2S3 兼容) |
| Frontend | HTMX + Alpine.js + Tailwind CSS | HTMX 2.x, Alpine 3.x, Tailwind 3.x |
| Icons | Heroicons v2 | inline SVG via templatetag |
| Server | Gunicorn + Uvicorn workers | ASGI |
| Container | Docker + Docker Compose | — |
| Monitoring | Sentry SDK | — |
**绝对禁止**React / Vue / Angular任何 JS 框架;`nodeIntegration: true`;硬编码密钥/ID跨租户 SQL 查询。
---
## 二、目录结构(严格按此创建,不得增减顶层结构)
```
fonrey/
├── apps/
│ ├── tenant/ # django-tenants 配置in SHARED_APPS
│ ├── account/ # 登录认证in TENANT_APPS
│ ├── permission/ # 权限管理in TENANT_APPS
│ ├── org/ # 组织人事in TENANT_APPS
│ ├── region/ # 区域管理in TENANT_APPS
│ ├── complex/ # 楼盘管理in TENANT_APPS
│ ├── property/ # 房源核心in TENANT_APPS
│ ├── client/ # 客源管理in TENANT_APPS
│ ├── setting/ # 系统设置in TENANT_APPS
│ └── release/ # 客户端发布管理in SHARED_APPS
├── core/
│ ├── __init__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── base.py # 抽象基类(见第四节规范)
│ ├── encryption.py # PII 加密AES-256-GCM
│ ├── cache.py # Redis 工具
│ ├── templatetags/
│ │ ├── __init__.py
│ │ └── heroicons.py # {% heroicon 'plus' %} templatetag
│ └── middleware/
│ ├── __init__.py
│ └── audit.py # 审计日志中间件骨架
├── shared/
│ ├── __init__.py
│ └── apps.py # 公共 Schema App 配置
├── config/
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py # 基础配置
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py
│ ├── asgi.py # ASGI 入口
│ └── wsgi.py
├── templates/
│ ├── base.html # 全局基础模板
│ ├── layouts/
│ │ ├── auth.html # 认证页独立布局
│ │ └── app.html # 主应用布局(含 Topbar + Sidebar
│ ├── components/ # 可复用组件片段
│ │ ├── topbar.html
│ │ ├── sidebar.html
│ │ ├── pagination.html
│ │ ├── toast.html
│ │ ├── modal.html
│ │ └── empty-state.html
│ └── errors/
│ ├── 403.html
│ ├── 404.html
│ └── 500.html
├── static/
│ ├── css/
│ │ └── main.css # Tailwind 入口(@tailwind directives
│ ├── js/
│ │ └── main.js # Alpine.js 初始化 + 全局 HTMX 事件
│ └── vendor/ # 第三方 JS/CSShtmx.min.js, alpine.min.js 等)
├── locale/ # 预留国际化v2当前仅中文
├── .env.example
├── .env # 不入 git
├── .gitignore
├── manage.py
├── requirements/
│ ├── base.txt
│ ├── development.txt
│ └── production.txt
├── docker-compose.yml
├── docker-compose.prod.yml
├── Dockerfile
├── Makefile # 常用命令快捷方式
├── tailwind.config.js
├── package.json # 仅用于 Tailwind 构建,不含业务 JS
└── pyproject.toml # ruff + black + isort 配置
```
每个 `apps/<name>/` 内部结构如下(以 `property` 为典型,其他 App 骨架相同):
```
apps/property/
├── __init__.py
├── apps.py
├── admin.py
├── models/
│ ├── __init__.py # 统一 re-export
│ └── .gitkeep # 骨架阶段留空,后续一表一文件
├── services/
│ ├── __init__.py
│ └── .gitkeep
├── tasks.py # Celery 任务骨架
├── views.py # HTMX/JSON 视图骨架
├── urls.py
├── templates/
│ └── property/ # App 级模板(可覆盖全局同名模板)
└── tests/
├── __init__.py
└── .gitkeep
```
---
## 三、Django 配置规范
### 3.1 INSTALLED_APPS 分区
```python
# config/settings/base.py
SHARED_APPS = [
"django_tenants", # 必须第一位
"apps.tenant", # Tenant / Domain 模型
"apps.release", # ClientRelease 模型
"shared", # 公共 Schema App
# Django 内置
"django.contrib.contenttypes",
"django.contrib.auth",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# 第三方shared
"django_celery_beat",
"django_celery_results",
]
TENANT_APPS = [
"apps.account",
"apps.permission",
"apps.org",
"apps.region",
"apps.complex",
"apps.property",
"apps.client",
"apps.setting",
"core",
]
INSTALLED_APPS = list(SHARED_APPS) + list(TENANT_APPS)
```
### 3.2 核心配置项base.py 必须包含以下所有项)
```python
# 多租户
TENANT_MODEL = "tenant.Tenant"
TENANT_DOMAIN_MODEL = "tenant.Domain"
DEFAULT_AUTO_FIELD = "django.db.models.UUIDField" # 全局 UUID PK
# 数据库(从环境变量读取)
DATABASES = {
"default": {
"ENGINE": "django_tenants.postgresql_backend",
"NAME": env("DB_NAME"),
"USER": env("DB_USER"),
"PASSWORD": env("DB_PASSWORD"),
"HOST": env("DB_HOST", default="localhost"),
"PORT": env("DB_PORT", default="5432"),
"CONN_MAX_AGE": 60,
"OPTIONS": {"pool_size": 10}, # PgBouncer 协同
}
}
DATABASE_ROUTERS = ["django_tenants.routers.TenantSyncRouter"]
# RedisCache + Session + Celery Broker
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": env("REDIS_URL", default="redis://127.0.0.1:6379/0"),
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
"KEY_PREFIX": "fonrey",
}
}
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
# Celery
CELERY_BROKER_URL = env("CELERY_BROKER_URL", default="redis://127.0.0.1:6379/1")
CELERY_RESULT_BACKEND = "django-db"
CELERY_TASK_ALWAYS_EAGER = False
CELERY_TASK_TIME_LIMIT = 300
CELERY_TASK_SOFT_TIME_LIMIT = 270
# 存储Cloudflare R2
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
AWS_S3_ENDPOINT_URL = env("R2_ENDPOINT_URL")
AWS_ACCESS_KEY_ID = env("R2_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = env("R2_SECRET_ACCESS_KEY")
AWS_STORAGE_BUCKET_NAME = env("R2_BUCKET_NAME", default="media")
AWS_S3_CUSTOM_DOMAIN = env("R2_CUSTOM_DOMAIN", default=None)
AWS_DEFAULT_ACL = "private"
# ASGI
ASGI_APPLICATION = "config.asgi.application"
# Sentryproduction 环境激活)
# 见 production.py
# 安全
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_HTTPONLY = False # HTMX 需要读取
X_FRAME_OPTIONS = "DENY"
# 模板
TEMPLATES = [{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
}]
# HTMX
HTMX_GLOBAL_CSRF = True # 全局 CSRF 注入
# 日志骨架production 扩展)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"root": {"handlers": ["console"], "level": "INFO"},
}
```
### 3.3 中间件顺序(严格按此,不得调整)
```python
MIDDLEWARE = [
"django_tenants.middleware.main.TenantMainMiddleware", # 必须第一位
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # 静态文件
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"core.middleware.audit.AuditMiddleware", # 自定义审计(骨架)
]
```
---
## 四、核心抽象基类core/models/base.py
严格按以下规范实现,不得修改字段名、类型、顺序:
```python
import uuid
from django.db import models
from django.utils import timezone
class UUIDPrimaryKeyModel(models.Model):
"""所有业务模型的根基类UUID v4 主键"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class Meta:
abstract = True
class TimeStampedModel(UUIDPrimaryKeyModel):
"""追加创建/更新时间TIMESTAMPTZ"""
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
ordering = ["-created_at"]
class SoftDeleteModel(TimeStampedModel):
"""软删除deleted_at=NULL 表示未删除"""
deleted_at = models.DateTimeField(null=True, blank=True, db_index=True)
objects = ActiveManager() # 默认过滤已删除
all_objects = models.Manager() # 包含已删除记录
def delete(self, using=None, keep_parents=False):
self.deleted_at = timezone.now()
self.save(update_fields=["deleted_at"])
def hard_delete(self):
super().delete()
def restore(self):
self.deleted_at = None
self.save(update_fields=["deleted_at"])
@property
def is_deleted(self):
return self.deleted_at is not None
class Meta:
abstract = True
class AuditedModel(SoftDeleteModel):
"""审计字段操作人FK to Staff允许 NULL 表示系统操作)"""
created_by = models.ForeignKey(
"org.Staff",
null=True, blank=True,
on_delete=models.SET_NULL,
related_name="%(app_label)s_%(class)s_created",
db_index=True,
)
updated_by = models.ForeignKey(
"org.Staff",
null=True, blank=True,
on_delete=models.SET_NULL,
related_name="%(app_label)s_%(class)s_updated",
)
class Meta:
abstract = True
class ActiveManager(models.Manager):
"""默认只返回未软删除的记录"""
def get_queryset(self):
return super().get_queryset().filter(deleted_at__isnull=True)
```
---
## 五、PII 加密core/encryption.py
骨架实现,接口固定(后续补充实现体),确保接口签名正确:
```python
from cryptography.fernet import Fernet
import hashlib
import base64
from django.conf import settings
class PhoneEncryption:
"""
手机号 AES-256-GCM 加密存储 + SHA-256 哈希索引
存储字段phone_encrypted加密密文+ phone_hash哈希用于精确查询
显示:脱敏格式 138****1234
"""
@staticmethod
def encrypt(phone: str) -> str:
"""加密手机号,返回 base64 密文"""
... # TODO: 实现
@staticmethod
def decrypt(ciphertext: str) -> str:
"""解密返回明文"""
... # TODO: 实现
@staticmethod
def hash(phone: str) -> str:
"""返回 SHA-256 哈希(用于 DB 索引查询)"""
... # TODO: 实现
@staticmethod
def mask(phone: str) -> str:
"""返回脱敏格式138****1234"""
if not phone or len(phone) < 7:
return "***"
return phone[:3] + "****" + phone[-4:]
```
---
## 六、Heroicons Templatetagcore/templatetags/heroicons.py
```python
from django import template
from django.utils.safestring import mark_safe
import os
register = template.Library()
ICONS_PATH = os.path.join(os.path.dirname(__file__), "..", "static", "icons")
@register.simple_tag
def heroicon(name: str, size: str = "24", style: str = "outline", css_class: str = "") -> str:
"""
用法: {% heroicon 'plus' %}
{% heroicon 'trash' size='20' style='solid' css_class='text-danger-600' %}
"""
# 骨架:实际从 heroicons vendor 文件读取 SVG
# size 可选: 12, 16, 20, 24
# style 可选: outline, solid, mini
css = f'class="w-{size//4 if isinstance(size, int) else size} h-{size//4 if isinstance(size, int) else size} {css_class}"'
return mark_safe(f'<!-- heroicon:{style}/{name} -->') # TODO: 替换为实际 SVG
```
---
## 七、模板体系
### 7.1 base.html全局根模板
包含以下 block 定义(骨架,后续填充):
- `{% block title %}` — 页面标题
- `{% block extra_head %}` — 额外 CSS/meta
- `{% block body_class %}` — body class 注入
- `{% block content %}` — 页面主内容
- `{% block extra_js %}` — 页面级 JS
引入资源顺序:
1. Tailwind CSS编译后的 `main.css`
2. Flatpickr CSS条件加载
3. HTMX `htmx.min.js`
4. Alpine.js `alpine.min.js`defer必须在 HTMX 之后)
5. 全局 `main.js`(初始化 Toast 监听、HTMX 事件、CSP nonce 等)
### 7.2 layouts/app.html主应用布局
继承 `base.html`,包含:
- Topbar`bg-primary-800`,高 56pxsticky top-0 z-20
-Logo 150px 区
-8 个主导航 Tab主页/房源/客源/营销/交易/数据/人事/系统)+ 全局搜索
- 右:通知铃 + 设置齿轮 + 头像菜单
- Sidebar固定展开 240px / 收起 64pxAlpine `$persist` 记忆状态z-20
- 主内容区(`ml-60``ml-16``px-6 py-4`
- Toast 容器fixed bottom-rightz-70
- 小屏拦截门(`window.innerWidth < 1280` 时显示全屏提示)
### 7.3 layouts/auth.html认证页布局
独立布局,无 Sidebar/Topbar居中卡片 `max-w-md`
### 7.4 HTMX Toast 约定
后端响应头触发 Toast所有需要通知用户的操作必须返回此头
```python
# 工具函数骨架core/htmx.py
from django.http import HttpResponse
def htmx_response(content="", status=200, toast=None, redirect=None):
"""
toast: {"type": "success|error|warning|info", "message": "..."}
"""
response = HttpResponse(content, status=status)
if toast:
import json
response["HX-Trigger"] = json.dumps({"fonrey:toast": toast})
if redirect:
response["HX-Redirect"] = redirect
return response
```
---
## 八、Docker Compose 规范
### docker-compose.yml开发环境
包含以下服务,网络统一使用 `fonrey_net`
| 服务 | 镜像 | 端口 | 说明 |
|------|------|------|------|
| `web` | 本地 Dockerfile | 8000:8000 | Django ASGIUvicorn |
| `db` | postgres:16-alpine | 5432:5432 | PostgreSQL |
| `redis` | redis:7-alpine | 6379:6379 | Cache + Broker |
| `celery` | 同 web 镜像 | — | `celery -A config worker` |
| `celery-beat` | 同 web 镜像 | — | `celery -A config beat` |
| `tailwind` | node:20-alpine | — | `npm run watch`(开发热重载) |
所有服务通过环境变量从 `.env` 文件读取配置(`env_file: .env`)。
`db``redis` 必须配置 `volumes` 持久化数据。
### Dockerfile
```dockerfile
FROM python:3.12-slim
WORKDIR /app
# 系统依赖PostgreSQL 客户端、构建工具)
RUN apt-get update && apt-get install -y \
libpq-dev gcc \
&& rm -rf /var/lib/apt/lists/*
COPY requirements/base.txt requirements/base.txt
RUN pip install --no-cache-dir -r requirements/base.txt
COPY . .
# 收集静态文件production 阶段)
# RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["uvicorn", "config.asgi:application", "--host", "0.0.0.0", "--port", "8000"]
```
---
## 九、requirements 规范
### requirements/base.txt精确版本锁定
```
Django==4.2.16
django-tenants==3.7.0
psycopg2-binary==2.9.9
django-redis==5.4.0
celery==5.4.0
django-celery-beat==2.7.0
django-celery-results==2.5.1
django-storages[s3]==1.14.4
boto3==1.35.0
cryptography==43.0.0
whitenoise==6.8.2
gunicorn==23.0.0
uvicorn[standard]==0.32.0
sentry-sdk[django]==2.18.0
python-decouple==3.8 # .env 读取
Pillow==11.0.0 # 图片处理
```
### requirements/development.txt
```
-r base.txt
ruff==0.7.0
black==24.10.0
pytest-django==4.9.0
factory-boy==3.3.1
django-debug-toolbar==4.4.6
```
---
## 十、Makefile 快捷命令
```makefile
.PHONY: dev migrate shell createsuperuser test lint
dev:
docker compose up
migrate:
docker compose exec web python manage.py migrate_schemas --shared
docker compose exec web python manage.py migrate_schemas
shell:
docker compose exec web python manage.py shell_plus
test:
docker compose exec web pytest apps/ -v
lint:
ruff check . && black --check .
tailwind-build:
npm run build
createsuperuser:
docker compose exec web python manage.py create_tenant_superuser
```
---
## 十一、.env.example 模板
```bash
# Django
SECRET_KEY=your-secret-key-here
DEBUG=True
DJANGO_SETTINGS_MODULE=config.settings.development
ALLOWED_HOSTS=localhost,127.0.0.1
# Database
DB_NAME=fonrey
DB_USER=fonrey
DB_PASSWORD=fonrey
DB_HOST=db
DB_PORT=5432
# Redis
REDIS_URL=redis://redis:6379/0
CELERY_BROKER_URL=redis://redis:6379/1
# Cloudflare R2
R2_ENDPOINT_URL=https://<account_id>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET_NAME=media
R2_CUSTOM_DOMAIN=
# Sentryproduction 填写)
SENTRY_DSN=
# PII 加密密钥AES-256生产环境必须替换
PHONE_ENCRYPTION_KEY=
```
---
## 十二、tailwind.config.js完整规范
严格按照 UI_SYSTEM.md §2.7 和 §10.1 的规范实现,包含:
1. **Primary 色Teal**`primary-50` (#F0FDFA) 到 `primary-800` (#134E4A)`primary-600` (#0F766E) 为主色
2. **Neutral 色Slate**`neutral-50` (#F8FAFC) 到 `neutral-900` (#0F172A)
3. **语义色**`success-600` (#16A34A), `warning-600` (#D97706), `danger-600` (#DC2626), `info-600` (#2563EB)
4. **字体栈**Inter, PingFang SC, Microsoft YaHei, sans-serif
5. **自定义 z-index**z-60, z-70Toast 层)
6. **自定义 boxShadow**xs
7. **动画**`slide-in-right`Drawer 进场)
8. **content 扫描路径**`./templates/**/*.html`, `./apps/**/templates/**/*.html`, `./static/js/**/*.js`
---
## 十三、package.json仅 Tailwind 构建)
```json
{
"name": "fonrey-frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "tailwindcss -i ./static/css/main.css -o ./static/css/output.css --minify",
"watch": "tailwindcss -i ./static/css/main.css -o ./static/css/output.css --watch"
},
"devDependencies": {
"tailwindcss": "^3.4.0"
}
}
```
---
## 十四、pyproject.toml代码质量工具
```toml
[tool.ruff]
line-length = 100
select = ["E", "F", "I", "N", "W", "UP"]
ignore = ["E501"]
target-version = "py312"
[tool.black]
line-length = 100
target-version = ["py312"]
[tool.isort]
profile = "black"
line_length = 100
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings.development"
python_files = ["test_*.py", "*_test.py"]
addopts = "--reuse-db"
```
---
## 十五、执行顺序与验证清单
按以下顺序执行,每步完成后打 ✅:
```
[ ] 1. 创建根目录 fonrey/ 及上述完整目录树(含所有 __init__.py
[ ] 2. 创建 pyproject.toml / .gitignore / .env.example / Makefile
[ ] 3. 创建 requirements/ 三个文件
[ ] 4. 创建 config/settings/base.py完整配置
[ ] 5. 创建 config/settings/development.py 和 production.py
[ ] 6. 创建 config/urls.py骨架路由含 django-tenants URL routing
[ ] 7. 创建 config/asgi.pyASGI 入口)
[ ] 8. 创建 core/models/base.py四个抽象基类
[ ] 9. 创建 core/encryption.pyPhoneEncryption 骨架)
[ ] 10. 创建 core/cache.pyRedis 工具骨架)
[ ] 11. 创建 core/htmx.pyhtmx_response 工具)
[ ] 12. 创建 core/templatetags/heroicons.py
[ ] 13. 创建 core/middleware/audit.py骨架
[ ] 14. 为每个 App 创建目录结构(含 apps.py、models/__init__.py、services/__init__.py、tasks.py 骨架、views.py 骨架、urls.py 骨架)
[ ] 15. 创建 apps/tenant/models.pyTenant、Domain 模型django-tenants 规范)
[ ] 16. 创建 templates/ 完整目录树及 base.html、layouts/app.html、layouts/auth.html 骨架
[ ] 17. 创建 components/ 模板骨架topbar, sidebar, pagination, toast, modal, empty-state
[ ] 18. 创建 templates/errors/ 三个错误页骨架
[ ] 19. 创建 static/css/main.cssTailwind 入口)
[ ] 20. 创建 static/js/main.jsAlpine 初始化 + HTMX 全局事件)
[ ] 21. 创建 tailwind.config.js完整色彩/字体规范)
[ ] 22. 创建 package.json
[ ] 23. 创建 Dockerfile
[ ] 24. 创建 docker-compose.yml5 个服务)
[ ] 25. 创建 manage.py
[ ] 26. 验证python manage.py check --deploy 无致命错误
[ ] 27. 验证:项目目录树与第二节规范 100% 匹配
```
---
## 十六、关键注意事项
1. **django-tenants `apps/tenant/models.py`** 必须定义 `Tenant`(继承 `TenantMixin`)和 `Domain`(继承 `DomainMixin`),且 `Tenant``auto_create_schema = True`
2. **`shared/` App** 的 `apps.py``name = "shared"`,用于公共 Schema 的跨租户共享数据(如 PermissionDef 等)。
3. **所有 App 的 `apps.py`** 必须包含正确的 `name`(含包路径,如 `apps.property`)和 `verbose_name`(中文)。
4. **`config/urls.py`** 使用 `django-tenants` 的 URL 路由模式,区分 public schema 路由和 tenant schema 路由。
5. **`apps/release/`** 放在 `SHARED_APPS`(所有租户共享一张版本表),其余业务 App 放 `TENANT_APPS`
6. **`.gitignore`** 必须包含:`.env``*.pyc``__pycache__/``.DS_Store``node_modules/``static/css/output.css``media/``dist/`
7. **模板中所有异步 HTMX 请求**在骨架阶段只需占位,但必须包含正确的 `hx-` 属性结构,不可省略 `hx-target``hx-swap`
8. **Toast 系统**:前端监听 `htmx:afterRequest` 事件,检查响应头 `HX-Trigger` 中的 `fonrey:toast`,动态插入 Toast DOM4 秒自动消失。
9. **小屏拦截**`layouts/app.html` 中内嵌 JS`window.innerWidth < 1280` 时显示全屏遮罩,文案:"Fonrey 当前仅支持桌面端≥1280px请在电脑上访问"。
10. **所有密码、密钥、Tenant ID** 禁止出现在任何 Python 文件中,统一从 `python-decouple``env()` 读取。

View File

@@ -260,13 +260,14 @@
## 额外要求
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 状态
1. **类似页面UI设计有限** 如有已完成的类似页面设计,请作为主要参考,设计页面布局,内容布局等
2. **PRD优先**有截图的功能,以截图呈现的 UI 为补充说明以PRD 文字为主要参考;截图和 PRD 有冲突时以PRD为准并在文档中注明差异
3. **组件引用**:每次使用特殊组件(如 Data Table、Tree Select、Drawer 等),必须在"使用的特殊组件"表格中引用组件规范设计.md 的对应章节编号,并说明如有差异的自定义部分
4. **HTMX 落地**:每个需要异步更新的交互(筛选、分页、弹窗提交)必须写出完整的 HTMX 属性Engineer 可以直接复制使用
5. **Alpine.js 分工**:说明哪些状态由 Alpine.js 管理(弹窗开关、选中状态、表单联动),哪些交互走 HTMX数据加载、表单提交
6. **禁止设计移动端**:所有布局仅针对 ≥1280px 桌面端
7. **优先级标注**P0 功能用 🔴P1 用 🟡P2 用 ⚫,确保 Engineer 知道实现顺序
8. **不要遗漏边界状态**:每个列表页必须包含空状态设计;每个表单必须包含校验失败状态;每个异步操作必须包含 Loading 状态。
---PROMPT END---

View File

@@ -0,0 +1,108 @@
你是一个 B2B SaaS 项目的产品/技术项目经理。你的任务是根据下方的 PRD.md及PRD_MPV.md 内容,生成一份完整的 TASK.md 项目任务看板文件。
## 任务说明:
根据{{模块名称}}的PRD文档生成具体能落地的项目实现TASK列表
## 输入材料
- 模块PRD文档
- 模块DATA_MODEL文档
- TECH_STACK文档
- UI_SYSTEM文档
## 输出要求
### 文件结构规则
按以下 Phase 划分,严格对应 PRD 优先级:
- **Phase 1 - MVPP0上线前必须完成** → PRD 中标注 P0 的功能
- **Phase 2 - 增强功能P1MVP 后第一迭代)** → PRD 中标注 P1 的功能
- **Phase 3 - 路线图功能P2已规划未排期** → PRD 中标注 P2 的功能
- **Phase 4 - 明确不做Out of Scope** → PRD 第 3 节"非目标"中的功能,仅列出条目作为备忘,不写 task 详情
### 模块顺序(按 PRD 顺序)
1. 用户登录
2. 楼盘管理
3. 房源管理
4. 客源管理
5. 组织人事
6. 权限管理
7. 系统配置
8. 系统管理(运营后台)
9. 客户端发布
**请在项目目录里寻找匹配的文档填入下面具体的任务列表**
### 每条 Task 格式(严格遵守)
```
#### [模块中文名]
- [ ] US-XXX [动词开头的功能描述,主语为"经纪人/店长/管理员/系统"]
- 参考PRD文档`Project/fonrey/PRD/[模块名]/[模块名]模块PRD.md` - [对应 Story 或功能点名称]
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_[模块英文名].md`
- 参考UI_Design文档`Project/fonrey/UI_DESIGN/[模块名]/[功能名]_UI.md`
- 参考UI静态原型页面`Project/fonrey/UI_DESIGN/[模块名]/[功能名]_UI.html`
- 验收标准:[从 PRD 功能描述提炼 2-4 条可测试的验收标准,用分号分隔]
```
### US 编号规则
- US 编号全局唯一,按模块分段:
- 用户登录US-ACCOUNT-001 US-ACCOUNT-100
- 楼盘管理US-COMPLEX-001 US-COMPLEX-100
- 房源管理US-PROPERTY-001 US-PROPERTY-100
- 客源管理US-CLIENT-001 US-CLIENT-100
- 组织人事US-ORG-001 US-ORG-100
- 权限管理US-PERMISSION-001 US-PERMISSION-100
- 系统配置US-SETTING-001 US-SETTING-100
- 系统管理US-SYSTEM-001 US-SYSTEM-100
- 客户端发布US-RELEASE-001 US-RELEASE-100
### 文件头部格式
```
## Project Task Board
### 项目状态总览
- 产品名称Fonrey 房睿
- 当前阶段MVP Phase 1
- 技术栈:[待填写]
- 最后更新2026-04-24
---
```
### 文件尾部格式
```
---
### 已完成
(暂无)
```
## 生成规则与约束
1. **PRD 里每一行功能条目都必须生成至少一条 Task**,不得遗漏
2. **Phase 4Out of Scope** 只用注释格式列出,不写 US 编号和子项:
```
<!-- OUT OF SCOPE: 移动端适配 - v2 规划 -->
```
3. **验收标准**必须是可测试的具体行为,禁止写"功能正常"等模糊描述
4. 文档路径中的模块英文名对照:
- 房源管理 → PROPERTY
- 客源管理 → CLIENT
- 楼盘管理 → COMPLEX
- 组织人事 → ORG
- 权限管理 → PERMISSION
- 用户登录 → ACCOUNT
- 系统配置 → SETTING
- 系统管理 → SYSTEM
3. 一个 PRD 功能条目如果包含多个子操作(如"新增/编辑/查看"),可以拆成多条 Task也可以合并为一条根据复杂度判断
4. 输出纯 Markdown 格式,不加任何解释说明,直接输出文件内容