From d54fdb2d26b442c69353d1ef541483cc1ffded9d Mon Sep 17 00:00:00 2001 From: weishen Date: Fri, 24 Apr 2026 21:43:10 +0800 Subject: [PATCH] Sync: add prd and ui system notes --- Project/fonrey/PRD/PRD_MVP.md | 280 +++++++ Project/fonrey/UI&UX/UI_SYSTEM.md | 987 +++++++++++++++++++++++++ wiki/index.md | 2 +- wiki/log.md | 9 + wiki/overview.md | 2 + wiki/sources/design-whimsy-injector.md | 47 ++ 6 files changed, 1326 insertions(+), 1 deletion(-) create mode 100644 Project/fonrey/PRD/PRD_MVP.md create mode 100644 Project/fonrey/UI&UX/UI_SYSTEM.md create mode 100644 wiki/sources/design-whimsy-injector.md diff --git a/Project/fonrey/PRD/PRD_MVP.md b/Project/fonrey/PRD/PRD_MVP.md new file mode 100644 index 00000000..80c0af23 --- /dev/null +++ b/Project/fonrey/PRD/PRD_MVP.md @@ -0,0 +1,280 @@ +# Fonrey 房睿 — MVP 范围书 + +**Status**: Draft +**Author**: Product Team +**Last Updated**: 2026-04-24 +**Version**: 1.0 + +> **For AI assistants**: 本文件定义 Phase 1(MVP)的边界。在任何功能实现前,先对照本文确认是否在范围内。范围外的功能禁止在 MVP 阶段实现。 + +--- + +## 1. 产品背景与目标 + +**Fonrey(房睿)** 是一套面向中小型房产经纪公司的 B2B SaaS 管理平台,解决以下核心痛点: + +- 房源/客源信息散乱,全靠人工记录 +- 跟进记录缺失,数据流失严重 +- 重复录入浪费大量经纪人时间 +- 无法支撑 89,000+ 数据量级下的高效房客匹配 + +**MVP 目标**:在一家种子客户(单租户)环境下,完整跑通"录入房源 → 录入客源 → 匹配带看 → 成交"的核心业务链路。 + +--- + +## 2. MVP 核心功能清单(Phase 1 必须实现) + +### 2.1 优先级定义 + +| 优先级 | 含义 | +|--------|------| +| **P0** | MVP 上线前必须完成,阻断核心业务链路 | +| **P1** | MVP 上线后第一个迭代周期内完成 | +| **P2** | 已规划,列入路线图但不阻断上线 | + +--- + +### 2.2 模块优先级矩阵 + +#### 🏠 房源管理 + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| 录入住宅(二手出售/出租) | **P0** | 核心业务入口 | +| 房源列表(二手&租赁) | **P0** | 含筛选、排序、分页 | +| 房源详情页 | **P0** | 含基本信息、产证、交易信息展示 | +| 跟进记录(全部/写入/修改/其他) | **P0** | 含钥匙、委托、实勘 | +| 图片管理(相册上传/分类/排序) | **P0** | 核心房源内容 | +| 业主联系人管理 | **P0** | 含新增/编辑/查看同业主房源 | +| 价格调整(调价/调价记录) | **P0** | 核心运营操作 | +| 房源状态变更(在售/暂缓/成交/下架) | **P0** | 状态机核心 | +| 房源维护完成度(诊断面板) | **P1** | 提升数据质量 | +| 敏感信息跟进(查看权限控制) | **P1** | 需配合权限模块 | +| 附件管理 | **P1** | 非阻断性 | +| 市场报盘 | **P1** | 运营辅助功能 | +| 价格解读 | **P1** | 分析辅助 | +| 录入别墅/商铺/商住/写字楼/其他 | **P2** | 住宅优先,商业类低频 | +| 全部商铺列表 / 全部写字楼列表 | **P2** | 配合 P2 录入功能 | +| 房源广场 | **P2** | 跨租户/公共池功能 | + +#### 🏙️ 楼盘管理 + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| 楼盘列表 + 楼盘详情(楼盘信息/楼栋/结构) | **P0** | 房源数据底座,必须先行 | +| 区域管理(城区/商圈) | **P0** | 房源关联必须 | +| 楼盘照片管理 | **P1** | 数据完善 | +| 楼盘价格走势 | **P1** | 分析辅助 | +| 周边配套(学校管理) | **P1** | 补充信息 | +| 应用数据标准 | **P2** | 明确不做 | + +#### 👥 客源管理 + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| 录入私客(求购/求租) | **P0** | 核心业务 | +| 私客列表(全部/求购/求租) | **P0** | 含筛选、排序 | +| 私客详情(基本信息/需求信息) | **P0** | | +| 跟进记录(全部/写入/修改/其他) | **P0** | | +| 带看管理(预约带看/新增带看) | **P0** | 房客匹配核心 | +| 联系人管理 | **P0** | | +| 客源状态变更(改等级/改状态) | **P0** | | +| 转公客 / 转成交 / 转无效 | **P0** | 生命周期核心 | +| 二手配房(智能匹配) | **P1** | 核心价值,但可后续迭代 | +| 客源解读 | **P1** | AI 辅助分析 | +| 客源信息概览 | **P1** | 汇总视图 | +| 客源收藏夹 | **P1** | 辅助功能 | +| 公客管理 | **P2** | 私客优先 | +| 成交客管理 | **P2** | | +| 暂缓私客 | **P2** | | + +#### 🏢 组织人事 + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| 公司组织结构(部门/门店树) | **P0** | 权限系统基础 | +| 员工列表/员工详情 | **P0** | | +| 员工入职/账号创建 | **P0** | | +| 员工离职 / 调动 | **P1** | | +| 员工通讯录 | **P1** | | +| 异动记录 | **P1** | | +| 奖惩记录 | **P2** | | +| 职务管理 | **P1** | | +| 门店分布地图 | **P2** | | + +#### 🔐 权限管理 + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| 角色管理(预设角色 + 自定义角色) | **P0** | 权限基础 | +| 人员权限列表 | **P0** | | +| 角色批量分配 | **P0** | | +| 功能权限(菜单级) | **P0** | | +| 数据权限(部门/个人/全司) | **P0** | | +| 字段级权限(敏感字段可见性) | **P1** | 配合房源/客源敏感信息 | +| 个人特定权限覆盖 | **P1** | | + +#### 🔑 用户登录 + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| 账号密码登录 | **P0** | | +| 多租户识别(子域名/域名) | **P0** | | +| Token 管理 / 会话超时 | **P0** | | +| 短信验证码登录 | **P1** | | +| 密码重置 | **P1** | | +| 记住登录状态 | **P1** | | + +#### ⚙️ 系统配置 + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| 首页设置 | **P1** | | +| 房源设置(字段必填/自定义字段/标签) | **P0** | 影响录入表单 | +| 相关方设置 | **P1** | | +| 客源设置(基本配置/参数配置) | **P1** | | +| 人事OA设置 | **P2** | | +| 交易设置 | **P2** | | +| 财务设置 | **P2** | | +| 合同设置 | **P2** | | + +#### 🖥️ 系统管理(运营后台) + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| 租户管理(开通/暂停/配置) | **P1** | 单租户种子阶段可手动 | +| 系统健康监控 | **P1** | | +| 操作审计日志 | **P2** | | +| 灰度发布 / 滚动升级 | **P2** | | + +#### 💻 客户端发布 + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| Windows 桌面客户端(内置浏览器) | **P1** | 种子客户使用 Web 端可先行 | +| 自动更新机制 | **P1** | 配合客户端 | + +--- + +## 3. 非目标(Out of Scope — MVP 阶段绝对不做) + +以下功能在 MVP 阶段**明确不实现**,AI 生成代码时不得为这些功能预留接口或引入相关依赖: + +| 功能 | 原因 | +|------|------| +| 移动端适配 | v2 规划 | +| 新房模块(新房管理/新房设置) | 独立模块,后续版本 | +| 合同管理模块 | 独立模块,后续版本 | +| 财务管理/提成结算 | 独立模块,后续版本 | +| 三网发布(安居客/链家/贝壳对接) | 独立模块,后续版本 | +| 数据报表/行程量化 | 独立模块,后续版本 | +| 在线充值/增值服务 | 独立模块,后续版本 | +| 任务管理(OA任务/入职祝福) | 低优先 | +| 考勤管理 | 独立 HR 模块 | +| 审批流程 | 独立 OA 模块 | +| 智慧大屏 / VR换装 | 增值产品 | +| 房源广场(跨租户公共池) | 多租户复杂场景 | + +--- + +## 4. 用户故事(MVP 核心路径) + +### Story 1 — 经纪人录入房源 +> As a **一线经纪人**, +> I want to **快速录入一套二手住宅并上传图片和业主联系方式**, +> So that **这套房源的信息能被团队所有成员找到和跟进**. + +**验收标准**: +- 可在 3 分钟内完成住宅基本信息录入 +- 上传图片后自动按分类展示 +- 录入后即刻出现在房源列表 + +--- + +### Story 2 — 经纪人跟进房源 +> As a **一线经纪人**, +> I want to **对我负责的房源记录每次跟进(面访/电话/钥匙/实勘)**, +> So that **我的跟进历史有据可查,团队不会重复联系同一业主**. + +**验收标准**: +- 跟进记录按时间线倒序展示 +- 支持写入跟进、修改跟进、其他跟进(钥匙/委托/实勘) +- 敏感信息跟进只对有权限的人员可见 + +--- + +### Story 3 — 经纪人录入客源 +> As a **一线经纪人**, +> I want to **录入意向购房/租房客户并跟进其需求变化**, +> So that **我能在合适时机将客户与合适房源匹配**. + +**验收标准**: +- 区分求购/求租两种意向 +- 支持跟进记录 +- 可安排带看并记录带看结果 + +--- + +### Story 4 — 转成交 +> As a **一线经纪人**, +> I want to **将已达成交易的客源标记为"成交"并关联成交房源**, +> So that **成交数据进入系统留存,房源状态自动更新**. + +**验收标准**: +- 转成交时必须选择关联房源 +- 成交后客源状态自动变为"成交客" +- 关联房源状态建议变更为"成交"(可手动确认) + +--- + +### Story 5 — 店长查看团队数据 +> As a **门店店长**, +> I want to **查看本门店所有员工的房源和客源列表**, +> So that **我能掌握团队整体情况并合理分配资源**. + +**验收标准**: +- 数据权限按部门隔离,店长可见本门店数据 +- 可筛选查看特定员工的房源/客源 +- 无法看到其他门店的数据 + +--- + +## 5. MVP 技术边界 + +| 约束 | 决策 | +|------|------| +| 租户数 | **单租户**种子阶段,多租户架构已就位但不激活多租户切换 UI | +| 数据量 | 目标支撑 **89,000 条**房源,测试阶段以 10,000 条压测 | +| 浏览器支持 | Chrome 最新版 / Edge 最新版,不支持 IE | +| 语言 | 简体中文,不做国际化 | +| 移动端 | **不做**,Web 端 Desktop-first | +| 导出 | Excel/CSV 导出通过 Celery 异步,不超时 | + +--- + +## 6. MVP 交付检查清单 + +在 MVP 正式上线前,以下项目必须全部勾选: + +- [ ] 房源录入(住宅)完整流程可用 +- [ ] 房源列表可筛选/排序/分页 +- [ ] 客源录入(求购/求租)完整流程可用 +- [ ] 带看创建与记录可用 +- [ ] 转成交流程可用 +- [ ] 楼盘数据可录入(为房源提供底座) +- [ ] 员工账号可创建/分配角色 +- [ ] 权限隔离:经纪人只能看自己数据,店长能看本店数据 +- [ ] 89,000 条数据量下列表查询 < 2 秒(含索引优化) +- [ ] 图片上传到 Cloudflare R2 可用 +- [ ] 多租户 Schema 隔离验证通过 + +--- + +## 7. 版本路线图 + +| 版本 | 目标 | 核心功能 | +|------|------|---------| +| **v0.1 MVP** | 单租户种子验证 | P0 功能全部上线 | +| **v0.2** | 功能完善 | P1 功能上线,开始多租户测试 | +| **v0.3** | 商业化就绪 | Windows 客户端、多租户正式开放、系统配置完善 | +| **v1.0** | 正式发布 | 新房模块、合同/财务模块路线图确认 | diff --git a/Project/fonrey/UI&UX/UI_SYSTEM.md b/Project/fonrey/UI&UX/UI_SYSTEM.md new file mode 100644 index 00000000..81cfea29 --- /dev/null +++ b/Project/fonrey/UI&UX/UI_SYSTEM.md @@ -0,0 +1,987 @@ +> **For AI assistants**: Read this entire file before writing any frontend code or template. All decisions here are final. When in doubt about styling, spacing, color, or component behavior — the answer is in this document. Do not invent values. + +--- + +## Design Philosophy + +**Core aesthetic**: Clean, functional, low-friction. +We build tools, not experiences. Every UI element must earn its place. + +**Principles** (in priority order): + +1. **Clarity over cleverness** — if you have to explain the UI, it failed +2. **Density over whitespace** — our users are power users; do not waste screen space +3. **Consistency over novelty** — use existing patterns before inventing new ones +4. **Motion is functional** — animate only to communicate state change, never for decoration + +**Anti-patterns we actively avoid**: + +- Skeleton loaders for data that loads in < 300ms (use a spinner instead) +- Modal dialogs for destructive actions that are easily reversible +- Infinite scroll (we use pagination; users need to share URLs to specific pages) +- Tooltips on mobile +- Full-width buttons on desktop (max-width: 320px for standalone CTAs) +- Mixing card and table layouts in the same list view +- Auto-submitting forms on change without explicit confirmation +- Generic error messages ("Something went wrong" is never acceptable) +- `window.alert` — always use our Dialog/Toast components + +--- + +## Design Tokens + +All colors, spacing, radius, and shadow values must come from these tokens. **Never write raw hex values or arbitrary px values in components.** + +### Color System + +We use CSS custom properties (variables). Define these in `base.css` under `:root`. + +```css +:root { + /* Backgrounds */ + --color-bg-base: #ffffff; /* page background */ + --color-bg-subtle: #f9fafb; /* card, sidebar, panel backgrounds */ + --color-bg-muted: #f3f4f6; /* disabled states, placeholders, table zebra */ + + /* Text */ + --color-text-primary: #111827; /* body text */ + --color-text-secondary:#6b7280; /* labels, captions, helper text */ + --color-text-disabled: #9ca3af; /* disabled text */ + --color-text-inverse: #ffffff; /* text on dark/colored backgrounds */ + + /* Borders */ + --color-border: #e5e7eb; /* default borders */ + --color-border-strong: #d1d5db; /* focused, emphasized borders */ + + /* Brand / Accent — orange as primary action color */ + --color-accent: #f97316; /* primary actions, links, active states */ + --color-accent-hover: #ea6c0a; /* hover state of accent */ + --color-accent-subtle: #fff7ed; /* light tint for accent backgrounds */ + + /* Semantic */ + --color-success: #16a34a; /* confirmations, completed states */ + --color-success-bg: #f0fdf4; + --color-warning: #d97706; /* non-blocking alerts */ + --color-warning-bg: #fffbeb; + --color-danger: #dc2626; /* destructive actions, errors */ + --color-danger-bg: #fef2f2; + --color-info: #2563eb; /* informational, neutral alerts */ + --color-info-bg: #eff6ff; +} +``` + +**Mapping to Tailwind**: Configure `tailwind.config.js` to extend colors using these CSS variables so you can write `text-accent`, `bg-bg-subtle`, etc. If that is not yet configured, use the following Tailwind equivalents as a temporary fallback — but never use arbitrary hex: + +| Token | Tailwind equivalent | +|---|---| +| `--color-accent` | `text-orange-500` / `bg-orange-500` | +| `--color-accent-hover` | `hover:bg-orange-600` | +| `--color-text-primary` | `text-gray-900` | +| `--color-text-secondary` | `text-gray-500` | +| `--color-text-disabled` | `text-gray-400` | +| `--color-border` | `border-gray-200` | +| `--color-border-strong` | `border-gray-300` | +| `--color-bg-subtle` | `bg-gray-50` | +| `--color-bg-muted` | `bg-gray-100` | +| `--color-danger` | `text-red-600` / `bg-red-600` | +| `--color-success` | `text-green-600` | + +**Rule**: If you find yourself writing `text-gray-500`, stop. Ask: is this a label, a caption, or secondary content? Then use the semantic token. If you find yourself writing `text-gray-400`, that is disabled text. + +--- + +### Spacing Scale + +We use a **4px base grid**. Only these values are permitted: + +| px | Tailwind | +|---|---| +| 4px | `p-1` / `m-1` / `gap-1` | +| 8px | `p-2` / `m-2` / `gap-2` | +| 12px | `p-3` / `m-3` / `gap-3` | +| 16px | `p-4` / `m-4` / `gap-4` | +| 24px | `p-6` / `m-6` / `gap-6` | +| 32px | `p-8` / `m-8` / `gap-8` | +| 48px | `p-12` / `m-12` / `gap-12` | +| 64px | `p-16` / `m-16` / `gap-16` | +| 96px | `p-24` / `m-24` / `gap-24` | + +**Never use**: `p-5`, `p-7`, `p-9`, `p-10`, `p-11` — these break the grid. + +--- + +### Border Radius + +``` +--radius-sm: 4px → rounded-sm (inputs, badges, table cells) +--radius-md: 8px → rounded (cards, buttons) ← default +--radius-lg: 12px → rounded-xl (modals, drawers, panels) +--radius-full: 9999px → rounded-full (avatars, pill badges, toggles) +``` + +**Rule**: Never mix radius sizes within the same component. A card with `rounded` should not have children with `rounded-xl`. + +--- + +### Elevation / Shadow + +``` +--shadow-sm → shadow-sm (subtle card lift, focused inputs) +--shadow-md → shadow-md (dropdowns, popovers, floating panels) +--shadow-lg → shadow-xl (modals, drawers, dialogs) +``` + +**Rules**: +- Never use `drop-shadow` filter — use `box-shadow` (`shadow-*`) only +- Never use `shadow-2xl` — `shadow-xl` is the maximum + +--- + +## Typography + +**Font stack**: +- UI: `Inter` (variable weight, loaded via `` from self-hosted or CDN) +- Code / monospace data: `JetBrains Mono` (used for code blocks and numeric data columns only) +- Never import fonts via Google Fonts `@import` inside CSS — use `` in `` + +**Type scale** — use only these sizes, no arbitrary values: + +| Tailwind class | Size | Weight | Line-height | Usage | +|---|---|---|---|---| +| `text-xs` | 12px | 400 | 1.5 | Labels, badges, metadata, table captions | +| `text-sm` | 14px | 400 | 1.5 | Body text, secondary content, form helpers | +| `text-base` | 16px | 400 | 1.6 | Primary body text | +| `text-lg` | 18px | 500 | 1.4 | Section headings, drawer titles | +| `text-xl` | 20px | 600 | 1.3 | Page sub-headings | +| `text-2xl` | 24px | 700 | 1.2 | Page titles | +| `text-3xl` | 30px | 700 | 1.1 | Hero headings only — never in app UI | + +**Rules**: +- Max 2 font sizes per component +- `font-medium` (500) is the minimum weight for anything interactive (buttons, links, tab labels) +- Never use `text-3xl` in application views — only marketing/landing pages +- Body text line length: max 72 characters (`max-w-prose`) +- Numbers in tables: use `font-mono` (JetBrains Mono) and `tabular-nums` + +--- + +## Page Layout + +### App Shell + +The standard application layout is a **fixed sidebar + scrollable main content** pattern. + +``` +┌─────────────────────────────────────────────────────┐ +│ Top Nav Bar (h-14, fixed, z-30) │ +├──────────────┬──────────────────────────────────────┤ +│ │ Page Header (breadcrumb + actions) │ +│ Sidebar ├──────────────────────────────────────┤ +│ (w-56, │ │ +│ fixed, │ Main Content Area │ +│ z-20) │ (overflow-y-auto, flex-1) │ +│ │ │ +│ │ │ +└──────────────┴──────────────────────────────────────┘ +``` + +- **Top Nav**: `h-14`, `bg-white`, `border-b border-gray-200`, `fixed top-0 inset-x-0 z-30` +- **Sidebar**: `w-56`, `bg-gray-50`, `border-r border-gray-200`, `fixed left-0 top-14 bottom-0 z-20`, `overflow-y-auto` +- **Main**: `ml-56 mt-14`, `min-h-screen`, `bg-white` +- **Page content padding**: `p-6` on all sides + +### Page Header + +Every page has a header section directly below the top nav, inside the main area: + +```html +
+ + + +
+

