Files
outlookEmailPlus/templates/index.html
T
2026-01-23 18:00:03 +08:00

3853 lines
150 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Outlook 邮件管理</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #ffffff;
color: #1a1a1a;
line-height: 1.5;
}
/* 顶部导航栏 */
.navbar {
height: 56px;
background-color: #ffffff;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
padding: 0 24px;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
.navbar-brand {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
display: flex;
align-items: center;
gap: 8px;
}
.navbar-brand svg {
width: 24px;
height: 24px;
}
.navbar-center {
flex: 1;
display: flex;
justify-content: center;
}
.current-account {
font-size: 14px;
color: #666;
background-color: #f5f5f5;
padding: 6px 16px;
border-radius: 20px;
display: none;
}
.current-account.show {
display: block;
}
.current-account strong {
color: #1a1a1a;
font-weight: 500;
}
.navbar-actions {
display: flex;
gap: 12px;
}
.navbar-btn {
background: none;
border: 1px solid #e5e5e5;
color: #666;
cursor: pointer;
font-size: 13px;
padding: 6px 12px;
border-radius: 6px;
transition: all 0.15s;
}
.navbar-btn:hover {
background-color: #f5f5f5;
border-color: #ccc;
}
/* 主容器 */
.main-container {
display: flex;
margin-top: 56px;
height: calc(100vh - 56px);
}
/* 分组面板 */
.group-panel {
width: 200px;
background-color: #fafafa;
border-right: 1px solid #e5e5e5;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.panel-header {
padding: 0 16px;
height: 52px;
border-bottom: 1px solid #e5e5e5;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
}
.panel-action-btn {
background: none;
border: none;
color: #666;
cursor: pointer;
font-size: 18px;
padding: 2px 6px;
border-radius: 4px;
line-height: 1;
}
.panel-action-btn:hover {
background-color: #e5e5e5;
}
.group-list {
flex: 1;
overflow-y: auto;
}
.group-item {
padding: 10px 16px;
cursor: pointer;
transition: background-color 0.15s;
border-left: 3px solid transparent;
}
.group-item:hover {
background-color: #f0f0f0;
}
.group-item.active {
background-color: #e8e8e8;
border-left-color: #1a1a1a;
}
.group-row-1 {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
}
.group-color {
width: 10px;
height: 10px;
border-radius: 3px;
flex-shrink: 0;
}
.group-name {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.group-row-2 {
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 20px;
}
.group-count {
font-size: 12px;
color: #999;
background-color: #f0f0f0;
padding: 2px 8px;
border-radius: 10px;
}
.group-item.active .group-count {
background-color: #d8d8d8;
}
.group-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.15s;
}
.group-item:hover .group-actions {
opacity: 1;
}
.group-action-btn {
background: none;
border: none;
color: #999;
cursor: pointer;
padding: 2px 6px;
font-size: 12px;
border-radius: 4px;
}
.group-action-btn:hover {
background-color: #e0e0e0;
color: #666;
}
/* 邮箱账号面板 */
.account-panel {
width: 260px;
background-color: #ffffff;
border-right: 1px solid #e5e5e5;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.account-panel-header {
padding: 0 16px;
height: 52px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
}
.account-panel-title {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
display: flex;
align-items: center;
gap: 8px;
}
.account-panel-title .group-color {
width: 12px;
height: 12px;
}
.account-list {
flex: 1;
overflow-y: auto;
}
.account-panel-footer {
padding: 16px;
border-top: 1px solid #e5e5e5;
}
.add-account-btn {
width: 100%;
padding: 10px 16px;
background-color: #1a1a1a;
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.add-account-btn:hover {
background-color: #333;
}
.account-item {
padding: 10px 16px;
cursor: pointer;
transition: background-color 0.15s;
border-left: 3px solid transparent;
border-bottom: 1px solid #f5f5f5;
}
.account-item:hover {
background-color: #f5f5f5;
}
.account-item.active {
background-color: #f0f0f0;
border-left-color: #1a1a1a;
}
.account-item.inactive {
opacity: 0.6;
}
.account-item.inactive .account-email {
text-decoration: line-through;
color: #999;
}
.account-email {
font-size: 13px;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 6px;
}
.account-status-tag {
font-size: 10px;
padding: 1px 6px;
border-radius: 3px;
margin-left: 8px;
background-color: #f0f0f0;
color: #999;
}
.account-actions {
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.15s;
}
.account-item:hover .account-actions {
opacity: 1;
}
.account-action-btn {
background: none;
border: none;
color: #666;
cursor: pointer;
padding: 2px 0;
font-size: 11px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 2px;
}
.account-action-btn:hover {
color: #1a1a1a;
}
.account-action-btn.delete:hover {
color: #dc3545;
}
/* 内容区域 */
.content-area {
flex: 1;
display: flex;
overflow: hidden;
}
/* 邮件列表面板 */
.email-list-panel {
width: 380px;
background-color: #ffffff;
border-right: 1px solid #e5e5e5;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.email-list-panel.hidden {
display: none;
}
/* 邮件列表头部 */
.email-list-header {
padding: 16px 20px;
border-bottom: 1px solid #e5e5e5;
background-color: #ffffff;
}
.email-list-header h2 {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
}
.email-count {
font-size: 13px;
color: #999;
font-weight: 400;
margin-left: 8px;
}
.refresh-btn {
background: none;
border: 1px solid #e5e5e5;
color: #666;
cursor: pointer;
font-size: 13px;
padding: 6px 12px;
border-radius: 6px;
transition: all 0.15s;
}
.refresh-btn:hover {
background-color: #f5f5f5;
border-color: #ccc;
}
.email-list {
flex: 1;
overflow-y: auto;
}
.email-item {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.15s;
}
.email-item:hover {
background-color: #fafafa;
}
.email-item.active {
background-color: #f5f5f5;
}
.email-item.unread .email-subject {
font-weight: 600;
}
.email-from {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.email-subject {
font-size: 14px;
color: #1a1a1a;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.email-preview {
font-size: 13px;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.email-date {
font-size: 12px;
color: #999;
margin-top: 8px;
}
/* 邮件详情面板 */
.email-detail-panel {
flex: 1;
background-color: #ffffff;
display: flex;
flex-direction: column;
min-width: 0;
}
.email-detail-toolbar {
padding: 12px 24px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
gap: 12px;
background-color: #fafafa;
}
.back-btn {
display: none;
background: none;
border: 1px solid #e5e5e5;
color: #666;
cursor: pointer;
font-size: 13px;
padding: 6px 12px;
border-radius: 6px;
transition: all 0.15s;
}
.back-btn:hover {
background-color: #f0f0f0;
}
.toggle-list-btn {
background: none;
border: 1px solid #e5e5e5;
color: #666;
cursor: pointer;
font-size: 13px;
padding: 6px 12px;
border-radius: 6px;
transition: all 0.15s;
}
.toggle-list-btn:hover {
background-color: #f0f0f0;
}
.email-detail-header {
padding: 24px 32px;
border-bottom: 1px solid #e5e5e5;
}
.email-detail-subject {
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
line-height: 1.3;
}
.email-detail-meta {
font-size: 14px;
color: #666;
line-height: 1.8;
}
.email-detail-meta-row {
display: flex;
margin-bottom: 4px;
}
.email-detail-meta-label {
color: #999;
width: 60px;
flex-shrink: 0;
}
.email-detail-meta-value {
color: #1a1a1a;
}
.email-detail-body {
flex: 1;
padding: 24px 32px;
overflow-y: auto;
background-color: #ffffff;
min-height: 0;
}
.email-detail-body iframe {
width: 100%;
min-height: 600px;
border: none;
background-color: #ffffff;
display: block;
overflow: visible;
}
.email-body-text {
white-space: pre-wrap;
font-size: 15px;
line-height: 1.7;
color: #333;
}
/* 全屏查看邮件 */
.fullscreen-email-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.95);
z-index: 2000;
overflow: hidden;
}
.fullscreen-email-modal.show {
display: flex;
flex-direction: column;
}
.fullscreen-email-header {
background-color: #1a1a1a;
color: #ffffff;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.fullscreen-email-title {
font-size: 16px;
font-weight: 500;
}
.fullscreen-close-btn {
background: none;
border: none;
color: #ffffff;
font-size: 24px;
cursor: pointer;
padding: 4px 12px;
border-radius: 4px;
transition: background-color 0.2s;
}
.fullscreen-close-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.fullscreen-email-content {
flex: 1;
overflow-y: auto;
background-color: #ffffff;
padding: 32px;
}
.fullscreen-email-content iframe {
width: 100%;
border: none;
background-color: #ffffff;
display: block;
}
.fullscreen-email-content .email-body-text {
max-width: 900px;
margin: 0 auto;
}
.fullscreen-btn {
background: none;
border: 1px solid #e5e5e5;
color: #666;
cursor: pointer;
font-size: 13px;
padding: 6px 12px;
border-radius: 6px;
transition: all 0.15s;
margin-left: auto;
}
.fullscreen-btn:hover {
background-color: #f0f0f0;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
padding: 40px;
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state-text {
font-size: 15px;
text-align: center;
}
/* 加载状态 */
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 40px;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 2px solid #e5e5e5;
border-top-color: #1a1a1a;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loading-small {
padding: 20px;
}
.loading-small .loading-spinner {
width: 20px;
height: 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 模态框 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.4);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
.modal-content {
background-color: #ffffff;
border-radius: 12px;
width: 520px;
max-width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
}
.modal-header {
padding: 20px 24px;
border-bottom: 1px solid #e5e5e5;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
background-color: #ffffff;
}
.modal-header h3 {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
padding: 4px;
line-height: 1;
border-radius: 4px;
transition: all 0.15s;
}
.modal-close:hover {
background-color: #f0f0f0;
color: #666;
}
.modal-body {
padding: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.form-input {
width: 100%;
padding: 10px 14px;
border: 1px solid #e5e5e5;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.15s;
}
.form-input:focus {
outline: none;
border-color: #1a1a1a;
}
.form-textarea {
width: 100%;
height: 120px;
padding: 12px 14px;
border: 1px solid #e5e5e5;
border-radius: 6px;
font-size: 13px;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
resize: vertical;
transition: border-color 0.15s;
}
.form-textarea:focus {
outline: none;
border-color: #1a1a1a;
}
.form-select {
width: 100%;
padding: 10px 14px;
border: 1px solid #e5e5e5;
border-radius: 6px;
font-size: 14px;
background-color: #ffffff;
cursor: pointer;
}
.form-select:focus {
outline: none;
border-color: #1a1a1a;
}
.form-hint {
font-size: 12px;
color: #999;
margin-top: 6px;
}
.color-picker-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.color-option {
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.15s;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.selected {
border-color: #1a1a1a;
}
.custom-color-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
}
.custom-color-input {
width: 60px;
height: 36px;
padding: 2px;
border: 1px solid #e5e5e5;
border-radius: 6px;
cursor: pointer;
}
.custom-color-label {
font-size: 13px;
color: #666;
}
.custom-color-hex {
width: 80px;
padding: 8px 10px;
border: 1px solid #e5e5e5;
border-radius: 6px;
font-size: 13px;
font-family: monospace;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid #e5e5e5;
display: flex;
justify-content: flex-end;
gap: 12px;
position: sticky;
bottom: 0;
background-color: #ffffff;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.15s;
}
.btn-primary {
background-color: #1a1a1a;
color: #ffffff;
}
.btn-primary:hover {
background-color: #333;
}
.btn-secondary {
background-color: #f5f5f5;
color: #1a1a1a;
border: 1px solid #e5e5e5;
}
.btn-secondary:hover {
background-color: #e5e5e5;
}
.btn-danger {
background-color: #dc3545;
color: #ffffff;
}
.btn-danger:hover {
background-color: #c82333;
}
.btn-warning {
background-color: #ffc107;
color: #1a1a1a;
}
.btn-warning:hover {
background-color: #e0a800;
}
.btn-sm {
padding: 4px 12px;
font-size: 12px;
}
/* 消息提示 */
.toast {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
padding: 14px 24px;
background-color: #1a1a1a;
color: #ffffff;
border-radius: 8px;
font-size: 14px;
z-index: 2000;
opacity: 0;
transition: all 0.3s;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
min-width: 300px;
text-align: center;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.toast.success {
background-color: #1a1a1a;
}
.toast.error {
background-color: #dc3545;
}
/* 方法标签 */
.method-tag {
font-size: 11px;
padding: 3px 8px;
background-color: #f5f5f5;
color: #666;
border-radius: 4px;
margin-left: 8px;
font-weight: 500;
}
/* 文件夹切换标签 */
.folder-tabs {
display: inline-flex;
gap: 8px;
background-color: #f5f5f5;
padding: 4px;
border-radius: 8px;
}
.folder-tab {
padding: 8px 16px;
border: none;
background: transparent;
color: #666;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
font-weight: 500;
white-space: nowrap;
}
.folder-tab:hover {
background-color: #e5e5e5;
color: #333;
}
.folder-tab.active {
background-color: #1a1a1a;
color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 响应式 */
@media (max-width: 1200px) {
.group-panel {
width: 180px;
}
.account-panel {
width: 220px;
}
.email-list-panel {
width: 320px;
}
}
@media (max-width: 1024px) {
.group-panel {
width: 160px;
}
.account-panel {
width: 200px;
}
.email-list-panel {
width: 280px;
}
}
@media (max-width: 768px) {
.group-panel,
.account-panel {
position: fixed;
left: -280px;
top: 56px;
height: calc(100vh - 56px);
z-index: 50;
transition: left 0.3s;
}
.group-panel.show,
.account-panel.show {
left: 0;
}
.email-list-panel {
width: 100%;
}
.email-list-panel.hidden {
display: none;
}
.back-btn {
display: block;
}
}
</style>
</head>
<body>
<!-- 顶部导航栏 -->
<nav class="navbar">
<div class="navbar-brand">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
Outlook 邮件
</div>
<div class="navbar-center">
<div class="current-account" id="currentAccount">
当前邮箱:<strong id="currentAccountEmail"></strong>
</div>
</div>
<div class="navbar-actions">
<button class="navbar-btn" onclick="showRefreshModal()">全量刷新Token</button>
<button class="navbar-btn" onclick="showExportModal()">导出邮箱</button>
<button class="navbar-btn" onclick="showSettingsModal()">⚙️ 设置</button>
<button class="navbar-btn" onclick="logout()" style="color: #dc3545;">退出登录</button>
</div>
</nav>
<!-- 主容器 -->
<div class="main-container">
<!-- 分组面板 -->
<div class="group-panel" id="groupPanel">
<div class="panel-header">
<span class="panel-title">分组</span>
<button class="panel-action-btn" onclick="showAddGroupModal()" title="添加分组">+</button>
</div>
<div class="group-list" id="groupList">
<!-- 分组列表将通过 JS 动态加载 -->
</div>
</div>
<!-- 邮箱账号面板 -->
<div class="account-panel" id="accountPanel">
<div class="account-panel-header">
<div class="account-panel-title" id="accountPanelTitle">
<span class="group-color" id="currentGroupColor" style="background-color: #666"></span>
<span id="currentGroupName">选择分组</span>
</div>
</div>
<div class="account-list" id="accountList">
<div class="empty-state">
<div class="empty-state-icon">📁</div>
<div class="empty-state-text">请从左侧选择一个分组</div>
</div>
</div>
<div class="account-panel-footer">
<button class="add-account-btn" onclick="showAddAccountModal()">+ 导入邮箱</button>
</div>
</div>
<!-- 内容区域 -->
<div class="content-area">
<!-- 邮件列表面板 -->
<div class="email-list-panel" id="emailListPanel">
<div class="email-list-header">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
<div class="folder-tabs" id="folderTabs" style="display: none;">
<button class="folder-tab active" data-folder="inbox" onclick="switchFolder('inbox')">
📨 收件箱
</button>
<button class="folder-tab" data-folder="junkemail" onclick="switchFolder('junkemail')">
⚠️ 垃圾邮件
</button>
</div>
<button class="refresh-btn" onclick="refreshEmails()">获取邮件</button>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<h2 style="margin: 0; font-size: 16px; font-weight: 600;">
邮件
<span class="email-count" id="emailCount"></span>
</h2>
<span class="method-tag" id="methodTag" style="display:none;"></span>
</div>
</div>
<div class="email-list" id="emailList">
<div class="empty-state">
<div class="empty-state-icon">📬</div>
<div class="empty-state-text">请从左侧选择一个邮箱账号</div>
</div>
</div>
</div>
<!-- 邮件详情面板 -->
<div class="email-detail-panel">
<div class="email-detail-toolbar" id="emailDetailToolbar" style="display:none;">
<button class="back-btn" onclick="showEmailList()">← 返回</button>
<button class="toggle-list-btn" onclick="toggleEmailList()">
<span id="toggleListText">隐藏列表</span>
</button>
<button class="fullscreen-btn" onclick="openFullscreenEmail()">🔍 全屏查看</button>
</div>
<div id="emailDetail">
<div class="empty-state">
<div class="empty-state-icon">📄</div>
<div class="empty-state-text">选择一封邮件查看详情</div>
</div>
</div>
</div>
</div>
</div>
<!-- 添加分组模态框 -->
<div class="modal" id="addGroupModal">
<div class="modal-content">
<div class="modal-header">
<h3 id="groupModalTitle">添加分组</h3>
<button class="modal-close" onclick="hideAddGroupModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">分组名称</label>
<input type="text" class="form-input" id="groupName" placeholder="输入分组名称">
</div>
<div class="form-group">
<label class="form-label">分组描述</label>
<input type="text" class="form-input" id="groupDescription" placeholder="可选">
</div>
<div class="form-group">
<label class="form-label">分组颜色</label>
<div class="color-picker-group" id="colorPicker">
<div class="color-option selected" style="background-color: #1a1a1a" data-color="#1a1a1a"></div>
<div class="color-option" style="background-color: #666666" data-color="#666666"></div>
<div class="color-option" style="background-color: #0078d4" data-color="#0078d4"></div>
<div class="color-option" style="background-color: #107c10" data-color="#107c10"></div>
<div class="color-option" style="background-color: #ff8c00" data-color="#ff8c00"></div>
<div class="color-option" style="background-color: #e81123" data-color="#e81123"></div>
<div class="color-option" style="background-color: #5c2d91" data-color="#5c2d91"></div>
<div class="color-option" style="background-color: #008272" data-color="#008272"></div>
<div class="color-option" style="background-color: #00bcf2" data-color="#00bcf2"></div>
<div class="color-option" style="background-color: #bad80a" data-color="#bad80a"></div>
<div class="color-option" style="background-color: #ff6f61" data-color="#ff6f61"></div>
<div class="color-option" style="background-color: #744da9" data-color="#744da9"></div>
<div class="color-option" style="background-color: #f7630c" data-color="#f7630c"></div>
<div class="color-option" style="background-color: #038387" data-color="#038387"></div>
<div class="color-option" style="background-color: #c239b3" data-color="#c239b3"></div>
<div class="color-option" style="background-color: #6b69d6" data-color="#6b69d6"></div>
</div>
<div class="custom-color-row">
<input type="color" class="custom-color-input" id="customColorInput" value="#1a1a1a" onchange="selectCustomColor(this.value)">
<span class="custom-color-label">自定义颜色</span>
<input type="text" class="custom-color-hex" id="customColorHex" placeholder="#000000" maxlength="7" onchange="selectCustomColorHex(this.value)">
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideAddGroupModal()">取消</button>
<button class="btn btn-primary" onclick="saveGroup()">保存</button>
</div>
</div>
</div>
<!-- 添加账号模态框 -->
<div class="modal" id="addAccountModal">
<div class="modal-content">
<div class="modal-header">
<h3>导入邮箱账号</h3>
<button class="modal-close" onclick="hideAddAccountModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">选择分组</label>
<select class="form-select" id="importGroupSelect">
<!-- 分组选项将通过 JS 动态加载 -->
</select>
</div>
<div class="form-group">
<label class="form-label">账号信息</label>
<textarea class="form-textarea" id="accountInput" placeholder="邮箱----密码----client_id----refresh_token"></textarea>
<div class="form-hint">格式:邮箱----密码----client_id----refresh_token,支持批量导入(每行一个)</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideAddAccountModal()">取消</button>
<button class="btn btn-primary" onclick="addAccount()">导入</button>
</div>
</div>
</div>
<!-- 编辑账号模态框 -->
<div class="modal" id="editAccountModal">
<div class="modal-content">
<div class="modal-header">
<h3>编辑邮箱账号</h3>
<button class="modal-close" onclick="hideEditAccountModal()">&times;</button>
</div>
<div class="modal-body">
<input type="hidden" id="editAccountId">
<div class="form-group">
<label class="form-label">邮箱地址</label>
<input type="text" class="form-input" id="editEmail" placeholder="example@outlook.com">
</div>
<div class="form-group">
<label class="form-label">密码</label>
<input type="text" class="form-input" id="editPassword" placeholder="可选">
</div>
<div class="form-group">
<label class="form-label">Client ID</label>
<input type="text" class="form-input" id="editClientId">
</div>
<div class="form-group">
<label class="form-label">Refresh Token</label>
<textarea class="form-textarea" id="editRefreshToken" style="height: 80px;"></textarea>
</div>
<div class="form-group">
<label class="form-label">所属分组</label>
<select class="form-select" id="editGroupSelect">
<!-- 分组选项将通过 JS 动态加载 -->
</select>
</div>
<div class="form-group">
<label class="form-label">备注</label>
<input type="text" class="form-input" id="editRemark" placeholder="可选">
</div>
<div class="form-group">
<label class="form-label">状态</label>
<select class="form-select" id="editStatus">
<option value="active">正常</option>
<option value="inactive">停用</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-danger" onclick="deleteCurrentAccount()" style="margin-right: auto;">删除</button>
<button class="btn btn-secondary" onclick="hideEditAccountModal()">取消</button>
<button class="btn btn-primary" onclick="updateAccount()">保存</button>
</div>
</div>
</div>
<!-- 导出邮箱模态框 -->
<div class="modal" id="exportModal">
<div class="modal-content" style="width: 500px;">
<div class="modal-header">
<h3>导出邮箱</h3>
<button class="modal-close" onclick="hideExportModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">选择要导出的分组</label>
<div id="exportGroupList" style="max-height: 300px; overflow-y: auto; border: 1px solid #e5e5e5; border-radius: 6px; padding: 8px;">
<!-- 分组列表将通过 JS 动态加载 -->
</div>
<div class="form-hint" style="margin-top: 8px;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="selectAllGroups" onchange="toggleSelectAllGroups()">
<span>全选</span>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideExportModal()">取消</button>
<button class="btn btn-primary" onclick="exportSelectedGroups()">导出</button>
</div>
</div>
</div>
<!-- 设置模态框 -->
<div class="modal" id="settingsModal">
<div class="modal-content" style="width: 480px;">
<div class="modal-header">
<h3>⚙️ 系统设置</h3>
<button class="modal-close" onclick="hideSettingsModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">登录密码</label>
<input type="password" class="form-input" id="settingsPassword" placeholder="输入新密码(留空则不修改)">
<div class="form-hint">用于登录系统的密码</div>
</div>
<div class="form-group">
<label class="form-label">GPTMail API Key</label>
<input type="text" class="form-input" id="settingsApiKey" placeholder="输入 GPTMail API Key">
<div class="form-hint">用于临时邮箱功能,可从 <a href="https://mail.chatgpt.org.uk/api" target="_blank" style="color: #0078d4;">gptmail.io</a> 获取</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideSettingsModal()">取消</button>
<button class="btn btn-primary" onclick="saveSettings()">保存设置</button>
</div>
</div>
</div>
<!-- Token 刷新管理模态框 -->
<div class="modal" id="refreshModal">
<div class="modal-content" style="width: 800px; max-width: 90vw;">
<div class="modal-header">
<h3>🔄 Token 刷新管理</h3>
<button class="modal-close" onclick="hideRefreshModal()">&times;</button>
</div>
<div class="modal-body">
<!-- 刷新统计 -->
<div style="background-color: #f8f9fa; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
<div style="font-size: 14px; font-weight: 600; margin-bottom: 12px; color: #333;">本次刷新结果</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px;">
<div>
<div style="font-size: 12px; color: #666; margin-bottom: 4px;">上次刷新时间</div>
<div style="font-size: 14px; font-weight: 600;" id="lastRefreshTime">-</div>
</div>
<div>
<div style="font-size: 12px; color: #666; margin-bottom: 4px;">总计</div>
<div style="font-size: 14px; font-weight: 600;" id="totalRefreshCount">0</div>
</div>
<div>
<div style="font-size: 12px; color: #666; margin-bottom: 4px;">成功</div>
<div style="font-size: 14px; font-weight: 600; color: #28a745;" id="successRefreshCount">0</div>
</div>
<div>
<div style="font-size: 12px; color: #666; margin-bottom: 4px;">失败</div>
<div style="font-size: 14px; font-weight: 600; color: #dc3545;" id="failedRefreshCount">0</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
<button class="btn btn-primary" onclick="refreshAllAccounts()" id="refreshAllBtn">
🔄 全量刷新
</button>
<button class="btn btn-warning" onclick="retryFailedAccounts()" id="retryFailedBtn">
🔁 重试失败
</button>
<button class="btn btn-secondary" onclick="loadSuccessLogs()">
✅ 成功列表
</button>
<button class="btn btn-secondary" onclick="loadFailedLogs()">
❌ 失败列表
</button>
</div>
<!-- 刷新进度 -->
<div id="refreshProgress" style="display: none; background-color: #e3f2fd; padding: 16px; border-radius: 6px; margin-bottom: 16px; border-left: 4px solid #2196f3;">
<div style="font-size: 14px; font-weight: 600; margin-bottom: 12px; color: #1976d2;">正在刷新...</div>
<div style="font-size: 13px; line-height: 1.8;" id="refreshProgressText">请稍候</div>
</div>
<!-- 失败列表 -->
<div id="failedListContainer" style="display: none;">
<div style="font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center;">
<span>失败列表</span>
<button class="btn btn-sm btn-secondary" onclick="hideFailedList()">隐藏</button>
</div>
<div id="failedList" style="max-height: 300px; overflow-y: auto; border: 1px solid #e5e5e5; border-radius: 6px;">
<!-- 失败列表将通过 JS 动态加载 -->
</div>
</div>
<!-- 成功列表 -->
<div id="successListContainer" style="display: none;">
<div style="font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center;">
<span>成功列表</span>
<button class="btn btn-sm btn-secondary" onclick="hideSuccessList()">隐藏</button>
</div>
<div id="successList" style="max-height: 400px; overflow-y: auto; border: 1px solid #e5e5e5; border-radius: 6px;">
<!-- 成功列表将通过 JS 动态加载 -->
</div>
</div>
<!-- 刷新历史 -->
<div id="refreshLogsContainer" style="display: none;">
<div style="font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center;">
<span>刷新历史</span>
<button class="btn btn-sm btn-secondary" onclick="hideRefreshLogs()">隐藏</button>
</div>
<div id="refreshLogsList" style="max-height: 400px; overflow-y: auto; border: 1px solid #e5e5e5; border-radius: 6px;">
<!-- 刷新历史将通过 JS 动态加载 -->
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideRefreshModal()">关闭</button>
</div>
</div>
</div>
<!-- 获取 Refresh Token 模态框 -->
<div class="modal" id="getRefreshTokenModal">
<div class="modal-content" style="width: 600px;">
<div class="modal-header">
<h3>🔑 获取 Refresh Token</h3>
<button class="modal-close" onclick="hideGetRefreshTokenModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">步骤 1: 访问授权链接</label>
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
<input type="text" class="form-input" id="authUrlInput" readonly style="flex: 1; font-size: 12px; font-family: monospace;">
<button class="btn btn-secondary" onclick="copyAuthUrl()" style="white-space: nowrap;">📋 复制</button>
<button class="btn btn-primary" onclick="openAuthUrl()" style="white-space: nowrap;">🔗 打开</button>
</div>
<div class="form-hint">点击"打开"按钮在浏览器中授权,或点击"复制"后手动粘贴到浏览器</div>
</div>
<div class="form-group">
<label class="form-label">步骤 2: 粘贴授权后的完整 URL</label>
<textarea class="form-textarea" id="redirectUrlInput" placeholder="授权成功后,浏览器会跳转到一个空白页,请复制地址栏中的完整 URL 并粘贴到这里" style="height: 80px; font-size: 12px; font-family: monospace;"></textarea>
<div class="form-hint">URL 格式类似:http://localhost:8080/?code=xxxxx&state=12345</div>
</div>
<div class="form-group" id="refreshTokenResult" style="display: none;">
<label class="form-label">✅ 账号信息(已获取成功)</label>
<textarea class="form-textarea" id="refreshTokenOutput" readonly style="height: 100px; font-size: 12px; font-family: monospace; background-color: #f0f9ff; border-color: #0078d4;"></textarea>
<div class="form-hint" style="color: #0078d4;">
<strong>格式说明:</strong>邮箱----密码----client_id----refresh_token<br>
请将"your@outlook.com"和"yourpassword"替换为您的实际邮箱和密码,然后复制整行内容到"导入邮箱"中使用
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideGetRefreshTokenModal()">关闭</button>
<button class="btn btn-primary" id="exchangeTokenBtn" onclick="exchangeToken()">换取 Token</button>
</div>
</div>
</div>
<!-- 消息提示 -->
<div class="toast" id="toast"></div>
<script>
// 全局状态
let currentAccount = null;
let currentGroupId = null;
let currentEmails = [];
let currentMethod = 'graph';
let currentFolder = 'inbox'; // 当前文件夹:inbox 或 deleteditems
let isListVisible = true;
let groups = [];
let accountsCache = {}; // 缓存各分组的邮箱列表
let editingGroupId = null;
let selectedColor = '#1a1a1a';
let isTempEmailGroup = false; // 是否是临时邮箱分组
let tempEmailGroupId = null; // 临时邮箱分组 ID
let isLoadingMore = false; // 是否正在加载更多邮件
let hasMoreEmails = true; // 是否还有更多邮件
let currentSkip = 0; // 当前分页偏移量
// 初始化
document.addEventListener('DOMContentLoaded', function() {
loadGroups();
initColorPicker();
initEmailListScroll();
});
// 初始化颜色选择器
function initColorPicker() {
document.querySelectorAll('.color-option').forEach(option => {
option.addEventListener('click', function() {
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
this.classList.add('selected');
selectedColor = this.dataset.color;
// 同步更新自定义颜色输入框
document.getElementById('customColorInput').value = selectedColor;
document.getElementById('customColorHex').value = selectedColor;
});
});
}
// 初始化邮件列表滚动监听
function initEmailListScroll() {
const emailList = document.getElementById('emailList');
emailList.addEventListener('scroll', function() {
// 检查是否滚动到底部
if (emailList.scrollHeight - emailList.scrollTop <= emailList.clientHeight + 50) {
if (!isLoadingMore && hasMoreEmails && currentAccount && !isTempEmailGroup) {
loadMoreEmails();
}
}
});
}
// 加载更多邮件
async function loadMoreEmails() {
if (isLoadingMore || !hasMoreEmails) return;
isLoadingMore = true;
currentSkip += 20; // 每页20封
// 在列表底部显示加载状态
const emailList = document.getElementById('emailList');
const loadingDiv = document.createElement('div');
loadingDiv.className = 'loading loading-small';
loadingDiv.id = 'loadingMore';
loadingDiv.innerHTML = '<div class="loading-spinner"></div>';
emailList.appendChild(loadingDiv);
try {
const response = await fetch(
`/api/emails/${encodeURIComponent(currentAccount)}?method=${currentMethod}&folder=${currentFolder}&skip=${currentSkip}&top=20`
);
const data = await response.json();
if (data.success && data.emails.length > 0) {
// 追加新邮件到列表
currentEmails = currentEmails.concat(data.emails);
hasMoreEmails = data.has_more;
// 移除加载状态
const loadingEl = document.getElementById('loadingMore');
if (loadingEl) loadingEl.remove();
// 重新渲染邮件列表
renderEmailList(currentEmails);
// 更新邮件数量
document.getElementById('emailCount').textContent = `(${currentEmails.length})`;
} else {
hasMoreEmails = false;
// 显示"没有更多邮件"
const loadingEl = document.getElementById('loadingMore');
if (loadingEl) {
loadingEl.innerHTML = '<div style="text-align:center;padding:20px;color:#999;font-size:13px;">没有更多邮件了</div>';
}
}
} catch (error) {
const loadingEl = document.getElementById('loadingMore');
if (loadingEl) loadingEl.remove();
showToast('加载失败', 'error');
} finally {
isLoadingMore = false;
}
}
// 切换文件夹(不触发查询)
function switchFolder(folder) {
if (currentFolder === folder) return;
currentFolder = folder;
// 更新按钮状态
document.querySelectorAll('.folder-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.folder === folder);
});
// 清空邮件列表,显示提示
document.getElementById('emailList').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📬</div>
<div class="empty-state-text">点击"获取邮件"按钮获取${folder === 'inbox' ? '收件箱' : '垃圾邮件'}</div>
</div>
`;
document.getElementById('emailCount').textContent = '';
document.getElementById('methodTag').style.display = 'none';
// 重置分页状态
currentEmails = [];
currentSkip = 0;
hasMoreEmails = true;
}
// 选择自定义颜色(颜色选择器)
function selectCustomColor(color) {
selectedColor = color;
document.getElementById('customColorHex').value = color;
// 取消预设颜色的选中状态
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
}
// 选择自定义颜色(十六进制输入)
function selectCustomColorHex(value) {
// 验证十六进制颜色格式
const hexPattern = /^#[0-9A-Fa-f]{6}$/;
if (hexPattern.test(value)) {
selectedColor = value;
document.getElementById('customColorInput').value = value;
// 取消预设颜色的选中状态
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
} else {
showToast('请输入有效的十六进制颜色(如 #FF5500)', 'error');
}
}
// 显示消息提示
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast show ' + type;
setTimeout(() => {
toast.className = 'toast';
}, 3000);
}
// ==================== 分组相关 ====================
// 加载分组列表
async function loadGroups() {
const container = document.getElementById('groupList');
container.innerHTML = '<div class="loading loading-small"><div class="loading-spinner"></div></div>';
try {
const response = await fetch('/api/groups');
const data = await response.json();
if (data.success) {
groups = data.groups;
// 找到临时邮箱分组
const tempGroup = groups.find(g => g.name === '临时邮箱');
if (tempGroup) {
tempEmailGroupId = tempGroup.id;
}
renderGroupList(data.groups);
updateGroupSelects();
// 如果之前选中了分组,保持选中状态并刷新邮箱列表
if (currentGroupId) {
const group = groups.find(g => g.id === currentGroupId);
if (group) {
// 刷新当前分组的邮箱列表
if (currentGroupId === tempEmailGroupId) {
loadTempEmails(true);
} else {
loadAccountsByGroup(currentGroupId, true);
}
}
}
}
} catch (error) {
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">加载失败</div></div>';
showToast('加载分组失败', 'error');
}
}
// 渲染分组列表
function renderGroupList(groups) {
const container = document.getElementById('groupList');
if (groups.length === 0) {
container.innerHTML = `
<div class="empty-state" style="padding: 40px 20px;">
<div class="empty-state-text">暂无分组</div>
</div>
`;
return;
}
container.innerHTML = groups.map(group => {
const isSystem = group.is_system === 1 || group.name === '临时邮箱';
const isTempGroup = group.name === '临时邮箱';
const isDefault = group.id === 1;
return `
<div class="group-item ${currentGroupId === group.id ? 'active' : ''} ${isTempGroup ? 'temp-email-group' : ''}"
data-group-id="${group.id}"
onclick="selectGroup(${group.id})">
<div class="group-row-1">
<div class="group-color" style="background-color: ${group.color || '#666'}"></div>
<span class="group-name">${escapeHtml(group.name)}${isTempGroup ? ' ⚡' : ''}</span>
</div>
<div class="group-row-2">
<span class="group-count">${group.account_count || 0} 个邮箱</span>
<div class="group-actions">
${!isSystem ? `<button class="group-action-btn" onclick="event.stopPropagation(); editGroup(${group.id})" title="编辑">✏️</button>` : ''}
${!isDefault && !isSystem ? `<button class="group-action-btn" onclick="event.stopPropagation(); deleteGroup(${group.id})" title="删除">🗑️</button>` : ''}
</div>
</div>
</div>
`;
}).join('');
}
// 选择分组
async function selectGroup(groupId) {
currentGroupId = groupId;
// 检查是否是临时邮箱分组
const group = groups.find(g => g.id === groupId);
isTempEmailGroup = group && group.name === '临时邮箱';
// 更新分组列表 UI
document.querySelectorAll('.group-item').forEach(item => {
item.classList.toggle('active', parseInt(item.dataset.groupId) === groupId);
});
// 更新邮箱面板标题
if (group) {
document.getElementById('currentGroupName').textContent = group.name;
document.getElementById('currentGroupColor').style.backgroundColor = group.color || '#666';
// 更新导入邮箱时的默认分组
const importSelect = document.getElementById('importGroupSelect');
if (importSelect) {
importSelect.value = groupId;
}
}
// 更新底部按钮
updateAccountPanelFooter();
// 加载该分组的邮箱
if (isTempEmailGroup) {
await loadTempEmails();
} else {
await loadAccountsByGroup(groupId);
}
}
// 更新账号面板底部按钮
function updateAccountPanelFooter() {
const footer = document.querySelector('.account-panel-footer');
if (isTempEmailGroup) {
footer.innerHTML = `
<button class="add-account-btn" onclick="generateTempEmail()">+ 生成临时邮箱</button>
`;
} else {
footer.innerHTML = `
<button class="add-account-btn" onclick="showGetRefreshTokenModal()" style="background-color: #0078d4; margin-bottom: 8px;">🔑 获取 Refresh Token</button>
<button class="add-account-btn" onclick="showAddAccountModal()">+ 导入邮箱</button>
`;
}
}
// 加载分组下的账号
async function loadAccountsByGroup(groupId, forceRefresh = false) {
const container = document.getElementById('accountList');
// 如果有缓存且不强制刷新,直接使用缓存
if (!forceRefresh && accountsCache[groupId]) {
renderAccountList(accountsCache[groupId]);
return;
}
container.innerHTML = '<div class="loading loading-small"><div class="loading-spinner"></div></div>';
try {
const response = await fetch(`/api/accounts?group_id=${groupId}`);
const data = await response.json();
if (data.success) {
// 缓存数据
accountsCache[groupId] = data.accounts;
renderAccountList(data.accounts);
}
} catch (error) {
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">加载失败</div></div>';
}
}
// 渲染邮箱列表
function renderAccountList(accounts) {
const container = document.getElementById('accountList');
if (accounts.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-text">该分组暂无邮箱</div>
</div>
`;
return;
}
container.innerHTML = accounts.map(acc => `
<div class="account-item ${currentAccount === acc.email ? 'active' : ''} ${acc.status === 'inactive' ? 'inactive' : ''}"
onclick="selectAccount('${acc.email}')">
<div class="account-email" title="${acc.email}">
${acc.email}
${acc.status === 'inactive' ? '<span class="account-status-tag">已停用</span>' : ''}
</div>
<div class="account-actions">
<button class="account-action-btn" onclick="event.stopPropagation(); copyEmail('${acc.email}')" title="复制邮箱">复制</button>
<button class="account-action-btn" onclick="event.stopPropagation(); toggleAccountStatus(${acc.id}, '${acc.status || 'active'}')" title="${acc.status === 'inactive' ? '启用' : '停用'}">${acc.status === 'inactive' ? '启用' : '停用'}</button>
<button class="account-action-btn" onclick="event.stopPropagation(); showEditAccountModal(${acc.id})" title="编辑">编辑</button>
<button class="account-action-btn delete" onclick="event.stopPropagation(); deleteAccount(${acc.id}, '${acc.email}')" title="删除">删除</button>
</div>
</div>
`).join('');
}
// 更新分组下拉选择框
function updateGroupSelects() {
const selects = ['importGroupSelect', 'editGroupSelect'];
selects.forEach(selectId => {
const select = document.getElementById(selectId);
if (select) {
const currentValue = select.value;
// 过滤掉临时邮箱分组(导入邮箱时不能选择临时邮箱分组)
const filteredGroups = selectId === 'importGroupSelect'
? groups.filter(g => g.name !== '临时邮箱')
: groups;
select.innerHTML = filteredGroups.map(g =>
`<option value="${g.id}">${escapeHtml(g.name)}</option>`
).join('');
// 恢复之前的选择
if (currentValue && filteredGroups.find(g => g.id === parseInt(currentValue))) {
select.value = currentValue;
} else if (currentGroupId && filteredGroups.find(g => g.id === currentGroupId)) {
select.value = currentGroupId;
}
}
});
}
// 显示添加分组模态框
function showAddGroupModal() {
editingGroupId = null;
document.getElementById('groupModalTitle').textContent = '添加分组';
document.getElementById('groupName').value = '';
document.getElementById('groupDescription').value = '';
selectedColor = '#1a1a1a';
document.querySelectorAll('.color-option').forEach(o => {
o.classList.toggle('selected', o.dataset.color === selectedColor);
});
document.getElementById('customColorInput').value = selectedColor;
document.getElementById('customColorHex').value = selectedColor;
document.getElementById('addGroupModal').classList.add('show');
}
// 隐藏添加分组模态框
function hideAddGroupModal() {
document.getElementById('addGroupModal').classList.remove('show');
}
// 编辑分组
async function editGroup(groupId) {
try {
const response = await fetch(`/api/groups/${groupId}`);
const data = await response.json();
if (data.success) {
editingGroupId = groupId;
document.getElementById('groupModalTitle').textContent = '编辑分组';
document.getElementById('groupName').value = data.group.name;
document.getElementById('groupDescription').value = data.group.description || '';
selectedColor = data.group.color || '#1a1a1a';
// 检查是否是预设颜色
let isPresetColor = false;
document.querySelectorAll('.color-option').forEach(o => {
if (o.dataset.color === selectedColor) {
o.classList.add('selected');
isPresetColor = true;
} else {
o.classList.remove('selected');
}
});
// 更新自定义颜色输入框
document.getElementById('customColorInput').value = selectedColor;
document.getElementById('customColorHex').value = selectedColor;
document.getElementById('addGroupModal').classList.add('show');
}
} catch (error) {
showToast('加载分组信息失败', 'error');
}
}
// 保存分组
async function saveGroup() {
const name = document.getElementById('groupName').value.trim();
const description = document.getElementById('groupDescription').value.trim();
if (!name) {
showToast('请输入分组名称', 'error');
return;
}
try {
const url = editingGroupId ? `/api/groups/${editingGroupId}` : '/api/groups';
const method = editingGroupId ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description, color: selectedColor })
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
hideAddGroupModal();
loadGroups();
} else {
showToast(data.error, 'error');
}
} catch (error) {
showToast('保存失败', 'error');
}
}
// 删除分组
async function deleteGroup(groupId) {
if (!confirm('确定要删除该分组吗?分组下的邮箱将移至默认分组。')) {
return;
}
try {
const response = await fetch(`/api/groups/${groupId}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
// 清除缓存
delete accountsCache[groupId];
// 如果删除的是当前选中的分组,切换到默认分组
if (currentGroupId === groupId) {
currentGroupId = 1;
}
loadGroups();
} else {
showToast(data.error, 'error');
}
} catch (error) {
showToast('删除失败', 'error');
}
}
// ==================== 临时邮箱相关 ====================
// 加载临时邮箱列表
async function loadTempEmails(forceRefresh = false) {
const container = document.getElementById('accountList');
if (!forceRefresh && accountsCache['temp']) {
renderTempEmailList(accountsCache['temp']);
return;
}
container.innerHTML = '<div class="loading loading-small"><div class="loading-spinner"></div></div>';
try {
const response = await fetch('/api/temp-emails');
const data = await response.json();
if (data.success) {
accountsCache['temp'] = data.emails;
renderTempEmailList(data.emails);
const group = groups.find(g => g.name === '临时邮箱');
if (group) {
group.account_count = data.emails.length;
renderGroupList(groups);
}
}
} catch (error) {
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">加载失败</div></div>';
}
}
// 渲染临时邮箱列表
function renderTempEmailList(emails) {
const container = document.getElementById('accountList');
if (emails.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚡</div>
<div class="empty-state-text">暂无临时邮箱<br>点击下方按钮生成</div>
</div>
`;
return;
}
container.innerHTML = emails.map(email => `
<div class="account-item ${currentAccount === email.email ? 'active' : ''}"
onclick="selectTempEmail('${email.email}')">
<div class="account-email" title="${email.email}">
${email.email}
</div>
<div class="account-actions">
<button class="account-action-btn" onclick="event.stopPropagation(); copyEmail('${email.email}')" title="复制邮箱">复制</button>
<button class="account-action-btn" onclick="event.stopPropagation(); clearTempEmailMessages('${email.email}')" title="清空邮件">清空</button>
<button class="account-action-btn delete" onclick="event.stopPropagation(); deleteTempEmail('${email.email}')" title="删除">删除</button>
</div>
</div>
`).join('');
}
// 生成临时邮箱
async function generateTempEmail() {
// 获取按钮并显示加载状态
const btn = document.querySelector('.account-panel-footer .add-account-btn');
const originalText = btn.textContent;
btn.disabled = true;
btn.textContent = '⏳ 生成中...';
btn.style.opacity = '0.7';
btn.style.cursor = 'not-allowed';
try {
const response = await fetch('/api/temp-emails/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const data = await response.json();
if (data.success) {
showToast(`临时邮箱已生成: ${data.email}`, 'success');
delete accountsCache['temp'];
loadTempEmails(true);
loadGroups();
} else {
showToast(data.error || '生成失败', 'error');
}
} catch (error) {
showToast('生成临时邮箱失败', 'error');
} finally {
// 恢复按钮状态
btn.disabled = false;
btn.textContent = originalText;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
}
// 选择临时邮箱
function selectTempEmail(email) {
currentAccount = email;
isTempEmailGroup = true;
document.getElementById('currentAccount').classList.add('show');
document.getElementById('currentAccountEmail').textContent = email + ' (临时)';
document.querySelectorAll('.account-item').forEach(item => {
item.classList.remove('active');
const emailEl = item.querySelector('.account-email');
if (emailEl && emailEl.textContent.includes(email)) {
item.classList.add('active');
}
});
// 隐藏文件夹切换按钮(临时邮箱不支持文件夹)
const folderTabs = document.getElementById('folderTabs');
if (folderTabs) {
folderTabs.style.display = 'none';
}
document.getElementById('emailList').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📬</div>
<div class="empty-state-text">点击"获取邮件"按钮获取邮件</div>
</div>
`;
document.getElementById('emailDetail').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📄</div>
<div class="empty-state-text">选择一封邮件查看详情</div>
</div>
`;
document.getElementById('emailDetailToolbar').style.display = 'none';
document.getElementById('emailCount').textContent = '';
document.getElementById('methodTag').style.display = 'none';
}
// 清空临时邮箱的所有邮件
async function clearTempEmailMessages(email) {
if (!confirm(`确定要清空临时邮箱 ${email} 的所有邮件吗?`)) {
return;
}
try {
const response = await fetch(`/api/temp-emails/${encodeURIComponent(email)}/clear`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('邮件已清空', 'success');
// 如果当前选中的就是这个邮箱,清空邮件列表
if (currentAccount === email) {
currentEmails = [];
document.getElementById('emailCount').textContent = '(0)';
document.getElementById('emailList').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-text">收件箱为空</div>
</div>
`;
document.getElementById('emailDetail').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📄</div>
<div class="empty-state-text">选择一封邮件查看详情</div>
</div>
`;
document.getElementById('emailDetailToolbar').style.display = 'none';
}
} else {
showToast(data.error || '清空失败', 'error');
}
} catch (error) {
showToast('清空失败', 'error');
}
}
// 删除临时邮箱
async function deleteTempEmail(email) {
if (!confirm(`确定要删除临时邮箱 ${email} 吗?\n该邮箱的所有邮件也将被删除。`)) {
return;
}
try {
const response = await fetch(`/api/temp-emails/${encodeURIComponent(email)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('临时邮箱已删除', 'success');
delete accountsCache['temp'];
if (currentAccount === email) {
currentAccount = null;
document.getElementById('currentAccount').classList.remove('show');
document.getElementById('emailList').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📬</div>
<div class="empty-state-text">请从左侧选择一个邮箱账号</div>
</div>
`;
document.getElementById('emailDetail').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📄</div>
<div class="empty-state-text">选择一封邮件查看详情</div>
</div>
`;
}
loadTempEmails(true);
loadGroups();
} else {
showToast(data.error || '删除失败', 'error');
}
} catch (error) {
showToast('删除失败', 'error');
}
}
// 加载临时邮箱的邮件
async function loadTempEmailMessages(email) {
const container = document.getElementById('emailList');
container.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>';
try {
const response = await fetch(`/api/temp-emails/${encodeURIComponent(email)}/messages`);
const data = await response.json();
if (data.success) {
currentEmails = data.emails;
currentMethod = 'gptmail';
const methodTag = document.getElementById('methodTag');
methodTag.textContent = 'GPTMail';
methodTag.style.display = 'inline';
methodTag.style.backgroundColor = '#00bcf2';
methodTag.style.color = 'white';
document.getElementById('emailCount').textContent = `(${data.count})`;
renderEmailList(data.emails);
} else {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">${data.error || '加载失败'}</div>
</div>
`;
}
} catch (error) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">网络错误,请重试</div>
</div>
`;
}
}
// 获取临时邮件详情
async function getTempEmailDetail(messageId, index) {
document.querySelectorAll('.email-item').forEach((item, i) => {
item.classList.toggle('active', i === index);
});
document.getElementById('emailDetailToolbar').style.display = 'flex';
const container = document.getElementById('emailDetail');
container.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>';
try {
const response = await fetch(`/api/temp-emails/${encodeURIComponent(currentAccount)}/messages/${encodeURIComponent(messageId)}`);
const data = await response.json();
if (data.success) {
renderEmailDetail(data.email);
} else {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">${data.error || '加载失败'}</div>
</div>
`;
}
} catch (error) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">网络错误,请重试</div>
</div>
`;
}
}
// ==================== 账号相关 ====================
// 选择账号
function selectAccount(email) {
currentAccount = email;
isTempEmailGroup = false;
currentFolder = 'inbox'; // 重置为收件箱
document.getElementById('currentAccount').classList.add('show');
document.getElementById('currentAccountEmail').textContent = email;
document.querySelectorAll('.account-item').forEach(item => {
item.classList.remove('active');
const emailEl = item.querySelector('.account-email');
if (emailEl && emailEl.textContent.includes(email)) {
item.classList.add('active');
}
});
// 显示文件夹切换按钮
const folderTabs = document.getElementById('folderTabs');
if (folderTabs) {
folderTabs.style.display = 'flex';
// 重置为收件箱
document.querySelectorAll('.folder-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.folder === 'inbox');
});
}
document.getElementById('emailList').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📬</div>
<div class="empty-state-text">点击"获取邮件"按钮获取邮件</div>
</div>
`;
document.getElementById('emailDetail').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📄</div>
<div class="empty-state-text">选择一封邮件查看详情</div>
</div>
`;
document.getElementById('emailDetailToolbar').style.display = 'none';
document.getElementById('emailCount').textContent = '';
document.getElementById('methodTag').style.display = 'none';
}
// 显示添加账号模态框
function showAddAccountModal() {
document.getElementById('accountInput').value = '';
// 设置默认分组为当前选中的分组
if (currentGroupId) {
document.getElementById('importGroupSelect').value = currentGroupId;
}
document.getElementById('addAccountModal').classList.add('show');
}
// 隐藏添加账号模态框
function hideAddAccountModal() {
document.getElementById('addAccountModal').classList.remove('show');
}
// 添加账号
async function addAccount() {
const input = document.getElementById('accountInput').value.trim();
const groupId = parseInt(document.getElementById('importGroupSelect').value);
if (!input) {
showToast('请输入账号信息', 'error');
return;
}
try {
const response = await fetch('/api/accounts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ account_string: input, group_id: groupId })
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
hideAddAccountModal();
// 清除该分组的缓存
delete accountsCache[groupId];
// 刷新分组列表(更新数量)
loadGroups();
// 如果当前选中的就是这个分组,刷新邮箱列表
if (currentGroupId === groupId) {
loadAccountsByGroup(groupId, true);
}
} else {
showToast(data.error, 'error');
}
} catch (error) {
showToast('添加失败', 'error');
}
}
// 显示编辑账号模态框
async function showEditAccountModal(accountId) {
try {
const response = await fetch(`/api/accounts/${accountId}`);
const data = await response.json();
if (data.success) {
const acc = data.account;
document.getElementById('editAccountId').value = acc.id;
document.getElementById('editEmail').value = acc.email;
document.getElementById('editPassword').value = acc.password || '';
document.getElementById('editClientId').value = acc.client_id;
document.getElementById('editRefreshToken').value = acc.refresh_token;
document.getElementById('editGroupSelect').value = acc.group_id || 1;
document.getElementById('editRemark').value = acc.remark || '';
document.getElementById('editStatus').value = acc.status || 'active';
document.getElementById('editAccountModal').classList.add('show');
}
} catch (error) {
showToast('加载账号信息失败', 'error');
}
}
// 隐藏编辑账号模态框
function hideEditAccountModal() {
document.getElementById('editAccountModal').classList.remove('show');
}
// 更新账号
async function updateAccount() {
const accountId = document.getElementById('editAccountId').value;
const oldGroupId = currentGroupId;
const newGroupId = parseInt(document.getElementById('editGroupSelect').value);
const data = {
email: document.getElementById('editEmail').value.trim(),
password: document.getElementById('editPassword').value,
client_id: document.getElementById('editClientId').value.trim(),
refresh_token: document.getElementById('editRefreshToken').value.trim(),
group_id: newGroupId,
remark: document.getElementById('editRemark').value.trim(),
status: document.getElementById('editStatus').value
};
if (!data.email || !data.client_id || !data.refresh_token) {
showToast('邮箱、Client ID 和 Refresh Token 不能为空', 'error');
return;
}
try {
const response = await fetch(`/api/accounts/${accountId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showToast(result.message, 'success');
hideEditAccountModal();
// 清除相关分组的缓存
delete accountsCache[oldGroupId];
if (oldGroupId !== newGroupId) {
delete accountsCache[newGroupId];
}
// 刷新分组列表
loadGroups();
// 刷新当前分组的邮箱列表
if (currentGroupId) {
loadAccountsByGroup(currentGroupId, true);
}
} else {
showToast(result.error, 'error');
}
} catch (error) {
showToast('更新失败', 'error');
}
}
// 删除当前编辑的账号
async function deleteCurrentAccount() {
const accountId = document.getElementById('editAccountId').value;
const email = document.getElementById('editEmail').value;
const groupId = parseInt(document.getElementById('editGroupSelect').value);
if (!confirm(`确定要删除账号 ${email} 吗?`)) {
return;
}
try {
const response = await fetch(`/api/accounts/${accountId}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
showToast('删除成功', 'success');
hideEditAccountModal();
// 清除缓存
delete accountsCache[groupId];
if (currentAccount === email) {
currentAccount = null;
document.getElementById('currentAccount').classList.remove('show');
document.getElementById('emailList').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📬</div>
<div class="empty-state-text">请从左侧选择一个邮箱账号</div>
</div>
`;
document.getElementById('emailDetail').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📄</div>
<div class="empty-state-text">选择一封邮件查看详情</div>
</div>
`;
}
// 刷新分组列表
loadGroups();
// 刷新当前分组的邮箱列表
if (currentGroupId) {
loadAccountsByGroup(currentGroupId, true);
}
}
} catch (error) {
showToast('删除失败', 'error');
}
}
// 切换账号状态(启用/停用)
async function toggleAccountStatus(accountId, currentStatus) {
const newStatus = currentStatus === 'inactive' ? 'active' : 'inactive';
const action = newStatus === 'inactive' ? '停用' : '启用';
try {
const response = await fetch(`/api/accounts/${accountId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
});
const data = await response.json();
if (data.success) {
showToast(`${action}成功`, 'success');
// 清除当前分组的缓存
if (currentGroupId) {
delete accountsCache[currentGroupId];
loadAccountsByGroup(currentGroupId, true);
}
} else {
showToast(data.error || `${action}失败`, 'error');
}
} catch (error) {
showToast(`${action}失败`, 'error');
}
}
// 删除账号(快捷方式)
async function deleteAccount(accountId, email) {
if (!confirm(`确定要删除账号 ${email} 吗?`)) {
return;
}
try {
const response = await fetch(`/api/accounts/${accountId}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
showToast('删除成功', 'success');
// 清除当前分组的缓存
if (currentGroupId) {
delete accountsCache[currentGroupId];
}
if (currentAccount === email) {
currentAccount = null;
document.getElementById('currentAccount').classList.remove('show');
document.getElementById('emailList').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📬</div>
<div class="empty-state-text">请从左侧选择一个邮箱账号</div>
</div>
`;
document.getElementById('emailDetail').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📄</div>
<div class="empty-state-text">选择一封邮件查看详情</div>
</div>
`;
}
// 刷新分组列表
loadGroups();
// 刷新当前分组的邮箱列表
if (currentGroupId) {
loadAccountsByGroup(currentGroupId, true);
}
} else {
showToast(data.error || '删除失败', 'error');
}
} catch (error) {
showToast('删除失败', 'error');
}
}
// 显示导出邮箱模态框
async function showExportModal() {
document.getElementById('exportModal').classList.add('show');
await loadExportGroupList();
}
// 隐藏导出邮箱模态框
function hideExportModal() {
document.getElementById('exportModal').classList.remove('show');
}
// 加载导出分组列表
async function loadExportGroupList() {
const container = document.getElementById('exportGroupList');
container.innerHTML = '<div class="loading loading-small"><div class="loading-spinner"></div></div>';
try {
// 使用已加载的分组数据
if (groups.length === 0) {
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">暂无分组</div>';
} else {
container.innerHTML = groups.map(group => `
<label style="display: flex; align-items: center; gap: 10px; padding: 10px 12px; cursor: pointer; border-radius: 6px; transition: background-color 0.15s;"
onmouseover="this.style.backgroundColor='#f5f5f5'"
onmouseout="this.style.backgroundColor='transparent'">
<input type="checkbox" class="export-group-checkbox" value="${group.id}" style="width: 16px; height: 16px;">
<span style="display: flex; align-items: center; gap: 8px; flex: 1;">
<span style="width: 12px; height: 12px; border-radius: 3px; background-color: ${group.color || '#666'}"></span>
<span style="font-size: 14px; color: #1a1a1a;">${escapeHtml(group.name)}</span>
</span>
<span style="font-size: 12px; color: #999; background-color: #f0f0f0; padding: 2px 8px; border-radius: 10px;">${group.account_count || 0}</span>
</label>
`).join('');
}
} catch (error) {
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #dc3545;">加载失败</div>';
}
// 重置全选复选框
document.getElementById('selectAllGroups').checked = false;
}
// 全选/取消全选分组
function toggleSelectAllGroups() {
const selectAll = document.getElementById('selectAllGroups').checked;
document.querySelectorAll('.export-group-checkbox').forEach(cb => {
cb.checked = selectAll;
});
}
// 导出选中的分组
async function exportSelectedGroups() {
const checkboxes = document.querySelectorAll('.export-group-checkbox:checked');
const groupIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
if (groupIds.length === 0) {
showToast('请选择要导出的分组', 'error');
return;
}
try {
const response = await fetch('/api/accounts/export-selected', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group_ids: groupIds })
});
if (response.ok) {
// 获取文件名
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'accounts.txt';
if (contentDisposition) {
const match = contentDisposition.match(/filename\*?=(?:UTF-8'')?([^;\n]+)/i);
if (match) {
filename = decodeURIComponent(match[1]);
}
}
// 下载文件
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showToast('导出成功', 'success');
hideExportModal();
} else {
const data = await response.json();
showToast(data.error || '导出失败', 'error');
}
} catch (error) {
showToast('导出失败', 'error');
}
}
// ==================== 邮件相关 ====================
// 加载邮件列表
async function loadEmails(email) {
const container = document.getElementById('emailList');
// 重置分页状态
currentSkip = 0;
hasMoreEmails = true;
container.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>';
try {
// 每次只查询20封邮件
const response = await fetch(
`/api/emails/${encodeURIComponent(email)}?method=${currentMethod}&folder=${currentFolder}&skip=0&top=20`
);
const data = await response.json();
if (data.success) {
currentEmails = data.emails;
currentMethod = data.method === 'Graph API' ? 'graph' : 'imap';
hasMoreEmails = data.has_more;
// 显示使用的方法和邮件数量
const methodTag = document.getElementById('methodTag');
methodTag.textContent = data.method;
methodTag.style.display = 'inline';
document.getElementById('emailCount').textContent = `(${data.emails.length})`;
renderEmailList(data.emails);
} else {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">${data.error || '加载失败'}</div>
</div>
`;
}
} catch (error) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">网络错误,请重试</div>
</div>
`;
}
}
// 渲染邮件列表
function renderEmailList(emails) {
const container = document.getElementById('emailList');
if (emails.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-text">收件箱为空</div>
</div>
`;
return;
}
// 根据是否是临时邮箱选择不同的点击处理函数
const clickHandler = isTempEmailGroup ? 'getTempEmailDetail' : 'selectEmail';
container.innerHTML = emails.map((email, index) => `
<div class="email-item ${email.is_read === false ? 'unread' : ''}"
onclick="${clickHandler}('${email.id}', ${index})">
<div class="email-from">${escapeHtml(email.from)}</div>
<div class="email-subject">${escapeHtml(email.subject || '无主题')}</div>
<div class="email-preview">${escapeHtml(email.body_preview || '')}</div>
<div class="email-date">${formatDate(email.date)}</div>
</div>
`).join('');
}
// 选择邮件
async function selectEmail(messageId, index) {
// 更新 UI
document.querySelectorAll('.email-item').forEach((item, i) => {
item.classList.toggle('active', i === index);
});
// 显示工具栏
document.getElementById('emailDetailToolbar').style.display = 'flex';
// 加载邮件详情
const container = document.getElementById('emailDetail');
container.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>';
try {
const response = await fetch(`/api/email/${encodeURIComponent(currentAccount)}/${encodeURIComponent(messageId)}?method=${currentMethod}`);
const data = await response.json();
if (data.success) {
renderEmailDetail(data.email);
} else {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">${data.error || '加载失败'}</div>
</div>
`;
}
} catch (error) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">网络错误,请重试</div>
</div>
`;
}
}
// 渲染邮件详情
function renderEmailDetail(email) {
const container = document.getElementById('emailDetail');
const isHtml = email.body_type === 'html' ||
(email.body && (email.body.includes('<html') || email.body.includes('<div') || email.body.includes('<p>')));
const bodyContent = isHtml
? `<iframe id="emailBodyFrame" onload="adjustIframeHeight(this)"></iframe>`
: `<div class="email-body-text">${escapeHtml(email.body)}</div>`;
container.innerHTML = `
<div class="email-detail-header">
<div class="email-detail-subject">${escapeHtml(email.subject || '无主题')}</div>
<div class="email-detail-meta">
<div class="email-detail-meta-row">
<span class="email-detail-meta-label">发件人</span>
<span class="email-detail-meta-value">${escapeHtml(email.from)}</span>
</div>
<div class="email-detail-meta-row">
<span class="email-detail-meta-label">收件人</span>
<span class="email-detail-meta-value">${escapeHtml(email.to || '-')}</span>
</div>
${email.cc ? `
<div class="email-detail-meta-row">
<span class="email-detail-meta-label">抄送</span>
<span class="email-detail-meta-value">${escapeHtml(email.cc)}</span>
</div>
` : ''}
<div class="email-detail-meta-row">
<span class="email-detail-meta-label">时间</span>
<span class="email-detail-meta-value">${formatDate(email.date)}</span>
</div>
</div>
</div>
<div class="email-detail-body">
${bodyContent}
</div>
`;
// 如果是 HTML 内容,设置 iframe 内容
if (isHtml) {
const iframe = document.getElementById('emailBodyFrame');
if (iframe) {
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 15px;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
background-color: #ffffff;
}
img {
max-width: 100%;
height: auto;
}
a {
color: #0078d4;
}
</style>
</head>
<body>${email.body}</body>
</html>
`;
iframe.srcdoc = htmlContent;
}
}
}
// 动态调整 iframe 高度
function adjustIframeHeight(iframe) {
try {
// 多次尝试调整高度,确保内容完全加载
const adjustHeight = () => {
if (iframe.contentDocument && iframe.contentDocument.body) {
const body = iframe.contentDocument.body;
const html = iframe.contentDocument.documentElement;
// 获取实际内容高度(取最大值)
const height = Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight
);
// 设置最小高度为 600px,添加 100px 余量确保长邮件能完整显示
iframe.style.height = Math.max(height + 100, 600) + 'px';
}
};
// 立即调整一次
adjustHeight();
// 100ms 后再调整(等待图片等资源加载)
setTimeout(adjustHeight, 100);
// 300ms 后再调整
setTimeout(adjustHeight, 300);
// 500ms 后再调整(确保所有内容都已加载)
setTimeout(adjustHeight, 500);
// 1秒后最后调整一次
setTimeout(adjustHeight, 1000);
// 2秒后再次调整(处理延迟加载的内容)
setTimeout(adjustHeight, 2000);
// 监听 iframe 内容变化
if (iframe.contentDocument) {
const observer = new MutationObserver(adjustHeight);
observer.observe(iframe.contentDocument.body, {
childList: true,
subtree: true,
attributes: true
});
// 监听图片加载完成事件
const images = iframe.contentDocument.querySelectorAll('img');
images.forEach(img => {
img.addEventListener('load', adjustHeight);
img.addEventListener('error', adjustHeight);
});
}
} catch (e) {
console.log('Cannot adjust iframe height:', e);
}
}
// 切换邮件列表显示
function toggleEmailList() {
const panel = document.getElementById('emailListPanel');
const toggleText = document.getElementById('toggleListText');
isListVisible = !isListVisible;
if (isListVisible) {
panel.classList.remove('hidden');
toggleText.textContent = '隐藏列表';
} else {
panel.classList.add('hidden');
toggleText.textContent = '显示列表';
}
}
// 全屏查看邮件
let currentFullscreenEmail = null;
function openFullscreenEmail() {
const emailDetail = document.getElementById('emailDetail');
const modal = document.getElementById('fullscreenEmailModal');
const content = document.getElementById('fullscreenEmailContent');
const title = document.getElementById('fullscreenEmailTitle');
// 获取当前邮件的标题
const subjectElement = emailDetail.querySelector('.email-detail-subject');
if (subjectElement) {
title.textContent = subjectElement.textContent;
}
// 克隆邮件内容
const emailHeader = emailDetail.querySelector('.email-detail-header');
const emailBody = emailDetail.querySelector('.email-detail-body');
if (emailHeader && emailBody) {
// 清空内容
content.innerHTML = '';
// 克隆头部信息
const headerClone = emailHeader.cloneNode(true);
content.appendChild(headerClone);
// 处理邮件正文
const iframe = emailBody.querySelector('iframe');
const textContent = emailBody.querySelector('.email-body-text');
if (iframe) {
// 如果是 HTML 邮件,创建新的 iframe
const newIframe = document.createElement('iframe');
newIframe.id = 'fullscreenEmailBodyFrame';
newIframe.style.width = '100%';
newIframe.style.border = 'none';
newIframe.style.backgroundColor = '#ffffff';
// 复制原 iframe 的内容
if (iframe.contentDocument) {
const htmlContent = iframe.contentDocument.documentElement.outerHTML;
newIframe.srcdoc = htmlContent;
}
content.appendChild(newIframe);
// 调整 iframe 高度
newIframe.onload = function() {
adjustFullscreenIframeHeight(newIframe);
};
} else if (textContent) {
// 如果是纯文本邮件,直接克隆
const textClone = textContent.cloneNode(true);
content.appendChild(textClone);
}
// 显示模态框
modal.classList.add('show');
document.body.style.overflow = 'hidden';
}
}
function closeFullscreenEmail() {
const modal = document.getElementById('fullscreenEmailModal');
modal.classList.remove('show');
document.body.style.overflow = '';
}
function closeFullscreenEmailOnBackdrop(event) {
// 只有点击背景时才关闭,点击内容区域不关闭
if (event.target.id === 'fullscreenEmailModal') {
closeFullscreenEmail();
}
}
function adjustFullscreenIframeHeight(iframe) {
try {
const adjustHeight = () => {
if (iframe.contentDocument && iframe.contentDocument.body) {
const body = iframe.contentDocument.body;
const html = iframe.contentDocument.documentElement;
const height = Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight
);
// 全屏模式下设置实际高度,添加余量
iframe.style.height = (height + 100) + 'px';
}
};
// 多次调整高度
adjustHeight();
setTimeout(adjustHeight, 100);
setTimeout(adjustHeight, 300);
setTimeout(adjustHeight, 500);
setTimeout(adjustHeight, 1000);
// 监听内容变化
if (iframe.contentDocument) {
const observer = new MutationObserver(adjustHeight);
observer.observe(iframe.contentDocument.body, {
childList: true,
subtree: true,
attributes: true
});
// 监听图片加载
const images = iframe.contentDocument.querySelectorAll('img');
images.forEach(img => {
img.addEventListener('load', adjustHeight);
img.addEventListener('error', adjustHeight);
});
}
} catch (e) {
console.log('Cannot adjust fullscreen iframe height:', e);
}
}
// 显示邮件列表(移动端)
function showEmailList() {
document.getElementById('emailListPanel').classList.remove('hidden');
isListVisible = true;
document.getElementById('toggleListText').textContent = '隐藏列表';
}
// 刷新邮件
function refreshEmails() {
if (currentAccount) {
if (isTempEmailGroup) {
loadTempEmailMessages(currentAccount);
} else {
loadEmails(currentAccount);
}
} else {
showToast('请先选择一个邮箱账号', 'error');
}
}
// 复制邮箱地址
function copyEmail(email) {
navigator.clipboard.writeText(email).then(() => {
showToast('邮箱地址已复制', 'success');
}).catch(() => {
// 降级方案
const textarea = document.createElement('textarea');
textarea.value = email;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showToast('邮箱地址已复制', 'success');
});
}
// 退出登录
function logout() {
if (confirm('确定要退出登录吗?')) {
window.location.href = '/logout';
}
}
// ==================== 工具函数 ====================
// HTML 转义
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 格式化日期
function formatDate(dateStr) {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr;
const now = new Date();
const isToday = date.toDateString() === now.toDateString();
if (isToday) {
return '今天 ' + date.toLocaleTimeString('zh-CN', {
timeZone: 'Asia/Shanghai',
hour: '2-digit',
minute: '2-digit'
});
} else {
return date.toLocaleDateString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: 'long',
day: 'numeric'
}) + ' ' + date.toLocaleTimeString('zh-CN', {
timeZone: 'Asia/Shanghai',
hour: '2-digit',
minute: '2-digit'
});
}
} catch (e) {
return dateStr;
}
}
// ==================== OAuth Refresh Token 相关 ====================
// 显示获取 Refresh Token 模态框
async function showGetRefreshTokenModal() {
document.getElementById('getRefreshTokenModal').classList.add('show');
// 重置表单
document.getElementById('redirectUrlInput').value = '';
document.getElementById('refreshTokenResult').style.display = 'none';
document.getElementById('refreshTokenOutput').value = '';
// 重置按钮状态
const btn = document.getElementById('exchangeTokenBtn');
btn.disabled = false;
btn.textContent = '换取 Token';
btn.style.display = '';
// 获取授权 URL
try {
const response = await fetch('/api/oauth/auth-url');
const data = await response.json();
if (data.success) {
document.getElementById('authUrlInput').value = data.auth_url;
} else {
showToast('获取授权链接失败', 'error');
}
} catch (error) {
showToast('获取授权链接失败', 'error');
}
}
// 隐藏获取 Refresh Token 模态框
function hideGetRefreshTokenModal() {
document.getElementById('getRefreshTokenModal').classList.remove('show');
}
// 复制授权 URL
function copyAuthUrl() {
const input = document.getElementById('authUrlInput');
input.select();
document.execCommand('copy');
showToast('授权链接已复制到剪贴板', 'success');
}
// 打开授权 URL
function openAuthUrl() {
const url = document.getElementById('authUrlInput').value;
if (url) {
window.open(url, '_blank');
showToast('已在新窗口打开授权页面', 'info');
}
}
// 换取 Token
async function exchangeToken() {
const redirectUrl = document.getElementById('redirectUrlInput').value.trim();
if (!redirectUrl) {
showToast('请先粘贴授权后的完整 URL', 'error');
return;
}
const btn = document.getElementById('exchangeTokenBtn');
btn.disabled = true;
btn.textContent = '⏳ 换取中...';
try {
const response = await fetch('/api/oauth/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
redirected_url: redirectUrl
})
});
const data = await response.json();
if (data.success) {
// 生成完整的导入格式
const importFormat = `your@outlook.com----yourpassword----${data.client_id}----${data.refresh_token}`;
// 显示结果
document.getElementById('refreshTokenOutput').value = importFormat;
document.getElementById('refreshTokenResult').style.display = 'block';
showToast('✅ Refresh Token 获取成功!', 'success');
// 重置按钮状态(不隐藏,允许重复使用)
btn.disabled = false;
btn.textContent = '换取 Token';
} else {
showToast(data.error || '换取 Token 失败', 'error');
btn.disabled = false;
btn.textContent = '换取 Token';
}
} catch (error) {
showToast('换取 Token 失败: ' + error.message, 'error');
btn.disabled = false;
btn.textContent = '换取 Token';
}
}
// ==================== 设置相关 ====================
// 显示设置模态框
async function showSettingsModal() {
document.getElementById('settingsModal').classList.add('show');
await loadSettings();
}
// 隐藏设置模态框
function hideSettingsModal() {
document.getElementById('settingsModal').classList.remove('show');
// 清空密码输入框
document.getElementById('settingsPassword').value = '';
}
// 加载设置
async function loadSettings() {
try {
const response = await fetch('/api/settings');
const data = await response.json();
if (data.success) {
// 密码不显示,只显示 API Key
document.getElementById('settingsApiKey').value = data.settings.gptmail_api_key || '';
// 密码框留空
document.getElementById('settingsPassword').value = '';
}
} catch (error) {
showToast('加载设置失败', 'error');
}
}
// 保存设置
async function saveSettings() {
const password = document.getElementById('settingsPassword').value;
const apiKey = document.getElementById('settingsApiKey').value.trim();
const settings = {};
// 只有输入了密码才更新密码
if (password) {
settings.login_password = password;
}
// API Key 可以为空(清除)
settings.gptmail_api_key = apiKey;
try {
const response = await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
const data = await response.json();
if (data.success) {
showToast('设置已保存', 'success');
hideSettingsModal();
} else {
showToast(data.error || '保存失败', 'error');
}
} catch (error) {
showToast('保存设置失败', 'error');
}
}
// ==================== Token 刷新管理 ====================
// 显示刷新模态框
async function showRefreshModal() {
document.getElementById('refreshModal').classList.add('show');
// 加载统计数据
await loadRefreshStats();
// 自动加载失败列表(如果有失败记录)
await autoLoadFailedListIfNeeded();
}
// 自动加载失败列表(如果有失败记录)
async function autoLoadFailedListIfNeeded() {
try {
const response = await fetch('/api/accounts/refresh-logs/failed');
const data = await response.json();
if (data.success && data.logs && data.logs.length > 0) {
// 有失败记录,自动显示失败列表
showFailedListFromData(data.logs.map(log => ({
id: log.account_id,
email: log.account_email,
error: log.error_message
})));
}
} catch (error) {
console.error('自动加载失败列表失败:', error);
}
}
// 隐藏刷新模态框
function hideRefreshModal() {
document.getElementById('refreshModal').classList.remove('show');
// 不隐藏列表,保留上次的数据
}
// 加载刷新统计
async function loadRefreshStats() {
try {
const response = await fetch('/api/accounts/refresh-stats');
const data = await response.json();
console.log('刷新统计数据:', data);
if (data.success) {
const stats = data.stats;
// 优先使用保存的本地刷新时间
if (window.lastRefreshTime && window.lastRefreshTime instanceof Date) {
document.getElementById('lastRefreshTime').textContent = formatDateTime(window.lastRefreshTime.toISOString());
} else if (stats.last_refresh_time) {
document.getElementById('lastRefreshTime').textContent = formatDateTime(stats.last_refresh_time);
} else {
document.getElementById('lastRefreshTime').textContent = '-';
}
document.getElementById('totalRefreshCount').textContent = stats.total;
document.getElementById('successRefreshCount').textContent = stats.success_count;
document.getElementById('failedRefreshCount').textContent = stats.failed_count;
console.log('统计数据已更新到页面');
}
} catch (error) {
console.error('加载刷新统计失败:', error);
}
}
// 全量刷新所有账号
async function refreshAllAccounts() {
const btn = document.getElementById('refreshAllBtn');
const progress = document.getElementById('refreshProgress');
const progressText = document.getElementById('refreshProgressText');
if (btn.disabled) return;
if (!confirm('确定要刷新所有账号的 Token 吗?这将清除之前的刷新记录,只保留本次记录。')) {
return;
}
btn.disabled = true;
btn.textContent = '刷新中...';
progress.style.display = 'block';
progressText.innerHTML = '正在初始化...';
try {
const eventSource = new EventSource('/api/accounts/refresh-all');
let totalCount = 0;
let successCount = 0;
let failedCount = 0;
eventSource.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'start') {
totalCount = data.total;
progressText.innerHTML = `总共 <strong>${totalCount}</strong> 个账号,准备开始刷新...`;
// 初始化统计
document.getElementById('totalRefreshCount').textContent = totalCount;
document.getElementById('successRefreshCount').textContent = '0';
document.getElementById('failedRefreshCount').textContent = '0';
} else if (data.type === 'progress') {
successCount = data.success_count;
failedCount = data.failed_count;
// 实时更新统计
document.getElementById('successRefreshCount').textContent = successCount;
document.getElementById('failedRefreshCount').textContent = failedCount;
progressText.innerHTML = `
正在处理: <strong>${data.email}</strong><br>
进度: <strong>${data.current}/${data.total}</strong> |
成功: <strong style="color: #28a745;">${successCount}</strong> |
失败: <strong style="color: #dc3545;">${failedCount}</strong>
`;
} else if (data.type === 'complete') {
eventSource.close();
progress.style.display = 'none';
btn.disabled = false;
btn.textContent = '🔄 全量刷新';
// 直接更新统计数据,使用本地时间
const now = new Date();
window.lastRefreshTime = now; // 保存刷新时间
document.getElementById('lastRefreshTime').textContent = '刚刚';
document.getElementById('totalRefreshCount').textContent = data.total;
document.getElementById('successRefreshCount').textContent = data.success_count;
document.getElementById('failedRefreshCount').textContent = data.failed_count;
showToast(`刷新完成!成功: ${data.success_count}, 失败: ${data.failed_count}`,
data.failed_count > 0 ? 'warning' : 'success');
// 如果成功列表是打开的,刷新成功列表
const successContainer = document.getElementById('successListContainer');
if (successContainer.style.display === 'block') {
loadSuccessLogs();
}
// 如果有失败的,显示失败列表
if (data.failed_count > 0) {
showFailedListFromData(data.failed_list);
}
}
} catch (e) {
console.error('解析进度数据失败:', e);
}
};
eventSource.onerror = function(error) {
console.error('EventSource 错误:', error);
eventSource.close();
progress.style.display = 'none';
btn.disabled = false;
btn.textContent = '🔄 全量刷新';
showToast('刷新过程中出现错误', 'error');
};
} catch (error) {
progress.style.display = 'none';
btn.disabled = false;
btn.textContent = '🔄 全量刷新';
showToast('刷新请求失败', 'error');
}
}
// 重试失败的账号
async function retryFailedAccounts() {
const btn = document.getElementById('retryFailedBtn');
const progress = document.getElementById('refreshProgress');
const progressText = document.getElementById('refreshProgressText');
if (btn.disabled) return;
btn.disabled = true;
btn.textContent = '重试中...';
progress.style.display = 'block';
progressText.textContent = '正在重试失败的账号...';
try {
const response = await fetch('/api/accounts/refresh-failed', {
method: 'POST'
});
const data = await response.json();
progress.style.display = 'none';
btn.disabled = false;
btn.textContent = '🔁 重试失败';
if (data.success) {
if (data.total === 0) {
showToast('没有需要重试的失败账号', 'info');
} else {
showToast(`重试完成!成功: ${data.success_count}, 失败: ${data.failed_count}`,
data.failed_count > 0 ? 'warning' : 'success');
// 刷新统计
loadRefreshStats();
// 如果成功列表是打开的,刷新成功列表
const successContainer = document.getElementById('successListContainer');
if (successContainer.style.display === 'block') {
loadSuccessLogs();
}
// 如果还有失败的,显示失败列表
if (data.failed_count > 0) {
showFailedListFromData(data.failed_list);
} else {
hideFailedList();
}
}
} else {
showToast(data.error || '重试失败', 'error');
}
} catch (error) {
progress.style.display = 'none';
btn.disabled = false;
btn.textContent = '🔁 重试失败';
showToast('重试请求失败', 'error');
}
}
// 单个账号重试
async function retrySingleAccount(accountId, accountEmail) {
try {
const response = await fetch(`/api/accounts/${accountId}/retry-refresh`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast(`${accountEmail} 刷新成功`, 'success');
loadRefreshStats();
// 如果成功列表是打开的,刷新成功列表
const successContainer = document.getElementById('successListContainer');
if (successContainer.style.display === 'block') {
loadSuccessLogs();
}
// 刷新失败列表
loadFailedLogs();
} else {
showToast(`${accountEmail} 刷新失败: ${data.error}`, 'error');
}
} catch (error) {
showToast('刷新请求失败', 'error');
}
}
// 显示失败列表(从数据)
function showFailedListFromData(failedList) {
const container = document.getElementById('failedListContainer');
const listEl = document.getElementById('failedList');
// 隐藏其他列表
hideSuccessList();
hideRefreshLogs();
if (!failedList || failedList.length === 0) {
container.style.display = 'none';
return;
}
let html = '';
failedList.forEach(item => {
html += `
<div style="padding: 12px; border-bottom: 1px solid #e5e5e5; display: flex; justify-content: space-between; align-items: start;">
<div style="flex: 1;">
<div style="font-weight: 600; margin-bottom: 4px;">${escapeHtml(item.email)}</div>
<div style="font-size: 12px; color: #dc3545;">${escapeHtml(item.error || '未知错误')}</div>
</div>
<button class="btn btn-sm btn-primary" onclick="retrySingleAccount(${item.id}, '${escapeHtml(item.email)}')">
重试
</button>
</div>
`;
});
listEl.innerHTML = html;
container.style.display = 'block';
}
// 隐藏失败列表
function hideFailedList() {
document.getElementById('failedListContainer').style.display = 'none';
}
// 加载失败日志
async function loadFailedLogs() {
try {
const response = await fetch('/api/accounts/refresh-logs/failed');
const data = await response.json();
if (data.success) {
showFailedListFromData(data.logs.map(log => ({
id: log.account_id,
email: log.account_email,
error: log.error_message
})));
}
} catch (error) {
showToast('加载失败列表失败', 'error');
}
}
// 加载成功列表
async function loadSuccessLogs() {
const container = document.getElementById('successListContainer');
const listEl = document.getElementById('successList');
// 隐藏其他列表
hideFailedList();
hideRefreshLogs();
try {
const response = await fetch('/api/accounts/refresh-logs?limit=100');
const data = await response.json();
if (data.success) {
const successLogs = data.logs.filter(log => log.status === 'success');
if (successLogs.length === 0) {
listEl.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">暂无成功记录</div>';
} else {
let html = '';
successLogs.forEach(log => {
const typeText = log.refresh_type === 'manual' ? '手动' :
log.refresh_type === 'retry' ? '重试' : '自动';
html += `
<div style="padding: 12px; border-bottom: 1px solid #e5e5e5;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 4px;">
<div style="font-weight: 600;">${escapeHtml(log.account_email)}</div>
<div style="display: flex; gap: 8px; align-items: center;">
<span style="font-size: 11px; padding: 2px 6px; background-color: #f0f0f0; border-radius: 3px;">${typeText}</span>
<span style="font-size: 12px; color: #28a745; font-weight: 600;">成功</span>
</div>
</div>
<div style="font-size: 12px; color: #666;">${formatDateTime(log.created_at)}</div>
</div>
`;
});
listEl.innerHTML = html;
}
container.style.display = 'block';
}
} catch (error) {
showToast('加载成功列表失败', 'error');
}
}
// 隐藏成功列表
function hideSuccessList() {
document.getElementById('successListContainer').style.display = 'none';
}
// 加载刷新历史
async function loadRefreshLogs() {
const container = document.getElementById('refreshLogsContainer');
const listEl = document.getElementById('refreshLogsList');
try {
const response = await fetch('/api/accounts/refresh-logs?limit=50');
const data = await response.json();
if (data.success) {
if (data.logs.length === 0) {
listEl.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">暂无刷新历史</div>';
} else {
let html = '';
data.logs.forEach(log => {
const statusColor = log.status === 'success' ? '#28a745' : '#dc3545';
const statusText = log.status === 'success' ? '成功' : '失败';
const typeText = log.refresh_type === 'manual' ? '手动' :
log.refresh_type === 'retry' ? '重试' : '自动';
html += `
<div style="padding: 12px; border-bottom: 1px solid #e5e5e5;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 4px;">
<div style="font-weight: 600;">${escapeHtml(log.account_email)}</div>
<div style="display: flex; gap: 8px; align-items: center;">
<span style="font-size: 11px; padding: 2px 6px; background-color: #f0f0f0; border-radius: 3px;">${typeText}</span>
<span style="font-size: 12px; color: ${statusColor}; font-weight: 600;">${statusText}</span>
</div>
</div>
<div style="font-size: 12px; color: #666;">${formatDateTime(log.created_at)}</div>
${log.error_message ? `<div style="font-size: 12px; color: #dc3545; margin-top: 4px;">${escapeHtml(log.error_message)}</div>` : ''}
</div>
`;
});
listEl.innerHTML = html;
}
container.style.display = 'block';
}
} catch (error) {
showToast('加载刷新历史失败', 'error');
}
}
// 隐藏刷新历史
function hideRefreshLogs() {
document.getElementById('refreshLogsContainer').style.display = 'none';
}
// 格式化日期时间
function formatDateTime(dateStr) {
if (!dateStr) return '-';
let date;
if (dateStr instanceof Date) {
date = dateStr;
} else {
// 如果字符串不包含时区信息,假定为 UTC 时间
if (!dateStr.includes('Z') && !dateStr.includes('+') && !dateStr.includes('-', 10)) {
dateStr = dateStr + 'Z';
}
date = new Date(dateStr);
}
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
if (hours < 24) return `${hours}小时前`;
if (days < 7) return `${days}天前`;
return date.toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// HTML 转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 键盘快捷键
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
hideAddGroupModal();
hideAddAccountModal();
hideEditAccountModal();
hideExportModal();
hideSettingsModal();
hideRefreshModal();
closeFullscreenEmail();
}
});
</script>
<!-- 全屏查看邮件模态框 -->
<div id="fullscreenEmailModal" class="fullscreen-email-modal" onclick="closeFullscreenEmailOnBackdrop(event)">
<div class="fullscreen-email-header">
<div class="fullscreen-email-title" id="fullscreenEmailTitle">邮件详情</div>
<button class="fullscreen-close-btn" onclick="closeFullscreenEmail()">×</button>
</div>
<div class="fullscreen-email-content" id="fullscreenEmailContent">
<!-- 邮件内容将在这里显示 -->
</div>
</div>
</body>
</html>