/** * MetonaToast Utils - 工具函数 * @module utils * @version 2.0.0 * @description 通用工具函数集合 */ /** * 生成唯一ID * @returns {string} 唯一ID */ const generateId = () => { return 'met-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8); }; /** * HTML转义 * @param {string} s - 输入字符串 * @returns {string} 转义后的字符串 */ const escapeHTML = (s) => { if (typeof document === 'undefined') { // Node.js环境 return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } const div = document.createElement('div'); div.textContent = String(s == null ? '' : s); return div.innerHTML; }; /** * 检测暗色模式偏好 * @returns {boolean} 是否偏好暗色模式 */ const prefersDark = () => { return typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; }; /** * MetonaToast Icons — 图标SVG定义 * @module icons * @version 2.0.0 * @description 80+ 内置SVG图标 */ const ICONS = { success: ` `, error: ` `, warning: ` `, info: ` `, loading: ` `, close: ` `, check: ` `, x: ` `, alert: ` `, question: ` `, star: ` `, heart: ` `, bell: ` `, mail: ` `, settings: ` `, user: ` `, home: ` `, search: ` `, plus: ` `, minus: ` `, edit: ` `, trash: ` `, download: ` `, upload: ` `, share: ` `, link: ` `, external: ` `, clock: ` `, calendar: ` `, map: ` `, compass: ` `, globe: ` `, wifi: ` `, cloud: ` `, sun: ` `, moon: ` `, zap: ` `, activity: ` `, cpu: ` `, database: ` `, server: ` `, terminal: ` `, code: ` `, git: ` `, package: ` `, layers: ` `, grid: ` `, list: ` `, filter: ` `, sort: ` `, refresh: ` `, sync: ` `, power: ` `, battery: ` `, bluetooth: ` `, volume: ` `, mic: ` `, camera: ` `, image: ` `, video: ` `, music: ` `, file: ` `, folder: ` `, clipboard: ` `, save: ` `, print: ` `, eye: ` `, eyeOff: ` `, lock: ` `, unlock: ` `, shield: ` `, key: ` `, flag: ` `, bookmark: ` `, tag: ` `, gift: ` `, award: ` `, target: ` `, crosshair: ` `, move: ` `, maximize: ` `, minimize: ` `, copy: ` `, cut: ` `, paste: ` `, rotateCw: ` `, rotateCcw: ` `, zoomIn: ` `, zoomOut: ` `, crop: ` `, sliders: ` `, toggleLeft: ` `, toggleRight: ` `, checkCircle: ` `, xCircle: ` `, alertCircle: ` `, infoCircle: ` `, helpCircle: ` `, alertTriangle: ` `, checkSquare: ` `, square: ` `, circle: ` `, triangle: ` `, hexagon: ` `, octagon: ` `, pentagon: ` `, diamond: ` `, }; /** * MetonaToast Locales — 国际化翻译数据 * @module locales * @version 2.0.0 * @description 内置 zh-CN / en-US 完整翻译 */ const LOCALES = { 'zh-CN': { close: '关闭', loading: '加载中...', success: '操作成功', error: '操作失败', warning: '警告', info: '信息', confirm: '确认', cancel: '取消', yes: '是', no: '否', ok: '确定', retry: '重试', undo: '撤销', redo: '重做', save: '保存', delete: '删除', edit: '编辑', add: '添加', remove: '移除', search: '搜索', filter: '筛选', sort: '排序', refresh: '刷新', sync: '同步', upload: '上传', download: '下载', share: '分享', copy: '复制', cut: '剪切', paste: '粘贴', print: '打印', export: '导出', import: '导入', settings: '设置', help: '帮助', about: '关于', version: '版本', language: '语言', theme: '主题', dark: '暗色', light: '亮色', auto: '自动', notifications: '通知', enable: '启用', disable: '禁用', on: '开启', off: '关闭', max: '最大', min: '最小', expand: '展开', collapse: '折叠', fullscreen: '全屏', exitFullscreen: '退出全屏', zoomIn: '放大', zoomOut: '缩小', reset: '重置', apply: '应用', submit: '提交', send: '发送', receive: '接收', connect: '连接', disconnect: '断开', start: '开始', stop: '停止', pause: '暂停', resume: '继续', next: '下一个', previous: '上一个', first: '第一个', last: '最后一个', back: '返回', forward: '前进', home: '首页', menu: '菜单', more: '更多', less: '更少', show: '显示', hide: '隐藏', visible: '可见', hidden: '隐藏', enabled: '已启用', disabled: '已禁用', active: '活跃', inactive: '非活跃', online: '在线', offline: '离线', connected: '已连接', disconnected: '已断开', loading: '加载中', loaded: '已加载', saving: '保存中', saved: '已保存', deleting: '删除中', deleted: '已删除', uploading: '上传中', uploaded: '已上传', downloading: '下载中', downloaded: '已下载', processing: '处理中', processed: '已处理', syncing: '同步中', synced: '已同步', sending: '发送中', sent: '已发送', receiving: '接收中', received: '已接收', connecting: '连接中', error: '错误', warning: '警告', info: '信息', success: '成功', question: '问题', help: '帮助', feedback: '反馈', report: '报告', bug: '错误', feature: '功能', improvement: '改进', suggestion: '建议', request: '请求', response: '响应', status: '状态', progress: '进度', complete: '完成', incomplete: '未完成', pending: '待处理', processing: '处理中', failed: '失败', cancelled: '已取消', timeout: '超时', expired: '已过期', invalid: '无效', valid: '有效', required: '必填', optional: '可选', enabled: '启用', disabled: '禁用', allowed: '允许', denied: '拒绝', approved: '已批准', rejected: '已拒绝', accepted: '已接受', declined: '已拒绝', confirmed: '已确认', unconfirmed: '未确认', verified: '已验证', unverified: '未验证', authenticated: '已认证', unauthenticated: '未认证', authorized: '已授权', unauthorized: '未授权', public: '公开', private: '私有', protected: '受保护', internal: '内部', external: '外部', local: '本地', remote: '远程', global: '全局', system: '系统', user: '用户', admin: '管理员', moderator: '版主', guest: '访客', member: '成员', owner: '所有者', creator: '创建者', editor: '编辑者', viewer: '查看者', subscriber: '订阅者', follower: '关注者', following: '关注中', friend: '朋友', contact: '联系人', group: '群组', team: '团队', organization: '组织', company: '公司', department: '部门', project: '项目', task: '任务', issue: '问题', ticket: '工单', request: '请求', response: '响应', message: '消息', notification: '通知', alert: '警报', reminder: '提醒', event: '事件', activity: '活动', log: '日志', report: '报告', analytics: '分析', statistics: '统计', metrics: '指标', performance: '性能', security: '安全', privacy: '隐私', compliance: '合规', policy: '政策', terms: '条款', conditions: '条件', agreement: '协议', contract: '合同', license: '许可证', copyright: '版权', trademark: '商标', patent: '专利', tradeSecret: '商业秘密', confidential: '机密', sensitive: '敏感', critical: '关键', important: '重要', urgent: '紧急', high: '高', medium: '中', low: '低', none: '无', all: '全部', any: '任何', some: '一些', each: '每个', every: '每个', both: '两者', neither: '两者都不', either: '任一', other: '其他', another: '另一个', this: '这个', that: '那个', these: '这些', those: '那些', here: '这里', there: '那里', where: '哪里', when: '何时', why: '为什么', how: '如何', what: '什么', who: '谁', which: '哪个', whose: '谁的', whom: '谁', myself: '我自己', yourself: '你自己', himself: '他自己', herself: '她自己', itself: '它自己', ourselves: '我们自己', yourselves: '你们自己', themselves: '他们自己', mine: '我的', yours: '你的', his: '他的', hers: '她的', its: '它的', ours: '我们的', yours: '你们的', theirs: '他们的', me: '我', you: '你', him: '他', her: '她', it: '它', us: '我们', them: '他们', my: '我的', your: '你的', his: '他的', her: '她的', its: '它的', our: '我们的', your: '你们的', their: '他们的', i: '我', you: '你', he: '他', she: '她', it: '它', we: '我们', they: '他们', am: '是', is: '是', are: '是', was: '是', were: '是', be: '是', been: '是', being: '是', have: '有', has: '有', had: '有', having: '有', do: '做', does: '做', did: '做', doing: '做', will: '会', would: '会', shall: '将', should: '应该', may: '可以', might: '可能', can: '能', could: '能', must: '必须', need: '需要', dare: '敢', ought: '应该', used: '过去常常', to: '到', of: '的', in: '在', for: '为了', on: '在', with: '和', at: '在', by: '通过', from: '从', into: '进入', during: '在...期间', before: '在...之前', after: '在...之后', above: '在...上面', below: '在...下面', between: '在...之间', under: '在...下面', over: '在...上面', across: '穿过', through: '通过', into: '进入', towards: '朝向', upon: '在...上面', about: '关于', against: '反对', among: '在...之中', along: '沿着', around: '周围', beyond: '超出', but: '但是', despite: '尽管', except: '除了', inside: '里面', outside: '外面', since: '自从', until: '直到', unless: '除非', whether: '是否', while: '当...时候', although: '虽然', because: '因为', if: '如果', once: '一旦', since: '自从', so: '所以', that: '那个', though: '虽然', till: '直到', unless: '除非', until: '直到', when: '当...时候', whenever: '每当', where: '哪里', wherever: '无论哪里', whereas: '然而', wherever: '无论哪里', while: '当...时候', why: '为什么', }, 'en-US': { close: 'Close', loading: 'Loading...', success: 'Success', error: 'Error', warning: 'Warning', info: 'Info', confirm: 'Confirm', cancel: 'Cancel', yes: 'Yes', no: 'No', ok: 'OK', retry: 'Retry', undo: 'Undo', redo: 'Redo', save: 'Save', delete: 'Delete', edit: 'Edit', add: 'Add', remove: 'Remove', search: 'Search', filter: 'Filter', sort: 'Sort', refresh: 'Refresh', sync: 'Sync', upload: 'Upload', download: 'Download', share: 'Share', copy: 'Copy', cut: 'Cut', paste: 'Paste', print: 'Print', export: 'Export', import: 'Import', settings: 'Settings', help: 'Help', about: 'About', version: 'Version', language: 'Language', theme: 'Theme', dark: 'Dark', light: 'Light', auto: 'Auto', notifications: 'Notifications', enable: 'Enable', disable: 'Disable', on: 'On', off: 'Off', max: 'Max', min: 'Min', expand: 'Expand', collapse: 'Collapse', fullscreen: 'Fullscreen', exitFullscreen: 'Exit Fullscreen', zoomIn: 'Zoom In', zoomOut: 'Zoom Out', reset: 'Reset', apply: 'Apply', submit: 'Submit', send: 'Send', receive: 'Receive', connect: 'Connect', disconnect: 'Disconnect', start: 'Start', stop: 'Stop', pause: 'Pause', resume: 'Resume', next: 'Next', previous: 'Previous', first: 'First', last: 'Last', back: 'Back', forward: 'Forward', home: 'Home', menu: 'Menu', more: 'More', less: 'Less', show: 'Show', hide: 'Hide', visible: 'Visible', hidden: 'Hidden', enabled: 'Enabled', disabled: 'Disabled', active: 'Active', inactive: 'Inactive', online: 'Online', offline: 'Offline', connected: 'Connected', disconnected: 'Disconnected', loading: 'Loading', loaded: 'Loaded', saving: 'Saving', saved: 'Saved', deleting: 'Deleting', deleted: 'Deleted', uploading: 'Uploading', uploaded: 'Uploaded', downloading: 'Downloading', downloaded: 'Downloaded', processing: 'Processing', processed: 'Processed', syncing: 'Syncing', synced: 'Synced', sending: 'Sending', sent: 'Sent', receiving: 'Receiving', received: 'Received', connecting: 'Connecting', error: 'Error', warning: 'Warning', info: 'Info', success: 'Success', question: 'Question', help: 'Help', feedback: 'Feedback', report: 'Report', bug: 'Bug', feature: 'Feature', improvement: 'Improvement', suggestion: 'Suggestion', request: 'Request', response: 'Response', status: 'Status', progress: 'Progress', complete: 'Complete', incomplete: 'Incomplete', pending: 'Pending', processing: 'Processing', failed: 'Failed', cancelled: 'Cancelled', timeout: 'Timeout', expired: 'Expired', invalid: 'Invalid', valid: 'Valid', required: 'Required', optional: 'Optional', enabled: 'Enabled', disabled: 'Disabled', allowed: 'Allowed', denied: 'Denied', approved: 'Approved', rejected: 'Rejected', accepted: 'Accepted', declined: 'Declined', confirmed: 'Confirmed', unconfirmed: 'Unconfirmed', verified: 'Verified', unverified: 'Unverified', authenticated: 'Authenticated', unauthenticated: 'Unauthenticated', authorized: 'Authorized', unauthorized: 'Unauthorized', public: 'Public', private: 'Private', protected: 'Protected', internal: 'Internal', external: 'External', local: 'Local', remote: 'Remote', global: 'Global', system: 'System', user: 'User', admin: 'Admin', moderator: 'Moderator', guest: 'Guest', member: 'Member', owner: 'Owner', creator: 'Creator', editor: 'Editor', viewer: 'Viewer', subscriber: 'Subscriber', follower: 'Follower', following: 'Following', friend: 'Friend', contact: 'Contact', group: 'Group', team: 'Team', organization: 'Organization', company: 'Company', department: 'Department', project: 'Project', task: 'Task', issue: 'Issue', ticket: 'Ticket', request: 'Request', response: 'Response', message: 'Message', notification: 'Notification', alert: 'Alert', reminder: 'Reminder', event: 'Event', activity: 'Activity', log: 'Log', report: 'Report', analytics: 'Analytics', statistics: 'Statistics', metrics: 'Metrics', performance: 'Performance', security: 'Security', privacy: 'Privacy', compliance: 'Compliance', policy: 'Policy', terms: 'Terms', conditions: 'Conditions', agreement: 'Agreement', contract: 'Contract', license: 'License', copyright: 'Copyright', trademark: 'Trademark', patent: 'Patent', tradeSecret: 'Trade Secret', confidential: 'Confidential', sensitive: 'Sensitive', critical: 'Critical', important: 'Important', urgent: 'Urgent', high: 'High', medium: 'Medium', low: 'Low', none: 'None', all: 'All', any: 'Any', some: 'Some', each: 'Each', every: 'Every', both: 'Both', neither: 'Neither', either: 'Either', other: 'Other', another: 'Another', this: 'This', that: 'That', these: 'These', those: 'Those', here: 'Here', there: 'There', where: 'Where', when: 'When', why: 'Why', how: 'How', what: 'What', who: 'Who', which: 'Which', whose: 'Whose', whom: 'Whom', myself: 'Myself', yourself: 'Yourself', himself: 'Himself', herself: 'Herself', itself: 'Itself', ourselves: 'Ourselves', yourselves: 'Yourselves', themselves: 'Themselves', mine: 'Mine', yours: 'Yours', his: 'His', hers: 'Hers', its: 'Its', ours: 'Ours', yours: 'Yours', theirs: 'Theirs', me: 'Me', you: 'You', him: 'Him', her: 'Her', it: 'It', us: 'Us', them: 'Them', my: 'My', your: 'Your', his: 'His', her: 'Her', its: 'Its', our: 'Our', your: 'Your', their: 'Their', i: 'I', you: 'You', he: 'He', she: 'She', it: 'It', we: 'We', they: 'They', am: 'Am', is: 'Is', are: 'Are', was: 'Was', were: 'Were', be: 'Be', been: 'Been', being: 'Being', have: 'Have', has: 'Has', had: 'Had', having: 'Having', do: 'Do', does: 'Does', did: 'Did', doing: 'Doing', will: 'Will', would: 'Would', shall: 'Shall', should: 'Should', may: 'May', might: 'Might', can: 'Can', could: 'Could', must: 'Must', need: 'Need', dare: 'Dare', ought: 'Ought', used: 'Used', to: 'To', of: 'Of', in: 'In', for: 'For', on: 'On', with: 'With', at: 'At', by: 'By', from: 'From', into: 'Into', during: 'During', before: 'Before', after: 'After', above: 'Above', below: 'Below', between: 'Between', under: 'Under', over: 'Over', across: 'Across', through: 'Through', into: 'Into', towards: 'Towards', upon: 'Upon', about: 'About', against: 'Against', among: 'Among', along: 'Along', around: 'Around', beyond: 'Beyond', but: 'But', despite: 'Despite', except: 'Except', inside: 'Inside', outside: 'Outside', since: 'Since', until: 'Until', unless: 'Unless', whether: 'Whether', while: 'While', although: 'Although', because: 'Because', if: 'If', once: 'Once', since: 'Since', so: 'So', that: 'That', though: 'Though', till: 'Till', unless: 'Unless', until: 'Until', when: 'When', whenever: 'Whenever', where: 'Where', wherever: 'Wherever', whereas: 'Whereas', wherever: 'Wherever', while: 'While', why: 'Why', }, }; /** * MetonaToast Constants - 常量定义 * @module constants * @version 2.0.0 * @description 默认配置、颜色、动画、主题等常量 */ /** * 默认配置 */ const DEFAULTS = Object.freeze({ // 基础配置 position: 'top-right', duration: 4000, max: 6, gap: 12, offset: 24, // 交互配置 pauseOnHover: true, closeOnClick: true, draggable: true, // 显示配置 showProgress: true, progressDirection: 'horizontal', icon: true, closeButton: true, // 主题配置 theme: 'auto', animation: 'slide', // 布局配置 zIndex: 9999, width: 360, // 自定义配置 className: '', style: {}, // 回调函数 onShow: null, onClose: null, onClick: null, onUpdate: null, // 高级 resetTimerOnUpdate: false, notifyWhenHidden: false, // 国际化 locale: 'zh-CN', // 插件 plugins: [], }); /** * 类型颜色配置 */ const TYPE_COLORS = { success: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, error: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, warning: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, info: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, loading: { fg: '#6366f1', bg: 'rgba(99, 102, 241, 0.1)' }, default: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, check: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, x: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, alert: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, question: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, star: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, heart: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' }, bell: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, mail: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, settings: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, user: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, home: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, search: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, plus: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, minus: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, edit: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, trash: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, download: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, upload: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, share: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, link: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, external: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, clock: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, calendar: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, map: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, compass: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, globe: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, wifi: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, cloud: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, sun: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, moon: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, zap: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, activity: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, cpu: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, database: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, server: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, terminal: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, code: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, git: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, package: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, layers: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, grid: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, list: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, filter: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, sort: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, refresh: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, sync: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, power: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, battery: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, bluetooth: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, volume: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, mic: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, camera: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, image: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, video: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, music: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, file: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, folder: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, clipboard: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, save: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, print: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, eye: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, eyeOff: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, lock: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, unlock: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, shield: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, key: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, flag: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, bookmark: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, tag: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, gift: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' }, award: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, target: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, crosshair: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, move: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, maximize: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, minimize: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, copy: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, cut: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, paste: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, rotateCw: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, rotateCcw: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, zoomIn: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, zoomOut: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, crop: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, sliders: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, toggleLeft: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, toggleRight: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, checkCircle: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, xCircle: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, alertCircle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, infoCircle: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, helpCircle: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, alertTriangle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, checkSquare: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, square: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, circle: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, triangle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, hexagon: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, octagon: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, pentagon: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, diamond: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' }, }; /** * 动画配置 */ const ANIMATIONS = { slide: { enter: { transform: 'translateX(80px)', opacity: 0 }, leave: { transform: 'translateX(120%)', opacity: 0 }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, fade: { enter: { opacity: 0, filter: 'blur(3px)' }, leave: { opacity: 0, filter: 'blur(3px)' }, duration: 500, easing: 'ease', }, scale: { enter: { transform: 'scale(0.55)', opacity: 0 }, leave: { transform: 'scale(0.55)', opacity: 0 }, duration: 450, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', }, bounce: { enter: { transform: 'translateY(-80px)', opacity: 0 }, leave: { transform: 'translateY(20px)', opacity: 0 }, duration: 650, easing: 'ease', }, flip: { enter: { transform: 'perspective(500px) rotateX(-90deg)', opacity: 0 }, leave: { transform: 'perspective(500px) rotateX(90deg)', opacity: 0 }, duration: 500, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, rotate: { enter: { transform: 'rotate(-25deg) scale(0.6)', opacity: 0 }, leave: { transform: 'rotate(25deg) scale(0.6)', opacity: 0 }, duration: 500, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, zoom: { enter: { transform: 'scale(0.1)', opacity: 0 }, leave: { transform: 'scale(0.1)', opacity: 0 }, duration: 500, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', }, slideUp: { enter: { transform: 'translateY(60px)', opacity: 0 }, leave: { transform: 'translateY(-120%)', opacity: 0 }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, slideDown: { enter: { transform: 'translateY(-60px)', opacity: 0 }, leave: { transform: 'translateY(120%)', opacity: 0 }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, slideLeft: { enter: { transform: 'translateX(-90px)', opacity: 0 }, leave: { transform: 'translateX(120%)', opacity: 0 }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, slideRight: { enter: { transform: 'translateX(90px)', opacity: 0 }, leave: { transform: 'translateX(-120%)', opacity: 0 }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, }; /** * 主题配置 */ const THEMES = { light: { bg: 'rgba(255, 255, 255, 0.96)', text: '#1f2937', border: 'rgba(0, 0, 0, 0.06)', shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08)', hoverShadow: '0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10)', progressBg: 'rgba(0, 0, 0, 0.06)', closeHoverBg: 'rgba(0, 0, 0, 0.06)', }, dark: { bg: 'rgba(28, 32, 40, 0.94)', text: '#e6e8eb', border: 'rgba(255, 255, 255, 0.08)', shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4)', hoverShadow: '0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4)', progressBg: 'rgba(255, 255, 255, 0.08)', closeHoverBg: 'rgba(255, 255, 255, 0.08)', }, auto: 'auto', warm: { bg: 'rgba(255, 251, 235, 0.96)', text: '#78350f', border: 'rgba(245, 158, 11, 0.2)', shadow: '0 10px 36px -10px rgba(245, 158, 11, 0.18), 0 4px 14px -4px rgba(245, 158, 11, 0.08)', hoverShadow: '0 14px 48px -10px rgba(245, 158, 11, 0.22), 0 6px 18px -4px rgba(245, 158, 11, 0.10)', progressBg: 'rgba(245, 158, 11, 0.1)', closeHoverBg: 'rgba(245, 158, 11, 0.1)', }, }; /** * MetonaToast Styles - 样式管理 * @module styles * @version 2.0.0 * @description 样式注入、更新和主题管理 */ let styleElement = null; /** * 生成CSS样式 * @param {Object} theme - 主题配置 * @returns {string} CSS字符串 */ const generateCSS = (theme) => { return ` /* MetonaToast 基础样式 */ .met-container { pointer-events: none; display: flex; flex-direction: column; gap: 12px; box-sizing: border-box; padding: 24px; max-width: 100vw; 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; } /* Toast基础样式 */ .met-toast { position: relative; display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border-radius: 12px; box-sizing: border-box; backdrop-filter: blur(14px) saturate(180%); -webkit-backdrop-filter: blur(14px) saturate(180%); 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, opacity 0.25s ease; will-change: transform, opacity; flex-shrink: 0; pointer-events: auto; min-width: 240px; max-width: calc(100vw - 48px); width: 360px; border: 1px solid var(--met-border, rgba(0,0,0,0.06)); 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)); } /* 悬停效果 */ .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; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-top: 1px; } /* 内容样式 */ .met-content { flex: 1; min-width: 0; 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; background: transparent; border: 0; padding: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: background 0.2s, color 0.2s; line-height: 0; 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; 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; 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%; transform-origin: left; transform: scaleX(1); } /* 进度条指示器 - 垂直 */ .met-bar-v { position: absolute; left: 0; bottom: 0; width: 100%; height: 100%; transform-origin: bottom; transform: scaleY(1); } /* 侧边指示器 */ .met-side { position: absolute; 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); } } /* === 进入动画:初始隐藏态 === */ .met-anim-slide.met-toast, .met-anim-fade.met-toast, .met-anim-scale.met-toast, .met-anim-bounce.met-toast, .met-anim-flip.met-toast, .met-anim-rotate.met-toast, .met-anim-zoom.met-toast, .met-anim-slideUp.met-toast, .met-anim-slideDown.met-toast, .met-anim-slideLeft.met-toast, .met-anim-slideRight.met-toast { opacity: 0; } /* === 进入动画:@keyframes 定义 === */ @keyframes met-slide-in { 0% { transform: translateX(80px); opacity: 0; } 65% { transform: translateX(-6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } @keyframes met-fade-in { 0% { opacity: 0; filter: blur(3px); } 100% { opacity: 1; filter: blur(0); } } @keyframes met-scale-in { 0% { transform: scale(0.55); opacity: 0; } 65% { transform: scale(1.07); opacity: 1; } 100% { transform: scale(1); opacity: 1; } } @keyframes met-bounce-in { 0% { transform: translateY(-80px); opacity: 0; animation-timing-function: ease-in; } 45% { transform: translateY(14px); opacity: 1; animation-timing-function: ease-out; } 65% { transform: translateY(-12px); animation-timing-function: ease-in; } 82% { transform: translateY(4px); animation-timing-function: ease-out; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-flip-in { 0% { transform: perspective(500px) rotateX(-90deg); opacity: 0; } 60% { transform: perspective(500px) rotateX(8deg); opacity: 1; } 85% { transform: perspective(500px) rotateX(-2deg); } 100% { transform: perspective(500px) rotateX(0); opacity: 1; } } @keyframes met-rotate-in { 0% { transform: rotate(-25deg) scale(0.6); opacity: 0; } 60% { transform: rotate(4deg) scale(1.05); opacity: 1; } 85% { transform: rotate(-1deg) scale(0.98); } 100% { transform: rotate(0) scale(1); opacity: 1; } } @keyframes met-zoom-in { 0% { transform: scale(0.1); opacity: 0; } 50% { transform: scale(1.12); opacity: 1; } 75% { transform: scale(0.94); } 100% { transform: scale(1); opacity: 1; } } @keyframes met-slideUp-in { 0% { transform: translateY(60px); opacity: 0; } 70% { transform: translateY(-6px); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-slideDown-in { 0% { transform: translateY(-60px); opacity: 0; } 70% { transform: translateY(6px); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-slideLeft-in { 0% { transform: translateX(-90px); opacity: 0; } 65% { transform: translateX(6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } @keyframes met-slideRight-in { 0% { transform: translateX(90px); opacity: 0; } 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; } .met-anim-scale.met-toast.met-show { animation: met-scale-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } .met-anim-bounce.met-toast.met-show { animation: met-bounce-in 0.65s ease forwards; } .met-anim-flip.met-toast.met-show { animation: met-flip-in 0.50s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-rotate.met-toast.met-show { animation: met-rotate-in 0.50s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-zoom.met-toast.met-show { animation: met-zoom-in 0.50s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } .met-anim-slideUp.met-toast.met-show { animation: met-slideUp-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .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; } } /* 暗色主题特定样式 */ .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); } .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; } .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); } .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; } .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); --met-text: var(--met-theme-text); --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); } .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; } .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); } } /* 亮色模式自动检测 */ @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); } .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; } .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); } `; }; /** * 注入样式 * @param {Object} theme - 主题配置 */ 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); }; /** * MetonaToast Themes - 主题管理 * @module themes * @version 2.0.0 * @description 主题系统、自定义主题和主题切换 */ // 当前主题状态 let currentTheme = 'auto'; let themeListeners = new Set(); let systemThemeMediaQuery = null; let systemThemeListener = null; /** * 获取系统主题 * @returns {string} 系统主题 ('light' 或 'dark') */ const getSystemTheme = () => { if (typeof window === 'undefined') return 'light'; return prefersDark() ? 'dark' : 'light'; }; /** * 获取主题(别名) * @param {string} theme - 主题名称 * @returns {string} 解析后的主题 */ const getTheme = (theme) => { return resolveTheme(theme); }; /** * 解析主题 * @param {string} theme - 主题名称 * @returns {string} 解析后的主题 */ const resolveTheme = (theme) => { if (theme === 'auto') { // 优先使用用户设置的全局主题,否则使用系统主题 if (currentTheme && currentTheme !== 'auto') { return currentTheme; } return getSystemTheme(); } return theme; }; /** * 获取主题配置 * @param {string} theme - 主题名称 * @returns {Object} 主题配置 */ const getThemeConfig = (theme) => { const resolved = resolveTheme(theme); return THEMES[resolved] || THEMES.light; }; /** * 应用主题 * @param {string} theme - 主题名称 */ const applyTheme = (theme) => { currentTheme = theme; const resolved = resolveTheme(theme); // 应用主题到文档 if (typeof document !== 'undefined') { document.documentElement.setAttribute('data-met-theme', resolved); document.documentElement.classList.remove('met-theme-light', 'met-theme-dark', 'met-theme-auto'); document.documentElement.classList.add(`met-theme-${resolved}`); // 设置CSS变量 const config = getThemeConfig(theme); setThemeVariables(config); } // 通知监听器 notifyThemeListeners(theme, resolved); }; /** * 设置主题CSS变量 * @param {Object} config - 主题配置 */ const setThemeVariables = (config) => { if (typeof document === 'undefined') return; const root = document.documentElement; root.style.setProperty('--met-bg', config.bg); root.style.setProperty('--met-text', config.text); root.style.setProperty('--met-border', config.border); root.style.setProperty('--met-shadow', config.shadow); root.style.setProperty('--met-hover-shadow', config.hoverShadow); root.style.setProperty('--met-progress-bg', config.progressBg); root.style.setProperty('--met-close-hover-bg', config.closeHoverBg); // 设置颜色变量 root.style.setProperty('--met-success', '#10b981'); root.style.setProperty('--met-error', '#ef4444'); root.style.setProperty('--met-warning', '#f59e0b'); root.style.setProperty('--met-info', '#3b82f6'); root.style.setProperty('--met-loading', '#6366f1'); }; /** * 获取当前主题 * @returns {string} 当前主题 */ const getCurrentTheme = () => { return currentTheme; }; /** * 获取解析后的主题 * @returns {string} 解析后的主题 */ const getResolvedTheme = () => { return resolveTheme(currentTheme); }; /** * 切换主题 * @param {string} theme - 新主题 */ const switchTheme = (theme) => { applyTheme(theme); saveTheme(theme); }; /** * 切换亮色/暗色主题 */ const toggleTheme = () => { const resolved = getResolvedTheme(); const newTheme = resolved === 'dark' ? 'light' : 'dark'; switchTheme(newTheme); }; /** * 重置为自动主题 */ const resetToAuto = () => { switchTheme('auto'); }; /** * 保存主题到本地存储 * @param {string} theme - 主题名称 */ const saveTheme = (theme) => { if (typeof localStorage !== 'undefined') { try { localStorage.setItem('metona-toast-theme', theme); } catch (e) { console.warn('Failed to save theme:', e); } } }; /** * 从本地存储加载主题 * @returns {string} 主题名称 */ const loadTheme = () => { if (typeof localStorage !== 'undefined') { try { return localStorage.getItem('metona-toast-theme') || 'auto'; } catch (e) { console.warn('Failed to load theme:', e); } } return 'auto'; }; /** * 初始化主题系统 */ const initTheme = () => { // 加载保存的主题 const savedTheme = loadTheme(); applyTheme(savedTheme); // 监听系统主题变化 watchSystemTheme(); }; /** * 监听系统主题变化 */ const watchSystemTheme = () => { if (typeof window === 'undefined' || !window.matchMedia) return; // 移除旧的监听器 if (systemThemeMediaQuery && systemThemeListener) { systemThemeMediaQuery.removeEventListener('change', systemThemeListener); } // 创建新的监听器 systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); systemThemeListener = (e) => { if (currentTheme === 'auto') { applyTheme('auto'); } }; systemThemeMediaQuery.addEventListener('change', systemThemeListener); }; /** * 停止监听系统主题变化 */ const unwatchSystemTheme = () => { if (systemThemeMediaQuery && systemThemeListener) { systemThemeMediaQuery.removeEventListener('change', systemThemeListener); systemThemeMediaQuery = null; systemThemeListener = null; } }; /** * 添加主题监听器 * @param {Function} listener - 监听器函数 * @returns {Function} 移除监听器函数 */ const addThemeListener = (listener) => { themeListeners.add(listener); return () => { themeListeners.delete(listener); }; }; /** * 移除主题监听器 * @param {Function} listener - 监听器函数 */ const removeThemeListener = (listener) => { themeListeners.delete(listener); }; /** * 通知主题监听器 * @param {string} theme - 主题名称 * @param {string} resolved - 解析后的主题 */ const notifyThemeListeners = (theme, resolved) => { themeListeners.forEach((listener) => { try { listener(theme, resolved); } catch (e) { console.error('Theme listener error:', e); } }); }; /** * 清除所有主题监听器 */ const clearThemeListeners = () => { themeListeners.clear(); }; /** * 注册自定义主题 * @param {string} name - 主题名称 * @param {Object} config - 主题配置 */ const registerTheme = (name, config) => { THEMES[name] = { bg: config.bg || THEMES.light.bg, text: config.text || THEMES.light.text, border: config.border || THEMES.light.border, shadow: config.shadow || THEMES.light.shadow, hoverShadow: config.hoverShadow || THEMES.light.hoverShadow, progressBg: config.progressBg || THEMES.light.progressBg, closeHoverBg: config.closeHoverBg || THEMES.light.closeHoverBg, }; }; /** * 注销自定义主题 * @param {string} name - 主题名称 */ const unregisterTheme = (name) => { if (name === 'light' || name === 'dark' || name === 'auto') { console.warn('Cannot unregister built-in theme:', name); return; } delete THEMES[name]; }; /** * 获取所有主题 * @returns {Object} 主题映射 */ const getAllThemes = () => { return { ...THEMES }; }; /** * 获取主题名称列表 * @returns {Array} 主题名称数组 */ const getThemeNames = () => { return Object.keys(THEMES); }; /** * 检查主题是否存在 * @param {string} name - 主题名称 * @returns {boolean} 是否存在 */ const hasTheme = (name) => { return name in THEMES; }; /** * 获取主题预览 * @param {string} theme - 主题名称 * @returns {Object} 主题预览 */ const getThemePreview = (theme) => { const config = getThemeConfig(theme); const resolved = resolveTheme(theme); return { name: theme, resolved, colors: { background: config.bg, text: config.text, border: config.border, }, isDark: resolved === 'dark', isLight: resolved === 'light', isAuto: theme === 'auto', }; }; /** * 生成主题CSS * @param {string} theme - 主题名称 * @returns {string} CSS字符串 */ const generateThemeCSS = (theme) => { const config = getThemeConfig(theme); const resolved = resolveTheme(theme); return ` .met-theme-${resolved} { --met-bg: ${config.bg}; --met-text: ${config.text}; --met-border: ${config.border}; --met-shadow: ${config.shadow}; --met-hover-shadow: ${config.hoverShadow}; --met-progress-bg: ${config.progressBg}; --met-close-hover-bg: ${config.closeHoverBg}; } `; }; /** * 应用主题CSS * @param {string} theme - 主题名称 */ const applyThemeCSS = (theme) => { if (typeof document === 'undefined') return; const css = generateThemeCSS(theme); const styleId = 'metona-toast-theme-css'; // 移除旧样式 const oldStyle = document.getElementById(styleId); if (oldStyle) { oldStyle.remove(); } // 添加新样式 const style = document.createElement('style'); style.id = styleId; style.textContent = css; document.head.appendChild(style); }; /** * 移除主题CSS */ const removeThemeCSS = () => { if (typeof document === 'undefined') return; const styleId = 'metona-toast-theme-css'; const style = document.getElementById(styleId); if (style) { style.remove(); } }; /** * 主题工具 — 代理层,直接引用模块函数 */ const themeUtils = { getSystemTheme, resolveTheme, getThemeConfig, applyTheme, getCurrentTheme, getResolvedTheme, switchTheme, toggleTheme, resetToAuto, initTheme, watchSystemTheme, unwatchSystemTheme, addThemeListener, removeThemeListener, clearThemeListeners, registerTheme, unregisterTheme, getAllThemes, getThemeNames, hasTheme, getThemePreview, generateThemeCSS, applyThemeCSS, removeThemeCSS, saveTheme, loadTheme, }; /** * 预设主题 */ const presetThemes = { /** * 浅色主题 */ light: { name: '浅色', description: '明亮清晰的主题', config: THEMES.light, }, /** * 深色主题 */ dark: { name: '深色', description: '护眼舒适的暗色主题', config: THEMES.dark, }, /** * 自动主题 */ auto: { name: '自动', description: '跟随系统主题设置', config: 'auto', }, warm: { name: '暖色', description: '温馨舒适的暖色主题', config: THEMES.warm, }, }; // 注册预设主题 Object.entries(presetThemes).forEach(([name, theme]) => { if (name !== 'auto' && theme.config !== 'auto') { THEMES[name] = theme.config; } }); /** * MetonaToast i18n - 国际化管理 * @module i18n * @version 2.0.0 * @description 多语言支持、语言切换和翻译管理 */ // 当前语言状态 let currentLocale = 'zh-CN'; let localeListeners = new Set(); let fallbackLocale = 'zh-CN'; /** * 获取当前语言 * @returns {string} 当前语言 */ const getCurrentLocale = () => { return currentLocale; }; /** * 设置当前语言 * @param {string} locale - 语言代码 */ const setCurrentLocale = (locale) => { if (!LOCALES[locale]) { console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`); locale = fallbackLocale; } currentLocale = locale; notifyLocaleListeners(locale); saveLocale(locale); }; /** * 获取回退语言 * @returns {string} 回退语言 */ const getFallbackLocale = () => { return fallbackLocale; }; /** * 设置回退语言 * @param {string} locale - 语言代码 */ const setFallbackLocale = (locale) => { if (!LOCALES[locale]) { console.warn(`Fallback locale "${locale}" not found`); return; } fallbackLocale = locale; }; /** * 翻译函数 * @param {string} key - 翻译键 * @param {Object} params - 插值参数 * @returns {string} 翻译后的字符串 */ const t = (key, params = {}) => { // 尝试当前语言 const currentTranslation = getTranslation(currentLocale, key); if (currentTranslation !== undefined) { return interpolate(currentTranslation, params); } // 尝试回退语言 if (currentLocale !== fallbackLocale) { const fallbackTranslation = getTranslation(fallbackLocale, key); if (fallbackTranslation !== undefined) { return interpolate(fallbackTranslation, params); } } // 返回键名 console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`); return key; }; /** * 获取翻译 * @param {string} locale - 语言代码 * @param {string} key - 翻译键 * @returns {string|undefined} 翻译字符串 */ const getTranslation = (locale, key) => { const localeData = LOCALES[locale]; if (!localeData) return undefined; // 支持嵌套键 (如 'common.close') const keys = key.split('.'); let result = localeData; for (const k of keys) { if (result && typeof result === 'object' && k in result) { result = result[k]; } else { return undefined; } } return typeof result === 'string' ? result : undefined; }; /** * 插值函数 * @param {string} str - 包含占位符的字符串 * @param {Object} params - 插值参数 * @returns {string} 插值后的字符串 */ const interpolate = (str, params) => { return str.replace(/\{(\w+)\}/g, (match, key) => { return params[key] !== undefined ? params[key] : match; }); }; /** * 检查翻译是否存在 * @param {string} key - 翻译键 * @returns {boolean} 是否存在 */ const hasTranslation = (key) => { return getTranslation(currentLocale, key) !== undefined || getTranslation(fallbackLocale, key) !== undefined; }; /** * 获取所有翻译 * @param {string} locale - 语言代码 * @returns {Object} 翻译对象 */ const getTranslations = (locale) => { return LOCALES[locale] || {}; }; /** * 添加翻译 * @param {string} locale - 语言代码 * @param {Object} translations - 翻译对象 */ const addTranslations = (locale, translations) => { if (!LOCALES[locale]) { LOCALES[locale] = {}; } deepMerge(LOCALES[locale], translations); }; /** * 深度合并对象 * @param {Object} target - 目标对象 * @param {Object} source - 源对象 * @returns {Object} 合并后的对象 */ const deepMerge = (target, source) => { for (const key in source) { if (source[key] instanceof Object && key in target && target[key] instanceof Object) { deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }; /** * 移除翻译 * @param {string} locale - 语言代码 * @param {string} key - 翻译键 */ const removeTranslation = (locale, key) => { const localeData = LOCALES[locale]; if (!localeData) return; const keys = key.split('.'); let current = localeData; for (let i = 0; i < keys.length - 1; i++) { if (current[keys[i]] && typeof current[keys[i]] === 'object') { current = current[keys[i]]; } else { return; } } delete current[keys[keys.length - 1]]; }; /** * 清除翻译 * @param {string} locale - 语言代码 */ const clearTranslations = (locale) => { if (LOCALES[locale]) { LOCALES[locale] = {}; } }; /** * 获取支持的语言列表 * @returns {Array} 语言代码数组 */ const getSupportedLocales = () => { return Object.keys(LOCALES); }; /** * 检查语言是否支持 * @param {string} locale - 语言代码 * @returns {boolean} 是否支持 */ const isLocaleSupported = (locale) => { return locale in LOCALES; }; /** * 获取语言名称 * @param {string} locale - 语言代码 * @returns {string} 语言名称 */ const getLocaleName = (locale) => { const names = { 'zh-CN': '简体中文', 'zh-TW': '繁體中文', 'en-US': 'English (US)', 'en-GB': 'English (UK)', 'ja': '日本語', 'ko': '한국어', 'fr': 'Français', 'de': 'Deutsch', 'es': 'Español', 'pt': 'Português', 'ru': 'Русский', 'ar': 'العربية', 'hi': 'हिन्दी', 'th': 'ไทย', 'vi': 'Tiếng Việt', 'id': 'Bahasa Indonesia', 'ms': 'Bahasa Melayu', 'tr': 'Türkçe', 'it': 'Italiano', 'nl': 'Nederlands', 'pl': 'Polski', 'uk': 'Українська', 'cs': 'Čeština', 'sv': 'Svenska', 'da': 'Dansk', 'fi': 'Suomi', 'nb': 'Norsk', 'el': 'Ελληνικά', 'he': 'עברית', 'hu': 'Magyar', 'ro': 'Română', 'bg': 'Български', 'hr': 'Hrvatski', 'sk': 'Slovenčina', 'sl': 'Slovenščina', 'et': 'Eesti', 'lv': 'Latviešu', 'lt': 'Lietuvių', 'ca': 'Català', 'gl': 'Galego', 'eu': 'Euskara', 'cy': 'Cymraeg', 'ga': 'Gaeilge', 'mt': 'Malti', 'is': 'Íslenska', 'mk': 'Македонски', 'sq': 'Shqip', 'sr': 'Српски', 'bs': 'Bosanski', 'me': 'Crnogorski', 'ka': 'ქართული', 'hy': 'Հայերեն', 'az': 'Azərbaycan', 'uz': "O'zbek", 'kk': 'Қазақ', 'ky': 'Кыргыз', 'tg': 'Тоҷикӣ', 'tk': 'Türkmen', 'mn': 'Монгол', 'ne': 'नेपाली', 'si': 'සිංහල', 'my': 'မြန်မာ', 'km': 'ខ្មែរ', 'lo': 'ລາວ', 'am': 'አማርኛ', 'sw': 'Kiswahili', 'yo': 'Yorùbá', 'ig': 'Igbo', 'ha': 'Hausa', 'zu': 'isiZulu', 'af': 'Afrikaans', 'xh': 'isiXhosa', 'st': 'Sesotho', 'tn': 'Setswana', 'ts': 'Xitsonga', 'ss': 'siSwati', 've': 'Tshivenda', 'nr': 'isiNdebele', }; return names[locale] || locale; }; /** * 获取语言方向 * @param {string} locale - 语言代码 * @returns {string} 语言方向 ('ltr' 或 'rtl') */ const getLocaleDirection = (locale) => { const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug']; return rtlLocales.includes(locale) ? 'rtl' : 'ltr'; }; /** * 获取语言信息 * @param {string} locale - 语言代码 * @returns {Object} 语言信息 */ const getLocaleInfo = (locale) => { return { code: locale, name: getLocaleName(locale), direction: getLocaleDirection(locale), isSupported: isLocaleSupported(locale), isCurrent: locale === currentLocale, isFallback: locale === fallbackLocale, }; }; /** * 获取所有语言信息 * @returns {Array} 语言信息数组 */ const getAllLocaleInfo = () => { return getSupportedLocales().map(getLocaleInfo); }; /** * 保存语言到本地存储 * @param {string} locale - 语言代码 */ const saveLocale = (locale) => { if (typeof localStorage !== 'undefined') { try { localStorage.setItem('metona-toast-locale', locale); } catch (e) { console.warn('Failed to save locale:', e); } } }; /** * 从本地存储加载语言 * @returns {string} 语言代码 */ const loadLocale = () => { if (typeof localStorage !== 'undefined') { try { return localStorage.getItem('metona-toast-locale') || getDefaultLocale(); } catch (e) { console.warn('Failed to load locale:', e); } } return getDefaultLocale(); }; /** * 获取默认语言 * @returns {string} 语言代码 */ const getDefaultLocale = () => { if (typeof navigator !== 'undefined') { // 尝试从浏览器获取语言 const browserLocale = navigator.language || navigator.userLanguage; if (browserLocale && isLocaleSupported(browserLocale)) { return browserLocale; } // 尝试语言代码的前两位 const shortLocale = browserLocale?.split('-')[0]; if (shortLocale && isLocaleSupported(shortLocale)) { return shortLocale; } } return fallbackLocale; }; /** * 初始化国际化系统 */ const initI18n = () => { const savedLocale = loadLocale(); setCurrentLocale(savedLocale); }; /** * 切换语言 * @param {string} locale - 新语言 */ const switchLocale = (locale) => { setCurrentLocale(locale); }; /** * 添加语言监听器 * @param {Function} listener - 监听器函数 * @returns {Function} 移除监听器函数 */ const addLocaleListener = (listener) => { localeListeners.add(listener); return () => { localeListeners.delete(listener); }; }; /** * 移除语言监听器 * @param {Function} listener - 监听器函数 */ const removeLocaleListener = (listener) => { localeListeners.delete(listener); }; /** * 通知语言监听器 * @param {string} locale - 语言代码 */ const notifyLocaleListeners = (locale) => { localeListeners.forEach((listener) => { try { listener(locale); } catch (e) { console.error('Locale listener error:', e); } }); }; /** * 清除所有语言监听器 */ const clearLocaleListeners = () => { localeListeners.clear(); }; /** * 格式化数字 * @param {number} number - 数字 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatNumber = (number, options = {}) => { try { return new Intl.NumberFormat(currentLocale, options).format(number); } catch (e) { return number.toString(); } }; /** * 格式化货币 * @param {number} amount - 金额 * @param {string} currency - 货币代码 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatCurrency = (amount, currency = 'USD', options = {}) => { try { return new Intl.NumberFormat(currentLocale, { style: 'currency', currency, ...options, }).format(amount); } catch (e) { return amount.toString(); } }; /** * 格式化百分比 * @param {number} value - 值 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatPercent = (value, options = {}) => { try { return new Intl.NumberFormat(currentLocale, { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 2, ...options, }).format(value / 100); } catch (e) { return `${value}%`; } }; /** * 格式化日期 * @param {Date|number|string} date - 日期 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatDate = (date, options = {}) => { try { const dateObj = date instanceof Date ? date : new Date(date); return new Intl.DateTimeFormat(currentLocale, options).format(dateObj); } catch (e) { return date.toString(); } }; /** * 格式化时间 * @param {Date|number|string} date - 日期 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatTime = (date, options = {}) => { return formatDate(date, { hour: 'numeric', minute: 'numeric', second: 'numeric', ...options, }); }; /** * 格式化相对时间 * @param {Date|number|string} date - 日期 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatRelativeTime = (date, options = {}) => { try { const dateObj = date instanceof Date ? date : new Date(date); const now = new Date(); const diff = dateObj - now; const rtf = new Intl.RelativeTimeFormat(currentLocale, { numeric: 'auto', ...options, }); const units = [ { unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 }, { unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 }, { unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 }, { unit: 'day', ms: 24 * 60 * 60 * 1000 }, { unit: 'hour', ms: 60 * 60 * 1000 }, { unit: 'minute', ms: 60 * 1000 }, { unit: 'second', ms: 1000 }, ]; for (const { unit, ms } of units) { if (Math.abs(diff) >= ms || unit === 'second') { const value = Math.round(diff / ms); return rtf.format(value, unit); } } return date.toString(); } catch (e) { return date.toString(); } }; /** * 格式化列表 * @param {Array} list - 列表 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatList = (list, options = {}) => { try { return new Intl.ListFormat(currentLocale, options).format(list); } catch (e) { return list.join(', '); } }; /** * 格式化复数 * @param {number} count - 数量 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatPlural = (count, options = {}) => { try { return new Intl.PluralRules(currentLocale, options).select(count); } catch (e) { return count === 1 ? 'one' : 'other'; } }; /** * 复数翻译 * @param {string} key - 翻译键 * @param {number} count - 数量 * @param {Object} params - 插值参数 * @returns {string} 翻译后的字符串 */ const plural = (key, count, params = {}) => { const pluralForm = formatPlural(count); const pluralKey = `${key}.${pluralForm}`; if (hasTranslation(pluralKey)) { return t(pluralKey, { ...params, count }); } // 尝试通用形式 if (hasTranslation(key)) { return t(key, { ...params, count }); } return key; }; /** * 国际化工具 — 代理层,直接引用模块函数 */ const i18nUtils = { t, plural, getCurrentLocale, setCurrentLocale, switchLocale, getFallbackLocale, setFallbackLocale, hasTranslation, getTranslations, addTranslations, removeTranslation, clearTranslations, getSupportedLocales, isLocaleSupported, getLocaleName, getLocaleDirection, getLocaleInfo, getAllLocaleInfo, formatNumber, formatCurrency, formatPercent, formatDate, formatTime, formatRelativeTime, formatList, formatPlural, addLocaleListener, removeLocaleListener, clearLocaleListeners, initI18n, saveLocale, loadLocale, getDefaultLocale, }; /** * 预设语言包 */ ({ 'zh-CN': { name: '简体中文', nativeName: '简体中文', direction: 'ltr', translations: LOCALES['zh-CN'], }, 'en-US': { name: 'English (US)', nativeName: 'English (US)', direction: 'ltr', translations: LOCALES['en-US'], }, }); /** * MetonaToast Core - 核心Toast逻辑 * @module core * @version 2.0.0 * @description 重构后的核心模块,包含Toast类的优化实现 */ // 模块级共享容器缓存,所有 Toast 实例共用 // 使用 Map 而非 WeakMap:document.body 在页面存活期间永不被 GC, // 容器需要显式通过 destroy() 清理,WeakMap 在此场景无实际收益。 const _containerCache = new Map(); /** * 参数标准化工具 * 支持多种调用方式: * - success('message') * - success({ message: 'text' }) * - success({ title: 'title', message: 'text' }) * - success('message', { duration: 5000 }) */ const normalizeArgs = (args, defaultType = 'default') => { const [first, second] = args; // 情况1: success('message') if (typeof first === 'string') { return { ...second, type: second?.type || defaultType, message: first, }; } // 情况2: success({ message: 'text' }) 或 success({ title: 't', message: 'm' }) if (first && typeof first === 'object') { return { ...first, type: first.type || defaultType, }; } // 情况3: success() 无参数 return { type: defaultType, message: '', }; }; /** * Toast类 - 核心通知组件 */ class Toast { // 静态钩子系统 static _hooks = new Map(); static on(name, fn) { if (!this._hooks.has(name)) this._hooks.set(name, []); this._hooks.get(name).push(fn); return () => this.off(name, fn); } static off(name, fn) { const list = this._hooks.get(name); if (list) this._hooks.set(name, list.filter(f => f !== fn)); } static trigger(name, toast) { const list = this._hooks.get(name); if (list) list.forEach(fn => { try { fn(toast); } catch (e) { console.error('Hook error:', name, e); } }); } constructor(opts) { this.id = opts.id || generateId(); this.type = opts.type || 'default'; this.title = opts.title || ''; this.message = opts.message ?? opts.content ?? ''; this.html = opts.html || ''; this.iconHTML = opts.iconHTML || ''; this.config = { ...DEFAULTS, ...opts }; // 防止嵌套对象被多个 toast 实例共享引用 if (opts.style && typeof opts.style === 'object') { this.config.style = { ...opts.style }; } this.el = null; this.barEl = null; this.rafId = null; this.remaining = this.config.duration; this.startedAt = 0; this.paused = false; this.closing = false; this._cleanups = []; this.group = opts.group || null; } _palette() { const theme = getTheme(this.config.theme); const c = TYPE_COLORS[this.type] || TYPE_COLORS.default; const tc = THEMES[theme] || THEMES.light; const t = { bg: tc.bg, border: tc.border, shadow: tc.shadow }; return { theme, c, t }; } create() { if (typeof window === 'undefined' || typeof document === 'undefined') return this; Toast.trigger('beforeShow', this); injectStyles(); const container = this._getContainer(); const { theme, c, t } = this._palette(); this._limitToasts(container); const el = document.createElement('div'); el.className = this._buildClassName(theme); el.setAttribute('role', (this.type === 'error' || this.type === 'warning') ? 'alert' : 'status'); el.setAttribute('aria-live', this.type === 'error' ? 'assertive' : 'polite'); el.dataset.id = this.id; this._applyStyles(el, theme, t); this._buildContent(el, c); this.el = el; // Chrome bug: column-reverse + appendChild 会导致重叠。改用 column + insertBefore const pos = this.config.position || 'top-right'; if (pos.startsWith('top')) { container.insertBefore(el, container.firstChild); } else { container.appendChild(el); } this._bindEvents(el); this._startTimer(); // 进入动画 requestAnimationFrame(() => { requestAnimationFrame(() => { el.classList.add('met-show'); }); }); if (typeof this.config.onShow === 'function') { try { this.config.onShow(this); } catch (e) { console.error('onShow callback error:', e); } } Toast.trigger('afterShow', this); return this; } _getContainer() { const position = this.config.position || 'top-right'; const zIndex = this.config.zIndex || 9999; if (_containerCache.has(document.body)) { const containers = _containerCache.get(document.body); if (containers.has(position)) { return containers.get(position); } } else { _containerCache.set(document.body, new Map()); } const el = document.createElement('div'); el.className = `met-container ${position}`; el.setAttribute('aria-label', 'Notifications'); el.setAttribute('role', 'region'); // cssText 完全接管布局,不依赖 CSS class const posStyles = { 'top-left': 'top:0;left:0;align-items:flex-start', 'top-center': 'top:0;left:0;right:0;align-items:center', 'top-right': 'top:0;right:0;align-items:flex-end', 'bottom-left': 'bottom:0;left:0;align-items:flex-start', 'bottom-center':'bottom:0;left:0;right:0;align-items:center', 'bottom-right': 'bottom:0;right:0;align-items:flex-end', }; el.style.cssText = ` display:flex; flex-direction:column; gap:${this.config.gap || 12}px; box-sizing:border-box; padding:${this.config.offset || 24}px; max-width:100vw; position:fixed; z-index:${zIndex}; pointer-events:none; ${posStyles[position] || 'top:0;right:0;align-items:flex-end'} `; document.body.appendChild(el); _containerCache.get(document.body).set(position, el); return el; } _limitToasts(container) { const max = this.config.max || 6; const list = Array.from(container.querySelectorAll('.met-toast')); if (list.length >= max) { const first = list[0]; const id = first && first.dataset.id; if (id) { const old = meToast._toasts.get(id); if (old) old.close(true); } } } _buildClassName(theme) { // CSS 支持的动画类型,未列出的 fallback 到 slide const CSS_ANIMS = ['slide','fade','scale','bounce','flip','rotate','zoom', 'slideUp','slideDown','slideLeft','slideRight']; const anim = CSS_ANIMS.includes(this.config.animation) ? this.config.animation : 'slide'; return [ 'met-toast', `met-${this.type}`, `met-theme-${theme}`, `met-anim-${anim}`, (this.config.closeOnClick || this.config.draggable) ? 'met-clickable' : '', this.config.className || '', ].filter(Boolean).join(' '); } _applyStyles(el, theme, t) { const widthValue = typeof this.config.width === 'number' ? `${this.config.width}px` : (typeof this.config.width === 'string' ? this.config.width : '360px'); // 只强制核心布局属性,动画/滤镜/颜色由 CSS class 控制 const set = (p, v) => el.style.setProperty(p, v, 'important'); set('position', 'relative'); set('flex-shrink', '0'); set('min-width', '240px'); set('max-width', 'calc(100vw - 48px)'); set('width', widthValue); // 用户自定义样式最后应用 if (this.config.style && Object.keys(this.config.style).length > 0) { Object.entries(this.config.style).forEach(([k, v]) => { el.style[k] = v; }); } } _buildContent(el, c) { // 自定义渲染函数 — 完全接管 DOM 构建 if (typeof this.config.render === 'function') { el.innerHTML = this.config.render(this); this.barEl = el.querySelector('.met-bar, .met-bar-v'); return; } const showIcon = this.config.icon !== false && (this.iconHTML || ICONS[this.type]); const showClose = this.config.closeButton !== false; const showProgress = this.config.showProgress !== false && this.config.duration > 0; const showSide = (!showIcon && this.type !== 'default'); const safeTitle = this.title ? `
${escapeHTML(this.title)}
` : ''; const safeMessage = this.html ? `
${this.html}
` : (this.message ? `
${escapeHTML(this.message)}
` : ''); const iconHTML = showIcon ? `
${this.iconHTML || ICONS[this.type]}
` : ''; const closeHTML = showClose ? `` : ''; const progressHTML = showProgress ? (this.config.progressDirection === 'vertical' ? `
` : `
`) : ''; const sideHTML = showSide ? `
` : ''; el.innerHTML = ` ${iconHTML} ${sideHTML}
${safeTitle} ${safeMessage}
${closeHTML} ${progressHTML} `; this.barEl = el.querySelector('.met-bar, .met-bar-v'); } _bindEvents(el) { const eventHandler = (e) => { const target = e.target; if (target.closest('.met-close')) { e.stopPropagation(); this.close(); return; } if (this.config.closeOnClick && !target.closest('.met-close')) { if (typeof this.config.onClick === 'function') { try { this.config.onClick(this); } catch (err) { console.error('onClick callback error:', err); } } this.close(); return; } }; el.addEventListener('click', eventHandler); this._cleanups.push(() => el.removeEventListener('click', eventHandler)); if (this.config.pauseOnHover && this.config.duration > 0) { const mouseEnter = () => this._pause(); const mouseLeave = () => this._resume(); el.addEventListener('mouseenter', mouseEnter); el.addEventListener('mouseleave', mouseLeave); this._cleanups.push(() => { el.removeEventListener('mouseenter', mouseEnter); el.removeEventListener('mouseleave', mouseLeave); }); } if (this.config.draggable) { this._bindDrag(el); } } _bindDrag(el) { let sx = 0, sy = 0, dx = 0, dy = 0, dragging = false; const down = (e) => { if (e.target.closest('.met-close')) return; dragging = true; sx = e.clientX; sy = e.clientY; el.setPointerCapture(e.pointerId); el.style.transition = 'none'; this._pause(); }; const move = (e) => { if (!dragging) return; dx = e.clientX - sx; dy = e.clientY - sy; el.style.transform = `translate(${dx}px, ${dy}px) rotate(${dx * 0.1}deg)`; el.style.opacity = String(Math.max(0, 1 - Math.abs(dx) / 200)); }; const up = (e) => { if (!dragging) return; dragging = false; el.releasePointerCapture(e.pointerId); el.style.transition = ''; if (Math.abs(dx) > 120) { el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`; el.style.opacity = '0'; setTimeout(() => this.close(true), 250); } else { el.style.transform = ''; el.style.opacity = ''; this._resume(); } dx = dy = 0; }; el.addEventListener('pointerdown', down); el.addEventListener('pointermove', move); el.addEventListener('pointerup', up); el.addEventListener('pointercancel', up); this._cleanups.push(() => { el.removeEventListener('pointerdown', down); el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', up); el.removeEventListener('pointercancel', up); }); } _startTimer() { if (this.config.duration <= 0) return; this.startedAt = Date.now(); this.remaining = this.config.duration; const tick = () => { if (this.paused || this.closing) return; const elapsed = Date.now() - this.startedAt; this.remaining = Math.max(0, this.config.duration - elapsed); if (this.barEl) { const ratio = this.remaining / this.config.duration; const t = this.config.progressDirection === 'vertical' ? `scaleY(${ratio})` : `scaleX(${ratio})`; this.barEl.style.transform = t; } if (this.remaining <= 0) { this.close(); return; } this.rafId = requestAnimationFrame(tick); }; this.rafId = requestAnimationFrame(tick); } _pause() { if (this.paused || this.config.duration <= 0) return; this.paused = true; cancelAnimationFrame(this.rafId); this.remaining = Math.max(0, this.config.duration - (Date.now() - this.startedAt)); } _resume() { if (!this.paused) return; this.paused = false; this.startedAt = Date.now() - (this.config.duration - this.remaining); this._startTimer(); } update(partial) { Toast.trigger('beforeUpdate', this); if (partial.type) this.type = partial.type; if (partial.title !== undefined) this.title = partial.title; if (partial.message !== undefined) this.message = partial.message; if (partial.html !== undefined) this.html = partial.html; if (!this.el) { Toast.trigger('afterUpdate', this); return this; } const content = this.el.querySelector('.met-content'); if (content) { const safeTitle = this.title ? `
${escapeHTML(this.title)}
` : ''; const safeMessage = this.html ? `
${this.html}
` : (this.message ? `
${escapeHTML(this.message)}
` : ''); content.innerHTML = `${safeTitle}${safeMessage}`; } // resetTimerOnUpdate: 更新内容后重置计时器 if (this.config.resetTimerOnUpdate && this.config.duration > 0) { cancelAnimationFrame(this.rafId); this.startedAt = Date.now(); this.remaining = this.config.duration; this._startTimer(); } if (typeof this.config.onUpdate === 'function') { try { this.config.onUpdate(this); } catch (e) { console.error('onUpdate callback error:', e); } } Toast.trigger('afterUpdate', this); return this; } close(immediate = false) { if (this.closing) return; this.closing = true; Toast.trigger('beforeClose', this); cancelAnimationFrame(this.rafId); this._cleanups.forEach(fn => { try { fn(); } catch (_) {} }); this._cleanups = []; const el = this.el; if (!el) { meToast._remove(this.id); return; } // 立即脱离文档流:记录当前位置,然后设为 absolute,让下方 toast 自然上移 const container = el.parentNode; if (container && !immediate) { const elRect = el.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); el.style.position = 'absolute'; el.style.top = (elRect.top - containerRect.top) + 'px'; el.style.left = (elRect.left - containerRect.left) + 'px'; el.style.width = elRect.width + 'px'; el.style.margin = '0'; } el.classList.add('met-leaving'); el.classList.remove('met-show'); const pos = this.config.position || 'top-right'; const isRTL = getLocaleDirection(getCurrentLocale()) === 'rtl'; let transform = 'scale(.96)'; if (pos.includes('right')) transform = isRTL ? 'translateX(-120%)' : 'translateX(120%)'; else if (pos.includes('left')) transform = isRTL ? 'translateX(120%)' : 'translateX(-120%)'; else if (pos.startsWith('top')) transform = 'translateY(-20px)'; else transform = 'translateY(20px)'; el.style.transform = transform; el.style.opacity = '0'; setTimeout(() => this._destroy(), immediate ? 0 : 300); } _destroy() { if (this.el && this.el.parentNode) { this.el.parentNode.removeChild(this.el); } this.el = null; if (typeof this.config.onClose === 'function') { try { this.config.onClose(this); } catch (e) { console.error('onClose callback error:', e); } } Toast.trigger('afterClose', this); meToast._remove(this.id); } } // ========== HTML模板辅助函数 ========== /** * 按钮行模板 — confirm/prompt 共用 * @param {string} buttons - 按钮HTML * @returns {string} 按钮行HTML */ const _btnRow = (buttons) => `
${buttons}
`; /** * 确认对话框模板 * @param {Object} opts - 选项 * @returns {string} HTML */ const _confirmHTML = (opts) => { const confirmText = opts.confirmText || t('confirm'); const cancelText = opts.cancelText || t('cancel'); const confirmColor = opts.confirmColor || '#10b981'; const cancelColor = opts.cancelColor || '#6b7280'; return _btnRow(` `); }; /** * 输入对话框模板 * @param {Object} opts - 选项 * @returns {string} HTML */ const _promptHTML = (opts) => { const submitText = opts.submitText || t('confirm'); const cancelText = opts.cancelText || t('cancel'); const submitColor = opts.submitColor || '#3b82f6'; const cancelColor = opts.cancelColor || '#6b7280'; return ` ${_btnRow(` `)} `; }; /** * 进度条模板 * @param {Object} opts - 选项 * @returns {string} HTML */ const _progressHTML = (opts) => { const color = opts.progressColor || '#3b82f6'; return `
0%
`; }; /** * Action Toast 模板 * @param {Array} actions - 操作按钮数组 [{ text, onClick, style }] * @returns {string} HTML */ const _actionHTML = (actions) => { if (!Array.isArray(actions) || actions.length === 0) return ''; const buttons = actions.map((a, i) => { const bg = a.style?.background || a.color || '#6366f1'; const cls = `met-action-btn-${i}`; return ``; }).join(''); return _btnRow(buttons); }; /** * 主对象 */ const meToast = { _toasts: new Map(), _config: { ...DEFAULTS }, version: '2.0.0', configure(opts) { if (!opts || typeof opts !== 'object') return this; this._config = { ...this._config, ...opts }; if (opts.theme) { applyTheme(opts.theme); } if (opts.locale) { setCurrentLocale(opts.locale); } return this; }, _emit(opts) { const merged = { ...this._config, ...opts }; if (merged.content && !merged.message) merged.message = merged.content; const t = new Toast(merged); t.create(); this._toasts.set(t.id, t); return t; }, _remove(id) { this._toasts.delete(id); }, find(id) { if (!id) return undefined; return this._toasts.get(id); }, /** * 显示默认Toast * @param {string|Object} messageOrOpts - 消息字符串或选项对象 * @param {Object} [opts] - 选项对象(当第一个参数是字符串时) */ show(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'default'); return this._emit(normalized); }, /** * 显示成功Toast * @param {string|Object} messageOrOpts - 消息字符串或选项对象 * @param {Object} [opts] - 选项对象 */ success(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'success'); return this._emit(normalized); }, /** * 显示错误Toast * @param {string|Object} messageOrOpts - 消息字符串或选项对象 * @param {Object} [opts] - 选项对象 */ error(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'error'); return this._emit(normalized); }, /** * 显示警告Toast * @param {string|Object} messageOrOpts - 消息字符串或选项对象 * @param {Object} [opts] - 选项对象 */ warning(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'warning'); return this._emit(normalized); }, /** * 显示信息Toast * @param {string|Object} messageOrOpts - 消息字符串或选项对象 * @param {Object} [opts] - 选项对象 */ info(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'info'); return this._emit(normalized); }, /** * 显示加载Toast * @param {string|Object} messageOrOpts - 消息字符串或选项对象 * @param {Object} [opts] - 选项对象 */ loading(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'loading'); normalized.duration = 0; normalized.closeButton = false; normalized.showProgress = false; const t = this._emit(normalized); return { id: t.id, success: (msg, o = {}) => this._resolve(t, 'success', msg, o), error: (msg, o = {}) => this._resolve(t, 'error', msg, o), info: (msg, o = {}) => this._resolve(t, 'info', msg, o), warning: (msg, o = {}) => this._resolve(t, 'warning', msg, o), update: (p) => { t.update(p); return this; }, dismiss: () => t.close(), }; }, promise(promise, opts = {}) { if (!promise || typeof promise.then !== 'function') { console.error('MeToast.promise: first argument must be a Promise'); return Promise.reject(new Error('Invalid promise')); } const loadingMsg = opts.loading || t('loading'); const successMsg = opts.success || t('success'); const errorMsg = opts.error || t('error'); const ctrl = this.loading(loadingMsg); return Promise.resolve(promise) .then((data) => { ctrl.success(successMsg); return data; }) .catch((err) => { ctrl.error(errorMsg); throw err; }); }, _resolve(loadingToast, type, message, opts) { const id = loadingToast.id; const old = this._toasts.get(id); if (!old) return null; const position = old.config.position; old.close(); return this._emit({ ...opts, type, message, position }); }, /** * 确认对话框 * @param {string} message - 消息内容 * @param {Object} [opts] - 选项 * @returns {Promise} */ confirm(message, opts = {}) { if (typeof message !== 'string') { console.error('MeToast.confirm: message must be a string'); return Promise.resolve(false); } return new Promise((resolve) => { let resolved = false; const safeResolve = (val) => { if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); } }; const safetyTimeout = setTimeout(() => safeResolve(false), 10000); const t = this._emit({ ...opts, type: opts.type || 'warning', message, duration: 0, closeButton: false, closeOnClick: false, draggable: false, html: _confirmHTML(opts), }); setTimeout(() => { const confirmBtn = t.el?.querySelector('.met-confirm-btn'); const cancelBtn = t.el?.querySelector('.met-cancel-btn'); if (confirmBtn) { confirmBtn.addEventListener('click', () => { t.close(); safeResolve(true); }); } if (cancelBtn) { cancelBtn.addEventListener('click', () => { t.close(); safeResolve(false); }); } }, 0); }); }, /** * 输入对话框 * @param {string} message - 消息内容 * @param {Object} [opts] - 选项 * @returns {Promise} */ prompt(message, opts = {}) { if (typeof message !== 'string') { console.error('MeToast.prompt: message must be a string'); return Promise.resolve(null); } return new Promise((resolve) => { let resolved = false; const safeResolve = (val) => { if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); } }; const safetyTimeout = setTimeout(() => safeResolve(null), 10000); const t = this._emit({ ...opts, type: opts.type || 'info', message, duration: 0, closeButton: false, closeOnClick: false, draggable: false, html: _promptHTML(opts), }); setTimeout(() => { const input = t.el?.querySelector('.met-input'); const submitBtn = t.el?.querySelector('.met-submit-btn'); const cancelBtn = t.el?.querySelector('.met-cancel-btn'); if (input) { input.focus(); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { t.close(); safeResolve(input.value); } }); } if (submitBtn) { submitBtn.addEventListener('click', () => { t.close(); safeResolve(input?.value || null); }); } if (cancelBtn) { cancelBtn.addEventListener('click', () => { t.close(); safeResolve(null); }); } }, 0); }); }, /** * 进度Toast * @param {string|Object} messageOrOpts - 消息或选项 * @param {Object} [opts] - 选项 * @returns {Object} 控制对象 */ progress(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'info'); normalized.duration = 0; normalized.closeButton = false; normalized.showProgress = false; const t$1 = this._emit({ ...normalized, html: _progressHTML(opts), }); return { id: t$1.id, setProgress(percent) { const fill = t$1.el?.querySelector('.met-progress-fill'); const text = t$1.el?.querySelector('.met-progress-text'); if (fill) { fill.style.width = `${Math.min(100, Math.max(0, percent))}%`; } if (text) { text.textContent = `${Math.round(percent)}%`; } }, complete(message) { this.setProgress(100); setTimeout(() => { t$1.update({ type: 'success', message: message || t('success'), html: '', }); setTimeout(() => t$1.close(), 1000); }, 300); }, error(message) { t$1.update({ type: 'error', message: message || t('error'), html: '', }); setTimeout(() => t$1.close(), 2000); }, dismiss() { t$1.close(); }, }; }, /** * 倒计时Toast * @param {string} message - 消息内容 * @param {number} seconds - 秒数 * @param {Object} [opts] - 选项 * @returns {Object} 控制对象 */ countdown(message, seconds = 10, opts = {}) { if (typeof message !== 'string') { console.error('MeToast.countdown: message must be a string'); return { cancel: () => {}, pause: () => {}, resume: () => {} }; } let remaining = Math.max(1, parseInt(seconds) || 10); let timer = null; const t = this._emit({ ...opts, type: opts.type || 'warning', message: message.replace(/\{seconds\}/g, remaining), duration: 0, closeButton: true, showProgress: false, }); const tick = () => { remaining--; if (remaining <= 0) { clearInterval(timer); t.close(); if (typeof opts.onComplete === 'function') { try { opts.onComplete(); } catch (e) { console.error('countdown onComplete error:', e); } } return; } t.update({ message: message.replace(/\{seconds\}/g, remaining), }); }; timer = setInterval(tick, 1000); return { id: t.id, cancel() { clearInterval(timer); t.close(); }, pause() { clearInterval(timer); }, resume() { clearInterval(timer); timer = setInterval(tick, 1000); }, }; }, /** * Action Toast — 带操作按钮的通知 * @param {string|Object} messageOrOpts - 消息 * @param {Array} actions - 操作按钮 [{ text, onClick, color }] * @param {Object} [opts] - 选项 * @returns {Object} toast实例 + dismiss */ action(messageOrOpts, actions = [], opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'info'); normalized.duration = opts.duration ?? 0; normalized.closeButton = opts.closeButton ?? true; const t = this._emit({ ...normalized, html: (normalized.html || '') + _actionHTML(actions), }); if (Array.isArray(actions)) { setTimeout(() => { actions.forEach((a, i) => { const btn = t.el?.querySelector(`.met-action-btn-${i}`); if (btn && typeof a.onClick === 'function') { btn.addEventListener('click', () => { try { a.onClick(t); } catch (e) { console.error('Action onClick error:', e); } if (a.close !== false) t.close(); }); } }); }, 0); } return { id: t.id, toast: t, dismiss: () => t.close() }; }, /** * 队列Toast * @param {Array} messages - 消息数组 * @param {Object} [opts] - 选项 * @returns {Promise} */ queue(messages, opts = {}) { if (!Array.isArray(messages)) { console.error('MeToast.queue: messages must be an array'); return { then: (fn) => Promise.resolve().then(fn), cancel: () => {} }; } let cancelled = false; const promise = new Promise((resolve) => { let index = 0; const delay = opts.delay || 1000; const userOnClose = opts.onClose; const showNext = () => { if (cancelled || index >= messages.length) { resolve(); return; } const message = messages[index]; index++; const msg = typeof message === 'string' ? message : (message?.message || ''); const msgObj = (typeof message === 'object' && message !== null) ? message : {}; const msgOnClose = msgObj.onClose; this._emit({ ...opts, ...msgObj, message: msg, duration: msgObj.duration || opts.duration || 3000, onClose: (toast) => { if (typeof msgOnClose === 'function') { try { msgOnClose(toast); } catch (_) {} } if (typeof userOnClose === 'function') { try { userOnClose(toast); } catch (_) {} } setTimeout(showNext, delay); }, }); }; showNext(); }); return { then: (fn, rj) => promise.then(fn, rj), catch: (rj) => promise.catch(rj), cancel: () => { cancelled = true; }, }; }, /** * 堆叠Toast * @param {Array} messages - 消息数组 * @param {Object} [opts] - 选项 */ stack(messages, opts = {}) { if (!Array.isArray(messages)) { console.error('MeToast.stack: messages must be an array'); return; } messages.forEach((message, index) => { setTimeout(() => { const msg = typeof message === 'string' ? message : (message?.message || ''); this._emit({ ...opts, ...((typeof message === 'object' && message !== null) ? message : {}), message: msg, }); }, index * (opts.stagger || 100)); }); }, dismiss(id) { if (id) { const t = this._toasts.get(id); if (t) t.close(); return; } this._toasts.forEach(t => t.close()); }, clear(position) { this._toasts.forEach(t => { if (!position || t.config.position === position) t.close(); }); }, destroy() { this.dismiss(); if (typeof document !== 'undefined') { const containers = document.querySelectorAll('.met-container'); containers.forEach(c => c.parentNode && c.parentNode.removeChild(c)); const style = document.getElementById('metona-toast-styles'); if (style) style.parentNode.removeChild(style); } this._toasts.clear(); _containerCache.clear(); }, getAll() { return new Map(this._toasts); }, count() { return this._toasts.size; }, /** * 创建Toast分组 — 返回一个自动添加 group 参数的子对象 * @param {string} name - 分组名称 * @returns {Object} */ group(name) { const self = this; const methods = ['show','success','error','warning','info','loading','action']; const g = { _group: name }; methods.forEach(m => { g[m] = (...args) => { const lastArg = args[args.length - 1]; const isObj = typeof lastArg === 'object' && lastArg !== null && !Array.isArray(lastArg); // 单对象参数:直接合并group if (isObj && args.length === 1) { return self[m]({ ...lastArg, group: name }); } // 末尾是配置对象:pop后合并 if (isObj) { args.pop(); return self[m](...args, { ...lastArg, group: name }); } // 无配置对象 return self[m](...args, { group: name }); }; }); g.dismiss = () => self.dismissGroup(name); g.count = () => self._groupCount(name); return g; }, /** * 按组关闭Toast * @param {string} name - 分组名称 */ dismissGroup(name) { this._toasts.forEach(t => { if (t.group === name) t.close(); }); }, _groupCount(name) { let c = 0; this._toasts.forEach(t => { if (t.group === name) c++; }); return c; }, }; /** * MetonaToast Animations - 动画管理 * @module animations * @version 2.0.0 * @description 动画效果管理和自定义 */ // 动画缓存 const animationCache = 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; }); /** * 创建动画管理器实例 * @returns {AnimationManager} 动画管理器实例 */ const createAnimationManager = () => { return new AnimationManager(); }; // 创建默认实例 const defaultAnimationManager = createAnimationManager(); /** * 动画工具函数 */ 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 - 动画配置 */ register(name, config) { defaultAnimationManager.register(name, config); }, /** * 注销动画 * @param {string} name - 动画名称 */ unregister(name) { defaultAnimationManager.unregister(name); }, /** * 获取动画配置 * @param {string} name - 动画名称 * @returns {Object|null} 动画配置 */ get(name) { return defaultAnimationManager.get(name); }, /** * 获取所有动画名称 * @returns {Array} 动画名称数组 */ getAnimationNames() { return Array.from(defaultAnimationManager.animations.keys()); }, /** * 获取动画配置 * @returns {Object} 动画配置 */ getConfig() { return defaultAnimationManager.getConfig(); }, /** * 重置动画管理器 */ reset() { defaultAnimationManager.reset(); }, /** * 销毁动画管理器 */ destroy() { defaultAnimationManager.destroy(); }, }; /** * MetonaToast Plugins - 插件系统 * @module plugins * @version 2.0.0 * @description 插件管理器 + 3 款预设插件 (keyboard / persistence / accessibility) */ /** * 插件管理器 */ class PluginManager { constructor() { this.plugins = new Map(); this.initialized = false; } register(name, plugin) { if (this.plugins.has(name)) { console.warn(`Plugin "${name}" is already registered`); return this; } if (!plugin || typeof plugin !== 'object' || (!plugin.name && !plugin.version)) { console.error(`Invalid plugin "${name}"`); return this; } this.plugins.set(name, { name, ...plugin, installed: false, enabled: true }); if (plugin.install) { try { plugin.install(this); this.plugins.get(name).installed = true; } catch (e) { console.error(`Failed to install plugin "${name}":`, e); } } return this; } unregister(name) { const plugin = this.plugins.get(name); if (!plugin) return this; if (plugin.uninstall) { try { plugin.uninstall(this); } catch (e) { console.error(`Failed to uninstall plugin "${name}":`, e); } } this.plugins.delete(name); return this; } get(name) { return this.plugins.get(name) || null; } has(name) { return this.plugins.has(name); } getAll() { return Array.from(this.plugins.values()); } getNames() { return Array.from(this.plugins.keys()); } enable(name) { const p = this.plugins.get(name); if (p) p.enabled = true; return this; } disable(name) { const p = this.plugins.get(name); if (p) p.enabled = false; return this; } isEnabled(name) { const p = this.plugins.get(name); return p ? p.enabled : false; } destroy() { this.plugins.forEach((p, name) => { if (p.destroy) { try { p.destroy(this); } catch (e) {} } }); this.plugins.clear(); } } /** * 预设插件 */ const presetPlugins = { /** * 键盘快捷键插件 — ESC 关闭所有 Toast */ keyboard: { name: 'keyboard', version: '1.0.0', description: 'ESC 关闭所有 Toast', _handler: null, install() { if (typeof document === 'undefined') return; this._handler = (e) => { if (e.key === 'Escape') { // 点击所有关闭按钮 document.querySelectorAll('.met-toast .met-close').forEach(btn => { try { btn.click(); } catch (_) {} }); } }; document.addEventListener('keydown', this._handler); }, uninstall() { if (this._handler && typeof document !== 'undefined') { document.removeEventListener('keydown', this._handler); this._handler = null; } }, }, /** * 持久化插件 — 自动保存/加载配置到 localStorage */ persistence: { name: 'persistence', version: '1.0.0', description: '自动持久化配置到 localStorage', storageKey: 'metona-toast-config', install() { // 加载已保存的配置 if (typeof localStorage !== 'undefined') { try { const saved = localStorage.getItem(this.storageKey); return saved ? JSON.parse(saved) : null; } catch (_) { return null; } } return null; }, save(config) { if (typeof localStorage !== 'undefined') { try { localStorage.setItem(this.storageKey, JSON.stringify(config)); } catch (_) {} } }, uninstall() { if (typeof localStorage !== 'undefined') { try { localStorage.removeItem(this.storageKey); } catch (_) {} } }, }, /** * 无障碍插件 — 屏幕阅读器公告 */ accessibility: { name: 'accessibility', version: '1.0.0', description: '通过屏幕阅读器朗读 Toast 内容', announce(toast) { if (typeof document === 'undefined') return; const el = document.createElement('div'); el.setAttribute('aria-live', 'assertive'); el.setAttribute('aria-atomic', 'true'); el.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;'; const typeName = t(toast.type) || toast.type; el.textContent = `${typeName}: ${toast.title || ''} ${toast.message || ''}`; document.body.appendChild(el); setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000); }, }, }; /** * 插件工具 */ const pluginUtils = { createManager() { return new PluginManager(); }, register(name, plugin) { return defaultPluginManager.register(name, plugin); }, unregister(name) { return defaultPluginManager.unregister(name); }, get(name) { return defaultPluginManager.get(name); }, has(name) { return defaultPluginManager.has(name); }, getAll() { return defaultPluginManager.getAll(); }, getNames() { return defaultPluginManager.getNames(); }, enable(name) { return defaultPluginManager.enable(name); }, disable(name) { return defaultPluginManager.disable(name); }, isEnabled(name) { return defaultPluginManager.isEnabled(name); }, getPreset(name) { return presetPlugins[name] || null; }, getAllPresets() { return { ...presetPlugins }; }, createPlugin(config) { return { name: config.name || 'custom', version: config.version || '1.0.0', description: config.description || '', hooks: config.hooks || {}, install: config.install || null, uninstall: config.uninstall || null, ...config, }; }, validatePlugin(plugin) { const errors = []; if (!plugin || typeof plugin !== 'object') errors.push('Plugin must be an object'); if (!plugin.name && !plugin.version) errors.push('Plugin must have a name or version'); return { valid: errors.length === 0, errors }; }, }; const defaultPluginManager = new PluginManager(); /** * MetonaToast - 轻量级Toast通知库 * @module metona-toast * @version 2.0.0 * @author thzxx * @description 轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。 * @license MIT */ // 版本信息 const VERSION = '2.0.0'; /** * 主对象增强 */ const enhancedMeToast = { ...meToast, version: VERSION, animations: animationUtils, themes: themeUtils, i18n: i18nUtils, plugins: pluginUtils, presetPlugins, /** * 安装插件,并连接必要的生命周期钩子 */ use(plugin, options = {}) { if (typeof plugin === 'string') { const preset = presetPlugins[plugin]; if (!preset) { console.warn(`Preset plugin "${plugin}" not found`); return this; } this.plugins.register(plugin, { ...preset, ...options }); // 连接插件钩子 if (plugin === 'accessibility') { Toast.on('afterShow', (toast) => preset.announce(toast)); } if (plugin === 'persistence') { const saved = preset.install(); if (saved) this.configure(saved); Toast.on('afterClose', () => { preset.save(this.getConfig()); }); } } else if (plugin && typeof plugin === 'object') { const name = plugin.name || 'custom'; this.plugins.register(name, { ...plugin, ...options }); } return this; }, /** * 初始化 */ init(options = {}) { if (options.config) { this.configure(options.config); } if (options.theme) { this.themes.switchTheme(options.theme); } if (options.locale) { this.i18n.switchLocale(options.locale); } if (options.plugins && Array.isArray(options.plugins)) { options.plugins.forEach((plugin) => { this.use(plugin); }); } return this; }, /** * 销毁 */ destroy() { this.dismiss(); if (this.plugins && typeof this.plugins.destroy === 'function') { this.plugins.destroy(); } if (this.themes) { if (typeof this.themes.clearThemeListeners === 'function') { this.themes.clearThemeListeners(); } if (typeof this.themes.unwatchSystemTheme === 'function') { this.themes.unwatchSystemTheme(); } } if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') { this.i18n.clearLocaleListeners(); } if (this.animations && typeof this.animations.cancelAll === 'function') { this.animations.cancelAll(); } if (typeof document !== 'undefined') { const containers = document.querySelectorAll('.met-container'); containers.forEach((c) => c.parentNode && c.parentNode.removeChild(c)); const style = document.getElementById('metona-toast-styles'); if (style) style.parentNode.removeChild(style); } this._toasts.clear(); }, /** * 获取状态 */ getStatus() { return { version: VERSION, toasts: this._toasts.size, theme: this.themes?.getCurrentTheme?.() || 'auto', locale: this.i18n?.getCurrentLocale?.() || 'zh-CN', plugins: this.plugins?.getNames?.() || [], animations: this.animations?.getActiveCount?.() || 0, }; }, /** * 获取配置 */ getConfig() { return { ...this._config }; }, /** * 更新配置 */ updateConfig(config) { if (config && typeof config === 'object') { this._config = { ...this._config, ...config }; } return this; }, /** * 重置配置 */ resetConfig() { this._config = { ...DEFAULTS }; return this; }, /** * 获取Toast列表 */ getToasts() { return Array.from(this._toasts.values()); }, /** * 检查是否有Toast */ hasToasts() { return this._toasts.size > 0; }, /** * 获取Toast */ getToast(id) { if (!id) return null; return this._toasts.get(id) || null; }, /** * 关闭所有Toast */ closeAll() { this._toasts.forEach((t) => t.close()); }, /** * 清除所有Toast */ clearAll() { this._toasts.forEach((t) => t.close()); }, /** * 暂停所有Toast */ pauseAll() { this._toasts.forEach((t) => t._pause()); }, /** * 恢复所有Toast */ resumeAll() { this._toasts.forEach((t) => t._resume()); }, /** * 更新所有Toast */ updateAll(partial) { if (partial && typeof partial === 'object') { this._toasts.forEach((t) => t.update(partial)); } }, /** * 查找Toast */ findToasts(predicate) { if (typeof predicate !== 'function') return []; return Array.from(this._toasts.values()).filter(predicate); }, /** * 按类型查找Toast */ findByType(type) { return this.findToasts((t) => t.type === type); }, /** * 按位置查找Toast */ findByPosition(position) { return this.findToasts((t) => t.config.position === position); }, }; // 初始化主题和国际化 if (typeof themeUtils.initTheme === 'function') { themeUtils.initTheme(); } if (typeof i18nUtils.initI18n === 'function') { i18nUtils.initI18n(); } // 浏览器环境全局注册 if (typeof window !== 'undefined') { window.MeToast = enhancedMeToast; window.Met = enhancedMeToast; // Notification API — 页面不可见时自动发送系统通知 enhancedMeToast._notify = (toast) => { if (typeof Notification === 'undefined') return; if (Notification.permission === 'denied') return; if (!document.hidden) return; if (Notification.permission === 'default') { Notification.requestPermission().then(p => { if (p === 'granted') sendNotification(toast); }); return; } sendNotification(toast); }; const sendNotification = (toast) => { try { new Notification(toast.title || toast.type, { body: toast.message }); } catch (_) {} }; Toast.on('afterShow', (toast) => { if (toast.config.notifyWhenHidden) enhancedMeToast._notify(toast); }); } export { enhancedMeToast as MeToast, enhancedMeToast as Met, Toast, VERSION, enhancedMeToast as default, enhancedMeToast as meToast }; //# sourceMappingURL=metona-toast.esm.js.map