# Xiuno BBS 4.5+ Bootstrap 5 UI+ Tabler Icons +JS
本文档记录 Xiuno BBS 从旧版(Bootstrap 3 + jQuery + 多图标库)升级到新版(Bootstrap 5.3 + htmx + Alpine.js + Tabler Icons)的完整迁移路径,供开发者在升级插件、定制主题或排查兼容问题时参考。
---
## 目录
- [第一章:升级总览](#第一章升级总览)
- [第二章:Bootstrap 5 升级与适配](#第二章bootstrap-5-升级与适配)
- [第三章:htmx + Alpine.js 协作规范](#第三章htmx--alpinejs-协作规范)
- [第四章:图标库迁移](#第四章图标库迁移)
- [第五章:旧插件兼容与升级](#第五章旧插件兼容与升级)
- [第六章:数据流与状态管理](#第六章数据流与状态管理)
- [第七章:迁移检查清单与排错](#第七章迁移检查清单与排错)
- [第八章:关键文件路径速查](#第八章关键文件路径速查)
---
# 第一章:升级总览
## 1.1 技术栈版本清单
以下为当前系统所使用的前端库及其版本、路径和作用:
| 库名称 | 版本 | 路径 | 作用 |
|--------|------|------|------|
| Bootstrap | 5.3.7 | `view/vendor/bootstrap/css/bootstrap.min.css` + `view/vendor/bootstrap/js/bootstrap.bundle.min.js` | UI 框架,提供布局、组件、工具类 |
| htmx | 2.0.10 | `view/vendor/htmx/htmx.min.js` | 声明式 AJAX 交互,服务端渲染 HTML 片段 |
| idiomorph | — | `view/vendor/idiomorph/idiomorph-ext.min.js` | htmx 扩展,提供 morph DOM 交换算法 |
| htmx-ext-alpine-morph | — | `view/vendor/htmx-ext-alpine-morph/alpine-morph.js` | htmx 扩展,桥接 Alpine.js morph 与 htmx swap |
| Alpine.js Morph 插件 | 3.x | `view/vendor/alpinejs-morph/cdn.min.js` | Alpine.js 官方 morph 插件,支持 DOM 差异补丁 |
| Alpine.js | 3.x | `view/vendor/alpinejs/cdn.min.js` | 轻量前端响应式框架,管理 UI 状态 |
| Tabler Icons | 3.31.0 | `view/vendor/tabler-icons/tabler-icons.min.css` | 图标字体库,统一图标风格 |
| jQuery | 3.1.0 | `view/js/jquery-3.1.0.js` | 旧代码兼容保留,新代码禁止使用 |
| xiuno-modern.js | — | `view/js/xiuno-modern.js` | 原生 JS 兼容层,提供选择器、AJAX、DOM、toast 等 API |
> **说明**:idiomorph、htmx-ext-alpine-morph、Alpine.js Morph 插件均无内嵌版本号标注,以仓库最新发布版为准。Alpine.js 3.x 的具体小版本号在压缩文件中不可直接读取。
## 1.2 升级前后对比表
| 维度 | 旧版 | 新版 |
|------|------|------|
| **UI 框架** | Bootstrap 3.x | Bootstrap 5.3.7 |
| **图标库** | Font Awesome / Glyphicons 等多套 | Tabler Icons 3.31.0(统一) |
| **JS 框架** | jQuery 3.1.0(全局依赖) | Alpine.js 3.x(局部状态) + htmx 2.0.10(服务端交互) |
| **AJAX 方式** | `$.ajax()` / `$.post()` / `$.get()` | htmx 声明式属性(`hx-get` / `hx-post`)+ `XN.post()` 兼容层 |
| **表单提交** | jQuery 序列化 + AJAX | htmx `hx-post` 或原生 `fetch()` + `FormData` |
| **状态管理** | jQuery DOM 操作 / 全局变量 | Alpine.js `x-data` 局部状态 + `Alpine.data()` 复用组件 |
| **主题系统** | CSS 覆盖 / 无暗色模式 | `data-bs-theme` 暗色模式 + `data-theme` 自定义主题色 |
| **DOM 更新** | jQuery `.html()` / `.append()` | htmx swap + idiomorph morph + Alpine morph |
| **页面导航** | 全页刷新 | htmx `hx-boost` 增强导航,局部 swap |
| **组件 API** | jQuery 插件式(`$('#modal').modal('show')`) | 原生 `new bootstrap.Modal(el).show()` |
## 1.3 升级核心理念
### 渐进式迁移
本次升级采用渐进式策略,不要求一次性重写所有代码:
1. **旧代码可继续运行**:jQuery 3.1.0 保留加载,旧插件中使用 `$` 的代码无需立即修改
2. **新代码使用新栈**:新增页面、组件必须使用 htmx + Alpine.js + 原生 JS,禁止引入新的 jQuery 依赖
3. **兼容层桥接**:`xiuno-modern.js` 提供了 `XN.post()`、`XN.toast()` 等与旧 `xn.js` 对等的 API,方便逐步替换
### jQuery 保留兼容
```javascript
// 旧代码 — 仍然可用,不强制删除
$.post(url, data, function(code, msg) { ... });
// 新代码 — 使用兼容层
XN.post(url, data, function(code, msg) { ... });
// 新代码 — 使用 htmx 声明式
// 提交
```
### 新代码禁止 jQuery
所有新增的插件、页面、组件必须遵守以下规则:
- 使用 `XN.xxx()` 或原生 JS,不依赖 jQuery
- 服务端交互使用 htmx 属性或 `XN.post()` / `fetch()`
- UI 状态使用 Alpine.js `x-data`,禁止 `Alpine.store()` 和 `$store`
- DOM 操作使用原生 API(`document.querySelector`、`el.classList` 等)
---
# 第二章:Bootstrap 5 升级与适配
## 2.1 CSS 类名变更对照表
Bootstrap 5 对大量类名进行了重命名和调整,以下是迁移中常见的类名变更:
### 布局与网格
| 旧类名(Bootstrap 3) | 新类名(Bootstrap 5) | 说明 |
|------------------------|----------------------|------|
| `col-xs-*` | `col-*` | 超小断点不再需要 `xs` 后缀,直接使用 `col-1` ~ `col-12` |
| `col-sm-*` / `col-md-*` / `col-lg-*` | 保持不变 | 响应式断点类名未变 |
| `col-xs-offset-*` | `offset-*` | 偏移类名同步简化 |
| `row`(默认有负 margin) | `row` + `g-*` / `gx-*` / `gy-*` | gutter 改用 `g-*` 系列控制,不再使用负 margin hack |
### 排版与内容
| 旧类名(Bootstrap 3) | 新类名(Bootstrap 5) | 说明 |
|------------------------|----------------------|------|
| `pull-left` | `float-start` | 浮动方向改用逻辑属性命名 |
| `pull-right` | `float-end` | 浮动方向改用逻辑属性命名 |
| `text-left` | `text-start` | 文本对齐改用逻辑属性命名 |
| `text-right` | `text-end` | 文本对齐改用逻辑属性命名 |
| `text-justify` | 移除 | 不再提供,需自定义 CSS |
### 组件
| 旧类名(Bootstrap 3) | 新类名(Bootstrap 5) | 说明 |
|------------------------|----------------------|------|
| `label` | `badge` | 标签组件更名为徽章 |
| `panel` / `panel-heading` / `panel-body` / `panel-footer` | `card` / `card-header` / `card-body` / `card-footer` | 面板组件被卡片组件替代 |
| `well` | `card` + 自定义内边距 | Well 组件移除,用 card 替代 |
| `thumbnail` | `card` + `img-fluid` | 缩略图组件移除 |
| `pager` | `pagination` | 分页器合并到分页组件 |
| `nav-justified` | `nav-fill` / `nav-justified` | `nav-fill` 等宽分配,`nav-justified` 保留 |
| `carousel-item` 内 `item` | `carousel-item` | 轮播项不再需要额外的 `item` 类 |
| `list-inline` 子项 `list-inline-item` | 保持不变 | 无变化 |
### 图片与媒体
| 旧类名(Bootstrap 3) | 新类名(Bootstrap 5) | 说明 |
|------------------------|----------------------|------|
| `img-responsive` | `img-fluid` | 响应式图片更名 |
| `img-circle` | `rounded-circle` | 圆形图片改用圆角工具类 |
| `img-rounded` | `rounded` | 圆角图片改用圆角工具类 |
| `img-thumbnail` | `img-thumbnail` | 保持不变 |
| `media` / `media-body` | Flex 工具类(`d-flex` + `flex-fill`) | 媒体对象组件移除,改用 flex 布局 |
### 显示与隐藏
| 旧类名(Bootstrap 3) | 新类名(Bootstrap 5) | 说明 |
|------------------------|----------------------|------|
| `hidden-xs` | `d-none d-sm-block` | 隐藏/显示改用 `d-*` 工具类组合 |
| `hidden-sm` | `d-sm-none d-md-block` | 按断点组合 |
| `hidden-md` | `d-md-none d-lg-block` | 按断点组合 |
| `hidden-lg` | `d-lg-none d-xl-block` | 按断点组合 |
| `visible-xs` | `d-block d-sm-none` | 反向组合 |
| `visible-sm` | `d-none d-sm-block d-md-none` | 反向组合 |
| `visible-md` | `d-none d-md-block d-lg-none` | 反向组合 |
| `visible-lg` | `d-none d-lg-block d-xl-none` | 反向组合 |
| `hidden` | `d-none` | 通用隐藏 |
| `show` | `d-block`(或移除 `d-none`) | 通用显示 |
| `invisible` | `invisible` | 保持不变(仅影响可见性,不脱离布局) |
### 表单
| 旧类名(Bootstrap 3) | 新类名(Bootstrap 5) | 说明 |
|------------------------|----------------------|------|
| `form-group` | `mb-3` | 表单组移除,改用间距工具类 |
| `form-control-lg` | `form-control-lg` | 保持不变 |
| `form-control-sm` | `form-control-sm` | 保持不变 |
| `custom-select` | `form-select` | 自定义选择框更名 |
| `custom-file` | `form-control`(type="file") | 自定义文件输入移除 |
| `custom-control custom-checkbox` | `form-check` + `form-check-input` | 自定义控件改为标准 form-check |
| `custom-switch` | `form-check form-switch` | 开关控件更名 |
### 其他工具类
| 旧类名(Bootstrap 3) | 新类名(Bootstrap 5) | 说明 |
|------------------------|----------------------|------|
| `ml-*` | `ms-*` | margin-left 改用逻辑属性 start |
| `mr-*` | `me-*` | margin-right 改用逻辑属性 end |
| `pl-*` | `ps-*` | padding-left 改用逻辑属性 start |
| `pr-*` | `pe-*` | padding-right 改用逻辑属性 end |
| `close` | `btn-close` | 关闭按钮更名,结构变化 |
| `embed-responsive` | `ratio` | 嵌入式响应改为比例工具类 |
| `border-*`(单边) | `border-top` / `border-end` / `border-bottom` / `border-start` | 边框方向改用逻辑属性 |
## 2.2 组件 API 变化
### Modal(模态框)
Bootstrap 5 移除了 jQuery 插件式 API,改用原生 JavaScript 构造函数:
```javascript
// 旧版(Bootstrap 3 + jQuery)
$('#myModal').modal('show');
$('#myModal').modal('hide');
$('#myModal').on('hidden.bs.modal', function() { ... });
// 新版(Bootstrap 5)
var modalEl = document.getElementById('myModal');
var modal = new bootstrap.Modal(modalEl);
modal.show();
modal.hide();
// 获取已有实例
var instance = bootstrap.Modal.getInstance(modalEl);
if (instance) instance.hide();
// 事件监听
modalEl.addEventListener('hidden.bs.modal', function() { ... });
```
### Tooltip(工具提示)
```javascript
// 旧版
$('#element').tooltip({ title: '提示文本' });
$('#element').tooltip('show');
// 新版
var tooltipEl = document.getElementById('element');
var tooltip = new bootstrap.Tooltip(tooltipEl, { title: '提示文本' });
tooltip.show();
```
### Popover(弹出框)
```javascript
// 旧版
$('#element').popover({ content: '内容', trigger: 'hover' });
// 新版
var popoverEl = document.getElementById('element');
var popover = new bootstrap.Popover(popoverEl, { content: '内容', trigger: 'hover' });
popover.show();
```
### data 属性前缀变更
Bootstrap 5 的所有 data 属性统一添加 `bs` 前缀,避免与其他库冲突:
| 旧属性 | 新属性 | 说明 |
|--------|--------|------|
| `data-toggle` | `data-bs-toggle` | 触发器 |
| `data-dismiss` | `data-bs-dismiss` | 关闭 |
| `data-target` | `data-bs-target` | 目标元素 |
| `data-spy` | `data-bs-spy` | 滚动监听 |
| `data-ride` | `data-bs-ride` | 自动播放 |
| `data-slide` | `data-bs-slide` | 轮播方向 |
| `data-slide-to` | `data-bs-slide-to` | 轮播跳转 |
| `data-offset` | `data-bs-offset` | 偏移量 |
| `data-placement` | `data-bs-placement` | 定位方向 |
| `data-delay` | `data-bs-delay` | 延迟 |
实际项目中的使用示例(来自 `footer.inc.htm`):
```html
```
## 2.3 主题系统
### 暗色模式(data-bs-theme)
Bootstrap 5.3+ 原生支持暗色模式,通过 `data-bs-theme` 属性控制:
```html
```
项目中的实现方式(来自 `header.inc.htm`):
```javascript
// 根据用户偏好或系统设置初始化主题
(function(){
var theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-bs-theme', theme);
})();
```
暗色模式下的 CSS 变量覆盖(来自 `bootstrap-bbs.css`):
```css
[data-bs-theme="dark"] {
--bbs-main: #e0e0e0;
--bbs-sub: #888;
--bbs-contrast: #1b1b1b;
--bbs-card-bg: #1e1e1e;
--bbs-body-bg: #1b1b1b;
--bbs-input-bg: #2a2a2a;
--bbs-dividing-line: #2a2a2a;
--bbs-card-border: #2a2a2a;
/* ... 更多暗色变量 */
}
```
### 自定义主题色(data-theme)
项目在 Bootstrap 原生主题基础上,通过 `data-theme` 属性实现了多色主题切换:
```javascript
// 初始化主题色
var themeColor = localStorage.getItem('theme-color') || 'blue';
document.documentElement.setAttribute('data-theme', themeColor);
```
当前支持的主题色(来自 `bootstrap-bbs.css`):
| 主题名 | data-theme 值 | 主色(--bbs-brand) | 背景色(--bbs-body-bg) |
|--------|---------------|---------------------|------------------------|
| 蓝色(默认) | `blue` | `#2563eb` | `#f1f2f5` |
| 绿色 | `green` | `#16a34a` | `#f4f7f5` |
| 紫色 | `purple` | `#9333ea` | `#f9f8fa` |
| 红色 | `red` | `#dc2626` | `#fbf6f5` |
| 橙色 | `orange` | `#ea580c` | `#fbf6f5` |
| 自定义 | `custom` | 用户自定义 | `#f8f8fa` |
每个主题色通过 CSS 变量覆盖实现,包含完整的 50~950 色阶:
```css
[data-theme="blue"] {
--bbs-brand: #2563eb;
--bbs-brand-hover: #1d4ed8;
--bbs-brand-light: #60a5fa;
--bbs-brand-50: #eff6ff;
--bbs-brand-100: #dbeafe;
/* ... 50~950 完整色阶 */
--bbs-hover: rgba(37, 99, 235, .08);
--bbs-body-bg: #f1f2f5;
--bs-primary-rgb: 37, 99, 235;
}
```
## 2.4 自定义 CSS 文件关键覆盖项
`view/css/bootstrap-bbs.css` 是项目的核心自定义样式文件,通过 CSS 变量覆盖和组件样式扩展实现品牌化定制。以下为关键覆盖项:
### CSS 变量体系
文件在 `:root` 中定义了完整的 BBS 变量体系,覆盖 Bootstrap 默认值:
```css
:root {
/* 品牌色 */
--bbs-brand: #2563eb;
--bbs-brand-hover: #1d4ed8;
/* 文字色 */
--bbs-main: #333; /* 主文字 */
--bbs-sub: #939393; /* 辅助文字 */
--bbs-contrast: #fff; /* 对比色 */
/* 交互色 */
--bbs-hover: rgba(37, 99, 235, .08); /* 悬停背景 */
--bbs-alink: #eb7350; /* 链接色 */
--bbs-special: #e14123; /* 特殊标记 */
/* 功能色 */
--bbs-error: #dc3545;
--bbs-success: #198754;
--bbs-warning: #ffc107;
--bbs-info: #0dcaf0;
/* 背景色 */
--bbs-card-bg: var(--bbs-contrast);
--bbs-body-bg: #f8f8fa;
--bbs-input-bg: var(--bbs-gray-6);
/* 圆角 */
--bbs-radius-sm: 0.25rem;
--bbs-radius-md: 0.5rem;
--bbs-radius-lg: 0.75rem;
--bbs-radius-pill: 50rem;
/* 阴影 */
--bbs-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bbs-shadow-md: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
/* 动画 */
--bbs-transition: all 0.2s ease;
}
```
### Bootstrap 变量覆盖
文件直接覆盖了 Bootstrap 的 CSS 变量,使品牌色生效于所有 Bootstrap 组件:
```css
:root {
--bs-primary: var(--bbs-brand);
--bs-primary-rgb: 37, 99, 235;
--bs-link-color: var(--bbs-brand);
--bs-link-hover-color: var(--bbs-brand-hover);
--bs-btn-primary-bg: var(--bbs-brand);
--bs-btn-primary-border-color: var(--bbs-brand);
--bs-btn-primary-hover-bg: var(--bbs-brand-hover);
--bs-nav-pills-link-active-bg: var(--bs-primary);
--bs-pagination-active-bg: var(--bs-primary);
--bs-dropdown-link-active-bg: var(--bs-primary);
--bs-form-check-bg-checked: var(--bs-primary);
--bs-link-decoration: none;
--bs-link-hover-decoration: none;
}
```
### 组件样式覆盖
| 组件 | 覆盖内容 | 说明 |
|------|---------|------|
| `html, body` | `height: 100%; display: flex; flex-direction: column;` | 粘性页脚布局 |
| `body` | `background-color: var(--bbs-body-bg); font-size: 14px;` | 全局背景和字号 |
| `.card` | `padding: 12px; border: none; background-color: var(--bbs-card-bg);` | 去除边框,统一内边距 |
| `.card .card-body` | `padding: 0;` | 卡片体内边距归零 |
| `.card-body` | `padding: 12px;` | 通用卡片体内边距 |
| `.btn` | `transition: all 0.15s ease;` | 按钮过渡动画 |
| `.btn:hover` | `transform: translateY(-1px);` | 悬停微上移 |
| `.btn:active` | `transform: scale(0.97);` | 点击缩放反馈 |
| `.dropdown-menu` | `animation: fadeIn 0.15s ease; background-color: var(--bbs-card-bg);` | 下拉菜单淡入动画 |
| `.nav-tabs` | `border: none;` | 去除标签页默认边框 |
| `.nav-tabs .nav-link` | `border: none; background-color: transparent!important;` | 标签页链接无边框 |
| `a` | `text-decoration: none; transition: color 0.15s ease;` | 全局链接无下划线 |
| `.pagination .page-link` | `border-radius: var(--bbs-radius-md);` | 分页圆角 |
| `.form-check-input:checked` | `background-color: var(--bbs-brand);` | 复选框选中色 |
### 自定义组件
文件还定义了项目特有的组件样式:
- **导航栏**(`.navbar-bbs`):带阴影和底部边框
- **底部导航**(`.bottom-nav`):移动端固定底部导航栏,支持 safe-area
- **侧边栏卡片**(`.sidebar-card`):无边框侧边栏样式
- **帖子列表**(`.threadlist`、`.thread-item`):悬停高亮、无底部边框
- **时间线**(`.timeline-*`):微博模式卡片样式
- **瀑布流**(`.masonry-*`):多列瀑布流布局
- **头像**(`.avatar-xs/sm/md/lg/xl`):多尺寸圆形头像
- **Toast 通知**(`.toast-msg`):右上角滑入通知
- **加载动画**(`.loading-spinner`):CSS 旋转加载指示器
- **上传系统**(`.upload-*`):拖拽上传、进度条、预览网格
## 2.5 网格系统变化
### Flex 布局
Bootstrap 5 的网格系统底层从 `float` 改为 `flexbox`,主要影响:
1. **行内对齐**:可直接使用 `align-items-*` 和 `justify-content-*` 控制行内对齐
2. **列排序**:`col-*-offset-*` 仍可用,但推荐使用 `order-*` 进行排序
3. **嵌套行**:嵌套 `row` 不再需要额外处理,flex 自动处理
### Gutter 类名变化
Bootstrap 5 将 gutter(沟槽)从负 margin + padding 改为专用类名:
| 旧方式(Bootstrap 3) | 新方式(Bootstrap 5) | 说明 |
|------------------------|----------------------|------|
| 在 `row` 上设置 `margin-left: -15px; margin-right: -15px;` | `g-0` ~ `g-5` | 水平和垂直沟槽 |
| 无 | `gx-0` ~ `gx-5` | 仅水平沟槽 |
| 无 | `gy-0` ~ `gy-5` | 仅垂直沟槽 |
| 响应式无 | `g-sm-*` / `g-md-*` / `g-lg-*` | 响应式沟槽 |
```html
内容
内容
内容
```
### 断点变化
| 断点 | Bootstrap 3 | Bootstrap 5 | 说明 |
|------|-------------|-------------|------|
| 超小 | `< 768px`(`col-xs-*`) | `< 576px`(`col-*`) | 新增 576px 断点,xs 不再需要后缀 |
| 小 | `≥ 768px`(`col-sm-*`) | `≥ 576px`(`col-sm-*`) | 断点值下移 |
| 中 | `≥ 992px`(`col-md-*`) | `≥ 768px`(`col-md-*`) | 断点值下移 |
| 大 | `≥ 1200px`(`col-lg-*`) | `≥ 992px`(`col-lg-*`) | 断点值下移 |
| 超大 | 无 | `≥ 1200px`(`col-xl-*`) | 新增断点 |
| 超超大 | 无 | `≥ 1400px`(`col-xxl-*`) | 新增断点 |
> **迁移注意**:如果旧代码中使用了 `col-md-*` 来适配 768px 以上,在 Bootstrap 5 中应改为 `col-sm-*`,因为 `md` 断点已变为 768px。建议逐一检查响应式断点是否仍符合设计意图。
---
# 第三章:htmx + Alpine.js 协作规范
本章详细阐述 Xiuno BBS 4.5+ 前端架构中 htmx 与 Alpine.js 的协作方式,包括 jQuery 到 `xiuno-modern.js` 兼容层的迁移、htmx 2.x 和 Alpine.js 3.x 的集成配置、脚本加载顺序以及七条核心原则。
## 3.1 jQuery → xiuno-modern.js 兼容层
`xiuno-modern.js`(路径:`view/js/xiuno-modern.js`)是原生 JS 兼容层,提供与 jQuery 对等的 API,新代码必须使用 `XN.xxx()` 替代 `$`。旧代码(xiuno.js / bbs.js / form.js)仍依赖 jQuery,保持不变。
### API 对照表
| jQuery 用法 | XN 兼容层用法 | 说明 |
|------------|-------------|------|
| `$(selector)` | `XN.$(selector)` | 单元素选择,返回 Element 或 null;传入 Element 原样返回 |
| `$(selector)` (多元素) | `XN.$$(selector)` | 多元素选择,返回 Array;支持字符串、NodeList、Array |
| `$.ajax({url, method, data, success})` | `XN.ajax(method, url, data, options)` | Promise 风格 AJAX,返回 `{code, message, data}` |
| `$.post(url, data, callback)` | `XN.post(url, data, callback)` | POST 请求,callback(code, msg),code=0 表示成功 |
| `$.get(url, callback)` | `XN.get(url, callback)` | GET 请求,callback(code, msg) |
| `$.xpost(url, data, callback)` | `XN.post(url, data, callback)` | 注意:旧 API code=1 成功,新 API code=0 成功 |
| `$.xget(url, callback)` | `XN.get(url, callback)` | 同上,注意 code 值差异 |
| `$(el).addClass('cls')` | `XN.addClass(el, 'cls')` | 支持多类名(空格分隔),el 可传选择器字符串 |
| `$(el).removeClass('cls')` | `XN.removeClass(el, 'cls')` | 支持多类名(空格分隔) |
| `$(el).toggleClass('cls')` | `XN.toggleClass(el, 'cls')` | 切换单个类名 |
| `$(el).hasClass('cls')` | `XN.hasClass(el, 'cls')` | 返回 boolean |
| `$(el).show()` | `XN.show(el)` | 设置 `style.display = ''` |
| `$(el).hide()` | `XN.hide(el)` | 设置 `style.display = 'none'` |
| `$(el).toggle()` | `XN.toggle(el)` | 切换 display |
| `$(el).html()` / `$(el).html(content)` | `XN.html(el)` / `XN.html(el, content)` | 读取或设置 innerHTML |
| `$(el).text()` / `$(el).text(content)` | `XN.text(el)` / `XN.text(el, content)` | 读取或设置 textContent |
| `$(el).val()` / `$(el).val(value)` | `XN.val(el)` / `XN.val(el, value)` | 读取或设置表单值 |
| `$(el).attr('name')` / `$(el).attr('name', val)` | `XN.attr(el, 'name')` / `XN.attr(el, 'name', val)` | 读取或设置属性 |
| `$(el).removeAttr('name')` | `XN.removeAttr(el, 'name')` | 移除属性 |
| `$(el).on('click', handler)` | `XN.on(el, 'click', handler)` | 事件绑定,el 可传选择器 |
| `$(parent).on('click', '.child', handler)` | `XN.on(el, 'click', '.child', handler)` | 事件委托,第三个参数为选择器 |
| `$(el).off('click', handler)` | `XN.off(el, 'click', handler)` | 移除事件监听 |
| `$(document).ready(fn)` | `XN.ready(fn)` | DOM Ready,已加载时立即执行 |
| `$(form).serialize()` | `XN.serialize(form)` | 返回 Object(非字符串),同名键自动合并为数组 |
| `$(form).submit()` + `$.post()` | `XN.submit(form, url, callback)` | 自动提取 FormData + CSRF token,POST 提交 |
| `$.cookie(name, value, time)` | `XN.cookie(name, value, time, path)` | Cookie 读写;value=null 删除;time 单位秒 |
| `$.alert(msg)` | `XN.toast(msg, 'danger')` | Toast 提示替代弹窗 |
| `xn.url(route)` | `XN.url(route)` | URL 生成,支持 url_rewrite_on 四种模式 |
| `window.location.href = url` | `XN.redirect(url, delay)` | 延迟重定向,delay 单位秒;url 为空则刷新 |
| `localStorage.getItem/setItem` | `XN.storage.get/set(key, value)` | 自动 JSON 序列化/反序列化 |
| `localStorage.removeItem` | `XN.storage.remove(key)` | 删除存储项 |
| — | `XN.toast(message, type, duration)` | Bootstrap 5 Toast 集成,type: success/danger/warning/info |
| — | `XN.escapeHtml(s)` | HTML 转义,防 XSS |
| — | `XN.intval(s)` | 整数转换,NaN 返回 0 |
| — | `XN.empty(v)` | 空值检测,支持 null/undefined/''/'0'/[]/{}/false |
| — | `XN.htmx.post(url, data, target, swap)` | htmx.ajax POST 封装,默认 target='#main' |
| — | `XN.htmx.get(url, target, swap)` | htmx.ajax GET 封装,默认 target='#main' |
### XN.ajax() 的 Promise 风格用法和错误码约定
`XN.ajax()` 是底层 AJAX 方法,返回 Promise,所有其他请求方法(`XN.get`、`XN.post`)均基于它封装:
```javascript
// 基本用法
XN.ajax('POST', 'thread-like-1-2', {key: 'value'}, {timeout: 10000})
.then(function(result) {
// result = {code: 0, message: '...', data: {...}}
console.log(result.code); // 0 表示成功
console.log(result.message); // 服务端返回的消息
console.log(result.data); // 服务端返回的完整 JSON
})
.catch(function(err) {
console.log(err.code); // 错误码
console.log(err.message); // 错误信息
});
```
**错误码约定**:
| 错误码 | 含义 | 来源 |
|--------|------|------|
| `0` | 成功 | 服务端返回 `json.code == 0` |
| `> 0` | 业务错误 | 服务端返回 `json.code != 0`,message 为错误描述 |
| `-HTTP状态码` | HTTP 错误 | 如 `-404`、`-500`,响应状态码 >= 400 |
| `-100` | 服务端响应为空 | `response.text()` 返回空字符串 |
| `-101` | 响应非 JSON | JSON.parse 失败,message 为原始文本 |
| `-1001` | 请求超时 | AbortController 触发,默认 30 秒 |
**options 参数**:
```javascript
XN.ajax(method, url, data, {
timeout: 30000, // 超时时间(毫秒),默认 30000
headers: { // 自定义请求头,默认包含 X-Requested-With: XMLHttpRequest
'X-Custom-Header': 'value'
}
});
```
### XN.post() 自动携带 CSRF token 机制
`XN.post()` 本身不自动附加 CSRF token,但系统通过以下两种机制确保 POST 请求携带 token:
**机制一:XMLHttpRequest 全局拦截**(`footer.inc.htm`)
```javascript
// footer.inc.htm 中的全局拦截器
var token = document.querySelector('meta[name="csrf-token"]');
if(token) token = token.getAttribute('content');
if(token && typeof XMLHttpRequest !== 'undefined') {
var origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(data) {
this.setRequestHeader('X-CSRF-Token', token);
return origSend.apply(this, arguments);
};
}
```
此拦截器自动为所有 XMLHttpRequest 请求添加 `X-CSRF-Token` 请求头。
**机制二:XN.submit() 表单提交**
```javascript
// XN.submit() 自动从表单中提取 CSRF token
XN.submit(form, url, callback);
// 内部逻辑:
// var csrfToken = form.querySelector('input[name="csrf_token"]');
// if (csrfToken && !fd.has('csrf_token')) {
// fd.set('csrf_token', csrfToken.value);
// }
```
**手动获取 CSRF token**(使用原生 `fetch()` 时):
```javascript
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
if(csrfMeta) {
data.csrf_token = csrfMeta.getAttribute('content');
}
```
### XN.toast() 与 Bootstrap 5 Toast 集成
`XN.toast()` 封装了 Bootstrap 5 的 Toast 组件,自动创建容器和元素:
```javascript
// 基本用法
XN.toast('操作成功'); // 默认 info 类型,3 秒后消失
XN.toast('保存成功', 'success'); // 绿色提示
XN.toast('操作失败', 'danger'); // 红色提示
XN.toast('请注意', 'warning', 5000); // 黄色提示,5 秒后消失
// 类型与颜色映射
// success → bg-success(绿色)
// danger → bg-danger(红色)
// warning → bg-warning text-dark(黄色)
// info → bg-primary(蓝色,默认)
```
**实现原理**:
1. 检查 `.toast-container` 是否存在,不存在则创建(`position-fixed top-0 end-0 p-3`,z-index: 9999)
2. 创建 `.toast` 元素,设置类型对应的背景色
3. 使用 `new bootstrap.Toast(toast, {delay: duration})` 初始化并显示
4. 监听 `hidden.bs.toast` 事件自动移除 DOM 元素
5. 消息内容通过 `XN.escapeHtml()` 转义,防止 XSS
## 3.2 htmx 2.x 集成
### body 标签配置详解
来自 `view/htm/header.inc.htm` 第 81 行:
```html
```
| 属性 | 值 | 作用 |
|------|-----|------|
| `hx-boost` | `"true"` | 全局启用链接和表单的 htmx 增强,普通 `` 和 `XIUNOX版本发布地址:https://xiuno.xcxgy.cn