3853 lines
150 KiB
HTML
3853 lines
150 KiB
HTML
<!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()">×</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()">×</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()">×</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()">×</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()">×</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()">×</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()">×</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> |