- settings.go:5 组 save handler 改 JSON 绑定(siteSettingsRequest/ uploadSettingsRequest/downloadSettingsRequest/commentSettingsRequest/ navLinkSettingsRequest);request struct 变更子函数签名(不再触碰 c.PostForm),upload/navlink/download 的 action 分发保留,未知 action 返回 400;enabled 用 *bool(nil 沿用旧默认启用语义) - favicon/logo 上传拆出:POST /api/admin/settings/site/favicon|logo (multipart,图片类别校验 + 旧本地文件替换),SiteSettingsSave 只 处理文本/URL/clear(存储路径校验保留 illegal_dir 400) - main.go:设置旧 POST 路由移除,新 /api/admin/settings 分组注册 - 模板:base.html 新增 blogSettingsForm 委托(data-api-url/data-action/ data-confirm → POST + redirect/alert);settings_site 主表单 JSON + logo/favicon 选择即上传;navlinks/upload/download/comments 页全部 小表单改委托(约 14 个) - blogForm:剔除 file 字段(文件走 multipart) - 测试:TestStorageDirTraversalRejected、TestAddUploadFileTypeRejectsDangerousExtensions 更新 JSON 断言;env 路由补 /api/admin/settings - main_test 冒烟补设置 API 断言;go build/vet/test 全绿
204 lines
11 KiB
HTML
204 lines
11 KiB
HTML
{{define "settings_site"}}
|
|
{{template "header" .}}
|
|
<section class="max-w-3xl mx-auto px-4 py-12">
|
|
<h2 class="text-3xl font-bold text-gray-900 mb-2">{{index .Tr "settings_site_title"}}</h2>
|
|
<p class="text-gray-500 mb-4">{{index .Tr "settings_site_desc"}}</p>
|
|
|
|
<div class="flex gap-2 mb-8">
|
|
<a href="/admin/settings/site" class="px-4 py-2 rounded-lg text-sm font-medium bg-blue-600 text-white">{{index .Tr "settings_site_title"}}</a>
|
|
<a href="/admin/settings/navlinks" class="px-4 py-2 rounded-lg text-sm font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors">{{index .Tr "settings_navlinks_title"}}</a>
|
|
<a href="/admin/settings/upload" class="px-4 py-2 rounded-lg text-sm font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors">{{index .Tr "settings_upload_title"}}</a>
|
|
<a href="/admin/settings/download" class="px-4 py-2 rounded-lg text-sm font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors">{{index .Tr "settings_download_title"}}</a>
|
|
</div>
|
|
|
|
{{if .Success}}
|
|
<div class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg mb-6">{{.Success}}</div>
|
|
{{end}}
|
|
|
|
<div id="siteSettingsError" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 hidden"></div>
|
|
|
|
<form id="siteSettingsForm" action="/api/admin/settings/site" method="post" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
|
|
<!-- Logo -->
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">{{index .Tr "settings_logo"}}</label>
|
|
{{if .Site.Logo}}
|
|
{{if .SiteLogoIsURL}}
|
|
<img src="{{.Site.Logo}}" alt="logo" class="h-12 w-auto mb-2">
|
|
{{else}}
|
|
<img src="/uploads/logos/{{.Site.Logo}}" alt="logo" class="h-12 w-auto mb-2">
|
|
{{end}}
|
|
{{end}}
|
|
<input type="text" name="logo_url" placeholder="{{index .Tr "settings_logo_url"}}"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 mb-2" value="{{if .SiteLogoIsURL}}{{.Site.Logo}}{{end}}">
|
|
<input type="file" id="logoFileInput" name="logo" accept="image/*"
|
|
class="block w-full text-sm text-gray-500 mb-2">
|
|
<label class="inline-flex items-center gap-2 text-sm text-gray-600">
|
|
<input type="checkbox" name="logo_clear" value="1"> {{index .Tr "settings_logo_clear"}}
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Favicon -->
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">{{index .Tr "settings_favicon"}}</label>
|
|
{{if .Site.Favicon}}
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<span class="text-sm text-gray-600">{{index .Tr "settings_favicon_current"}}:</span>
|
|
{{if .SiteFaviconIsURL}}
|
|
<img src="{{.Site.Favicon}}" alt="favicon" class="h-8 w-8">
|
|
{{else}}
|
|
<img src="/uploads/logos/{{.Site.Favicon}}" alt="favicon" class="h-8 w-8">
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
<input type="text" name="favicon_url" placeholder="{{index .Tr "settings_favicon_url"}}"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 mb-2" value="{{if .SiteFaviconIsURL}}{{.Site.Favicon}}{{end}}">
|
|
<input type="file" id="faviconFileInput" name="favicon" accept="image/x-icon,image/png,image/svg+xml"
|
|
class="block w-full text-sm text-gray-500 mb-2">
|
|
<p class="text-xs text-gray-500 mb-2">{{index .Tr "settings_favicon_hint"}}</p>
|
|
<label class="inline-flex items-center gap-2 text-sm text-gray-600">
|
|
<input type="checkbox" name="favicon_clear" value="1"> {{index .Tr "settings_favicon_clear"}}
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Title texts -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{index .Tr "settings_logo_text_zh"}}</label>
|
|
<input type="text" name="logo_text_zh" value="{{.Site.LogoTextZh}}"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{index .Tr "settings_logo_text_en"}}</label>
|
|
<input type="text" name="logo_text_en" value="{{.Site.LogoTextEn}}"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Header texts -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{index .Tr "settings_header_text_zh"}}</label>
|
|
<textarea name="header_text_zh" rows="2"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2">{{.Site.HeaderTextZh}}</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{index .Tr "settings_header_text_en"}}</label>
|
|
<textarea name="header_text_en" rows="2"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2">{{.Site.HeaderTextEn}}</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Home welcome & subtitle -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{index .Tr "settings_home_welcome_zh"}}</label>
|
|
<input type="text" name="home_welcome_zh" value="{{.Site.HomeWelcomeZh}}"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{index .Tr "settings_home_welcome_en"}}</label>
|
|
<input type="text" name="home_welcome_en" value="{{.Site.HomeWelcomeEn}}"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2">
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{index .Tr "settings_home_subtitle_zh"}}</label>
|
|
<textarea name="home_subtitle_zh" rows="2"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2">{{.Site.HomeSubtitleZh}}</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{index .Tr "settings_home_subtitle_en"}}</label>
|
|
<textarea name="home_subtitle_en" rows="2"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2">{{.Site.HomeSubtitleEn}}</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer texts -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{index .Tr "settings_footer_text_zh"}}</label>
|
|
<textarea name="footer_text_zh" rows="2"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2">{{.Site.FooterTextZh}}</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{index .Tr "settings_footer_text_en"}}</label>
|
|
<textarea name="footer_text_en" rows="2"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2">{{.Site.FooterTextEn}}</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="text-xs text-gray-400">{{index .Tr "settings_leave_blank"}}</p>
|
|
|
|
<!-- Canonical site URL (RSS) -->
|
|
<div class="pt-4 border-t border-gray-200">
|
|
<label class="block text-sm font-semibold text-gray-700 mb-1">{{index .Tr "settings_site_url"}}</label>
|
|
<input type="url" name="site_url" value="{{.Site.SiteURL}}" placeholder="https://example.com"
|
|
class="w-full border border-gray-300 rounded-lg px-3 py-2">
|
|
<p class="text-xs text-gray-500 mt-1">{{index .Tr "settings_site_url_hint"}}</p>
|
|
</div>
|
|
|
|
<!-- Registration settings -->
|
|
<div class="pt-4 border-t border-gray-200">
|
|
<label class="flex items-center gap-3 cursor-pointer">
|
|
<input type="checkbox" name="allow_registration" value="1" {{if .Site.AllowRegistration}}checked{{end}}
|
|
class="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
<div>
|
|
<span class="block text-sm font-semibold text-gray-700">{{index .Tr "settings_allow_registration"}}</span>
|
|
<span class="block text-xs text-gray-500">{{index .Tr "settings_allow_registration_hint"}}</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<button type="submit"
|
|
class="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 transition-colors">
|
|
{{index .Tr "settings_save"}}
|
|
</button>
|
|
</form>
|
|
</section>
|
|
{{template "footer" .}}
|
|
|
|
<script>
|
|
// 站点设置主表单:文本字段走 JSON API。
|
|
(function () {
|
|
var form = document.getElementById('siteSettingsForm');
|
|
if (!form) return;
|
|
form.addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
var btn = e.submitter || null;
|
|
blogAPI('POST', '/api/admin/settings/site', blogForm(form, btn)).then(function (r) {
|
|
if (r.ok) { window.location.href = r.redirect || '/admin/settings/site'; }
|
|
else { blogShowError('siteSettingsError', r.error || 'Failed to save settings.'); }
|
|
});
|
|
});
|
|
})();
|
|
|
|
// logo / favicon 文件选择即上传(multipart)。
|
|
(function () {
|
|
function bindImageUpload(inputId, url, field) {
|
|
var input = document.getElementById(inputId);
|
|
if (!input) return;
|
|
input.addEventListener('change', function () {
|
|
var file = this.files[0];
|
|
if (!file) return;
|
|
var meta = document.querySelector('meta[name="csrf-token"]');
|
|
var fd = new FormData();
|
|
fd.append(field, file);
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-Token': meta ? meta.getAttribute('content') : '' },
|
|
body: fd,
|
|
credentials: 'same-origin'
|
|
}).then(function (r) { return r.json(); })
|
|
.then(function (r) {
|
|
if (r.ok) { window.location.href = r.redirect || '/admin/settings/site'; }
|
|
else { blogShowError('siteSettingsError', r.error || 'Upload failed.'); }
|
|
});
|
|
});
|
|
}
|
|
bindImageUpload('logoFileInput', '/api/admin/settings/site/logo', 'logo');
|
|
bindImageUpload('faviconFileInput', '/api/admin/settings/site/favicon', 'favicon');
|
|
})();
|
|
</script>
|
|
{{end}}
|