|
<!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-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> |
|
|
|
|
|
<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"> |
|
|
|
<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": "<html><body>HTML邮件内容</body></html>" |
|
}</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()">×</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'); |
|
} |
|
|
|
|
|
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;"> ${result.message}</span> |
|
</div>`; |
|
}); |
|
html += `</div></div>`; |
|
} |
|
|
|
resultsContainer.innerHTML = html; |
|
document.getElementById('batchResults').classList.remove('hidden'); |
|
} |
|
|
|
|
|
function initApiDocs() { |
|
|
|
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': |
|
|
|
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 { |
|
|
|
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); |
|
} |
|
|
|
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, '"'); |
|
|
|
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, '<').replace(/>/g, '>')}</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 { |
|
|
|
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 { |
|
|
|
|
|
showSuccess('账户删除成功'); |
|
loadAccounts(); |
|
} catch (error) { |
|
showError('删除账户失败: ' + error.message); |
|
} |
|
} |
|
|
|
|
|
document.getElementById('emailModal').addEventListener('click', function(e) { |
|
if (e.target === this) { |
|
closeEmailModal(); |
|
} |
|
}); |
|
|
|
|
|
document.addEventListener('keydown', function(e) { |
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'r' && currentAccount) { |
|
e.preventDefault(); |
|
refreshEmails(); |
|
} |
|
|
|
|
|
if (e.key === 'Escape') { |
|
closeEmailModal(); |
|
} |
|
|
|
|
|
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) { |
|
|
|
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> |