Compare commits

..
10 Commits
Author SHA1 Message Date
熊熊熊子路 9e2cf53dc9 docs: 重构文档,将详细指南从README中移出 2026-07-31 14:17:13 +08:00
熊熊熊子路 880d8e758f feat: 为iOS ATS限制添加图片URL兼容性
- 引入`urlToBase64`和`getImageDisplayUrl`工具函数,以处理iOS/Mac WKWebView ATS策略下的HTTP图片加载问题。在需要时自动将HTTP图片转换为base64数据URL,同时保持HTTPS和非Apple平台不受影响。
- 更新README以添加使用示例
- 在AGENTS.md中添加新的工具文件引用。
2026-07-31 13:53:13 +08:00
熊熊熊子路 11c6fb21cd refactor: 将page-container重命名为root-page-layout
将page-container组件重命名为root-page-layout,并更新所有文档中的引用,以更好地反映其作为页面根布局的角色。已更新AGENTS.md、README.md中的引用,并相应重命名了组件使用指南。
2026-07-31 11:59:34 +08:00
熊熊熊子路 3f75ab4ef6 docs: 添加AGENTS.md和CLAUDE.md用于代理配置
添加文档文件,为在此仓库中工作的AI编码代理提供上下文和指令。这些文件描述了项目结构、命令、架构决策和关键约定。
2026-07-31 11:49:04 +08:00
熊熊熊子路 99e49ef286 feat(config): 添加全局SASS/SCSS导入配置
设置`sassCommonUseFilePath`和`scssCommonUseFilePath`以引用全局SASS和SCSS文件,实现项目范围内的样式导入,无需在每个组件中显式编写@use语句。
2026-07-31 11:32:16 +08:00
熊熊熊子路 f19f4b8d41 feat(app): 添加平台信息工具并将其集成到应用初始化中
- 创建 utils/platform.ts 以提取并规范化设备/操作系统信息
- 将 globalData 中的普通 `system` 字符串替换为结构化的 `platformInfo` 对象
- 推导硬件/软件详情、设备类别和平台标志
- 更新 TypeScript 类型定义以支持新的平台数据类型
2026-07-31 11:31:31 +08:00
熊熊熊子路 58cd96aaa3 docs: 更新 page-container 组件文档,重命名导航栏属性,添加样式变量说明及示例,优化返回按钮逻辑 2026-03-19 14:07:25 +08:00
熊熊熊子路 caf5803e44 feat: 更新 page-container 组件,重命名导航栏相关属性并添加背景色和底部安全区适配功能 2026-02-09 16:06:18 +08:00
熊熊熊子路 7e05fc2a67 style: 为页面容器添加 overflow-x hidden 以优化布局 2026-02-09 15:25:05 +08:00
熊熊熊子路 f371245e7a refactor: 移除 delta 属性并简化 goBack 方法逻辑 2026-02-09 15:24:33 +08:00
25 changed files with 1012 additions and 566 deletions

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
# CLAUDE.md
This file provides guidance to AI Coding Agent when working with code in this repository.
## 项目概况
微信小程序 TypeScript 模板 — 基于 TypeScript + SASS 的小程序开发脚手架,集成 TDesign 组件库、axios-miniprogram 网络请求、crypto-js AES 加密等。
## 常用命令
```bash
# 安装依赖(需要 pnpm)
pnpm install
# ESLint 检查
pnpm lint
# ESLint 自动修复
pnpm lint:fix
# Prettier 格式化
pnpm format
# Prettier 格式检查
pnpm format:check
```
**微信开发者工具操作:**
- 修改 `package.json` 依赖后,需在微信开发者工具中执行「工具 → 构建 npm」
- TypeScript 和 SASS 编译由微信开发者工具内置编译器完成(无独立构建命令)
- 开发时在微信开发者工具中点击「编译」即可预览
## 技术栈
| 类别 | 技术 |
| -------- | -------------------------------------------------------------------- |
| 语言 | TypeScript (ES2020 target, CommonJS module) |
| 样式 | SASS/SCSS(通过 `project.config.json` 的 `useCompilerPlugins` 启用) |
| UI 组件 | TDesign Miniprogram v1.12+ |
| HTTP | axios-miniprogram v2.7+ |
| 加密 | crypto-js v4.2+ |
| 包管理 | pnpm |
| 代码规范 | ESLint 9 + TypeScript ESLint + Prettier |
## 项目架构
```
miniprogram/
├── app.ts # 入口:计算导航栏/安全区尺寸、初始化 platformInfo 到 globalData
├── app.json # 页面注册、自定义 TabBar、navigationStyle: "custom"
├── app.scss # 全局样式重置
├── api/ # API 接口层(每接口一个文件 + types.ts 定义 StandardResponse<T>)
├── components/ # 共享组件(目前仅有 root-page-layout)
├── custom-tab-bar/ # 自定义 TabBar(基于 TDesign t-tab-bar)
├── pages/ # 页面(index, login, date, one~four)
├── utils/
│ ├── request.ts # axios-miniprogram 封装:Token 注入、loading 管理、401 跳转
│ ├── aes.ts # AES-256-CBC 加密/解密 + URL Safe Base64 变体
│ ├── util.ts # 日期格式化(formatTime / formatDateTime)
│ ├── image.ts # 图片 URL 兼容(iOS ATS http→base64)
│ └── platform.ts # 微信宿主平台信息检测(设备类别/操作系统/桌面or移动)
└── miniprogram_npm/ # 微信开发者工具构建的 npm 产物(已 gitignore,不直接编辑)
typings/ # 全局类型声明(IAppOption、PlatformInfoType 等)+ 微信 API 类型
```
## 内置工具模块
所有工具位于 `miniprogram/utils/`,均有对应的详细文档:
| 模块 | 文件 | 文档 | 用途 |
|------|------|------|------|
| 网络请求 | `request.ts` | `docs/request.md` | axios-miniprogram 封装,Token 注入、loading 管理、401 跳转、`upload()` |
| AES 加解密 | `aes.ts` | `docs/aes.md` | AES-256-CBC,`Encrypt`/`Decrypt`/`BASE64Encrypt`/`BASE64Decrypt` |
| 图片工具 | `image.ts` | `docs/image.md` | iOS ATS 兼容,HTTP 图片自动转 base64,`getImageDisplayUrl`/`urlToBase64` |
| 日期格式化 | `util.ts` | `docs/util.md` | `formatTime`/`formatDateTime`,支持多输入类型和自定义模板 |
| 平台检测 | `platform.ts` | — | `getPlatformInfo()`,启动时调用,结果存入 `globalData.platformInfo` |
所有组件文档:
| 组件 | 目录 | 文档 | 用途 |
|------|------|------|------|
| RootPageLayout | `components/root-page-layout/` | `docs/root-page-layout.md` | 页面容器,封装 TDesign Navbar,自动返回按钮逻辑、安全区适配 |
| 自定义 TabBar | `custom-tab-bar/` | `docs/custom-tab-bar.md` | 基于 TDesign t-tab-bar,需在 TabBar 页 `onShow` 中调用 `init()` |
## 核心架构决策
### 自定义导航栏
`app.json` 中 `window.navigationStyle: "custom"` 全局关闭了原生导航栏。所有需要导航栏的页面必须使用 `root-page-layout` 组件(封装 TDesign `t-navbar`),该组件自动根据页面栈深度决定是否显示返回按钮。
### 网络请求模式
所有 API 调用遵循统一模式:
1. `api/types.ts` 定义 `StandardResponse<T>` 结构(`{ code, data, message }`)
2. 每个 API 模块从 `utils/request` 导入 axios 实例,定义请求函数
3. 请求拦截器自动注入 `Authorization` header(从 `app.globalData.token` 读取),白名单接口除外
4. 响应拦截器在 `code === 200` 时自动解包返回 `result.data`,401 时 `reLaunch` 到登录页
5. 并发请求的 loading 动画通过计数器管理(`netWorkNum`),避免闪烁
### GlobalData 结构
`app.globalData` 在 `onLaunch` 中初始化以下数据:
- **UI 布局数据**:`navBarHeight`、`navBarContentHeight`、`statusBarHeight`、`menuRight`、`menuTop`、`menuHeight`、`safeAreaBottom`、`safeAreaInsetBottom` — 通过 `wx.getMenuButtonBoundingClientRect()` 和 `wx.getWindowInfo()` 计算
- **平台信息**:`platformInfo` — 通过 `utils/platform.ts` 的 `getPlatformInfo()` 获取,包含 hardware、software、isMobile、isDesktop、isDevtools
- **认证**:`token`(登录成功后由业务代码设置)
类型定义位于 `typings/index.d.ts` 的 `IAppOption` 接口。
### 自定义 TabBar
- `app.json` 中 `tabBar.custom: true` 启用自定义 TabBar
- 组件位于 `miniprogram/custom-tab-bar/`,基于 TDesign `t-tab-bar`
- TabBar 页面必须在 `onShow` 中调用 `this.getTabBar().init()` 同步激活状态
- `app.json` 的 `tabBar.list` 和 `custom-tab-bar/index.ts` 的 `data.list` 需保持配置一致
### SASS 全局注入
`project.config.json` 中通过 `sassSetting` 配置了 `sassCommonUseFilePath: "global.sass"` 和 `scssCommonUseFilePath: "global.scss"`,微信开发者工具会自动将这些全局文件注入每个 SCSS/SASS 文件。当前 miniprogram 根目录下尚无 `global.scss` / `global.sass` 文件,需手动创建后使用。
### 无独立构建系统
本项目不依赖 Webpack/Vite 等构建工具。TypeScript 和 SASS 编译由微信开发者工具的 SWC 编译器内置完成(`project.config.json` 中 `swc: true`)。ESLint/Prettier 仅用于代码质量检查,不参与编译流程。
## 关键约定
- **npm 镜像**:`.npmrc` 配置了 npmmirror.com 镜像和 node-sass 二进制镜像
- **代码风格**:分号关闭、单引号、100 字符宽度、2 空格缩进、LF 换行
- **ESLint**:`no-console` 为 warn 级别(仅允许 `console.warn` 和 `console.error`),`@typescript-eslint/no-explicit-any` 为 warn
- **路径别名**:无路径别名 — 所有 import 使用相对路径
- **微信基础库版本**:`libVersion: "3.14.1"`(`project.config.json`),`3.17.0`(`project.private.config.json`)
- **npm 构建模式**:手动模式(`packNpmManually: true`),npm 包输出到 `miniprogram/` 目录
- **Git 忽略**:`miniprogram/miniprogram_npm/`、`project.private.config.json`、`*.js.map` 等不纳入版本控制
## 类型系统
全局类型通过 `typings/index.d.ts` 的三斜线指令引入,定义了:
- `IAppOption` — App 实例类型(含 `globalData` 完整结构)
- `PlatformInfoType` / `HardwarePlatformInfoType` / `SoftwarePlatformInfoType` — 平台信息层级类型
- `ClientPlatformType` / `OperatingSystemType` / `HardwareDeviceClassType` — 联合类型
- 微信小程序 API 类型位于 `typings/types/wx/` 目录
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+13 -227
View File
@@ -17,6 +17,7 @@
- 完整的 TypeScript 类型支持,提供更好的开发体验和代码提示
- 基于 axios-miniprogram 的网络请求封装,支持自动 Token 注入、统一错误处理、智能加载动画管理
- AES-256 加密解密工具,支持标准 Base64 和 URL Safe Base64 两种格式
- 图片 URL 兼容工具,自动处理 iOS WKWebView 下 HTTP 图片无法加载的问题(ATS 限制),按需转 base64
- 集成 TDesign 组件库,提供丰富的 UI 组件
- 自定义 TabBar 实现,基于 TDesign 组件,支持自动状态同步和页面切换
- 完善的代码规范配置,支持 ESLint 和 Prettier 自动格式化
@@ -89,7 +90,9 @@ ts-weapp-template/
│ ├── utils/ # 工具函数
│ │ ├── request.ts # 网络请求封装
│ │ ├── aes.ts # AES 加密解密工具
│ │ └── util.ts # 其他工具函数
│ │ ├── image.ts # 图片 URL 兼容工具
│ │ ├── util.ts # 日期格式化工具
│ │ └── platform.ts # 平台信息检测
│ ├── app.ts # 小程序入口文件
│ ├── app.json # 小程序配置文件
│ └── app.scss # 全局样式
@@ -102,233 +105,16 @@ ts-weapp-template/
## 核心功能说明
### 网络请求封装
本模板内置了以下工具和组件,方便开发者进行小程序业务的开发:
项目提供了基于 `axios-miniprogram` 的请求封装,位于 `miniprogram/utils/request.ts`。
**主要特性:**
- 自动添加 Authorization 请求头(支持白名单配置)
- 统一的响应格式处理(标准格式:`{ code, data, message }`)
- 智能加载动画管理(支持并发请求计数)
- 401 未授权自动跳转登录页
- 完整的 TypeScript 类型支持
- 文件上传功能封装
**使用示例:**
```typescript
import request from '../utils/request'
// GET 请求
const data = await request.get('/api/user/info')
// POST 请求
const result = await request.post('/api/user/update', { name: '张三' })
// 禁用加载动画
const data = await request.get('/api/user/info', { loading: false })
// 文件上传
import { upload } from '../utils/request'
const result = await upload({
apiUrl: '/api/upload',
filePath: 'tempFilePath',
name: 'file',
formData: { userId: 123 },
})
```
### AES 加密解密工具
项目提供了完整的 AES-256 加密解密工具,位于 `miniprogram/utils/aes.ts`。
**主要功能:**
- 标准 AES-256-CBC 加密解密
- URL Safe Base64 格式支持
- 完整的错误处理和类型校验
- 自定义密钥和 IV 支持
**使用示例:**
```typescript
import { Encrypt, Decrypt, BASE64Encrypt, BASE64Decrypt } from '../utils/aes'
// 标准加密解密
const encrypted = Encrypt('hello world')
const decrypted = Decrypt(encrypted)
// URL Safe Base64 加密解密
const urlSafeEncrypted = BASE64Encrypt('hello world')
const urlSafeDecrypted = BASE64Decrypt(urlSafeEncrypted)
// 自定义密钥和 IV
const customEncrypted = Encrypt('hello world', 'your-key-32-bytes-long', 'your-iv-16-bytes')
```
### 时间日期格式化工具
项目提供了灵活的时间日期格式化工具,位于 `miniprogram/utils/util.ts`。
**主要功能:**
- 支持 Date 对象、时间戳、日期字符串等多种输入格式
- 自定义格式化模板,支持丰富的占位符
- 兼容中文日期格式(如 "2020年1月1日")
- 兼容 iOS/Safari 日期解析问题
- 支持 12/24 小时制转换
- 支持毫秒显示
**使用示例:**
```typescript
import { formatTime, formatDateTime } from '../utils/util'
// 标准格式:YYYY/MM/DD HH:mm:ss
const time1 = formatTime(new Date())
// 输出:2024/01/15 14:30:45
// 自定义格式
const time2 = formatDateTime(new Date(), 'YYYY-MM-DD HH:mm:ss')
// 输出:2024-01-15 14:30:45
// 只显示日期
const date = formatDateTime(new Date(), 'YYYY年MM月DD日')
// 输出:2024年01月15日
// 12小时制
const time12 = formatDateTime(new Date(), 'YYYY/MM/DD hh:mm:ss A')
// 输出:2024/01/15 02:30:45 下午
// 时间戳格式化
const timestamp = formatDateTime(1705291845000, 'YYYY-MM-DD HH:mm')
// 输出:2024-01-15 14:30
// 字符串日期格式化
const strDate = formatDateTime('2024-01-15', 'YYYY年MM月DD日')
// 输出:2024年01月15日
```
**支持的占位符:**
- `YYYY`: 4位年份
- `YY`: 2位年份
- `MM`: 2位月份
- `M`: 1位月份
- `DD`: 2位日期
- `D`: 1位日期
- `HH`: 2位小时(24小时制)
- `H`: 1位小时(24小时制)
- `hh`: 2位小时(12小时制)
- `h`: 1位小时(12小时制)
- `mm`: 2位分钟
- `m`: 1位分钟
- `ss`: 2位秒
- `s`: 1位秒
- `SSS`: 3位毫秒
- `A`: 上午/下午
- `a`: 上午/下午(小写)
### PageContainer 页面容器组件
项目提供了基于 TDesign Navbar 封装的页面容器组件,位于 `miniprogram/components/page-container/` 目录。
**主要特性:**
- 基于 TDesign Navbar 组件,提供统一的导航栏和内容区域布局
- 自动判断返回按钮显示(根据页面栈深度)
- 支持安全区域适配,适配不同设备
- 支持自定义导航栏样式和插槽内容
- 完整的返回事件回调支持
详细使用说明请参考 [PageContainer 组件使用手册](docs/page-container.md)。
### 自定义 TabBar
项目提供了基于 TDesign 组件的自定义 TabBar 实现,位于 `miniprogram/custom-tab-bar/` 目录。
**主要特性:**
- 基于 TDesign 的 `t-tab-bar` 和 `t-tab-bar-item` 组件,样式统一美观
- 自动状态同步,根据当前页面路由自动更新激活状态
- 支持页面切换,点击 TabBar 项自动跳转到对应页面
- 完整的 TypeScript 类型支持
- 易于扩展和自定义
**配置说明:**
1. **在 `app.json` 中启用自定义 TabBar:**
```json
{
"tabBar": {
"custom": true,
"color": "#000000",
"selectedColor": "#000000",
"backgroundColor": "#000000",
"list": [
{
"pagePath": "pages/one/index",
"text": "页面一"
},
{
"pagePath": "pages/two/index",
"text": "页面二"
},
{
"pagePath": "pages/three/index",
"text": "页面三"
},
{
"pagePath": "pages/four/index",
"text": "页面四"
}
]
}
}
```
2. **TabBar 组件配置:**
在 `miniprogram/custom-tab-bar/index.ts` 中配置 TabBar 项:
```typescript
data: {
value: 'label_1',
list: [
{ value: 'label_1', label: '首页', icon: 'home', pagePath: '/pages/one/index' },
{ value: 'label_2', label: '应用', icon: 'app', pagePath: '/pages/two/index' },
{ value: 'label_3', label: '聊天', icon: 'chat', pagePath: '/pages/three/index' },
{ value: 'label_4', label: '我的', icon: 'user', pagePath: '/pages/four/index' },
],
}
```
**在 TabBar 页面中使用:**
每个 TabBar 页面需要在 `onShow` 生命周期中调用 `init()` 方法来同步 TabBar 状态:
```typescript
Page({
onShow() {
// 同步 TabBar 激活状态
this.getTabBar().init()
},
})
```
**自定义 TabBar 项:**
如需添加或修改 TabBar 项,请按以下步骤操作:
1. 在 `miniprogram/custom-tab-bar/index.ts` 的 `data.list` 数组中添加或修改配置项
2. 在 `miniprogram/app.json` 的 `tabBar.list` 数组中同步更新配置
3. 确保对应的页面已创建并在 `app.json` 的 `pages` 数组中注册
**支持的图标类型:**
TDesign TabBar 组件支持多种内置图标,常用的有:`home`、`app`、`chat`、`user`、`setting` 等。更多图标类型请参考 [TDesign 图标文档](https://tdesign.tencent.com/miniprogram/components/tab-bar)。
| 工具 / 组件 | 说明 | 文档 |
|------------|------|------|
| 网络请求封装 | 基于 axios-miniprogram,Token 注入、loading 管理、401 跳转、文件上传 | [查看文档](docs/request.md) |
| AES 加密解密 | AES-256-CBC 加密解密,支持标准 Base64 和 URL Safe Base64 | [查看文档](docs/aes.md) |
| 图片 URL 兼容 | iOS WKWebView 下 HTTP 图片因 ATS 无法加载的自动转 base64 方案 | [查看文档](docs/image.md) |
| 日期时间格式化 | 多输入格式兼容、自定义模板、iOS 日期解析修复 | [查看文档](docs/util.md) |
| RootPageLayout 组件 | 基于 TDesign Navbar 的页面容器,自动返回按钮、安全区适配 | [查看文档](docs/root-page-layout.md) |
| 自定义 TabBar | 基于 TDesign 的自定义 TabBar,路由同步、状态管理 | [查看文档](docs/custom-tab-bar.md) |
## 开发指南
+34
View File
@@ -0,0 +1,34 @@
# AES 加密解密工具
完整的 AES-256 加密解密工具,位于 `miniprogram/utils/aes.ts`。
## 主要功能
- 标准 AES-256-CBC 加密解密
- URL Safe Base64 格式支持
- 完整的错误处理和类型校验(`AESError`)
- 自定义密钥和 IV 支持
## 使用示例
```typescript
import { Encrypt, Decrypt, BASE64Encrypt, BASE64Decrypt } from '../utils/aes'
// 标准加密解密
const encrypted = Encrypt('hello world')
const decrypted = Decrypt(encrypted)
// URL Safe Base64 加密解密
const urlSafeEncrypted = BASE64Encrypt('hello world')
const urlSafeDecrypted = BASE64Decrypt(urlSafeEncrypted)
// 自定义密钥和 IV
const customEncrypted = Encrypt('hello world', 'your-key-32-bytes-long', 'your-iv-16-bytes')
```
## 注意事项
- 默认的 AES 密钥和 IV 仅用于开发测试,**生产环境请务必修改**
- Key 强制要求 32 字节以触发 AES-256,不足或超出会抛出 `AESError`
- IV 强制要求 16 字节
- 解密失败(密钥不匹配/密文被篡改)会抛出 `AESError`,请调用方 catch 处理
+87
View File
@@ -0,0 +1,87 @@
# 自定义 TabBar
基于 TDesign 组件的自定义 TabBar 实现,位于 `miniprogram/custom-tab-bar/` 目录。
## 主要特性
- 基于 TDesign 的 `t-tab-bar` 和 `t-tab-bar-item` 组件,样式统一美观
- 自动状态同步,根据当前页面路由自动更新激活状态
- 支持页面切换,点击 TabBar 项自动跳转到对应页面
- 完整的 TypeScript 类型支持
- 易于扩展和自定义
## 配置说明
### 1. 在 `app.json` 中启用自定义 TabBar
```json
{
"tabBar": {
"custom": true, // 配置为 true 开启自定义 tabbar
"color": "#000000",
"selectedColor": "#000000",
"backgroundColor": "#000000",
"list": [
{
"pagePath": "pages/one/index",
"text": "页面一"
},
{
"pagePath": "pages/two/index",
"text": "页面二"
},
{
"pagePath": "pages/three/index",
"text": "页面三"
},
{
"pagePath": "pages/four/index",
"text": "页面四"
}
]
}
}
```
### 2. TabBar 组件配置
在 `miniprogram/custom-tab-bar/index.ts` 中配置 TabBar 项:
```typescript
data: {
value: 'label_1',
list: [
{ value: 'label_1', label: '首页', icon: 'home', pagePath: '/pages/one/index' },
{ value: 'label_2', label: '应用', icon: 'app', pagePath: '/pages/two/index' },
{ value: 'label_3', label: '聊天', icon: 'chat', pagePath: '/pages/three/index' },
{ value: 'label_4', label: '我的', icon: 'user', pagePath: '/pages/four/index' },
],
}
```
## 在 TabBar 页面中使用
每个 TabBar 页面需要在 `onShow` 生命周期中调用 `init()` 方法来同步 TabBar 状态:
```typescript
Page({
onShow() {
// 同步 TabBar 激活状态
this.getTabBar().init()
},
})
```
## 自定义 TabBar 项
如需添加或修改 TabBar 项,请按以下步骤操作:
1. 在 `miniprogram/custom-tab-bar/index.ts` 的 `data.list` 数组中添加或修改配置项
2. 在 `miniprogram/app.json` 的 `tabBar.list` 数组中同步更新配置
3. 确保对应的页面已创建并在 `app.json` 的 `pages` 数组中注册
## 注意事项
- TabBar 页面必须在 `onShow` 中调用 `this.getTabBar().init()`,否则可能出现状态不同步
- `app.json` 的 `tabBar.list` 和 `custom-tab-bar/index.ts` 的 `data.list` 配置需保持一致(页面路径和顺序)
- 支持的 TDesign 内置图标:`home`、`app`、`chat`、`user`、`setting` 等,更多参考 [TDesign 图标文档](https://tdesign.tencent.com/miniprogram/components/tab-bar)
+26
View File
@@ -0,0 +1,26 @@
# 图片 URL 兼容工具(iOS ATS)
解决微信小程序在 iOS/Mac WKWebView 下 HTTP 图片因 ATS 限制无法加载的问题,位于 `miniprogram/utils/image.ts`。
## 背景
微信小程序在 iOS/Mac 平台的 WKWebView 下,`<image>` 组件加载 HTTP 图片会被 ATS(App Transport Security)拦截,必须通过 HTTPS 才能远程渲染。本工具自动判断当前平台和协议,对 HTTP 图片在 iOS/Mac 下通过 `wx.request` 下载后转为 base64 data URL,确保图片正常显示。
## 主要功能
- 自动检测当前设备平台(读取 `app.globalData.platformInfo`),仅在 iOS/Mac 下对 HTTP 图片触发 base64 转码
- HTTPS 链接和非 iOS/Mac 平台直接返回原始 URL,零额外开销
- 非完整 URL(不以 `http://` 或 `https://` 开头)会 `console.warn` 提示后原样返回
- `urlToBase64` 使用 `wx.request` + `arraybuffer`,不经过 axios 拦截器
## 使用示例
```typescript
import { getImageDisplayUrl, urlToBase64 } from '../utils/image'
// 推荐:直接获取可用于 <image> 显示的 URL(自动判断是否需要转 base64)
const displayUrl = await getImageDisplayUrl('http://example.com/photo.jpg')
// 或手动将任意图片转为 base64
const base64 = await urlToBase64('https://example.com/photo.jpg')
```
-214
View File
@@ -1,214 +0,0 @@
# PageContainer 组件使用手册
## 概述
PageContainer 是一个基于 TDesign Navbar 组件封装的页面容器组件,提供了统一的导航栏和内容区域布局,简化了小程序页面的开发流程。组件自动处理返回按钮显示逻辑、安全区域适配等常见需求。
## 功能特性
- 基于 TDesign Navbar 组件,样式统一美观
- 自动判断返回按钮显示(根据页面栈深度)
- 支持安全区域适配(适配刘海屏等特殊设备)
- 支持固定导航栏和占位符
- 支持自定义导航栏样式(背景色、文本颜色)
- 提供多个插槽支持自定义内容
- 支持返回事件回调
## 引入组件
在页面的 `json` 配置文件中引入组件:
```json
{
"usingComponents": {
"page-container": "../../components/page-container/page-container"
}
}
```
## 基础用法
### 最简单的用法
```xml
<page-container title="页面标题">
<view>页面内容</view>
</page-container>
```
### 自定义导航栏样式
```xml
<page-container
title="页面标题"
backgroundColor="#123321"
textColor="#ffffff">
<view>页面内容</view>
</page-container>
```
### 隐藏返回按钮
```xml
<page-container title="页面标题" left-arrow="{{false}}">
<view>页面内容</view>
</page-container>
```
### 隐藏导航栏
```xml
<page-container title="页面标题" visible="{{false}}">
<view>页面内容</view>
</page-container>
```
## 属性说明
| 属性名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| title | String | '' | 导航栏标题 |
| leftArrow | Boolean | true | 是否显示返回按钮 |
| fixed | Boolean | true | 导航栏是否固定 |
| placeholder | Boolean | true | 导航栏占位 |
| safeAreaInsetTop | Boolean | true | 是否适配安全区域顶部 |
| zIndex | Number | 10000 | 导航栏层级 |
| animation | Boolean | true | 是否带动画效果 |
| delta | Number | 1 | 后退的层数 |
| titleMaxLength | Number | undefined | 标题最大长度,超出用 "…" 表示 |
| visible | Boolean | true | 是否显示导航栏 |
| backgroundColor | String | '#ffffff' | 导航栏背景色 |
| textColor | String | '#000000' | 导航栏文本颜色(标题、返回按钮等) |
## 插槽说明
组件提供了三个插槽用于自定义导航栏内容:
### left 插槽
自定义左侧内容(返回按钮区域):
```xml
<page-container title="页面标题">
<view slot="left">自定义左侧内容</view>
<view>页面内容</view>
</page-container>
```
### capsule 插槽
自定义胶囊区域内容:
```xml
<page-container title="页面标题">
<view slot="capsule">自定义胶囊内容</view>
<view>页面内容</view>
</page-container>
```
### title 插槽
自定义标题区域内容:
```xml
<page-container title="页面标题">
<view slot="title">自定义标题内容</view>
<view>页面内容</view>
</page-container>
```
## 事件说明
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| success | 返回成功时触发 | event.detail |
| fail | 返回失败时触发 | event.detail |
| complete | 返回完成时触发(无论成功或失败) | event.detail |
### 事件使用示例
```xml
<page-container
title="页面标题"
bind:success="onBackSuccess"
bind:fail="onBackFail"
bind:complete="onBackComplete">
<view>页面内容</view>
</page-container>
```
```typescript
Page({
onBackSuccess(res) {
console.log('返回成功', res)
},
onBackFail(err) {
console.log('返回失败', err)
},
onBackComplete(res) {
console.log('返回完成', res)
},
})
```
## 返回按钮显示逻辑
组件会自动判断是否显示返回按钮,判断规则如下:
1. 如果页面栈中只有一个页面(首页),则不显示返回按钮
2. 如果 `leftArrow` 属性为 `false`,则不显示返回按钮
3. 如果 `visible` 属性为 `false`,则不显示返回按钮
4. 其他情况显示返回按钮
## 注意事项
1. 组件依赖 TDesign 的 `t-navbar` 组件,确保已正确安装和构建 TDesign 组件库
2. 使用组件时,建议在页面的 `json` 配置中关闭原生导航栏:
```json
{
"navigationStyle": "custom"
}
```
3. 组件会自动处理返回按钮的显示逻辑,无需手动判断页面栈
4. 如需自定义返回行为,可以通过监听 `success`、`fail`、`complete` 事件来实现
5. 使用 `delta` 属性可以控制返回的页面层数,默认为 1(返回上一页)
## 完整示例
```xml
<!-- pages/example/index.wxml -->
<page-container
title="示例页面"
backgroundColor="#ffffff"
textColor="#000000"
left-arrow="{{true}}"
fixed="{{true}}"
placeholder="{{true}}"
safe-area-inset-top="{{true}}"
bind:success="onBackSuccess">
<view class="content">
<view>这是页面内容区域</view>
</view>
</page-container>
```
```json
// pages/example/index.json
{
"navigationStyle": "custom",
"usingComponents": {
"page-container": "../../components/page-container/page-container"
}
}
```
```typescript
// pages/example/index.ts
Page({
onBackSuccess(res) {
console.log('页面返回成功', res)
},
})
```
+44
View File
@@ -0,0 +1,44 @@
# 网络请求封装
基于 `axios-miniprogram` 的网络请求封装,位于 `miniprogram/utils/request.ts`。
## 主要特性
- 自动添加 Authorization 请求头(支持白名单配置)
- 统一的响应格式处理(标准格式:`{ code, data, message }`)
- 智能加载动画管理(支持并发请求计数)
- 401 未授权自动跳转登录页
- 完整的 TypeScript 类型支持
- 文件上传功能封装
## 使用示例
```typescript
import request from '../utils/request'
// GET 请求
const data = await request.get('/api/user/info')
// POST 请求
const result = await request.post('/api/user/update', { name: '张三' })
// 禁用加载动画
const data = await request.get('/api/user/info', { loading: false })
// 文件上传
import { upload } from '../utils/request'
const result = await upload({
apiUrl: '/api/upload',
filePath: 'tempFilePath',
name: 'file',
formData: { userId: 123 },
})
```
## 注意事项
1. **API 地址配置**: 请在 `request.ts` 中配置正确的 `baseURL`
2. **Token 管理**: Token 存储在 `app.globalData.token` 中,登录成功后需要手动设置
3. **请求白名单**: 不需要认证的接口需要在 `request.ts` 的 `requestWhiteList` 数组中配置
4. 响应拦截器在 `code === 200` 时自动解包返回 `result.data`,调用方直接拿到业务数据
5. 并发请求的 loading 动画通过计数器管理,避免闪烁
+268
View File
@@ -0,0 +1,268 @@
# RootPageLayout 组件使用手册
## 概述
RootPageLayout 是一个基于 TDesign `t-navbar` 封装的页面容器组件,提供统一的导航栏与内容区域布局。组件内部已引入 `t-navbar`,并通过样式变量透传导航栏与容器背景配置,适用于需要自定义导航栏的页面。
## 功能特性
- 基于 TDesign Navbar 组件,样式统一美观
- 根据页面栈深度与属性配置,自动计算返回按钮显示
- 支持安全区域适配(顶部安全区由 `t-navbar` 处理,底部安全区由容器处理)
- 支持固定导航栏和占位符
- 支持自定义导航栏样式(背景色、文本颜色)
- 支持自定义内容区域背景色
- 提供多个插槽支持自定义导航栏内容与页面内容
- 支持返回事件回调
## 引入组件
在页面的 `json` 配置文件中引入组件:
```json
{
"usingComponents": {
"root-page-layout": "../../components/root-page-layout/root-page-layout"
}
}
```
## 基础用法
### 最简单的用法
```xml
<root-page-layout nav-bar-title="页面标题">
<view>页面内容</view>
</root-page-layout>
```
### 自定义导航栏样式
```xml
<root-page-layout
nav-bar-title="页面标题"
nav-bar-background-color="#123321"
nav-bar-text-color="#ffffff"
>
<view>页面内容</view>
</root-page-layout>
```
### 隐藏返回按钮
```xml
<root-page-layout nav-bar-title="页面标题" nav-bar-left-arrow="{{false}}">
<view>页面内容</view>
</root-page-layout>
```
### 隐藏导航栏
```xml
<root-page-layout nav-bar-title="页面标题" nav-bar-visible="{{false}}">
<view>页面内容</view>
</root-page-layout>
```
### 关闭底部安全区适配
```xml
<root-page-layout
nav-bar-title="页面标题"
container-safe-area-inset-bottom="{{false}}"
>
<view>页面内容</view>
</root-page-layout>
```
## 样式与主题
### 容器背景色
组件通过样式变量 `--pg-container-bg-color` 设置容器背景色,对应属性 `containerBackgroundColor`。
### 导航栏颜色
组件通过 `t-navbar` 的样式变量设置导航栏颜色:
- `--td-navbar-bg-color`:对应 `navBarBackgroundColor`
- `--td-navbar-color`:对应 `navBarTextColor`
## 属性说明
### 导航栏相关
| 属性名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| navBarTitle | String | '' | 导航栏标题 |
| navBarLeftArrow | Boolean | true | 是否显示返回按钮 |
| navBarFixed | Boolean | true | 导航栏是否固定 |
| navBarPlaceholder | Boolean | true | 导航栏占位 |
| navBarSafeAreaInsetTop | Boolean | true | 是否适配安全区域顶部 |
| navBarZIndex | Number | 10000 | 导航栏层级 |
| navBarAnimation | Boolean | true | 是否带动画效果 |
| navBarTitleMaxLength | Number | undefined | 标题最大长度,超出用 "…" 表示 |
| navBarVisible | Boolean | true | 是否显示导航栏 |
| navBarBackgroundColor | String | '#ffffff' | 导航栏背景色 |
| navBarTextColor | String | '#000000' | 导航栏文本颜色(标题、返回按钮等) |
### 内容容器相关
| 属性名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| containerSafeAreaInsetBottom | Boolean | true | 是否开启底部安全区适配 |
| containerBackgroundColor | String | '#f5f5f5' | 内容区域背景色 |
## 插槽说明
组件提供导航栏插槽与默认插槽,用于自定义页面内容布局。
### 默认插槽
默认插槽用于渲染页面主体内容(位于内容区域 `root-page-layout__content` 内):
```xml
<root-page-layout nav-bar-title="页面标题">
<view>页面内容</view>
</root-page-layout>
```
### left 插槽
自定义左侧内容(返回按钮区域):
```xml
<root-page-layout nav-bar-title="页面标题">
<view slot="left">自定义左侧内容</view>
<view>页面内容</view>
</root-page-layout>
```
### capsule 插槽
自定义胶囊区域内容:
```xml
<root-page-layout nav-bar-title="页面标题">
<view slot="capsule">自定义胶囊内容</view>
<view>页面内容</view>
</root-page-layout>
```
### title 插槽
自定义标题区域内容:
```xml
<root-page-layout nav-bar-title="页面标题">
<view slot="title">自定义标题内容</view>
<view>页面内容</view>
</root-page-layout>
```
## 事件说明
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| success | `wx.navigateBack` 成功时触发 | `event.detail`(navigateBack 成功回调参数) |
| fail | `wx.navigateBack` 失败时触发 | `event.detail`(navigateBack 失败回调参数) |
| complete | `wx.navigateBack` 完成时触发(无论成功或失败) | `event.detail`(success/fail 的参数之一) |
### 事件使用示例
```xml
<root-page-layout
nav-bar-title="页面标题"
bind:success="onBackSuccess"
bind:fail="onBackFail"
bind:complete="onBackComplete"
>
<view>页面内容</view>
</root-page-layout>
```
```typescript
Page({
onBackSuccess(res) {
console.log('返回成功', res)
},
onBackFail(err) {
console.log('返回失败', err)
},
onBackComplete(res) {
console.log('返回完成', res)
},
})
```
## 返回按钮显示逻辑
组件在实例进入页面节点树时,根据以下条件计算返回按钮是否显示:
```text
showLeftArrow = getCurrentPages().length > 1 && navBarLeftArrow && navBarVisible
```
## 注意事项
1. 组件内部依赖 TDesign 的 `t-navbar`(组件自身已在 `usingComponents` 中引入),确保项目已正确集成 `tdesign-miniprogram`
2. 使用组件时,建议在页面的 `json` 配置中关闭原生导航栏:
```json
{
"navigationStyle": "custom"
}
```
3. 组件会自动处理返回按钮的显示逻辑,无需手动判断页面栈
4. 如需自定义返回行为,可以通过监听 `success`、`fail`、`complete` 事件来实现
## 完整示例
```xml
<!-- pages/example/index.wxml -->
<root-page-layout
nav-bar-title="示例页面"
nav-bar-background-color="#ffffff"
nav-bar-text-color="#000000"
nav-bar-left-arrow="{{true}}"
nav-bar-fixed="{{true}}"
nav-bar-placeholder="{{true}}"
nav-bar-safe-area-inset-top="{{true}}"
container-background-color="#f5f5f5"
container-safe-area-inset-bottom="{{true}}"
bind:success="onBackSuccess"
bind:fail="onBackFail"
bind:complete="onBackComplete"
>
<view class="content">
<view>这是页面内容区域</view>
</view>
</root-page-layout>
```
```json
// pages/example/index.json
{
"navigationStyle": "custom",
"usingComponents": {
"root-page-layout": "../../components/root-page-layout/root-page-layout"
}
}
```
```typescript
// pages/example/index.ts
Page({
onBackSuccess(res) {
console.log('页面返回成功', res)
},
onBackFail(err) {
console.log('页面返回失败', err)
},
onBackComplete(res) {
console.log('页面返回完成', res)
},
})
```
+64
View File
@@ -0,0 +1,64 @@
# 时间日期格式化工具
灵活的时间日期格式化工具,位于 `miniprogram/utils/util.ts`。
## 主要功能
- 支持 Date 对象、时间戳、日期字符串等多种输入格式
- 自定义格式化模板,支持丰富的占位符
- 兼容中文日期格式(如 "2020年1月1日")
- 兼容 iOS/Safari 日期解析问题
- 支持 12/24 小时制转换
- 支持毫秒显示
## 使用示例
```typescript
import { formatTime, formatDateTime } from '../utils/util'
// 标准格式:YYYY/MM/DD HH:mm:ss
const time1 = formatTime(new Date())
// 输出:2024/01/15 14:30:45
// 自定义格式
const time2 = formatDateTime(new Date(), 'YYYY-MM-DD HH:mm:ss')
// 输出:2024-01-15 14:30:45
// 只显示日期
const date = formatDateTime(new Date(), 'YYYY年MM月DD日')
// 输出:2024年01月15日
// 12小时制
const time12 = formatDateTime(new Date(), 'YYYY/MM/DD hh:mm:ss A')
// 输出:2024/01/15 02:30:45 下午
// 时间戳格式化
const timestamp = formatDateTime(1705291845000, 'YYYY-MM-DD HH:mm')
// 输出:2024-01-15 14:30
// 字符串日期格式化
const strDate = formatDateTime('2024-01-15', 'YYYY年MM月DD日')
// 输出:2024年01月15日
```
## 占位符一览
| 占位符 | 说明 | 示例 |
|--------|------|------|
| `YYYY` | 4位年份 | 2024 |
| `YY` | 2位年份 | 24 |
| `MM` | 2位月份 | 01 |
| `M` | 1位月份 | 1 |
| `DD` | 2位日期 | 15 |
| `D` | 1位日期 | 15 |
| `HH` | 2位小时(24小时制) | 14 |
| `H` | 1位小时(24小时制) | 14 |
| `hh` | 2位小时(12小时制) | 02 |
| `h` | 1位小时(12小时制) | 2 |
| `mm` | 2位分钟 | 30 |
| `m` | 1位分钟 | 30 |
| `ss` | 2位秒 | 45 |
| `s` | 1位秒 | 45 |
| `SSS` | 3位毫秒 | 123 |
| `A` | 上午/下午 | 下午 |
| `a` | am/pm | pm |
+31 -5
View File
@@ -1,4 +1,6 @@
// app.ts
import { getPlatformInfo } from './utils/platform'
App<IAppOption>({
globalData: {
navBarHeight: 0, // 导航栏总高度
@@ -10,19 +12,43 @@ App<IAppOption>({
tabbarHeight: 0, // 底部导航栏高度
safeAreaBottom: 0, // 底部安全区域坐标
safeAreaInsetBottom: 0, // 中间区域内容的底边坐标
system: '', // 系统信息
platformInfo: {
hardware: {
deviceClass: 'unknown',
brand: '',
model: '',
abi: '',
deviceAbi: '',
cpuType: '',
memorySize: '',
},
software: {
clientPlatform: 'unknown',
rawClientPlatform: '',
operatingSystem: 'unknown',
system: '',
wechatVersion: '',
baseLibraryVersion: '',
hostAppId: '',
},
isMobile: false,
isDesktop: false,
isDevtools: false,
},
token: '',
},
onLaunch() {
// 1. 获取胶囊按钮位置信息
// 获取小程序宿主环境信息
const platformInfo = getPlatformInfo()
this.globalData.platformInfo = platformInfo
// 获取胶囊按钮位置信息
const menuButtonInfo = wx.getMenuButtonBoundingClientRect()
// 2. 获取系统窗口信息
// 获取系统窗口信息
const windowInfo = wx.getWindowInfo()
const deviceInfo = wx.getDeviceInfo()
// 赋值基础信息
this.globalData.system = deviceInfo.system
this.globalData.menuRight = windowInfo.screenWidth - menuButtonInfo.right
this.globalData.menuTop = menuButtonInfo.top
this.globalData.menuHeight = menuButtonInfo.height
@@ -1,18 +0,0 @@
/* components/page-container/page-container.wxss */
.page-container {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
box-sizing: border-box;
}
.page-container__content {
flex: 1;
width: 100%;
box-sizing: border-box;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
@@ -1,35 +0,0 @@
<!--components/page-container/page-container.wxml-->
<view class="page-container">
<!-- 导航栏 -->
<t-navbar
title="{{title}}"
left-arrow="{{showLeftArrow}}"
fixed="{{fixed}}"
placeholder="{{placeholder}}"
safe-area-inset-top="{{safeAreaInsetTop}}"
z-index="{{zIndex}}"
animation="{{animation}}"
delta="{{delta}}"
title-max-length="{{titleMaxLength}}"
visible="{{visible}}"
bind:go-back="goBack"
bind:success="handleBackSuccess"
bind:fail="handleBackFail"
bind:complete="handleBackComplete"
style="--td-navbar-color: {{textColor}};--td-navbar-bg-color: {{backgroundColor}};"
>
<!-- 左侧自定义内容插槽 -->
<slot name="left" slot="left"></slot>
<!-- 胶囊区域自定义内容插槽 -->
<slot name="capsule" slot="capsule"></slot>
<!-- 标题自定义内容插槽 -->
<slot name="title" slot="title"></slot>
</t-navbar>
<!-- 内容区域 -->
<view
class="page-container__content"
>
<slot></slot>
</view>
</view>
@@ -0,0 +1,26 @@
/* components/root-page-layout/root-page-layout.wxss */
.root-page-layout {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
box-sizing: border-box;
background-color: var(--pg-container-bg-color);
// 预留安全区
&--safe {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}
.root-page-layout__content {
flex: 1;
width: 100%;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
@@ -1,4 +1,4 @@
// components/page-container/page-container.ts
// components/root-page-layout/root-page-layout.ts
Component({
/**
@@ -6,65 +6,70 @@ Component({
*/
properties: {
// 导航栏标题
title: {
navBarTitle: {
type: String,
value: '',
},
// 是否显示返回按钮
leftArrow: {
navBarLeftArrow: {
type: Boolean,
value: true,
},
// 导航栏是否固定
fixed: {
navBarFixed: {
type: Boolean,
value: true,
},
// 导航栏占位
placeholder: {
navBarPlaceholder: {
type: Boolean,
value: true,
},
// 是否适配安全区域顶部
safeAreaInsetTop: {
navBarSafeAreaInsetTop: {
type: Boolean,
value: true,
},
// 导航栏层级
zIndex: {
navBarZIndex: {
type: Number,
value: 10000,
},
// 是否带动画效果
animation: {
navBarAnimation: {
type: Boolean,
value: true,
},
// 后退的层数
delta: {
type: Number,
value: 1,
},
// 标题最大长度,超出用 "…" 表示
titleMaxLength: {
navBarTitleMaxLength: {
type: Number,
value: undefined,
},
// 是否显示导航栏
visible: {
navBarVisible: {
type: Boolean,
value: true,
},
// 导航栏背景色
backgroundColor: {
navBarBackgroundColor: {
type: String,
value: '#ffffff',
},
// 导航栏文本颜色(标题、返回按钮等)
textColor: {
navBarTextColor: {
type: String,
value: '#000000',
},
// 是否开启底部安全区适配
containerSafeAreaInsetBottom: {
type: Boolean,
value: true,
},
// rootPageLayout 背景色
containerBackgroundColor: {
type: String,
value: '#f5f5f5',
},
},
/**
@@ -106,8 +111,8 @@ Component({
*/
checkShowLeftArrow() {
const pages = getCurrentPages()
const leftArrow = this.properties.leftArrow
const visible = this.properties.visible
const leftArrow = this.properties.navBarLeftArrow
const visible = this.properties.navBarVisible
const showLeftArrow = pages.length > 1 && leftArrow && visible
this.setData({
showLeftArrow,
@@ -118,46 +123,21 @@ Component({
* 返回按钮点击事件
*/
goBack() {
const pages = getCurrentPages()
const delta = this.properties.delta
if (pages.length > 1 && delta > 0) {
wx.navigateBack({
delta,
success: (res) => {
// 转发成功事件
this.triggerEvent('success', res)
// 转发完成事件
this.triggerEvent('complete', res)
},
fail: (err) => {
// 转发失败事件
this.triggerEvent('fail', err)
// 转发完成事件
this.triggerEvent('complete', err)
},
})
}
},
/**
* 处理返回成功事件(从 t-navbar 转发)
*/
handleBackSuccess(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('success', e.detail)
},
/**
* 处理返回失败事件(从 t-navbar 转发)
*/
handleBackFail(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('fail', e.detail)
},
/**
* 处理返回完成事件(从 t-navbar 转发)
*/
handleBackComplete(e: WechatMiniprogram.CustomEvent) {
this.triggerEvent('complete', e.detail)
console.warn('点击了箭头按钮')
wx.navigateBack({
success: (res) => {
// 转发成功事件
this.triggerEvent('success', res)
// 转发完成事件
this.triggerEvent('complete', res)
},
fail: (err) => {
// 转发失败事件
this.triggerEvent('fail', err)
// 转发完成事件
this.triggerEvent('complete', err)
},
})
},
},
})
@@ -0,0 +1,34 @@
<!--components/root-page-layout/root-page-layout.wxml-->
<view
class="root-page-layout {{containerSafeAreaInsetBottom ? 'root-page-layout--safe' : ''}}"
style="--pg-container-bg-color: {{containerBackgroundColor}};"
>
<!-- 导航栏 -->
<t-navbar
title="{{navBarTitle}}"
left-arrow="{{showLeftArrow}}"
fixed="{{navBarFixed}}"
placeholder="{{navBarPlaceholder}}"
safe-area-inset-top="{{navBarSafeAreaInsetTop}}"
z-index="{{navBarZIndex}}"
animation="{{navBarAnimation}}"
title-max-length="{{navBarTitleMaxLength}}"
visible="{{navBarVisible}}"
bind:go-back="goBack"
style="--td-navbar-color: {{navBarTextColor}};--td-navbar-bg-color: {{navBarBackgroundColor}};"
>
<!-- 左侧自定义内容插槽 -->
<slot name="left" slot="left"></slot>
<!-- 胶囊区域自定义内容插槽 -->
<slot name="capsule" slot="capsule"></slot>
<!-- 标题自定义内容插槽 -->
<slot name="title" slot="title"></slot>
</t-navbar>
<!-- 内容区域 -->
<view
class="root-page-layout__content"
>
<slot></slot>
</view>
</view>
+1 -1
View File
@@ -1,5 +1,5 @@
{
"usingComponents": {
"page-container": "../../components/page-container/page-container"
"root-page-layout": "../../components/root-page-layout/root-page-layout"
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
<!--pages/date/date.wxml-->
<page-container title="日期时间格式化示例" backgroundColor="#123321" textColor="#ffffff">
<root-page-layout navBarTitle="日期时间格式化示例" navBarBackgroundColor="#123321" navBarTextColor="#ffffff">
<view class="container">
<!-- formatTime 函数示例 -->
<view class="section">
@@ -69,4 +69,4 @@
</view>
</view>
</view>
</page-container>
</root-page-layout>
+1 -1
View File
@@ -3,7 +3,7 @@
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black",
"usingComponents": {
"page-container": "../../components/page-container/page-container",
"root-page-layout": "../../components/root-page-layout/root-page-layout",
"t-input": "/miniprogram_npm/tdesign-miniprogram/input/input",
"t-button": "/miniprogram_npm/tdesign-miniprogram/button/button"
}
+2 -2
View File
@@ -1,5 +1,5 @@
<!--pages/login/login.wxml-->
<page-container title="Request 使用示例">
<root-page-layout title="Request 使用示例">
<view class="login-container">
<!-- 登录表单区域 -->
<view class="form-section">
@@ -27,4 +27,4 @@
</view>
</view>
</view>
</page-container>
</root-page-layout>
+71
View File
@@ -0,0 +1,71 @@
/**
* 图片工具 — iOS ATS 兼容处理
*
* 背景:微信小程序在 iOS WKWebView 下,<image> 组件加载 HTTP 图片会被 ATS 拦截,
* 必须先通过 wx.request 下载为 ArrayBuffer,再转为 base64 data URL 才能正常显示。
*/
// ==================== 图片 URL 兼容(iOS ATS) ====================
/**
* 将网络图片转为 base64,用于 iOS 图片回显等场景
* 使用 wx.request 直接请求,避免经过 axios 的 StandardResponse 拦截逻辑
* @param url 图片完整 URL
* @returns Promise<data:image/xxx;base64,xxx> 格式的 base64 字符串
*/
function urlToBase64(url: string): Promise<string> {
if (!url || typeof url !== 'string') {
return Promise.reject(new Error('url 不能为空'))
}
return new Promise((resolve, reject) => {
wx.request({
url,
method: 'GET',
responseType: 'arraybuffer',
success: (res) => {
if (res.statusCode !== 200) {
reject(new Error(`请求失败: ${res.statusCode}`))
return
}
const arrayBuffer = res.data as ArrayBuffer
const base64 = wx.arrayBufferToBase64(arrayBuffer)
const ext = (url.match(/\.(jpg|jpeg|png|gif|webp)/i) || ['', 'png'])[1]
resolve(`data:image/${ext};base64,${base64}`)
},
fail: (err) => reject(err),
})
})
}
/**
* 获取图片显示用的 URL,仅在 HTTP + iOS/Mac 时转为 base64 以解决 ATS 回显问题
* - HTTPS 链接:任何平台均直接返回原始 URL
* - HTTP 链接 + iOS/Mac:转为 base64
* - HTTP 链接 + 其他平台:直接返回原始 URL
* @param url 完整的图片 URL(必须是 http:// 或 https:// 开头)
* @returns Promise<string> 返回可用的 url(网络链接或 base64 字符串)
*/
async function getImageDisplayUrl(url: string): Promise<string> {
const isFullUrl = /^https?:\/\//i.test(url)
if (!isFullUrl) {
console.warn(
'[getImageDisplayUrl] 传入的不是完整 URL,请传入 http:// 或 https:// 开头的完整链接',
)
return url
}
const app: IAppOption = getApp()
const { clientPlatform } = app.globalData.platformInfo.software
const isHttp = /^http:\/\//i.test(url)
const isIosOrMac = clientPlatform === 'ios' || clientPlatform === 'mac'
if (isHttp && isIosOrMac) {
return urlToBase64(url).catch(() => url)
}
return url
}
export { urlToBase64, getImageDisplayUrl }
+81
View File
@@ -0,0 +1,81 @@
const MOBILE_CLIENT_PLATFORMS: ClientPlatformType[] = ['ios', 'android', 'ohos']
const DESKTOP_CLIENT_PLATFORMS: ClientPlatformType[] = ['windows', 'mac', 'ohos_pc']
function normalizeClientPlatform(platform: string): ClientPlatformType {
switch (platform.toLowerCase()) {
case 'ios':
case 'android':
case 'ohos':
case 'ohos_pc':
case 'windows':
case 'mac':
case 'devtools':
return platform.toLowerCase() as ClientPlatformType
default:
return 'unknown'
}
}
function resolveOperatingSystem(platform: ClientPlatformType): OperatingSystemType {
switch (platform) {
case 'ios':
return 'ios'
case 'android':
return 'android'
case 'ohos':
case 'ohos_pc':
return 'harmonyos'
case 'windows':
return 'windows'
case 'mac':
return 'macos'
case 'devtools':
return 'devtools'
default:
return 'unknown'
}
}
function resolveDeviceClass(platform: ClientPlatformType): HardwareDeviceClassType {
if (MOBILE_CLIENT_PLATFORMS.includes(platform)) {
return 'mobile'
}
if (DESKTOP_CLIENT_PLATFORMS.includes(platform)) {
return 'desktop'
}
return platform === 'devtools' ? 'devtools' : 'unknown'
}
export function getPlatformInfo(): PlatformInfoType {
const deviceInfo = wx.getDeviceInfo()
const appBaseInfo = wx.getAppBaseInfo()
const rawClientPlatform = deviceInfo.platform || ''
const clientPlatform = normalizeClientPlatform(rawClientPlatform)
const deviceClass = resolveDeviceClass(clientPlatform)
return {
hardware: {
deviceClass,
brand: deviceInfo.brand || '',
model: deviceInfo.model || '',
abi: deviceInfo.abi || '',
deviceAbi: deviceInfo.deviceAbi || '',
cpuType: deviceInfo.cpuType || '',
memorySize: deviceInfo.memorySize || '',
},
software: {
clientPlatform,
rawClientPlatform,
operatingSystem: resolveOperatingSystem(clientPlatform),
system: deviceInfo.system || '',
wechatVersion: appBaseInfo.version || '',
baseLibraryVersion: appBaseInfo.SDKVersion || '',
hostAppId: appBaseInfo.host?.appId || '',
},
isMobile: deviceClass === 'mobile',
isDesktop: deviceClass === 'desktop',
isDevtools: deviceClass === 'devtools',
}
}
+5 -1
View File
@@ -38,7 +38,11 @@
"swc": true,
"disableSWC": false,
"disableUseStrict": false,
"urlCheck": false
"urlCheck": false,
"sassSetting": {
"sassCommonUseFilePath": "global.sass",
"scssCommonUseFilePath": "global.scss"
}
},
"simulatorType": "wechat",
"simulatorPluginLibVersion": {},
+38 -1
View File
@@ -1,6 +1,43 @@
/// <reference path="./types/index.d.ts" />
/// <reference path="./npm.d.ts" />
/** 微信小程序当前运行的客户端平台,对应 wx.getDeviceInfo().platform */
type ClientPlatformType = 'ios' | 'android' | 'ohos' | 'ohos_pc' | 'windows' | 'mac' | 'devtools' | 'unknown'
/** 根据客户端平台归一化后的操作系统类型 */
type OperatingSystemType = 'ios' | 'android' | 'harmonyos' | 'windows' | 'macos' | 'devtools' | 'unknown'
/** 根据客户端平台归类的硬件设备类别 */
type HardwareDeviceClassType = 'mobile' | 'desktop' | 'devtools' | 'unknown'
interface HardwarePlatformInfoType {
deviceClass: HardwareDeviceClassType
brand: string
model: string
abi: string
deviceAbi: string
cpuType: string
memorySize: string
}
interface SoftwarePlatformInfoType {
clientPlatform: ClientPlatformType
rawClientPlatform: string
operatingSystem: OperatingSystemType
system: string
wechatVersion: string
baseLibraryVersion: string
hostAppId: string
}
interface PlatformInfoType {
hardware: HardwarePlatformInfoType
software: SoftwarePlatformInfoType
isMobile: boolean
isDesktop: boolean
isDevtools: boolean
}
interface IAppOption {
globalData: {
navBarHeight: number,
@@ -12,7 +49,7 @@ interface IAppOption {
tabbarHeight: number,
safeAreaBottom: number,
safeAreaInsetBottom: number
system: string,
platformInfo: PlatformInfoType
token: string
userInfo?: WechatMiniprogram.UserInfo
}