API 文档
-MetonaToast v2.0.0 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 配置项 作为可选第二参数。
+MetonaToast v2.0.1 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 配置项 作为可选第二参数。
show(message, opts?)
diff --git a/site/index.html b/site/index.html index 3da0c1c..cc06394 100644 --- a/site/index.html +++ b/site/index.html @@ -237,7 +237,7 @@ orderGroup.dismiss(); // 一键关闭 diff --git a/src/animations.js b/src/animations.js index 9eae7fc..b1388f3 100644 --- a/src/animations.js +++ b/src/animations.js @@ -1,856 +1,111 @@ /** - * MetonaToast Animations - 动画管理 + * MetonaToast Animations - 动画管理(精简版 v2.0.1) * @module animations - * @version 2.0.0 - * @description 动画效果管理和自定义 + * @description 动画注册与管理,移除未使用的 Web Animations API dead code */ import { ANIMATIONS } from './constants.js'; // 动画缓存 -const animationCache = new Map(); +const animationMap = new Map(); /** - * 动画管理器类 + * 注册默认动画 */ -class AnimationManager { - constructor() { - this.animations = new Map(); - this.activeAnimations = new Map(); - this.animationId = 0; - - // 注册默认动画 - this._registerDefaults(); - } - - /** - * 注册默认动画 - */ - _registerDefaults() { - Object.entries(ANIMATIONS).forEach(([name, config]) => { - this.register(name, config); - }); - } - - /** - * 注册动画 - * @param {string} name - 动画名称 - * @param {Object} config - 动画配置 - */ - register(name, config) { - const animation = { - name, - enter: config.enter || {}, - leave: config.leave || {}, - duration: config.duration || 300, - easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)', - delay: config.delay || 0, - iterations: config.iterations || 1, - direction: config.direction || 'normal', - fillMode: config.fillMode || 'forwards', - }; - - this.animations.set(name, animation); - animationCache.set(name, animation); - } - - /** - * 注销动画 - * @param {string} name - 动画名称 - */ - unregister(name) { - this.animations.delete(name); - animationCache.delete(name); - } - - /** - * 获取动画配置 - * @param {string} name - 动画名称 - * @returns {Object|null} 动画配置 - */ - get(name) { - return this.animations.get(name) || ANIMATIONS[name] || null; - } - - /** - * 应用动画 - * @param {HTMLElement} element - 目标元素 - * @param {string} animationName - 动画名称 - * @param {Object} options - 额外选项 - * @returns {Promise} 动画完成Promise - */ - apply(element, animationName, options = {}) { - return new Promise((resolve, reject) => { - const animation = this.get(animationName); - if (!animation) { - resolve(); - return; - } - - const config = { ...animation, ...options }; - const animId = ++this.animationId; - - // 设置初始状态 - Object.assign(element.style, config.enter); - - // 强制重绘 - element.offsetHeight; - - // 创建动画 - const keyframes = [ - { ...config.enter }, - { ...config.leave }, - ]; - - const animationOptions = { - duration: config.duration, - easing: config.easing, - delay: config.delay, - iterations: config.iterations, - direction: config.direction, - fill: config.fillMode, - }; - - // 应用动画 - const anim = element.animate(keyframes, animationOptions); - - // 存储活动动画 - this.activeAnimations.set(animId, { - element, - animation: anim, - config, - }); - - // 动画完成处理 - anim.onfinish = () => { - Object.assign(element.style, config.leave); - this.activeAnimations.delete(animId); - resolve(); - }; - - // 动画取消处理 — reject 让调用者感知取消 - anim.oncancel = () => { - this.activeAnimations.delete(animId); - reject(new Error(`Animation "${animationName}" was cancelled`)); - }; - }); - } - - /** - * 应用进入动画 - * @param {HTMLElement} element - 目标元素 - * @param {string} animationName - 动画名称 - * @param {Object} options - 额外选项 - * @returns {Promise} 动画完成Promise - */ - enter(element, animationName, options = {}) { - const animation = this.get(animationName); - if (!animation) { - return Promise.resolve(); - } - - return this.apply(element, animationName, { - ...options, - direction: 'normal', - }); - } - - /** - * 应用离开动画 - * @param {HTMLElement} element - 目标元素 - * @param {string} animationName - 动画名称 - * @param {Object} options - 额外选项 - * @returns {Promise} 动画完成Promise - */ - leave(element, animationName, options = {}) { - const animation = this.get(animationName); - if (!animation) { - return Promise.resolve(); - } - - return this.apply(element, animationName, { - ...options, - direction: 'reverse', - }); - } - - /** - * 取消所有动画 - */ - cancelAll() { - this.activeAnimations.forEach(({ animation }) => { - animation.cancel(); - }); - - this.activeAnimations.clear(); - } - - /** - * 取消元素动画 - * @param {HTMLElement} element - 目标元素 - */ - cancel(element) { - this.activeAnimations.forEach(({ element: el, animation }, id) => { - if (el === element) { - animation.cancel(); - this.activeAnimations.delete(id); - } - }); - } - - /** - * 暂停所有动画 - */ - pauseAll() { - this.activeAnimations.forEach(({ animation }) => { - animation.pause(); - }); - } - - /** - * 恢复所有动画 - */ - resumeAll() { - this.activeAnimations.forEach(({ animation }) => { - animation.play(); - }); - } - - /** - * 暂停元素动画 - * @param {HTMLElement} element - 目标元素 - */ - pause(element) { - this.activeAnimations.forEach(({ element: el, animation }) => { - if (el === element) { - animation.pause(); - } - }); - } - - /** - * 恢复元素动画 - * @param {HTMLElement} element - 目标元素 - */ - resume(element) { - this.activeAnimations.forEach(({ element: el, animation }) => { - if (el === element) { - animation.play(); - } - }); - } - - /** - * 获取活动动画数量 - * @returns {number} 动画数量 - */ - getActiveCount() { - return this.activeAnimations.size; - } - - /** - * 获取所有活动动画 - * @returns {Map} 活动动画映射 - */ - getActiveAnimations() { - return new Map(this.activeAnimations); - } - - /** - * 检查元素是否有活动动画 - * @param {HTMLElement} element - 目标元素 - * @returns {boolean} 是否有活动动画 - */ - hasActiveAnimation(element) { - for (const { element: el } of this.activeAnimations.values()) { - if (el === element) { - return true; - } - } - return false; - } - - /** - * 获取动画配置 - * @returns {Object} 动画配置 - */ - getConfig() { - return { - animations: Array.from(this.animations.keys()), - activeCount: this.getActiveCount(), - }; - } - - /** - * 重置动画管理器 - */ - reset() { - this.cancelAll(); - this.animations.clear(); - this._registerDefaults(); - } - - /** - * 销毁动画管理器 - */ - destroy() { - this.cancelAll(); - this.animations.clear(); - this.activeAnimations.clear(); - } -} - -/** - * 预设动画效果 - */ -const presetAnimations = { - bounce: { - enter: { transform: 'translateY(-80px)', opacity: 0 }, - leave: { transform: 'translateY(20px)', opacity: 1 }, - duration: 650, - easing: 'ease', - }, - slideUp: { - enter: { transform: 'translateY(60px)', opacity: 0 }, - leave: { transform: 'translateY(0)', opacity: 1 }, - duration: 400, - easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', - }, - slideDown: { - enter: { transform: 'translateY(-60px)', opacity: 0 }, - leave: { transform: 'translateY(0)', opacity: 1 }, - duration: 400, - easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', - }, - slideLeft: { - enter: { transform: 'translateX(-90px)', opacity: 0 }, - leave: { transform: 'translateX(0)', opacity: 1 }, - duration: 400, - easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', - }, - slideRight: { - enter: { transform: 'translateX(90px)', opacity: 0 }, - leave: { transform: 'translateX(0)', opacity: 1 }, - duration: 400, - easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', - }, - -}; - -// 注册预设动画 -Object.entries(presetAnimations).forEach(([name, config]) => { - ANIMATIONS[name] = config; +Object.entries(ANIMATIONS).forEach(([name, config]) => { + animationMap.set(name, config); }); -/** - * 创建动画管理器实例 - * @returns {AnimationManager} 动画管理器实例 - */ -export const createAnimationManager = () => { - return new AnimationManager(); -}; - -// 创建默认实例 -const defaultAnimationManager = createAnimationManager(); - /** * 动画工具函数 */ export const animationUtils = { /** - * 应用动画 - * @param {HTMLElement} element - 目标元素 - * @param {string} animationName - 动画名称 - * @param {Object} options - 额外选项 - * @returns {Promise} 动画完成Promise - */ - apply(element, animationName, options = {}) { - return defaultAnimationManager.apply(element, animationName, options); - }, - - /** - * 应用进入动画 - * @param {HTMLElement} element - 目标元素 - * @param {string} animationName - 动画名称 - * @param {Object} options - 额外选项 - * @returns {Promise} 动画完成Promise - */ - enter(element, animationName, options = {}) { - return defaultAnimationManager.enter(element, animationName, options); - }, - - /** - * 应用离开动画 - * @param {HTMLElement} element - 目标元素 - * @param {string} animationName - 动画名称 - * @param {Object} options - 额外选项 - * @returns {Promise} 动画完成Promise - */ - leave(element, animationName, options = {}) { - return defaultAnimationManager.leave(element, animationName, options); - }, - - /** - * 取消所有动画 - */ - cancelAll() { - defaultAnimationManager.cancelAll(); - }, - - /** - * 取消元素动画 - * @param {HTMLElement} element - 目标元素 - */ - cancel(element) { - defaultAnimationManager.cancel(element); - }, - - /** - * 暂停所有动画 - */ - pauseAll() { - defaultAnimationManager.pauseAll(); - }, - - /** - * 恢复所有动画 - */ - resumeAll() { - defaultAnimationManager.resumeAll(); - }, - - /** - * 暂停元素动画 - * @param {HTMLElement} element - 目标元素 - */ - pause(element) { - defaultAnimationManager.pause(element); - }, - - /** - * 恢复元素动画 - * @param {HTMLElement} element - 目标元素 - */ - resume(element) { - defaultAnimationManager.resume(element); - }, - - /** - * 获取活动动画数量 - * @returns {number} 动画数量 - */ - getActiveCount() { - return defaultAnimationManager.getActiveCount(); - }, - - /** - * 检查元素是否有活动动画 - * @param {HTMLElement} element - 目标元素 - * @returns {boolean} 是否有活动动画 - */ - hasActiveAnimation(element) { - return defaultAnimationManager.hasActiveAnimation(element); - }, - - /** - * 注册动画 + * 注册自定义动画 * @param {string} name - 动画名称 - * @param {Object} config - 动画配置 + * @param {Object} config - 动画配置 { enter, leave, duration, easing } */ register(name, config) { - defaultAnimationManager.register(name, config); + animationMap.set(name, { + name, + enter: config.enter || {}, + leave: config.leave || {}, + duration: config.duration || 300, + easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)', + }); }, - + /** * 注销动画 * @param {string} name - 动画名称 */ unregister(name) { - defaultAnimationManager.unregister(name); + animationMap.delete(name); }, - + /** * 获取动画配置 * @param {string} name - 动画名称 * @returns {Object|null} 动画配置 */ get(name) { - return defaultAnimationManager.get(name); + return animationMap.get(name) || ANIMATIONS[name] || null; }, - + /** * 获取所有动画名称 * @returns {Array} 动画名称数组 */ getAnimationNames() { - return Array.from(defaultAnimationManager.animations.keys()); + return Array.from(animationMap.keys()); }, - + /** - * 获取动画配置 - * @returns {Object} 动画配置 + * 获取活动动画数量(兼容API,当前CSS动画不跟踪活动实例) + * @returns {number} 动画数量 */ - getConfig() { - return defaultAnimationManager.getConfig(); + getActiveCount() { + return 0; }, - + + /** + * 取消所有动画(兼容API,当前CSS动画由浏览器管理) + */ + cancelAll() { + // CSS动画由浏览器原生管理,无需手动取消 + }, + /** * 重置动画管理器 */ reset() { - defaultAnimationManager.reset(); + animationMap.clear(); + Object.entries(ANIMATIONS).forEach(([name, config]) => { + animationMap.set(name, config); + }); }, - + /** * 销毁动画管理器 */ destroy() { - defaultAnimationManager.destroy(); + animationMap.clear(); }, }; /** * 动画预设 */ -export const animationPresets = presetAnimations; +export const animationPresets = {}; /** - * 创建自定义动画 + * 创建自定义动画配置 * @param {Object} config - 动画配置 * @returns {Object} 动画配置 */ -export const createAnimation = (config) => { - return { - enter: config.enter || {}, - leave: config.leave || {}, - duration: config.duration || 300, - easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)', - delay: config.delay || 0, - iterations: config.iterations || 1, - direction: config.direction || 'normal', - fillMode: config.fillMode || 'forwards', - }; -}; - -/** - * 组合动画 - * @param {Array} animations - 动画数组 - * @returns {Object} 组合后的动画配置 - */ -export const combineAnimations = (animations) => { - const combined = { - enter: {}, - leave: {}, - duration: 0, - easing: 'cubic-bezier(0.4, 0, 0.2, 1)', - delay: 0, - iterations: 1, - direction: 'normal', - fillMode: 'forwards', - }; - - animations.forEach((anim) => { - if (anim.enter) { - Object.assign(combined.enter, anim.enter); - } - if (anim.leave) { - Object.assign(combined.leave, anim.leave); - } - if (anim.duration) { - combined.duration = Math.max(combined.duration, anim.duration); - } - if (anim.easing) { - combined.easing = anim.easing; - } - if (anim.delay) { - combined.delay = Math.max(combined.delay, anim.delay); - } - }); - - return combined; -}; - -/** - * 链式动画 - * @param {Array} animations - 动画数组 - * @returns {Promise} 动画完成Promise - */ -export const chainAnimations = async (element, animations) => { - for (const anim of animations) { - await defaultAnimationManager.apply(element, anim.name, anim.options); - } -}; - -/** - * 并行动画 - * @param {Array} animations - 动画数组 - * @returns {Promise} 动画完成Promise - */ -export const parallelAnimations = (element, animations) => { - return Promise.all( - animations.map((anim) => - defaultAnimationManager.apply(element, anim.name, anim.options) - ) - ); -}; - -/** - * 延迟动画 - * @param {number} delay - 延迟时间(毫秒) - * @returns {Promise} Promise对象 - */ -export const delayAnimation = (delay) => { - return new Promise((resolve) => setTimeout(resolve, delay)); -}; - -/** - * 动画队列 - */ -export class AnimationQueue { - constructor() { - this.queue = []; - this.isProcessing = false; - } - - /** - * 添加动画到队列 - * @param {Function} animationFn - 动画函数 - * @returns {Promise} 动画完成Promise - */ - add(animationFn) { - return new Promise((resolve, reject) => { - this.queue.push({ - fn: animationFn, - resolve, - reject, - }); - - this._process(); - }); - } - - /** - * 处理队列 - */ - async _process() { - if (this.isProcessing || this.queue.length === 0) { - return; - } - - this.isProcessing = true; - - while (this.queue.length > 0) { - const { fn, resolve, reject } = this.queue.shift(); - - try { - const result = await fn(); - resolve(result); - } catch (error) { - reject(error); - } - } - - this.isProcessing = false; - } - - /** - * 清空队列 - */ - clear() { - this.queue = []; - } - - /** - * 暂停队列 - */ - pause() { - this.isProcessing = true; - } - - /** - * 恢复队列 - */ - resume() { - this.isProcessing = false; - this._process(); - } - - /** - * 获取队列长度 - * @returns {number} 队列长度 - */ - get length() { - return this.queue.length; - } - - /** - * 检查是否正在处理 - * @returns {boolean} 是否正在处理 - */ - get processing() { - return this.isProcessing; - } -} - -/** - * 创建动画队列 - * @returns {AnimationQueue} 动画队列实例 - */ -export const createAnimationQueue = () => { - return new AnimationQueue(); -}; - -/** - * 动画性能监控 - */ -export class AnimationPerformanceMonitor { - constructor() { - this.metrics = { - totalAnimations: 0, - activeAnimations: 0, - averageDuration: 0, - maxDuration: 0, - minDuration: Infinity, - }; - - this.history = []; - } - - /** - * 记录动画开始 - * @param {string} animationName - 动画名称 - */ - recordStart(animationName) { - this.metrics.totalAnimations++; - this.metrics.activeAnimations++; - - this.history.push({ - name: animationName, - startTime: performance.now(), - endTime: null, - duration: null, - }); - } - - /** - * 记录动画结束 - * @param {string} animationName - 动画名称 - */ - recordEnd(animationName) { - this.metrics.activeAnimations--; - - const entry = this.history.find( - (h) => h.name === animationName && h.endTime === null - ); - - if (entry) { - entry.endTime = performance.now(); - entry.duration = entry.endTime - entry.startTime; - - // 更新统计 - this.metrics.maxDuration = Math.max(this.metrics.maxDuration, entry.duration); - this.metrics.minDuration = Math.min(this.metrics.minDuration, entry.duration); - - // 计算平均值 - const completed = this.history.filter((h) => h.duration !== null); - this.metrics.averageDuration = - completed.reduce((sum, h) => sum + h.duration, 0) / completed.length; - } - } - - /** - * 获取指标 - * @returns {Object} 性能指标 - */ - getMetrics() { - return { ...this.metrics }; - } - - /** - * 获取历史记录 - * @returns {Array} 历史记录 - */ - getHistory() { - return [...this.history]; - } - - /** - * 重置监控 - */ - reset() { - this.metrics = { - totalAnimations: 0, - activeAnimations: 0, - averageDuration: 0, - maxDuration: 0, - minDuration: Infinity, - }; - - this.history = []; - } -} - -/** - * 创建性能监控实例 - * @returns {AnimationPerformanceMonitor} 性能监控实例 - */ -export const createPerformanceMonitor = () => { - return new AnimationPerformanceMonitor(); -}; - -/** - * 动画缓存管理 - */ -export const animationCacheManager = { - /** - * 缓存动画 - * @param {string} key - 缓存键 - * @param {Object} animation - 动画配置 - */ - set(key, animation) { - animationCache.set(key, animation); - }, - - /** - * 获取缓存动画 - * @param {string} key - 缓存键 - * @returns {Object|null} 动画配置 - */ - get(key) { - return animationCache.get(key) || null; - }, - - /** - * 检查缓存 - * @param {string} key - 缓存键 - * @returns {boolean} 是否存在 - */ - has(key) { - return animationCache.has(key); - }, - - /** - * 删除缓存 - * @param {string} key - 缓存键 - */ - delete(key) { - animationCache.delete(key); - }, - - /** - * 清空缓存 - */ - clear() { - animationCache.clear(); - }, - - /** - * 获取缓存大小 - * @returns {number} 缓存大小 - */ - size() { - return animationCache.size; - }, -}; - -export { AnimationManager, defaultAnimationManager }; +export const createAnimation = (config) => ({ + enter: config.enter || {}, + leave: config.leave || {}, + duration: config.duration || 300, + easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)', +}); diff --git a/src/constants.js b/src/constants.js index 6b0ff04..0e0750c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,7 +1,7 @@ /** * MetonaToast Constants - 常量定义 * @module constants - * @version 2.0.0 + * @version 2.0.1 * @description 默认配置、颜色、动画、主题等常量 */ diff --git a/src/core.js b/src/core.js index 28b53ca..1f1bbe7 100644 --- a/src/core.js +++ b/src/core.js @@ -1,7 +1,7 @@ /** * MetonaToast Core - 核心Toast逻辑 * @module core - * @version 2.0.0 + * @version 2.0.1 * @description 重构后的核心模块,包含Toast类的优化实现 */ @@ -621,7 +621,7 @@ const _actionHTML = (actions) => { const meToast = { _toasts: new Map(), _config: { ...DEFAULTS }, - version: '2.0.0', + version: '2.0.1', configure(opts) { if (!opts || typeof opts !== 'object') return this; diff --git a/src/i18n.js b/src/i18n.js index 0f02e9a..0005b9a 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,7 +1,7 @@ /** * MetonaToast i18n - 国际化管理 * @module i18n - * @version 2.0.0 + * @version 2.0.1 * @description 多语言支持、语言切换和翻译管理 */ diff --git a/src/icons.js b/src/icons.js index eb8d8c9..fe472ff 100644 --- a/src/icons.js +++ b/src/icons.js @@ -1,7 +1,7 @@ /** * MetonaToast Icons — 图标SVG定义 * @module icons - * @version 2.0.0 + * @version 2.0.1 * @description 80+ 内置SVG图标 */ diff --git a/src/index.js b/src/index.js index cbd7a3c..66c20da 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ /** * MetonaToast - 轻量级Toast通知库 * @module metona-toast - * @version 2.0.0 + * @version 2.0.1 * @author thzxx * @description 轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。 * @license MIT @@ -15,7 +15,7 @@ import { pluginUtils, presetPlugins } from './plugins.js'; import { DEFAULTS } from './constants.js'; // 版本信息 -const VERSION = '2.0.0'; +const VERSION = '2.0.1'; /** * 主对象增强 diff --git a/src/locales.js b/src/locales.js index 12134ce..df7d4df 100644 --- a/src/locales.js +++ b/src/locales.js @@ -1,7 +1,7 @@ /** * MetonaToast Locales — 国际化翻译数据 * @module locales - * @version 2.0.0 + * @version 2.0.1 * @description 内置 zh-CN / en-US 完整翻译 */ diff --git a/src/plugins.js b/src/plugins.js index e6b23d8..c7febf5 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -1,7 +1,7 @@ /** * MetonaToast Plugins - 插件系统 * @module plugins - * @version 2.0.0 + * @version 2.0.1 * @description 插件管理器 + 3 款预设插件 (keyboard / persistence / accessibility) */ diff --git a/src/styles.js b/src/styles.js index f13b876..0d9be1b 100644 --- a/src/styles.js +++ b/src/styles.js @@ -1,20 +1,16 @@ /** - * MetonaToast Styles - 样式管理 + * MetonaToast Styles - 样式管理(精简版 v2.0.1) * @module styles - * @version 2.0.0 - * @description 样式注入、更新和主题管理 + * @description 样式注入与主题管理,移除未使用的组件样式 */ import { THEMES } from './constants.js'; // 样式缓存 -let injectedStyles = null; let styleElement = null; /** * 生成CSS样式 - * @param {Object} theme - 主题配置 - * @returns {string} CSS字符串 */ const generateCSS = (theme) => { return ` @@ -30,52 +26,15 @@ const generateCSS = (theme) => { position: fixed; z-index: 9999; } - - /* 位置样式 — 顶部位置用 column-reverse,新 toast 出现在最上方 */ - .met-container.top-left { - top: 0; - left: 0; - align-items: flex-start; - flex-direction: column-reverse; - } - - .met-container.top-center { - top: 0; - left: 0; - right: 0; - align-items: center; - flex-direction: column-reverse; - } - - .met-container.top-right { - top: 0; - right: 0; - align-items: flex-end; - flex-direction: column-reverse; - } - - .met-container.bottom-left { - bottom: 0; - left: 0; - align-items: flex-start; - flex-direction: column-reverse; - } - - .met-container.bottom-center { - bottom: 0; - left: 0; - right: 0; - align-items: center; - flex-direction: column-reverse; - } - - .met-container.bottom-right { - bottom: 0; - right: 0; - align-items: flex-end; - flex-direction: column-reverse; - } - + + /* 位置样式 */ + .met-container.top-left { top: 0; left: 0; align-items: flex-start; } + .met-container.top-center { top: 0; left: 0; right: 0; align-items: center; } + .met-container.top-right { top: 0; right: 0; align-items: flex-end; } + .met-container.bottom-left { bottom: 0; left: 0; align-items: flex-start; } + .met-container.bottom-center{ bottom: 0; left: 0; right: 0; align-items: center; } + .met-container.bottom-right { bottom: 0; right: 0; align-items: flex-end; } + /* Toast基础样式 */ .met-toast { position: relative; @@ -90,8 +49,8 @@ const generateCSS = (theme) => { font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; user-select: none; -webkit-user-select: none; - transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 0.25s ease, + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.25s ease, opacity 0.25s ease; will-change: transform, opacity; flex-shrink: 0; @@ -103,18 +62,23 @@ const generateCSS = (theme) => { background: var(--met-bg, rgba(255,255,255,0.96)); color: var(--met-text, #1f2937); box-shadow: var(--met-shadow, 0 10px 36px -10px rgba(0,0,0,0.18), 0 4px 14px -4px rgba(0,0,0,0.08)); + transform: translateZ(0); + -webkit-transform: translateZ(0); + backface-visibility: hidden; + -webkit-backface-visibility: hidden; + perspective: 1000; } - + /* 悬停效果 */ .met-toast:hover { box-shadow: var(--met-hover-shadow, 0 14px 48px -10px rgba(0,0,0,0.22), 0 6px 18px -4px rgba(0,0,0,0.10)) !important; } - + /* 可点击状态 */ .met-toast.met-clickable { cursor: pointer; } - + /* 图标样式 */ .met-icon { flex-shrink: 0; @@ -125,7 +89,7 @@ const generateCSS = (theme) => { height: 24px; margin-top: 1px; } - + /* 内容样式 */ .met-content { flex: 1; @@ -133,21 +97,21 @@ const generateCSS = (theme) => { word-wrap: break-word; overflow-wrap: break-word; } - + /* 标题样式 */ .met-title { font-weight: 600; font-size: 14px; letter-spacing: 0.1px; } - + /* 消息样式 */ .met-message { font-size: 13px; opacity: 0.85; margin-top: 2px; } - + /* 关闭按钮样式 */ .met-close { flex-shrink: 0; @@ -164,87 +128,74 @@ const generateCSS = (theme) => { color: inherit; opacity: 0.5; } - + .met-close:hover { opacity: 1; background: var(--met-close-hover-bg, rgba(0,0,0,0.06)); } - + /* 进度条样式 - 水平 */ .met-progress { position: absolute; - left: 0; - right: 0; - bottom: 0; + left: 0; right: 0; bottom: 0; height: 3px; overflow: hidden; background: var(--met-progress-bg, rgba(0,0,0,0.06)); border-radius: 0 0 12px 12px; } - + /* 进度条样式 - 垂直 */ .met-progress-v { position: absolute; - left: 0; - top: 0; - bottom: 0; + left: 0; top: 0; bottom: 0; width: 3px; overflow: hidden; background: var(--met-progress-bg, rgba(0,0,0,0.06)); border-radius: 12px 0 0 12px; } - + /* 进度条指示器 - 水平 */ .met-bar { position: absolute; - left: 0; - top: 0; - height: 100%; - width: 100%; + left: 0; top: 0; + height: 100%; width: 100%; transform-origin: left; transform: scaleX(1); } - + /* 进度条指示器 - 垂直 */ .met-bar-v { position: absolute; - left: 0; - bottom: 0; - width: 100%; - height: 100%; + left: 0; bottom: 0; + width: 100%; height: 100%; transform-origin: bottom; transform: scaleY(1); } - + /* 侧边指示器 */ .met-side { position: absolute; - left: 0; - top: 0; - bottom: 0; + left: 0; top: 0; bottom: 0; width: 4px; border-radius: 12px 0 0 12px; } - + /* 加载动画 */ .met-spin { animation: met-rot 1s linear infinite; transform-origin: 50% 50%; } - + /* 离开状态 */ .met-toast.met-leaving { pointer-events: none; z-index: 1; } - - /* 动画关键帧 */ + @keyframes met-rot { - to { - transform: rotate(360deg); - } + to { transform: rotate(360deg); } } - + /* === 进入动画:初始隐藏态 === */ .met-anim-slide.met-toast, .met-anim-fade.met-toast, @@ -259,7 +210,7 @@ const generateCSS = (theme) => { .met-anim-slideRight.met-toast { opacity: 0; } - + /* === 进入动画:@keyframes 定义 === */ @keyframes met-slide-in { 0% { transform: translateX(80px); opacity: 0; } @@ -320,7 +271,7 @@ const generateCSS = (theme) => { 65% { transform: translateX(-6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } - + /* === 进入动画:绑定到 .met-show === */ .met-anim-slide.met-toast.met-show { animation: met-slide-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-fade.met-toast.met-show { animation: met-fade-in 0.50s ease forwards; } @@ -333,80 +284,166 @@ const generateCSS = (theme) => { .met-anim-slideDown.met-toast.met-show { animation: met-slideDown-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-slideLeft.met-toast.met-show { animation: met-slideLeft-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-slideRight.met-toast.met-show { animation: met-slideRight-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } - + /* 响应式设计 */ - @media (prefers-reduced-motion: reduce) { - .met-toast { - transition: opacity 0.15s !important; - } - - .met-toast.met-show { - animation: none !important; - opacity: 1 !important; - } - - .met-spin { - animation-duration: 2.5s !important; - } - } - @media (max-width: 480px) { + .met-container { padding: 12px !important; } + .met-toast { width: 100% !important; min-width: 0 !important; } + } + + @media (max-width: 768px) { + .met-toast { font-size: 13px; } + .met-title { font-size: 13px; } + .met-message { font-size: 12px; } + } + + @media (min-width: 1200px) { + .met-toast { width: 400px; } + } + + @media (min-width: 1920px) { + .met-toast { width: 440px; } + } + + @media (max-width: 320px) { + .met-toast { width: 100%; min-width: 0; padding: 12px 14px; } + .met-container { padding: 8px; } + } + + @media (orientation: landscape) and (max-height: 500px) { + .met-container { padding: 12px 24px; } + .met-toast { min-height: 44px; padding: 10px 14px; } + } + + @media (orientation: portrait) and (max-width: 500px) { + .met-container { padding: 12px; } + .met-toast { width: 100%; min-width: 0; } + } + + /* 无障碍支持 */ + .met-toast[role="alert"] { } + .met-toast[role="status"] { } + + @media (prefers-reduced-motion: reduce) { + .met-toast { transition: opacity 0.15s !important; } + .met-toast.met-show { animation: none !important; opacity: 1 !important; } + .met-spin { animation-duration: 2.5s !important; } + } + + @media (forced-colors: active) { + .met-toast { border: 2px solid ButtonText; } + .met-close { border: 1px solid ButtonText; } + } + + @media (prefers-contrast: high) { + .met-toast { border-width: 3px; border-style: solid; } + .met-close { border: 2px solid currentColor; border-radius: 4px; } + } + + /* 焦点样式 */ + .met-toast:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; + } + .met-close:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; + } + + /* 触摸设备优化 */ + @media (hover: none) and (pointer: coarse) { + .met-toast:hover { box-shadow: inherit; } + .met-toast { min-height: 48px; } + .met-close { min-width: 48px; min-height: 48px; } + } + + @media (hover: none) and (pointer: coarse) { + .met-toast { min-height: 52px; padding: 16px 18px; } + .met-close { min-width: 52px; min-height: 52px; padding: 8px; } + .met-icon { width: 28px; height: 28px; } + } + + /* 打印样式 */ + @media print { + .met-container { display: none !important; } + .met-toast { display: none !important; } + } + + /* 全屏/画中画模式 */ + :fullscreen .met-container, + :picture-in-picture .met-container { + z-index: 2147483647; + } + + /* 安全区域适配 */ + @supports (padding: max(0px)) { .met-container { - padding: 12px !important; - } - - .met-toast { - width: 100% !important; - min-width: 0 !important; + padding: max(24px, env(safe-area-inset-top)) + max(24px, env(safe-area-inset-right)) + max(24px, env(safe-area-inset-bottom)) + max(24px, env(safe-area-inset-left)); } } - + + /* 按下状态 */ + .met-toast:active { + transform: scale(0.98); + } + + /* 类型左侧边框 */ + .met-toast.met-success { border-left: 4px solid #10b981; } + .met-toast.met-error { border-left: 4px solid #ef4444; } + .met-toast.met-warning { border-left: 4px solid #f59e0b; } + .met-toast.met-info { border-left: 4px solid #3b82f6; } + .met-toast.met-loading { border-left: 4px solid #6366f1; } + + /* 进度条过渡 */ + .met-bar, .met-bar-v { + transition: transform 0.1s linear; + } + + /* 滚动条样式 */ + .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; } + .met-container::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.3); } + /* 暗色主题特定样式 */ .met-toast.met-theme-dark { background: rgba(28, 32, 40, 0.94); color: #e6e8eb; border-color: rgba(255, 255, 255, 0.08); - box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.6), - 0 4px 14px -4px rgba(0, 0, 0, 0.4); + box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4); } - .met-toast.met-theme-dark:hover { - box-shadow: 0 14px 48px -8px rgba(0, 0, 0, 0.6), - 0 6px 18px -4px rgba(0, 0, 0, 0.4) !important; + box-shadow: 0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4) !important; } - .met-toast.met-theme-dark .met-close:hover { background: rgba(255, 255, 255, 0.08); } - .met-toast.met-theme-dark .met-progress, .met-toast.met-theme-dark .met-progress-v { background: rgba(255, 255, 255, 0.08); } - + /* 亮色主题特定样式 */ .met-toast.met-theme-light { background: rgba(255, 255, 255, 0.96); color: #1f2937; border-color: rgba(0, 0, 0, 0.06); - box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.18), - 0 4px 14px -4px rgba(0, 0, 0, 0.08); + box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08); } - .met-toast.met-theme-light:hover { - box-shadow: 0 14px 48px -10px rgba(0, 0, 0, 0.22), - 0 6px 18px -4px rgba(0, 0, 0, 0.10) !important; + box-shadow: 0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10) !important; } - .met-toast.met-theme-light .met-close:hover { background: rgba(0, 0, 0, 0.06); } - .met-toast.met-theme-light .met-progress, .met-toast.met-theme-light .met-progress-v { background: rgba(0, 0, 0, 0.06); } - + /* 自定义主题支持 */ .met-toast[data-theme] { --met-bg: var(--met-theme-bg); @@ -414,1284 +451,70 @@ const generateCSS = (theme) => { --met-border: var(--met-theme-border); --met-shadow: var(--met-theme-shadow); } - - /* 动画性能优化 */ - .met-toast { - backface-visibility: hidden; - -webkit-backface-visibility: hidden; - perspective: 1000; - } - - /* 滚动条样式 */ - .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; - } - - .met-container::-webkit-scrollbar-thumb:hover { - background: rgba(0, 0, 0, 0.3); - } - - /* 打印样式 */ - @media print { - .met-container { - display: none !important; - } - .met-toast { - display: none !important; - } - } - - /* 高对比度模式 */ - @media (forced-colors: active) { - .met-toast { - border: 2px solid ButtonText; - } - - .met-close { - border: 1px solid ButtonText; - } - } - - /* 焦点样式 */ - .met-toast:focus-visible { - outline: 2px solid #3b82f6; - outline-offset: 2px; - } - - .met-close:focus-visible { - outline: 2px solid #3b82f6; - outline-offset: 2px; - } - - /* 禁用状态 */ - .met-toast:disabled, - .met-toast[disabled] { - opacity: 0.5; - cursor: not-allowed; - } - - /* 加载状态 */ - .met-toast.met-loading { - cursor: wait; - } - - /* 成功状态 */ - .met-toast.met-success { - border-left: 4px solid #10b981; - } - - /* 错误状态 */ - .met-toast.met-error { - border-left: 4px solid #ef4444; - } - - /* 警告状态 */ - .met-toast.met-warning { - border-left: 4px solid #f59e0b; - } - - /* 信息状态 */ - .met-toast.met-info { - border-left: 4px solid #3b82f6; - } - - /* 加载状态 */ - .met-toast.met-loading { - border-left: 4px solid #6366f1; - } - - /* 进度条动画 */ - .met-bar, - .met-bar-v { - transition: transform 0.1s linear; - } - - /* 容器动画 */ - .met-container { - transition: all 0.3s ease; - } - - /* Toast进入动画 */ - .met-toast.met-enter { - animation: met-enter 0.3s ease forwards; - } - - /* Toast离开动画 */ - .met-toast.met-leave { - animation: met-leave 0.3s ease forwards; - } - - @keyframes met-enter { - from { - opacity: 0; - transform: translateY(-20px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - @keyframes met-leave { - from { - opacity: 1; - transform: translateY(0); - } - to { - opacity: 0; - transform: translateY(-20px); - } - } - - /* 响应式字体大小 */ - @media (max-width: 768px) { - .met-toast { - font-size: 13px; - } - - .met-title { - font-size: 13px; - } - - .met-message { - font-size: 12px; - } - } - - @media (max-width: 480px) { - .met-toast { - font-size: 12px; - padding: 12px 14px; - } - - .met-title { - font-size: 12px; - } - - .met-message { - font-size: 11px; - } - } - - /* 无障碍支持 */ - .met-toast[role="alert"] { - /* 警告类型Toast的特殊样式 */ - } - - .met-toast[role="status"] { - /* 状态类型Toast的特殊样式 */ - } - - /* 高对比度模式增强 */ - @media (prefers-contrast: high) { - .met-toast { - border-width: 2px; - } - - .met-close { - border: 1px solid currentColor; - } - } - + /* 暗色模式自动检测 */ @media (prefers-color-scheme: dark) { .met-theme-auto .met-toast { background: rgba(28, 32, 40, 0.94); color: #e6e8eb; border-color: rgba(255, 255, 255, 0.08); - box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.6), - 0 4px 14px -4px rgba(0, 0, 0, 0.4); + box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4); } - .met-theme-auto .met-toast:hover { - box-shadow: 0 14px 48px -8px rgba(0, 0, 0, 0.6), - 0 6px 18px -4px rgba(0, 0, 0, 0.4) !important; + box-shadow: 0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4) !important; } - - .met-theme-auto .met-close:hover { - background: rgba(255, 255, 255, 0.08); - } - + .met-theme-auto .met-close:hover { background: rgba(255, 255, 255, 0.08); } .met-theme-auto .met-progress, - .met-theme-auto .met-progress-v { - background: rgba(255, 255, 255, 0.08); - } + .met-theme-auto .met-progress-v { background: rgba(255, 255, 255, 0.08); } } - - /* 亮色模式自动检测 */ + @media (prefers-color-scheme: light) { .met-theme-auto .met-toast { background: rgba(255, 255, 255, 0.96); color: #1f2937; border-color: rgba(0, 0, 0, 0.06); - box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.18), - 0 4px 14px -4px rgba(0, 0, 0, 0.08); + box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08); } - .met-theme-auto .met-toast:hover { - box-shadow: 0 14px 48px -10px rgba(0, 0, 0, 0.22), - 0 6px 18px -4px rgba(0, 0, 0, 0.10) !important; + box-shadow: 0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10) !important; } - - .met-theme-auto .met-close:hover { - background: rgba(0, 0, 0, 0.06); - } - + .met-theme-auto .met-close:hover { background: rgba(0, 0, 0, 0.06); } .met-theme-auto .met-progress, - .met-theme-auto .met-progress-v { - background: rgba(0, 0, 0, 0.06); - } - } - - /* 动画性能优化 */ - .met-toast { - transform: translateZ(0); - -webkit-transform: translateZ(0); - } - - /* 触摸设备优化 */ - @media (hover: none) { - .met-toast:hover { - box-shadow: inherit; - } - } - - /* 滚动时固定位置 */ - .met-container { - position: fixed; - z-index: 9999; - } - - /* 全屏模式 */ - :fullscreen .met-container { - z-index: 2147483647; - } - - /* 画中画模式 */ - :picture-in-picture .met-container { - z-index: 2147483647; - } - - /* 安全区域适配 */ - @supports (padding: max(0px)) { - .met-container { - padding: max(24px, env(safe-area-inset-top)) - max(24px, env(safe-area-inset-right)) - max(24px, env(safe-area-inset-bottom)) - max(24px, env(safe-area-inset-left)); - } - } - - /* 暗色模式安全区域 */ - @media (prefers-color-scheme: dark) { - @supports (padding: max(0px)) { - .met-container { - padding: max(24px, env(safe-area-inset-top)) - max(24px, env(safe-area-inset-right)) - max(24px, env(safe-area-inset-bottom)) - max(24px, env(safe-area-inset-left)); - } - } - } - - /* 高分辨率屏幕优化 */ - @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { - .met-toast { - border-width: 0.5px; - } - } - - /* 触摸设备优化 */ - @media (hover: none) and (pointer: coarse) { - .met-toast { - min-height: 48px; - } - - .met-close { - min-width: 48px; - min-height: 48px; - } - } - - /* 键盘导航优化 */ - .met-toast:focus { - outline: 2px solid #3b82f6; - outline-offset: 2px; - } - - .met-toast:focus:not(:focus-visible) { - outline: none; - } - - .met-toast:focus-visible { - outline: 2px solid #3b82f6; - outline-offset: 2px; - } - - /* 减少动画模式 */ - @media (prefers-reduced-motion: reduce) { - .met-toast { - transition: opacity 0.15s ease !important; - animation: none !important; - } - - .met-bar, - .met-bar-v { - transition: none !important; - } - - .met-container { - transition: none !important; - } - } - - /* 高对比度模式 */ - @media (prefers-contrast: high) { - .met-toast { - border-width: 3px; - border-style: solid; - } - - .met-close { - border: 2px solid currentColor; - border-radius: 4px; - } - - .met-progress, - .met-progress-v { - height: 4px; - } - - .met-bar, - .met-bar-v { - height: 100%; - } - } - - /* 低分辨率屏幕优化 */ - @media (max-resolution: 1dppx) { - .met-toast { - border-width: 1px; - } - } - - /* 触摸设备优化 */ - @media (hover: none) and (pointer: coarse) { - .met-toast { - min-height: 52px; - padding: 16px 18px; - } - - .met-close { - min-width: 52px; - min-height: 52px; - padding: 8px; - } - - .met-icon { - width: 28px; - height: 28px; - } - } - - /* 大屏幕优化 */ - @media (min-width: 1200px) { - .met-toast { - width: 400px; - } - } - - /* 超大屏幕优化 */ - @media (min-width: 1920px) { - .met-toast { - width: 440px; - } - } - - /* 小屏幕优化 */ - @media (max-width: 320px) { - .met-toast { - width: 100%; - min-width: 0; - padding: 12px 14px; - } - - .met-container { - padding: 8px; - } - } - - /* 横屏优化 */ - @media (orientation: landscape) and (max-height: 500px) { - .met-container { - padding: 12px 24px; - } - - .met-toast { - min-height: 44px; - padding: 10px 14px; - } - } - - /* 竖屏优化 */ - @media (orientation: portrait) and (max-width: 500px) { - .met-container { - padding: 12px; - } - - .met-toast { - width: 100%; - min-width: 0; - } - } - - /* 无障碍支持 */ - .met-toast[aria-live="assertive"] { - /* 紧急通知样式 */ - } - - .met-toast[aria-live="polite"] { - /* 普通通知样式 */ - } - - /* 焦点陷阱 */ - .met-toast:focus-within { - /* 包含焦点元素的Toast样式 */ - } - - /* 加载状态 */ - .met-toast[aria-busy="true"] { - cursor: wait; - } - - /* 禁用状态 */ - .met-toast[aria-disabled="true"] { - opacity: 0.5; - cursor: not-allowed; - pointer-events: none; - } - - /* 隐藏状态 */ - .met-toast[aria-hidden="true"] { - display: none; - } - - /* 展开状态 */ - .met-toast[aria-expanded="true"] { - /* 展开状态样式 */ - } - - /* 选中状态 */ - .met-toast[aria-selected="true"] { - /* 选中状态样式 */ - } - - /* 按下状态 */ - .met-toast:active { - transform: scale(0.98); - } - - /* 加载状态动画 */ - .met-toast.met-loading::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.1), - transparent - ); - animation: met-loading 1.5s infinite; - } - - @keyframes met-loading { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(100%); - } - } - - /* 成功状态动画 */ - .met-toast.met-success::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 4px; - height: 100%; - background: #10b981; - border-radius: 12px 0 0 12px; - animation: met-success 0.3s ease; - } - - @keyframes met-success { - from { - transform: scaleY(0); - } - to { - transform: scaleY(1); - } - } - - /* 错误状态动画 */ - .met-toast.met-error::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 4px; - height: 100%; - background: #ef4444; - border-radius: 12px 0 0 12px; - animation: met-error 0.3s ease; - } - - @keyframes met-error { - from { - transform: scaleY(0); - } - to { - transform: scaleY(1); - } - } - - /* 警告状态动画 */ - .met-toast.met-warning::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 4px; - height: 100%; - background: #f59e0b; - border-radius: 12px 0 0 12px; - animation: met-warning 0.3s ease; - } - - @keyframes met-warning { - from { - transform: scaleY(0); - } - to { - transform: scaleY(1); - } - } - - /* 信息状态动画 */ - .met-toast.met-info::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 4px; - height: 100%; - background: #3b82f6; - border-radius: 12px 0 0 12px; - animation: met-info 0.3s ease; - } - - @keyframes met-info { - from { - transform: scaleY(0); - } - to { - transform: scaleY(1); - } - } - - /* 加载状态动画 */ - .met-toast.met-loading::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 4px; - height: 100%; - background: #6366f1; - border-radius: 12px 0 0 12px; - animation: met-loading-bar 1s infinite; - } - - @keyframes met-loading-bar { - 0% { - transform: scaleY(0); - transform-origin: top; - } - 50% { - transform: scaleY(1); - transform-origin: top; - } - 51% { - transform-origin: bottom; - } - 100% { - transform: scaleY(0); - transform-origin: bottom; - } - } - - /* 进度条脉冲动画 */ - .met-bar, - .met-bar-v { - animation: met-pulse 2s infinite; - } - - @keyframes met-pulse { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.8; - } - } - - /* Toast堆叠效果 */ - .met-toast + .met-toast { - margin-top: 8px; - } - - /* Toast分组 */ - .met-toast-group { - display: flex; - flex-direction: column; - gap: 8px; - } - - /* Toast头部 */ - .met-toast-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; - } - - /* Toast底部 */ - .met-toast-footer { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 8px; - margin-top: 12px; - } - - /* Toast操作按钮 */ - .met-toast-action { - background: transparent; - border: 1px solid currentColor; - padding: 6px 12px; - border-radius: 6px; - cursor: pointer; - font-size: 12px; - opacity: 0.7; - transition: opacity 0.2s; - } - - .met-toast-action:hover { - opacity: 1; - } - - /* Toast进度文本 */ - .met-toast-progress-text { - font-size: 12px; - opacity: 0.7; - margin-left: 8px; - } - - /* Toast时间戳 */ - .met-toast-timestamp { - font-size: 11px; - opacity: 0.5; - margin-left: auto; - } - - /* Toast头像 */ - .met-toast-avatar { - width: 32px; - height: 32px; - border-radius: 50%; - object-fit: cover; - flex-shrink: 0; - } - - /* Toast图片 */ - .met-toast-image { - max-width: 100%; - max-height: 200px; - border-radius: 8px; - margin-top: 8px; - object-fit: cover; - } - - /* Toast视频 */ - .met-toast-video { - max-width: 100%; - max-height: 200px; - border-radius: 8px; - margin-top: 8px; - } - - /* Toast音频 */ - .met-toast-audio { - width: 100%; - margin-top: 8px; - } - - /* Toast代码块 */ - .met-toast-code { - background: rgba(0, 0, 0, 0.05); - padding: 8px 12px; - border-radius: 6px; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; - font-size: 12px; - margin-top: 8px; - overflow-x: auto; - } - - .met-theme-dark .met-toast-code { - background: rgba(255, 255, 255, 0.05); - } - - /* Toast引用 */ - .met-toast-quote { - border-left: 3px solid currentColor; - padding-left: 12px; - margin-top: 8px; - opacity: 0.8; - } - - /* Toast列表 */ - .met-toast-list { - margin: 8px 0 0 0; - padding-left: 20px; - } - - .met-toast-list li { - margin-bottom: 4px; - } - - /* Toast表格 */ - .met-toast-table { - width: 100%; - border-collapse: collapse; - margin-top: 8px; - font-size: 12px; - } - - .met-toast-table th, - .met-toast-table td { - border: 1px solid rgba(0, 0, 0, 0.1); - padding: 6px 8px; - text-align: left; - } - - .met-theme-dark .met-toast-table th, - .met-theme-dark .met-toast-table td { - border-color: rgba(255, 255, 255, 0.1); - } - - .met-toast-table th { - background: rgba(0, 0, 0, 0.05); - font-weight: 600; - } - - .met-theme-dark .met-toast-table th { - background: rgba(255, 255, 255, 0.05); - } - - /* Toast分割线 */ - .met-toast-divider { - height: 1px; - background: rgba(0, 0, 0, 0.1); - margin: 12px 0; - } - - .met-theme-dark .met-toast-divider { - background: rgba(255, 255, 255, 0.1); - } - - /* Toast标签 */ - .met-toast-tag { - display: inline-block; - background: rgba(0, 0, 0, 0.05); - padding: 2px 8px; - border-radius: 4px; - font-size: 11px; - margin-right: 4px; - margin-bottom: 4px; - } - - .met-theme-dark .met-toast-tag { - background: rgba(255, 255, 255, 0.05); - } - - /* Toast徽章 */ - .met-toast-badge { - display: inline-flex; - align-items: center; - justify-content: center; - background: #3b82f6; - color: white; - padding: 2px 8px; - border-radius: 10px; - font-size: 11px; - font-weight: 600; - min-width: 20px; - height: 20px; - } - - /* Toast进度环 */ - .met-toast-progress-ring { - width: 40px; - height: 40px; - transform: rotate(-90deg); - } - - .met-toast-progress-ring circle { - fill: none; - stroke-width: 3; - stroke-linecap: round; - } - - .met-toast-progress-ring .met-progress-ring-bg { - stroke: rgba(0, 0, 0, 0.1); - } - - .met-theme-dark .met-toast-progress-ring .met-progress-ring-bg { - stroke: rgba(255, 255, 255, 0.1); - } - - .met-toast-progress-ring .met-progress-ring-fill { - stroke: #3b82f6; - stroke-dasharray: 100; - stroke-dashoffset: 100; - transition: stroke-dashoffset 0.3s ease; - } - - /* Toast滑块 */ - .met-toast-slider { - width: 100%; - height: 4px; - background: rgba(0, 0, 0, 0.1); - border-radius: 2px; - margin-top: 8px; - position: relative; - } - - .met-theme-dark .met-toast-slider { - background: rgba(255, 255, 255, 0.1); - } - - .met-toast-slider-fill { - position: absolute; - left: 0; - top: 0; - height: 100%; - background: #3b82f6; - border-radius: 2px; - transition: width 0.3s ease; - } - - /* Toast开关 */ - .met-toast-switch { - position: relative; - display: inline-block; - width: 36px; - height: 20px; - margin-top: 8px; - } - - .met-toast-switch input { - opacity: 0; - width: 0; - height: 0; - } - - .met-toast-switch-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.1); - border-radius: 20px; - transition: 0.3s; - } - - .met-theme-dark .met-toast-switch-slider { - background: rgba(255, 255, 255, 0.1); - } - - .met-toast-switch-slider:before { - content: ''; - position: absolute; - height: 16px; - width: 16px; - left: 2px; - bottom: 2px; - background: white; - border-radius: 50%; - transition: 0.3s; - } - - .met-toast-switch input:checked + .met-toast-switch-slider { - background: #3b82f6; - } - - .met-toast-switch input:checked + .met-toast-switch-slider:before { - transform: translateX(16px); - } - - /* Toast复选框 */ - .met-toast-checkbox { - display: flex; - align-items: center; - gap: 8px; - margin-top: 8px; - cursor: pointer; - } - - .met-toast-checkbox input { - width: 16px; - height: 16px; - cursor: pointer; - } - - /* Toast单选框 */ - .met-toast-radio { - display: flex; - align-items: center; - gap: 8px; - margin-top: 8px; - cursor: pointer; - } - - .met-toast-radio input { - width: 16px; - height: 16px; - cursor: pointer; - } - - /* Toast输入框 */ - .met-toast-input { - width: 100%; - padding: 8px 12px; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 6px; - font-size: 13px; - margin-top: 8px; - background: transparent; - color: inherit; - } - - .met-theme-dark .met-toast-input { - border-color: rgba(255, 255, 255, 0.1); - } - - .met-toast-input:focus { - outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); - } - - /* Toast文本域 */ - .met-toast-textarea { - width: 100%; - padding: 8px 12px; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 6px; - font-size: 13px; - margin-top: 8px; - background: transparent; - color: inherit; - resize: vertical; - min-height: 80px; - } - - .met-theme-dark .met-toast-textarea { - border-color: rgba(255, 255, 255, 0.1); - } - - .met-toast-textarea:focus { - outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); - } - - /* Toast选择框 */ - .met-toast-select { - width: 100%; - padding: 8px 12px; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 6px; - font-size: 13px; - margin-top: 8px; - background: transparent; - color: inherit; - cursor: pointer; - } - - .met-theme-dark .met-toast-select { - border-color: rgba(255, 255, 255, 0.1); - } - - .met-toast-select:focus { - outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); - } - - /* Toast下拉菜单 */ - .met-toast-dropdown { - position: relative; - display: inline-block; - } - - .met-toast-dropdown-content { - display: none; - position: absolute; - background: ${theme.bg}; - min-width: 160px; - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); - border-radius: 8px; - z-index: 1; - padding: 8px 0; - margin-top: 4px; - } - - .met-theme-dark .met-toast-dropdown-content { - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); - } - - .met-toast-dropdown:hover .met-toast-dropdown-content { - display: block; - } - - .met-toast-dropdown-item { - padding: 8px 16px; - cursor: pointer; - font-size: 13px; - } - - .met-toast-dropdown-item:hover { - background: rgba(0, 0, 0, 0.05); - } - - .met-theme-dark .met-toast-dropdown-item:hover { - background: rgba(255, 255, 255, 0.05); - } - - /* Toast工具提示 */ - .met-toast-tooltip { - position: relative; - display: inline-block; - } - - .met-toast-tooltip .met-toast-tooltip-text { - visibility: hidden; - width: 120px; - background: ${theme.bg}; - color: ${theme.text}; - text-align: center; - border-radius: 6px; - padding: 6px 8px; - position: absolute; - z-index: 1; - bottom: 125%; - left: 50%; - margin-left: -60px; - opacity: 0; - transition: opacity 0.3s; - font-size: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - } - - .met-theme-dark .met-toast-tooltip .met-toast-tooltip-text { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - } - - .met-toast-tooltip:hover .met-toast-tooltip-text { - visibility: visible; - opacity: 1; - } - - /* Toast弹出框 */ - .met-toast-popover { - position: relative; - display: inline-block; - } - - .met-toast-popover .met-toast-popover-content { - visibility: hidden; - background: ${theme.bg}; - color: ${theme.text}; - border-radius: 8px; - padding: 12px 16px; - position: absolute; - z-index: 1; - bottom: 125%; - left: 50%; - transform: translateX(-50%); - opacity: 0; - transition: opacity 0.3s; - min-width: 200px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); - } - - .met-theme-dark .met-toast-popover .met-toast-popover-content { - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); - } - - .met-toast-popover:hover .met-toast-popover-content { - visibility: visible; - opacity: 1; - } - - /* Toast模态框 */ - .met-toast-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; - } - - .met-toast-modal-content { - background: ${theme.bg}; - color: ${theme.text}; - border-radius: 12px; - padding: 24px; - max-width: 500px; - width: 90%; - max-height: 80vh; - overflow-y: auto; - } - - /* Toast抽屉 */ - .met-toast-drawer { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - z-index: 10000; - } - - .met-toast-drawer-content { - position: absolute; - top: 0; - right: 0; - width: 300px; - height: 100%; - background: ${theme.bg}; - color: ${theme.text}; - padding: 24px; - transform: translateX(100%); - transition: transform 0.3s ease; - } - - .met-toast-drawer.open .met-toast-drawer-content { - transform: translateX(0); - } - - /* Toast通知栏 */ - .met-toast-notification-bar { - position: fixed; - top: 0; - left: 0; - right: 0; - background: ${theme.bg}; - color: ${theme.text}; - padding: 12px 24px; - display: flex; - align-items: center; - justify-content: space-between; - z-index: 9999; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - } - - /* Toast状态栏 */ - .met-toast-status-bar { - position: fixed; - bottom: 0; - left: 0; - right: 0; - background: ${theme.bg}; - color: ${theme.text}; - padding: 8px 24px; - display: flex; - align-items: center; - justify-content: space-between; - z-index: 9999; - font-size: 12px; - box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); + .met-theme-auto .met-progress-v { background: rgba(0, 0, 0, 0.06); } } `; }; /** * 注入样式 - * @param {Object} theme - 主题配置 */ export const injectStyles = (theme = THEMES.light) => { - // 检查是否在浏览器环境 if (typeof document === 'undefined') return; - - // 检查是否已注入 + if (styleElement && document.getElementById('metona-toast-styles')) { return; } - - // 生成CSS + const css = generateCSS(theme); - - // 创建样式元素 + styleElement = document.createElement('style'); styleElement.id = 'metona-toast-styles'; styleElement.textContent = css.replace(/\s+/g, ' '); - - // 添加到文档头 + document.head.appendChild(styleElement); - - // 缓存样式 - injectedStyles = css; }; /** * 更新样式 - * @param {Object} theme - 新主题配置 */ export const updateStyles = (theme) => { if (!styleElement) { injectStyles(theme); return; } - + const css = generateCSS(theme); styleElement.textContent = css.replace(/\s+/g, ' '); - injectedStyles = css; }; /** @@ -1701,64 +524,12 @@ export const removeStyles = () => { if (styleElement && styleElement.parentNode) { styleElement.parentNode.removeChild(styleElement); } - + styleElement = null; - injectedStyles = null; }; /** - * 获取当前样式 - * @returns {string|null} 当前CSS字符串 - */ -export const getStyles = () => { - return injectedStyles; -}; - -/** - * 检查样式是否已注入 - * @returns {boolean} 是否已注入 - */ -export const isStylesInjected = () => { - return !!styleElement && !!document.getElementById('metona-toast-styles'); -}; - -/** - * 重置样式 - */ -export const resetStyles = () => { - removeStyles(); - injectStyles(); -}; - -/** - * 添加自定义CSS - * @param {string} css - CSS字符串 - */ -export const addCustomCSS = (css) => { - if (!styleElement) { - injectStyles(); - } - - const customStyle = document.createElement('style'); - customStyle.id = 'metona-toast-custom-styles'; - customStyle.textContent = css; - document.head.appendChild(customStyle); -}; - -/** - * 移除自定义CSS - */ -export const removeCustomCSS = () => { - const customStyle = document.getElementById('metona-toast-custom-styles'); - if (customStyle && customStyle.parentNode) { - customStyle.parentNode.removeChild(customStyle); - } -}; - -/** - * 生成主题变量 - * @param {Object} theme - 主题配置 - * @returns {string} CSS变量字符串 + * 生成主题CSS变量 */ export const generateThemeVariables = (theme) => { return ` @@ -1776,51 +547,55 @@ export const generateThemeVariables = (theme) => { /** * 应用主题变量 - * @param {Object} theme - 主题配置 */ export const applyThemeVariables = (theme) => { + if (typeof document === 'undefined') return; + const css = generateThemeVariables(theme); - addCustomCSS(css); + const customStyle = document.createElement('style'); + customStyle.id = 'metona-toast-custom-styles'; + customStyle.textContent = css; + document.head.appendChild(customStyle); }; /** * 清除主题变量 */ export const clearThemeVariables = () => { - removeCustomCSS(); + const customStyle = document.getElementById('metona-toast-custom-styles'); + if (customStyle && customStyle.parentNode) { + customStyle.parentNode.removeChild(customStyle); + } }; /** * 获取系统主题 - * @returns {string} 系统主题 */ export const getSystemTheme = () => { if (typeof window === 'undefined') return 'light'; - - return window.matchMedia && - window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' + + return window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' : 'light'; }; /** * 监听系统主题变化 - * @param {Function} callback - 回调函数 - * @returns {Function} 取消监听函数 */ export const watchSystemTheme = (callback) => { if (typeof window === 'undefined' || !window.matchMedia) { return () => {}; } - + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - + const handler = (e) => { callback(e.matches ? 'dark' : 'light'); }; - + mediaQuery.addEventListener('change', handler); - + return () => { mediaQuery.removeEventListener('change', handler); }; @@ -1828,17 +603,14 @@ export const watchSystemTheme = (callback) => { /** * 自动应用系统主题 - * @returns {Function} 取消监听函数 */ export const autoApplySystemTheme = () => { const applyTheme = (theme) => { updateStyles(THEMES[theme]); applyThemeVariables(THEMES[theme]); }; - - // 应用当前系统主题 + applyTheme(getSystemTheme()); - - // 监听系统主题变化 + return watchSystemTheme(applyTheme); }; diff --git a/src/themes.js b/src/themes.js index 3c924fe..7932bbe 100644 --- a/src/themes.js +++ b/src/themes.js @@ -1,7 +1,7 @@ /** * MetonaToast Themes - 主题管理 * @module themes - * @version 2.0.0 + * @version 2.0.1 * @description 主题系统、自定义主题和主题切换 */ diff --git a/src/utils.js b/src/utils.js index fa4e9dd..869b137 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,8 +1,8 @@ /** - * MetonaToast Utils - 工具函数 + * MetonaToast Utils - 精简工具函数 * @module utils - * @version 2.0.0 - * @description 通用工具函数集合 + * @version 2.0.1 + * @description 仅保留核心模块实际使用的工具函数 */ /** @@ -20,7 +20,6 @@ export const generateId = () => { */ export const escapeHTML = (s) => { if (typeof document === 'undefined') { - // Node.js环境 return String(s == null ? '' : s) .replace(/&/g, '&') .replace(/ { .replace(/"/g, '"') .replace(/'/g, '''); } - + const div = document.createElement('div'); div.textContent = String(s == null ? '' : s); return div.innerHTML; }; -/** - * 事件绑定 - * @param {HTMLElement} el - 元素 - * @param {string} evt - 事件名 - * @param {Function} handler - 事件处理函数 - * @param {Object} opts - 选项 - * @returns {Function} 清理函数 - */ -export const on = (el, evt, handler, opts) => { - el.addEventListener(evt, handler, opts); - return () => el.removeEventListener(evt, handler, opts); -}; - /** * 检测暗色模式偏好 * @returns {boolean} 是否偏好暗色模式 */ export const prefersDark = () => { - return typeof window !== 'undefined' && - window.matchMedia && + return typeof window !== 'undefined' && + window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; }; - -/** - * 防抖函数 - * @param {Function} func - 要防抖的函数 - * @param {number} wait - 等待时间(毫秒) - * @param {boolean} immediate - 是否立即执行 - * @returns {Function} 防抖后的函数 - */ -export const debounce = (func, wait, immediate = false) => { - let timeout; - return function executedFunction(...args) { - const context = this; - const later = () => { - timeout = null; - if (!immediate) func.apply(context, args); - }; - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) func.apply(context, args); - }; -}; - -/** - * 节流函数 - * @param {Function} func - 要节流的函数 - * @param {number} limit - 限制时间(毫秒) - * @returns {Function} 节流后的函数 - */ -export const throttle = (func, limit) => { - let inThrottle; - return function executedFunction(...args) { - const context = this; - if (!inThrottle) { - func.apply(context, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - }; -}; - -/** - * 深度合并对象 - * @param {Object} target - 目标对象 - * @param {Object} source - 源对象 - * @returns {Object} 合并后的对象 - */ -export const deepMerge = (target, source) => { - const output = { ...target }; - - Object.keys(source).forEach(key => { - if (source[key] instanceof Object && key in target && target[key] instanceof Object) { - output[key] = deepMerge(target[key], source[key]); - } else { - output[key] = source[key]; - } - }); - - return output; -}; - -/** - * 检查是否为浏览器环境 - * @returns {boolean} 是否为浏览器环境 - */ -export const isBrowser = () => { - return typeof window !== 'undefined' && typeof document !== 'undefined'; -}; - -/** - * 安全获取嵌套对象属性 - * @param {Object} obj - 对象 - * @param {string} path - 属性路径 - * @param {*} defaultValue - 默认值 - * @returns {*} 属性值 - */ -export const getNestedValue = (obj, path, defaultValue = undefined) => { - const keys = path.split('.'); - let result = obj; - - for (const key of keys) { - result = result?.[key]; - if (result === undefined) return defaultValue; - } - - return result; -}; - -/** - * 格式化文件大小 - * @param {number} bytes - 字节数 - * @param {number} decimals - 小数位数 - * @returns {string} 格式化后的字符串 - */ -export const formatFileSize = (bytes, decimals = 2) => { - if (bytes === 0) return '0 Bytes'; - - const k = 1024; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - - const i = Math.floor(Math.log(bytes) / Math.log(k)); - - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; -}; - -/** - * 格式化日期 - * @param {Date|string|number} date - 日期 - * @param {string} format - 格式 - * @returns {string} 格式化后的日期字符串 - */ -export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => { - const d = new Date(date); - - const year = d.getFullYear(); - const month = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - const hours = String(d.getHours()).padStart(2, '0'); - const minutes = String(d.getMinutes()).padStart(2, '0'); - const seconds = String(d.getSeconds()).padStart(2, '0'); - - return format - .replace('YYYY', year) - .replace('MM', month) - .replace('DD', day) - .replace('HH', hours) - .replace('mm', minutes) - .replace('ss', seconds); -}; - -/** - * 生成随机数 - * @param {number} min - 最小值 - * @param {number} max - 最大值 - * @returns {number} 随机数 - */ -export const random = (min, max) => { - return Math.floor(Math.random() * (max - min + 1)) + min; -}; - -/** - * 检查元素是否在视口中 - * @param {HTMLElement} el - 元素 - * @returns {boolean} 是否在视口中 - */ -export const isInViewport = (el) => { - const rect = el.getBoundingClientRect(); - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && - rect.right <= (window.innerWidth || document.documentElement.clientWidth) - ); -}; - -/** - * 平滑滚动到元素 - * @param {HTMLElement} el - 元素 - * @param {Object} options - 选项 - */ -export const scrollToElement = (el, options = {}) => { - const defaultOptions = { - behavior: 'smooth', - block: 'start', - inline: 'nearest', - }; - - el.scrollIntoView({ ...defaultOptions, ...options }); -}; - -/** - * 复制文本到剪贴板 - * @param {string} text - 文本 - * @returns {Promiseparagraph
')).toBe('paragraph'); - }); - - test('应该能够转义正则表达式', () => { - const { escapeRegExp } = require('../src/utils.js'); - - expect(escapeRegExp('[test]')).toBe('\\[test\\]'); - expect(escapeRegExp('hello.world')).toBe('hello\\.world'); - }); - - test('应该能够生成随机字符串', () => { - const { randomString } = require('../src/utils.js'); - - const str = randomString(10); - expect(str).toHaveLength(10); - }); - - test('应该能够延迟执行', async () => { - const { sleep } = require('../src/utils.js'); - - const start = Date.now(); - await sleep(100); - const end = Date.now(); - - expect(end - start).toBeGreaterThanOrEqual(90); - }); - - test('应该能够重试函数', async () => { - const { retry } = require('../src/utils.js'); - - let attempts = 0; - const fn = jest.fn(() => { - attempts++; - if (attempts < 3) { - throw new Error('fail'); - } - return 'success'; - }); - - const result = await retry(fn, 3, 10); - expect(result).toBe('success'); - expect(fn).toHaveBeenCalledTimes(3); - }); - - test('应该能够超时控制', async () => { - const { timeout } = require('../src/utils.js'); - - const promise = new Promise((resolve) => setTimeout(resolve, 200)); - - await expect(timeout(promise, 100)).rejects.toThrow('Timeout'); - }); - - test('应该能够并发控制', async () => { - const { parallel } = require('../src/utils.js'); - - const tasks = [ - () => Promise.resolve(1), - () => Promise.resolve(2), - () => Promise.resolve(3), - ]; - - const results = await parallel(tasks, 2); - expect(results).toEqual([1, 2, 3]); - }); - - test('应该能够序列执行', async () => { - const { sequence } = require('../src/utils.js'); - - const tasks = [ - () => Promise.resolve(1), - () => Promise.resolve(2), - () => Promise.resolve(3), - ]; - - const results = await sequence(tasks); - expect(results).toEqual([1, 2, 3]); - }); - - test('应该能够缓存函数', () => { - const { memoize } = require('../src/utils.js'); - - let count = 0; - const fn = jest.fn((x) => { - count++; - return x * 2; - }); - - const memoized = memoize(fn); - - expect(memoized(2)).toBe(4); - expect(memoized(2)).toBe(4); - expect(fn).toHaveBeenCalledTimes(1); - }); - - test('应该能够单例模式', () => { - const { singleton } = require('../src/utils.js'); - - let count = 0; - const fn = jest.fn(() => { - count++; - return { id: count }; - }); - - const singletonFn = singleton(fn); - - const obj1 = singletonFn(); - const obj2 = singletonFn(); - - expect(obj1).toBe(obj2); - expect(fn).toHaveBeenCalledTimes(1); - }); - - test('应该能够创建观察者', () => { - const { createObserver } = require('../src/utils.js'); - - const observer = createObserver(); - const callback = jest.fn(); - - observer.on('test', callback); - observer.emit('test', 'data'); - - expect(callback).toHaveBeenCalledWith('data'); - }); - - test('应该能够创建状态机', () => { - const { createStateMachine } = require('../src/utils.js'); - - const machine = createStateMachine({ - initial: 'idle', - transitions: { - idle: { start: 'running' }, - running: { stop: 'idle' }, - }, - }); - - expect(machine.state).toBe('idle'); - - machine.transition('start'); - expect(machine.state).toBe('running'); - - machine.transition('stop'); - expect(machine.state).toBe('idle'); - }); }); describe('常量', () => { @@ -1395,79 +859,6 @@ describe('常量', () => { }); }); -describe('动画管理器', () => { - test('应该能够创建动画管理器', () => { - const { createAnimationManager } = require('../src/animations.js'); - - const manager = createAnimationManager(); - expect(manager).toBeDefined(); - expect(manager.register).toBeInstanceOf(Function); - expect(manager.apply).toBeInstanceOf(Function); - }); - - test('应该能够注册动画', () => { - const { createAnimationManager } = require('../src/animations.js'); - - const manager = createAnimationManager(); - - manager.register('test', { - enter: { opacity: 0 }, - leave: { opacity: 1 }, - duration: 300, - }); - - expect(manager.get('test')).toBeDefined(); - }); - - test('应该能够应用动画', async () => { - const { createAnimationManager } = require('../src/animations.js'); - - const manager = createAnimationManager(); - const element = document.createElement('div'); - // 确保 animate 方法存在并在设置 onfinish 后自动调用 - element.animate = jest.fn(() => { - const anim = { - onfinish: null, - oncancel: null, - cancel: jest.fn(), - pause: jest.fn(), - play: jest.fn(), - }; - // 下一个微任务自动触发 onfinish,让 Promise resolve - Promise.resolve().then(() => { - if (anim.onfinish) anim.onfinish(); - }); - return anim; - }); - - manager.register('test', { - enter: { opacity: 0 }, - leave: { opacity: 1 }, - duration: 100, - }); - - await manager.apply(element, 'test'); - - expect(element.animate).toHaveBeenCalled(); - }); - - test('应该能够取消动画', () => { - const { createAnimationManager } = require('../src/animations.js'); - - const manager = createAnimationManager(); - - manager.cancelAll(); - expect(manager.getActiveCount()).toBe(0); - }); - - test('应该能够获取活动动画数量', () => { - const { createAnimationManager } = require('../src/animations.js'); - - const manager = createAnimationManager(); - expect(manager.getActiveCount()).toBe(0); - }); -}); - describe('主题管理器', () => { test('应该能够获取系统主题', () => { const { getSystemTheme } = require('../src/themes.js'); diff --git a/types/index.d.ts b/types/index.d.ts index 3cd6a62..4cffbdf 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,6 +1,6 @@ /** * MetonaToast TypeScript 类型定义 - * @version 2.0.0 + * @version 2.0.1 */ // 基础类型