修改文档

This commit is contained in:
Shen Wei
2026-04-28 07:34:37 +08:00
parent 3224ec4787
commit cf4001c4f8
16 changed files with 1489 additions and 171 deletions

View File

@@ -0,0 +1,287 @@
> **For AI assistants**: Read this entire file before designing or implementing any API. Contract rules here are mandatory. Do not invent per-module variants unless explicitly allowed.
# Fonrey API 契约规范API_CONTRACT
**版本**: 1.0
**适用范围**: 全模块account / permission / property / client / complex / org / setting
**关联总纲**: `TECH_STACK/TECH_STACK.md`
**最后更新**: 2026-04-27
---
## 1. 文档定位与原则
本文件定义 Fonrey 全局 API 契约标准,解决跨模块接口风格漂移问题。模块技术方案中的 API 章节必须遵循本文件,不得各自定义冲突规则。
### 1.1 强制级别
- **MUST**:必须遵守,违反视为缺陷
- **SHOULD**:建议遵守,若不遵守需在模块文档注明原因
- **MAY**:可选能力
### 1.2 适用接口类型
- JSON API`/api/**`
- HTMX 片段端点HTML response
- 文件上传/下载端点
---
## 2. 请求 / 响应格式规范
## 2.1 请求体规范JSON API
- `Content-Type` MUST 为 `application/json`
- `charset=utf-8` SHOULD 显式声明
- 写操作POST/PUT/PATCHMUST 传业务 payload禁止空对象写入
推荐请求结构:
```json
{
"data": {
"...": "业务字段"
},
"meta": {
"request_id": "可选,客户端透传"
}
}
```
兼容说明:历史端点已存在 `filters/sort/pagination` 平铺结构时可继续使用,但新接口 SHOULD 迁移到 `data` 容器。
## 2.2 成功响应规范JSON API
成功响应 MUST 使用统一 envelope
```json
{
"ok": true,
"data": {},
"meta": {
"request_id": "uuid",
"timestamp": "2026-04-27T16:30:00+08:00"
}
}
```
说明:
- `ok` MUST 为 `true`
- `data` MUST 存在(可为空对象 `{}` 或空数组 `[]`
- `meta` SHOULD 包含 `request_id` 与服务端时间
## 2.3 失败响应规范JSON API
失败响应 MUST 使用统一 envelope
```json
{
"ok": false,
"error": "权限不足",
"code": "PROPERTY_PERMISSION_DENIED",
"details": {},
"meta": {
"request_id": "uuid",
"timestamp": "2026-04-27T16:30:00+08:00"
}
}
```
说明:
- `error` MUST 为面向用户/调用方可读消息
- `code` MUST 为稳定机器可读码(大写下划线)
- `details` MAY 提供字段级错误(如校验失败)
## 2.4 HTMX 响应规范
- 成功:返回 HTML 片段;必要时通过 `HX-Trigger` 触发前端事件
- 失败:
- 状态码 MUST 正确4xx/5xx
- SHOULD 在响应头返回 `HX-Trigger`,例如:
- `{"toast:error":"权限不足"}`
- `{"toast:error":"请求失败,请重试"}`
---
## 3. 错误码规范
## 3.1 命名规则
- 错误码 MUST 为 `UPPER_SNAKE_CASE`
- 推荐前缀:`<MODULE>_`(如 `PROPERTY_` / `CLIENT_` / `ORG_`
## 3.2 HTTP 状态码基线
| HTTP | 使用场景 | 示例 code |
|---|---|---|
| 400 | 参数错误、业务前置条件不满足 | `PROPERTY_VALIDATION_ERROR` |
| 401 | 仅用于纯 API Token 鉴权失败(当前 Web 会话模式一般不用) | `AUTH_UNAUTHORIZED` |
| 403 | 已登录但无权限 | `*_PERMISSION_DENIED` |
| 404 | 资源不存在或不可见 | `*_NOT_FOUND` |
| 409 | 状态冲突、任务未就绪 | `*_STATE_CONFLICT` / `*_JOB_NOT_READY` |
| 422 | 字段级校验错误(可选) | `*_VALIDATION_FAILED` |
| 429 | 频控触发 | `RATE_LIMITED` |
| 500 | 未预期异常 | `INTERNAL_ERROR` |
## 3.3 稳定性要求
- `code` MUST 可稳定依赖,不得频繁改名
- 错误文案 `error` 可优化,但不应影响调用方流程判断
---
## 4. 分页规范
Fonrey 列表查询 MUST 使用 Keyset 分页;禁止 OFFSET 深分页。
## 4.1 请求格式
```json
{
"filters": {},
"sort": {"field": "updated_at", "order": "desc"},
"pagination": {"mode": "keyset", "cursor": null, "limit": 20}
}
```
## 4.2 响应格式
```json
{
"ok": true,
"data": {
"items": [],
"next_cursor": "opaque_cursor_2"
},
"meta": {
"pagination": {"mode": "keyset", "cursor": "opaque_cursor", "limit": 20}
}
}
```
## 4.3 约束
- `limit` MUST 有上限(建议 ≤ 100
- `cursor` MUST 为不透明字符串,禁止暴露内部排序字段组合
- 排序字段 MUST 来自白名单,防止 SQL 注入与慢查询
---
## 5. 搜索 / 筛选规范
## 5.1 推荐请求结构
```json
{
"filters": {
"keyword": "保利",
"status": ["active", "pending"],
"district_id": "uuid"
},
"sort": {"field": "updated_at", "order": "desc"},
"pagination": {"mode": "keyset", "cursor": null, "limit": 20}
}
```
## 5.2 语义规范
- `keyword`:模糊检索词(服务端统一做 trim
- 多选条件 MUST 使用数组(如 `status: []`
- 空数组 `[]` 语义:不限制该条件
- `null` 语义:由模块文档明确(默认建议等同“不传”)
## 5.3 安全与性能
- 仅允许白名单字段参与筛选和排序
- LIKE/全文检索字段 SHOULD 建立索引或搜索策略
- 查询快照哈希(用于缓存/导出SHOULD 对 filters+sort+scope 进行规范化后计算
---
## 6. 上传规范
Fonrey 优先采用“预签名上传 + 回执提交commit”两段式。
## 6.1 标准流程
1. 客户端请求 upload-token业务 API
2. 客户端直传对象存储R2
3. 客户端调用 commit API 回写元数据
## 6.2 合约要求
- upload-token MUST 短时有效(建议 5~15 分钟)
- commit MUST 幂等(建议支持 `idempotency_key`
- 上传白名单与大小限制 MUST 在模块文档声明并在服务端校验
- SHOULD 校验 `content_type``size`
- MAY 增加 `sha256` 校验确保完整性
## 6.3 错误码建议
- `*_UPLOAD_TOKEN_EXPIRED` (409/400)
- `*_UPLOAD_FILE_TOO_LARGE` (400)
- `*_UPLOAD_FILE_TYPE_NOT_ALLOWED` (400)
- `*_UPLOAD_COMMIT_CONFLICT` (409)
---
## 7. 文件下载规范
下载统一采用“导出任务 + 状态查询 + download endpoint”。
## 7.1 标准流程
1. 创建导出任务 `POST /api/**/export/jobs/`
2. 轮询任务状态 `GET /api/**/export/jobs/{job_id}/`
3. 下载结果 `GET /api/**/export/jobs/{job_id}/download/`
## 7.2 合约要求
- 任务未完成下载 MUST 返回 `409` + `*_EXPORT_JOB_NOT_READY`
- 下载链接 SHOULD 为一次性或短时有效 URL
- 响应 SHOULD 设置 `Content-Disposition`(附件下载)
- 文件名 SHOULD 带模块与日期,便于审计
---
## 8. 权限拒绝返回规范
## 8.1 JSON API
- 未登录MUST 返回 `302`Web Session 场景)或 `401`(纯 API 场景)
- 已登录无权限MUST 返回 `403`
- 失败体 MUST 使用统一错误 envelope`code``*_PERMISSION_DENIED`
## 8.2 页面路由SSR/HTMX
- 未登录302 跳转登录页
- 已登录无权限403 页面(或 HTMX 403 片段)
- HTMX 拒绝 SHOULD 触发 `HX-Trigger` toast 事件
## 8.3 测试强约束
每个受保护端点 MUST 覆盖三态:
- 200有权限
- 403已登录无权限
- 302/401未登录视端点类型
---
## 9. 与模块文档的衔接规则
- 各模块技术方案中的“四、API 设计原则”“六、关键 API 规范”“十二、错误码建议”必须引用本文件
- 模块文档可补充模块特有 code 与字段,但不得与本规范冲突
- 冲突时以本文件为准;若需例外,必须在模块文档显式记录 ADR 链接
---
## 10. 落地检查清单Review Checklist
- [ ] 是否使用统一成功/失败 envelope
- [ ] 错误码是否为稳定 `UPPER_SNAKE_CASE`
- [ ] 列表接口是否全部 Keyset 分页
- [ ] filters/sort 字段是否白名单化
- [ ] 上传是否采用 token+commit 且具备幂等保障
- [ ] 下载是否采用 job 流程并处理未就绪 409
- [ ] 权限拒绝是否遵循 200/403/302(401) 三态
- [ ] 测试是否覆盖契约关键路径

View File

@@ -2,7 +2,7 @@
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked. > **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
**版本**: 2.1 **最后更新**: 2026-04-27 **版本**: 2.2 **最后更新**: 2026-04-27
**定位**: 本文档是 Fonrey 项目技术栈的**总索引**。所有跨模块的技术决策、版本约束、目录规范、禁止项在此定稿;**单一模块的具体技术方案**数据模型、服务层、HTMX 交互、Celery 任务等)见各自子文档(见 §9 索引)。 **定位**: 本文档是 Fonrey 项目技术栈的**总索引**。所有跨模块的技术决策、版本约束、目录规范、禁止项在此定稿;**单一模块的具体技术方案**数据模型、服务层、HTMX 交互、Celery 任务等)见各自子文档(见 §9 索引)。
--- ---
@@ -128,18 +128,19 @@ apps/property/
每个模块的具体技术决策模型字段、服务层、缓存策略、HTMX/Celery 集成等)见对应子文档: 每个模块的具体技术决策模型字段、服务层、缓存策略、HTMX/Celery 集成等)见对应子文档:
| 模块 | 技术方案文档 | PRD | 数据模型 | | 模块 | 技术方案文档 | PRD | 数据模型 | 测试文件 | 最近版本 |
| ----- | ---------------------------------- | -------------------------- | ------------------------------------- | | ----- | ---------------------------------- | -------------------------- | ------------------------------------- | --- | --- |
| 登录认证 | [`登录管理技术方案.md`](./登录管理技术方案.md) | `PRD/登录管理/` | `DATA_MODEL/DATA_MODEL_LOGIN.md` | | 登录认证 | [`登录管理技术方案.md`](./登录管理技术方案.md) | `PRD/登录管理/` | `DATA_MODEL/DATA_MODEL_LOGIN.md` | `tests/integration/account/test_us_account.py` | `v3.1` |
| 权限管理 | [`权限管理系统技术方案.md`](./权限管理系统技术方案.md) | `PRD/权限管理/` | `DATA_MODEL/DATA_MODEL_PERMISSION.md` | | 权限管理 | [`权限管理系统技术方案.md`](./权限管理系统技术方案.md) | `PRD/权限管理/` | `DATA_MODEL/DATA_MODEL_PERMISSION.md` | `tests/integration/permission/test_us_permission.py` | `v2.1` |
| 房源管理 | [`房源管理技术方案.md`](./房源管理技术方案.md) | `PRD/房源管理/` | `DATA_MODEL/DATA_MODEL_PROPERTY.md` | | 房源管理 | [`房源管理技术方案.md`](./房源管理技术方案.md) | `PRD/房源管理/` | `DATA_MODEL/DATA_MODEL_PROPERTY.md` | `tests/integration/property/test_us_property.py` | `v1.0` |
| 客源管理 | [`客源管理技术方案.md`](./客源管理技术方案.md) | `PRD/客源管理/` | `DATA_MODEL/DATA_MODEL_CLIENT.md` | | 客源管理 | [`客源管理技术方案.md`](./客源管理技术方案.md) | `PRD/客源管理/` | `DATA_MODEL/DATA_MODEL_CLIENT.md` | `tests/integration/client/test_us_client.py` | `v1.0` |
| 楼盘管理 | [`楼盘管理技术方案.md`](./楼盘管理技术方案.md) | `PRD/房源管理/`(含楼盘) | `DATA_MODEL/DATA_MODEL_COMPLEX.md` | | 楼盘管理 | [`楼盘管理技术方案.md`](./楼盘管理技术方案.md) | `PRD/房源管理/`(含楼盘) | `DATA_MODEL/DATA_MODEL_COMPLEX.md` | `tests/integration/complex/test_us_complex.py` | `v1.0` |
| 组织人事 | [`组织人事技术方案.md`](./组织人事技术方案.md) | `PRD/组织人事管理/` | `DATA_MODEL/DATA_MODEL_ORG.md` | | 组织人事 | [`组织人事技术方案.md`](./组织人事技术方案.md) | `PRD/组织人事管理/` | `DATA_MODEL/DATA_MODEL_ORG.md` | `tests/integration/org/test_us_org.py` | `v1.0` |
| 系统设置 | [`系统设置技术方案.md`](./系统设置技术方案.md) | `PRD/系统配置/``PRD/系统管理/` | `DATA_MODEL/DATA_MODEL_SETTING.md` | | 系统设置 | [`系统设置技术方案.md`](./系统设置技术方案.md) | `PRD/系统配置/``PRD/系统管理/` | `DATA_MODEL/DATA_MODEL_SETTING.md` | `tests/integration/setting/test_us_setting.py` | `v1.2` |
| 客户端发布 | 见本文档 §7 | `PRD/发布管理/客户端发布管理模块PRD.md` | — | | 客户端发布 | 见本文档 §7 | `PRD/发布管理/客户端发布管理模块PRD.md` | — | — | `§7本文` |
**总览数据模型**[`DATA_MODEL/DATA_MODEL.md`](../DATA_MODEL/DATA_MODEL.md) **总览数据模型**[`DATA_MODEL/DATA_MODEL.md`](../DATA_MODEL/DATA_MODEL.md)
**全局 API 契约**[`API_CONTRACT.md`](./API_CONTRACT.md)
**MVP 范围与产品总览**[`PRD/PRD_MVP.md`](../PRD/PRD_MVP.md) **MVP 范围与产品总览**[`PRD/PRD_MVP.md`](../PRD/PRD_MVP.md)
--- ---
@@ -231,6 +232,7 @@ Fonrey 采用 AI vibe coding 模式开发,测试是保证每日迭代质量的
- 本文档仅记录**跨模块共识**与**模块索引**,不展开模块细节 - 本文档仅记录**跨模块共识**与**模块索引**,不展开模块细节
- 模块技术方案在子文档中维护,并通过 §8 表格回链 - 模块技术方案在子文档中维护,并通过 §8 表格回链
- API 契约总则在 `API_CONTRACT.md` 维护;模块文档只做模块特化,不得覆盖全局契约
- 任何技术栈变更(替换组件、升级主版本、新增外部服务)须同步更新本文档 §2、§5、§6 - 任何技术栈变更(替换组件、升级主版本、新增外部服务)须同步更新本文档 §2、§5、§6
- 新增模块时,先在 §4 目录结构补位,再在 §8 索引登记子文档 - 新增模块时,先在 §4 目录结构补位,再在 §8 索引登记子文档
- 测试规范变更须同步更新 §10 关键结论,完整细节在 [`测试规范.md`](./测试规范.md) 中维护 - 测试规范变更须同步更新 §10 关键结论,完整细节在 [`测试规范.md`](./测试规范.md) 中维护

View File

@@ -7,6 +7,7 @@
**技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery + Cloudflare R2 **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery + Cloudflare R2
**关联 PRD**: `PRD/客源管理/客源管理模块PRD.md`v1.4 **关联 PRD**: `PRD/客源管理/客源管理模块PRD.md`v1.4
**关联数据模型**: `DATA_MODEL/DATA_MODEL_CLIENT.md`(本方案不重复 DDL **关联数据模型**: `DATA_MODEL/DATA_MODEL_CLIENT.md`(本方案不重复 DDL
**关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威)
**关联枚举字典**: `DATA_MODEL/ENUMS.md` **关联枚举字典**: `DATA_MODEL/ENUMS.md`
**最后更新**: 2026-04-27 **最后更新**: 2026-04-27

View File

@@ -7,6 +7,7 @@
**技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery + Cloudflare R2 **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery + Cloudflare R2
**关联 PRD**: `PRD/房源管理/房源管理模块PRD.md`v2.1 **关联 PRD**: `PRD/房源管理/房源管理模块PRD.md`v2.1
**关联数据模型**: `DATA_MODEL/DATA_MODEL_PROPERTY.md`(本方案不重复 DDL **关联数据模型**: `DATA_MODEL/DATA_MODEL_PROPERTY.md`(本方案不重复 DDL
**关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威)
**关联枚举字典**: `DATA_MODEL/ENUMS.md` **关联枚举字典**: `DATA_MODEL/ENUMS.md`
**最后更新**: 2026-04-27 **最后更新**: 2026-04-27

View File

@@ -7,6 +7,7 @@
**技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis
**关联 PRD**: `PRD/权限管理/权限管理模块PRD.md` **关联 PRD**: `PRD/权限管理/权限管理模块PRD.md`
**关联数据模型**: `DATA_MODEL/DATA_MODEL_PERMISSION.md`(本方案不重复 DDL **关联数据模型**: `DATA_MODEL/DATA_MODEL_PERMISSION.md`(本方案不重复 DDL
**关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威)
**最后更新**: 2026-04-27 **最后更新**: 2026-04-27
--- ---

View File

@@ -7,6 +7,7 @@
**技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery + Cloudflare R2 **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery + Cloudflare R2
**关联 PRD**: `PRD/房源管理/楼盘管理模块PRD.md`v1.0 **关联 PRD**: `PRD/房源管理/楼盘管理模块PRD.md`v1.0
**关联数据模型**: `DATA_MODEL/DATA_MODEL_COMPLEX.md`(本方案不重复 DDL **关联数据模型**: `DATA_MODEL/DATA_MODEL_COMPLEX.md`(本方案不重复 DDL
**关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威)
**关联枚举字典**: `DATA_MODEL/ENUMS.md` **关联枚举字典**: `DATA_MODEL/ENUMS.md`
**最后更新**: 2026-04-27 **最后更新**: 2026-04-27

View File

@@ -7,6 +7,7 @@
**技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery
**关联 PRD**: `PRD/登录管理/用户登录管理模块PRD.md` **关联 PRD**: `PRD/登录管理/用户登录管理模块PRD.md`
**关联数据模型**: `DATA_MODEL/DATA_MODEL_LOGIN.md`(本方案不重复 DDL **关联数据模型**: `DATA_MODEL/DATA_MODEL_LOGIN.md`(本方案不重复 DDL
**关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威)
**最后更新**: 2026-04-27 **最后更新**: 2026-04-27
--- ---

View File

@@ -7,6 +7,7 @@
**技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis
**关联 PRD**: `PRD/系统配置/系统配置模块PRD.md` **关联 PRD**: `PRD/系统配置/系统配置模块PRD.md`
**关联数据模型**: `DATA_MODEL/DATA_MODEL_SETTING.md`(本方案不重复 DDL **关联数据模型**: `DATA_MODEL/DATA_MODEL_SETTING.md`(本方案不重复 DDL
**关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威)
**关联枚举字典**: `DATA_MODEL/ENUMS.md` **关联枚举字典**: `DATA_MODEL/ENUMS.md`
**最后更新**: 2026-04-27 **最后更新**: 2026-04-27

View File

@@ -7,6 +7,7 @@
**技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery
**关联 PRD**: `PRD/组织人事管理/组织人事管理模块PRD.md`v1.2 **关联 PRD**: `PRD/组织人事管理/组织人事管理模块PRD.md`v1.2
**关联数据模型**: `DATA_MODEL/DATA_MODEL_ORG.md`(本方案不重复 DDL **关联数据模型**: `DATA_MODEL/DATA_MODEL_ORG.md`(本方案不重复 DDL
**关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威)
**关联枚举字典**: `DATA_MODEL/ENUMS.md` **关联枚举字典**: `DATA_MODEL/ENUMS.md`
**最后更新**: 2026-04-27 **最后更新**: 2026-04-27

View File

@@ -32,7 +32,7 @@
| 序号 | 优先级 | 模块 | 覆盖 US | UI.md 目标文件 | HTML 目标文件 | 当前状态 | 下一步 | | 序号 | 优先级 | 模块 | 覆盖 US | UI.md 目标文件 | HTML 目标文件 | 当前状态 | 下一步 |
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|
| 01 | P0-A | 登录管理 | US-ACCOUNT-001~003 | `UI_DESIGN/登录管理/登录_UI.md` | `UI_DESIGN/登录_UI.html` | 设计中 | 先输出登录 UI.md第一个任务 | | 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后开始 | | 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后开始 | | 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后开始 | | 04 | P0-B | 楼盘管理(列表) | US-COMPLEX-002 | `UI_DESIGN/楼盘管理/楼盘列表_UI.md` | `UI_DESIGN/楼盘列表_UI.html` | 待设计 | 完成任务03后开始 |

View File

@@ -1111,5 +1111,32 @@
</div><!-- /主容器 --> </div><!-- /主容器 -->
<script>
// 静态原型串联:从登录页跳转后显示一次登录成功提示
(function () {
const params = new URLSearchParams(window.location.search);
if (params.get('from') === 'login' && params.get('login') === 'success') {
const displayName = params.get('name') || '经纪人';
const toast = document.createElement('div');
toast.className = 'fixed bottom-6 right-6 z-[70] w-80 bg-white rounded-lg shadow-lg border border-neutral-200 flex items-start gap-3 p-3';
toast.innerHTML = `
<svg class="w-5 h-5 text-success-600 mt-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
<div class="min-w-0">
<p class="text-sm font-medium text-neutral-800">登录成功</p>
<p class="text-xs text-neutral-500 mt-0.5">欢迎回来,${displayName}</p>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.transition = 'opacity .25s ease';
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 260);
}, 2200);
}
})();
</script>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,440 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<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>
<style>
[x-cloak] { display: none !important; }
.tabular-nums { font-variant-numeric: tabular-nums; }
.captcha-track { background: linear-gradient(90deg, #E2E8F0 0%, #F1F5F9 100%); }
.captcha-success { background: linear-gradient(90deg, #dcfce7 0%, #bbf7d0 100%); }
</style>
</head>
<body class="min-h-screen bg-neutral-50 font-sans text-sm text-neutral-700 antialiased" x-data="loginPrototype()">
<div class="fixed inset-0 -z-10">
<div class="absolute inset-y-0 left-0 w-[56%] bg-gradient-to-br from-primary-800 via-primary-700 to-primary-600"></div>
<div class="absolute -top-16 -left-24 w-96 h-96 rounded-full bg-white/10 blur-2xl"></div>
<div class="absolute bottom-0 left-1/3 w-80 h-80 rounded-full bg-primary-200/15 blur-2xl"></div>
</div>
<main class="mx-auto max-w-[1440px] min-h-screen grid grid-cols-12">
<section class="col-span-7 px-12 py-12 text-white flex flex-col justify-between">
<div>
<div class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-white/10 border border-white/20">
<div class="w-7 h-7 rounded-md bg-primary-500/90 flex items-center justify-center text-white font-semibold">F</div>
<span class="text-base font-semibold">Fonrey 房睿</span>
</div>
<h1 class="mt-8 text-4xl font-semibold leading-tight">面向经纪业务的<br>高密度工作台</h1>
<p class="mt-4 text-primary-100 text-base leading-7 max-w-xl">
多租户隔离、角色权限控制、房客源高频操作一致体验。
本页面原型覆盖 Tenant 识别、账号密码登录、验证码验证、锁定与会话过期等 P0 场景。
</p>
</div>
<div class="grid grid-cols-2 gap-4 max-w-2xl">
<div class="rounded-lg border border-white/20 bg-white/10 p-4">
<div class="text-xs text-primary-100">多租户识别</div>
<div class="mt-1 text-xl font-semibold tabular-nums">12位 Tenant ID</div>
</div>
<div class="rounded-lg border border-white/20 bg-white/10 p-4">
<div class="text-xs text-primary-100">安全策略</div>
<div class="mt-1 text-xl font-semibold tabular-nums">5次失败锁定30分钟</div>
</div>
</div>
</section>
<section class="col-span-5 px-10 py-10 flex items-center justify-center">
<div class="w-full max-w-md rounded-xl bg-white border border-neutral-200 shadow-lg p-6">
<template x-if="view === 'tenant'">
<div x-cloak>
<h2 class="text-xl font-semibold text-neutral-800">欢迎使用 Fonrey 房睿</h2>
<p class="mt-2 text-sm text-neutral-500">请输入您公司的专属识别码,以进入对应租户登录页</p>
<div class="mt-6 space-y-1.5">
<label for="tenant-id" class="block text-sm font-medium text-neutral-700">公司识别码Tenant ID<span class="text-danger-600">*</span></label>
<input
id="tenant-id"
type="text"
inputmode="numeric"
maxlength="12"
:disabled="tenantLoading"
x-model="tenantId"
@input="sanitizeTenantId"
placeholder="请输入12位数字识别码"
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20 disabled:bg-neutral-100 disabled:text-neutral-400"
aria-describedby="tenant-help tenant-error"
>
<p id="tenant-help" class="text-xs text-neutral-500">支持粘贴,系统将自动去除空格与非数字字符</p>
<div class="min-h-[22px]">
<p id="tenant-error" x-show="tenantError" x-text="tenantError" class="text-xs text-danger-600"></p>
</div>
</div>
<button
type="button"
@click="submitTenant"
:disabled="tenantLoading"
class="mt-1 inline-flex w-full items-center justify-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg x-show="tenantLoading" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.75 7.25a6.5 6.5 0 1 0 0 9.5"/></svg>
<span x-text="tenantLoading ? '识别中…' : '确认'"></span>
</button>
<template x-if="tenantNetworkError">
<div class="mt-3 rounded-md border border-warning-600/30 bg-warning-50 px-3 py-2 text-xs text-warning-600 flex items-center justify-between">
<span>网络连接失败,请检查网络后重试</span>
<button @click="tenantNetworkError=false" class="text-primary-600 hover:underline">重试</button>
</div>
</template>
<p class="mt-4 text-xs text-neutral-500">不知道识别码?请联系您公司的系统管理员</p>
</div>
</template>
<template x-if="view === 'login'">
<div x-cloak>
<div class="flex items-center justify-between mb-4">
<div class="min-w-0">
<p class="text-xs text-neutral-500">正在登录</p>
<p class="text-sm font-semibold text-neutral-800 truncate" x-text="tenantName"></p>
</div>
<button type="button" @click="openSwitchModal = true" class="text-sm text-primary-600 hover:text-primary-700 hover:underline underline-offset-2">切换公司</button>
</div>
<template x-if="sessionExpiredNotice">
<div class="mb-3 rounded-md border border-warning-600/30 bg-warning-50 px-3 py-2 text-xs text-warning-600 flex items-start justify-between gap-2">
<span>登录已过期,请重新登录</span>
<button @click="sessionExpiredNotice=false" class="text-neutral-500 hover:text-neutral-700" aria-label="关闭会话过期提示"></button>
</div>
</template>
<h2 class="text-xl font-semibold text-neutral-800">账号登录</h2>
<p class="mt-1 text-sm text-neutral-500">请输入用户名和密码,并完成行为验证</p>
<form class="mt-5 space-y-4" @submit.prevent="submitLogin">
<div class="space-y-1">
<label for="username" class="block text-sm font-medium text-neutral-700">用户名<span class="text-danger-600">*</span></label>
<input id="username" type="text" x-model.trim="username" placeholder="请输入用户名"
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20"
:disabled="loginLoading || accountState==='locked' || accountState==='disabled'">
</div>
<div class="space-y-1">
<label for="password" class="block text-sm font-medium text-neutral-700">密码<span class="text-danger-600">*</span></label>
<div class="relative">
<input id="password" :type="passwordVisible ? 'text' : 'password'" x-model="password" placeholder="请输入密码"
class="block w-full px-3 py-2 pr-10 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20"
:disabled="loginLoading || accountState==='locked' || accountState==='disabled'">
<button type="button" @click="passwordVisible=!passwordVisible" class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-neutral-500 hover:text-neutral-700" aria-label="显示或隐藏密码">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.644C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.009 9.963 7.178.07.207.07.431 0 .638C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.009-9.964-7.178Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/></svg>
</button>
</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<label class="block text-sm font-medium text-neutral-700">行为验证<span class="text-danger-600">*</span></label>
<button type="button" @click="refreshCaptcha" class="inline-flex items-center gap-1 text-xs text-neutral-500 hover:text-neutral-700" aria-label="刷新验证码">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182"/></svg>
刷新
</button>
</div>
<div class="rounded-lg border border-neutral-200 p-3">
<div class="relative h-24 rounded-md overflow-hidden bg-gradient-to-r from-neutral-100 to-neutral-200">
<div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_30%,rgba(15,118,110,0.18),transparent_45%),radial-gradient(circle_at_80%_70%,rgba(37,99,235,0.15),transparent_45%)]"></div>
<div class="absolute top-7 h-10 w-9 rounded bg-neutral-300/90 border border-neutral-400/70" :style="`left:${captchaTarget}%`"></div>
<div class="absolute top-7 h-10 w-9 rounded border border-primary-600/70 bg-primary-100/90 transition-all" :style="`left: calc(${sliderValue}% - 18px)`"></div>
</div>
<div class="mt-2 rounded-md p-2 border"
:class="captchaState==='pass' ? 'captcha-success border-success-600/30' : 'captcha-track border-neutral-200'">
<input type="range" min="0" max="100" step="1" x-model="sliderValue" @change="verifyCaptcha" @input="captchaState='idle'"
:disabled="captchaState==='pass' || loginLoading || accountState==='locked' || accountState==='disabled'"
class="w-full accent-primary-600">
</div>
<p class="mt-1 text-xs"
:class="captchaState==='pass' ? 'text-success-600' : (captchaState==='fail' ? 'text-danger-600' : 'text-neutral-500')"
x-text="captchaState==='pass' ? '验证通过' : (captchaState==='fail' ? '验证失败,请重试' : '拖动滑块完成拼图')"></p>
</div>
</div>
<template x-if="loginError">
<div class="rounded-md border border-danger-600/30 bg-danger-50 px-3 py-2 text-xs text-danger-600" x-text="loginError"></div>
</template>
<button type="submit"
class="inline-flex w-full items-center justify-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!canSubmit || loginLoading || accountState==='locked' || accountState==='disabled'">
<svg x-show="loginLoading" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.75 7.25a6.5 6.5 0 1 0 0 9.5"/></svg>
<span x-text="loginLoading ? '登录中…' : '登录'"></span>
</button>
</form>
<div class="mt-3 flex items-center justify-between text-xs">
<a href="javascript:void(0)" class="text-primary-600 hover:text-primary-700 hover:underline underline-offset-2">忘记用户名</a>
<a href="javascript:void(0)" class="text-primary-600 hover:text-primary-700 hover:underline underline-offset-2">忘记密码</a>
</div>
<div class="mt-4 border-t border-neutral-200 pt-4 space-y-2">
<button type="button" disabled class="w-full px-3 py-2 rounded-md border border-neutral-300 bg-neutral-100 text-neutral-400 cursor-not-allowed text-sm">手机验证码登录(即将开放)</button>
<button type="button" disabled class="w-full px-3 py-2 rounded-md border border-neutral-300 bg-neutral-100 text-neutral-400 cursor-not-allowed text-sm">微信扫码登录(即将开放)</button>
</div>
<details class="mt-4 rounded-md border border-neutral-200 bg-neutral-50 p-2">
<summary class="cursor-pointer text-xs text-neutral-500">原型状态切换(仅评审演示)</summary>
<div class="mt-2 grid grid-cols-2 gap-2 text-xs">
<button @click="simulateInvalidCredential" class="px-2 py-1.5 rounded border border-neutral-300 bg-white hover:bg-neutral-100">模拟账号密码错误</button>
<button @click="simulateLock" class="px-2 py-1.5 rounded border border-neutral-300 bg-white hover:bg-neutral-100">模拟账号锁定</button>
<button @click="simulateDisabled" class="px-2 py-1.5 rounded border border-neutral-300 bg-white hover:bg-neutral-100">模拟账号停用</button>
<button @click="sessionExpiredNotice=true" class="px-2 py-1.5 rounded border border-neutral-300 bg-white hover:bg-neutral-100">模拟会话过期</button>
<button @click="resetLoginState" class="col-span-2 px-2 py-1.5 rounded border border-neutral-300 bg-white hover:bg-neutral-100">重置状态</button>
</div>
</details>
</div>
</template>
</div>
</section>
</main>
<div x-show="openSwitchModal" x-cloak class="fixed inset-0 z-50" @keydown.escape.window="openSwitchModal=false">
<div class="absolute inset-0 bg-neutral-900/40" @click="openSwitchModal=false"></div>
<div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
<div class="w-full max-w-sm bg-white rounded-xl shadow-lg border border-neutral-200 pointer-events-auto">
<div class="p-5 text-center space-y-3">
<div class="mx-auto w-12 h-12 rounded-full bg-warning-50 flex items-center justify-center">
<svg class="w-6 h-6 text-warning-600" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9.303 3.376c.866 1.5-.217 3.374-1.948 3.374H4.646c-1.73 0-2.813-1.874-1.948-3.374l7.354-12.748c.866-1.5 3.03-1.5 3.896 0l7.355 12.748Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5h.008v.008H12v-.008Z"/></svg>
</div>
<h3 class="text-base font-semibold text-neutral-800">切换公司</h3>
<p class="text-sm text-neutral-500">切换公司将清除当前租户识别信息,并返回识别页。是否继续?</p>
</div>
<div class="flex items-center justify-center gap-2 px-5 py-3 border-t border-neutral-200 bg-neutral-50">
<button @click="openSwitchModal=false" class="px-4 py-1.5 text-sm border border-neutral-300 rounded-md bg-white hover:bg-neutral-50">取消</button>
<button @click="confirmSwitchCompany" class="px-4 py-1.5 text-sm rounded-md bg-danger-600 text-white hover:bg-danger-600/90">继续切换</button>
</div>
</div>
</div>
</div>
<script>
function loginPrototype() {
return {
view: 'tenant',
tenantId: '',
tenantName: '',
tenantLoading: false,
tenantError: '',
tenantNetworkError: false,
username: '',
password: '',
passwordVisible: false,
captchaTarget: 46,
sliderValue: 0,
captchaState: 'idle',
loginLoading: false,
loginError: '',
accountState: 'active',
failedCount: 0,
sessionExpiredNotice: false,
openSwitchModal: false,
sanitizeTenantId() {
this.tenantId = this.tenantId.replace(/\D/g, '').slice(0, 12)
this.tenantError = ''
this.tenantNetworkError = false
},
submitTenant() {
this.tenantError = ''
this.tenantNetworkError = false
if (this.tenantId.length !== 12) {
this.tenantError = '识别码须为 12 位数字'
return
}
this.tenantLoading = true
setTimeout(() => {
this.tenantLoading = false
if (this.tenantId === '999999999999') {
this.tenantNetworkError = true
return
}
if (this.tenantId === '202500010001') {
this.tenantName = '沪居地产(演示租户)'
localStorage.setItem('tenant_id', this.tenantId)
localStorage.setItem('tenant_name', this.tenantName)
// 串联到 Story 2 独立登录页
this.view = 'login'
setTimeout(() => {
window.location.href = `./登录_账号密码_UI.html?tenantId=${this.tenantId}&tenantName=${encodeURIComponent(this.tenantName)}`
}, 350)
this.resetLoginState()
} else {
this.tenantError = '识别码无效,请联系您的系统管理员获取正确的识别码'
}
}, 800)
},
refreshCaptcha() {
this.captchaTarget = Math.floor(Math.random() * 60) + 20
this.sliderValue = 0
this.captchaState = 'idle'
},
verifyCaptcha() {
const diff = Math.abs(this.sliderValue - this.captchaTarget)
if (diff <= 3) {
this.captchaState = 'pass'
this.loginError = ''
} else {
this.captchaState = 'fail'
setTimeout(() => this.refreshCaptcha(), 700)
}
},
get canSubmit() {
return this.username.trim() && this.password && this.captchaState === 'pass'
},
submitLogin() {
this.loginError = ''
if (this.accountState === 'locked') {
this.loginError = '账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁'
return
}
if (this.accountState === 'disabled') {
this.loginError = '账号已停用,请联系您的管理员'
return
}
if (!this.canSubmit) {
this.loginError = '请先完成用户名、密码和行为验证'
return
}
this.loginLoading = true
setTimeout(() => {
this.loginLoading = false
const credentialPass = this.username === 'agent_001' && this.password === 'Fonrey@2025'
if (credentialPass) {
this.loginError = ''
this.failedCount = 0
this.sessionExpiredNotice = false
this.password = ''
this.refreshCaptcha()
// 静态原型串联:登录成功后跳转到主页(当前用房源列表页作为首页)
setTimeout(() => {
window.location.href = './房源列表_UI.html?from=login&login=success'
}, 350)
return
}
this.failedCount += 1
this.loginError = '用户名或密码错误,请重新输入'
this.password = ''
this.refreshCaptcha()
if (this.failedCount >= 5) {
this.accountState = 'locked'
this.loginError = '账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁'
}
}, 900)
},
simulateInvalidCredential() {
this.loginError = '用户名或密码错误,请重新输入'
this.password = ''
this.refreshCaptcha()
},
simulateLock() {
this.accountState = 'locked'
this.loginError = '账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁'
},
simulateDisabled() {
this.accountState = 'disabled'
this.loginError = '账号已停用,请联系您的管理员'
},
resetLoginState() {
this.username = ''
this.password = ''
this.passwordVisible = false
this.loginLoading = false
this.loginError = ''
this.failedCount = 0
this.accountState = 'active'
this.sessionExpiredNotice = false
this.refreshCaptcha()
},
confirmSwitchCompany() {
this.openSwitchModal = false
this.view = 'tenant'
this.tenantName = ''
this.tenantId = ''
this.tenantError = ''
this.tenantNetworkError = false
this.resetLoginState()
}
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,316 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280">
<title>Fonrey 登录管理 · 账号密码登录Story 2</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>
[x-cloak] { display: none !important; }
.captcha-track { background: linear-gradient(90deg, #E2E8F0 0%, #F1F5F9 100%); }
.captcha-success { background: linear-gradient(90deg, #dcfce7 0%, #bbf7d0 100%); }
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}
.captcha-shake { animation: shake .22s linear 2; }
</style>
</head>
<body class="min-h-screen bg-neutral-50 font-sans text-sm text-neutral-700 antialiased" x-data="story2LoginPage()">
<div class="fixed inset-0 -z-10">
<div class="absolute inset-y-0 left-0 w-[56%] bg-gradient-to-br from-primary-800 via-primary-700 to-primary-600"></div>
<div class="absolute -top-16 -left-24 w-96 h-96 rounded-full bg-white/10 blur-2xl"></div>
<div class="absolute bottom-0 left-1/3 w-80 h-80 rounded-full bg-primary-200/15 blur-2xl"></div>
</div>
<main class="mx-auto max-w-[1440px] min-h-screen grid grid-cols-12">
<section class="col-span-7 px-12 py-12 text-white flex flex-col justify-between">
<div>
<div class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-white/10 border border-white/20">
<div class="w-7 h-7 rounded-md bg-primary-500/90 flex items-center justify-center text-white font-semibold">F</div>
<span class="text-base font-semibold">Fonrey 房睿</span>
</div>
<h1 class="mt-8 text-4xl font-semibold leading-tight">经纪人账号登录</h1>
<p class="mt-4 text-primary-100 text-base leading-7 max-w-xl">
已完成 Tenant 识别,请使用经纪人账号和密码登录。
本页对应 PRD《用户登录管理模块》User Story 2。
</p>
</div>
<div class="grid grid-cols-2 gap-4 max-w-2xl">
<div class="rounded-lg border border-white/20 bg-white/10 p-4">
<div class="text-xs text-primary-100">当前租户</div>
<div class="mt-1 text-xl font-semibold truncate" x-text="tenantName || '未识别租户'"></div>
</div>
<div class="rounded-lg border border-white/20 bg-white/10 p-4">
<div class="text-xs text-primary-100">登录策略</div>
<div class="mt-1 text-xl font-semibold">账号密码 + 滑块验证</div>
</div>
</div>
</section>
<section class="col-span-5 px-10 py-10 flex items-center justify-center">
<div class="w-full max-w-md rounded-xl bg-white border border-neutral-200 shadow-lg p-6">
<template x-if="tenantMissing">
<div class="rounded-md border border-warning-600/30 bg-warning-50 px-3 py-2 text-xs text-warning-600 mb-4 flex items-center justify-between gap-2">
<span>未检测到有效 Tenant 信息,请先完成识别</span>
<a href="./登录_UI.html" class="text-primary-600 hover:underline">返回识别页</a>
</div>
</template>
<div class="flex items-center justify-between mb-4">
<div class="min-w-0">
<p class="text-xs text-neutral-500">正在登录</p>
<p class="text-sm font-semibold text-neutral-800 truncate" x-text="tenantName || '租户未识别'"></p>
</div>
<button type="button" @click="openSwitchModal = true" class="text-sm text-primary-600 hover:text-primary-700 hover:underline underline-offset-2">切换公司</button>
</div>
<template x-if="sessionExpiredNotice">
<div class="mb-3 rounded-md border border-warning-600/30 bg-warning-50 px-3 py-2 text-xs text-warning-600 flex items-start justify-between gap-2">
<span>登录已过期,请重新登录</span>
<button @click="sessionExpiredNotice=false" class="text-neutral-500 hover:text-neutral-700" aria-label="关闭会话过期提示"></button>
</div>
</template>
<h2 class="text-xl font-semibold text-neutral-800">账号登录</h2>
<p class="mt-1 text-sm text-neutral-500">请输入用户名和密码,并完成行为验证</p>
<form class="mt-5 space-y-4" @submit.prevent="submitLogin">
<div class="space-y-1">
<label for="username" class="block text-sm font-medium text-neutral-700">用户名<span class="text-danger-600">*</span></label>
<input id="username" type="text" x-model.trim="username" placeholder="请输入用户名" maxlength="50"
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20"
:disabled="loginLoading || accountState==='locked' || accountState==='disabled' || tenantMissing">
<p class="text-xs text-neutral-500">支持英文字母、数字、下划线,最大 50 字符</p>
</div>
<div class="space-y-1">
<label for="password" class="block text-sm font-medium text-neutral-700">密码<span class="text-danger-600">*</span></label>
<div class="relative">
<input id="password" :type="passwordVisible ? 'text' : 'password'" x-model="password" placeholder="请输入密码"
class="block w-full px-3 py-2 pr-10 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20"
:disabled="loginLoading || accountState==='locked' || accountState==='disabled' || tenantMissing">
<button type="button" @click="passwordVisible=!passwordVisible" class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-neutral-500 hover:text-neutral-700" aria-label="显示或隐藏密码">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.644C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.009 9.963 7.178.07.207.07.431 0 .638C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.009-9.964-7.178Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/></svg>
</button>
</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<label class="block text-sm font-medium text-neutral-700">行为验证<span class="text-danger-600">*</span></label>
<button type="button" @click="refreshCaptcha" class="inline-flex items-center gap-1 text-xs text-neutral-500 hover:text-neutral-700" aria-label="刷新验证码">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182"/></svg>
刷新
</button>
</div>
<div :class="captchaState==='fail' ? 'captcha-shake' : ''" class="rounded-lg border border-neutral-200 p-3">
<div class="relative h-24 rounded-md overflow-hidden bg-gradient-to-r from-neutral-100 to-neutral-200">
<div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_30%,rgba(15,118,110,0.18),transparent_45%),radial-gradient(circle_at_80%_70%,rgba(37,99,235,0.15),transparent_45%)]"></div>
<div class="absolute top-7 h-10 w-9 rounded bg-neutral-300/90 border border-neutral-400/70" :style="`left:${captchaTarget}%`"></div>
<div class="absolute top-7 h-10 w-9 rounded border border-primary-600/70 bg-primary-100/90 transition-all" :style="`left: calc(${sliderValue}% - 18px)`"></div>
</div>
<div class="mt-2 rounded-md p-2 border" :class="captchaState==='pass' ? 'captcha-success border-success-600/30' : 'captcha-track border-neutral-200'">
<input type="range" min="0" max="100" step="1" x-model="sliderValue" @change="verifyCaptcha" @input="captchaState='idle'"
:disabled="captchaState==='pass' || loginLoading || accountState==='locked' || accountState==='disabled' || tenantMissing"
class="w-full accent-primary-600">
</div>
<p class="mt-1 text-xs"
:class="captchaState==='pass' ? 'text-success-600' : (captchaState==='fail' ? 'text-danger-600' : 'text-neutral-500')"
x-text="captchaState==='pass' ? '验证通过' : (captchaState==='fail' ? '验证码有误,请重新输入' : '拖动滑块完成拼图')"></p>
</div>
</div>
<template x-if="loginError">
<div class="rounded-md border border-danger-600/30 bg-danger-50 px-3 py-2 text-xs text-danger-600" x-text="loginError"></div>
</template>
<button type="submit"
class="inline-flex w-full items-center justify-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!canSubmit || loginLoading || accountState==='locked' || accountState==='disabled' || tenantMissing">
<svg x-show="loginLoading" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.75 7.25a6.5 6.5 0 1 0 0 9.5"/></svg>
<span x-text="loginLoading ? '登录中…' : '登录'"></span>
</button>
</form>
<div class="mt-3 flex items-center justify-between text-xs">
<a href="javascript:void(0)" class="text-primary-600 hover:text-primary-700 hover:underline underline-offset-2">忘记用户名</a>
<a href="javascript:void(0)" class="text-primary-600 hover:text-primary-700 hover:underline underline-offset-2">忘记密码</a>
</div>
<div class="mt-4 border-t border-neutral-200 pt-4 space-y-2">
<button type="button" disabled class="w-full px-3 py-2 rounded-md border border-neutral-300 bg-neutral-100 text-neutral-400 cursor-not-allowed text-sm">手机验证码登录(即将开放)</button>
<button type="button" disabled class="w-full px-3 py-2 rounded-md border border-neutral-300 bg-neutral-100 text-neutral-400 cursor-not-allowed text-sm">微信扫码登录(即将开放)</button>
</div>
</div>
</section>
</main>
<div x-show="openSwitchModal" x-cloak class="fixed inset-0 z-50" @keydown.escape.window="openSwitchModal=false">
<div class="absolute inset-0 bg-neutral-900/40" @click="openSwitchModal=false"></div>
<div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
<div class="w-full max-w-sm bg-white rounded-xl shadow-lg border border-neutral-200 pointer-events-auto">
<div class="p-5 text-center space-y-3">
<div class="mx-auto w-12 h-12 rounded-full bg-warning-50 flex items-center justify-center">
<svg class="w-6 h-6 text-warning-600" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9.303 3.376c.866 1.5-.217 3.374-1.948 3.374H4.646c-1.73 0-2.813-1.874-1.948-3.374l7.354-12.748c.866-1.5 3.03-1.5 3.896 0l7.355 12.748Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5h.008v.008H12v-.008Z"/></svg>
</div>
<h3 class="text-base font-semibold text-neutral-800">切换公司</h3>
<p class="text-sm text-neutral-500">切换公司将清除当前租户识别信息,并返回识别页。是否继续?</p>
</div>
<div class="flex items-center justify-center gap-2 px-5 py-3 border-t border-neutral-200 bg-neutral-50">
<button @click="openSwitchModal=false" class="px-4 py-1.5 text-sm border border-neutral-300 rounded-md bg-white hover:bg-neutral-50">取消</button>
<button @click="confirmSwitchCompany" class="px-4 py-1.5 text-sm rounded-md bg-danger-600 text-white hover:bg-danger-600/90">继续切换</button>
</div>
</div>
</div>
</div>
<script>
function story2LoginPage() {
return {
tenantId: '',
tenantName: '',
tenantMissing: false,
username: '',
password: '',
passwordVisible: false,
captchaTarget: 46,
sliderValue: 0,
captchaState: 'idle',
loginLoading: false,
loginError: '',
accountState: 'active',
failedCount: 0,
sessionExpiredNotice: false,
openSwitchModal: false,
init() {
const params = new URLSearchParams(window.location.search)
const tenantId = params.get('tenantId') || localStorage.getItem('tenant_id') || ''
const tenantName = params.get('tenantName') || localStorage.getItem('tenant_name') || ''
this.tenantId = tenantId
this.tenantName = tenantName
this.tenantMissing = !tenantId || !tenantName
this.sessionExpiredNotice = params.get('reason') === 'session_expired'
this.refreshCaptcha()
},
refreshCaptcha() {
this.captchaTarget = Math.floor(Math.random() * 60) + 20
this.sliderValue = 0
this.captchaState = 'idle'
},
verifyCaptcha() {
const diff = Math.abs(this.sliderValue - this.captchaTarget)
if (diff <= 3) {
this.captchaState = 'pass'
this.loginError = ''
} else {
this.captchaState = 'fail'
setTimeout(() => this.refreshCaptcha(), 700)
}
},
get canSubmit() {
return this.username.trim() && this.password && this.captchaState === 'pass'
},
submitLogin() {
this.loginError = ''
if (this.accountState === 'locked') {
this.loginError = '账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁'
return
}
if (this.accountState === 'disabled') {
this.loginError = '账号已停用,请联系您的管理员'
return
}
if (!this.username.trim()) {
this.loginError = '请输入用户名'
return
}
if (!this.password) {
this.loginError = '请输入密码'
return
}
if (this.captchaState !== 'pass') {
this.loginError = '请输入验证码'
return
}
this.loginLoading = true
setTimeout(() => {
this.loginLoading = false
const credentialPass = this.username === 'agent_001' && this.password === 'Fonrey@2025'
if (credentialPass) {
this.loginError = ''
this.failedCount = 0
this.password = ''
this.refreshCaptcha()
const displayName = '王顺'
window.location.href = `./房源列表_UI.html?from=login&login=success&name=${encodeURIComponent(displayName)}`
return
}
this.failedCount += 1
this.loginError = '用户名或密码错误,请重新输入'
this.password = ''
this.refreshCaptcha()
if (this.failedCount >= 5) {
this.accountState = 'locked'
this.loginError = '账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁'
}
}, 900)
},
confirmSwitchCompany() {
this.openSwitchModal = false
localStorage.removeItem('tenant_id')
localStorage.removeItem('tenant_name')
window.location.href = './登录_UI.html?from=switch-company'
}
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,239 @@
# 登录管理 UI 设计文档
> **版本**v1.0 · **日期**2026-04-27
> **依赖规范**`UI_SYSTEM/UI_SYSTEM.md v1.2`、`UI_SYSTEM/组件规范设计.md v1.0`
> **PRD 来源**`PRD/登录管理/用户登录管理模块PRD.md`Story 1、Story 2 + 会话相关要求)
> **数据模型来源**`DATA_MODEL/DATA_MODEL_LOGIN.md`
> **技术约束来源**`TECH_STACK/登录管理技术方案.md`
---
## 1. 模块概述
### 1.1 设计目标P0
本设计文档覆盖 `TASK.md` 的登录管理 P0 范围:
- **US-ACCOUNT-001**:账号密码登录(含验证码、错误提示、锁定态)
- **US-ACCOUNT-002**多租户识别Tenant ID 识别与切换公司)
- **US-ACCOUNT-003**Token/会话超时相关前端状态(过期提示、重新登录入口)
### 1.2 页面职责
| 页面 | URL 建议 | 优先级 | 对应 US |
|---|---|---|---|
| Tenant 识别页 | `/account/tenant/verify/` | P0 🔴 | US-ACCOUNT-002 |
| 登录页 | `/account/login/` | P0 🔴 | US-ACCOUNT-001 / US-ACCOUNT-003 |
| 切换公司确认弹窗 | 登录页内 Modal | P0 🔴 | US-ACCOUNT-002 |
| 会话过期提示态 | 登录页内 Alert/Toast | P0 🔴 | US-ACCOUNT-003 |
> 注:`忘记用户名/忘记密码` 链接在登录页展示;其完整流程页面在后续登录模块增强迭代中展开。
---
## 2. 视觉与组件基线(对齐 UI System
### 2.1 色彩与层级
- 页面背景:`bg-neutral-50`
- 登录主按钮:`bg-primary-600 hover:bg-primary-700 active:bg-primary-800`
- 错误提示:`text-danger-600`
- 成功提示:`text-success-600`
- 卡片容器:`bg-white border border-neutral-200 rounded-xl shadow-lg`
### 2.2 组件复用清单
| 场景 | 组件规范来源 | 使用说明 |
|---|---|---|
| 主/次按钮 | `UI_SYSTEM.md §3.1` | 登录=Primary刷新验证码/切换公司=Secondary/Link |
| 输入框/密码框 | `UI_SYSTEM.md §3.2` | 统一 Label 在上、错误提示在下、密码可见切换 |
| 确认弹窗 | `UI_SYSTEM.md §3.6` | 切换公司二次确认使用 Confirm Modal |
| Toast 提示 | `UI_SYSTEM.md §3.8` | 网络异常、登录成功/失败统一 Toast 反馈 |
| 登录页布局模板 | `UI_SYSTEM.md §5.7` | 独立布局,无 Sidebar品牌区 + 表单区 |
### 2.3 主题策略说明
依据 `UI_SYSTEM.md §9.1`**v1 仅 Light 主题**。
本页面不提供用户可见主题切换按钮,但保留 `data-theme="light"` 扩展点,为后续主题系统接入预留。
---
## 3. 页面设计规范
## 3.1 Tenant 识别页P0 🔴)
### 3.1.1 页面结构
```
┌────────────────────────────────────────────────────────────┐
│ 左侧品牌区Logo + Slogan + 租户价值说明) │
│ 右侧识别卡片 │
│ 标题:欢迎使用 Fonrey 房睿 │
│ 描述:请输入您公司的专属识别码 │
│ [公司识别码输入框] │
│ [确认按钮] │
│ 错误提示区(固定高度,防布局抖动) │
│ 帮助文案:不知道识别码?请联系管理员 │
└────────────────────────────────────────────────────────────┘
```
### 3.1.2 字段与校验
| 字段 | 类型 | 必填 | 规则 |
|---|---|---|---|
| Tenant ID | 文本输入(仅数字) | 是 | 固定 12 位;自动 trim非数字过滤 |
### 3.1.3 交互状态
| 状态 | 触发 | 视觉反馈 |
|---|---|---|
| Idle | 首次进入 | 按钮可点击(输入满足 12 位) |
| Loading | 点击“确认”后 | 按钮 Loading + 禁用;输入框禁用 |
| Success | 验证通过 | 展示租户名,自动跳转登录页 |
| Invalid | Tenant 无效 | 输入框下方红色文案:识别码无效… |
| Network Error | 请求失败/超时 | 错误提示 + “重试”按钮 |
### 3.1.4 API 对齐
- `POST /api/auth/tenant/verify/`PRD
- 请求体:`{ tenant_id }`
- 成功返回:`tenant_name / tenant_logo_url / login_url`
- 失败返回:`TENANT_NOT_FOUND`
---
## 3.2 登录页P0 🔴)
### 3.2.1 布局结构
```
┌────────────────────────────────────────────────────────────┐
│ 左侧品牌区(租户 Logo / 公司名 / 产品卖点) │
│ 右侧登录卡片max-w-md
│ [用户名] │
│ [密码 + 显示/隐藏] │
│ [滑块拼图验证区域 + 刷新] │
│ [登录按钮] │
│ [忘记用户名] [忘记密码] │
│ [手机验证码登录(即将开放,禁用)] │
│ [微信扫码登录(即将开放,禁用)] │
│ [切换公司]Link
└────────────────────────────────────────────────────────────┘
```
### 3.2.2 字段规范
| 字段 | 组件 | 必填 | 校验 | 数据模型映射 |
|---|---|---|---|---|
| 用户名 | Input | 是 | 1~50 字符;允许字母/数字/下划线(兼容管理员) | `user_accounts.username` |
| 密码 | Password Input | 是 | 非空;提交后后端校验 | `user_accounts.password`(哈希) |
| 验证码通过票据 | 滑块拼图区域 | 是 | 位置偏差 ±5px + 轨迹特征校验 | Redis `captcha_pass:*` |
### 3.2.3 主要交互规则
1. 用户名/密码/验证码三者满足后,“登录”按钮可点击。
2. 点击登录后按钮进入 `loading`,避免重复提交。
3. 登录失败(账号或密码错误):
- 统一提示 `用户名或密码错误,请重新输入`
- 自动刷新验证码
- 清空密码,保留用户名
4. 验证码失败:提示 `验证码有误,请重新验证`,不计入密码错误次数。
5. 连续密码错误 ≥ 5 次:
- 展示 `账号已被临时锁定请30分钟后重试`
- 登录按钮禁用
6. 账号停用:提示 `账号已停用,请联系管理员`
7. Session 过期跳转后,顶部显示提示条:`登录已过期,请重新登录`
8. 登录成功后,前端跳转到首页路由(本静态原型当前映射为 `./房源列表_UI.html?from=login&login=success`,后续可替换为正式 `/home/`)。
### 3.2.4 登录页状态矩阵
| 状态 | 触发条件 | UI 表现 |
|---|---|---|
| Default | 初始打开 | 空表单 + 新验证码 |
| Captcha Passed | 验证通过 | 验证区绿色对勾 + 文案 |
| Submitting | 点击登录后 | 按钮 spinner表单禁用 |
| Invalid Credential | 401 | 错误 Alert + 密码清空 |
| Locked | 423/锁定态 | 锁定警示条 + 按钮 disabled |
| Disabled | 账号停用 | 错误提示 + 禁止提交 |
| Session Expired | 过期重定向 | 顶部 warning 条 |
---
## 3.3 切换公司确认弹窗P0 🔴)
### 3.3.1 触发入口
- 登录卡片底部 Link`切换公司`
### 3.3.2 弹窗内容
- 标题:`切换公司`
- 文案:`切换公司将清除当前租户识别信息,并返回识别页。是否继续?`
- 按钮:`取消`Secondary/ `继续切换`Danger
### 3.3.3 行为
- 确认后:清除本地 tenant 缓存并跳转 `/account/tenant/verify/`
- 取消后:关闭弹窗,不改变当前状态
---
## 3.4 会话过期提示P0 🔴)
- 场景:用户访问业务页时 Session 失效,被重定向回登录页
- 位置:登录卡片顶部 Alertwarning
- 文案:`登录已过期,请重新登录`
- 可关闭:是(仅隐藏提示,不恢复会话)
---
## 4. 与数据模型/技术方案映射
## 4.1 关键字段映射
| UI 关注点 | 数据模型字段/实体 | 说明 |
|---|---|---|
| 账号状态 | `user_accounts.status` | `active/disabled/locked` 驱动登录态文案 |
| 锁定截止时间 | `user_accounts.locked_until` | 锁定倒计时文案来源 |
| 初始密码标记 | `user_accounts.is_initial_password` | 登录成功后是否强制跳转改密页 |
| 登录失败计数 | Redis `login_fail:{tenant}:{username}` | 达阈值触发锁定 |
| 登录审计 | `login_attempts` | 失败原因不在前端细分展示 |
## 4.2 API 映射(前端使用)
| 目标 | 接口 |
|---|---|
| 租户识别 | `/api/auth/tenant/verify/` |
| 获取验证码 | `/api/account/captcha/generate/` |
| 校验验证码 | `/api/account/captcha/verify/` |
| 登录提交 | `/api/account/login/` |
| 登出 | `/api/account/logout/` |
---
## 5. 可访问性与易用性
1. 所有输入框均有可见 Label不使用仅 placeholder 方案。
2. 错误信息与字段通过 `aria-describedby` 关联。
3. 图标按钮(显示密码、刷新验证码)必须有 `aria-label`
4. `Tab` 顺序Tenant ID/用户名 → 密码 → 验证区 → 登录按钮 → 辅助链接。
5. Enter 键:当表单合法时触发提交。
---
## 6. 交付物与实现顺序
1. 本文档:`UI_DESIGN/登录管理/登录_UI.md`(当前)
2. 静态原型:`UI_DESIGN/登录_UI.html`(基于本文档)
3. 评审后迭代:先改 HTML再回写本 UI 文档
---
## 7. 验收检查清单UI 维度)
- [ ] Tenant ID 12 位数字校验与错误提示完整
- [ ] 登录页三要素(用户名/密码/验证码)联动提交规则完整
- [ ] 锁定态、停用态、会话过期态均有明确视觉反馈
- [ ] 切换公司有二次确认弹窗
- [ ] 所有颜色/按钮/输入框样式遵循 UI_SYSTEM Token 与组件规范
- [ ] 静态页可用于你进行第一轮视觉与交互评审

View File

@@ -1,102 +1,102 @@
--- ---
title: GitHub 上 5000 人收藏的 Vibe Coding 神级指南。 title: GitHub 上 5000 人收藏的 Vibe Coding 神级指南。
source: https://mp.weixin.qq.com/s/QMPMSGW6XXk8L-yx4ujQcw source: https://mp.weixin.qq.com/s/QMPMSGW6XXk8L-yx4ujQcw
author: shenwei author: shenwei
published: published:
created: 2025-12-30 created: 2025-12-30
description: description:
tags: [ai, github, vibe-coding] tags: [ai, github, vibe-coding]
--- ---
#vibe-coding #ai #github #vibe-coding #ai #github
![cover_image](https://mmbiz.qpic.cn/sz_mmbiz_jpg/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8X6dFlWpZWiaKic2p2Jk4d5GicMaRbaAqvDEpOIH818qqAYKicNEo5voykBg/0?wx_fmt=jpeg) ![cover_image](https://mmbiz.qpic.cn/sz_mmbiz_jpg/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8X6dFlWpZWiaKic2p2Jk4d5GicMaRbaAqvDEpOIH818qqAYKicNEo5voykBg/0?wx_fmt=jpeg)
原创 逛逛 [逛逛GitHub](https://mp.weixin.qq.com/s/) *2025年12月27日 15:03* 原创 逛逛 [逛逛GitHub](https://mp.weixin.qq.com/s/) *2025年12月27日 15:03*
Vibe Coding 说白了就是开发个应用不再像程序员一样,苦哈哈地写每一行代码,而是化身为导演。 Vibe Coding 说白了就是开发个应用不再像程序员一样,苦哈哈地写每一行代码,而是化身为导演。
只需要 保持一种感觉 ,这种感觉可能是对产品逻辑、用户流程、审美和交互的把握,剩下的体力活全交给 Cursor、Windsurf、Trae 等 AI 编程工具。 只需要 保持一种感觉 ,这种感觉可能是对产品逻辑、用户流程、审美和交互的把握,剩下的体力活全交给 Cursor、Windsurf、Trae 等 AI 编程工具。
用 Karpathy 的话说: 我几乎不写代码了我只负责调整氛围Vibe代码会自动长出来。 用 Karpathy 的话说: 我几乎不写代码了我只负责调整氛围Vibe代码会自动长出来。
![图片](https://mmbiz.qpic.cn/sz_mmbiz_jpg/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8X7crQiaY9oD4ASLkbm2vTBDZMFKEM4g1PCaRrlBhLKcbPElVmqOt7kyA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=0) ![图片](https://mmbiz.qpic.cn/sz_mmbiz_jpg/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8X7crQiaY9oD4ASLkbm2vTBDZMFKEM4g1PCaRrlBhLKcbPElVmqOt7kyA/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=0)
前段时间发了一篇文章,盘点了 GitHub 上比较有用的 Vibe Coding 相关开源项目。 前段时间发了一篇文章,盘点了 GitHub 上比较有用的 Vibe Coding 相关开源项目。
然后在一个 AI 编程的群里,有一个读者分享了另外一个开源项目: vibe-coding-cn 然后在一个 AI 编程的群里,有一个读者分享了另外一个开源项目: vibe-coding-cn
仔细研究了一下,还挺不错的,分享给大家。 仔细研究了一下,还挺不错的,分享给大家。
![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8XUdd7HGky0prYeibicuObDtHMbx8aTsqmrWjZChYgDdA0ibDeiaVgiaKvJ8A/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=3) ![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8XUdd7HGky0prYeibicuObDtHMbx8aTsqmrWjZChYgDdA0ibDeiaVgiaKvJ8A/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=3)
01 01
**项目简介** **项目简介**
这个叫 vibe-coding-cn 的开源项目 让国内开发者能光速跟上这波浪潮。 这个叫 vibe-coding-cn 的开源项目 让国内开发者能光速跟上这波浪潮。
是 Vibe Coding 氛围感编程的 中文指南 ,汇集了目前全球最顶尖的 AI 编程资源。 是 Vibe Coding 氛围感编程的 中文指南 ,汇集了目前全球最顶尖的 AI 编程资源。
下面是这个开源项目的核心目录: 下面是这个开源项目的核心目录:
![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8XYibjPhibcVS0icTwzlLYduRnLwWRWYYxwjRk29LVkgZTYxupL7C4UvI7Q/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=4) ![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8XYibjPhibcVS0icTwzlLYduRnLwWRWYYxwjRk29LVkgZTYxupL7C4UvI7Q/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=4)
这个开源项目对 Vibe Coding 进行了定义,还挺有意思的。 这个开源项目对 Vibe Coding 进行了定义,还挺有意思的。
Vibe Coding \= **规划驱动 + 上下文固定 + AI 结对执行** ,让「从想法到可维护代码」变成一条可审计的流水线,而不是一团无法迭代的巨石文件。 Vibe Coding \= **规划驱动 + 上下文固定 + AI 结对执行** ,让「从想法到可维护代码」变成一条可审计的流水线,而不是一团无法迭代的巨石文件。
![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8XeBr2ISrVNtaIeHwZpG5UGaMd8LrOchqRe8k50OQG9mnFklo14icd97A/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=5) ![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8XeBr2ISrVNtaIeHwZpG5UGaMd8LrOchqRe8k50OQG9mnFklo14icd97A/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=5)
这个中文的 Vibe Coding 中文指南,包括如下几个新的点: 这个中文的 Vibe Coding 中文指南,包括如下几个新的点:
方法论: 这一部分感觉还是比较玄乎的,其实就是几种准则,看一看就好。 方法论: 这一部分感觉还是比较玄乎的,其实就是几种准则,看一看就好。
![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8Xibh6m6799WmDbKouUgjyOQicbHzIic9pxGia4qtVMKXT6brtkmNLOa71dg/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=6) ![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8Xibh6m6799WmDbKouUgjyOQicbHzIic9pxGia4qtVMKXT6brtkmNLOa71dg/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=6)
AI 编程资源 AI 编程资源
还推荐了 AI 模型、IDE 等环境。如果你懒得筛选,直接 Cursor + claude-opus-4.5-xhigh准没错。 还推荐了 AI 模型、IDE 等环境。如果你懒得筛选,直接 Cursor + claude-opus-4.5-xhigh准没错。
![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8X8ic54zckDDdqMojp9xqPuerLL3yhJXKs27r0XUAdMSVOHnfkgCj6D6w/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=7) ![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8X8ic54zckDDdqMojp9xqPuerLL3yhJXKs27r0XUAdMSVOHnfkgCj6D6w/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=7)
除此之外,还有很多学习资源和文档, 大量提示词 Prompt 优化技巧。 除此之外,还有很多学习资源和文档, 大量提示词 Prompt 优化技巧。
包含数百个精选提示词,涵盖了需求澄清、系统架构设计、分步执行、自测等全链路脚本。支持 Excel 与 Markdown 互转。 包含数百个精选提示词,涵盖了需求澄清、系统架构设计、分步执行、自测等全链路脚本。支持 Excel 与 Markdown 互转。
教你如何用自然语言清晰地定义需求,如何让 AI 保持上下文一致,如何一分钟写出一个完整的 Web 应用, 也可以一同学习一下。 教你如何用自然语言清晰地定义需求,如何让 AI 保持上下文一致,如何一分钟写出一个完整的 Web 应用, 也可以一同学习一下。
![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8XtLYLCxONBlmnaIBlyKzOtOHxRt2n1Ur8cz8ZL09Tv5HqAO50JlSu8w/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=8) ![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8XtLYLCxONBlmnaIBlyKzOtOHxRt2n1Ur8cz8ZL09Tv5HqAO50JlSu8w/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=8)
紧接着这个开源项目,提供一个一个完整流程。帮助你完成基础的设置、开发基础游戏、丰富细节,修复 Bug。 紧接着这个开源项目,提供一个一个完整流程。帮助你完成基础的设置、开发基础游戏、丰富细节,修复 Bug。
![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8XeF2eaCMj8RNJiaf3Mib6yFVP5CwFJtDAfj9w77ibxz0Pobx9H94nR6mibw/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=9) ![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRruzib6uAZIwnice3OuOOI6Kb8XeF2eaCMj8RNJiaf3Mib6yFVP5CwFJtDAfj9w77ibxz0Pobx9H94nR6mibw/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=9)
给我的感觉,这个开源项目践行 规划就是一切 的理念。 给我的感觉,这个开源项目践行 规划就是一切 的理念。
让 AI 写代码前,必须有清晰的技术选型、实施规划和模块化设计,防止 AI 因为理解偏差导致项目逻辑混乱。 让 AI 写代码前,必须有清晰的技术选型、实施规划和模块化设计,防止 AI 因为理解偏差导致项目逻辑混乱。
总而言之,这个开源项目就是 专门为中文开发者设计的 **Vibe Coding 资源库与工作站。** 总而言之,这个开源项目就是 专门为中文开发者设计的 **Vibe Coding 资源库与工作站。**
**它不仅包含了相关的哲学理论,还提供了一套成体系的工具链、提示词库和开发经验总结,旨在帮助开发者更高效地利用 AI 进行软件开发。** **它不仅包含了相关的哲学理论,还提供了一套成体系的工具链、提示词库和开发经验总结,旨在帮助开发者更高效地利用 AI 进行软件开发。**
```javascript ```javascript
开源地址https://github.com/tukuaiai/vibe-coding-cn 开源地址https://github.com/tukuaiai/vibe-coding-cn
``` ```
02 02
**点击下方卡片,关注逛逛 GitHub** **点击下方卡片,关注逛逛 GitHub**
这个公众号历史发布过很多有趣的开源项目,如果你懒得翻文章一个个找,你直接关注微信公众号:逛逛 GitHub ,后台对话聊天就行了: 这个公众号历史发布过很多有趣的开源项目,如果你懒得翻文章一个个找,你直接关注微信公众号:逛逛 GitHub ,后台对话聊天就行了:
![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRrux2sRxwJzmfe1lK8ic33XvtVPsIPCMV7hjicmScibtxIZ1NsjXxNoVNMb3zLy32Al7PSpfbVAtrACYqQ/640?wx_fmt=other&from=appmsg&wxfrom=5&wx_lazy=1&wx_co=1&tp=webp#imgIndex=11) ![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/ePw3ZeGRrux2sRxwJzmfe1lK8ic33XvtVPsIPCMV7hjicmScibtxIZ1NsjXxNoVNMb3zLy32Al7PSpfbVAtrACYqQ/640?wx_fmt=other&from=appmsg&wxfrom=5&wx_lazy=1&wx_co=1&tp=webp#imgIndex=11)
继续滑动看下一个 继续滑动看下一个
逛逛GitHub 逛逛GitHub
向上滑动看下一个 向上滑动看下一个
逛逛GitHub 逛逛GitHub

View File

@@ -1,58 +1,58 @@
--- ---
title: YouTube Content Pipeline title: YouTube Content Pipeline
source: source:
author: shenwei author: shenwei
published: published:
created: created:
description: description:
tags: [] tags: []
--- ---
# YouTube Content Pipeline # YouTube Content Pipeline
As a daily YouTube creator, finding fresh, timely video ideas across the web and X/Twitter is time-consuming. Tracking what you've already covered prevents duplicates and helps you stay ahead of trends. As a daily YouTube creator, finding fresh, timely video ideas across the web and X/Twitter is time-consuming. Tracking what you've already covered prevents duplicates and helps you stay ahead of trends.
This workflow automates the entire content scouting and research pipeline: This workflow automates the entire content scouting and research pipeline:
• Hourly cron job scans breaking AI news (web + X/Twitter) and pitches video ideas to Telegram • Hourly cron job scans breaking AI news (web + X/Twitter) and pitches video ideas to Telegram
• Maintains a 90-day video catalog with view counts and topic analysis to avoid re-covering topics • Maintains a 90-day video catalog with view counts and topic analysis to avoid re-covering topics
• Stores all pitches in a SQLite database with vector embeddings for semantic dedup (so you never get pitched the same idea twice) • Stores all pitches in a SQLite database with vector embeddings for semantic dedup (so you never get pitched the same idea twice)
• When you share a link in Slack, OpenClaw researches the topic, searches X for related posts, queries your knowledge base, and creates an Asana card with a full outline • When you share a link in Slack, OpenClaw researches the topic, searches X for related posts, queries your knowledge base, and creates an Asana card with a full outline
## Skills you Need ## Skills you Need
- `web_search` (built-in) - `web_search` (built-in)
- [x-research-v2](https://clawhub.ai) or custom X/Twitter search skill - [x-research-v2](https://clawhub.ai) or custom X/Twitter search skill
- [knowledge-base](https://clawhub.ai) skill for RAG - [knowledge-base](https://clawhub.ai) skill for RAG
- Asana integration (or Todoist) - Asana integration (or Todoist)
- `gog` CLI for YouTube Analytics - `gog` CLI for YouTube Analytics
- Telegram topic for receiving pitches - Telegram topic for receiving pitches
## How to Set it Up ## How to Set it Up
1. Set up a Telegram topic for video ideas and configure it in OpenClaw. 1. Set up a Telegram topic for video ideas and configure it in OpenClaw.
2. Install the knowledge-base skill and x-research skill. 2. Install the knowledge-base skill and x-research skill.
3. Create a SQLite database for pitch tracking: 3. Create a SQLite database for pitch tracking:
```sql ```sql
CREATE TABLE pitches ( CREATE TABLE pitches (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
timestamp TEXT, timestamp TEXT,
topic TEXT, topic TEXT,
embedding BLOB, embedding BLOB,
sources TEXT sources TEXT
); );
``` ```
4. Prompt OpenClaw: 4. Prompt OpenClaw:
```text ```text
Run an hourly cron job to: Run an hourly cron job to:
1. Search web and X/Twitter for breaking AI news 1. Search web and X/Twitter for breaking AI news
2. Check against my 90-day YouTube catalog (fetch from YouTube Analytics via gog) 2. Check against my 90-day YouTube catalog (fetch from YouTube Analytics via gog)
3. Check semantic similarity against all past pitches in the database 3. Check semantic similarity against all past pitches in the database
4. If novel, pitch the idea to my Telegram "video ideas" topic with sources 4. If novel, pitch the idea to my Telegram "video ideas" topic with sources
Also: when I share a link in Slack #ai_trends, automatically: Also: when I share a link in Slack #ai_trends, automatically:
1. Research the topic 1. Research the topic
2. Search X for related posts 2. Search X for related posts
3. Query my knowledge base 3. Query my knowledge base
4. Create an Asana card in Video Pipeline with a full outline 4. Create an Asana card in Video Pipeline with a full outline
``` ```