Files
go_blog/templates/layouts/base.html
T
dsh cc49152f1b fix: 更换头像后 header 头像样式异常——只替换头像容器内容
旧逻辑在头像上传成功后把整个 avatarBtn 按钮的 innerHTML 换成单个
<img>:头像圆形容器(w-8 h-8 rounded-full)、用户名 span 全部丢失,
头像失去圆形裁剪与边框、布局错乱,直到刷新后服务端重新渲染才恢复。

- base.html:头像是像圆形容器加 id="headerAvatarWrap"(服务器渲染结构
  不变,仅加标记)
- profile.html:上传成功后仅更新容器内容(img/svg → 新头像),
  保留按钮结构与用户名文本
2026-08-28 20:26:00 +08:00

350 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "header"}}
<!DOCTYPE html>
<html lang="{{.Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{.CSRFToken}}">
<title>{{.Title}} - {{index .Tr "site_title"}}</title>
{{if .SiteFavicon}}
{{if .SiteFaviconIsURL}}
<link rel="icon" href="{{.SiteFavicon}}">
{{else}}
<link rel="icon" href="/uploads/logos/{{.SiteFavicon}}">
{{end}}
{{end}}
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="/rss">
<!-- All third-party assets are vendored locally (static/vendor, embedded
via go:embed) — no external CDN origins, see SECURITY_TODO #9. -->
<link rel="stylesheet" href="/static/css/app.css?v=1">
<link rel="stylesheet" href="/static/vendor/cropper.min.css?v=1">
<script src="/static/vendor/cropper.min.js?v=1"></script>
<link rel="stylesheet" href="/static/vendor/easymde.min.css?v=1">
<link rel="stylesheet" href="/static/vendor/font-awesome/4.7.0/css/font-awesome.min.css?v=1">
<script src="/static/vendor/easymde.min.js?v=1"></script>
</head>
<body class="bg-gray-50 min-h-screen flex flex-col">
<!-- Navigation -->
<nav class="bg-white shadow-sm border-b border-gray-200">
<div class="max-w-5xl mx-auto px-4 py-2.5 flex items-center justify-between">
<a href="/" class="text-xl font-bold text-gray-800 hover:text-blue-600 transition-colors flex items-center gap-2">
{{if .SiteLogo}}
{{if .SiteLogoIsURL}}
<img src="{{.SiteLogo}}" alt="logo" class="h-7 w-auto">
{{else}}
<img src="/uploads/logos/{{.SiteLogo}}" alt="logo" class="h-7 w-auto">
{{end}}
{{end}}
{{if .SiteLogoText}}{{.SiteLogoText}}{{else}}{{index .Tr "site_title"}}{{end}}
</a>
<div class="flex items-center gap-4">
<a href="/" class="text-gray-600 hover:text-blue-600 transition-colors text-sm flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
<span>{{index .Tr "home"}}</span>
</a>
{{range .NavLinks}}
<a href="{{.URL}}" {{if .OpenNew}}target="_blank" rel="noopener noreferrer"{{end}} class="text-gray-600 hover:text-blue-600 transition-colors text-sm flex items-center gap-1.5">
<span>{{.Title $.Lang}}</span>
{{if .OpenNew}}
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
{{end}}
</a>
{{end}}
{{if .IsLoggedIn}}
<!-- Avatar Dropdown (click to toggle) -->
<div class="relative" id="avatarDropdown">
<button type="button" id="avatarBtn" class="flex items-center gap-2 hover:opacity-80 transition-opacity cursor-pointer" onclick="toggleDropdown()">
<div id="headerAvatarWrap" class="w-8 h-8 rounded-full overflow-hidden border-2 border-gray-300 hover:border-blue-500 transition-colors flex items-center justify-center bg-gray-200 text-gray-600 font-bold text-sm">
{{if .Avatar}}
<img src="/uploads/avatars/{{.Avatar}}" alt="avatar" class="w-full h-full object-cover">
{{else}}
<svg class="w-4 h-4 text-gray-500 pointer-events-none" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v1.2c0 .66.54 1.2 1.2 1.2h16.8c.66 0 1.2-.54 1.2-1.2v-1.2c0-3.2-6.4-4.8-9.6-4.8z"/>
</svg>
{{end}}
</div>
<span class="text-sm font-medium text-gray-700">{{if .DisplayName}}{{.DisplayName}}{{else}}{{.Username}}{{end}}</span>
</button>
<div id="dropdownMenu" class="absolute right-0 mt-2 w-44 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50 hidden">
<a href="/profile" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
{{index .Tr "profile"}}
</a>
<a href="/my/articles" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
{{index .Tr "my_articles"}}
</a>
{{if eq .Role "admin"}}
<div class="border-t border-gray-100 my-1"></div>
<a href="/admin" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
{{index .Tr "admin_panel"}}
</a>
{{end}}
<div class="border-t border-gray-100 my-1"></div>
<form action="/logout" method="post" class="m-0 logout-form">
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
<button type="submit" class="w-full text-left block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors cursor-pointer bg-transparent border-none">
{{index .Tr "logout"}}
</button>
</form>
</div>
</div>
{{else}}
<a href="/login" class="text-gray-600 hover:text-blue-600 transition-colors text-sm flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"/>
</svg>
<span>{{index .Tr "login"}}</span>
</a>
{{end}}
</div>
</div>
</nav>
<!-- Main Content -->
<main class="flex-1">
{{if .SiteHeaderText}}
<!-- Header banner -->
<div class="bg-blue-50 border-b border-blue-100 text-blue-800 text-sm text-center py-2 px-4">
{{.SiteHeaderText}}
</div>
{{end}}
{{end}}
{{define "markdown_assets"}}
{{/* Markdown rendering assets: marked (pinned UMD build), DOMPurify,
highlight.js and the shared renderer + styles. Include on any page that
renders Markdown (articles, comments, editor previews).
Local static files carry a ?v= cache-buster: bump it whenever
static/js/markdown.js or static/css/markdown.css changes, otherwise
browsers may keep serving stale cached copies. */}}
<link rel="stylesheet" href="/static/vendor/github.min.css?v=1">
<link rel="stylesheet" href="/static/css/markdown.css?v=2">
<script src="/static/vendor/marked.min.js?v=1"></script>
<script src="/static/vendor/purify.min.js?v=1"></script>
<script src="/static/vendor/highlight.min.js?v=1"></script>
<script src="/static/js/markdown.js?v=2"></script>
{{end}}
{{define "footer"}}
</main>
<!-- Footer -->
<footer class="bg-white border-t border-gray-200 py-4 mt-auto">
<div class="max-w-5xl mx-auto px-4">
<!-- Footer Actions Row -->
<div class="flex justify-center items-center gap-3 mb-2">
<!-- RSS Subscribe Button -->
<div class="relative inline-block">
<button onclick="toggleRSSMenu()" class="flex items-center gap-2 px-3 py-1.5 bg-orange-500 hover:bg-orange-600 text-white rounded-lg transition-colors text-sm font-medium">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20 5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93V10.1z"/>
</svg>
<span>{{if eq .Lang "zh"}}RSS 订阅{{else}}RSS Feed{{end}}</span>
</button>
<!-- RSS Dropdown Menu -->
<div id="rssMenu" class="hidden absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
<a href="/rss" target="_blank" class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
<span>{{if eq .Lang "zh"}}打开 RSS 链接{{else}}Open RSS Link{{end}}</span>
</a>
<button onclick="copyRSSLink()" class="w-full flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors text-left">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
<span id="copyText">{{if eq .Lang "zh"}}复制订阅链接{{else}}Copy Feed URL{{end}}</span>
</button>
</div>
</div>
<!-- Language Switcher -->
<a href="?lang={{.SwitchLang}}" class="flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-600 hover:text-blue-600 hover:bg-gray-50 transition-colors border border-gray-300 rounded-lg">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"/>
</svg>
<span>{{index .Tr "lang_switch"}}</span>
</a>
</div>
<!-- Footer Text -->
<div class="text-center text-gray-500 text-xs">
{{if .SiteFooterText}}{{.SiteFooterText}}{{else}}{{index .Tr "footer_text"}}{{end}}
</div>
<div class="text-center text-gray-400 text-[10px] mt-1">
{{.BuildInfo}}
</div>
</div>
</footer>
<script>
// ---- Shared API helpers/api/* JSON 接口统一入口) ----
window.blogAPI = function (method, url, body) {
var meta = document.querySelector('meta[name="csrf-token"]');
var csrf = meta ? meta.getAttribute('content') : '';
var opts = {
method: method,
headers: {
'X-CSRF-Token': csrf,
'Accept': 'application/json'
},
credentials: 'same-origin'
};
if (body !== undefined) {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(body);
}
return fetch(url, opts).then(function (r) {
return r.json().catch(function () {
return { ok: false, code: 'api_error', error: r.statusText };
});
});
};
// 上传本地图片(multipart)到文章附件接口(files 表,type=attachments)。
// 新建页传 session_token,编辑页传 article_id;成功 resolve {…, url, is_image}。
// 供文章新建/编辑页编辑器“上传图片”按钮使用。
window.blogUploadImage = function (opts) {
var meta = document.querySelector('meta[name="csrf-token"]');
var csrf = meta ? meta.getAttribute('content') : '';
var fd = new FormData();
fd.append('file', opts.file);
if (opts.articleID) { fd.append('article_id', opts.articleID); }
if (!opts.articleID && opts.sessionToken) { fd.append('session_token', opts.sessionToken); }
return fetch(opts.url, {
method: 'POST',
headers: { 'X-CSRF-Token': csrf, 'Accept': 'application/json' },
credentials: 'same-origin',
body: fd
}).then(function (r) {
return r.json();
});
};
// 将表单序列化为 JSON 数据对象:
// - 文本/select/textarea 从 FormData 取值(checkboxes 在下述循环覆盖)
// - checkbox 一律转 bool(未选中也发送 false,匹配服务端 JSON 绑定)
// - file 字段剔除(文件上传走单独的 multipart 端点)
// - 提交按钮(如文章表单的草稿/发布 name=status)取自 submitter
window.blogForm = function (form, submitter) {
var data = {};
new FormData(form).forEach(function (v, k) {
if (k === '_csrf') return;
if (!(k in data)) data[k] = v;
});
Array.prototype.forEach.call(form.querySelectorAll('input[type="file"]'), function (el) {
if (el.name) { data[el.name] = undefined; delete data[el.name]; }
});
Array.prototype.forEach.call(form.querySelectorAll('input[type="checkbox"]'), function (el) {
if (el.name) data[el.name] = el.checked;
});
if (submitter && submitter.name) {
data[submitter.name] = submitter.value;
}
return data;
};
// 在指定错误 div 中显示 API 错误文案。
window.blogShowError = function (divId, msg) {
var el = document.getElementById(divId);
if (el) { el.textContent = msg; el.classList.remove('hidden'); }
};
// Logout formsclass=logout-form)统一改走 JSON API 后跳转首页。
document.addEventListener('submit', function (e) {
var form = e.target;
if (form.classList && form.classList.contains('logout-form')) {
e.preventDefault();
blogAPI('POST', '/api/auth/logout').then(function (r) {
window.location.href = (r && r.redirect) || '/';
});
}
});
// 列表删除提交(onsubmit = "return blogDelete(this)"):
// DELETE data-url 指向的 JSON 接口,成功后 reload(保留筛选状态)。
window.blogDelete = function (form) {
var url = form.getAttribute('data-url');
var confirmText = form.getAttribute('data-confirm');
if (confirmText && !confirm(confirmText)) return false;
blogAPI('DELETE', url).then(function (r) {
if (r.ok) { window.location.reload(); }
else { alert(r.error || 'Failed'); }
});
return false;
};
// 设置页小表单委托(onsubmit = "return blogSettingsForm(this)"):
// POST data-api-urlbody 为 {action: data-action} + 表单序列化字段,
// 成功后跳转 data.redirect(通常刷新列表),失败 alert 文案。
window.blogSettingsForm = function (form) {
var url = form.getAttribute('data-api-url');
var confirmText = form.getAttribute('data-confirm');
if (confirmText && !confirm(confirmText)) return false;
var data = blogForm(form, null);
var action = form.getAttribute('data-action');
if (action) data.action = action;
blogAPI('POST', url, data).then(function (r) {
if (r.ok) { window.location.href = r.redirect || window.location.href; }
else { alert(r.error || 'Failed'); }
});
return false;
};
function toggleDropdown() {
var menu = document.getElementById('dropdownMenu');
menu.classList.toggle('hidden');
}
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
var dropdown = document.getElementById('avatarDropdown');
var menu = document.getElementById('dropdownMenu');
if (dropdown && !dropdown.contains(e.target)) {
menu.classList.add('hidden');
}
});
// RSS Menu Functions
function toggleRSSMenu() {
var menu = document.getElementById('rssMenu');
menu.classList.toggle('hidden');
}
function copyRSSLink() {
var rssURL = window.location.origin + '/rss';
var copyText = document.getElementById('copyText');
var originalText = copyText.textContent;
var lang = '{{.Lang}}';
// Copy to clipboard
navigator.clipboard.writeText(rssURL).then(function() {
// Show success feedback
copyText.textContent = lang === 'zh' ? '✓ 已复制!' : '✓ Copied!';
copyText.parentElement.classList.add('text-green-600');
// Reset after 2 seconds
setTimeout(function() {
copyText.textContent = originalText;
copyText.parentElement.classList.remove('text-green-600');
}, 2000);
}).catch(function(err) {
console.error('Failed to copy:', err);
copyText.textContent = lang === 'zh' ? '✗ 复制失败' : '✗ Copy failed';
});
}
// Close RSS menu when clicking outside
document.addEventListener('click', function(e) {
var rssButton = document.querySelector('[onclick="toggleRSSMenu()"]');
var rssMenu = document.getElementById('rssMenu');
if (rssButton && rssMenu && !rssButton.contains(e.target) && !rssMenu.contains(e.target)) {
rssMenu.classList.add('hidden');
}
});
</script>
</body>
</html>
{{end}}