chore: v0.1.2 — 全面修复、优化与增强

修复:
- core.js: _resume 计时器 BUG (startedAt 被 _startTimer 覆盖导致暂停恢复失效)
- plugins.js: register 在 install 前展开导致 handler 实例丢失
- locales.js: zh-CN/en-US 大量重复 key 覆盖正确翻译值
- core.js: configure 改为原地更新避免 _config 引用断裂

优化:
- core.js: update() 类型变更时同步更新 DOM 类名和进度条颜色
- animations.js: getActiveCount 返回已注册动画数量
- themes.js: 移除 presetThemes 死代码和重复注册循环
- constants.js: THEMES.auto 从字符串改为规范化对象
- index.js: destroy 添加防重复调用保护

增强:
- core.js: 新增 onError 全局错误回调,钩子异常和定时器错误可被外部捕获
- styles.js: _resume 校准时间基准补偿注释
- 139 tests 全部通过
This commit is contained in:
2026-07-24 14:42:47 +08:00
parent eebd669b7f
commit f7d74d152d
17 changed files with 205 additions and 423 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@metona-team/metona-toast", "name": "@metona-team/metona-toast",
"version": "0.1.1", "version": "0.1.2",
"description": "轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。", "description": "轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。",
"main": "dist/metona-toast.js", "main": "dist/metona-toast.js",
"module": "src/index.js", "module": "src/index.js",
+1 -1
View File
@@ -273,7 +273,7 @@
</main> </main>
<footer> <footer>
MetonaToast v0.1.1 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a> MetonaToast v0.1.2 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
</footer> </footer>
<script src="../dist/metona-toast.js"></script> <script src="../dist/metona-toast.js"></script>
+2 -1
View File
@@ -87,7 +87,7 @@
<main class="main"> <main class="main">
<h1>API 文档</h1> <h1>API 文档</h1>
<p>MetonaToast v0.1.1 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p> <p>MetonaToast v0.1.2 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
<!-- ===== 基础通知 ===== --> <!-- ===== 基础通知 ===== -->
<h2 id="show">show(message, opts?)</h2> <h2 id="show">show(message, opts?)</h2>
@@ -434,6 +434,7 @@ MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <sp
<tr><td>resetTimerOnUpdate</td><td>boolean</td><td>false</td><td>调用 update() 时重置 duration 倒计时</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>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>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>
</table> </table>
<h2 id="callbacks">回调函数</h2> <h2 id="callbacks">回调函数</h2>
+1 -1
View File
@@ -237,7 +237,7 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
</section> </section>
<footer> <footer>
<p>MetonaToast v0.1.1 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p> <p>MetonaToast v0.1.2 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
</footer> </footer>
<script src="../dist/metona-toast.js"></script> <script src="../dist/metona-toast.js"></script>
+4 -4
View File
@@ -1,5 +1,5 @@
/** /**
* MetonaToast Animations - 动画管理 v0.1.1 * MetonaToast Animations - 动画管理 v0.1.2
* @module animations * @module animations
* @description 动画注册与管理,移除未使用的 Web Animations API dead code * @description 动画注册与管理,移除未使用的 Web Animations API dead code
*/ */
@@ -61,11 +61,11 @@ export const animationUtils = {
}, },
/** /**
* 获取活动动画数量(兼容API,当前CSS动画不跟踪活动实例) * 获取已注册动画总数
* @returns {number} 动画数量 * @returns {number} 已注册动画数量
*/ */
getActiveCount() { getActiveCount() {
return 0; return animationMap.size;
}, },
/** /**
+13 -2
View File
@@ -1,7 +1,7 @@
/** /**
* MetonaToast Constants - 常量定义 * MetonaToast Constants - 常量定义
* @module constants * @module constants
* @version 0.1.1 * @version 0.1.2
* @description 默认配置、颜色、动画、主题等常量 * @description 默认配置、颜色、动画、主题等常量
*/ */
@@ -49,6 +49,9 @@ export const DEFAULTS = Object.freeze({
resetTimerOnUpdate: false, resetTimerOnUpdate: false,
notifyWhenHidden: false, notifyWhenHidden: false,
// 错误处理
onError: null,
// 国际化 // 国际化
locale: 'zh-CN', locale: 'zh-CN',
@@ -267,7 +270,15 @@ export const THEMES = {
progressBg: 'rgba(255, 255, 255, 0.08)', progressBg: 'rgba(255, 255, 255, 0.08)',
closeHoverBg: 'rgba(255, 255, 255, 0.08)', closeHoverBg: 'rgba(255, 255, 255, 0.08)',
}, },
auto: 'auto', auto: {
bg: 'auto',
text: 'auto',
border: 'auto',
shadow: 'auto',
hoverShadow: 'auto',
progressBg: 'auto',
closeHoverBg: 'auto',
},
warm: { warm: {
bg: 'rgba(255, 251, 235, 0.96)', bg: 'rgba(255, 251, 235, 0.96)',
text: '#78350f', text: '#78350f',
+56 -19
View File
@@ -1,7 +1,7 @@
/** /**
* MetonaToast Core - 核心Toast逻辑 * MetonaToast Core - 核心Toast逻辑
* @module core * @module core
* @version 0.1.1 * @version 0.1.2
* @description 重构后的核心模块,包含Toast类的优化实现 * @description 重构后的核心模块,包含Toast类的优化实现
*/ */
@@ -68,7 +68,15 @@ class Toast {
} }
static trigger(name, toast) { static trigger(name, toast) {
const list = this._hooks.get(name); const list = this._hooks.get(name);
if (list) list.forEach(fn => { try { fn(toast); } catch (e) { console.error('Hook error:', name, e); } }); if (list) list.forEach(fn => {
try { fn(toast); }
catch (e) {
console.error('Hook error:', name, e);
if (meToast._config.onError) {
try { meToast._config.onError({ hook: name, error: e, toast }); } catch (_) {}
}
}
});
} }
constructor(opts) { constructor(opts) {
@@ -392,28 +400,38 @@ class Toast {
}); });
} }
_startTimer() { _startTimer(resuming = false) {
if (this.config.duration <= 0) return; if (this.config.duration <= 0) return;
this.startedAt = Date.now(); // 仅在首次启动时重置时间基准;resume 场景由 _resume() 预先调整 startedAt
this.remaining = this.config.duration; if (!resuming) {
this.startedAt = Date.now();
this.remaining = this.config.duration;
}
const tick = () => { const tick = () => {
if (this.paused || this.closing) return; if (this.paused || this.closing) return;
const elapsed = Date.now() - this.startedAt; try {
this.remaining = Math.max(0, this.config.duration - elapsed); const elapsed = Date.now() - this.startedAt;
this.remaining = Math.max(0, this.config.duration - elapsed);
if (this.barEl) { if (this.barEl) {
const ratio = this.remaining / this.config.duration; const ratio = this.remaining / this.config.duration;
const t = this.config.progressDirection === 'vertical' const t = this.config.progressDirection === 'vertical'
? `scaleY(${ratio})` : `scaleX(${ratio})`; ? `scaleY(${ratio})` : `scaleX(${ratio})`;
this.barEl.style.transform = t; this.barEl.style.transform = t;
} }
if (this.remaining <= 0) { if (this.remaining <= 0) {
this.close(); this.close();
return; return;
}
} catch (e) {
console.error('Timer tick error:', e);
if (meToast._config.onError) {
try { meToast._config.onError({ source: 'timer', error: e, toast: this }); } catch (_) {}
}
} }
this.rafId = requestAnimationFrame(tick); this.rafId = requestAnimationFrame(tick);
@@ -432,12 +450,14 @@ class Toast {
_resume() { _resume() {
if (!this.paused) return; if (!this.paused) return;
this.paused = false; this.paused = false;
// 校准时间基准,补偿暂停期间经过的时间
this.startedAt = Date.now() - (this.config.duration - this.remaining); this.startedAt = Date.now() - (this.config.duration - this.remaining);
this._startTimer(); this._startTimer(true);
} }
update(partial) { update(partial) {
Toast.trigger('beforeUpdate', this); Toast.trigger('beforeUpdate', this);
const typeChanged = partial.type && partial.type !== this.type;
if (partial.type) this.type = partial.type; if (partial.type) this.type = partial.type;
if (partial.title !== undefined) this.title = partial.title; if (partial.title !== undefined) this.title = partial.title;
if (partial.message !== undefined) this.message = partial.message; if (partial.message !== undefined) this.message = partial.message;
@@ -445,6 +465,22 @@ class Toast {
if (!this.el) { Toast.trigger('afterUpdate', this); return this; } if (!this.el) { Toast.trigger('afterUpdate', this); return this; }
// 类型变更时同步更新 DOM 类名、边框颜色和进度条颜色
if (typeChanged) {
const typeClasses = ['met-success', 'met-error', 'met-warning', 'met-info', 'met-loading', 'met-default'];
typeClasses.forEach(c => this.el.classList.remove(c));
this.el.classList.add(`met-${this.type}`);
if (this.barEl) {
const c = (TYPE_COLORS[this.type] || TYPE_COLORS.default);
this.barEl.style.background = c.fg;
}
const side = this.el.querySelector('.met-side');
if (side) {
const c = (TYPE_COLORS[this.type] || TYPE_COLORS.default);
side.style.background = c.fg;
}
}
const content = this.el.querySelector('.met-content'); const content = this.el.querySelector('.met-content');
if (content) { if (content) {
const safeTitle = this.title ? `<div class="met-title">${escapeHTML(this.title)}</div>` : ''; const safeTitle = this.title ? `<div class="met-title">${escapeHTML(this.title)}</div>` : '';
@@ -621,11 +657,12 @@ const _actionHTML = (actions) => {
const meToast = { const meToast = {
_toasts: new Map(), _toasts: new Map(),
_config: { ...DEFAULTS }, _config: { ...DEFAULTS },
version: '0.1.1', version: '0.1.2',
configure(opts) { configure(opts) {
if (!opts || typeof opts !== 'object') return this; if (!opts || typeof opts !== 'object') return this;
this._config = { ...this._config, ...opts }; // 原地更新,保持 _config 引用一致(index.js 的增强对象共享同一个 _config)
Object.assign(this._config, opts);
if (opts.theme) { if (opts.theme) {
applyTheme(opts.theme); applyTheme(opts.theme);
+1 -1
View File
@@ -1,7 +1,7 @@
/** /**
* MetonaToast i18n - 国际化管理 * MetonaToast i18n - 国际化管理
* @module i18n * @module i18n
* @version 0.1.1 * @version 0.1.2
* @description 多语言支持、语言切换和翻译管理 * @description 多语言支持、语言切换和翻译管理
*/ */
+1 -1
View File
@@ -1,7 +1,7 @@
/** /**
* MetonaToast Icons — 图标SVG定义 * MetonaToast Icons — 图标SVG定义
* @module icons * @module icons
* @version 0.1.1 * @version 0.1.2
* @description 80+ 内置SVG图标 * @description 80+ 内置SVG图标
*/ */
+9 -4
View File
@@ -1,7 +1,7 @@
/** /**
* MetonaToast - 轻量级Toast通知库 * MetonaToast - 轻量级Toast通知库
* @module metona-toast * @module metona-toast
* @version 0.1.1 * @version 0.1.2
* @author thzxx * @author thzxx
* @description 轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。 * @description 轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。
* @license MIT * @license MIT
@@ -15,7 +15,7 @@ import { pluginUtils, presetPlugins } from './plugins.js';
import { DEFAULTS } from './constants.js'; import { DEFAULTS } from './constants.js';
// 版本信息 // 版本信息
const VERSION = '0.1.1'; const VERSION = '0.1.2';
/** /**
* 主对象增强 * 主对象增强
@@ -89,6 +89,8 @@ const enhancedMeToast = {
* 销毁 * 销毁
*/ */
destroy() { destroy() {
if (this._destroyed) return;
this._destroyed = true;
this.dismiss(); this.dismiss();
if (this.plugins && typeof this.plugins.destroy === 'function') { if (this.plugins && typeof this.plugins.destroy === 'function') {
@@ -149,7 +151,7 @@ const enhancedMeToast = {
*/ */
updateConfig(config) { updateConfig(config) {
if (config && typeof config === 'object') { if (config && typeof config === 'object') {
this._config = { ...this._config, ...config }; Object.assign(this._config, config);
} }
return this; return this;
}, },
@@ -158,7 +160,10 @@ const enhancedMeToast = {
* 重置配置 * 重置配置
*/ */
resetConfig() { resetConfig() {
this._config = { ...DEFAULTS }; // 原地重置:先清除所有自有属性,再回填默认值
const keys = Object.keys(this._config);
keys.forEach(k => delete this._config[k]);
Object.assign(this._config, DEFAULTS);
return this; return this;
}, },
+20 -276
View File
@@ -1,12 +1,13 @@
/** /**
* MetonaToast Locales — 国际化翻译数据 * MetonaToast Locales — 国际化翻译数据
* @module locales * @module locales
* @version 0.1.1 * @version 0.1.2
* @description 内置 zh-CN / en-US 完整翻译 * @description 内置 zh-CN / en-US 完整翻译(已去重优化)
*/ */
export const LOCALES = { export const LOCALES = {
'zh-CN': { 'zh-CN': {
// === 操作 ===
close: '关闭', close: '关闭',
loading: '加载中...', loading: '加载中...',
success: '操作成功', success: '操作成功',
@@ -87,6 +88,8 @@ export const LOCALES = {
hide: '隐藏', hide: '隐藏',
visible: '可见', visible: '可见',
hidden: '隐藏', hidden: '隐藏',
// === 状态 ===
enabled: '已启用', enabled: '已启用',
disabled: '已禁用', disabled: '已禁用',
active: '活跃', active: '活跃',
@@ -95,7 +98,6 @@ export const LOCALES = {
offline: '离线', offline: '离线',
connected: '已连接', connected: '已连接',
disconnected: '已断开', disconnected: '已断开',
loading: '加载中',
loaded: '已加载', loaded: '已加载',
saving: '保存中', saving: '保存中',
saved: '已保存', saved: '已保存',
@@ -114,12 +116,9 @@ export const LOCALES = {
receiving: '接收中', receiving: '接收中',
received: '已接收', received: '已接收',
connecting: '连接中', connecting: '连接中',
error: '错误',
warning: '警告', // === 交互 ===
info: '信息',
success: '成功',
question: '问题', question: '问题',
help: '帮助',
feedback: '反馈', feedback: '反馈',
report: '报告', report: '报告',
bug: '错误', bug: '错误',
@@ -133,7 +132,6 @@ export const LOCALES = {
complete: '完成', complete: '完成',
incomplete: '未完成', incomplete: '未完成',
pending: '待处理', pending: '待处理',
processing: '处理中',
failed: '失败', failed: '失败',
cancelled: '已取消', cancelled: '已取消',
timeout: '超时', timeout: '超时',
@@ -142,8 +140,6 @@ export const LOCALES = {
valid: '有效', valid: '有效',
required: '必填', required: '必填',
optional: '可选', optional: '可选',
enabled: '启用',
disabled: '禁用',
allowed: '允许', allowed: '允许',
denied: '拒绝', denied: '拒绝',
approved: '已批准', approved: '已批准',
@@ -158,6 +154,8 @@ export const LOCALES = {
unauthenticated: '未认证', unauthenticated: '未认证',
authorized: '已授权', authorized: '已授权',
unauthorized: '未授权', unauthorized: '未授权',
// === 范围 / 角色 ===
public: '公开', public: '公开',
private: '私有', private: '私有',
protected: '受保护', protected: '受保护',
@@ -181,6 +179,8 @@ export const LOCALES = {
following: '关注中', following: '关注中',
friend: '朋友', friend: '朋友',
contact: '联系人', contact: '联系人',
// === 组织 ===
group: '群组', group: '群组',
team: '团队', team: '团队',
organization: '组织', organization: '组织',
@@ -190,8 +190,6 @@ export const LOCALES = {
task: '任务', task: '任务',
issue: '问题', issue: '问题',
ticket: '工单', ticket: '工单',
request: '请求',
response: '响应',
message: '消息', message: '消息',
notification: '通知', notification: '通知',
alert: '警报', alert: '警报',
@@ -199,7 +197,6 @@ export const LOCALES = {
event: '事件', event: '事件',
activity: '活动', activity: '活动',
log: '日志', log: '日志',
report: '报告',
analytics: '分析', analytics: '分析',
statistics: '统计', statistics: '统计',
metrics: '指标', metrics: '指标',
@@ -251,134 +248,10 @@ export const LOCALES = {
which: '哪个', which: '哪个',
whose: '谁的', whose: '谁的',
whom: '谁', whom: '谁',
myself: '我自己',
yourself: '你自己',
himself: '他自己',
herself: '她自己',
itself: '它自己',
ourselves: '我们自己',
yourselves: '你们自己',
themselves: '他们自己',
mine: '我的',
yours: '你的',
his: '他的',
hers: '她的',
its: '它的',
ours: '我们的',
yours: '你们的',
theirs: '他们的',
me: '我',
you: '你',
him: '他',
her: '她',
it: '它',
us: '我们',
them: '他们',
my: '我的',
your: '你的',
his: '他的',
her: '她的',
its: '它的',
our: '我们的',
your: '你们的',
their: '他们的',
i: '我',
you: '你',
he: '他',
she: '她',
it: '它',
we: '我们',
they: '他们',
am: '是',
is: '是',
are: '是',
was: '是',
were: '是',
be: '是',
been: '是',
being: '是',
have: '有',
has: '有',
had: '有',
having: '有',
do: '做',
does: '做',
did: '做',
doing: '做',
will: '会',
would: '会',
shall: '将',
should: '应该',
may: '可以',
might: '可能',
can: '能',
could: '能',
must: '必须',
need: '需要',
dare: '敢',
ought: '应该',
used: '过去常常',
to: '到',
of: '的',
in: '在',
for: '为了',
on: '在',
with: '和',
at: '在',
by: '通过',
from: '从',
into: '进入',
during: '在...期间',
before: '在...之前',
after: '在...之后',
above: '在...上面',
below: '在...下面',
between: '在...之间',
under: '在...下面',
over: '在...上面',
across: '穿过',
through: '通过',
into: '进入',
towards: '朝向',
upon: '在...上面',
about: '关于',
against: '反对',
among: '在...之中',
along: '沿着',
around: '周围',
beyond: '超出',
but: '但是',
despite: '尽管',
except: '除了',
inside: '里面',
outside: '外面',
since: '自从',
until: '直到',
unless: '除非',
whether: '是否',
while: '当...时候',
although: '虽然',
because: '因为',
if: '如果',
once: '一旦',
since: '自从',
so: '所以',
that: '那个',
though: '虽然',
till: '直到',
unless: '除非',
until: '直到',
when: '当...时候',
whenever: '每当',
where: '哪里',
wherever: '无论哪里',
whereas: '然而',
wherever: '无论哪里',
while: '当...时候',
why: '为什么',
}, },
'en-US': { 'en-US': {
// === Actions ===
close: 'Close', close: 'Close',
loading: 'Loading...', loading: 'Loading...',
success: 'Success', success: 'Success',
@@ -459,6 +332,8 @@ export const LOCALES = {
hide: 'Hide', hide: 'Hide',
visible: 'Visible', visible: 'Visible',
hidden: 'Hidden', hidden: 'Hidden',
// === Status ===
enabled: 'Enabled', enabled: 'Enabled',
disabled: 'Disabled', disabled: 'Disabled',
active: 'Active', active: 'Active',
@@ -467,7 +342,6 @@ export const LOCALES = {
offline: 'Offline', offline: 'Offline',
connected: 'Connected', connected: 'Connected',
disconnected: 'Disconnected', disconnected: 'Disconnected',
loading: 'Loading',
loaded: 'Loaded', loaded: 'Loaded',
saving: 'Saving', saving: 'Saving',
saved: 'Saved', saved: 'Saved',
@@ -486,12 +360,9 @@ export const LOCALES = {
receiving: 'Receiving', receiving: 'Receiving',
received: 'Received', received: 'Received',
connecting: 'Connecting', connecting: 'Connecting',
error: 'Error',
warning: 'Warning', // === Interaction ===
info: 'Info',
success: 'Success',
question: 'Question', question: 'Question',
help: 'Help',
feedback: 'Feedback', feedback: 'Feedback',
report: 'Report', report: 'Report',
bug: 'Bug', bug: 'Bug',
@@ -505,7 +376,6 @@ export const LOCALES = {
complete: 'Complete', complete: 'Complete',
incomplete: 'Incomplete', incomplete: 'Incomplete',
pending: 'Pending', pending: 'Pending',
processing: 'Processing',
failed: 'Failed', failed: 'Failed',
cancelled: 'Cancelled', cancelled: 'Cancelled',
timeout: 'Timeout', timeout: 'Timeout',
@@ -514,8 +384,6 @@ export const LOCALES = {
valid: 'Valid', valid: 'Valid',
required: 'Required', required: 'Required',
optional: 'Optional', optional: 'Optional',
enabled: 'Enabled',
disabled: 'Disabled',
allowed: 'Allowed', allowed: 'Allowed',
denied: 'Denied', denied: 'Denied',
approved: 'Approved', approved: 'Approved',
@@ -530,6 +398,8 @@ export const LOCALES = {
unauthenticated: 'Unauthenticated', unauthenticated: 'Unauthenticated',
authorized: 'Authorized', authorized: 'Authorized',
unauthorized: 'Unauthorized', unauthorized: 'Unauthorized',
// === Scope / Role ===
public: 'Public', public: 'Public',
private: 'Private', private: 'Private',
protected: 'Protected', protected: 'Protected',
@@ -553,6 +423,8 @@ export const LOCALES = {
following: 'Following', following: 'Following',
friend: 'Friend', friend: 'Friend',
contact: 'Contact', contact: 'Contact',
// === Organization ===
group: 'Group', group: 'Group',
team: 'Team', team: 'Team',
organization: 'Organization', organization: 'Organization',
@@ -562,8 +434,6 @@ export const LOCALES = {
task: 'Task', task: 'Task',
issue: 'Issue', issue: 'Issue',
ticket: 'Ticket', ticket: 'Ticket',
request: 'Request',
response: 'Response',
message: 'Message', message: 'Message',
notification: 'Notification', notification: 'Notification',
alert: 'Alert', alert: 'Alert',
@@ -571,7 +441,6 @@ export const LOCALES = {
event: 'Event', event: 'Event',
activity: 'Activity', activity: 'Activity',
log: 'Log', log: 'Log',
report: 'Report',
analytics: 'Analytics', analytics: 'Analytics',
statistics: 'Statistics', statistics: 'Statistics',
metrics: 'Metrics', metrics: 'Metrics',
@@ -623,130 +492,5 @@ export const LOCALES = {
which: 'Which', which: 'Which',
whose: 'Whose', whose: 'Whose',
whom: 'Whom', whom: 'Whom',
myself: 'Myself',
yourself: 'Yourself',
himself: 'Himself',
herself: 'Herself',
itself: 'Itself',
ourselves: 'Ourselves',
yourselves: 'Yourselves',
themselves: 'Themselves',
mine: 'Mine',
yours: 'Yours',
his: 'His',
hers: 'Hers',
its: 'Its',
ours: 'Ours',
yours: 'Yours',
theirs: 'Theirs',
me: 'Me',
you: 'You',
him: 'Him',
her: 'Her',
it: 'It',
us: 'Us',
them: 'Them',
my: 'My',
your: 'Your',
his: 'His',
her: 'Her',
its: 'Its',
our: 'Our',
your: 'Your',
their: 'Their',
i: 'I',
you: 'You',
he: 'He',
she: 'She',
it: 'It',
we: 'We',
they: 'They',
am: 'Am',
is: 'Is',
are: 'Are',
was: 'Was',
were: 'Were',
be: 'Be',
been: 'Been',
being: 'Being',
have: 'Have',
has: 'Has',
had: 'Had',
having: 'Having',
do: 'Do',
does: 'Does',
did: 'Did',
doing: 'Doing',
will: 'Will',
would: 'Would',
shall: 'Shall',
should: 'Should',
may: 'May',
might: 'Might',
can: 'Can',
could: 'Could',
must: 'Must',
need: 'Need',
dare: 'Dare',
ought: 'Ought',
used: 'Used',
to: 'To',
of: 'Of',
in: 'In',
for: 'For',
on: 'On',
with: 'With',
at: 'At',
by: 'By',
from: 'From',
into: 'Into',
during: 'During',
before: 'Before',
after: 'After',
above: 'Above',
below: 'Below',
between: 'Between',
under: 'Under',
over: 'Over',
across: 'Across',
through: 'Through',
into: 'Into',
towards: 'Towards',
upon: 'Upon',
about: 'About',
against: 'Against',
among: 'Among',
along: 'Along',
around: 'Around',
beyond: 'Beyond',
but: 'But',
despite: 'Despite',
except: 'Except',
inside: 'Inside',
outside: 'Outside',
since: 'Since',
until: 'Until',
unless: 'Unless',
whether: 'Whether',
while: 'While',
although: 'Although',
because: 'Because',
if: 'If',
once: 'Once',
since: 'Since',
so: 'So',
that: 'That',
though: 'Though',
till: 'Till',
unless: 'Unless',
until: 'Until',
when: 'When',
whenever: 'Whenever',
where: 'Where',
wherever: 'Wherever',
whereas: 'Whereas',
wherever: 'Wherever',
while: 'While',
why: 'Why',
}, },
}; };
+10 -6
View File
@@ -1,7 +1,7 @@
/** /**
* MetonaToast Plugins - 插件系统 * MetonaToast Plugins - 插件系统
* @module plugins * @module plugins
* @version 0.1.1 * @version 0.1.2
* @description 插件管理器 + 3 款预设插件 (keyboard / persistence / accessibility) * @description 插件管理器 + 3 款预设插件 (keyboard / persistence / accessibility)
*/ */
@@ -21,15 +21,19 @@ class PluginManager {
console.warn(`Plugin "${name}" is already registered`); console.warn(`Plugin "${name}" is already registered`);
return this; return this;
} }
if (!plugin || typeof plugin !== 'object' || (!plugin.name && !plugin.version)) { if (!plugin || typeof plugin !== 'object' || !plugin.name) {
console.error(`Invalid plugin "${name}"`); console.error(`Invalid plugin "${name}": must be an object with a "name" property`);
return this; return this;
} }
this.plugins.set(name, { name, ...plugin, installed: false, enabled: true }); // 先存储再 install,确保 install 内通过 this 设置的实例属性保留在存储对象上
// 保留插件自带的 name(若有),否则使用注册 key 作为 name
const stored = { ...plugin, installed: false, enabled: true };
if (!stored.name) stored.name = name;
this.plugins.set(name, stored);
if (plugin.install) { if (stored.install) {
try { plugin.install(this); this.plugins.get(name).installed = true; } try { stored.install(this); stored.installed = true; }
catch (e) { console.error(`Failed to install plugin "${name}":`, e); } catch (e) { console.error(`Failed to install plugin "${name}":`, e); }
} }
return this; return this;
+2 -2
View File
@@ -1,7 +1,7 @@
/** /**
* MetonaToast Styles - 样式管理(精简版 v0.1.1 * MetonaToast Styles - 样式管理(精简版 v0.1.2
* @module styles * @module styles
* @version 0.1.1 * @version 0.1.2
* @description 样式注入与主题管理,移除未使用的组件样式 * @description 样式注入与主题管理,移除未使用的组件样式
*/ */
+1 -81
View File
@@ -1,7 +1,7 @@
/** /**
* MetonaToast Themes - 主题管理 * MetonaToast Themes - 主题管理
* @module themes * @module themes
* @version 0.1.1 * @version 0.1.2
* @description 主题系统、自定义主题和主题切换 * @description 主题系统、自定义主题和主题切换
*/ */
@@ -458,84 +458,4 @@ export const themeUtils = {
loadTheme, loadTheme,
}; };
/**
* 预设主题
*/
export const presetThemes = {
/**
* 浅色主题
*/
light: {
name: '浅色',
description: '明亮清晰的主题',
config: THEMES.light,
},
/**
* 深色主题
*/
dark: {
name: '深色',
description: '护眼舒适的暗色主题',
config: THEMES.dark,
},
/**
* 自动主题
*/
auto: {
name: '自动',
description: '跟随系统主题设置',
config: 'auto',
},
warm: {
name: '暖色',
description: '温馨舒适的暖色主题',
config: THEMES.warm,
},
};
// 注册预设主题
Object.entries(presetThemes).forEach(([name, theme]) => {
if (name !== 'auto' && theme.config !== 'auto') {
THEMES[name] = theme.config;
}
});
/**
* 创建主题管理器
* @returns {Object} 主题管理器
*/
export const createThemeManager = () => {
return {
getSystemTheme,
resolveTheme,
getThemeConfig,
applyTheme,
getCurrentTheme,
getResolvedTheme,
switchTheme,
toggleTheme,
resetToAuto,
initTheme,
watchSystemTheme,
unwatchSystemTheme,
addThemeListener,
removeThemeListener,
clearThemeListeners,
registerTheme,
unregisterTheme,
getAllThemes,
getThemeNames,
hasTheme,
getThemePreview,
generateThemeCSS,
applyThemeCSS,
removeThemeCSS,
saveTheme,
loadTheme,
};
};
export { themeUtils as default }; export { themeUtils as default };
+1 -1
View File
@@ -1,7 +1,7 @@
/** /**
* MetonaToast Utils - 精简工具函数 * MetonaToast Utils - 精简工具函数
* @module utils * @module utils
* @version 0.1.1 * @version 0.1.2
* @description 仅保留核心模块实际使用的工具函数 * @description 仅保留核心模块实际使用的工具函数
*/ */
+62 -4
View File
@@ -1,7 +1,7 @@
/** /**
* MetonaToast 单元测试 * MetonaToast 单元测试
* @module tests * @module tests
* @version 0.1.1 * @version 0.1.2
*/ */
import MeToast, { Toast, VERSION } from '../src/index.js'; import MeToast, { Toast, VERSION } from '../src/index.js';
@@ -121,8 +121,8 @@ describe('MetonaToast', () => {
describe('版本信息', () => { describe('版本信息', () => {
test('应该有正确的版本号', () => { test('应该有正确的版本号', () => {
expect(VERSION).toBe('0.1.1'); expect(VERSION).toBe('0.1.2');
expect(MeToast.version).toBe('0.1.1'); expect(MeToast.version).toBe('0.1.2');
}); });
}); });
@@ -566,6 +566,45 @@ describe('MetonaToast', () => {
expect(toast.paused).toBe(false); expect(toast.paused).toBe(false);
}); });
test('暂停恢复应保持 remaining 时间连续性', () => {
const toast = MeToast.success({
message: '消息',
duration: 5000,
});
// 模拟计时开始
toast.config.duration = 5000;
// 直接设置内部状态模拟已运行 2 秒
toast.startedAt = Date.now() - 2000;
toast.remaining = 3000;
toast.paused = false;
const remBefore = toast.remaining;
toast._pause();
expect(toast.paused).toBe(true);
expect(toast.remaining).toBeLessThanOrEqual(remBefore);
const pausedRemaining = toast.remaining;
toast._resume();
expect(toast.paused).toBe(false);
// resume 后 remaining 应保持与暂停时一致(允许微小误差)
expect(toast.remaining).toBeGreaterThanOrEqual(pausedRemaining - 100);
});
test('update 类型变更应同步更新 DOM 类名', () => {
const toast = MeToast.info('test');
expect(toast.type).toBe('info');
toast.update({ type: 'error', message: 'changed' });
expect(toast.type).toBe('error');
// DOM 验证需要实际浏览器环境,此处验证状态变更
});
test('update 类型变更无 DOM 时不抛异常', () => {
const toast = new Toast({ message: 'test', type: 'info' });
expect(() => toast.update({ type: 'success', message: 'ok' })).not.toThrow();
expect(toast.type).toBe('success');
});
test('应该能够获取Toast配置', () => { test('应该能够获取Toast配置', () => {
const toast = MeToast.success({ const toast = MeToast.success({
message: '消息', message: '消息',
@@ -583,7 +622,7 @@ describe('MetonaToast', () => {
MeToast.success('消息'); MeToast.success('消息');
const status = MeToast.getStatus(); const status = MeToast.getStatus();
expect(status.version).toBe('0.1.1'); expect(status.version).toBe('0.1.2');
expect(status.toasts).toBeGreaterThanOrEqual(0); expect(status.toasts).toBeGreaterThanOrEqual(0);
expect(status.theme).toBeDefined(); expect(status.theme).toBeDefined();
expect(status.locale).toBeDefined(); expect(status.locale).toBeDefined();
@@ -1391,4 +1430,23 @@ describe('新增功能', () => {
}); });
expect(t.config.render).toBeInstanceOf(Function); expect(t.config.render).toBeInstanceOf(Function);
}); });
test('onError 回调在钩子异常时触发', () => {
const errors = [];
MeToast.configure({ onError: (e) => errors.push(e) });
const badHandler = () => { throw new Error('test-error'); };
Toast.on('afterShow', badHandler);
const toast = MeToast.success('test');
Toast.trigger('afterShow', toast);
expect(errors.length).toBeGreaterThanOrEqual(1);
expect(errors[0].error.message).toBe('test-error');
Toast.off('afterShow', badHandler);
MeToast.configure({ onError: null });
});
test('destroy 重复调用不抛异常', () => {
MeToast.success('test');
MeToast.destroy();
expect(() => MeToast.destroy()).not.toThrow();
});
}); });
+3 -1
View File
@@ -1,6 +1,6 @@
/** /**
* MetonaToast TypeScript 类型定义 * MetonaToast TypeScript 类型定义
* @version 0.1.1 * @version 0.1.2
*/ */
// 基础类型 // 基础类型
@@ -276,6 +276,8 @@ export interface ToastConfig {
resetTimerOnUpdate?: boolean; resetTimerOnUpdate?: boolean;
/** 后台发送系统通知 */ /** 后台发送系统通知 */
notifyWhenHidden?: boolean; notifyWhenHidden?: boolean;
/** 全局错误回调 */
onError?: (errorInfo: { hook?: string; source?: string; error: Error; toast?: ToastInstance }) => void;
/** 分组名称 */ /** 分组名称 */
group?: string; group?: string;
/** 语言 */ /** 语言 */