Files
nexus/Project/fonrey/UI_SYSTEM/UI_SYSTEM.md
2026-04-26 19:50:01 +08:00

1743 lines
78 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fonrey UI System 设计规范
**版本**v1.2
**最后更新**2026-04-25
**维护者**UI/UX 架构组
**适用技术栈**Tailwind CSS + HTMX + Alpine.js + Django 模板
**目标分辨率**:桌面 Web≥ 1280pxv1 不做移动端适配)
> **For developers**: 本文是开发还原 UI 的唯一权威。所有组件以 HTML + Tailwind class 描述,不输出 JSX。新增页面须先到此处查找可复用组件与模板如需新模式先补本文再落地。
> **关联文档**
> - `Project/fonrey/TECH_STACK/TECH_STACK.md`(技术总纲)
> - `Project/fonrey/UI_SYSTEM/组件清单.md`(组件可行性分析)
> - `Project/fonrey/PRD/*`(业务需求)
---
## 0. 设计语言定调Design Language Overview
参考 Linear 的克制、Notion 的信息密度、Salesforce Lightning 的企业严谨,结合竞品(链家/贝壳式房产工具的操作效率要求Fonrey 的设计语言定位为:
- **专业、克制、高密度**:表格为王,单屏尽可能多展示数据;色彩只为信息服务,不用于装饰。
- **主色靛青Teal**:低饱和、冷静,与状态色(绿/黄/红)形成强区分;与房产行业"稳健/可靠"意象吻合,同时避免与 success 绿产生语义歧义。
- **中等圆角8px**既不像消费端12px+过于柔软也不像传统企业端0-2px过于呆板。
- **紧凑密度**:表格默认行高 56px含 40×40 封面缩略图),表单字段间距 12px信息密度优先于呼吸感。
---
## 1. 设计原则Design Principles
1. **效率优先Efficiency First**
减少视觉噪音,让用户聚焦在数据和操作上。表格为核心场景,一屏可见行数 ≥ 15。
2. **状态可见Visible State**
任何异步请求必须有反馈(骨架屏 / Spinner / Toast任何数据变更必须给出成功或失败的即时提示。HTMX 的"静默成功"是 Bug。
3. **复用先于新建Reuse Over Reinvent**
每个组件在本文中有唯一标准实现;新需求须先尝试组合既有组件,再提案新组件。
4. **键盘友好Keyboard First**
高频操作(搜索、翻页、新增、保存)须支持键盘;表格、表单、弹窗均支持 Tab / Enter / ESC。
5. **一致性高于美观Consistency Over Cleverness**
相同的动作在全产品使用相同的图标、色彩、位置。经纪人的肌肉记忆比单页的视觉惊喜更重要。
---
## 2. 设计 TokenDesign Tokens
所有 Token 均映射到 `tailwind.config.js``theme.extend`,禁止在模板中使用任意十六进制色值。
### 2.1 颜色系统
#### 2.1.1 品牌色Primary — Teal
| Token | Hex | Tailwind 类 | 使用场景 |
| ------------- | --------- | ---------------- | ------------------------- |
| `primary-50` | `#F0FDFA` | `bg-primary-50` | 页面强调区微底色、Tag 极淡底 |
| `primary-100` | `#CCFBF1 | `bg-primary-100` | 选中背景、标签底色 |
| `primary-200` | `#99F6E4` | `bg-primary-200` | Hover 标签底色 |
| `primary-500` | `#14B8A6` | `bg-primary-500` | 辅助主色(图标、强调文字) |
| `primary-600` | `#0F766E` | `bg-primary-600` | **主按钮、激活态、Tab 下划线(基准主色)** |
| `primary-700` | `#115E59` | `bg-primary-700` | 主按钮 Hover |
| `primary-800` | `#134E4A` | `bg-primary-800` | 主按钮 Active / 深色文字 |
#### 2.1.2 中性色Neutral — Slate 系,偏冷灰)
| Token | Hex | 使用场景 |
|---|---|---|
| `neutral-50` | `#F8FAFC` | 页面背景 |
| `neutral-100` | `#F1F5F9` | Hover 底色、表头底色、禁用输入框底色 |
| `neutral-200` | `#E2E8F0` | 分隔线、默认边框 |
| `neutral-300` | `#CBD5E1` | 输入框边框、次级按钮边框 |
| `neutral-400` | `#94A3B8` | 占位符、禁用文字、辅助图标 |
| `neutral-500` | `#64748B` | 辅助文字、副标题 |
| `neutral-600` | `#475569` | 次级正文 |
| `neutral-700` | `#334155` | 正文 |
| `neutral-800` | `#1E293B` | 标题 |
| `neutral-900` | `#0F172A` | 强调标题 |
#### 2.1.3 语义色Semantic
| Token | Hex | 使用场景 |
|---|---|---|
| `success-600` | `#16A34A` | 操作成功 Toast、在售/激活状态 |
| `success-50` | `#F0FDF4` | Success Tag 底色 |
| `warning-600` | `#D97706` | 待确认/临期提醒 |
| `warning-50` | `#FFFBEB` | Warning Tag 底色 |
| `danger-600` | `#DC2626` | 删除/错误/逾期 |
| `danger-50` | `#FEF2F2` | Danger Tag 底色 |
| `info-600` | `#2563EB` | 信息提示、Link、已成交状态 |
| `info-50` | `#EFF6FF` | Info Tag 底色 |
> **语义色与主色分离**:主色是 Tealsuccess 是独立绿,避免"主操作按钮看起来像成功提示"。
#### 2.1.4 背景层级
| 层级 | Tailwind 类 | 使用场景 |
|---|---|---|
| L0 页面背景 | `bg-neutral-50` | 整体页面底色 |
| L1 卡片/面板 | `bg-white` | 内容区块 |
| L2 表头/次级区块 | `bg-neutral-100` | 表头、工具栏底色、代码块 |
| L3 悬浮层 | `bg-white shadow-lg border border-neutral-200` | 弹窗、下拉、抽屉 |
| L4 遮罩 | `bg-neutral-900/40` | Modal / Drawer 遮罩 |
### 2.2 字体系统
基础字体栈Tailwind 默认即可):
```
font-sans: "Inter", "PingFang SC", "Microsoft YaHei", -apple-system, sans-serif;
font-mono: "JetBrains Mono", "SFMono-Regular", Menlo, monospace;
```
| 层级 | 字号 | 字重 | 行高 | Tailwind 类 | 使用场景 |
|---|---|---|---|---|---|
| H1 页面标题 | 20px | 600 | 28px | `text-xl font-semibold text-neutral-800` | 页面 H1紧凑型避免占太多垂直空间 |
| H2 区块标题 | 16px | 600 | 24px | `text-base font-semibold text-neutral-800` | 卡片/面板标题 |
| H3 次级标题 | 14px | 600 | 20px | `text-sm font-semibold text-neutral-700` | 表单分组、Section 内标题 |
| Body 正文 | 14px | 400 | 20px | `text-sm text-neutral-700` | 表单标签、描述、表格数据(默认) |
| Data 数据强调 | 14px | 500 | 20px | `text-sm font-medium text-neutral-900` | 表格关键列(房源标题、价格) |
| Number 数值 | 20px | 600 | 28px | `text-xl font-semibold tabular-nums` | Stat Card 数值、价格展示 |
| Caption 辅助 | 12px | 400 | 16px | `text-xs text-neutral-500` | 提示、占位符、时间戳 |
| Mono 代码/ID | 12px | 400 | 16px | `text-xs font-mono text-neutral-600` | 房源编号、系统 ID |
> **关键约定**:表格与表单中**所有数字列**必须加 `tabular-nums`(等宽数字),保证纵向对齐。
### 2.3 间距系统
4px 基础栅格,映射到 Tailwind 默认间距 Token。
| 场景 | Token | 值 |
|---|---|---|
| 原子间距(图标与文字) | `gap-1` / `gap-1.5` | 4 / 6 px |
| 组件内边距(密集) | `p-2` | 8 px如 Tag 内) |
| 组件内边距(标准) | `px-3 py-2` | 12/8 px输入框、按钮 md |
| 卡片内边距 | `p-4` 或 `p-6` | 16 / 24 px |
| 表单字段纵向间距 | `space-y-3` | 12 px |
| 区块纵向间距 | `space-y-6` | 24 px |
| 页面两侧边距 | `px-6` | 24 px |
| 列表页内容区边距 | `px-6 py-4` | — |
### 2.4 阴影与圆角
| Token | 值 | 使用场景 |
|---|---|---|
| `rounded` | 4px | 紧凑 Tag、小 Badge |
| `rounded-md` | 6px | 按钮、输入框、下拉选项 |
| `rounded-lg` | 8px | **卡片、面板、表格容器(基准)** |
| `rounded-xl` | 12px | Modal / Drawer |
| `rounded-full` | 圆 | 头像、开关滑块、圆点 |
| 阴影 | 使用场景 |
|---|---|
| `shadow-xs`(自定义 `0 1px 2px rgba(15,23,42,0.04)` | 卡片静态 |
| `shadow-sm` | 卡片 Hover、次级浮层如 Tooltip |
| `shadow-md` | Dropdown、Popover |
| `shadow-lg` | Modal、Drawer、Toast |
### 2.5 圆角圆点与边框
- 默认边框:`border border-neutral-200`
- 强调边框Focus/激活):`ring-2 ring-primary-600/30 border-primary-600`
- 错误边框:`border-danger-600 ring-2 ring-danger-600/20`
### 2.6 Z-index 层级
| 层级 | 值 | 场景 |
|---|---|---|
| `z-20` | 20 | 侧边栏、顶部导航 |
| `z-30` | 30 | 页面内 Sticky 工具栏 |
| `z-40` | 40 | Dropdown / Popover |
| `z-50` | 50 | Modal / Drawer 遮罩 |
| `z-60` | 60 | Modal / Drawer 面板 |
| `z-70` | 70 | Toast 容器(始终最顶层) |
### 2.7 Tailwind 配置示例(节选)
```js
// tailwind.config.js
module.exports = {
content: ['./apps/**/templates/**/*.html', './templates/**/*.html'],
theme: {
extend: {
colors: {
primary: {
50: '#F0FDFA', 100: '#CCFBF1', 200: '#99F6E4',
500: '#14B8A6', 600: '#0F766E', 700: '#115E59', 800: '#134E4A',
},
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'],
mono: ['JetBrains Mono', 'SFMono-Regular', 'Menlo', 'monospace'],
},
},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
}
```
---
## 3. 基础组件规范Base Components
### 3.1 按钮Button
#### 3.1.1 变体
| 变体 | 用途 | Tailwind 类 |
|---|---|---|
| Primary | 主操作(每个区域唯一) | `bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800` |
| Secondary | 次级操作 | `bg-white border border-neutral-300 text-neutral-700 hover:bg-neutral-50 hover:border-neutral-400` |
| Danger | 删除、不可逆操作 | `bg-danger-600 text-white hover:bg-danger-600/90` |
| Ghost | 工具栏、表格行操作 | `text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900` |
| Link | 内联跳转 | `text-primary-600 hover:text-primary-700 hover:underline underline-offset-2` |
| Icon | 仅图标的工具按钮 | `text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 rounded-md p-1.5` |
#### 3.1.2 尺寸
| 尺寸 | 场景 | Tailwind 类 |
|---|---|---|
| sm | 表格操作、Tag 内 | `px-2.5 py-1 text-xs rounded` |
| md默认 | 表单提交、工具栏 | `px-3 py-1.5 text-sm rounded-md` |
| lg | 页面主操作(新增按钮) | `px-4 py-2 text-sm rounded-md` |
#### 3.1.3 状态
- **Focus**`focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40`
- **Loading**:禁用 + 内嵌 Spinner + 文案改为进行时("保存中…"
- **Disabled**`disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-current`(即 hover 无效果)
#### 3.1.4 标准 HTML 片段
```html
<!-- Primary 按钮(含图标) -->
<button type="submit"
class="inline-flex items-center gap-1.5 px-3 py-1.5 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-50 disabled:cursor-not-allowed">
<svg class="w-4 h-4" aria-hidden="true"><!-- PlusIcon --></svg>
新增房源
</button>
<!-- Loading 态 -->
<button disabled
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium
bg-primary-600 text-white rounded-md opacity-70 cursor-wait">
<svg class="w-4 h-4 animate-spin" aria-hidden="true"><!-- Spinner --></svg>
保存中…
</button>
```
#### 3.1.5 禁忌
- ❌ 同一视觉区域不得出现两个 Primary 按钮
- ❌ Danger 按钮必须二次确认Modal不得直接触发删除
- ❌ 不得用颜色以外的方式(如加粗)表达"危险"
- ❌ Ghost 按钮不允许填充背景色做变体(容易与 Secondary 混淆)
---
### 3.2 输入框Input
#### 3.2.1 状态
| 状态 | Tailwind 类 |
|---|---|
| 默认 | `border-neutral-300` |
| Hover | `hover:border-neutral-400` |
| Focus | `focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20` |
| 错误 | `border-danger-600 focus:border-danger-600 focus:ring-danger-600/20` |
| 禁用 | `bg-neutral-100 text-neutral-400 cursor-not-allowed` |
| 只读 | `bg-neutral-50 text-neutral-700` |
#### 3.2.2 标准结构
```html
<div class="space-y-1">
<label for="title" class="block text-sm font-medium text-neutral-700">
房源标题 <span class="text-danger-600">*</span>
</label>
<input id="title" type="text" name="title"
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:bg-neutral-100 disabled:text-neutral-400">
<!-- 错误提示(优先) -->
<p class="text-xs text-danger-600">标题不能为空</p>
<!-- 辅助说明(仅无错误时显示) -->
<p class="text-xs text-neutral-500">不超过 50 字,将用于客户端展示</p>
</div>
```
#### 3.2.3 变体
- **Textarea**:同上,`rows="3"` 起步,右下角 `resize-y`;需字数统计时右下角显示 `x-text="val.length + '/200'"`
- **带前缀/后缀**:用 `relative` 包裹,前缀/后缀绝对定位 `absolute left-3 / right-3`
- **带单位**:右侧灰色文字(如"万元"),用 `pr-14` 留白
- **数字输入**`type="number"` + `tabular-nums` + 取消浏览器 spinner用 `appearance-none`
- **密码输入**:默认 `type="password"`右侧眼睛图标按钮Alpine.js 切换 type
---
### 3.3 下拉选择Select / Dropdown
#### 3.3.1 三种实现路径
| 场景 | 实现 |
|---|---|
| 单选、选项 ≤ 10、不需搜索 | **原生 `<select>`**(配合 `@tailwindcss/forms` 统一样式) |
| 单选带搜索、多选、分组 | **Alpine.js 自定义下拉** |
| 选项依赖其他字段(如选了"区"后加载"商圈" | **HTMX 动态拉取** `hx-get="/api/districts/{{id}}/areas/" hx-trigger="change from:#district" hx-target="#area"` |
| 树形选择(员工、门店、组织) | **Tree Select**(详见 3.3.4 |
| 多选标签式 | **Multi-select Tag Input**(详见 3.3.5 |
#### 3.3.2 原生 Select 标准
```html
<select class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300
focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20
bg-white">
<option value="">请选择</option>
<option value="sale">出售</option>
<option value="rent">出租</option>
</select>
```
#### 3.3.3 Alpine.js 自定义下拉(带搜索)
```html
<div x-data="{ open: false, query: '', selected: null,
options: [{id:1,name:'浦东新区'},{id:2,name:'徐汇区'}],
get filtered() { return this.options.filter(o => o.name.includes(this.query)) } }"
@click.away="open=false" class="relative">
<button type="button" @click="open=!open"
class="w-full flex items-center justify-between px-3 py-2 text-sm rounded-md
border border-neutral-300 bg-white text-left
hover:border-neutral-400 focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20">
<span x-text="selected?.name || '请选择区域'" :class="{'text-neutral-400': !selected}"></span>
<svg class="w-4 h-4 text-neutral-400"><!-- ChevronDownIcon --></svg>
</button>
<div x-show="open" x-transition.opacity
class="absolute z-40 mt-1 w-full bg-white rounded-md shadow-md border border-neutral-200">
<div class="p-2 border-b border-neutral-100">
<input x-model="query" placeholder="搜索…"
class="w-full px-2 py-1 text-sm rounded border border-neutral-200">
</div>
<ul class="max-h-60 overflow-y-auto py-1">
<template x-for="o in filtered" :key="o.id">
<li @click="selected=o; open=false"
class="px-3 py-2 text-sm text-neutral-700 hover:bg-primary-50 cursor-pointer flex items-center justify-between">
<span x-text="o.name"></span>
<svg x-show="selected?.id===o.id" class="w-4 h-4 text-primary-600"><!-- CheckIcon --></svg>
</li>
</template>
<li x-show="filtered.length===0" class="px-3 py-6 text-xs text-neutral-400 text-center">暂无匹配</li>
</ul>
</div>
</div>
```
#### 3.3.4 Tree Select树形选择器
适用组织架构、员工选择、部门选择等。
- 数据:后端一次性返回完整 JSON 树(组织树通常 < 500 节点,无压力)
- 交互:节点左侧 `` 图标控制子树展开/折叠,叶节点可点击选中
- 搜索:输入时过滤,**命中子节点的父节点强制展开**
- 底部固定操作行:`隐藏离职员工` 开关
实现要点见 `组件清单.md` Tree Select 章节。
#### 3.3.5 Multi-select Tag Input多选标签输入
适用于多个平级标签(房源状态、客源需求类型)。见 3.7 状态标签组件的 Tag 样式。
```html
<!-- 容器Focus 时 ring -->
<div class="flex flex-wrap gap-1 min-h-[38px] px-2 py-1 rounded-md border border-neutral-300
focus-within:border-primary-600 focus-within:ring-2 focus-within:ring-primary-600/20">
<!-- 每个已选 Tag -->
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded
bg-primary-50 text-primary-700">
出售
<button type="button" class="hover:text-primary-900">
<svg class="w-3 h-3"><!-- XMarkIcon --></svg>
</button>
</span>
<input class="flex-1 min-w-[80px] text-sm outline-none" placeholder="选择或输入…">
</div>
```
---
### 3.4 表格Table
Fonrey 的核心场景,规范必须严格执行。
#### 3.4.1 结构规范
| 区域 | 说明 | Tailwind 类 |
|---|---|---|
| 外壳 | 圆角卡片容器 | `bg-white rounded-lg border border-neutral-200 overflow-hidden` |
| 表头 `<thead>` | 粘性、小字、中性色底 | `bg-neutral-50 text-xs font-medium text-neutral-500 uppercase tracking-wider sticky top-0 z-10` |
| 表头 `<th>` | 高 36px排序箭头右对齐 | `px-4 py-2 text-left` |
| 数据行 `<tr>` | 密度可切换Hover 高亮 | `hover:bg-neutral-50` |
| 数据单元 `<td>` | 正文字号;垂直居中 | `px-3 py-3 text-sm text-neutral-700 align-middle whitespace-nowrap` |
| 选中行 | 浅主色高亮 | `bg-primary-50/40` |
| 操作列 | 固定右侧Ghost 图标按钮Hover 显形 | `sticky right-0 bg-white opacity-0 group-hover:opacity-100` |
| 表格底部(分页栏) | | `px-4 py-3 border-t border-neutral-200 bg-white flex items-center justify-between` |
- **斑马纹**B2B 高密度场景**不启用**视觉噪音。Hover 行高亮已足够区分。
- **封面缩略图**:房源/楼盘等含图业务表格,第一数据列(通常为"标题"列)内嵌缩略图(`<img>` 或占位 `<div>`),尺寸随密度档联动(见下表)。缩略图统一 `rounded` (4px),对象填充 `object-cover`,加载失败占位为 `bg-neutral-100` + 图片占位图标。
- **三档密度**:工具栏右侧"密度"图标按钮切换Alpine.js + `localStorage`key: `fonrey:table:{module}:density`)持久化,默认 `standard`。
| 档位 | Key | 行高 | 缩略图尺寸 | 单元内边距 | 使用场景 |
|---|---|---|---|---|---|
| 紧凑 Compact | `compact` | 40px | 无(图片列隐藏) | `px-3 py-2` | 数据核对、大批量浏览、导出前预览 |
| 标准 Standard默认 | `standard` | 56px | 40×40 | `px-3 py-3` | 日常工作,"一眼认房" |
| 舒适 Comfortable | `comfortable` | 72px | 56×56 | `px-3 py-4` | 含更多副信息(楼层/朝向/装修标签换行展示) |
> **实现提示**:密度切换仅改 `<tbody>` 上的 class如 `table-density-standard`),通过父级 class + 子选择器统一控制行高、内边距、图片显隐,避免逐行改 class。
> **无图业务**(客源、跟进记录、权限等)表格固定使用 `compact` 40px 行高,不提供密度切换。
#### 3.4.2 标准片段
```html
<div class="bg-white rounded-lg border border-neutral-200 overflow-hidden">
<!-- 工具栏:选中条数 + 批量操作 + 视图切换 + 导出 + 自定义列 -->
<div id="property-toolbar"
class="hidden px-4 py-2 bg-primary-50 border-b border-primary-100 items-center justify-between text-sm"
:class="selected.length > 0 ? 'flex' : 'hidden'">
<span class="text-primary-700">已选中 <span x-text="selected.length"></span> 条</span>
<div class="flex items-center gap-2">
<button class="text-sm text-neutral-700 hover:text-primary-700">批量分享</button>
<button class="text-sm text-neutral-700 hover:text-danger-600">批量删除</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-neutral-200">
<thead class="bg-neutral-50">
<tr>
<th class="px-4 py-2 w-10"><input type="checkbox" class="rounded border-neutral-300"></th>
<th class="px-4 py-2 text-left text-xs font-medium text-neutral-500 uppercase tracking-wider">房源编号</th>
<th class="px-4 py-2 text-left text-xs font-medium text-neutral-500 uppercase tracking-wider">
<button class="inline-flex items-center gap-1 hover:text-neutral-700">
挂牌价 <svg class="w-3 h-3"><!-- ChevronUpDownIcon --></svg>
</button>
</th>
<th class="px-4 py-2 text-left text-xs font-medium text-neutral-500 uppercase tracking-wider">状态</th>
<th class="px-4 py-2 sticky right-0 bg-neutral-50">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral-100 bg-white"
hx-get="/property/list/" hx-trigger="load" hx-swap="innerHTML">
<!-- 行由 HTMX 填充 -->
</tbody>
</table>
</div>
<!-- 分页栏 -->
<div class="px-4 py-3 border-t border-neutral-200 bg-white flex items-center justify-between">
<div class="text-xs text-neutral-500">共 3,629 条</div>
<div class="flex items-center gap-1"><!-- 分页按钮组 --></div>
<div class="flex items-center gap-2 text-xs text-neutral-500">
<select class="text-xs rounded border-neutral-300">
<option>20 条/页</option><option>50 条/页</option><option>100 条/页</option>
</select>
<span>跳至</span>
<input type="number" class="w-12 text-xs rounded border-neutral-300 text-center">
<span>页</span>
</div>
</div>
</div>
```
#### 3.4.3 HTMX 局部刷新约定
- 所有筛选、排序、翻页触发 `hx-get`,目标容器为 `<tbody>` 或 `#table-wrapper`
- **加载态**`htmx:beforeRequest` 给 `<tbody>` 叠加骨架屏(`animate-pulse` 的 5 行占位)
- **错误态**`htmx:responseError` 保留原内容 + 触发 Error Toast
#### 3.4.4 自定义列Column Visibility
右上角"自定义列表"按钮,弹出 Checkbox 面板,选择状态持久化到 `localStorage`Key: `fonrey:table:{module}:cols`)。隐藏的列通过 Alpine.js `:class="{'hidden': !col.visible}"` 控制。
#### 3.4.5 空状态
见 6.3 空状态设计。表格空状态占整个 `<tbody>`,不使用 `colspan` 占所有列的骚操作,改用覆盖层(`absolute inset-0 flex items-center justify-center`)。
---
### 3.5 分页Pagination
```html
<nav class="flex items-center gap-1">
<button class="p-1.5 text-neutral-500 hover:bg-neutral-100 rounded disabled:opacity-30" disabled>
<svg class="w-4 h-4"><!-- ChevronLeftIcon --></svg>
</button>
<button class="min-w-[32px] h-8 px-2 text-sm bg-primary-600 text-white rounded">1</button>
<button class="min-w-[32px] h-8 px-2 text-sm text-neutral-700 hover:bg-neutral-100 rounded">2</button>
<button class="min-w-[32px] h-8 px-2 text-sm text-neutral-700 hover:bg-neutral-100 rounded">3</button>
<span class="px-1 text-neutral-400">…</span>
<button class="min-w-[32px] h-8 px-2 text-sm text-neutral-700 hover:bg-neutral-100 rounded">182</button>
<button class="p-1.5 text-neutral-500 hover:bg-neutral-100 rounded">
<svg class="w-4 h-4"><!-- ChevronRightIcon --></svg>
</button>
</nav>
```
- **当前页**`bg-primary-600 text-white`
- **省略号**Django 后端生成(前 3 后 3 + 当前 ±1
- **同页多表格独立分页**:每个分页器 `hx-target` 指向各自的表格容器 id见 `组件清单.md`
---
### 3.6 弹窗Modal与抽屉Drawer
#### 3.6.1 选择原则
| 类型 | 场景 | 宽度 | 关闭行为 |
|---|---|---|---|
| Confirm Modal | 删除确认、不可逆操作 | `max-w-sm`400px | 点击遮罩**不关闭**防误触ESC 关闭 |
| Form Modal | 新增/编辑(字段 ≤ 6 | `max-w-lg`560px | 点击遮罩关闭ESC 关闭 |
| Right Drawer | 查看详情、新增/编辑(字段多)、参考主页面 | `w-[640px]` 或 `w-[480px]` | 点击遮罩关闭ESC 关闭 |
| Full Modal | 复杂配置(权限矩阵) | `w-[80vw] max-w-6xl` | 点击遮罩**不关闭**,顶部关闭按钮 |
#### 3.6.2 Modal 标准结构
```html
<div x-data="{ open: false }" @keydown.escape.window="open=false">
<!-- 触发 -->
<button @click="open=true">新增房源</button>
<!-- 遮罩 -->
<div x-show="open" x-transition.opacity
class="fixed inset-0 z-50 bg-neutral-900/40"
@click="open=false"></div>
<!-- 面板 -->
<div x-show="open" x-transition
class="fixed inset-0 z-60 flex items-center justify-center p-4 pointer-events-none">
<div class="w-full max-w-lg bg-white rounded-xl shadow-lg pointer-events-auto flex flex-col max-h-[85vh]">
<!-- Header -->
<div class="flex items-center justify-between px-5 py-4 border-b border-neutral-200">
<h2 class="text-base font-semibold text-neutral-800">新增房源</h2>
<button @click="open=false" class="p-1 text-neutral-500 hover:bg-neutral-100 rounded-md">
<svg class="w-5 h-5"><!-- XMarkIcon --></svg>
</button>
</div>
<!-- Body -->
<div class="flex-1 overflow-y-auto px-5 py-4 space-y-4">
<!-- 表单内容 -->
</div>
<!-- Footer -->
<div class="flex items-center justify-end gap-2 px-5 py-3 border-t border-neutral-200 bg-neutral-50">
<button @click="open=false" class="px-3 py-1.5 text-sm border border-neutral-300 rounded-md hover:bg-white">取消</button>
<button class="px-3 py-1.5 text-sm bg-primary-600 text-white rounded-md hover:bg-primary-700">保存</button>
</div>
</div>
</div>
</div>
```
#### 3.6.3 Drawer 标准结构
```html
<div x-show="open" x-transition:enter="ease-out duration-200"
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
x-transition:leave="ease-in duration-150"
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
class="fixed right-0 top-0 h-full w-[640px] z-60 bg-white shadow-lg flex flex-col border-l border-neutral-200">
<!-- 同 Modal 的 Header / Body(overflow-y-auto) / Footer 结构 -->
</div>
```
#### 3.6.4 Confirm Modal
用于删除、下架、离职等不可逆操作。固定使用 Danger 风格:
```html
<!-- 面板内结构 -->
<div class="p-5 text-center space-y-3">
<div class="mx-auto w-12 h-12 rounded-full bg-danger-50 flex items-center justify-center">
<svg class="w-6 h-6 text-danger-600"><!-- ExclamationTriangleIcon --></svg>
</div>
<h2 class="text-base font-semibold text-neutral-800">确认删除该房源?</h2>
<p class="text-sm text-neutral-500">删除后将进入回收站30 天内可恢复。</p>
</div>
<div class="flex items-center justify-center gap-2 px-5 py-3 border-t bg-neutral-50">
<button class="px-4 py-1.5 text-sm border border-neutral-300 rounded-md">取消</button>
<button class="px-4 py-1.5 text-sm bg-danger-600 text-white rounded-md hover:bg-danger-600/90">确认删除</button>
</div>
```
---
### 3.7 状态标签Badge / Tag
用于状态展示,不可点击,不可编辑(编辑用 Multi-select Tag Input
| 样式类型 | 场景 | Tailwind 类 |
|---|---|---|
| Subtle默认底色+文字) | 高频状态展示 | `bg-{color}-50 text-{color}-700` |
| Solid | 极少数强调(如"紧急" | `bg-{color}-600 text-white` |
| Outline | 低密度场景 | `border border-{color}-600 text-{color}-700` |
#### 3.7.1 业务状态色板
| 状态 | 色系 | 示例 |
|---|---|---|
| 在售 / 激活 / 在职 | success | `bg-success-50 text-success-600` |
| 出租 | info | `bg-info-50 text-info-600` |
| 跟进中 / 待确认 | warning | `bg-warning-50 text-warning-600` |
| 已成交 / 完成 | primary | `bg-primary-50 text-primary-700` |
| 已下架 / 停用 / 离职 | neutral | `bg-neutral-100 text-neutral-500` |
| 逾期 / 紧急 / 冻结 | danger | `bg-danger-50 text-danger-600` |
| 暂缓 | 浅灰 + 斜体 | `bg-neutral-100 text-neutral-600 italic` |
#### 3.7.2 交易类型标签色板
交易类型标签(买卖 / 租赁 / 租售)属于**分类标签**不表达状态语义禁止使用语义色danger / info solid
**视觉层级要求**:交易类型标签须比同行副标签(满五、独家等)**更大、更醒目**
- 交易类型标签:`text-xs`12px+ `px-2 py-0.5` + `font-semibold` + 较深底色
- 副标签(房源属性 Tag`text-[10px]` + `px-1.5 py-0.5` + `font-medium` + 极淡底色
| 交易类型 | 色系 | Tailwind 类 | 设计理由 |
|---|---|---|---|
| 买卖 | primaryTeal | `bg-primary-200 text-primary-800` | 核心出售业务,用品牌主色强调 |
| 租赁 | warningAmber | `bg-warning-200 text-warning-800` | 与买卖形成色相区分,暖色低调 |
| 租售 | neutral | `bg-neutral-300 text-neutral-800` | 兼含两种类型,中性色避免歧义 |
> **禁止**对交易类型标签使用 Solid 样式(`bg-{color}-600 text-white`),该样式仅用于极少数强调性状态(如"紧急")。
#### 3.7.2 标准片段
```html
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded
bg-success-50 text-success-600">
<span class="w-1.5 h-1.5 rounded-full bg-success-600"></span>
在售
</span>
```
---
### 3.8 Toast 通知
- 统一出现在**右下角**`fixed bottom-6 right-6 z-70`
- 多条堆叠,新消息**追加在底部**,超出视窗则顶部旧消息自动移除
- 固定宽度 `w-80`
| 类型 | 图标色 | 停留 | 手动关闭 |
|---|---|---|---|
| Success | `text-success-600` | 3s | 否 |
| Error | `text-danger-600` | 5s | 是 |
| Warning | `text-warning-600` | 5s | 是 |
| Info | `text-info-600` | 3s | 否 |
```html
<div class="w-80 bg-white rounded-lg shadow-lg border border-neutral-200
flex items-start gap-3 p-3"
x-data="{ show: true }" x-show="show" x-transition>
<svg class="w-5 h-5 text-success-600 shrink-0 mt-0.5"><!-- CheckCircleIcon --></svg>
<div class="flex-1 text-sm">
<p class="font-medium text-neutral-800">保存成功</p>
<p class="text-xs text-neutral-500">房源已更新</p>
</div>
<button @click="show=false" class="text-neutral-400 hover:text-neutral-600">
<svg class="w-4 h-4"><!-- XMarkIcon --></svg>
</button>
</div>
```
**HTMX 触发规范**:后端响应 `HX-Trigger` header
```
HX-Trigger: {"fonrey:toast": {"type": "success", "message": "保存成功", "detail": "房源已更新"}}
```
前端全局监听 `document.addEventListener('fonrey:toast', ...)` 插入 Toast 节点。
---
### 3.9 加载状态Loading States
| 场景 | 实现方式 |
|---|---|
| HTMX 局部请求 | 目标区域叠加 Skeleton`animate-pulse` 灰条占位),`htmx:beforeRequest` 添加,`htmx:afterSettle` 移除 |
| 按钮提交中 | 禁用按钮 + 内嵌 Spinner + 文案改为进行时("保存中…" |
| 页面首次加载 | 内容区骨架屏(与最终结构同骨架) |
| 长耗时任务(导出) | Celery 异步Info Toast 提示"任务已提交,完成后通知";结果通过站内消息推送 |
#### 3.9.1 Skeleton 标准
```html
<div class="animate-pulse space-y-2">
<div class="h-4 bg-neutral-200 rounded w-3/4"></div>
<div class="h-4 bg-neutral-200 rounded w-1/2"></div>
</div>
```
#### 3.9.2 Spinner
```html
<svg class="w-4 h-4 animate-spin text-current" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" fill="none" opacity="0.25"/>
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" fill="none"/>
</svg>
```
---
### 3.10 Tab 导航Tabs
用于详情页内容区切换、筛选维度切换。
```html
<div x-data="{ active: 'basic' }">
<div class="border-b border-neutral-200 flex items-center gap-4">
<button @click="active='basic'"
:class="active==='basic' ? 'border-primary-600 text-primary-700' : 'border-transparent text-neutral-500 hover:text-neutral-700'"
class="px-1 py-3 text-sm font-medium border-b-2 transition-colors">
基本信息
</button>
<button @click="active='follow'"
:class="active==='follow' ? 'border-primary-600 text-primary-700' : 'border-transparent text-neutral-500 hover:text-neutral-700'"
class="px-1 py-3 text-sm font-medium border-b-2 transition-colors">
跟进记录
<span class="ml-1 px-1.5 py-0.5 text-xs bg-neutral-100 text-neutral-600 rounded">12</span>
</button>
</div>
<div x-show="active==='basic'" class="pt-4"
hx-get="/property/123/basic/" hx-trigger="intersect once">
<!-- 懒加载 -->
</div>
</div>
```
- **激活指示**:底部 2px 主色下划线 + 主色文字
- **计数 Badge**:中性色小圆角标
- **懒加载**:切换到 Tab 时用 HTMX `hx-get` 拉取,首次加载后缓存
---
### 3.11 折叠面板Accordion / Collapsible
引入 Alpine 官方插件 `@alpinejs/collapse`1KB处理高度过渡。
```html
<div x-data="{ open: true }" class="bg-white rounded-lg border border-neutral-200">
<button @click="open=!open"
class="w-full flex items-center justify-between px-4 py-3 text-left">
<span class="text-sm font-semibold text-neutral-800">重点信息</span>
<div class="flex items-center gap-2">
<span class="text-sm text-neutral-500">8% / 8%</span>
<svg :class="open ? 'rotate-180' : ''" class="w-4 h-4 text-neutral-400 transition-transform">
<!-- ChevronDownIcon -->
</svg>
</div>
</button>
<div x-show="open" x-collapse class="px-4 pb-3 space-y-2 border-t border-neutral-100">
<!-- 子内容 -->
</div>
</div>
```
---
### 3.12 Toggle 开关Switch
```html
<button type="button" role="switch"
x-data="{ on: false }" @click="on=!on"
:aria-checked="on"
:class="on ? 'bg-primary-600' : 'bg-neutral-300'"
class="relative inline-flex items-center w-9 h-5 rounded-full transition-colors
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40
disabled:opacity-50 disabled:cursor-not-allowed">
<span :class="on ? 'translate-x-4' : 'translate-x-0.5'"
class="inline-block w-4 h-4 bg-white rounded-full shadow transition-transform"></span>
</button>
```
---
### 3.13 日期与日期范围
- **单日期**:原生 `<input type="date">` + `@tailwindcss/forms` 样式
- **日期范围(核心)**:引入 **Flatpickr**16KB无框架依赖。参考 `组件清单.md` 推荐。
```html
<input type="text" id="date-range"
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300
focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20"
placeholder="开始日期 - 结束日期">
<script>
flatpickr('#date-range', { mode: 'range', showMonths: 2, locale: 'zh', dateFormat: 'Y-m-d' });
</script>
```
Flatpickr 自定义样式覆盖见附录 10.3。
---
### 3.14 文件上传
- **单/少量图片(头像、实勘主图)**:原生 `<input type="file">` + Alpine 预览
- **多图批量上传(相册管理)**:引入 **Filepond**,支持拖拽、预览、进度、队列
- **拖拽排序**:引入 **SortableJS**3KB
统一上传目标 Cloudflare R2后端接收后转存并返回 URL。上传进度条使用 Filepond 内置样式,颜色 override 为 `primary-600`。
---
### 3.15 图片预览Lightbox
引入 **Viewer.js**5KB无依赖。覆盖缩放、旋转、全屏、翻页、缩略图条全部需求。样式用 Tailwind 覆盖。
---
## 4. 业务组件规范Business Components
### 4.1 房源卡片Property Card
用于"卡片视图"模式(列表页允许用户切换表格/卡片两种视图)。
```
┌──────────────────────────────────────┐
│ ┌─────┐ 房源标题2行截断 ⋯ │
│ │封面 │ 浦东 · 张江高科 │
│ │ 图 │ 89㎡ · 2室1厅 · 高层 │
│ └─────┘ ¥580 万 [在售] │
│ ──────────────────────────────── │
│ 👤 张三 · 2 小时前更新 │
└──────────────────────────────────────┘
```
- 宽度:响应式栅格 `grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`
- 封面图:`w-32 h-24 rounded object-cover`,固定比例 4:3
- 点击整卡:进入详情页
- 右上角 ``:触发 Action Menu见 4.4
### 4.2 筛选栏Filter Bar
```
┌─────────────────────────────────────────────────────────────────┐
│ 区域: [浦东新区▾] 价格: [¥300万-¥800万▾] 户型:[▾] 状态:[在售▾] │
│ [+ 高级筛选 (3)] [清空] [搜索] │
├─────────────────────────────────────────────────────────────────┤
│ 已选: [浦东新区 ×] [¥300-800万 ×] [2室1厅 ×] [清空所有] │
└─────────────────────────────────────────────────────────────────┘
```
- 常驻筛选横向排列,用 Select / Multi-select / Date Range
- 高级筛选按钮(带数字 Badge 表示已填的高级条件数量),点击展开折叠区
- **筛选变化触发 `hx-get`**`hx-trigger="change delay:200ms"`,结果刷新列表 `hx-target`
- 已选条件 Tag 可单独删除(`×`)或一键清空
### 4.3 数据统计卡片Stat Card
```html
<div class="bg-white rounded-lg border border-neutral-200 p-4 flex items-start gap-3">
<div class="w-10 h-10 rounded-lg bg-primary-50 flex items-center justify-center text-primary-600 shrink-0">
<svg class="w-5 h-5"><!-- HomeIcon --></svg>
</div>
<div class="flex-1 min-w-0">
<div class="text-xs text-neutral-500">在售房源</div>
<div class="mt-1 flex items-baseline gap-2">
<span class="text-xl font-semibold tabular-nums text-neutral-900">1,289</span>
<span class="inline-flex items-center text-xs text-success-600">
<svg class="w-3 h-3"><!-- ArrowTrendingUpIcon --></svg> 12.5%
</span>
</div>
<div class="text-xs text-neutral-400 mt-0.5">较上月</div>
</div>
</div>
```
### 4.4 操作菜单Action Menu
三点按钮 `` 触发下拉,危险操作(删除)永远在底部且有分隔线。
```html
<div x-data="{ open: false }" @click.away="open=false" class="relative">
<button @click="open=!open" class="p-1 rounded hover:bg-neutral-100 text-neutral-500">
<svg class="w-5 h-5"><!-- EllipsisHorizontalIcon --></svg>
</button>
<div x-show="open" x-transition class="absolute right-0 mt-1 w-44 z-40
bg-white rounded-md shadow-md border border-neutral-200 py-1">
<button class="w-full text-left px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-50 flex items-center gap-2">
<svg class="w-4 h-4 text-neutral-500"><!-- PencilIcon --></svg> 编辑
</button>
<button class="w-full text-left px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-50 flex items-center gap-2">
<svg class="w-4 h-4 text-neutral-500"><!-- ShareIcon --></svg> 分享
</button>
<button class="w-full text-left px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-50 flex items-center gap-2">
<svg class="w-4 h-4 text-neutral-500"><!-- ArrowUpOnSquareIcon --></svg> 下架
</button>
<div class="my-1 border-t border-neutral-100"></div>
<button class="w-full text-left px-3 py-2 text-sm text-danger-600 hover:bg-danger-50 flex items-center gap-2">
<svg class="w-4 h-4"><!-- TrashIcon --></svg> 删除
</button>
</div>
</div>
```
### 4.5 跟进时间线Follow-up Timeline
用于房源/客源详情页的跟进记录展示。
```html
<ol class="relative border-l-2 border-neutral-200 ml-3 space-y-5 pl-5">
<li class="relative">
<span class="absolute -left-[27px] top-1 w-3 h-3 rounded-full bg-primary-600 ring-4 ring-white"></span>
<div class="flex items-center gap-2 text-xs text-neutral-500">
<span class="font-medium text-neutral-700">张三</span>
<span>修改跟进</span>
<span>·</span>
<time>2026-04-25 14:30</time>
</div>
<div class="mt-1 text-sm text-neutral-700">
业主同意降价 20 万,挂牌价调整为 580 万,已通知重点客户。
</div>
</li>
<!-- 更多条目 -->
</ol>
<div class="mt-4 text-center">
<button hx-get="/property/123/follow/?page=2" hx-target="this" hx-swap="beforeend"
class="text-sm text-primary-600 hover:text-primary-700">查看全部跟进</button>
</div>
```
### 4.6 相册管理Photo Gallery Manager
参考 `组件清单.md`。核心组件:
- 可横向滚动分类 Tab + 溢出 ``
- 多选图片网格(`grid-cols-6 gap-2`,每格 `aspect-[4/3]`
- 批量工具栏(依赖选中状态启用)
- 上传 ModalFilepond
- 排序模式SortableJS
### 4.7 字段填写要求配置表Dynamic Form Table
用于 `系统设置 → 房源设置 → 字段标签设置`。一张可增删行 + 拖拽排序 + Toggle 切换的表格,结合 SortableJS + Alpine.js。区分系统预置行不可删与用户自定义行。详见 `组件清单.md` §Sortable Table with Drag Handle。
### 4.8 权限矩阵Permission Matrix
纵向:功能模块(房源、客源、楼盘 …);横向:权限动作(查看、新增、编辑、删除、审核)。交叉单元格为 Toggle 或三态(继承 / 开 / 关)。宽度超屏时 **左列固定** + 横向滚动:
```html
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr>
<th class="sticky left-0 bg-white z-10 px-4 py-2 text-left text-xs font-medium text-neutral-500">
功能模块
</th>
<th class="px-4 py-2 text-center text-xs">查看</th>
<th class="px-4 py-2 text-center text-xs">新增</th>
<!-- ... -->
</tr>
</thead>
<!-- ... -->
</table>
</div>
```
### 4.9 业主/客源联系人卡片Contact Card
头像 + 姓名 + 电话(敏感脱敏 `138****1234` + 角色标签 + 操作菜单。手机号悬浮显示"查看完整"按钮(触发权限校验 + 审计日志)。
---
## 5. 页面布局模板Page Layout Templates
### 5.1 整体框架
```
┌──────────────────────────────────────────────────────────────────────┐
│ Topbar56pxsticky top-0 z-20
│ [Logo 150px] [主导航: 主页 房源 客源 营销 交易 数据 人事 系统] │
│ [消息🔔] [帮助?] [头像▾] │
├─────────────────┬────────────────────────────────────────────────────┤
│ Sidebar │ 内容区Content Area
│ 展开 240px │ 面包屑 > 房源管理 > 二手 & 租赁 [次操作] [主操作] │
│ 折叠 64px │ ───────────────────────────────────────────────── │
│ │ 页面标题 H1 │
│ [当前主分类 │ ───────────────────────────────────────────────── │
│ 的子菜单] │ 筛选 / 工具栏Stickytop-14
│ │ ───────────────────────────────────────────────── │
│ ← 折叠按钮 │ 内容主体(表格 / 卡片 / 表单) │
│ │ │
│ │ 分页栏 │
└─────────────────┴────────────────────────────────────────────────────┘
```
**尺寸约定**
| 区域 | 规格 |
|---|---|
| Topbar 高度 | 56px`h-14``sticky top-0 z-20` |
| Sidebar 展开宽度 | 240px`w-60``fixed left-0 top-14 h-[calc(100vh-56px)] z-20` |
| Sidebar 折叠宽度 | 64px`w-16` |
| 内容区偏移 | 展开态 `ml-60`,折叠态 `ml-16``transition-[margin] duration-200` |
| 内容区内边距 | `px-6 py-4` |
| 工具栏 Sticky 偏移 | `sticky top-14 z-30`Topbar 下方紧贴) |
---
### 5.2 顶部导航栏Topbar
#### 5.2.1 结构规范
Topbar 分三区左区Logo、中区主导航 + 搜索)、右区(工具区)。
```
┌─────────────────────────────────────────────────────────────────────┐
│ [🏢 Fonrey ▾] 主页 房源 客源 营销 [搜索框] [🔔] [⚙] [WS 王顺] │
└─────────────────────────────────────────────────────────────────────┘
```
| 区域 | 内容 | 样式 |
|---|---|---|
| 左区(`w-60 shrink-0` | Logo 图标 + 产品名"Fonrey" | `flex items-center gap-2 px-4 text-base font-semibold text-white` |
| 中区(`flex-1` | 主分类导航 + 全局搜索框 | `flex items-center gap-4 flex-1 px-2` |
| 右区(`shrink-0` | 消息通知 / 设置 / 头像+姓名 | `flex items-center gap-1 px-4 shrink-0` |
> **配色**Topbar 背景使用 `bg-primary-800``#134E4A`,深青绿),与下方白色 Sidebar 和 `bg-neutral-50` 内容区形成明确层次区分,同时保持品牌色系一致性。
#### 5.2.2 主导航 Tab
主分类8项**主页 / 房源 / 客源 / 营销 / 交易 / 数据 / 人事 / 系统**
- 每项:`px-3 py-1.5 text-sm rounded-md`
- 默认态:`text-primary-100 hover:bg-primary-700 hover:text-white`
- 激活态:`bg-primary-600 text-white font-medium`
- 点击切换主分类 → Sidebar 同步切换为该分类的子菜单
#### 5.2.3 全局搜索框
位于主导航右侧,宽度 `max-w-xs`。
- 默认态:`bg-primary-700/60 border-transparent text-white placeholder:text-primary-300`
- Hover`hover:bg-primary-700`
- Focus`focus:bg-white focus:text-neutral-700 focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20`
#### 5.2.4 右区工具
| 元素 | 图标 | 样式 |
|---|---|---|
| 消息通知 | `BellIcon` 20px + 红点 Badge | `p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md` |
| 设置 | `Cog6ToothIcon` 20px | 同上 |
| 头像菜单 | `w-7 h-7 rounded-full bg-primary-600` + 姓名 | 头像缩写 + `text-sm font-medium text-primary-100`,左侧 `border-l border-primary-700` 分隔 |
#### 5.2.5 Topbar HTML 片段
```html
<header class="fixed top-0 left-0 right-0 h-14 z-20 bg-primary-800 flex items-center justify-between">
<!-- 左区Logo -->
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
<span class="text-base font-semibold text-white">Fonrey</span>
</div>
<!-- 中区:主导航 + 搜索 -->
<div class="flex items-center gap-4 flex-1 px-2">
<nav class="flex items-center gap-1 text-sm" aria-label="主导航">
{% for item in nav_items %}
<a href="{{ item.url }}"
class="px-3 py-1.5 rounded-md
{% if item.active %}bg-primary-600 text-white font-medium
{% else %}text-primary-100 hover:bg-primary-700 hover:text-white{% endif %}">
{{ item.label }}
</a>
{% endfor %}
</nav>
<!-- 全局搜索 -->
<div class="max-w-xs w-full relative">
<svg class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-primary-300" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-4.3-4.3M11 19a8 8 0 1 1 0-16 8 8 0 0 1 0 16Z"/>
</svg>
<input type="text" placeholder="搜索房源 / 客户 / 楼盘 ⌘K"
class="w-full pl-9 pr-3 py-1.5 text-sm rounded-md
bg-primary-700/60 border border-transparent text-white placeholder:text-primary-300
hover:bg-primary-700
focus:bg-white focus:text-neutral-700 focus:border-primary-600
focus:ring-2 focus:ring-primary-600/20 focus:outline-none
focus:placeholder:text-neutral-400">
</div>
</div>
<!-- 右区:工具 -->
<div class="flex items-center gap-1 px-4 shrink-0">
<!-- 消息通知 -->
<button class="relative p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md" aria-label="消息通知">
<svg class="w-5 h-5"><!-- BellIcon --></svg>
<span class="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-danger-600"></span>
</button>
<!-- 设置 -->
<button class="p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md" aria-label="设置">
<svg class="w-5 h-5"><!-- Cog6ToothIcon --></svg>
</button>
<!-- 头像 + 姓名 -->
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
<div class="w-7 h-7 rounded-full bg-primary-600 text-white flex items-center justify-center text-xs font-semibold">WS</div>
<span class="text-sm font-medium text-primary-100">王顺</span>
</div>
</div>
</header>
```
┌─────────────────────────────────────────────────────────────────────┐
│ [🏢 Fonrey ▾] 主页 房源 客源 营销 交易 数据 人事 系统 │
│ [🔍] [🔔2] [?] [WS 王顺▾] │
└─────────────────────────────────────────────────────────────────────┘
```
| 区域 | 内容 | 样式 |
|---|---|---|
| 左区150px | Logo 图 + 产品名"Fonrey" | `flex items-center gap-2 px-4 text-base font-semibold text-white` |
| 中区flex-1 | 主分类导航 Tab | 见 5.2.2 |
| 右区auto | 全局搜索图标 / 消息 / 帮助 / 头像菜单 | `flex items-center gap-1 px-4` |
#### 5.2.2 主导航 Tab
主分类8项**主页 / 房源 / 客源 / 营销 / 交易 / 数据 / 人事 / 系统**
- 每项:`px-3 py-1.5 text-sm font-medium rounded-md`
- 默认态:`text-primary-100 hover:bg-primary-700 hover:text-white`
- 激活态:`bg-primary-600 text-white`
- 点击切换主分类 → Sidebar 同步切换为该分类的子菜单
#### 5.2.3 右区工具
| 元素 | 图标 | 说明 |
|---|---|---|
| 全局搜索 | `MagnifyingGlassIcon` 20px | 点击展开全局搜索 Popover`max-w-lg` |
| 消息通知 | `BellIcon` 20px + 红点 Badge | 未读数 ≤ 99超出显示"99+";点击打开通知 Drawer |
| 帮助 | `QuestionMarkCircleIcon` 20px | 链接到帮助中心或触发 Tour |
| 头像菜单 | 头像(`w-8 h-8 rounded-full`+ 姓名缩写 | 下拉菜单:个人设置 / 切换角色 / 退出 |
> **配色说明**Topbar 背景使用 `bg-primary-800``#134E4A`,深青绿),与下方白色 Sidebar 和 `bg-neutral-50` 内容区形成明确层次区分,同时保持品牌色系一致性。
#### 5.2.4 Topbar HTML 片段
```html
<header class="fixed top-0 left-0 right-0 h-14 z-20
bg-primary-800
flex items-center justify-between">
<!-- 左区Logo -->
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
<span class="text-base font-semibold text-white">Fonrey</span>
</div>
<!-- 中区:主导航 -->
<nav class="flex items-center gap-0.5 flex-1 px-2" aria-label="主导航">
{% for item in nav_items %}
<a href="{{ item.url }}"
class="px-3 py-1.5 text-sm font-medium rounded-md
{% if item.active %}bg-primary-600 text-white
{% else %}text-primary-100 hover:bg-primary-700 hover:text-white{% endif %}">
{{ item.label }}
</a>
{% endfor %}
</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"><!-- MagnifyingGlassIcon --></svg>
</button>
<!-- 消息通知 -->
<button class="relative p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md"
aria-label="消息通知">
<svg class="w-5 h-5"><!-- BellIcon --></svg>
<span class="absolute top-1 right-1 min-w-[16px] h-4 px-1 text-[10px] font-bold
bg-danger-600 text-white rounded-full flex items-center justify-center
leading-none">2</span>
</button>
<!-- 帮助 -->
<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"><!-- QuestionMarkCircleIcon --></svg>
</button>
<!-- 头像菜单 -->
<div x-data="{ open: false }" @click.away="open=false" class="relative ml-1 pl-3 border-l border-primary-700">
<button @click="open=!open"
class="flex items-center gap-2 p-1 rounded-md hover:bg-primary-700">
<span class="w-8 h-8 rounded-full bg-primary-600 text-white
flex items-center justify-center text-sm font-semibold">
</span>
<span class="text-sm text-primary-100 font-medium">王顺</span>
<svg class="w-4 h-4 text-primary-300"><!-- ChevronDownIcon --></svg>
</button>
<div x-show="open" x-transition
class="absolute right-0 mt-1 w-44 bg-white rounded-md shadow-md
border border-neutral-200 py-1 z-40">
<a href="/profile/" class="flex items-center gap-2 px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-50">
<svg class="w-4 h-4 text-neutral-400"><!-- UserIcon --></svg> 个人设置
</a>
<div class="my-1 border-t border-neutral-100"></div>
<a href="/logout/" class="flex items-center gap-2 px-3 py-2 text-sm text-danger-600 hover:bg-danger-50">
<svg class="w-4 h-4"><!-- ArrowRightOnRectangleIcon --></svg> 退出登录
</a>
</div>
</div>
</div>
</header>
```
---
### 5.3 侧边栏Sidebar
#### 5.3.1 结构与折叠规范
```
展开态240px 折叠态64px
┌─────────────────────────┐ ┌──────┐
│ ▾ 房源管理 │ │ 🏘️ │ ← 一级图标Hover Tooltip
│ 全部房源 ● │ │──────│
│ 二手 & 租赁 │ │ 👤 │
│ 商铺 / 写字楼 │ │ 🏢 │
│─────────────────────────│ │ ⚙️ │
│ 客源管理 │ └──────┘
│ 私客管理 ● │
│ 公客池 │
│─────────────────────────│
│ [← 折叠] │ ← 折叠按钮(固定在 Sidebar 底部)
└─────────────────────────┘
```
- **Sidebar 定位**`fixed left-0 top-14 h-[calc(100vh-56px)] z-20 overflow-y-auto`
- **展开宽度**`w-60`240px折叠宽度`w-16`64px
- **Alpine.js 状态**`x-data="{ collapsed: $persist(false).as('fonrey:sidebar:collapsed') }"`(使用 `@alpinejs/persist` 持久化)
- **内容区联动**`<main :class="collapsed ? 'ml-16' : 'ml-60'" class="transition-[margin] duration-200">`
#### 5.3.2 菜单层级
Sidebar 展示**当前主分类的子菜单**,分为一级菜单和二级菜单(可选)。
| 层级 | 样式 | 激活态 |
|---|---|---|
| 一级菜单(有子项) | `flex items-center gap-3 px-3 py-2 text-sm font-medium text-neutral-700 rounded-md hover:bg-neutral-100 cursor-pointer` | `text-neutral-900 bg-neutral-100` |
| 一级菜单(无子项,直链) | 同上 | `bg-primary-50 text-primary-700 font-semibold border-l-2 border-primary-600 rounded-l-none` |
| 二级菜单 | `ml-8 flex items-center gap-2 px-3 py-1.5 text-sm text-neutral-600 rounded-md hover:bg-neutral-100` | `bg-primary-50 text-primary-700 font-medium border-l-2 border-primary-600 rounded-l-none` |
| 分组标题(可选) | `px-3 pt-4 pb-1 text-xs font-semibold text-neutral-400 uppercase tracking-wider` | — |
#### 5.3.3 折叠态行为
- 仅显示一级菜单图标(`w-5 h-5`),居中 `justify-center`
- 文字、箭头、二级菜单全部隐藏(`x-show="!collapsed"`
- Hover 图标时,右侧弹出 Tooltip菜单名称`absolute left-16 ml-1 px-2 py-1 text-xs bg-neutral-900 text-white rounded whitespace-nowrap z-40`
- 折叠按钮变为展开图标(`ChevronRightIcon`),固定在底部
#### 5.3.4 Sidebar HTML 片段
```html
<aside x-data="{ collapsed: $persist(false).as('fonrey:sidebar:collapsed'),
openGroup: $persist('property').as('fonrey:sidebar:openGroup') }"
:class="collapsed ? 'w-16' : 'w-60'"
class="fixed left-0 top-14 h-[calc(100vh-56px)] z-20
bg-white border-r border-neutral-200
flex flex-col overflow-y-auto overflow-x-hidden
transition-[width] duration-200">
<!-- 菜单主体 -->
<nav class="flex-1 py-3 space-y-0.5 px-2">
<!-- 一级菜单(有子菜单,可折叠) -->
<div>
<button @click="openGroup = (openGroup === 'property' ? '' : 'property')"
class="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium
text-neutral-700 rounded-md hover:bg-neutral-100
relative group">
<svg class="w-5 h-5 text-neutral-500 shrink-0"><!-- HomeModernIcon --></svg>
<span x-show="!collapsed" class="flex-1 text-left">房源管理</span>
<svg x-show="!collapsed"
:class="openGroup === 'property' ? 'rotate-90' : ''"
class="w-4 h-4 text-neutral-400 transition-transform shrink-0"><!-- ChevronRightIcon --></svg>
<!-- 折叠态 Tooltip -->
<span x-show="collapsed"
class="pointer-events-none absolute left-14 ml-1 px-2 py-1 text-xs
bg-neutral-900 text-white rounded whitespace-nowrap z-40
opacity-0 group-hover:opacity-100 transition-opacity">
房源管理
</span>
</button>
<!-- 二级菜单 -->
<div x-show="!collapsed && openGroup === 'property'" x-collapse class="mt-0.5 space-y-0.5">
<a href="/property/all/"
class="ml-8 flex items-center gap-2 px-3 py-1.5 text-sm text-neutral-600
rounded-r-md hover:bg-neutral-100
{% if active_sub == 'all' %}bg-primary-50 text-primary-700 font-medium
border-l-2 border-primary-600{% endif %}">
全部房源
</a>
<a href="/property/sale/"
class="ml-8 flex items-center gap-2 px-3 py-1.5 text-sm text-neutral-600
rounded-r-md hover:bg-neutral-100">
二手 &amp; 租赁
</a>
<a href="/property/commercial/"
class="ml-8 flex items-center gap-2 px-3 py-1.5 text-sm text-neutral-600
rounded-r-md hover:bg-neutral-100">
商铺 / 写字楼
</a>
</div>
</div>
<!-- 一级菜单(直链,无子菜单) -->
<a href="/complex/"
class="flex items-center gap-3 px-3 py-2 text-sm font-medium
text-neutral-700 rounded-md hover:bg-neutral-100
relative group
{% if active_nav == 'complex' %}bg-primary-50 text-primary-700 border-l-2 border-primary-600 rounded-l-none{% endif %}">
<svg class="w-5 h-5 text-neutral-500 shrink-0"><!-- BuildingOffice2Icon --></svg>
<span x-show="!collapsed">楼盘管理</span>
<span x-show="collapsed"
class="pointer-events-none absolute left-14 ml-1 px-2 py-1 text-xs
bg-neutral-900 text-white rounded whitespace-nowrap z-40
opacity-0 group-hover:opacity-100 transition-opacity">
楼盘管理
</span>
</a>
</nav>
<!-- 折叠按钮 -->
<div class="border-t border-neutral-200 p-2">
<button @click="collapsed = !collapsed"
class="w-full flex items-center justify-center gap-2 px-3 py-2 text-sm
text-neutral-500 hover:bg-neutral-100 rounded-md">
<svg :class="collapsed ? 'rotate-180' : ''"
class="w-4 h-4 transition-transform shrink-0"><!-- ChevronLeftIcon --></svg>
<span x-show="!collapsed" class="text-xs">收起</span>
</button>
</div>
</aside>
<!-- 内容区偏移联动(写在 body 或 layout wrapper 层) -->
<main :class="collapsed ? 'ml-16' : 'ml-60'"
class="mt-14 transition-[margin] duration-200 min-h-[calc(100vh-56px)] bg-neutral-50">
{% block content %}{% endblock %}
</main>
```
---
### 5.4 列表页模板
适用:房源列表、客源列表、楼盘列表、权限人员列表、组织人员列表等。
```
面包屑 > 房源管理 > 二手 & 租赁 [导出▾] [+ 新增房源]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[Tab: 全部 (3629) | 在售 | 跟进中 | 已成交 | 已下架]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[筛选栏:区域 / 价格 / 户型 / 状态 / 经纪人 | 高级筛选(3) | 搜索]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[批量工具栏(选中时出现):已选 N 条 | 分享 | 收藏 | 下架 | 删除]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[视图切换: 表格 | 卡片] [密度: 紧凑] [自定义列▾]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
表格主体HTMX 局部刷新)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
分页栏(共 3629 条 · 1 2 3 … · 20/页 · 跳至)
```
### 5.5 详情页模板
适用:房源详情、客源详情、楼盘详情、员工详情。
```
面包屑 > 房源 > 浦东张江花园 89㎡
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[房源标题] [在售] [钥匙房] [收藏] [分享] [⋯ 更多]
浦东新区 · 张江高科路 · 89㎡ · 2室1厅 · ¥580 万
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[Tab: 基本信息 | 跟进记录(12) | 相册(8) | 附件(3) | 操作日志]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
主内容Tab 内容HTMX 懒加载)
┌───────────────────────┐ ┌────────────────────┐
│ 房源信息(含编辑链接)│ │ 维护完成度 69% │
│ │ │ [Accordion 进度] │
│ 产证信息(折叠) │ │ │
│ 房屋介绍 │ │ 相关员工(折叠) │
└───────────────────────┘ └────────────────────┘
```
- 详情页头部 Sticky便于长页面滚动时保持操作按钮可见
- Tab 计数 Badge 实时更新
- 右侧栏(`w-80`)用于维护度、相关员工等辅助信息;主区 `flex-1`
- **编辑入口 = 详情字段旁的"编辑"链接 → 右侧 Drawer 滑入**(遵循"保留上下文"原则)
### 5.6 设置页模板
适用:系统设置、权限管理、个人设置。
```
面包屑 > 系统设置 > 房源设置 > 字段标签设置
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌──────────────────┬────────────────────────────┐
│ 左侧分组导航 │ 右侧内容区 │
240px │ │
│ │ [搜索设置项…] [编辑] │
│ ▾ 房源设置 │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ 新增编辑查看 │ 员工信息设置 │
│ 字段标签设置 ● │ ┃ 个人信息 │
│ 相关方设置 │ ┃ 工龄计算方式: [...] │
│ ▾ 客源设置 │ ┃ 自动工号: [Toggle] │
│ ▾ 人事OA设置 │ │
│ │ [保存变更]Sticky 底部) │
└──────────────────┴────────────────────────────┘
```
- **Read/Edit 模式切换**:右上角"编辑"按钮 → 全页切换到编辑态Alpine.js 全局 `editing` 状态 + 数据快照以便"取消"还原)
- 左侧导航支持二级折叠,激活项左侧 2px 主色竖条
- 底部保存按钮 Sticky编辑态
### 5.7 登录/认证页
独立布局,不使用 Sidebar。居中卡片 `max-w-md`,品牌色背景装饰(左侧 logo + slogan右侧表单参考 Salesforce Lightning 登录页密度。
### 5.8 空状态页Empty Page
见 6.3。
### 5.9 错误页403/404/500
三视图共享同一模板,仅图案与文案不同。`max-w-md mx-auto text-center` + SVG 插图 + 主按钮"返回首页"+ 次按钮"联系管理员"。
---
## 6. 交互状态规范Interaction States
### 6.1 HTMX 请求生命周期
| 阶段 | 事件 | 视觉反馈 |
|---|---|---|
| 发送前 | `htmx:beforeRequest` | 目标区域 Skeleton / 按钮进入 Loading 态 / 触发元素禁用 |
| 进行中 | `htmx:beforeOnLoad` | 保持 Loading |
| 成功 | `htmx:afterSettle` | 移除骨架;如响应头含 `HX-Trigger: fonrey:toast` 则显示 Toast |
| 422校验失败 | 返回 Form Partial | 字段级红色提示 |
| 其他失败 | `htmx:responseError` | 保留原内容 + 全局 Error Toast |
| 网络超时 | `htmx:timeout` | 保留原内容 + "网络异常,请重试" Toast |
### 6.2 表单校验反馈
- **实时校验Alpine.js**`blur` 时触发,不阻断提交
- **提交时校验(后端)**:后端返回 HTTP 422 + 包含错误 Partial字段下方红色小字提示
- **错误提示位置**:字段下方,不使用顶部汇总(经纪人字段多,滚动回找浪费时间)
- **提交成功**Toast + 可选页面跳转(如新增后跳详情)
### 6.3 空状态设计
| 场景 | 标题 | 描述 | 引导操作 |
|---|---|---|---|
| 列表无数据 | 暂无房源 | 可以录入首条房源开始管理 | `+ 新增房源`Primary |
| 搜索无结果 | 未找到匹配结果 | 请尝试调整筛选条件 | `清除筛选条件`Link |
| 权限不足 | 暂无访问权限 | 请联系管理员申请权限 | `联系管理员`Secondary |
| 功能关闭 | 功能未启用 | 请前往系统设置开启 | `前往设置`Link含权限判断 |
结构:
```html
<div class="py-16 px-6 text-center">
<img src="/static/img/empty-list.svg" class="w-40 mx-auto mb-4" alt="">
<h3 class="text-base font-semibold text-neutral-800">暂无房源</h3>
<p class="text-sm text-neutral-500 mt-1">可以录入首条房源开始管理</p>
<button class="mt-4 inline-flex items-center gap-1.5 px-4 py-2 text-sm
bg-primary-600 text-white rounded-md hover:bg-primary-700">
<svg class="w-4 h-4"><!-- PlusIcon --></svg> 新增房源
</button>
</div>
```
### 6.4 键盘快捷键基线
| 快捷键 | 动作 |
|---|---|
| `Ctrl/Cmd + K` | 打开全局搜索 |
| `Esc` | 关闭当前 Modal / Drawer |
| `Enter` | 提交当前表单 / 确认 Modal非 Textarea 场景) |
| `Tab / Shift+Tab` | 表单字段间跳转 |
| `/` | 聚焦当前页面主搜索框 |
| `N`(列表页) | 打开"新增"入口 |
---
## 7. 图标规范Icon Guidelines
### 7.1 图标库
**Heroicons v2**Tailwind 官方出品)
- **Outline 24px**:默认选择(工具栏、导航、行内)
- **Solid 20px**:用于状态指示、已选中态、强调
- **Mini 16pxsolid**极密场景表格行内、Tag 内)
- 所有图标**继承文字颜色**`currentColor`),不单独设置 `fill`
引入方式Django 模板内联 SVG通过自定义 templatetag `{% heroicon 'plus' %}`),避免远程请求,可利用 CDN 缓存。
### 7.2 尺寸规范
| 尺寸 | 场景 | Tailwind |
|---|---|---|
| 24px | 侧边栏一级菜单 | `w-6 h-6` |
| 20px | 顶部栏、Stat Card、Toast | `w-5 h-5` |
| 16px | 按钮内、Tab、行内图标 | `w-4 h-4` |
| 12px | Tag 内、状态点 | `w-3 h-3` |
### 7.3 核心图标映射
| 业务动作 | Heroicon |
|---|---|
| 新增 | `plus` |
| 编辑 | `pencil-square` |
| 删除 | `trash` |
| 搜索 | `magnifying-glass` |
| 筛选 | `funnel` |
| 排序 | `chevron-up-down` |
| 导出 | `arrow-down-tray` |
| 导入 | `arrow-up-tray` |
| 更多 | `ellipsis-horizontal` |
| 关闭 | `x-mark` |
| 返回 | `arrow-left` |
| 刷新 | `arrow-path` |
| 收藏 | `star`solid = 已收藏) |
| 分享 | `share` |
| 通知 | `bell` |
| 帮助 | `question-mark-circle` |
| 用户 | `user` |
| 组织 | `users` |
| 房源 | `home`(首页为 `squares-2x2` |
| 客源 | `user-group` |
| 楼盘 | `building-office-2` |
| 权限 | `lock-closed` |
| 设置 | `cog-6-tooth` |
| 日历 | `calendar` |
| 电话 | `phone` |
| 钥匙 | `key` |
| 附件 | `paper-clip` |
| 图片 | `photo` |
| 视频 | `video-camera` |
| 下载 | `arrow-down-tray` |
| 成功 | `check-circle` |
| 警告 | `exclamation-triangle` |
| 错误 | `x-circle` |
| 信息 | `information-circle` |
**一致性铁律**:同一业务动作在全产品使用**完全相同**的图标。新增业务动作需登记本映射表。
---
## 8. 可访问性基线Accessibility Baseline
- 所有表单字段必须关联 `<label>``for` 属性或包裹)
- 颜色对比度达 WCAG AA 级(正文 4.5:1大文字 3:1已验证主色 `#0F766E` 与白色对比度 5.7:1 ✓
- 所有交互元素支持键盘:`Tab` 聚焦、`Enter/Space` 触发、`ESC` 关闭浮层
- 焦点可见:统一使用 `focus-visible:ring-2 focus-visible:ring-primary-600/40`
- 错误态不仅靠颜色:必须附带文字提示
- 纯图标按钮必须有 `aria-label` 或 `<span class="sr-only">`
- 图片必须有 `alt` 属性;装饰性图片用 `alt=""` + `aria-hidden="true"`
- 图标 SVG 用 `aria-hidden="true"`(除非是唯一语义载体)
- 表格使用语义化 `<th scope="col">`
---
## 9. 已决策事项Resolved Decisions
v1.0 遗留的 6 个问题,已于 v1.1 评审决策如下。
| # | 问题 | 决策 | 实施要点 |
|---|---|---|---|
| 1 | 是否支持暗色主题 | **v1 不做,开发预留接口** | 所有颜色走 Tailwind Token不硬编码 Hex根标签预留 `data-theme="light"` 属性CSS 变量层接入待 v2 |
| 2 | 全局搜索⌘K覆盖范围 | **房源 + 客源 + 楼盘 + 同事 四类** | Command Palette 分组展示;↑↓ 切换、Enter 跳转、ESC 关闭;后端接口 `/api/search/?q=&types=property,customer,building,user` |
| 3 | 表格列是否支持拖拽排序 | **支持:拖拽 + 显隐 + localStorage 记忆** | SortableJS"列设置"Drawer 右侧打开;状态 key `fonrey:table:{module}:cols`,结构 `[{key, visible, order}]` |
| 4 | 已成交状态色 | **info 蓝(`#2563EB`** | 仅用于状态 Tag不扩散到其他元素与 success 绿在售、warning 橙待核验、neutral 灰(已下架)形成完整状态色阶 |
| 5 | 屏幕 <1280px 降级 | **显示引导提示页,锁定主内容** | 全屏 Splash品牌 logo + "Fonrey 为桌面端设计,请放大浏览器窗口或使用 ≥1280px 屏幕"JS 监听 resize 自动显隐;不做响应式适配 |
| 6 | 国际化 | **v1 仅中文,文案硬编码** | 但 Token 层保持中性(不嵌入"万""㎡"等单位到 Token 名);字体栈保留 Inter + PingFang SC 双语准备v2 接入 Django i18n 时无需重构组件 |
### 9.1 小屏降级提示页(规范)
```html
<!-- 监听 window.innerWidth < 1280 时插入id="screen-gate" -->
<div id="screen-gate"
class="fixed inset-0 z-[100] bg-white flex flex-col items-center justify-center px-8 text-center">
<div class="w-16 h-16 rounded-xl bg-primary-600 text-white flex items-center justify-center text-2xl font-semibold mb-6">F</div>
<h1 class="text-xl font-semibold text-neutral-800 mb-2">请使用桌面端访问 Fonrey</h1>
<p class="text-sm text-neutral-600 max-w-md mb-6">
Fonrey 为桌面工作场景设计,建议屏幕宽度 ≥ 1280px。
请放大浏览器窗口,或切换到电脑端访问。
</p>
<p class="text-xs text-neutral-400">当前窗口:<span id="screen-gate-width" class="tabular-nums"></span> px</p>
</div>
```
- 触发阈值:`window.innerWidth < 1280`
- 不阻止登录、不阻止 Token 校验,仅遮罩 UI
- 移动端浏览器同样展示此页(不引导下载 Appv1 无 App
### 9.2 全局搜索⌘K数据结构约定
```js
// 返回示例
{
"property": [{id, code, title, status, subtitle}], // 房源F编号 + 标题 + 商圈
"customer": [{id, name, phone_mask, tag, subtitle}], // 客源:姓名 + 脱敏手机 + 意向
"building": [{id, name, district, unit_price}], // 楼盘:名称 + 商圈 + 均价
"user": [{id, name, dept, avatar_char}] // 同事:姓名 + 部门
}
```
- 分组固定顺序:房源 → 客源 → 楼盘 → 同事
- 每组最多 5 条,更多结果跳对应模块列表页
- 空查询时展示"最近访问"(前 8 条,不分组)
---
## 10. 附录Appendix
### 10.1 完整 Tailwind 配置文件
```js
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./apps/**/templates/**/*.html',
'./templates/**/*.html',
'./apps/**/*.py', // 若使用 django-components
],
theme: {
extend: {
colors: {
primary: {
50: '#F0FDFA',
100: '#CCFBF1',
200: '#99F6E4',
300: '#5EEAD4',
400: '#2DD4BF',
500: '#14B8A6',
600: '#0F766E', // 主基准
700: '#115E59',
800: '#134E4A',
900: '#042F2E',
},
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', 700: '#15803D' },
warning: { 50: '#FFFBEB', 600: '#D97706', 700: '#B45309' },
danger: { 50: '#FEF2F2', 600: '#DC2626', 700: '#B91C1C' },
info: { 50: '#EFF6FF', 600: '#2563EB', 700: '#1D4ED8' },
},
fontFamily: {
sans: ['Inter', '"PingFang SC"', '"Microsoft YaHei"', 'sans-serif'],
mono: ['"JetBrains Mono"', '"SFMono-Regular"', 'Menlo', 'monospace'],
},
boxShadow: {
xs: '0 1px 2px rgba(15, 23, 42, 0.04)',
},
zIndex: {
60: '60',
70: '70',
},
animation: {
'slide-in-right': 'slideInRight 0.2s ease-out',
},
keyframes: {
slideInRight: {
'0%': { transform: 'translateX(100%)' },
'100%': { transform: 'translateX(0)' },
},
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
}
```
### 10.2 第三方库清单(均为无框架依赖)
| 库 | 用途 | 体积 | 引入方式 |
|---|---|---|---|
| HTMX | 服务端驱动交互 | ~14KB | CDN |
| Alpine.js + `@alpinejs/collapse` + `@alpinejs/focus` | 前端状态 | ~18KB | CDN |
| Heroicons | 图标 | 按需内联 | npm/静态文件 |
| Flatpickr | 日期范围选择 | ~16KB | CDN |
| SortableJS | 拖拽排序(图片、表格行) | ~3KB | CDN |
| Filepond | 图片多文件上传 | ~50KB | CDN |
| Viewer.js | 图片灯箱预览 | ~5KB | CDN |
### 10.3 Flatpickr 样式覆盖(配合主色)
```css
/* static/css/flatpickr-overrides.css */
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.endRange { background: #0F766E; border-color: #0F766E; }
.flatpickr-day.inRange { background: #CCFBF1; border-color: #CCFBF1; color: #115E59; }
.flatpickr-day.today { border-color: #D97706; }
```
### 10.4 关联文档
- `Project/fonrey/TECH_STACK/TECH_STACK.md` —— 技术总纲
- `Project/fonrey/UI&UX/组件清单.md` —— 组件可行性与实现建议(本文的工程侧补充)
- `Project/fonrey/PRD/*` —— 各业务模块产品需求
- `Project/fonrey/screenshots/*` —— 竞品截图(设计参考)
### 10.5 变更记录
| 版本 | 日期 | 变更 | 作者 |
|---|---|---|---|
| v1.0 | 2026-04-25 | 首次建立 UI SystemToken / 基础组件 15 项 / 业务组件 9 项 / 5 类页面模板 | UI/UX 架构组 |