Compare commits
9
Commits
8014dedf93
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f8b9d2f75 | ||
|
|
2516a3a36b | ||
|
|
a4a757b15b | ||
|
|
95f5652e9c | ||
|
|
dd061025e0 | ||
|
|
1f718c4ef8 | ||
|
|
bbff631f4f | ||
|
|
2937ce5e2d | ||
|
|
cb8dd623db |
+2
-2
@@ -14,9 +14,9 @@
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended"],
|
||||
"rules": {
|
||||
"no-console": "warn",
|
||||
"no-console": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"prefer-const": "warn",
|
||||
"no-undef": "off",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# 统一文本文件行尾为 LF(跨平台协作)
|
||||
* text=auto eol=lf
|
||||
|
||||
# 二进制文件不进行行尾转换
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ico binary
|
||||
*.gif binary
|
||||
*.svg text
|
||||
@@ -31,10 +31,9 @@ jobs:
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
run: npx jest --forceExit --maxWorkers=2 --no-cache
|
||||
- name: 'Run tests (coverage gate: lines >= 95%)'
|
||||
run: npx jest --forceExit --maxWorkers=2 --no-cache --coverage
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: debian-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: https://git.metona.cn/api/packages/MetonaTeam/npm/
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Type check
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Run tests
|
||||
run: npx jest --forceExit --maxWorkers=2 --no-cache --coverage
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Publish to Gitea npm registry
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
||||
@@ -1,5 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
.zcode/
|
||||
|
||||
@@ -2,6 +2,64 @@
|
||||
|
||||
All notable changes to MetonaToast will be documented in this file.
|
||||
|
||||
## [0.5.0] - 2026-08-08
|
||||
|
||||
### Changed
|
||||
- **覆盖率 96.42%(行)**:新增 59 个测试用例(共 385),覆盖 DOM 交互事件(click/drag/hover/animation)、confirm/prompt/action 按钮交互、Intl/localStorage 异常路径、React 渲染生命周期
|
||||
- **测试环境重构**:SSR 分支测试用 `Object.defineProperty` 真正覆盖 document/window(jsdom getter 使此前赋值无效);新增 `tests/dom.test.ts`(jsdom 真实 DOM 事件驱动)、`tests/setup.ts`(TextEncoder 补丁)
|
||||
- **jest 覆盖率门禁**:`lines >= 95%`,未达标测试即失败
|
||||
|
||||
### Fixed
|
||||
- **`Toast.off` 空数组残留**:移除全部钩子后删除 Map key(此前空数组残留导致 `_hooks.has()` 误报 true)
|
||||
- **`Toast.on` 返回的取消函数 this 绑定**:改用类名引用,任意调用上下文可用
|
||||
- **action 按钮 `close:false` 失效**:按钮 click 冒泡到 toast 的 closeOnClick 处理器导致总是关闭,已 stopPropagation 修复
|
||||
- **`use()` 重复注册钩子**:accessibility/persistence 预设的钩子注册改为防重复(先卸载旧钩子),并在插件 `uninstall` 中一并清理
|
||||
- **i18n catch 引用错误**:`catch {}` 中残留 `e` 引用导致的 ReferenceError(saveLocale/loadLocale/notifyLocaleListeners)
|
||||
- **删除残缺死代码**:`styles.autoApplySystemTheme`(无实际效果的占位实现)
|
||||
|
||||
### Removed
|
||||
- **lint 零警告**:`no-console` 规则关闭(库的错误报告依赖 console,生产构建已由 terser `drop_console` 剥离);catch 变量通过 `caughtErrorsIgnorePattern` 豁免
|
||||
|
||||
### Added
|
||||
- **Gitea Actions 发布自动化**:`publish.yml` — 推送 `v*` tag 时自动 测试+构建+发布到 Gitea npm registry(需配置 `PACKAGES_TOKEN` secret)
|
||||
- **CI 强化**:lint 改为必过(移除 continue-on-error);测试命令带覆盖率门禁
|
||||
- **CJS 产物兼容性修复**:包声明 `type: module` 下 `.js` 产物被 Node 按 ESM 解析导致 `require()` 失效,CJS 产物改 `.cjs` 扩展名并将 default 提升为 `module.exports`(`require()` 直接返回 MeToast,兼容 `.default`/`.MeToast`/`.Met`/`.Toast`/`.VERSION` 全部用法)
|
||||
|
||||
## [0.4.0] - 2026-08-08
|
||||
|
||||
### Added
|
||||
- **React 适配器子包**:`@metona-team/metona-toast/react` — `useToast()` hook(组件卸载时自动移除本组件创建的 Toast)+ 声明式 `<Toast />` 组件(props 变化更新、卸载自动清理)。主包保持零依赖,`react` 为 optional peerDependency
|
||||
- **`dedupe` 预设插件**:相同 type+message 的 Toast 自动去重(更新已有实例而非重复弹出),支持 uninstall
|
||||
- **`dragThreshold` 配置项**:拖拽关闭阈值可配置(默认 120px,原为硬编码)
|
||||
- **RTL 完整适配**:容器设置 `dir="rtl"`,进度条/side 条/类型色条/圆角正确镜像
|
||||
- **`update({ duration })` 支持**:动态变更显示时长并自动重启/停止计时器(此前 update 忽略 duration)
|
||||
- **幽灵实例修复**:`_emit` 检查 `create()` 结果,beforeShow 拦截后的 toast 不再注册到 `_toasts`(此前会残留幽灵记录,影响 count 与去重类插件)
|
||||
|
||||
### Changed
|
||||
- **loading 链式转换 id 稳定**:`loading.success/error/info/warning` 由"关闭旧 + 新建"改为原地 `update()` 同一实例,返回的 Toast id 与 loading id 一致;转换后 duration 自动恢复默认值(自动关闭)。注:转换不再产生新的入场动画,改为平滑更新;关闭按钮/进度条保持 loading 配置(如需默认外观,请通过 opts 覆盖)
|
||||
|
||||
## [0.3.0] - 2026-08-08
|
||||
|
||||
### Fixed
|
||||
- **自定义动画真实生效**:`animations.register()` 注册的动画此前仅写入内存 Map,实际渲染始终 fallback 到 slide。现在注册表驱动 `_buildClassName` 并动态注入对应 `@keyframes` + 类规则,README 的注册教程名实相符
|
||||
- **`max` 超限真正关闭最早的 toast**:此前超限时仅从内存 Map 删除记录,DOM 上的旧 toast 依然显示。现在通过实例注册表找到最早的 toast 并真正 `close()`(触发离场动画与生命周期回调)
|
||||
- **`destroy()` 全量清理**:新增清理 `Toast._hooks` 钩子、`Toast._registry` 实例注册表、`metona-toast-theme-css` / `metona-toast-custom-styles` / `metona-toast-anim-styles` 样式标签;动画注册表改为真正清空
|
||||
- **`applyThemeVariables` 重复注入**:修复多次调用追加相同 id 的 style 标签问题(改为先移除旧元素)
|
||||
- **`init()` 复活**:`destroy()` 后调用 `init()` 重置 `_destroyed` 标志,恢复可初始化状态
|
||||
- **失效脚本清理**:删除 `docs`(typedoc 未安装)、`example`(examples/ 目录不存在)脚本,新增 `serve` 脚本
|
||||
- **`AnimationConfig.enter/leave` 类型放宽**:支持 `opacity: 0` 等数值属性(此前仅 `string`,README 示例在 TS 严格模式下报错)
|
||||
|
||||
### Added
|
||||
- **钩子系统完整化**:此前 `HOOK_NAMES` 声明 20+ 钩子但多数永不触发。现在全部声明均有触发点:
|
||||
- 拦截语义:`beforeClose` / `beforeUpdate` 返回 `false` 可分别阻止关闭、更新
|
||||
- 新触发点:`click` / `hover` / `dragStart` / `dragEnd` / `animationStart` / `animationEnd` / `progressStart` / `progressEnd` / `configChange` / `themeChange` / `localeChange` / `beforeInit` / `afterInit` / `beforeDestroy` / `afterDestroy`
|
||||
- **`init({ plugins })` 支持插件对象**:与 `use()` 一致,可传 `string | Plugin`
|
||||
|
||||
### Changed
|
||||
- **VERSION 唯一来源**:版本号统一在 `src/constants.ts` 维护,`api.ts` / `index.ts` 引用,消除三处硬编码漂移
|
||||
- **`HOOK_NAMES` 收敛**:移除永不触发的 `progressUpdate` / `custom` 声明
|
||||
- 版本升级至 **v0.3.0**(含行为增强,按 semver minor 处理)
|
||||
|
||||
## [0.2.1] - 2026-07-25
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -5,18 +5,21 @@
|
||||
[](https://www.npmjs.com/package/@metona-team/metona-toast)
|
||||
[](https://bundlephobia.com/package/@metona-team/metona-toast)
|
||||
[](https://git.metona.cn/MetonaTeam/MetonaToast)
|
||||
[](https://git.metona.cn/MetonaTeam/MetonaToast)
|
||||
|
||||
## 特性
|
||||
|
||||
- **零依赖** — TypeScript 严格模式源码,gzip 后不到 10KB
|
||||
- **107 种图标类型** — success/error/warning/info/loading 及更多扩展类型,覆盖所有常见场景
|
||||
- **11 种动画** — slide/fade/scale/bounce/flip/rotate/zoom/slideUp/slideDown/slideLeft/slideRight,每种效果明显不同
|
||||
- **主题系统** — light/dark/auto/warm 四种内置主题,支持 `registerTheme()` 注册自定义主题
|
||||
- **国际化** — 内置 zh-CN / en-US 完整翻译(去重优化),可通过 `addTranslations()` 扩展
|
||||
- **插件系统** — 3 款预设插件 (keyboard/persistence/accessibility) + 自定义插件
|
||||
- **可拖拽关闭** — 拖动 Toast 任意方向即可关闭
|
||||
- **TypeScript** — 严格模式,源码级类型安全,类型从源码自动生成无手动维护
|
||||
- **全局错误回调** — `onError` 捕获所有钩子异常和定时器错误,方便接入监控系统
|
||||
- **零依赖** — TypeScript 严格模式源码,gzip 后不到 10KB,无任何运行时依赖
|
||||
- **107 种图标类型** — success/error/warning/info/loading 及 100+ 扩展类型,覆盖所有常见场景
|
||||
- **11 种动画 + 自定义** — slide/fade/scale/bounce/flip/rotate/zoom/slideUp/slideDown/slideLeft/slideRight,`animations.register()` 可注册任意自定义动画(动态注入 keyframes,真实生效)
|
||||
- **主题系统** — light/dark/auto/warm 四种内置主题,`registerTheme()` 注册自定义主题,跟随系统主题自动切换
|
||||
- **国际化** — 内置 zh-CN / en-US 完整翻译,`addTranslations()` 扩展任意语言,RTL 语言完整适配
|
||||
- **插件系统** — 4 款预设插件 (keyboard / persistence / accessibility / dedupe) + 自定义插件
|
||||
- **完整钩子系统** — 17 个生命周期与事件钩子,`beforeShow` / `beforeClose` / `beforeUpdate` 支持返回 `false` 拦截
|
||||
- **可拖拽关闭** — 拖动 Toast 任意方向即可关闭,阈值 `dragThreshold` 可配置
|
||||
- **React 适配器** — `@metona-team/metona-toast/react` 子路径导入,`useToast()` hook + 声明式 `<Toast />` 组件,主包保持零依赖
|
||||
- **全局错误回调** — `onError` 捕获所有钩子异常和定时器错误,方便接入 Sentry、DataDog 等监控系统
|
||||
- **工程质量** — 385 个测试全过,行覆盖率 96.42%,lint 零警告,CI 含覆盖率门禁(≥95%)
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -59,13 +62,13 @@ const MeToast = require('@metona-team/metona-toast');
|
||||
| iOS Safari | 11+ |
|
||||
| Android Chrome | 64+ |
|
||||
| Node.js | 16+(ESM & CJS 双格式) |
|
||||
| TypeScript | 4.5+(完整类型声明) |
|
||||
| TypeScript | 4.5+(完整类型声明,类型从源码自动生成) |
|
||||
|
||||
核心依赖:Web Animations API(Chrome 64+, Firefox 63+, Safari 11+)、Pointer Events、CSS Grid、CSS Custom Properties。
|
||||
|
||||
## 快速开始
|
||||
|
||||
浏览器中 `window` 上会注册两个全局变量:`MeToast` 和 `Met`,两者完全等价,任选其一使用。
|
||||
浏览器 `window` 上注册两个全局变量:`MeToast` 和 `Met`,完全等价。
|
||||
|
||||
```javascript
|
||||
import MeToast from '@metona-team/metona-toast';
|
||||
@@ -82,11 +85,11 @@ MeToast.info('系统维护中');
|
||||
// 带标题
|
||||
MeToast.success({ title: '保存成功', message: '文件已同步到云端' });
|
||||
|
||||
// 加载 → 成功链式转换
|
||||
// 加载 → 链式转换(原地更新同一实例,id 稳定)
|
||||
const loading = MeToast.loading('正在提交...');
|
||||
setTimeout(() => loading.success('提交成功!'), 2000);
|
||||
|
||||
// Promise 风格
|
||||
// Promise 风格 — 自动 loading → success/error
|
||||
await MeToast.promise(fetch('/api/data'), {
|
||||
loading: '加载中...',
|
||||
success: '加载完成!',
|
||||
@@ -100,15 +103,22 @@ await MeToast.promise(fetch('/api/data'), {
|
||||
MeToast.configure({
|
||||
position: 'top-right', // top-left | top-center | top-right | bottom-left | bottom-center | bottom-right
|
||||
duration: 4000, // 显示时长(ms),0 为不自动关闭
|
||||
max: 6, // 同时最多显示条数
|
||||
theme: 'auto', // light | dark | auto | warm
|
||||
animation: 'slide', // slide | fade | scale | bounce | flip | rotate | zoom | slideUp | slideDown | slideLeft | slideRight
|
||||
max: 6, // 同一位置最多显示条数,超出关闭最早的
|
||||
gap: 12, // Toast 间距(px)
|
||||
offset: 24, // 容器距屏幕边缘(px)
|
||||
theme: 'auto', // light | dark | auto(跟随系统)| warm
|
||||
animation: 'slide', // 11 种内置动画或自定义动画名
|
||||
pauseOnHover: true, // 悬停暂停计时
|
||||
closeOnClick: true, // 点击关闭
|
||||
showProgress: true, // 显示进度条
|
||||
draggable: true, // 允许拖拽关闭
|
||||
dragThreshold: 120, // 拖拽关闭阈值(px)
|
||||
showProgress: true, // 显示进度条
|
||||
progressDirection: 'horizontal', // horizontal | vertical
|
||||
zIndex: 9999,
|
||||
width: 360,
|
||||
locale: 'zh-CN', // 语言
|
||||
onError: ({ hook, error, toast }) => { // 全局错误回调(钩子/定时器异常)
|
||||
onError: ({ hook, source, error, toast }) => {
|
||||
// 钩子/定时器异常全局捕获,可接入监控系统
|
||||
console.error('Toast error:', hook, error);
|
||||
},
|
||||
});
|
||||
@@ -116,40 +126,42 @@ MeToast.configure({
|
||||
|
||||
## 动画效果
|
||||
|
||||
11 种动画,每种感官差异明显:
|
||||
11 种内置动画,每种感官差异明显:
|
||||
|
||||
| 动画 | 效果描述 |
|
||||
|------|----------|
|
||||
| `slide` | 从右侧滑入 + 轻微过冲回弹 |
|
||||
| `fade` | 纯淡入(blur→清晰),从容优雅 |
|
||||
| `scale` | 弹性放大(0.55→1.07→1.0) |
|
||||
| `bounce` | 从天而降四段弹跳 |
|
||||
| `flip` | 3D 翻转入场 + 回摆 |
|
||||
| `rotate` | 旋转摇摆进入 |
|
||||
| `zoom` | 从中心爆发式弹出 |
|
||||
| `slideUp` | 从下方弹入 |
|
||||
| `slideDown` | 从上方弹入 |
|
||||
| `slideLeft` | 从左侧滑入 |
|
||||
| `slideRight` | 从右侧滑入 |
|
||||
| 动画 | 效果描述 | 时长 |
|
||||
|------|----------|------|
|
||||
| `slide` | 从右侧滑入 + 轻微过冲回弹 | 400ms |
|
||||
| `fade` | 纯淡入(blur→清晰),从容优雅 | 500ms |
|
||||
| `scale` | 弹性放大(0.55→1.07→1.0) | 450ms |
|
||||
| `bounce` | 从天而降四段弹跳 | 650ms |
|
||||
| `flip` | 3D 翻转入场 + 回摆 | 500ms |
|
||||
| `rotate` | 旋转摇摆进入 | 500ms |
|
||||
| `zoom` | 从中心爆发式弹出 | 500ms |
|
||||
| `slideUp` | 从下方弹入 | 400ms |
|
||||
| `slideDown` | 从上方弹入 | 400ms |
|
||||
| `slideLeft` | 从左侧滑入 | 400ms |
|
||||
| `slideRight` | 从右侧滑入 | 400ms |
|
||||
|
||||
```javascript
|
||||
MeToast.success('弹跳动画', { animation: 'bounce' });
|
||||
|
||||
// 注册自定义动画
|
||||
// 注册自定义动画 — 动态注入 keyframes,真实生效
|
||||
MeToast.animations.register('myAnim', {
|
||||
enter: { transform: 'rotate(-30deg) scale(0.5)', opacity: 0 },
|
||||
leave: { transform: 'rotate(0) scale(1)', opacity: 1 },
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
});
|
||||
MeToast.success('自定义动画', { animation: 'myAnim' });
|
||||
```
|
||||
|
||||
## 主题
|
||||
|
||||
```javascript
|
||||
// 切换主题
|
||||
// 切换主题(自动持久化到 localStorage)
|
||||
MeToast.themes.switchTheme('dark');
|
||||
MeToast.themes.switchTheme('warm');
|
||||
MeToast.themes.toggleTheme(); // light ↔ dark
|
||||
|
||||
// 注册自定义主题
|
||||
MeToast.themes.registerTheme('ocean', {
|
||||
@@ -188,6 +200,18 @@ await MeToast.queue(['步骤一', '步骤二', '步骤三'], { delay: 800 });
|
||||
|
||||
// 堆叠展示(同时错峰)
|
||||
MeToast.stack(['消息1', '消息2', '消息3'], { stagger: 150 });
|
||||
|
||||
// Action Toast — 内嵌操作按钮(close:false 可多次点击不关闭)
|
||||
MeToast.action('文件已删除', [
|
||||
{ text: '撤销', onClick: () => restore(), color: '#3b82f6' },
|
||||
{ text: '查看', onClick: () => open(), color: '#10b981', close: false },
|
||||
]);
|
||||
|
||||
// 分组管理
|
||||
const orders = MeToast.group('orders');
|
||||
orders.success('订单已创建');
|
||||
orders.count(); // 该组 toast 数量
|
||||
orders.dismiss(); // 一键关闭整组
|
||||
```
|
||||
|
||||
## 插件
|
||||
@@ -197,8 +221,9 @@ MeToast.stack(['消息1', '消息2', '消息3'], { stagger: 150 });
|
||||
MeToast.use('keyboard'); // ESC 关闭所有 Toast
|
||||
MeToast.use('persistence'); // 配置持久化到 localStorage
|
||||
MeToast.use('accessibility'); // 屏幕阅读器朗读公告
|
||||
MeToast.use('dedupe'); // 相同 type+message 自动去重(更新已有实例)
|
||||
|
||||
// 自定义插件(通过 install 中注册 Toast 生命周期钩子)
|
||||
// 自定义插件(install 中注册生命周期钩子)
|
||||
import MeToast, { Toast } from '@metona-team/metona-toast';
|
||||
|
||||
MeToast.use({
|
||||
@@ -208,6 +233,36 @@ MeToast.use({
|
||||
console.log('Toast shown:', toast.message);
|
||||
});
|
||||
},
|
||||
uninstall() {
|
||||
console.log('plugin removed');
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 钩子系统
|
||||
|
||||
`Toast.on(name, handler)` 注册钩子,返回取消函数。`beforeShow` / `beforeClose` / `beforeUpdate` 的 handler 返回 `false` 可拦截对应操作。
|
||||
|
||||
| 钩子 | 触发时机 | 拦截 |
|
||||
|------|----------|------|
|
||||
| `beforeInit` / `afterInit` | `init()` 前后 | — |
|
||||
| `beforeDestroy` / `afterDestroy` | `destroy()` 前后 | — |
|
||||
| `beforeShow` / `afterShow` | Toast 显示前后 | ✅ 返回 false 阻止显示 |
|
||||
| `beforeClose` / `afterClose` | Toast 关闭前后 | ✅ 返回 false 阻止关闭 |
|
||||
| `beforeUpdate` / `afterUpdate` | `update()` 前后 | ✅ 返回 false 阻止更新 |
|
||||
| `configChange` | `configure`/`updateConfig`/`resetConfig` | — |
|
||||
| `themeChange` | 主题切换 | — |
|
||||
| `localeChange` | 语言切换 | — |
|
||||
| `click` | 点击 Toast 主体 | — |
|
||||
| `hover` | 鼠标悬停进出 | — |
|
||||
| `dragStart` / `dragEnd` | 拖拽开始/结束 | — |
|
||||
| `animationStart` / `animationEnd` | 入场动画开始/结束 | — |
|
||||
| `progressStart` / `progressEnd` | 倒计时开始/归零 | — |
|
||||
|
||||
```javascript
|
||||
// 拦截示例:指定类型禁止显示
|
||||
Toast.on('beforeShow', (toast) => {
|
||||
if (toast.type === 'error' && !isAllowed) return false;
|
||||
});
|
||||
```
|
||||
|
||||
@@ -216,7 +271,7 @@ MeToast.use({
|
||||
```javascript
|
||||
MeToast.i18n.switchLocale('en-US');
|
||||
|
||||
// 添加语言
|
||||
// 添加语言(嵌套结构支持深度合并)
|
||||
MeToast.i18n.addTranslations('ja', {
|
||||
success: '成功',
|
||||
error: 'エラー',
|
||||
@@ -224,9 +279,11 @@ MeToast.i18n.addTranslations('ja', {
|
||||
cancel: 'キャンセル',
|
||||
});
|
||||
|
||||
// 格式化工具
|
||||
// 格式化工具(基于 Intl)
|
||||
MeToast.i18n.formatNumber(1234567); // "1,234,567"
|
||||
MeToast.i18n.formatCurrency(99, 'CNY'); // "¥99.00"
|
||||
MeToast.i18n.formatDate(new Date());
|
||||
MeToast.i18n.formatRelativeTime(Date.now() + 3600000); // "1 小时后"
|
||||
```
|
||||
|
||||
## Toast 管理
|
||||
@@ -235,11 +292,17 @@ MeToast.i18n.formatCurrency(99, 'CNY'); // "¥99.00"
|
||||
MeToast.count(); // 当前数量
|
||||
MeToast.getAll(); // 获取原生 Map<id, ToastInstance>
|
||||
MeToast.getToasts(); // 获取所有实例数组
|
||||
MeToast.find(id); // 查找实例(不存在返回 undefined)
|
||||
MeToast.findByType(type); // 按类型过滤
|
||||
MeToast.findByPosition(pos);// 按位置过滤
|
||||
MeToast.dismiss(); // 关闭所有
|
||||
MeToast.dismiss(id); // 关闭指定
|
||||
MeToast.removeToast(id); // 立即移除(无离场动画)
|
||||
MeToast.clear(position?); // 按位置清除
|
||||
MeToast.pauseAll(); // 暂停所有计时
|
||||
MeToast.resumeAll(); // 恢复所有计时
|
||||
MeToast.destroy(); // 完全销毁(支持重复调用)
|
||||
MeToast.updateAll(partial); // 批量更新
|
||||
MeToast.destroy(); // 完全销毁(支持重复调用,init() 可恢复)
|
||||
```
|
||||
|
||||
## 回调
|
||||
@@ -247,6 +310,7 @@ MeToast.destroy(); // 完全销毁(支持重复调用)
|
||||
```javascript
|
||||
MeToast.success({
|
||||
message: '操作成功',
|
||||
onBeforeShow: (toast) => true, // 返回 false 阻止显示
|
||||
onShow: (toast) => console.log('显示:', toast.id),
|
||||
onClose: (toast) => console.log('关闭:', toast.id),
|
||||
onClick: (toast) => console.log('点击:', toast.id),
|
||||
@@ -256,12 +320,53 @@ MeToast.success({
|
||||
// 全局错误回调 — 捕获钩子异常和定时器错误
|
||||
MeToast.configure({
|
||||
onError: ({ hook, source, error, toast }) => {
|
||||
// 接入监控系统 (Sentry, DataDog 等)
|
||||
reportError(error, { hook, source, toastId: toast?.id });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## React 适配器
|
||||
|
||||
主包保持零依赖;React 适配器通过子路径导入,`react` 为 optional peerDependency,未使用 React 的项目不受影响。
|
||||
|
||||
```bash
|
||||
npm install @metona-team/metona-toast react
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { useToast, Toast } from '@metona-team/metona-toast/react';
|
||||
|
||||
function SubmitButton() {
|
||||
// 本组件创建的 Toast 会在组件卸载时自动移除
|
||||
const toast = useToast();
|
||||
|
||||
const submit = async () => {
|
||||
const loading = toast.loading('正在提交...');
|
||||
try {
|
||||
await api.submit();
|
||||
loading.success('提交成功!');
|
||||
} catch (e) {
|
||||
loading.error('提交失败');
|
||||
}
|
||||
};
|
||||
|
||||
return <button onClick={submit}>提交</button>;
|
||||
}
|
||||
|
||||
// 声明式 Toast — props 变化时更新内容,组件卸载时自动移除(autoClose 默认 true)
|
||||
function SaveIndicator({ saving }: { saving: boolean }) {
|
||||
return saving ? <Toast type="info" message="正在保存..." /> : null;
|
||||
}
|
||||
```
|
||||
|
||||
## 工程质量
|
||||
|
||||
- **385 个测试** 全部通过(单元 + DOM 交互 + React 渲染 + SSR 环境隔离)
|
||||
- **行覆盖率 96.42%**,jest 覆盖率门禁 `lines >= 95%`,未达标构建即失败
|
||||
- **lint 零警告**(TypeScript ESLint 严格检查)
|
||||
- **CI**(Gitea Actions):Node 18/20/22/24 矩阵 — typecheck → lint → 测试+覆盖率门禁 → 构建 → bundle size 检查
|
||||
- **自动发布**:推送 `v*` tag 触发测试、构建并发布到 Gitea npm registry
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
Vendored
+4536
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+563
@@ -0,0 +1,563 @@
|
||||
/**
|
||||
* MetonaToast — 核心类型定义
|
||||
* @module types
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/** Toast 通知类型(107种内置图标类型) */
|
||||
type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading' | 'check' | 'x' | 'alert' | 'question' | 'star' | 'heart' | 'bell' | 'mail' | 'settings' | 'user' | 'home' | 'search' | 'plus' | 'minus' | 'edit' | 'trash' | 'download' | 'upload' | 'share' | 'link' | 'external' | 'clock' | 'calendar' | 'map' | 'compass' | 'globe' | 'wifi' | 'cloud' | 'sun' | 'moon' | 'zap' | 'activity' | 'cpu' | 'database' | 'server' | 'terminal' | 'code' | 'git' | 'package' | 'layers' | 'grid' | 'list' | 'filter' | 'sort' | 'refresh' | 'sync' | 'power' | 'battery' | 'bluetooth' | 'volume' | 'mic' | 'camera' | 'image' | 'video' | 'music' | 'file' | 'folder' | 'clipboard' | 'save' | 'print' | 'eye' | 'eyeOff' | 'lock' | 'unlock' | 'shield' | 'key' | 'flag' | 'bookmark' | 'tag' | 'gift' | 'award' | 'target' | 'crosshair' | 'move' | 'maximize' | 'minimize' | 'copy' | 'cut' | 'paste' | 'rotateCw' | 'rotateCcw' | 'zoomIn' | 'zoomOut' | 'crop' | 'sliders' | 'toggleLeft' | 'toggleRight' | 'checkCircle' | 'xCircle' | 'alertCircle' | 'infoCircle' | 'helpCircle' | 'alertTriangle' | 'checkSquare' | 'square' | 'circle' | 'triangle' | 'hexagon' | 'octagon' | 'pentagon' | 'diamond';
|
||||
/** Toast 显示位置 */
|
||||
type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
||||
/** Toast 主题 */
|
||||
type ToastTheme = 'light' | 'dark' | 'auto' | 'warm' | (string & {});
|
||||
/** Toast 动画类型 */
|
||||
type ToastAnimation = 'slide' | 'fade' | 'scale' | 'bounce' | 'flip' | 'rotate' | 'zoom' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | (string & {});
|
||||
/** 进度条方向 */
|
||||
type ToastProgressDirection = 'horizontal' | 'vertical';
|
||||
interface TypeColor {
|
||||
fg: string;
|
||||
bg: string;
|
||||
}
|
||||
interface ToastConfig {
|
||||
position?: ToastPosition;
|
||||
duration?: number;
|
||||
max?: number;
|
||||
gap?: number;
|
||||
offset?: number;
|
||||
pauseOnHover?: boolean;
|
||||
closeOnClick?: boolean;
|
||||
draggable?: boolean;
|
||||
dragThreshold?: number;
|
||||
showProgress?: boolean;
|
||||
progressDirection?: ToastProgressDirection;
|
||||
icon?: boolean;
|
||||
closeButton?: boolean;
|
||||
theme?: ToastTheme;
|
||||
animation?: ToastAnimation;
|
||||
zIndex?: number;
|
||||
width?: number | string;
|
||||
className?: string;
|
||||
style?: Record<string, string>;
|
||||
onShow?: ((toast: ToastInstance) => void) | null;
|
||||
onBeforeShow?: ((toast: ToastInstance) => boolean | void) | null;
|
||||
onClose?: ((toast: ToastInstance) => void) | null;
|
||||
onClick?: ((toast: ToastInstance) => void) | null;
|
||||
onUpdate?: ((toast: ToastInstance) => void) | null;
|
||||
render?: ((toast: ToastInstance) => string) | null;
|
||||
resetTimerOnUpdate?: boolean;
|
||||
notifyWhenHidden?: boolean;
|
||||
onError?: ((errorInfo: ErrorInfo) => void) | null;
|
||||
group?: string | null;
|
||||
locale?: string;
|
||||
plugins?: Array<string | Plugin>;
|
||||
}
|
||||
interface ErrorInfo {
|
||||
hook?: string;
|
||||
source?: string;
|
||||
error: Error;
|
||||
toast?: ToastInstance;
|
||||
}
|
||||
interface ToastOptions extends Partial<ToastConfig> {
|
||||
type?: string;
|
||||
title?: string;
|
||||
message?: string;
|
||||
content?: string;
|
||||
html?: string;
|
||||
iconHTML?: string;
|
||||
id?: string;
|
||||
}
|
||||
interface ToastInstance {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
message: string;
|
||||
html: string;
|
||||
iconHTML: string;
|
||||
config: ToastConfig;
|
||||
el: HTMLElement | null;
|
||||
barEl: HTMLElement | null;
|
||||
rafId: number | null;
|
||||
remaining: number;
|
||||
startedAt: number;
|
||||
paused: boolean;
|
||||
closing: boolean;
|
||||
group: string | null;
|
||||
_cleanups: Array<() => void>;
|
||||
_palette(): {
|
||||
theme: string;
|
||||
c: TypeColor;
|
||||
t: Partial<ThemeConfig>;
|
||||
};
|
||||
create(): this;
|
||||
update(partial: Partial<ToastOptions>): this;
|
||||
updatePosition(position: string): this;
|
||||
remove(): void;
|
||||
close(immediate?: boolean): void;
|
||||
_pause(): void;
|
||||
_resume(): void;
|
||||
_destroy(): void;
|
||||
}
|
||||
interface LoadingControl {
|
||||
id: string;
|
||||
success(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
error(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
info(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
warning(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
update(partial: Partial<ToastOptions>): unknown;
|
||||
dismiss(): void;
|
||||
}
|
||||
interface ProgressControl {
|
||||
id: string;
|
||||
setProgress(percent: number): void;
|
||||
complete(message?: string): void;
|
||||
error(message?: string): void;
|
||||
dismiss(): void;
|
||||
}
|
||||
interface CountdownControl {
|
||||
id: string;
|
||||
cancel(): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
}
|
||||
interface ActionControl {
|
||||
id: string;
|
||||
toast: ToastInstance;
|
||||
dismiss(): void;
|
||||
}
|
||||
interface QueueControl {
|
||||
then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
||||
catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<void | TResult>;
|
||||
cancel(): void;
|
||||
}
|
||||
interface GroupAPI {
|
||||
_group: string;
|
||||
show: (...args: unknown[]) => ToastInstance;
|
||||
success: (...args: unknown[]) => ToastInstance;
|
||||
error: (...args: unknown[]) => ToastInstance;
|
||||
warning: (...args: unknown[]) => ToastInstance;
|
||||
info: (...args: unknown[]) => ToastInstance;
|
||||
loading: (...args: unknown[]) => LoadingControl;
|
||||
action: (...args: unknown[]) => ActionControl;
|
||||
dismiss(): void;
|
||||
count(): number;
|
||||
}
|
||||
interface ActionButton {
|
||||
text: string;
|
||||
onClick: (toast: ToastInstance) => void;
|
||||
color?: string;
|
||||
style?: Record<string, string>;
|
||||
close?: boolean;
|
||||
}
|
||||
interface PromiseOptions extends ToastOptions {
|
||||
loading?: string;
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
interface ConfirmOptions extends ToastOptions {
|
||||
confirmText?: string;
|
||||
confirmColor?: string;
|
||||
cancelText?: string;
|
||||
cancelColor?: string;
|
||||
}
|
||||
interface PromptOptions extends ToastOptions {
|
||||
inputType?: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
submitText?: string;
|
||||
submitColor?: string;
|
||||
cancelText?: string;
|
||||
cancelColor?: string;
|
||||
}
|
||||
interface ProgressOptions extends ToastOptions {
|
||||
progressColor?: string;
|
||||
}
|
||||
interface CountdownOptions extends ToastOptions {
|
||||
onComplete?: (() => void) | null;
|
||||
}
|
||||
interface QueueOptions extends ToastOptions {
|
||||
delay?: number;
|
||||
}
|
||||
interface StackOptions extends ToastOptions {
|
||||
stagger?: number;
|
||||
}
|
||||
interface InitOptions {
|
||||
config?: Partial<ToastConfig>;
|
||||
theme?: ToastTheme;
|
||||
locale?: string;
|
||||
plugins?: Array<string | Plugin>;
|
||||
}
|
||||
interface StatusInfo {
|
||||
version: string;
|
||||
toasts: number;
|
||||
theme: string;
|
||||
locale: string;
|
||||
plugins: string[];
|
||||
animations: number;
|
||||
}
|
||||
interface AnimationConfig {
|
||||
enter: Record<string, string | number>;
|
||||
leave: Record<string, string | number>;
|
||||
duration: number;
|
||||
easing: string;
|
||||
name?: string;
|
||||
delay?: number;
|
||||
iterations?: number;
|
||||
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
|
||||
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
|
||||
}
|
||||
interface AnimationUtils {
|
||||
register(name: string, config: Partial<AnimationConfig>): void;
|
||||
unregister(name: string): void;
|
||||
get(name: string): AnimationConfig | null;
|
||||
getAnimationNames(): string[];
|
||||
getActiveCount(): number;
|
||||
cancelAll(): void;
|
||||
reset(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
interface ThemeConfig {
|
||||
bg: string;
|
||||
text: string;
|
||||
border: string;
|
||||
shadow: string;
|
||||
hoverShadow: string;
|
||||
progressBg: string;
|
||||
closeHoverBg: string;
|
||||
backdropFilter?: string;
|
||||
}
|
||||
interface ThemePreview {
|
||||
name: string;
|
||||
resolved: string;
|
||||
colors: {
|
||||
background: string;
|
||||
text: string;
|
||||
border: string;
|
||||
};
|
||||
isDark: boolean;
|
||||
isLight: boolean;
|
||||
isAuto: boolean;
|
||||
}
|
||||
interface ThemeUtils {
|
||||
getSystemTheme(): 'light' | 'dark';
|
||||
resolveTheme(theme: string): string;
|
||||
getThemeConfig(theme: string): ThemeConfig;
|
||||
applyTheme(theme: string): void;
|
||||
getCurrentTheme(): string;
|
||||
getResolvedTheme(): string;
|
||||
switchTheme(theme: string): void;
|
||||
toggleTheme(): void;
|
||||
resetToAuto(): void;
|
||||
initTheme(): void;
|
||||
watchSystemTheme(): void;
|
||||
unwatchSystemTheme(): void;
|
||||
addThemeListener(listener: (theme: string, resolved: string) => void): () => void;
|
||||
removeThemeListener(listener: (theme: string, resolved: string) => void): void;
|
||||
clearThemeListeners(): void;
|
||||
registerTheme(name: string, config: ThemeConfig): void;
|
||||
unregisterTheme(name: string): void;
|
||||
getAllThemes(): Record<string, ThemeConfig>;
|
||||
getThemeNames(): string[];
|
||||
hasTheme(name: string): boolean;
|
||||
getThemePreview(theme: string): ThemePreview;
|
||||
generateThemeCSS(theme: string): string;
|
||||
applyThemeCSS(theme: string): void;
|
||||
removeThemeCSS(): void;
|
||||
saveTheme(theme: string): void;
|
||||
loadTheme(): string;
|
||||
}
|
||||
interface LocaleInfo {
|
||||
code: string;
|
||||
name: string;
|
||||
direction: 'ltr' | 'rtl';
|
||||
isSupported: boolean;
|
||||
isCurrent: boolean;
|
||||
isFallback: boolean;
|
||||
}
|
||||
interface I18nUtils {
|
||||
t(key: string, params?: Record<string, string | number>): string;
|
||||
plural(key: string, count: number, params?: Record<string, string | number>): string;
|
||||
getCurrentLocale(): string;
|
||||
setCurrentLocale(locale: string): void;
|
||||
switchLocale(locale: string): void;
|
||||
getFallbackLocale(): string;
|
||||
setFallbackLocale(locale: string): void;
|
||||
hasTranslation(key: string): boolean;
|
||||
getTranslations(locale: string): Record<string, unknown>;
|
||||
addTranslations(locale: string, translations: Record<string, unknown>): void;
|
||||
removeTranslation(locale: string, key: string): void;
|
||||
clearTranslations(locale: string): void;
|
||||
getSupportedLocales(): string[];
|
||||
isLocaleSupported(locale: string): boolean;
|
||||
getLocaleName(locale: string): string;
|
||||
getLocaleDirection(locale: string): 'ltr' | 'rtl';
|
||||
getLocaleInfo(locale: string): LocaleInfo;
|
||||
getAllLocaleInfo(): LocaleInfo[];
|
||||
formatNumber(number: number, options?: Intl.NumberFormatOptions): string;
|
||||
formatCurrency(amount: number, currency?: string, options?: Intl.NumberFormatOptions): string;
|
||||
formatPercent(value: number, options?: Intl.NumberFormatOptions): string;
|
||||
formatDate(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
|
||||
formatTime(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
|
||||
formatRelativeTime(date: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string;
|
||||
formatList(list: string[], options?: Record<string, unknown>): string;
|
||||
formatPlural(count: number, options?: Intl.PluralRulesOptions): string;
|
||||
addLocaleListener(listener: (locale: string) => void): () => void;
|
||||
removeLocaleListener(listener: (locale: string) => void): void;
|
||||
clearLocaleListeners(): void;
|
||||
initI18n(): void;
|
||||
saveLocale(locale: string): void;
|
||||
loadLocale(): string;
|
||||
getDefaultLocale(): string;
|
||||
}
|
||||
interface Plugin {
|
||||
name: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
hooks?: Record<string, (...args: unknown[]) => void>;
|
||||
middleware?: (...args: unknown[]) => unknown;
|
||||
install?: (manager: PluginManager) => void | Record<string, unknown> | null;
|
||||
uninstall?: (manager: PluginManager) => void;
|
||||
init?: (manager: PluginManager) => void;
|
||||
destroy?: (manager: PluginManager) => void;
|
||||
installed?: boolean;
|
||||
enabled?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
interface PluginManager {
|
||||
register(name: string, plugin: Plugin): PluginManager;
|
||||
unregister(name: string): PluginManager;
|
||||
get(name: string): Plugin | null;
|
||||
has(name: string): boolean;
|
||||
getAll(): Plugin[];
|
||||
getNames(): string[];
|
||||
enable(name: string): PluginManager;
|
||||
disable(name: string): PluginManager;
|
||||
isEnabled(name: string): boolean;
|
||||
destroy(): void;
|
||||
}
|
||||
interface PluginUtils {
|
||||
createManager(): PluginManager;
|
||||
register(name: string, plugin: Plugin): PluginManager;
|
||||
unregister(name: string): PluginManager;
|
||||
get(name: string): Plugin | null;
|
||||
has(name: string): boolean;
|
||||
getAll(): Plugin[];
|
||||
getNames(): string[];
|
||||
enable(name: string): PluginManager;
|
||||
disable(name: string): PluginManager;
|
||||
isEnabled(name: string): boolean;
|
||||
getPreset(name: string): Plugin | null;
|
||||
getAllPresets(): Record<string, Plugin>;
|
||||
createPlugin(config: Partial<Plugin>): Plugin;
|
||||
validatePlugin(plugin: Partial<Plugin>): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
};
|
||||
}
|
||||
interface MeToast {
|
||||
version: string;
|
||||
_toasts: Map<string, ToastInstance>;
|
||||
_config: ToastConfig;
|
||||
_destroyed?: boolean;
|
||||
_emit(opts: ToastOptions): ToastInstance;
|
||||
_remove(id: string): void;
|
||||
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
|
||||
_groupCount(name: string): number;
|
||||
configure(opts: Partial<ToastConfig>): MeToast;
|
||||
init(options?: InitOptions): MeToast;
|
||||
destroy(): void;
|
||||
getStatus(): StatusInfo;
|
||||
getConfig(): ToastConfig;
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast;
|
||||
resetConfig(): MeToast;
|
||||
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
|
||||
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
|
||||
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
|
||||
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
|
||||
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
|
||||
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
|
||||
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
|
||||
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
|
||||
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
|
||||
group(name: string): GroupAPI;
|
||||
dismissGroup(name: string): void;
|
||||
find(id: string): ToastInstance | undefined;
|
||||
dismiss(id?: string): void;
|
||||
removeToast(id: string): void;
|
||||
clear(position?: ToastPosition): void;
|
||||
getToasts(): ToastInstance[];
|
||||
count(): number;
|
||||
hasToasts(): boolean;
|
||||
getToast(id: string): ToastInstance | null;
|
||||
closeAll(): void;
|
||||
clearAll(): void;
|
||||
pauseAll(): void;
|
||||
resumeAll(): void;
|
||||
updateAll(partial: Partial<ToastOptions>): void;
|
||||
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
|
||||
findByType(type: ToastType): ToastInstance[];
|
||||
findByPosition(position: ToastPosition): ToastInstance[];
|
||||
getAll(): Map<string, ToastInstance>;
|
||||
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
|
||||
animations: AnimationUtils;
|
||||
themes: ThemeUtils;
|
||||
i18n: I18nUtils;
|
||||
plugins: PluginUtils;
|
||||
presetPlugins: Record<string, Plugin>;
|
||||
}
|
||||
declare global {
|
||||
interface Window {
|
||||
MeToast: MeToast;
|
||||
Met: MeToast;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Toast 类 — 核心通知组件
|
||||
*/
|
||||
declare class Toast implements ToastInstance {
|
||||
static _hooks: Map<string, Array<(toast: Toast) => boolean | void>>;
|
||||
static _registry: Map<string, Toast>;
|
||||
private static _onError;
|
||||
private static _removeToast;
|
||||
/**
|
||||
* 设置 Toast 类级别的回调函数
|
||||
*/
|
||||
static setCallbacks(callbacks: {
|
||||
onError?: ((errorInfo: ErrorInfo) => void) | null;
|
||||
removeToast?: ((id: string) => void) | null;
|
||||
}): void;
|
||||
static on(name: string, fn: (toast: Toast) => boolean | void): () => void;
|
||||
static off(name: string, fn: (toast: Toast) => boolean | void): void;
|
||||
/**
|
||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
|
||||
*/
|
||||
static trigger(name: string, toast?: Toast | null): boolean;
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
message: string;
|
||||
html: string;
|
||||
iconHTML: string;
|
||||
config: ToastConfig;
|
||||
el: HTMLElement | null;
|
||||
barEl: HTMLElement | null;
|
||||
rafId: number | null;
|
||||
remaining: number;
|
||||
startedAt: number;
|
||||
paused: boolean;
|
||||
closing: boolean;
|
||||
group: string | null;
|
||||
_cleanups: Array<() => void>;
|
||||
constructor(opts: ToastOptions);
|
||||
_palette(): {
|
||||
theme: string;
|
||||
c: TypeColor;
|
||||
t: Partial<ThemeConfig>;
|
||||
};
|
||||
create(): this;
|
||||
_getContainer(): HTMLElement;
|
||||
_limitToasts(container: HTMLElement): void;
|
||||
_buildClassName(theme: string): string;
|
||||
_applyStyles(el: HTMLElement, _theme: string, _t: Partial<ThemeConfig>): void;
|
||||
_buildContent(el: HTMLElement, c: TypeColor): void;
|
||||
_bindEvents(el: HTMLElement): void;
|
||||
_bindDrag(el: HTMLElement): void;
|
||||
_startTimer(resuming?: boolean): void;
|
||||
_pause(): void;
|
||||
_resume(): void;
|
||||
update(partial: Partial<ToastOptions>): this;
|
||||
/**
|
||||
* 运行时移动 Toast 到新位置
|
||||
*/
|
||||
updatePosition(position: string): this;
|
||||
/**
|
||||
* 立即从 DOM 和内存中移除(不触发离场动画)
|
||||
*/
|
||||
remove(): void;
|
||||
close(immediate?: boolean): void;
|
||||
_destroy(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* 版本号 — 唯一来源,发布时只需修改此处
|
||||
*/
|
||||
declare const VERSION = "0.5.0";
|
||||
|
||||
/**
|
||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||
*/
|
||||
declare const enhancedMeToast: {
|
||||
version: string;
|
||||
animations: AnimationUtils;
|
||||
themes: ThemeUtils;
|
||||
i18n: I18nUtils;
|
||||
plugins: PluginUtils;
|
||||
presetPlugins: Record<string, Plugin>;
|
||||
_toasts: Map<string, ToastInstance>;
|
||||
_config: ToastConfig;
|
||||
_destroyed?: boolean;
|
||||
_emit(opts: ToastOptions): ToastInstance;
|
||||
_remove(id: string): void;
|
||||
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
|
||||
_groupCount(name: string): number;
|
||||
configure(opts: Partial<ToastConfig>): MeToast;
|
||||
init(options?: InitOptions): MeToast;
|
||||
destroy(): void;
|
||||
getStatus(): StatusInfo;
|
||||
getConfig(): ToastConfig;
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast;
|
||||
resetConfig(): MeToast;
|
||||
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
|
||||
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
|
||||
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
|
||||
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
|
||||
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
|
||||
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
|
||||
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
|
||||
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
|
||||
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
|
||||
group(name: string): GroupAPI;
|
||||
dismissGroup(name: string): void;
|
||||
find(id: string): ToastInstance | undefined;
|
||||
dismiss(id?: string): void;
|
||||
removeToast(id: string): void;
|
||||
clear(position?: ToastPosition): void;
|
||||
getToasts(): ToastInstance[];
|
||||
count(): number;
|
||||
hasToasts(): boolean;
|
||||
getToast(id: string): ToastInstance | null;
|
||||
closeAll(): void;
|
||||
clearAll(): void;
|
||||
pauseAll(): void;
|
||||
resumeAll(): void;
|
||||
updateAll(partial: Partial<ToastOptions>): void;
|
||||
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
|
||||
findByType(type: ToastType): ToastInstance[];
|
||||
findByPosition(position: ToastPosition): ToastInstance[];
|
||||
getAll(): Map<string, ToastInstance>;
|
||||
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
|
||||
};
|
||||
|
||||
export { ActionButton, ActionControl, AnimationConfig, AnimationUtils, ConfirmOptions, CountdownControl, CountdownOptions, ErrorInfo, GroupAPI, I18nUtils, InitOptions, LoadingControl, LocaleInfo, enhancedMeToast as MeToast, enhancedMeToast as Met, Plugin, PluginManager, PluginUtils, ProgressControl, ProgressOptions, PromiseOptions, PromptOptions, QueueControl, QueueOptions, StackOptions, StatusInfo, ThemeConfig, ThemePreview, ThemeUtils, Toast, ToastAnimation, ToastConfig, ToastInstance, ToastOptions, ToastPosition, ToastProgressDirection, ToastTheme, ToastType, TypeColor, VERSION, enhancedMeToast as default, enhancedMeToast as meToast };
|
||||
Vendored
+4526
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+4541
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+4617
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+509
@@ -0,0 +1,509 @@
|
||||
/**
|
||||
* MetonaToast — 核心类型定义
|
||||
* @module types
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/** Toast 通知类型(107种内置图标类型) */
|
||||
type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading' | 'check' | 'x' | 'alert' | 'question' | 'star' | 'heart' | 'bell' | 'mail' | 'settings' | 'user' | 'home' | 'search' | 'plus' | 'minus' | 'edit' | 'trash' | 'download' | 'upload' | 'share' | 'link' | 'external' | 'clock' | 'calendar' | 'map' | 'compass' | 'globe' | 'wifi' | 'cloud' | 'sun' | 'moon' | 'zap' | 'activity' | 'cpu' | 'database' | 'server' | 'terminal' | 'code' | 'git' | 'package' | 'layers' | 'grid' | 'list' | 'filter' | 'sort' | 'refresh' | 'sync' | 'power' | 'battery' | 'bluetooth' | 'volume' | 'mic' | 'camera' | 'image' | 'video' | 'music' | 'file' | 'folder' | 'clipboard' | 'save' | 'print' | 'eye' | 'eyeOff' | 'lock' | 'unlock' | 'shield' | 'key' | 'flag' | 'bookmark' | 'tag' | 'gift' | 'award' | 'target' | 'crosshair' | 'move' | 'maximize' | 'minimize' | 'copy' | 'cut' | 'paste' | 'rotateCw' | 'rotateCcw' | 'zoomIn' | 'zoomOut' | 'crop' | 'sliders' | 'toggleLeft' | 'toggleRight' | 'checkCircle' | 'xCircle' | 'alertCircle' | 'infoCircle' | 'helpCircle' | 'alertTriangle' | 'checkSquare' | 'square' | 'circle' | 'triangle' | 'hexagon' | 'octagon' | 'pentagon' | 'diamond';
|
||||
/** Toast 显示位置 */
|
||||
type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
||||
/** Toast 主题 */
|
||||
type ToastTheme = 'light' | 'dark' | 'auto' | 'warm' | (string & {});
|
||||
/** Toast 动画类型 */
|
||||
type ToastAnimation = 'slide' | 'fade' | 'scale' | 'bounce' | 'flip' | 'rotate' | 'zoom' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | (string & {});
|
||||
/** 进度条方向 */
|
||||
type ToastProgressDirection = 'horizontal' | 'vertical';
|
||||
interface TypeColor {
|
||||
fg: string;
|
||||
bg: string;
|
||||
}
|
||||
interface ToastConfig {
|
||||
position?: ToastPosition;
|
||||
duration?: number;
|
||||
max?: number;
|
||||
gap?: number;
|
||||
offset?: number;
|
||||
pauseOnHover?: boolean;
|
||||
closeOnClick?: boolean;
|
||||
draggable?: boolean;
|
||||
dragThreshold?: number;
|
||||
showProgress?: boolean;
|
||||
progressDirection?: ToastProgressDirection;
|
||||
icon?: boolean;
|
||||
closeButton?: boolean;
|
||||
theme?: ToastTheme;
|
||||
animation?: ToastAnimation;
|
||||
zIndex?: number;
|
||||
width?: number | string;
|
||||
className?: string;
|
||||
style?: Record<string, string>;
|
||||
onShow?: ((toast: ToastInstance) => void) | null;
|
||||
onBeforeShow?: ((toast: ToastInstance) => boolean | void) | null;
|
||||
onClose?: ((toast: ToastInstance) => void) | null;
|
||||
onClick?: ((toast: ToastInstance) => void) | null;
|
||||
onUpdate?: ((toast: ToastInstance) => void) | null;
|
||||
render?: ((toast: ToastInstance) => string) | null;
|
||||
resetTimerOnUpdate?: boolean;
|
||||
notifyWhenHidden?: boolean;
|
||||
onError?: ((errorInfo: ErrorInfo) => void) | null;
|
||||
group?: string | null;
|
||||
locale?: string;
|
||||
plugins?: Array<string | Plugin>;
|
||||
}
|
||||
interface ErrorInfo {
|
||||
hook?: string;
|
||||
source?: string;
|
||||
error: Error;
|
||||
toast?: ToastInstance;
|
||||
}
|
||||
interface ToastOptions extends Partial<ToastConfig> {
|
||||
type?: string;
|
||||
title?: string;
|
||||
message?: string;
|
||||
content?: string;
|
||||
html?: string;
|
||||
iconHTML?: string;
|
||||
id?: string;
|
||||
}
|
||||
interface ToastInstance {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
message: string;
|
||||
html: string;
|
||||
iconHTML: string;
|
||||
config: ToastConfig;
|
||||
el: HTMLElement | null;
|
||||
barEl: HTMLElement | null;
|
||||
rafId: number | null;
|
||||
remaining: number;
|
||||
startedAt: number;
|
||||
paused: boolean;
|
||||
closing: boolean;
|
||||
group: string | null;
|
||||
_cleanups: Array<() => void>;
|
||||
_palette(): {
|
||||
theme: string;
|
||||
c: TypeColor;
|
||||
t: Partial<ThemeConfig>;
|
||||
};
|
||||
create(): this;
|
||||
update(partial: Partial<ToastOptions>): this;
|
||||
updatePosition(position: string): this;
|
||||
remove(): void;
|
||||
close(immediate?: boolean): void;
|
||||
_pause(): void;
|
||||
_resume(): void;
|
||||
_destroy(): void;
|
||||
}
|
||||
interface LoadingControl {
|
||||
id: string;
|
||||
success(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
error(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
info(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
warning(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
update(partial: Partial<ToastOptions>): unknown;
|
||||
dismiss(): void;
|
||||
}
|
||||
interface ProgressControl {
|
||||
id: string;
|
||||
setProgress(percent: number): void;
|
||||
complete(message?: string): void;
|
||||
error(message?: string): void;
|
||||
dismiss(): void;
|
||||
}
|
||||
interface CountdownControl {
|
||||
id: string;
|
||||
cancel(): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
}
|
||||
interface ActionControl {
|
||||
id: string;
|
||||
toast: ToastInstance;
|
||||
dismiss(): void;
|
||||
}
|
||||
interface QueueControl {
|
||||
then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
||||
catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<void | TResult>;
|
||||
cancel(): void;
|
||||
}
|
||||
interface GroupAPI {
|
||||
_group: string;
|
||||
show: (...args: unknown[]) => ToastInstance;
|
||||
success: (...args: unknown[]) => ToastInstance;
|
||||
error: (...args: unknown[]) => ToastInstance;
|
||||
warning: (...args: unknown[]) => ToastInstance;
|
||||
info: (...args: unknown[]) => ToastInstance;
|
||||
loading: (...args: unknown[]) => LoadingControl;
|
||||
action: (...args: unknown[]) => ActionControl;
|
||||
dismiss(): void;
|
||||
count(): number;
|
||||
}
|
||||
interface ActionButton {
|
||||
text: string;
|
||||
onClick: (toast: ToastInstance) => void;
|
||||
color?: string;
|
||||
style?: Record<string, string>;
|
||||
close?: boolean;
|
||||
}
|
||||
interface PromiseOptions extends ToastOptions {
|
||||
loading?: string;
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
interface ConfirmOptions extends ToastOptions {
|
||||
confirmText?: string;
|
||||
confirmColor?: string;
|
||||
cancelText?: string;
|
||||
cancelColor?: string;
|
||||
}
|
||||
interface PromptOptions extends ToastOptions {
|
||||
inputType?: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
submitText?: string;
|
||||
submitColor?: string;
|
||||
cancelText?: string;
|
||||
cancelColor?: string;
|
||||
}
|
||||
interface ProgressOptions extends ToastOptions {
|
||||
progressColor?: string;
|
||||
}
|
||||
interface CountdownOptions extends ToastOptions {
|
||||
onComplete?: (() => void) | null;
|
||||
}
|
||||
interface QueueOptions extends ToastOptions {
|
||||
delay?: number;
|
||||
}
|
||||
interface StackOptions extends ToastOptions {
|
||||
stagger?: number;
|
||||
}
|
||||
interface InitOptions {
|
||||
config?: Partial<ToastConfig>;
|
||||
theme?: ToastTheme;
|
||||
locale?: string;
|
||||
plugins?: Array<string | Plugin>;
|
||||
}
|
||||
interface StatusInfo {
|
||||
version: string;
|
||||
toasts: number;
|
||||
theme: string;
|
||||
locale: string;
|
||||
plugins: string[];
|
||||
animations: number;
|
||||
}
|
||||
interface AnimationConfig {
|
||||
enter: Record<string, string | number>;
|
||||
leave: Record<string, string | number>;
|
||||
duration: number;
|
||||
easing: string;
|
||||
name?: string;
|
||||
delay?: number;
|
||||
iterations?: number;
|
||||
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
|
||||
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
|
||||
}
|
||||
interface AnimationUtils {
|
||||
register(name: string, config: Partial<AnimationConfig>): void;
|
||||
unregister(name: string): void;
|
||||
get(name: string): AnimationConfig | null;
|
||||
getAnimationNames(): string[];
|
||||
getActiveCount(): number;
|
||||
cancelAll(): void;
|
||||
reset(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
interface ThemeConfig {
|
||||
bg: string;
|
||||
text: string;
|
||||
border: string;
|
||||
shadow: string;
|
||||
hoverShadow: string;
|
||||
progressBg: string;
|
||||
closeHoverBg: string;
|
||||
backdropFilter?: string;
|
||||
}
|
||||
interface ThemePreview {
|
||||
name: string;
|
||||
resolved: string;
|
||||
colors: {
|
||||
background: string;
|
||||
text: string;
|
||||
border: string;
|
||||
};
|
||||
isDark: boolean;
|
||||
isLight: boolean;
|
||||
isAuto: boolean;
|
||||
}
|
||||
interface ThemeUtils {
|
||||
getSystemTheme(): 'light' | 'dark';
|
||||
resolveTheme(theme: string): string;
|
||||
getThemeConfig(theme: string): ThemeConfig;
|
||||
applyTheme(theme: string): void;
|
||||
getCurrentTheme(): string;
|
||||
getResolvedTheme(): string;
|
||||
switchTheme(theme: string): void;
|
||||
toggleTheme(): void;
|
||||
resetToAuto(): void;
|
||||
initTheme(): void;
|
||||
watchSystemTheme(): void;
|
||||
unwatchSystemTheme(): void;
|
||||
addThemeListener(listener: (theme: string, resolved: string) => void): () => void;
|
||||
removeThemeListener(listener: (theme: string, resolved: string) => void): void;
|
||||
clearThemeListeners(): void;
|
||||
registerTheme(name: string, config: ThemeConfig): void;
|
||||
unregisterTheme(name: string): void;
|
||||
getAllThemes(): Record<string, ThemeConfig>;
|
||||
getThemeNames(): string[];
|
||||
hasTheme(name: string): boolean;
|
||||
getThemePreview(theme: string): ThemePreview;
|
||||
generateThemeCSS(theme: string): string;
|
||||
applyThemeCSS(theme: string): void;
|
||||
removeThemeCSS(): void;
|
||||
saveTheme(theme: string): void;
|
||||
loadTheme(): string;
|
||||
}
|
||||
interface LocaleInfo {
|
||||
code: string;
|
||||
name: string;
|
||||
direction: 'ltr' | 'rtl';
|
||||
isSupported: boolean;
|
||||
isCurrent: boolean;
|
||||
isFallback: boolean;
|
||||
}
|
||||
interface I18nUtils {
|
||||
t(key: string, params?: Record<string, string | number>): string;
|
||||
plural(key: string, count: number, params?: Record<string, string | number>): string;
|
||||
getCurrentLocale(): string;
|
||||
setCurrentLocale(locale: string): void;
|
||||
switchLocale(locale: string): void;
|
||||
getFallbackLocale(): string;
|
||||
setFallbackLocale(locale: string): void;
|
||||
hasTranslation(key: string): boolean;
|
||||
getTranslations(locale: string): Record<string, unknown>;
|
||||
addTranslations(locale: string, translations: Record<string, unknown>): void;
|
||||
removeTranslation(locale: string, key: string): void;
|
||||
clearTranslations(locale: string): void;
|
||||
getSupportedLocales(): string[];
|
||||
isLocaleSupported(locale: string): boolean;
|
||||
getLocaleName(locale: string): string;
|
||||
getLocaleDirection(locale: string): 'ltr' | 'rtl';
|
||||
getLocaleInfo(locale: string): LocaleInfo;
|
||||
getAllLocaleInfo(): LocaleInfo[];
|
||||
formatNumber(number: number, options?: Intl.NumberFormatOptions): string;
|
||||
formatCurrency(amount: number, currency?: string, options?: Intl.NumberFormatOptions): string;
|
||||
formatPercent(value: number, options?: Intl.NumberFormatOptions): string;
|
||||
formatDate(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
|
||||
formatTime(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
|
||||
formatRelativeTime(date: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string;
|
||||
formatList(list: string[], options?: Record<string, unknown>): string;
|
||||
formatPlural(count: number, options?: Intl.PluralRulesOptions): string;
|
||||
addLocaleListener(listener: (locale: string) => void): () => void;
|
||||
removeLocaleListener(listener: (locale: string) => void): void;
|
||||
clearLocaleListeners(): void;
|
||||
initI18n(): void;
|
||||
saveLocale(locale: string): void;
|
||||
loadLocale(): string;
|
||||
getDefaultLocale(): string;
|
||||
}
|
||||
interface Plugin {
|
||||
name: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
hooks?: Record<string, (...args: unknown[]) => void>;
|
||||
middleware?: (...args: unknown[]) => unknown;
|
||||
install?: (manager: PluginManager) => void | Record<string, unknown> | null;
|
||||
uninstall?: (manager: PluginManager) => void;
|
||||
init?: (manager: PluginManager) => void;
|
||||
destroy?: (manager: PluginManager) => void;
|
||||
installed?: boolean;
|
||||
enabled?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
interface PluginManager {
|
||||
register(name: string, plugin: Plugin): PluginManager;
|
||||
unregister(name: string): PluginManager;
|
||||
get(name: string): Plugin | null;
|
||||
has(name: string): boolean;
|
||||
getAll(): Plugin[];
|
||||
getNames(): string[];
|
||||
enable(name: string): PluginManager;
|
||||
disable(name: string): PluginManager;
|
||||
isEnabled(name: string): boolean;
|
||||
destroy(): void;
|
||||
}
|
||||
interface PluginUtils {
|
||||
createManager(): PluginManager;
|
||||
register(name: string, plugin: Plugin): PluginManager;
|
||||
unregister(name: string): PluginManager;
|
||||
get(name: string): Plugin | null;
|
||||
has(name: string): boolean;
|
||||
getAll(): Plugin[];
|
||||
getNames(): string[];
|
||||
enable(name: string): PluginManager;
|
||||
disable(name: string): PluginManager;
|
||||
isEnabled(name: string): boolean;
|
||||
getPreset(name: string): Plugin | null;
|
||||
getAllPresets(): Record<string, Plugin>;
|
||||
createPlugin(config: Partial<Plugin>): Plugin;
|
||||
validatePlugin(plugin: Partial<Plugin>): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
};
|
||||
}
|
||||
interface MeToast {
|
||||
version: string;
|
||||
_toasts: Map<string, ToastInstance>;
|
||||
_config: ToastConfig;
|
||||
_destroyed?: boolean;
|
||||
_emit(opts: ToastOptions): ToastInstance;
|
||||
_remove(id: string): void;
|
||||
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
|
||||
_groupCount(name: string): number;
|
||||
configure(opts: Partial<ToastConfig>): MeToast;
|
||||
init(options?: InitOptions): MeToast;
|
||||
destroy(): void;
|
||||
getStatus(): StatusInfo;
|
||||
getConfig(): ToastConfig;
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast;
|
||||
resetConfig(): MeToast;
|
||||
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
|
||||
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
|
||||
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
|
||||
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
|
||||
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
|
||||
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
|
||||
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
|
||||
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
|
||||
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
|
||||
group(name: string): GroupAPI;
|
||||
dismissGroup(name: string): void;
|
||||
find(id: string): ToastInstance | undefined;
|
||||
dismiss(id?: string): void;
|
||||
removeToast(id: string): void;
|
||||
clear(position?: ToastPosition): void;
|
||||
getToasts(): ToastInstance[];
|
||||
count(): number;
|
||||
hasToasts(): boolean;
|
||||
getToast(id: string): ToastInstance | null;
|
||||
closeAll(): void;
|
||||
clearAll(): void;
|
||||
pauseAll(): void;
|
||||
resumeAll(): void;
|
||||
updateAll(partial: Partial<ToastOptions>): void;
|
||||
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
|
||||
findByType(type: ToastType): ToastInstance[];
|
||||
findByPosition(position: ToastPosition): ToastInstance[];
|
||||
getAll(): Map<string, ToastInstance>;
|
||||
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
|
||||
animations: AnimationUtils;
|
||||
themes: ThemeUtils;
|
||||
i18n: I18nUtils;
|
||||
plugins: PluginUtils;
|
||||
presetPlugins: Record<string, Plugin>;
|
||||
}
|
||||
declare global {
|
||||
interface Window {
|
||||
MeToast: MeToast;
|
||||
Met: MeToast;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||
*/
|
||||
declare const enhancedMeToast: {
|
||||
version: string;
|
||||
animations: AnimationUtils;
|
||||
themes: ThemeUtils;
|
||||
i18n: I18nUtils;
|
||||
plugins: PluginUtils;
|
||||
presetPlugins: Record<string, Plugin>;
|
||||
_toasts: Map<string, ToastInstance>;
|
||||
_config: ToastConfig;
|
||||
_destroyed?: boolean;
|
||||
_emit(opts: ToastOptions): ToastInstance;
|
||||
_remove(id: string): void;
|
||||
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
|
||||
_groupCount(name: string): number;
|
||||
configure(opts: Partial<ToastConfig>): MeToast;
|
||||
init(options?: InitOptions): MeToast;
|
||||
destroy(): void;
|
||||
getStatus(): StatusInfo;
|
||||
getConfig(): ToastConfig;
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast;
|
||||
resetConfig(): MeToast;
|
||||
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
|
||||
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
|
||||
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
|
||||
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
|
||||
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
|
||||
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
|
||||
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
|
||||
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
|
||||
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
|
||||
group(name: string): GroupAPI;
|
||||
dismissGroup(name: string): void;
|
||||
find(id: string): ToastInstance | undefined;
|
||||
dismiss(id?: string): void;
|
||||
removeToast(id: string): void;
|
||||
clear(position?: ToastPosition): void;
|
||||
getToasts(): ToastInstance[];
|
||||
count(): number;
|
||||
hasToasts(): boolean;
|
||||
getToast(id: string): ToastInstance | null;
|
||||
closeAll(): void;
|
||||
clearAll(): void;
|
||||
pauseAll(): void;
|
||||
resumeAll(): void;
|
||||
updateAll(partial: Partial<ToastOptions>): void;
|
||||
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
|
||||
findByType(type: ToastType): ToastInstance[];
|
||||
findByPosition(position: ToastPosition): ToastInstance[];
|
||||
getAll(): Map<string, ToastInstance>;
|
||||
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
|
||||
};
|
||||
|
||||
/**
|
||||
* MetonaToast React — React 适配器
|
||||
* @module react
|
||||
* @version 0.5.0
|
||||
* @description useToast hook + 声明式 <Toast /> 组件。主包保持零依赖,React 为 optional peerDependency
|
||||
*/
|
||||
|
||||
/** 支持声明式渲染的 Toast 类型 */
|
||||
type ReactToastType = 'success' | 'error' | 'warning' | 'info' | 'show' | (string & {});
|
||||
interface ToastProps extends Omit<ToastOptions, 'message' | 'type'> {
|
||||
message: string;
|
||||
type?: ReactToastType;
|
||||
/** 组件卸载时自动移除该 Toast(默认 true)。设为 false 则 Toast 独立于组件生命周期 */
|
||||
autoClose?: boolean;
|
||||
}
|
||||
/**
|
||||
* 创建绑定清理集合的 API — 组件卸载时自动移除本组件创建的 Toast。
|
||||
* 纯函数,便于独立测试与扩展。
|
||||
*/
|
||||
declare const createBoundApi: (cleanup: Set<string>) => Record<string, unknown>;
|
||||
/**
|
||||
* useToast — 返回 MeToast API,但本组件创建的 Toast 会在组件卸载时自动移除。
|
||||
* 适合"通知随组件生命周期"的场景(如表单提交、异步任务页)。
|
||||
*/
|
||||
declare const useToast: () => Record<string, unknown>;
|
||||
/**
|
||||
* Toast — 声明式 Toast 组件。props 变化时更新内容(重新创建实例),
|
||||
* 组件卸载时自动移除(autoClose 默认 true)。
|
||||
*/
|
||||
declare const Toast: (props: ToastProps) => null;
|
||||
|
||||
export { enhancedMeToast as MeToast, ReactToastType, Toast, ToastProps, createBoundApi, useToast };
|
||||
Vendored
+4612
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
setupFiles: ['<rootDir>/tests/setup.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'babel-jest',
|
||||
},
|
||||
@@ -17,5 +18,13 @@ module.exports = {
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 85,
|
||||
branches: 65,
|
||||
functions: 85,
|
||||
lines: 95,
|
||||
},
|
||||
},
|
||||
verbose: true,
|
||||
};
|
||||
|
||||
Generated
+99
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@metona-team/metona-toast",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@metona-team/metona-toast",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.0",
|
||||
@@ -18,6 +18,8 @@
|
||||
"@rollup/plugin-terser": "^0.4.0",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/react": "^18.3.31",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"babel-jest": "^29.5.0",
|
||||
@@ -25,6 +27,8 @@
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"prettier": "^2.8.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"rollup": "^3.20.0",
|
||||
"rollup-plugin-dts": "^5.3.0",
|
||||
"rollup-plugin-livereload": "^2.0.5",
|
||||
@@ -34,6 +38,14 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -2722,6 +2734,34 @@
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.31",
|
||||
"resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz",
|
||||
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
@@ -3810,6 +3850,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-3.0.2.tgz",
|
||||
@@ -6103,6 +6150,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -6713,6 +6773,33 @@
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
|
||||
@@ -7055,6 +7142,16 @@
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
|
||||
|
||||
+21
-5
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@metona-team/metona-toast",
|
||||
"version": "0.2.1",
|
||||
"version": "0.5.0",
|
||||
"description": "轻量、零依赖、精致美观的Toast通知库。TypeScript源码,开箱即用。",
|
||||
"type": "module",
|
||||
"main": "dist/metona-toast.cjs.js",
|
||||
"main": "dist/metona-toast.cjs",
|
||||
"module": "dist/metona-toast.esm.js",
|
||||
"unpkg": "dist/metona-toast.min.js",
|
||||
"jsdelivr": "dist/metona-toast.min.js",
|
||||
@@ -11,8 +11,21 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/metona-toast.esm.js",
|
||||
"require": "./dist/metona-toast.cjs.js",
|
||||
"require": "./dist/metona-toast.cjs",
|
||||
"types": "./dist/metona-toast.d.ts"
|
||||
},
|
||||
"./react": {
|
||||
"import": "./dist/react.js",
|
||||
"require": "./dist/react.cjs",
|
||||
"types": "./dist/react.d.ts"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -31,8 +44,7 @@
|
||||
"format": "prettier --write src/**/*.ts tests/**/*.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build",
|
||||
"docs": "typedoc src/ --out docs",
|
||||
"example": "serve examples/"
|
||||
"serve": "bash serve.sh"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -65,6 +77,8 @@
|
||||
"@rollup/plugin-terser": "^0.4.0",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/react": "^18.3.31",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"babel-jest": "^29.5.0",
|
||||
@@ -72,6 +86,8 @@
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"prettier": "^2.8.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"rollup": "^3.20.0",
|
||||
"rollup-plugin-dts": "^5.3.0",
|
||||
"rollup-plugin-livereload": "^2.0.5",
|
||||
|
||||
+33
-2
@@ -59,14 +59,16 @@ export default [
|
||||
},
|
||||
plugins: basePlugins,
|
||||
},
|
||||
// CommonJS
|
||||
// CommonJS(.cjs 扩展名:包声明 type:module,.js 会被 Node 按 ESM 解析)
|
||||
// footer 将 default 提升为 module.exports,保证 require() 直接返回 MeToast
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.cjs.js',
|
||||
file: 'dist/metona-toast.cjs',
|
||||
format: 'cjs',
|
||||
exports: 'named',
|
||||
sourcemap: true,
|
||||
footer: 'module.exports = Object.assign(exports.default, exports);',
|
||||
},
|
||||
plugins: basePlugins,
|
||||
},
|
||||
@@ -103,5 +105,34 @@ export default [
|
||||
},
|
||||
plugins: [dts()],
|
||||
},
|
||||
// React 适配器(ESM + CJS),react 为 optional peer 外部依赖
|
||||
{
|
||||
input: 'src/react.ts',
|
||||
external: ['react'],
|
||||
output: [
|
||||
{
|
||||
file: 'dist/react.js',
|
||||
format: 'es',
|
||||
exports: 'named',
|
||||
sourcemap: true,
|
||||
},
|
||||
{
|
||||
file: 'dist/react.cjs',
|
||||
format: 'cjs',
|
||||
exports: 'named',
|
||||
sourcemap: true,
|
||||
},
|
||||
],
|
||||
plugins: basePlugins,
|
||||
},
|
||||
{
|
||||
input: 'src/react.ts',
|
||||
external: ['react'],
|
||||
output: {
|
||||
file: 'dist/react.d.ts',
|
||||
format: 'es',
|
||||
},
|
||||
plugins: [dts()],
|
||||
},
|
||||
]),
|
||||
];
|
||||
|
||||
+215
-114
@@ -7,51 +7,78 @@
|
||||
<title>在线演示 — MetonaToast</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b1121; --surface: #151d33; --border: #1e2d4d;
|
||||
--text: #e2e8f0; --muted: #64748b; --accent: #6366f1; --accent2: #818cf8;
|
||||
--bg: #070b14; --surface: rgba(255,255,255,0.03); --surface2: rgba(255,255,255,0.05);
|
||||
--border: rgba(255,255,255,0.08); --text: #e8ecf4; --muted: #8b94a7;
|
||||
--accent: #a5b4fc; --grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
|
||||
--radius: 14px;
|
||||
}
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; background:var(--bg); color:var(--text); line-height:1.6; min-height:100vh; }
|
||||
a { color:var(--accent2); text-decoration:none; }
|
||||
code { background:rgba(99,102,241,0.12); color:#a5b4fc; padding:1px 6px; border-radius:4px; font-size:0.85em; }
|
||||
.nav { position:sticky; top:0; z-index:100; background:rgba(11,17,33,0.88); backdrop-filter:blur(14px); border-bottom:1px solid var(--border); }
|
||||
.nav-inner { max-width:1300px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; padding:0 20px; height:56px; }
|
||||
.nav-logo { font-size:18px; font-weight:700; background:linear-gradient(135deg,#818cf8,#c084fc,#f472b6); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
|
||||
.nav-links { display:flex; gap:20px; }
|
||||
.nav-links a { color:var(--muted); font-size:13px; font-weight:500; transition:.2s; }
|
||||
.nav-links a:hover { color:var(--accent2); }
|
||||
.hero { text-align:center; padding:40px 20px 30px; border-bottom:1px solid var(--border); background:radial-gradient(ellipse at 50% 0%,rgba(99,102,241,0.12) 0%,transparent 50%); }
|
||||
.hero h1 { font-size:36px; font-weight:800; }
|
||||
.hero h1 span { background:linear-gradient(135deg,#818cf8,#c084fc,#f472b6); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
|
||||
body {
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
|
||||
background:var(--bg); color:var(--text); line-height:1.65; min-height:100vh;
|
||||
background-image:
|
||||
radial-gradient(ellipse 70% 45% at 50% -10%, rgba(99,102,241,0.12), transparent),
|
||||
radial-gradient(ellipse 50% 40% at 90% 20%, rgba(168,85,247,0.07), transparent);
|
||||
}
|
||||
a { color:var(--accent); text-decoration:none; }
|
||||
code { background:rgba(99,102,241,0.15); color:#a5b4fc; padding:1px 6px; border-radius:5px; font-size:.85em; }
|
||||
.grad-text { background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; color:transparent; }
|
||||
|
||||
.nav { position:sticky; top:0; z-index:100; background:rgba(7,11,20,0.72); backdrop-filter:blur(16px); border-bottom:1px solid var(--border); }
|
||||
.nav-inner { max-width:1320px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; padding:0 22px; height:58px; }
|
||||
.nav-logo { font-size:18px; font-weight:800; }
|
||||
.nav-links { display:flex; gap:22px; }
|
||||
.nav-links a { color:var(--muted); font-size:13.5px; font-weight:500; transition:.2s; }
|
||||
.nav-links a:hover { color:var(--accent); }
|
||||
|
||||
.hero { text-align:center; padding:38px 20px 28px; }
|
||||
.hero h1 { font-size:34px; font-weight:800; }
|
||||
.hero p { color:var(--muted); font-size:14px; margin-top:6px; }
|
||||
|
||||
.container { max-width:1300px; margin:0 auto; padding:24px 20px; display:grid; grid-template-columns:repeat(auto-fill,minmax(400px,1fr)); gap:18px; }
|
||||
.card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:20px; }
|
||||
.card h2 { font-size:15px; margin-bottom:2px; }
|
||||
.card > p { color:var(--muted); font-size:11px; margin-bottom:12px; }
|
||||
.container { max-width:1320px; margin:0 auto; padding:20px; display:grid; grid-template-columns:repeat(auto-fill,minmax(400px,1fr)); gap:16px; }
|
||||
.card {
|
||||
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
|
||||
padding:20px; transition:border-color .2s, transform .2s; position:relative; overflow:hidden;
|
||||
}
|
||||
.card:hover { border-color:rgba(129,140,248,.35); }
|
||||
.card::before {
|
||||
content:''; position:absolute; top:0; left:0; right:0; height:2px;
|
||||
background:var(--grad); opacity:0; transition:opacity .25s;
|
||||
}
|
||||
.card:hover::before { opacity:.8; }
|
||||
.card h2 { font-size:14.5px; margin-bottom:2px; font-weight:700; }
|
||||
.card > p { color:var(--muted); font-size:11.5px; margin-bottom:12px; }
|
||||
.btn-row { display:flex; flex-wrap:wrap; gap:6px; }
|
||||
.btn { padding:8px 14px; border:none; border-radius:7px; font-size:12px; font-weight:500; cursor:pointer; transition:.15s; color:#fff; }
|
||||
.btn:hover { transform:translateY(-1px); filter:brightness(1.15); }
|
||||
.btn-sm { padding:5px 10px; font-size:11px; border-radius:5px; }
|
||||
.btn {
|
||||
padding:8px 14px; border:none; border-radius:8px; font-size:12px; font-weight:600;
|
||||
cursor:pointer; transition:transform .15s, filter .15s, box-shadow .15s; color:#fff;
|
||||
}
|
||||
.btn:hover { transform:translateY(-1.5px); filter:brightness(1.15); box-shadow:0 6px 18px -6px rgba(0,0,0,.5); }
|
||||
.btn-sm { padding:5.5px 11px; font-size:11px; border-radius:6px; }
|
||||
.btn-tiny { padding:4px 9px; font-size:10px; border-radius:5px; }
|
||||
.btn-outline { background:transparent; border:1px solid var(--border); color:var(--text); }
|
||||
.btn-outline:hover { border-color:var(--accent); background:rgba(99,102,241,0.06); }
|
||||
.btn-tiny { padding:4px 9px; font-size:10px; border-radius:4px; }
|
||||
.btn-outline:hover { border-color:rgba(129,140,248,.5); background:rgba(129,140,248,.08); }
|
||||
.c-s { background:#10b981; } .c-e { background:#ef4444; } .c-w { background:#f59e0b; color:#1e293b; }
|
||||
.c-i { background:#3b82f6; } .c-p { background:#6366f1; } .c-pp { background:#8b5cf6; }
|
||||
.c-pk { background:#ec4899; } .c-t { background:#14b8a6; } .c-o { background:#f97316; }
|
||||
.c-sl { background:#475569; } .c-r { background:#f43f5e; } .c-g { background:#22c55e; }
|
||||
.label { font-size:10px; text-transform:uppercase; letter-spacing:0.8px; color:var(--muted); margin-bottom:10px; font-weight:600; }
|
||||
.demo-section { border-top:1px solid var(--border); padding-top:8px; margin-top:8px; }
|
||||
.c-grad { background:var(--grad); }
|
||||
.label {
|
||||
font-size:10px; text-transform:uppercase; letter-spacing:1px; color:var(--muted);
|
||||
margin-bottom:10px; font-weight:700;
|
||||
}
|
||||
.demo-section { border-top:1px solid var(--border); padding-top:10px; margin-top:10px; }
|
||||
.status-line { font-size:11.5px; color:#34d399; margin-top:10px; font-family:monospace; min-height:18px; }
|
||||
footer { text-align:center; padding:30px 20px; color:var(--muted); font-size:12px; border-top:1px solid var(--border); }
|
||||
@media (max-width:768px) { .container { grid-template-columns:1fr; } }
|
||||
|
||||
@media (max-width:820px) { .container { grid-template-columns:1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<div class="nav-inner">
|
||||
<div class="nav-logo">MetonaToast</div>
|
||||
<div class="nav-logo"><span class="grad-text">MetonaToast</span></div>
|
||||
<div class="nav-links">
|
||||
<a href="index.html">首页</a>
|
||||
<a href="docs.html">文档</a>
|
||||
@@ -62,7 +89,7 @@
|
||||
</nav>
|
||||
|
||||
<header class="hero">
|
||||
<h1><span>在线演示</span></h1>
|
||||
<h1><span class="grad-text">在线演示</span></h1>
|
||||
<p>覆盖项目全部功能的交互演示 — 点击即可体验</p>
|
||||
</header>
|
||||
|
||||
@@ -81,10 +108,10 @@
|
||||
<div class="demo-section">
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.success({title:'保存成功',message:'文件已同步到云端'})">带标题</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('持续10秒',{duration:10000})">长时10s</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('持续 10 秒',{duration:10000})">长时 10s</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.warning('手动关闭',{duration:0,closeButton:true})">手动关闭</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('',{icon:false})">无图标</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('无按钮',{closeButton:false})">无关闭按钮</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('无关闭按钮',{closeButton:false})">无按钮</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoCustomIcon()">自定义图标</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,16 +119,17 @@
|
||||
|
||||
<!-- 2. Loading 链式 -->
|
||||
<div class="card">
|
||||
<div class="label">🔄 Loading 链式转换</div>
|
||||
<div class="label">🔄 Loading 链式转换 <span style="color:#34d399">id 稳定</span></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-s" onclick="demoLoading()">提交成功</button>
|
||||
<button class="btn c-e" onclick="demoLoadingFail()">提交失败</button>
|
||||
<button class="btn c-w" onclick="demoLoadingWarn()">转为警告</button>
|
||||
<button class="btn c-i" onclick="demoLoadingUpdate()">原地更新</button>
|
||||
<button class="btn c-s" onclick="demoLoading('success')">提交成功</button>
|
||||
<button class="btn c-e" onclick="demoLoading('error')">提交失败</button>
|
||||
<button class="btn c-w" onclick="demoLoading('warning')">转为警告</button>
|
||||
<button class="btn c-i" onclick="demoLoading('update')">原地更新</button>
|
||||
</div>
|
||||
<p class="status-line" id="loading-status">转换后返回的 toast 与 loading 是同一个实例(id 不变)</p>
|
||||
</div>
|
||||
|
||||
<!-- 3. Promise + confirm + prompt -->
|
||||
<!-- 3. 对话框 -->
|
||||
<div class="card">
|
||||
<div class="label">💬 对话框</div>
|
||||
<div class="btn-row">
|
||||
@@ -116,8 +144,8 @@
|
||||
<div class="card">
|
||||
<div class="label">📊 进度 & 倒计时</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-o" onclick="demoProgress()">进度条(水平)</button>
|
||||
<button class="btn c-g" onclick="demoProgressV()">进度条(垂直)</button>
|
||||
<button class="btn c-o" onclick="demoProgress('horizontal')">进度条(水平)</button>
|
||||
<button class="btn c-g" onclick="demoProgress('vertical')">进度条(垂直)</button>
|
||||
<button class="btn c-pk" onclick="demoCountdown()">倒计时 5s</button>
|
||||
<button class="btn c-r" onclick="demoCountdownPause()">倒计时(可暂停)</button>
|
||||
</div>
|
||||
@@ -136,7 +164,7 @@
|
||||
|
||||
<!-- 6. Action Toast -->
|
||||
<div class="card">
|
||||
<div class="label">⚡ Action Toast</div>
|
||||
<div class="label">⚡ Action Toast <span style="color:#34d399">close:false 不关闭</span></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-e" onclick="demoActionDelete()">删除+撤销</button>
|
||||
<button class="btn c-i" onclick="demoActionMulti()">多按钮</button>
|
||||
@@ -146,7 +174,7 @@
|
||||
|
||||
<!-- 7. 分组管理 -->
|
||||
<div class="card">
|
||||
<div class="label">📁 分组管理</div>
|
||||
<div class="label">📁 分组管理</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-p" onclick="demoGroup()">创建分组消息</button>
|
||||
<button class="btn c-e" onclick="dismissGroup()">关闭分组</button>
|
||||
@@ -157,7 +185,7 @@
|
||||
|
||||
<!-- 8. 动画效果 -->
|
||||
<div class="card">
|
||||
<div class="label">🎭 11种动画效果</div>
|
||||
<div class="label">🎭 11 种动画效果</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-outline btn-tiny" onclick="anim('slide')">slide</button>
|
||||
<button class="btn btn-outline btn-tiny" onclick="anim('fade')">fade</button>
|
||||
@@ -171,9 +199,14 @@
|
||||
<button class="btn btn-outline btn-tiny" onclick="anim('slideLeft')">slideLeft</button>
|
||||
<button class="btn btn-outline btn-tiny" onclick="anim('slideRight')">slideRight</button>
|
||||
</div>
|
||||
<div class="demo-section">
|
||||
<div class="btn-row">
|
||||
<button class="btn c-grad" onclick="demoCustomAnim()">✨ 注册自定义动画</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 9. 6种位置 -->
|
||||
<!-- 9. 位置 -->
|
||||
<div class="card">
|
||||
<div class="label">📍 全部位置</div>
|
||||
<div class="btn-row">
|
||||
@@ -186,7 +219,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 10. 主题切换 + 自定义 -->
|
||||
<!-- 10. 主题 -->
|
||||
<div class="card">
|
||||
<div class="label">🎨 主题切换 & 自定义</div>
|
||||
<div class="btn-row">
|
||||
@@ -194,24 +227,43 @@
|
||||
<button class="btn btn-outline btn-sm" onclick="switchTheme('dark')">🌙 Dark</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="switchTheme('auto')">🔄 Auto</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="switchTheme('warm')">🔥 Warm</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoRegisterTheme()">注册自定义主题</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoRegisterTheme()">注册自定义</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 11. 插件系统 -->
|
||||
<!-- 11. 插件 -->
|
||||
<div class="card">
|
||||
<div class="label">🔌 插件系统</div>
|
||||
<div class="label">🔌 插件系统 <span style="color:#34d399">4 款预设</span></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('keyboard');MeToast.info('已安装 — 按 ESC 关闭所有 Toast')">keyboard ESC关闭</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('persistence');MeToast.info('已安装 — 配置将持久化到 localStorage')">persistence 持久化</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('accessibility');MeToast.info('已安装 — 屏幕阅读器可用')">accessibility 无障碍</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('keyboard');MeToast.info('已安装 — 按 ESC 关闭所有 Toast')">keyboard</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('persistence');MeToast.info('已安装 — 配置持久化')">persistence</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('accessibility');MeToast.info('已安装 — 屏幕阅读器')">accessibility</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('dedupe');MeToast.info('已安装 — 同消息去重')">dedupe</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoCustomPlugin()">自定义插件</button>
|
||||
</div>
|
||||
<div class="demo-section">
|
||||
<div class="btn-row">
|
||||
<button class="btn c-t btn-sm" onclick="demoDedupe()">去重演示</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 12. 高级特性 -->
|
||||
<!-- 12. 钩子系统 -->
|
||||
<div class="card">
|
||||
<div class="label">🔥 高级特性 <span style="color:#34d399;font-size:10px;">resetTimer · onUpdate · render · notify</span></div>
|
||||
<div class="label">🔗 钩子系统 <span style="color:#34d399">beforeShow 拦截演示</span></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-p" onclick="demoHookBlock()">开启拦截 error</button>
|
||||
<button class="btn c-e" onclick="MeToast.error('这条会被拦截')">触发被拦 error</button>
|
||||
<button class="btn c-s" onclick="MeToast.success('这条不受影响')">正常 success</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoHookUnblock()">关闭拦截</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoHookCount()">钩子统计</button>
|
||||
</div>
|
||||
<p class="status-line" id="hook-status">钩子:未注册</p>
|
||||
</div>
|
||||
|
||||
<!-- 13. 高级特性 -->
|
||||
<div class="card">
|
||||
<div class="label">🔥 高级特性</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-i" onclick="demoResetTimer()">resetTimerOnUpdate</button>
|
||||
<button class="btn c-p" onclick="demoOnUpdate()">onUpdate 回调</button>
|
||||
@@ -220,7 +272,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 13. Toast 管理 -->
|
||||
<!-- 14. Toast 管理 -->
|
||||
<div class="card">
|
||||
<div class="label">🔧 Toast 管理</div>
|
||||
<div class="btn-row">
|
||||
@@ -230,7 +282,7 @@
|
||||
<button class="btn c-sl" onclick="MeToast.pauseAll();MeToast.info('已暂停全部')">暂停全部</button>
|
||||
<button class="btn c-sl" onclick="MeToast.resumeAll();MeToast.info('已恢复全部')">恢复全部</button>
|
||||
<button class="btn c-e" onclick="MeToast.dismiss()">关闭全部</button>
|
||||
<button class="btn c-r" onclick="MeToast.destroy();MeToast.info('已销毁')">销毁</button>
|
||||
<button class="btn c-r" onclick="MeToast.destroy();MeToast.init({});MeToast.info('已销毁并重建')">销毁重建</button>
|
||||
</div>
|
||||
<div class="demo-section">
|
||||
<div class="btn-row">
|
||||
@@ -238,32 +290,34 @@
|
||||
<button class="btn btn-outline btn-tiny" onclick="demoGetAll()">getAll</button>
|
||||
<button class="btn btn-outline btn-tiny" onclick="demoClearByPos()">clear(位置)</button>
|
||||
<button class="btn btn-outline btn-tiny" onclick="demoGetConfig()">查看配置</button>
|
||||
<button class="btn btn-outline btn-tiny" onclick="demoRemove()">removeToast 立即移除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 14. 错误回调 -->
|
||||
<!-- 15. onError -->
|
||||
<div class="card">
|
||||
<div class="label">🛡️ onError 错误捕获 <span style="color:#34d399;font-size:10px;">v0.2.1</span></div>
|
||||
<div class="label">🛡️ onError 错误捕获</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-e" onclick="demoOnError()">演示 onError</button>
|
||||
<button class="btn c-w" onclick="demoOnErrorReset()">重置回调</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 15. 交互特性 -->
|
||||
<!-- 16. 交互特性 -->
|
||||
<div class="card">
|
||||
<div class="label">✋ 交互特性</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-i" onclick="demoDraggable()">拖拽关闭</button>
|
||||
<button class="btn c-p" onclick="demoPauseHover()">悬停暂停</button>
|
||||
<button class="btn c-w" onclick="demoCloseOnClick()">点击关闭</button>
|
||||
<button class="btn c-pp" onclick="demoWidth()">自定义宽度</button>
|
||||
<button class="btn c-t" onclick="demoClassName()">自定义className</button>
|
||||
<button class="btn c-p" onclick="demoDragThreshold()">拖拽阈值 300px</button>
|
||||
<button class="btn c-w" onclick="demoPauseHover()">悬停暂停</button>
|
||||
<button class="btn c-pp" onclick="demoCloseOnClick()">点击关闭</button>
|
||||
<button class="btn c-t" onclick="demoWidth()">自定义宽度</button>
|
||||
<button class="btn c-sl" onclick="demoClassName()">自定义 className</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 16. 国际化切换 -->
|
||||
<!-- 17. 国际化 -->
|
||||
<div class="card">
|
||||
<div class="label">🌍 国际化 & 格式化</div>
|
||||
<div class="btn-row">
|
||||
@@ -273,16 +327,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 17. 107种预设图标 -->
|
||||
<!-- 18. 图标网格 -->
|
||||
<div class="card" style="grid-column:1/-1;">
|
||||
<div class="label">🎨 107种预设图标 — 点击任意图标发送Toast</div>
|
||||
<div class="btn-row" id="icon-grid" style="max-height:300px;overflow-y:auto;padding:4px 0;"></div>
|
||||
<div class="label">🎨 107 种预设图标 — 点击任意图标发送 Toast</div>
|
||||
<div class="btn-row" id="icon-grid" style="max-height:320px;overflow-y:auto;padding:4px 0;"></div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
MetonaToast v0.2.1 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
||||
MetonaToast v0.5.0 · MIT ·
|
||||
<a href="index.html">首页</a> ·
|
||||
<a href="docs.html">文档</a> ·
|
||||
<a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
||||
</footer>
|
||||
|
||||
<script src="../dist/metona-toast.js"></script>
|
||||
@@ -290,27 +347,41 @@
|
||||
MeToast.configure({ position:'top-right', duration:4000, theme:'dark', animation:'slide', pauseOnHover:true, showProgress:true });
|
||||
|
||||
// === Loading ===
|
||||
function demoLoading() { const l=MeToast.loading('正在提交...'); setTimeout(()=>l.success('提交成功!'),2000); }
|
||||
function demoLoadingFail() { const l=MeToast.loading('正在处理...'); setTimeout(()=>l.error('处理失败'),2000); }
|
||||
function demoLoadingWarn() { const l=MeToast.loading('检查中...'); setTimeout(()=>l.warning('发现潜在问题'),2000); }
|
||||
function demoLoadingUpdate() { const l=MeToast.loading('处理中 0%'); let i=1; const t=setInterval(()=>{if(i>3){clearInterval(t);l.success('完成!');return;} l.update({message:'处理中 '+i*33+'%'}); i++;},600); }
|
||||
function demoLoading(mode) {
|
||||
const l = MeToast.loading('正在处理...');
|
||||
const showId = () => document.getElementById('loading-status').textContent = 'loading id: ' + l.id;
|
||||
showId();
|
||||
setTimeout(() => {
|
||||
const map = { success:['提交成功!','success'], error:['处理失败','error'], warning:['发现潜在问题','warning'], update:null };
|
||||
if (mode === 'update') {
|
||||
l.update({ message: '进度 50%' });
|
||||
setTimeout(() => l.success('完成!'), 1000);
|
||||
return;
|
||||
}
|
||||
const [msg, type] = map[mode];
|
||||
const t = l[type](msg);
|
||||
if (t) document.getElementById('loading-status').textContent = '转换后 id: ' + t.id + '(' + (t.id === l.id ? '同一实例 ✓' : '不同实例 ✗') + ')';
|
||||
}, 1600);
|
||||
}
|
||||
|
||||
// === Promise / confirm / prompt ===
|
||||
async function demoPromise() {
|
||||
const p=new Promise((r,e)=>setTimeout(()=>Math.random()>0.3?r('ok'):e('fail'),2000));
|
||||
try{await MeToast.promise(p,{loading:'处理中...',success:'成功!',error:'失败'});}catch(_){}
|
||||
const p = new Promise((r,e)=>setTimeout(()=>Math.random()>0.3?r('ok'):e('fail'),2000));
|
||||
try{ await MeToast.promise(p,{loading:'处理中...',success:'成功!',error:'失败'}); }catch(_){}
|
||||
}
|
||||
async function demoConfirm() { const ok=await MeToast.confirm('确定删除?',{confirmText:'删除',confirmColor:'#ef4444'}); if(ok)MeToast.success('已删除'); }
|
||||
async function demoPrompt() { const v=await MeToast.prompt('请输入姓名',{placeholder:'请输入...'}); if(v) MeToast.info('你好,'+v+'!'); }
|
||||
async function demoPromptPassword() { const v=await MeToast.prompt('请输入密码',{placeholder:'密码',inputType:'password',submitText:'登录'}); if(v) MeToast.info('密码已输入'); }
|
||||
|
||||
// === Progress & Countdown ===
|
||||
function demoProgress() { const p=MeToast.progress('上传中...',{progressColor:'#3b82f6'}); let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('上传完成!');}else p.setProgress(v);},400); }
|
||||
function demoProgressV() { const p=MeToast.progress('上传中...',{progressColor:'#10b981',progressDirection:'vertical'}); let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('完成!');}else p.setProgress(v);},400); }
|
||||
function demoProgress(dir) {
|
||||
const p=MeToast.progress('上传中...',{progressColor: dir==='vertical'?'#10b981':'#3b82f6', progressDirection: dir});
|
||||
let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('上传完成!');}else p.setProgress(v);},400);
|
||||
}
|
||||
function demoCountdown() { MeToast.countdown('{seconds} 秒后执行',5,{onComplete:()=>MeToast.success('已执行')}); }
|
||||
function demoCountdownPause() {
|
||||
const c=MeToast.countdown('{seconds}s 可暂停',10,{type:'info',onComplete:()=>MeToast.success('倒计时结束')});
|
||||
setTimeout(()=>{c.pause();MeToast.warning('已暂停3秒后恢复',{duration:3000})},4000);
|
||||
setTimeout(()=>{c.pause();MeToast.warning('已暂停 3 秒后恢复',{duration:3000})},4000);
|
||||
setTimeout(()=>{c.resume();MeToast.info('已恢复倒计时')},7000);
|
||||
}
|
||||
|
||||
@@ -321,14 +392,15 @@
|
||||
setTimeout(()=>{q.cancel();MeToast.warning('队列已取消',{duration:2000});},3500);
|
||||
}
|
||||
function demoStack() { MeToast.stack(['消息1','消息2','消息3','消息4','消息5'],{stagger:120,type:'info'}); }
|
||||
function demoStackMixed() {
|
||||
MeToast.stack(['普通消息',{message:'警告消息',type:'warning',duration:5000},{message:'错误消息',type:'error'}],{stagger:150});
|
||||
}
|
||||
function demoStackMixed() { MeToast.stack(['普通消息',{message:'警告消息',type:'warning',duration:5000},{message:'错误消息',type:'error'}],{stagger:150}); }
|
||||
|
||||
// === Action ===
|
||||
function demoActionDelete() { MeToast.action('文件 data.json 已删除',[{text:'撤销',onClick:()=>MeToast.success('已撤销'),color:'#3b82f6'}]); }
|
||||
function demoActionMulti() { MeToast.action('邮件已发送',[{text:'查看',onClick:()=>MeToast.info('打开邮件详情'),color:'#3b82f6'},{text:'撤回',onClick:()=>MeToast.warning('已撤回'),color:'#f59e0b'}]); }
|
||||
function demoActionNoClose() { MeToast.action('完成后点击关闭',[{text:'我知道了',onClick:()=>{},color:'#10b981'}]); }
|
||||
function demoActionNoClose() {
|
||||
let n=0;
|
||||
MeToast.action('点「+1」不会关闭,可连续点击',[{text:'+1',onClick:(t)=>{n++;t.update({message:'已点击 '+n+' 次(close:false 不关闭)'});},color:'#10b981',close:false}],{duration:8000});
|
||||
}
|
||||
|
||||
// === Group ===
|
||||
const group = MeToast.group('demo-group');
|
||||
@@ -342,32 +414,71 @@
|
||||
|
||||
// === Animation ===
|
||||
function anim(name) { MeToast.success('动画: '+name,{animation:name,duration:3000,showProgress:false}); }
|
||||
function demoCustomAnim() {
|
||||
MeToast.animations.register('swing', {
|
||||
enter: { transform: 'rotate(-40deg) scale(0.3)', opacity: 0 },
|
||||
leave: { transform: 'rotate(20deg) scale(0.5)', opacity: 0 },
|
||||
duration: 600,
|
||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
});
|
||||
MeToast.success('自定义 swing 动画!', { animation: 'swing', duration: 3000 });
|
||||
}
|
||||
|
||||
// === Theme ===
|
||||
function switchTheme(t) { MeToast.themes.switchTheme(t); MeToast.configure({theme:t}); MeToast.info('主题: '+t,{theme:t}); }
|
||||
function demoRegisterTheme() {
|
||||
try {
|
||||
MeToast.themes.registerTheme('sunset',{bg:'rgba(255,248,240,0.96)',text:'#92400e',border:'rgba(249,115,22,0.2)',shadow:'0 10px 36px -10px rgba(249,115,22,0.2)',hoverShadow:'0 14px 48px -10px rgba(249,115,22,0.3)',progressBg:'rgba(249,115,22,0.1)',closeHoverBg:'rgba(249,115,22,0.1)'});
|
||||
MeToast.themes.switchTheme('sunset');
|
||||
MeToast.configure({theme:'sunset'});
|
||||
MeToast.themes.switchTheme('sunset'); MeToast.configure({theme:'sunset'});
|
||||
MeToast.success('已切换到 sunset 主题',{theme:'sunset'});
|
||||
} catch(_) { MeToast.warning('主题已注册,已切换'); }
|
||||
}
|
||||
|
||||
// === Plugins ===
|
||||
function demoCustomPlugin() {
|
||||
MeToast.use({name:'click-counter',version:'1.0',install(){let c=0;Toast.on('afterShow',t=>{c++;setTimeout(()=>t.update({message:t.message.replace(/\\(\\d+\\)/,'')+' (总通知#'+c+')'}),1);});}});
|
||||
MeToast.use({name:'click-counter',version:'1.0',install(){let c=0;Toast.on('afterShow',t=>{c++;setTimeout(()=>t.update({message:t.message.replace(/\(\d+\)/,'')+' (总通知#'+c+')'}),1);});}});
|
||||
MeToast.info('自定义插件已安装(消息后追加计数)');
|
||||
}
|
||||
function demoDedupe() {
|
||||
if (!MeToast.plugins.has('dedupe')) MeToast.use('dedupe');
|
||||
MeToast.info('同一条消息');
|
||||
MeToast.info('同一条消息');
|
||||
MeToast.info('同一条消息');
|
||||
MeToast.info('当前仅 ' + MeToast.count() + ' 条 — 相同消息自动合并');
|
||||
}
|
||||
|
||||
// === Hooks ===
|
||||
let hookOff = null;
|
||||
function demoHookBlock() {
|
||||
if (hookOff) hookOff();
|
||||
hookOff = Toast.on('beforeShow', (toast) => {
|
||||
if (toast.type === 'error') { document.getElementById('hook-status').textContent = '钩子:error 已被拦截 ✋'; return false; }
|
||||
});
|
||||
document.getElementById('hook-status').textContent = '钩子:已注册(error 类型将被拦截)';
|
||||
MeToast.info('拦截器已开启 — 试试触发 error');
|
||||
}
|
||||
function demoHookUnblock() {
|
||||
if (hookOff) { hookOff(); hookOff = null; }
|
||||
document.getElementById('hook-status').textContent = '钩子:已移除';
|
||||
MeToast.info('拦截器已关闭');
|
||||
}
|
||||
function demoHookCount() {
|
||||
const stats = [];
|
||||
['afterShow','afterClose','configChange','themeChange'].forEach(name => {
|
||||
const list = Toast._hooks.get(name);
|
||||
stats.push(name + ':' + (list ? list.length : 0));
|
||||
});
|
||||
MeToast.info('钩子数量 — ' + stats.join(' '), { duration: 5000 });
|
||||
}
|
||||
|
||||
// === 高级特性 ===
|
||||
function demoResetTimer() {
|
||||
const t=MeToast.info('这条消息不会自动消失(resetTimer)',{duration:3000,resetTimerOnUpdate:true,closeButton:true});
|
||||
let n=0; const iv=setInterval(()=>{n++;if(n>8){clearInterval(iv);t.close();return;}t.update({message:'重置计时器 #'+n+' — 每次更新重置3秒'});},1000);
|
||||
const t=MeToast.info('这条不会自动消失(每次更新重置 3 秒)',{duration:3000,resetTimerOnUpdate:true,closeButton:true});
|
||||
let n=0; const iv=setInterval(()=>{n++;if(n>8){clearInterval(iv);t.close();return;}t.update({message:'重置计时器 #'+n});},1000);
|
||||
}
|
||||
function demoOnUpdate() { const t=MeToast.success('onUpdate 演示',{onUpdate:t=>{console.log('更新了:',t.message);}}); setTimeout(()=>t.update({message:'已更新!'}),1000); setTimeout(()=>t.update({message:'再次更新!'}),2000); }
|
||||
function demoOnUpdate() { const t=MeToast.success('onUpdate 演示',{onUpdate:t=>{console.log('更新了:',t.message);}}); setTimeout(()=>t.update({message:'已更新!'}),1000); }
|
||||
function demoCustomRender() {
|
||||
MeToast.show({render:toast=>'<div style="background:#111;color:#0f0;padding:16px 20px;border-radius:10px;border:1px solid #0f0;font-family:monospace">🟢 <b>'+toast.message+'</b><br><span style="font-size:11px;opacity:0.6"># custom render by thzxx</span></div>',message:'自定义渲染内容',duration:5000});
|
||||
MeToast.show({render:toast=>'<div style="background:#111;color:#0f0;padding:16px 20px;border-radius:10px;border:1px solid #0f0;font-family:monospace">🟢 <b>'+toast.message+'</b><br><span style="font-size:11px;opacity:0.6"># custom render</span></div>',message:'自定义渲染内容',duration:5000});
|
||||
}
|
||||
function demoNotify() {
|
||||
if(typeof Notification==='undefined'){MeToast.error('浏览器不支持 Notification');return;}
|
||||
@@ -382,48 +493,38 @@
|
||||
}
|
||||
function demoGetAll() { const all=MeToast.getAll(); MeToast.info('当前共 '+all.size+' 条 toast'); }
|
||||
function demoClearByPos() { MeToast.error('右下角临时消息',{position:'bottom-right',duration:3000}); setTimeout(()=>{MeToast.clear('bottom-right');MeToast.info('右下角已清除');},500); }
|
||||
function demoGetConfig() { try{const c=MeToast.getConfig();MeToast.info('配置: '+JSON.stringify({position:c.position,duration:c.duration,theme:c.theme,animation:c.animation,locale:c.locale}).replace(/"/g,''),{duration:5000});}catch(e){MeToast.error('无 getConfig 方法');} }
|
||||
|
||||
// === Interaction ===
|
||||
function demoDraggable() { MeToast.info('← 试试拖动我来关闭',{draggable:true,duration:8000,closeButton:true}); }
|
||||
function demoPauseHover() { MeToast.info('鼠标悬停在我上面,倒计时会暂停',{pauseOnHover:true,duration:8000}); }
|
||||
function demoCloseOnClick() { MeToast.warning('点击我即可关闭',{closeOnClick:true,duration:10000}); }
|
||||
function demoWidth() { MeToast.info('宽550px',{width:550}); setTimeout(()=>MeToast.info('宽280px(窄)',{width:280}),500); }
|
||||
function demoClassName() { MeToast.success('带自定义 className',{className:'demo-custom-class'}); }
|
||||
function demoCustomIcon() {
|
||||
MeToast.info('通用通知', {
|
||||
iconHTML: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>'
|
||||
});
|
||||
}
|
||||
function demoGetConfig() { const c=MeToast.getConfig(); MeToast.info('配置: '+JSON.stringify({position:c.position,duration:c.duration,theme:c.theme,animation:c.animation,locale:c.locale}).replace(/"/g,''),{duration:5000}); }
|
||||
function demoRemove() { const t=MeToast.warning('这条将被立即移除(无动画)'); setTimeout(()=>{MeToast.removeToast(t.id);MeToast.info('已立即移除');},800); }
|
||||
|
||||
// === onError ===
|
||||
function demoOnError() {
|
||||
MeToast.configure({
|
||||
onError: function(info) {
|
||||
MeToast.error('⚠️ 捕获错误: ' + (info.hook || info.source), { duration: 3000, position: 'bottom-center' });
|
||||
}
|
||||
});
|
||||
MeToast.info('已启用 onError 监控(钩子/定时器异常将被捕获)', { duration: 3000 });
|
||||
// 触发一个钩子错误来演示
|
||||
MeToast.configure({ onError: function(info) { MeToast.error('⚠️ 捕获: ' + (info.hook || info.source), { duration: 3000, position: 'bottom-center' }); } });
|
||||
MeToast.info('已启用 onError 监控', { duration: 3000 });
|
||||
var badFn = function() { throw new Error('demo-error'); };
|
||||
Toast.on('afterShow', badFn);
|
||||
MeToast.success('这条消息会触发错误', { duration: 2000 });
|
||||
MeToast.success('这条会触发错误回调', { duration: 2000 });
|
||||
setTimeout(function() { Toast.off('afterShow', badFn); }, 3000);
|
||||
}
|
||||
function demoOnErrorReset() {
|
||||
MeToast.configure({ onError: null });
|
||||
MeToast.info('onError 已重置为 null');
|
||||
function demoOnErrorReset() { MeToast.configure({ onError: null }); MeToast.info('onError 已重置'); }
|
||||
|
||||
// === Interaction ===
|
||||
function demoDraggable() { MeToast.info('← 拖动我超过 120px 关闭',{draggable:true,duration:8000,closeButton:true}); }
|
||||
function demoDragThreshold() { MeToast.info('← 拖动阈值 300px(要拖更远)',{draggable:true,dragThreshold:300,duration:8000,closeButton:true}); }
|
||||
function demoPauseHover() { MeToast.info('悬停时倒计时会暂停',{pauseOnHover:true,duration:8000}); }
|
||||
function demoCloseOnClick() { MeToast.warning('点击我即可关闭',{closeOnClick:true,duration:10000}); }
|
||||
function demoWidth() { MeToast.info('宽 550px',{width:550}); setTimeout(()=>MeToast.info('宽 280px(窄)',{width:280}),500); }
|
||||
function demoClassName() { MeToast.success('带自定义 className',{className:'demo-custom-class'}); }
|
||||
function demoCustomIcon() {
|
||||
MeToast.info('通用通知', { iconHTML: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>' });
|
||||
}
|
||||
|
||||
// === i18n ===
|
||||
function switchLocale(loc) {
|
||||
try{MeToast.i18n.switchLocale(loc);MeToast.configure({locale:loc});MeToast.success(loc==='zh-CN'?'已切换到中文':'Switched to English',{theme:'dark'});}
|
||||
catch(e){MeToast.info('已切换语言');}
|
||||
MeToast.i18n.switchLocale(loc); MeToast.configure({locale:loc});
|
||||
MeToast.success(loc==='zh-CN'?'已切换到中文':'Switched to English');
|
||||
}
|
||||
function demoFormat() {
|
||||
try{
|
||||
MeToast.info('数字: '+MeToast.i18n.formatNumber(1234567)+' | 货币: '+MeToast.i18n.formatCurrency(99.5,'CNY')+' | 日期: '+MeToast.i18n.formatDate(new Date()),{duration:8000});
|
||||
}catch(e){MeToast.info('格式化: 1234567 → 1,234,567');}
|
||||
MeToast.info('数字: '+MeToast.i18n.formatNumber(1234567)+' | 货币: '+MeToast.i18n.formatCurrency(99.5,'CNY')+' | 日期: '+MeToast.i18n.formatDate(new Date()),{duration:8000});
|
||||
}
|
||||
|
||||
// === 图标网格 ===
|
||||
|
||||
+353
-347
@@ -7,448 +7,442 @@
|
||||
<title>API 文档 — MetonaToast</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b1121; --surface: #1a2332; --border: #1e2d4d;
|
||||
--text: #e2e8f0; --muted: #64748b; --accent: #6366f1; --accent2: #818cf8;
|
||||
--radius: 10px;
|
||||
--bg: #070b14; --surface: rgba(255,255,255,0.03); --border: rgba(255,255,255,0.08);
|
||||
--text: #e8ecf4; --muted: #8b94a7; --accent: #a5b4fc; --accent2: #c7d2fe;
|
||||
--grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
|
||||
--radius: 12px;
|
||||
}
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; background:var(--bg); color:var(--text); line-height:1.7; display:flex; min-height:100vh; }
|
||||
code { background:rgba(99,102,241,0.12); color:#a5b4fc; padding:1px 6px; border-radius:4px; font-size:0.9em; white-space:nowrap; }
|
||||
pre { background:#0a0f1a; border:1px solid var(--border); border-radius:8px; padding:14px 18px; overflow-x:auto; font:13px/1.7 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color:#cbd5e1; margin:10px 0; }
|
||||
pre .kw { color:#c084fc; } pre .fn { color:#60a5fa; } pre .s { color:#34d399; } pre .cm { color:#475569; }
|
||||
a { color:var(--accent2); text-decoration:none; }
|
||||
html { scroll-behavior:smooth; }
|
||||
body {
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
|
||||
background:var(--bg); color:var(--text); line-height:1.75; display:flex; min-height:100vh;
|
||||
background-image:
|
||||
radial-gradient(ellipse 70% 40% at 80% -10%, rgba(99,102,241,0.12), transparent),
|
||||
radial-gradient(ellipse 50% 40% at 0% 20%, rgba(236,72,153,0.05), transparent);
|
||||
}
|
||||
code {
|
||||
background:rgba(99,102,241,0.15); color:#a5b4fc; padding:1px 6px;
|
||||
border-radius:5px; font-size:.88em; white-space:nowrap;
|
||||
}
|
||||
a { color:var(--accent); text-decoration:none; }
|
||||
a:hover { color:var(--accent2); }
|
||||
|
||||
.sidebar { width:260px; min-width:260px; background:var(--surface); border-right:1px solid var(--border); padding:24px 0; position:sticky; top:0; height:100vh; overflow-y:auto; }
|
||||
.sidebar h2 { font-size:15px; padding:0 24px; margin-bottom:16px; color:var(--accent2); }
|
||||
.sidebar a { display:block; padding:7px 24px; font-size:13px; color:var(--muted); transition:.15s; border-left:2px solid transparent; }
|
||||
.sidebar a:hover, .sidebar a.active { color:var(--accent2); border-left-color:var(--accent); background:rgba(99,102,241,0.06); }
|
||||
.sidebar .section { font-size:11px; text-transform:uppercase; letter-spacing:1px; padding:16px 24px 6px; color:var(--muted); font-weight:600; }
|
||||
/* ===== 侧边栏 ===== */
|
||||
.sidebar {
|
||||
width:264px; min-width:264px; background:rgba(10,15,26,.8);
|
||||
border-right:1px solid var(--border); padding:22px 0;
|
||||
position:sticky; top:0; height:100vh; overflow-y:auto;
|
||||
backdrop-filter:blur(14px);
|
||||
}
|
||||
.sidebar::-webkit-scrollbar { width:5px; }
|
||||
.sidebar::-webkit-scrollbar-thumb { background:rgba(255,255,255,.12); border-radius:3px; }
|
||||
.sidebar-logo { font-size:16px; font-weight:800; padding:0 24px; margin-bottom:4px; }
|
||||
.grad-text { background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; color:transparent; }
|
||||
.sidebar .back { display:block; font-size:12.5px; padding:6px 24px; color:var(--muted); }
|
||||
.sidebar .back:hover { color:var(--accent2); }
|
||||
.sidebar a.snav {
|
||||
display:block; padding:6.5px 24px; font-size:13px; color:var(--muted);
|
||||
transition:.15s; border-left:2px solid transparent;
|
||||
}
|
||||
.sidebar a.snav:hover { color:var(--accent2); }
|
||||
.sidebar a.snav.active { color:var(--accent2); border-left-color:#a855f7; background:rgba(139,92,246,.08); }
|
||||
.sidebar .section {
|
||||
font-size:11px; text-transform:uppercase; letter-spacing:1.2px;
|
||||
padding:18px 24px 6px; color:var(--muted); font-weight:700; opacity:.75;
|
||||
}
|
||||
|
||||
.main { flex:1; padding:40px 48px; max-width:950px; }
|
||||
.main h1 { font-size:30px; font-weight:700; margin-bottom:8px; }
|
||||
.main h2 { font-size:22px; font-weight:700; margin:44px 0 14px; padding-top:22px; border-top:1px solid var(--border); }
|
||||
.main h3 { font-size:17px; font-weight:600; margin:28px 0 10px; color:var(--accent2); }
|
||||
/* ===== 主内容 ===== */
|
||||
.main { flex:1; padding:44px 52px; max-width:980px; min-width:0; }
|
||||
.main h1 { font-size:32px; font-weight:800; margin-bottom:6px; letter-spacing:-.5px; }
|
||||
.main .h1-desc { color:var(--muted); font-size:14px; margin-bottom:8px; }
|
||||
.main h2 {
|
||||
font-size:22px; font-weight:700; margin:52px 0 12px; padding-top:22px;
|
||||
border-top:1px solid var(--border); scroll-margin-top:24px;
|
||||
}
|
||||
.main h2::before { content:'#'; margin-right:8px; background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
|
||||
.main h3 { font-size:16.5px; font-weight:700; margin:26px 0 10px; color:var(--accent2); }
|
||||
.main p { margin:8px 0; color:var(--muted); font-size:14px; }
|
||||
.main table { width:100%; border-collapse:collapse; margin:14px 0 20px; font-size:13px; }
|
||||
.main p code { font-size:.9em; }
|
||||
.main table { width:100%; border-collapse:collapse; margin:14px 0 22px; font-size:13px; }
|
||||
.main th, .main td { text-align:left; padding:9px 14px; border-bottom:1px solid var(--border); vertical-align:top; }
|
||||
.main th { color:var(--muted); font-weight:500; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; }
|
||||
.main td:first-child { color:var(--accent2); font-family:monospace; font-size:12px; white-space:nowrap; }
|
||||
.main td:nth-child(3) { font-family:monospace; font-size:11px; color:var(--muted); }
|
||||
.badge { display:inline-block; padding:1px 7px; border-radius:3px; font-size:10px; font-weight:700; margin-left:6px; vertical-align:middle; }
|
||||
.badge-new { background:rgba(16,185,129,0.2); color:#34d399; }
|
||||
.hint { font-size:12px; color:var(--muted); margin-top:2px; }
|
||||
.return-box { background:rgba(16,185,129,0.08); border:1px solid rgba(16,185,129,0.2); border-radius:8px; padding:14px 18px; margin:12px 0; }
|
||||
.return-box h4 { font-size:12px; color:#34d399; margin-bottom:6px; text-transform:uppercase; letter-spacing:0.5px; }
|
||||
.main th { color:var(--muted); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.8px; }
|
||||
.main td:first-child { color:var(--accent); font-family:monospace; font-size:12px; white-space:nowrap; }
|
||||
.main td:nth-child(3) { font-family:monospace; font-size:11.5px; color:var(--muted); }
|
||||
.badge {
|
||||
display:inline-block; padding:1px 8px; border-radius:4px; font-size:10px;
|
||||
font-weight:700; margin-left:6px; vertical-align:middle;
|
||||
}
|
||||
.badge-new { background:rgba(16,185,129,.18); color:#34d399; border:1px solid rgba(16,185,129,.3); }
|
||||
.badge-v5 { background:rgba(168,85,247,.2); color:#d8b4fe; border:1px solid rgba(168,85,247,.4); }
|
||||
.hint { font-size:12.5px; color:var(--muted); margin-top:2px; }
|
||||
.return-box {
|
||||
background:rgba(16,185,129,.06); border:1px solid rgba(16,185,129,.2);
|
||||
border-radius:10px; padding:14px 18px; margin:14px 0;
|
||||
}
|
||||
.return-box h4 { font-size:11.5px; color:#34d399; margin-bottom:8px; text-transform:uppercase; letter-spacing:.8px; }
|
||||
|
||||
@media (max-width:768px) {
|
||||
.terminal {
|
||||
background:#0a0f1c; border:1px solid var(--border); border-radius:10px;
|
||||
overflow:hidden; margin:12px 0 20px;
|
||||
}
|
||||
.terminal-bar { display:flex; align-items:center; gap:7px; padding:9px 14px; background:rgba(255,255,255,0.03); border-bottom:1px solid var(--border); }
|
||||
.terminal-bar .t-dot { width:10px; height:10px; border-radius:50%; }
|
||||
.terminal-bar .t-dot:nth-child(1){ background:#ff5f57; } .terminal-bar .t-dot:nth-child(2){ background:#febc2e; } .terminal-bar .t-dot:nth-child(3){ background:#28c840; }
|
||||
.terminal-bar .t-title { margin-left:10px; font-size:11px; color:var(--muted); }
|
||||
.terminal pre { padding:16px 18px; overflow-x:auto; font:12.5px/1.75 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color:#cbd5e1; }
|
||||
.terminal pre .kw { color:#c084fc; } .terminal pre .fn { color:#60a5fa; } .terminal pre .s { color:#34d399; } .terminal pre .cm { color:#475569; }
|
||||
|
||||
@media (max-width:860px) {
|
||||
body { flex-direction:column; }
|
||||
.sidebar { width:100%; min-width:0; height:auto; position:static; border-right:none; border-bottom:1px solid var(--border); padding:12px 16px; }
|
||||
.sidebar a { display:inline-block; padding:4px 10px; border-left:none; }
|
||||
.main { padding:24px 20px; }
|
||||
.sidebar .section, .sidebar .back { padding:4px 8px; }
|
||||
.sidebar a.snav { display:inline-block; padding:4px 10px; border-left:none; font-size:12.5px; }
|
||||
.sidebar a.snav.active { border-left:none; border-radius:6px; }
|
||||
.main { padding:26px 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="sidebar">
|
||||
<h2>📘 MetonaToast 文档</h2>
|
||||
<a href="index.html">← 返回首页</a>
|
||||
<a href="demo.html">🎮 在线演示</a>
|
||||
<div class="sidebar-logo"><span class="grad-text">MetonaToast 文档</span></div>
|
||||
<a class="back" href="index.html">← 返回首页</a>
|
||||
<a class="back" href="demo.html">🎮 在线演示</a>
|
||||
<div class="section">基础通知</div>
|
||||
<a href="#show">show()</a>
|
||||
<a href="#success">success()</a>
|
||||
<a href="#error">error()</a>
|
||||
<a href="#warning">warning()</a>
|
||||
<a href="#info">info()</a>
|
||||
<a href="#loading">loading()</a>
|
||||
<a class="snav" href="#show">show()</a>
|
||||
<a class="snav" href="#success">success()</a>
|
||||
<a class="snav" href="#error">error()</a>
|
||||
<a class="snav" href="#warning">warning()</a>
|
||||
<a class="snav" href="#info">info()</a>
|
||||
<a class="snav" href="#loading">loading()</a>
|
||||
<div class="section">高级功能</div>
|
||||
<a href="#promise">promise()</a>
|
||||
<a href="#confirm">confirm()</a>
|
||||
<a href="#prompt">prompt()</a>
|
||||
<a href="#progress">progress()</a>
|
||||
<a href="#countdown">countdown()</a>
|
||||
<a href="#queue">queue()</a>
|
||||
<a href="#stack">stack()</a>
|
||||
<a href="#action">action()</a>
|
||||
<a class="snav" href="#promise">promise()</a>
|
||||
<a class="snav" href="#confirm">confirm()</a>
|
||||
<a class="snav" href="#prompt">prompt()</a>
|
||||
<a class="snav" href="#progress">progress()</a>
|
||||
<a class="snav" href="#countdown">countdown()</a>
|
||||
<a class="snav" href="#queue">queue()</a>
|
||||
<a class="snav" href="#stack">stack()</a>
|
||||
<a class="snav" href="#action">action()</a>
|
||||
<div class="section">管理方法</div>
|
||||
<a href="#group">group()</a>
|
||||
<a href="#dismiss">dismiss()</a>
|
||||
<a href="#clear">clear()</a>
|
||||
<a href="#configure">configure()</a>
|
||||
<a href="#use">use()</a>
|
||||
<a href="#destroy">destroy()</a>
|
||||
<div class="section">配置参考</div>
|
||||
<a href="#config">全部配置项</a>
|
||||
<a href="#callbacks">回调函数</a>
|
||||
<a href="#animations">动画列表</a>
|
||||
<a href="#themes">主题参考</a>
|
||||
<a class="snav" href="#group">group()</a>
|
||||
<a class="snav" href="#dismiss">dismiss()</a>
|
||||
<a class="snav" href="#clear">clear()</a>
|
||||
<a class="snav" href="#updatePosition">updatePosition()</a>
|
||||
<a class="snav" href="#remove">remove() / removeToast()</a>
|
||||
<a class="snav" href="#configure">configure()</a>
|
||||
<a class="snav" href="#use">use()</a>
|
||||
<a class="snav" href="#destroy">destroy()</a>
|
||||
<div class="section">扩展</div>
|
||||
<a class="snav" href="#hooks">钩子系统</a>
|
||||
<a class="snav" href="#react">React 适配器</a>
|
||||
<a class="snav" href="#config">全部配置项</a>
|
||||
<a class="snav" href="#callbacks">回调函数</a>
|
||||
<a class="snav" href="#animations">动画列表</a>
|
||||
<a class="snav" href="#themes">主题参考</a>
|
||||
</nav>
|
||||
|
||||
<main class="main">
|
||||
|
||||
<h1>API 文档</h1>
|
||||
<p>MetonaToast v0.2.1 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
|
||||
<p class="h1-desc">MetonaToast v0.5.0 完整 API 参考。所有基础通知方法支持字符串与对象两种调用形式,均可传入任何<a href="#config">配置项</a>作为可选参数。</p>
|
||||
|
||||
<!-- ===== 基础通知 ===== -->
|
||||
<h2 id="show">show(message, opts?)</h2>
|
||||
<p>显示默认类型的 Toast 通知,无特定颜色和图标。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串,或包含 title/message 等属性的配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置对象,覆盖 <a href="#config">全部配置项</a>。仅当 message 为字符串时有效</td></tr>
|
||||
</table>
|
||||
<p>显示默认类型 Toast,无特定颜色与图标。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">show.js</span></div>
|
||||
<pre><span class="cm">// 字符串形式</span>
|
||||
MeToast.<span class="fn">show</span>(<span class="s">'默认消息'</span>);
|
||||
MeToast.<span class="fn">show</span>(<span class="s">'自定义'</span>, { <span class="fn">duration</span>: 2000, <span class="fn">position</span>: <span class="s">'bottom-center'</span> });
|
||||
<span class="cm">// 对象形式(所有 opts 作为一级属性)</span>
|
||||
MeToast.<span class="fn">show</span>({ <span class="fn">title</span>: <span class="s">'标题'</span>, <span class="fn">message</span>: <span class="s">'内容'</span>, <span class="fn">duration</span>: 3000 });</pre>
|
||||
MeToast.<span class="fn">show</span>(<span class="s">'自定义'</span>, { <span class="fn">duration</span>: <span class="num">2000</span>, <span class="fn">position</span>: <span class="s">'bottom-center'</span> });
|
||||
<span class="cm">// 对象形式</span>
|
||||
MeToast.<span class="fn">show</span>({ <span class="fn">title</span>: <span class="s">'标题'</span>, <span class="fn">message</span>: <span class="s">'内容'</span>, <span class="fn">duration</span>: <span class="num">3000</span> });</pre></div>
|
||||
|
||||
<h2 id="success">success(message, opts?)</h2>
|
||||
<p>显示成功通知。绿色对勾图标 <code>#10b981</code>,type = success。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 success</td></tr>
|
||||
</table>
|
||||
<p>绿色对勾图标 <code>#10b981</code>,type = success。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">success.js</span></div>
|
||||
<pre>MeToast.<span class="fn">success</span>(<span class="s">'保存成功!'</span>);
|
||||
MeToast.<span class="fn">success</span>({ <span class="fn">title</span>: <span class="s">'已保存'</span>, <span class="fn">message</span>: <span class="s">'数据已同步'</span> });
|
||||
Met.<span class="fn">success</span>(<span class="s">'MeToast和Met等价'</span>);</pre>
|
||||
Met.<span class="fn">success</span>(<span class="s">'Met 与 MeToast 等价'</span>);</pre></div>
|
||||
|
||||
<h2 id="error">error(message, opts?)</h2>
|
||||
<p>显示错误通知。红色叉号图标 <code>#ef4444</code>,type = error。aria-live 设为 assertive。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 error</td></tr>
|
||||
</table>
|
||||
<p>红色叉号图标 <code>#ef4444</code>,type = error。aria-live = assertive。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">error.js</span></div>
|
||||
<pre>MeToast.<span class="fn">error</span>(<span class="s">'网络错误,请重试'</span>);
|
||||
MeToast.<span class="fn">error</span>({ <span class="fn">title</span>: <span class="s">'提交失败'</span>, <span class="fn">message</span>: <span class="s">'服务器不可达'</span> });</pre>
|
||||
MeToast.<span class="fn">error</span>({ <span class="fn">title</span>: <span class="s">'提交失败'</span>, <span class="fn">message</span>: <span class="s">'服务器不可达'</span> });</pre></div>
|
||||
|
||||
<h2 id="warning">warning(message, opts?)</h2>
|
||||
<p>显示警告通知。黄色三角图标 <code>#f59e0b</code>,type = warning。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 warning</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">warning</span>(<span class="s">'请注意检查输入内容'</span>);</pre>
|
||||
<p>黄色三角图标 <code>#f59e0b</code>,type = warning。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">warning.js</span></div>
|
||||
<pre>MeToast.<span class="fn">warning</span>(<span class="s">'请注意检查输入内容'</span>);</pre></div>
|
||||
|
||||
<h2 id="info">info(message, opts?)</h2>
|
||||
<p>显示信息通知。蓝色圆形图标 <code>#3b82f6</code>,type = info。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 info</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">info</span>(<span class="s">'系统将于 22:00 维护'</span>);</pre>
|
||||
<p>蓝色圆形图标 <code>#3b82f6</code>,type = info。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">info.js</span></div>
|
||||
<pre>MeToast.<span class="fn">info</span>(<span class="s">'系统将于 22:00 维护'</span>);</pre></div>
|
||||
|
||||
<h2 id="loading">loading(message, opts?)</h2>
|
||||
<p>显示加载状态。type = loading,duration 强制为 0(不自动关闭),closeButton 和 showProgress 强制为 false。返回 LoadingControl 对象。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>加载提示文本或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖</td></tr>
|
||||
</table>
|
||||
<p>type = loading,duration 强制 0(不自动关闭)。返回 LoadingControl。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">loading.js</span></div>
|
||||
<pre><span class="kw">const</span> loading = MeToast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
|
||||
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), 2000);</pre>
|
||||
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), <span class="num">2000</span>);</pre></div>
|
||||
<div class="return-box">
|
||||
<h4>返回 LoadingControl</h4>
|
||||
<h4>LoadingControl(链式转换 id 稳定)</h4>
|
||||
<table>
|
||||
<tr><th>方法</th><th>签名</th><th>说明</th></tr>
|
||||
<tr><td>success</td><td>(msg, opts?) => ToastInstance</td><td>关闭加载 toast,原地替换为 success 类型</td></tr>
|
||||
<tr><td>error</td><td>(msg, opts?) => ToastInstance</td><td>替换为 error 类型</td></tr>
|
||||
<tr><td>info</td><td>(msg, opts?) => ToastInstance</td><td>替换为 info 类型</td></tr>
|
||||
<tr><td>warning</td><td>(msg, opts?) => ToastInstance</td><td>替换为 warning 类型</td></tr>
|
||||
<tr><td>update</td><td>(partial) => MeToast</td><td>不关闭加载 toast,原地更新其内容(支持 resetTimerOnUpdate)</td></tr>
|
||||
<tr><td>dismiss</td><td>()</td><td>直接关闭加载 toast 不替换</td></tr>
|
||||
<tr><th>方法</th><th>说明</th></tr>
|
||||
<tr><td>success/error/info/warning</td><td>原地更新为对应类型(同一实例,id 不变),duration 自动恢复默认值</td></tr>
|
||||
<tr><td>update(partial)</td><td>原地更新内容,不改变类型</td></tr>
|
||||
<tr><td>dismiss()</td><td>直接关闭,不转换</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ===== 高级功能 ===== -->
|
||||
<h2 id="promise">promise(promise, opts)</h2>
|
||||
<p>监听 Promise 生命周期。自动显示 loading → 根据 resolve/reject 自动切换 success/error。返回原 Promise,支持 await 获取结果。</p>
|
||||
<p>监听 Promise 生命周期,自动 loading → resolve/reject 切换 success/error,返回原 Promise。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>promise</td><td>Promise</td><td>—</td><td>要监听的 Promise 对象。非 Promise 会打印错误并返回 rejected Promise</td></tr>
|
||||
<tr><td>opts.loading</td><td>string</td><td>"加载中..."</td><td>加载中显示的文本</td></tr>
|
||||
<tr><td>opts.success</td><td>string</td><td>"操作成功"</td><td>resolve 后显示的文本</td></tr>
|
||||
<tr><td>opts.error</td><td>string</td><td>"操作失败"</td><td>reject 后显示的文本</td></tr>
|
||||
<tr><th>参数</th><th>类型</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>promise</td><td>Promise</td><td>—</td><td>要监听的 Promise。非 Promise 打印错误并返回 rejected Promise</td></tr>
|
||||
<tr><td>opts.loading / success / error</td><td>string</td><td>加载中... / 操作成功 / 操作失败</td><td>各阶段文本</td></tr>
|
||||
</table>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">promise.js</span></div>
|
||||
<pre><span class="kw">try</span> {
|
||||
<span class="kw">await</span> MeToast.<span class="fn">promise</span>(<span class="fn">fetch</span>(<span class="s">'/api/data'</span>), {
|
||||
<span class="fn">loading</span>: <span class="s">'加载中...'</span>,
|
||||
<span class="fn">success</span>: <span class="s">'加载完成!'</span>,
|
||||
<span class="fn">error</span>: <span class="s">'加载失败'</span>,
|
||||
<span class="fn">loading</span>: <span class="s">'加载中...'</span>, <span class="fn">success</span>: <span class="s">'完成!'</span>, <span class="fn">error</span>: <span class="s">'失败'</span>,
|
||||
});
|
||||
} <span class="kw">catch</span> (<span class="fn">e</span>) { <span class="cm">/* promise reject 会继续抛出 */</span> }</pre>
|
||||
} <span class="kw">catch</span> (e) { <span class="cm">/* reject 会继续抛出 */</span> }</pre></div>
|
||||
|
||||
<h2 id="confirm">confirm(message, opts?)</h2>
|
||||
<p>确认对话框。返回 <code>Promise<boolean></code>。内置 10 秒安全超时,超时自动 resolve(false)。</p>
|
||||
<p>确认对话框,返回 <code>Promise<boolean></code>,内置 10 秒安全超时自动 resolve(false)。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string</td><td>—</td><td>对话框消息。非字符串会打印错误并 resolve(false)</td></tr>
|
||||
<tr><td>opts.confirmText</td><td>string</td><td>"确认"</td><td>确认按钮文字</td></tr>
|
||||
<tr><td>opts.confirmColor</td><td>string</td><td>"#10b981"</td><td>确认按钮背景色</td></tr>
|
||||
<tr><td>opts.cancelText</td><td>string</td><td>"取消"</td><td>取消按钮文字</td></tr>
|
||||
<tr><td>opts.cancelColor</td><td>string</td><td>"#6b7280"</td><td>取消按钮背景色</td></tr>
|
||||
<tr><td>opts.type</td><td>string</td><td>"warning"</td><td>Toast 类型(影响图标和颜色)</td></tr>
|
||||
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>confirmText / confirmColor</td><td>确认 / #10b981</td><td>确认按钮文字与颜色</td></tr>
|
||||
<tr><td>cancelText / cancelColor</td><td>取消 / #6b7280</td><td>取消按钮文字与颜色</td></tr>
|
||||
<tr><td>type</td><td>warning</td><td>Toast 类型</td></tr>
|
||||
</table>
|
||||
<pre><span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, {
|
||||
<span class="fn">confirmText</span>: <span class="s">'删除'</span>,
|
||||
<span class="fn">confirmColor</span>: <span class="s">'#ef4444'</span>,
|
||||
<span class="fn">cancelText</span>: <span class="s">'保留'</span>,
|
||||
});
|
||||
<span class="kw">if</span> (ok) MeToast.<span class="fn">success</span>(<span class="s">'已删除'</span>);</pre>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">confirm.js</span></div>
|
||||
<pre><span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span>, <span class="fn">confirmColor</span>: <span class="s">'#ef4444'</span> });
|
||||
<span class="kw">if</span> (ok) MeToast.<span class="fn">success</span>(<span class="s">'已删除'</span>);</pre></div>
|
||||
|
||||
<h2 id="prompt">prompt(message, opts?)</h2>
|
||||
<p>输入对话框。返回 <code>Promise<string|null></code>。按 Enter 或点击提交按钮返回输入值,取消返回 null。内置 10 秒安全超时。</p>
|
||||
<p>输入对话框,返回 <code>Promise<string|null></code>。Enter / 提交返回输入值,取消返回 null,10 秒超时。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string</td><td>—</td><td>对话框消息。非字符串会打印错误并 resolve(null)</td></tr>
|
||||
<tr><td>opts.placeholder</td><td>string</td><td>""</td><td>输入框占位文字</td></tr>
|
||||
<tr><td>opts.defaultValue</td><td>string</td><td>""</td><td>输入框默认值</td></tr>
|
||||
<tr><td>opts.inputType</td><td>string</td><td>"text"</td><td>input 标签 type 属性(如 password/email/number)</td></tr>
|
||||
<tr><td>opts.submitText</td><td>string</td><td>"确认"</td><td>提交按钮文字</td></tr>
|
||||
<tr><td>opts.submitColor</td><td>string</td><td>"#3b82f6"</td><td>提交按钮背景色</td></tr>
|
||||
<tr><td>opts.cancelText</td><td>string</td><td>"取消"</td><td>取消按钮文字</td></tr>
|
||||
<tr><td>opts.cancelColor</td><td>string</td><td>"#6b7280"</td><td>取消按钮背景色</td></tr>
|
||||
<tr><td>opts.type</td><td>string</td><td>"info"</td><td>Toast 类型</td></tr>
|
||||
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>placeholder / defaultValue</td><td>""</td><td>输入框占位与默认值</td></tr>
|
||||
<tr><td>inputType</td><td>text</td><td>input 的 type(password/email/number 等)</td></tr>
|
||||
<tr><td>submitText / submitColor</td><td>确认 / #3b82f6</td><td>提交按钮</td></tr>
|
||||
<tr><td>cancelText / cancelColor</td><td>取消 / #6b7280</td><td>取消按钮</td></tr>
|
||||
</table>
|
||||
<pre><span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>, {
|
||||
<span class="fn">placeholder</span>: <span class="s">'请输入...'</span>,
|
||||
<span class="fn">defaultValue</span>: <span class="s">'张三'</span>,
|
||||
<span class="fn">submitText</span>: <span class="s">'确定'</span>,
|
||||
});
|
||||
<span class="kw">if</span> (name) MeToast.<span class="fn">info</span>(<span class="s">'你好,'</span> + name);</pre>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">prompt.js</span></div>
|
||||
<pre><span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>, { <span class="fn">placeholder</span>: <span class="s">'请输入...'</span> });
|
||||
<span class="kw">if</span> (name) MeToast.<span class="fn">info</span>(<span class="s">'你好,'</span> + name);</pre></div>
|
||||
|
||||
<h2 id="progress">progress(message, opts?)</h2>
|
||||
<p>进度条通知。不自动关闭。返回 ProgressControl 对象以手动更新进度。</p>
|
||||
<p>进度条通知,不自动关闭。返回 ProgressControl。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>进度提示文本或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置。type 默认 info</td></tr>
|
||||
<tr><td>opts.progressColor</td><td>string</td><td>"#3b82f6"</td><td>进度条填充颜色</td></tr>
|
||||
<tr><th>方法</th><th>说明</th></tr>
|
||||
<tr><td>setProgress(percent)</td><td>设置进度 0~100,自动 clamp</td></tr>
|
||||
<tr><td>complete(message?)</td><td>跳 100%,300ms 后转 success,1s 后自动关闭</td></tr>
|
||||
<tr><td>error(message?)</td><td>转 error,2s 后自动关闭</td></tr>
|
||||
<tr><td>dismiss()</td><td>直接关闭</td></tr>
|
||||
</table>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">progress.js</span></div>
|
||||
<pre><span class="kw">const</span> p = MeToast.<span class="fn">progress</span>(<span class="s">'上传中...'</span>, { <span class="fn">progressColor</span>: <span class="s">'#10b981'</span> });
|
||||
p.<span class="fn">setProgress</span>(45); <span class="cm">// → 45%</span>
|
||||
p.<span class="fn">setProgress</span>(90); <span class="cm">// → 90%</span>
|
||||
p.<span class="fn">complete</span>(<span class="s">'上传完成!'</span>); <span class="cm">// → 100% → success</span>
|
||||
<span class="cm">// 或</span>
|
||||
p.<span class="fn">error</span>(<span class="s">'上传失败'</span>);
|
||||
p.<span class="fn">dismiss</span>();</pre>
|
||||
<div class="return-box">
|
||||
<h4>返回 ProgressControl</h4>
|
||||
<table>
|
||||
<tr><th>方法</th><th>签名</th><th>说明</th></tr>
|
||||
<tr><td>setProgress</td><td>(percent: number)</td><td>设置进度 0~100,自动 clamp。更新进度条宽度和百分比文字</td></tr>
|
||||
<tr><td>complete</td><td>(message?: string)</td><td>跳到 100%,300ms 后替换为 success toast,1s 后自动关闭</td></tr>
|
||||
<tr><td>error</td><td>(message?: string)</td><td>替换为 error toast,2s 后自动关闭</td></tr>
|
||||
<tr><td>dismiss</td><td>()</td><td>直接关闭</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
p.<span class="fn">setProgress</span>(<span class="num">45</span>);
|
||||
p.<span class="fn">complete</span>(<span class="s">'上传完成!'</span>);</pre></div>
|
||||
|
||||
<h2 id="countdown">countdown(message, seconds, opts?)</h2>
|
||||
<p>倒计时 Toast。<code>{seconds}</code> 占位符每秒自动替换为剩余秒数。</p>
|
||||
<p>倒计时 Toast,<code>{seconds}</code> 占位符每秒自动替换。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string</td><td>—</td><td>消息文本。支持 <code>{seconds}</code> 占位符。非字符串打印错误并返回空控制对象</td></tr>
|
||||
<tr><td>seconds</td><td>number</td><td>10</td><td>倒计时秒数,最小 1</td></tr>
|
||||
<tr><td>opts.onComplete</td><td>function</td><td>—</td><td>倒计时归零时的回调</td></tr>
|
||||
<tr><td>opts.type</td><td>string</td><td>"warning"</td><td>Toast 类型</td></tr>
|
||||
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>seconds</td><td>10</td><td>倒计时秒数,最小 1</td></tr>
|
||||
<tr><td>opts.onComplete</td><td>—</td><td>归零回调</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">countdown</span>(<span class="s">'{seconds} 秒后执行'</span>, 5, {
|
||||
<span class="fn">onComplete</span>: () => MeToast.<span class="fn">success</span>(<span class="s">'已执行'</span>),
|
||||
});</pre>
|
||||
<div class="return-box">
|
||||
<h4>返回 CountdownControl</h4>
|
||||
<table>
|
||||
<tr><th>方法</th><th>说明</th></tr>
|
||||
<tr><td>cancel()</td><td>清除计时器并关闭 toast</td></tr>
|
||||
<tr><td>pause()</td><td>暂停倒计时</td></tr>
|
||||
<tr><td>resume()</td><td>恢复倒计时</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">countdown.js</span></div>
|
||||
<pre>MeToast.<span class="fn">countdown</span>(<span class="s">'{seconds} 秒后执行'</span>, <span class="num">5</span>, { <span class="fn">onComplete</span>: () => MeToast.<span class="fn">success</span>(<span class="s">'已执行'</span>) });
|
||||
<span class="cm">// 返回 { cancel(), pause(), resume() }</span></pre></div>
|
||||
|
||||
<h2 id="queue">queue(messages, opts?)</h2>
|
||||
<p>顺序逐个显示消息队列。前一条关闭后延时显示下一条。返回 QueueControl(thenable + cancel)。</p>
|
||||
<p>顺序逐个显示消息队列,返回 thenable + cancel。支持 await。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>messages</td><td>Array<string|object></td><td>—</td><td>消息数组。可为字符串或带 message/type/duration/onClose 的对象。非数组打印错误并返回空控制对象</td></tr>
|
||||
<tr><td>opts.delay</td><td>number</td><td>1000</td><td>每条消息关闭后到显示下一条的间隔(ms)</td></tr>
|
||||
<tr><td>opts.duration</td><td>number</td><td>3000</td><td>每条消息显示时长(ms),可被消息级 duration 覆盖</td></tr>
|
||||
<tr><td>opts.onClose</td><td>function</td><td>—</td><td>全部队列完成后的回调。消息级 onClose 和队列级 onClose 都会依次调用</td></tr>
|
||||
<tr><td>opts.type</td><td>string</td><td>—</td><td>默认类型</td></tr>
|
||||
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>opts.delay</td><td>1000</td><td>每条关闭后到下一条的间隔(ms)</td></tr>
|
||||
<tr><td>opts.duration</td><td>3000</td><td>每条显示时长,可被消息级 duration 覆盖</td></tr>
|
||||
</table>
|
||||
<pre><span class="kw">const</span> q = MeToast.<span class="fn">queue</span>([
|
||||
<span class="s">'步骤一'</span>,
|
||||
{ <span class="fn">message</span>: <span class="s">'步骤二'</span>, <span class="fn">duration</span>: 5000, <span class="fn">type</span>: <span class="s">'warning'</span> },
|
||||
<span class="s">'步骤三'</span>,
|
||||
], { <span class="fn">delay</span>: 800, <span class="fn">duration</span>: 2000, <span class="fn">type</span>: <span class="s">'info'</span> });
|
||||
q.<span class="fn">cancel</span>(); <span class="cm">// 中途取消</span>
|
||||
<span class="kw">await</span> q; <span class="cm">// 等待完成(thenable 支持 await)</span></pre>
|
||||
<div class="return-box">
|
||||
<h4>返回 QueueControl (thenable)</h4>
|
||||
<table>
|
||||
<tr><th>方法</th><th>说明</th></tr>
|
||||
<tr><td>.then(fn, rj)</td><td>Promise.then 代理,支持 await</td></tr>
|
||||
<tr><td>.catch(rj)</td><td>Promise.catch 代理</td></tr>
|
||||
<tr><td>.cancel()</td><td>设置取消标志,不再显示下一条消息</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">queue.js</span></div>
|
||||
<pre><span class="kw">const</span> q = MeToast.<span class="fn">queue</span>([<span class="s">'步骤一'</span>, <span class="s">'步骤二'</span>, <span class="s">'步骤三'</span>], { <span class="fn">delay</span>: <span class="num">800</span>, <span class="fn">duration</span>: <span class="num">2000</span> });
|
||||
q.<span class="fn">cancel</span>(); <span class="cm">// 中途取消</span>
|
||||
<span class="kw">await</span> q; <span class="cm">// 等待完成</span></pre></div>
|
||||
|
||||
<h2 id="stack">stack(messages, opts?)</h2>
|
||||
<p>同时错峰显示多条消息。每条间隔 stagger 毫秒依次出现,全部叠加在屏幕上。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>messages</td><td>Array<string|object></td><td>—</td><td>消息数组。可为字符串或带 message/type/duration 的对象。非数组打印错误</td></tr>
|
||||
<tr><td>opts.stagger</td><td>number</td><td>100</td><td>每条消息之间的显示间隔(ms)</td></tr>
|
||||
<tr><td>opts.type</td><td>string</td><td>—</td><td>默认类型</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">stack</span>([<span class="s">'消息1'</span>, <span class="s">'消息2'</span>, { <span class="fn">message</span>: <span class="s">'警告'</span>, <span class="fn">type</span>: <span class="s">'warning'</span> }], {
|
||||
<span class="fn">stagger</span>: 150,
|
||||
<span class="fn">type</span>: <span class="s">'info'</span>,
|
||||
});</pre>
|
||||
<p>同时错峰显示多条消息。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">stack.js</span></div>
|
||||
<pre>MeToast.<span class="fn">stack</span>([<span class="s">'消息1'</span>, <span class="s">'消息2'</span>, { <span class="fn">message</span>: <span class="s">'警告'</span>, <span class="fn">type</span>: <span class="s">'warning'</span> }], { <span class="fn">stagger</span>: <span class="num">150</span> });</pre></div>
|
||||
|
||||
<h2 id="action">action(message, actions, opts?)</h2>
|
||||
<p>Action Toast:内嵌操作按钮。默认 duration=0 不自动关闭,closeButton=true。</p>
|
||||
<p>内嵌操作按钮。默认 duration=0 不自动关闭。点击按钮不会误触 closeOnClick(已隔离冒泡)。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
||||
<tr><td>actions</td><td>ActionButton[]</td><td>[]</td><td>按钮数组。每个按钮可配 text/onClick/color/style/close</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置。duration 默认 0,closeButton 默认 true</td></tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr><th>ActionButton 字段</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>text</td><td>string</td><td>—(必填)</td><td>按钮显示文字</td></tr>
|
||||
<tr><td>onClick</td><td>(toast) => void</td><td>—(必填)</td><td>点击回调函数,参数为当前 toast 实例</td></tr>
|
||||
<tr><td>color</td><td>string</td><td>"#6366f1"</td><td>按钮背景色</td></tr>
|
||||
<tr><td>style</td><td>object</td><td>{}</td><td>按钮内联样式,可覆盖 color(style.background 优先)</td></tr>
|
||||
<tr><td>close</td><td>boolean</td><td>true</td><td>点击后是否自动关闭 toast。设为 false 可多次点击</td></tr>
|
||||
<tr><th>ActionButton</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>text</td><td>—(必填)</td><td>按钮文字</td></tr>
|
||||
<tr><td>onClick</td><td>—(必填)</td><td>点击回调,参数为当前 toast</td></tr>
|
||||
<tr><td>color / style</td><td>#6366f1</td><td>背景色 / 内联样式(style.background 优先)</td></tr>
|
||||
<tr><td>close</td><td>true</td><td>点击后是否关闭。false 可多次点击</td></tr>
|
||||
</table>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">action.js</span></div>
|
||||
<pre>MeToast.<span class="fn">action</span>(<span class="s">'文件已删除'</span>, [
|
||||
{ <span class="fn">text</span>: <span class="s">'撤销'</span>, <span class="fn">onClick</span>: () => <span class="fn">restore</span>(), <span class="fn">color</span>: <span class="s">'#3b82f6'</span> },
|
||||
{ <span class="fn">text</span>: <span class="s">'查看详情'</span>, <span class="fn">onClick</span>: (t) => <span class="fn">openFile</span>(), <span class="fn">color</span>: <span class="s">'#10b981'</span>, <span class="fn">close</span>: false },
|
||||
]);</pre>
|
||||
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>(), <span class="fn">color</span>: <span class="s">'#10b981'</span>, <span class="fn">close</span>: <span class="kw">false</span> },
|
||||
]);</pre></div>
|
||||
|
||||
<!-- ===== 管理方法 ===== -->
|
||||
<h2 id="group">group(name)</h2>
|
||||
<p>创建 Toast 分组,返回 GroupAPI 对象。该对象所有方法自动传入 <code>group: name</code>,按组管理。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td>name</td><td>string</td><td>分组名称。GroupAPI 和 dismissGroup 通过此名称关联</td></tr>
|
||||
</table>
|
||||
<p>创建分组,返回 GroupAPI。所有方法自动注入 <code>group: name</code>。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">group.js</span></div>
|
||||
<pre><span class="kw">const</span> orders = MeToast.<span class="fn">group</span>(<span class="s">'orders'</span>);
|
||||
orders.<span class="fn">success</span>(<span class="s">'订单已创建'</span>);
|
||||
orders.<span class="fn">error</span>(<span class="s">'支付失败'</span>, { <span class="fn">duration</span>: 5000 });
|
||||
orders.<span class="fn">count</span>(); <span class="cm">// 该组当前 toast 数量</span>
|
||||
orders.<span class="fn">dismiss</span>(); <span class="cm">// 关闭该组全部 toast</span>
|
||||
<span class="cm">// 也支持主对象关闭</span>
|
||||
MeToast.<span class="fn">dismissGroup</span>(<span class="s">'orders'</span>);</pre>
|
||||
<div class="return-box">
|
||||
<h4>返回 GroupAPI</h4>
|
||||
<table>
|
||||
<tr><th>方法</th><th>说明</th></tr>
|
||||
<tr><td>show(msg, opts?)</td><td>等价 MeToast.show,自动注入 group</td></tr>
|
||||
<tr><td>success(msg, opts?)</td><td>等价 MeToast.success</td></tr>
|
||||
<tr><td>error(msg, opts?)</td><td>等价 MeToast.error</td></tr>
|
||||
<tr><td>warning(msg, opts?)</td><td>等价 MeToast.warning</td></tr>
|
||||
<tr><td>info(msg, opts?)</td><td>等价 MeToast.info</td></tr>
|
||||
<tr><td>loading(msg, opts?)</td><td>等价 MeToast.loading</td></tr>
|
||||
<tr><td>action(msg, actions, opts?)</td><td>等价 MeToast.action</td></tr>
|
||||
<tr><td>dismiss()</td><td>关闭该组所有 toast</td></tr>
|
||||
<tr><td>count()</td><td>返回该组当前 toast 数量</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
orders.<span class="fn">count</span>(); <span class="cm">// 该组数量</span>
|
||||
orders.<span class="fn">dismiss</span>(); <span class="cm">// 关闭整组</span>
|
||||
MeToast.<span class="fn">dismissGroup</span>(<span class="s">'orders'</span>);</pre></div>
|
||||
|
||||
<h2 id="dismiss">dismiss(id?)</h2>
|
||||
<p>关闭 Toast。无参数则关闭全部。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td>id</td><td>string</td><td>可选。Toast 的 id,不传则关闭所有</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">dismiss</span>(); <span class="cm">// 关闭所有</span>
|
||||
MeToast.<span class="fn">dismiss</span>(toast.id); <span class="cm">// 关闭指定</span></pre>
|
||||
<p>关闭 Toast。无参数关闭全部,传入 id 关闭指定。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">dismiss.js</span></div>
|
||||
<pre>MeToast.<span class="fn">dismiss</span>(); <span class="cm">// 全部</span>
|
||||
MeToast.<span class="fn">dismiss</span>(toast.id); <span class="cm">// 指定</span></pre></div>
|
||||
|
||||
<h2 id="clear">clear(position?)</h2>
|
||||
<p>按位置清除 Toast。不传参数清除所有位置。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td>position</td><td>string</td><td>可选。位置如 'top-right',不传则清除全部</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">clear</span>(); <span class="cm">// 全部</span>
|
||||
MeToast.<span class="fn">clear</span>(<span class="s">'bottom-right'</span>); <span class="cm">// 仅右下角</span></pre>
|
||||
<p>按位置清除。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">clear.js</span></div>
|
||||
<pre>MeToast.<span class="fn">clear</span>(); <span class="cm">// 全部</span>
|
||||
MeToast.<span class="fn">clear</span>(<span class="s">'bottom-right'</span>); <span class="cm">// 仅右下角</span></pre></div>
|
||||
|
||||
<h2 id="updatePosition">updatePosition(position) <span class="badge badge-v5">v0.3</span></h2>
|
||||
<p>运行时将 Toast 移动到新位置容器,立即生效。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">position.js</span></div>
|
||||
<pre><span class="kw">const</span> t = MeToast.<span class="fn">info</span>(<span class="s">'可移动的 Toast'</span>);
|
||||
t.<span class="fn">updatePosition</span>(<span class="s">'bottom-left'</span>);</pre></div>
|
||||
|
||||
<h2 id="remove">remove() / removeToast(id) <span class="badge badge-new">立即移除</span></h2>
|
||||
<p>立即从 DOM 和内存移除,<b>不触发离场动画</b>。适用于无动画快速清除。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">remove.js</span></div>
|
||||
<pre>t.<span class="fn">remove</span>(); <span class="cm">// 实例方法</span>
|
||||
MeToast.<span class="fn">removeToast</span>(t.id); <span class="cm">// 按 id(等价)</span></pre></div>
|
||||
|
||||
<h2 id="configure">configure(opts)</h2>
|
||||
<p>全局配置,影响后续所有 Toast。theme 和 locale 变化会触发相应副作用(应用主题 CSS / 切换语言)。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td>opts</td><td>object</td><td>包含任意 <a href="#config">配置项</a> 的对象</td></tr>
|
||||
</table>
|
||||
<p>全局配置,影响后续所有 Toast。theme/locale 变化触发副作用。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">configure.js</span></div>
|
||||
<pre>MeToast.<span class="fn">configure</span>({
|
||||
<span class="fn">position</span>: <span class="s">'top-right'</span>,
|
||||
<span class="fn">duration</span>: 4000,
|
||||
<span class="fn">theme</span>: <span class="s">'dark'</span>,
|
||||
<span class="fn">animation</span>: <span class="s">'slide'</span>,
|
||||
<span class="fn">locale</span>: <span class="s">'zh-CN'</span>,
|
||||
});</pre>
|
||||
<span class="fn">position</span>: <span class="s">'top-right'</span>, <span class="fn">duration</span>: <span class="num">4000</span>, <span class="fn">theme</span>: <span class="s">'dark'</span>,
|
||||
<span class="fn">animation</span>: <span class="s">'slide'</span>, <span class="fn">locale</span>: <span class="s">'zh-CN'</span>,
|
||||
});</pre></div>
|
||||
|
||||
<h2 id="use">use(plugin)</h2>
|
||||
<p>安装插件。支持字符串(内置预设名)或插件对象。已被拒绝注册的无效插件会打印错误。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td>plugin</td><td>string | object</td><td>预设名 'keyboard'/'persistence'/'accessibility' 或自定义插件对象 { name, version?, install, uninstall? }</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">use</span>(<span class="s">'keyboard'</span>); <span class="cm">// ESC 关闭所有 Toast</span>
|
||||
MeToast.<span class="fn">use</span>(<span class="s">'persistence'</span>); <span class="cm">// 配置自动保存到 localStorage</span>
|
||||
MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <span class="cm">// 屏幕阅读器实时朗读 toast 内容</span></pre>
|
||||
<p class="hint">内置插件详情:keyboard 监听 keydown Escape 键;persistence 将配置写入 localStorage 并在页面加载时恢复;accessibility 在 afterShow/afterUpdate 钩子中通过 aria-live 区域朗读 toast 内容。</p>
|
||||
<p>安装插件:字符串预设名或插件对象。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">use.js</span></div>
|
||||
<pre>MeToast.<span class="fn">use</span>(<span class="s">'keyboard'</span>); <span class="cm">// ESC 关闭所有</span>
|
||||
MeToast.<span class="fn">use</span>(<span class="s">'persistence'</span>); <span class="cm">// 配置持久化 localStorage</span>
|
||||
MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <span class="cm">// 屏幕阅读器朗读</span>
|
||||
MeToast.<span class="fn">use</span>(<span class="s">'dedupe'</span>); <span class="cm">// 相同 type+message 自动去重</span></pre></div>
|
||||
<p class="hint">dedupe:检测已有相同 toast,更新它并阻止重复弹出(beforeShow 钩子实现,支持 uninstall 卸载钩子)。</p>
|
||||
|
||||
<h2 id="destroy">destroy()</h2>
|
||||
<p>完全销毁。关闭所有 Toast、移除所有 DOM 容器和注入的样式标签、清空内存缓存。</p>
|
||||
<pre>MeToast.<span class="fn">destroy</span>();</pre>
|
||||
<p>完全销毁:关闭所有 Toast、移除全部 DOM 容器与注入样式、清空内存缓存与钩子。支持重复调用,<code>init()</code> 可恢复。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">destroy.js</span></div>
|
||||
<pre>MeToast.<span class="fn">destroy</span>();
|
||||
MeToast.<span class="fn">init</span>({ <span class="fn">config</span>: { <span class="fn">duration</span>: <span class="num">3000</span> } }); <span class="cm">// 恢复</span></pre></div>
|
||||
|
||||
<!-- ===== 钩子系统 ===== -->
|
||||
<h2 id="hooks">钩子系统 <span class="badge badge-v5">v0.3</span></h2>
|
||||
<p><code>Toast.on(name, handler)</code> 注册钩子并返回取消函数。<code>beforeShow</code> / <code>beforeClose</code> / <code>beforeUpdate</code> 的 handler 返回 <code>false</code> 可拦截对应操作。</p>
|
||||
<table>
|
||||
<tr><th>钩子</th><th>触发时机</th><th>拦截</th></tr>
|
||||
<tr><td>beforeInit / afterInit</td><td>init() 前后</td><td>—</td></tr>
|
||||
<tr><td>beforeDestroy / afterDestroy</td><td>destroy() 前后</td><td>—</td></tr>
|
||||
<tr><td>beforeShow / afterShow</td><td>显示前后</td><td>✅ 返回 false 阻止</td></tr>
|
||||
<tr><td>beforeClose / afterClose</td><td>关闭前后</td><td>✅ 返回 false 阻止</td></tr>
|
||||
<tr><td>beforeUpdate / afterUpdate</td><td>update() 前后</td><td>✅ 返回 false 阻止</td></tr>
|
||||
<tr><td>configChange</td><td>configure / updateConfig / resetConfig</td><td>—</td></tr>
|
||||
<tr><td>themeChange / localeChange</td><td>主题 / 语言切换</td><td>—</td></tr>
|
||||
<tr><td>click / hover</td><td>点击 / 悬停进出</td><td>—</td></tr>
|
||||
<tr><td>dragStart / dragEnd</td><td>拖拽开始 / 结束</td><td>—</td></tr>
|
||||
<tr><td>animationStart / animationEnd</td><td>入场动画开始 / 结束</td><td>—</td></tr>
|
||||
<tr><td>progressStart / progressEnd</td><td>倒计时开始 / 归零</td><td>—</td></tr>
|
||||
</table>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">hooks.js</span></div>
|
||||
<pre><span class="kw">import</span> MeToast, { <span class="fn">Toast</span> } <span class="kw">from</span> <span class="s">'@metona-team/metona-toast'</span>;
|
||||
|
||||
<span class="cm">// 拦截:非允许时段禁止错误提示</span>
|
||||
<span class="kw">const</span> off = <span class="fn">Toast</span>.<span class="fn">on</span>(<span class="s">'beforeShow'</span>, (toast) => {
|
||||
<span class="kw">if</span> (toast.type === <span class="s">'error'</span> && !isAllowed) <span class="kw">return</span> <span class="kw">false</span>;
|
||||
});
|
||||
<span class="cm">// ...</span>
|
||||
off(); <span class="cm">// 取消注册</span></pre></div>
|
||||
|
||||
<!-- ===== React ===== -->
|
||||
<h2 id="react">React 适配器 <span class="badge badge-v5">v0.4</span></h2>
|
||||
<p>主包保持零依赖。通过子路径 <code>@metona-team/metona-toast/react</code> 导入,<code>react</code> 为 optional peerDependency。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">react.tsx</span></div>
|
||||
<pre><span class="kw">import</span> { <span class="fn">useToast</span>, <span class="fn">Toast</span> } <span class="kw">from</span> <span class="s">'@metona-team/metona-toast/react'</span>;
|
||||
|
||||
<span class="kw">function</span> <span class="fn">SubmitButton</span>() {
|
||||
<span class="kw">const</span> toast = <span class="fn">useToast</span>(); <span class="cm">// 组件卸载自动清理本组件创建的 Toast</span>
|
||||
<span class="kw">const</span> submit = <span class="kw">async</span> () => {
|
||||
<span class="kw">const</span> loading = toast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
|
||||
<span class="kw">try</span> { <span class="kw">await</span> api(); loading.<span class="fn">success</span>(<span class="s">'完成!'</span>); }
|
||||
<span class="kw">catch</span> (e) { loading.<span class="fn">error</span>(<span class="s">'失败'</span>); }
|
||||
};
|
||||
<span class="kw">return</span> <button onClick={submit}>提交</button>;
|
||||
}
|
||||
|
||||
<span class="cm">// 声明式 Toast:props 变化更新,卸载自动移除(autoClose 默认 true)</span>
|
||||
<span class="kw">function</span> <span class="fn">SaveIndicator</span>({ saving }) {
|
||||
<span class="kw">return</span> saving ? <<span class="fn">Toast</span> type=<span class="s">"info"</span> message=<span class="s">"正在保存..."</span> /> : null;
|
||||
}</pre></div>
|
||||
|
||||
<!-- ===== 配置参考 ===== -->
|
||||
<h2 id="config">全部配置项</h2>
|
||||
<p>以下配置可用于 <code>configure()</code>、<code>init()</code> 或单个 Toast 方法的 opts 参数。</p>
|
||||
<p>可用于 <code>configure()</code>、<code>init()</code> 或单个 Toast 方法的 opts。</p>
|
||||
<table>
|
||||
<tr><th>配置项</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>position</td><td>string</td><td>'top-right'</td><td>位置:top-left / top-center / top-right / bottom-left / bottom-center / bottom-right</td></tr>
|
||||
<tr><td>duration</td><td>number</td><td>4000</td><td>显示时长(ms),0=不自动关闭</td></tr>
|
||||
<tr><td>max</td><td>number</td><td>6</td><td>同一位置最多同时显示条数,超出则关闭最早的</td></tr>
|
||||
<tr><td>gap</td><td>number</td><td>12</td><td>Toast 之间的间距(px)</td></tr>
|
||||
<tr><td>offset</td><td>number</td><td>24</td><td>容器到屏幕边缘的距离(px)</td></tr>
|
||||
<tr><td>pauseOnHover</td><td>boolean</td><td>true</td><td>鼠标悬停时暂停 duration 倒计时和进度条</td></tr>
|
||||
<tr><td>closeOnClick</td><td>boolean</td><td>true</td><td>点击 Toast 任意位置关闭。关闭按钮始终触发关闭</td></tr>
|
||||
<tr><td>draggable</td><td>boolean</td><td>true</td><td>允许拖拽关闭。拖拽超过 120px 触发关闭</td></tr>
|
||||
<tr><td>showProgress</td><td>boolean</td><td>true</td><td>显示 duration 倒计时进度条</td></tr>
|
||||
<tr><td>progressDirection</td><td>string</td><td>'horizontal'</td><td>进度条方向:horizontal(底部水平) / vertical(右侧垂直)</td></tr>
|
||||
<tr><td>icon</td><td>boolean</td><td>true</td><td>显示类型对应图标(success→对勾等)</td></tr>
|
||||
<tr><td>closeButton</td><td>boolean</td><td>true</td><td>显示右上角关闭 × 按钮</td></tr>
|
||||
<tr><td>theme</td><td>string</td><td>'auto'</td><td>主题:light / dark / auto(跟随系统)/ warm / 自定义注册名</td></tr>
|
||||
<tr><td>animation</td><td>string</td><td>'slide'</td><td>入场动画名称,支持 CSS 动画列表见下</td></tr>
|
||||
<tr><td>zIndex</td><td>number</td><td>9999</td><td>容器 CSS z-index</td></tr>
|
||||
<tr><td>width</td><td>number|string</td><td>360</td><td>Toast 宽度。数字表示 px</td></tr>
|
||||
<tr><td>className</td><td>string</td><td>''</td><td>附加到 Toast 元素上的 CSS 类名</td></tr>
|
||||
<tr><td>style</td><td>object</td><td>{}</td><td>附加到 Toast 元素上的内联样式对象</td></tr>
|
||||
<tr><td>locale</td><td>string</td><td>'zh-CN'</td><td>语言代码(zh-CN / en-US 等)</td></tr>
|
||||
<tr><td>resetTimerOnUpdate</td><td>boolean</td><td>false</td><td>调用 update() 时重置 duration 倒计时</td></tr>
|
||||
<tr><td>notifyWhenHidden</td><td>boolean</td><td>false</td><td>页面不可见时自动通过 Notification API 发送系统通知</td></tr>
|
||||
<tr><td>render</td><td>function</td><td>—</td><td>自定义渲染函数 <code>(toast) => htmlString</code>,完全接管 DOM 构建</td></tr>
|
||||
<tr><td>onError</td><td>function</td><td>—</td><td>全局错误回调 <code>({ hook, source, error, toast }) => void</code>,钩子异常或定时器错误时触发</td></tr>
|
||||
<tr><td>position</td><td>string</td><td>'top-right'</td><td>6 个位置之一(RTL 自动翻转)</td></tr>
|
||||
<tr><td>duration</td><td>number</td><td>4000</td><td>显示时长(ms),0 = 不自动关闭</td></tr>
|
||||
<tr><td>max</td><td>number</td><td>6</td><td>同位置最多条数,超出真正关闭最早的</td></tr>
|
||||
<tr><td>gap</td><td>number</td><td>12</td><td>Toast 间距(px)</td></tr>
|
||||
<tr><td>offset</td><td>number</td><td>24</td><td>容器距屏幕边缘(px)</td></tr>
|
||||
<tr><td>pauseOnHover</td><td>boolean</td><td>true</td><td>悬停暂停倒计时</td></tr>
|
||||
<tr><td>closeOnClick</td><td>boolean</td><td>true</td><td>点击关闭</td></tr>
|
||||
<tr><td>draggable</td><td>boolean</td><td>true</td><td>允许拖拽关闭</td></tr>
|
||||
<tr><td>dragThreshold</td><td>number</td><td>120</td><td>拖拽关闭阈值(px)</td></tr>
|
||||
<tr><td>showProgress</td><td>boolean</td><td>true</td><td>显示倒计时进度条</td></tr>
|
||||
<tr><td>progressDirection</td><td>string</td><td>'horizontal'</td><td>horizontal / vertical</td></tr>
|
||||
<tr><td>icon</td><td>boolean</td><td>true</td><td>显示类型图标</td></tr>
|
||||
<tr><td>closeButton</td><td>boolean</td><td>true</td><td>显示关闭 × 按钮</td></tr>
|
||||
<tr><td>theme</td><td>string</td><td>'auto'</td><td>light / dark / auto / warm / 自定义名</td></tr>
|
||||
<tr><td>animation</td><td>string</td><td>'slide'</td><td>11 种内置或自定义动画名</td></tr>
|
||||
<tr><td>zIndex</td><td>number</td><td>9999</td><td>容器 z-index</td></tr>
|
||||
<tr><td>width</td><td>number|string</td><td>360</td><td>宽度,数字表示 px</td></tr>
|
||||
<tr><td>className</td><td>string</td><td>''</td><td>附加 CSS 类名</td></tr>
|
||||
<tr><td>style</td><td>object</td><td>{}</td><td>附加内联样式</td></tr>
|
||||
<tr><td>locale</td><td>string</td><td>'zh-CN'</td><td>语言代码</td></tr>
|
||||
<tr><td>resetTimerOnUpdate</td><td>boolean</td><td>false</td><td>update() 时重置倒计时</td></tr>
|
||||
<tr><td>notifyWhenHidden</td><td>boolean</td><td>false</td><td>页面不可见时发系统通知</td></tr>
|
||||
<tr><td>render</td><td>function</td><td>—</td><td>自定义渲染函数,完全接管 DOM</td></tr>
|
||||
<tr><td>onBeforeShow</td><td>function</td><td>—</td><td>返回 false 阻止显示</td></tr>
|
||||
<tr><td>onError</td><td>function</td><td>—</td><td>钩子/定时器异常全局回调</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="callbacks">回调函数</h2>
|
||||
<table>
|
||||
<tr><th>回调</th><th>签名</th><th>触发时机</th></tr>
|
||||
<tr><td>onShow</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 创建并播放入场动画后</td></tr>
|
||||
<tr><td>onClose</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 被移除后(离场动画完成时)</td></tr>
|
||||
<tr><td>onClick</td><td>(toast: ToastInstance) => void</td><td>Toast 被点击时(closeOnClick 为 true 时还会自动关闭)</td></tr>
|
||||
<tr><td>onUpdate</td><td>(toast: ToastInstance) => void</td><td>Toast 内容通过 update() 更新后</td></tr>
|
||||
<tr><td>onError</td><td>({ hook, source, error, toast }) => void</td><td>钩子回调或定时器发生异常时(全局配置,适用于接入错误监控)</td></tr>
|
||||
<tr><td>onBeforeShow</td><td>(toast) => boolean | void</td><td>DOM 创建前,返回 false 阻止显示</td></tr>
|
||||
<tr><td>onShow</td><td>(toast) => void</td><td>创建并播放入场动画后</td></tr>
|
||||
<tr><td>onClose</td><td>(toast) => void</td><td>DOM 移除后(离场完成)</td></tr>
|
||||
<tr><td>onClick</td><td>(toast) => void</td><td>点击时(closeOnClick=true 时还会关闭)</td></tr>
|
||||
<tr><td>onUpdate</td><td>(toast) => void</td><td>update() 后</td></tr>
|
||||
<tr><td>onError</td><td>({ hook, source, error, toast }) => void</td><td>钩子/定时器异常时</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="animations">动画列表</h2>
|
||||
<p>配置 <code>animation</code> 的值,支持以下 CSS 动画。未在此列表中的值将 fallback 到 slide。</p>
|
||||
<p>未注册的动画名自动 fallback 到 slide。</p>
|
||||
<table>
|
||||
<tr><th>名称</th><th>效果</th><th>时长</th></tr>
|
||||
<tr><td>slide</td><td>从右侧滑入 + 回弹</td><td>400ms</td></tr>
|
||||
@@ -458,20 +452,13 @@ MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <sp
|
||||
<tr><td>flip</td><td>3D 翻转入场 + 回摆</td><td>500ms</td></tr>
|
||||
<tr><td>rotate</td><td>旋转摇摆进入</td><td>500ms</td></tr>
|
||||
<tr><td>zoom</td><td>从中心爆发式弹出</td><td>500ms</td></tr>
|
||||
<tr><td>slideUp</td><td>从下方弹入</td><td>400ms</td></tr>
|
||||
<tr><td>slideDown</td><td>从上方弹入</td><td>400ms</td></tr>
|
||||
<tr><td>slideLeft</td><td>从左侧滑入</td><td>400ms</td></tr>
|
||||
<tr><td>slideRight</td><td>从右侧滑入</td><td>400ms</td></tr>
|
||||
<tr><td>slideUp / slideDown</td><td>从下方 / 上方弹入</td><td>400ms</td></tr>
|
||||
<tr><td>slideLeft / slideRight</td><td>从左侧 / 右侧滑入</td><td>400ms</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="themes">主题参考</h2>
|
||||
<pre><span class="cm">// 内置主题</span>
|
||||
light <span class="cm">— 白色玻璃质感</span>
|
||||
dark <span class="cm">— 深色玻璃质感</span>
|
||||
auto <span class="cm">— 跟随系统主题设置</span>
|
||||
warm <span class="cm">— 暖色调</span>
|
||||
|
||||
<span class="cm">// 注册自定义主题</span>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">theme.js</span></div>
|
||||
<pre><span class="cm">// 内置主题:light · dark · auto(跟随系统)· warm</span>
|
||||
MeToast.themes.<span class="fn">registerTheme</span>(<span class="s">'ocean'</span>, {
|
||||
<span class="fn">bg</span>: <span class="s">'rgba(240,249,255,0.96)'</span>,
|
||||
<span class="fn">text</span>: <span class="s">'#0c4a6e'</span>,
|
||||
@@ -481,19 +468,38 @@ MeToast.themes.<span class="fn">registerTheme</span>(<span class="s">'ocean'</sp
|
||||
<span class="fn">progressBg</span>: <span class="s">'rgba(14,165,233,0.1)'</span>,
|
||||
<span class="fn">closeHoverBg</span>: <span class="s">'rgba(14,165,233,0.1)'</span>,
|
||||
});
|
||||
MeToast.themes.<span class="fn">switchTheme</span>(<span class="s">'ocean'</span>);</pre>
|
||||
MeToast.themes.<span class="fn">switchTheme</span>(<span class="s">'ocean'</span>);</pre></div>
|
||||
|
||||
</main>
|
||||
|
||||
<script src="../dist/metona-toast.js"></script>
|
||||
<script>
|
||||
MeToast.configure({ position: 'top-right', duration: 4000, theme: 'dark', animation: 'slide' });
|
||||
document.querySelectorAll('.sidebar a[href^="#"]').forEach(a => {
|
||||
a.addEventListener('click', () => {
|
||||
document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
|
||||
a.classList.add('active');
|
||||
});
|
||||
|
||||
// 侧边栏滚动高亮
|
||||
const links = document.querySelectorAll('.sidebar a.snav');
|
||||
const sections = [];
|
||||
links.forEach(l => {
|
||||
const id = l.getAttribute('href').slice(1);
|
||||
const el = document.getElementById(id);
|
||||
if (el) sections.push({ id, el, link: l });
|
||||
});
|
||||
const spy = new IntersectionObserver(entries => {
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting) {
|
||||
links.forEach(l => l.classList.remove('active'));
|
||||
const hit = sections.find(s => s.el === e.target);
|
||||
if (hit) hit.link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}, { rootMargin: '-15% 0px -70% 0px' });
|
||||
sections.forEach(s => spy.observe(s.el));
|
||||
|
||||
// 点击关闭高亮
|
||||
links.forEach(l => l.addEventListener('click', () => {
|
||||
links.forEach(x => x.classList.remove('active'));
|
||||
l.classList.add('active');
|
||||
}));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+355
-146
@@ -5,62 +5,231 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<title>MetonaToast — 轻量精致的 Toast 通知库</title>
|
||||
<meta name="description" content="MetonaToast 是轻量、零依赖、精致美观的 Toast 通知库。80+ 图标类型、11 种动画、主题系统、国际化、插件架构。">
|
||||
<meta name="description" content="MetonaToast 是轻量、零依赖、精致美观的 Toast 通知库。107 种图标、11 种动画、主题系统、国际化、插件架构、React 适配器。">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b1121; --bg2: #111827; --surface: #1a2332;
|
||||
--border: #1e2d4d; --text: #e2e8f0; --muted: #64748b;
|
||||
--accent: #6366f1; --accent2: #818cf8; --radius: 14px;
|
||||
--bg: #070b14;
|
||||
--surface: rgba(255,255,255,0.03);
|
||||
--surface-hover: rgba(255,255,255,0.06);
|
||||
--border: rgba(255,255,255,0.08);
|
||||
--text: #e8ecf4;
|
||||
--muted: #8b94a7;
|
||||
--accent1: #6366f1;
|
||||
--accent2: #a855f7;
|
||||
--accent3: #ec4899;
|
||||
--grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
|
||||
--radius: 16px;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; background: var(--bg); color: var(--text); line-height: 1.7; }
|
||||
a { color: var(--accent2); text-decoration: none; }
|
||||
code { background: rgba(99,102,241,0.12); color: #a5b4fc; padding: 2px 7px; border-radius: 5px; font-size: 0.9em; }
|
||||
pre { background: #0a0f1a; border: 1px solid var(--border); border-radius: 10px; padding: 18px 22px; overflow-x: auto; font: 13px/1.8 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color: #cbd5e1; }
|
||||
pre .kw { color: #c084fc; } pre .fn { color: #60a5fa; } pre .s { color: #34d399; } pre .cm { color: #475569; }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
html { scroll-behavior:smooth; }
|
||||
body {
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
|
||||
background:var(--bg); color:var(--text); line-height:1.7; overflow-x:hidden;
|
||||
background-image:
|
||||
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(99,102,241,0.15), transparent),
|
||||
radial-gradient(ellipse 60% 40% at 90% 10%, rgba(168,85,247,0.08), transparent),
|
||||
radial-gradient(ellipse 50% 40% at 10% 30%, rgba(236,72,153,0.06), transparent);
|
||||
}
|
||||
/* 背景网格 */
|
||||
body::before {
|
||||
content:''; position:fixed; inset:0; pointer-events:none; z-index:0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px);
|
||||
background-size:56px 56px;
|
||||
mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 40%, transparent 100%);
|
||||
-webkit-mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 40%, transparent 100%);
|
||||
}
|
||||
a { color:#a5b4fc; text-decoration:none; transition:color .2s; }
|
||||
a:hover { color:#c7d2fe; }
|
||||
::selection { background:rgba(139,92,246,.4); }
|
||||
|
||||
.nav { position: sticky; top: 0; z-index: 100; background: rgba(11,17,33,0.88); backdrop-filter: blur(14px); border-bottom: 1px solid var(--border); }
|
||||
.nav-inner { max-width: 1200px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; height: 56px; }
|
||||
.nav-logo { font-size: 18px; font-weight: 700; background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
||||
.nav-links { display: flex; gap: 24px; }
|
||||
.nav-links a { color: var(--muted); font-size: 14px; font-weight: 500; transition: .2s; }
|
||||
.nav-links a:hover { color: var(--accent2); }
|
||||
.grad-text {
|
||||
background:var(--grad);
|
||||
-webkit-background-clip:text; background-clip:text;
|
||||
-webkit-text-fill-color:transparent; color:transparent;
|
||||
}
|
||||
|
||||
.hero { text-align: center; padding: 100px 20px 70px; background: radial-gradient(ellipse at 50% 0%, rgba(99,102,241,0.18) 0%, transparent 60%), linear-gradient(180deg, #0f172a 0%, var(--bg) 100%); border-bottom: 1px solid var(--border); }
|
||||
.hero h1 { font-size: 52px; font-weight: 800; }
|
||||
.hero h1 span { background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
||||
.hero p { color: var(--muted); font-size: 17px; margin-top: 12px; max-width: 600px; margin-left: auto; margin-right: auto; }
|
||||
.hero .badges { display: flex; gap: 12px; justify-content: center; margin-top: 24px; flex-wrap: wrap; }
|
||||
.hero .badges span { background: var(--surface); border: 1px solid var(--border); border-radius: 20px; padding: 6px 18px; font-size: 13px; color: var(--accent2); }
|
||||
.hero-actions { margin-top: 32px; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
|
||||
.btn { padding: 11px 28px; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: .2s; border: none; }
|
||||
.btn-primary { background: var(--accent); color: #fff; }
|
||||
.btn-primary:hover { background: var(--accent2); transform: translateY(-1px); }
|
||||
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
||||
.btn-outline:hover { border-color: var(--accent); color: var(--accent2); }
|
||||
/* ===== Nav ===== */
|
||||
.nav {
|
||||
position:sticky; top:0; z-index:100;
|
||||
background:rgba(7,11,20,0.7);
|
||||
backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px);
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
.nav-inner {
|
||||
max-width:1180px; margin:0 auto; display:flex; align-items:center;
|
||||
justify-content:space-between; padding:0 28px; height:60px;
|
||||
}
|
||||
.nav-logo { font-size:19px; font-weight:800; letter-spacing:.2px; cursor:pointer; }
|
||||
.nav-links { display:flex; gap:26px; }
|
||||
.nav-links a { color:var(--muted); font-size:14px; font-weight:500; position:relative; }
|
||||
.nav-links a::after {
|
||||
content:''; position:absolute; left:0; right:100%; bottom:-6px; height:2px;
|
||||
background:var(--grad); border-radius:2px; transition:right .25s;
|
||||
}
|
||||
.nav-links a:hover::after { right:0; }
|
||||
.nav-links a:hover { color:var(--text); }
|
||||
|
||||
.container { max-width: 1100px; margin: 0 auto; padding: 60px 24px; }
|
||||
.section-title { font-size: 28px; font-weight: 700; text-align: center; margin-bottom: 8px; }
|
||||
.section-desc { color: var(--muted); text-align: center; margin-bottom: 40px; font-size: 15px; }
|
||||
/* ===== Hero ===== */
|
||||
.hero { text-align:center; padding:110px 24px 60px; position:relative; z-index:1; }
|
||||
.hero-badge {
|
||||
display:inline-flex; align-items:center; gap:8px;
|
||||
background:rgba(99,102,241,0.1); border:1px solid rgba(99,102,241,0.3);
|
||||
color:#a5b4fc; font-size:12px; font-weight:600; letter-spacing:1px;
|
||||
padding:6px 16px; border-radius:999px; margin-bottom:26px;
|
||||
}
|
||||
.hero-badge .dot {
|
||||
width:7px; height:7px; border-radius:50%; background:#34d399;
|
||||
box-shadow:0 0 12px #34d399; animation:pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse { 50% { opacity:.4; } }
|
||||
.hero h1 { font-size:clamp(42px, 7vw, 76px); font-weight:800; letter-spacing:-1px; line-height:1.1; }
|
||||
.hero h1 .grad-text { animation:shimmer 6s linear infinite; }
|
||||
@keyframes shimmer {
|
||||
0%,100% { filter:hue-rotate(0deg); }
|
||||
50% { filter:hue-rotate(30deg); }
|
||||
}
|
||||
.hero-sub {
|
||||
color:var(--muted); font-size:clamp(15px, 2.2vw, 18px);
|
||||
max-width:640px; margin:18px auto 0;
|
||||
}
|
||||
.hero-badges { display:flex; gap:10px; justify-content:center; margin-top:26px; flex-wrap:wrap; }
|
||||
.hero-badges span {
|
||||
background:var(--surface); border:1px solid var(--border); border-radius:999px;
|
||||
padding:5px 14px; font-size:12.5px; color:#cbd5e1;
|
||||
transition:border-color .2s, transform .2s;
|
||||
}
|
||||
.hero-badges span:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); }
|
||||
.hero-actions { margin-top:36px; display:flex; gap:14px; justify-content:center; flex-wrap:wrap; }
|
||||
.btn {
|
||||
padding:12px 30px; border-radius:12px; font-size:14.5px; font-weight:600;
|
||||
cursor:pointer; transition:transform .2s, box-shadow .2s, border-color .2s;
|
||||
border:1px solid transparent;
|
||||
}
|
||||
.btn-primary {
|
||||
background:var(--grad); color:#fff;
|
||||
box-shadow:0 8px 30px -8px rgba(139,92,246,.6);
|
||||
}
|
||||
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 12px 40px -8px rgba(139,92,246,.8); }
|
||||
.btn-ghost { background:var(--surface); border-color:var(--border); color:var(--text); }
|
||||
.btn-ghost:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); background:var(--surface-hover); }
|
||||
|
||||
.features { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 18px; }
|
||||
.feature { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; transition: .2s; }
|
||||
.feature:hover { border-color: var(--accent); }
|
||||
.feature-icon { font-size: 28px; margin-bottom: 12px; }
|
||||
.feature h3 { font-size: 16px; margin-bottom: 6px; }
|
||||
.feature p { color: var(--muted); font-size: 13px; line-height: 1.6; }
|
||||
/* 浮动光球 */
|
||||
.orb { position:absolute; border-radius:50%; filter:blur(70px); pointer-events:none; z-index:0; }
|
||||
.orb-1 { width:340px; height:340px; background:rgba(99,102,241,.25); top:-80px; left:12%; animation:float 12s ease-in-out infinite; }
|
||||
.orb-2 { width:260px; height:260px; background:rgba(236,72,153,.18); top:40px; right:10%; animation:float 15s ease-in-out infinite reverse; }
|
||||
.orb-3 { width:200px; height:200px; background:rgba(168,85,247,.2); bottom:-40px; left:40%; animation:float 18s ease-in-out infinite 2s; }
|
||||
@keyframes float { 0%,100%{transform:translateY(0) translateX(0);} 50%{transform:translateY(-30px) translateX(20px);} }
|
||||
|
||||
.code-block { max-width: 750px; margin: 0 auto; }
|
||||
/* ===== 数据统计 ===== */
|
||||
.stats { position:relative; z-index:1; max-width:1000px; margin:10px auto 0; padding:0 24px; }
|
||||
.stats-grid {
|
||||
display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:1px;
|
||||
background:var(--border); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden;
|
||||
}
|
||||
.stat { background:rgba(10,15,26,.85); padding:22px 12px; text-align:center; backdrop-filter:blur(8px); }
|
||||
.stat-num { font-size:26px; font-weight:800; }
|
||||
.stat-label { font-size:12px; color:var(--muted); margin-top:2px; letter-spacing:.5px; }
|
||||
|
||||
.theme-demo { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin: 24px 0; }
|
||||
.theme-btn { padding: 10px 20px; border-radius: 8px; border: 1px solid var(--border); cursor: pointer; background: var(--surface); color: var(--text); font-size: 13px; font-weight: 500; transition: .2s; }
|
||||
.theme-btn:hover { border-color: var(--accent); }
|
||||
/* ===== 通用 Section ===== */
|
||||
.container { position:relative; z-index:1; max-width:1100px; margin:0 auto; padding:80px 24px; }
|
||||
.section-title { font-size:30px; font-weight:800; text-align:center; letter-spacing:-.5px; }
|
||||
.section-desc { color:var(--muted); text-align:center; margin-top:8px; font-size:15px; }
|
||||
|
||||
footer { text-align: center; padding: 40px 24px; color: var(--muted); font-size: 13px; border-top: 1px solid var(--border); }
|
||||
/* ===== 特性卡片 ===== */
|
||||
.features { display:grid; grid-template-columns:repeat(auto-fill, minmax(300px, 1fr)); gap:16px; margin-top:44px; }
|
||||
.feature {
|
||||
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
|
||||
padding:26px 24px; transition:transform .25s, border-color .25s, background .25s;
|
||||
position:relative; overflow:hidden;
|
||||
}
|
||||
.feature::before {
|
||||
content:''; position:absolute; top:0; left:0; right:0; height:2px;
|
||||
background:var(--grad); opacity:0; transition:opacity .25s;
|
||||
}
|
||||
.feature:hover { transform:translateY(-4px); border-color:rgba(129,140,248,.4); background:var(--surface-hover); }
|
||||
.feature:hover::before { opacity:1; }
|
||||
.feature-icon { font-size:26px; margin-bottom:12px; display:inline-block; }
|
||||
.feature h3 { font-size:15.5px; margin-bottom:6px; font-weight:700; }
|
||||
.feature p { color:var(--muted); font-size:13px; line-height:1.65; }
|
||||
.feature code {
|
||||
background:rgba(99,102,241,0.15); color:#a5b4fc;
|
||||
padding:1px 6px; border-radius:5px; font-size:12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero h1 { font-size: 32px; }
|
||||
.nav-links { gap: 14px; }
|
||||
/* ===== 代码块 ===== */
|
||||
.code-block { max-width:760px; margin:36px auto 0; }
|
||||
.terminal {
|
||||
background:#0a0f1c; border:1px solid var(--border); border-radius:12px; overflow:hidden;
|
||||
box-shadow:0 20px 60px -20px rgba(0,0,0,.6);
|
||||
}
|
||||
.terminal-bar {
|
||||
display:flex; align-items:center; gap:7px; padding:11px 16px;
|
||||
background:rgba(255,255,255,0.03); border-bottom:1px solid var(--border);
|
||||
}
|
||||
.terminal-bar .t-dot { width:11px; height:11px; border-radius:50%; }
|
||||
.terminal-bar .t-dot:nth-child(1){ background:#ff5f57; } .terminal-bar .t-dot:nth-child(2){ background:#febc2e; } .terminal-bar .t-dot:nth-child(3){ background:#28c840; }
|
||||
.terminal-bar .t-title { margin-left:10px; font-size:11.5px; color:var(--muted); letter-spacing:.5px; }
|
||||
.terminal pre {
|
||||
padding:20px 22px; overflow-x:auto;
|
||||
font:13px/1.8 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace;
|
||||
color:#cbd5e1;
|
||||
}
|
||||
.terminal pre .kw { color:#c084fc; } .terminal pre .fn { color:#60a5fa; }
|
||||
.terminal pre .s { color:#34d399; } .terminal pre .cm { color:#475569; }
|
||||
.terminal pre .num { color:#f472b6; }
|
||||
|
||||
/* ===== 主题预览 ===== */
|
||||
.theme-demo { display:flex; gap:12px; justify-content:center; flex-wrap:wrap; margin-top:36px; }
|
||||
.theme-btn {
|
||||
padding:11px 22px; border-radius:10px; border:1px solid var(--border);
|
||||
cursor:pointer; background:var(--surface); color:var(--text);
|
||||
font-size:13.5px; font-weight:500; transition:all .2s;
|
||||
}
|
||||
.theme-btn:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); }
|
||||
.theme-btn.active {
|
||||
background:var(--grad); color:#fff; border-color:transparent;
|
||||
box-shadow:0 6px 24px -6px rgba(139,92,246,.6);
|
||||
}
|
||||
|
||||
/* ===== 环境支持 ===== */
|
||||
.env-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(160px, 1fr)); gap:12px; margin-top:40px; }
|
||||
.env-card {
|
||||
background:var(--surface); border:1px solid var(--border); border-radius:12px;
|
||||
padding:20px 14px; text-align:center; transition:border-color .2s, transform .2s;
|
||||
}
|
||||
.env-card:hover { border-color:rgba(129,140,248,.4); transform:translateY(-3px); }
|
||||
.env-card .e-ico { font-size:26px; }
|
||||
.env-card h3 { font-size:14px; margin:8px 0 2px; }
|
||||
.env-card p { font-size:12.5px; color:var(--muted); }
|
||||
|
||||
/* ===== CTA ===== */
|
||||
.cta {
|
||||
max-width:760px; margin:0 auto; text-align:center;
|
||||
background:var(--grad); border-radius:20px; padding:56px 32px;
|
||||
position:relative; overflow:hidden;
|
||||
}
|
||||
.cta::before {
|
||||
content:''; position:absolute; inset:0;
|
||||
background:radial-gradient(ellipse 60% 60% at 50% 120%, rgba(255,255,255,.25), transparent);
|
||||
}
|
||||
.cta h2 { font-size:30px; font-weight:800; color:#fff; position:relative; }
|
||||
.cta p { color:rgba(255,255,255,.85); margin-top:10px; position:relative; }
|
||||
.cta .btn { margin-top:26px; background:#fff; color:#6d28d9; position:relative; }
|
||||
.cta .btn:hover { transform:translateY(-2px); }
|
||||
|
||||
footer { text-align:center; padding:44px 24px; color:var(--muted); font-size:13px; border-top:1px solid var(--border); position:relative; z-index:1; }
|
||||
footer a { color:#a5b4fc; }
|
||||
|
||||
/* 滚动淡入 */
|
||||
.reveal { opacity:0; transform:translateY(24px); transition:opacity .6s ease, transform .6s ease; }
|
||||
.reveal.visible { opacity:1; transform:none; }
|
||||
|
||||
@media (max-width:768px) {
|
||||
.hero { padding:80px 20px 44px; }
|
||||
.nav-links { gap:16px; }
|
||||
.container { padding:56px 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -68,7 +237,7 @@
|
||||
|
||||
<nav class="nav">
|
||||
<div class="nav-inner">
|
||||
<div class="nav-logo">MetonaToast</div>
|
||||
<div class="nav-logo"><span class="grad-text">MetonaToast</span></div>
|
||||
<div class="nav-links">
|
||||
<a href="index.html">首页</a>
|
||||
<a href="docs.html">文档</a>
|
||||
@@ -79,117 +248,117 @@
|
||||
</nav>
|
||||
|
||||
<header class="hero">
|
||||
<h1><span>MetonaToast</span></h1>
|
||||
<p>轻量 · 零依赖 · 精致美观的 Toast 通知库。TypeScript 严格模式源码,gzip 不到 10KB。</p>
|
||||
<div class="badges">
|
||||
<span>📦 <10KB gzip</span><span>🎨 107 图标</span><span>🎭 11 动画</span><span>🌍 国际化</span><span>🔌 插件</span><span>📘 TypeScript</span>
|
||||
<div class="orb orb-1"></div>
|
||||
<div class="orb orb-2"></div>
|
||||
<div class="orb orb-3"></div>
|
||||
<div class="hero-badge"><span class="dot"></span> v0.5.0 · 385 测试 · 覆盖率 96%</div>
|
||||
<h1><span class="grad-text">MetonaToast</span></h1>
|
||||
<p class="hero-sub">轻量 · 零依赖 · 精致美观的 Toast 通知库。TypeScript 严格模式,gzip 不到 10KB,一切开箱即用。</p>
|
||||
<div class="hero-badges">
|
||||
<span>📦 <10KB gzip</span>
|
||||
<span>🎨 107 图标</span>
|
||||
<span>🎭 11 动画</span>
|
||||
<span>🧩 4 插件</span>
|
||||
<span>🔗 17 钩子</span>
|
||||
<span>⚛️ React 适配器</span>
|
||||
<span>🌍 国际化</span>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<button class="btn btn-primary" onclick="demoQuickStart()">快速预览</button>
|
||||
<button class="btn btn-outline" onclick="location.href='docs.html'">查看文档</button>
|
||||
<button class="btn btn-primary" onclick="demoQuickStart()">✨ 快速预览</button>
|
||||
<button class="btn btn-ghost" onclick="location.href='docs.html'">📖 查看文档</button>
|
||||
<button class="btn btn-ghost" onclick="location.href='demo.html'">🎮 在线演示</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="stats reveal">
|
||||
<div class="stats-grid">
|
||||
<div class="stat"><div class="stat-num grad-text"><10KB</div><div class="stat-label">gzip 体积</div></div>
|
||||
<div class="stat"><div class="stat-num grad-text">107</div><div class="stat-label">内置图标</div></div>
|
||||
<div class="stat"><div class="stat-num grad-text">11</div><div class="stat-label">动画效果</div></div>
|
||||
<div class="stat"><div class="stat-num grad-text">17</div><div class="stat-label">生命周期钩子</div></div>
|
||||
<div class="stat"><div class="stat-num grad-text">385</div><div class="stat-label">测试用例</div></div>
|
||||
<div class="stat"><div class="stat-num grad-text">96%</div><div class="stat-label">行覆盖率</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main class="container">
|
||||
<h2 class="section-title">核心特性</h2>
|
||||
<p class="section-desc">一切你需要的,都在一个不到 10KB 的包中</p>
|
||||
<h2 class="section-title reveal">核心特性</h2>
|
||||
<p class="section-desc reveal">你需要的一切,都在一个不到 10KB 的包中</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">⚡</div>
|
||||
<h3>零依赖</h3>
|
||||
<p>TypeScript 严格模式,纯原生实现,无任何运行时依赖。兼容所有现代浏览器。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🎨</div>
|
||||
<h3>107 种图标类型</h3>
|
||||
<p>success / error / warning / info / loading 以及 100+ 扩展类型,覆盖所有常见场景。</p>
|
||||
<p>success / error / warning / info / loading 及 100+ 扩展类型,覆盖所有常见场景。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🎭</div>
|
||||
<h3>11 种动画效果</h3>
|
||||
<p>slide / fade / scale / bounce / flip / rotate / zoom / slideUp / slideDown / slideLeft / slideRight,每种感官差异明显。</p>
|
||||
<h3>11 种动画 + 自定义</h3>
|
||||
<p>每种感官差异明显,<code>animations.register()</code> 注册自定义动画,动态注入 keyframes 真实生效。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🖌️</div>
|
||||
<h3>主题系统</h3>
|
||||
<p>内置 light / dark / auto / warm 四种主题,支持 <code>registerTheme()</code> 自定义注册任意主题。</p>
|
||||
<p>light / dark / auto / warm 内置主题,<code>registerTheme()</code> 自定义,跟随系统自动切换。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🌍</div>
|
||||
<h3>国际化</h3>
|
||||
<p>内置 zh-CN 和 en-US 完整翻译,400+ 词汇。支持 <code>addTranslations()</code> 扩展任意语言。</p>
|
||||
<p>zh-CN / en-US 完整翻译,<code>addTranslations()</code> 扩展任意语言,RTL 完整适配。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">🔌</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🧩</div>
|
||||
<h3>插件系统</h3>
|
||||
<p>3 款预设插件 + 自定义插件架构。keyboard / persistence / accessibility 开箱即用。</p>
|
||||
<p>4 款预设:keyboard / persistence / accessibility / dedupe,支持自定义插件架构。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🔗</div>
|
||||
<h3>17 个钩子</h3>
|
||||
<p>完整生命周期钩子,<code>beforeShow</code> / <code>beforeClose</code> / <code>beforeUpdate</code> 支持拦截。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">✋</div>
|
||||
<h3>拖拽关闭</h3>
|
||||
<p>任意方向拖动 Toast 即可关闭。支持 Pointer Events,移动端和桌面端体验一致。</p>
|
||||
<p>任意方向拖动即关,阈值 <code>dragThreshold</code> 可配置。Pointer Events 全端一致。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">⚡</div>
|
||||
<h3>零依赖</h3>
|
||||
<p>TypeScript 严格模式,纯原生实现,无需任何运行时依赖。兼容所有现代浏览器。</p>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">⚛️</div>
|
||||
<h3>React 适配器</h3>
|
||||
<p><code>@metona-team/metona-toast/react</code> — useToast hook + 声明式 Toast 组件,主包保持零依赖。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🛡️</div>
|
||||
<h3>错误捕获</h3>
|
||||
<p>全局 <code>onError</code> 回调可捕获钩子异常和定时器错误,方便接入 Sentry、DataDog 等监控系统。</p>
|
||||
<p>全局 <code>onError</code> 回调捕获钩子与定时器异常,方便接入 Sentry、DataDog。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">⏱️</div>
|
||||
<h3>暂停恢复</h3>
|
||||
<p>悬停自动暂停、拖拽临时暂停、手动 pause/resume,剩余时间精确保持,不丢失进度。</p>
|
||||
<p>悬停暂停、拖拽暂停、手动 pause/resume,剩余时间精确保持不丢失。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">📘</div>
|
||||
<h3>TypeScript</h3>
|
||||
<p>严格模式源码,类型从源码自动生成。strictNullChecks、noImplicitAny 全开,零类型漂移。</p>
|
||||
<h3>类型安全</h3>
|
||||
<p>严格模式源码,类型自动生成零漂移。加载态转换 id 稳定,原地更新。</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<section class="container">
|
||||
<h2 class="section-title">环境支持</h2>
|
||||
<p class="section-desc">所有现代浏览器原生支持,无需 polyfill</p>
|
||||
<div class="env-grid" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(165px,1fr)); gap:12px;">
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">🌐</div>
|
||||
<h3>Chrome</h3>
|
||||
<p>64+</p>
|
||||
</div>
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">🦊</div>
|
||||
<h3>Firefox</h3>
|
||||
<p>63+</p>
|
||||
</div>
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">🧭</div>
|
||||
<h3>Safari</h3>
|
||||
<p>11+</p>
|
||||
</div>
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">🪟</div>
|
||||
<h3>Edge</h3>
|
||||
<p>79+</p>
|
||||
</div>
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">📦</div>
|
||||
<h3>Node.js</h3>
|
||||
<p>16+</p>
|
||||
</div>
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">📘</div>
|
||||
<h3>TypeScript</h3>
|
||||
<p>4.5+</p>
|
||||
</div>
|
||||
</div>
|
||||
<p style="text-align:center;color:var(--muted);font-size:13px;margin-top:16px">核心依赖:Web Animations API · Pointer Events · CSS Grid · CSS Custom Properties</p>
|
||||
</section>
|
||||
|
||||
<section class="container">
|
||||
<h2 class="section-title">快速开始</h2>
|
||||
<p class="section-desc">浏览器 window 上仅注册两个全局变量:<code>MeToast</code> 和 <code>Met</code>,完全等价</p>
|
||||
<div class="code-block">
|
||||
<pre><span class="cm"># npm 安装</span>
|
||||
<section class="container" style="padding-top:0">
|
||||
<h2 class="section-title reveal">快速开始</h2>
|
||||
<p class="section-desc reveal">浏览器上仅注册两个全局变量:<code>MeToast</code> 和 <code>Met</code>,完全等价</p>
|
||||
<div class="code-block reveal">
|
||||
<div class="terminal">
|
||||
<div class="terminal-bar">
|
||||
<span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span>
|
||||
<span class="t-title">quickstart.js</span>
|
||||
</div>
|
||||
<pre><span class="cm"># 安装</span>
|
||||
npm install @metona-team/metona-toast
|
||||
|
||||
<span class="cm">// 导入</span>
|
||||
@@ -202,43 +371,45 @@ MeToast.<span class="fn">loading</span>(<span class="s">'加载中...'</span>);
|
||||
|
||||
<span class="cm">// CDN 直接引用</span>
|
||||
<span class="kw"><script</span> <span class="fn">src</span>=<span class="s">"https://git.metona.cn/MetonaTeam/MetonaToast/raw/branch/master/dist/metona-toast.min.js"</span><span class="kw">></script></span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container">
|
||||
<h2 class="section-title">高级功能速览</h2>
|
||||
<p class="section-desc">不仅仅是一个 Toast——确认框、输入框、进度条、倒计时、队列,应有尽有</p>
|
||||
<div class="code-block">
|
||||
<pre><span class="cm">// Promise 风格 — 自动 loading → success/error</span>
|
||||
<section class="container" style="padding-top:0">
|
||||
<h2 class="section-title reveal">高级功能速览</h2>
|
||||
<p class="section-desc reveal">不仅仅是 Toast — 确认框、输入框、进度条、倒计时、队列、Action,应有尽有</p>
|
||||
<div class="code-block reveal">
|
||||
<div class="terminal">
|
||||
<div class="terminal-bar">
|
||||
<span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span>
|
||||
<span class="t-title">advanced.js</span>
|
||||
</div>
|
||||
<pre><span class="cm">// Promise 风格 — 自动 loading → success/error</span>
|
||||
<span class="kw">await</span> MeToast.<span class="fn">promise</span>(<span class="fn">fetch</span>(<span class="s">'/api/data'</span>), {
|
||||
<span class="fn">loading</span>: <span class="s">'加载中...'</span>,
|
||||
<span class="fn">success</span>: <span class="s">'加载完成!'</span>,
|
||||
<span class="fn">error</span>: <span class="s">'加载失败'</span>,
|
||||
<span class="fn">loading</span>: <span class="s">'加载中...'</span>, <span class="fn">success</span>: <span class="s">'完成'</span>, <span class="fn">error</span>: <span class="s">'失败'</span>
|
||||
});
|
||||
|
||||
<span class="cm">// 确认对话框 → Promise<boolean></span>
|
||||
<span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除吗?'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span> });
|
||||
|
||||
<span class="cm">// 输入对话框 → Promise<string|null></span>
|
||||
<span class="cm">// 确认 / 输入对话框</span>
|
||||
<span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span> });
|
||||
<span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>);
|
||||
|
||||
<span class="cm">// Action Toast — 内嵌操作按钮</span>
|
||||
MeToast.<span class="fn">action</span>(<span class="s">'文件已删除'</span>, [
|
||||
{ <span class="fn">text</span>: <span class="s">'撤销'</span>, <span class="fn">onClick</span>: () => <span class="fn">restore</span>() },
|
||||
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>() },
|
||||
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>(), <span class="fn">close</span>: <span class="kw">false</span> },
|
||||
]);
|
||||
|
||||
<span class="cm">// Toast 分组管理</span>
|
||||
<span class="kw">const</span> orderGroup = MeToast.<span class="fn">group</span>(<span class="s">'orders'</span>);
|
||||
orderGroup.<span class="fn">success</span>(<span class="s">'订单已创建'</span>);
|
||||
orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭整组</span></pre>
|
||||
<span class="cm">// 加载 → 链式转换(id 稳定)</span>
|
||||
<span class="kw">const</span> loading = MeToast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
|
||||
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), <span class="num">2000</span>);</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container">
|
||||
<h2 class="section-title">主题切换</h2>
|
||||
<p class="section-desc">点击按钮实时预览四种内置主题</p>
|
||||
<div class="theme-demo">
|
||||
<section class="container" style="padding-top:0">
|
||||
<h2 class="section-title reveal">主题切换</h2>
|
||||
<p class="section-desc reveal">点击按钮实时预览内置主题,切换结果自动持久化</p>
|
||||
<div class="theme-demo reveal">
|
||||
<button class="theme-btn" onclick="switchTheme('light')">☀️ 浅色 Light</button>
|
||||
<button class="theme-btn" onclick="switchTheme('dark')">🌙 深色 Dark</button>
|
||||
<button class="theme-btn" onclick="switchTheme('auto')">🔄 自动 Auto</button>
|
||||
@@ -246,8 +417,34 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container" style="padding-top:0">
|
||||
<h2 class="section-title reveal">环境支持</h2>
|
||||
<p class="section-desc reveal">所有现代浏览器原生支持,无需 polyfill</p>
|
||||
<div class="env-grid reveal">
|
||||
<div class="env-card"><div class="e-ico">🌐</div><h3>Chrome</h3><p>64+</p></div>
|
||||
<div class="env-card"><div class="e-ico">🦊</div><h3>Firefox</h3><p>63+</p></div>
|
||||
<div class="env-card"><div class="e-ico">🧭</div><h3>Safari</h3><p>11+</p></div>
|
||||
<div class="env-card"><div class="e-ico">🪟</div><h3>Edge</h3><p>79+</p></div>
|
||||
<div class="env-card"><div class="e-ico">📱</div><h3>iOS Safari</h3><p>11+</p></div>
|
||||
<div class="env-card"><div class="e-ico">🤖</div><h3>Android</h3><p>64+</p></div>
|
||||
<div class="env-card"><div class="e-ico">📦</div><h3>Node.js</h3><p>16+</p></div>
|
||||
<div class="env-card"><div class="e-ico">📘</div><h3>TypeScript</h3><p>4.5+</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container" style="padding-top:0">
|
||||
<div class="cta reveal">
|
||||
<h2>开始使用 MetonaToast</h2>
|
||||
<p>一条命令安装,立即体验精致优雅的 Toast 通知</p>
|
||||
<button class="btn" onclick="location.href='demo.html'">🎮 打开在线演示</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>MetonaToast v0.2.1 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
|
||||
MetonaToast v0.5.0 · MIT License ·
|
||||
<a href="https://git.metona.cn/MetonaTeam/MetonaToast" target="_blank">Gitea</a> ·
|
||||
<a href="docs.html">文档</a> ·
|
||||
<a href="demo.html">演示</a>
|
||||
</footer>
|
||||
|
||||
<script src="../dist/metona-toast.js"></script>
|
||||
@@ -256,16 +453,28 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
|
||||
|
||||
function demoQuickStart() {
|
||||
MeToast.success('✨ 欢迎使用 MetonaToast!');
|
||||
setTimeout(() => MeToast.info('💡 试试点击右上角的 Toast 来关闭它'), 800);
|
||||
setTimeout(() => MeToast.warning('⚠️ 你也可以拖动 Toast 来关闭'), 1600);
|
||||
setTimeout(() => MeToast.info('💡 点击 Toast 或点击 × 即可关闭'), 800);
|
||||
setTimeout(() => MeToast.warning('⚠️ 拖动 Toast 也能关闭', { draggable: true }), 1600);
|
||||
setTimeout(() => MeToast.success('🎭 试试 bounce 动画', { animation: 'bounce' }), 2400);
|
||||
}
|
||||
|
||||
function switchTheme(t) {
|
||||
MeToast.themes.switchTheme(t);
|
||||
MeToast.configure({ theme: t });
|
||||
document.querySelectorAll('.theme-btn').forEach(b => b.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
const names = { light: '浅色', dark: '深色', auto: '自动', warm: '暖色' };
|
||||
MeToast.info('已切换到「' + names[t] + '」主题', { theme: t });
|
||||
}
|
||||
|
||||
// 滚动淡入
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); observer.unobserve(e.target); } });
|
||||
}, { threshold: 0.12 });
|
||||
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
|
||||
|
||||
// 页面加载自动演示
|
||||
setTimeout(demoQuickStart, 600);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+62
-1
@@ -4,12 +4,71 @@
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
import { ANIMATIONS } from './constants.js';
|
||||
import { ANIMATIONS, ANIMATION_TYPES } from './constants.js';
|
||||
import type { AnimationConfig, AnimationUtils } from './types.js';
|
||||
|
||||
// 动画缓存
|
||||
const animationMap: Map<string, AnimationConfig> = new Map();
|
||||
|
||||
// 内置动画(keyframes 由 styles.ts 注入,无需动态生成)
|
||||
const BUILTIN_ANIMATIONS: Set<string> = new Set(ANIMATION_TYPES);
|
||||
|
||||
// 动态注入的自定义动画样式(惰性创建)
|
||||
let animStyleElement: HTMLStyleElement | null = null;
|
||||
const injectedAnimations: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* 将 CSS 属性对象转换为内联 CSS 字符串
|
||||
*/
|
||||
const toCss = (props: Record<string, string | number>): string =>
|
||||
Object.entries(props)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join(';');
|
||||
|
||||
/**
|
||||
* 为自定义动画生成入场 keyframes + 类规则
|
||||
*/
|
||||
const buildAnimationCSS = (name: string, config: AnimationConfig): string => {
|
||||
const enter = toCss(config.enter);
|
||||
return `
|
||||
@keyframes met-${name}-in {
|
||||
from { ${enter}; }
|
||||
}
|
||||
.met-anim-${name}.met-toast { opacity: 0; }
|
||||
.met-anim-${name}.met-toast.met-show {
|
||||
animation: met-${name}-in ${config.duration}ms ${config.easing} forwards;
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 确保自定义动画的 CSS 已注入文档(幂等,内置动画自动跳过)
|
||||
*/
|
||||
export const ensureAnimationCSS = (name: string): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
if (BUILTIN_ANIMATIONS.has(name)) return;
|
||||
|
||||
const config = animationMap.get(name);
|
||||
if (!config || injectedAnimations.has(name)) return;
|
||||
|
||||
if (!animStyleElement) {
|
||||
animStyleElement = document.getElementById('metona-toast-anim-styles') as HTMLStyleElement | null;
|
||||
if (!animStyleElement) {
|
||||
animStyleElement = document.createElement('style');
|
||||
animStyleElement.id = 'metona-toast-anim-styles';
|
||||
document.head.appendChild(animStyleElement);
|
||||
}
|
||||
}
|
||||
|
||||
animStyleElement.textContent += buildAnimationCSS(name, config);
|
||||
injectedAnimations.add(name);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查动画是否存在(内置或自定义)
|
||||
*/
|
||||
export const hasAnimation = (name: string): boolean => animationMap.has(name);
|
||||
|
||||
// 注册默认动画
|
||||
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
||||
animationMap.set(name, {
|
||||
@@ -70,6 +129,8 @@ export const animationUtils: AnimationUtils = {
|
||||
|
||||
destroy(): void {
|
||||
animationMap.clear();
|
||||
injectedAnimations.clear();
|
||||
animStyleElement = null;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+63
-30
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* MetonaToast API — meToast 核心 API 对象
|
||||
* @module api
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { Toast, _containerCache } from './toast.js';
|
||||
import { DEFAULTS } from './constants.js';
|
||||
import { escapeHTML } from './utils.js';
|
||||
import { DEFAULTS, VERSION } from './constants.js';
|
||||
import { t, setCurrentLocale } from './i18n.js';
|
||||
import { applyTheme } from './themes.js';
|
||||
import { confirmHTML, promptHTML, progressHTML, actionHTML } from './templates.js';
|
||||
@@ -48,16 +47,13 @@ const normalizeArgs = (args: unknown[], defaultType = 'default'): ToastOptions =
|
||||
};
|
||||
};
|
||||
|
||||
/** 版本号常量 */
|
||||
const VERSION = '0.2.1';
|
||||
|
||||
/**
|
||||
* meToast API 对象
|
||||
*/
|
||||
const meToast: MeToast = {
|
||||
_toasts: new Map<string, ToastInstance>(),
|
||||
_config: { ...DEFAULTS } as unknown as ToastConfig,
|
||||
version: '0.2.1',
|
||||
version: VERSION,
|
||||
|
||||
configure(opts: Partial<ToastConfig>): MeToast {
|
||||
if (!opts || typeof opts !== 'object') return this;
|
||||
@@ -71,6 +67,7 @@ const meToast: MeToast = {
|
||||
setCurrentLocale(opts.locale);
|
||||
}
|
||||
|
||||
Toast.trigger('configChange');
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -80,7 +77,10 @@ const meToast: MeToast = {
|
||||
|
||||
const t = new Toast(merged);
|
||||
t.create();
|
||||
this._toasts.set(t.id, t);
|
||||
// beforeShow/onBeforeShow 拦截(返回 false)时 el 为 null,不注册幽灵实例
|
||||
if (t.el !== null) {
|
||||
this._toasts.set(t.id, t);
|
||||
}
|
||||
|
||||
return t;
|
||||
},
|
||||
@@ -161,15 +161,22 @@ const meToast: MeToast = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* loading 链式转换 — 原地 update 同一实例(id 稳定,不重建 DOM)
|
||||
* duration 从 0 恢复为默认值,使转换后的 toast 自动关闭
|
||||
*/
|
||||
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null {
|
||||
const id = loadingToast.id;
|
||||
const old = this._toasts.get(id);
|
||||
const old = this._toasts.get(loadingToast.id);
|
||||
if (!old) return null;
|
||||
|
||||
const position = old.config.position;
|
||||
old.close();
|
||||
old.update({
|
||||
...opts,
|
||||
type,
|
||||
message,
|
||||
duration: opts.duration ?? (old.config.duration || this._config.duration),
|
||||
} as ToastOptions);
|
||||
|
||||
return this._emit({ ...opts, type, message, position } as ToastOptions);
|
||||
return old;
|
||||
},
|
||||
|
||||
confirm(message: string, opts: ConfirmOptions = {}): Promise<boolean> {
|
||||
@@ -387,8 +394,10 @@ const meToast: MeToast = {
|
||||
actions.forEach((a, i) => {
|
||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`) as HTMLElement | null;
|
||||
if (btn && typeof a.onClick === 'function') {
|
||||
btn.addEventListener('click', () => {
|
||||
try { a.onClick(toast); } catch (e) { console.error('Action onClick error:', e); }
|
||||
btn.addEventListener('click', (e: Event) => {
|
||||
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||
e.stopPropagation();
|
||||
try { a.onClick(toast); } catch (err) { console.error('Action onClick error:', err); }
|
||||
if (a.close !== false) toast.close();
|
||||
});
|
||||
}
|
||||
@@ -588,6 +597,9 @@ const meToast: MeToast = {
|
||||
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
||||
|
||||
init(options: InitOptions = {}): MeToast {
|
||||
Toast.trigger('beforeInit');
|
||||
this._destroyed = false;
|
||||
|
||||
if (options.config) {
|
||||
this.configure(options.config);
|
||||
}
|
||||
@@ -599,9 +611,11 @@ const meToast: MeToast = {
|
||||
}
|
||||
if (options.plugins && Array.isArray(options.plugins)) {
|
||||
options.plugins.forEach((p) => {
|
||||
this.use(p as string);
|
||||
this.use(p);
|
||||
});
|
||||
}
|
||||
|
||||
Toast.trigger('afterInit');
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -623,6 +637,7 @@ const meToast: MeToast = {
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast {
|
||||
if (config && typeof config === 'object') {
|
||||
Object.assign(this._config, config);
|
||||
Toast.trigger('configChange');
|
||||
}
|
||||
return this;
|
||||
},
|
||||
@@ -631,6 +646,7 @@ const meToast: MeToast = {
|
||||
const keys = Object.keys(this._config);
|
||||
keys.forEach(k => delete (this._config as Record<string, unknown>)[k]);
|
||||
Object.assign(this._config, DEFAULTS);
|
||||
Toast.trigger('configChange');
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -643,22 +659,30 @@ const meToast: MeToast = {
|
||||
}
|
||||
this.plugins.register(plugin, { ...preset, ...options });
|
||||
|
||||
// 连接插件钩子
|
||||
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||
const stored = this.plugins.get(plugin) as (Record<string, unknown> & Plugin) | null;
|
||||
if (plugin === 'accessibility') {
|
||||
Toast.on('afterShow', (toast: ToastInstance) => {
|
||||
const acc = preset as Record<string, (t: ToastInstance) => void>;
|
||||
if (typeof acc.announce === 'function') acc.announce(toast);
|
||||
});
|
||||
if (stored && typeof stored._off === 'function') { (stored._off as () => void)(); }
|
||||
const acc = preset as Record<string, (t: ToastInstance) => void>;
|
||||
if (stored) {
|
||||
stored._off = Toast.on('afterShow', (toast: ToastInstance) => {
|
||||
if (typeof acc.announce === 'function') acc.announce(toast);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (plugin === 'persistence') {
|
||||
if (stored && typeof stored._saveOff === 'function') { (stored._saveOff as () => void)(); }
|
||||
const saved = typeof preset.install === 'function'
|
||||
? preset.install(this.plugins as unknown as import('./types.js').PluginManager)
|
||||
: null;
|
||||
if (saved) this.configure(saved as Partial<ToastConfig>);
|
||||
Toast.on('afterClose', () => {
|
||||
const p = preset as Record<string, (c: unknown) => void>;
|
||||
if (typeof p.save === 'function') p.save(this.getConfig());
|
||||
});
|
||||
const p = preset as Record<string, (c: unknown) => void>;
|
||||
if (stored) {
|
||||
stored._saveOff = Toast.on('afterClose', () => {
|
||||
if (typeof p.save === 'function') p.save(this.getConfig());
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (plugin && typeof plugin === 'object') {
|
||||
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options } as Plugin);
|
||||
@@ -670,6 +694,8 @@ const meToast: MeToast = {
|
||||
destroy(): void {
|
||||
if (this._destroyed) return;
|
||||
this._destroyed = true;
|
||||
|
||||
Toast.trigger('beforeDestroy');
|
||||
this.dismiss();
|
||||
|
||||
// 清理子模块
|
||||
@@ -687,21 +713,28 @@ const meToast: MeToast = {
|
||||
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
||||
this.i18n.clearLocaleListeners();
|
||||
}
|
||||
if (this.animations && typeof this.animations.cancelAll === 'function') {
|
||||
this.animations.cancelAll();
|
||||
if (this.animations && typeof this.animations.destroy === 'function') {
|
||||
this.animations.destroy();
|
||||
}
|
||||
|
||||
// 清理 DOM
|
||||
// 清理 DOM 与静态资源
|
||||
if (typeof document !== 'undefined') {
|
||||
const containers = document.querySelectorAll('.met-container');
|
||||
containers.forEach((c) => { if (c.parentNode) c.parentNode.removeChild(c); });
|
||||
|
||||
const style = document.getElementById('metona-toast-styles');
|
||||
if (style && style.parentNode) style.parentNode.removeChild(style);
|
||||
['metona-toast-styles', 'metona-toast-theme-css', 'metona-toast-custom-styles', 'metona-toast-anim-styles'].forEach((id) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el && el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
this._toasts.clear();
|
||||
_containerCache.clear();
|
||||
Toast._registry.clear();
|
||||
|
||||
// afterDestroy 必须在清理 hooks 之前触发
|
||||
Toast.trigger('afterDestroy');
|
||||
Toast._hooks.clear();
|
||||
},
|
||||
|
||||
// 子模块引用(由 index.ts 注入实际实现)
|
||||
|
||||
+7
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { ICONS } from './icons.js';
|
||||
@@ -9,6 +9,11 @@ import { LOCALES } from './locales.js';
|
||||
|
||||
export { ICONS, LOCALES };
|
||||
|
||||
/**
|
||||
* 版本号 — 唯一来源,发布时只需修改此处
|
||||
*/
|
||||
export const VERSION = '0.5.0';
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
@@ -21,6 +26,7 @@ export const DEFAULTS = Object.freeze({
|
||||
pauseOnHover: true,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
dragThreshold: 120,
|
||||
showProgress: true,
|
||||
progressDirection: 'horizontal' as const,
|
||||
icon: true,
|
||||
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast i18n — 国际化管理
|
||||
* @module i18n
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { LOCALES } from './constants.js';
|
||||
@@ -405,7 +405,7 @@ export const clearLocaleListeners = (): void => {
|
||||
export const formatNumber = (number: number, options: Intl.NumberFormatOptions = {}): string => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return number.toString();
|
||||
}
|
||||
};
|
||||
@@ -420,7 +420,7 @@ export const formatCurrency = (amount: number, currency = 'USD', options: Intl.N
|
||||
currency,
|
||||
...options,
|
||||
}).format(amount);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return amount.toString();
|
||||
}
|
||||
};
|
||||
@@ -436,7 +436,7 @@ export const formatPercent = (value: number, options: Intl.NumberFormatOptions =
|
||||
maximumFractionDigits: 2,
|
||||
...options,
|
||||
}).format(value / 100);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return `${value}%`;
|
||||
}
|
||||
};
|
||||
@@ -448,7 +448,7 @@ export const formatDate = (date: Date | number | string, options: Intl.DateTimeF
|
||||
try {
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -497,7 +497,7 @@ export const formatRelativeTime = (date: Date | number | string, options: Intl.R
|
||||
}
|
||||
|
||||
return String(date);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -524,7 +524,7 @@ export const formatList = (list: string[], _options: Record<string, unknown> = {
|
||||
export const formatPlural = (count: number, options: Intl.PluralRulesOptions = {}): string => {
|
||||
try {
|
||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return count === 1 ? 'one' : 'other';
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Icons — 图标SVG定义
|
||||
* @module icons
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
* @description 107 个内置 SVG 图标
|
||||
*/
|
||||
|
||||
|
||||
+11
-5
@@ -1,22 +1,20 @@
|
||||
/**
|
||||
* MetonaToast — 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
* @author thzxx
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { meToast } from './api.js';
|
||||
import { Toast, _containerCache } from './toast.js';
|
||||
import { Toast } from './toast.js';
|
||||
import { animationUtils } from './animations.js';
|
||||
import { themeUtils } from './themes.js';
|
||||
import { i18nUtils } from './i18n.js';
|
||||
import { pluginUtils, presetPlugins } from './plugins.js';
|
||||
import { VERSION } from './constants.js';
|
||||
import type { ToastInstance } from './types.js';
|
||||
|
||||
// 版本信息
|
||||
const VERSION = '0.2.1';
|
||||
|
||||
/**
|
||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||
*/
|
||||
@@ -40,6 +38,14 @@ if (typeof i18nUtils.initI18n === 'function') {
|
||||
i18nUtils.initI18n();
|
||||
}
|
||||
|
||||
// 全局钩子 — 主题/语言变化转发到 Toast 钩子系统
|
||||
if (typeof themeUtils.addThemeListener === 'function') {
|
||||
themeUtils.addThemeListener(() => Toast.trigger('themeChange'));
|
||||
}
|
||||
if (typeof i18nUtils.addLocaleListener === 'function') {
|
||||
i18nUtils.addLocaleListener(() => Toast.trigger('localeChange'));
|
||||
}
|
||||
|
||||
// 浏览器环境全局注册
|
||||
if (typeof window !== 'undefined') {
|
||||
window.MeToast = enhancedMeToast;
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Locales — 国际化翻译数据
|
||||
* @module locales
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
* @description 内置 zh-CN / en-US 完整翻译
|
||||
*/
|
||||
|
||||
|
||||
+48
-2
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* MetonaToast Plugins — 插件系统
|
||||
* @module plugins
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
import type { Plugin, PluginManager as IPluginManager, PluginUtils } from './types.js';
|
||||
import { Toast } from './toast.js';
|
||||
import type { Plugin, PluginManager as IPluginManager, PluginUtils, ToastInstance } from './types.js';
|
||||
|
||||
/**
|
||||
* 插件管理器
|
||||
@@ -153,6 +154,12 @@ const presetPlugins: Record<string, Plugin> = {
|
||||
localStorage.removeItem(key);
|
||||
} catch (_e) { /* noop */ }
|
||||
}
|
||||
// 卸载 use() 注册的配置保存钩子
|
||||
const meta = this as Record<string, unknown>;
|
||||
if (typeof meta._saveOff === 'function') {
|
||||
(meta._saveOff as () => void)();
|
||||
meta._saveOff = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -175,6 +182,45 @@ const presetPlugins: Record<string, Plugin> = {
|
||||
document.body.appendChild(el);
|
||||
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
|
||||
},
|
||||
|
||||
uninstall(this: Plugin) {
|
||||
// 卸载 use() 注册的朗读钩子
|
||||
const meta = this as Record<string, unknown>;
|
||||
if (typeof meta._off === 'function') {
|
||||
(meta._off as () => void)();
|
||||
meta._off = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||
*/
|
||||
dedupe: {
|
||||
name: 'dedupe',
|
||||
version: '1.0.0',
|
||||
description: '相同类型和消息的 Toast 自动去重,更新已有实例而非重复弹出',
|
||||
_off: null as (() => void) | null,
|
||||
|
||||
install(this: Plugin) {
|
||||
const off = Toast.on('beforeShow', (toast: ToastInstance) => {
|
||||
const existing = Array.from(Toast._registry.values()).find(
|
||||
t => !t.closing && t.type === toast.type && t.message === toast.message
|
||||
);
|
||||
if (existing) {
|
||||
existing.update({ title: toast.title || existing.title });
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
(this as Record<string, unknown>)._off = off;
|
||||
},
|
||||
|
||||
uninstall(this: Plugin) {
|
||||
const off = (this as Record<string, unknown>)._off as (() => void) | null;
|
||||
if (off) off();
|
||||
(this as Record<string, unknown>)._off = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* MetonaToast React — React 适配器
|
||||
* @module react
|
||||
* @version 0.5.0
|
||||
* @description useToast hook + 声明式 <Toast /> 组件。主包保持零依赖,React 为 optional peerDependency
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import MeToast from './index.js';
|
||||
import type { ToastInstance, ToastOptions } from './types.js';
|
||||
|
||||
/** 支持声明式渲染的 Toast 类型 */
|
||||
export type ReactToastType = 'success' | 'error' | 'warning' | 'info' | 'show' | (string & {});
|
||||
|
||||
export interface ToastProps extends Omit<ToastOptions, 'message' | 'type'> {
|
||||
message: string;
|
||||
type?: ReactToastType;
|
||||
/** 组件卸载时自动移除该 Toast(默认 true)。设为 false 则 Toast 独立于组件生命周期 */
|
||||
autoClose?: boolean;
|
||||
}
|
||||
|
||||
const SHOW_METHODS = ['success', 'error', 'warning', 'info', 'show'] as const;
|
||||
type ShowMethod = (typeof SHOW_METHODS)[number];
|
||||
|
||||
/** 有 id 的可追踪对象(ToastInstance / LoadingControl) */
|
||||
interface Tracked {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const showToast = (method: ShowMethod, message: string, opts: Omit<ToastProps, 'message' | 'type' | 'autoClose'>): ToastInstance => {
|
||||
switch (method) {
|
||||
case 'success': return MeToast.success(message, opts);
|
||||
case 'error': return MeToast.error(message, opts);
|
||||
case 'warning': return MeToast.warning(message, opts);
|
||||
case 'info': return MeToast.info(message, opts);
|
||||
default: return MeToast.show(message, opts);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建绑定清理集合的 API — 组件卸载时自动移除本组件创建的 Toast。
|
||||
* 纯函数,便于独立测试与扩展。
|
||||
*/
|
||||
export const createBoundApi = (cleanup: Set<string>): Record<string, unknown> => {
|
||||
const track = <T extends Tracked | null>(fn: () => T): T => {
|
||||
const result = fn();
|
||||
if (result && result.id) cleanup.add(result.id);
|
||||
return result;
|
||||
};
|
||||
|
||||
const bound: Record<string, unknown> = {
|
||||
...MeToast,
|
||||
|
||||
success: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.success(messageOrOpts, opts)),
|
||||
error: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.error(messageOrOpts, opts)),
|
||||
warning: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.warning(messageOrOpts, opts)),
|
||||
info: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.info(messageOrOpts, opts)),
|
||||
show: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.show(messageOrOpts, opts)),
|
||||
loading: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.loading(messageOrOpts, opts)),
|
||||
|
||||
// 通过 id 关闭时同步清理集合,避免卸载时重复移除
|
||||
dismiss: (id?: string) => {
|
||||
if (id) cleanup.delete(id);
|
||||
return MeToast.dismiss(id);
|
||||
},
|
||||
removeToast: (id: string) => {
|
||||
cleanup.delete(id);
|
||||
return MeToast.removeToast(id);
|
||||
},
|
||||
};
|
||||
|
||||
return bound;
|
||||
};
|
||||
|
||||
/**
|
||||
* useToast — 返回 MeToast API,但本组件创建的 Toast 会在组件卸载时自动移除。
|
||||
* 适合"通知随组件生命周期"的场景(如表单提交、异步任务页)。
|
||||
*/
|
||||
export const useToast = (): Record<string, unknown> => {
|
||||
const cleanupRef = useRef<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
const set = cleanupRef.current;
|
||||
return () => {
|
||||
set.forEach(id => MeToast.removeToast(id));
|
||||
set.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useMemo(() => createBoundApi(cleanupRef.current), []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toast — 声明式 Toast 组件。props 变化时更新内容(重新创建实例),
|
||||
* 组件卸载时自动移除(autoClose 默认 true)。
|
||||
*/
|
||||
export const Toast = (props: ToastProps): null => {
|
||||
const { message, type, autoClose = true, ...rest } = props;
|
||||
const toastRef = useRef<ToastInstance | null>(null);
|
||||
const restKey = JSON.stringify(rest);
|
||||
|
||||
useEffect(() => {
|
||||
const method: ShowMethod = SHOW_METHODS.includes(type as ShowMethod) ? (type as ShowMethod) : 'show';
|
||||
toastRef.current = showToast(method, message, rest);
|
||||
|
||||
return () => {
|
||||
if (autoClose) toastRef.current?.remove();
|
||||
toastRef.current = null;
|
||||
};
|
||||
// autoClose 不参与重建依赖:它只影响卸载时的清理行为
|
||||
}, [message, type, restKey]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export { MeToast };
|
||||
+41
-16
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Styles — 样式管理
|
||||
* @module styles
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import type { ThemeConfig } from './types.js';
|
||||
@@ -363,6 +363,41 @@ const generateCSS = (): string => {
|
||||
transition: transform 0.1s linear;
|
||||
}
|
||||
|
||||
/* RTL 镜像:进度条从右向左收缩、side 条移到右侧、类型色条翻转到右边 */
|
||||
[dir="rtl"] .met-bar {
|
||||
transform-origin: right;
|
||||
}
|
||||
[dir="rtl"] .met-side {
|
||||
left: auto;
|
||||
right: 0;
|
||||
border-radius: 0 12px 12px 0;
|
||||
}
|
||||
[dir="rtl"] .met-progress-v {
|
||||
left: auto;
|
||||
right: 0;
|
||||
border-radius: 0 12px 12px 0;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-success {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #10b981;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-error {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #ef4444;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-warning {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #f59e0b;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-info {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #3b82f6;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-loading {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #6366f1;
|
||||
}
|
||||
|
||||
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.met-container::-webkit-scrollbar-track { background: transparent; }
|
||||
.met-container::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); border-radius: 3px; }
|
||||
@@ -506,6 +541,11 @@ export const generateThemeVariables = (theme: ThemeConfig): string => {
|
||||
export const applyThemeVariables = (theme: ThemeConfig): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
const oldStyle = document.getElementById('metona-toast-custom-styles');
|
||||
if (oldStyle && oldStyle.parentNode) {
|
||||
oldStyle.parentNode.removeChild(oldStyle);
|
||||
}
|
||||
|
||||
const css = generateThemeVariables(theme);
|
||||
const customStyle = document.createElement('style');
|
||||
customStyle.id = 'metona-toast-custom-styles';
|
||||
@@ -555,18 +595,3 @@ export const watchSystemTheme = (callback: (theme: string) => void): (() => void
|
||||
mediaQuery.removeEventListener('change', handler);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 自动应用系统主题
|
||||
*/
|
||||
export const autoApplySystemTheme = (): (() => void) => {
|
||||
const applyTheme = (theme: string): void => {
|
||||
updateStyles();
|
||||
// _THEMES reference is from constants, but here we just need to use theme string
|
||||
// applyThemeVariables is called externally
|
||||
};
|
||||
|
||||
applyTheme(getSystemTheme());
|
||||
|
||||
return watchSystemTheme(applyTheme);
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Templates — HTML 模板辅助函数
|
||||
* @module templates
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||
*/
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { THEMES } from './constants.js';
|
||||
|
||||
+78
-19
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { generateId, escapeHTML } from './utils.js';
|
||||
@@ -9,6 +9,7 @@ import { DEFAULTS, ICONS, TYPE_COLORS, THEMES } from './constants.js';
|
||||
import { injectStyles } from './styles.js';
|
||||
import { getTheme } from './themes.js';
|
||||
import { t, getLocaleDirection, getCurrentLocale } from './i18n.js';
|
||||
import { hasAnimation, ensureAnimationCSS } from './animations.js';
|
||||
import type { ToastConfig, ToastOptions, ToastInstance, ErrorInfo, TypeColor, ThemeConfig } from './types.js';
|
||||
|
||||
/**
|
||||
@@ -18,6 +19,9 @@ export class Toast implements ToastInstance {
|
||||
// 静态钩子系统
|
||||
static _hooks: Map<string, Array<(toast: Toast) => boolean | void>> = new Map();
|
||||
|
||||
// 实例注册表 — 用于通过 id 反查实例(max 超限移除、外部接管等)
|
||||
static _registry: Map<string, Toast> = new Map();
|
||||
|
||||
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
||||
private static _onError: ((errorInfo: ErrorInfo) => void) | null = null;
|
||||
private static _removeToast: ((id: string) => void) | null = null;
|
||||
@@ -36,31 +40,38 @@ export class Toast implements ToastInstance {
|
||||
static on(name: string, fn: (toast: Toast) => boolean | void): () => void {
|
||||
if (!this._hooks.has(name)) this._hooks.set(name, []);
|
||||
this._hooks.get(name)!.push(fn);
|
||||
return () => this.off(name, fn);
|
||||
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||
return () => Toast.off(name, fn);
|
||||
}
|
||||
|
||||
static off(name: string, fn: (toast: Toast) => boolean | void): void {
|
||||
const list = this._hooks.get(name);
|
||||
if (list) this._hooks.set(name, list.filter(f => f !== fn));
|
||||
if (list) {
|
||||
const next = list.filter(f => f !== fn);
|
||||
// 空数组直接删除 key,避免残留
|
||||
if (next.length === 0) this._hooks.delete(name);
|
||||
else this._hooks.set(name, next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
|
||||
*/
|
||||
static trigger(name: string, toast: Toast): boolean {
|
||||
static trigger(name: string, toast: Toast | null = null): boolean {
|
||||
const list = this._hooks.get(name);
|
||||
if (!list || list.length === 0) return true;
|
||||
|
||||
let allow = true;
|
||||
list.forEach(fn => {
|
||||
try {
|
||||
const result = fn(toast);
|
||||
const result = fn(toast as Toast);
|
||||
if (result === false) allow = false;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Hook error:', name, e);
|
||||
if (Toast._onError) {
|
||||
try { Toast._onError({ hook: name, error: e as Error, toast }); } catch (_e) { /* noop */ }
|
||||
try { Toast._onError({ hook: name, error: e as Error, toast: toast as ToastInstance }); } catch (_e) { /* noop */ }
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -124,6 +135,7 @@ export class Toast implements ToastInstance {
|
||||
}
|
||||
|
||||
injectStyles();
|
||||
ensureAnimationCSS(this.config.animation || 'slide');
|
||||
|
||||
const container = this._getContainer();
|
||||
const { theme, c, t } = this._palette();
|
||||
@@ -148,6 +160,8 @@ export class Toast implements ToastInstance {
|
||||
container.appendChild(el);
|
||||
}
|
||||
|
||||
Toast._registry.set(this.id, this);
|
||||
|
||||
this._bindEvents(el);
|
||||
this._startTimer();
|
||||
|
||||
@@ -196,6 +210,8 @@ export class Toast implements ToastInstance {
|
||||
el.className = `met-container ${position}`;
|
||||
el.setAttribute('aria-label', 'Notifications');
|
||||
el.setAttribute('role', 'region');
|
||||
// RTL 语言设置容器文字方向,使内容/关闭按钮/进度条正确镜像
|
||||
if (isRTL) el.setAttribute('dir', 'rtl');
|
||||
|
||||
const posStyles: Record<string, string> = {
|
||||
'top-left': 'top:0;left:0;align-items:flex-start',
|
||||
@@ -227,19 +243,21 @@ export class Toast implements ToastInstance {
|
||||
_limitToasts(container: HTMLElement): void {
|
||||
const max = this.config.max || 6;
|
||||
const list = Array.from(container.querySelectorAll('.met-toast'));
|
||||
if (list.length >= max) {
|
||||
const first = list[0] as HTMLElement;
|
||||
const id = first?.dataset.id;
|
||||
if (id && Toast._removeToast) {
|
||||
while (list.length >= max) {
|
||||
const el = list.shift() as HTMLElement;
|
||||
const id = el?.dataset.id;
|
||||
if (!id) continue;
|
||||
const existing = Toast._registry.get(id);
|
||||
if (existing) {
|
||||
existing.close(true);
|
||||
} else if (Toast._removeToast) {
|
||||
Toast._removeToast(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_buildClassName(theme: string): string {
|
||||
const CSS_ANIMS = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom',
|
||||
'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
||||
const anim = CSS_ANIMS.includes(this.config.animation || '') ? this.config.animation : 'slide';
|
||||
const anim = hasAnimation(this.config.animation || '') ? (this.config.animation as string) : 'slide';
|
||||
return [
|
||||
'met-toast',
|
||||
`met-${this.type}`,
|
||||
@@ -327,6 +345,8 @@ export class Toast implements ToastInstance {
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.trigger('click', this);
|
||||
|
||||
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
||||
if (typeof this.config.onClick === 'function') {
|
||||
try {
|
||||
@@ -344,8 +364,14 @@ export class Toast implements ToastInstance {
|
||||
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
||||
|
||||
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
||||
const mouseEnter = () => this._pause();
|
||||
const mouseLeave = () => this._resume();
|
||||
const mouseEnter = () => {
|
||||
Toast.trigger('hover', this);
|
||||
this._pause();
|
||||
};
|
||||
const mouseLeave = () => {
|
||||
Toast.trigger('hover', this);
|
||||
this._resume();
|
||||
};
|
||||
|
||||
el.addEventListener('mouseenter', mouseEnter);
|
||||
el.addEventListener('mouseleave', mouseLeave);
|
||||
@@ -356,6 +382,16 @@ export class Toast implements ToastInstance {
|
||||
});
|
||||
}
|
||||
|
||||
// 入场动画生命周期钩子(离场为过渡/内联样式,不产生 animation 事件)
|
||||
const animStart = () => Toast.trigger('animationStart', this);
|
||||
const animEnd = () => Toast.trigger('animationEnd', this);
|
||||
el.addEventListener('animationstart', animStart);
|
||||
el.addEventListener('animationend', animEnd);
|
||||
this._cleanups.push(() => {
|
||||
el.removeEventListener('animationstart', animStart);
|
||||
el.removeEventListener('animationend', animEnd);
|
||||
});
|
||||
|
||||
if (this.config.draggable) {
|
||||
this._bindDrag(el);
|
||||
}
|
||||
@@ -372,6 +408,7 @@ export class Toast implements ToastInstance {
|
||||
el.setPointerCapture(e.pointerId);
|
||||
el.style.transition = 'none';
|
||||
this._pause();
|
||||
Toast.trigger('dragStart', this);
|
||||
};
|
||||
|
||||
const move = (e: PointerEvent) => {
|
||||
@@ -388,7 +425,8 @@ export class Toast implements ToastInstance {
|
||||
el.releasePointerCapture(e.pointerId);
|
||||
el.style.transition = '';
|
||||
|
||||
if (Math.abs(dx) > 120) {
|
||||
const threshold = this.config.dragThreshold ?? 120;
|
||||
if (Math.abs(dx) > threshold) {
|
||||
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
||||
el.style.opacity = '0';
|
||||
setTimeout(() => this.close(true), 250);
|
||||
@@ -398,6 +436,7 @@ export class Toast implements ToastInstance {
|
||||
this._resume();
|
||||
}
|
||||
dx = dy = 0;
|
||||
Toast.trigger('dragEnd', this);
|
||||
};
|
||||
|
||||
el.addEventListener('pointerdown', down);
|
||||
@@ -419,6 +458,7 @@ export class Toast implements ToastInstance {
|
||||
if (!resuming) {
|
||||
this.startedAt = Date.now();
|
||||
this.remaining = this.config.duration || 0;
|
||||
Toast.trigger('progressStart', this);
|
||||
}
|
||||
|
||||
const tick = (): void => {
|
||||
@@ -436,6 +476,7 @@ export class Toast implements ToastInstance {
|
||||
}
|
||||
|
||||
if (this.remaining <= 0) {
|
||||
Toast.trigger('progressEnd', this);
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
@@ -467,7 +508,9 @@ export class Toast implements ToastInstance {
|
||||
}
|
||||
|
||||
update(partial: Partial<ToastOptions>): this {
|
||||
Toast.trigger('beforeUpdate', this);
|
||||
// beforeUpdate 钩子 — 返回 false 可阻止更新
|
||||
if (!Toast.trigger('beforeUpdate', this)) return this;
|
||||
|
||||
const typeChanged = partial.type && partial.type !== this.type;
|
||||
if (partial.type) this.type = partial.type;
|
||||
if (partial.title !== undefined) this.title = partial.title;
|
||||
@@ -501,6 +544,18 @@ export class Toast implements ToastInstance {
|
||||
content.innerHTML = `${safeTitle}${safeMessage}`;
|
||||
}
|
||||
|
||||
// duration 变更:重启或停止计时器(loading 0 → N 转换、动态调整时长等场景)
|
||||
if (partial.duration !== undefined && partial.duration !== this.config.duration) {
|
||||
this.config.duration = partial.duration;
|
||||
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
this.remaining = Math.max(0, partial.duration);
|
||||
if (partial.duration > 0 && !this.paused) {
|
||||
this.startedAt = Date.now();
|
||||
this._startTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// resetTimerOnUpdate: 更新内容后重置计时器
|
||||
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
||||
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||
@@ -559,14 +614,17 @@ export class Toast implements ToastInstance {
|
||||
}
|
||||
this.el = null;
|
||||
|
||||
Toast._registry.delete(this.id);
|
||||
if (Toast._removeToast) Toast._removeToast(this.id);
|
||||
}
|
||||
|
||||
close(immediate = false): void {
|
||||
if (this.closing) return;
|
||||
this.closing = true;
|
||||
|
||||
Toast.trigger('beforeClose', this);
|
||||
// beforeClose 钩子 — 返回 false 可阻止关闭
|
||||
if (!Toast.trigger('beforeClose', this)) return;
|
||||
|
||||
this.closing = true;
|
||||
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||
this._cleanups.forEach(fn => { try { fn(); } catch (_e) { /* noop */ } });
|
||||
this._cleanups = [];
|
||||
@@ -622,6 +680,7 @@ export class Toast implements ToastInstance {
|
||||
}
|
||||
|
||||
Toast.trigger('afterClose', this);
|
||||
Toast._registry.delete(this.id);
|
||||
if (Toast._removeToast) Toast._removeToast(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast — 核心类型定义
|
||||
* @module types
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
// ========== 基础类型 ==========
|
||||
@@ -72,6 +72,7 @@ export interface ToastConfig {
|
||||
pauseOnHover?: boolean;
|
||||
closeOnClick?: boolean;
|
||||
draggable?: boolean;
|
||||
dragThreshold?: number;
|
||||
showProgress?: boolean;
|
||||
progressDirection?: ToastProgressDirection;
|
||||
icon?: boolean;
|
||||
@@ -271,8 +272,8 @@ export interface StatusInfo {
|
||||
// ========== 动画 ==========
|
||||
|
||||
export interface AnimationConfig {
|
||||
enter: Record<string, string>;
|
||||
leave: Record<string, string>;
|
||||
enter: Record<string, string | number>;
|
||||
leave: Record<string, string | number>;
|
||||
duration: number;
|
||||
easing: string;
|
||||
name?: string;
|
||||
@@ -538,10 +539,8 @@ export const HOOK_NAMES = {
|
||||
ANIMATION_START: 'animationStart',
|
||||
ANIMATION_END: 'animationEnd',
|
||||
PROGRESS_START: 'progressStart',
|
||||
PROGRESS_UPDATE: 'progressUpdate',
|
||||
PROGRESS_END: 'progressEnd',
|
||||
ERROR: 'error',
|
||||
CUSTOM: 'custom',
|
||||
} as const;
|
||||
|
||||
export type HookName = (typeof HOOK_NAMES)[keyof typeof HOOK_NAMES];
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Utils — 工具函数
|
||||
* @module utils
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
+505
-63
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast 覆盖率补充测试 — 目标 95%+
|
||||
* @module tests
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import MeToast, { Toast, VERSION } from '../src/index';
|
||||
@@ -139,6 +139,70 @@ describe('animations.ts 覆盖率', () => {
|
||||
test('getActiveCount 返回动画数量', () => {
|
||||
expect(MeToast.animations.getActiveCount()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('ensureAnimationCSS 内置动画不注入', () => {
|
||||
const { ensureAnimationCSS } = require('../src/animations.js');
|
||||
// jsdom 真实 DOM:内置动画由 styles.ts 统一注入,不应产生独立 style 标签
|
||||
ensureAnimationCSS('slide');
|
||||
expect(document.getElementById('metona-toast-anim-styles')).toBeNull();
|
||||
});
|
||||
|
||||
test('ensureAnimationCSS 自定义动画注入样式(幂等)', () => {
|
||||
const { ensureAnimationCSS } = require('../src/animations.js');
|
||||
MeToast.animations.destroy(); // 清理模块注入状态,保证测试独立
|
||||
MeToast.animations.register('my-anim', {
|
||||
enter: { opacity: 0, transform: 'scale(0.5)' },
|
||||
leave: { opacity: 1 },
|
||||
duration: 400,
|
||||
easing: 'ease',
|
||||
});
|
||||
ensureAnimationCSS('my-anim');
|
||||
const styleEl = document.getElementById('metona-toast-anim-styles');
|
||||
expect(styleEl).not.toBeNull();
|
||||
expect(styleEl!.textContent).toContain('@keyframes met-my-anim-in');
|
||||
expect(styleEl!.textContent).toContain('.met-anim-my-anim.met-toast.met-show');
|
||||
// 幂等:二次调用不重复追加
|
||||
const before = styleEl!.textContent!.length;
|
||||
ensureAnimationCSS('my-anim');
|
||||
expect(styleEl!.textContent!.length).toBe(before);
|
||||
MeToast.animations.unregister('my-anim');
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
test('ensureAnimationCSS 未注册动画不注入', () => {
|
||||
const { ensureAnimationCSS } = require('../src/animations.js');
|
||||
// 清理 jsdom 中可能残留的动态 style 元素
|
||||
document.getElementById('metona-toast-anim-styles')?.remove();
|
||||
ensureAnimationCSS('not-registered');
|
||||
expect(document.getElementById('metona-toast-anim-styles')).toBeNull();
|
||||
});
|
||||
|
||||
test('hasAnimation 检查内置与自定义动画', () => {
|
||||
const { hasAnimation } = require('../src/animations.js');
|
||||
expect(hasAnimation('slide')).toBe(true);
|
||||
expect(hasAnimation('nonexistent')).toBe(false);
|
||||
MeToast.animations.register('has-anim-test', { enter: {}, leave: {}, duration: 300 });
|
||||
expect(hasAnimation('has-anim-test')).toBe(true);
|
||||
MeToast.animations.unregister('has-anim-test');
|
||||
});
|
||||
|
||||
test('自定义动画注册后 _buildClassName 使用该动画', () => {
|
||||
MeToast.animations.register('custom-anim', {
|
||||
enter: { opacity: 0 },
|
||||
leave: { opacity: 1 },
|
||||
duration: 400,
|
||||
easing: 'ease',
|
||||
});
|
||||
const t = MeToast.info({ message: 'custom', animation: 'custom-anim' });
|
||||
expect(t.el?.className).toContain('met-anim-custom-anim');
|
||||
MeToast.animations.unregister('custom-anim');
|
||||
});
|
||||
|
||||
test('未注册动画名 fallback 到 slide', () => {
|
||||
const t = new Toast({ message: 'test', animation: 'unknown-anim' });
|
||||
t.create();
|
||||
expect(t.el?.className).toContain('met-anim-slide');
|
||||
});
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
@@ -745,7 +809,7 @@ describe('api.ts 覆盖率', () => {
|
||||
|
||||
test('getStatus 返回完整状态', () => {
|
||||
const status = MeToast.getStatus();
|
||||
expect(status.version).toBe('0.2.1');
|
||||
expect(status.version).toBe('0.5.0');
|
||||
expect(status.toasts).toBeGreaterThanOrEqual(0);
|
||||
expect(status.theme).toBeDefined();
|
||||
expect(status.locale).toBeDefined();
|
||||
@@ -1016,41 +1080,11 @@ describe('styles.ts 覆盖率', () => {
|
||||
expect(typeof unsub).toBe('function');
|
||||
unsub();
|
||||
});
|
||||
|
||||
test('autoApplySystemTheme 返回取消函数', () => {
|
||||
const unsub = styles.autoApplySystemTheme();
|
||||
expect(typeof unsub).toBe('function');
|
||||
unsub();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
// 9. utils.ts 覆盖率补充
|
||||
// 9. utils.ts 覆盖率补充(SSR 分支见 tests/ssr.test.ts — 独立环境隔离)
|
||||
// ==================================================================
|
||||
describe('utils.ts 覆盖率', () => {
|
||||
test('escapeHTML SSR 路径(无 document)', () => {
|
||||
// 需要绕过 mock document
|
||||
const originalDoc = (global as Record<string, unknown>).document;
|
||||
(global as Record<string, unknown>).document = undefined;
|
||||
jest.resetModules();
|
||||
const { escapeHTML } = require('../src/utils.js');
|
||||
const result = escapeHTML('<script>alert("xss")</script>');
|
||||
expect(result).not.toContain('<script>');
|
||||
expect(result).toContain('<script>');
|
||||
(global as Record<string, unknown>).document = originalDoc;
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
test('prefersDark 无 window 时返回 false', () => {
|
||||
const originalWindow = (global as Record<string, unknown>).window;
|
||||
(global as Record<string, unknown>).window = undefined;
|
||||
jest.resetModules();
|
||||
const { prefersDark } = require('../src/utils.js');
|
||||
expect(prefersDark()).toBe(false);
|
||||
(global as Record<string, unknown>).window = originalWindow;
|
||||
jest.resetModules();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
// 10. 最终补漏 — 针对性覆盖剩余未覆盖行
|
||||
@@ -1213,17 +1247,7 @@ describe('最终补漏', () => {
|
||||
unsub();
|
||||
});
|
||||
|
||||
// === themes.ts localStorage 异常路径 ===
|
||||
test('saveTheme localStorage 异常被捕获', () => {
|
||||
const orig = (global as Record<string, unknown>).localStorage;
|
||||
(global as Record<string, unknown>).localStorage = undefined;
|
||||
jest.resetModules();
|
||||
const { saveTheme } = require('../src/themes.js');
|
||||
expect(() => saveTheme('dark')).not.toThrow();
|
||||
(global as Record<string, unknown>).localStorage = orig;
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
// === themes.ts localStorage 异常路径(详见 tests/ssr.test.ts) ===
|
||||
test('loadTheme/loadLocale 异常路径不抛异常', () => {
|
||||
const { loadTheme } = require('../src/themes.js');
|
||||
const { loadLocale } = require('../src/i18n.js');
|
||||
@@ -1231,25 +1255,6 @@ describe('最终补漏', () => {
|
||||
expect(typeof loadLocale()).toBe('string');
|
||||
});
|
||||
|
||||
test('loadLocale localStorage 异常返回 default', () => {
|
||||
const i18n = require('../src/i18n.js');
|
||||
const orig = (global as Record<string, unknown>).localStorage;
|
||||
(global as Record<string, unknown>).localStorage = {
|
||||
getItem: () => { throw new Error('quota'); },
|
||||
};
|
||||
jest.resetModules();
|
||||
const { loadLocale } = require('../src/i18n.js');
|
||||
expect(typeof loadLocale()).toBe('string');
|
||||
(global as Record<string, unknown>).localStorage = orig;
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
// === styles.ts 剩余 ===
|
||||
test('autoApplySystemTheme 内部逻辑不抛异常', () => {
|
||||
const styles = require('../src/styles.js');
|
||||
expect(() => styles.autoApplySystemTheme()).not.toThrow();
|
||||
});
|
||||
|
||||
// === api.ts prompt 流程 ===
|
||||
test('prompt 创建后按钮事件绑定', async () => {
|
||||
const promise = MeToast.prompt('name?', {
|
||||
@@ -1299,3 +1304,440 @@ describe('最终补漏', () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
// 11. v0.3.0 修复验证
|
||||
// ==================================================================
|
||||
describe('v0.3.0 修复验证', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MeToast._toasts.clear();
|
||||
MeToast.resetConfig();
|
||||
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||
Toast._hooks.clear();
|
||||
Toast._registry.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
_containerCache.clear();
|
||||
// 清理动画模块状态(injected 集合 + 动态 style 引用),再恢复默认动画
|
||||
MeToast.animations.destroy();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
test('_limitToasts 超出 max 时真正关闭最早的 toast', () => {
|
||||
const t1 = MeToast.info('m1');
|
||||
const t2 = MeToast.info('m2');
|
||||
const el = createMockElement() as unknown as HTMLElement;
|
||||
(el as unknown as { querySelectorAll: unknown }).querySelectorAll = jest.fn(() => [
|
||||
{ dataset: { id: t1.id } },
|
||||
{ dataset: { id: t2.id } },
|
||||
]);
|
||||
const newToast = new Toast({ message: 'm3', max: 2 });
|
||||
newToast._limitToasts(el as unknown as HTMLElement);
|
||||
expect(t1.closing).toBe(true);
|
||||
expect(t2.closing).toBe(false);
|
||||
});
|
||||
|
||||
test('beforeClose 钩子返回 false 阻止关闭', () => {
|
||||
const t = MeToast.info('test');
|
||||
const blocker = () => false;
|
||||
Toast.on('beforeClose', blocker);
|
||||
t.close();
|
||||
expect(t.closing).toBe(false);
|
||||
Toast.off('beforeClose', blocker);
|
||||
});
|
||||
|
||||
test('beforeUpdate 钩子返回 false 阻止更新', () => {
|
||||
const t = MeToast.info('原始');
|
||||
const blocker = () => false;
|
||||
Toast.on('beforeUpdate', blocker);
|
||||
t.update({ message: '新消息' });
|
||||
expect(t.message).toBe('原始');
|
||||
Toast.off('beforeUpdate', blocker);
|
||||
});
|
||||
|
||||
test('configure/updateConfig/resetConfig 触发 configChange 钩子', () => {
|
||||
const fn = jest.fn();
|
||||
Toast.on('configChange', fn);
|
||||
MeToast.configure({ duration: 2500 });
|
||||
MeToast.updateConfig({ gap: 8 });
|
||||
MeToast.resetConfig();
|
||||
expect(fn).toHaveBeenCalledTimes(3);
|
||||
Toast.off('configChange', fn);
|
||||
});
|
||||
|
||||
test('init 触发 beforeInit/afterInit 钩子', () => {
|
||||
const calls: string[] = [];
|
||||
const before = jest.fn(() => calls.push('beforeInit'));
|
||||
const after = jest.fn(() => calls.push('afterInit'));
|
||||
Toast.on('beforeInit', before);
|
||||
Toast.on('afterInit', after);
|
||||
MeToast.init();
|
||||
expect(calls).toEqual(['beforeInit', 'afterInit']);
|
||||
Toast.off('beforeInit', before);
|
||||
Toast.off('afterInit', after);
|
||||
});
|
||||
|
||||
test('destroy 触发 beforeDestroy/afterDestroy 钩子', () => {
|
||||
const calls: string[] = [];
|
||||
const before = jest.fn(() => calls.push('beforeDestroy'));
|
||||
const after = jest.fn(() => calls.push('afterDestroy'));
|
||||
Toast.on('beforeDestroy', before);
|
||||
Toast.on('afterDestroy', after);
|
||||
MeToast.destroy();
|
||||
expect(calls).toEqual(['beforeDestroy', 'afterDestroy']);
|
||||
});
|
||||
|
||||
test('destroy 清理全部钩子', () => {
|
||||
const fn = jest.fn();
|
||||
Toast.on('afterShow', fn);
|
||||
MeToast.destroy();
|
||||
MeToast.success('after-destroy');
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
test('destroy 清理实例注册表', () => {
|
||||
MeToast.info('m1');
|
||||
expect(Toast._registry.size).toBe(1);
|
||||
MeToast.destroy();
|
||||
expect(Toast._registry.size).toBe(0);
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
test('destroy 后 init 恢复可用', () => {
|
||||
MeToast.destroy();
|
||||
MeToast.init({});
|
||||
const t = MeToast.success('revived');
|
||||
expect(t).toBeDefined();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
test('init plugins 支持插件对象', () => {
|
||||
const install = jest.fn();
|
||||
MeToast.init({ plugins: [{ name: 'obj-plugin', install }] });
|
||||
expect(MeToast.plugins.has('obj-plugin')).toBe(true);
|
||||
expect(install).toHaveBeenCalled();
|
||||
MeToast.plugins.unregister('obj-plugin');
|
||||
});
|
||||
|
||||
test('animate 生命周期钩子 progressStart/progressEnd 触发', (done) => {
|
||||
const start = jest.fn();
|
||||
const end = jest.fn();
|
||||
Toast.on('progressStart', start);
|
||||
Toast.on('progressEnd', end);
|
||||
const t = MeToast.info({ message: 'timed', duration: 30 });
|
||||
setTimeout(() => {
|
||||
expect(start).toHaveBeenCalled();
|
||||
Toast.off('progressStart', start);
|
||||
Toast.off('progressEnd', end);
|
||||
done();
|
||||
}, 120);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
// 12. v0.4.0 能力增强验证
|
||||
// ==================================================================
|
||||
describe('v0.4.0 能力增强', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MeToast._toasts.clear();
|
||||
MeToast.resetConfig();
|
||||
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||
Toast._hooks.clear();
|
||||
Toast._registry.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
_containerCache.clear();
|
||||
MeToast.animations.destroy();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
test('loading 链式转换 id 稳定(原地 update)', () => {
|
||||
const l = MeToast.loading('正在加载');
|
||||
const idBefore = l.id;
|
||||
const toast = l.success('加载完成');
|
||||
expect(toast).not.toBeNull();
|
||||
expect(toast!.id).toBe(idBefore);
|
||||
expect(toast!.type).toBe('success');
|
||||
expect(toast!.message).toBe('加载完成');
|
||||
expect(MeToast.count()).toBe(1);
|
||||
});
|
||||
|
||||
test('loading 转换后 duration 恢复默认自动关闭', () => {
|
||||
MeToast.configure({ duration: 4000 });
|
||||
const l = MeToast.loading('loading');
|
||||
const toast = l.error('失败');
|
||||
expect(toast!.config.duration).toBe(4000);
|
||||
MeToast.resetConfig();
|
||||
});
|
||||
|
||||
test('loading 转换支持 opts 覆盖 duration', () => {
|
||||
const l = MeToast.loading('loading');
|
||||
const toast = l.warning('警告', { duration: 8000 });
|
||||
expect(toast!.config.duration).toBe(8000);
|
||||
});
|
||||
|
||||
test('update duration 变更重启计时器', () => {
|
||||
const t = MeToast.info('test', { duration: 0 });
|
||||
expect(t.rafId).toBeNull();
|
||||
t.update({ duration: 5000 });
|
||||
expect(t.config.duration).toBe(5000);
|
||||
expect(t.rafId).not.toBeNull();
|
||||
t.update({ duration: 0 });
|
||||
expect(t.config.duration).toBe(0);
|
||||
expect(t.rafId).toBeNull();
|
||||
});
|
||||
|
||||
test('dragThreshold 配置生效且默认 120', () => {
|
||||
expect(MeToast.getConfig().dragThreshold).toBe(120);
|
||||
const t = MeToast.info('x', { dragThreshold: 200 });
|
||||
expect(t.config.dragThreshold).toBe(200);
|
||||
});
|
||||
|
||||
test('RTL 容器设置 dir 属性', () => {
|
||||
const { getCurrentLocale, setCurrentLocale, addTranslations } = require('../src/i18n.js');
|
||||
const originalLocale = getCurrentLocale();
|
||||
// 'ar' 需先注册翻译,否则 setCurrentLocale fallback 回默认语言
|
||||
addTranslations('ar', { close: 'إغلاق' });
|
||||
setCurrentLocale('ar');
|
||||
try {
|
||||
const toast = new Toast({ message: 'test', position: 'top-left' });
|
||||
const container = toast._getContainer();
|
||||
expect(container.getAttribute('dir')).toBe('rtl');
|
||||
toast.close(true);
|
||||
} finally {
|
||||
setCurrentLocale(originalLocale);
|
||||
}
|
||||
});
|
||||
|
||||
test('LTR 容器不设置 dir 属性', () => {
|
||||
const { getCurrentLocale, setCurrentLocale } = require('../src/i18n.js');
|
||||
const originalLocale = getCurrentLocale();
|
||||
setCurrentLocale('zh-CN');
|
||||
try {
|
||||
const toast = new Toast({ message: 'test' });
|
||||
const container = toast._getContainer();
|
||||
expect(container.getAttribute('dir')).toBeNull();
|
||||
toast.close(true);
|
||||
} finally {
|
||||
setCurrentLocale(originalLocale);
|
||||
}
|
||||
});
|
||||
|
||||
test('dedupe 插件同消息去重', () => {
|
||||
MeToast.use('dedupe');
|
||||
MeToast.info('same message');
|
||||
MeToast.info('same message');
|
||||
expect(MeToast.count()).toBe(1);
|
||||
MeToast.info('different message');
|
||||
expect(MeToast.count()).toBe(2);
|
||||
MeToast.plugins.unregister('dedupe');
|
||||
// 卸载后不再去重
|
||||
MeToast._toasts.clear();
|
||||
MeToast.info('same message');
|
||||
MeToast.info('same message');
|
||||
expect(MeToast.count()).toBe(2);
|
||||
});
|
||||
|
||||
test('dedupe 插件不同 type 不去重', () => {
|
||||
MeToast.use('dedupe');
|
||||
MeToast.info('msg');
|
||||
MeToast.success('msg');
|
||||
expect(MeToast.count()).toBe(2);
|
||||
MeToast.plugins.unregister('dedupe');
|
||||
});
|
||||
|
||||
test('dedupe 插件 uninstall 移除钩子', () => {
|
||||
MeToast.use('dedupe');
|
||||
MeToast.plugins.unregister('dedupe');
|
||||
MeToast._toasts.clear();
|
||||
MeToast.info('m');
|
||||
MeToast.info('m');
|
||||
expect(MeToast.count()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
// 13. v0.5.0 异常路径与边界覆盖
|
||||
// ==================================================================
|
||||
describe('v0.5.0 异常路径与边界', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MeToast._toasts.clear();
|
||||
MeToast.resetConfig();
|
||||
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||
Toast._hooks.clear();
|
||||
Toast._registry.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
_containerCache.clear();
|
||||
MeToast.animations.destroy();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
describe('i18n.ts 异常与 fallback', () => {
|
||||
const i18n = require('../src/i18n.js');
|
||||
|
||||
test('t fallback 到 fallbackLocale', () => {
|
||||
i18n.setFallbackLocale('en-US');
|
||||
i18n.removeTranslation('zh-CN', 'retry');
|
||||
expect(i18n.t('retry')).toBe('Retry');
|
||||
i18n.addTranslations('zh-CN', { retry: '重试' });
|
||||
i18n.setFallbackLocale('zh-CN');
|
||||
});
|
||||
|
||||
test('removeTranslation 中间层级缺失提前返回', () => {
|
||||
i18n.addTranslations('ja', { a: { b: 'x' } });
|
||||
expect(() => i18n.removeTranslation('ja', 'a.nonexistent.c')).not.toThrow();
|
||||
});
|
||||
|
||||
test('saveLocale localStorage 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(() => i18n.saveLocale('en-US')).not.toThrow();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('loadLocale localStorage 异常返回默认', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(typeof i18n.loadLocale()).toBe('string');
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('getDefaultLocale 不支持浏览器语言时回退', () => {
|
||||
const orig = Object.getOwnPropertyDescriptor(navigator, 'language');
|
||||
Object.defineProperty(navigator, 'language', { value: 'zz-ZZ', configurable: true });
|
||||
expect(typeof i18n.getDefaultLocale()).toBe('string');
|
||||
if (orig) Object.defineProperty(navigator, 'language', orig);
|
||||
});
|
||||
|
||||
test('locale listener 异常不中断其他监听器', () => {
|
||||
const bad = () => { throw new Error('listener-err'); };
|
||||
const good = jest.fn();
|
||||
i18n.addLocaleListener(bad);
|
||||
i18n.addLocaleListener(good);
|
||||
expect(() => i18n.switchLocale('en-US')).not.toThrow();
|
||||
expect(good).toHaveBeenCalled();
|
||||
i18n.removeLocaleListener(bad);
|
||||
i18n.removeLocaleListener(good);
|
||||
i18n.switchLocale('zh-CN');
|
||||
});
|
||||
|
||||
test('Intl 异常时 format 系列降级返回原始值', () => {
|
||||
const origIntl = (global as Record<string, unknown>).Intl;
|
||||
(global as Record<string, unknown>).Intl = new Proxy(origIntl as object, {
|
||||
get: (t: Record<string, unknown>, p: string) => {
|
||||
if (['NumberFormat', 'DateTimeFormat', 'RelativeTimeFormat', 'PluralRules'].includes(p)) {
|
||||
return class { constructor() { throw new RangeError('invalid'); } };
|
||||
}
|
||||
return (t as Record<string, unknown>)[p];
|
||||
},
|
||||
});
|
||||
expect(i18n.formatNumber(1)).toBe('1');
|
||||
expect(i18n.formatCurrency(1)).toBe('1');
|
||||
expect(i18n.formatPercent(50)).toBe('50%');
|
||||
expect(i18n.formatDate('2026-01-01')).toBe('2026-01-01');
|
||||
expect(typeof i18n.formatRelativeTime(Date.now())).toBe('string');
|
||||
expect(i18n.formatPlural(5)).toBe('other');
|
||||
(global as Record<string, unknown>).Intl = origIntl;
|
||||
});
|
||||
|
||||
test('formatList Intl.ListFormat 缺失时逗号拼接', () => {
|
||||
const origLF = (Intl as unknown as Record<string, unknown>).ListFormat;
|
||||
(Intl as unknown as Record<string, unknown>).ListFormat = undefined;
|
||||
const result = i18n.formatList(['a', 'b']);
|
||||
expect(result).toBe('a, b');
|
||||
(Intl as unknown as Record<string, unknown>).ListFormat = origLF;
|
||||
});
|
||||
|
||||
test('plural 缺失时返回原 key', () => {
|
||||
expect(i18n.plural('missing_plural_key_xyz', 5)).toBe('missing_plural_key_xyz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('themes.ts 异常与监听', () => {
|
||||
const themes = require('../src/themes.js');
|
||||
|
||||
test('saveTheme/loadTheme localStorage 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(() => themes.saveTheme('dark')).not.toThrow();
|
||||
spy.mockRestore();
|
||||
const spy2 = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(themes.loadTheme()).toBe('auto');
|
||||
spy2.mockRestore();
|
||||
});
|
||||
|
||||
test('watchSystemTheme 重复调用先移除旧监听', () => {
|
||||
expect(() => { themes.watchSystemTheme(); themes.watchSystemTheme(); }).not.toThrow();
|
||||
themes.unwatchSystemTheme();
|
||||
});
|
||||
|
||||
test('unwatchSystemTheme 清理监听', () => {
|
||||
themes.watchSystemTheme();
|
||||
expect(() => themes.unwatchSystemTheme()).not.toThrow();
|
||||
});
|
||||
|
||||
test('theme listener 异常不中断其他监听器', () => {
|
||||
const bad = () => { throw new Error('theme-listener-err'); };
|
||||
const good = jest.fn();
|
||||
themes.addThemeListener(bad);
|
||||
themes.addThemeListener(good);
|
||||
expect(() => themes.switchTheme('dark')).not.toThrow();
|
||||
expect(good).toHaveBeenCalled();
|
||||
themes.removeThemeListener(bad);
|
||||
themes.removeThemeListener(good);
|
||||
themes.switchTheme('auto');
|
||||
});
|
||||
|
||||
test('removeThemeCSS 移除已存在样式', () => {
|
||||
themes.applyThemeCSS('dark');
|
||||
expect(() => themes.removeThemeCSS()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('styles.ts 边界', () => {
|
||||
const styles = require('../src/styles.js');
|
||||
|
||||
test('applyThemeVariables 先移除旧样式再追加', () => {
|
||||
const config = { bg: '#fff', text: '#000', border: '#ccc', shadow: 'none', hoverShadow: 'none', progressBg: '#eee', closeHoverBg: '#ddd' };
|
||||
styles.applyThemeVariables(config);
|
||||
styles.applyThemeVariables(config);
|
||||
const els = document.querySelectorAll('#metona-toast-custom-styles');
|
||||
expect(els.length).toBeLessThanOrEqual(1);
|
||||
styles.clearThemeVariables();
|
||||
});
|
||||
|
||||
test('watchSystemTheme 无 matchMedia 时返回空函数', () => {
|
||||
const orig = (window as unknown as Record<string, unknown>).matchMedia;
|
||||
(window as unknown as Record<string, unknown>).matchMedia = undefined;
|
||||
const unsub = styles.watchSystemTheme(jest.fn());
|
||||
expect(unsub).toBeInstanceOf(Function);
|
||||
(window as unknown as Record<string, unknown>).matchMedia = orig;
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.ts use 预设插件', () => {
|
||||
test('use persistence 从 localStorage 恢复配置', () => {
|
||||
localStorage.setItem('metona-toast-config', JSON.stringify({ duration: 3000 }));
|
||||
MeToast.use('persistence');
|
||||
expect(MeToast.getConfig().duration).toBe(3000);
|
||||
MeToast.plugins.unregister('persistence');
|
||||
localStorage.removeItem('metona-toast-config');
|
||||
MeToast.resetConfig();
|
||||
});
|
||||
|
||||
test('use accessibility 注册朗读钩子', () => {
|
||||
const announceSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
MeToast.use('accessibility');
|
||||
MeToast.success('announce-test');
|
||||
MeToast.plugins.unregister('accessibility');
|
||||
announceSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('重复 use persistence 不重复注册保存钩子', () => {
|
||||
localStorage.removeItem('metona-toast-config');
|
||||
MeToast.use('persistence');
|
||||
MeToast.use('persistence');
|
||||
MeToast.plugins.unregister('persistence');
|
||||
expect(Toast._hooks.has('afterClose')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* MetonaToast DOM 交互测试 — 基于 jsdom 真实 DOM 事件驱动
|
||||
* @module tests
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import MeToast, { Toast } from '../src/index';
|
||||
import type { ToastInstance } from '../src/types';
|
||||
|
||||
// jsdom polyfill: PointerEvent 相关方法
|
||||
if (typeof Element !== 'undefined') {
|
||||
if (!(Element.prototype as unknown as { setPointerCapture?: unknown }).setPointerCapture) {
|
||||
(Element.prototype as unknown as Record<string, unknown>).setPointerCapture = jest.fn();
|
||||
}
|
||||
if (!(Element.prototype as unknown as { releasePointerCapture?: unknown }).releasePointerCapture) {
|
||||
(Element.prototype as unknown as Record<string, unknown>).releasePointerCapture = jest.fn();
|
||||
}
|
||||
}
|
||||
|
||||
const pointer = (type: string, x: number, y: number): PointerEvent =>
|
||||
new MouseEvent(type, { clientX: x, clientY: y, bubbles: true }) as unknown as PointerEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MeToast._toasts.clear();
|
||||
MeToast.resetConfig();
|
||||
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||
Toast._hooks.clear();
|
||||
Toast._registry.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
_containerCache.clear();
|
||||
MeToast.animations.destroy();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
describe('toast.ts — DOM 交互', () => {
|
||||
test('onShow 回调异常被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
MeToast.success('x', { onShow: () => { throw new Error('show-err'); } });
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('onUpdate 回调异常被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const t = MeToast.success('x', { onUpdate: () => { throw new Error('upd-err'); } });
|
||||
t.update({ message: 'y' });
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('_applyStyles 字符串 width 生效', () => {
|
||||
const t = MeToast.info({ message: 'w', width: '80%' });
|
||||
expect(t.el!.style.width).toBe('80%');
|
||||
});
|
||||
|
||||
test('点击 close 按钮关闭 toast 且不触发 click 钩子', () => {
|
||||
const fn = jest.fn();
|
||||
Toast.on('click', fn);
|
||||
const t = MeToast.info('test');
|
||||
const closeBtn = t.el!.querySelector('.met-close') as HTMLElement;
|
||||
closeBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(t.closing).toBe(true);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
Toast.off('click', fn);
|
||||
});
|
||||
|
||||
test('点击主体触发 onClick 并关闭', () => {
|
||||
const onClick = jest.fn();
|
||||
const t = MeToast.info('test', { onClick });
|
||||
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(onClick).toHaveBeenCalledWith(t);
|
||||
expect(t.closing).toBe(true);
|
||||
});
|
||||
|
||||
test('onClick 回调异常被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const t = MeToast.info('test', { onClick: () => { throw new Error('click-err'); } });
|
||||
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('closeOnClick false 时点击不关闭但触发 click 钩子', () => {
|
||||
const fn = jest.fn();
|
||||
Toast.on('click', fn);
|
||||
const t = MeToast.info('test', { closeOnClick: false });
|
||||
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(t.closing).toBe(false);
|
||||
expect(fn).toHaveBeenCalled();
|
||||
Toast.off('click', fn);
|
||||
});
|
||||
|
||||
test('hover 触发暂停/恢复与 hover 钩子', () => {
|
||||
const fn = jest.fn();
|
||||
Toast.on('hover', fn);
|
||||
const t = MeToast.info({ message: 'h', duration: 5000 });
|
||||
t.el!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
||||
expect(t.paused).toBe(true);
|
||||
t.el!.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
|
||||
expect(t.paused).toBe(false);
|
||||
expect(fn.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
Toast.off('hover', fn);
|
||||
});
|
||||
|
||||
test('animationstart/animationend 触发钩子', () => {
|
||||
const start = jest.fn();
|
||||
const end = jest.fn();
|
||||
Toast.on('animationStart', start);
|
||||
Toast.on('animationEnd', end);
|
||||
const t = MeToast.info('a');
|
||||
t.el!.dispatchEvent(new Event('animationstart'));
|
||||
t.el!.dispatchEvent(new Event('animationend'));
|
||||
expect(start).toHaveBeenCalled();
|
||||
expect(end).toHaveBeenCalled();
|
||||
Toast.off('animationStart', start);
|
||||
Toast.off('animationEnd', end);
|
||||
});
|
||||
|
||||
test('拖拽 down→move→up 超过阈值触发关闭', (done) => {
|
||||
const t = MeToast.info('drag', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 100, 50));
|
||||
el.dispatchEvent(pointer('pointermove', 250, 60));
|
||||
expect(el.style.transform).toContain('translate(150px');
|
||||
expect(el.style.opacity).toBe('0.25');
|
||||
el.dispatchEvent(pointer('pointerup', 250, 60));
|
||||
expect(el.style.opacity).toBe('0');
|
||||
setTimeout(() => {
|
||||
expect(t.closing).toBe(true);
|
||||
done();
|
||||
}, 350);
|
||||
});
|
||||
|
||||
test('拖拽未超阈值松手恢复', () => {
|
||||
const t = MeToast.info('drag2', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 100, 50));
|
||||
el.dispatchEvent(pointer('pointermove', 150, 60));
|
||||
el.dispatchEvent(pointer('pointerup', 150, 60));
|
||||
expect(t.closing).toBe(false);
|
||||
expect(t.paused).toBe(false);
|
||||
expect(el.style.transform).toBe('');
|
||||
});
|
||||
|
||||
test('拖拽触发 dragStart/dragEnd 钩子', () => {
|
||||
const start = jest.fn();
|
||||
const end = jest.fn();
|
||||
Toast.on('dragStart', start);
|
||||
Toast.on('dragEnd', end);
|
||||
const t = MeToast.info('d', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 0, 0));
|
||||
el.dispatchEvent(pointer('pointerup', 0, 0));
|
||||
expect(start).toHaveBeenCalled();
|
||||
expect(end).toHaveBeenCalled();
|
||||
Toast.off('dragStart', start);
|
||||
Toast.off('dragEnd', end);
|
||||
});
|
||||
|
||||
test('pointercancel 等价于 pointerup', () => {
|
||||
const t = MeToast.info('pc', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 10, 10));
|
||||
el.dispatchEvent(pointer('pointercancel', 10, 10));
|
||||
expect(t.closing).toBe(false);
|
||||
});
|
||||
|
||||
test('拖拽从 close 按钮按下时早退', () => {
|
||||
const t = MeToast.info('dc', { duration: 5000 });
|
||||
const closeBtn = t.el!.querySelector('.met-close') as HTMLElement;
|
||||
closeBtn.dispatchEvent(pointer('pointerdown', 0, 0));
|
||||
expect(t.paused).toBe(false);
|
||||
});
|
||||
|
||||
test('close 非 immediate 走离场动画并清理', (done) => {
|
||||
const t = MeToast.info('anim-close');
|
||||
t.close();
|
||||
expect(t.el!.classList.contains('met-leaving')).toBe(true);
|
||||
setTimeout(() => {
|
||||
expect(t.el).toBeNull();
|
||||
expect(MeToast.count()).toBe(0);
|
||||
done();
|
||||
}, 400);
|
||||
});
|
||||
|
||||
test('updatePosition 移动到新容器', () => {
|
||||
const t = MeToast.info('move', { position: 'top-left' });
|
||||
t.updatePosition('bottom-right');
|
||||
expect(t.config.position).toBe('bottom-right');
|
||||
expect(t.el!.parentElement!.className).toContain('bottom-right');
|
||||
});
|
||||
|
||||
test('updatePosition 同位置直接返回', () => {
|
||||
const t = MeToast.info('same');
|
||||
const container = t.el!.parentElement;
|
||||
t.updatePosition('top-right');
|
||||
expect(t.el!.parentElement).toBe(container);
|
||||
});
|
||||
|
||||
test('_limitToasts registry 无实例时走 _removeToast 兜底', () => {
|
||||
const el = document.createElement('div');
|
||||
const ghost = document.createElement('div');
|
||||
ghost.dataset.id = 'ghost-id';
|
||||
(el.querySelectorAll as unknown) = () => [ghost];
|
||||
const t = new Toast({ message: 'x', max: 0 });
|
||||
expect(() => t._limitToasts(el)).not.toThrow();
|
||||
});
|
||||
|
||||
test('remove() 清理注册表与内存', () => {
|
||||
const t = MeToast.info('rm');
|
||||
expect(Toast._registry.has(t.id)).toBe(true);
|
||||
t.remove();
|
||||
expect(Toast._registry.has(t.id)).toBe(false);
|
||||
expect(MeToast.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.ts — 对话框与按钮交互', () => {
|
||||
test('confirm 点击确认按钮 resolve true', async () => {
|
||||
const promise = MeToast.confirm('确定删除?');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
(toast.el!.querySelector('.met-confirm-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('confirm 点击取消按钮 resolve false', async () => {
|
||||
const promise = MeToast.confirm('确定删除?');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
(toast.el!.querySelector('.met-cancel-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('prompt Enter 提交输入值', async () => {
|
||||
const promise = MeToast.prompt('请输入姓名');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
const input = toast.el!.querySelector('.met-input') as HTMLInputElement;
|
||||
input.value = '张三';
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
||||
const result = await promise;
|
||||
expect(result).toBe('张三');
|
||||
});
|
||||
|
||||
test('prompt 点击提交按钮', async () => {
|
||||
const promise = MeToast.prompt('请输入姓名');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
const input = toast.el!.querySelector('.met-input') as HTMLInputElement;
|
||||
input.value = '李四';
|
||||
(toast.el!.querySelector('.met-submit-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBe('李四');
|
||||
});
|
||||
|
||||
test('prompt 点击取消按钮 resolve null', async () => {
|
||||
const promise = MeToast.prompt('请输入姓名');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
(toast.el!.querySelector('.met-cancel-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('action 按钮点击触发回调并关闭', async () => {
|
||||
const onClick = jest.fn();
|
||||
const a = MeToast.action('msg', [{ text: 'OK', onClick }]);
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||
expect(onClick).toHaveBeenCalledWith(a.toast);
|
||||
expect(a.toast.closing).toBe(true);
|
||||
});
|
||||
|
||||
test('action 按钮 close:false 点击不关闭', async () => {
|
||||
const onClick = jest.fn();
|
||||
const a = MeToast.action('msg', [{ text: 'Keep', onClick, close: false }]);
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||
expect(a.toast.closing).toBe(false);
|
||||
});
|
||||
|
||||
test('action 按钮 onClick 异常被捕获', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const a = MeToast.action('msg', [{ text: 'Boom', onClick: () => { throw new Error('act-err'); } }]);
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('countdown onComplete 异常被捕获', (done) => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
MeToast.countdown('{seconds} 秒', 1, { onComplete: () => { throw new Error('cd-err'); } });
|
||||
setTimeout(() => {
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
done();
|
||||
}, 1600);
|
||||
});
|
||||
|
||||
test('loading control 支持 update 与 dismiss', () => {
|
||||
const l = MeToast.loading('l1');
|
||||
l.update({ message: 'l2' });
|
||||
const toast = MeToast.find(l.id);
|
||||
expect(toast!.message).toBe('l2');
|
||||
l.dismiss();
|
||||
expect(toast!.closing).toBe(true);
|
||||
});
|
||||
|
||||
test('group 单对象参数分支', () => {
|
||||
const g = MeToast.group('obj-group');
|
||||
const t = g.show({ message: 'obj-arg' });
|
||||
expect(t.group).toBe('obj-group');
|
||||
expect(t.message).toBe('obj-arg');
|
||||
g.dismiss();
|
||||
});
|
||||
|
||||
test('queue 消息级 onClose 异常被静默捕获', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const q = MeToast.queue([
|
||||
{ message: 'a', onClose: () => { throw new Error('q-err'); } },
|
||||
], { delay: 5, duration: 10 });
|
||||
await q;
|
||||
expect(consoleError).not.toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('_remove 从内存移除指定 id', () => {
|
||||
const t = MeToast.info('x');
|
||||
MeToast._remove(t.id);
|
||||
expect(MeToast.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugins.ts — 异常与钩子管理', () => {
|
||||
test('插件 install 抛错被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
MeToast.plugins.register('bad-install', { name: 'bad-install', install: () => { throw new Error('install-err'); } });
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('keyboard 插件 install/uninstall 移除事件监听', () => {
|
||||
MeToast.use('keyboard');
|
||||
MeToast.plugins.unregister('keyboard');
|
||||
expect(MeToast.plugins.has('keyboard')).toBe(false);
|
||||
});
|
||||
|
||||
test('persistence 插件 save 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
const pluginUtils = require('../src/plugins.js').pluginUtils;
|
||||
const preset = pluginUtils.getPreset('persistence');
|
||||
const saveFn = (preset as Record<string, (c: unknown) => void>).save;
|
||||
expect(() => saveFn({ duration: 3000 })).not.toThrow();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('persistence 插件 uninstall removeItem 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
const pluginUtils = require('../src/plugins.js').pluginUtils;
|
||||
const preset = pluginUtils.getPreset('persistence');
|
||||
expect(() => preset?.uninstall?.()).not.toThrow();
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast 单元测试
|
||||
* @module tests
|
||||
* @version 0.2.1
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import MeToast, { Toast, VERSION } from '../src/index';
|
||||
@@ -105,8 +105,8 @@ const mockWindow = {
|
||||
|
||||
describe('版本信息', () => {
|
||||
test('应该有正确的版本号', () => {
|
||||
expect(VERSION).toBe('0.2.1');
|
||||
expect(MeToast.version).toBe('0.2.1');
|
||||
expect(VERSION).toBe('0.5.0');
|
||||
expect(MeToast.version).toBe('0.5.0');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -526,7 +526,7 @@ const mockWindow = {
|
||||
test('应该能够获取状态信息', () => {
|
||||
MeToast.success('消息');
|
||||
const status = MeToast.getStatus();
|
||||
expect(status.version).toBe('0.2.1');
|
||||
expect(status.version).toBe('0.5.0');
|
||||
expect(status.toasts).toBeGreaterThanOrEqual(0);
|
||||
expect(status.theme).toBeDefined();
|
||||
expect(status.locale).toBeDefined();
|
||||
@@ -1213,9 +1213,9 @@ describe('新增功能', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ========== v0.2.1 新增功能测试 ==========
|
||||
// ========== v0.4.0 新增功能测试 ==========
|
||||
|
||||
describe('v0.2.1 新增功能', () => {
|
||||
describe('v0.4.0 新增功能', () => {
|
||||
beforeEach(() => {
|
||||
MeToast._toasts.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* MetonaToast React 适配器测试
|
||||
* @module tests
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { act } from 'react';
|
||||
import MeToast, { Toast } from '../src/index';
|
||||
import { createBoundApi, Toast as ToastComponent, useToast } from '../src/react';
|
||||
import type { ToastInstance } from '../src/types';
|
||||
|
||||
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MeToast._toasts.clear();
|
||||
MeToast.resetConfig();
|
||||
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||
Toast._hooks.clear();
|
||||
Toast._registry.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
_containerCache.clear();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
describe('React 适配器 — createBoundApi', () => {
|
||||
test('跟踪创建的 toast id', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
(api.info as (m: string) => ToastInstance)('hello');
|
||||
expect(cleanup.size).toBe(1);
|
||||
});
|
||||
|
||||
test('success/error/warning/show 均被跟踪', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
(api.success as (m: string) => ToastInstance)('a');
|
||||
(api.error as (m: string) => ToastInstance)('b');
|
||||
(api.warning as (m: string) => ToastInstance)('c');
|
||||
(api.show as (m: string) => ToastInstance)('d');
|
||||
expect(cleanup.size).toBe(4);
|
||||
});
|
||||
|
||||
test('loading 控制对象也被跟踪', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
(api.loading as (m: string) => { id: string })('loading...');
|
||||
expect(cleanup.size).toBe(1);
|
||||
});
|
||||
|
||||
test('dismiss 同步清理集合', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
const t = (api.success as (m: string) => ToastInstance)('hi');
|
||||
expect(cleanup.has(t.id)).toBe(true);
|
||||
(api.dismiss as (id?: string) => void)(t.id);
|
||||
expect(cleanup.has(t.id)).toBe(false);
|
||||
});
|
||||
|
||||
test('removeToast 同步清理集合', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
const t = (api.info as (m: string) => ToastInstance)('hi');
|
||||
(api.removeToast as (id: string) => void)(t.id);
|
||||
expect(cleanup.size).toBe(0);
|
||||
});
|
||||
|
||||
test('未跟踪的方法直接透传', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
expect(typeof (api as Record<string, unknown>).count).toBe('function');
|
||||
expect((api as { count(): number }).count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('React 适配器 — Toast 组件', () => {
|
||||
test('SSR 渲染为空(effect 不执行)', () => {
|
||||
const html = renderToString(React.createElement(ToastComponent, { message: 'ssr message' }));
|
||||
expect(html).toBe('');
|
||||
});
|
||||
|
||||
test('SSR 带 type 不抛异常', () => {
|
||||
const html = renderToString(React.createElement(ToastComponent, { message: 'x', type: 'success' }));
|
||||
expect(html).toBe('');
|
||||
});
|
||||
|
||||
test('SSR 未知 type fallback 到 show 不抛异常', () => {
|
||||
const html = renderToString(React.createElement(ToastComponent, { message: 'y', type: 'custom-type' }));
|
||||
expect(html).toBe('');
|
||||
});
|
||||
|
||||
test('组件挂载创建 toast,卸载自动移除', () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'mounted', type: 'success' })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(0);
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('组件 autoClose:false 卸载不移除', () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'keep', type: 'error', autoClose: false })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
MeToast.dismiss();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('组件 message 变化重新创建实例', () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'v1', type: 'info' })); });
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'v2', type: 'warning' })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
const t = MeToast.getToasts()[0];
|
||||
expect(t.message).toBe('v2');
|
||||
expect(t.type).toBe('warning');
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(0);
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('useToast 组件内创建的 toast 在卸载时清理', () => {
|
||||
const Harness = (): React.ReactElement => {
|
||||
const toast = useToast() as Record<string, (m: string) => ToastInstance>;
|
||||
return React.createElement('button', { onClick: () => toast.info('tracked') });
|
||||
};
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(Harness)); });
|
||||
const btn = host.querySelector('button')!;
|
||||
act(() => { btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(0);
|
||||
host.remove();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Jest 环境补丁 — jsdom 未暴露 Node 全局对象
|
||||
*/
|
||||
import { TextEncoder, TextDecoder } from 'util';
|
||||
|
||||
const g = globalThis as Record<string, unknown>;
|
||||
|
||||
if (typeof g.TextEncoder === 'undefined') {
|
||||
g.TextEncoder = TextEncoder;
|
||||
}
|
||||
if (typeof g.TextDecoder === 'undefined') {
|
||||
g.TextDecoder = TextDecoder;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 环境隔离测试 — 需要切换 document/window/localStorage 环境并调用 jest.resetModules()。
|
||||
* 独立成文件:resetModules 会清空模块缓存,若与其他测试同文件会导致
|
||||
* 后续 require 拿到新模块实例、与 import 时绑定的实例状态分裂。
|
||||
* 注:jsdom 以 getter 暴露 document/window,普通赋值无效,须用 defineProperty 覆盖。
|
||||
* @module tests
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
/** 临时移除全局对象,返回恢复函数 */
|
||||
const hideGlobal = (key: string): (() => void) => {
|
||||
const desc = Object.getOwnPropertyDescriptor(global, key);
|
||||
Object.defineProperty(global, key, { value: undefined, configurable: true, writable: true });
|
||||
return () => {
|
||||
if (desc) Object.defineProperty(global, key, desc);
|
||||
else delete (global as Record<string, unknown>)[key];
|
||||
};
|
||||
};
|
||||
|
||||
describe('环境隔离(SSR / localStorage 异常路径)', () => {
|
||||
test('escapeHTML SSR 路径(无 document)', () => {
|
||||
const restore = hideGlobal('document');
|
||||
jest.resetModules();
|
||||
const { escapeHTML } = require('../src/utils.js');
|
||||
const result = escapeHTML('<script>alert("xss")</script>');
|
||||
expect(result).not.toContain('<script>');
|
||||
expect(result).toContain('<script>');
|
||||
restore();
|
||||
});
|
||||
|
||||
test('prefersDark 无 window 时返回 false', () => {
|
||||
const restore = hideGlobal('window');
|
||||
jest.resetModules();
|
||||
const { prefersDark } = require('../src/utils.js');
|
||||
expect(prefersDark()).toBe(false);
|
||||
restore();
|
||||
});
|
||||
|
||||
test('saveTheme localStorage 异常被捕获', () => {
|
||||
const orig = (global as Record<string, unknown>).localStorage;
|
||||
(global as Record<string, unknown>).localStorage = undefined;
|
||||
jest.resetModules();
|
||||
const { saveTheme } = require('../src/themes.js');
|
||||
expect(() => saveTheme('dark')).not.toThrow();
|
||||
(global as Record<string, unknown>).localStorage = orig;
|
||||
});
|
||||
|
||||
test('loadLocale localStorage 异常返回 default', () => {
|
||||
const orig = (global as Record<string, unknown>).localStorage;
|
||||
(global as Record<string, unknown>).localStorage = {
|
||||
getItem: () => { throw new Error('quota'); },
|
||||
};
|
||||
jest.resetModules();
|
||||
const { loadLocale } = require('../src/i18n.js');
|
||||
expect(typeof loadLocale()).toBe('string');
|
||||
(global as Record<string, unknown>).localStorage = orig;
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user