attractive-alert
Version:
一个美观易用的弹框库,提供丰富的图标和动画效果
901 lines (780 loc) • 30.7 kB
JavaScript
/**
* Copyright 2025 王小玗
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Attractive Alert库
* 一个美观易用的弹框库
* 可拓展性高
*/
class Atal {
constructor(executor) {
this.state = 'pending';
this.value = null;
this.okCallbacks = [];
this.cancelCallbacks = [];
this.customCallbacks = [];
this.createdCallback = () => {};
this.closeCallback = () => {};
const executeCallbacks = (callbacks, value = null) => {
const copies = callbacks.slice();
callbacks.length = 0;
copies.forEach(cb => {
try {
value !== null ? cb(value) : cb();
} catch (error) {
console.error('Atal callback error:', error);
}
});
};
const resolve = (type, value = null) => {
if (this.state !== 'pending') return;
this.state = type;
this.value = value;
if (type === 'ok') {
executeCallbacks(this.okCallbacks);
} else if (type === 'cancel') {
executeCallbacks(this.cancelCallbacks);
} else if (type === 'custom') {
executeCallbacks(this.customCallbacks, value);
}
try {
this.closeCallback();
} catch (error) {
console.error('Atal close callback error:', error);
}
};
const ok = () => resolve('ok');
const cancel = () => resolve('cancel');
const custom = (value) => resolve('custom', value);
const close = (fn) => {
this.closeCallback = fn;
};
const created = (value) => {
this.createdCallback(value);
};
try {
executor(ok, cancel, custom, created, close);
} catch (error) {
cancel();
}
this._executeCallbacks = executeCallbacks;
}
ok(callback) {
if (this.state === 'ok') {
callback();
} else if (this.state === 'pending') {
this.okCallbacks.push(callback);
}
return this;
}
cancel(callback) {
if (this.state === 'cancel') {
callback();
} else if (this.state === 'pending') {
this.cancelCallbacks.push(callback);
}
return this;
}
custom(callback) {
if (this.state === 'custom') {
callback(this.value);
} else if (this.state === 'pending') {
this.customCallbacks.push(callback);
}
return this;
}
created(callback) {
this.createdCallback = callback;
return this;
}
close(type = 'cancel', value = null) {
if (this.state !== 'pending') return this;
try {
this.closeCallback();
} catch (error) {
console.error('Atal close callback error:', error);
}
if (type === 'ok') {
this.state = 'ok';
this._executeCallbacks(this.okCallbacks);
} else if (type === 'cancel') {
this.state = 'cancel';
this._executeCallbacks(this.cancelCallbacks);
} else if (type === 'custom') {
this.state = 'custom';
this.value = value;
this._executeCallbacks(this.customCallbacks, value);
}
return this;
}
static alert() {
return new Atal((ok, cancel, custom, created, close) => {
let options;
if (arguments.length === 1 && typeof arguments[0] === 'object') {
options = { ...Atal.defaults, ...arguments[0] };
} else {
options = { ...Atal.defaults };
if (arguments.length >= 1) options.title = arguments[0];
if (arguments.length >= 2) options.content = arguments[1];
if (arguments.length >= 3) options.icon = arguments[2];
}
options.buttons = Atal.processButtons(options.buttons);
Atal.addStyles();
const alertElement = Atal.createAlertHTML(options);
document.body.appendChild(alertElement);
const overlayElement = alertElement.querySelector('.atal-overlay');
// 如果不启用动画,移除所有动画样式
if (!options.animateIcon) {
const icon = overlayElement.querySelector('.atal-icon');
if (icon) {
const paths = icon.querySelectorAll('*');
paths.forEach(path => {
path.style.animation = 'none';
path.style.strokeDasharray = 'none';
path.style.strokeDashoffset = '0';
if (path.classList.contains('dot')) {
path.style.opacity = '1';
}
});
}
}
// 设置定时器
if (options.timer) {
setTimeout(() => {
Atal.closeAlert(alertElement);
ok();
}, options.timer);
}
// 绑定按钮事件
const confirmBtn = overlayElement.querySelector('.atal-confirm');
if (confirmBtn) {
confirmBtn.addEventListener('click', () => {
Atal.closeAlert(alertElement);
ok();
});
}
const cancelBtn = overlayElement.querySelector('.atal-cancel');
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
Atal.closeAlert(alertElement);
cancel();
});
}
// 绑定自定义按钮事件
const customBtns = overlayElement.querySelectorAll('.atal-custom');
customBtns.forEach((btn, index) => {
btn.addEventListener('click', () => {
Atal.closeAlert(alertElement);
// 传递自定义按钮的索引和配置信息
custom({
index: index,
button: options.buttons.custom[index]
});
});
});
// 点击遮罩层关闭
overlayElement.addEventListener('click', (e) => {
if (e.target === overlayElement) {
Atal.closeAlert(alertElement);
cancel();
}
});
close(() => Atal.closeAlert(alertElement));
Promise.resolve().then(() => {
created(alertElement);
});
});
}
// 静态方法 - 处理按钮配置
static processButtons(buttonsConfig) {
const result = {
confirm: null,
cancel: null,
custom: []
};
// 处理确认按钮
if (buttonsConfig.confirm !== undefined && buttonsConfig.confirm !== null) {
if (buttonsConfig.confirm === true) {
result.confirm = Atal.defaultButtons.confirm;
} else if (typeof buttonsConfig.confirm === 'string' || typeof buttonsConfig.confirm === 'object') {
result.confirm = buttonsConfig.confirm;
}
}
// 处理取消按钮
if (buttonsConfig.cancel !== undefined && buttonsConfig.cancel !== null) {
if (buttonsConfig.cancel === true) {
result.cancel = Atal.defaultButtons.cancel;
} else if (typeof buttonsConfig.cancel === 'string' || typeof buttonsConfig.cancel === 'object') {
result.cancel = buttonsConfig.cancel;
}
}
// 处理自定义按钮
if (Array.isArray(buttonsConfig.custom)) {
result.custom = buttonsConfig.custom.filter(btn =>
btn !== null && btn !== undefined && btn !== false
);
}
return result;
}
// 静态方法 - 设置原生alert
static native() {
window.alert = Atal.alert;
}
// 静态方法 - 关闭弹窗
static closeAlert(element) {
if (element && element.parentNode) {
element.parentNode.removeChild(element);
}
}
// 静态方法 - 添加样式
static addStyles() {
if (document.getElementById('atal-styles')) return;
const style = document.createElement('style');
style.id = 'atal-styles';
style.textContent = Atal.styles;
document.head.appendChild(style);
}
// 静态方法 - 创建弹窗HTML
static createAlertHTML(options) {
let iconHTML;
let iconColor;
if (typeof options.icon === 'object') {
iconHTML = options.icon ? Atal.icons[options.icon.type](options.icon.color) || '' : '';
iconColor = options.icon.color;
} else if (typeof options.icon === 'string') {
iconHTML = options.icon ? Atal.icons[options.icon](Atal.getIconColor(options.icon)) || '' : '';
iconColor = Atal.getIconColor(options.icon);
} else {
iconHTML = '';
iconColor = Atal.getIconColor('');
}
let content = '';
if (options.content instanceof HTMLElement) {
content = '<div id="atal-custom-content"></div>';
} else if (typeof options.content === 'string') {
content = options.html ? options.content : `<p>${Atal.escapeHTML(options.content)}</p>`;
}
// 生成自定义按钮HTML
let customButtonsHTML = '';
if (options.buttons.custom && options.buttons.custom.length > 0) {
customButtonsHTML = options.buttons.custom.map((button, index) => {
return `<button class="atal-button atal-custom" data-index="${index}" style="--icon-color: ${button.color || iconColor}">${button.text}</button>`;
}).join('');
}
let confirmButtonHTML = '';
if (options.buttons.confirm) {
if (typeof options.buttons.confirm === "object") {
confirmButtonHTML = `<button class="atal-button atal-confirm" style="--icon-color: ${options.buttons.confirm.color || iconColor}">${options.buttons.confirm.text}</button>`;
} else if (typeof options.buttons.confirm === "string") {
confirmButtonHTML = `<button class="atal-button atal-confirm" style="--icon-color: ${iconColor}">${options.buttons.confirm}</button>`;
}
}
let cancelButtonHTML = '';
if (options.buttons.cancel) {
if (typeof options.buttons.cancel === "object") {
cancelButtonHTML = `<button class="atal-button atal-cancel" style="--icon-color: ${options.buttons.cancel.color || iconColor}">${options.buttons.cancel.text}</button>`;
} else if (typeof options.buttons.cancel === "string") {
cancelButtonHTML = `<button class="atal-button atal-cancel" style="--icon-color: ${iconColor}">${options.buttons.cancel}</button>`;
}
}
const alertElement = document.createElement('div');
alertElement.innerHTML = `
<div class="atal-overlay">
<div class="atal-container">
<div class="atal-header">
${iconHTML}
<h3 class="atal-title">${options.title}</h3>
</div>
<div class="atal-content" id="atal-content">
${content}
</div>
<div class="atal-footer">
${cancelButtonHTML}
${customButtonsHTML}
${confirmButtonHTML}
</div>
</div>
</div>
`;
// 处理HTML元素内容
if (options.content instanceof HTMLElement) {
const contentContainer = alertElement.querySelector('#atal-custom-content');
if (contentContainer) {
contentContainer.appendChild(options.content);
}
}
return alertElement;
}
// 静态方法 - 获取图标颜色
static getIconColor(icon) {
const colors = {
success: '#4CAF50', // 绿色
error: '#F44336', // 红色
warning: '#FF9800', // 橙色
info: '#2196F3', // 蓝色
question: '#9C27B0', // 紫色
heart: '#E91E63', // 粉色
star: '#FFC107', // 黄色
download: '#2196F3', // 蓝色
upload: '#2196F3', // 蓝色
key: '#FF9800' // 橙色
};
return colors[icon] || '#2196F3'; // 默认蓝色
}
// 静态方法 - HTML转义
static escapeHTML(str) {
return str.replace(/[&<>"']/g, function (match) {
switch (match) {
case '&': return '&';
case '<': return '<';
case '>': return '>';
case '"': return '"';
case "'": return ''';
default: return match;
}
});
}
static at(defaultOptions) {
// 创建并返回一个函数,该函数可以使用预设配置
const atAtal = function() {
let options;
// 处理不同参数形式
if (arguments.length === 1 && typeof arguments[0] === 'object') {
// 单个对象参数
options = { ...defaultOptions, ...arguments[0] };
} else {
// 多个参数形式
options = { ...defaultOptions };
if (arguments.length >= 1) options.title = arguments[0];
if (arguments.length >= 2) options.content = arguments[1];
if (arguments.length >= 3) options.icon = arguments[2];
}
// 调用原始的Atal.alert方法
return Atal.alert(options);
};
// 复制静态方法和属性
Object.keys(Atal).forEach(key => {
if (typeof Atal[key] === 'function' && key !== 'at') {
atAtal[key] = Atal[key];
}
});
// 添加一个方法来更新默认配置
atAtal.updateDefaults = function(newDefaults) {
Object.assign(defaultOptions, newDefaults);
};
// 添加一个方法来获取当前配置
atAtal.getDefaults = function() {
return { ...defaultOptions };
};
// 添加at方法,允许链式创建更深层次at
atAtal.at = function(newDefaults) {
return Atal.at({ ...defaultOptions, ...newDefaults });
};
return atAtal;
}
static setDefaults(options) {
if (options) {
Atal.defaults = {...Atal.defaults, ...options};
} else {
return Atal.defaults;
}
}
static setDefaultButtons(confirm, cancel) {
if (confirm === undefined && cancel === undefined) return Atal.defaultButtons;
if (confirm !== undefined) Atal.defaultButtons.confirm = confirm;
if (cancel !== undefined) Atal.defaultButtons.cancel = cancel;
}
}
// 静态属性 - 默认配置
Atal.defaults = {
title: '',
content: '',
icon: '',
buttons: {
confirm: true, // 使用布尔值表示使用默认按钮
cancel: false, // 使用布尔值表示使用默认按钮
custom: [] // 自定义按钮数组
},
timer: null,
animateIcon: true,
html: false
};
// 静态属性 - 默认按钮配置
Atal.defaultButtons = {
confirm: 'OK',
cancel: 'Cancel'
};
// 静态属性 - 图标
Atal.icons = {
success: (color) => `
<svg class="atal-icon success-icon" viewBox="0 0 64 64" stroke="${color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path class="circle" d="M32,4A28,28,0,1,1,4,32,28,28,0,0,1,32,4Z" style="stroke-dasharray: 180; stroke-dashoffset: 180;"/>
<path class="check" d="M20,32l10,10,14-20" style="stroke-dasharray: 44; stroke-dashoffset: 44;"/>
</svg>
`,
error: (color) => `
<svg class="atal-icon error-icon" viewBox="0 0 64 64" stroke="${color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path class="circle" d="M32,4A28,28,0,1,1,4,32,28,28,0,0,1,32,4Z" style="stroke-dasharray: 180; stroke-dashoffset: 180;"/>
<path class="line line1" d="M24,24L40,40" style="stroke-dasharray: 22.6; stroke-dashoffset: 22.6;"/>
<path class="line line2" d="M40,24L24,40" style="stroke-dasharray: 22.6; stroke-dashoffset: 22.6;"/>
</svg>
`,
warning: (color) => `
<svg class="atal-icon warning-icon" viewBox="0 0 64 64" stroke="${color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path class="triangle" d="M32,10L4,54H60L32,10Z" style="stroke-dasharray: 160; stroke-dashoffset: 160;"/>
<path class="exclamation" d="M32,26V40M32,44V44" style="stroke-dasharray: 22; stroke-dashoffset: 22;"/>
<path class="dot" d="M32,46a1,1,0,1,1-1,1A1,1,0,0,1,32,46Z" fill="${color}" style="opacity: 0;"/>
</svg>
`,
info: (color) => `
<svg class="atal-icon info-icon" viewBox="0 0 64 64" stroke="${color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path class="circle" d="M32,4A28,28,0,1,1,4,32,28,28,0,0,1,32,4Z" style="stroke-dasharray: 180; stroke-dashoffset: 180;"/>
<path class="dot" d="M32,16a2,2,0,1,1-2,2A2,2,0,0,1,32,16Z" style="opacity: 0;"/>
<path class="line" d="M32,28V44" style="stroke-dasharray: 20; stroke-dashoffset: 20; stroke-width: 4;"/>
</svg>
`,
question: (color) => `
<svg class="atal-icon question-icon" viewBox="0 0 64 64" stroke="${color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path class="circle" d="M32,4A28,28,0,1,1,4,32,28,28,0,0,1,32,4Z" style="stroke-dasharray: 180; stroke-dashoffset: 180;"/>
<path class="question-mark" d="M24,24c0-4,4-8,8-8,6,0,8,4,8,8,0,4-4,6-6,8-2,2-2,6-2,6" style="stroke-dasharray: 60; stroke-dashoffset: 60;"/>
<path class="dot" d="M32,48a2,2,0,1,1-2,2A2,2,0,0,1,32,48Z" fill="${color}" style="opacity: 0;"/>
</svg>
`,
heart: (color) => `
<svg class="atal-icon heart-icon" viewBox="0 0 64 64" stroke="${color}" stroke-width="3" fill="none">
<path class="heart" d="M32,14c5.5-11,22-8.5,22,5.5,0,16.5-22,27.5-22,27.5S10,36,10,19.5C10,6.5,26.5,3,32,14Z" style="stroke-dasharray: 142; stroke-dashoffset: 142;"/>
</svg>
`,
star: (color) => `
<svg class="atal-icon star-icon" viewBox="0 0 64 64" stroke="${color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path class="star" d="M32,8l8,16,18,2-12,14,2,18-16-10-16,10,2-18L6,26l18-2L32,8Z" style="stroke-dasharray: 181; stroke-dashoffset: 181;"/>
</svg>
`,
download: (color) => `
<svg class="atal-icon download-icon" viewBox="0 0 64 64" stroke="${color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path class="circle" d="M32,4A28,28,0,1,1,4,32,28,28,0,0,1,32,4Z" style="stroke-dasharray: 180; stroke-dashoffset: 180;"/>
<path class="arrow" d="M32,20V44M32,44L24,36M32,44L40,36" style="stroke-dasharray: 48; stroke-dashoffset: 48;"/>
<path class="line" d="M20,52H44" style="stroke-dasharray: 24; stroke-dashoffset: 24;"/>
</svg>
`,
upload: (color) => `
<svg class="atal-icon upload-icon" viewBox="0 0 64 64" stroke="${color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path class="circle" d="M32,4A28,28,0,1,1,4,32,28,28,0,0,1,32,4Z" style="stroke-dasharray: 180; stroke-dashoffset: 180;"/>
<path class="arrow" d="M32,44V20M32,20L24,28M32,20L40,28" style="stroke-dasharray: 48; stroke-dashoffset: 48;"/>
<path class="line" d="M20,52H44" style="stroke-dasharray: 24; stroke-dashoffset: 24;"/>
</svg>
`,
key: (color) => `
<svg class="atal-icon key-icon" viewBox="0 0 64 64" stroke="${color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path class="circle" d="M32,4A28,28,0,1,1,4,32,28,28,0,0,1,32,4Z" style="stroke-dasharray: 180; stroke-dashoffset: 180;"/>
<path class="key-handle" d="M44,32a6,6,0,0,1-6,6,6,6,0,0,1-6-6,6,6,0,0,1,6-6A6,6,0,0,1,44,32Z" style="stroke-dasharray: 38; stroke-dashoffset: 38;"/>
<path class="key-teeth" d="M44,32H52" style="stroke-dasharray: 8; stroke-dashoffset: 8;"/>
<path class="key-shaft" d="M32,32H40" style="stroke-dasharray: 8; stroke-dashoffset: 8;"/>
</svg>
`
};
// 静态属性 - 样式
Atal.styles = `
.atal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
.atal-container {
background-color: white;
border-radius: 16px;
width: 90%;
max-width: 420px;
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.15);
overflow: hidden;
animation: atalFadeIn 0.3s ease-out;
}
.atal-header {
padding: 30px 25px 15px;
text-align: center;
}
.atal-icon {
width: 80px;
height: 80px;
margin-bottom: 20px;
transform-origin: center;
}
.atal-title {
margin: 0;
font-size: 1.75rem;
color: #333;
font-weight: 600;
letter-spacing: 0.5px;
}
.atal-content {
padding: 15px 30px;
text-align: center;
color: #555;
font-size: 1.1rem;
line-height: 1.6;
}
.atal-footer {
display: flex;
justify-content: center;
padding: 25px;
gap: 15px;
flex-wrap: wrap;
}
.atal-button {
padding: 12px 28px;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: 500;
letter-spacing: 0.5px;
min-width: 80px;
}
.atal-confirm {
background-color: var(--icon-color);
color: white;
}
.atal-confirm:hover {
background-color: var(--icon-color);
filter: brightness(0.9);
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.12);
}
.atal-cancel {
background-color: #f5f5f5;
color: #666;
border: 1px solid #ddd;
}
.atal-cancel:hover {
background-color: #e5e5e5;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
}
.atal-custom {
background-color: var(--icon-color, #6c757d);
color: white;
}
.atal-custom:hover {
background-color: var(--icon-color, #5a6268);
filter: brightness(0.9);
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.12);
}
@keyframes atalFadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
/* 图标动画 */
/* 成功图标动画 */
.success-icon .circle {
animation: circle-anim 0.4s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.success-icon .check {
animation: check-anim 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.5s forwards;
}
/* 错误图标动画 */
.error-icon .circle {
animation: circle-anim 0.4s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.error-icon .line {
animation: error-line-anim 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.5s forwards;
}
.error-icon .line2 {
animation-delay: 0.8s;
}
/* 警告图标动画 */
.warning-icon .triangle {
animation: triangle-anim 0.4s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.warning-icon .exclamation {
animation: exclamation-anim 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.5s forwards;
}
.warning-icon .dot {
animation: dot-anim 0.3s ease-out 0.8s forwards;
}
/* 信息图标动画 */
.info-icon .circle {
animation: circle-anim 0.4s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.info-icon .line {
animation: line-anim 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.5s forwards;
}
.info-icon .dot {
animation: dot-anim 0.3s ease-out 0.8s forwards;
}
/* 问题图标动画 */
.question-icon .circle {
animation: circle-anim 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.question-icon .question-mark {
animation: question-anim 0.6s cubic-bezier(0.65, 0, 0.45, 1) 0.6s forwards;
}
.question-icon .dot {
animation: dot-anim 0.3s ease-out 1.2s forwards;
}
/* 心形图标动画 */
.heart-icon .heart {
animation: heart-anim 0.5s cubic-bezier(0.65, 0, 0.45, 1) forwards;
transform-origin: center;
}
/* 星星图标动画 */
.star-icon .star {
animation: star-anim 0.8s cubic-bezier(0.65, 0, 0.45, 1) forwards;
transform-origin: center;
}
/* 动画关键帧 */
@keyframes circle-anim {
0% { stroke-dashoffset: 180; }
100% { stroke-dashoffset: 0; }
}
@keyframes check-anim {
0% { stroke-dashoffset: 44; }
100% { stroke-dashoffset: 0; }
}
@keyframes error-line-anim {
0% { stroke-dashoffset: 22.6; }
100% { stroke-dashoffset: 0; }
}
@keyframes triangle-anim {
0% { stroke-dashoffset: 150; }
100% { stroke-dashoffset: 0; }
}
@keyframes exclamation-anim {
0% { stroke-dashoffset: 22; }
100% { stroke-dashoffset: 0; }
}
@keyframes line-anim {
0% { stroke-dashoffset: 22; }
100% { stroke-dashoffset: 0; }
}
@keyframes dot-anim {
0% { opacity: 0; transform: scale(0.3); }
50% { opacity: 1; transform: scale(1.2); }
100% { opacity: 1; transform: scale(1); }
}
@keyframes question-anim {
0% { stroke-dashoffset: 60; }
100% { stroke-dashoffset: 0; }
}
@keyframes heart-anim {
0% {
stroke-dashoffset: 120;
transform: scale(0.8);
}
50% {
stroke-dashoffset: 60;
transform: scale(1.1);
}
100% {
stroke-dashoffset: 0;
transform: scale(1);
}
}
@keyframes star-anim {
0% {
stroke-dashoffset: 150;
transform: scale(0.8) rotate(-30deg);
opacity: 0;
}
50% {
stroke-dashoffset: 75;
transform: scale(1.1) rotate(10deg);
opacity: 1;
}
100% {
stroke-dashoffset: 0;
transform: scale(1) rotate(0);
opacity: 1;
}
}
/* 下载图标动画 */
.download-icon .circle {
animation: circle-anim 0.4s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.download-icon .arrow {
animation: download-arrow-anim 0.4s cubic-bezier(0.65, 0, 0.45, 1) 0.4s forwards;
}
.download-icon .line {
animation: download-line-anim 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}
/* 上传图标动画 */
.upload-icon .circle {
animation: circle-anim 0.4s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.upload-icon .arrow {
animation: upload-arrow-anim 0.4s cubic-bezier(0.65, 0, 0.45, 1) 0.4s forwards;
}
.upload-icon .line {
animation: upload-line-anim 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}
/* 钥匙图标动画 */
.key-icon .circle {
animation: circle-anim 0.4s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.key-icon .key-handle {
animation: key-handle-anim 0.4s cubic-bezier(0.65, 0, 0.45, 1) 0.4s forwards;
}
.key-icon .key-teeth {
animation: key-teeth-anim 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}
.key-icon .key-shaft {
animation: key-shaft-anim 0.3s cubic-bezier(0.65, 0, 0.45, 1) 1.1s forwards;
}
/* 新增动画关键帧 */
@keyframes download-arrow-anim {
0% { stroke-dashoffset: 48; }
100% { stroke-dashoffset: 0; }
}
@keyframes download-line-anim {
0% { stroke-dashoffset: 24; }
100% { stroke-dashoffset: 0; }
}
@keyframes upload-arrow-anim {
0% { stroke-dashoffset: 48; }
100% { stroke-dashoffset: 0; }
}
@keyframes upload-line-anim {
0% { stroke-dashoffset: 24; }
100% { stroke-dashoffset: 0; }
}
@keyframes key-handle-anim {
0% {
stroke-dashoffset: 38;
transform: scale(0.8);
}
100% {
stroke-dashoffset: 0;
transform: scale(1);
}
}
@keyframes key-teeth-anim {
0% { stroke-dashoffset: 8; }
100% { stroke-dashoffset: 0; }
}
@keyframes key-shaft-anim {
0% { stroke-dashoffset: 8; }
100% { stroke-dashoffset: 0; }
}
`;
window.Atal = Atal;