住宅出售

+
+ +
+
+
+``` + +### Sidebar Navigation + +- Active item: `bg-orange-50 text-orange-600 font-medium border-r-2 border-orange-500` +- Inactive item: `text-gray-600 hover:bg-gray-100` +- Section label: `text-xs font-semibold text-gray-400 uppercase tracking-wide px-3 mt-4 mb-1` +- Item height: `h-9` (`36px`) +- Item padding: `px-3` +- Icon size: `w-4 h-4`, `mr-2` +- Second-level items: `pl-9` + +--- + +## Core Component Specs + +### Button + +**Variants**: + +| Variant | Tailwind classes | Use case | Never use for | +|---|---|---|---| +| `primary` | `bg-orange-500 hover:bg-orange-600 text-white font-medium rounded` | Single main CTA per view | Destructive actions | +| `secondary` | `bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 font-medium rounded` | Secondary actions | Main CTA | +| `ghost` | `bg-transparent hover:bg-gray-100 text-gray-600 font-medium rounded` | Toolbar actions, low-priority | Standalone CTAs | +| `danger` | `bg-red-600 hover:bg-red-700 text-white font-medium rounded` | Irreversible destructive actions only | Anything reversible | +| `link` | `text-orange-500 hover:text-orange-600 underline-offset-2 hover:underline` | Inline navigation links only | Form submissions | + +**Sizes**: + +| Size | Height | Padding | Font | +|---|---|---|---| +| `sm` | `h-7` (28px) | `px-3` | `text-xs` | +| `md` | `h-9` (36px) | `px-4` | `text-sm` — default | +| `lg` | `h-11` (44px) | `px-6` | `text-base` | + +**States** (all must be handled in every button): + +- `default` — base styles above +- `hover` — defined in variant above +- `active` — `active:scale-95 active:opacity-90` +- `focus-visible` — `focus-visible:outline-2 focus-visible:outline-orange-500 focus-visible:outline-offset-2` +- `disabled` — `disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none` +- `loading` — spinner icon replaces or precedes label; button is disabled + +**Loading state rule**: Show spinner and disable button immediately on click. Never allow double-submission. + +```html + + + + + +``` + +**Icon buttons**: Always include `aria-label`. Never use icon-only buttons as the sole primary CTA. + +--- + +### Form Inputs + +**Anatomy** (always in this order, no exceptions): + +``` +[Label] ← always visible, never placeholder-only +[Input field] +[Helper text] ← optional, describes expected format +[Error message] ← replaces helper text on error +``` + +**Base input class**: +``` +block w-full rounded border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 +placeholder:text-gray-400 +focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500 +disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed +``` + +**States**: + +| State | Additional classes | +|---|---| +| `default` | `border-gray-300` | +| `focus` | `ring-2 ring-orange-500 border-orange-500` | +| `error` | `border-red-500 ring-2 ring-red-500 focus:ring-red-500` | +| `disabled` | `bg-gray-100 text-gray-400 cursor-not-allowed` | +| `readonly` | `bg-gray-50 text-gray-600 cursor-default` | + +**Label**: +```html + +``` + +**Helper text**: +```html +

