diff --git a/package.json b/package.json index 4b57c9e..b947e8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metona-team/metona-toast", - "version": "2.0.0", + "version": "2.0.1", "description": "轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。", "main": "dist/metona-toast.js", "module": "src/index.js", diff --git a/site/demo.html b/site/demo.html index 8885f8a..677e342 100644 --- a/site/demo.html +++ b/site/demo.html @@ -273,7 +273,7 @@ diff --git a/site/docs.html b/site/docs.html index cd0757e..55efa52 100644 --- a/site/docs.html +++ b/site/docs.html @@ -87,7 +87,7 @@

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(); // 一键关闭
-

MetonaToast v2.0.0 · MIT License · Gitea

+

MetonaToast v2.0.1 · MIT License · Gitea

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 {Promise} 是否成功 - */ -export const copyToClipboard = async (text) => { - try { - if (navigator.clipboard) { - await navigator.clipboard.writeText(text); - return true; - } - - // 降级方案 - const textarea = document.createElement('textarea'); - textarea.value = text; - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); - - return true; - } catch (err) { - console.error('Failed to copy text: ', err); - return false; - } -}; - -/** - * 检测设备类型 - * @returns {string} 设备类型 - */ -export const getDeviceType = () => { - const ua = navigator.userAgent; - - if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) { - return 'tablet'; - } - - if (/Mobile|Android|iP(hone|od)|IEMobile|Kindle|NetFront|Silk-Accelerated|(hpw|web)OS|Fennec|Minimo|Opera M(obi|ini)|Blazer|Dolfin|Dolphin|Skyfire|Zune/.test(ua)) { - return 'mobile'; - } - - return 'desktop'; -}; - -/** - * 检测浏览器 - * @returns {Object} 浏览器信息 - */ -export const getBrowserInfo = () => { - const ua = navigator.userAgent; - let browser = 'unknown'; - let version = 'unknown'; - - if (ua.includes('Firefox/')) { - browser = 'Firefox'; - version = ua.split('Firefox/')[1]; - } else if (ua.includes('Edg/')) { - browser = 'Edge'; - version = ua.split('Edg/')[1]; - } else if (ua.includes('Chrome/')) { - browser = 'Chrome'; - version = ua.split('Chrome/')[1]; - } else if (ua.includes('Safari/')) { - browser = 'Safari'; - version = ua.split('Version/')[1]; - } - - return { browser, version }; -}; - -/** - * 检测操作系统 - * @returns {string} 操作系统 - */ -export const getOS = () => { - const ua = navigator.userAgent; - - if (ua.includes('Win')) return 'Windows'; - if (ua.includes('Mac')) return 'MacOS'; - if (ua.includes('Linux')) return 'Linux'; - if (ua.includes('Android')) return 'Android'; - if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad')) return 'iOS'; - - return 'Unknown'; -}; - -/** - * 检测网络状态 - * @returns {Object} 网络信息 - */ -export const getNetworkInfo = () => { - if (!navigator.connection) { - return { online: navigator.onLine, type: 'unknown', speed: 'unknown' }; - } - - const connection = navigator.connection; - - return { - online: navigator.onLine, - type: connection.effectiveType || 'unknown', - speed: connection.downlink || 'unknown', - rtt: connection.rtt || 'unknown', - }; -}; - -/** - * 存储数据到localStorage - * @param {string} key - 键 - * @param {*} value - 值 - * @param {number} expiry - 过期时间(毫秒) - */ -export const setStorage = (key, value, expiry = null) => { - const item = { - value, - timestamp: Date.now(), - expiry: expiry ? Date.now() + expiry : null, - }; - - localStorage.setItem(key, JSON.stringify(item)); -}; - -/** - * 从localStorage获取数据 - * @param {string} key - 键 - * @param {*} defaultValue - 默认值 - * @returns {*} 值 - */ -export const getStorage = (key, defaultValue = null) => { - const itemStr = localStorage.getItem(key); - - if (!itemStr) return defaultValue; - - try { - const item = JSON.parse(itemStr); - - // 检查是否过期 - if (item.expiry && Date.now() > item.expiry) { - localStorage.removeItem(key); - return defaultValue; - } - - return item.value; - } catch (err) { - console.error('Error parsing storage item:', err); - return defaultValue; - } -}; - -/** - * 从localStorage删除数据 - * @param {string} key - 键 - */ -export const removeStorage = (key) => { - localStorage.removeItem(key); -}; - -/** - * 清空localStorage - */ -export const clearStorage = () => { - localStorage.clear(); -}; - -/** - * 生成UUID - * @returns {string} UUID - */ -export const generateUUID = () => { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -}; - -/** - * 检查是否支持某个CSS属性 - * @param {string} property - CSS属性 - * @returns {boolean} 是否支持 - */ -export const supportsCSSProperty = (property) => { - return typeof document !== 'undefined' && - property in document.documentElement.style; -}; - -/** - * 检查是否支持某个JavaScript API - * @param {string} api - API名称 - * @returns {boolean} 是否支持 - */ -export const supportsAPI = (api) => { - return api in window || api in navigator; -}; - -/** - * 获取URL参数 - * @param {string} name - 参数名 - * @returns {string|null} 参数值 - */ -export const getURLParam = (name) => { - const urlParams = new URLSearchParams(window.location.search); - return urlParams.get(name); -}; - -/** - * 设置URL参数 - * @param {string} name - 参数名 - * @param {string} value - 参数值 - */ -export const setURLParam = (name, value) => { - const url = new URL(window.location); - url.searchParams.set(name, value); - window.history.pushState({}, '', url); -}; - -/** - * 删除URL参数 - * @param {string} name - 参数名 - */ -export const removeURLParam = (name) => { - const url = new URL(window.location); - url.searchParams.delete(name); - window.history.pushState({}, '', url); -}; - -/** - * 格式化数字 - * @param {number} number - 数字 - * @param {number} decimals - 小数位数 - * @param {string} decimalSeparator - 小数分隔符 - * @param {string} thousandsSeparator - 千位分隔符 - * @returns {string} 格式化后的字符串 - */ -export const formatNumber = (number, decimals = 0, decimalSeparator = '.', thousandsSeparator = ',') => { - const fixed = number.toFixed(decimals); - const [intPart, decPart] = fixed.split('.'); - - const formattedInt = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator); - - return decimals > 0 ? `${formattedInt}${decimalSeparator}${decPart}` : formattedInt; -}; - -/** - * 格式化货币 - * @param {number} amount - 金额 - * @param {string} currency - 货币代码 - * @param {string} locale - 地区 - * @returns {string} 格式化后的字符串 - */ -export const formatCurrency = (amount, currency = 'USD', locale = 'en-US') => { - return new Intl.NumberFormat(locale, { - style: 'currency', - currency, - }).format(amount); -}; - -/** - * 格式化百分比 - * @param {number} value - 值 - * @param {number} decimals - 小数位数 - * @param {string} locale - 地区 - * @returns {string} 格式化后的字符串 - */ -export const formatPercent = (value, decimals = 2, locale = 'en-US') => { - return new Intl.NumberFormat(locale, { - style: 'percent', - minimumFractionDigits: decimals, - maximumFractionDigits: decimals, - }).format(value / 100); -}; - -/** - * 检查是否为空值 - * @param {*} value - 值 - * @returns {boolean} 是否为空 - */ -export const isEmpty = (value) => { - if (value === null || value === undefined) return true; - if (typeof value === 'string') return value.trim().length === 0; - if (Array.isArray(value)) return value.length === 0; - if (typeof value === 'object') return Object.keys(value).length === 0; - return false; -}; - -/** - * 深拷贝对象 - * @param {*} obj - 对象 - * @returns {*} 拷贝后的对象 - */ -export const deepClone = (obj) => { - if (obj === null || typeof obj !== 'object') return obj; - if (obj instanceof Date) return new Date(obj.getTime()); - if (obj instanceof RegExp) return new RegExp(obj); - if (obj instanceof Map) return new Map([...obj].map(([k, v]) => [k, deepClone(v)])); - if (obj instanceof Set) return new Set([...obj].map(item => deepClone(item))); - - const cloned = Array.isArray(obj) ? [] : {}; - - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - cloned[key] = deepClone(obj[key]); - } - } - - return cloned; -}; - -/** - * 比较两个对象是否相等 - * @param {*} obj1 - 对象1 - * @param {*} obj2 - 对象2 - * @returns {boolean} 是否相等 - */ -export const isEqual = (obj1, obj2) => { - if (obj1 === obj2) return true; - if (obj1 === null || obj2 === null) return false; - if (typeof obj1 !== typeof obj2) return false; - - if (typeof obj1 !== 'object') return obj1 === obj2; - - const keys1 = Object.keys(obj1); - const keys2 = Object.keys(obj2); - - if (keys1.length !== keys2.length) return false; - - for (const key of keys1) { - if (!keys2.includes(key)) return false; - if (!isEqual(obj1[key], obj2[key])) return false; - } - - return true; -}; - -/** - * 验证邮箱格式 - * @param {string} email - 邮箱 - * @returns {boolean} 是否有效 - */ -export const isValidEmail = (email) => { - const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return re.test(email); -}; - -/** - * 验证URL格式 - * @param {string} url - URL - * @returns {boolean} 是否有效 - */ -export const isValidURL = (url) => { - try { - new URL(url); - return true; - } catch { - return false; - } -}; - -/** - * 验证手机号格式 - * @param {string} phone - 手机号 - * @returns {boolean} 是否有效 - */ -export const isValidPhone = (phone) => { - const re = /^1[3-9]\d{9}$/; - return re.test(phone); -}; - -/** - * 验证身份证号格式 - * @param {string} idCard - 身份证号 - * @returns {boolean} 是否有效 - */ -export const isValidIDCard = (idCard) => { - const re = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/; - return re.test(idCard); -}; - -/** - * 生成随机颜色 - * @returns {string} 颜色值 - */ -export const randomColor = () => { - return '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'); -}; - -/** - * 颜色转RGBA - * @param {string} color - 颜色值 - * @param {number} alpha - 透明度 - * @returns {string} RGBA颜色值 - */ -export const colorToRGBA = (color, alpha = 1) => { - const hex = color.replace('#', ''); - const r = parseInt(hex.substring(0, 2), 16); - const g = parseInt(hex.substring(2, 4), 16); - const b = parseInt(hex.substring(4, 6), 16); - - return `rgba(${r}, ${g}, ${b}, ${alpha})`; -}; - -/** - * 获取颜色亮度 - * @param {string} color - 颜色值 - * @returns {number} 亮度值(0-255) - */ -export const getColorBrightness = (color) => { - const hex = color.replace('#', ''); - const r = parseInt(hex.substring(0, 2), 16); - const g = parseInt(hex.substring(2, 4), 16); - const b = parseInt(hex.substring(4, 6), 16); - - return (r * 299 + g * 587 + b * 114) / 1000; -}; - -/** - * 判断是否为浅色 - * @param {string} color - 颜色值 - * @returns {boolean} 是否为浅色 - */ -export const isLightColor = (color) => { - return getColorBrightness(color) > 128; -}; - -/** - * 获取对比色 - * @param {string} color - 颜色值 - * @returns {string} 对比色 - */ -export const getContrastColor = (color) => { - return isLightColor(color) ? '#000000' : '#FFFFFF'; -}; - -/** - * 生成渐变色 - * @param {string} color1 - 起始颜色 - * @param {string} color2 - 结束颜色 - * @param {number} steps - 步数 - * @returns {Array} 颜色数组 - */ -export const generateGradient = (color1, color2, steps = 10) => { - const hex = (color) => { - const c = color.replace('#', ''); - return [ - parseInt(c.substring(0, 2), 16), - parseInt(c.substring(2, 4), 16), - parseInt(c.substring(4, 6), 16), - ]; - }; - - const [r1, g1, b1] = hex(color1); - const [r2, g2, b2] = hex(color2); - - const gradient = []; - - for (let i = 0; i < steps; i++) { - const r = Math.round(r1 + (r2 - r1) * (i / (steps - 1))); - const g = Math.round(g1 + (g2 - g1) * (i / (steps - 1))); - const b = Math.round(b1 + (b2 - b1) * (i / (steps - 1))); - - gradient.push(`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`); - } - - return gradient; -}; - -/** - * 获取字符长度(支持中文) - * @param {string} str - 字符串 - * @returns {number} 长度 - */ -export const getStringLength = (str) => { - let length = 0; - for (let i = 0; i < str.length; i++) { - const code = str.charCodeAt(i); - if (code > 0 && code <= 128) { - length += 1; - } else { - length += 2; - } - } - return length; -}; - -/** - * 截取字符串(支持中文) - * @param {string} str - 字符串 - * @param {number} length - 长度 - * @param {string} suffix - 后缀 - * @returns {string} 截取后的字符串 - */ -export const truncateString = (str, length, suffix = '...') => { - let currentLength = 0; - let result = ''; - - for (let i = 0; i < str.length; i++) { - const char = str[i]; - const code = str.charCodeAt(i); - - if (code > 0 && code <= 128) { - currentLength += 1; - } else { - currentLength += 2; - } - - if (currentLength > length) { - return result + suffix; - } - - result += char; - } - - return result; -}; - -/** - * 驼峰转换 - * @param {string} str - 字符串 - * @returns {string} 驼峰格式字符串 - */ -export const toCamelCase = (str) => { - return str - .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')) - .replace(/^[A-Z]/, (c) => c.toLowerCase()); -}; - -/** - * 短横线转换 - * @param {string} str - 字符串 - * @returns {string} 短横线格式字符串 - */ -export const toKebabCase = (str) => { - return str - .replace(/([a-z])([A-Z])/g, '$1-$2') - .replace(/[\s_]+/g, '-') - .toLowerCase(); -}; - -/** - * 下划线转换 - * @param {string} str - 字符串 - * @returns {string} 下划线格式字符串 - */ -export const toSnakeCase = (str) => { - return str - .replace(/([a-z])([A-Z])/g, '$1_$2') - .replace(/[\s\-]+/g, '_') - .toLowerCase(); -}; - -/** - * 首字母大写 - * @param {string} str - 字符串 - * @returns {string} 首字母大写字符串 - */ -export const capitalize = (str) => { - return str.charAt(0).toUpperCase() + str.slice(1); -}; - -/** - * 每个单词首字母大写 - * @param {string} str - 字符串 - * @returns {string} 每个单词首字母大写字符串 - */ -export const capitalizeWords = (str) => { - return str.replace(/\b\w/g, (char) => char.toUpperCase()); -}; - -/** - * 移除HTML标签 - * @param {string} html - HTML字符串 - * @returns {string} 纯文本 - */ -export const stripHTML = (html) => { - const tmp = document.createElement('div'); - tmp.innerHTML = html; - return tmp.textContent || tmp.innerText || ''; -}; - -/** - * 转义正则表达式 - * @param {string} str - 字符串 - * @returns {string} 转义后的字符串 - */ -export const escapeRegExp = (str) => { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -}; - -/** - * 生成随机字符串 - * @param {number} length - 长度 - * @param {string} chars - 字符集 - * @returns {string} 随机字符串 - */ -export const randomString = (length = 8, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') => { - let result = ''; - for (let i = 0; i < length; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return result; -}; - -/** - * 延迟执行 - * @param {number} ms - 毫秒数 - * @returns {Promise} Promise对象 - */ -export const sleep = (ms) => { - return new Promise(resolve => setTimeout(resolve, ms)); -}; - -/** - * 重试函数 - * @param {Function} fn - 函数 - * @param {number} maxAttempts - 最大尝试次数 - * @param {number} delay - 延迟时间 - * @returns {Promise} Promise对象 - */ -export const retry = async (fn, maxAttempts = 3, delay = 1000) => { - let lastError; - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - return await fn(); - } catch (err) { - lastError = err; - if (attempt < maxAttempts) { - await sleep(delay * attempt); - } - } - } - - throw lastError; -}; - -/** - * 超时控制 - * @param {Promise} promise - Promise对象 - * @param {number} ms - 毫秒数 - * @returns {Promise} Promise对象 - */ -export const timeout = (promise, ms) => { - return Promise.race([ - promise, - new Promise((_, reject) => { - setTimeout(() => reject(new Error('Timeout')), ms); - }), - ]); -}; - -/** - * 并发控制 - * @param {Array} tasks - 任务数组 - * @param {number} concurrency - 并发数 - * @returns {Promise} Promise对象 - */ -export const parallel = async (tasks, concurrency = 5) => { - const results = []; - const executing = new Set(); - - for (const [index, task] of tasks.entries()) { - const p = Promise.resolve().then(() => task()); - results.push(p); - executing.add(p); - - const clean = () => executing.delete(p); - p.then(clean, clean); - - if (executing.size >= concurrency) { - await Promise.race(executing); - } - } - - return Promise.all(results); -}; - -/** - * 序列执行 - * @param {Array} tasks - 任务数组 - * @returns {Promise} Promise对象 - */ -export const sequence = async (tasks) => { - const results = []; - - for (const task of tasks) { - results.push(await task()); - } - - return results; -}; - -/** - * 缓存函数 - * @param {Function} fn - 函数 - * @param {number} maxSize - 最大缓存数 - * @returns {Function} 缓存后的函数 - */ -export const memoize = (fn, maxSize = 100) => { - const cache = new Map(); - - return (...args) => { - const key = JSON.stringify(args); - - if (cache.has(key)) { - return cache.get(key); - } - - const result = fn(...args); - - if (cache.size >= maxSize) { - const firstKey = cache.keys().next().value; - cache.delete(firstKey); - } - - cache.set(key, result); - - return result; - }; -}; - -/** - * 单例模式 - * @param {Function} fn - 函数 - * @returns {Function} 单例函数 - */ -export const singleton = (fn) => { - let instance; - - return (...args) => { - if (!instance) { - instance = fn(...args); - } - return instance; - }; -}; - -/** - * 观察者模式 - * @returns {Object} 观察者对象 - */ -export const createObserver = () => { - const listeners = new Map(); - - return { - on(event, callback) { - if (!listeners.has(event)) { - listeners.set(event, new Set()); - } - listeners.get(event).add(callback); - - return () => this.off(event, callback); - }, - - off(event, callback) { - if (listeners.has(event)) { - listeners.get(event).delete(callback); - } - }, - - emit(event, ...args) { - if (listeners.has(event)) { - listeners.get(event).forEach(callback => callback(...args)); - } - }, - - once(event, callback) { - const wrapper = (...args) => { - callback(...args); - this.off(event, wrapper); - }; - this.on(event, wrapper); - }, - - clear() { - listeners.clear(); - }, - }; -}; - -/** - * 状态机 - * @param {Object} config - 配置 - * @returns {Object} 状态机对象 - */ -export const createStateMachine = (config) => { - let currentState = config.initial; - const listeners = new Map(); - - return { - get state() { - return currentState; - }, - - transition(event) { - const transitions = config.transitions[currentState]; - if (!transitions || !transitions[event]) { - throw new Error(`Invalid transition: ${event} from ${currentState}`); - } - - const nextState = transitions[event]; - const prevState = currentState; - currentState = nextState; - - if (listeners.has(nextState)) { - listeners.get(nextState).forEach(callback => callback(prevState, nextState)); - } - - return nextState; - }, - - on(state, callback) { - if (!listeners.has(state)) { - listeners.set(state, new Set()); - } - listeners.get(state).add(callback); - - return () => this.off(state, callback); - }, - - off(state, callback) { - if (listeners.has(state)) { - listeners.get(state).delete(callback); - } - }, - - can(event) { - const transitions = config.transitions[currentState]; - return transitions && transitions[event] !== undefined; - }, - }; -}; diff --git a/tests/index.test.js b/tests/index.test.js index 53e307d..5f232d8 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -1,7 +1,7 @@ /** * MetonaToast 单元测试 * @module tests - * @version 2.0.0 + * @version 2.0.1 */ import MeToast, { Toast, VERSION } from '../src/index.js'; @@ -121,8 +121,8 @@ describe('MetonaToast', () => { describe('版本信息', () => { test('应该有正确的版本号', () => { - expect(VERSION).toBe('2.0.0'); - expect(MeToast.version).toBe('2.0.0'); + expect(VERSION).toBe('2.0.1'); + expect(MeToast.version).toBe('2.0.1'); }); }); @@ -583,7 +583,7 @@ describe('MetonaToast', () => { MeToast.success('消息'); const status = MeToast.getStatus(); - expect(status.version).toBe('2.0.0'); + expect(status.version).toBe('2.0.1'); expect(status.toasts).toBeGreaterThanOrEqual(0); expect(status.theme).toBeDefined(); expect(status.locale).toBeDefined(); @@ -776,542 +776,6 @@ describe('工具函数', () => { const result = prefersDark(); expect(typeof result).toBe('boolean'); }); - - test('应该能够防抖函数', (done) => { - const { debounce } = require('../src/utils.js'); - - let count = 0; - const fn = jest.fn(() => count++); - const debouncedFn = debounce(fn, 100); - - debouncedFn(); - debouncedFn(); - debouncedFn(); - - expect(fn).not.toHaveBeenCalled(); - - setTimeout(() => { - expect(fn).toHaveBeenCalledTimes(1); - done(); - }, 150); - }); - - test('应该能够节流函数', (done) => { - const { throttle } = require('../src/utils.js'); - - let count = 0; - const fn = jest.fn(() => count++); - const throttledFn = throttle(fn, 100); - - throttledFn(); - throttledFn(); - throttledFn(); - - expect(fn).toHaveBeenCalledTimes(1); - - setTimeout(() => { - throttledFn(); - expect(fn).toHaveBeenCalledTimes(2); - done(); - }, 150); - }); - - test('应该能够深度合并对象', () => { - const { deepMerge } = require('../src/utils.js'); - - const obj1 = { a: 1, b: { c: 2 } }; - const obj2 = { b: { d: 3 }, e: 4 }; - - const merged = deepMerge(obj1, obj2); - - expect(merged.a).toBe(1); - expect(merged.b.c).toBe(2); - expect(merged.b.d).toBe(3); - expect(merged.e).toBe(4); - }); - - test('应该能够检查是否为浏览器环境', () => { - const { isBrowser } = require('../src/utils.js'); - - const result = isBrowser(); - expect(typeof result).toBe('boolean'); - }); - - test('应该能够安全获取嵌套对象属性', () => { - const { getNestedValue } = require('../src/utils.js'); - - const obj = { a: { b: { c: 1 } } }; - - expect(getNestedValue(obj, 'a.b.c')).toBe(1); - expect(getNestedValue(obj, 'a.b.d')).toBeUndefined(); - expect(getNestedValue(obj, 'a.b.d', 'default')).toBe('default'); - }); - - test('应该能够格式化文件大小', () => { - const { formatFileSize } = require('../src/utils.js'); - - expect(formatFileSize(0)).toBe('0 Bytes'); - expect(formatFileSize(1024)).toBe('1 KB'); - expect(formatFileSize(1048576)).toBe('1 MB'); - expect(formatFileSize(1073741824)).toBe('1 GB'); - }); - - test('应该能够格式化日期', () => { - const { formatDate } = require('../src/utils.js'); - - const date = new Date('2024-01-01 12:00:00'); - const formatted = formatDate(date, 'YYYY-MM-DD HH:mm:ss'); - - expect(formatted).toBe('2024-01-01 12:00:00'); - }); - - test('应该能够生成随机数', () => { - const { random } = require('../src/utils.js'); - - const result = random(1, 100); - expect(result).toBeGreaterThanOrEqual(1); - expect(result).toBeLessThanOrEqual(100); - }); - - test('应该能够检查元素是否在视口中', () => { - const { isInViewport } = require('../src/utils.js'); - - const el = { - getBoundingClientRect: () => ({ - top: 0, - left: 0, - bottom: 100, - right: 100, - }), - }; - - expect(isInViewport(el)).toBe(true); - }); - - test('应该能够复制文本到剪贴板', async () => { - const { copyToClipboard } = require('../src/utils.js'); - - // Mock clipboard.writeText 返回 resolved promise - if (!navigator.clipboard) navigator.clipboard = {}; - const origWriteText = navigator.clipboard.writeText; - navigator.clipboard.writeText = jest.fn(() => Promise.resolve()); - - const result = await copyToClipboard('test'); - expect(result).toBe(true); - - navigator.clipboard.writeText = origWriteText; - }); - - test('应该能够检测设备类型', () => { - const { getDeviceType } = require('../src/utils.js'); - - const result = getDeviceType(); - expect(['desktop', 'mobile', 'tablet']).toContain(result); - }); - - test('应该能够检测浏览器信息', () => { - const { getBrowserInfo } = require('../src/utils.js'); - - const result = getBrowserInfo(); - expect(result).toBeDefined(); - expect(result.browser).toBeDefined(); - expect(result.version).toBeDefined(); - }); - - test('应该能够检测操作系统', () => { - const { getOS } = require('../src/utils.js'); - - const result = getOS(); - expect(result).toBeDefined(); - }); - - test('应该能够检测网络状态', () => { - const { getNetworkInfo } = require('../src/utils.js'); - - const result = getNetworkInfo(); - expect(result).toBeDefined(); - expect(result.online).toBeDefined(); - }); - - test('应该能够存储和获取数据', () => { - const { setStorage, getStorage, removeStorage } = require('../src/utils.js'); - - setStorage('test', 'value'); - expect(getStorage('test')).toBe('value'); - - removeStorage('test'); - expect(getStorage('test')).toBeNull(); - }); - - test('应该能够生成UUID', () => { - const { generateUUID } = require('../src/utils.js'); - - const uuid = generateUUID(); - expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); - }); - - test('应该能够检查CSS属性支持', () => { - const { supportsCSSProperty } = require('../src/utils.js'); - - const result = supportsCSSProperty('transform'); - expect(typeof result).toBe('boolean'); - }); - - test('应该能够检查JavaScript API支持', () => { - const { supportsAPI } = require('../src/utils.js'); - - const result = supportsAPI('fetch'); - expect(typeof result).toBe('boolean'); - }); - - test('应该能够获取URL参数', () => { - const { getURLParam } = require('../src/utils.js'); - - // Mock URLSearchParams 来绕过 jsdom location.search 限制 - const origSearchParams = global.URLSearchParams; - global.URLSearchParams = jest.fn(() => ({ - get: jest.fn((name) => name === 'test' ? 'value' : null), - })); - - const result = getURLParam('test'); - expect(result).toBe('value'); - - global.URLSearchParams = origSearchParams; - }); - - test('应该能够格式化数字', () => { - const { formatNumber } = require('../src/utils.js'); - - const result = formatNumber(1234567.89, 2); - expect(result).toBeDefined(); - }); - - test('应该能够格式化货币', () => { - const { formatCurrency } = require('../src/utils.js'); - - const result = formatCurrency(99.99, 'USD'); - expect(result).toBeDefined(); - }); - - test('应该能够格式化百分比', () => { - const { formatPercent } = require('../src/utils.js'); - - const result = formatPercent(75.5, 2); - expect(result).toBeDefined(); - }); - - test('应该能够检查是否为空值', () => { - const { isEmpty } = require('../src/utils.js'); - - expect(isEmpty(null)).toBe(true); - expect(isEmpty(undefined)).toBe(true); - expect(isEmpty('')).toBe(true); - expect(isEmpty(' ')).toBe(true); - expect(isEmpty([])).toBe(true); - expect(isEmpty({})).toBe(true); - expect(isEmpty('text')).toBe(false); - expect(isEmpty([1])).toBe(false); - expect(isEmpty({ a: 1 })).toBe(false); - }); - - test('应该能够深拷贝对象', () => { - const { deepClone } = require('../src/utils.js'); - - const obj = { a: 1, b: { c: 2 }, d: [1, 2, 3] }; - const cloned = deepClone(obj); - - expect(cloned).toEqual(obj); - expect(cloned).not.toBe(obj); - expect(cloned.b).not.toBe(obj.b); - expect(cloned.d).not.toBe(obj.d); - }); - - test('应该能够比较对象是否相等', () => { - const { isEqual } = require('../src/utils.js'); - - expect(isEqual({ a: 1 }, { a: 1 })).toBe(true); - expect(isEqual({ a: 1 }, { a: 2 })).toBe(false); - expect(isEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false); - expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true); - expect(isEqual([1, 2, 3], [1, 2])).toBe(false); - }); - - test('应该能够验证邮箱格式', () => { - const { isValidEmail } = require('../src/utils.js'); - - expect(isValidEmail('test@example.com')).toBe(true); - expect(isValidEmail('invalid-email')).toBe(false); - expect(isValidEmail('test@')).toBe(false); - expect(isValidEmail('@example.com')).toBe(false); - }); - - test('应该能够验证URL格式', () => { - const { isValidURL } = require('../src/utils.js'); - - expect(isValidURL('https://example.com')).toBe(true); - expect(isValidURL('http://example.com')).toBe(true); - expect(isValidURL('invalid-url')).toBe(false); - }); - - test('应该能够验证手机号格式', () => { - const { isValidPhone } = require('../src/utils.js'); - - expect(isValidPhone('13800138000')).toBe(true); - expect(isValidPhone('12345678901')).toBe(false); - expect(isValidPhone('1380013800')).toBe(false); - }); - - test('应该能够验证身份证号格式', () => { - const { isValidIDCard } = require('../src/utils.js'); - - expect(isValidIDCard('110101199003077777')).toBe(true); - expect(isValidIDCard('123456789012345678')).toBe(false); - }); - - test('应该能够生成随机颜色', () => { - const { randomColor } = require('../src/utils.js'); - - const color = randomColor(); - expect(color).toMatch(/^#[0-9a-f]{6}$/); - }); - - test('应该能够颜色转RGBA', () => { - const { colorToRGBA } = require('../src/utils.js'); - - const rgba = colorToRGBA('#ff0000', 0.5); - expect(rgba).toBe('rgba(255, 0, 0, 0.5)'); - }); - - test('应该能够获取颜色亮度', () => { - const { getColorBrightness } = require('../src/utils.js'); - - const brightness = getColorBrightness('#ffffff'); - expect(brightness).toBe(255); - }); - - test('应该能够判断是否为浅色', () => { - const { isLightColor } = require('../src/utils.js'); - - expect(isLightColor('#ffffff')).toBe(true); - expect(isLightColor('#000000')).toBe(false); - }); - - test('应该能够获取对比色', () => { - const { getContrastColor } = require('../src/utils.js'); - - expect(getContrastColor('#ffffff')).toBe('#000000'); - expect(getContrastColor('#000000')).toBe('#FFFFFF'); - }); - - test('应该能够生成渐变色', () => { - const { generateGradient } = require('../src/utils.js'); - - const gradient = generateGradient('#ff0000', '#0000ff', 5); - expect(gradient).toHaveLength(5); - expect(gradient[0]).toBe('#ff0000'); - expect(gradient[4]).toBe('#0000ff'); - }); - - test('应该能够获取字符长度', () => { - const { getStringLength } = require('../src/utils.js'); - - expect(getStringLength('abc')).toBe(3); - expect(getStringLength('中文')).toBe(4); - expect(getStringLength('abc中文')).toBe(7); - }); - - test('应该能够截取字符串', () => { - const { truncateString } = require('../src/utils.js'); - - expect(truncateString('abcdefghij', 5)).toBe('abcde...'); - expect(truncateString('中文字符串测试', 8)).toBe('中文字符...'); - expect(truncateString('abc', 5)).toBe('abc'); - }); - - test('应该能够驼峰转换', () => { - const { toCamelCase } = require('../src/utils.js'); - - expect(toCamelCase('hello-world')).toBe('helloWorld'); - expect(toCamelCase('hello_world')).toBe('helloWorld'); - expect(toCamelCase('hello world')).toBe('helloWorld'); - }); - - test('应该能够短横线转换', () => { - const { toKebabCase } = require('../src/utils.js'); - - expect(toKebabCase('helloWorld')).toBe('hello-world'); - expect(toKebabCase('hello_world')).toBe('hello-world'); - expect(toKebabCase('hello world')).toBe('hello-world'); - }); - - test('应该能够下划线转换', () => { - const { toSnakeCase } = require('../src/utils.js'); - - expect(toSnakeCase('helloWorld')).toBe('hello_world'); - expect(toSnakeCase('hello-world')).toBe('hello_world'); - expect(toSnakeCase('hello world')).toBe('hello_world'); - }); - - test('应该能够首字母大写', () => { - const { capitalize } = require('../src/utils.js'); - - expect(capitalize('hello')).toBe('Hello'); - expect(capitalize('HELLO')).toBe('HELLO'); - }); - - test('应该能够每个单词首字母大写', () => { - const { capitalizeWords } = require('../src/utils.js'); - - expect(capitalizeWords('hello world')).toBe('Hello World'); - }); - - test('应该能够移除HTML标签', () => { - const { stripHTML } = require('../src/utils.js'); - - expect(stripHTML('bold')).toBe('bold'); - expect(stripHTML('

paragraph

')).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 */ // 基础类型