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

133 lines
8.7 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 "admin_protocol_logs"}}
<!DOCTYPE html>
<html lang="{{htmlLang .Lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>{{t .Lang "协议日志"}} - MailGo</title>
{{template "styles" .}}
</head>
<body>
{{template "navbar" .}}
<div class="container">
<div class="clearfix">
<div class="sidebar">
<a href="/inbox">{{t .Lang "返回邮箱"}}</a>
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>{{t .Lang "控制面板"}}</a>
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>{{t .Lang "域名管理"}}</a>
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>{{t .Lang "用户管理"}}</a>
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>{{t .Lang "所有邮件"}}</a>
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>{{t .Lang "外发队列"}}</a>
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>{{t .Lang "协议日志"}}</a>
<a href="/admin/connections" {{if eq .activeFolder "connections"}}class="active"{{end}}>{{t .Lang "当前连接"}}</a>
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>{{t .Lang "IP黑名单"}}</a>
</div>
<div class="content">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h2>{{t .Lang "协议日志(SMTP / IMAP / POP3"}}</h2>
<form method="POST" action="/admin/protocol-logs/cleanup" style="display:inline;"
onsubmit="return confirm('{{tf .Lang "确认清理 %d 天前的协议日志" .keepDays}}');">
<button type="submit" class="btn btn-primary">{{t .Lang "清理旧日志"}}</button>
</form>
</div>
<div style="margin-bottom:24px;">
<div class="stat-card">
<h3>{{index .todayStats "smtp" "fail"}}</h3>
<p>{{t .Lang "今日 SMTP 失败"}}</p>
</div>
<div class="stat-card">
<h3>{{index .todayStats "imap" "fail"}}</h3>
<p>{{t .Lang "今日 IMAP 失败"}}</p>
</div>
<div class="stat-card">
<h3>{{index .todayStats "pop3" "fail"}}</h3>
<p>{{t .Lang "今日 POP3 失败"}}</p>
</div>
<div class="stat-card">
<h3>{{add (add (index .allStats "smtp" "fail") (index .allStats "imap" "fail")) (index .allStats "pop3" "fail")}}</h3>
<p>{{t .Lang "历史失败(全部)"}}</p>
</div>
</div>
<div class="card">
<form method="GET" action="/admin/protocol-logs" style="margin-bottom:16px;">
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;">
<select name="protocol" style="padding:6px 10px;">
<option value="">{{t .Lang "全部协议"}}</option>
<option value="smtp" {{if eq .filter.protocol "smtp"}}selected{{end}}>SMTP</option>
<option value="imap" {{if eq .filter.protocol "imap"}}selected{{end}}>IMAP</option>
<option value="pop3" {{if eq .filter.protocol "pop3"}}selected{{end}}>POP3</option>
</select>
<select name="success" style="padding:6px 10px;">
<option value="">{{t .Lang "全部状态"}}</option>
<option value="success" {{if eq .filter.success "success"}}selected{{end}}>{{t .Lang "成功"}}</option>
<option value="fail" {{if eq .filter.success "fail"}}selected{{end}}>{{t .Lang "失败"}}</option>
</select>
<input type="text" name="ip" placeholder="{{t .Lang "来源 IP模糊"}}" value="{{.filter.ip}}" style="padding:6px 10px;width:160px;">
<input type="text" name="username" placeholder="{{t .Lang "用户名模糊"}}" value="{{.filter.username}}" style="padding:6px 10px;width:160px;">
<input type="date" name="from" value="{{.filter.from}}" style="padding:6px 10px;">
<span>{{t .Lang "至"}}</span>
<input type="date" name="to" value="{{.filter.to}}" style="padding:6px 10px;">
<button type="submit" class="btn btn-sm btn-primary">{{t .Lang "筛选"}}</button>
<a href="/admin/protocol-logs" class="btn btn-sm">{{t .Lang "重置"}}</a>
</div>
</form>
<table>
<thead>
<tr>
<th>{{t .Lang "时间"}}</th>
<th>{{t .Lang "协议"}}</th>
<th>{{t .Lang "端口"}}</th>
<th>{{t .Lang "来源 IP"}}</th>
<th>{{t .Lang "用户名"}}</th>
<th>{{t .Lang "状态"}}</th>
<th>{{t .Lang "失败原因"}}</th>
<th>{{t .Lang "操作摘要"}}</th>
<th>{{t .Lang "消息数"}}</th>
<th>{{t .Lang "时长"}}</th>
</tr>
</thead>
<tbody>
{{range .logs}}
<tr>
<td style="white-space:nowrap;">{{time12 $.Lang .CreatedAt}}</td>
<td>
{{if eq .Protocol "smtp"}}<span class="badge" style="background:#3498db;color:#fff;">SMTP</span>
{{else if eq .Protocol "imap"}}<span class="badge" style="background:#9b59b6;color:#fff;">IMAP</span>
{{else}}<span class="badge" style="background:#16a085;color:#fff;">POP3</span>{{end}}
</td>
<td>{{.Port}}</td>
<td>{{.ClientIP}}</td>
<td>{{if .Username}}{{.Username}}{{else}}—{{end}}</td>
<td>
{{if .Success}}<span class="badge" style="background:#27ae60;color:#fff;">{{t $.Lang "成功"}}</span>
{{else}}<span class="badge badge-unread">{{t $.Lang "失败"}}</span>{{end}}
</td>
<td style="max-width:200px;">{{if .FailReason}}{{.FailReason}}{{else}}—{{end}}</td>
<td style="max-width:320px;word-break:break-all;font-size:13px;color:#555;">{{.Detail}}</td>
<td>{{if .MsgCount}}{{.MsgCount}}{{else}}—{{end}}</td>
<td>{{if .DurationMs}}{{div .DurationMs 1000}}s{{else}}—{{end}}</td>
</tr>
{{else}}
<tr><td colspan="10" style="text-align:center;color:#7f8c8d;">{{t $.Lang "暂无记录"}}</td></tr>
{{end}}
</tbody>
</table>
{{if gt .totalPages 1}}
<div class="pagination">
{{if gt .page 1}}<a href="/admin/protocol-logs?page={{sub .page 1}}&protocol={{.filter.protocol}}&success={{.filter.success}}&ip={{.filter.ip}}&username={{.filter.username}}&from={{.filter.from}}&to={{.filter.to}}">{{t $.Lang "上一页"}}</a>{{end}}
<span class="current">{{tf $.Lang "第 %d / %d 页(共 %d 条)" .page .totalPages .total}}</span>
{{if lt .page .totalPages}}<a href="/admin/protocol-logs?page={{add .page 1}}&protocol={{.filter.protocol}}&success={{.filter.success}}&ip={{.filter.ip}}&username={{.filter.username}}&from={{.filter.from}}&to={{.filter.to}}">{{t $.Lang "下一页"}}</a>{{end}}
</div>
{{end}}
</div>
</div>
</div>
</div>
</body>
</html>
{{end}}