格式说明文字

+``` + +**Error message**: +```html + +``` + +**Rules**: +- Label always above input, never to the side (exception: checkbox and radio) +- Placeholder text is NOT a label — both must exist +- Error messages: specific and actionable ("请输入有效的手机号", not "格式错误") +- Required fields: mark with `*` next to label; explain at top of form ("* 为必填项") +- Never disable a submit button to prevent submission — show errors inline instead +- `aria-describedby` must link input to its error element when error is shown + +**Select / Dropdown**: +``` +block w-full rounded border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 +focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500 +``` + +**Textarea**: same as input, add `resize-y min-h-[80px]`. Always include character counter when there is a max length. + +--- + +### Data Table + +**Structure**: +``` +[Toolbar: bulk actions + filter chips + column visibility + export] +[Table: sticky header, checkbox col, data cols, actions col] +[Pagination: count + page controls + per-page selector + jump-to] +``` + +**Table base classes**: +```html +
+ + + + + + + + + + + + + + + + +
+ + + 列名 ... +
... + ... +
+
+``` + +Add `group` class to `` to enable `group-hover` on the actions column. + +**Column rules**: +- Numbers: `text-right font-mono tabular-nums` +- Dates: relative time for < 7 days ("2小时前"), absolute date for older ("2025-10-15") +- Status: always a colored badge — never plain text +- Long text: `truncate max-w-[200px]` with `title` attribute showing full value + +**Default page size**: 25 rows. Options: 10 / 25 / 50 / 100. + +**Pagination display**: Always show total count — "共 3,629 条,第 1–25 条" + +**Empty state** (never just "暂无数据"): +```html + + +
+ +

