1265 lines
44 KiB
Markdown
1265 lines
44 KiB
Markdown
## 技术选型
|
||
|
||
- Frontend: HTMX + Alpine.js + Tailwind CSS
|
||
- Backend: Django 4.x (ASGI mode)
|
||
- Multi-tenant: django-tenants (Postgres schema isolation)
|
||
- Database: PostgreSQL + PgBouncer
|
||
- Cache: Redis
|
||
- Tasks: Celery + Celery Beat
|
||
- Storage: Cloudflare R2 (or AWS S3)
|
||
- CDN: Cloudflare
|
||
- Server: Gunicorn + Uvicorn workers + Nginx
|
||
- Monitoring: Sentry + Grafana
|
||
|
||
## Data Table (数据表格)
|
||
|
||
![[IMG-20260425085420706.png]]
|
||
|
||
#### 1. Data Table(数据表格)— 核心组件
|
||
|
||
**你描述的"数据列表"正式名称是:Sortable Data Table with Column Visibility Control**
|
||
|
||
图中包含的子特性:
|
||
|
||
|子特性|技术实现方式|可行性|
|
||
|---|---|---|
|
||
|多列表头,带排序箭头(↑↓)|Django 后端排序 + HTMX 请求 + Tailwind 样式|✅ 完全可行|
|
||
|行 Checkbox 多选|Alpine.js 管理选中状态|✅ 完全可行|
|
||
|行高亮(图中蓝色高亮行)|Alpine.js `:class` 绑定|✅ 完全可行|
|
||
|红色/橙色 Tag 标签(买卖、出租)|Tailwind `badge` 样式|✅ 完全可行|
|
||
|行内小图标按钮(电梯、满五等)|Tailwind + inline SVG icon|✅ 完全可行|
|
||
|价格字段带趋势小箭头(↑绿色)|Alpine.js 条件渲染|✅ 完全可行|
|
||
|列固定宽度 + 横向滚动条|Tailwind `overflow-x-auto` + `min-w`|✅ 完全可行|
|
||
|
||
---
|
||
|
||
#### 2. Pagination(分页组件)
|
||
|
||
图中右下角的 `共3629条 < 1 2 3 4 5 … 182 > 20条/页 跳至`
|
||
|
||
|子特性|实现方式|可行性|
|
||
|---|---|---|
|
||
|页码导航|Django Paginator + HTMX `hx-get`|✅ 完全可行|
|
||
|每页条数选择(20条/页)|Alpine.js 下拉 + HTMX 请求|✅ 完全可行|
|
||
|跳至指定页|Alpine.js 输入框 + HTMX 请求|✅ 完全可行|
|
||
|省略号(…)页码|Django 后端分页逻辑生成|✅ 完全可行|
|
||
|
||
---
|
||
|
||
#### 3. Column Visibility Panel(自定义列显示)
|
||
|
||
图中右上角的 **"自定义列表"** 按钮
|
||
|
||
|子特性|实现方式|可行性|
|
||
|---|---|---|
|
||
|弹出面板,勾选显示/隐藏列|Alpine.js `x-show` + `x-data`|✅ 完全可行|
|
||
|列显示状态持久化|Alpine.js + `localStorage` 或后端存储|✅ 完全可行|
|
||
|动态隐藏表格列|Alpine.js `:class="{'hidden': !col.visible}"`|✅ 完全可行|
|
||
|
||
---
|
||
|
||
#### 4. Toolbar(操作工具栏)
|
||
|
||
图中顶部的 `房源海报 批量分享 批量收藏 取消收藏 设置保护房 更多▼`
|
||
|
||
|子特性|实现方式|可行性|
|
||
|---|---|---|
|
||
|批量操作按钮(依赖 Checkbox 选中状态)|Alpine.js 全局选中状态 + 按钮 disabled 控制|✅ 完全可行|
|
||
|"更多"下拉菜单(Dropdown Menu)|Alpine.js `x-show` + `@click.away`|✅ 完全可行|
|
||
|共 N 条(动态数字)|HTMX 局部刷新 或 Alpine.js 绑定|✅ 完全可行|
|
||
|
||
---
|
||
|
||
#### 5. Export Button(导出按钮)
|
||
|
||
图中右上角 **"导出"** 按钮
|
||
|
||
|实现方式|可行性|
|
||
|---|---|
|
||
|Django 后端生成 CSV/Excel(用 `openpyxl`),返回文件流|✅ 完全可行,无需前端特殊处理|
|
||
|异步导出(数据量大时走 Celery 任务,邮件通知)|✅ 你的 Celery 栈已完全支持|
|
||
|
||
---
|
||
|
||
#### 6. Smart Sort(智能排序)
|
||
|
||
图中右上角 **"按智能排序"** 标签
|
||
|
||
| 实现方式 | 可行性 |
|
||
| ------------------------------- | ------ |
|
||
| 后端 Django ORM 多字段排序,HTMX 切换排序模式 | ✅ 完全可行 |
|
||
|
||
|
||
## Modal Dialog (模态对话框)
|
||
![[IMG-20260425085420782.png]]
|
||
### 🪟 组件名称:Modal Dialog(模态对话框)
|
||
|
||
图中这个"编辑房源价格"弹窗,包含以下子组件:
|
||
|
||
#### 弹窗本体结构
|
||
|
||
|部位|组件名称|实现方式|可行性|
|
||
|---|---|---|---|
|
||
|灰色半透明遮罩层|Backdrop / Overlay|Tailwind `bg-black/50 fixed inset-0`|✅|
|
||
|白色弹窗卡片|Modal Panel|Tailwind `bg-white rounded-lg shadow-xl`|✅|
|
||
|顶部标题栏 + ✕ 关闭按钮|Modal Header|Alpine.js `@click="open=false"`|✅|
|
||
|底部确定/取消按钮组|Modal Footer / Button Group|Tailwind 按钮样式|✅|
|
||
|拖拽手柄(左上角 ⠿ 图标)|Draggable Handle|需要少量原生 JS 或 Alpine.js 插件|⚠️|
|
||
|
||
#### 弹窗内的表单组件
|
||
|
||
|组件|名称|实现方式|可行性|
|
||
|---|---|---|---|
|
||
|带星号必填标记的标签|Required Field Label|Tailwind `text-red-500` + `*` 字符|✅|
|
||
|数字输入框 + 单位后缀(万)|Input with Suffix Addon|Tailwind Input Group|✅|
|
||
|普通文本输入框(售价/备案价)|Text Input|Tailwind `input` 样式|✅|
|
||
|多行文本域(更改理由)|Textarea|Tailwind `textarea` + 字数统计 `0/200`|✅|
|
||
|字数计数(右下角 0/200)|Character Counter|Alpine.js `x-model` + `:text="val.length + '/200'"`|✅|
|
||
|
||
---
|
||
|
||
### ⚠️ 唯一需要注意的点:弹窗拖拽
|
||
|
||
图中左上角有一个 **⠿ 拖拽图标**,说明这个弹窗支持**拖动位置**。这个功能:
|
||
|
||
- Alpine.js 原生**不内置**拖拽
|
||
- 可以用 **Alpine.js 官方插件 `@alpinejs/drag`** 解决,无需引入额外大型库
|
||
|
||
### ✅ 结论
|
||
|
||
> 这个 Modal 弹窗及其内部所有表单组件,**用 Alpine.js + Tailwind CSS 完全可以实现**,包括开关状态、表单验证、字数统计。拖拽功能用 Alpine 官方插件即可覆盖,无需引入 Vue/React。
|
||
|
||
## Tree Select/Cascading Dropdown(树形下拉选择器)
|
||
|
||
### 🌲 组件名称:Tree Select / Cascading Dropdown(树形下拉选择器)
|
||
![[IMG-20260425085420857.png]]
|
||
#### 结构拆解
|
||
|
||
|部位|组件名称|说明|
|
||
|---|---|---|
|
||
|触发框(点击展开)|Select Trigger Input|显示已选内容,点击展开下拉|
|
||
|下拉面板|Dropdown Panel|绝对定位浮层,`z-index` 覆盖页面|
|
||
|可折叠父节点(▶ 沪居地产)|Tree Node / Collapsible Group|点击 ▶ 展开/收起子节点|
|
||
|子节点列表(上海豪园店等)|Tree Leaf Node|可点击选中|
|
||
|带头像的叶节点(沪居地产共享)|Tree Leaf with Avatar|头像 + 姓名 + 编号 + 操作链接|
|
||
|带状态标签的节点(沪居 关闭)|Node with Badge|橙色"关闭"tag|
|
||
|底部操作行(隐藏离职员工)|Footer Action|固定在下拉底部的全局操作|
|
||
|
||
---
|
||
|
||
### 技术实现评估
|
||
|
||
#### ✅ 可行部分(Alpine.js 处理)
|
||
|
||
```
|
||
树形数据展开/折叠 → Alpine.js x-data 维护每个节点的 open 状态
|
||
选中节点高亮 → Alpine.js :class 绑定
|
||
点击外部关闭 → Alpine.js @click.away
|
||
头像/Badge/标签 → 纯 Tailwind CSS
|
||
底部固定操作行 → Tailwind sticky bottom
|
||
```
|
||
|
||
#### ⚠️ 需要重点关注的点
|
||
|
||
**这是你上传的三张图里技术难度最高的组件**,原因:
|
||
|
||
1. **树形数据递归渲染**:HTMX 本身不擅长递归组件,需要 Alpine.js 配合后端返回的 JSON 数据,在前端用 `template` + `x-for` 递归渲染树节点
|
||
2. **多级嵌套状态管理**:每个父节点独立维护 `open/close` 状态,需要 Alpine.js `x-data` 设计合理的数据结构
|
||
3. **搜索过滤**(如果需要在下拉内搜索员工):需要 Alpine.js 实时过滤树节点,或 HTMX 请求后端过滤
|
||
|
||
---
|
||
|
||
### 💡 实现建议
|
||
|
||
**方案一(推荐):Alpine.js 纯前端渲染**
|
||
|
||
> 后端一次性返回完整树形 JSON,Alpine.js 前端递归渲染,适合数据量不大的组织架构树
|
||
|
||
**方案二:HTMX 懒加载节点**
|
||
|
||
> 点击展开节点时,HTMX `hx-get` 请求该节点的子数据,适合层级很深、数据量很大的场景
|
||
|
||
---
|
||
|
||
### ✅ 结论
|
||
|
||
> 可以实现,但这是你目前三个组件里**唯一需要认真设计前端数据结构**的组件,不能直接套 Flowbite 现成组件(Flowbite 没有 TreeSelect)。建议直接基于 Alpine.js 手写,逻辑量约 80~120 行 JS,可复用。
|
||
|
||
|
||
## 带搜索的 Tree Select 完整功能清单
|
||
![[IMG-20260425085420932.png]]
|
||
### 🔍 带搜索的 Tree Select 完整功能清单
|
||
|
||
#### 新增的搜索相关特性
|
||
|
||
| 特性 | 说明 | 实现方式 | 可行性 |
|
||
| ------------ | -------------- | --------------------------- | --- |
|
||
| 下拉顶部搜索输入框 | 打开下拉后可直接键入关键词 | Alpine.js `x-model="query"` | ✅ |
|
||
| 实时过滤节点 | 输入时同步过滤匹配的节点 | Alpine.js computed 过滤 | ⚠️ |
|
||
| 搜索命中时自动展开父节点 | 子节点匹配时其父节点强制展开 | 需要递归遍历树形数据 | ⚠️ |
|
||
| 无结果时显示空状态 | "暂无匹配结果" | Alpine.js `x-show` | ✅ |
|
||
| 清空搜索恢复原始树结构 | 删除关键词后还原展开状态 | Alpine.js 状态重置 | ✅ |
|
||
|
||
## Data Range Picker (日期范围选择)
|
||
|
||
![[IMG-20260425085421006.png]]
|
||
这个组件的正式名称是 **Date Range Picker(日期范围选择器)**,也叫 **Dual-Month Calendar Picker**。
|
||
|
||
---
|
||
|
||
### 📅 组件名称:Date Range Picker / 双月日历范围选择器
|
||
|
||
#### 结构拆解
|
||
|
||
|部位|说明|
|
||
|---|---|
|
||
|顶部输入框组(开始日期 → 结束日期)|Range Input Group,两个输入框用箭头连接,点击任意一个触发日历展开|
|
||
|双月并排日历面板|Dual Calendar Panel,左侧当月、右侧次月,联动翻页|
|
||
|月份导航(« < 2026年4月 > »)|单箭头切换月,双箭头切换年|
|
||
|今日高亮(23日红框)|当天日期特殊样式|
|
||
|选中日期高亮(26日蓝底)|已选日期样式|
|
||
|范围区间高亮(开始~结束之间蓝色背景)|Range Highlight,区间内日期浅蓝背景|
|
||
|跨月灰色日期(上下月溢出日)|灰色不可选日期|
|
||
|
||
---
|
||
|
||
### 技术实现评估
|
||
|
||
#### ⚠️ 这是目前为止难度最高的组件
|
||
|
||
**难点在于:**
|
||
|
||
1. **双月联动**:左右两个月份需要同步,翻页时右月始终等于左月+1
|
||
2. **区间拖选高亮**:鼠标 hover 时实时预览区间,需要精细的状态管理
|
||
3. **日期计算**:需要处理月份天数、周几起始、跨年等边界情况
|
||
|
||
---
|
||
|
||
### 💡 强烈推荐:直接用成熟库,不要手写
|
||
|
||
用 Alpine.js 从零手写一个双月 Date Range Picker 工作量巨大(400~600 行),且边界情况多,极易出 Bug。
|
||
|
||
#### 推荐方案:**Pikaday** 或 **Flatpickr**
|
||
|
||
|库|特点|与你技术栈的兼容性|
|
||
|---|---|---|
|
||
|**Flatpickr**(强烈推荐)|轻量、无依赖、原生支持 Range 模式和双月显示,样式可用 Tailwind 覆盖|✅ 完美兼容,CDN 引入即用|
|
||
|**Pikaday**|更轻量,但双月需手动配置|✅ 可用,略需配置|
|
||
|
||
**Flatpickr 双月 Range 模式只需三行配置:**
|
||
|
||
javascript
|
||
|
||
```javascript
|
||
flatpickr("#dateRange", {
|
||
mode: "range",
|
||
showMonths: 2,
|
||
});
|
||
```
|
||
|
||
> 样式用 Tailwind 自定义覆盖即可达到图中效果,不破坏你"不混用技术"的原则——Flatpickr 是无框架依赖的纯 JS 工具库,不是 React/Vue 组件。
|
||
|
||
---
|
||
|
||
### ✅ 结论
|
||
|
||
> **不建议用 Alpine.js 手写**,引入 **Flatpickr**(CDN,约 16KB)是最优解,与你的 HTMX + Alpine.js + Tailwind 技术栈**完全兼容、无冲突**,实现图中效果只需少量配置。
|
||
|
||
|
||
## Tab Navigation (标签页导航)
|
||
|
||
### 🗂️ 组件名称:Tabs / Tab Navigation(标签页导航)
|
||
|
||
#### 结构拆解
|
||
|
||
|部位|说明|
|
||
|---|---|
|
||
|Tab 标签栏|横向排列的标签按钮组,当前激活项(修改跟进)有橙色下划线 + 橙色文字|
|
||
|激活指示器|Active Indicator,图中为橙色底部边框线|
|
||
|Tab 内容面板|切换标签时显示对应内容区域|
|
||
|内容区:Timeline(时间线)|按日期分组的操作记录列表,左侧橙色圆点|
|
||
|内容区:Banner 提示条|顶部"智能总结"广告/提示横条|
|
||
|内容区:Activity Item(活动记录项)|每条记录含【操作类型】+ 操作人 + 时间 + 详情文字|
|
||
|每条记录右侧:Visibility Toggle|"公开/隐藏"文字切换按钮|
|
||
|底部:Load More Button|"查看全部跟进"加载更多按钮|
|
||
|
||
---
|
||
|
||
### 技术实现评估
|
||
|
||
#### Tab 切换本体
|
||
|
||
这是所有组件里**实现最简单**的之一:
|
||
|
||
html
|
||
|
||
```html
|
||
<!-- Alpine.js 实现 Tab 切换,核心逻辑 -->
|
||
<div x-data="{ activeTab: 'all' }">
|
||
<button @click="activeTab = 'modify'"
|
||
:class="activeTab === 'modify' ? 'border-b-2 border-orange-500 text-orange-500' : ''">
|
||
修改跟进
|
||
</button>
|
||
|
||
<div x-show="activeTab === 'modify'">
|
||
<!-- 内容区 -->
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
#### 各子组件评估
|
||
|
||
|子组件|实现方式|可行性|
|
||
|---|---|---|
|
||
|Tab 标签切换 + 激活样式|Alpine.js `x-data` + `:class`|✅ 极简单|
|
||
|切换时局部刷新内容|HTMX `hx-get` + `hx-target`|✅ 完全可行|
|
||
|Timeline 时间线布局|Tailwind `border-l` + 圆点 `rounded-full`|✅ 纯 CSS 实现|
|
||
|活动记录条目|Django 模板循环渲染|✅ 完全可行|
|
||
|公开/隐藏 Toggle|HTMX `hx-post` 局部更新|✅ 完全可行|
|
||
|查看全部(加载更多)|HTMX `hx-get` + `hx-swap="beforeend"`|✅ 完全可行,无需写 JS|
|
||
|
||
---
|
||
|
||
### 💡 HTMX 在这里特别适合
|
||
|
||
"查看全部跟进"这个**加载更多**的场景,正是 HTMX 的强项:
|
||
|
||
html
|
||
|
||
```html
|
||
<button hx-get="/logs/?page=2"
|
||
hx-target="#log-list"
|
||
hx-swap="beforeend">
|
||
查看全部跟进
|
||
</button>
|
||
```
|
||
|
||
点击后自动追加新内容到列表末尾,**零 JS**。
|
||
|
||
---
|
||
|
||
### ✅ 结论
|
||
|
||
> 这张图里的所有组件都是你技术栈的**舒适区**,Tab 切换 + Timeline + 加载更多,正是 HTMX + Alpine.js + Tailwind 组合最擅长的经典场景,实现难度低,代码量少。
|
||
|
||
|
||
## Collapsible Card Grid(折叠展开面板)
|
||
![[IMG-20260425085421096.png]]
|
||
|
||
### 🪗 组件名称:Collapsible Card Grid(可折叠卡片网格)
|
||
|
||
#### 结构拆解
|
||
|
||
|部位|说明|
|
||
|---|---|
|
||
|外层容器|Card / Panel,带边框圆角的白色区块|
|
||
|标题栏(相关员工 + 编辑)|Section Header,右侧"编辑"为文字链接按钮|
|
||
|内容网格|3列 Grid Layout,每格一个员工卡片|
|
||
|员工卡片|Avatar + 姓名 + 门店 + 电话 + 更多按钮(橙色 ···) + 角色标签 + 日期|
|
||
|空状态格子(暂无 暂未分配)|Empty State Cell,灰色占位|
|
||
|底部展开/收起按钮|Expand Toggle Button,带 ∨ 箭头图标|
|
||
|
||
---
|
||
|
||
### 技术实现评估
|
||
|
||
#### 折叠展开本体 — 极其简单
|
||
|
||
html
|
||
|
||
```html
|
||
<div x-data="{ expanded: false }">
|
||
|
||
<!-- 内容区,默认显示前两行,展开后显示全部 -->
|
||
<div :class="expanded ? 'max-h-full' : 'max-h-[某个高度] overflow-hidden'">
|
||
<!-- 员工网格卡片 -->
|
||
</div>
|
||
|
||
<!-- 展开/收起按钮 -->
|
||
<button @click="expanded = !expanded">
|
||
<span x-text="expanded ? '收起' : '展开全部'"></span>
|
||
<svg :class="expanded ? 'rotate-180' : ''" .../>
|
||
</button>
|
||
|
||
</div>
|
||
```
|
||
|
||
#### 各子组件评估
|
||
|
||
|子组件|实现方式|可行性|
|
||
|---|---|---|
|
||
|展开/收起动画|Tailwind `transition-all duration-300` + Alpine.js|✅ 极简单|
|
||
|3列网格布局|Tailwind `grid grid-cols-3 gap-4`|✅ 纯 CSS|
|
||
|员工头像|Tailwind `rounded-full` + `<img>`|✅ 纯 CSS|
|
||
|橙色 ··· 更多按钮|Alpine.js Dropdown `@click.away`|✅ 简单|
|
||
|角色标签(出售方/钥匙方…)|Tailwind `text-blue-500` 文字链接样式|✅ 纯 CSS|
|
||
|空状态格子|Django 模板条件渲染 `{% if %}`|✅ 后端处理|
|
||
|箭头旋转动画(∨ → ∧)|Tailwind `rotate-180 transition`|✅ 一个 class 搞定|
|
||
|
||
---
|
||
|
||
### ✅ 结论
|
||
|
||
> 这是你上传的所有截图里**实现最简单的组件**,Alpine.js 核心逻辑只需 2 行,配合 Tailwind 的 `transition` + `max-h` 控制即可实现带动画的折叠展开效果,完全不需要任何额外库。
|
||
|
||
|
||
## Photo Gallery Manager (相册管理器)
|
||
![[IMG-20260425085421159.png]]
|
||
![[IMG-20260425085421220.png]]
|
||
这两张图合在一起是一个完整的 **Photo Gallery Manager(相册管理器)**,是你上传的所有截图里**功能最复合、复杂度最高**的页面。下面逐一拆解:
|
||
|
||
---
|
||
|
||
### 🖼️ 组件清单 & 可行性分析
|
||
|
||
#### 1. Scrollable Tab Bar(可横向滚动的标签栏)
|
||
|
||
图一顶部的分类标签,超出宽度后有 `···` 溢出菜单
|
||
|
||
|特性|实现方式|可行性|
|
||
|---|---|---|
|
||
|横向滚动标签|Tailwind `overflow-x-auto flex`|✅|
|
||
|溢出 `···` 下拉菜单|Alpine.js Dropdown|✅|
|
||
|激活状态(橙色下划线)|Alpine.js `:class` 绑定|✅|
|
||
|切换后刷新图片网格|HTMX `hx-get` + `hx-target`|✅|
|
||
|
||
---
|
||
|
||
#### 2. Image Grid with Checkbox(可多选的图片网格)
|
||
|
||
|特性|实现方式|可行性|
|
||
|---|---|---|
|
||
|多列图片网格|Tailwind `grid grid-cols-6 gap-2`|✅|
|
||
|每张图左上角 Checkbox|Alpine.js 管理选中状态数组|✅|
|
||
|封面角标(红色"封面"标签)|Tailwind 绝对定位 `absolute top-0 right-0`|✅|
|
||
|图片底部信息条(类别+尺寸)|Tailwind `absolute bottom-0` 半透明黑条|✅|
|
||
|全选 Checkbox|Alpine.js `selectAll` 方法|✅|
|
||
|
||
---
|
||
|
||
#### 3. Batch Action Toolbar(批量操作工具栏)
|
||
|
||
`批量修改类别 ∨` `批量删除` `批量下载`
|
||
|
||
|特性|实现方式|可行性|
|
||
|---|---|---|
|
||
|按钮依赖选中状态启用/禁用|Alpine.js `:disabled="selected.length === 0"`|✅|
|
||
|批量删除|HTMX `hx-delete` 发送选中 ID 列表|✅|
|
||
|批量下载|Django 后端打包 zip,返回文件流(Celery 异步)|✅|
|
||
|批量修改类别下拉|Alpine.js Dropdown + HTMX `hx-post`|✅|
|
||
|
||
---
|
||
|
||
#### 4. Drag-and-Drop File Upload(拖拽上传)⚠️
|
||
|
||
图二弹窗中的 `+ 上传图片` 区域
|
||
|
||
|特性|实现方式|可行性|
|
||
|---|---|---|
|
||
|点击选择文件|原生 `<input type="file" multiple>`|✅|
|
||
|拖拽上传区域|Alpine.js 监听 `dragover` / `drop` 事件|✅|
|
||
|上传预览(传后显示缩略图)|Alpine.js `FileReader` API|✅|
|
||
|上传进度|`XMLHttpRequest` / `fetch` + Alpine.js 进度绑定|✅|
|
||
|上传到 Cloudflare R2|Django 后端接收后转存 R2(boto3)|✅|
|
||
|
||
> ⚠️ **建议引入 [Filepond](https://pqina.nl/filepond/)(轻量无框架依赖)**:拖拽、预览、进度、多文件队列全部内置,与你的技术栈完全兼容,避免手写大量事件处理代码。
|
||
|
||
---
|
||
|
||
#### 5. Drag-to-Reorder(拖拽调整图片顺序)⚠️ 最高难度
|
||
|
||
图一右上角"调整图片顺序"功能
|
||
|
||
|特性|实现方式|可行性|
|
||
|---|---|---|
|
||
|图片网格内拖拽排序|需要专门的拖拽排序库|⚠️|
|
||
|排序后保存到后端|HTMX `hx-post` 发送新顺序 ID 数组|✅|
|
||
|
||
> ⚠️ **建议引入 [SortableJS](https://sortablejs.github.io/Sortable/)(3KB,无框架依赖)**:专为网格/列表拖拽排序设计,有官方 Alpine.js 集成方案,与你的技术栈完美兼容。
|
||
|
||
---
|
||
|
||
#### 6. Upload Modal 内的批量分类(图二)
|
||
|
||
|特性|实现方式|可行性|
|
||
|---|---|---|
|
||
|上传后在弹窗内全选/选择|Alpine.js 管理已上传文件的选中状态|✅|
|
||
|批量设置分类下拉|Alpine.js Select + HTMX 提交|✅|
|
||
|
||
---
|
||
|
||
### 📦 最终引入库汇总建议
|
||
|
||
|库|用途|大小|是否破坏技术栈一致性|
|
||
|---|---|---|---|
|
||
|**Filepond**|拖拽上传 + 预览 + 进度|~50KB|❌ 无框架依赖,安全引入|
|
||
|**SortableJS**|图片拖拽排序|~3KB|❌ 无框架依赖,安全引入|
|
||
|
||
---
|
||
|
||
### ✅ 总结
|
||
|
||
> 这个相册管理器的**90%功能**可以用 HTMX + Alpine.js + Tailwind 原生实现。仅**拖拽上传**和**拖拽排序**这两个功能,强烈建议引入 Filepond 和 SortableJS,两者都是无框架依赖的纯 JS 工具库,不破坏你技术栈的一致性,且都有成熟的 Alpine.js 集成方案。
|
||
|
||
|
||
## Image Lightbox View (全屏图片灯箱预览器)
|
||
![[IMG-20260425085421290.jpg]]
|
||
### 🔍 组件名称:Image Lightbox Viewer(全屏图片灯箱预览器)
|
||
|
||
上一个是**管理后台的相册管理器**(用于上传/删除/分类),这个是**面向用户的全屏预览器**,两者职责完全不同。
|
||
|
||
#### 结构拆解
|
||
|
||
|部位|组件名称|可行性|
|
||
|---|---|---|
|
||
|全屏黑色遮罩背景|Fullscreen Overlay|✅ Tailwind `fixed inset-0 bg-gray-800`|
|
||
|左上角图片信息栏(上传人/时间/尺寸/来源)|Image Meta Header|✅ Django 模板渲染|
|
||
|右上角 ✕ 关闭按钮|Close Button|✅ Alpine.js `@click="open=false"`|
|
||
|左右切换箭头(`<` `>`)|Prev/Next Arrow Navigation|✅ Alpine.js 索引切换|
|
||
|中央主图显示区|Main Image Viewer|✅|
|
||
|底部工具栏(刷新/缩小/100%/放大/旋转/下载)|Image Toolbar|⚠️ 见下方说明|
|
||
|底部分类标签栏(全部/户型图/客厅/卧室…)|Category Filter Tab|✅ Alpine.js 过滤|
|
||
|底部缩略图条(Thumbnail Strip)|Thumbnail Filmstrip|✅ Tailwind 横向滚动|
|
||
|当前激活缩略图高亮(橙色边框)|Active Thumbnail|✅ Alpine.js `:class`|
|
||
|
||
---
|
||
|
||
### ⚠️ 底部工具栏的特殊说明
|
||
|
||
图中工具栏包含:**缩放(放大/缩小/100%)+ 旋转 + 下载**,这是难点:
|
||
|
||
|功能|建议方案|可行性|
|
||
|---|---|---|
|
||
|图片缩放 + 拖拽平移|引入 **[Viewer.js](https://fengyuanchen.github.io/viewerjs/)**(5KB,无依赖)|✅ 强烈推荐|
|
||
|图片旋转|Viewer.js 内置|✅|
|
||
|下载|原生 `<a download>` 或 Django 返回文件流|✅|
|
||
|
||
---
|
||
|
||
### 💡 最佳实现方案
|
||
|
||
> 直接引入 **Viewer.js**,它能覆盖这张图里**所有**预览交互(缩放/旋转/全屏/翻页/缩略图条),配合 Alpine.js 控制开关,Tailwind 覆盖样式,**完全不破坏技术栈一致性**。
|
||
|
||
---
|
||
|
||
### 📦 更新引入库汇总
|
||
|
||
|库|用途|大小|
|
||
|---|---|---|
|
||
|Filepond|拖拽上传|~50KB|
|
||
|SortableJS|图片拖拽排序|~3KB|
|
||
|**Viewer.js**|图片灯箱预览(新增)|~5KB|
|
||
|Flatpickr|日期范围选择|~16KB|
|
||
|
||
这四个库全部无框架依赖,与你的技术栈完全兼容。继续上传!
|
||
|
||
## Accordion Progress Panel(可折叠进度检查面板)
|
||
![[IMG-20260425085421354.png]]
|
||
|
||
### 📊 组件名称:Accordion Progress Panel(可折叠进度检查面板)
|
||
|
||
#### 整体结构拆解
|
||
|
||
|部位|组件名称|可行性|
|
||
|---|---|---|
|
||
|顶部标题 + 总进度百分比(69%)|Section Header with Score|✅|
|
||
|顶部橙色进度条|Progress Bar|✅ Tailwind `w-[69%] bg-orange-500`|
|
||
|说明文字 + 蓝色文字链接|Description with Text Link|✅|
|
||
|可折叠父行(重点信息 ∧)|Accordion Header Row|✅ Alpine.js|
|
||
|折叠后的子条目列表|Accordion Body|✅ Alpine.js `x-show`|
|
||
|不可折叠的普通行(实勘/VR…)|Static Row|✅ 纯 HTML|
|
||
|每行右侧分数(4% / 8%)|Score Display|✅|
|
||
|未达标分数红色高亮(0%)|Conditional Color|✅ Alpine.js / Django 模板条件|
|
||
|行内蓝色操作链接(去上传/新增委托)|Inline Action Link|✅|
|
||
|
||
---
|
||
|
||
### 技术实现评估
|
||
|
||
#### 折叠逻辑 — 与上一个 Collapsible 完全一致,极简单
|
||
|
||
html
|
||
|
||
```html
|
||
<div x-data="{ open: true }">
|
||
<!-- 父行:点击折叠 -->
|
||
<div @click="open = !open" class="flex justify-between cursor-pointer">
|
||
<span>重点信息
|
||
<svg :class="open ? 'rotate-180' : ''" .../> <!-- 箭头旋转 -->
|
||
</span>
|
||
<span>8% / 8%</span>
|
||
</div>
|
||
|
||
<!-- 子条目列表 -->
|
||
<div x-show="open" x-collapse>
|
||
<div class="flex justify-between pl-4">
|
||
<span>唯一住房</span>
|
||
<span>1% / 1%</span>
|
||
</div>
|
||
<!-- ... -->
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
#### 各细节评估
|
||
|
||
|细节|实现方式|可行性|
|
||
|---|---|---|
|
||
|箭头 ∧/∨ 旋转动画|Tailwind `:class="open ? 'rotate-0' : 'rotate-180'"`|✅|
|
||
|折叠动画(高度过渡)|Alpine.js 官方插件 **`x-collapse`**(官方出品,1KB)|✅|
|
||
|已得分红色/黑色条件样式|Django 模板 `{% if score == 0 %}text-red-500{% endif %}`|✅|
|
||
|多个独立折叠块互不影响|每个父行独立 `x-data="{ open: true }"`|✅|
|
||
|进度条宽度动态绑定|Django 后端计算百分比,模板输出 `style="width:69%"`|✅|
|
||
|
||
---
|
||
|
||
### 💡 一个小提示
|
||
|
||
图中箭头标注了 4 处可折叠行(重点信息、附件、营销、带看),其余行(实勘/VR/钥匙等)是**不可折叠的普通行**。建议后端返回数据时带一个 `is_collapsible` 字段,Django 模板根据此字段决定渲染折叠版还是普通版,逻辑清晰且易维护。
|
||
|
||
---
|
||
|
||
### ✅ 结论
|
||
|
||
> 这个组件完全在 Alpine.js + Tailwind 的舒适区内,加上官方 `x-collapse` 插件处理高度动画,实现难度**很低**,与之前分析的 Collapsible Section 是同一类组件,可以复用同一套折叠逻辑。
|
||
|
||
## Inline Edit Mode (页面级读写切换)
|
||
|
||
|
||
### ✏️ 组件名称:Inline Edit Mode / 页面级读写切换
|
||
![[IMG-20260425085421448.png]]
|
||
#### 整体页面结构拆解
|
||
|
||
|部位|组件名称|可行性|
|
||
|---|---|---|
|
||
|顶部面包屑导航(系统/设置/…)|Breadcrumb|✅ 纯 Tailwind|
|
||
|顶部搜索框(输入设置项名称)|Settings Search Input|✅ Alpine.js 实时过滤|
|
||
|左侧多级导航菜单|Sidebar Navigation with Accordion|✅ Alpine.js(与之前折叠组件同理)|
|
||
|右上角"编辑"按钮|Edit Toggle Button|✅ Alpine.js|
|
||
|内容区分组标题(员工信息设置…)|Section Divider with Title|✅ 纯 Tailwind|
|
||
|左侧橙色竖线分组标签(个人信息)|Labeled Group Divider|✅ Tailwind `border-l-4 border-orange-500`|
|
||
|Toggle 开关(橙色/灰色)|Toggle Switch|✅ Alpine.js + Tailwind|
|
||
|只读文字 → 编辑后变输入框|Read/Edit Mode Switch|✅ Alpine.js `x-show`|
|
||
|
||
---
|
||
|
||
### 核心交互:Read/Edit Mode Toggle
|
||
|
||
这是本页最关键的设计模式,实现思路非常清晰:
|
||
|
||
html
|
||
|
||
```html
|
||
<div x-data="{ editing: false }">
|
||
|
||
<!-- 编辑按钮:点击切换模式 -->
|
||
<button @click="editing = true" x-show="!editing">编辑</button>
|
||
<button @click="editing = false" x-show="editing">保存</button>
|
||
|
||
<!-- 某一行设置项 -->
|
||
<div class="flex justify-between">
|
||
<span>工龄计算方式</span>
|
||
|
||
<!-- 只读态 -->
|
||
<span x-show="!editing">从首次入职开始计算</span>
|
||
|
||
<!-- 编辑态 -->
|
||
<select x-show="editing">
|
||
<option>从首次入职开始计算</option>
|
||
...
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Toggle 开关类设置项(编辑/只读都显示,只读时禁用) -->
|
||
<input type="checkbox"
|
||
:disabled="!editing"
|
||
x-model="settings.autoId">
|
||
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 各组件详细评估
|
||
|
||
|组件|只读态|编辑态|可行性|
|
||
|---|---|---|---|
|
||
|文字类设置(工龄计算方式)|纯文本 `<span>`|`<select>` 或 `<input>`|✅ Alpine.js `x-show` 切换|
|
||
|Toggle 开关|显示当前状态,`disabled`|可操作|✅ `:disabled="!editing"`|
|
||
|保存操作|—|HTMX `hx-post` 提交表单|✅ 无需页面跳转|
|
||
|取消编辑|—|恢复原始值|✅ Alpine.js 保存快照 `JSON.parse(JSON.stringify(data))`|
|
||
|
||
---
|
||
|
||
### Toggle Switch 组件
|
||
|
||
图中橙色开关是高频组件,纯 Tailwind + Alpine.js 实现:
|
||
|
||
html
|
||
|
||
```html
|
||
<button @click="val = !val"
|
||
:class="val ? 'bg-orange-500' : 'bg-gray-300'"
|
||
:disabled="!editing"
|
||
class="relative w-10 h-5 rounded-full transition-colors">
|
||
<span :class="val ? 'translate-x-5' : 'translate-x-0'"
|
||
class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full transition-transform">
|
||
</span>
|
||
</button>
|
||
```
|
||
|
||
---
|
||
|
||
### 左侧 Sidebar 导航
|
||
|
||
图中左侧菜单含**二级展开**(人事OA设置 ∧ 展开子项),与之前分析的折叠组件完全相同的实现方式,可直接复用。
|
||
|
||
---
|
||
|
||
### ✅ 结论
|
||
|
||
> 这整个页面的所有交互,包括**读写模式切换、Toggle 开关、左侧折叠导航、保存提交**,全部在 Alpine.js + HTMX + Tailwind 的能力范围内,**无需引入任何新库**。
|
||
|
||
> 唯一需要注意的是**取消编辑时恢复原始值**,建议进入编辑态时用 Alpine.js 做一次数据快照,取消时还原,这是标准做法,约 3 行代码。
|
||
|
||
|
||
## Drawer / Slide-over Panel(右侧抽屉面板)
|
||
![[IMG-20260425085421515.png]]
|
||
### 🗂️ 组件名称:Drawer / Slide-over Panel(右侧抽屉面板)
|
||
|
||
#### 整体页面结构拆解
|
||
|
||
|部位|组件名称|可行性|
|
||
|---|---|---|
|
||
|左侧主内容区(数据表格)|Data Table(与第一张图同类)|✅|
|
||
|右侧从屏幕边缘滑入的面板|Drawer / Slide-over|✅ Alpine.js + Tailwind|
|
||
|右侧面板顶部标题栏|Drawer Header|✅|
|
||
|右侧面板底部确定/取消按钮|Drawer Footer(固定在底部)|✅ Tailwind `sticky bottom-0`|
|
||
|半透明遮罩(左侧内容变暗)|Backdrop Overlay|✅|
|
||
|右侧面板内的表格|Settings Table with Radio + Toggle|✅|
|
||
|
||
---
|
||
|
||
### 核心交互:Drawer 滑入滑出
|
||
|
||
html
|
||
|
||
```html
|
||
<div x-data="{ open: false }">
|
||
|
||
<!-- 触发按钮 -->
|
||
<button @click="open = true">编辑</button>
|
||
|
||
<!-- 遮罩层 -->
|
||
<div x-show="open"
|
||
x-transition:enter="transition ease-out duration-300"
|
||
x-transition:enter-start="opacity-0"
|
||
x-transition:enter-end="opacity-100"
|
||
@click="open = false"
|
||
class="fixed inset-0 bg-black/30 z-40">
|
||
</div>
|
||
|
||
<!-- 抽屉面板 -->
|
||
<div x-show="open"
|
||
x-transition:enter="transition ease-out duration-300"
|
||
x-transition:enter-start="translate-x-full"
|
||
x-transition:enter-end="translate-x-0"
|
||
class="fixed right-0 top-0 h-full w-[480px] bg-white z-50 shadow-xl flex flex-col">
|
||
|
||
<!-- 固定标题栏 -->
|
||
<div class="p-4 border-b font-bold flex justify-between">
|
||
住宅/商住/别墅——出售字段填写要求和新增页展示设置
|
||
<button @click="open = false">✕</button>
|
||
</div>
|
||
|
||
<!-- 可滚动内容区 -->
|
||
<div class="flex-1 overflow-y-auto p-4">
|
||
<!-- 设置表格内容 -->
|
||
</div>
|
||
|
||
<!-- 固定底部按钮 -->
|
||
<div class="p-4 border-t flex justify-end gap-2">
|
||
<button @click="open = false">取消</button>
|
||
<button class="bg-orange-500 text-white">确定</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 右侧面板内的设置表格拆解
|
||
|
||
|列|组件|可行性|
|
||
|---|---|---|
|
||
|录入字段(文字列)|普通文本|✅|
|
||
|填写要求(必填/选填 Radio)|Radio Button Group|✅ Alpine.js `x-model`|
|
||
|录入页显示(Toggle 开关)|Toggle Switch(与上张图同款)|✅ 复用之前的 Toggle 组件|
|
||
|整列表格可滚动|Drawer 内容区 `overflow-y-auto`|✅|
|
||
|
||
---
|
||
|
||
### Drawer vs Modal 的选择原则
|
||
|
||
图中选择用 Drawer 而非 Modal,是因为:
|
||
|
||
|场景|推荐组件|
|
||
|---|---|
|
||
|编辑内容**字段多、需要滚动**|✅ Drawer(右侧抽屉)|
|
||
|编辑内容**字段少、简单确认**|Modal(居中弹窗,如第二张图)|
|
||
|需要**同时参考主页面内容**|✅ Drawer(主页面仍可见)|
|
||
|
||
---
|
||
|
||
### ✅ 结论
|
||
|
||
> Drawer 是 Alpine.js + Tailwind 的经典实现场景,核心是 `x-show` + `x-transition` 的 `translate-x-full → translate-x-0` 动画,**无需引入任何新库**,与之前所有组件复用同一套 Alpine.js 状态管理模式。
|
||
|
||
> 这也是你系统里**Modal 和 Drawer 两种编辑入口并存**的合理设计——字段少用 Modal,字段多用 Drawer,体验上各司其职。
|
||
|
||
|
||
## Multi-select Tag Input(多选标签选择器)
|
||
![[IMG-20260425085421572.png]]
|
||
### 🏷️ 组件名称:Multi-select Tag Input(多选标签选择器)
|
||
|
||
#### 结构拆解
|
||
|
||
|部位|组件名称|可行性|
|
||
|---|---|---|
|
||
|外层输入框容器(橙色边框激活态)|Tag Input Container|✅ Tailwind `ring-2 ring-orange-400`|
|
||
|已选项显示为 Tag/Chip(出售 ×)|Tag / Chip|✅ Alpine.js `x-for` 循环渲染|
|
||
|Tag 右侧 × 删除按钮|Tag Remove Button|✅ Alpine.js `@click` 移除数组元素|
|
||
|末尾光标输入框(可继续输入搜索)|Inline Search Input|✅ Alpine.js `x-model="query"`|
|
||
|下拉选项列表|Dropdown Option List|✅ Alpine.js `x-show`|
|
||
|已选项显示橙色 ✓ 勾选状态|Selected Indicator|✅ Alpine.js 判断是否在已选数组中|
|
||
|点击已选项再次点击取消选中|Toggle Selection|✅ Alpine.js 数组 push/splice|
|
||
|
||
---
|
||
|
||
### 核心实现逻辑
|
||
|
||
javascript
|
||
|
||
```javascript
|
||
// Alpine.js 数据结构
|
||
{
|
||
selected: ['出售', '出租', '租售', '他售/不售', '他租/不租', '暂缓'],
|
||
options: ['出售', '出租', '租售', '他售/不售', '他租/不租', '暂缓'],
|
||
open: false,
|
||
|
||
toggle(option) {
|
||
const i = this.selected.indexOf(option)
|
||
i === -1 ? this.selected.push(option) : this.selected.splice(i, 1)
|
||
},
|
||
|
||
isSelected(option) {
|
||
return this.selected.includes(option)
|
||
},
|
||
|
||
remove(option) {
|
||
this.selected = this.selected.filter(s => s !== option)
|
||
}
|
||
}
|
||
```
|
||
|
||
html
|
||
|
||
```html
|
||
<!-- Tag Input 容器 -->
|
||
<div @click.away="open = false"
|
||
class="flex flex-wrap gap-1 border rounded p-2 cursor-text"
|
||
:class="open ? 'ring-2 ring-orange-400' : ''"
|
||
@click="open = true">
|
||
|
||
<!-- 已选 Tags -->
|
||
<template x-for="item in selected" :key="item">
|
||
<span class="flex items-center bg-gray-100 px-2 py-0.5 rounded text-sm">
|
||
<span x-text="item"></span>
|
||
<button @click.stop="remove(item)" class="ml-1 text-gray-400">×</button>
|
||
</span>
|
||
</template>
|
||
|
||
<!-- 末尾输入框 -->
|
||
<input x-model="query" class="outline-none flex-1 min-w-[60px]" />
|
||
</div>
|
||
|
||
<!-- 下拉列表 -->
|
||
<div x-show="open" class="border rounded mt-1 bg-white shadow">
|
||
<template x-for="option in options" :key="option">
|
||
<div @click="toggle(option)"
|
||
class="flex justify-between px-4 py-2 hover:bg-gray-50 cursor-pointer">
|
||
<span x-text="option"></span>
|
||
<span x-show="isSelected(option)" class="text-orange-500">✓</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 与之前组件的关系
|
||
|
||
|组件|选择结果呈现方式|适用场景|
|
||
|---|---|---|
|
||
|Tree Select|下拉关闭后输入框显示文字|单选,层级数据(员工/门店)|
|
||
|**Multi-select Tag Input**|每个选中项显示为可删除 Tag|**多选,平级数据(状态/标签)**|
|
||
|Date Range Picker|显示为日期区间文字|日期范围选择|
|
||
|
||
---
|
||
|
||
### ✅ 结论
|
||
|
||
> 这是一个**中等难度**组件,Alpine.js 完全胜任,核心是维护一个**已选项数组**,Tag 的增删、下拉勾选状态、× 删除按钮全部围绕这个数组操作,逻辑清晰,约 30~50 行 JS,**无需引入任何新库**。
|
||
|
||
|
||
## Dynamic Form Table(动态可增删行表格)
|
||
![[IMG-20260425085421637.png]]
|
||
### ➕ 组件名称:Dynamic Form Table(动态可增删行表格)
|
||
|
||
#### 结构拆解
|
||
|
||
|部位|组件名称|可行性|
|
||
|---|---|---|
|
||
|表格主体(字段名称/类型/可选内容/必填/操作)|Editable Table|✅|
|
||
|每行"是否必填"列的 Toggle+Label 组合|Toggle with Status Label|✅ Alpine.js|
|
||
|必填橙色底(必填●)/ 灰色(非必填●)|Conditional Badge + Toggle|✅ `:class` 绑定|
|
||
|操作列"隐藏不使用"文字按钮|Text Action Button|✅|
|
||
|操作列"-"(系统预置行不可删除)|Disabled State|✅ `:disabled`|
|
||
|底部"+ 添加"按钮|Add Row Button|✅ Alpine.js|
|
||
|
||
---
|
||
|
||
### 核心交互:动态增删行
|
||
|
||
javascript
|
||
|
||
```javascript
|
||
// Alpine.js 数据结构
|
||
{
|
||
rows: [
|
||
// 系统预置行(不可删除)
|
||
{ id: 1, name: '意向日期', type: '日期选择', options: '-', required: true, system: true },
|
||
{ id: 2, name: '意向价格', type: '金额输入', options: '单位-元', required: true, system: true },
|
||
// 用户自定义行(可删除/隐藏)
|
||
{ id: 3, name: '意向截止日', type: '日期选择', options: '-', required: false, system: false, hidden: false },
|
||
],
|
||
|
||
addRow() {
|
||
this.rows.push({
|
||
id: Date.now(),
|
||
name: '', type: '', options: '-',
|
||
required: false, system: false, hidden: false
|
||
})
|
||
},
|
||
|
||
removeRow(id) {
|
||
this.rows = this.rows.filter(r => r.id !== id)
|
||
},
|
||
|
||
toggleHidden(row) {
|
||
row.hidden = !row.hidden
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 关键业务逻辑拆解
|
||
|
||
#### 系统预置行 vs 用户自定义行的差异
|
||
|
||
|行为|系统预置行|用户自定义行|
|
||
|---|---|---|
|
||
|字段名称|不可编辑(纯文本)|可编辑(`<input>`)|
|
||
|删除|操作列显示"-",不可删|可删除(显示删除按钮)|
|
||
|隐藏不使用|无此操作(前两行)|有"隐藏不使用"按钮|
|
||
|必填 Toggle|禁用不可改(系统锁定)|可自由切换|
|
||
|
||
html
|
||
|
||
```html
|
||
<!-- 操作列条件渲染 -->
|
||
<template x-if="row.system">
|
||
<span class="text-gray-400">-</span>
|
||
</template>
|
||
<template x-if="!row.system">
|
||
<button @click="toggleHidden(row)"
|
||
:class="row.hidden ? 'text-gray-400' : 'text-blue-500'">
|
||
<span x-text="row.hidden ? '显示使用' : '隐藏不使用'"></span>
|
||
</button>
|
||
</template>
|
||
```
|
||
|
||
#### 必填 Toggle + Badge 联动
|
||
|
||
html
|
||
|
||
```html
|
||
<div class="flex items-center gap-1">
|
||
<!-- Badge 文字随状态变 -->
|
||
<span :class="row.required ? 'bg-orange-500 text-white' : 'bg-gray-200 text-gray-500'"
|
||
class="text-xs px-2 py-0.5 rounded-full"
|
||
x-text="row.required ? '必填' : '非必填'">
|
||
</span>
|
||
<!-- Toggle 开关 -->
|
||
<button @click="row.required = !row.required"
|
||
:disabled="row.system"
|
||
:class="row.required ? 'bg-orange-500' : 'bg-gray-300'"
|
||
class="w-9 h-5 rounded-full relative transition-colors">
|
||
<span :class="row.required ? 'translate-x-4' : 'translate-x-0.5'"
|
||
class="absolute top-0.5 w-4 h-4 bg-white rounded-full transition-transform">
|
||
</span>
|
||
</button>
|
||
</div>
|
||
```
|
||
|
||
#### 保存到后端
|
||
|
||
html
|
||
|
||
```html
|
||
<!-- 点击保存时,HTMX 将整个 rows 数组提交 -->
|
||
<button hx-post="/api/field-settings/"
|
||
hx-vals="js:{rows: JSON.stringify(rows)}"
|
||
hx-target="#feedback">
|
||
保存
|
||
</button>
|
||
```
|
||
|
||
---
|
||
|
||
### ✅ 结论
|
||
|
||
> 这个组件完全在 Alpine.js + Tailwind + HTMX 的能力范围内,**无需引入任何新库**。核心是 Alpine.js 维护一个 `rows` 数组,增删行操作数组即可,DOM 自动响应更新。
|
||
|
||
> 唯一需要注意的是**系统预置行和用户自定义行的权限差异**,建议后端返回数据时携带 `is_system` 字段,前端据此控制哪些列可编辑、哪些操作可用,逻辑清晰且安全。
|
||
|
||
|
||
## Sortable Table with Drag Handle(带拖拽手柄的可排序表格)
|
||
![[IMG-20260425085421706.png]]
|
||
### ↕️ 组件名称:Sortable Table with Drag Handle(带拖拽手柄的可排序表格)
|
||
|
||
#### 新增特性拆解
|
||
|
||
|部位|组件名称|说明|
|
||
|---|---|---|
|
||
|每行左侧 ⠿ 图标|Drag Handle|鼠标按住此处才能拖动,非整行拖动|
|
||
|拖动中的行样式|Dragging State|图中"业主"行被拖起,蓝色高亮背景|
|
||
|拖动目标位置指示|Drop Indicator|图中橙色虚线,显示将要放置的位置|
|
||
|拖动后顺序持久化|Order Persistence|拖完后保存新顺序到后端|
|
||
|
||
---
|
||
|
||
### 实现方案
|
||
|
||
这个功能之前分析相册排序时已经推荐过 **SortableJS**,在表格场景同样适用:
|
||
|
||
html
|
||
|
||
```html
|
||
<!-- 引入 SortableJS(CDN,3KB) -->
|
||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||
|
||
<tbody id="sortable-table" x-init="initSort()">
|
||
<template x-for="row in rows" :key="row.id">
|
||
<tr>
|
||
<!-- 拖拽手柄:只有抓住这里才能拖 -->
|
||
<td class="drag-handle cursor-grab text-gray-400">⠿</td>
|
||
<td x-text="row.name"></td>
|
||
<!-- Toggle、操作列... -->
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
```
|
||
|
||
javascript
|
||
|
||
```javascript
|
||
// Alpine.js 初始化 SortableJS
|
||
initSort() {
|
||
Sortable.create(document.getElementById('sortable-table'), {
|
||
handle: '.drag-handle', // 只有手柄可拖
|
||
animation: 150, // 拖动动画时长
|
||
ghostClass: 'bg-blue-50', // 拖动中行的样式(图中蓝色)
|
||
chosenClass: 'opacity-50',
|
||
|
||
onEnd: (evt) => {
|
||
// 拖完后同步 Alpine.js 数据顺序
|
||
const moved = this.rows.splice(evt.oldIndex, 1)[0]
|
||
this.rows.splice(evt.newIndex, 0, moved)
|
||
|
||
// 保存新顺序到后端
|
||
this.saveOrder()
|
||
}
|
||
})
|
||
},
|
||
|
||
saveOrder() {
|
||
const ids = this.rows.map(r => r.id)
|
||
// HTMX 或 fetch 提交新顺序
|
||
fetch('/api/field-options/reorder/', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ ids })
|
||
})
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Drop Indicator(橙色虚线放置指示线)
|
||
|
||
图中橙色虚线是视觉上最精细的部分,SortableJS 的 `ghostClass` 可以控制占位样式:
|
||
|
||
css
|
||
|
||
```css
|
||
/* 拖动占位行显示橙色虚线边框 */
|
||
.sortable-ghost {
|
||
border: 2px dashed #f97316; /* Tailwind orange-500 */
|
||
background: transparent;
|
||
opacity: 0.4;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 与上一张图的差异对比
|
||
|
||
|特性|上张(动态表格)|本张(可拖拽排序)|
|
||
|---|---|---|
|
||
|新增行|✅ + 添加|✅ + 新增|
|
||
|删除行|✅|✅|
|
||
|Toggle 开关|✅|✅|
|
||
|**行拖拽排序**|❌|✅ SortableJS|
|
||
|**拖拽手柄 ⠿**|❌|✅|
|
||
|**Drop 虚线指示**|❌|✅ CSS ghostClass|
|
||
|
||
---
|
||
|
||
### ✅ 结论
|
||
|
||
> SortableJS 在这个场景和之前相册排序场景是**同一个库**,引入一次即可在整个系统复用。与 Alpine.js 的集成非常自然,拖拽完成后通过 `onEnd` 回调同步 Alpine.js 数据状态,再用 `fetch` 或 HTMX 提交新顺序,**整体方案无缝衔接你的技术栈**。
|
||
|
||
|
||
## Multi-Table Independent Pagination(同页多表格独立分页)
|
||
![[IMG-20260425085421774.png]]
|
||
### 📋 组件名称:Multi-Table Independent Pagination(同页多表格独立分页)
|
||
|
||
#### 页面结构拆解
|
||
|
||
|部位|组件名称|可行性|
|
||
|---|---|---|
|
||
|顶部搜索栏 + 橙色搜索按钮 + 重置|Search Bar|✅|
|
||
|顶部 Tab 导航(交易信息/房产信息…)|Tab Navigation(与之前同款)|✅|
|
||
|右上角"已移除参数 / 新增参数"按钮组|Action Button Group|✅|
|
||
|多个独立表格分区(业客信息/合同应收费用…)|Section Table|✅|
|
||
|每个表格右下角**独立分页器**|Per-Table Pagination|✅ HTMX 精准局部刷新|
|
||
|分区标题 + 说明文字|Section Header with Description|✅|
|
||
|
||
---
|
||
|
||
### 核心难点:同页多表格独立分页
|
||
|
||
这正是 **HTMX 最擅长的场景**,每个表格是独立的 `hx-target`,互不干扰:
|
||
|
||
html
|
||
|
||
```html
|
||
<!-- 表格区块 1:业客信息 -->
|
||
<div id="section-customer">
|
||
<h3>业客信息</h3>
|
||
|
||
<!-- 表格内容区,分页刷新只更新这里 -->
|
||
<div id="table-customer">
|
||
{% include "partials/table_customer.html" %}
|
||
</div>
|
||
|
||
<!-- 该表格的独立分页器 -->
|
||
<div class="flex justify-end mt-2">
|
||
<button hx-get="/params/customer/?page=1"
|
||
hx-target="#table-customer"
|
||
hx-swap="innerHTML">‹</button>
|
||
<span>1</span>
|
||
<button hx-get="/params/customer/?page=2"
|
||
hx-target="#table-customer" <!-- 只刷新本表格 -->
|
||
hx-swap="innerHTML">2</button>
|
||
<button hx-get="/params/customer/?page=3"
|
||
hx-target="#table-customer"
|
||
hx-swap="innerHTML">3</button>
|
||
<button hx-get="/params/customer/?page=2"
|
||
hx-target="#table-customer"
|
||
hx-swap="innerHTML">›</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 表格区块 2:合同-应收费用(完全独立) -->
|
||
<div id="section-fee">
|
||
<h3>合同-应收费用</h3>
|
||
|
||
<div id="table-fee">
|
||
{% include "partials/table_fee.html" %}
|
||
</div>
|
||
|
||
<!-- 该表格的独立分页器,hx-target 指向自己 -->
|
||
<div class="flex justify-end mt-2">
|
||
<button hx-get="/params/fee/?page=1"
|
||
hx-target="#table-fee" <!-- 只刷新本表格,不影响其他 -->
|
||
hx-swap="innerHTML">‹</button>
|
||
<span>1</span>
|
||
<button hx-get="/params/fee/?page=2"
|
||
hx-target="#table-fee"
|
||
hx-swap="innerHTML">›</button>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 关键设计原则
|
||
|
||
每个表格区块的**三要素相互隔离**:
|
||
|
||
```
|
||
表格1 表格2 表格3
|
||
│ │ │
|
||
├─ id="table-1" ├─ id="table-2" ├─ id="table-3"
|
||
├─ hx-target 指向 ├─ hx-target 指向 ├─ hx-target 指向
|
||
│ #table-1 │ #table-2 │ #table-3
|
||
└─ /api/t1/?page=N └─ /api/t2/?page=N └─ /api/t3/?page=N
|
||
|
||
翻表格1的页 → 只有 #table-1 的 DOM 更新,表格2、3完全不动
|
||
```
|
||
|
||
---
|
||
|
||
### 顶部搜索的联动逻辑
|
||
|
||
顶部搜索框搜索时,需要**同时刷新所有表格**:
|
||
|
||
html
|
||
|
||
```html
|
||
<form hx-get="/params/search/"
|
||
hx-target="#all-tables" <!-- 刷新整个多表格容器 -->
|
||
hx-swap="innerHTML">
|
||
<input name="q" placeholder="请输入参数名称">
|
||
<button type="submit" class="bg-orange-500">搜索</button>
|
||
<button type="button"
|
||
hx-get="/params/"
|
||
hx-target="#all-tables">重置</button>
|
||
</form>
|
||
|
||
<div id="all-tables">
|
||
<!-- 所有表格区块放这里 -->
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### ✅ 结论
|
||
|
||
> 这个模式是 HTMX **`hx-target` 精准局部刷新**的最典型应用场景:每个表格的分页器只更新自己的 `target`,**天然实现多表格独立分页**,无需任何前端状态管理,**零 JS 代码**,完全由 HTML 属性声明驱动。
|
||
|
||
> Django 后端只需为每个表格区块提供独立的分页视图接口即可,架构清晰,维护成本极低。 |