Files
mailgo/internal/web/templates/compose.html
T
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

131 lines
6.1 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 "compose"}}
<!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>
<link href="/static/vendor/quill/quill.snow.css" rel="stylesheet">
{{template "styles" .}}
</head>
<body class="page-compose">
{{template "navbar" .}}
<div class="app-body">
{{template "sidebar" .}}
<main class="mail-main">
<div class="compose-toolbar">
<button type="submit" form="compose-form" class="btn btn-primary">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
{{t .Lang "发送"}}
</button>
<label class="tb-btn" style="cursor:pointer;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
{{t .Lang "附件"}}
<input type="file" name="attachments" id="attach-input" multiple style="display:none;">
</label>
<a href="/inbox" class="tb-btn">{{t .Lang "取消"}}</a>
<div class="toolbar-spacer"></div>
<span class="page-info">{{t .Lang "撰写新邮件"}}</span>
</div>
{{if .error}}<div class="alert alert-error" style="margin:12px 18px 0;">{{.error}}</div>{{end}}
<form id="compose-form" class="compose-form" method="POST" action="/compose" enctype="multipart/form-data">
<div class="compose-field">
<label>{{t .Lang "收件人"}}</label>
<input type="email" name="to" required value="{{.to}}" placeholder="{{t .Lang "输入收件人邮箱地址"}}">
</div>
<div class="compose-field">
<label>{{t .Lang "抄送"}}</label>
<input type="text" name="cc" value="{{.cc}}" placeholder="{{t .Lang "多个地址用逗号分隔可选"}}">
</div>
<div class="compose-field">
<label>{{t .Lang "主题"}}</label>
<input type="text" name="subject" value="{{.subject}}" placeholder="{{t .Lang "输入邮件主题"}}">
</div>
<div id="attach-chips" class="attach-chips" style="{{if not .attachments}}display:none;{{end}}"></div>
<div class="editor-wrap">
<div id="editor" data-placeholder="{{t .Lang "请输入邮件内容..."}}"></div>
<input type="hidden" name="body" id="body-hidden">
<input type="hidden" name="html_body" id="html-body-hidden">
</div>
<div class="compose-footer">
<span>{{t .Lang "附件配额"}}</span>
<span class="quota-bar" id="quota-bar" data-used="{{.usedBytes}}" data-quota="{{.quotaBytes}}"><i></i></span>
<span id="quota-text">{{formatBytes .usedBytes}} / {{formatBytes .quotaBytes}}</span>
</div>
</form>
</main>
</div>
<script src="/static/vendor/quill/quill.min.js"></script>
<script>
var quill = new Quill('#editor', {
theme: 'snow',
placeholder: document.getElementById('editor').dataset.placeholder || '{{t .Lang "请输入邮件内容..."}}',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'image'],
['clean']
]
}
});
{{if .bodyContent}}
quill.root.innerHTML = {{.bodyContent | jsonify}};
{{end}}
document.getElementById('compose-form').addEventListener('submit', function () {
document.getElementById('body-hidden').value = quill.getText();
document.getElementById('html-body-hidden').value = quill.root.innerHTML;
});
// 附件选择预览
var attachInput = document.getElementById('attach-input');
var chipsBox = document.getElementById('attach-chips');
var files = [];
function renderChips() {
chipsBox.innerHTML = '';
chipsBox.style.display = files.length ? 'flex' : 'none';
files.forEach(function (f, i) {
var chip = document.createElement('span');
chip.className = 'attach-chip';
chip.innerHTML = '📎 ' + f.name + ' (' + (f.size / 1024).toFixed(1) + ' KB)' +
'<button type="button" class="chip-del" data-i="' + i + '" title="{{t .Lang "移除"}}">✕</button>';
chipsBox.appendChild(chip);
});
}
attachInput.addEventListener('change', function () {
files = Array.prototype.slice.call(attachInput.files);
renderChips();
});
chipsBox.addEventListener('click', function (e) {
var del = e.target.closest('.chip-del');
if (!del) return;
files.splice(parseInt(del.dataset.i, 10), 1);
var dt = new DataTransfer();
files.forEach(function (f) { dt.items.add(f); });
attachInput.files = dt.files;
renderChips();
});
// 配额进度条
var bar = document.getElementById('quota-bar');
if (bar) {
var used = parseInt(bar.dataset.used, 10) || 0;
var quota = parseInt(bar.dataset.quota, 10) || 1;
var pct = Math.min(100, Math.round(used / quota * 100));
bar.querySelector('i').style.width = pct + '%';
if (pct >= 90) bar.classList.add('warn');
if (pct >= 100) bar.classList.add('over');
}
</script>
</body>
</html>
{{end}}