/** * Admin 后台管理系统 JavaScript * 提供通用的交互功能和工具函数 */ (function() { 'use strict'; // 通用工具函数 const AdminUtils = { /** * 显示Toast消息 * @param {string} type - 消息类型 (success, error, warning, info) * @param {string} message - 消息内容 * @param {number} duration - 显示时长(毫秒) */ showToast: function(type, message, duration = 3000) { // 移除现有的toast const existingToast = document.querySelector('.admin-toast'); if (existingToast) { existingToast.remove(); } const toast = document.createElement('div'); toast.className = `admin-toast toast-${type}`; toast.innerHTML = ` ${message} `; document.body.appendChild(toast); // 自动消失 setTimeout(() => { this.hideToast(toast); }, duration); // 点击关闭 const closeBtn = toast.querySelector('.toast-close'); if (closeBtn) { closeBtn.addEventListener('click', () => { this.hideToast(toast); }); } }, /** * 隐藏Toast消息 * @param {HTMLElement} toast - Toast元素 */ hideToast: function(toast) { if (toast && toast.parentNode) { toast.style.opacity = '0'; setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); } }, /** * 确认对话框 * @param {string} message - 确认消息 * @param {string} title - 对话框标题 * @returns {boolean} 用户确认结果 */ confirm: function(message, title = '确认') { return confirm(`${title}\n\n${message}`); }, /** * 发送AJAX请求 * @param {string} url - 请求URL * @param {object} options - 请求选项 * @returns {Promise} 请求Promise */ ajax: function(url, options = {}) { const defaultOptions = { method: 'GET', headers: { 'Content-Type': 'application/json', }, credentials: 'same-origin' }; const mergedOptions = Object.assign(defaultOptions, options); if (mergedOptions.body && typeof mergedOptions.body === 'object') { mergedOptions.body = JSON.stringify(mergedOptions.body); } return fetch(url, mergedOptions) .then(response => { if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); }) .catch(error => { console.error('AJAX请求失败:', error); throw error; }); }, /** * 防抖函数 * @param {Function} func - 要防抖的函数 * @param {number} delay - 延迟时间(毫秒) * @returns {Function} 防抖后的函数 */ debounce: function(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; }, /** * 格式化日期 * @param {Date|string} date - 日期对象或字符串 * @param {string} format - 格式类型 (date, time, datetime) * @returns {string} 格式化后的日期字符串 */ formatDate: function(date, format = 'datetime') { const d = new Date(date); if (isNaN(d.getTime())) { return '无效日期'; } const options = { date: { year: 'numeric', month: '2-digit', day: '2-digit' }, time: { hour: '2-digit', minute: '2-digit' }, datetime: { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' } }; return d.toLocaleString('zh-CN', options[format] || options.datetime); }, /** * 复制文本到剪贴板 * @param {string} text - 要复制的文本 * @returns {Promise} 复制是否成功 */ copyToClipboard: function(text) { if (navigator.clipboard) { return navigator.clipboard.writeText(text) .then(() => { this.showToast('success', '已复制到剪贴板'); return true; }) .catch(err => { console.error('复制失败:', err); return this.fallbackCopyTextToClipboard(text); }); } else { return Promise.resolve(this.fallbackCopyTextToClipboard(text)); } }, /** * 降级复制文本到剪贴板 * @param {string} text - 要复制的文本 * @returns {boolean} 复制是否成功 */ fallbackCopyTextToClipboard: function(text) { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { const successful = document.execCommand('copy'); if (successful) { this.showToast('success', '已复制到剪贴板'); } else { this.showToast('error', '复制失败,请手动复制'); } return successful; } catch (err) { console.error('降级复制失败:', err); this.showToast('error', '复制失败,请手动复制'); return false; } finally { document.body.removeChild(textArea); } } }; // 初始化函数 const AdminApp = { /** * 初始化应用 */ init: function() { this.initDropdowns(); this.initMobileNav(); this.initToasts(); this.initFormValidation(); this.initTableActions(); this.initSearch(); this.initArticleEditor(); }, /** * 初始化下拉菜单 */ initDropdowns: function() { document.addEventListener('click', (e) => { // 关闭所有下拉菜单 const dropdowns = document.querySelectorAll('.dropdown'); dropdowns.forEach(dropdown => { if (!dropdown.contains(e.target)) { dropdown.classList.remove('active'); } }); }); // 下拉菜单触发器 const dropdownTriggers = document.querySelectorAll('.dropdown-trigger'); dropdownTriggers.forEach(trigger => { trigger.addEventListener('click', (e) => { e.stopPropagation(); const dropdown = trigger.closest('.dropdown'); dropdown.classList.toggle('active'); }); }); }, /** * 初始化移动端导航 */ initMobileNav: function() { // 创建移动端菜单按钮 if (window.innerWidth <= 768) { this.createMobileMenuButton(); } window.addEventListener('resize', () => { if (window.innerWidth <= 768) { this.createMobileMenuButton(); } else { this.removeMobileMenuButton(); document.body.classList.remove('sidebar-open'); } }); }, /** * 创建移动端菜单按钮 */ createMobileMenuButton: function() { if (document.querySelector('.mobile-menu-btn')) return; const button = document.createElement('button'); button.className = 'mobile-menu-btn'; button.innerHTML = '☰'; button.style.cssText = ` background: none; border: none; font-size: 1.25rem; cursor: pointer; padding: 0.5rem; color: #4a5568; `; button.addEventListener('click', () => { document.body.classList.toggle('sidebar-open'); }); const headerLeft = document.querySelector('.admin-header-left'); if (headerLeft) { headerLeft.insertBefore(button, headerLeft.firstChild); } }, /** * 移除移动端菜单按钮 */ removeMobileMenuButton: function() { const button = document.querySelector('.mobile-menu-btn'); if (button) { button.remove(); } }, /** * 初始化现有Toast消息 */ initToasts: function() { const existingToasts = document.querySelectorAll('.admin-toast'); existingToasts.forEach(toast => { setTimeout(() => { AdminUtils.hideToast(toast); }, 3000); const closeBtn = toast.querySelector('.toast-close'); if (closeBtn) { closeBtn.addEventListener('click', () => { AdminUtils.hideToast(toast); }); } }); }, /** * 初始化表单验证 */ initFormValidation: function() { const forms = document.querySelectorAll('form'); forms.forEach(form => { form.addEventListener('submit', (e) => { const requiredFields = form.querySelectorAll('[required]'); let isValid = true; requiredFields.forEach(field => { if (!field.value.trim()) { isValid = false; field.style.borderColor = '#f56565'; // 移除已有的错误提示 const existingError = field.parentNode.querySelector('.field-error'); if (existingError) { existingError.remove(); } // 添加错误提示 const error = document.createElement('div'); error.className = 'field-error'; error.style.cssText = 'color: #f56565; font-size: 0.75rem; margin-top: 0.25rem;'; error.textContent = '此字段为必填项'; field.parentNode.appendChild(error); } else { field.style.borderColor = ''; const existingError = field.parentNode.querySelector('.field-error'); if (existingError) { existingError.remove(); } } }); if (!isValid) { e.preventDefault(); AdminUtils.showToast('error', '请填写所有必填字段'); } }); // 实时验证 const requiredFields = form.querySelectorAll('[required]'); requiredFields.forEach(field => { field.addEventListener('blur', () => { if (!field.value.trim()) { field.style.borderColor = '#f56565'; } else { field.style.borderColor = ''; const existingError = field.parentNode.querySelector('.field-error'); if (existingError) { existingError.remove(); } } }); }); }); }, /** * 初始化表格操作 */ initTableActions: function() { // 表格行点击 const tableRows = document.querySelectorAll('tbody tr'); tableRows.forEach(row => { row.addEventListener('click', (e) => { // 如果点击的是按钮或链接,不执行行点击事件 if (e.target.closest('button, a, select, input')) { return; } // 高亮当前行 tableRows.forEach(r => r.classList.remove('selected')); row.classList.add('selected'); }); }); // 状态选择器 const statusSelects = document.querySelectorAll('.status-select'); statusSelects.forEach(select => { select.addEventListener('change', (e) => { e.stopPropagation(); }); }); }, /** * 初始化搜索功能 */ initSearch: function() { const searchInputs = document.querySelectorAll('.search-input'); searchInputs.forEach(input => { // 搜索防抖 const debouncedSearch = AdminUtils.debounce((value) => { if (value.length >= 2 || value.length === 0) { // 可以在这里添加实时搜索功能 console.log('搜索:', value); } }, 300); input.addEventListener('input', (e) => { debouncedSearch(e.target.value); }); // 回车搜索 input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.target.closest('form').submit(); } }); }); }, /** * 初始化文章编辑器增强功能 */ initArticleEditor: function() { const titleInput = document.getElementById('title'); const slugInput = document.getElementById('slug'); const contentTextarea = document.getElementById('content'); if (titleInput && slugInput) { // 自动生成slug titleInput.addEventListener('input', AdminUtils.debounce((e) => { if (!slugInput.value.trim() || slugInput.dataset.autoGenerated === 'true') { const slug = this.generateSlug(e.target.value); slugInput.value = slug; slugInput.dataset.autoGenerated = 'true'; } }, 300)); // 手动编辑slug时停止自动生成 slugInput.addEventListener('input', () => { slugInput.dataset.autoGenerated = 'false'; }); } if (contentTextarea) { // 添加工具栏 this.addEditorToolbar(contentTextarea); // Tab键支持 contentTextarea.addEventListener('keydown', (e) => { if (e.key === 'Tab') { e.preventDefault(); const start = contentTextarea.selectionStart; const end = contentTextarea.selectionEnd; const value = contentTextarea.value; contentTextarea.value = value.substring(0, start) + ' ' + value.substring(end); contentTextarea.selectionStart = contentTextarea.selectionEnd = start + 4; } }); } // 初始化字符计数 this.initCharCounters(); }, /** * 生成URL别名 */ generateSlug: function(title) { return title .toLowerCase() .replace(/[^\w\u4e00-\u9fa5]+/g, '-') .replace(/^-+|-+$/g, '') .substring(0, 100); }, /** * 添加编辑器工具栏 */ addEditorToolbar: function(textarea) { const toolbar = document.createElement('div'); toolbar.className = 'editor-toolbar'; toolbar.innerHTML = `
`; // 添加样式 const style = document.createElement('style'); style.textContent = ` .editor-toolbar { display: flex; gap: 0.5rem; padding: 0.75rem; background: #f8fafc; border: 1px solid #e2e8f0; border-bottom: none; border-radius: 6px 6px 0 0; flex-wrap: wrap; } .editor-toolbar-group { display: flex; gap: 0.25rem; padding: 0 0.5rem; border-right: 1px solid #e2e8f0; } .editor-toolbar-group:last-child { border-right: none; } .editor-btn { background: none; border: 1px solid transparent; border-radius: 4px; padding: 0.25rem 0.5rem; cursor: pointer; font-size: 0.875rem; transition: all 0.2s; } .editor-btn:hover { background: #e2e8f0; border-color: #cbd5e0; } .editor-toolbar + textarea { border-radius: 0 0 6px 6px; } `; if (!document.querySelector('#editor-toolbar-style')) { style.id = 'editor-toolbar-style'; document.head.appendChild(style); } // 插入工具栏 textarea.parentNode.insertBefore(toolbar, textarea); // 绑定事件 toolbar.addEventListener('click', (e) => { if (e.target.classList.contains('editor-btn')) { e.preventDefault(); const action = e.target.dataset.action; this.executeEditorAction(textarea, action); } }); // 键盘快捷键 textarea.addEventListener('keydown', (e) => { if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case 'b': e.preventDefault(); this.executeEditorAction(textarea, 'bold'); break; case 'i': e.preventDefault(); this.executeEditorAction(textarea, 'italic'); break; } } }); }, /** * 执行编辑器动作 */ executeEditorAction: function(textarea, action) { const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); const beforeText = textarea.value.substring(0, start); const afterText = textarea.value.substring(end); let insertText = ''; let cursorOffset = 0; switch (action) { case 'bold': insertText = `**${selectedText || '粗体文字'}**`; cursorOffset = selectedText ? 0 : -2; break; case 'italic': insertText = `*${selectedText || '斜体文字'}*`; cursorOffset = selectedText ? 0 : -1; break; case 'code': insertText = `\`${selectedText || '代码'}\``; cursorOffset = selectedText ? 0 : -1; break; case 'h1': insertText = `# ${selectedText || '标题 1'}`; break; case 'h2': insertText = `## ${selectedText || '标题 2'}`; break; case 'h3': insertText = `### ${selectedText || '标题 3'}`; break; case 'ul': insertText = `- ${selectedText || '列表项'}`; break; case 'ol': insertText = `1. ${selectedText || '列表项'}`; break; case 'quote': insertText = `> ${selectedText || '引用文字'}`; break; case 'link': const linkText = selectedText || '链接文字'; insertText = `[${linkText}](URL)`; cursorOffset = -4; break; case 'image': const altText = selectedText || '图片描述'; insertText = `![${altText}](URL)`; cursorOffset = -4; break; } textarea.value = beforeText + insertText + afterText; const newCursorPos = start + insertText.length + cursorOffset; textarea.setSelectionRange(newCursorPos, newCursorPos); textarea.focus(); }, /** * 初始化字符计数器 */ initCharCounters: function() { const fields = [ { id: 'title', max: 200 }, { id: 'excerpt', max: 500 }, { id: 'tags', max: 200 }, { id: 'slug', max: 100 } ]; fields.forEach(field => { const element = document.getElementById(field.id); if (element) { this.setupCharCounter(element, field.max); } }); }, /** * 设置字符计数器 */ setupCharCounter: function(element, maxLength) { const helpElement = element.nextElementSibling; if (!helpElement || !helpElement.classList.contains('form-help')) { return; } const originalText = helpElement.textContent; const updateCounter = () => { const currentLength = element.value.length; const remaining = maxLength - currentLength; if (remaining < 50) { helpElement.textContent = `${originalText} (还可输入${remaining}字符)`; helpElement.style.color = remaining < 10 ? '#f56565' : '#ed8936'; } else { helpElement.textContent = originalText; helpElement.style.color = ''; } }; element.addEventListener('input', updateCounter); updateCounter(); } }; // 全局删除函数 window.deleteArticle = function(id, title) { if (AdminUtils.confirm(`确定要删除文章《${title}》吗?此操作不可撤销。`, '删除确认')) { AdminUtils.ajax(`/admin/articles/${id}`, { method: 'DELETE' }) .then(data => { if (data.success) { AdminUtils.showToast('success', data.message || '文章删除成功'); setTimeout(() => { window.location.reload(); }, 1000); } else { AdminUtils.showToast('error', data.message || '删除失败'); } }) .catch(error => { console.error('删除失败:', error); AdminUtils.showToast('error', '删除失败,请稍后重试'); }); } }; // 全局联系信息删除函数 window.deleteContact = function(id, title) { if (AdminUtils.confirm(`确定要删除联系信息《${title}》吗?此操作不可撤销。`, '删除确认')) { AdminUtils.ajax(`/admin/contacts/${id}`, { method: 'DELETE' }) .then(data => { if (data.success) { AdminUtils.showToast('success', data.message || '联系信息删除成功'); setTimeout(() => { window.location.reload(); }, 1000); } else { AdminUtils.showToast('error', data.message || '删除失败'); } }) .catch(error => { console.error('删除失败:', error); AdminUtils.showToast('error', '删除失败,请稍后重试'); }); } }; // 全局状态更新函数 window.updateContactStatus = function(id, status) { if (!status) return; AdminUtils.ajax(`/admin/contacts/${id}/status`, { method: 'PUT', body: { status } }) .then(data => { if (data.success) { AdminUtils.showToast('success', data.message || '状态更新成功'); setTimeout(() => { window.location.reload(); }, 1000); } else { AdminUtils.showToast('error', data.message || '状态更新失败'); } }) .catch(error => { console.error('状态更新失败:', error); AdminUtils.showToast('error', '状态更新失败,请稍后重试'); }); }; // 全局复制邮箱函数 window.copyEmail = function(email) { AdminUtils.copyToClipboard(email || document.querySelector('[data-email]')?.dataset.email || ''); }; // 全局预览文章函数 window.previewArticle = function() { const titleElement = document.getElementById('title'); const contentElement = document.getElementById('content'); if (!titleElement || !contentElement) { AdminUtils.showToast('error', '无法找到文章内容'); return; } const title = titleElement.value.trim(); const content = contentElement.value.trim(); if (!content) { AdminUtils.showToast('warning', '请先输入文章内容'); contentElement.focus(); return; } // 简单的Markdown预览 const previewWindow = window.open('', '_blank', 'width=800,height=600,scrollbars=yes'); if (!previewWindow) { AdminUtils.showToast('error', '无法打开预览窗口,请检查浏览器弹窗设置'); return; } // 基础的Markdown转换 let htmlContent = content .replace(/\n/g, '
') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/`(.*?)`/g, '$1') .replace(/^# (.*$)/gim, '

$1

') .replace(/^## (.*$)/gim, '

$1

') .replace(/^### (.*$)/gim, '

$1

'); previewWindow.document.write(` 文章预览 - ${title || '未设置标题'}

📄 文章预览

${title || '未设置标题'}

${htmlContent}
`); previewWindow.document.close(); // 聚焦到预览窗口 previewWindow.focus(); }; // 全局工具函数 window.AdminUtils = AdminUtils; // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { AdminApp.init(); }); } else { AdminApp.init(); } // CSS样式注入(移动端菜单按钮样式) const style = document.createElement('style'); style.textContent = ` .selected { background-color: #ebf8ff !important; } .field-error { color: #f56565; font-size: 0.75rem; margin-top: 0.25rem; } @media (max-width: 768px) { .admin-sidebar { transform: translateX(-100%); transition: transform 0.3s ease; } .sidebar-open .admin-sidebar { transform: translateX(0); } .sidebar-open::before { content: ''; position: fixed; top: 60px; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 40; } } `; document.head.appendChild(style); })();