暂无房源

+

符合条件的房源将出现在这里

+ 新增房源 +
+ + +``` + +--- + +### Status Badge + +Status must always be communicated with **color + icon + text** (never color alone). + +```html + + + + 状态文字 + +``` + +**Variant classes** (add to base): + +| Status | Class | +|---|---| +| Active / 在售 | `bg-green-100 text-green-700` | +| Warning / 即将过期 | `bg-yellow-100 text-yellow-700` | +| Danger / 已删除 | `bg-red-100 text-red-700` | +| Neutral / 已下架 | `bg-gray-100 text-gray-600` | +| Info / 跟进中 | `bg-blue-100 text-blue-700` | +| Brand / 出售 | `bg-orange-100 text-orange-700` | + +--- + +### Modal / Dialog + +**Size variants**: + +| Size | Max-width | Use case | +|---|---|---| +| `sm` | `max-w-sm` (400px) | Confirmation dialogs, simple alerts | +| `md` | `max-w-lg` (560px) | Forms with ≤ 5 fields | +| `lg` | `max-w-2xl` (720px) | Complex forms, detail previews | +| `xl` | `max-w-4xl` (960px) | Multi-step flows, wide content | + +**Base modal structure**: +```html +
+ + + + + +
+``` + +**Rules**: +- Always trap focus inside modal (`x-trap` from Alpine.js Focus plugin) +- ESC key always closes (Alpine handles this automatically with `x-trap`) +- Click outside backdrop closes — unless form has unsaved changes → show confirmation +- Never nest modals — use a multi-step flow instead +- Destructive confirm dialogs: danger button on RIGHT, cancel on LEFT +- Never auto-close a modal after an async action — wait for user to dismiss + +--- + +### Drawer / Slide-over Panel + +Used for editing content with many fields where the main page should remain visible for reference. + +**When to use Drawer vs Modal**: + +| Scenario | Component | +|---|---| +| Many fields, needs scrolling | Drawer | +| Few fields (≤ 5), simple confirm | Modal | +| User needs to reference main page data while editing | Drawer | + +**Standard drawer** (slides in from the right): +```html +
+ +
+
+ + + +
+``` + +**Width**: `w-[480px]` default; `w-[640px]` for wide content (image management, multi-column settings). + +--- + +### Tab Navigation + +**Standard tabs** (underline style): +```html +
+ +
+ + +
+ + +
+ ... +
+
+``` + +**Tab + HTMX** (for server-rendered tab content): +```html + +
...
+``` + +**URL-syncing tabs**: Use `hx-push-url="true"` on HTMX tab requests to make tabs bookmarkable. + +--- + +### Toggle Switch + +```html + +``` + +--- + +### Collapsible / Accordion + +```html +
+ +
+ 分组标题 + +
+ + +
+ +
+
+``` + +Requires Alpine.js `@alpinejs/collapse` plugin (official, ~1KB). + +--- + +### Tree Select + +For hierarchical data selection (org unit, staff assignment): + +- **Small datasets** (< 200 nodes): Alpine.js renders full JSON tree client-side +- **Large datasets**: HTMX lazy-loads child nodes on expand + +Alpine.js data structure: +```javascript +{ + open: false, + query: '', + selected: null, + nodes: [/* tree JSON from Django */], + + toggle(node) { node.expanded = !node.expanded }, + select(node) { this.selected = node; this.open = false }, + + filteredNodes() { + if (!this.query) return this.nodes + // Recursive filter — preserves ancestors of matching nodes + return this.filterTree(this.nodes, this.query.toLowerCase()) + } +} +``` + +--- + +### Multi-select Tag Input + +For multi-value fields (e.g., property status, amenities): + +```html +
+ + +
+ + +
+ + +
+ +
暂无匹配选项
+
+
+``` + +--- + +### Date Range Picker + +Use **Flatpickr** (CDN, ~16KB, zero framework dependency): + +```javascript +flatpickr("#date-range-input", { + mode: "range", + showMonths: 2, + dateFormat: "Y-m-d", + locale: "zh", +}); +``` + +Override Flatpickr default styles with Tailwind to match our design system. Never build a date picker from scratch. + +--- + +### Photo Gallery / Image Management + +- Upload: **Filepond** (~50KB, zero framework dependency) — drag-and-drop, preview, progress, multi-file queue +- Drag-to-reorder: **SortableJS** (~3KB) — use `handle: '.drag-handle'` +- Lightbox preview: **Viewer.js** (~5KB) — zoom, rotate, thumbnail strip + +All three are framework-free pure JS libraries, fully compatible with HTMX + Alpine.js. + +--- + +## State & Feedback Patterns + +### Loading States + +| Duration | Pattern | +|---|---| +| < 300ms | Nothing — avoid flash of spinner | +| 300ms – 1s | Inline spinner (`animate-spin`) | +| 1s – 3s | Spinner + "加载中..." text | +| > 3s | Progress bar + estimated time | +| Background Celery task | Subtle pulsing indicator in top nav | + +HTMX automatically adds `htmx-request` class during requests — use it to show/hide indicators: +```html +
+ +
+``` + +--- + +### Empty States + +Every list, table, and data view must handle the empty state. Required elements: + +1. Relevant icon (not a generic "no data" icon — use something contextually relevant) +2. Friendly headline ("暂无房源") +3. Explanation ("符合当前筛选条件的房源将出现在这里") +4. CTA if the user can fix it ("新增第一条房源 →") + +```html +
+ +

暂无房源

+

符合当前筛选条件的房源将出现在这里

+ 新增房源 +
+``` + +--- + +### Toast Notifications + +| Type | When to use | Duration | Classes | +|---|---|---|---| +| `success` | Async action completed | 3s auto-dismiss | `bg-green-50 border-green-200 text-green-800` | +| `error` | Action failed, user must retry | Persistent (manual dismiss) | `bg-red-50 border-red-200 text-red-800` | +| `warning` | Completed with caveats | 5s | `bg-yellow-50 border-yellow-200 text-yellow-700` | +| `info` | Background process started | 3s | `bg-blue-50 border-blue-200 text-blue-700` | + +**Rules**: +- Max 3 toasts visible at once (queue the rest) +- Never show success toast for page navigations +- Error toasts must include a retry action button when possible +- Never use toast for validation errors — show inline instead +- Position: `fixed bottom-4 right-4 z-50 flex flex-col gap-2` + +Trigger via HTMX response header: `HX-Trigger: {"showToast": {"type": "success", "message": "保存成功"}}` + +--- + +### Inline Edit (Read/Edit Mode Toggle) + +For settings pages and detail views that support in-place editing: + +```html +
+ + +
+ + +
+ + +
+ 工龄计算方式 + + +
+
+``` + +**Cancel rule**: Always snapshot data before entering edit mode. On cancel, restore from snapshot (3 lines). Never leave the user with unsaved partial edits on cancel. + +--- + +## Responsive Breakpoints + +**Strategy**: Desktop-first (target users are ≥ 85% desktop/Windows Electron client). + +| Breakpoint | Tailwind prefix | Viewport | Target | +|---|---|---|---| +| Desktop | (base, no prefix) | > 1280px | Primary design target | +| Laptop | `lg:` | ≥ 1024px | Minor layout adjustments | +| Tablet | `md:` | ≥ 768px | Collapsed sidebar | +| Mobile | `sm:` | ≥ 640px | Single column, no tables | + +**Component-specific rules**: +- Data tables → `overflow-x-auto` on `md` and below +- Sidebar → collapses to icon-only or hidden on `md`; bottom nav on `sm` +- Modals → full-screen (`inset-0 rounded-none`) on `sm` +- Multi-column forms → single column on `md` and below (`md:grid-cols-1`) +- Drawers → full-width on `sm` (`sm:w-full`) + +**Never**: +- Hide critical functionality on mobile — adapt it, do not remove it +- Use fixed px widths for layout containers (use `max-w-*` instead) +- Assume touch input on desktop + +--- + +## Motion & Animation + +**Principle**: Motion communicates state, it does not decorate. + +**Duration scale**: + +| Token | Duration | Use for | +|---|---|---| +| `duration-100` | 100ms | Micro-interactions (checkbox tick, toggle) | +| `duration-200` | 200ms | Most transitions (hover, focus, fade) — default | +| `duration-300` | 300ms | Larger elements (modal enter, drawer slide) | +| `duration-500` | 500ms | Page-level transitions only | + +**Easing**: +- Default UI transitions: `ease-out` (enter) / `ease-in` (leave) +- Playful/spring: avoid in this product — we are a business tool +- Progress bars: `linear` + +**What to animate**: +- `opacity` — enter/exit fades +- `transform: translateY / translateX` — panel slide-in +- `max-height` with `x-collapse` — accordion expand + +**Never animate**: +- `width` or `height` directly — use `max-height` with `x-collapse` or `transform` +- `top / left / right / bottom` — use `transform: translate` instead +- `box-shadow` on hover — use opacity-layered pseudo-element trick instead +- Anything if `prefers-reduced-motion: reduce` is set: + +```css +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} +``` + +--- + +## Accessibility (Non-negotiable) + +**Every component must**: +- Meet WCAG 2.1 AA contrast ratios (4.5:1 for body text, 3:1 for large text ≥ 18px) +- Be fully keyboard navigable (Tab, Shift+Tab, Enter, Space, Escape) +- Have visible focus indicators — never `outline: none` without a custom replacement using `focus-visible:` +- Work with screen readers (proper ARIA roles and labels) + +**Required patterns**: + +| Element | Requirement | +|---|---| +| Icon-only buttons | `aria-label="操作名称"` always | +| Form inputs | `id` attribute + `