wechat-robot/public/js/components.js
李寻欢 bd203141d8
All checks were successful
BuildImage / build-image (push) Successful in 1m55s
🎨 逻辑优化
2025-04-03 14:35:00 +08:00

263 lines
12 KiB
JavaScript

/**
* 共享UI组件库
*/
// 显示通知消息
function showNotification(message, type = 'success', duration = 3000) {
const notificationContainer = document.getElementById('notification-container') || createNotificationContainer();
const notification = document.createElement('div');
notification.className = `notification notification-${type} flex items-center p-4 mb-3 rounded-md shadow-md transform transition-all duration-300 ease-in-out translate-x-full`;
// 设置图标
let icon = '';
switch(type) {
case 'success':
icon = '<svg class="w-5 h-5 mr-3 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>';
break;
case 'warning':
icon = '<svg class="w-5 h-5 mr-3 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v4a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>';
break;
case 'error':
icon = '<svg class="w-5 h-5 mr-3 text-red-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>';
break;
default:
icon = '<svg class="w-5 h-5 mr-3 text-blue-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>';
}
notification.innerHTML = `
${icon}
<span class="flex-grow">${message}</span>
<button class="ml-3 text-gray-400 hover:text-gray-600 focus:outline-none">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</button>
`;
notificationContainer.appendChild(notification);
// 关闭按钮功能
notification.querySelector('button').addEventListener('click', () => {
closeNotification(notification);
});
// 显示动画
setTimeout(() => {
notification.classList.remove('translate-x-full');
notification.classList.add('translate-x-0');
}, 10);
// 自动关闭
if (duration) {
setTimeout(() => {
closeNotification(notification);
}, duration);
}
return notification;
}
function closeNotification(notification) {
notification.classList.remove('translate-x-0');
notification.classList.add('translate-x-full');
setTimeout(() => {
notification.remove();
}, 300);
}
function createNotificationContainer() {
const container = document.createElement('div');
container.id = 'notification-container';
container.className = 'fixed top-4 right-4 z-50 w-80 flex flex-col items-end';
document.body.appendChild(container);
return container;
}
// 确认对话框
function confirmDialog(message, options = {}) {
return new Promise((resolve) => {
const defaults = {
title: '确认操作',
confirmText: '确认',
cancelText: '取消',
type: 'question' // question, warning, danger
};
const settings = {...defaults, ...options};
// 创建遮罩
const overlay = document.createElement('div');
overlay.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 transition-opacity duration-300 opacity-0';
overlay.style.backdropFilter = 'blur(2px)';
// 选择颜色和图标
let iconClass = 'text-indigo-500';
let buttonClass = 'bg-indigo-600 hover:bg-indigo-700';
let icon = '<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"></path></svg>';
if (settings.type === 'warning') {
iconClass = 'text-amber-500';
buttonClass = 'bg-amber-600 hover:bg-amber-700';
icon = '<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v4a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>';
} else if (settings.type === 'danger') {
iconClass = 'text-red-500';
buttonClass = 'bg-red-600 hover:bg-red-700';
icon = '<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>';
}
// 创建对话框
overlay.innerHTML = `
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl overflow-hidden max-w-md w-full mx-4 transform transition-all duration-300 scale-95 opacity-0">
<div class="p-5 flex items-start space-x-4">
<div class="flex-shrink-0 ${iconClass} p-1">
${icon}
</div>
<div class="flex-grow">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">${settings.title}</h3>
<p class="text-gray-500 dark:text-gray-300">${message}</p>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700/50 px-5 py-4 flex justify-end space-x-3">
<button id="cancel-btn" class="px-4 py-2 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-200 rounded-lg border border-gray-300 dark:border-gray-500 text-sm font-medium shadow-sm transition-all">
${settings.cancelText}
</button>
<button id="confirm-btn" class="${buttonClass} text-white px-4 py-2 rounded-lg shadow-sm text-sm font-medium transition-all">
${settings.confirmText}
</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// 显示动画
setTimeout(() => {
overlay.classList.remove('opacity-0');
overlay.classList.add('opacity-100');
const dialog = overlay.querySelector('div.bg-white');
dialog.classList.remove('scale-95', 'opacity-0');
dialog.classList.add('scale-100', 'opacity-100');
}, 10);
// 事件处理
const closeDialog = (result) => {
// 关闭动画
overlay.classList.remove('opacity-100');
overlay.classList.add('opacity-0');
const dialog = overlay.querySelector('div.bg-white');
dialog.classList.remove('scale-100', 'opacity-100');
dialog.classList.add('scale-95', 'opacity-0');
// 等待动画完成后移除
setTimeout(() => {
overlay.remove();
resolve(result);
}, 300);
};
// 处理键盘事件
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
closeDialog(false);
document.removeEventListener('keydown', handleKeyDown);
} else if (e.key === 'Enter') {
closeDialog(true);
document.removeEventListener('keydown', handleKeyDown);
}
};
document.addEventListener('keydown', handleKeyDown);
// 绑定按钮事件
overlay.querySelector('#confirm-btn').addEventListener('click', () => {
closeDialog(true);
document.removeEventListener('keydown', handleKeyDown);
});
overlay.querySelector('#cancel-btn').addEventListener('click', () => {
closeDialog(false);
document.removeEventListener('keydown', handleKeyDown);
});
// 点击外部区域关闭
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeDialog(false);
document.removeEventListener('keydown', handleKeyDown);
}
});
});
}
// 表格排序
function initTableSort() {
document.querySelectorAll('table.sortable').forEach(table => {
table.querySelectorAll('th.sortable').forEach(header => {
header.addEventListener('click', () => {
const index = Array.from(header.parentNode.children).indexOf(header);
const isNumeric = header.classList.contains('sort-numeric');
const isDate = header.classList.contains('sort-date');
const isAsc = !header.classList.contains('sort-asc');
// 清除所有表头的排序状态
table.querySelectorAll('th.sortable').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc');
});
// 设置当前表头的排序状态
header.classList.add(isAsc ? 'sort-asc' : 'sort-desc');
// 获取并排序行
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
let aValue = a.children[index].textContent.trim();
let bValue = b.children[index].textContent.trim();
if (isNumeric) {
aValue = parseFloat(aValue);
bValue = parseFloat(bValue);
return isAsc ? aValue - bValue : bValue - aValue;
} else if (isDate) {
aValue = new Date(aValue);
bValue = new Date(bValue);
return isAsc ? aValue - bValue : bValue - aValue;
} else {
return isAsc ?
aValue.localeCompare(bValue) :
bValue.localeCompare(aValue);
}
});
// 移除现有行并按排序顺序重新添加
rows.forEach(row => tbody.appendChild(row));
});
});
});
}
// 初始化时绑定组件行为
document.addEventListener('DOMContentLoaded', function() {
// 初始化排序表格
initTableSort();
// 绑定确认删除按钮
document.querySelectorAll('[data-confirm]').forEach(button => {
button.addEventListener('click', async (e) => {
e.preventDefault();
const message = button.dataset.confirm || '确定要执行此操作吗?';
const type = button.dataset.confirmType || 'warning';
const confirmed = await confirmDialog(message, { type });
if (confirmed) {
if (button.tagName === 'A') {
window.location.href = button.href;
} else if (button.form) {
button.form.submit();
}
}
});
});
});