You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
973 lines
36 KiB
973 lines
36 KiB
/**
|
|
* 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 = `
|
|
<span>${message}</span>
|
|
<button class="toast-close" aria-label="关闭">×</button>
|
|
`;
|
|
|
|
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<boolean>} 复制是否成功
|
|
*/
|
|
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 = `
|
|
<div class="editor-toolbar-group">
|
|
<button type="button" class="editor-btn" data-action="bold" title="粗体 (Ctrl+B)">💪</button>
|
|
<button type="button" class="editor-btn" data-action="italic" title="斜体 (Ctrl+I)">🌯</button>
|
|
<button type="button" class="editor-btn" data-action="code" title="代码">💻</button>
|
|
</div>
|
|
<div class="editor-toolbar-group">
|
|
<button type="button" class="editor-btn" data-action="h1" title="标题 1">🏆</button>
|
|
<button type="button" class="editor-btn" data-action="h2" title="标题 2">🥈</button>
|
|
<button type="button" class="editor-btn" data-action="h3" title="标题 3">🥉</button>
|
|
</div>
|
|
<div class="editor-toolbar-group">
|
|
<button type="button" class="editor-btn" data-action="ul" title="无序列表">📝</button>
|
|
<button type="button" class="editor-btn" data-action="ol" title="有序列表">🔢</button>
|
|
<button type="button" class="editor-btn" data-action="quote" title="引用">💬</button>
|
|
</div>
|
|
<div class="editor-toolbar-group">
|
|
<button type="button" class="editor-btn" data-action="link" title="链接">🔗</button>
|
|
<button type="button" class="editor-btn" data-action="image" title="图片">🖼️</button>
|
|
</div>
|
|
`;
|
|
|
|
// 添加样式
|
|
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 = ``;
|
|
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, '<br>')
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
.replace(/`(.*?)`/g, '<code>$1</code>')
|
|
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
|
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
|
.replace(/^### (.*$)/gim, '<h3>$1</h3>');
|
|
|
|
previewWindow.document.write(`
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>文章预览 - ${title || '未设置标题'}</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
line-height: 1.7;
|
|
color: #2d3748;
|
|
background: #ffffff;
|
|
}
|
|
h1, h2, h3, h4, h5, h6 {
|
|
color: #1a202c;
|
|
margin-top: 2rem;
|
|
margin-bottom: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
h1 { font-size: 2rem; border-bottom: 2px solid #e2e8f0; padding-bottom: 0.5rem; }
|
|
h2 { font-size: 1.5rem; }
|
|
h3 { font-size: 1.25rem; }
|
|
p { margin-bottom: 1rem; }
|
|
code {
|
|
background: #f7fafc;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 0.875rem;
|
|
color: #e53e3e;
|
|
}
|
|
pre {
|
|
background: #f7fafc;
|
|
padding: 1rem;
|
|
border-radius: 0.5rem;
|
|
overflow-x: auto;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
pre code {
|
|
background: none;
|
|
padding: 0;
|
|
color: #2d3748;
|
|
}
|
|
blockquote {
|
|
border-left: 4px solid #4299e1;
|
|
padding-left: 1rem;
|
|
margin: 1rem 0;
|
|
color: #4a5568;
|
|
background: #f7fafc;
|
|
padding: 1rem;
|
|
border-radius: 0.5rem;
|
|
}
|
|
strong { font-weight: 600; color: #1a202c; }
|
|
em { font-style: italic; color: #4a5568; }
|
|
.preview-header {
|
|
background: #4299e1;
|
|
color: white;
|
|
padding: 1rem;
|
|
margin: -2rem -2rem 2rem -2rem;
|
|
border-radius: 0;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.preview-title {
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
}
|
|
.preview-close {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: none;
|
|
color: white;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
font-size: 0.875rem;
|
|
}
|
|
.preview-close:hover {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="preview-header">
|
|
<h1 class="preview-title">📄 文章预览</h1>
|
|
<button class="preview-close" onclick="window.close()">关闭预览</button>
|
|
</div>
|
|
<h1>${title || '未设置标题'}</h1>
|
|
<div class="article-content">${htmlContent}</div>
|
|
<script>
|
|
// 键盘快捷键
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
window.close();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`);
|
|
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);
|
|
|
|
})();
|