Files
dsh c881146a40 feat(web): 多语言界面(中/英/日)与用户语言偏好字段
- 新增 internal/i18n 包:以原始中文字符串为 key 的翻译目录(zh 恒等,
  en 兜底,ja 缺译回退 en),t/tf 模板函数与 Accept-Language 解析
  (按 q 值排序,兜底英语)
- User 模型新增 language 字段(auto|en|zh|ja,默认 auto),
  AutoMigrate 自动加列;UserStore 新增 UpdateLanguage
- 语言解析:登录用户按偏好(auto 时取浏览器 Accept-Language),
  登录/封禁页按浏览器语言;所有 c.HTML 渲染注入 Lang 模板数据
- 全部页面模板(用户页 + 管理后台 12 页)接入翻译:导航、文件夹、
  邮件列表/阅读/写信、设置、登录、封禁页、表单、表格、JS 确认框等;
  html lang 属性随语言切换;日期函数(shortDate/time12/time12m)
  与 folderLabel 改为语言感知(上午/下午 vs AM/PM vs 午前/午後)
- 设置页新增「界面语言」选择(自动/English/中文/日本語),
  /settings 表单支持单独提交语言偏好;管理后台用户表单可预设语言
- 封禁原因等入库中文文案在展示时经 t 翻译(未知 key 原样回退)
- 测试:i18n 单测(解析/回退/译文)、render 测试补 Lang 数据、
  时间格式化测试适配新签名;全量 go test 通过
2026-08-20 00:21:10 -04:00

