58 KiB
Fonrey UI System 设计规范
版本:v1.0 最后更新:2026-04-25 维护者:UI/UX 架构组 适用技术栈:Tailwind CSS + HTMX + Alpine.js + Django 模板 目标分辨率:桌面 Web,≥ 1280px(v1 不做移动端适配)
For developers: 本文是开发还原 UI 的唯一权威。所有组件以 HTML + Tailwind class 描述,不输出 JSX。新增页面须先到此处查找可复用组件与模板;如需新模式,先补本文再落地。 关联文档:
Project/fonrey/TECH_STACK/TECH_STACK.md(技术总纲)Project/fonrey/UI&UX/组件清单.md(组件可行性分析)Project/fonrey/PRD/*(业务需求)
0. 设计语言定调(Design Language Overview)
参考 Linear 的克制、Notion 的信息密度、Salesforce Lightning 的企业严谨,结合竞品(链家/贝壳式房产工具)的操作效率要求,Fonrey 的设计语言定位为:
- 专业、克制、高密度:表格为王,单屏尽可能多展示数据;色彩只为信息服务,不用于装饰。
- 主色靛青(Teal):低饱和、冷静,与状态色(绿/黄/红)形成强区分;与房产行业"稳健/可靠"意象吻合,同时避免与 success 绿产生语义歧义。
- 中等圆角(8px):既不像消费端(12px+)过于柔软,也不像传统企业端(0-2px)过于呆板。
- 紧凑密度:表格行高 40px、表单字段间距 12px,信息密度优先于呼吸感。
1. 设计原则(Design Principles)
- 效率优先(Efficiency First) 减少视觉噪音,让用户聚焦在数据和操作上。表格为核心场景,一屏可见行数 ≥ 15。
- 状态可见(Visible State) 任何异步请求必须有反馈(骨架屏 / Spinner / Toast);任何数据变更必须给出成功或失败的即时提示。HTMX 的"静默成功"是 Bug。
- 复用先于新建(Reuse Over Reinvent) 每个组件在本文中有唯一标准实现;新需求须先尝试组合既有组件,再提案新组件。
- 键盘友好(Keyboard First) 高频操作(搜索、翻页、新增、保存)须支持键盘;表格、表单、弹窗均支持 Tab / Enter / ESC。
- 一致性高于美观(Consistency Over Cleverness) 相同的动作在全产品使用相同的图标、色彩、位置。经纪人的肌肉记忆比单页的视觉惊喜更重要。
2. 设计 Token(Design 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 底色 |
语义色与主色分离:主色是 Teal,success 是独立绿,避免"主操作按钮看起来像成功提示"。
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 配置示例(节选)
// 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 片段
<!-- 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 标准结构
<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 标准
<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 自定义下拉(带搜索)
<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 样式。
<!-- 容器(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> |
高 44px;Hover 高亮 | hover:bg-neutral-50 |
数据单元 <td> |
正文字号;顶对齐 | px-4 py-2.5 text-sm text-neutral-700 whitespace-nowrap |
| 选中行 | 浅主色高亮 | bg-primary-50 |
| 操作列 | 固定右侧;Ghost 图标按钮 | sticky right-0 bg-white |
| 表格底部(分页栏) | px-4 py-3 border-t border-neutral-200 bg-white flex items-center justify-between |
- 斑马纹:B2B 高密度场景不启用(视觉噪音)。Hover 行高亮已足够区分。
- 密度切换:工具栏右侧允许用户切换"紧凑(40px)/ 标准(44px)/ 舒适(52px)"三档,通过 Alpine.js +
localStorage持久化。
3.4.2 标准片段
<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)
<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 标准结构
<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 标准结构
<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 风格:
<!-- 面板内结构 -->
<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 标准片段
<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 | 否 |
<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 标准
<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
<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)
用于详情页内容区切换、筛选维度切换。
<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)处理高度过渡。
<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)
<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推荐。
<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)
<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)
三点按钮 ⋯ 触发下拉,危险操作(删除)永远在底部且有分隔线。
<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)
用于房源/客源详情页的跟进记录展示。
<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]) - 批量工具栏(依赖选中状态启用)
- 上传 Modal(Filepond)
- 排序模式(SortableJS)
4.7 字段填写要求配置表(Dynamic Form Table)
用于 系统设置 → 房源设置 → 字段标签设置。一张可增删行 + 拖拽排序 + Toggle 切换的表格,结合 SortableJS + Alpine.js。区分系统预置行(不可删)与用户自定义行。详见 组件清单.md §Sortable Table with Drag Handle。
4.8 权限矩阵(Permission Matrix)
纵向:功能模块(房源、客源、楼盘 …);横向:权限动作(查看、新增、编辑、删除、审核)。交叉单元格为 Toggle 或三态(继承 / 开 / 关)。宽度超屏时 左列固定 + 横向滚动:
<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 整体框架
┌──────────────────────────────────────────────────────────────┐
│ 顶部导航 Topbar(56px) │
│ [Logo 150px] [全局搜索 360px] [消息] [帮助] [头像▾] │
├────────────┬─────────────────────────────────────────────────┤
│ │ 面包屑 [次要操作] [主操作] │
│ 侧边导航 │ ┌─────────────────────────────────────────────┐ │
│ Sidebar │ │ 页面标题 H1 │ │
│ 240px │ ├─────────────────────────────────────────────┤ │
│ 可折叠 │ │ 筛选 / 工具栏(Sticky) │ │
│ → 64px │ ├─────────────────────────────────────────────┤ │
│ │ │ │ │
│ │ │ 内容主体 │ │
│ │ │ │ │
│ │ └─────────────────────────────────────────────┘ │
└────────────┴─────────────────────────────────────────────────┘
- Topbar 高度:56px,
h-14,bg-white border-b border-neutral-200 sticky top-0 z-20 - Sidebar 宽度:展开 240px (
w-60) / 折叠 64px (w-16);Alpine.js 控制,状态持久化到localStorage - 内容区左内边距:展开态
ml-60,折叠态ml-16,配合transition-all - 主内容区内边距:
px-6 py-4
5.2 侧边栏(Sidebar)
[🏢 Fonrey] 展开态 折叠态
────────────── ──────────────
仪表盘 Dashboard 🏠
房源管理 ▾ Property 🏘️
全部房源
二手 & 租赁
商铺 / 写字楼
客源管理 ▾ Client 👤
楼盘管理 Complex 🏢
组织人事 ▾ Org 👥
权限管理 Permission 🔒
系统设置 ▾ Settings ⚙️
- 一级菜单:图标(20px) + 文字 + 右侧箭头(有子菜单时)
- 二级菜单:文字缩进 32px,激活态为
bg-primary-50 text-primary-700 font-medium+ 左侧 2px 主色竖条 - 折叠态:只显示图标,Hover 时 Tooltip 显示名称
5.3 列表页模板
适用:房源列表、客源列表、楼盘列表、权限人员列表、组织人员列表等。
面包屑 > 房源管理 > 二手 & 租赁 [导出▾] [+ 新增房源]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[Tab: 全部 (3629) | 在售 | 跟进中 | 已成交 | 已下架]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[筛选栏:区域 / 价格 / 户型 / 状态 / 经纪人 | 高级筛选(3) | 搜索]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[批量工具栏(选中时出现):已选 N 条 | 分享 | 收藏 | 下架 | 删除]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[视图切换: 表格 | 卡片] [密度: 紧凑] [自定义列▾]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
表格主体(HTMX 局部刷新)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
分页栏(共 3629 条 · 1 2 3 … · 20/页 · 跳至)
5.4 详情页模板
适用:房源详情、客源详情、楼盘详情、员工详情。
面包屑 > 房源 > 浦东张江花园 89㎡
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[房源标题] [在售] [钥匙房] [收藏] [分享] [⋯ 更多]
浦东新区 · 张江高科路 · 89㎡ · 2室1厅 · ¥580 万
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[Tab: 基本信息 | 跟进记录(12) | 相册(8) | 附件(3) | 操作日志]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
主内容(Tab 内容,HTMX 懒加载)
┌───────────────────────┐ ┌────────────────────┐
│ 房源信息(含编辑链接)│ │ 维护完成度 69% │
│ │ │ [Accordion 进度] │
│ 产证信息(折叠) │ │ │
│ 房屋介绍 │ │ 相关员工(折叠) │
└───────────────────────┘ └────────────────────┘
- 详情页头部 Sticky(便于长页面滚动时保持操作按钮可见)
- Tab 计数 Badge 实时更新
- 右侧栏(
w-80)用于维护度、相关员工等辅助信息;主区flex-1 - 编辑入口 = 详情字段旁的"编辑"链接 → 右侧 Drawer 滑入(遵循"保留上下文"原则)
5.5 设置页模板
适用:系统设置、权限管理、个人设置。
面包屑 > 系统设置 > 房源设置 > 字段标签设置
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌──────────────────┬────────────────────────────┐
│ 左侧分组导航 │ 右侧内容区 │
│ (240px) │ │
│ │ [搜索设置项…] [编辑] │
│ ▾ 房源设置 │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ 新增编辑查看 │ 员工信息设置 │
│ 字段标签设置 ● │ ┃ 个人信息 │
│ 相关方设置 │ ┃ 工龄计算方式: [...] │
│ ▾ 客源设置 │ ┃ 自动工号: [Toggle] │
│ ▾ 人事OA设置 │ │
│ │ [保存变更](Sticky 底部) │
└──────────────────┴────────────────────────────┘
- Read/Edit 模式切换:右上角"编辑"按钮 → 全页切换到编辑态(Alpine.js 全局
editing状态 + 数据快照以便"取消"还原) - 左侧导航支持二级折叠,激活项左侧 2px 主色竖条
- 底部保存按钮 Sticky(编辑态)
5.6 登录/认证页
独立布局,不使用 Sidebar。居中卡片 max-w-md,品牌色背景装饰(左侧 logo + slogan,右侧表单),参考 Salesforce Lightning 登录页密度。
5.7 空状态页(Empty Page)
见 6.3。
5.8 错误页(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,含权限判断) |
结构:
<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 16px(solid):极密场景(表格行内、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. 待确认问题(Open Questions)
| # | 问题 | 负责人 | 截止 |
|---|---|---|---|
| 1 | 需要暗色主题吗?(部分经纪人工作至深夜) | 产品 | v1.1 评估 |
| 2 | 全局搜索(Ctrl+K Command Palette)的信息架构:是否跨模块(房源+客源+楼盘+员工) |
产品 / 后端 | v1 首发范围待定 |
| 3 | 表格列是否支持拖拽调整顺序?(SortableJS 可支持,但需确认是否属于 v1 范围) | 产品 | — |
| 4 | 房源状态色:当前将"已成交"归为 primary(Teal)。部分行业习惯用蓝色,需确认是否可视化区分 | 设计 / 产品 | — |
| 5 | 移动端访问降级方案:v1 是否需要 1024px 以下显示"请使用桌面端访问"提示页? | 前端 | v1 首发前 |
| 6 | 国际化:当前仅中文。如规划英文/繁体,字体栈与行高需调整 | 产品 | v2 |
10. 附录(Appendix)
10.1 完整 Tailwind 配置文件
// 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 样式覆盖(配合主色)
/* 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 System:Token / 基础组件 15 项 / 业务组件 9 项 / 5 类页面模板 | UI/UX 架构组 |