263 lines
12 KiB
JavaScript
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();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|