621 lines
33 KiB
HTML
Raw Permalink 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 "styles"}}
<style>
/* ===== MailGo 设计系统(参考 QQ 邮箱布局) ===== */
:root {
--primary: #1677ff;
--primary-hover: #0e5fd8;
--primary-soft: #e8f1ff;
--orange: #ff9500;
--orange-hover: #f08800;
--danger: #e34d59;
--danger-soft: #fde8e8;
--success: #34a853;
--success-soft: #e6f7ec;
--sidebar-bg: #f6f8fc;
--topbar-h: 56px;
--border: #e5e6eb;
--border-light: #f0f1f3;
--text: #1f2329;
--text-2: #646a73;
--text-3: #8f959e;
--radius: 8px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
background: #f6f8fc; color: var(--text); font-size: 14px;
}
a { text-decoration: none; color: inherit; }
ul { list-style: none; }
input, textarea, select, button { font-family: inherit; }
input[type="checkbox"] { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; flex-shrink: 0; }
svg { flex-shrink: 0; }
a, button { touch-action: manipulation; }
/* ---------- 顶部导航栏 ---------- */
.topbar {
position: fixed; top: 0; left: 0; right: 0; height: var(--topbar-h);
background: #fff; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 28px; padding: 0 20px; z-index: 100;
}
.logo { display: flex; align-items: center; gap: 9px; }
.logo-icon {
width: 34px; height: 34px; border-radius: 9px; color: #fff; font-size: 17px;
background: linear-gradient(135deg, #1677ff, #4aa3ff);
display: inline-flex; align-items: center; justify-content: center;
box-shadow: 0 2px 6px rgba(22, 119, 255, 0.35);
}
.logo-text { font-size: 19px; font-weight: 700; color: var(--primary); letter-spacing: 0.5px; }
.logo-text em { font-style: normal; font-weight: 400; font-size: 13px; color: var(--text-2); margin-left: 3px; }
.topbar-search {
flex: 0 1 340px; height: 34px; display: none; align-items: center; gap: 8px;
background: #f2f3f5; border: 1px solid transparent; border-radius: 17px;
padding: 0 14px; color: var(--text-3);
}
body.page-list .topbar-search { display: flex; }
.topbar-search:focus-within { background: #fff; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.12); }
.topbar-search input { border: none; outline: none; background: transparent; flex: 1; font-size: 13px; color: var(--text); }
.topbar-search input::placeholder { color: var(--text-3); }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 18px; }
.topbar-link { color: var(--text-2); font-size: 13.5px; }
.topbar-link:hover { color: var(--primary); }
.user-chip { display: flex; align-items: center; gap: 8px; }
.user-email { font-size: 13px; color: var(--text-2); max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.logout-form { display: inline-flex; }
.icon-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 30px; height: 30px; border: 1px solid var(--border); border-radius: 6px;
background: #fff; color: var(--text-2); cursor: pointer;
}
.icon-btn:hover { color: var(--danger); border-color: var(--danger); }
/* ---------- 头像 ---------- */
.avatar {
width: 36px; height: 36px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-size: 15px; font-weight: 600; flex-shrink: 0; user-select: none;
}
.avatar-sm { width: 28px; height: 28px; font-size: 12.5px; }
/* ---------- 整体布局 ---------- */
.app-body { display: flex; min-height: 100vh; padding-top: var(--topbar-h); }
/* ---------- 左侧文件夹导航 ---------- */
.mail-sidebar {
width: 200px; flex-shrink: 0;
background: var(--sidebar-bg); border-right: 1px solid var(--border);
padding: 16px 12px; display: flex; flex-direction: column; gap: 14px;
position: sticky; top: var(--topbar-h); height: calc(100vh - var(--topbar-h));
}
.compose-btn {
display: flex; align-items: center; justify-content: center; gap: 7px;
height: 38px; border-radius: 19px; color: #fff; font-size: 14.5px; font-weight: 600;
background: linear-gradient(135deg, #ffa940, #ff9500);
box-shadow: 0 2px 8px rgba(255, 149, 0, 0.35);
transition: transform 0.1s, box-shadow 0.1s;
}
.compose-btn:hover { background: linear-gradient(135deg, #ff9d2e, #f08800); box-shadow: 0 3px 10px rgba(255, 149, 0, 0.45); transform: translateY(-1px); }
.folder-nav { display: flex; flex-direction: column; gap: 3px; }
.folder {
display: flex; align-items: center; gap: 10px;
height: 36px; padding: 0 10px; border-radius: 7px;
color: var(--text-2); font-size: 13.5px; position: relative;
}
.folder:hover { background: #eef1f6; color: var(--text); }
.folder.active { background: var(--primary-soft); color: var(--primary); font-weight: 600; }
.folder.active::before {
content: ""; position: absolute; left: -12px; top: 9px; bottom: 9px;
width: 3px; border-radius: 2px; background: var(--primary);
}
.folder .badge {
margin-left: auto; min-width: 20px; height: 20px; padding: 0 6px;
border-radius: 10px; background: #ff4d4f; color: #fff;
font-size: 11px; line-height: 20px; text-align: center; font-weight: 600;
}
.folder .count { margin-left: auto; color: var(--text-3); font-size: 12px; }
.sidebar-footer { margin-top: auto; border-top: 1px solid var(--border-light); padding-top: 12px; display: flex; flex-direction: column; gap: 3px; }
/* ---------- 主内容区 ---------- */
.mail-main { flex: 1; min-width: 0; background: #fff; display: flex; flex-direction: column; }
/* ---------- 列表工具条 ---------- */
.list-toolbar {
display: flex; align-items: center; gap: 10px;
padding: 10px 18px; border-bottom: 1px solid var(--border-light); background: #fff;
}
.tb-btn {
display: inline-flex; align-items: center; gap: 6px;
height: 30px; padding: 0 11px; border: 1px solid var(--border); border-radius: 6px;
background: #fff; color: var(--text-2); font-size: 13px; cursor: pointer;
}
.tb-btn:hover { color: var(--primary); border-color: var(--primary); }
.tb-btn.danger:hover { color: var(--danger); border-color: var(--danger); }
.check-all { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-2); cursor: pointer; user-select: none; }
.toolbar-spacer { flex: 1; }
.page-info { font-size: 13px; color: var(--text-3); }
/* ---------- 邮件列表 ---------- */
.mail-list { flex: 1; overflow-y: auto; }
.mail-row {
display: flex; align-items: center; gap: 10px;
padding: 0 18px; height: 54px; border-bottom: 1px solid var(--border-light);
cursor: pointer; transition: background 0.08s;
background: #fff;
}
.mail-row:not(.unread) { background: #fbfcfe; }
.mail-row:hover { background: #f2f6ff; }
.mail-row.selected { background: var(--primary-soft); }
.cell-check { display: flex; align-items: center; }
.cell-avatar { flex: 0 0 auto; }
.cell-from {
width: 150px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-size: 13.5px; color: var(--text-2);
}
.mail-row.unread .cell-from { color: var(--text); font-weight: 600; }
.cell-subject-wrap { display: flex; align-items: center; gap: 7px; flex: 0 1 36%; min-width: 150px; }
.unread-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--primary); flex-shrink: 0; }
.cell-subject {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-size: 13.5px; color: var(--text-2);
}
.mail-row.unread .cell-subject { color: var(--text); font-weight: 600; }
.cell-snippet {
flex: 1; min-width: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-size: 12.5px; color: var(--text-3);
}
.cell-date {
flex: 0 0 auto; white-space: nowrap; text-align: right;
font-size: 12.5px; color: var(--text-3);
}
.mail-row.unread .cell-date { color: var(--text-2); }
.row-del { opacity: 0; transition: opacity 0.12s; }
.mail-row:hover .row-del { opacity: 1; }
.empty-tip { padding: 90px 0; text-align: center; color: var(--text-3); font-size: 14px; }
.empty-tip .empty-icon { font-size: 46px; display: block; margin-bottom: 14px; opacity: 0.5; }
/* ---------- 列表底部分页 ---------- */
.list-footer {
display: flex; align-items: center; gap: 12px;
padding: 10px 18px; border-top: 1px solid var(--border-light);
font-size: 13px; color: var(--text-3); background: #fff;
}
.pager { margin-left: auto; display: flex; align-items: center; gap: 6px; }
.page-btn {
height: 28px; padding: 0 11px; border: 1px solid var(--border); border-radius: 6px;
background: #fff; color: var(--text-2); font-size: 13px;
display: inline-flex; align-items: center; gap: 4px;
}
.page-btn:hover:not(.disabled) { color: var(--primary); border-color: var(--primary); }
.page-btn.disabled { opacity: 0.45; cursor: not-allowed; }
.page-num { font-size: 13px; }
/* ---------- 邮件阅读页 ---------- */
.view-toolbar { display: flex; align-items: center; gap: 10px; padding: 10px 18px; border-bottom: 1px solid var(--border-light); }
.mail-head { padding: 22px 28px 0; }
.mail-title { font-size: 20px; font-weight: 600; line-height: 1.45; word-break: break-word; }
.mail-from-row { display: flex; align-items: center; gap: 12px; margin: 16px 0 18px; }
.mail-from-name { font-size: 14.5px; font-weight: 600; }
.mail-from-addr { color: var(--text-3); font-size: 12.5px; }
.mail-date { color: var(--text-3); font-size: 12.5px; margin-left: auto; }
.mail-body-wrap { padding: 22px 28px; flex: 1; overflow: auto; }
.mail-body { line-height: 1.7; font-size: 14.5px; }
.mail-body pre { white-space: pre-wrap; font-family: inherit; }
.mail-body-iframe {
width: 100%; min-height: 340px; border: 1px solid var(--border-light);
border-radius: 8px; background: #fff;
}
.attachment-list { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--border-light); }
.attachment-item {
display: inline-flex; align-items: center; gap: 7px;
margin: 0 12px 10px 0; padding: 7px 13px;
background: #f2f3f5; border-radius: 7px; font-size: 13px; color: var(--text);
}
.attachment-item a { color: var(--text); }
.attachment-item a:hover { color: var(--primary); }
.view-actions { display: flex; align-items: center; gap: 10px; padding: 18px 28px 24px; }
/* ---------- 写信页 ---------- */
.compose-toolbar { display: flex; align-items: center; gap: 10px; padding: 10px 18px; border-bottom: 1px solid var(--border-light); }
.compose-form { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.compose-field { display: flex; align-items: center; gap: 12px; padding: 9px 22px; border-bottom: 1px solid var(--border-light); }
.compose-field label { width: 54px; color: var(--text-2); font-size: 13.5px; flex-shrink: 0; }
.compose-field input { border: none; outline: none; flex: 1; font-size: 14px; color: var(--text); background: transparent; }
.editor-wrap { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.editor-wrap .ql-toolbar { border-left: none; border-right: none; border-top: none; }
.editor-wrap .ql-container { border: none; flex: 1; font-size: 14.5px; }
#editor { height: 100%; }
.attach-chips { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 22px; border-bottom: 1px solid var(--border-light); }
.attach-chip {
display: inline-flex; align-items: center; gap: 7px;
padding: 5px 11px; background: #f2f3f5; border-radius: 15px;
font-size: 12.5px; color: var(--text-2);
}
.attach-chip .chip-del { cursor: pointer; color: var(--text-3); border: none; background: none; font-size: 13px; line-height: 1; }
.attach-chip .chip-del:hover { color: var(--danger); }
.compose-footer {
display: flex; align-items: center; gap: 14px;
padding: 12px 22px; border-top: 1px solid var(--border-light);
font-size: 12.5px; color: var(--text-3); background: #fafbfc;
}
.quota-bar { width: 200px; height: 6px; background: #eceef1; border-radius: 3px; overflow: hidden; }
.quota-bar i { display: block; height: 100%; background: linear-gradient(90deg, #4aa3ff, var(--primary)); border-radius: 3px; }
.quota-bar.warn i { background: linear-gradient(90deg, #ffc53d, var(--orange)); }
.quota-bar.over i { background: var(--danger); }
/* ---------- 登录页 ---------- */
.login-page {
min-height: 100vh; display: flex; align-items: center; justify-content: center;
background: linear-gradient(165deg, #eaf2ff 0%, #f6f8fc 55%, #f0f6ff 100%);
}
.login-card {
width: 400px; max-width: calc(100vw - 32px);
background: #fff; border-radius: 14px;
box-shadow: 0 10px 40px rgba(22, 119, 255, 0.10);
padding: 42px 38px 36px;
}
.login-logo { display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 8px; }
.login-title { text-align: center; font-size: 21px; font-weight: 700; color: var(--text); margin-bottom: 26px; }
.login-sub { text-align: center; color: var(--text-3); font-size: 13px; margin-bottom: 26px; }
.divider { display: flex; align-items: center; gap: 10px; color: var(--text-3); font-size: 12.5px; margin: 18px 0; }
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--border-light); }
/* ---------- 通用组件(兼容管理后台) ---------- */
.container { max-width: 1400px; margin: 20px auto; padding: var(--topbar-h) 20px 0; }
.card {
background: #fff; border: 1px solid var(--border-light); border-radius: var(--radius);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); padding: 20px; margin-bottom: 16px;
}
.btn {
display: inline-block; padding: 8px 16px; border-radius: 6px; border: 1px solid transparent;
text-decoration: none; font-size: 14px; cursor: pointer; text-align: center;
background: #f2f3f5; color: var(--text); line-height: 1.4;
}
.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover { background: var(--primary-hover); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { background: #d23b47; }
.btn-sm { padding: 4px 10px; font-size: 12px; border-radius: 5px; }
.alert { padding: 12px 16px; border-radius: 7px; margin-bottom: 16px; font-size: 13.5px; }
.alert-error { background: var(--danger-soft); color: #c0392b; }
.alert-success { background: var(--success-soft); color: #2e9e4f; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 7px; font-weight: 600; font-size: 13.5px; }
.form-group input, .form-group textarea, .form-group select {
width: 100%; padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px;
font-size: 14px; outline: none; background: #fff; color: var(--text);
}
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
border-color: var(--primary); box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.12);
}
.form-group input[type="checkbox"] { width: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--border-light); font-size: 13.5px; }
th { background: #f8f9fb; font-weight: 600; color: var(--text-2); }
tbody tr:hover td { background: #fafbfc; }
.unread { font-weight: bold; }
.message-subject { color: var(--text); }
.message-subject:hover { color: var(--primary); }
.mail-meta { color: var(--text-2); font-size: 13px; margin-bottom: 10px; line-height: 1.9; }
.pagination { margin-top: 16px; text-align: center; }
.pagination a, .pagination span {
display: inline-block; padding: 6px 12px; margin: 0 2px;
border: 1px solid var(--border); border-radius: 6px; text-decoration: none;
color: var(--text-2); font-size: 13px;
}
.pagination .current { background: var(--primary); color: #fff; border-color: var(--primary); }
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: bold; }
.badge-unread { background: #ff4d4f; color: #fff; }
.stat-card {
display: inline-block; width: 200px; padding: 20px; margin: 0 20px 16px 0;
background: #fff; border: 1px solid var(--border-light); border-radius: var(--radius);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); text-align: center;
}
.stat-card h3 { font-size: 30px; color: var(--primary); margin-bottom: 4px; }
.stat-card p { color: var(--text-2); font-size: 13.5px; }
.dns-record {
background: #f8f9fb; padding: 12px 16px; border-radius: 6px; margin-bottom: 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12.5px; white-space: pre-wrap;
}
.clearfix::after { content: ""; display: table; clear: both; }
/* 管理后台左侧导航兼容 */
.sidebar { width: 200px; float: left; background: #fff; border: 1px solid var(--border-light); border-radius: var(--radius); padding: 8px; }
.sidebar a { display: block; padding: 9px 14px; color: var(--text-2); text-decoration: none; border-radius: 6px; margin-bottom: 2px; font-size: 13.5px; }
.sidebar a:hover, .sidebar a.active { background: var(--primary-soft); color: var(--primary); }
.content { margin-left: 220px; }
.settings-main { padding: 24px; background: var(--sidebar-bg); }
/* ===== 移动端适配(≤767px ===== */
@media (max-width: 767px) {
:root { --topbar-h: 50px; }
/* 顶栏:紧凑单行 + 列表页搜索框下移为第二行 */
.topbar { height: auto; min-height: 50px; padding: 8px 12px; gap: 8px; flex-wrap: wrap; }
.logo-icon { width: 30px; height: 30px; font-size: 15px; border-radius: 8px; }
.logo-text { font-size: 17px; }
.logo-text em { display: none; }
.topbar-search { flex: 1 1 100%; order: 9; border-radius: 8px; }
body.page-list { --topbar-h: 96px; }
.topbar-right { gap: 12px; }
.user-email { display: none; }
/* 左侧文件夹栏 → 底部导航条 */
.mail-sidebar {
position: fixed; top: auto; bottom: 0; left: 0; right: 0;
width: 100%; height: auto; min-height: 58px;
padding: 4px 8px calc(4px + env(safe-area-inset-bottom));
flex-direction: row; align-items: center; gap: 2px;
background: #fff; border-top: 1px solid var(--border); border-right: none;
z-index: 90; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
}
.folder-nav { display: contents; }
.folder {
flex: 1; flex-direction: column; justify-content: center; gap: 2px;
height: 46px; padding: 0; border-radius: 8px; font-size: 10.5px;
position: relative; order: 7; /* 自定义文件夹排在最后 */
}
.folder-nav .folder:nth-child(1) { order: 1; }
.folder-nav .folder:nth-child(2) { order: 2; }
.folder-nav .folder:nth-child(3) { order: 4; }
.folder-nav .folder:nth-child(4) { order: 6; }
.folder svg { width: 19px; height: 19px; }
.folder.active::before { display: none; }
.folder .badge {
position: absolute; top: 1px; right: calc(50% - 20px);
min-width: 16px; height: 16px; padding: 0 4px;
font-size: 10px; line-height: 16px; border-radius: 8px;
}
.folder .count { display: none; }
.compose-btn {
width: 46px; height: 46px; padding: 0; border-radius: 50%;
margin: 0 4px; flex-shrink: 0; order: 3;
font-size: 0; box-shadow: 0 3px 10px rgba(255, 149, 0, 0.5);
transform: translateY(-10px);
}
.compose-btn:hover { transform: translateY(-10px); }
.compose-btn svg { width: 20px; height: 20px; }
.sidebar-footer {
margin: 0; border: none; padding: 0;
flex-direction: row; gap: 2px; order: 8;
}
.mail-main { padding-bottom: calc(58px + env(safe-area-inset-bottom)); }
/* 邮件列表 */
.list-toolbar { padding: 8px 10px; gap: 8px; }
.check-all { font-size: 12.5px; }
.tb-btn { height: 30px; padding: 0 9px; font-size: 12.5px; }
.page-info { font-size: 12px; }
.mail-row { padding: 0 10px; height: 60px; gap: 8px; }
.avatar { width: 32px; height: 32px; font-size: 13px; }
.cell-from { width: 76px; font-size: 13px; }
.cell-subject-wrap { flex: 1; min-width: 0; }
.cell-subject { font-size: 13px; }
.cell-snippet { display: none; }
.cell-date { width: auto; font-size: 10.5px; }
.row-del { opacity: 1; }
.list-footer { padding: 8px 10px; }
.pager { margin-left: auto; gap: 4px; }
/* 阅读页 */
.view-toolbar { padding: 8px 10px; gap: 8px; }
.mail-head { padding: 16px 16px 0; }
.mail-title { font-size: 17px; }
.mail-from-row { gap: 10px; margin: 12px 0 14px; }
.mail-body-wrap { padding: 14px 16px; }
.mail-body-iframe { min-height: 260px; }
.view-actions { padding: 14px 16px 20px; flex-wrap: wrap; }
/* 写信页 */
.compose-toolbar { padding: 8px 10px; gap: 8px; }
.compose-field { padding: 8px 14px; gap: 8px; }
.compose-field label { width: 44px; font-size: 13px; }
.compose-footer { padding: 10px 14px; flex-wrap: wrap; }
.quota-bar { width: 130px; }
/* 设置页 */
.settings-main { padding: 14px; }
/* 管理后台 */
.container { padding: var(--topbar-h) 12px 0; margin: 12px auto; }
.sidebar { float: none; width: 100%; margin-bottom: 12px; display: flex; flex-wrap: wrap; gap: 2px; padding: 6px; }
.sidebar a { margin: 0; padding: 8px 12px; font-size: 13px; }
.content { margin-left: 0; overflow-x: auto; }
th, td { white-space: nowrap; }
.stat-card { width: calc(50% - 10px); margin: 0 0 10px; }
}
</style>
{{end}}
{{define "navbar"}}
{{if .currentUser}}
<header class="topbar">
<div class="topbar-left">
<a class="logo" href="/inbox">
<span class="logo-icon"></span>
<span class="logo-text">MailGo<em>{{t .Lang "邮箱"}}</em></span>
</a>
</div>
<div class="topbar-search">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input id="mail-search" type="text" placeholder="{{t .Lang "搜索邮件发件人 / 主题"}}" autocomplete="off">
</div>
<div class="topbar-right">
{{if .currentUser.IsAdmin}}<a class="topbar-link" href="/admin">{{t .Lang "管理后台"}}</a>{{end}}
<a class="topbar-link" href="/settings">{{t .Lang "设置"}}</a>
<span class="user-chip">
<span class="avatar avatar-sm" style="{{avatarStyle .currentUser.Username}}">{{initial .currentUser.Username}}</span>
<span class="user-email" title="{{.currentUser.Username}}@{{.currentUser.Domain.Name}}">{{.currentUser.Username}}@{{.currentUser.Domain.Name}}</span>
</span>
<form method="POST" action="/logout" class="logout-form">
<button type="submit" class="icon-btn" title="{{t .Lang "退出登录"}}">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
</button>
</form>
</div>
</header>
{{end}}
{{end}}
{{define "sidebar"}}
<aside class="mail-sidebar">
<a href="/compose" class="compose-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
{{t .Lang "写信"}}
</a>
<nav class="folder-nav">
{{range .folders}}
<a class="folder {{if eq $.activeFolder .Name}}active{{end}}" href="/folder/{{urlPath .Name}}">
{{if eq .SpecialUse "Sent"}}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
{{else if eq .SpecialUse "Drafts"}}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
{{else if eq .SpecialUse "Trash"}}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
{{else if eq .Name "INBOX"}}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
{{else}}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
{{end}}
{{folderLabel $.Lang .Name}}
{{if eq .Name "INBOX"}}
{{if .Unseen}}<span class="badge">{{.Unseen}}</span>{{end}}
{{else if .Total}}<span class="count">{{.Total}}</span>{{end}}
</a>
{{end}}
</nav>
<div class="sidebar-footer">
{{if .currentUser.IsAdmin}}
<a class="folder" href="/admin">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
{{t .Lang "管理后台"}}
</a>
{{end}}
</div>
</aside>
{{end}}
{{define "listjs"}}
<script>
(function () {
var searchInput = document.getElementById('mail-search');
var rows = Array.prototype.slice.call(document.querySelectorAll('.mail-row'));
var selectAll = document.getElementById('select-all');
var btnDelete = document.getElementById('btn-delete');
// 行点击跳转(复选框 / 行内删除按钮除外)
rows.forEach(function (row) {
row.addEventListener('click', function (e) {
if (e.target.closest('.cell-check') || e.target.closest('.row-del')) return;
var link = row.querySelector('.cell-subject');
if (link) window.location.href = link.getAttribute('href');
});
});
// 全选
selectAll && selectAll.addEventListener('change', function () {
rows.forEach(function (row) {
var cb = row.querySelector('.row-check');
cb.checked = selectAll.checked;
row.classList.toggle('selected', cb.checked);
});
updateDeleteState();
});
rows.forEach(function (row) {
var cb = row.querySelector('.row-check');
cb.addEventListener('change', function () {
row.classList.toggle('selected', cb.checked);
if (!cb.checked && selectAll) selectAll.checked = false;
updateDeleteState();
});
});
function updateDeleteState() {
if (!btnDelete) return;
var n = rows.filter(function (r) { return r.isConnected && r.querySelector('.row-check').checked; }).length;
btnDelete.disabled = n === 0;
}
// 批量删除
btnDelete && btnDelete.addEventListener('click', function () {
var ids = rows.filter(function (r) { return r.isConnected && r.querySelector('.row-check').checked; })
.map(function (r) { return r.dataset.id; });
if (!ids.length) return;
if (!confirm('{{t .Lang "确定要删除选中的 %s 封邮件吗?"}}'.replace('%s', ids.length))) return;
var done = 0;
ids.forEach(function (id) {
fetch('/mail/delete/' + id, { method: 'POST', body: new FormData() })
.then(function () { if (++done === ids.length) window.location.reload(); })
.catch(function () { if (++done === ids.length) window.location.reload(); });
});
});
// 刷新
var btnRefresh = document.getElementById('btn-refresh');
btnRefresh && btnRefresh.addEventListener('click', function () { window.location.reload(); });
// 搜索过滤(发件人 / 主题 / 摘要)
searchInput && searchInput.addEventListener('input', function () {
var q = searchInput.value.trim().toLowerCase();
rows.forEach(function (row) {
if (!row.isConnected) return;
var text = row.textContent.toLowerCase();
row.style.display = (!q || text.indexOf(q) !== -1) ? '' : 'none';
});
});
// 从 BFCache(往返缓存)恢复时静默同步最新列表状态:
// 阅读页已把邮件标记为已读/删除,恢复的旧 DOM 需要与服务端对齐,
// 避免“返回后仍是未读/已删邮件仍显示”。
// 真实浏览器在 BFCache 恢复时会触发 pageshow(persisted=true)
// visibilitychange 作为兜底(页面重新可见时也同步一次)。
var syncing = false;
function syncFromServer(forceReloadOnError) {
if (syncing) return;
syncing = true;
fetch(window.location.href, { headers: { 'Accept': 'text/html' }, credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (html) {
var doc = new DOMParser().parseFromString(html, 'text/html');
var fresh = {};
doc.querySelectorAll('.mail-row').forEach(function (r) { fresh[r.dataset.id] = r.classList.contains('unread'); });
rows.forEach(function (row) {
var id = row.dataset.id;
if (!(id in fresh)) { row.remove(); return; } // 已被删除
row.classList.toggle('unread', fresh[id]);
var wrap = row.querySelector('.cell-subject-wrap');
var dot = row.querySelector('.unread-dot');
if (fresh[id] && !dot && wrap) {
var s = document.createElement('span');
s.className = 'unread-dot';
wrap.insertBefore(s, wrap.firstChild);
} else if (!fresh[id] && dot) {
dot.remove();
}
});
// 同步“共 N 封”与侧栏未读角标
var total = doc.querySelector('.page-info');
var curTotal = document.querySelector('.page-info');
if (total && curTotal) curTotal.textContent = total.textContent;
var badge = doc.querySelector('.folder.active .badge');
var curBadge = document.querySelector('.folder.active .badge');
if (badge && curBadge) curBadge.textContent = badge.textContent;
else if (!badge && curBadge) curBadge.remove();
})
.catch(function () {
// BFCache 恢复时 DOM 是旧的,同步失败只能整页刷新兜底;
// 标签页切回触发的同步失败则静默忽略(可能只是离线)。
if (forceReloadOnError) window.location.reload();
})
.then(function () { syncing = false; });
}
window.addEventListener('pageshow', function (e) {
if (e.persisted) syncFromServer(true);
});
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') syncFromServer(false);
});
})();
</script>
{{end}}