chore: sync local project changes
This commit is contained in:
@@ -24,35 +24,33 @@
|
||||
5. 我按反馈修改静态页 + 回写 UI.md
|
||||
6. 该任务标记为 `已完成`
|
||||
|
||||
> 设计基线:所有新页面默认包含 **Light / Dark / System** 主题切换方案与状态说明。
|
||||
|
||||
---
|
||||
|
||||
## 3) P0 缺口任务(按优先级执行)
|
||||
|
||||
| 序号 | 优先级 | 模块 | 覆盖 US | UI.md 目标文件 | HTML 目标文件 | 当前状态 | 下一步 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 01 | P0-A | 登录管理 | US-ACCOUNT-001~003 | `UI_DESIGN/登录管理/登录_UI.md` | `UI_DESIGN/登录_UI.html` | 待评审 | 你评审登录 UI.md + 静态页,给我反馈我再迭代 |
|
||||
| 02 | P0-A | 房源管理(新增) | US-PROPERTY-001 | `UI_DESIGN/房源管理/新增房源_UI.md` | `UI_DESIGN/新增房源_UI.html` | 待设计 | 完成任务01后开始 |
|
||||
| 03 | P0-A | 房源管理(详情) | US-PROPERTY-003~008 | `UI_DESIGN/房源管理/房源详情_UI.md` | `UI_DESIGN/房源详情_UI.html` | 待设计 | 完成任务02后开始 |
|
||||
| 04 | P0-B | 楼盘管理(列表) | US-COMPLEX-002 | `UI_DESIGN/楼盘管理/楼盘列表_UI.md` | `UI_DESIGN/楼盘列表_UI.html` | 待设计 | 完成任务03后开始 |
|
||||
| 05 | P0-B | 楼盘管理(详情/维护) | US-COMPLEX-001 | `UI_DESIGN/楼盘管理/楼盘详情_UI.md` | `UI_DESIGN/楼盘详情_UI.html` | 待设计 | 完成任务04后开始 |
|
||||
| 06 | P0-B | 楼盘管理(区域) | US-COMPLEX-003 | `UI_DESIGN/楼盘管理/区域管理_UI.md` | `UI_DESIGN/区域管理_UI.html` | 待设计 | 完成任务05后开始 |
|
||||
| 07 | P0-C | 组织人事 | US-ORG-001~003 | `UI_DESIGN/组织人事管理/组织人事_UI.md` | `UI_DESIGN/组织人事_UI.html` | 待设计 | 完成任务06后开始 |
|
||||
| 08 | P0-C | 权限管理 | US-PERMISSION-001~005 | `UI_DESIGN/权限管理/权限管理_UI.md` | `UI_DESIGN/权限管理_UI.html` | 待设计 | 完成任务07后开始 |
|
||||
| 09 | P0-C | 系统配置 | US-SETTING-001-A/B/C | `UI_DESIGN/系统配置/系统配置_UI.md` | `UI_DESIGN/系统配置_UI.html` | 待设计 | 完成任务08后开始 |
|
||||
| 序号 | 优先级 | 模块 | 覆盖 US | UI.md 目标文件 | HTML 目标文件 | 当前状态 | 下一步 |
|
||||
| --- | ---- | ----------- | --------------------- | ----------------------------- | ------------------------ | ---- | --------------------------- |
|
||||
| 01 | P0-A | 登录管理 | US-ACCOUNT-001~003 | `UI_DESIGN/登录管理/登录_UI.md` | `UI_DESIGN/登录_UI.html` | 待评审 | 你评审登录 UI.md + 静态页,给我反馈我再迭代 |
|
||||
| 02 | P0-A | 房源管理(新增) | US-PROPERTY-001 | `UI_DESIGN/房源管理/新增房源_UI.md` | `UI_DESIGN/新增房源_UI.html` | 已完成 | 已完成评审迭代(壳层/按钮/结构一致化),进入任务03 |
|
||||
| 03 | P0-A | 房源管理(详情) | US-PROPERTY-003~008 | `UI_DESIGN/房源管理/房源详情_UI.md` | `UI_DESIGN/房源详情_UI.html` | 待评审 | 你评审房源详情 UI.md + 静态页,给我反馈我再迭代 |
|
||||
| 04 | P0-B | 楼盘管理(列表) | US-COMPLEX-002 | `UI_DESIGN/楼盘管理/楼盘列表_UI.md` | `UI_DESIGN/楼盘列表_UI.html` | 待评审 | 你评审楼盘列表 UI.md + 静态页,给我反馈我再迭代 |
|
||||
| 05 | P0-B | 楼盘管理(详情/维护) | US-COMPLEX-001 | `UI_DESIGN/楼盘管理/楼盘详情_UI.md` | `UI_DESIGN/楼盘详情_UI.html` | 待设计 | 完成任务04后开始 |
|
||||
| 06 | P0-B | 楼盘管理(区域) | US-COMPLEX-003 | `UI_DESIGN/楼盘管理/区域管理_UI.md` | `UI_DESIGN/区域管理_UI.html` | 待设计 | 完成任务05后开始 |
|
||||
| 07 | P0-C | 组织人事 | US-ORG-001~003 | `UI_DESIGN/组织人事管理/组织人事_UI.md` | `UI_DESIGN/组织人事_UI.html` | 待设计 | 完成任务06后开始 |
|
||||
| 08 | P0-C | 权限管理 | US-PERMISSION-001~005 | `UI_DESIGN/权限管理/权限管理_UI.md` | `UI_DESIGN/权限管理_UI.html` | 待设计 | 完成任务07后开始 |
|
||||
| 09 | P0-C | 系统配置 | US-SETTING-001-A/B/C | `UI_DESIGN/系统配置/系统配置_UI.md` | `UI_DESIGN/系统配置_UI.html` | 待设计 | 完成任务08后开始 |
|
||||
|
||||
---
|
||||
|
||||
## 4) 已有 UI(保留并在后续做回归校对)
|
||||
|
||||
| 模块 | 现有 UI.md | 当前状态 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 客源管理 | `UI_DESIGN/客源管理/新增客源_UI.md` | 已有 | 后续按你反馈可增量调整 |
|
||||
| 客源管理 | `UI_DESIGN/客源管理/客源列表_UI.md` | 已有 | 后续按你反馈可增量调整 |
|
||||
| 客源管理 | `UI_DESIGN/客源管理/客源详情_UI.md` | 已有 | 后续按你反馈可增量调整 |
|
||||
| 客源管理 | `UI_DESIGN/客源管理/编辑客源_UI.md` | 已有 | `US-CLIENT-014` 的新房/租房 Tab 边界待确认 |
|
||||
| 房源管理 | `UI_DESIGN/房源管理/房源列表_UI.md` | 已有 | 后续按你反馈可增量调整 |
|
||||
| 模块 | 现有 UI.md | 当前状态 | 说明 |
|
||||
| ---- | --------------------------- | ---- | -------------------------------- |
|
||||
| 客源管理 | `UI_DESIGN/客源管理/新增客源_UI.md` | 已有 | 后续按你反馈可增量调整 |
|
||||
| 客源管理 | `UI_DESIGN/客源管理/客源列表_UI.md` | 已有 | 后续按你反馈可增量调整 |
|
||||
| 客源管理 | `UI_DESIGN/客源管理/客源详情_UI.md` | 已有 | 后续按你反馈可增量调整 |
|
||||
| 客源管理 | `UI_DESIGN/客源管理/编辑客源_UI.md` | 已有 | `US-CLIENT-014` 的新房/租房 Tab 边界待确认 |
|
||||
| 房源管理 | `UI_DESIGN/房源管理/房源列表_UI.md` | 已有 | 后续按你反馈可增量调整 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
286
Project/fonrey/UI_DESIGN/房源管理/房源详情_UI.md
Normal file
286
Project/fonrey/UI_DESIGN/房源管理/房源详情_UI.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# 房源详情 UI 设计文档
|
||||
|
||||
> **版本**:v1.0 · **日期**:2026-04-28
|
||||
> **依赖规范**:`UI_SYSTEM/UI_SYSTEM.md v1.2`、`UI_SYSTEM/UI_SYSTEM文档要求.md`
|
||||
> **PRD 来源**:`PRD/房源管理/房源管理模块PRD.md` §5.2(房源详情)+ Story 3~8
|
||||
> **TASK 来源**:`PRD/TASK.md` · `US-PROPERTY-003 ~ US-PROPERTY-008`
|
||||
> **数据模型来源**:`DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
### 1.1 页面目标
|
||||
|
||||
本任务对应任务03(P0-A),目标是交付可评审的房源详情主页面原型,覆盖以下关键能力:
|
||||
|
||||
- 完整展示房源详情信息,按分区连续展示并通过二级锚点导航定位(US-PROPERTY-003)
|
||||
- 在详情页内支持写入/筛选跟进日志(US-PROPERTY-004)
|
||||
- 支持房源状态变更(改状态)弹层入口(US-PROPERTY-008)
|
||||
- 支持写跟进弹层入口(US-PROPERTY-004)
|
||||
- 提供图片与附件管理入口结构(US-PROPERTY-005)
|
||||
- 提供业主联系人与相关员工信息区(US-PROPERTY-006)
|
||||
- 提供调价/调级/改属性等快捷编辑入口位(US-PROPERTY-007)
|
||||
|
||||
### 1.2 页面清单
|
||||
|
||||
| 页面 | 文件 | 优先级 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 房源详情页 | `UI_DESIGN/房源详情_UI.html` | P0 🔴 | 主详情壳层 + 二级锚点导航 + 跟进子Tab + 右侧信息面板 + 关键弹层 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 信息架构与页面骨架
|
||||
|
||||
### 2.1 壳层结构(与新增客源页对齐)
|
||||
|
||||
```
|
||||
Top Bar(固定 56px,bg-primary-800)
|
||||
├─ 左:品牌区(Fonrey)
|
||||
├─ 中:主导航(工作台/房源/客源/营销/交易/数据/人事/系统)
|
||||
└─ 右:消息 + 用户
|
||||
|
||||
Sidebar(固定左侧 240px,位于 top-14 下)
|
||||
└─ 房源管理导航(全部房源 / 我的房源 / 公盘池 / 成交房源 / 已删房源)
|
||||
|
||||
Main Content(ml-60 + pt-[72px])
|
||||
├─ 面包屑 + 标题区 + 顶部操作区
|
||||
├─ 二级锚点导航(8个分区)
|
||||
└─ 双栏布局
|
||||
├─ 左侧:分区主内容(连续展示)
|
||||
└─ 右侧:固定信息面板(业主联系人 / 维护完成度 / 房源动态)
|
||||
```
|
||||
|
||||
### 2.2 二级锚点分区定义(PRD §5.2 对齐)
|
||||
|
||||
1. 核心信息
|
||||
2. 跟进日志
|
||||
3. 业务信息
|
||||
4. 房源信息
|
||||
5. 营销信息
|
||||
6. 相关员工
|
||||
7. 相册信息
|
||||
8. 附件信息
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心区块设计
|
||||
|
||||
### 3.1 顶部标题操作区
|
||||
|
||||
- 标题:`交易标签 + 房源名称`(例:出售 · 都市港湾055-0301)
|
||||
- 快捷操作:
|
||||
- `改状态`(弹层)
|
||||
- `编辑房源`(入口)
|
||||
- `分享`(入口)
|
||||
- `写跟进`(弹层)
|
||||
- `更多`(下拉入口位)
|
||||
|
||||
### 3.2 核心信息分区
|
||||
|
||||
由三层信息组成:
|
||||
|
||||
1. **价格与图片层**:主图、总价、单价、近30天变化、调价入口
|
||||
2. **房源概要层**:户型、面积、楼层、电梯、装修、朝向
|
||||
3. **详情字段层**:基础信息 + 交易信息(键值对)
|
||||
|
||||
快捷编辑入口位(文本链):
|
||||
|
||||
- 调价
|
||||
- 改等级
|
||||
- 改属性
|
||||
- 改用途
|
||||
- 看房时间
|
||||
- 挂牌历史
|
||||
|
||||
### 3.3 跟进日志分区
|
||||
|
||||
#### 3.3.1 子Tab结构
|
||||
|
||||
- 全部
|
||||
- 写入跟进
|
||||
- 敏感信息跟进
|
||||
- 敏感信息查看
|
||||
- 修改跟进
|
||||
- 其他跟进
|
||||
|
||||
#### 3.3.2 筛选区
|
||||
|
||||
- 日期范围(开始/结束)
|
||||
- 复选项:有录音 / 有附件 / 仅看我的
|
||||
- 关键字输入(记录内容检索)
|
||||
|
||||
#### 3.3.3 时间线
|
||||
|
||||
每条记录包含:
|
||||
|
||||
- 类型标签(颜色区分)
|
||||
- 内容摘要
|
||||
- 操作人 + 门店组别
|
||||
- 时间戳
|
||||
- 公开/隐藏状态位
|
||||
|
||||
### 3.4 业务信息分区
|
||||
|
||||
卡片化展示三个模块:
|
||||
|
||||
- 钥匙管理(新增钥匙 / 钥匙在他司)
|
||||
- 委托管理(新增委托 / 查看全部)
|
||||
- 实勘管理(新增实勘)
|
||||
|
||||
### 3.5 房源信息分区
|
||||
|
||||
四块可编辑信息:
|
||||
|
||||
- 基本信息
|
||||
- 产证信息
|
||||
- 房屋介绍
|
||||
- 楼盘信息
|
||||
|
||||
每块右上角保留 `编辑` 入口。
|
||||
|
||||
### 3.6 相关员工分区
|
||||
|
||||
以列表卡片展示角色与归属:
|
||||
|
||||
- 首录方
|
||||
- 号码方
|
||||
- 出售方
|
||||
- 实买方
|
||||
|
||||
### 3.7 相册信息分区
|
||||
|
||||
- 分类 Tab(全部/封面/客厅/卧室/卫生间/厨房…)
|
||||
- 操作行(上传图片/批量改类/批量删除/批量下载)
|
||||
- 图片网格(封面标记 + 分类 + 时间)
|
||||
|
||||
### 3.8 附件信息分区
|
||||
|
||||
- 分类 Tab(全部/身份证/房产证/委托书/其他)
|
||||
- 操作行(上传附件/批量改类/批量下载)
|
||||
- 空状态(暂无附件)
|
||||
|
||||
---
|
||||
|
||||
## 4. 右侧固定信息面板
|
||||
|
||||
> 布局:`w-80`,`sticky top-16`
|
||||
|
||||
### 4.1 业主联系人
|
||||
|
||||
字段与交互:
|
||||
|
||||
- 姓名 + 身份
|
||||
- 电话(默认打码)
|
||||
- `查看号码`(示例交互)
|
||||
- `新增联系人`(入口位)
|
||||
- `更多`(入口位)
|
||||
|
||||
### 4.2 房源维护完成度
|
||||
|
||||
- 总分(0~100)
|
||||
- 分项进度(重点信息/附件/实勘/VR/钥匙/委托)
|
||||
- 视觉:进度条 + 百分比
|
||||
|
||||
### 4.3 房源动态(近30天)
|
||||
|
||||
指标卡:
|
||||
|
||||
- 带看次数
|
||||
- 复看次数
|
||||
- 面访次数
|
||||
- 收藏人数
|
||||
- 分享次数
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键弹层与交互
|
||||
|
||||
### 5.1 改状态弹层(US-PROPERTY-008)
|
||||
|
||||
字段:
|
||||
|
||||
- 原状态(只读)
|
||||
- 原交易类型(只读)
|
||||
- 新状态(必填)
|
||||
- 更改理由(必填,≤50字)
|
||||
|
||||
交互规则:
|
||||
|
||||
- 未选新状态或理由为空不可提交
|
||||
- 提交成功后提示“状态已更新(原型模拟)”
|
||||
|
||||
### 5.2 写跟进弹层(US-PROPERTY-004)
|
||||
|
||||
字段:
|
||||
|
||||
- 跟进目的(必填)
|
||||
- 跟进内容(必填,6~500字)
|
||||
- 附件上传入口(原型位)
|
||||
|
||||
交互规则:
|
||||
|
||||
- 内容不足 6 字时给出错误提示
|
||||
- 提交后关闭弹层并提示“跟进已写入(原型模拟)”
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据模型映射(关键字段)
|
||||
|
||||
| UI字段 | 数据模型字段 | 备注 |
|
||||
|---|---|---|
|
||||
| 房源状态 | `properties.status` | 支持状态流转 |
|
||||
| 房源属性 | `properties.attribute` | 公盘/私盘 |
|
||||
| 售价 | `properties.sale_price` | 调价入口 |
|
||||
| 租价 | `properties.rent_price` | 状态联动显示 |
|
||||
| 单价 | `properties.sale_unit_price`(计算) | 展示字段 |
|
||||
| 户型(室厅卫厨阳) | `bedroom_count/living_room_count/bathroom_count/kitchen_count/balcony_count` | 概要展示 |
|
||||
| 建筑面积 | `properties.area` | m² |
|
||||
| 房本年限 | `properties.ownership_years` | 交易信息 |
|
||||
| 等级 | `properties.grade` | 改等级入口 |
|
||||
| 看房时间 | `properties.viewing_time` | 改看房时间入口 |
|
||||
| 最后跟进时间 | `properties.last_followed_at` | 动态区可关联 |
|
||||
| 联系人姓名/身份 | `property_contacts.name/identity` | 右侧面板 |
|
||||
| 联系人号码 | `property_contacts.phone_enc/phone_hash` | 默认打码展示 |
|
||||
| 跟进记录 | `follow_logs` | 跟进子Tab来源 |
|
||||
| 跟进附件 | `follow_log_attachments` | 有附件筛选 |
|
||||
| 调价记录 | `price_changes` | 历史追踪 |
|
||||
| 相册 | `property_photos` | 分类、封面、排序 |
|
||||
| 附件 | `property_attachments` | 分类、批量下载 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 状态矩阵
|
||||
|
||||
| 状态 | 触发 | UI反馈 |
|
||||
|---|---|---|
|
||||
| 默认 | 首次进入 | 页面滚动到“核心信息”分区,右侧面板可见,锚点高亮同步 |
|
||||
| 分区导航点击 | 点击二级锚点 | 平滑滚动至对应分区并更新高亮 |
|
||||
| 跟进子Tab切换 | 点击跟进类型子Tab | 时间线按类型过滤 |
|
||||
| 改状态弹层打开 | 点击改状态 | Modal 打开 |
|
||||
| 写跟进弹层打开 | 点击写跟进 | Modal 打开 |
|
||||
| 校验失败 | 必填未填/字数不达标 | 字段下方错误文案 |
|
||||
| 提交成功(模拟) | 校验通过 | Toast 成功提示 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 可访问性
|
||||
|
||||
### 8.1 可访问性
|
||||
|
||||
- 所有按钮具备可见文本或 `aria-label`
|
||||
- Modal 支持 ESC 关闭
|
||||
- 关键操作保留焦点态(focus ring)
|
||||
|
||||
---
|
||||
|
||||
## 9. 验收清单
|
||||
|
||||
- [x] 产出 `UI_DESIGN/房源管理/房源详情_UI.md`
|
||||
- [x] 产出 `UI_DESIGN/房源详情_UI.html`
|
||||
- [x] 覆盖 `US-PROPERTY-003 ~ US-PROPERTY-008` 的 UI 入口与结构
|
||||
- [x] 二级锚点分区导航 + 跟进子Tab + 时间线结构齐备
|
||||
- [x] 右侧信息面板(业主联系人/维护完成度/房源动态)齐备
|
||||
- [x] 改状态 / 写跟进 弹层原型齐备
|
||||
- [x] 页面内不包含 Light/Dark/System 主题切换控件
|
||||
- [ ] 待你评审后迭代(先改 HTML,再回写本 UI 文档)
|
||||
198
Project/fonrey/UI_DESIGN/房源管理/新增房源_UI.md
Normal file
198
Project/fonrey/UI_DESIGN/房源管理/新增房源_UI.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# 新增房源 UI 设计文档
|
||||
|
||||
> **版本**:v1.0 · **日期**:2026-04-28
|
||||
> **依赖规范**:`UI_SYSTEM/UI_SYSTEM.md v1.2`、`UI_SYSTEM/UI_SYSTEM文档要求.md`
|
||||
> **PRD 来源**:`PRD/房源管理/房源管理模块PRD.md` §5.3(新增房源)+ §5.3.1 / §5.3.2(类型差异)
|
||||
> **TASK 来源**:`PRD/TASK.md` · `US-PROPERTY-001`
|
||||
> **数据模型来源**:`DATA_MODEL/DATA_MODEL_PROPERTY.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
### 1.1 设计目标
|
||||
|
||||
本任务对应 `UI_设计任务总表.md` 任务 02(P0-A)与 `US-PROPERTY-001`:
|
||||
|
||||
- 支持经纪人录入二手房源核心信息,并完成必填校验
|
||||
- 以「新增住宅」为 P0 主链路
|
||||
- 同页支持切换房源类型并展示差异字段(便于评审与后续迭代)
|
||||
- 保存成功后给出“保存成功”反馈并进入详情页(静态原型中为模拟跳转)
|
||||
|
||||
### 1.2 页面清单
|
||||
|
||||
| 页面 | 文件 | 优先级 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 新增房源页 | `UI_DESIGN/新增房源_UI.html` | P0 🔴 | 主表单页,含类型切换、锚点导航、校验与保存反馈 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 信息架构与布局
|
||||
|
||||
### 2.1 页面骨架
|
||||
|
||||
```
|
||||
Top Bar(固定 56px,高优先主导航)
|
||||
├─ 左侧品牌区(Fonrey)
|
||||
├─ 中部主导航(工作台/房源/客源/营销/交易/数据/人事/系统)
|
||||
└─ 右侧消息+用户信息
|
||||
|
||||
Sidebar(固定左侧 240px,位于 Top Bar 下方)
|
||||
└─ 房源管理二级导航(全部房源/我的房源/公海池/成交房源/已删房源)
|
||||
|
||||
Main Content(ml-60 + pt-[72px])
|
||||
├─ 页面头卡(面包屑 + 页面标题 + Theme Toggle + 返回列表)
|
||||
├─ 房源类型选择器(住宅/别墅/商住/商铺/写字楼/其他)
|
||||
├─ 表单锚点导航(房源核心信息 / 业主联系人 / 基础信息 / 交易信息 / 相关方)
|
||||
├─ 五个纵向区块卡片(按锚点顺序)
|
||||
└─ 底部操作条(取消 / 保存并继续新增 / 保存)
|
||||
```
|
||||
|
||||
### 2.2 设计说明
|
||||
|
||||
- 页面壳层**对齐 `UI_DESIGN/新增客源_UI.html`**:同款 top bar + left sidebar + content 区域栅格
|
||||
- 表单采用“分区块竖向布局 + 顶部锚点”方案,对齐 PRD §5.3
|
||||
- 锚点导航 `sticky`,滚动时高亮当前区块
|
||||
- 必填未填时:字段红框 + 错误文案 + 自动滚动到首个错误字段
|
||||
|
||||
---
|
||||
|
||||
## 3. 字段设计(通用)
|
||||
|
||||
## 3.1 房源核心信息
|
||||
|
||||
| 字段 | 控件 | 必填 | 规则 / 说明 | 数据模型映射 |
|
||||
|---|---|---|---|---|
|
||||
| 状态 | Radio | 是 | 出售 / 出租 / 租售 / 暂缓 | `properties.status` |
|
||||
| 房源属性 | Radio | 是 | 公盘 / 私盘 | `properties.attribute` |
|
||||
| 用途 | Radio | 否 | 选项随房源类型变化 | `properties.usage_type` |
|
||||
| 小区名称 | 搜索输入 | 是 | 支持联想,原型中为文本输入 | `properties.complex_id`(落地后) |
|
||||
| 户室号 | 3 段输入 | 否 | 栋/幢/弄/胡同 + 单元/号 + 门牌/室号 | `block_no/unit_no/room_no` |
|
||||
| 所在楼层 | 数字输入×2 | 是 | 当前楼层 ≤ 总楼层 | `floor/total_floors` |
|
||||
| 户型 | 室厅卫厨阳台 | 条件必填 | 商铺/写字楼不显示 | `bedroom_count...balcony_count` |
|
||||
| 建筑面积 | 数字输入 | 是 | 单位 m² | `area` |
|
||||
| 售价 | 数字输入 | 条件必填 | 状态含出售/租售时显示 | `sale_price` |
|
||||
| 租价 | 数字输入 | 条件必填 | 状态含出租/租售时显示 | `rent_price` |
|
||||
| 开间 | 数字输入 | 条件必填 | 仅商铺显示,单位米 | `shop_frontage` |
|
||||
| 进深 | 数字输入 | 条件必填 | 仅商铺显示,单位米 | `shop_depth` |
|
||||
| 层高 | 数字输入 | 条件必填 | 仅商铺显示,单位米 | `shop_height` |
|
||||
| 位置 | Radio | 条件必填 | 仅商铺:临街/商场/小区/底商/商业综合体 | `shop_location` |
|
||||
|
||||
## 3.2 业主/联系人
|
||||
|
||||
| 字段 | 控件 | 必填 | 规则 | 数据模型映射 |
|
||||
|---|---|---|---|---|
|
||||
| 姓名 | Input | 是 | 联系人1必填 | `property_contacts.name` |
|
||||
| 性别 | Radio | 是 | 先生 / 女士,默认先生 | `property_contacts.gender` |
|
||||
| 身份 | Select | 是 | 默认业主 | `property_contacts.identity` |
|
||||
| 电话1 | Input | 否 | 手机/座机格式提示 | `phone_enc/phone_hash`(后端加密) |
|
||||
| 电话2 | Input | 否 | 备用号码 | `phone2_enc/phone2_hash` |
|
||||
|
||||
交互:支持“+ 添加联系人”,第 1 个联系人不可删除。
|
||||
|
||||
## 3.3 基础信息
|
||||
|
||||
| 字段 | 控件 | 必填 | 规则 | 数据模型映射 |
|
||||
|---|---|---|---|---|
|
||||
| 朝向 | Radio | 是 | 10个枚举值 | `properties.orientation` |
|
||||
| 装修 | Radio | 是 | 毛坯~豪装 | `properties.decoration` |
|
||||
|
||||
## 3.4 交易信息
|
||||
|
||||
| 字段 | 控件 | 必填 | 规则 | 数据模型映射 |
|
||||
|---|---|---|---|---|
|
||||
| 房本年限 | 双下拉 | 是(显示时) | 左侧年限 + 右侧补充 | `ownership_years / ownership_years_detail` |
|
||||
|
||||
显示规则:
|
||||
- 住宅 / 别墅 / 其他:显示完整交易信息入口(本版先落地房本年限)
|
||||
- 商住:仅显示房本年限
|
||||
- 商铺 / 写字楼:不显示该区块
|
||||
|
||||
## 3.5 相关方
|
||||
|
||||
| 字段 | 控件 | 必填 | 规则 | 数据模型映射 |
|
||||
|---|---|---|---|---|
|
||||
| 首录方 | 只读输入 | 自动 | 默认当前登录人 | `first_recorder_id` |
|
||||
| 号码方 | 员工选择 | 是 | 默认当前登录人,可清除重选 | `number_holder_id` |
|
||||
| 出售方 | 员工选择 | 是 | 默认当前登录人,可清除重选 | `seller_agent_id` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 房源类型差异(对齐 PRD §5.3.1)
|
||||
|
||||
| 类型 | 区块结构 | 与住宅差异 |
|
||||
|---|---|---|
|
||||
| 住宅(P0) | 5区块完整 | 用途:普通住宅/花园洋房 |
|
||||
| 别墅(P1) | 5区块完整 | 用途:联排/独栋/双拼/叠加 |
|
||||
| 商住(P2) | 5区块 | 用途字段保留但无选项;交易信息仅房本年限 |
|
||||
| 商铺(P2) | 4区块(无交易信息) | 无户型;新增开间/进深/层高/位置 |
|
||||
| 写字楼(P2) | 4区块(无交易信息) | 无户型;其余与住宅主干一致 |
|
||||
| 其他(P2) | 5区块完整 | 用途为10项杂类 |
|
||||
|
||||
> 说明:根据 PRD,P2 类型在 v1 为“即将上线”。原型保留展示与交互,提交时给出“即将上线”提示,避免与开发范围冲突。
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键交互与状态
|
||||
|
||||
## 5.1 联动规则
|
||||
|
||||
1. **状态-价格联动**
|
||||
- 出售:仅显示售价
|
||||
- 出租:仅显示租价
|
||||
- 租售:售价+租价均显示
|
||||
- 暂缓:隐藏售价/租价并给出提示文案
|
||||
|
||||
2. **类型-字段联动**
|
||||
- 商铺/写字楼:隐藏户型
|
||||
- 商铺:显示开间/进深/层高/位置
|
||||
- 商铺/写字楼:隐藏交易信息
|
||||
|
||||
3. **校验失败定位**
|
||||
- 点击保存后,自动滚动到首个错误字段
|
||||
- 字段红框 + 错误文案
|
||||
|
||||
## 5.2 页面状态
|
||||
|
||||
| 状态 | 触发 | 反馈 |
|
||||
|---|---|---|
|
||||
| 默认 | 首次进入 | 住宅类型 + 空白表单 |
|
||||
| 编辑中 | 任意字段修改 | 页面 `dirty` 标记 |
|
||||
| 保存中 | 点击保存 | 按钮 loading + 禁用 |
|
||||
| 保存成功 | 校验通过 | Success Toast:保存成功,正在跳转 |
|
||||
| 保存失败 | 校验不通过 | 错误提示 + 自动定位 |
|
||||
| 类型未开放 | 提交 P2 类型 | Warning Toast:该类型即将上线 |
|
||||
|
||||
## 5.3 离开确认
|
||||
|
||||
- 页面有未保存更改时,离开触发浏览器确认
|
||||
- 对齐 PRD“离开页面需提示是否放弃填写内容”
|
||||
|
||||
---
|
||||
|
||||
## 6. 主题与可访问性
|
||||
|
||||
## 6.1 主题(本任务新增)
|
||||
|
||||
页面内置 Theme Toggle:`Light / Dark / System`
|
||||
|
||||
- 存储键:`fonrey_theme`
|
||||
- `system` 跟随 `prefers-color-scheme`
|
||||
- 颜色通过 CSS 变量驱动,避免硬编码
|
||||
|
||||
## 6.2 可访问性
|
||||
|
||||
- 所有输入项均有可见 Label
|
||||
- 错误提示与字段 `aria-describedby` 关联
|
||||
- 锚点导航和按钮均支持键盘焦点态
|
||||
- 颜色之外辅以文字提示(如必填、错误、状态文案)
|
||||
|
||||
---
|
||||
|
||||
## 7. 实现与评审清单
|
||||
|
||||
- [x] 产出 `UI_DESIGN/房源管理/新增房源_UI.md`
|
||||
- [x] 产出 `UI_DESIGN/新增房源_UI.html`
|
||||
- [x] 覆盖 `US-PROPERTY-001` 的主流程与验收关键点
|
||||
- [x] 包含 Light / Dark / System 主题切换
|
||||
- [ ] 待你评审后按反馈迭代(先改 HTML,再回写本 UI 文档)
|
||||
662
Project/fonrey/UI_DESIGN/房源详情_UI.html
Normal file
662
Project/fonrey/UI_DESIGN/房源详情_UI.html
Normal file
@@ -0,0 +1,662 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1280" />
|
||||
<title>Fonrey 房源详情 · 静态原型</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#F0FDFA',
|
||||
100: '#CCFBF1',
|
||||
200: '#99F6E4',
|
||||
500: '#14B8A6',
|
||||
600: '#0F766E',
|
||||
700: '#115E59',
|
||||
800: '#134E4A'
|
||||
},
|
||||
neutral: {
|
||||
50: '#F8FAFC',
|
||||
100: '#F1F5F9',
|
||||
200: '#E2E8F0',
|
||||
300: '#CBD5E1',
|
||||
400: '#94A3B8',
|
||||
500: '#64748B',
|
||||
600: '#475569',
|
||||
700: '#334155',
|
||||
800: '#1E293B',
|
||||
900: '#0F172A'
|
||||
},
|
||||
success: { 50: '#F0FDF4', 600: '#16A34A' },
|
||||
warning: { 50: '#FFFBEB', 600: '#D97706' },
|
||||
danger: { 50: '#FEF2F2', 600: '#DC2626' },
|
||||
info: { 50: '#EFF6FF', 600: '#2563EB' }
|
||||
},
|
||||
boxShadow: {
|
||||
xs: '0 1px 2px rgba(15,23,42,0.04)'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'PingFang SC', 'Microsoft YaHei', 'sans-serif']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--bg-page: #F8FAFC;
|
||||
--bg-card: #FFFFFF;
|
||||
--bg-subtle: #F1F5F9;
|
||||
--text-primary: #0F172A;
|
||||
--text-secondary: #64748B;
|
||||
--border: #E2E8F0;
|
||||
}
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
[x-cloak] { display: none !important; }
|
||||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
|
||||
|
||||
.bg-surface { background: var(--bg-card); }
|
||||
.bg-subtle { background: var(--bg-subtle); }
|
||||
.border-surface { border-color: var(--border); }
|
||||
.text-surface { color: var(--text-primary); }
|
||||
.text-muted { color: var(--text-secondary); }
|
||||
|
||||
.section-nav-btn { color: #64748B; background: transparent; }
|
||||
.section-nav-btn.active {
|
||||
color: #0F766E;
|
||||
background: #F0FDFA;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtab-pill {
|
||||
border: 1px solid #E2E8F0;
|
||||
background: #FFFFFF;
|
||||
color: #64748B;
|
||||
}
|
||||
.subtab-pill.active {
|
||||
border-color: #0F766E;
|
||||
color: #0F766E;
|
||||
background: #F0FDFA;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-sm antialiased" x-data="propertyDetailPage()" x-init="init()">
|
||||
<!-- Topbar -->
|
||||
<header class="fixed top-0 left-0 right-0 h-14 z-20 bg-primary-800 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
|
||||
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
|
||||
<span class="text-base font-semibold text-white">Fonrey</span>
|
||||
</div>
|
||||
<nav class="flex items-center gap-1 flex-1 px-2" aria-label="主导航">
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">工作台</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md bg-primary-600 text-white font-medium">房源</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">客源</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">营销</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">交易</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">数据</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">人事</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">系统</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-1 px-4 shrink-0">
|
||||
<button class="p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md" aria-label="消息">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022 23.848 23.848 0 0 0 5.454 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
|
||||
<div class="w-8 h-8 rounded-full bg-primary-600 text-white flex items-center justify-center text-sm font-semibold">魏</div>
|
||||
<span class="text-sm font-medium text-primary-100">魏深</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-surface bg-surface overflow-y-auto">
|
||||
<nav class="p-3 space-y-0.5">
|
||||
<div class="px-2 pt-2 pb-1 text-xs font-medium text-muted uppercase tracking-wide">房源管理</div>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md bg-primary-50 text-primary-700 font-medium">全部房源</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">我的房源</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">公盘池</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">成交房源</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">已删房源</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main -->
|
||||
<main class="ml-60 pt-[72px] min-h-screen px-6 py-5">
|
||||
<div class="mx-auto max-w-[1600px] space-y-4">
|
||||
<div class="bg-surface border border-surface rounded-lg p-4">
|
||||
<nav class="flex items-center gap-1 text-xs text-muted mb-2" aria-label="面包屑">
|
||||
<a href="#" class="hover:text-neutral-700">房源</a>
|
||||
<span>/</span>
|
||||
<a href="#" class="hover:text-neutral-700">全部房源</a>
|
||||
<span>/</span>
|
||||
<span class="text-surface">房源详情</span>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-surface">出售 · 都市港湾055-0301</h1>
|
||||
<p class="text-xs text-muted mt-1">嘉定 · 丰庄 · 109.77㎡ · 2室2厅 · 房源编号 01DS016848</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-wrap justify-end">
|
||||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 text-neutral-700 hover:bg-neutral-50" @click="openStatusModal = true">改状态</button>
|
||||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 text-neutral-700 hover:bg-neutral-50">编辑房源</button>
|
||||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 text-neutral-700 hover:bg-neutral-50">分享</button>
|
||||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="openFollowModal = true">写跟进</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 二级导航:锚点 -->
|
||||
<section class="bg-surface border border-surface rounded-lg px-3 py-2 sticky top-16 z-30 shadow-xs">
|
||||
<nav class="flex items-center gap-1 overflow-x-auto whitespace-nowrap" aria-label="房源详情分区导航">
|
||||
<template x-for="item in navItems" :key="item.id">
|
||||
<a :href="'#' + item.id"
|
||||
@click.prevent="scrollToSection(item.id)"
|
||||
:aria-current="activeSection === item.id ? 'true' : 'false'"
|
||||
:class="activeSection === item.id ? 'section-nav-btn active' : 'section-nav-btn hover:bg-neutral-100 hover:text-neutral-800'"
|
||||
class="px-3 py-1.5 text-sm rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 transition-colors"
|
||||
x-text="item.label"></a>
|
||||
</template>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-12 gap-6 items-start">
|
||||
<section class="col-span-8 space-y-4">
|
||||
<!-- 核心信息 -->
|
||||
<div id="section-core" class="section-anchor scroll-mt-24 space-y-4">
|
||||
<article class="bg-surface border border-surface rounded-lg p-4">
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="col-span-4">
|
||||
<div class="aspect-[4/3] rounded-lg bg-neutral-200 flex items-center justify-center text-neutral-500">房源主图</div>
|
||||
<p class="text-xs text-muted mt-2">全部 16 张 · 封面 1 张</p>
|
||||
</div>
|
||||
<div class="col-span-8 space-y-3">
|
||||
<div class="flex items-end justify-between">
|
||||
<div>
|
||||
<div class="text-3xl font-bold text-danger-600 tabular-nums">650 万</div>
|
||||
<div class="text-sm text-muted tabular-nums">59215 元/㎡ · 近30天无价格变动</div>
|
||||
</div>
|
||||
<button class="text-sm text-primary-600 hover:underline">调价</button>
|
||||
</div>
|
||||
<dl class="grid grid-cols-3 gap-3 text-sm">
|
||||
<div><dt class="text-xs text-muted">户型</dt><dd class="text-surface">2室2厅1卫1厨2阳</dd></div>
|
||||
<div><dt class="text-xs text-muted">面积</dt><dd class="text-surface tabular-nums">109.77㎡</dd></div>
|
||||
<div><dt class="text-xs text-muted">楼层</dt><dd class="text-surface">3 / 14</dd></div>
|
||||
<div><dt class="text-xs text-muted">电梯</dt><dd class="text-surface">有电梯</dd></div>
|
||||
<div><dt class="text-xs text-muted">装修</dt><dd class="text-surface">精装</dd></div>
|
||||
<div><dt class="text-xs text-muted">朝向</dt><dd class="text-surface">南北</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="bg-surface border border-surface rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-base font-semibold">基础信息</h2>
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<a href="#" class="text-primary-600 hover:underline">改等级</a>
|
||||
<a href="#" class="text-primary-600 hover:underline">改属性</a>
|
||||
<a href="#" class="text-primary-600 hover:underline">改用途</a>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="grid grid-cols-3 gap-y-3 gap-x-6 text-sm">
|
||||
<div><dt class="text-xs text-muted">等级</dt><dd>C(一般)</dd></div>
|
||||
<div><dt class="text-xs text-muted">属性</dt><dd>公盘</dd></div>
|
||||
<div><dt class="text-xs text-muted">房屋现状</dt><dd>未知</dd></div>
|
||||
<div><dt class="text-xs text-muted">看房时间</dt><dd>随时可看</dd></div>
|
||||
<div><dt class="text-xs text-muted">用途</dt><dd>住宅</dd></div>
|
||||
<div><dt class="text-xs text-muted">录入时间</dt><dd class="tabular-nums">2015-05-01 19:05:53</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- 跟进日志 -->
|
||||
<div id="section-follow" class="section-anchor scroll-mt-24">
|
||||
<article class="bg-surface border border-surface rounded-lg p-4 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-base font-semibold">跟进日志</h2>
|
||||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="openFollowModal = true">写跟进</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<template x-for="sub in followTabs" :key="sub.key">
|
||||
<button class="subtab-pill px-3 py-1 rounded-full text-xs" :class="{ 'active': activeFollowTab === sub.key }" @click="activeFollowTab = sub.key" x-text="sub.label"></button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="bg-subtle border border-surface rounded-md p-3 flex flex-wrap items-center gap-3 text-xs">
|
||||
<label class="flex items-center gap-1.5">开始 <input type="date" class="px-2 py-1 rounded border border-surface bg-surface"></label>
|
||||
<label class="flex items-center gap-1.5">结束 <input type="date" class="px-2 py-1 rounded border border-surface bg-surface"></label>
|
||||
<label class="flex items-center gap-1.5"><input type="checkbox" class="rounded border-surface"> 有录音</label>
|
||||
<label class="flex items-center gap-1.5"><input type="checkbox" class="rounded border-surface"> 有附件</label>
|
||||
<label class="flex items-center gap-1.5"><input type="checkbox" class="rounded border-surface"> 仅看我的</label>
|
||||
<input type="text" placeholder="搜索跟进内容" class="px-2 py-1 rounded border border-surface bg-surface min-w-[180px]" />
|
||||
</div>
|
||||
|
||||
<ol class="relative border-l-2 border-surface ml-3 pl-5 space-y-4">
|
||||
<template x-for="item in filteredTimeline" :key="item.id">
|
||||
<li class="relative">
|
||||
<span class="absolute -left-[29px] top-1.5 w-3 h-3 rounded-full" :class="item.dot"></span>
|
||||
<div class="border border-surface rounded-md p-3 bg-surface space-y-1.5">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium" :class="item.tagClass" x-text="item.tag"></span>
|
||||
<button class="text-xs text-muted hover:text-neutral-700">公开</button>
|
||||
</div>
|
||||
<p class="text-sm" x-text="item.content"></p>
|
||||
<p class="text-xs text-muted" x-text="item.meta"></p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ol>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- 业务信息 -->
|
||||
<div id="section-biz" class="section-anchor scroll-mt-24">
|
||||
<article class="bg-surface border border-surface rounded-lg p-4">
|
||||
<h2 class="text-base font-semibold mb-3">业务信息</h2>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="border border-surface rounded-md p-3 bg-subtle">
|
||||
<p class="text-sm font-medium">钥匙管理</p>
|
||||
<p class="text-xs text-muted mt-1">当前:无钥匙</p>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button class="px-2 py-1 text-xs rounded border border-surface bg-surface">钥匙在他司</button>
|
||||
<button class="px-2 py-1 text-xs rounded border border-surface bg-surface">新增钥匙</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border border-surface rounded-md p-3 bg-subtle">
|
||||
<p class="text-sm font-medium">委托管理</p>
|
||||
<p class="text-xs text-muted mt-1">当前:无委托</p>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button class="px-2 py-1 text-xs rounded border border-surface bg-surface">新增委托</button>
|
||||
<button class="px-2 py-1 text-xs rounded border border-surface bg-surface">全部(0)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border border-surface rounded-md p-3 bg-subtle">
|
||||
<p class="text-sm font-medium">实勘管理</p>
|
||||
<p class="text-xs text-muted mt-1">当前:无实勘</p>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button class="px-2 py-1 text-xs rounded border border-surface bg-surface">新增实勘</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- 房源信息 -->
|
||||
<div id="section-info" class="section-anchor scroll-mt-24">
|
||||
<article class="bg-surface border border-surface rounded-lg p-4">
|
||||
<h2 class="text-base font-semibold mb-3">房源信息</h2>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="border border-surface rounded-md p-3">
|
||||
<div class="flex justify-between"><p class="font-medium">基本信息</p><a href="#" class="text-primary-600 text-xs">编辑</a></div>
|
||||
<p class="text-xs text-muted mt-2">购房付款方式、原购价、包税费、来源</p>
|
||||
</div>
|
||||
<div class="border border-surface rounded-md p-3">
|
||||
<div class="flex justify-between"><p class="font-medium">产证信息</p><a href="#" class="text-primary-600 text-xs">编辑</a></div>
|
||||
<p class="text-xs text-muted mt-2">权属方名称、证件号、房屋坐落</p>
|
||||
</div>
|
||||
<div class="border border-surface rounded-md p-3">
|
||||
<div class="flex justify-between"><p class="font-medium">房屋介绍</p><a href="#" class="text-primary-600 text-xs">编辑</a></div>
|
||||
<p class="text-xs text-muted mt-2">营销标题、核心卖点、业主心态</p>
|
||||
</div>
|
||||
<div class="border border-surface rounded-md p-3">
|
||||
<div class="flex justify-between"><p class="font-medium">楼盘信息</p><a href="#" class="text-primary-600 text-xs">编辑</a></div>
|
||||
<p class="text-xs text-muted mt-2">建成年代、物业公司、物业费、绿化率</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div id="section-marketing" class="section-anchor scroll-mt-24">
|
||||
<article class="bg-surface border border-surface rounded-lg p-4">
|
||||
<h2 class="text-base font-semibold mb-3">营销信息</h2>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="rounded-md p-3 bg-info-50 border border-info-600/20">
|
||||
<p class="font-medium text-info-600">AI 视频</p>
|
||||
<p class="text-xs text-muted mt-1">生成讲房短视频素材</p>
|
||||
</div>
|
||||
<div class="rounded-md p-3 bg-warning-50 border border-warning-600/20">
|
||||
<p class="font-medium text-warning-600">抖音营销</p>
|
||||
<p class="text-xs text-muted mt-1">一键生成发布文案</p>
|
||||
</div>
|
||||
<div class="rounded-md p-3 bg-success-50 border border-success-600/20">
|
||||
<p class="font-medium text-success-600">小红书营销</p>
|
||||
<p class="text-xs text-muted mt-1">多模板图文内容</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div id="section-staff" class="section-anchor scroll-mt-24">
|
||||
<article class="bg-surface border border-surface rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-base font-semibold">相关员工</h2>
|
||||
<a href="#" class="text-sm text-primary-600 hover:underline">编辑</a>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<template x-for="staff in staffList" :key="staff.role">
|
||||
<div class="border border-surface rounded-md p-3 flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-full bg-primary-100 text-primary-700 flex items-center justify-center font-semibold" x-text="staff.name.slice(0,1)"></div>
|
||||
<div>
|
||||
<p class="text-sm font-medium" x-text="staff.name"></p>
|
||||
<p class="text-xs text-muted" x-text="staff.role + ' · ' + staff.team"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div id="section-album" class="section-anchor scroll-mt-24">
|
||||
<article class="bg-surface border border-surface rounded-lg p-4 space-y-3">
|
||||
<h2 class="text-base font-semibold">相册信息</h2>
|
||||
<div class="flex items-center gap-2 flex-wrap text-xs">
|
||||
<span class="subtab-pill active px-3 py-1 rounded-full">全部(16)</span>
|
||||
<span class="subtab-pill px-3 py-1 rounded-full">封面(1)</span>
|
||||
<span class="subtab-pill px-3 py-1 rounded-full">客厅(2)</span>
|
||||
<span class="subtab-pill px-3 py-1 rounded-full">卧室(4)</span>
|
||||
<span class="subtab-pill px-3 py-1 rounded-full">厨房(1)</span>
|
||||
<span class="subtab-pill px-3 py-1 rounded-full">卫生间(1)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button class="px-2 py-1 rounded border border-surface bg-surface">上传图片</button>
|
||||
<button class="px-2 py-1 rounded border border-surface bg-surface">批量改类</button>
|
||||
<button class="px-2 py-1 rounded border border-surface bg-surface">批量删除</button>
|
||||
<button class="px-2 py-1 rounded border border-surface bg-surface">批量下载</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<template x-for="idx in 8" :key="idx">
|
||||
<div class="aspect-[4/3] rounded-md border border-surface bg-neutral-100 flex items-center justify-center text-xs text-neutral-500">图片{{idx}}</div>
|
||||
</template>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div id="section-attachment" class="section-anchor scroll-mt-24">
|
||||
<article class="bg-surface border border-surface rounded-lg p-4 space-y-3">
|
||||
<h2 class="text-base font-semibold">附件信息</h2>
|
||||
<div class="flex items-center gap-2 text-xs flex-wrap">
|
||||
<span class="subtab-pill active px-3 py-1 rounded-full">全部(0)</span>
|
||||
<span class="subtab-pill px-3 py-1 rounded-full">身份证(0)</span>
|
||||
<span class="subtab-pill px-3 py-1 rounded-full">房产证(0)</span>
|
||||
<span class="subtab-pill px-3 py-1 rounded-full">委托书(0)</span>
|
||||
<span class="subtab-pill px-3 py-1 rounded-full">其他(0)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button class="px-2 py-1 rounded border border-surface bg-surface">上传附件</button>
|
||||
<button class="px-2 py-1 rounded border border-surface bg-surface">批量改类</button>
|
||||
<button class="px-2 py-1 rounded border border-surface bg-surface">批量下载</button>
|
||||
</div>
|
||||
<div class="border border-dashed border-surface rounded-lg p-10 text-center text-sm text-muted">暂无附件</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="col-span-4 space-y-3 sticky top-16 max-h-[calc(100vh-80px)] overflow-y-auto">
|
||||
<section class="bg-surface border border-surface rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-semibold">业主/联系人</h3>
|
||||
<button class="text-xs text-primary-600 hover:underline">新增联系人</button>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="rounded-md border border-surface p-2.5 bg-subtle">
|
||||
<p class="text-sm font-medium">方叔叔 <span class="text-xs text-muted">业主</span></p>
|
||||
<p class="text-xs text-muted mt-1">电话:135****2201</p>
|
||||
<button class="mt-2 text-xs text-primary-600 hover:underline">查看号码</button>
|
||||
</div>
|
||||
<p class="text-xs text-warning-600">TIPS:该业主名下还有 2 套房源,建议一起跟进。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-surface border border-surface rounded-lg p-4">
|
||||
<h3 class="text-sm font-semibold mb-3">房源维护完成度</h3>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-muted">总分</span>
|
||||
<span class="text-base font-semibold tabular-nums">69%</span>
|
||||
</div>
|
||||
<div class="w-full h-2 rounded-full bg-neutral-200 overflow-hidden mb-3">
|
||||
<div class="h-full bg-primary-600" style="width:69%"></div>
|
||||
</div>
|
||||
<div class="space-y-2 text-xs">
|
||||
<template x-for="item in completeness" :key="item.name">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted" x-text="item.name"></span>
|
||||
<span class="tabular-nums" x-text="item.score + '%' "></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-surface border border-surface rounded-lg p-4">
|
||||
<h3 class="text-sm font-semibold mb-3">房源动态(近30天)</h3>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<template x-for="m in metrics" :key="m.name">
|
||||
<div class="rounded-md border border-surface bg-subtle p-2.5">
|
||||
<p class="text-muted" x-text="m.name"></p>
|
||||
<p class="text-base font-semibold mt-1 tabular-nums" x-text="m.value"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 改状态弹层 -->
|
||||
<div x-show="openStatusModal" x-cloak class="fixed inset-0 z-40 flex items-center justify-center bg-black/40 p-4">
|
||||
<div class="w-full max-w-md bg-surface border border-surface rounded-lg shadow-lg">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-surface">
|
||||
<h3 class="text-base font-semibold">改状态</h3>
|
||||
<button @click="closeStatusModal()" class="text-muted hover:text-surface">✕</button>
|
||||
</div>
|
||||
<div class="p-4 space-y-3">
|
||||
<div>
|
||||
<label class="text-xs text-muted">新状态 <span class="text-danger-600">*</span></label>
|
||||
<select x-model="statusForm.nextStatus" class="mt-1 w-full px-3 py-2 rounded-md border" :class="statusErrors.nextStatus ? 'border-danger-600' : 'border-surface'">
|
||||
<option value="">请选择</option>
|
||||
<option value="suspended">暂缓</option>
|
||||
<option value="sold_elsewhere">他售</option>
|
||||
<option value="sold">成交</option>
|
||||
</select>
|
||||
<p class="text-xs text-danger-600 mt-1" x-show="statusErrors.nextStatus">请选择新状态</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-muted">更改理由 <span class="text-danger-600">*</span></label>
|
||||
<textarea x-model.trim="statusForm.reason" rows="3" maxlength="50" placeholder="最多50字" class="mt-1 w-full px-3 py-2 rounded-md border" :class="statusErrors.reason ? 'border-danger-600' : 'border-surface'"></textarea>
|
||||
<p class="text-xs text-danger-600 mt-1" x-show="statusErrors.reason">请填写更改理由</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 py-3 border-t border-surface flex justify-end gap-2">
|
||||
<button class="px-3 py-1.5 rounded-md border border-surface" @click="closeStatusModal()">取消</button>
|
||||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white" @click="submitStatusChange()">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 写跟进弹层 -->
|
||||
<div x-show="openFollowModal" x-cloak class="fixed inset-0 z-40 flex items-center justify-center bg-black/40 p-4">
|
||||
<div class="w-full max-w-lg bg-surface border border-surface rounded-lg shadow-lg">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-surface">
|
||||
<h3 class="text-base font-semibold">写跟进</h3>
|
||||
<button @click="closeFollowModal()" class="text-muted hover:text-surface">✕</button>
|
||||
</div>
|
||||
<div class="p-4 space-y-3">
|
||||
<div>
|
||||
<label class="text-xs text-muted">跟进目的 <span class="text-danger-600">*</span></label>
|
||||
<select x-model="followForm.purpose" class="mt-1 w-full px-3 py-2 rounded-md border" :class="followErrors.purpose ? 'border-danger-600' : 'border-surface'">
|
||||
<option value="">请选择</option>
|
||||
<option value="manual">写入跟进</option>
|
||||
<option value="sensitive">敏感信息跟进</option>
|
||||
<option value="modified">修改跟进</option>
|
||||
<option value="other">其他跟进</option>
|
||||
</select>
|
||||
<p class="text-xs text-danger-600 mt-1" x-show="followErrors.purpose">请选择跟进目的</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-muted">跟进内容 <span class="text-danger-600">*</span></label>
|
||||
<textarea x-model.trim="followForm.content" rows="4" maxlength="500" placeholder="最少6字,最多500字" class="mt-1 w-full px-3 py-2 rounded-md border" :class="followErrors.content ? 'border-danger-600' : 'border-surface'"></textarea>
|
||||
<p class="text-xs text-danger-600 mt-1" x-show="followErrors.content">跟进内容至少 6 字</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 py-3 border-t border-surface flex justify-end gap-2">
|
||||
<button class="px-3 py-1.5 rounded-md border border-surface" @click="closeFollowModal()">取消</button>
|
||||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white" @click="submitFollow()">提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="toast.show" x-cloak class="fixed bottom-5 right-5 z-50 px-4 py-2 rounded-md text-sm text-white shadow-lg" :class="toast.type==='success' ? 'bg-success-600' : 'bg-danger-600'" x-text="toast.message"></div>
|
||||
|
||||
<script>
|
||||
function propertyDetailPage() {
|
||||
return {
|
||||
navItems: [
|
||||
{ id: 'section-core', label: '核心信息' },
|
||||
{ id: 'section-follow', label: '跟进日志' },
|
||||
{ id: 'section-biz', label: '业务信息' },
|
||||
{ id: 'section-info', label: '房源信息' },
|
||||
{ id: 'section-marketing', label: '营销信息' },
|
||||
{ id: 'section-staff', label: '相关员工' },
|
||||
{ id: 'section-album', label: '相册信息' },
|
||||
{ id: 'section-attachment', label: '附件信息' }
|
||||
],
|
||||
activeSection: 'section-core',
|
||||
observer: null,
|
||||
|
||||
followTabs: [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'manual', label: '写入跟进' },
|
||||
{ key: 'sensitive', label: '敏感信息跟进' },
|
||||
{ key: 'sensitive_view', label: '敏感信息查看' },
|
||||
{ key: 'modified', label: '修改跟进' },
|
||||
{ key: 'other', label: '其他跟进' }
|
||||
],
|
||||
activeFollowTab: 'all',
|
||||
|
||||
timeline: [
|
||||
{ id: 1, type: 'manual', tag: '跟进-业主跟进', tagClass: 'bg-info-50 text-info-600', dot: 'bg-info-600', content: '业主可接受 630-640 万区间,周末可安排第二次带看。', meta: '[经纪人] 魏深 · 都市港湾店一组 · 2026-04-28 10:23:12' },
|
||||
{ id: 2, type: 'modified', tag: '改价格', tagClass: 'bg-warning-50 text-warning-600', dot: 'bg-warning-600', content: '售价:660 => 650(万);备注:业主接受小幅让价。', meta: '[系统] system · 2026-04-27 18:11:05' },
|
||||
{ id: 3, type: 'sensitive_view', tag: '查看号码', tagClass: 'bg-danger-50 text-danger-600', dot: 'bg-danger-600', content: '经纪人查看业主隐号信息。', meta: '[经纪人] 魏深 · 都市港湾店一组 · 2026-04-26 13:04:47' },
|
||||
{ id: 4, type: 'other', tag: '图片下载', tagClass: 'bg-neutral-100 text-neutral-700', dot: 'bg-neutral-400', content: '下载了房源相册(8张)。', meta: '[经纪人] 魏深 · 都市港湾店一组 · 2026-04-25 21:30:10' }
|
||||
],
|
||||
|
||||
staffList: [
|
||||
{ role: '首录方', name: '魏深', team: '都市港湾店一组' },
|
||||
{ role: '号码方', name: '雷威', team: '都市港湾店一组' },
|
||||
{ role: '出售方', name: '史彬彬', team: '都市港湾店一组' },
|
||||
{ role: '实买方', name: '待分配', team: '—' }
|
||||
],
|
||||
|
||||
completeness: [
|
||||
{ name: '重点信息', score: 90 },
|
||||
{ name: '附件信息', score: 40 },
|
||||
{ name: '实勘', score: 0 },
|
||||
{ name: 'VR', score: 0 },
|
||||
{ name: '钥匙', score: 20 },
|
||||
{ name: '委托', score: 10 }
|
||||
],
|
||||
|
||||
metrics: [
|
||||
{ name: '带看次数', value: 8 },
|
||||
{ name: '复看次数', value: 2 },
|
||||
{ name: '面访次数', value: 5 },
|
||||
{ name: '收藏人数', value: 13 },
|
||||
{ name: '分享次数', value: 21 },
|
||||
{ name: '空看人次', value: 3 }
|
||||
],
|
||||
|
||||
openStatusModal: false,
|
||||
statusForm: { nextStatus: '', reason: '' },
|
||||
statusErrors: {},
|
||||
|
||||
openFollowModal: false,
|
||||
followForm: { purpose: '', content: '' },
|
||||
followErrors: {},
|
||||
|
||||
toast: { show: false, message: '', type: 'success' },
|
||||
|
||||
get filteredTimeline() {
|
||||
if (this.activeFollowTab === 'all') return this.timeline
|
||||
return this.timeline.filter((x) => x.type === this.activeFollowTab)
|
||||
},
|
||||
|
||||
scrollToSection(id) {
|
||||
const el = document.getElementById(id)
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
},
|
||||
|
||||
init() {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (this.openStatusModal) this.closeStatusModal()
|
||||
if (this.openFollowModal) this.closeFollowModal()
|
||||
}
|
||||
})
|
||||
|
||||
const sections = Array.from(document.querySelectorAll('.section-anchor'))
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) this.activeSection = entry.target.id
|
||||
})
|
||||
}, {
|
||||
root: null,
|
||||
rootMargin: '-140px 0px -55% 0px',
|
||||
threshold: 0.01
|
||||
})
|
||||
sections.forEach((s) => this.observer.observe(s))
|
||||
},
|
||||
|
||||
closeStatusModal() {
|
||||
this.openStatusModal = false
|
||||
this.statusErrors = {}
|
||||
},
|
||||
submitStatusChange() {
|
||||
this.statusErrors = {}
|
||||
if (!this.statusForm.nextStatus) this.statusErrors.nextStatus = true
|
||||
if (!this.statusForm.reason.trim()) this.statusErrors.reason = true
|
||||
if (Object.keys(this.statusErrors).length) return
|
||||
this.openStatusModal = false
|
||||
this.statusForm = { nextStatus: '', reason: '' }
|
||||
this.notify('状态已更新(原型模拟)', 'success')
|
||||
},
|
||||
|
||||
closeFollowModal() {
|
||||
this.openFollowModal = false
|
||||
this.followErrors = {}
|
||||
},
|
||||
submitFollow() {
|
||||
this.followErrors = {}
|
||||
if (!this.followForm.purpose) this.followErrors.purpose = true
|
||||
if (!this.followForm.content || this.followForm.content.length < 6) this.followErrors.content = true
|
||||
if (Object.keys(this.followErrors).length) return
|
||||
this.openFollowModal = false
|
||||
this.followForm = { purpose: '', content: '' }
|
||||
this.notify('跟进已写入(原型模拟)', 'success')
|
||||
},
|
||||
|
||||
notify(message, type = 'success') {
|
||||
this.toast = { show: true, message, type }
|
||||
window.setTimeout(() => { this.toast.show = false }, 1800)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -55,6 +55,11 @@
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
|
||||
|
||||
.chip { border:1px solid #E2E8F0; background:#FFFFFF; color:#64748B; }
|
||||
.chip.active { border-color:#0F766E; color:#0F766E; background:#F0FDFA; }
|
||||
.subtab { color:#64748B; border-bottom:2px solid transparent; }
|
||||
.subtab.active { color:#0F766E; border-bottom-color:#0F766E; font-weight:600; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-neutral-50 text-sm text-neutral-700 antialiased" x-data="createClientPage()">
|
||||
@@ -108,8 +113,78 @@
|
||||
<h1 class="text-xl font-semibold text-neutral-800">录入私客</h1>
|
||||
</div>
|
||||
|
||||
<form id="create-client-form" @submit.prevent="submitForm" class="space-y-4">
|
||||
<section class="bg-white rounded-lg border border-neutral-200 p-6">
|
||||
<form id="create-client-form" @submit.prevent="submitForm" class="space-y-4 pb-28">
|
||||
<section class="bg-white rounded-lg border border-neutral-200 p-4">
|
||||
<div>
|
||||
<div class="text-xs text-neutral-500 mb-2">需求状态</div>
|
||||
<div class="flex items-center gap-2" id="demandStatusGroup" data-field-key="status">
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" :class="{ 'active': form.status==='buying' }" @click="form.status='buying'">求购</button>
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" :class="{ 'active': form.status==='renting' }" @click="form.status='renting'">求租</button>
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" :class="{ 'active': form.status==='buy_or_rent' }" @click="form.status='buy_or_rent'">租购</button>
|
||||
</div>
|
||||
<p class="text-xs text-danger-600 mt-1" x-show="errors.status" x-text="errors.status"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav class="bg-white rounded-lg border border-neutral-200 px-4 sticky top-[74px] z-10">
|
||||
<div class="flex items-center gap-5 overflow-x-auto text-sm">
|
||||
<a href="#section-client-info" class="subtab py-3" :class="{ 'active': activeSection==='client-info' }">房源信息</a>
|
||||
<a href="#section-contact" class="subtab py-3" :class="{ 'active': activeSection==='contact' }">联系人</a>
|
||||
<a href="#section-basic" class="subtab py-3" :class="{ 'active': activeSection==='basic' }">基础信息</a>
|
||||
<a href="#section-staff" class="subtab py-3" :class="{ 'active': activeSection==='staff' }">相关员工</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section id="section-client-info" class="bg-white rounded-lg border border-neutral-200 p-6" data-section-key="client-info">
|
||||
<h2 class="text-base font-semibold text-neutral-800 mb-4">房源信息</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">用途 <span class="text-danger-600">*</span></label>
|
||||
<div data-field-key="property_usage" class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]" :class="groupClass('property_usage')">
|
||||
<template x-for="item in usageOptions" :key="item.value">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" x-model="form.propertyUsage" :value="item.value" class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700" x-text="item.label"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<p class="text-xs text-danger-600" x-show="errors.property_usage" x-text="errors.property_usage"></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">等级 <span class="text-danger-600">*</span></label>
|
||||
<div data-field-key="grade" class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]" :class="groupClass('grade')">
|
||||
<template x-for="item in gradeOptions" :key="item.value">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" x-model="form.grade" :value="item.value" class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700" x-text="item.label"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<p class="text-xs text-danger-600" x-show="errors.grade" x-text="errors.grade"></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">来源 <span class="text-danger-600">*</span></label>
|
||||
<select
|
||||
data-field-key="source"
|
||||
x-model="form.source"
|
||||
:disabled="sourceLoading"
|
||||
class="block w-full max-w-xs px-3 py-2 text-sm rounded-md border bg-white focus:outline-none focus:ring-2 disabled:bg-neutral-100 disabled:text-neutral-400"
|
||||
:class="inputClass('source')"
|
||||
>
|
||||
<option value="" x-text="sourceLoading ? '加载中...' : '请选择'"></option>
|
||||
<template x-for="item in sourceOptions" :key="item.value">
|
||||
<option :value="item.value" x-text="item.label"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p class="text-xs text-danger-600" x-show="errors.source" x-text="errors.source"></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="section-contact" class="bg-white rounded-lg border border-neutral-200 p-6" data-section-key="contact">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-base font-semibold text-neutral-800">联系人</h2>
|
||||
<span class="text-xs text-neutral-500">最多添加 5 位联系人</span>
|
||||
@@ -250,72 +325,10 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-white rounded-lg border border-neutral-200 p-6">
|
||||
<section id="section-basic" class="bg-white rounded-lg border border-neutral-200 p-6" data-section-key="basic">
|
||||
<h2 class="text-base font-semibold text-neutral-800 mb-4">基础信息</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">状态 <span class="text-danger-600">*</span></label>
|
||||
<div data-field-key="status" class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]" :class="groupClass('status')">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" x-model="form.status" value="buying" class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700">求购</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" x-model="form.status" value="renting" class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700">求租</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" x-model="form.status" value="buy_or_rent" class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700">租购</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-danger-600" x-show="errors.status" x-text="errors.status"></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">用途 <span class="text-danger-600">*</span></label>
|
||||
<div data-field-key="property_usage" class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]" :class="groupClass('property_usage')">
|
||||
<template x-for="item in usageOptions" :key="item.value">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" x-model="form.propertyUsage" :value="item.value" class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700" x-text="item.label"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<p class="text-xs text-danger-600" x-show="errors.property_usage" x-text="errors.property_usage"></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">等级 <span class="text-danger-600">*</span></label>
|
||||
<div data-field-key="grade" class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]" :class="groupClass('grade')">
|
||||
<template x-for="item in gradeOptions" :key="item.value">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="radio" x-model="form.grade" :value="item.value" class="w-4 h-4 accent-primary-600">
|
||||
<span class="text-sm text-neutral-700" x-text="item.label"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<p class="text-xs text-danger-600" x-show="errors.grade" x-text="errors.grade"></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-sm font-medium text-neutral-700">来源 <span class="text-danger-600">*</span></label>
|
||||
<select
|
||||
data-field-key="source"
|
||||
x-model="form.source"
|
||||
:disabled="sourceLoading"
|
||||
class="block w-full max-w-xs px-3 py-2 text-sm rounded-md border bg-white focus:outline-none focus:ring-2 disabled:bg-neutral-100 disabled:text-neutral-400"
|
||||
:class="inputClass('source')"
|
||||
>
|
||||
<option value="" x-text="sourceLoading ? '加载中...' : '请选择'"></option>
|
||||
<template x-for="item in sourceOptions" :key="item.value">
|
||||
<option :value="item.value" x-text="item.label"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p class="text-xs text-danger-600" x-show="errors.source" x-text="errors.source"></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button" @click="infoExpanded = !infoExpanded" class="flex items-center gap-1 text-sm text-neutral-500 hover:text-neutral-700">
|
||||
<span>证件类型、证件号码、意向学校等</span>
|
||||
@@ -358,7 +371,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-white rounded-lg border border-neutral-200 p-6">
|
||||
<section id="section-staff" class="bg-white rounded-lg border border-neutral-200 p-6" data-section-key="staff">
|
||||
<h2 class="text-base font-semibold text-neutral-800 mb-4">相关员工</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-1.5">
|
||||
@@ -374,27 +387,39 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex items-center gap-3 mt-2 pb-8">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="submitting"
|
||||
class="inline-flex items-center gap-1.5 px-6 py-2 text-sm font-medium bg-primary-600 text-white rounded-md hover:bg-primary-700 active:bg-primary-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 disabled:opacity-70 disabled:cursor-wait"
|
||||
>
|
||||
<svg x-show="submitting" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-opacity=".25" stroke-width="4"></circle>
|
||||
<path d="M22 12a10 10 0 0 1-10 10" stroke="currentColor" stroke-width="4"></path>
|
||||
</svg>
|
||||
<span x-text="submitting ? '保存中...' : '确定'"></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="cancelForm"
|
||||
:disabled="submitting"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium bg-white border border-neutral-300 text-neutral-700 rounded-md hover:bg-neutral-50 hover:border-neutral-400 disabled:opacity-60"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<p x-show="redirectHint" x-text="redirectHint" class="text-xs text-success-600"></p>
|
||||
<div class="fixed bottom-4 left-[calc(15rem+1.5rem)] right-6 z-30">
|
||||
<div class="mx-auto max-w-5xl bg-white rounded-lg border border-neutral-200 shadow-xs p-4">
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="cancelForm"
|
||||
:disabled="submitting"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium bg-white border border-neutral-300 text-neutral-700 rounded-md hover:bg-neutral-50 hover:border-neutral-400 disabled:opacity-60"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="submitAndContinue"
|
||||
:disabled="submitting"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium bg-white border border-neutral-300 text-neutral-700 rounded-md hover:bg-neutral-50 hover:border-neutral-400 disabled:opacity-60"
|
||||
>
|
||||
保存并继续新增
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="submitting"
|
||||
class="inline-flex items-center gap-1.5 px-6 py-2 text-sm font-medium bg-primary-600 text-white rounded-md hover:bg-primary-700 active:bg-primary-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 disabled:opacity-70 disabled:cursor-wait"
|
||||
>
|
||||
<svg x-show="submitting" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-opacity=".25" stroke-width="4"></circle>
|
||||
<path d="M22 12a10 10 0 0 1-10 10" stroke="currentColor" stroke-width="4"></path>
|
||||
</svg>
|
||||
<span x-text="submitting ? '保存中...' : '保存'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<p x-show="redirectHint" x-text="redirectHint" class="text-xs text-success-600 text-right mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -419,7 +444,6 @@
|
||||
{ value: 'villa', label: '别墅' },
|
||||
{ value: 'commercial_residential', label: '商住' },
|
||||
{ value: 'shop', label: '商铺' },
|
||||
{ value: 'office', label: '写字楼' },
|
||||
{ value: 'other', label: '其他' }
|
||||
],
|
||||
gradeOptions: [
|
||||
@@ -435,6 +459,7 @@
|
||||
infoExpanded: false,
|
||||
checkingDuplicate: false,
|
||||
duplicateHint: '',
|
||||
activeSection: 'client-info',
|
||||
submitting: false,
|
||||
redirectHint: '',
|
||||
errors: {},
|
||||
@@ -458,6 +483,7 @@
|
||||
init() {
|
||||
this.form.contacts = [this.newContact()];
|
||||
this.simulateSourceLoading();
|
||||
this.bindSectionObserver();
|
||||
},
|
||||
|
||||
newContact() {
|
||||
@@ -516,6 +542,24 @@
|
||||
this.form.schools.splice(idx, 1);
|
||||
},
|
||||
|
||||
setPropertyUsage(type) {
|
||||
this.form.propertyUsage = type;
|
||||
},
|
||||
|
||||
bindSectionObserver() {
|
||||
this.$nextTick(() => {
|
||||
const sections = Array.from(document.querySelectorAll('[data-section-key]'));
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.activeSection = entry.target.getAttribute('data-section-key');
|
||||
}
|
||||
});
|
||||
}, { rootMargin: '-35% 0px -55% 0px', threshold: 0 });
|
||||
sections.forEach((s) => observer.observe(s));
|
||||
});
|
||||
},
|
||||
|
||||
simulateSourceLoading() {
|
||||
setTimeout(() => {
|
||||
this.sourceOptions = [
|
||||
@@ -619,7 +663,7 @@
|
||||
}, 2200);
|
||||
},
|
||||
|
||||
submitForm() {
|
||||
submitForm(continueMode = false) {
|
||||
if (this.submitting) return;
|
||||
if (!this.validateForm()) return;
|
||||
|
||||
@@ -629,10 +673,28 @@
|
||||
setTimeout(() => {
|
||||
this.submitting = false;
|
||||
this.showToast('保存成功');
|
||||
this.redirectHint = '模拟跳转中:即将进入客源详情页 /clients/CL20260426001/';
|
||||
if (continueMode) {
|
||||
this.form.contacts = [this.newContact()];
|
||||
this.form.status = '';
|
||||
this.form.grade = '';
|
||||
this.form.source = '';
|
||||
this.form.idType = '';
|
||||
this.form.idNumber = '';
|
||||
this.form.schools = [''];
|
||||
this.infoExpanded = false;
|
||||
this.errors = {};
|
||||
this.redirectHint = '已保存当前客源,表单已重置,可继续新增。';
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else {
|
||||
this.redirectHint = '模拟跳转中:即将进入客源详情页 /clients/CL20260426001/';
|
||||
}
|
||||
}, 1200);
|
||||
},
|
||||
|
||||
submitAndContinue() {
|
||||
this.submitForm(true);
|
||||
},
|
||||
|
||||
cancelForm() {
|
||||
this.redirectHint = '已取消录入,模拟返回客源列表 /clients/';
|
||||
}
|
||||
|
||||
758
Project/fonrey/UI_DESIGN/新增房源_UI.html
Normal file
758
Project/fonrey/UI_DESIGN/新增房源_UI.html
Normal file
@@ -0,0 +1,758 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1280" />
|
||||
<title>Fonrey 房源管理 · 新增房源(任务02)</title>
|
||||
<script src="https://cdn.tailwindcss.com"></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' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root {
|
||||
--bg-page:#F8FAFC; --bg-panel:#FFF; --bg-subtle:#F1F5F9; --text-main:#1E293B; --text-sub:#64748B;
|
||||
--border:#E2E8F0; --input-bg:#FFF; --input-text:#1E293B; --header-bg:#FFFFFFD9;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg-page:#0B1220; --bg-panel:#111A2E; --bg-subtle:#1A253C; --text-main:#E2E8F0; --text-sub:#94A3B8;
|
||||
--border:#334155; --input-bg:#0F172A; --input-text:#E2E8F0; --header-bg:#0F172AD9;
|
||||
}
|
||||
[data-theme="system"] {
|
||||
--bg-page:#F8FAFC; --bg-panel:#FFF; --bg-subtle:#F1F5F9; --text-main:#1E293B; --text-sub:#64748B;
|
||||
--border:#E2E8F0; --input-bg:#FFF; --input-text:#1E293B; --header-bg:#FFFFFFD9;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[data-theme="system"] {
|
||||
--bg-page:#0B1220; --bg-panel:#111A2E; --bg-subtle:#1A253C; --text-main:#E2E8F0; --text-sub:#94A3B8;
|
||||
--border:#334155; --input-bg:#0F172A; --input-text:#E2E8F0; --header-bg:#0F172AD9;
|
||||
}
|
||||
}
|
||||
|
||||
body { background:var(--bg-page); color:var(--text-main); transition:all .2s ease; }
|
||||
.page-header { background:var(--header-bg); border-bottom:1px solid var(--border); backdrop-filter:blur(8px); }
|
||||
.panel { background:var(--bg-panel); border:1px solid var(--border); }
|
||||
.subtle { background:var(--bg-subtle); border:1px solid var(--border); }
|
||||
.text-main { color:var(--text-main); }
|
||||
.text-sub { color:var(--text-sub); }
|
||||
|
||||
.input { background:var(--input-bg); color:var(--input-text); border:1px solid var(--border); }
|
||||
.input::placeholder { color:#94A3B8; }
|
||||
.input:focus { outline:none; border-color:#0F766E; box-shadow:0 0 0 2px rgba(15,118,110,.2); }
|
||||
.input.error { border-color:#DC2626!important; box-shadow:0 0 0 2px rgba(220,38,38,.14)!important; }
|
||||
|
||||
.seg { border:1px solid var(--border); background:var(--bg-panel); }
|
||||
.seg-btn { color:var(--text-sub); border:1px solid transparent; }
|
||||
.seg-btn.active { background:#0F766E; border-color:#0F766E; color:#FFF; }
|
||||
|
||||
.chip { border:1px solid var(--border); background:var(--bg-panel); color:var(--text-sub); }
|
||||
.chip.active { border-color:#0F766E; color:#0F766E; background:#F0FDFA; }
|
||||
[data-theme="dark"] .chip.active, [data-theme="system"] .chip.active { background:rgba(20,184,166,.12); }
|
||||
|
||||
.anchor-link { color:var(--text-sub); border-bottom:2px solid transparent; }
|
||||
.anchor-link.active { color:#0F766E; border-bottom-color:#0F766E; font-weight:600; }
|
||||
|
||||
.error-msg { min-height:18px; font-size:12px; color:#DC2626; }
|
||||
.toast-in { animation:toastIn .2s ease-out; }
|
||||
@keyframes toastIn { from{opacity:0;transform:translateY(-8px)} to{opacity:1;transform:translateY(0)} }
|
||||
</style>
|
||||
</head>
|
||||
<body class="antialiased">
|
||||
<header class="fixed top-0 left-0 right-0 h-14 z-20 bg-primary-800 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
|
||||
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
|
||||
<span class="text-base font-semibold text-white">Fonrey</span>
|
||||
</div>
|
||||
<nav class="flex items-center gap-1 flex-1 px-2" aria-label="主导航">
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">工作台</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md bg-primary-600 text-white font-medium">房源</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">客源</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">营销</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">交易</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">数据</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">人事</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">系统</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-1 px-4 shrink-0">
|
||||
<button class="p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md" aria-label="消息">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022 23.848 23.848 0 0 0 5.454 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
|
||||
<div class="w-8 h-8 rounded-full bg-primary-600 text-white flex items-center justify-center text-sm font-semibold">魏</div>
|
||||
<span class="text-sm font-medium text-primary-100">魏深</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-neutral-200 bg-white">
|
||||
<nav class="p-3 space-y-0.5">
|
||||
<div class="px-2 pt-2 pb-1 text-xs font-medium text-neutral-500 uppercase tracking-wide">房源管理</div>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md bg-primary-50 text-primary-700 font-medium">全部房源</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">我的房源</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">公海池</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">成交房源</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">已删房源</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="ml-60 pt-[72px] min-h-screen px-6 py-5">
|
||||
<div class="mx-auto max-w-[1240px] space-y-4">
|
||||
<div class="panel rounded-lg p-4">
|
||||
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<nav class="flex items-center gap-1 text-xs text-sub mb-2" aria-label="面包屑">
|
||||
<a href="javascript:void(0)" class="hover:text-neutral-700">房源</a><span>/</span>
|
||||
<a href="./房源列表_UI.html" class="hover:text-neutral-700">二手/租赁</a><span>/</span>
|
||||
<span class="text-main font-medium">新增房源</span>
|
||||
</nav>
|
||||
<h1 class="text-xl font-semibold text-main">新增房源</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="./房源列表_UI.html" class="inline-flex items-center px-4 py-2 text-sm font-medium bg-white border border-neutral-300 text-neutral-700 rounded-md hover:bg-neutral-50 hover:border-neutral-400">返回列表</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel rounded-lg p-4">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<div class="text-xs text-sub mb-2">房源类型</div>
|
||||
<div id="propertyTypeGroup" class="flex items-center gap-2 flex-wrap">
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-type="residential">住宅 <span class="ml-1 text-[10px] px-1.5 py-0.5 rounded bg-neutral-100 text-neutral-500">P0</span></button>
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-type="villa">别墅 <span class="ml-1 text-[10px] px-1.5 py-0.5 rounded bg-neutral-100 text-neutral-500">P1</span></button>
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-type="commercial_residential">商住 <span class="ml-1 text-[10px] px-1.5 py-0.5 rounded bg-neutral-100 text-neutral-500">P2</span></button>
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-type="shop">商铺 <span class="ml-1 text-[10px] px-1.5 py-0.5 rounded bg-neutral-100 text-neutral-500">P2</span></button>
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-type="office">写字楼 <span class="ml-1 text-[10px] px-1.5 py-0.5 rounded bg-neutral-100 text-neutral-500">P2</span></button>
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-type="other">其他 <span class="ml-1 text-[10px] px-1.5 py-0.5 rounded bg-neutral-100 text-neutral-500">P2</span></button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-sub">当前类型:<span id="currentTypeLabel" class="font-medium text-main">住宅</span> <span id="p2Badge" class="ml-2 hidden inline-flex items-center px-2 py-0.5 rounded bg-warning-50 text-warning-600 border border-warning-600/20">v2 预留类型</span></p>
|
||||
</div>
|
||||
<div class="subtle rounded-lg px-3 py-2 text-xs text-sub max-w-xl">
|
||||
<div class="font-medium text-main mb-1">验收重点(US-PROPERTY-001)</div>
|
||||
<ul class="list-disc pl-4 space-y-1">
|
||||
<li>必填缺失时红色提示并定位</li>
|
||||
<li>状态与价格字段联动(出售/出租/租售/暂缓)</li>
|
||||
<li>保存成功给出反馈(原型中模拟详情跳转)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav class="panel rounded-lg px-4 sticky top-[74px] z-30">
|
||||
<div class="flex items-center gap-5 overflow-x-auto text-sm">
|
||||
<a href="#section-core" class="anchor-link py-3" data-anchor-link="core">房源核心信息</a>
|
||||
<a href="#section-contact" class="anchor-link py-3" data-anchor-link="contact">业主/联系人</a>
|
||||
<a href="#section-basic" class="anchor-link py-3" data-anchor-link="basic">基础信息</a>
|
||||
<a href="#section-trade" class="anchor-link py-3" data-anchor-link="trade" id="tradeAnchor">交易信息</a>
|
||||
<a href="#section-related" class="anchor-link py-3" data-anchor-link="related">相关方</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<form id="propertyForm" class="space-y-4">
|
||||
<section id="section-core" class="panel rounded-lg p-5 space-y-4" data-section-key="core">
|
||||
<h2 class="text-base font-semibold text-main">房源核心信息</h2>
|
||||
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="col-span-6" data-field="status">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">状态 <span class="text-danger-600">*</span></label>
|
||||
<div class="flex items-center gap-2 flex-wrap" id="statusGroup">
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-status="for_sale">出售</button>
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-status="for_rent">出租</button>
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-status="for_sale_rent">租售</button>
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-status="suspended">暂缓</button>
|
||||
</div>
|
||||
<p class="error-msg" data-error="status"></p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6" data-field="attribute">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">房源属性 <span class="text-danger-600">*</span></label>
|
||||
<div class="flex items-center gap-2" id="attributeGroup">
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-attr="public">公盘</button>
|
||||
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-attr="private">私盘</button>
|
||||
</div>
|
||||
<p class="error-msg" data-error="attribute"></p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6" data-field="usage">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">用途</label>
|
||||
<div class="flex items-center gap-2 flex-wrap" id="usageGroup"></div>
|
||||
<p id="usageHint" class="text-xs text-sub rounded-md subtle px-2 py-1.5 inline-block hidden">当前类型用途选项待补充(按 PRD 预留)</p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6" data-field="complexName">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">小区名称 <span class="text-danger-600">*</span></label>
|
||||
<input id="complexName" type="text" class="input w-full px-3 py-2 rounded-md text-sm" placeholder="请输入小区名称(联想搜索)" data-error-input="complexName" />
|
||||
<p class="error-msg" data-error="complexName"></p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">户室号</label>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<input id="blockNo" type="text" class="input w-full px-3 py-2 rounded-md text-sm" placeholder="栋/幢/弄/胡同" />
|
||||
<input id="unitNo" type="text" class="input w-full px-3 py-2 rounded-md text-sm" placeholder="单元/号" />
|
||||
<input id="roomNo" type="text" class="input w-full px-3 py-2 rounded-md text-sm" placeholder="门牌/室号" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6" data-field="floors">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">所在楼层 <span class="text-danger-600">*</span></label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input id="floor" type="number" min="1" class="input w-28 px-3 py-2 rounded-md text-sm" placeholder="请输入" data-error-input="floors" />
|
||||
<span class="text-sub">楼,共</span>
|
||||
<input id="totalFloors" type="number" min="1" class="input w-28 px-3 py-2 rounded-md text-sm" placeholder="请输入" data-error-input="floors" />
|
||||
<span class="text-sub">层</span>
|
||||
</div>
|
||||
<p class="error-msg" data-error="floors"></p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6" data-field="area">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">建筑面积 <span class="text-danger-600">*</span></label>
|
||||
<div class="relative">
|
||||
<input id="area" type="number" min="0" step="0.01" class="input w-full px-3 py-2 rounded-md text-sm pr-12" placeholder="请输入" data-error-input="area" />
|
||||
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-sub text-sm">m²</span>
|
||||
</div>
|
||||
<p class="error-msg" data-error="area"></p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12" id="layoutWrap" data-field="layout">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">户型 <span class="text-danger-600">*</span></label>
|
||||
<div class="flex items-center flex-wrap gap-2">
|
||||
<select id="bedroom" class="input px-3 py-2 rounded-md text-sm" data-error-input="layout"><option value="">室</option></select>
|
||||
<select id="living" class="input px-3 py-2 rounded-md text-sm" data-error-input="layout"><option value="">厅</option></select>
|
||||
<select id="bathroom" class="input px-3 py-2 rounded-md text-sm" data-error-input="layout"><option value="">卫</option></select>
|
||||
<select id="kitchen" class="input px-3 py-2 rounded-md text-sm" data-error-input="layout"><option value="">厨</option></select>
|
||||
<select id="balcony" class="input px-3 py-2 rounded-md text-sm" data-error-input="layout"><option value="">阳台</option></select>
|
||||
</div>
|
||||
<p class="error-msg" data-error="layout"></p>
|
||||
</div>
|
||||
|
||||
<div id="shopFields" class="col-span-12 hidden">
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="col-span-4" data-field="shopFrontage">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">开间 <span class="text-danger-600">*</span></label>
|
||||
<div class="relative"><input id="shopFrontage" type="number" min="0" step="0.01" class="input w-full px-3 py-2 rounded-md text-sm pr-10" placeholder="请输入" data-error-input="shopFrontage" /><span class="absolute right-3 top-1/2 -translate-y-1/2 text-sub text-sm">米</span></div>
|
||||
<p class="error-msg" data-error="shopFrontage"></p>
|
||||
</div>
|
||||
<div class="col-span-4" data-field="shopDepth">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">进深 <span class="text-danger-600">*</span></label>
|
||||
<div class="relative"><input id="shopDepth" type="number" min="0" step="0.01" class="input w-full px-3 py-2 rounded-md text-sm pr-10" placeholder="请输入" data-error-input="shopDepth" /><span class="absolute right-3 top-1/2 -translate-y-1/2 text-sub text-sm">米</span></div>
|
||||
<p class="error-msg" data-error="shopDepth"></p>
|
||||
</div>
|
||||
<div class="col-span-4" data-field="shopHeight">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">层高 <span class="text-danger-600">*</span></label>
|
||||
<div class="relative"><input id="shopHeight" type="number" min="0" step="0.01" class="input w-full px-3 py-2 rounded-md text-sm pr-10" placeholder="请输入" data-error-input="shopHeight" /><span class="absolute right-3 top-1/2 -translate-y-1/2 text-sub text-sm">米</span></div>
|
||||
<p class="error-msg" data-error="shopHeight"></p>
|
||||
</div>
|
||||
<div class="col-span-12" data-field="shopLocation">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">位置 <span class="text-danger-600">*</span></label>
|
||||
<div class="flex items-center gap-2 flex-wrap" id="shopLocationGroup"></div>
|
||||
<p class="error-msg" data-error="shopLocation"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6" id="salePriceWrap" data-field="salePrice">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">售价 <span class="text-danger-600">*</span></label>
|
||||
<div class="relative"><input id="salePrice" type="number" min="0" step="0.01" class="input w-full px-3 py-2 rounded-md text-sm pr-10" placeholder="请输入" data-error-input="salePrice" /><span class="absolute right-3 top-1/2 -translate-y-1/2 text-sub text-sm">万</span></div>
|
||||
<p class="error-msg" data-error="salePrice"></p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 hidden" id="rentPriceWrap" data-field="rentPrice">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">租价 <span class="text-danger-600">*</span></label>
|
||||
<div class="relative"><input id="rentPrice" type="number" min="0" step="1" class="input w-full px-3 py-2 rounded-md text-sm pr-16" placeholder="请输入" data-error-input="rentPrice" /><span class="absolute right-3 top-1/2 -translate-y-1/2 text-sub text-sm">元/月</span></div>
|
||||
<p class="error-msg" data-error="rentPrice"></p>
|
||||
</div>
|
||||
|
||||
<div id="suspendHint" class="col-span-12 hidden"><p class="text-xs text-warning-600 bg-warning-50 border border-warning-600/20 rounded-md px-3 py-2">当前状态为“暂缓”,售价/租价字段已隐藏。</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="section-contact" class="panel rounded-lg p-5 space-y-4" data-section-key="contact">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-base font-semibold text-main">业主/联系人</h2>
|
||||
<button id="addContactBtn" type="button" class="px-3 py-1.5 rounded-md text-sm bg-primary-600 text-white hover:bg-primary-700">+ 添加联系人</button>
|
||||
</div>
|
||||
<div id="contactsContainer"></div>
|
||||
</section>
|
||||
|
||||
<section id="section-basic" class="panel rounded-lg p-5 space-y-4" data-section-key="basic">
|
||||
<h2 class="text-base font-semibold text-main">基础信息</h2>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-main mb-1.5">朝向 <span class="text-danger-600">*</span></label>
|
||||
<div class="flex items-center gap-2 flex-wrap" id="orientationGroup"></div>
|
||||
<p class="error-msg" data-error="orientation"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-main mb-1.5">装修 <span class="text-danger-600">*</span></label>
|
||||
<div class="flex items-center gap-2 flex-wrap" id="decorationGroup"></div>
|
||||
<p class="error-msg" data-error="decoration"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="section-trade" class="panel rounded-lg p-5 space-y-4" data-section-key="trade">
|
||||
<h2 class="text-base font-semibold text-main">交易信息</h2>
|
||||
<div class="grid grid-cols-12 gap-3" data-field="ownershipYears">
|
||||
<div class="col-span-3">
|
||||
<label class="block text-sm font-medium text-main mb-1.5">房本年限 <span class="text-danger-600">*</span></label>
|
||||
<select id="ownershipYears" class="input w-full px-3 py-2 rounded-md text-sm" data-error-input="ownershipYears">
|
||||
<option value="">具体年限</option><option value="lt2">不满2年</option><option value="gte2">满2年</option><option value="gte5">满5年</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-span-3 pt-6">
|
||||
<select id="ownershipYearsDetail" class="input w-full px-3 py-2 rounded-md text-sm" data-error-input="ownershipYears">
|
||||
<option value="">请选择</option><option value="full5">满五</option><option value="not5">不满五</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-span-6 flex items-end"><p class="text-xs text-sub">商住类型仅保留房本年限;商铺/写字楼不展示该区块。</p></div>
|
||||
</div>
|
||||
<p class="error-msg" data-error="ownershipYears"></p>
|
||||
</section>
|
||||
|
||||
<section id="section-related" class="panel rounded-lg p-5 space-y-4" data-section-key="related">
|
||||
<h2 class="text-base font-semibold text-main">相关方</h2>
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-4">
|
||||
<label class="block text-sm font-medium text-main mb-1">首录方</label>
|
||||
<input id="firstRecorder" type="text" readonly value="杜利强 - 系统管理组" class="input w-full px-3 py-2 rounded-md text-sm bg-neutral-100 cursor-not-allowed" />
|
||||
</div>
|
||||
<div class="col-span-4" data-field="numberHolder">
|
||||
<label class="block text-sm font-medium text-main mb-1">号码方 <span class="text-danger-600">*</span></label>
|
||||
<div class="flex gap-2"><select id="numberHolder" class="input flex-1 px-3 py-2 rounded-md text-sm" data-error-input="numberHolder"></select><button type="button" id="clearNumberHolder" class="px-2 rounded-md border border-neutral-300 text-sub hover:bg-neutral-50">⊗</button></div>
|
||||
<p class="error-msg" data-error="numberHolder"></p>
|
||||
</div>
|
||||
<div class="col-span-4" data-field="sellerAgent">
|
||||
<label class="block text-sm font-medium text-main mb-1">出售方 <span class="text-danger-600">*</span></label>
|
||||
<div class="flex gap-2"><select id="sellerAgent" class="input flex-1 px-3 py-2 rounded-md text-sm" data-error-input="sellerAgent"></select><button type="button" id="clearSellerAgent" class="px-2 rounded-md border border-neutral-300 text-sub hover:bg-neutral-50">⊗</button></div>
|
||||
<p class="error-msg" data-error="sellerAgent"></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel rounded-lg p-4 sticky bottom-4 z-20">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div id="dirtyText" class="text-xs text-sub">当前内容已保存或未变更</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button" id="cancelBtn" class="inline-flex items-center px-4 py-2 text-sm font-medium bg-white border border-neutral-300 text-neutral-700 rounded-md hover:bg-neutral-50 hover:border-neutral-400 disabled:opacity-60">取消</button>
|
||||
<button type="button" id="saveContinueBtn" class="inline-flex items-center px-4 py-2 text-sm font-medium bg-white border border-neutral-300 text-neutral-700 rounded-md hover:bg-neutral-50 hover:border-neutral-400 disabled:opacity-60">保存并继续新增</button>
|
||||
<button type="submit" id="saveBtn" class="inline-flex items-center gap-1.5 px-6 py-2 text-sm font-medium bg-primary-600 text-white rounded-md hover:bg-primary-700 active:bg-primary-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 disabled:opacity-70 disabled:cursor-wait">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="toastContainer" class="fixed top-4 right-4 z-[70] space-y-2 w-[340px]" aria-live="polite"></div>
|
||||
|
||||
<template id="contactTemplate">
|
||||
<div class="subtle rounded-lg p-4 space-y-3 contact-item">
|
||||
<div class="flex items-center justify-between"><div class="text-sm font-medium text-main contact-title">业主/联系人1</div><button type="button" class="text-xs text-danger-600 hover:underline remove-contact hidden">删除</button></div>
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-3" data-field="contactName"><label class="block text-sm font-medium text-main mb-1">姓名 <span class="text-danger-600">*</span></label><input type="text" class="input w-full px-3 py-2 rounded-md text-sm contact-name" placeholder="请输入" /><p class="error-msg contact-error-name"></p></div>
|
||||
<div class="col-span-3"><label class="block text-sm font-medium text-main mb-1">性别 <span class="text-danger-600">*</span></label><div class="flex items-center gap-2 gender-group"><button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-gender="male">先生</button><button type="button" class="chip px-3 py-1.5 rounded-md text-sm" data-gender="female">女士</button></div></div>
|
||||
<div class="col-span-2"><label class="block text-sm font-medium text-main mb-1">身份 <span class="text-danger-600">*</span></label><select class="input w-full px-3 py-2 rounded-md text-sm contact-identity"><option value="owner">业主</option><option value="contact">联系人</option><option value="agent">代理人</option><option value="tenant">租客</option><option value="subletter">二房东</option></select></div>
|
||||
<div class="col-span-2"><label class="block text-sm font-medium text-main mb-1">电话1</label><input type="text" class="input w-full px-3 py-2 rounded-md text-sm contact-phone1" placeholder="手机号或座机号" /></div>
|
||||
<div class="col-span-2"><label class="block text-sm font-medium text-main mb-1">电话2</label><input type="text" class="input w-full px-3 py-2 rounded-md text-sm contact-phone2" placeholder="手机号或座机号" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const state = {
|
||||
propertyType: 'residential', status: 'for_sale', attribute: 'public', usage: '', shopLocation: '',
|
||||
orientation: '', decoration: '', dirty: false, saving: false,
|
||||
contacts: [{ name:'', gender:'male', identity:'owner', phone1:'', phone2:'' }],
|
||||
errors: {},
|
||||
options: {
|
||||
usage: {
|
||||
residential: [['normal_residential','普通住宅'], ['garden_house','花园洋房']],
|
||||
villa: [['townhouse','联排别墅'], ['detached','独栋别墅'], ['semi_detached','双拼别墅'], ['stacked','叠加别墅']],
|
||||
commercial_residential: [], shop: [], office: [],
|
||||
other: [['garage','车库'], ['parking','车位'], ['bungalow','平房'], ['siheyuan','四合院'], ['warehouse','仓库'], ['factory','厂房'], ['land','地皮'], ['shop_factory','铺厂'], ['outlet','网点'], ['office_factory','写厂']]
|
||||
},
|
||||
shopLocation: [['street','临街'], ['mall','商场'], ['residential','小区'], ['ground_floor','底商'], ['complex','商业综合体']],
|
||||
orientation: [['east','东'],['south','南'],['west','西'],['north','北'],['southeast','东南'],['northeast','东北'],['east_west','东西'],['south_north','南北'],['southwest','西南'],['northwest','西北']],
|
||||
decoration: [['rough','毛坯'],['plain','清水'],['simple','简装'],['medium','中装'],['fine','精装'],['luxury','豪装']],
|
||||
staff: [['','请选择'], ['du_liqiang','杜利强 - 系统管理组'], ['wei_shen','魏深 - 房源一组'], ['li_min','李敏 - 房源二组']]
|
||||
}
|
||||
}
|
||||
|
||||
const byId = id => document.getElementById(id)
|
||||
const q = s => document.querySelector(s)
|
||||
const qa = s => Array.from(document.querySelectorAll(s))
|
||||
|
||||
function setDirty(v = true) {
|
||||
state.dirty = v
|
||||
byId('dirtyText').textContent = state.dirty ? '你有未保存的更改' : '当前内容已保存或未变更'
|
||||
}
|
||||
|
||||
function markActive(groupSelector, attr, value) {
|
||||
qa(`${groupSelector} [${attr}]`).forEach(btn => btn.classList.toggle('active', btn.getAttribute(attr) === value))
|
||||
}
|
||||
|
||||
function buildNumberSelects() {
|
||||
;['bedroom','living','bathroom','kitchen','balcony'].forEach(id => {
|
||||
const sel = byId(id)
|
||||
for (let i = 0; i <= 9; i++) {
|
||||
const op = document.createElement('option')
|
||||
op.value = String(i)
|
||||
op.textContent = `${i}${id==='bedroom'?'室':id==='living'?'厅':id==='bathroom'?'卫':id==='kitchen'?'厨':'阳台'}`
|
||||
sel.appendChild(op)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildOptions() {
|
||||
const usage = byId('usageGroup')
|
||||
usage.innerHTML = ''
|
||||
const arr = state.options.usage[state.propertyType] || []
|
||||
if (!arr.length) {
|
||||
byId('usageHint').classList.remove('hidden')
|
||||
} else {
|
||||
byId('usageHint').classList.add('hidden')
|
||||
arr.forEach(([v, t]) => {
|
||||
const b = document.createElement('button')
|
||||
b.type = 'button'; b.className = 'chip px-3 py-1.5 rounded-md text-sm'; b.textContent = t; b.dataset.usage = v
|
||||
if (state.usage === v) b.classList.add('active')
|
||||
b.addEventListener('click', () => { state.usage = v; markActive('#usageGroup','data-usage',v); setDirty() })
|
||||
usage.appendChild(b)
|
||||
})
|
||||
}
|
||||
|
||||
const shopLoc = byId('shopLocationGroup')
|
||||
shopLoc.innerHTML = ''
|
||||
state.options.shopLocation.forEach(([v, t]) => {
|
||||
const b = document.createElement('button')
|
||||
b.type = 'button'; b.className = 'chip px-3 py-1.5 rounded-md text-sm'; b.textContent = t; b.dataset.shoploc = v
|
||||
if (state.shopLocation === v) b.classList.add('active')
|
||||
b.addEventListener('click', () => { state.shopLocation = v; markActive('#shopLocationGroup','data-shoploc',v); clearError('shopLocation'); setDirty() })
|
||||
shopLoc.appendChild(b)
|
||||
})
|
||||
|
||||
const orientation = byId('orientationGroup')
|
||||
orientation.innerHTML = ''
|
||||
state.options.orientation.forEach(([v, t]) => {
|
||||
const b = document.createElement('button')
|
||||
b.type = 'button'; b.className = 'chip px-3 py-1.5 rounded-md text-sm'; b.textContent = t; b.dataset.orient = v
|
||||
if (state.orientation === v) b.classList.add('active')
|
||||
b.addEventListener('click', () => { state.orientation = v; markActive('#orientationGroup','data-orient',v); clearError('orientation'); setDirty() })
|
||||
orientation.appendChild(b)
|
||||
})
|
||||
|
||||
const decoration = byId('decorationGroup')
|
||||
decoration.innerHTML = ''
|
||||
state.options.decoration.forEach(([v, t]) => {
|
||||
const b = document.createElement('button')
|
||||
b.type = 'button'; b.className = 'chip px-3 py-1.5 rounded-md text-sm'; b.textContent = t; b.dataset.deco = v
|
||||
if (state.decoration === v) b.classList.add('active')
|
||||
b.addEventListener('click', () => { state.decoration = v; markActive('#decorationGroup','data-deco',v); clearError('decoration'); setDirty() })
|
||||
decoration.appendChild(b)
|
||||
})
|
||||
|
||||
;['numberHolder','sellerAgent'].forEach(id => {
|
||||
const sel = byId(id)
|
||||
const old = sel.value
|
||||
sel.innerHTML = ''
|
||||
state.options.staff.forEach(([v, t]) => {
|
||||
const op = document.createElement('option'); op.value = v; op.textContent = id==='numberHolder' && v==='' ? '请选择号码方' : id==='sellerAgent' && v==='' ? '请选择出售方' : t
|
||||
sel.appendChild(op)
|
||||
})
|
||||
sel.value = old || 'du_liqiang'
|
||||
})
|
||||
}
|
||||
|
||||
function renderContacts() {
|
||||
const wrap = byId('contactsContainer')
|
||||
wrap.innerHTML = ''
|
||||
state.contacts.forEach((c, idx) => {
|
||||
const el = byId('contactTemplate').content.firstElementChild.cloneNode(true)
|
||||
el.querySelector('.contact-title').textContent = `业主/联系人${idx + 1}`
|
||||
const removeBtn = el.querySelector('.remove-contact')
|
||||
if (idx > 0) {
|
||||
removeBtn.classList.remove('hidden')
|
||||
removeBtn.addEventListener('click', () => { state.contacts.splice(idx, 1); renderContacts(); setDirty() })
|
||||
}
|
||||
|
||||
const name = el.querySelector('.contact-name'); name.value = c.name
|
||||
name.addEventListener('input', e => { state.contacts[idx].name = e.target.value; setDirty() })
|
||||
|
||||
const genderBtns = el.querySelectorAll('[data-gender]')
|
||||
genderBtns.forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.gender === c.gender)
|
||||
btn.addEventListener('click', () => {
|
||||
state.contacts[idx].gender = btn.dataset.gender
|
||||
genderBtns.forEach(b => b.classList.toggle('active', b === btn)); setDirty()
|
||||
})
|
||||
})
|
||||
|
||||
const identity = el.querySelector('.contact-identity'); identity.value = c.identity
|
||||
identity.addEventListener('change', e => { state.contacts[idx].identity = e.target.value; setDirty() })
|
||||
const p1 = el.querySelector('.contact-phone1'); p1.value = c.phone1; p1.addEventListener('input', e => { state.contacts[idx].phone1 = e.target.value; setDirty() })
|
||||
const p2 = el.querySelector('.contact-phone2'); p2.value = c.phone2; p2.addEventListener('input', e => { state.contacts[idx].phone2 = e.target.value; setDirty() })
|
||||
|
||||
if (idx === 0 && state.errors['contacts.0.name']) {
|
||||
name.classList.add('error')
|
||||
el.querySelector('.contact-error-name').textContent = state.errors['contacts.0.name']
|
||||
}
|
||||
|
||||
wrap.appendChild(el)
|
||||
})
|
||||
}
|
||||
|
||||
function isShop() { return state.propertyType === 'shop' }
|
||||
function showLayout() { return !['shop','office'].includes(state.propertyType) }
|
||||
function showTrade() { return !['shop','office'].includes(state.propertyType) }
|
||||
|
||||
function refreshTypeUI() {
|
||||
markActive('#propertyTypeGroup', 'data-type', state.propertyType)
|
||||
const map = { residential:'住宅', villa:'别墅', commercial_residential:'商住', shop:'商铺', office:'写字楼', other:'其他' }
|
||||
byId('currentTypeLabel').textContent = map[state.propertyType]
|
||||
byId('p2Badge').classList.toggle('hidden', !['commercial_residential','shop','office','other'].includes(state.propertyType))
|
||||
|
||||
byId('layoutWrap').classList.toggle('hidden', !showLayout())
|
||||
byId('shopFields').classList.toggle('hidden', !isShop())
|
||||
byId('section-trade').classList.toggle('hidden', !showTrade())
|
||||
byId('tradeAnchor').classList.toggle('hidden', !showTrade())
|
||||
buildOptions()
|
||||
updateStatusUI()
|
||||
}
|
||||
|
||||
function updateStatusUI() {
|
||||
markActive('#statusGroup', 'data-status', state.status)
|
||||
byId('salePriceWrap').classList.toggle('hidden', !['for_sale','for_sale_rent'].includes(state.status))
|
||||
byId('rentPriceWrap').classList.toggle('hidden', !['for_rent','for_sale_rent'].includes(state.status))
|
||||
byId('suspendHint').classList.toggle('hidden', state.status !== 'suspended')
|
||||
}
|
||||
|
||||
function clearErrors() {
|
||||
state.errors = {}
|
||||
qa('.error-msg').forEach(e => e.textContent = '')
|
||||
qa('.input.error').forEach(i => i.classList.remove('error'))
|
||||
}
|
||||
|
||||
function clearError(key) {
|
||||
state.errors[key] = ''
|
||||
const msg = q(`[data-error="${CSS.escape(key)}"]`)
|
||||
if (msg) msg.textContent = ''
|
||||
qa(`[data-error-input="${CSS.escape(key)}"]`).forEach(i => i.classList.remove('error'))
|
||||
}
|
||||
|
||||
function setError(key, msg) {
|
||||
state.errors[key] = msg
|
||||
const msgEl = q(`[data-error="${CSS.escape(key)}"]`)
|
||||
if (msgEl) msgEl.textContent = msg
|
||||
qa(`[data-error-input="${CSS.escape(key)}"]`).forEach(i => i.classList.add('error'))
|
||||
}
|
||||
|
||||
function firstErrorKey() {
|
||||
return Object.keys(state.errors).find(k => state.errors[k])
|
||||
}
|
||||
|
||||
function validate() {
|
||||
clearErrors()
|
||||
const v = id => (byId(id)?.value || '').trim()
|
||||
|
||||
if (!state.status) setError('status', '请选择状态')
|
||||
if (!state.attribute) setError('attribute', '请选择房源属性')
|
||||
if (!v('complexName')) setError('complexName', '请输入小区名称')
|
||||
|
||||
const floor = Number(v('floor')), total = Number(v('totalFloors'))
|
||||
if (!floor || !total) setError('floors', '请输入所在楼层与总楼层')
|
||||
else if (floor > total) setError('floors', '所在楼层不能大于总楼层')
|
||||
|
||||
if (!Number(v('area')) || Number(v('area')) <= 0) setError('area', '请输入建筑面积')
|
||||
|
||||
if (showLayout()) {
|
||||
const keys = ['bedroom','living','bathroom','kitchen','balcony']
|
||||
if (keys.some(k => v(k) === '')) setError('layout', '请完整填写户型(室/厅/卫/厨/阳台)')
|
||||
}
|
||||
|
||||
if (['for_sale','for_sale_rent'].includes(state.status) && (!Number(v('salePrice')) || Number(v('salePrice')) <= 0)) {
|
||||
setError('salePrice', '请输入有效售价')
|
||||
}
|
||||
if (['for_rent','for_sale_rent'].includes(state.status) && (!Number(v('rentPrice')) || Number(v('rentPrice')) <= 0)) {
|
||||
setError('rentPrice', '请输入有效租价')
|
||||
}
|
||||
|
||||
if (isShop()) {
|
||||
if (!Number(v('shopFrontage')) || Number(v('shopFrontage')) <= 0) setError('shopFrontage', '请输入开间')
|
||||
if (!Number(v('shopDepth')) || Number(v('shopDepth')) <= 0) setError('shopDepth', '请输入进深')
|
||||
if (!Number(v('shopHeight')) || Number(v('shopHeight')) <= 0) setError('shopHeight', '请输入层高')
|
||||
if (!state.shopLocation) setError('shopLocation', '请选择位置')
|
||||
}
|
||||
|
||||
if (!state.contacts[0]?.name?.trim()) setError('contacts.0.name', '联系人1姓名必填')
|
||||
if (!state.orientation) setError('orientation', '请选择朝向')
|
||||
if (!state.decoration) setError('decoration', '请选择装修')
|
||||
|
||||
if (showTrade()) {
|
||||
if (!v('ownershipYears') || !v('ownershipYearsDetail')) setError('ownershipYears', '请选择完整房本年限')
|
||||
}
|
||||
|
||||
if (!v('numberHolder')) setError('numberHolder', '请选择号码方')
|
||||
if (!v('sellerAgent')) setError('sellerAgent', '请选择出售方')
|
||||
|
||||
renderContacts()
|
||||
return !firstErrorKey()
|
||||
}
|
||||
|
||||
function scrollToFirstError() {
|
||||
const key = firstErrorKey()
|
||||
if (!key) return
|
||||
const selector = key === 'contacts.0.name' ? '#section-contact' : `[data-field="${CSS.escape(key)}"]`
|
||||
const target = q(selector)
|
||||
if (target) target.scrollIntoView({ behavior:'smooth', block:'center' })
|
||||
}
|
||||
|
||||
function toast(type, title, message) {
|
||||
const c = byId('toastContainer')
|
||||
const el = document.createElement('div')
|
||||
const cls = type==='success' ? 'border-success-600/30 bg-success-50' : type==='warning' ? 'border-warning-600/30 bg-warning-50' : type==='error' ? 'border-danger-600/30 bg-danger-50' : 'border-info-600/30 bg-info-50'
|
||||
el.className = `panel rounded-lg px-3 py-2 shadow-lg toast-in ${cls}`
|
||||
el.innerHTML = `<div class="flex items-start gap-2"><div class="text-sm font-medium">${title}</div><button class="ml-auto text-xs text-sub">✕</button></div><p class="text-xs mt-1 text-sub">${message}</p>`
|
||||
el.querySelector('button').addEventListener('click', ()=> el.remove())
|
||||
c.appendChild(el)
|
||||
setTimeout(()=>el.remove(), 3200)
|
||||
}
|
||||
|
||||
function setButtonsDisabled(disabled) {
|
||||
;['saveBtn','saveContinueBtn','cancelBtn'].forEach(id => byId(id).disabled = disabled)
|
||||
byId('saveBtn').textContent = disabled ? '保存中...' : '保存'
|
||||
}
|
||||
|
||||
async function submit(mode) {
|
||||
if (!validate()) {
|
||||
toast('error','保存失败','请先修正必填项与格式错误')
|
||||
scrollToFirstError()
|
||||
return
|
||||
}
|
||||
|
||||
setButtonsDisabled(true)
|
||||
await new Promise(r => setTimeout(r, 650))
|
||||
|
||||
if (['commercial_residential','shop','office','other'].includes(state.propertyType)) {
|
||||
toast('warning','类型预留提醒','该房源类型在 PRD 中为 v2 预留,当前原型仅用于评审。')
|
||||
setButtonsDisabled(false)
|
||||
setDirty(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'save_continue') {
|
||||
byId('propertyForm').reset()
|
||||
state.contacts = [{ name:'', gender:'male', identity:'owner', phone1:'', phone2:'' }]
|
||||
state.orientation = ''; state.decoration = ''; state.usage = ''; state.shopLocation = ''
|
||||
state.status = 'for_sale'; state.attribute = 'public'
|
||||
buildOptions(); renderContacts(); updateStatusUI(); clearErrors()
|
||||
toast('success','保存成功','已保存当前房源,表单已重置,可继续新增。')
|
||||
window.scrollTo({ top:0, behavior:'smooth' })
|
||||
} else {
|
||||
toast('success','保存成功','已完成保存。原型阶段将于任务03接入详情页跳转。')
|
||||
}
|
||||
|
||||
setButtonsDisabled(false)
|
||||
setDirty(false)
|
||||
}
|
||||
|
||||
function bindAnchors() {
|
||||
const links = {
|
||||
core:q('[data-anchor-link="core"]'), contact:q('[data-anchor-link="contact"]'), basic:q('[data-anchor-link="basic"]'),
|
||||
trade:q('[data-anchor-link="trade"]'), related:q('[data-anchor-link="related"]')
|
||||
}
|
||||
const obs = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (!entry.isIntersecting) return
|
||||
const key = entry.target.dataset.sectionKey
|
||||
if (key==='trade' && byId('section-trade').classList.contains('hidden')) return
|
||||
Object.values(links).forEach(a => a && a.classList.remove('active'))
|
||||
if (links[key]) links[key].classList.add('active')
|
||||
})
|
||||
}, { rootMargin:'-35% 0px -55% 0px', threshold:0 })
|
||||
|
||||
;['section-core','section-contact','section-basic','section-trade','section-related'].forEach(id => {
|
||||
const el = byId(id); if (el) obs.observe(el)
|
||||
})
|
||||
links.core.classList.add('active')
|
||||
}
|
||||
|
||||
function initTheme() {
|
||||
const saved = localStorage.getItem('fonrey_theme') || 'light'
|
||||
document.documentElement.setAttribute('data-theme', saved)
|
||||
qa('[data-theme-btn]').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.themeBtn === saved)
|
||||
btn.addEventListener('click', () => {
|
||||
const t = btn.dataset.themeBtn
|
||||
document.documentElement.setAttribute('data-theme', t)
|
||||
localStorage.setItem('fonrey_theme', t)
|
||||
qa('[data-theme-btn]').forEach(x => x.classList.toggle('active', x === btn))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function init() {
|
||||
initTheme()
|
||||
buildNumberSelects()
|
||||
buildOptions()
|
||||
renderContacts()
|
||||
bindAnchors()
|
||||
|
||||
markActive('#statusGroup', 'data-status', state.status)
|
||||
markActive('#attributeGroup', 'data-attr', state.attribute)
|
||||
markActive('#propertyTypeGroup', 'data-type', state.propertyType)
|
||||
refreshTypeUI()
|
||||
|
||||
qa('#propertyTypeGroup [data-type]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
state.propertyType = btn.dataset.type
|
||||
state.usage = ''; state.shopLocation = ''
|
||||
clearErrors(); refreshTypeUI(); setDirty()
|
||||
})
|
||||
})
|
||||
|
||||
qa('#statusGroup [data-status]').forEach(btn => btn.addEventListener('click', () => { state.status = btn.dataset.status; updateStatusUI(); clearError('status'); setDirty() }))
|
||||
qa('#attributeGroup [data-attr]').forEach(btn => btn.addEventListener('click', () => { state.attribute = btn.dataset.attr; markActive('#attributeGroup','data-attr',state.attribute); clearError('attribute'); setDirty() }))
|
||||
|
||||
byId('addContactBtn').addEventListener('click', () => { state.contacts.push({ name:'', gender:'male', identity:'contact', phone1:'', phone2:'' }); renderContacts(); setDirty() })
|
||||
|
||||
byId('numberHolder').addEventListener('change', () => { clearError('numberHolder'); setDirty() })
|
||||
byId('sellerAgent').addEventListener('change', () => { clearError('sellerAgent'); setDirty() })
|
||||
byId('clearNumberHolder').addEventListener('click', () => { byId('numberHolder').value=''; setDirty() })
|
||||
byId('clearSellerAgent').addEventListener('click', () => { byId('sellerAgent').value=''; setDirty() })
|
||||
|
||||
qa('input,select,textarea').forEach(el => {
|
||||
el.addEventListener('input', () => setDirty())
|
||||
el.addEventListener('change', () => setDirty())
|
||||
})
|
||||
|
||||
byId('propertyForm').addEventListener('submit', e => { e.preventDefault(); submit('save') })
|
||||
byId('saveContinueBtn').addEventListener('click', () => submit('save_continue'))
|
||||
byId('cancelBtn').addEventListener('click', () => {
|
||||
if (!state.dirty) { window.location.href = './房源列表_UI.html'; return }
|
||||
if (window.confirm('当前有未保存内容,确认放弃并离开吗?')) {
|
||||
state.dirty = false
|
||||
window.location.href = './房源列表_UI.html'
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
if (state.dirty) { e.preventDefault(); e.returnValue = '' }
|
||||
})
|
||||
|
||||
setDirty(false)
|
||||
}
|
||||
|
||||
init()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
513
Project/fonrey/UI_DESIGN/楼盘列表_UI.html
Normal file
513
Project/fonrey/UI_DESIGN/楼盘列表_UI.html
Normal file
@@ -0,0 +1,513 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1280" />
|
||||
<title>Fonrey 楼盘列表 · 静态原型</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#F0FDFA',
|
||||
100: '#CCFBF1',
|
||||
200: '#99F6E4',
|
||||
500: '#14B8A6',
|
||||
600: '#0F766E',
|
||||
700: '#115E59',
|
||||
800: '#134E4A'
|
||||
},
|
||||
neutral: {
|
||||
50: '#F8FAFC',
|
||||
100: '#F1F5F9',
|
||||
200: '#E2E8F0',
|
||||
300: '#CBD5E1',
|
||||
400: '#94A3B8',
|
||||
500: '#64748B',
|
||||
600: '#475569',
|
||||
700: '#334155',
|
||||
800: '#1E293B',
|
||||
900: '#0F172A'
|
||||
},
|
||||
success: { 50: '#F0FDF4', 600: '#16A34A' },
|
||||
warning: { 50: '#FFFBEB', 600: '#D97706' },
|
||||
danger: { 50: '#FEF2F2', 600: '#DC2626' },
|
||||
info: { 50: '#EFF6FF', 600: '#2563EB' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root {
|
||||
--bg-page: #F8FAFC;
|
||||
--bg-card: #FFFFFF;
|
||||
--bg-subtle: #F1F5F9;
|
||||
--text-primary: #0F172A;
|
||||
--text-secondary: #64748B;
|
||||
--border: #E2E8F0;
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
transition: background-color .2s ease, color .2s ease;
|
||||
}
|
||||
[x-cloak] { display: none !important; }
|
||||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||
|
||||
.bg-surface { background: var(--bg-card); }
|
||||
.bg-subtle { background: var(--bg-subtle); }
|
||||
.border-surface { border-color: var(--border); }
|
||||
.text-surface { color: var(--text-primary); }
|
||||
.text-muted { color: var(--text-secondary); }
|
||||
|
||||
.module-tab {
|
||||
color: #64748B;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.module-tab.active {
|
||||
color: #0F766E;
|
||||
border-bottom-color: #0F766E;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid #E2E8F0;
|
||||
background: #FFFFFF;
|
||||
color: #64748B;
|
||||
}
|
||||
.chip.active {
|
||||
border-color: #0F766E;
|
||||
color: #0F766E;
|
||||
background: #F0FDFA;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-sm antialiased" x-data="complexListPage()" x-init="init()">
|
||||
<!-- Top Bar -->
|
||||
<header class="fixed top-0 left-0 right-0 h-14 z-20 bg-primary-800 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
|
||||
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
|
||||
<span class="text-base font-semibold text-white">Fonrey</span>
|
||||
</div>
|
||||
<nav class="flex items-center gap-1 flex-1 px-2" aria-label="主导航">
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">工作台</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md bg-primary-600 text-white font-medium">房源</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">客源</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">营销</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">交易</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">数据</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">人事</a>
|
||||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">系统</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-1 px-4 shrink-0">
|
||||
<button class="p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md" aria-label="消息">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022 23.848 23.848 0 0 0 5.454 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
|
||||
<div class="w-8 h-8 rounded-full bg-primary-600 text-white flex items-center justify-center text-sm font-semibold">魏</div>
|
||||
<span class="text-sm font-medium text-primary-100">魏深</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Side Bar -->
|
||||
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-surface bg-surface overflow-y-auto">
|
||||
<nav class="p-3 space-y-0.5">
|
||||
<div class="px-2 pt-2 pb-1 text-xs font-medium text-muted uppercase tracking-wide">房源管理</div>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md bg-primary-50 text-primary-700 font-medium">楼盘管理</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">房源列表</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">新增房源</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">成交房源</a>
|
||||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">已删房源</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main -->
|
||||
<main class="ml-60 pt-[72px] min-h-screen px-6 py-5">
|
||||
<div class="mx-auto max-w-[1680px] space-y-4">
|
||||
<!-- Breadcrumb + title -->
|
||||
<section class="bg-surface border border-surface rounded-lg p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<nav class="flex items-center gap-1 text-xs text-muted mb-2" aria-label="面包屑">
|
||||
<a href="#" class="hover:text-neutral-700">房源</a>
|
||||
<span>/</span>
|
||||
<a href="#" class="hover:text-neutral-700">小区</a>
|
||||
<span>/</span>
|
||||
<span class="text-surface">楼盘管理系统-楼盘管理</span>
|
||||
</nav>
|
||||
<h1 class="text-xl font-semibold text-surface">楼盘管理</h1>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Module Tabs -->
|
||||
<section class="bg-surface border border-surface rounded-lg px-4">
|
||||
<nav class="flex items-center gap-6 overflow-x-auto" aria-label="楼盘管理模块导航">
|
||||
<button class="module-tab py-3 whitespace-nowrap active">楼盘</button>
|
||||
<button class="module-tab py-3 whitespace-nowrap">区域管理</button>
|
||||
<button class="module-tab py-3 whitespace-nowrap">学校管理</button>
|
||||
<button class="module-tab py-3 whitespace-nowrap">应用标准数据</button>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<!-- Completeness metrics -->
|
||||
<section class="bg-info-50 border border-info-600/20 rounded-lg p-3">
|
||||
<div class="flex items-center justify-between gap-3 mb-2">
|
||||
<h2 class="text-sm font-semibold text-info-600">数据完整度指标</h2>
|
||||
<button class="text-xs px-2 py-1 rounded border border-info-600/30 text-info-600 bg-white hover:bg-info-50" @click="notify('已触发重新计算(原型)')">重新计算</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-2 text-xs">
|
||||
<template x-for="item in metrics" :key="item.name">
|
||||
<div class="bg-white rounded-md border border-info-600/10 p-2">
|
||||
<p class="text-muted" x-text="item.name"></p>
|
||||
<p class="font-semibold tabular-nums mt-0.5" x-text="item.value"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Search + Filter -->
|
||||
<section class="bg-surface border border-surface rounded-lg p-4 space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<input x-model.trim="filters.keyword" type="text" placeholder="楼盘名/别名/拼音/详细地址" class="w-[380px] px-3 py-2 rounded-md border border-surface bg-white focus:outline-none focus:ring-2 focus:ring-primary-600/40" />
|
||||
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="applyFilters()">查询</button>
|
||||
<button class="px-3 py-2 rounded-md border border-surface hover:bg-neutral-50" @click="resetFilters()">清除</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="text-xs text-muted">区域:</span>
|
||||
<template x-for="item in regionOptions" :key="item">
|
||||
<button class="chip px-2.5 py-1 rounded-md text-xs" :class="{ 'active': filters.region === item }" @click="filters.region = item" x-text="item"></button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="text-xs text-muted">用途:</span>
|
||||
<template x-for="item in usageOptions" :key="item">
|
||||
<button class="chip px-2.5 py-1 rounded-md text-xs" :class="{ 'active': filters.usage === item }" @click="filters.usage = item" x-text="item"></button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 gap-2">
|
||||
<template x-for="cfg in extraFilters" :key="cfg.key">
|
||||
<select x-model="filters[cfg.key]" class="px-2.5 py-2 rounded-md border border-surface bg-white text-xs focus:outline-none focus:ring-2 focus:ring-primary-600/40">
|
||||
<option value="" x-text="cfg.label + ':不限'"></option>
|
||||
<template x-for="opt in cfg.options" :key="opt">
|
||||
<option :value="opt" x-text="opt"></option>
|
||||
</template>
|
||||
</select>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Action bar -->
|
||||
<section class="bg-surface border border-surface rounded-lg p-3 flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="notify('新增楼盘(原型入口)')">+ 新增楼盘</button>
|
||||
<button class="px-3 py-1.5 rounded-md border border-surface" :disabled="selectedCount===0" :class="selectedCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'">批量新增楼栋</button>
|
||||
<button class="px-3 py-1.5 rounded-md border border-surface" :disabled="selectedCount===0" :class="selectedCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'">批改区域商圈</button>
|
||||
<button class="px-3 py-1.5 rounded-md border border-surface" :disabled="selectedCount===0" :class="selectedCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'">删除</button>
|
||||
<button class="px-3 py-1.5 rounded-md border border-surface" :disabled="selectedCount===0" :class="selectedCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'">合并楼盘</button>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-muted">
|
||||
<span x-show="selectedCount===0">未选中楼盘</span>
|
||||
<span x-show="selectedCount>0" x-text="'已选 ' + selectedCount + ' 条'"></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Table -->
|
||||
<section class="bg-surface border border-surface rounded-lg overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-subtle border-b border-surface">
|
||||
<tr class="text-left">
|
||||
<th scope="col" class="px-3 py-2 w-10">
|
||||
<input type="checkbox" class="rounded border-surface" :checked="allOnPageSelected" @click.prevent="toggleSelectPage(!allOnPageSelected)" aria-label="全选当前页" />
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-2">楼盘名称</th>
|
||||
<th scope="col" class="px-3 py-2">楼盘类型</th>
|
||||
<th scope="col" class="px-3 py-2">详细地址</th>
|
||||
<th scope="col" class="px-3 py-2">城区商圈</th>
|
||||
<th scope="col" class="px-3 py-2">当月挂牌均价(元/㎡)</th>
|
||||
<th scope="col" class="px-3 py-2">楼栋数</th>
|
||||
<th scope="col" class="px-3 py-2">产品数</th>
|
||||
<th scope="col" class="px-3 py-2">房源数</th>
|
||||
<th scope="col" class="px-3 py-2">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-if="paginatedRows.length === 0">
|
||||
<tr>
|
||||
<td colspan="10" class="px-4 py-12 text-center">
|
||||
<p class="text-base font-medium text-surface">暂无匹配楼盘</p>
|
||||
<p class="text-xs text-muted mt-1">请尝试调整筛选条件</p>
|
||||
<button class="mt-3 px-3 py-1.5 rounded-md border border-surface hover:bg-neutral-50" @click="resetFilters()">清除筛选</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template x-for="row in paginatedRows" :key="row.id">
|
||||
<tr class="border-b border-surface hover:bg-neutral-50/50">
|
||||
<td class="px-3 py-2 align-top">
|
||||
<input type="checkbox" class="rounded border-surface" :checked="isSelected(row.id)" @click.prevent="toggleSelect(row.id, !isSelected(row.id))" aria-label="选择楼盘" />
|
||||
</td>
|
||||
<td class="px-3 py-2 align-top">
|
||||
<button class="text-left text-info-600 hover:underline font-medium" @click="openDetail(row)" x-text="row.name"></button>
|
||||
<div class="mt-1 flex items-center gap-1 flex-wrap">
|
||||
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-info-50 text-info-600">信息</span>
|
||||
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-warning-50 text-warning-600">标准楼盘</span>
|
||||
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-danger-50 text-danger-600">标准房号</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2 align-top" x-text="row.usage"></td>
|
||||
<td class="px-3 py-2 align-top" x-text="row.address"></td>
|
||||
<td class="px-3 py-2 align-top" x-text="row.region + '-' + row.biz"></td>
|
||||
<td class="px-3 py-2 align-top tabular-nums" x-text="row.avgPrice"></td>
|
||||
<td class="px-3 py-2 align-top tabular-nums" x-text="row.buildingCount"></td>
|
||||
<td class="px-3 py-2 align-top tabular-nums" x-text="row.productCount"></td>
|
||||
<td class="px-3 py-2 align-top">
|
||||
<span class="text-xs text-muted">出售</span>
|
||||
<span class="tabular-nums" x-text="row.saleCount"></span>
|
||||
<span class="text-xs text-muted">/出租</span>
|
||||
<span class="tabular-nums" x-text="row.rentCount"></span>
|
||||
<span class="text-xs text-muted">/共</span>
|
||||
<span class="tabular-nums" x-text="row.saleCount + row.rentCount"></span>
|
||||
</td>
|
||||
<td class="px-3 py-2 align-top">
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="text-info-600 hover:underline text-xs" @click="notify('编辑楼盘(原型)')">编辑</button>
|
||||
<button class="text-danger-600 hover:underline text-xs" @click="notify('删除楼盘(原型)')">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pagination -->
|
||||
<section class="bg-surface border border-surface rounded-lg px-4 py-3 flex items-center justify-between">
|
||||
<p class="text-xs text-muted" x-text="'共 ' + filteredRows.length + ' 条'" aria-live="polite"></p>
|
||||
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<button class="px-2 py-1 rounded border border-surface" :disabled="currentPage===1" :class="currentPage===1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="goPrev()">上一页</button>
|
||||
<span class="tabular-nums" x-text="currentPage + ' / ' + totalPages"></span>
|
||||
<button class="px-2 py-1 rounded border border-surface" :disabled="currentPage===totalPages || totalPages===0" :class="(currentPage===totalPages || totalPages===0) ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="goNext()">下一页</button>
|
||||
|
||||
<span class="ml-2">20条/页</span>
|
||||
<span class="ml-2">跳至</span>
|
||||
<input type="number" min="1" :max="totalPages || 1" x-model.number="jumpPage" class="w-16 px-2 py-1 rounded border border-surface" />
|
||||
<button class="px-2 py-1 rounded border border-surface hover:bg-neutral-50" @click="goJump()">确定</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Toast -->
|
||||
<div x-show="toast.show" x-cloak class="fixed bottom-5 right-5 z-50 px-4 py-2 rounded-md text-sm text-white shadow-lg bg-success-600" x-text="toast.message"></div>
|
||||
|
||||
<script>
|
||||
function complexListPage() {
|
||||
return {
|
||||
rows: [],
|
||||
filters: {
|
||||
keyword: '',
|
||||
region: '不限',
|
||||
usage: '不限',
|
||||
fixedStatus: '',
|
||||
completion: '',
|
||||
complexType: '',
|
||||
hasProperty: '',
|
||||
buildingType: '',
|
||||
ownership: '',
|
||||
hasCoord: ''
|
||||
},
|
||||
regionOptions: ['不限', '静安', '闵行', '普陀', '松江', '长宁', '嘉定'],
|
||||
usageOptions: ['不限', '住宅', '别墅', '商住', '商业', '写字楼', '其他'],
|
||||
extraFilters: [
|
||||
{ key: 'fixedStatus', label: '固定情况', options: ['已固定', '未固定'] },
|
||||
{ key: 'completion', label: '完善情况', options: ['高', '中', '低'] },
|
||||
{ key: 'complexType', label: '楼盘类型', options: ['标准', '非标'] },
|
||||
{ key: 'hasProperty', label: '有无房源', options: ['有', '无'] },
|
||||
{ key: 'buildingType', label: '楼栋类型', options: ['板楼', '塔楼', '板塔结合'] },
|
||||
{ key: 'ownership', label: '权属关系', options: ['商品房', '房改房', '经济适用房'] },
|
||||
{ key: 'hasCoord', label: '有无坐标', options: ['有坐标', '无坐标'] }
|
||||
],
|
||||
metrics: [
|
||||
{ name: '楼盘关联率', value: '47.61%' },
|
||||
{ name: '楼栋及单元完整率', value: '100.38%' },
|
||||
{ name: '房号匹配率', value: '100%' },
|
||||
{ name: '处置率', value: '12.05%' },
|
||||
{ name: '入住人结构数据', value: '58 / 3000' },
|
||||
{ name: '有效结构数量', value: '523 / 523' },
|
||||
{ name: '房源对标', value: '1.83%' }
|
||||
],
|
||||
|
||||
pageSize: 20,
|
||||
currentPage: 1,
|
||||
jumpPage: 1,
|
||||
selectedIds: [],
|
||||
|
||||
toast: { show: false, message: '' },
|
||||
|
||||
init() {
|
||||
this.rows = this.mockRows();
|
||||
},
|
||||
|
||||
mockRows() {
|
||||
const seeds = [
|
||||
['都市港湾', '住宅', '嘉定', '丰庄', '上海 嘉定 海波路1000弄'],
|
||||
['阳光威尼斯四期', '别墅', '普陀', '真光', '上海 普陀 金鼎路1600弄'],
|
||||
['嘉城名都', '住宅', '嘉定', '江桥', '上海 嘉定 嘉城路188弄'],
|
||||
['中海臻如府', '商住', '普陀', '真如', '上海 普陀 真如路88弄'],
|
||||
['凯旋华庭', '写字楼', '长宁', '中山公园', '上海 长宁 凯旋路888号'],
|
||||
['虹桥商务中心', '商业', '闵行', '虹桥', '上海 闵行 申长路699号'],
|
||||
['静安云邸', '住宅', '静安', '大宁', '上海 静安 万荣路66弄'],
|
||||
['松江壹号院', '别墅', '松江', '大学城', '上海 松江 文汇路188号']
|
||||
];
|
||||
|
||||
const list = [];
|
||||
for (let i = 1; i <= 46; i++) {
|
||||
const s = seeds[(i - 1) % seeds.length];
|
||||
list.push({
|
||||
id: i,
|
||||
name: `${s[0]}${i}`,
|
||||
usage: s[1],
|
||||
region: s[2],
|
||||
biz: s[3],
|
||||
address: s[4],
|
||||
avgPrice: (32000 + i * 137).toFixed(2),
|
||||
buildingCount: (6 + (i % 16)),
|
||||
productCount: (50 + (i % 90)),
|
||||
saleCount: (i % 12),
|
||||
rentCount: (i % 8)
|
||||
});
|
||||
}
|
||||
return list;
|
||||
},
|
||||
|
||||
applyFilters() {
|
||||
this.currentPage = 1;
|
||||
this.jumpPage = 1;
|
||||
this.selectedIds = [];
|
||||
},
|
||||
|
||||
resetFilters() {
|
||||
this.filters = {
|
||||
keyword: '',
|
||||
region: '不限',
|
||||
usage: '不限',
|
||||
fixedStatus: '',
|
||||
completion: '',
|
||||
complexType: '',
|
||||
hasProperty: '',
|
||||
buildingType: '',
|
||||
ownership: '',
|
||||
hasCoord: ''
|
||||
};
|
||||
this.currentPage = 1;
|
||||
this.jumpPage = 1;
|
||||
this.selectedIds = [];
|
||||
},
|
||||
|
||||
get filteredRows() {
|
||||
const keyword = (this.filters.keyword || '').toLowerCase();
|
||||
return this.rows.filter((r) => {
|
||||
const hitKeyword = !keyword || [r.name, r.address, `${r.region}${r.biz}`].some(v => String(v).toLowerCase().includes(keyword));
|
||||
const hitRegion = this.filters.region === '不限' || r.region === this.filters.region;
|
||||
const hitUsage = this.filters.usage === '不限' || r.usage === this.filters.usage;
|
||||
return hitKeyword && hitRegion && hitUsage;
|
||||
});
|
||||
},
|
||||
|
||||
get totalPages() {
|
||||
return Math.max(1, Math.ceil(this.filteredRows.length / this.pageSize));
|
||||
},
|
||||
|
||||
get paginatedRows() {
|
||||
const page = Math.min(this.currentPage, this.totalPages);
|
||||
const start = (page - 1) * this.pageSize;
|
||||
return this.filteredRows.slice(start, start + this.pageSize);
|
||||
},
|
||||
|
||||
get selectedCount() {
|
||||
return this.selectedIds.length;
|
||||
},
|
||||
|
||||
isSelected(id) {
|
||||
return this.selectedIds.includes(id);
|
||||
},
|
||||
|
||||
toggleSelect(id, checked) {
|
||||
if (checked) {
|
||||
if (!this.selectedIds.includes(id)) this.selectedIds.push(id);
|
||||
} else {
|
||||
this.selectedIds = this.selectedIds.filter(x => x !== id);
|
||||
}
|
||||
},
|
||||
|
||||
get allOnPageSelected() {
|
||||
if (this.paginatedRows.length === 0) return false;
|
||||
return this.paginatedRows.every(r => this.selectedIds.includes(r.id));
|
||||
},
|
||||
|
||||
toggleSelectPage(checked) {
|
||||
const pageIds = this.paginatedRows.map(r => r.id);
|
||||
if (checked) {
|
||||
const set = new Set([...this.selectedIds, ...pageIds]);
|
||||
this.selectedIds = [...set];
|
||||
} else {
|
||||
this.selectedIds = this.selectedIds.filter(id => !pageIds.includes(id));
|
||||
}
|
||||
},
|
||||
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) this.currentPage -= 1;
|
||||
this.jumpPage = this.currentPage;
|
||||
this.selectedIds = [];
|
||||
},
|
||||
|
||||
goNext() {
|
||||
if (this.currentPage < this.totalPages) this.currentPage += 1;
|
||||
this.jumpPage = this.currentPage;
|
||||
this.selectedIds = [];
|
||||
},
|
||||
|
||||
goJump() {
|
||||
let p = Number(this.jumpPage || 1);
|
||||
if (!Number.isFinite(p)) p = 1;
|
||||
p = Math.max(1, Math.min(this.totalPages, Math.floor(p)));
|
||||
this.currentPage = p;
|
||||
this.jumpPage = p;
|
||||
this.selectedIds = [];
|
||||
},
|
||||
|
||||
openDetail(row) {
|
||||
window.location.hash = `complex/${row.id}`;
|
||||
this.notify(`进入楼盘详情(原型):${row.name}`);
|
||||
},
|
||||
|
||||
notify(message) {
|
||||
this.toast = { show: true, message };
|
||||
window.setTimeout(() => { this.toast.show = false; }, 1800);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
174
Project/fonrey/UI_DESIGN/楼盘管理/楼盘列表_UI.md
Normal file
174
Project/fonrey/UI_DESIGN/楼盘管理/楼盘列表_UI.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# 楼盘列表 UI 设计文档
|
||||
|
||||
> **任务编号**:04(P0-B)
|
||||
> **覆盖范围**:`US-COMPLEX-002`(经纪人查看楼盘列表与详情入口)
|
||||
> **输出文件**:`UI_DESIGN/楼盘列表_UI.html`
|
||||
> **设计基线**:`UI_SYSTEM/UI_SYSTEM.md`(列表页模板、Top Bar + Sidebar 壳层、分页规范)
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
### 1.1 页面目标
|
||||
|
||||
楼盘列表页用于经纪人和运营人员进行楼盘检索与筛选,并作为楼盘详情页的统一入口。
|
||||
|
||||
核心目标:
|
||||
|
||||
1. 支持关键词搜索(楼盘名称 / 别名 / 概要地址)
|
||||
2. 支持区域、用途等维度筛选
|
||||
3. 支持分页(默认 20 条/页 + 跳页)
|
||||
4. 列表中楼盘名称可点击进入楼盘详情
|
||||
5. 提供完整度统计面板与批量操作入口位(P0 原型占位)
|
||||
|
||||
### 1.2 本任务边界
|
||||
|
||||
本任务只交付“楼盘列表页”的静态高保真原型,不实现真实后端查询。
|
||||
|
||||
- ✅ 包含:筛选交互、分页演示、详情跳转入口位
|
||||
- ⛔ 不包含:真实接口联动、真实批量操作执行、真实权限校验
|
||||
|
||||
---
|
||||
|
||||
## 2. 信息架构
|
||||
|
||||
## 2.1 壳层结构(与新增客源/房源详情一致)
|
||||
|
||||
- **Top Bar(固定 56px)**:品牌 + 一级导航 + 用户区
|
||||
- **Sidebar(固定 240px)**:房源管理二级导航
|
||||
- **Main Content(`ml-60 pt-[72px]`)**:
|
||||
1. 面包屑 + 页面标题
|
||||
2. 模块 Tab(楼盘 / 区域管理 / 学校管理 / 应用标准数据)
|
||||
3. 完整度统计面板
|
||||
4. 搜索筛选区
|
||||
5. 批量操作条
|
||||
6. 列表表格
|
||||
7. 分页条
|
||||
|
||||
## 2.2 楼盘列表表格字段
|
||||
|
||||
| 列字段 | 说明 |
|
||||
|---|---|
|
||||
| 勾选 | 批量操作选择 |
|
||||
| 楼盘名称 | 蓝色可点击,进入详情页 |
|
||||
| 楼盘类型 | 住宅 / 别墅 / 商住 / 商业 / 写字楼 / 其他 |
|
||||
| 详细地址 | 城市 + 街道 / 弄号 |
|
||||
| 城区商圈 | 形如「嘉定-江桥新城」 |
|
||||
| 当月挂牌均价(元/㎡) | 数值,支持排序入口位 |
|
||||
| 楼栋数 | 数值 |
|
||||
| 产品数 | 数值 |
|
||||
| 房源数 | 出售N / 出租N / 共N |
|
||||
| 操作 | 编辑 / 删除 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 交互设计
|
||||
|
||||
## 3.1 搜索与筛选
|
||||
|
||||
### 3.1.1 关键词搜索
|
||||
|
||||
- 输入框占位:`楼盘名/别名/拼音/详细地址`
|
||||
- 点击「查询」执行前端过滤演示
|
||||
- 点击「清除」重置全部条件
|
||||
|
||||
### 3.1.2 快捷筛选
|
||||
|
||||
- 区域:不限、静安、闵行、普陀、松江、长宁、嘉定
|
||||
- 用途:不限、住宅、别墅、商住、商业、写字楼、其他
|
||||
|
||||
### 3.1.3 下拉筛选(占位)
|
||||
|
||||
- 固定情况
|
||||
- 完善情况
|
||||
- 楼盘类型
|
||||
- 有无房源
|
||||
- 楼栋类型
|
||||
- 权属关系
|
||||
- 有无坐标
|
||||
|
||||
> 以上下拉项在本原型为可见控件,不做真实复杂筛选逻辑。
|
||||
|
||||
## 3.2 批量操作
|
||||
|
||||
按钮区:
|
||||
|
||||
- + 新增楼盘(主 CTA)
|
||||
- 批量新增楼栋
|
||||
- 批改区域商圈
|
||||
- 删除
|
||||
- 合并楼盘
|
||||
|
||||
规则(原型态):
|
||||
|
||||
- 当未选择行时,批量按钮显示禁用态
|
||||
- 选中 1 条及以上时,禁用态解除(仅视觉)
|
||||
|
||||
## 3.3 列表与跳转
|
||||
|
||||
- 点击楼盘名称:跳转 `#/complex/{id}`(原型 hash)并 Toast 提示“进入楼盘详情(原型)”
|
||||
- 点击编辑 / 删除:仅触发提示,不执行真实动作
|
||||
|
||||
## 3.4 分页
|
||||
|
||||
- 默认每页 20 条
|
||||
- 支持上一页 / 下一页
|
||||
- 支持输入页码跳转(超范围自动纠正)
|
||||
- 展示 `共 X 条` + 当前页码
|
||||
|
||||
---
|
||||
|
||||
## 4. 状态矩阵
|
||||
|
||||
| 状态 | 触发 | 页面反馈 |
|
||||
|---|---|---|
|
||||
| 默认 | 首次加载 | 展示全部 mock 数据第一页 |
|
||||
| 搜索结果 | 输入关键词并查询 | 表格按关键词过滤 |
|
||||
| 空结果 | 条件过严 | 空状态(暂无匹配楼盘)+ 清除筛选 |
|
||||
| 行选中 | 勾选一行或全选 | 批量按钮解除禁用 |
|
||||
| 分页切换 | 点击页码/上一页/下一页 | 列表刷新为对应页 |
|
||||
| 页面主题 | 统一浅色管理后台主题 | 不在页面内提供 Light/Dark 切换控件 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据模型映射(DATA_MODEL_COMPLEX)
|
||||
|
||||
| UI 字段 | 数据模型字段 |
|
||||
|---|---|
|
||||
| 楼盘名称 | `complexes.name` |
|
||||
| 详细地址 | `complexes.address` |
|
||||
| 概要地址(搜索) | `complexes.address_summary` |
|
||||
| 城区 | `complexes.district_id -> districts.name` |
|
||||
| 主商圈 | `complex_business_areas.is_primary -> business_areas.name` |
|
||||
| 楼栋数 | `COUNT(buildings.id)` |
|
||||
| 出售/出租数量 | `properties.status` 聚合 |
|
||||
| 搜索向量 | `complexes.search_vector` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 可访问性与规范
|
||||
|
||||
- 表头使用语义化 `<th scope="col">`
|
||||
- 纯图标按钮添加 `aria-label`
|
||||
- 所有输入项含可见 label 或占位 + 分组标题
|
||||
- 焦点态统一 `focus-visible:ring-2`
|
||||
- 空状态提供主动作按钮(清除筛选)
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收清单
|
||||
|
||||
- [x] 壳层结构:Top Bar + Sidebar + Main
|
||||
- [x] 楼盘列表页模块 Tab 完整
|
||||
- [x] 统计面板、筛选区、批量区、表格区、分页区完整
|
||||
- [x] 默认 20 条/页的分页演示
|
||||
- [x] 楼盘名称可点击触发“进入详情”原型行为
|
||||
- [x] 页面内不包含主题切换控件(遵循统一后台视觉)
|
||||
- [x] 控制台 0 报错(本地预览验证阶段)
|
||||
|
||||
---
|
||||
|
||||
## 8. 后续衔接
|
||||
|
||||
- 本页评审通过后,进入任务05:楼盘详情/维护(`US-COMPLEX-001`)
|
||||
- 任务05将复用本页的详情入口与模块 Tab 风格,保证楼盘模块前后页体验一致
|
||||
Reference in New Issue
Block a user