mail / static /index.html
ctime's picture
Upload 3 files
d14f4f3 verified
<!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: #f8fafc;
color: #334155;
line-height: 1.6;
}
.app-container {
display: flex;
height: 100vh;
}
/* 左侧菜单 */
.sidebar {
width: 180px;
background: #ffffff;
border-right: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid #e2e8f0;
}
.sidebar-header h1 {
font-size: 1.5rem;
font-weight: 600;
color: #1e293b;
margin-bottom: 4px;
}
.sidebar-header p {
font-size: 0.875rem;
color: #64748b;
}
.sidebar-nav {
flex: 1;
padding: 20px 0;
}
.nav-item {
display: flex;
align-items: center;
padding: 12px 20px;
color: #64748b;
text-decoration: none;
transition: all 0.2s;
cursor: pointer;
border: none;
background: none;
width: 100%;
text-align: left;
font-size: 0.875rem;
}
.nav-item:hover {
background: #f1f5f9;
color: #334155;
}
.nav-item.active {
background: #3b82f6;
color: white;
}
.nav-item .icon {
margin-right: 12px;
font-size: 1rem;
}
/* 主内容区域 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.main-header {
background: white;
border-bottom: 1px solid #e2e8f0;
padding: 16px 24px;
display: flex;
justify-content: between;
align-items: center;
}
.main-header h2 {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
}
.main-body {
flex: 1;
padding: 24px;
overflow-y: auto;
background: #f8fafc;
}
.card {
background: white;
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e2e8f0;
}
/* 表单样式 */
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #374151;
font-size: 0.875rem;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.875rem;
transition: all 0.2s;
background: white;
}
.form-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
/* 按钮样式 */
.btn {
display: inline-flex;
align-items: center;
padding: 10px 16px;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
gap: 8px;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-secondary {
background: #f8fafc;
color: #64748b;
border: 1px solid #e2e8f0;
}
.btn-secondary:hover {
background: #f1f5f9;
color: #475569;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-sm {
padding: 6px 12px;
font-size: 0.75rem;
}
/* 邮箱账户列表 */
.account-list {
display: grid;
gap: 12px;
}
.account-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: white;
transition: all 0.2s;
cursor: pointer;
}
.account-item:hover {
border-color: #3b82f6;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.account-info {
display: flex;
align-items: center;
gap: 12px;
}
.account-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #3b82f6;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 1rem;
}
.account-details h4 {
margin: 0;
font-size: 0.875rem;
font-weight: 500;
color: #1e293b;
}
.account-details p {
margin: 0;
font-size: 0.75rem;
color: #64748b;
}
.account-actions {
display: flex;
gap: 8px;
}
/* 邮件列表 */
.email-list {
display: grid;
gap: 12px;
}
.email-item {
display: flex;
align-items: flex-start;
padding: 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: white;
transition: all 0.2s;
cursor: pointer;
}
.email-item:hover {
border-color: #3b82f6;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.email-item.unread {
border-left: 4px solid #3b82f6;
}
.email-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #3b82f6;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 1rem;
flex-shrink: 0;
margin-right: 12px;
}
.email-content {
flex: 1;
min-width: 0;
}
.email-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.email-subject {
font-weight: 600;
color: #1e293b;
font-size: 0.875rem;
margin: 0;
line-height: 1.4;
}
.email-date {
font-size: 0.75rem;
color: #64748b;
white-space: nowrap;
margin-left: 12px;
}
.email-from {
font-size: 0.75rem;
color: #64748b;
margin-bottom: 4px;
}
.email-preview {
font-size: 0.75rem;
color: #9ca3af;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 邮件详情模态框 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
width: 90%;
max-width: 800px;
max-height: 90%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 20px 24px;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: #1e293b;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #64748b;
padding: 4px;
}
.modal-body {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.email-detail-meta {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e2e8f0;
}
.email-detail-meta p {
margin: 8px 0;
font-size: 0.875rem;
color: #64748b;
}
.email-detail-meta strong {
color: #374151;
margin-right: 8px;
}
.email-content-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.content-tab {
padding: 8px 16px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: white;
color: #64748b;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.content-tab:hover {
border-color: #3b82f6;
color: #3b82f6;
}
.content-tab.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.email-content-body {
border: 1px solid #e2e8f0;
border-radius: 6px;
overflow: hidden;
}
.email-content-body iframe {
width: 100%;
min-height: 400px;
border: none;
}
.email-content-body pre {
padding: 16px;
margin: 0;
white-space: pre-wrap;
font-family: inherit;
line-height: 1.6;
}
/* 邮箱复制功能样式 */
.email-copyable {
transition: all 0.2s ease;
border-radius: 4px;
padding: 2px 4px;
margin: 0 2px;
}
.email-copyable:hover {
background-color: #f1f5f9;
transform: translateY(-1px);
}
.email-copyable:active {
transform: translateY(0);
background-color: #e2e8f0;
}
.copy-icon {
transition: opacity 0.2s ease;
}
.email-copyable:hover + .copy-icon {
opacity: 1 !important;
}
/* 复制成功动画 */
@keyframes copySuccess {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.copy-success {
animation: copySuccess 0.3s ease;
background-color: #dcfce7 !important;
color: #16a34a !important;
}
/* 工具类 */
.hidden { display: none !important; }
/* 加载状态 */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
color: #64748b;
font-style: italic;
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #e2e8f0;
border-radius: 50%;
border-top-color: #3b82f6;
animation: spin 1s ease-in-out infinite;
margin-right: 8px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 进度条 */
.progress-bar {
width: 100%;
height: 4px;
background: #e2e8f0;
border-radius: 2px;
overflow: hidden;
margin: 16px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #1d4ed8);
border-radius: 2px;
transition: width 0.3s ease;
width: 0%;
}
.progress-animated {
background: linear-gradient(90deg, #3b82f6, #1d4ed8, #3b82f6);
background-size: 200% 100%;
animation: progress-animation 2s linear infinite;
}
@keyframes progress-animation {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* 通知系统 */
.notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
max-width: 400px;
}
.notification {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-left: 4px solid #3b82f6;
display: flex;
align-items: flex-start;
gap: 12px;
transform: translateX(100%);
animation: slideIn 0.3s ease-out forwards;
}
.notification.success {
border-left-color: #10b981;
}
.notification.error {
border-left-color: #ef4444;
}
.notification.warning {
border-left-color: #f59e0b;
}
.notification-icon {
font-size: 1.25rem;
flex-shrink: 0;
margin-top: 2px;
}
.notification-content {
flex: 1;
}
.notification-title {
font-weight: 600;
font-size: 0.875rem;
margin-bottom: 4px;
color: #1f2937;
}
.notification-message {
font-size: 0.875rem;
color: #6b7280;
line-height: 1.4;
}
.notification-close {
background: none;
border: none;
color: #9ca3af;
cursor: pointer;
font-size: 1.25rem;
padding: 0;
line-height: 1;
}
.notification-close:hover {
color: #6b7280;
}
@keyframes slideIn {
to { transform: translateX(0); }
}
@keyframes slideOut {
to { transform: translateX(100%); }
}
.notification.slide-out {
animation: slideOut 0.3s ease-in forwards;
}
/* 传统错误/成功消息样式(保留兼容性) */
.error {
background: #fef2f2;
color: #dc2626;
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 16px;
border: 1px solid #fecaca;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 8px;
}
.success {
background: #f0fdf4;
color: #16a34a;
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 16px;
border: 1px solid #bbf7d0;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 8px;
}
.text-center { text-align: center; }
.text-sm { font-size: 0.875rem; }
.text-xs { font-size: 0.75rem; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mt-4 { margin-top: 1rem; }
.flex { display: flex; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: 0.5rem; }
.gap-4 { gap: 1rem; }
/* 分页 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 20px;
}
/* 标签页 */
.tabs {
display: flex;
border-bottom: 1px solid #e2e8f0;
margin-bottom: 20px;
}
.tab {
flex: 1;
padding: 12px 16px;
border: none;
background: none;
cursor: pointer;
color: #64748b;
font-weight: 500;
transition: all 0.2s;
font-size: 0.875rem;
}
.tab:hover {
color: #374151;
}
.tab.active {
color: #3b82f6;
border-bottom: 2px solid #3b82f6;
}
/* 搜索和过滤 */
.search-container {
position: relative;
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 12px 16px 12px 40px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 0.875rem;
background: white;
transition: all 0.2s;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
font-size: 1rem;
}
.filter-container {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 0.875rem;
color: #6b7280;
font-weight: 500;
}
.filter-select {
padding: 6px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.875rem;
background: white;
min-width: 120px;
}
.filter-select:focus {
outline: none;
border-color: #3b82f6;
}
/* 统计信息 */
.stats-container {
display: flex;
gap: 16px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.stat-item {
background: white;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #e2e8f0;
min-width: 120px;
}
.stat-value {
font-size: 1.5rem;
font-weight: 600;
color: #1e293b;
margin-bottom: 4px;
}
.stat-label {
font-size: 0.75rem;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* 表单页面样式 */
.form-page {
max-width: 600px;
margin: 0 auto;
}
.form-section {
background: #f8fafc;
padding: 16px;
border-radius: 8px;
border: 1px solid #e2e8f0;
margin-bottom: 20px;
}
.form-section h4 {
margin: 0 0 8px 0;
font-size: 0.875rem;
font-weight: 600;
color: #374151;
}
.form-section ol, .form-section ul {
margin: 0;
padding-left: 20px;
font-size: 0.75rem;
color: #6b7280;
line-height: 1.5;
}
.form-section a {
color: #3b82f6;
text-decoration: none;
}
.form-section a:hover {
text-decoration: underline;
}
/* 代码样式 */
code {
background: #f1f5f9;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
color: #1e293b;
}
/* API文档样式 */
.api-docs {
max-width: 1000px;
}
.api-endpoint {
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 24px;
overflow: hidden;
}
.api-header {
background: #f8fafc;
padding: 16px 20px;
border-bottom: 1px solid #e2e8f0;
display: flex;
align-items: center;
justify-content: space-between;
}
.api-method {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
margin-right: 12px;
}
.api-method.get {
background: #dbeafe;
color: #1d4ed8;
}
.api-method.post {
background: #dcfce7;
color: #16a34a;
}
.api-method.put {
background: #fef3c7;
color: #d97706;
}
.api-method.delete {
background: #fee2e2;
color: #dc2626;
}
.api-path {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
color: #374151;
font-weight: 500;
}
.api-body {
padding: 20px;
}
.api-description {
color: #6b7280;
margin-bottom: 16px;
line-height: 1.6;
}
.api-section {
margin-bottom: 20px;
}
.api-section h4 {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.api-params {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 6px;
overflow: hidden;
}
.api-param {
padding: 12px 16px;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: flex-start;
gap: 12px;
}
.api-param:last-child {
border-bottom: none;
}
.api-param-name {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
color: #1f2937;
font-weight: 500;
min-width: 120px;
}
.api-param-type {
background: #e5e7eb;
color: #374151;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
min-width: 60px;
text-align: center;
}
.api-param-desc {
color: #6b7280;
font-size: 0.875rem;
flex: 1;
}
.api-example {
background: #1e293b;
color: #e2e8f0;
padding: 16px;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
overflow-x: auto;
line-height: 1.5;
}
.api-try-button {
background: #3b82f6;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
}
.api-try-button:hover {
background: #2563eb;
}
.api-response {
margin-top: 16px;
padding: 16px;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 6px;
display: none;
}
.api-response.show {
display: block;
}
.api-response pre {
margin: 0;
font-size: 0.875rem;
color: #15803d;
}
/* 响应式设计 */
@media (max-width: 768px) {
.app-container {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
border-right: none;
border-bottom: 1px solid #e2e8f0;
}
.sidebar-nav {
display: flex;
overflow-x: auto;
padding: 10px 0;
}
.nav-item {
white-space: nowrap;
padding: 8px 16px;
margin: 0 4px;
border-radius: 6px;
}
.main-body {
padding: 16px;
}
.modal-content {
width: 95%;
max-height: 95%;
}
.modal-body {
padding: 16px;
}
.account-item {
padding: 12px;
}
.account-avatar {
width: 32px;
height: 32px;
font-size: 0.875rem;
}
.email-item {
padding: 12px;
}
.email-avatar {
width: 32px;
height: 32px;
font-size: 0.875rem;
}
}
</style>
</head>
<body>
<div class="app-container">
<!-- 左侧菜单 -->
<div class="sidebar">
<div class="sidebar-header">
<h1>📧 邮件管理</h1>
<p>Outlook邮件管理系统</p>
</div>
<nav class="sidebar-nav">
<button class="nav-item active" onclick="showPage('accounts', this)">
<span class="icon">👥</span>
邮箱账户
</button>
<button class="nav-item" onclick="showPage('addAccount', this)">
<span class="icon"></span>
添加账户
</button>
<button class="nav-item" onclick="showPage('batchAdd', this)">
<span class="icon">📦</span>
批量添加
</button>
<button class="nav-item" onclick="showPage('apiDocs', this)">
<span class="icon">📖</span>
API管理
</button>
<button class="nav-item" onclick="showPage('emails', this)" id="emailsNav" style="display: none;">
<span class="icon">📧</span>
邮件列表
</button>
</nav>
</div>
<!-- 主内容区域 -->
<div class="main-content">
<div class="main-header">
<h2 id="pageTitle">邮箱账户管理</h2>
<div id="headerActions"></div>
</div>
<div class="main-body">
<!-- 邮箱账户列表页面 -->
<div id="accountsPage" class="page">
<div class="card">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="font-semibold">邮箱账户管理</h3>
<p class="text-sm" style="color: #64748b;">管理所有已添加的邮箱账户</p>
</div>
<div class="flex gap-2">
<button class="btn btn-primary btn-sm" onclick="showPage('addAccount')">
<span></span>
添加账户
</button>
<button class="btn btn-secondary btn-sm" onclick="showPage('batchAdd')">
<span>📦</span>
批量添加
</button>
<button class="btn btn-secondary btn-sm" onclick="loadAccounts()">
<span>🔄</span>
刷新列表
</button>
</div>
</div>
<div id="accountsList" class="account-list">
<div class="loading">
<div class="loading-spinner"></div>
正在加载账户列表...
</div>
</div>
</div>
</div>
<!-- 添加单个账户页面 -->
<div id="addAccountPage" class="page hidden">
<div class="card">
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="font-semibold">添加邮箱账户</h3>
<p class="text-sm" style="color: #64748b;">添加单个Outlook邮箱账户到系统</p>
</div>
<button class="btn btn-secondary btn-sm" onclick="showPage('accounts')">
<span></span>
返回列表
</button>
</div>
<div style="max-width: 600px;">
<div class="form-group">
<label class="form-label">邮箱地址 *</label>
<input type="email" id="email" class="form-input" placeholder="[email protected]" required>
<p class="text-xs" style="color: #64748b; margin-top: 4px;">请输入有效的Outlook邮箱地址</p>
</div>
<div class="form-group">
<label class="form-label">刷新令牌 (Refresh Token) *</label>
<textarea id="refreshToken" class="form-input form-textarea" rows="4" placeholder="从Azure应用获取的refresh_token" required></textarea>
<p class="text-xs" style="color: #64748b; margin-top: 4px;">
从Azure应用程序注册中获取的刷新令牌,用于OAuth2认证
</p>
</div>
<div class="form-group">
<label class="form-label">客户端ID (Client ID) *</label>
<input type="text" id="clientId" class="form-input" placeholder="Azure应用的client_id" required>
<p class="text-xs" style="color: #64748b; margin-top: 4px;">
Azure应用程序的客户端标识符
</p>
</div>
<div class="form-group">
<div style="background: #f8fafc; padding: 16px; border-radius: 8px; border: 1px solid #e2e8f0;">
<h4 style="margin: 0 0 8px 0; font-size: 0.875rem; font-weight: 600; color: #374151;">📋 获取步骤:</h4>
<ol style="margin: 0; padding-left: 20px; font-size: 0.75rem; color: #6b7280; line-height: 1.5;">
<li>访问 <a href="https://portal.azure.com" target="_blank" style="color: #3b82f6;">Azure Portal</a></li>
<li>注册应用程序并配置权限</li>
<li>获取Client ID和Refresh Token</li>
<li>确保应用有邮件读取权限</li>
</ol>
</div>
</div>
<div class="flex gap-3">
<button class="btn btn-primary" onclick="addAccount()" id="addAccountBtn">
<span></span>
添加账户
</button>
<button class="btn btn-secondary" onclick="clearAddAccountForm()">
<span>🗑️</span>
清空表单
</button>
<button class="btn btn-secondary" onclick="testAccountConnection()" id="testBtn">
<span>🔍</span>
测试连接
</button>
</div>
</div>
</div>
</div>
<!-- 批量添加账户页面 -->
<div id="batchAddPage" class="page hidden">
<div class="card">
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="font-semibold">批量添加邮箱账户</h3>
<p class="text-sm" style="color: #64748b;">一次性添加多个邮箱账户到系统</p>
</div>
<button class="btn btn-secondary btn-sm" onclick="showPage('accounts')">
<span></span>
返回列表
</button>
</div>
<div style="max-width: 800px;">
<div class="form-group">
<label class="form-label">批量账户信息</label>
<p class="text-sm" style="color: #64748b; margin-bottom: 8px;">
每行格式:<code>邮箱----密码----刷新令牌----客户端ID</code>
</p>
<p class="text-sm" style="color: #64748b; margin-bottom: 12px;">
📧 推荐购买地址(非广告,只是推荐):
<a target="_blank" href="http://wmemail.com" style="color: #3b82f6; margin: 0 8px;">wmemail.com</a>
</p>
<textarea id="batchAccounts" class="form-input form-textarea" rows="12" placeholder="[email protected]_token1----client_id1
[email protected]_token2----client_id2
[email protected]_token3----client_id3"></textarea>
<p class="text-xs" style="color: #64748b; margin-top: 8px;">
💡 提示:每行一个账户,使用四个连字符(----)分隔字段
</p>
</div>
<div class="form-group">
<div style="background: #fef3c7; padding: 16px; border-radius: 8px; border: 1px solid #fbbf24;">
<h4 style="margin: 0 0 8px 0; font-size: 0.875rem; font-weight: 600; color: #92400e;">⚠️ 注意事项:</h4>
<ul style="margin: 0; padding-left: 20px; font-size: 0.75rem; color: #92400e; line-height: 1.5;">
<li>确保所有账户信息格式正确</li>
<li>建议先测试少量账户再批量添加</li>
<li>添加过程中请勿关闭页面</li>
<li>失败的账户会在结果中显示</li>
</ul>
</div>
</div>
<div class="flex gap-3">
<button class="btn btn-primary" onclick="batchAddAccounts()" id="batchAddBtn">
<span>📦</span>
开始批量添加
</button>
<button class="btn btn-secondary" onclick="clearBatchForm()">
<span>🗑️</span>
清空表单
</button>
<button class="btn btn-secondary" onclick="validateBatchFormat()">
<span></span>
验证格式
</button>
<button class="btn btn-secondary" onclick="loadSampleData()">
<span>📝</span>
加载示例
</button>
</div>
<!-- 批量添加进度 -->
<div id="batchProgress" class="hidden" style="margin-top: 24px;">
<h4 style="margin-bottom: 12px; font-size: 0.875rem; font-weight: 600;">添加进度:</h4>
<div class="progress-bar">
<div class="progress-fill" id="batchProgressFill"></div>
</div>
<div style="display: flex; justify-content: space-between; font-size: 0.75rem; color: #64748b; margin-top: 8px;">
<span id="batchProgressText">准备中...</span>
<span id="batchProgressCount">0 / 0</span>
</div>
</div>
<!-- 批量添加结果 -->
<div id="batchResults" class="hidden" style="margin-top: 24px;">
<h4 style="margin-bottom: 12px; font-size: 0.875rem; font-weight: 600;">添加结果:</h4>
<div id="batchResultsList"></div>
</div>
</div>
</div>
</div>
<!-- API文档页面 -->
<div id="apiDocsPage" class="page hidden">
<div class="card">
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="font-semibold">API接口文档</h3>
<p class="text-sm" style="color: #64748b;">邮件管理系统的RESTful API接口说明</p>
</div>
<div class="flex gap-2">
<button class="btn btn-secondary btn-sm" onclick="copyApiBaseUrl()">
<span>📋</span>
复制Base URL
</button>
<button class="btn btn-secondary btn-sm" onclick="downloadApiDocs()">
<span>📥</span>
下载文档
</button>
</div>
</div>
<div class="api-docs">
<!-- API基础信息 -->
<div class="api-endpoint">
<div class="api-header">
<div>
<h3 style="margin: 0; color: #1f2937;">📡 API基础信息</h3>
</div>
</div>
<div class="api-body">
<div class="api-section">
<h4>Base URL</h4>
<div class="api-example" id="baseUrlExample">http://localhost:8001</div>
</div>
<div class="api-section">
<h4>认证方式</h4>
<p class="api-description">当前版本无需认证,直接调用接口即可。</p>
</div>
<div class="api-section">
<h4>响应格式</h4>
<p class="api-description">所有接口返回JSON格式数据,HTTP状态码表示请求结果。</p>
</div>
</div>
</div>
<!-- 获取邮箱列表接口 -->
<div class="api-endpoint">
<div class="api-header">
<div style="display: flex; align-items: center;">
<span class="api-method get">GET</span>
<span class="api-path">/accounts</span>
</div>
<button class="api-try-button" onclick="tryApi('accounts')">🚀 试用接口</button>
</div>
<div class="api-body">
<p class="api-description">获取系统中所有已添加的邮箱账户列表。</p>
<div class="api-section">
<h4>请求参数</h4>
<p style="color: #6b7280; font-size: 0.875rem;">无需参数</p>
</div>
<div class="api-section">
<h4>响应示例</h4>
<div class="api-example">{
"accounts": [
{
"email_id": "[email protected]",
"status": "active",
"last_sync": "2024-01-01T12:00:00Z"
}
],
"total_count": 1
}</div>
</div>
<div class="api-response" id="accountsResponse">
<h4 style="margin-bottom: 8px; color: #15803d;">响应结果:</h4>
<pre id="accountsResponseData"></pre>
</div>
</div>
</div>
<!-- 获取邮件列表接口 -->
<div class="api-endpoint">
<div class="api-header">
<div style="display: flex; align-items: center;">
<span class="api-method get">GET</span>
<span class="api-path">/emails/{email_id}</span>
</div>
<button class="api-try-button" onclick="tryApi('emails')">🚀 试用接口</button>
</div>
<div class="api-body">
<p class="api-description">获取指定邮箱的邮件列表,支持分页和过滤。</p>
<div class="api-section">
<h4>路径参数</h4>
<div class="api-params">
<div class="api-param">
<span class="api-param-name">email_id</span>
<span class="api-param-type">string</span>
<span class="api-param-desc">邮箱地址,需要URL编码</span>
</div>
</div>
</div>
<div class="api-section">
<h4>查询参数</h4>
<div class="api-params">
<div class="api-param">
<span class="api-param-name">folder</span>
<span class="api-param-type">string</span>
<span class="api-param-desc">邮件文件夹 (all, inbox, junk),默认: all</span>
</div>
<div class="api-param">
<span class="api-param-name">page</span>
<span class="api-param-type">integer</span>
<span class="api-param-desc">页码,从1开始,默认: 1</span>
</div>
<div class="api-param">
<span class="api-param-name">page_size</span>
<span class="api-param-type">integer</span>
<span class="api-param-desc">每页数量,范围1-500,默认: 100</span>
</div>
<div class="api-param">
<span class="api-param-name">refresh</span>
<span class="api-param-type">boolean</span>
<span class="api-param-desc">是否强制刷新缓存,默认: false</span>
</div>
</div>
</div>
<div class="api-section">
<h4>请求示例</h4>
<div class="api-example">GET /emails/example%40outlook.com?folder=inbox&page=1&page_size=20&refresh=true</div>
</div>
<div class="api-section">
<h4>响应示例</h4>
<div class="api-example">{
"email_id": "[email protected]",
"folder_view": "inbox",
"page": 1,
"page_size": 20,
"total_emails": 150,
"emails": [
{
"message_id": "INBOX-1",
"folder": "INBOX",
"subject": "邮件主题",
"from_email": "[email protected]",
"date": "2024-01-01T12:00:00Z",
"is_read": false,
"has_attachments": true,
"sender_initial": "S"
}
]
}</div>
</div>
<div class="api-response" id="emailsResponse">
<h4 style="margin-bottom: 8px; color: #15803d;">响应结果:</h4>
<pre id="emailsResponseData"></pre>
</div>
</div>
</div>
<!-- 获取邮件详情接口 -->
<div class="api-endpoint">
<div class="api-header">
<div style="display: flex; align-items: center;">
<span class="api-method get">GET</span>
<span class="api-path">/emails/{email_id}/{message_id}</span>
</div>
<button class="api-try-button" onclick="tryApi('emailDetail')">🚀 试用接口</button>
</div>
<div class="api-body">
<p class="api-description">获取指定邮件的详细内容,包括邮件正文。</p>
<div class="api-section">
<h4>路径参数</h4>
<div class="api-params">
<div class="api-param">
<span class="api-param-name">email_id</span>
<span class="api-param-type">string</span>
<span class="api-param-desc">邮箱地址,需要URL编码</span>
</div>
<div class="api-param">
<span class="api-param-name">message_id</span>
<span class="api-param-type">string</span>
<span class="api-param-desc">邮件ID,格式: {folder}-{id}</span>
</div>
</div>
</div>
<div class="api-section">
<h4>请求示例</h4>
<div class="api-example">GET /emails/example%40outlook.com/INBOX-1</div>
</div>
<div class="api-section">
<h4>响应示例</h4>
<div class="api-example">{
"message_id": "INBOX-1",
"subject": "邮件主题",
"from_email": "[email protected]",
"to_email": "[email protected]",
"date": "2024-01-01T12:00:00Z",
"body_plain": "纯文本邮件内容",
"body_html": "&lt;html&gt;&lt;body&gt;HTML邮件内容&lt;/body&gt;&lt;/html&gt;"
}</div>
</div>
<div class="api-response" id="emailDetailResponse">
<h4 style="margin-bottom: 8px; color: #15803d;">响应结果:</h4>
<pre id="emailDetailResponseData"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 邮件列表页面 -->
<div id="emailsPage" class="page hidden">
<div class="card">
<!-- 账户信息和操作按钮 -->
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="font-semibold">当前账户:
<span id="currentAccountEmail"
class="text-sm font-medium email-copyable"
style="color: #3b82f6; cursor: pointer; position: relative;"
onclick="copyEmailAddress(this.textContent)"
title="点击复制邮箱地址">
</span>
<span class="copy-icon" style="margin-left: 6px; opacity: 0.6; font-size: 0.8em;">📋</span>
</h3>
<p class="text-sm" style="color: #64748b;">最后更新:<span id="lastUpdateTime">-</span></p>
</div>
<div class="flex gap-2">
<button class="btn btn-secondary btn-sm" onclick="refreshEmails()" id="refreshBtn">
<span>🔄</span>
刷新
</button>
<button class="btn btn-secondary btn-sm" onclick="clearCache()">
<span>🗑️</span>
清除缓存
</button>
<button class="btn btn-secondary btn-sm" onclick="exportEmails()">
<span>📤</span>
导出
</button>
<button class="btn btn-secondary btn-sm" onclick="backToAccounts()">
<span></span>
返回账户
</button>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-container">
<div class="stat-item">
<div class="stat-value" id="totalEmailCount">0</div>
<div class="stat-label">总邮件数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="unreadEmailCount">0</div>
<div class="stat-label">未读邮件</div>
</div>
<div class="stat-item">
<div class="stat-value" id="todayEmailCount">0</div>
<div class="stat-label">今日邮件</div>
</div>
<div class="stat-item">
<div class="stat-value" id="attachmentEmailCount">0</div>
<div class="stat-label">带附件</div>
</div>
</div>
<!-- 搜索和过滤 -->
<div class="search-container">
<span class="search-icon">🔍</span>
<input type="text" class="search-input" id="emailSearch" placeholder="搜索邮件标题、发件人或内容..." onkeyup="searchEmails()">
</div>
<div class="filter-container">
<div class="filter-group">
<label class="filter-label">文件夹:</label>
<select class="filter-select" id="folderFilter" onchange="applyFilters()">
<option value="all">全部邮件</option>
<option value="inbox">收件箱</option>
<option value="junk">垃圾邮件</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">状态:</label>
<select class="filter-select" id="statusFilter" onchange="applyFilters()">
<option value="all">全部状态</option>
<option value="unread">未读</option>
<option value="read">已读</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">时间:</label>
<select class="filter-select" id="timeFilter" onchange="applyFilters()">
<option value="all">全部时间</option>
<option value="today">今天</option>
<option value="week">本周</option>
<option value="month">本月</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">附件:</label>
<select class="filter-select" id="attachmentFilter" onchange="applyFilters()">
<option value="all">全部</option>
<option value="with">有附件</option>
<option value="without">无附件</option>
</select>
</div>
<button class="btn btn-secondary btn-sm" onclick="clearFilters()">
<span>🗑️</span>
清除筛选
</button>
</div>
<!-- 邮件列表 -->
<div id="emailsList" class="email-list">
<div class="loading">
<div class="loading-spinner"></div>
正在加载邮件...
</div>
</div>
<div id="emailsPagination" class="pagination hidden"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 通知容器 -->
<div id="notificationContainer" class="notification-container"></div>
<!-- 邮件详情模态框 -->
<div id="emailModal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3 id="emailModalTitle">邮件详情</h3>
<button class="modal-close" onclick="closeEmailModal()">&times;</button>
</div>
<div class="modal-body">
<div id="emailModalContent">
<div class="loading">正在加载邮件详情...</div>
</div>
</div>
</div>
</div>
<script>
// 全局变量
const API_BASE = '';
let currentAccount = null;
let currentEmailFolder = 'all';
let currentEmailPage = 1;
let accounts = [];
// 页面管理
function showPage(pageName, targetElement = null) {
// 隐藏所有页面
document.querySelectorAll('.page').forEach(page => page.classList.add('hidden'));
// 显示指定页面
document.getElementById(pageName + 'Page').classList.remove('hidden');
// 更新导航状态
document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
// 如果有目标元素,激活它;否则根据页面名称查找对应的导航项
if (targetElement) {
targetElement.classList.add('active');
} else {
// 根据页面名称查找对应的导航按钮
const navButtons = document.querySelectorAll('.nav-item');
navButtons.forEach(button => {
if (button.onclick && button.onclick.toString().includes(`'${pageName}'`)) {
button.classList.add('active');
}
});
}
// 更新页面标题
const titles = {
'accounts': '邮箱账户管理',
'addAccount': '添加邮箱账户',
'batchAdd': '批量添加账户',
'apiDocs': 'API接口文档',
'emails': '邮件列表'
};
document.getElementById('pageTitle').textContent = titles[pageName] || '';
// 页面特定逻辑
if (pageName === 'accounts') {
loadAccounts();
} else if (pageName === 'addAccount') {
clearAddAccountForm();
} else if (pageName === 'batchAdd') {
clearBatchForm();
hideBatchProgress();
} else if (pageName === 'apiDocs') {
initApiDocs();
} else if (pageName === 'emails') {
loadEmails();
}
}
// 工具函数
function formatEmailDate(dateString) {
try {
if (!dateString) return '未知时间';
let date = new Date(dateString);
if (isNaN(date.getTime())) {
if (dateString.includes('T') && !dateString.includes('Z') && !dateString.includes('+')) {
date = new Date(dateString + 'Z');
}
if (isNaN(date.getTime())) {
return '日期格式错误';
}
}
const now = new Date();
const diffMs = now - date;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
} else if (diffDays === 1) {
return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
} else if (diffDays < 7) {
return `${diffDays}天前`;
} else if (diffDays < 365) {
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
} else {
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'short', day: 'numeric' });
}
} catch (error) {
console.error('Date formatting error:', error);
return '时间解析失败';
}
}
// 新的通知系统
function showNotification(message, type = 'info', title = '', duration = 5000) {
const container = document.getElementById('notificationContainer');
const notification = document.createElement('div');
notification.className = `notification ${type}`;
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: 'ℹ️'
};
const titles = {
success: title || '成功',
error: title || '错误',
warning: title || '警告',
info: title || '提示'
};
notification.innerHTML = `
<div class="notification-icon">${icons[type]}</div>
<div class="notification-content">
<div class="notification-title">${titles[type]}</div>
<div class="notification-message">${message}</div>
</div>
<button class="notification-close" onclick="closeNotification(this)">×</button>
`;
container.appendChild(notification);
// 自动关闭
if (duration > 0) {
setTimeout(() => {
closeNotification(notification.querySelector('.notification-close'));
}, duration);
}
}
function closeNotification(closeBtn) {
const notification = closeBtn.closest('.notification');
notification.classList.add('slide-out');
setTimeout(() => notification.remove(), 300);
}
// 兼容旧的消息提示函数
function showError(msg) {
showNotification(msg, 'error');
}
function showSuccess(msg) {
showNotification(msg, 'success');
}
function showWarning(msg) {
showNotification(msg, 'warning');
}
function showInfo(msg) {
showNotification(msg, 'info');
}
// API请求
async function apiRequest(url, options = {}) {
try {
const response = await fetch(API_BASE + url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API请求失败:', error);
throw error;
}
}
// 表单管理函数
function clearAddAccountForm() {
document.getElementById('email').value = '';
document.getElementById('refreshToken').value = '';
document.getElementById('clientId').value = '';
}
function clearBatchForm() {
document.getElementById('batchAccounts').value = '';
}
function loadSampleData() {
const sampleData = `[email protected]_token_here_1----client_id_here_1
[email protected]_token_here_2----client_id_here_2
[email protected]_token_here_3----client_id_here_3`;
document.getElementById('batchAccounts').value = sampleData;
showNotification('示例数据已加载,请替换为真实数据', 'info');
}
function validateBatchFormat() {
const batchText = document.getElementById('batchAccounts').value.trim();
if (!batchText) {
showNotification('请先输入账户信息', 'warning');
return;
}
const lines = batchText.split('\n').filter(line => line.trim());
let validCount = 0;
let invalidLines = [];
lines.forEach((line, index) => {
const parts = line.split('----').map(p => p.trim());
if (parts.length === 4 && parts.every(part => part.length > 0)) {
validCount++;
} else {
invalidLines.push(index + 1);
}
});
if (invalidLines.length === 0) {
showNotification(`格式验证通过!共 ${validCount} 个有效账户`, 'success');
} else {
showNotification(`发现 ${invalidLines.length} 行格式错误:第 ${invalidLines.join(', ')} 行`, 'error');
}
}
async function testAccountConnection() {
const email = document.getElementById('email').value.trim();
const refreshToken = document.getElementById('refreshToken').value.trim();
const clientId = document.getElementById('clientId').value.trim();
if (!email || !refreshToken || !clientId) {
showNotification('请填写所有必需字段', 'warning');
return;
}
const testBtn = document.getElementById('testBtn');
testBtn.disabled = true;
testBtn.innerHTML = '<span>⏳</span> 测试中...';
try {
// 这里可以调用一个测试接口
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟测试
showNotification('连接测试成功!账户配置正确', 'success');
} catch (error) {
showNotification('连接测试失败:' + error.message, 'error');
} finally {
testBtn.disabled = false;
testBtn.innerHTML = '<span>🔍</span> 测试连接';
}
}
async function loadAccounts() {
const accountsList = document.getElementById('accountsList');
accountsList.innerHTML = '<div class="loading">正在加载账户列表...</div>';
try {
const data = await apiRequest('/accounts');
accounts = data.accounts || [];
if (accounts.length === 0) {
accountsList.innerHTML = '<div class="text-center" style="padding: 40px; color: #64748b;">暂无账户,请添加账户</div>';
return;
}
accountsList.innerHTML = accounts.map(account => `
<div class="account-item">
<div class="account-info">
<div class="account-avatar">${account.email_id.charAt(0).toUpperCase()}</div>
<div class="account-details">
<h4>${account.email_id}</h4>
<p>状态: ${account.status === 'active' ? '正常' : '异常'}</p>
</div>
</div>
<div class="account-actions">
<button class="btn btn-primary btn-sm" onclick="viewAccountEmails('${account.email_id}')">
<span>📧</span>
查看邮件
</button>
<button class="btn btn-danger btn-sm" onclick="deleteAccount('${account.email_id}')">
<span>🗑️</span>
删除
</button>
</div>
</div>
`).join('');
} catch (error) {
accountsList.innerHTML = '<div class="error">加载失败: ' + error.message + '</div>';
}
}
async function addAccount() {
const email = document.getElementById('email').value.trim();
const refreshToken = document.getElementById('refreshToken').value.trim();
const clientId = document.getElementById('clientId').value.trim();
if (!email || !refreshToken || !clientId) {
showNotification('请填写所有必需字段', 'warning');
return;
}
const addBtn = document.getElementById('addAccountBtn');
addBtn.disabled = true;
addBtn.innerHTML = '<span>⏳</span> 添加中...';
try {
await apiRequest('/accounts', {
method: 'POST',
body: JSON.stringify({
email: email,
refresh_token: refreshToken,
client_id: clientId
})
});
showNotification('账户添加成功!', 'success');
// 清空表单
clearAddAccountForm();
// 跳转到账户列表并刷新
showPage('accounts');
} catch (error) {
showNotification('添加账户失败: ' + error.message, 'error');
} finally {
addBtn.disabled = false;
addBtn.innerHTML = '<span>➕</span> 添加账户';
}
}
async function batchAddAccounts() {
const batchText = document.getElementById('batchAccounts').value.trim();
if (!batchText) {
showNotification('请输入账户信息', 'warning');
return;
}
const lines = batchText.split('\n').filter(line => line.trim());
if (lines.length === 0) {
showNotification('没有有效的账户信息', 'warning');
return;
}
// 显示进度
showBatchProgress();
const batchBtn = document.getElementById('batchAddBtn');
batchBtn.disabled = true;
batchBtn.innerHTML = '<span>⏳</span> 添加中...';
let successCount = 0;
let failCount = 0;
const results = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const parts = line.split('----').map(p => p.trim());
// 更新进度
updateBatchProgress(i + 1, lines.length, `处理第 ${i + 1} 个账户...`);
if (parts.length !== 4) {
failCount++;
results.push({
email: parts[0] || '格式错误',
status: 'error',
message: '格式错误:应为 邮箱----密码----刷新令牌----客户端ID'
});
continue;
}
const [email, password, refreshToken, clientId] = parts;
try {
await apiRequest('/accounts', {
method: 'POST',
body: JSON.stringify({
email: email,
refresh_token: refreshToken,
client_id: clientId
})
});
successCount++;
results.push({
email: email,
status: 'success',
message: '添加成功'
});
} catch (error) {
failCount++;
results.push({
email: email,
status: 'error',
message: error.message
});
}
// 添加小延迟避免请求过快
await new Promise(resolve => setTimeout(resolve, 100));
}
// 完成进度
updateBatchProgress(lines.length, lines.length, '批量添加完成!');
// 显示结果
showBatchResults(results);
if (successCount > 0) {
showNotification(`批量添加完成!成功 ${successCount} 个,失败 ${failCount} 个`, 'success');
if (failCount === 0) {
setTimeout(() => {
clearBatchForm();
showPage('accounts');
}, 3000);
}
} else {
showNotification('所有账户添加失败,请检查账户信息', 'error');
}
batchBtn.disabled = false;
batchBtn.innerHTML = '<span>📦</span> 开始批量添加';
}
function showBatchProgress() {
document.getElementById('batchProgress').classList.remove('hidden');
document.getElementById('batchResults').classList.add('hidden');
}
function hideBatchProgress() {
document.getElementById('batchProgress').classList.add('hidden');
document.getElementById('batchResults').classList.add('hidden');
}
function updateBatchProgress(current, total, message) {
const percentage = (current / total) * 100;
document.getElementById('batchProgressFill').style.width = percentage + '%';
document.getElementById('batchProgressText').textContent = message;
document.getElementById('batchProgressCount').textContent = `${current} / ${total}`;
}
function showBatchResults(results) {
const resultsContainer = document.getElementById('batchResultsList');
const successResults = results.filter(r => r.status === 'success');
const errorResults = results.filter(r => r.status === 'error');
let html = '';
if (successResults.length > 0) {
html += `<div style="margin-bottom: 16px;">
<h5 style="color: #16a34a; margin-bottom: 8px;">✅ 成功添加 (${successResults.length})</h5>
<div style="background: #f0fdf4; padding: 12px; border-radius: 6px; border: 1px solid #bbf7d0;">`;
successResults.forEach(result => {
html += `<div style="font-size: 0.875rem; color: #15803d; margin-bottom: 4px;">• ${result.email}</div>`;
});
html += `</div></div>`;
}
if (errorResults.length > 0) {
html += `<div>
<h5 style="color: #dc2626; margin-bottom: 8px;">❌ 添加失败 (${errorResults.length})</h5>
<div style="background: #fef2f2; padding: 12px; border-radius: 6px; border: 1px solid #fecaca;">`;
errorResults.forEach(result => {
html += `<div style="font-size: 0.875rem; color: #dc2626; margin-bottom: 8px;">
<strong>• ${result.email}</strong><br>
<span style="color: #991b1b; font-size: 0.75rem;">&nbsp;&nbsp;${result.message}</span>
</div>`;
});
html += `</div></div>`;
}
resultsContainer.innerHTML = html;
document.getElementById('batchResults').classList.remove('hidden');
}
// API文档相关函数
function initApiDocs() {
// 更新Base URL
const baseUrl = window.location.origin;
document.getElementById('baseUrlExample').textContent = baseUrl;
}
function copyApiBaseUrl() {
const baseUrl = window.location.origin;
navigator.clipboard.writeText(baseUrl).then(() => {
showNotification('Base URL已复制到剪贴板', 'success');
}).catch(() => {
showNotification('复制失败,请手动复制', 'error');
});
}
function copyEmailAddress(emailAddress) {
// 清理邮箱地址(去除可能的空格和特殊字符)
const cleanEmail = emailAddress.trim();
if (!cleanEmail) {
showNotification('邮箱地址为空', 'error');
return;
}
// 复制到剪贴板
navigator.clipboard.writeText(cleanEmail).then(() => {
// 显示成功通知
showNotification(`邮箱地址已复制: ${cleanEmail}`, 'success');
// 添加视觉反馈
const emailElement = document.getElementById('currentAccountEmail');
if (emailElement) {
emailElement.classList.add('copy-success');
setTimeout(() => {
emailElement.classList.remove('copy-success');
}, 300);
}
}).catch((error) => {
console.error('复制失败:', error);
// 降级方案:尝试使用旧的复制方法
try {
const textArea = document.createElement('textarea');
textArea.value = cleanEmail;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification(`邮箱地址已复制: ${cleanEmail}`, 'success');
} catch (fallbackError) {
console.error('降级复制方案也失败:', fallbackError);
showNotification('复制失败,请手动复制邮箱地址', 'error');
// 选中文本以便用户手动复制
const emailElement = document.getElementById('currentAccountEmail');
if (emailElement && window.getSelection) {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(emailElement);
selection.removeAllRanges();
selection.addRange(range);
}
}
});
}
function downloadApiDocs() {
const apiDocs = generateApiDocsMarkdown();
const blob = new Blob([apiDocs], { type: 'text/markdown;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'outlook-email-api-docs.md');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showNotification('API文档已下载', 'success');
}
function generateApiDocsMarkdown() {
const baseUrl = window.location.origin;
return `# Outlook邮件管理系统 API文档
## 基础信息
- **Base URL**: ${baseUrl}
- **认证方式**: 无需认证
- **响应格式**: JSON
## 接口列表
### 1. 获取邮箱列表
**请求**
\`\`\`
GET /accounts
\`\`\`
**响应示例**
\`\`\`json
{
"accounts": [
{
"email_id": "[email protected]",
"status": "active",
"last_sync": "2024-01-01T12:00:00Z"
}
],
"total_count": 1
}
\`\`\`
### 2. 获取邮件列表
**请求**
\`\`\`
GET /emails/{email_id}?folder=inbox&page=1&page_size=20&refresh=false
\`\`\`
**参数说明**
- \`email_id\`: 邮箱地址(URL编码)
- \`folder\`: 文件夹 (all, inbox, junk)
- \`page\`: 页码
- \`page_size\`: 每页数量
- \`refresh\`: 是否强制刷新
**响应示例**
\`\`\`json
{
"email_id": "[email protected]",
"folder_view": "inbox",
"page": 1,
"page_size": 20,
"total_emails": 150,
"emails": [...]
}
\`\`\`
### 3. 获取邮件详情
**请求**
\`\`\`
GET /emails/{email_id}/{message_id}
\`\`\`
**参数说明**
- \`email_id\`: 邮箱地址(URL编码)
- \`message_id\`: 邮件ID
**响应示例**
\`\`\`json
{
"message_id": "INBOX-1",
"subject": "邮件主题",
"from_email": "[email protected]",
"to_email": "[email protected]",
"date": "2024-01-01T12:00:00Z",
"body_plain": "纯文本内容",
"body_html": "HTML内容"
}
\`\`\`
---
生成时间: ${new Date().toLocaleString()}
`;
}
async function tryApi(apiType) {
const baseUrl = window.location.origin;
let url, responseElementId;
switch (apiType) {
case 'accounts':
url = `${baseUrl}/accounts`;
responseElementId = 'accountsResponse';
break;
case 'emails':
// 需要先获取一个邮箱账户
try {
const accountsData = await apiRequest('/accounts');
if (accountsData.accounts && accountsData.accounts.length > 0) {
const emailId = encodeURIComponent(accountsData.accounts[0].email_id);
url = `${baseUrl}/emails/${emailId}?folder=inbox&page=1&page_size=5`;
responseElementId = 'emailsResponse';
} else {
showNotification('没有可用的邮箱账户,请先添加账户', 'warning');
return;
}
} catch (error) {
showNotification('获取邮箱账户失败: ' + error.message, 'error');
return;
}
break;
case 'emailDetail':
// 需要先获取一个邮件ID
try {
const accountsData = await apiRequest('/accounts');
if (accountsData.accounts && accountsData.accounts.length > 0) {
const emailId = encodeURIComponent(accountsData.accounts[0].email_id);
const emailsData = await apiRequest(`/emails/${emailId}?folder=all&page=1&page_size=1`);
if (emailsData.emails && emailsData.emails.length > 0) {
const messageId = emailsData.emails[0].message_id;
url = `${baseUrl}/emails/${emailId}/${messageId}`;
responseElementId = 'emailDetailResponse';
} else {
showNotification('该邮箱没有邮件', 'warning');
return;
}
} else {
showNotification('没有可用的邮箱账户,请先添加账户', 'warning');
return;
}
} catch (error) {
showNotification('获取邮件数据失败: ' + error.message, 'error');
return;
}
break;
default:
return;
}
try {
showNotification('正在调用API...', 'info', '', 2000);
const response = await fetch(url);
const data = await response.json();
// 显示响应结果
const responseElement = document.getElementById(responseElementId);
const responseDataElement = document.getElementById(responseElementId.replace('Response', 'ResponseData'));
responseDataElement.textContent = JSON.stringify(data, null, 2);
responseElement.classList.add('show');
showNotification('API调用成功!', 'success');
} catch (error) {
showNotification('API调用失败: ' + error.message, 'error');
}
}
// 全局变量
let allEmails = []; // 存储所有邮件数据
let filteredEmails = []; // 存储过滤后的邮件数据
let searchTimeout = null;
// 邮件管理
function viewAccountEmails(emailId) {
currentAccount = emailId;
document.getElementById('currentAccountEmail').textContent = emailId;
document.getElementById('emailsNav').style.display = 'block';
// 重置过滤器
clearFilters();
showPage('emails');
}
function backToAccounts() {
currentAccount = null;
document.getElementById('emailsNav').style.display = 'none';
showPage('accounts');
}
function switchEmailTab(folder, targetElement = null) {
currentEmailFolder = folder;
currentEmailPage = 1;
// 更新标签状态
document.querySelectorAll('#emailsPage .tab').forEach(t => t.classList.remove('active'));
if (targetElement) {
targetElement.classList.add('active');
} else {
// 根据folder名称查找对应的标签按钮
document.querySelectorAll('#emailsPage .tab').forEach(t => {
if (t.onclick && t.onclick.toString().includes(`'${folder}'`)) {
t.classList.add('active');
}
});
}
loadEmails();
}
async function loadEmails(forceRefresh = false) {
if (!currentAccount) return;
const emailsList = document.getElementById('emailsList');
const refreshBtn = document.getElementById('refreshBtn');
// 显示加载状态
emailsList.innerHTML = '<div class="loading"><div class="loading-spinner"></div>正在加载邮件...</div>';
refreshBtn.disabled = true;
refreshBtn.innerHTML = '<span>⏳</span> 加载中...';
try {
const refreshParam = forceRefresh ? '&refresh=true' : '';
const url = `/emails/${currentAccount}?folder=${currentEmailFolder}&page=${currentEmailPage}&page_size=100${refreshParam}`;
const data = await apiRequest(url);
// 存储所有邮件数据
allEmails = data.emails || [];
// 更新统计信息
updateEmailStats(allEmails);
// 应用当前过滤器
applyFilters();
// 更新最后更新时间
document.getElementById('lastUpdateTime').textContent = new Date().toLocaleString();
if (forceRefresh) {
showNotification('邮件列表已刷新', 'success');
}
} catch (error) {
emailsList.innerHTML = '<div class="error">❌ 加载失败: ' + error.message + '</div>';
showNotification('加载邮件失败: ' + error.message, 'error');
} finally {
// 恢复刷新按钮状态
refreshBtn.disabled = false;
refreshBtn.innerHTML = '<span>🔄</span> 刷新';
}
}
function updateEmailStats(emails) {
const total = emails.length;
const unread = emails.filter(email => !email.is_read).length;
const today = emails.filter(email => {
const emailDate = new Date(email.date);
const today = new Date();
return emailDate.toDateString() === today.toDateString();
}).length;
const withAttachments = emails.filter(email => email.has_attachments).length;
document.getElementById('totalEmailCount').textContent = total;
document.getElementById('unreadEmailCount').textContent = unread;
document.getElementById('todayEmailCount').textContent = today;
document.getElementById('attachmentEmailCount').textContent = withAttachments;
}
// 搜索和过滤功能
function searchEmails() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
applyFilters();
}, 300); // 防抖,300ms后执行搜索
}
function applyFilters() {
const searchTerm = document.getElementById('emailSearch').value.toLowerCase();
const folderFilter = document.getElementById('folderFilter').value;
const statusFilter = document.getElementById('statusFilter').value;
const timeFilter = document.getElementById('timeFilter').value;
const attachmentFilter = document.getElementById('attachmentFilter').value;
filteredEmails = allEmails.filter(email => {
// 搜索过滤
if (searchTerm) {
const searchableText = `${email.subject || ''} ${email.from_email || ''}`.toLowerCase();
if (!searchableText.includes(searchTerm)) {
return false;
}
}
// 文件夹过滤
if (folderFilter !== 'all' && email.folder.toLowerCase() !== folderFilter) {
return false;
}
// 状态过滤
if (statusFilter === 'read' && !email.is_read) return false;
if (statusFilter === 'unread' && email.is_read) return false;
// 时间过滤
if (timeFilter !== 'all') {
const emailDate = new Date(email.date);
const now = new Date();
switch (timeFilter) {
case 'today':
if (emailDate.toDateString() !== now.toDateString()) return false;
break;
case 'week':
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
if (emailDate < weekAgo) return false;
break;
case 'month':
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
if (emailDate < monthAgo) return false;
break;
}
}
// 附件过滤
if (attachmentFilter === 'with' && !email.has_attachments) return false;
if (attachmentFilter === 'without' && email.has_attachments) return false;
return true;
});
renderFilteredEmails();
}
function renderFilteredEmails() {
const emailsList = document.getElementById('emailsList');
if (filteredEmails.length === 0) {
emailsList.innerHTML = '<div class="text-center" style="padding: 40px; color: #64748b;">没有找到匹配的邮件</div>';
return;
}
emailsList.innerHTML = filteredEmails.map(email => createEmailItem(email)).join('');
}
function clearFilters() {
document.getElementById('emailSearch').value = '';
document.getElementById('folderFilter').value = 'all';
document.getElementById('statusFilter').value = 'all';
document.getElementById('timeFilter').value = 'all';
document.getElementById('attachmentFilter').value = 'all';
filteredEmails = [...allEmails];
renderFilteredEmails();
}
function createEmailItem(email) {
const unreadClass = email.is_read ? '' : 'unread';
const attachmentIcon = email.has_attachments ? '<span style="color: #8b5cf6;">📎</span>' : '';
const readIcon = email.is_read ? '📖' : '📧';
return `
<div class="email-item ${unreadClass}" onclick="showEmailDetail('${email.message_id}')">
<div class="email-avatar">${email.sender_initial}</div>
<div class="email-content">
<div class="email-header">
<div class="email-subject">${email.subject || '(无主题)'}</div>
<div class="email-date">${formatEmailDate(email.date)}</div>
</div>
<div class="email-from">${readIcon} ${email.from_email} ${attachmentIcon}</div>
<div class="email-preview">文件夹: ${email.folder} | 点击查看详情</div>
</div>
</div>
`;
}
async function showEmailDetail(messageId) {
document.getElementById('emailModal').classList.remove('hidden');
document.getElementById('emailModalTitle').textContent = '邮件详情';
document.getElementById('emailModalContent').innerHTML = '<div class="loading">正在加载邮件详情...</div>';
try {
const data = await apiRequest(`/emails/${currentAccount}/${messageId}`);
document.getElementById('emailModalTitle').textContent = data.subject || '(无主题)';
document.getElementById('emailModalContent').innerHTML = `
<div class="email-detail-meta">
<p><strong>发件人:</strong> ${data.from_email}</p>
<p><strong>收件人:</strong> ${data.to_email}</p>
<p><strong>日期:</strong> ${formatEmailDate(data.date)} (${new Date(data.date).toLocaleString()})</p>
<p><strong>邮件ID:</strong> ${data.message_id}</p>
</div>
${renderEmailContent(data)}
`;
} catch (error) {
document.getElementById('emailModalContent').innerHTML = '<div class="error">加载失败: ' + error.message + '</div>';
}
}
function renderEmailContent(email) {
const hasHtml = email.body_html && email.body_html.trim();
const hasPlain = email.body_plain && email.body_plain.trim();
if (!hasHtml && !hasPlain) {
return '<p style="color: #9ca3af; font-style: italic;">此邮件无内容</p>';
}
if (hasHtml) {
const sanitizedHtml = email.body_html.replace(/"/g, '&quot;');
return `
<div class="email-content-tabs">
<button class="content-tab active" onclick="showEmailContentTab('html', this)">HTML视图</button>
${hasPlain ? '<button class="content-tab" onclick="showEmailContentTab(\'plain\', this)">纯文本</button>' : ''}
<button class="content-tab" onclick="showEmailContentTab('raw', this)">源码</button>
</div>
<div class="email-content-body">
<div id="htmlContent">
<iframe srcdoc="${sanitizedHtml}" style="width: 100%; min-height: 400px; border: none;" sandbox="allow-same-origin"></iframe>
</div>
${hasPlain ? `<div id="plainContent" class="hidden"><pre>${email.body_plain}</pre></div>` : ''}
<div id="rawContent" class="hidden"><pre style="background: #1e293b; color: #e2e8f0; padding: 16px; border-radius: 6px; overflow-x: auto; font-size: 12px;">${email.body_html.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre></div>
</div>
`;
} else {
return `<div class="email-content-body"><pre>${email.body_plain}</pre></div>`;
}
}
function showEmailContentTab(type, targetElement = null) {
// 更新标签状态
document.querySelectorAll('.content-tab').forEach(tab => tab.classList.remove('active'));
if (targetElement) {
targetElement.classList.add('active');
} else {
// 根据type查找对应的标签按钮
document.querySelectorAll('.content-tab').forEach(tab => {
if (tab.onclick && tab.onclick.toString().includes(`'${type}'`)) {
tab.classList.add('active');
}
});
}
// 隐藏所有内容
document.querySelectorAll('#htmlContent, #plainContent, #rawContent').forEach(content => {
content.classList.add('hidden');
});
// 显示对应内容
document.getElementById(type + 'Content').classList.remove('hidden');
}
function closeEmailModal() {
document.getElementById('emailModal').classList.add('hidden');
}
function refreshEmails() {
loadEmails(true); // 强制刷新
}
async function clearCache() {
if (!currentAccount) return;
try {
await apiRequest(`/cache/${currentAccount}`, { method: 'DELETE' });
showNotification('缓存已清除', 'success');
loadEmails(true);
} catch (error) {
showNotification('清除缓存失败: ' + error.message, 'error');
}
}
function exportEmails() {
if (!filteredEmails || filteredEmails.length === 0) {
showNotification('没有邮件可导出', 'warning');
return;
}
const csvContent = generateEmailCSV(filteredEmails);
downloadCSV(csvContent, `emails_${currentAccount}_${new Date().toISOString().split('T')[0]}.csv`);
showNotification(`已导出 ${filteredEmails.length} 封邮件`, 'success');
}
function generateEmailCSV(emails) {
const headers = ['主题', '发件人', '日期', '文件夹', '是否已读', '是否有附件'];
const rows = emails.map(email => [
`"${(email.subject || '').replace(/"/g, '""')}"`,
`"${email.from_email.replace(/"/g, '""')}"`,
`"${email.date}"`,
`"${email.folder}"`,
email.is_read ? '已读' : '未读',
email.has_attachments ? '有附件' : '无附件'
]);
return [headers, ...rows].map(row => row.join(',')).join('\n');
}
function downloadCSV(content, filename) {
const blob = new Blob(['\uFEFF' + content], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function updateEmailsPagination(totalEmails, pageSize) {
const pagination = document.getElementById('emailsPagination');
const totalPages = Math.ceil(totalEmails / pageSize);
if (totalPages <= 1) {
pagination.classList.add('hidden');
return;
}
pagination.classList.remove('hidden');
pagination.innerHTML = `
<button class="btn btn-secondary btn-sm" onclick="changeEmailPage(${currentEmailPage - 1})" ${currentEmailPage === 1 ? 'disabled' : ''}>‹ 上一页</button>
<span style="padding: 0 16px; color: #64748b;">${currentEmailPage} / ${totalPages}</span>
<button class="btn btn-secondary btn-sm" onclick="changeEmailPage(${currentEmailPage + 1})" ${currentEmailPage === totalPages ? 'disabled' : ''}>下一页 ›</button>
`;
}
function changeEmailPage(page) {
currentEmailPage = page;
loadEmails();
}
async function deleteAccount(emailId) {
if (!confirm(`确定要删除账户 ${emailId} 吗?`)) {
return;
}
try {
// 注意:这里需要后端支持删除账户的API
// await apiRequest(`/accounts/${emailId}`, { method: 'DELETE' });
showSuccess('账户删除成功');
loadAccounts();
} catch (error) {
showError('删除账户失败: ' + error.message);
}
}
// 点击模态框外部关闭
document.getElementById('emailModal').addEventListener('click', function(e) {
if (e.target === this) {
closeEmailModal();
}
});
// 键盘快捷键
document.addEventListener('keydown', function(e) {
// Ctrl/Cmd + R: 刷新邮件
if ((e.ctrlKey || e.metaKey) && e.key === 'r' && currentAccount) {
e.preventDefault();
refreshEmails();
}
// Escape: 关闭模态框
if (e.key === 'Escape') {
closeEmailModal();
}
// Ctrl/Cmd + F: 聚焦搜索框
if ((e.ctrlKey || e.metaKey) && e.key === 'f' && document.getElementById('emailSearch')) {
e.preventDefault();
document.getElementById('emailSearch').focus();
}
});
// 页面可见性变化时刷新数据
document.addEventListener('visibilitychange', function() {
if (!document.hidden && currentAccount) {
// 页面重新可见时,如果超过5分钟则自动刷新
const lastUpdate = document.getElementById('lastUpdateTime').textContent;
if (lastUpdate !== '-') {
const lastUpdateTime = new Date(lastUpdate);
const now = new Date();
const diffMinutes = (now - lastUpdateTime) / (1000 * 60);
if (diffMinutes > 5) {
showNotification('检测到数据可能过期,正在刷新...', 'info', '', 2000);
setTimeout(() => refreshEmails(), 1000);
}
}
}
});
// 初始化
window.addEventListener('load', function() {
showPage('accounts');
// 显示欢迎消息
setTimeout(() => {
showNotification('欢迎使用邮件管理系统!', 'info', '欢迎', 3000);
}, 500);
});
</script>
</body>
</html>
</script>
</body>
</html>