Files
resume/templates/admin/settings.html
T

347 lines
18 KiB
HTML

<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="font-semibold text-slate-800 flex items-center gap-2">
<i class="fas fa-cog text-purple-500"></i> 系统设置
</h3>
<p class="text-sm text-slate-500 mt-1">配置系统参数</p>
</div>
<button onclick="saveSettings()" class="btn btn-primary">
<i class="fas fa-save"></i> 保存设置
</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 space-y-6">
<div class="p-6 bg-slate-50 rounded-lg">
<h4 class="font-medium text-slate-700 mb-4 flex items-center gap-2">
<i class="fas fa-globe text-blue-500"></i> 网站基本设置
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">网站域名</label>
<input type="text" id="settingDomain" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="{{.Config.WebsiteDomain}}">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">网站标题</label>
<input type="text" id="settingTitle" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="{{.Config.WebsiteTitle}}">
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-slate-700 mb-1">网站描述</label>
<textarea id="settingDescription" rows="3" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">{{.Config.WebsiteDescription}}</textarea>
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-slate-700 mb-1">网站关键词</label>
<input type="text" id="settingKeywords" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="{{.Config.WebsiteKeywords}}" placeholder="多个关键词用逗号分隔">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">管理员邮箱</label>
<input type="email" id="settingAdminEmail" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="{{.Config.AdminEmail}}">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">管理员姓名</label>
<input type="text" id="settingAdminName" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="{{.Config.AdminName}}">
</div>
</div>
</div>
<div class="p-6 bg-slate-50 rounded-lg">
<h4 class="font-medium text-slate-700 mb-4 flex items-center gap-2">
<i class="fas fa-image text-blue-500"></i> Logo设置
</h4>
<div class="flex items-center gap-6">
<div class="flex-shrink-0">
{{ if .Config.LogoPath }}
<div class="relative group">
<img id="logoPreview" src="{{.Config.LogoPath}}" data-path="{{.Config.LogoPath}}" class="w-24 h-24 rounded-xl object-cover border-2 border-blue-200 shadow-md">
<button onclick="clearLogo()" class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center shadow-lg hover:bg-red-600">
<i class="fas fa-times text-xs"></i>
</button>
</div>
{{ else }}
<div id="logoPreview" class="w-24 h-24 rounded-xl bg-white border-2 border-dashed border-slate-300 flex flex-col items-center justify-center text-slate-400 hover:border-blue-400 hover:text-blue-500 transition-all cursor-pointer" onclick="document.getElementById('logoFile').click()">
<i class="fas fa-image text-2xl mb-1"></i>
<span class="text-xs">点击上传</span>
</div>
{{ end }}
</div>
<div class="flex-1">
<div id="logoDropZone" class="w-full border-2 border-dashed border-slate-300 rounded-lg p-6 text-center hover:border-blue-400 hover:bg-blue-50 transition-all cursor-pointer" onclick="document.getElementById('logoFile').click()">
<i class="fas fa-cloud-upload-alt text-blue-500 text-3xl mb-3"></i>
<p class="text-sm text-slate-600">点击或拖动文件到此处自动上传</p>
</div>
<input type="file" id="logoFile" accept="image/*" class="hidden" onchange="handleLogoSelect(this)">
</div>
</div>
</div>
<div class="p-6 bg-slate-50 rounded-lg">
<h4 class="font-medium text-slate-700 mb-4 flex items-center gap-2">
<i class="fas fa-list text-blue-500"></i> 后台设置
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">后台列表每页数据条数</label>
<input type="number" id="settingAdminPageSize" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="{{.Config.AdminPageSize}}" min="1" max="100">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">SEO 关键词</label>
<input type="text" id="settingSEOKeywords" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="{{.Config.SEOKeywords}}" placeholder="多个关键词用逗号分隔">
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-slate-700 mb-1">SEO 描述</label>
<textarea id="settingSEODescription" rows="2" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">{{.Config.SEODescription}}</textarea>
</div>
</div>
</div>
</div>
<div class="space-y-6">
<div class="p-6 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg border border-blue-200">
<h4 class="font-medium text-blue-800 mb-4 flex items-center gap-2">
<i class="fas fa-server text-blue-600"></i> 系统信息
</h4>
<div class="space-y-3">
<div class="flex items-center justify-between text-sm">
<span class="text-slate-500">系统版本</span>
<span class="text-slate-800 font-medium">1.0.0</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-slate-500">Go 版本</span>
<span class="text-slate-800 font-medium" id="sysGoVersion">-</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-slate-500">数据库</span>
<span class="text-slate-800 font-medium">SQLite</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-slate-500">操作系统</span>
<span class="text-slate-800 font-medium" id="sysOS">-</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-slate-500">架构</span>
<span class="text-slate-800 font-medium" id="sysArch">-</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-slate-500">启动时间</span>
<span class="text-slate-800 font-medium" id="sysStartTime">-</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-slate-500">运行时长</span>
<span class="text-slate-800 font-medium" id="sysUptime">-</span>
</div>
</div>
</div>
<div class="p-6 bg-slate-50 rounded-lg">
<h4 class="font-medium text-slate-700 mb-4 flex items-center gap-2">
<i class="fas fa-history text-blue-500"></i> 最近登录记录
</h4>
<div id="loginHistoryList" class="space-y-3 max-h-60 overflow-y-auto">
<div class="text-center text-slate-400 text-sm py-4">加载中...</div>
</div>
</div>
</div>
</div>
</div>
<script>
function handleLogoSelect(input) {
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
const preview = document.getElementById('logoPreview');
if (preview.tagName === 'IMG') {
preview.src = e.target.result;
// 上传未完成前先清空 data-path,等待 uploadLogo 完成后再回填服务端路径
preview.removeAttribute('data-path');
} else {
preview.outerHTML = '<div id="logoPreview" class="relative group"><img src="' + e.target.result + '" class="w-24 h-24 rounded-xl object-cover border-2 border-blue-200 shadow-md"><button onclick="clearLogo()" class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center shadow-lg hover:bg-red-600"><i class="fas fa-times text-xs"></i></button></div>';
}
}
reader.readAsDataURL(input.files[0]);
uploadLogo(input.files[0]);
}
}
function clearLogo() {
const preview = document.getElementById('logoPreview');
preview.outerHTML = '<div id="logoPreview" class="w-24 h-24 rounded-xl bg-white border-2 border-dashed border-slate-300 flex flex-col items-center justify-center text-slate-400 hover:border-blue-400 hover:text-blue-500 transition-all cursor-pointer" onclick="document.getElementById(\'logoFile\').click()"><i class="fas fa-image text-2xl mb-1"></i><span class="text-xs">点击上传</span></div>';
showCustomAlert('success', '成功', '已清除预览,请点击保存设置生效');
}
function setupDropZone() {
const dropZone = document.getElementById('logoDropZone');
const fileInput = document.getElementById('logoFile');
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
dropZone.classList.add('border-blue-500', 'bg-blue-50');
});
dropZone.addEventListener('dragleave', function(e) {
e.preventDefault();
dropZone.classList.remove('border-blue-500', 'bg-blue-50');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
dropZone.classList.remove('border-blue-500', 'bg-blue-50');
const files = e.dataTransfer.files;
if (files.length > 0 && files[0].type.startsWith('image/')) {
fileInput.files = files;
handleLogoSelect(fileInput);
} else {
showCustomAlert('error', '错误', '请上传图片文件');
}
});
}
function uploadLogo(file) {
const formData = new FormData();
formData.append('logo', file);
fetch('/dashboard/api/settings/logo', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
const logoPath = result.data && result.data.logo_path;
if (result.success && logoPath) {
const preview = document.getElementById('logoPreview');
if (preview.tagName === 'IMG') {
preview.src = logoPath;
preview.setAttribute('data-path', logoPath);
} else {
const img = preview.querySelector('img');
if (img) {
img.src = logoPath;
img.setAttribute('data-path', logoPath);
}
}
showCustomAlert('success', '成功', 'Logo上传成功');
} else {
showCustomAlert('error', '错误', '上传失败:' + (result.message || '未知错误'));
}
})
.catch(error => {
console.error('Error:', error);
showCustomAlert('error', '错误', '上传失败');
});
}
function saveSettings() {
const logoPreview = document.getElementById('logoPreview');
let logoPath = '';
// 优先读取自定义 data-path 属性(保存的是相对路径),避免浏览器把 src 转为绝对 URL
if (logoPreview.tagName === 'IMG') {
logoPath = logoPreview.getAttribute('data-path') || '';
} else {
const img = logoPreview.querySelector('img');
if (img) logoPath = img.getAttribute('data-path') || '';
}
const adminPageSize = parseInt(document.getElementById('settingAdminPageSize').value, 10);
if (isNaN(adminPageSize) || adminPageSize < 1 || adminPageSize > 100) {
showCustomAlert('error', '错误', '后台列表每页数据条数必须在 1-100 之间');
return;
}
const data = {
website_domain: document.getElementById('settingDomain').value,
website_title: document.getElementById('settingTitle').value,
website_description: document.getElementById('settingDescription').value,
website_keywords: document.getElementById('settingKeywords').value,
admin_email: document.getElementById('settingAdminEmail').value,
admin_name: document.getElementById('settingAdminName').value,
logo_path: logoPath,
seo_keywords: document.getElementById('settingSEOKeywords').value,
seo_description: document.getElementById('settingSEODescription').value,
admin_page_size: adminPageSize || 20
};
fetch('/dashboard/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
showCustomAlert('success', '成功', '设置保存成功');
setTimeout(() => location.reload(), 1500);
} else {
showCustomAlert('error', '错误', '保存失败:' + (result.message || '未知错误'));
}
})
.catch(error => {
console.error('Error:', error);
showCustomAlert('error', '错误', '保存失败');
});
}
function loadSystemInfo() {
fetch('/dashboard/api/settings/system-info')
.then(response => response.json())
.then(result => {
if (result.success) {
const data = result.data;
document.getElementById('sysGoVersion').textContent = data.go_version || '-';
document.getElementById('sysOS').textContent = data.os || '-';
document.getElementById('sysArch').textContent = data.arch || '-';
document.getElementById('sysStartTime').textContent = data.start_time || '-';
document.getElementById('sysUptime').textContent = data.uptime || '-';
}
})
.catch(error => {
console.error('Error loading system info:', error);
});
}
function loadLoginHistory() {
fetch('/dashboard/api/settings/login-history?page=1&pageSize=5')
.then(response => response.json())
.then(result => {
if (result.success) {
const historyList = document.getElementById('loginHistoryList');
if (result.data.list.length === 0) {
historyList.innerHTML = '<div class="text-center text-slate-400 text-sm py-4">暂无登录记录</div>';
return;
}
historyList.innerHTML = '';
result.data.list.forEach(item => {
const statusIcon = item.success
? '<i class="fas fa-check-circle text-green-500"></i>'
: '<i class="fas fa-times-circle text-red-500"></i>';
const statusText = item.success ? '成功' : '失败';
const itemDiv = document.createElement('div');
itemDiv.className = 'flex items-center justify-between p-3 bg-white rounded-lg border border-slate-100';
itemDiv.innerHTML = `
<div class="flex items-center gap-2">
${statusIcon}
<span class="text-sm text-slate-700">${item.user_name || '未知用户'}</span>
</div>
<div class="text-right">
<div class="text-xs text-slate-500">${item.ip}</div>
<div class="text-xs text-slate-400">${item.created_at}</div>
</div>
`;
historyList.appendChild(itemDiv);
});
}
})
.catch(error => {
console.error('Error loading login history:', error);
document.getElementById('loginHistoryList').innerHTML = '<div class="text-center text-slate-400 text-sm py-4">加载失败</div>';
});
}
setupDropZone();
loadSystemInfo();
loadLoginHistory();
</script>