Files
nexus/Project/fonrey/UI_SYSTEM/UI_SYSTEM.md

61 KiB
Raw Blame History

Fonrey UI System 设计规范

版本v1.1 最后更新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.jstheme.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-4p-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 状态

  • Focusfocus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40
  • Loading:禁用 + 内嵌 Spinner + 文案改为进行时("保存中…"
  • Disableddisabled: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 + 取消浏览器 spinnerappearance-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> 密度可切换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 + localStoragekey: 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> 上的 classtable-density-standard),通过父级 class + 子选择器统一控制行高、内边距、图片显隐,避免逐行改 class。 无图业务(客源、跟进记录、权限等)表格固定使用 compact 40px 行高,不提供密度切换。

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 面板,选择状态持久化到 localStorageKey: 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-sm400px 点击遮罩不关闭防误触ESC 关闭
Form Modal 新增/编辑(字段 ≤ 6 max-w-lg560px 点击遮罩关闭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 局部请求 目标区域叠加 Skeletonanimate-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/collapse1KB处理高度过渡。

<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 样式
  • 日期范围(核心):引入 Flatpickr16KB无框架依赖。参考 组件清单.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,支持拖拽、预览、进度、队列
  • 拖拽排序:引入 SortableJS3KB

统一上传目标 Cloudflare R2后端接收后转存并返回 URL。上传进度条使用 Filepond 内置样式,颜色 override 为 primary-600


3.15 图片预览Lightbox

引入 Viewer.js5KB无依赖。覆盖缩放、旋转、全屏、翻页、缩略图条全部需求。样式用 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-gethx-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>

参考 组件清单.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 或三态(继承 / 开 / 关)。宽度超屏时 左列固定 + 横向滚动:

<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 整体框架

┌──────────────────────────────────────────────────────────────┐
│  顶部导航 Topbar56px                                       │
│  [Logo 150px] [全局搜索 360px]          [消息] [帮助] [头像▾] │
├────────────┬─────────────────────────────────────────────────┤
│            │  面包屑   [次要操作] [主操作]                    │
│ 侧边导航   │ ┌─────────────────────────────────────────────┐ │
│ Sidebar    │ │ 页面标题 H1                                  │ │
│ 240px      │ ├─────────────────────────────────────────────┤ │
│ 可折叠     │ │ 筛选 / 工具栏Sticky                      │ │
│ → 64px     │ ├─────────────────────────────────────────────┤ │
│            │ │                                              │ │
│            │ │  内容主体                                    │ │
│            │ │                                              │ │
│            │ └─────────────────────────────────────────────┘ │
└────────────┴─────────────────────────────────────────────────┘
  • Topbar 高度56pxh-14bg-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.jsblur 时触发,不阻断提交
  • 提交时校验(后端):后端返回 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 v2Tailwind 官方出品)

  • 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
收藏 starsolid = 已收藏)
分享 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 小屏降级提示页(规范)

<!-- 监听 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数据结构约定

// 返回示例
{
  "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 配置文件

// 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 SystemToken / 基础组件 15 项 / 业务组件 9 项 / 5 类页面模板 UI/UX 架构组