/** * -------------------------------------------------------------------- * @description 通用组件。 * @author https://t.me/CCfork * @copyright Copyright (c) 2025, https://t.me/CCfork * -------------------------------------------------------------------- */ /** * 切换标签页显示 * @param {Event} evt - 点击事件对象。 * @param {string} tabId - 要激活的目标标签页内容的ID。 */ function openTab(evt, tabId) { const clickedButton = evt.currentTarget; const buttonContainer = clickedButton.parentElement; if (!buttonContainer) return; const buttons = buttonContainer.querySelectorAll(`.${clickedButton.classList[0]}`); buttons.forEach(btn => btn.classList.remove('active')); const contentParent = buttonContainer.parentElement; const contentClass = clickedButton.classList[0].replace('-btn', '-content'); if (contentParent) { contentParent.querySelectorAll(`:scope > .${contentClass}`).forEach(content => { content.style.display = 'none'; content.classList.remove('active'); }); } clickedButton.classList.add('active'); const tabElement = document.getElementById(tabId); if (tabElement) { tabElement.style.display = 'block'; tabElement.classList.add('active'); const nestedActiveButton = tabElement.querySelector('.tab-btn.active'); if (nestedActiveButton) { const onclickAttr = nestedActiveButton.getAttribute('onclick'); if (onclickAttr) { const nestedContentId = onclickAttr.match(/'([^']*)'/)[1]; const nestedContentElement = document.getElementById(nestedContentId); if (nestedContentElement) { nestedContentElement.style.display = 'block'; nestedContentElement.classList.add('active'); } } } document.querySelector('.tab-btn.active')?.click(); } } /** * 平滑地将页面滚动到顶部。 */ function scrollToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); } /** * 显示一个短暂的通知消息 (Toast)。 * @param {string} message - 要显示的消息内容。 * @param {string} [type=''] -通知的类型,可选值: 'success', 'error', 'info'。 */ function showToast(message, type = '') { let toastContainer = document.querySelector('.toast-container'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.className = 'toast-container'; document.body.appendChild(toastContainer); } const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; toastContainer.appendChild(toast); setTimeout(() => toast.classList.add('show'), 10); setTimeout(() => { toast.classList.remove('show'); toast.addEventListener('transitionend', () => toast.remove()); }, 3000); } /** * 通用模态弹窗类 (Modal Class) */ class Modal { /** * @param {object} options - 弹窗配置 * @param {string} options.id - 弹窗的唯一ID * @param {string} options.title - 弹窗的标题 * @param {string|HTMLElement} [options.content=''] - 弹窗主体内容 * @param {string|HTMLElement} [options.footer=''] - 弹窗页脚内容 * @param {function} [options.onClose=null] - 弹窗关闭时的回调函数 * @param {function} [options.onOpen=null] - 弹窗打开时的回调函数 */ constructor(options) { this.options = Object.assign({ id: null, title: '', content: '', footer: '', onClose: null, onOpen: null }, options); this.modalElement = null; this._createModal(); this._attachEventListeners(); } /** * 内部方法:创建弹窗的HTML结构 * @private */ _createModal() { if (!this.options.id) { console.error('Modal ID is required.'); return; } this.modalElement = document.createElement('div'); this.modalElement.className = 'modal-container'; this.modalElement.id = this.options.id; this.modalElement.style.display = 'none'; const modalHTML = ` `; this.modalElement.innerHTML = modalHTML; this.bodyElement = this.modalElement.querySelector('.modal-body'); this.setBody(this.options.content); if (this.options.footer) { this.footerElement = this.modalElement.querySelector('.modal-footer'); this.setFooter(this.options.footer); } document.body.appendChild(this.modalElement); } /** * 内部方法:为关闭按钮和遮罩层绑定关闭事件 * @private */ _attachEventListeners() { this.modalElement.querySelector('.modal-close-btn').addEventListener('click', () => this.close()); this.modalElement.querySelector('.modal-overlay').addEventListener('click', () => this.close()); } /** * 公共方法:打开弹窗 */ open() { this.modalElement.style.display = 'flex'; setTimeout(() => this.modalElement.classList.add('active'), 10); if (typeof this.options.onOpen === 'function') { this.options.onOpen(); } } /** * 公共方法:关闭弹窗 (已修复) */ close() { if (this._isClosing) return; this._isClosing = true; this.modalElement.classList.remove('active'); const handleTransitionEnd = () => { this.modalElement.style.display = 'none'; this.modalElement.removeEventListener('transitionend', handleTransitionEnd); this._isClosing = false; if (typeof this.options.onClose === 'function') { this.options.onClose(); } }; this.modalElement.addEventListener('transitionend', handleTransitionEnd); setTimeout(handleTransitionEnd, this.options.animationDuration + 100); } /** * 公共方法:设置弹窗标题 * @param {string} title - 新的标题 */ setTitle(title) { const titleEl = this.modalElement.querySelector('.modal-title'); if (titleEl) titleEl.innerHTML = title; } /** * 公共方法:设置弹窗主体内容 * @param {string|HTMLElement} content - 新的内容 */ setBody(content) { if (!this.bodyElement) return; this.bodyElement.innerHTML = ''; if (typeof content === 'string') { this.bodyElement.innerHTML = content; } else if (content instanceof HTMLElement) { this.bodyElement.appendChild(content); } } /** * 公共方法:设置弹窗页脚内容 * @param {string|HTMLElement} footerContent - 新的页脚内容 */ setFooter(footerContent) { if (!this.footerElement) return; this.footerElement.innerHTML = ''; if (typeof footerContent === 'string') { this.footerElement.innerHTML = footerContent; } else if (footerContent instanceof HTMLElement) { this.footerElement.appendChild(footerContent); } } /** * 公共方法:获取弹窗主体元素 * @returns {HTMLElement} */ getBodyElement() { return this.bodyElement; } /** * 公共方法:获取弹窗页脚元素 * @returns {HTMLElement|undefined} */ getFooterElement() { return this.footerElement; } /** * 公共方法:销毁弹窗并从DOM中移除 */ destroy() { if (this.modalElement) { document.body.removeChild(this.modalElement); this.modalElement = null; } } } /** * 显示一个可配置的、返回Promise的异步对话框。(已修复) * @param {object} options - 对话框配置 * @param {string} options.title - 标题 * @param {string} options.message - 显示的文本消息 * @param {string} [options.type='alert'] - 类型: 'alert', 'confirm', 'prompt' * @param {string} [options.placeholder=''] - 输入框的占位符 (仅 prompt 类型有效) * @param {string} [options.okText='确认'] - 确认按钮的文本 * @param {string} [options.cancelText='取消'] - 取消按钮的文本 * @returns {Promise} - confirm类型resolve(true), prompt类型resolve(inputValue), alert类型resolve()。取消则reject。 */ function showDialog(options) { const config = Object.assign({ title: '', message: '', type: 'alert', placeholder: '', okText: '确认', cancelText: '取消', }, options); return new Promise((resolve, reject) => { const overlay = document.createElement('div'); overlay.className = 'dialog-overlay'; let inputHtml = ''; if (config.type === 'prompt') { inputHtml = ``; } let buttonsHtml = ``; if (config.type === 'confirm' || config.type === 'prompt') { buttonsHtml = `` + buttonsHtml; } const dialogHtml = ` `; overlay.innerHTML = dialogHtml; document.body.appendChild(overlay); const dialogContainer = overlay.querySelector('.dialog-container'); const inputElement = overlay.querySelector('.dialog-input'); const okBtn = overlay.querySelector('.ok-btn'); const cancelBtn = overlay.querySelector('.cancel-btn'); const closeDialog = (reason) => { overlay.classList.remove('visible'); const handleTransitionEnd = () => { overlay.removeEventListener('transitionend', handleTransitionEnd); overlay.remove(); if (reason === 'resolve') { const result = config.type === 'prompt' ? inputElement.value : true; resolve(result); } else { reject(); } }; overlay.addEventListener('transitionend', handleTransitionEnd); }; if (okBtn) { okBtn.addEventListener('click', () => closeDialog('resolve')); } if (cancelBtn) { cancelBtn.addEventListener('click', () => closeDialog('reject')); } overlay.addEventListener('click', (e) => { if (e.target === overlay) { closeDialog('reject'); } }); setTimeout(() => { overlay.classList.add('visible'); dialogContainer.focus(); if(inputElement) inputElement.focus(); }, 10); }); } document.addEventListener('DOMContentLoaded', () => { const scrollTopBtn = document.getElementById('scrollToTopBtn'); if (!scrollTopBtn) return; scrollTopBtn.addEventListener('click', scrollToTop); window.addEventListener('scroll', () => { if (window.scrollY > 200) { scrollTopBtn.classList.add('visible'); } else { scrollTopBtn.classList.remove('visible'); } }, { passive: true }); });