mirror of
https://github.com/zhinianboke/xianyu-auto-reply.git
synced 2025-08-03 04:57:35 +08:00
7795 lines
266 KiB
HTML
7795 lines
266 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>闲鱼自动回复管理系统</title>
|
||
<link rel="stylesheet" href="/static/lib/bootstrap/bootstrap.min.css">
|
||
<link rel="stylesheet" href="/static/lib/bootstrap-icons/bootstrap-icons.css">
|
||
<style>
|
||
:root {
|
||
--primary-color: #4f46e5;
|
||
--primary-hover: #4338ca;
|
||
--primary-light: #6366f1;
|
||
--secondary-color: #6b7280;
|
||
--success-color: #10b981;
|
||
--danger-color: #ef4444;
|
||
--warning-color: #f59e0b;
|
||
--light-color: #f9fafb;
|
||
--dark-color: #1f2937;
|
||
--border-color: #e5e7eb;
|
||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
body {
|
||
font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: #f8fafc;
|
||
min-height: 100vh;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
/* 侧边栏样式 */
|
||
.sidebar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
height: 100vh;
|
||
width: 250px;
|
||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
|
||
color: white;
|
||
z-index: 1000;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 1.5rem 1rem;
|
||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sidebar-brand {
|
||
font-size: 1.25rem;
|
||
font-weight: 700;
|
||
color: white;
|
||
text-decoration: none;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.sidebar-nav {
|
||
padding: 1rem 0;
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* 侧边栏滚动条样式 */
|
||
.sidebar-nav::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.sidebar-nav::-webkit-scrollbar-track {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.sidebar-nav::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
border-radius: 3px;
|
||
transition: background 0.3s ease;
|
||
}
|
||
|
||
.sidebar-nav::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(255, 255, 255, 0.5);
|
||
}
|
||
|
||
/* Firefox滚动条样式 */
|
||
.sidebar-nav {
|
||
scrollbar-width: thin;
|
||
scrollbar-color: rgba(255, 255, 255, 0.3) rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.nav-item {
|
||
margin: 0.25rem 0;
|
||
}
|
||
|
||
.nav-link {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0.75rem 1.5rem;
|
||
color: rgba(255,255,255,0.8);
|
||
text-decoration: none;
|
||
transition: all 0.3s ease;
|
||
border-left: 3px solid transparent;
|
||
}
|
||
|
||
.nav-link:hover {
|
||
color: white;
|
||
background: rgba(255,255,255,0.1);
|
||
border-left-color: rgba(255,255,255,0.5);
|
||
}
|
||
|
||
.nav-link.active {
|
||
color: white;
|
||
background: rgba(255,255,255,0.15);
|
||
border-left-color: white;
|
||
}
|
||
|
||
.nav-link i {
|
||
margin-right: 0.75rem;
|
||
width: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.nav-divider {
|
||
padding: 0.5rem 1rem;
|
||
border-top: 1px solid rgba(255,255,255,0.1);
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.nav-divider small {
|
||
font-weight: 500;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
/* 主内容区域 */
|
||
.main-content {
|
||
margin-left: 250px;
|
||
min-height: 100vh;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.content-header {
|
||
background: white;
|
||
padding: 1.5rem 2rem;
|
||
border-bottom: 1px solid var(--border-color);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.content-body {
|
||
padding: 2rem;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.sidebar {
|
||
transform: translateX(-100%);
|
||
}
|
||
|
||
.sidebar.show {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.main-content {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.mobile-toggle {
|
||
display: block !important;
|
||
}
|
||
}
|
||
|
||
.mobile-toggle {
|
||
display: none;
|
||
position: fixed;
|
||
top: 1rem;
|
||
left: 1rem;
|
||
z-index: 1001;
|
||
background: var(--primary-color);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 0.5rem;
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
/* 仪表盘样式 */
|
||
.dashboard-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 1.5rem;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.stat-card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 1.5rem;
|
||
box-shadow: var(--shadow-sm);
|
||
border: 1px solid var(--border-color);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.stat-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.5rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.stat-icon.primary {
|
||
background: rgba(79, 70, 229, 0.1);
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.stat-icon.success {
|
||
background: rgba(16, 185, 129, 0.1);
|
||
color: var(--success-color);
|
||
}
|
||
|
||
.stat-icon.warning {
|
||
background: rgba(245, 158, 11, 0.1);
|
||
color: var(--warning-color);
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 2rem;
|
||
font-weight: 700;
|
||
color: var(--dark-color);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.stat-label {
|
||
color: var(--secondary-color);
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 内容区域样式 */
|
||
.content-section {
|
||
display: none;
|
||
}
|
||
|
||
.content-section.active {
|
||
display: block;
|
||
}
|
||
|
||
/* 日志管理样式 */
|
||
.log-container {
|
||
height: 70vh;
|
||
overflow-y: auto;
|
||
background-color: #1e1e1e;
|
||
color: #d4d4d4;
|
||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||
font-size: 12px;
|
||
line-height: 1.2;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.log-entry {
|
||
margin-bottom: 0px;
|
||
padding: 1px 0;
|
||
word-wrap: break-word;
|
||
white-space: pre-wrap;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.log-entry.DEBUG {
|
||
color: #9cdcfe;
|
||
}
|
||
|
||
.log-entry.INFO {
|
||
color: #4ec9b0;
|
||
}
|
||
|
||
.log-entry.WARNING {
|
||
color: #dcdcaa;
|
||
}
|
||
|
||
.log-entry.ERROR {
|
||
color: #f48771;
|
||
}
|
||
|
||
.log-entry.CRITICAL {
|
||
color: #ff6b6b;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.log-timestamp {
|
||
color: #808080;
|
||
}
|
||
|
||
.log-level {
|
||
font-weight: bold;
|
||
margin: 0 5px;
|
||
}
|
||
|
||
.log-source {
|
||
color: #569cd6;
|
||
}
|
||
|
||
.log-message {
|
||
color: inherit;
|
||
}
|
||
|
||
.log-container::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
.log-container::-webkit-scrollbar-track {
|
||
background: #2d2d30;
|
||
}
|
||
|
||
.log-container::-webkit-scrollbar-thumb {
|
||
background: #464647;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.log-container::-webkit-scrollbar-thumb:hover {
|
||
background: #5a5a5c;
|
||
}
|
||
|
||
/* 关键词管理现代化样式 */
|
||
.keyword-container {
|
||
background: white;
|
||
border-radius: 16px;
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||
overflow: hidden;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.keyword-header {
|
||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
|
||
color: white;
|
||
padding: 1.5rem 2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.keyword-header h3 {
|
||
margin: 0;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.keyword-header .account-badge {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 20px;
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.keyword-input-area {
|
||
padding: 2rem;
|
||
background: #f8fafc;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.keyword-input-group {
|
||
display: grid;
|
||
grid-template-columns: 1fr 2fr 1fr auto;
|
||
gap: 1rem;
|
||
align-items: end;
|
||
}
|
||
|
||
.input-field {
|
||
position: relative;
|
||
}
|
||
|
||
.input-field label {
|
||
position: absolute;
|
||
top: -0.5rem;
|
||
left: 0.75rem;
|
||
background: #f8fafc;
|
||
padding: 0 0.5rem;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
color: #6b7280;
|
||
z-index: 1;
|
||
}
|
||
|
||
.input-field input, .input-field select {
|
||
width: 100%;
|
||
padding: 1rem 0.75rem 0.75rem;
|
||
border: 2px solid #e5e7eb;
|
||
border-radius: 12px;
|
||
font-size: 0.875rem;
|
||
transition: all 0.3s ease;
|
||
background: white;
|
||
}
|
||
|
||
.input-field input:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
.add-btn {
|
||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 12px;
|
||
padding: 1rem 1.5rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.add-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.3);
|
||
}
|
||
|
||
.add-btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.keywords-list {
|
||
padding: 1.5rem 2rem 2rem;
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.keyword-item {
|
||
background: white;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 12px;
|
||
margin-bottom: 1rem;
|
||
transition: all 0.3s ease;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.keyword-item:hover {
|
||
border-color: #667eea;
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.keyword-item-header {
|
||
padding: 1rem 1.5rem;
|
||
background: #f8fafc;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.keyword-tag {
|
||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
|
||
color: white;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 20px;
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.keyword-actions {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.action-btn {
|
||
width: 36px;
|
||
height: 36px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.edit-btn {
|
||
background: #f3f4f6;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.edit-btn:hover {
|
||
background: #e5e7eb;
|
||
color: #374151;
|
||
}
|
||
|
||
.delete-btn {
|
||
background: #fef2f2;
|
||
color: #ef4444;
|
||
}
|
||
|
||
.delete-btn:hover {
|
||
background: #fee2e2;
|
||
color: #dc2626;
|
||
}
|
||
|
||
.keyword-content {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.reply-text {
|
||
color: #374151;
|
||
line-height: 1.6;
|
||
margin: 0;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 4rem 2rem;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.empty-state i {
|
||
font-size: 4rem;
|
||
margin-bottom: 1rem;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.empty-state h3 {
|
||
margin: 0 0 0.5rem;
|
||
color: #374151;
|
||
}
|
||
|
||
.empty-state p {
|
||
margin: 0 0 2rem;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.quick-add-btn {
|
||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 12px;
|
||
padding: 0.75rem 1.5rem;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.quick-add-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
|
||
}
|
||
|
||
/* 账号选择器现代化 */
|
||
.account-selector {
|
||
background: white;
|
||
border-radius: 16px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
padding: 2rem;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.selector-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.selector-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
.selector-title {
|
||
margin: 0;
|
||
color: #1f2937;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.selector-subtitle {
|
||
margin: 0;
|
||
color: #6b7280;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.account-select-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.account-select {
|
||
width: 100%;
|
||
padding: 1rem 1.25rem;
|
||
border: 2px solid #e5e7eb;
|
||
border-radius: 12px;
|
||
font-size: 1rem;
|
||
background: white;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.account-select:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
/* 账号状态样式 */
|
||
.status-toggle {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 50px;
|
||
height: 24px;
|
||
}
|
||
|
||
.status-toggle input {
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
.status-slider {
|
||
position: absolute;
|
||
cursor: pointer;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: #ccc;
|
||
transition: .4s;
|
||
border-radius: 24px;
|
||
}
|
||
|
||
.status-slider:before {
|
||
position: absolute;
|
||
content: "";
|
||
height: 18px;
|
||
width: 18px;
|
||
left: 3px;
|
||
bottom: 3px;
|
||
background-color: white;
|
||
transition: .4s;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
input:checked + .status-slider {
|
||
background-color: #10b981;
|
||
}
|
||
|
||
input:checked + .status-slider:before {
|
||
transform: translateX(26px);
|
||
}
|
||
|
||
.status-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 12px;
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
min-width: 2rem;
|
||
height: 1.5rem;
|
||
}
|
||
|
||
.status-badge.enabled {
|
||
background: #dcfce7;
|
||
color: #166534;
|
||
border: 1px solid #bbf7d0;
|
||
}
|
||
|
||
.status-badge.disabled {
|
||
background: #fef2f2;
|
||
color: #991b1b;
|
||
border: 1px solid #fecaca;
|
||
}
|
||
|
||
.account-row.disabled {
|
||
opacity: 0.6;
|
||
background-color: #f9fafb;
|
||
}
|
||
|
||
.account-row.disabled .cookie-value {
|
||
background-color: #f3f4f6;
|
||
}
|
||
|
||
/* 关键词管理界面的状态提示 */
|
||
.account-badge .badge.bg-warning {
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% { opacity: 1; }
|
||
50% { opacity: 0.7; }
|
||
100% { opacity: 1; }
|
||
}
|
||
|
||
.disabled-account-notice {
|
||
background: #fef3c7;
|
||
border: 1px solid #f59e0b;
|
||
border-radius: 8px;
|
||
padding: 0.75rem;
|
||
margin-bottom: 1rem;
|
||
color: #92400e;
|
||
}
|
||
|
||
.disabled-account-notice .bi {
|
||
color: #f59e0b;
|
||
}
|
||
|
||
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.keyword-input-group {
|
||
grid-template-columns: 1fr;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.keyword-item-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.keyword-actions {
|
||
align-self: flex-end;
|
||
}
|
||
|
||
.status-toggle {
|
||
width: 40px;
|
||
height: 20px;
|
||
}
|
||
|
||
.status-slider:before {
|
||
height: 14px;
|
||
width: 14px;
|
||
left: 3px;
|
||
bottom: 3px;
|
||
}
|
||
|
||
input:checked + .status-slider:before {
|
||
transform: translateX(20px);
|
||
}
|
||
}
|
||
.card {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
border-radius: 16px;
|
||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||
transition: all 0.3s ease;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.card-header {
|
||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||
color: white;
|
||
border: none;
|
||
border-radius: 16px 16px 0 0 !important;
|
||
font-weight: 600;
|
||
font-size: 1.1rem;
|
||
padding: 1.25rem 1.5rem;
|
||
}
|
||
|
||
.card-body {
|
||
padding: 1.5rem;
|
||
}
|
||
.btn {
|
||
border-radius: 10px;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
padding: 0.5rem 1rem;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||
border: none;
|
||
}
|
||
|
||
.btn-success {
|
||
background: linear-gradient(135deg, var(--success-color), #059669);
|
||
border: none;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: linear-gradient(135deg, var(--danger-color), #dc2626);
|
||
border: none;
|
||
}
|
||
|
||
.btn-outline-primary {
|
||
border: 2px solid var(--primary-color);
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.btn-outline-primary:hover {
|
||
background: var(--primary-color);
|
||
border-color: var(--primary-color);
|
||
color: white;
|
||
}
|
||
|
||
.btn-outline-success {
|
||
border: 2px solid var(--success-color);
|
||
color: var(--success-color);
|
||
}
|
||
|
||
.btn-outline-success:hover {
|
||
background: var(--success-color);
|
||
border-color: var(--success-color);
|
||
color: white;
|
||
}
|
||
|
||
.btn-outline-info {
|
||
border: 2px solid #0dcaf0;
|
||
color: #0dcaf0;
|
||
}
|
||
|
||
.btn-outline-info:hover {
|
||
background: #0dcaf0;
|
||
border-color: #0dcaf0;
|
||
color: white;
|
||
}
|
||
|
||
.btn-outline-danger {
|
||
border: 2px solid var(--danger-color);
|
||
color: var(--danger-color);
|
||
}
|
||
|
||
.btn-outline-danger:hover {
|
||
background: var(--danger-color);
|
||
border-color: var(--danger-color);
|
||
color: white;
|
||
}
|
||
|
||
.table {
|
||
margin-bottom: 0;
|
||
background: transparent;
|
||
}
|
||
|
||
.table th {
|
||
border-top: none;
|
||
border-bottom: 2px solid var(--border-color);
|
||
font-weight: 600;
|
||
color: var(--dark-color);
|
||
background: rgba(79, 70, 229, 0.05);
|
||
padding: 1rem;
|
||
}
|
||
|
||
.table td {
|
||
padding: 1rem;
|
||
border-bottom: 1px solid var(--border-color);
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.table tbody tr:hover {
|
||
background: rgba(79, 70, 229, 0.02);
|
||
}
|
||
.badge {
|
||
font-weight: 500;
|
||
padding: 5px 10px;
|
||
}
|
||
.form-control:focus {
|
||
border-color: var(--primary-color);
|
||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||
}
|
||
.cookie-value {
|
||
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
|
||
font-size: 0.85rem;
|
||
background: #f8fafc;
|
||
padding: 0.75rem;
|
||
border-radius: 8px;
|
||
border: 1px solid #e5e7eb;
|
||
word-break: break-all;
|
||
line-height: 1.4;
|
||
max-height: 120px;
|
||
overflow-y: auto;
|
||
white-space: pre-wrap;
|
||
}
|
||
.loading {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(255,255,255,0.7);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 9999;
|
||
}
|
||
.loading-spinner {
|
||
width: 3rem;
|
||
height: 3rem;
|
||
}
|
||
.keyword-editor {
|
||
border-radius: 10px;
|
||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||
}
|
||
.modal-content {
|
||
border-radius: 10px;
|
||
border: none;
|
||
}
|
||
.modal-header {
|
||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||
}
|
||
.modal-footer {
|
||
border-top: 1px solid rgba(0,0,0,0.05);
|
||
}
|
||
.cookie-id {
|
||
font-weight: 600;
|
||
color: var(--primary-color);
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 3rem 2rem;
|
||
color: var(--secondary-color);
|
||
}
|
||
|
||
.empty-state i {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.form-control, .form-select {
|
||
border-radius: 10px;
|
||
border: 2px solid var(--border-color);
|
||
transition: all 0.3s ease;
|
||
padding: 0.75rem 1rem;
|
||
}
|
||
|
||
.form-control:focus, .form-select:focus {
|
||
border-color: var(--primary-color);
|
||
box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.1);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.form-label {
|
||
font-weight: 600;
|
||
color: var(--dark-color);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.modal-content {
|
||
border: none;
|
||
border-radius: 16px;
|
||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.modal-header {
|
||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||
color: white;
|
||
border: none;
|
||
border-radius: 16px 16px 0 0;
|
||
}
|
||
|
||
.toast-container {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
z-index: 9999;
|
||
}
|
||
|
||
.toast {
|
||
border: none;
|
||
border-radius: 12px;
|
||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.btn-group .btn {
|
||
margin-right: 0.25rem;
|
||
}
|
||
|
||
.btn-group .btn:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
margin-top: 1rem;
|
||
padding: 0 1rem;
|
||
}
|
||
|
||
.card-body {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.btn {
|
||
font-size: 0.875rem;
|
||
padding: 0.375rem 0.75rem;
|
||
}
|
||
|
||
.cookie-value {
|
||
font-size: 0.75rem;
|
||
max-height: 80px;
|
||
}
|
||
|
||
.btn-group {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.btn-group .btn {
|
||
margin-right: 0;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
/* 移动端商品表格优化 */
|
||
#itemsTableBody .btn-group {
|
||
flex-direction: row;
|
||
}
|
||
|
||
#itemsTableBody .btn-group .btn {
|
||
padding: 0.2rem 0.4rem;
|
||
font-size: 0.75rem;
|
||
}
|
||
}
|
||
|
||
/* 商品管理表格优化 */
|
||
#itemsTableBody .btn-group {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
#itemsTableBody .btn-group .btn {
|
||
padding: 0.25rem 0.5rem;
|
||
font-size: 0.875rem;
|
||
border-radius: 0.25rem;
|
||
}
|
||
|
||
#itemsTableBody .btn-group .btn:first-child {
|
||
border-top-right-radius: 0;
|
||
border-bottom-right-radius: 0;
|
||
}
|
||
|
||
#itemsTableBody .btn-group .btn:last-child {
|
||
border-top-left-radius: 0;
|
||
border-bottom-left-radius: 0;
|
||
border-left: 0;
|
||
}
|
||
|
||
#itemsTableBody .btn-group .btn i {
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
/* 表格操作列样式 */
|
||
.table td:last-child {
|
||
text-align: center;
|
||
vertical-align: middle;
|
||
padding: 0.5rem 0.25rem;
|
||
}
|
||
|
||
/* 表格文本截断优化 */
|
||
.table td {
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.table td[title] {
|
||
cursor: help;
|
||
}
|
||
|
||
/* 商品表格特定优化 */
|
||
#itemsTableBody td:nth-child(4),
|
||
#itemsTableBody td:nth-child(5) {
|
||
max-width: 200px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- 移动端菜单切换按钮 -->
|
||
<button class="mobile-toggle" onclick="toggleSidebar()">
|
||
<i class="bi bi-list"></i>
|
||
</button>
|
||
|
||
<!-- 侧边栏 -->
|
||
<div class="sidebar" id="sidebar">
|
||
<div class="sidebar-header">
|
||
<a href="#" class="sidebar-brand">
|
||
<i class="bi bi-chat-dots-fill me-2"></i>
|
||
闲鱼管理系统
|
||
</a>
|
||
</div>
|
||
<nav class="sidebar-nav">
|
||
<div class="nav-item">
|
||
<a href="#" class="nav-link active" onclick="showSection('dashboard')">
|
||
<i class="bi bi-speedometer2"></i>
|
||
仪表盘
|
||
</a>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="#" class="nav-link" onclick="showSection('accounts')">
|
||
<i class="bi bi-person-circle"></i>
|
||
账号管理
|
||
</a>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="#" class="nav-link" onclick="showSection('items')">
|
||
<i class="bi bi-box-seam"></i>
|
||
商品管理
|
||
</a>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="#" class="nav-link" onclick="showSection('auto-reply')">
|
||
<i class="bi bi-chat-left-text"></i>
|
||
自动回复
|
||
</a>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="#" class="nav-link" onclick="showSection('cards')">
|
||
<i class="bi bi-credit-card"></i>
|
||
卡券管理
|
||
</a>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="#" class="nav-link" onclick="showSection('auto-delivery')">
|
||
<i class="bi bi-truck"></i>
|
||
自动发货
|
||
</a>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="#" class="nav-link" onclick="showSection('notification-channels')">
|
||
<i class="bi bi-bell"></i>
|
||
通知渠道
|
||
</a>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="#" class="nav-link" onclick="showSection('message-notifications')">
|
||
<i class="bi bi-chat-dots"></i>
|
||
消息通知
|
||
</a>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="/item_search.html" class="nav-link" target="_blank">
|
||
<i class="bi bi-search"></i>
|
||
商品搜索
|
||
</a>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="#" class="nav-link" onclick="showSection('system-settings')">
|
||
<i class="bi bi-gear"></i>
|
||
系统设置
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 管理员专用菜单 -->
|
||
<div id="adminMenuSection" style="display: none;">
|
||
<div class="nav-divider mt-3 mb-2">
|
||
<small class="text-white-50">管理员功能</small>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="/user_management.html" class="nav-link" target="_blank">
|
||
<i class="bi bi-people"></i>
|
||
用户管理
|
||
</a>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="/log_management.html" class="nav-link" target="_blank">
|
||
<i class="bi bi-file-text-fill"></i>
|
||
系统日志
|
||
</a>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="/data_management.html" class="nav-link" target="_blank">
|
||
<i class="bi bi-database"></i>
|
||
数据管理
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="nav-item">
|
||
<a href="#" class="nav-link" onclick="showSection('about')">
|
||
<i class="bi bi-info-circle"></i>
|
||
关于
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 底部分隔符 -->
|
||
<div class="nav-divider mt-3 mb-2">
|
||
<small class="text-white-50">系统操作</small>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a href="#" class="nav-link" onclick="logout()">
|
||
<i class="bi bi-box-arrow-right"></i>
|
||
登出
|
||
</a>
|
||
</div>
|
||
</nav>
|
||
</div>
|
||
|
||
<!-- 主内容区域 -->
|
||
<div class="main-content">
|
||
<!-- 仪表盘内容 -->
|
||
<div id="dashboard-section" class="content-section active">
|
||
<div class="content-header">
|
||
<h2 class="mb-0">
|
||
<i class="bi bi-speedometer2 me-2"></i>
|
||
仪表盘
|
||
</h2>
|
||
<p class="text-muted mb-0">系统概览和统计信息</p>
|
||
</div>
|
||
<div class="content-body">
|
||
<div class="dashboard-stats">
|
||
<div class="stat-card">
|
||
<div class="stat-icon primary">
|
||
<i class="bi bi-person-circle"></i>
|
||
</div>
|
||
<div class="stat-number" id="totalAccounts">0</div>
|
||
<div class="stat-label">总账号数</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon success">
|
||
<i class="bi bi-chat-left-text"></i>
|
||
</div>
|
||
<div class="stat-number" id="totalKeywords">0</div>
|
||
<div class="stat-label">总关键词数</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon warning">
|
||
<i class="bi bi-activity"></i>
|
||
</div>
|
||
<div class="stat-number" id="activeAccounts">0</div>
|
||
<div class="stat-label">启用账号数</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 账号详情列表 -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">
|
||
<i class="bi bi-list-ul me-2"></i>
|
||
账号详情
|
||
</h5>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>账号ID</th>
|
||
<th>关键词数量</th>
|
||
<th>状态</th>
|
||
<th>最后更新</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="dashboardAccountsList">
|
||
<!-- 动态生成 -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 账号管理内容 -->
|
||
<div id="accounts-section" class="content-section">
|
||
<div class="content-header">
|
||
<h2 class="mb-0">
|
||
<i class="bi bi-person-circle me-2"></i>
|
||
账号管理
|
||
</h2>
|
||
<p class="text-muted mb-0">管理闲鱼账号Cookie信息</p>
|
||
</div>
|
||
<div class="content-body">
|
||
<!-- 添加Cookie卡片 -->
|
||
<div class="card mb-4">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span><i class="bi bi-plus-circle me-2"></i>添加新账号</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<form id="addForm" class="row g-3">
|
||
<div class="col-md-3">
|
||
<label for="cookieId" class="form-label">账号ID</label>
|
||
<input type="text" class="form-control" id="cookieId" placeholder="唯一标识" required>
|
||
</div>
|
||
<div class="col-md-9">
|
||
<label for="cookieValue" class="form-label">Cookie值</label>
|
||
<input type="text" class="form-control" id="cookieValue" placeholder="完整Cookie字符串" required>
|
||
</div>
|
||
<div class="col-12">
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="bi bi-plus-lg me-1"></i>添加账号
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cookie列表卡片 -->
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span><i class="bi bi-list-ul me-2"></i>账号列表</span>
|
||
<div class="btn-group">
|
||
<button class="btn btn-sm btn-outline-success" onclick="openDefaultReplyManager()">
|
||
<i class="bi bi-chat-text me-1"></i>默认回复管理
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-primary" onclick="loadCookies()">
|
||
<i class="bi bi-arrow-clockwise me-1"></i>刷新
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover" id="cookieTable">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 10%">账号ID</th>
|
||
<th style="width: 18%">Cookie值</th>
|
||
<th style="width: 8%">关键词</th>
|
||
<th style="width: 8%">状态</th>
|
||
<th style="width: 9%">默认回复</th>
|
||
<th style="width: 9%">AI回复</th>
|
||
<th style="width: 10%">自动确认发货</th>
|
||
<th style="width: 28%">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品管理内容 -->
|
||
<div id="items-section" class="content-section">
|
||
<div class="content-header">
|
||
<h2 class="mb-0">
|
||
<i class="bi bi-box-seam me-2"></i>
|
||
商品管理
|
||
</h2>
|
||
<p class="text-muted mb-0">管理各账号的商品信息</p>
|
||
</div>
|
||
|
||
<div class="content-body">
|
||
<!-- Cookie筛选 -->
|
||
<div class="card mb-4">
|
||
<div class="card-body">
|
||
<div class="row align-items-end">
|
||
<div class="col-md-6">
|
||
<label for="itemCookieFilter" class="form-label">筛选账号</label>
|
||
<select class="form-select" id="itemCookieFilter" onchange="loadItemsByCookie()">
|
||
<option value="">所有账号</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="d-flex justify-content-end align-items-end gap-2">
|
||
<!-- 页码输入 -->
|
||
<div class="d-flex align-items-center gap-2">
|
||
<label for="pageNumber" class="form-label mb-0 text-nowrap">页码:</label>
|
||
<input type="number" class="form-control" id="pageNumber" placeholder="页码" min="1" value="1" style="width: 80px;">
|
||
</div>
|
||
<div class="d-flex gap-2">
|
||
<button class="btn btn-success" onclick="getAllItemsFromAccount()">
|
||
<i class="bi bi-download me-1"></i>获取指定页
|
||
</button>
|
||
<button class="btn btn-warning" onclick="getAllItemsFromAccountAll()">
|
||
<i class="bi bi-collection me-1"></i>获取所有页
|
||
</button>
|
||
<button class="btn btn-primary" onclick="refreshItems()">
|
||
<i class="bi bi-arrow-clockwise me-1"></i>刷新
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 商品列表 -->
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">商品列表(自动发货根据商品标题和商品详情匹配关键字)</h5>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="batchDeleteItems()" id="batchDeleteBtn" disabled>
|
||
<i class="bi bi-trash"></i> 批量删除
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 5%">
|
||
<input type="checkbox" id="selectAllItems" onchange="toggleSelectAll(this)">
|
||
</th>
|
||
<th style="width: 12%">账号ID</th>
|
||
<th style="width: 12%">商品ID</th>
|
||
<th style="width: 18%">商品标题</th>
|
||
<th style="width: 20%">商品详情</th>
|
||
<th style="width: 8%">多规格</th>
|
||
<th style="width: 10%">更新时间</th>
|
||
<th style="width: 15%">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="itemsTableBody">
|
||
<tr>
|
||
<td colspan="7" class="text-center text-muted">加载中...</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 自动回复内容 -->
|
||
<div id="auto-reply-section" class="content-section">
|
||
<div class="content-header">
|
||
<h2 class="mb-0">
|
||
<i class="bi bi-chat-left-text me-2"></i>
|
||
自动回复
|
||
</h2>
|
||
<p class="text-muted mb-0">设置账号的关键词自动回复</p>
|
||
</div>
|
||
<div class="content-body">
|
||
<!-- 现代化账号选择器 -->
|
||
<div class="account-selector">
|
||
<div class="selector-header">
|
||
<div class="selector-icon">
|
||
<i class="bi bi-person-circle"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="selector-title">选择账号</h3>
|
||
<p class="selector-subtitle">选择要配置自动回复的闲鱼账号</p>
|
||
</div>
|
||
</div>
|
||
<div class="row g-3 align-items-end">
|
||
<div class="col-md-8">
|
||
<div class="account-select-wrapper">
|
||
<select class="account-select" id="accountSelect" onchange="loadAccountKeywords()">
|
||
<option value="">🔍 请选择一个账号开始配置...</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<button class="btn btn-primary w-100 d-flex align-items-center justify-content-center" onclick="refreshAccountList()" style="padding: 1rem 1.25rem; height: auto;">
|
||
<i class="bi bi-arrow-clockwise me-2"></i>
|
||
<span>刷新列表</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 现代化关键词管理容器 -->
|
||
<div class="keyword-container" id="keywordManagement" style="display: none;">
|
||
<div class="keyword-header">
|
||
<h3>
|
||
<i class="bi bi-chat-dots me-2"></i>
|
||
关键词管理
|
||
</h3>
|
||
<div class="d-flex align-items-center gap-2">
|
||
<div class="account-badge" id="currentAccountBadge">
|
||
<!-- 动态显示当前账号 -->
|
||
</div>
|
||
<div class="btn-group" role="group">
|
||
<button class="btn btn-outline-success btn-sm" onclick="exportKeywords()" title="导出关键词">
|
||
<i class="bi bi-download"></i> 导出
|
||
</button>
|
||
<button class="btn btn-outline-primary btn-sm" onclick="showImportModal()" title="导入关键词">
|
||
<i class="bi bi-upload"></i> 导入
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加关键词区域 -->
|
||
<div class="keyword-input-area">
|
||
<div class="keyword-input-group">
|
||
<div class="input-field">
|
||
<label>关键词</label>
|
||
<input type="text" id="newKeyword" placeholder="例如:你好">
|
||
</div>
|
||
<div class="input-field">
|
||
<label>自动回复内容</label>
|
||
<input type="text" id="newReply" placeholder="例如:您好,欢迎咨询!有什么可以帮助您的吗?">
|
||
</div>
|
||
<div class="input-field">
|
||
<label>商品ID(可选)</label>
|
||
<select id="newItemIdSelect" class="form-select">
|
||
<option value="">选择商品或留空表示通用关键词</option>
|
||
</select>
|
||
</div>
|
||
<button class="add-btn" onclick="addKeyword()">
|
||
<i class="bi bi-plus-lg"></i>
|
||
添加
|
||
</button>
|
||
</div>
|
||
<div class="mt-3">
|
||
<small class="text-muted">
|
||
<i class="bi bi-lightbulb me-1"></i>
|
||
<strong>支持变量:</strong>
|
||
<code>{send_user_name}</code> 用户昵称,
|
||
<code>{send_user_id}</code> 用户ID,
|
||
<code>{send_message}</code> 用户消息
|
||
</small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 关键词列表 -->
|
||
<div class="keywords-list" id="keywordsList">
|
||
<!-- 动态生成的关键词列表 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 卡券管理内容 -->
|
||
<div id="cards-section" class="content-section">
|
||
<div class="content-header">
|
||
<h2 class="mb-0">
|
||
<i class="bi bi-credit-card me-2"></i>
|
||
卡券管理
|
||
</h2>
|
||
<p class="text-muted mb-0">管理虚拟商品的卡券数据,支持API、固定文字和批量数据</p>
|
||
</div>
|
||
<div class="content-body">
|
||
<!-- 卡券列表 -->
|
||
<div class="row">
|
||
<div class="col-md-8">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">卡券列表</h5>
|
||
<button class="btn btn-primary btn-sm" onclick="showAddCardModal()">
|
||
<i class="bi bi-plus-lg me-1"></i>添加卡券
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>卡券名称</th>
|
||
<th>类型</th>
|
||
<th>规格信息</th>
|
||
<th>数据量</th>
|
||
<th>延时时间</th>
|
||
<th>状态</th>
|
||
<th>创建时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="cardsTableBody">
|
||
<tr>
|
||
<td colspan="7" class="text-center py-4 text-muted">
|
||
<i class="bi bi-credit-card fs-1 d-block mb-3"></i>
|
||
<h5>暂无卡券数据</h5>
|
||
<p class="mb-0">点击"添加卡券"开始创建您的第一个卡券</p>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">统计信息</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="stat-item mb-3">
|
||
<div class="d-flex justify-content-between">
|
||
<span>总卡券数</span>
|
||
<span class="badge bg-primary" id="totalCards">0</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-item mb-3">
|
||
<div class="d-flex justify-content-between">
|
||
<span>API类型</span>
|
||
<span class="badge bg-info" id="apiCards">0</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-item mb-3">
|
||
<div class="d-flex justify-content-between">
|
||
<span>固定文字</span>
|
||
<span class="badge bg-success" id="textCards">0</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="d-flex justify-content-between">
|
||
<span>批量数据</span>
|
||
<span class="badge bg-warning" id="dataCards">0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 自动发货内容 -->
|
||
<div id="auto-delivery-section" class="content-section">
|
||
<div class="content-header">
|
||
<h2 class="mb-0">
|
||
<i class="bi bi-truck me-2"></i>
|
||
自动发货
|
||
</h2>
|
||
<p class="text-muted mb-0">根据商品关键字自动匹配卡券进行发货</p>
|
||
</div>
|
||
<div class="content-body">
|
||
<!-- 发货规则列表 -->
|
||
<div class="row">
|
||
<div class="col-md-8">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">发货规则</h5>
|
||
<button class="btn btn-primary btn-sm" onclick="showAddDeliveryRuleModal()">
|
||
<i class="bi bi-plus-lg me-1"></i>添加规则
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>商品关键字</th>
|
||
<th>匹配卡券</th>
|
||
<th>卡券类型</th>
|
||
<th>发货数量</th>
|
||
<th>状态</th>
|
||
<th>已发货次数</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="deliveryRulesTableBody">
|
||
<tr>
|
||
<td colspan="7" class="text-center py-4 text-muted">
|
||
<i class="bi bi-truck fs-1 d-block mb-3"></i>
|
||
<h5>暂无发货规则</h5>
|
||
<p class="mb-0">点击"添加规则"开始配置自动发货规则</p>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">发货统计</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="stat-item mb-3">
|
||
<div class="d-flex justify-content-between">
|
||
<span>总规则数</span>
|
||
<span class="badge bg-primary" id="totalRules">0</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-item mb-3">
|
||
<div class="d-flex justify-content-between">
|
||
<span>启用规则</span>
|
||
<span class="badge bg-success" id="activeRules">0</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-item mb-3">
|
||
<div class="d-flex justify-content-between">
|
||
<span>今日发货</span>
|
||
<span class="badge bg-info" id="todayDeliveries">0</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="d-flex justify-content-between">
|
||
<span>总发货量</span>
|
||
<span class="badge bg-warning" id="totalDeliveries">0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 通知渠道管理内容 -->
|
||
<div id="notification-channels-section" class="content-section">
|
||
<div class="content-header">
|
||
<h2 class="mb-0">
|
||
<i class="bi bi-bell me-2"></i>
|
||
通知渠道管理
|
||
</h2>
|
||
<p class="text-muted mb-0">管理消息通知渠道,支持QQ通知等多种方式</p>
|
||
</div>
|
||
<div class="content-body">
|
||
<!-- 添加通知渠道 -->
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<span><i class="bi bi-plus-circle me-2"></i>添加QQ通知渠道</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="alert alert-info">
|
||
<i class="bi bi-info-circle me-2"></i>
|
||
<strong>使用说明:</strong>需要添加QQ号 <code>3668943488</code> 为好友才能正常接收消息通知
|
||
</div>
|
||
<form id="addChannelForm" class="row g-3">
|
||
<div class="col-md-4">
|
||
<label for="channelName" class="form-label">渠道名称</label>
|
||
<input type="text" class="form-control" id="channelName" placeholder="例如:我的QQ通知" required>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label for="channelQQ" class="form-label">接收QQ号码</label>
|
||
<input type="text" class="form-control" id="channelQQ" placeholder="输入QQ号码" required>
|
||
</div>
|
||
<div class="col-md-4 d-flex align-items-end">
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="bi bi-plus-lg me-1"></i>添加渠道
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 通知渠道列表 -->
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span><i class="bi bi-list-ul me-2"></i>通知渠道列表</span>
|
||
<button class="btn btn-sm btn-outline-primary" onclick="loadNotificationChannels()">
|
||
<i class="bi bi-arrow-clockwise me-1"></i>刷新
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover" id="channelsTable">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 10%">ID</th>
|
||
<th style="width: 25%">名称</th>
|
||
<th style="width: 15%">类型</th>
|
||
<th style="width: 20%">配置</th>
|
||
<th style="width: 10%">状态</th>
|
||
<th style="width: 20%">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="channelsTableBody">
|
||
<!-- 动态生成 -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 消息通知配置内容 -->
|
||
<div id="message-notifications-section" class="content-section">
|
||
<div class="content-header">
|
||
<h2 class="mb-0">
|
||
<i class="bi bi-chat-dots me-2"></i>
|
||
消息通知配置
|
||
</h2>
|
||
<p class="text-muted mb-0">为每个账号配置消息通知渠道</p>
|
||
</div>
|
||
<div class="content-body">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span><i class="bi bi-gear me-2"></i>账号通知配置</span>
|
||
<button class="btn btn-sm btn-outline-primary" onclick="loadMessageNotifications()">
|
||
<i class="bi bi-arrow-clockwise me-1"></i>刷新
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover" id="notificationsTable">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 20%">账号ID</th>
|
||
<th style="width: 30%">通知渠道</th>
|
||
<th style="width: 15%">状态</th>
|
||
<th style="width: 35%">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="notificationsTableBody">
|
||
<!-- 动态生成 -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日志管理内容 -->
|
||
<div id="logs-section" class="content-section">
|
||
<div class="content-header">
|
||
<h2 class="mb-0">
|
||
<i class="bi bi-file-text me-2"></i>
|
||
日志管理
|
||
</h2>
|
||
<p class="text-muted mb-0">实时显示系统日志,支持自动刷新和统计分析</p>
|
||
</div>
|
||
|
||
<div class="content-body">
|
||
<!-- 日志控制 -->
|
||
<div class="row mb-3">
|
||
<div class="col-md-3">
|
||
<label for="logLines" class="form-label">显示行数</label>
|
||
<select class="form-select" id="logLines" onchange="refreshLogs()">
|
||
<option value="100">100行</option>
|
||
<option value="200" selected>200行</option>
|
||
<option value="500">500行</option>
|
||
<option value="1000">1000行</option>
|
||
<option value="2000">2000行</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-9">
|
||
<label class="form-label"> </label>
|
||
<div class="d-flex gap-2">
|
||
<button class="btn btn-outline-primary" onclick="refreshLogs()">
|
||
<i class="bi bi-arrow-clockwise me-1"></i>刷新日志
|
||
</button>
|
||
<button class="btn btn-outline-secondary" onclick="clearLogsDisplay()">
|
||
<i class="bi bi-trash me-1"></i>清空显示
|
||
</button>
|
||
<button class="btn btn-outline-danger" onclick="clearLogsServer()">
|
||
<i class="bi bi-trash3 me-1"></i>清空服务器日志
|
||
</button>
|
||
<button class="btn btn-outline-info" onclick="showLogStats()">
|
||
<i class="bi bi-bar-chart me-1"></i>统计信息
|
||
</button>
|
||
<button class="btn btn-outline-info" onclick="toggleAutoRefresh()">
|
||
<i class="bi bi-play-circle me-1"></i><span id="autoRefreshText">开启自动刷新</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日志显示区域 -->
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span>系统日志</span>
|
||
<div class="d-flex gap-2">
|
||
<span class="badge bg-secondary" id="logCount">0 条日志</span>
|
||
<span class="badge bg-info" id="lastUpdate">未更新</span>
|
||
</div>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<div id="logContainer" class="log-container">
|
||
<div class="text-center p-4 text-muted">
|
||
<i class="bi bi-file-text fs-1"></i>
|
||
<p class="mt-2">点击刷新按钮加载日志</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系统设置内容 -->
|
||
<div id="system-settings-section" class="content-section">
|
||
<div class="content-header">
|
||
<h2 class="mb-0">
|
||
<i class="bi bi-gear me-2"></i>
|
||
系统设置
|
||
</h2>
|
||
<p class="text-muted mb-0">管理系统配置和用户设置</p>
|
||
</div>
|
||
<div class="content-body">
|
||
<div class="row">
|
||
<!-- 密码设置 -->
|
||
<div class="col-md-6">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="bi bi-shield-lock me-2"></i>密码设置
|
||
</div>
|
||
<div class="card-body">
|
||
<form id="passwordForm">
|
||
<div class="mb-3">
|
||
<label for="currentPassword" class="form-label">当前密码</label>
|
||
<input type="password" class="form-control" id="currentPassword" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="newPassword" class="form-label">新密码</label>
|
||
<input type="password" class="form-control" id="newPassword" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="confirmPassword" class="form-label">确认新密码</label>
|
||
<input type="password" class="form-control" id="confirmPassword" required>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="bi bi-check-circle me-1"></i>更新密码
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主题设置 -->
|
||
<div class="col-md-6">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="bi bi-palette me-2"></i>主题设置
|
||
</div>
|
||
<div class="card-body">
|
||
<form id="themeForm">
|
||
<div class="mb-3">
|
||
<label for="themeColor" class="form-label">主题颜色</label>
|
||
<select class="form-select" id="themeColor">
|
||
<option value="blue">蓝色</option>
|
||
<option value="green">绿色</option>
|
||
<option value="purple">紫色</option>
|
||
<option value="red">红色</option>
|
||
<option value="orange">橙色</option>
|
||
</select>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="bi bi-check-circle me-1"></i>应用主题
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 备份管理 (仅管理员可见) -->
|
||
<div id="backup-management" class="row mt-4" style="display: none;">
|
||
<div class="col-12">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="bi bi-archive me-2"></i>备份管理
|
||
<span class="badge bg-warning ms-2">管理员专用</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<!-- 数据库备份 -->
|
||
<div class="col-md-6">
|
||
<h6 class="mb-3">
|
||
<i class="bi bi-database-down me-2"></i>数据库备份
|
||
</h6>
|
||
<p class="text-muted mb-3">直接下载完整的数据库文件,包含所有用户数据和设置</p>
|
||
<button type="button" class="btn btn-success" onclick="downloadDatabaseBackup()">
|
||
<i class="bi bi-download me-1"></i>下载数据库
|
||
</button>
|
||
<div class="mt-2">
|
||
<small class="text-muted">
|
||
<i class="bi bi-info-circle me-1"></i>
|
||
推荐方式:完整备份,恢复简单
|
||
</small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 数据库恢复 -->
|
||
<div class="col-md-6">
|
||
<h6 class="mb-3">
|
||
<i class="bi bi-database-up me-2"></i>数据库恢复
|
||
</h6>
|
||
<p class="text-muted mb-3">上传数据库文件直接替换当前数据库,系统将自动重新加载</p>
|
||
<div class="mb-3">
|
||
<input type="file" class="form-control" id="databaseFile" accept=".db">
|
||
</div>
|
||
<button type="button" class="btn btn-danger" onclick="uploadDatabaseBackup()">
|
||
<i class="bi bi-upload me-1"></i>恢复数据库
|
||
</button>
|
||
<div class="mt-2">
|
||
<small class="text-danger">
|
||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||
警告:将覆盖所有当前数据!
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<!-- 系统缓存管理 -->
|
||
<div class="row mt-4">
|
||
<div class="col-12">
|
||
<h6 class="mb-3">
|
||
<i class="bi bi-arrow-clockwise me-2"></i>系统缓存管理
|
||
</h6>
|
||
<p class="text-muted mb-3">如果导入备份后关键字等数据没有立即更新,可以手动刷新系统缓存</p>
|
||
<button type="button" class="btn btn-info" onclick="reloadSystemCache()">
|
||
<i class="bi bi-arrow-clockwise me-1"></i>刷新系统缓存
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="alert alert-warning mt-4" role="alert">
|
||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||
<strong>注意:</strong>导入备份将会覆盖当前系统的所有数据,请谨慎操作!建议在导入前先导出当前数据作为备份。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 关于页面内容 -->
|
||
<div id="about-section" class="content-section">
|
||
<div class="content-header">
|
||
<h2 class="mb-0">
|
||
<i class="bi bi-info-circle me-2"></i>
|
||
关于
|
||
</h2>
|
||
<p class="text-muted mb-0">联系我们和技术支持</p>
|
||
</div>
|
||
<div class="content-body">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="bi bi-wechat me-2"></i>微信群
|
||
</div>
|
||
<div class="card-body text-center">
|
||
<img src="/static/wechat-group.png" alt="微信群二维码" class="img-fluid" style="max-width: 200px;">
|
||
<p class="mt-3 text-muted">扫码加入微信技术交流群</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="bi bi-chat-square me-2"></i>QQ群
|
||
</div>
|
||
<div class="card-body text-center">
|
||
<img src="/static/qq-group.png" alt="QQ群二维码" class="img-fluid" style="max-width: 200px;">
|
||
<p class="mt-3 text-muted">扫码加入QQ技术交流群</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div> <!-- 结束 main-content -->
|
||
|
||
<!-- 编辑通知渠道模态框 -->
|
||
<div class="modal fade" id="editChannelModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-pencil me-2"></i>
|
||
编辑通知渠道
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="editChannelForm">
|
||
<input type="hidden" id="editChannelId">
|
||
<div class="mb-3">
|
||
<label for="editChannelName" class="form-label">渠道名称 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="editChannelName" placeholder="例如:我的QQ通知" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="editChannelQQ" class="form-label">接收QQ号码 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="editChannelQQ" placeholder="输入QQ号码" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="editChannelEnabled" checked>
|
||
<label class="form-check-label" for="editChannelEnabled">
|
||
启用此通知渠道
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="updateNotificationChannel()">保存修改</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 配置账号通知模态框 -->
|
||
<div class="modal fade" id="configNotificationModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-gear me-2"></i>
|
||
配置账号通知
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="configNotificationForm">
|
||
<input type="hidden" id="configAccountId">
|
||
<div class="mb-3">
|
||
<label class="form-label">账号ID</label>
|
||
<input type="text" class="form-control" id="displayAccountId" readonly>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="notificationChannel" class="form-label">选择通知渠道 <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="notificationChannel" required>
|
||
<option value="">请选择通知渠道</option>
|
||
</select>
|
||
<small class="form-text text-muted">选择要接收此账号消息通知的渠道</small>
|
||
</div>
|
||
<div class="mb-3">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="notificationEnabled" checked>
|
||
<label class="form-check-label" for="notificationEnabled">
|
||
启用消息通知
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveAccountNotification()">保存配置</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加卡券模态框 -->
|
||
<div class="modal fade" id="addCardModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-credit-card me-2"></i>
|
||
添加卡券
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="addCardForm" onsubmit="event.preventDefault(); saveCard(); return false;">
|
||
<div class="mb-3">
|
||
<label class="form-label">卡券名称 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="cardName" placeholder="例如:游戏点卡、会员卡等" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">卡券类型 <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="cardType" onchange="toggleCardTypeFields()" required>
|
||
<option value="">请选择类型</option>
|
||
<option value="api">API接口</option>
|
||
<option value="text">固定文字</option>
|
||
<option value="data">批量数据</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- API配置 -->
|
||
<div id="apiFields" class="card mb-3" style="display: none;">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">API配置</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<label class="form-label">API地址</label>
|
||
<input type="url" class="form-control" id="apiUrl" placeholder="https://api.example.com/get-card">
|
||
</div>
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<label class="form-label">请求方法</label>
|
||
<select class="form-select" id="apiMethod">
|
||
<option value="GET">GET</option>
|
||
<option value="POST">POST</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">超时时间(秒)</label>
|
||
<input type="number" class="form-control" id="apiTimeout" value="10" min="1" max="60">
|
||
</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">请求头 (JSON格式)</label>
|
||
<textarea class="form-control" id="apiHeaders" rows="3" placeholder='{"Authorization": "Bearer token", "Content-Type": "application/json"}'></textarea>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">请求参数 (JSON格式)</label>
|
||
<textarea class="form-control" id="apiParams" rows="3" placeholder='{"type": "card", "count": 1}'></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 固定文字配置 -->
|
||
<div id="textFields" class="card mb-3" style="display: none;">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">固定文字配置</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<label class="form-label">固定文字内容</label>
|
||
<textarea class="form-control" id="textContent" rows="5" placeholder="请输入要发送的固定文字内容..."></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 批量数据配置 -->
|
||
<div id="dataFields" class="card mb-3" style="display: none;">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">批量数据配置</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<label class="form-label">数据内容 (一行一个)</label>
|
||
<textarea class="form-control" id="dataContent" rows="10" placeholder="请输入数据,每行一个: 卡号1:密码1 卡号2:密码2 或者 兑换码1 兑换码2"></textarea>
|
||
<small class="form-text text-muted">支持格式:卡号:密码 或 单独的兑换码</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">延时发货时间</label>
|
||
<div class="input-group">
|
||
<input type="number" class="form-control" id="cardDelaySeconds" value="0" min="0" max="3600" placeholder="0">
|
||
<span class="input-group-text">秒</span>
|
||
</div>
|
||
<small class="form-text text-muted">
|
||
<i class="bi bi-clock me-1"></i>
|
||
设置自动发货的延时时间,0表示立即发货,最大3600秒(1小时)
|
||
</small>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">备注信息</label>
|
||
<textarea class="form-control" id="cardDescription" rows="3" placeholder="可选的备注信息,支持变量替换: {DELIVERY_CONTENT} - 发货内容 例如:您的卡券信息:{DELIVERY_CONTENT},请妥善保管。"></textarea>
|
||
<small class="form-text text-muted">
|
||
<i class="bi bi-info-circle me-1"></i>
|
||
备注内容会与发货内容一起发送。使用 <code>{DELIVERY_CONTENT}</code> 变量可以在备注中插入实际的发货内容。
|
||
</small>
|
||
</div>
|
||
|
||
<!-- 多规格设置 -->
|
||
<div class="mb-3">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" id="isMultiSpec" onchange="toggleMultiSpecFields()">
|
||
<label class="form-check-label" for="isMultiSpec">
|
||
<strong>多规格卡券</strong>
|
||
</label>
|
||
</div>
|
||
<div class="form-text">开启后可以为同一商品的不同规格创建不同的卡券</div>
|
||
</div>
|
||
|
||
<!-- 多规格字段 -->
|
||
<div id="multiSpecFields" style="display: none;">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label class="form-label">规格名称 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="specName" placeholder="例如:套餐类型、颜色、尺寸">
|
||
<div class="form-text">规格的名称,如套餐类型、颜色等</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label class="form-label">规格值 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="specValue" placeholder="例如:30天、红色、XL">
|
||
<div class="form-text">具体的规格值,如30天、红色等</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="alert alert-info">
|
||
<i class="bi bi-info-circle"></i>
|
||
<strong>多规格说明:</strong>
|
||
<ul class="mb-0 mt-2">
|
||
<li>同一卡券名称可以创建多个不同规格的卡券</li>
|
||
<li>卡券名称+规格名称+规格值必须唯一</li>
|
||
<li>自动发货时会优先匹配精确规格,找不到时使用普通卡券兜底</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveCard()">保存卡券</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑卡券模态框 -->
|
||
<div class="modal fade" id="editCardModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-pencil me-2"></i>
|
||
编辑卡券
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="editCardForm">
|
||
<input type="hidden" id="editCardId">
|
||
<div class="mb-3">
|
||
<label class="form-label">卡券名称 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="editCardName" required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">卡券类型 <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="editCardType" onchange="toggleEditCardTypeFields()" required>
|
||
<option value="api">API接口</option>
|
||
<option value="text">固定文字</option>
|
||
<option value="data">批量数据</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- API配置 -->
|
||
<div id="editApiFields" class="card mb-3" style="display: none;">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">API配置</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<label class="form-label">API地址</label>
|
||
<input type="url" class="form-control" id="editApiUrl">
|
||
</div>
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<label class="form-label">请求方法</label>
|
||
<select class="form-select" id="editApiMethod">
|
||
<option value="GET">GET</option>
|
||
<option value="POST">POST</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">超时时间(秒)</label>
|
||
<input type="number" class="form-control" id="editApiTimeout" value="10" min="1" max="60">
|
||
</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">请求头 (JSON格式)</label>
|
||
<textarea class="form-control" id="editApiHeaders" rows="3"></textarea>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">请求参数 (JSON格式)</label>
|
||
<textarea class="form-control" id="editApiParams" rows="3"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 固定文字配置 -->
|
||
<div id="editTextFields" class="card mb-3" style="display: none;">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">固定文字配置</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<label class="form-label">固定文字内容</label>
|
||
<textarea class="form-control" id="editTextContent" rows="5"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 批量数据配置 -->
|
||
<div id="editDataFields" class="card mb-3" style="display: none;">
|
||
<div class="card-header">
|
||
<h6 class="mb-0">批量数据配置</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<label class="form-label">数据内容 (一行一个)</label>
|
||
<textarea class="form-control" id="editDataContent" rows="10"></textarea>
|
||
<small class="form-text text-muted">支持格式:卡号:密码 或 单独的兑换码</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="editCardEnabled">
|
||
<label class="form-check-label" for="editCardEnabled">
|
||
启用此卡券
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">延时发货时间</label>
|
||
<div class="input-group">
|
||
<input type="number" class="form-control" id="editCardDelaySeconds" value="0" min="0" max="3600" placeholder="0">
|
||
<span class="input-group-text">秒</span>
|
||
</div>
|
||
<small class="form-text text-muted">
|
||
<i class="bi bi-clock me-1"></i>
|
||
设置自动发货的延时时间,0表示立即发货,最大3600秒(1小时)
|
||
</small>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">备注信息</label>
|
||
<textarea class="form-control" id="editCardDescription" rows="3" placeholder="可选的备注信息,支持变量替换: {DELIVERY_CONTENT} - 发货内容 例如:您的卡券信息:{DELIVERY_CONTENT},请妥善保管。"></textarea>
|
||
<small class="form-text text-muted">
|
||
<i class="bi bi-info-circle me-1"></i>
|
||
备注内容会与发货内容一起发送。使用 <code>{DELIVERY_CONTENT}</code> 变量可以在备注中插入实际的发货内容。
|
||
</small>
|
||
</div>
|
||
|
||
<!-- 多规格设置 -->
|
||
<div class="mb-3">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" id="editIsMultiSpec" onchange="toggleEditMultiSpecFields()">
|
||
<label class="form-check-label" for="editIsMultiSpec">
|
||
<strong>多规格卡券</strong>
|
||
</label>
|
||
</div>
|
||
<div class="form-text">开启后可以为同一商品的不同规格创建不同的卡券</div>
|
||
</div>
|
||
|
||
<!-- 多规格字段 -->
|
||
<div id="editMultiSpecFields" style="display: none;">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label class="form-label">规格名称 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="editSpecName" placeholder="例如:套餐类型、颜色、尺寸">
|
||
<div class="form-text">规格的名称,如套餐类型、颜色等</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="mb-3">
|
||
<label class="form-label">规格值 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="editSpecValue" placeholder="例如:30天、红色、XL">
|
||
<div class="form-text">具体的规格值,如30天、红色等</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="alert alert-info">
|
||
<i class="bi bi-info-circle"></i>
|
||
<strong>多规格说明:</strong>
|
||
<ul class="mb-0 mt-2">
|
||
<li>同一卡券名称可以创建多个不同规格的卡券</li>
|
||
<li>卡券名称+规格名称+规格值必须唯一</li>
|
||
<li>自动发货时会优先匹配精确规格,找不到时使用普通卡券兜底</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="updateCard()">保存修改</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加发货规则模态框 -->
|
||
<div class="modal fade" id="addDeliveryRuleModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-truck me-2"></i>
|
||
添加发货规则
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="addDeliveryRuleForm">
|
||
<div class="mb-3">
|
||
<label class="form-label">商品关键字 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="productKeyword" placeholder="例如:游戏点卡、会员卡等" required>
|
||
<small class="form-text text-muted">当商品标题包含此关键字时触发自动发货</small>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">匹配卡券 <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="selectedCard" required>
|
||
<option value="">请选择卡券</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">发货数量</label>
|
||
<input type="number" class="form-control" id="deliveryCount" value="1" min="1" max="10">
|
||
<small class="form-text text-muted">每次发货的数量</small>
|
||
</div>
|
||
<div class="mb-3">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="ruleEnabled" checked>
|
||
<label class="form-check-label" for="ruleEnabled">
|
||
启用此规则
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">备注</label>
|
||
<textarea class="form-control" id="ruleDescription" rows="2" placeholder="可选的备注信息..."></textarea>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveDeliveryRule()">保存规则</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑发货规则模态框 -->
|
||
<div class="modal fade" id="editDeliveryRuleModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-pencil me-2"></i>
|
||
编辑发货规则
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="editDeliveryRuleForm">
|
||
<input type="hidden" id="editRuleId">
|
||
<div class="mb-3">
|
||
<label class="form-label">商品关键字 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="editProductKeyword" required>
|
||
<small class="form-text text-muted">当商品标题包含此关键字时触发自动发货</small>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">匹配卡券 <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="editSelectedCard" required>
|
||
<option value="">请选择卡券</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">发货数量</label>
|
||
<input type="number" class="form-control" id="editDeliveryCount" value="1" min="1" max="10">
|
||
<small class="form-text text-muted">每次发货的数量</small>
|
||
</div>
|
||
<div class="mb-3">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="editRuleEnabled">
|
||
<label class="form-check-label" for="editRuleEnabled">
|
||
启用此规则
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">备注</label>
|
||
<textarea class="form-control" id="editRuleDescription" rows="2"></textarea>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="updateDeliveryRule()">保存修改</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载动画 -->
|
||
<div class="loading d-none" id="loading">
|
||
<div class="spinner-border text-primary loading-spinner" role="status">
|
||
<span class="visually-hidden">加载中...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 提示消息容器 -->
|
||
<div class="toast-container"></div>
|
||
|
||
<!-- JS依赖 -->
|
||
<script src="/static/lib/bootstrap/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
// 全局变量
|
||
const apiBase = location.origin;
|
||
let keywordsData = {};
|
||
let currentCookieId = '';
|
||
let editCookieId = '';
|
||
let authToken = localStorage.getItem('auth_token');
|
||
let dashboardData = {
|
||
accounts: [],
|
||
totalKeywords: 0
|
||
};
|
||
|
||
// 账号关键词缓存
|
||
let accountKeywordCache = {};
|
||
let cacheTimestamp = 0;
|
||
const CACHE_DURATION = 30000; // 30秒缓存
|
||
|
||
// 菜单切换功能
|
||
function showSection(sectionName) {
|
||
console.log('切换到页面:', sectionName); // 调试信息
|
||
|
||
// 隐藏所有内容区域
|
||
document.querySelectorAll('.content-section').forEach(section => {
|
||
section.classList.remove('active');
|
||
});
|
||
|
||
// 移除所有菜单项的active状态
|
||
document.querySelectorAll('.nav-link').forEach(link => {
|
||
link.classList.remove('active');
|
||
});
|
||
|
||
// 显示选中的内容区域
|
||
const targetSection = document.getElementById(sectionName + '-section');
|
||
if (targetSection) {
|
||
targetSection.classList.add('active');
|
||
console.log('页面已激活:', sectionName + '-section'); // 调试信息
|
||
} else {
|
||
console.error('找不到页面元素:', sectionName + '-section'); // 调试信息
|
||
}
|
||
|
||
// 设置对应菜单项为active(修复event.target问题)
|
||
const menuLinks = document.querySelectorAll('.nav-link');
|
||
menuLinks.forEach(link => {
|
||
if (link.onclick && link.onclick.toString().includes(`showSection('${sectionName}')`)) {
|
||
link.classList.add('active');
|
||
}
|
||
});
|
||
|
||
// 根据不同section加载对应数据
|
||
switch(sectionName) {
|
||
case 'dashboard':
|
||
loadDashboard();
|
||
break;
|
||
case 'accounts':
|
||
loadCookies();
|
||
break;
|
||
case 'items':
|
||
loadItems();
|
||
break;
|
||
case 'auto-reply':
|
||
refreshAccountList();
|
||
break;
|
||
case 'cards':
|
||
loadCards();
|
||
break;
|
||
case 'auto-delivery':
|
||
loadDeliveryRules();
|
||
break;
|
||
case 'notification-channels':
|
||
loadNotificationChannels();
|
||
break;
|
||
case 'message-notifications':
|
||
loadMessageNotifications();
|
||
break;
|
||
case 'logs':
|
||
// 如果没有日志数据,则加载
|
||
setTimeout(() => {
|
||
if (!window.allLogs || window.allLogs.length === 0) {
|
||
refreshLogs();
|
||
}
|
||
}, 100);
|
||
break;
|
||
}
|
||
|
||
// 如果切换到非日志页面,停止自动刷新
|
||
if (sectionName !== 'logs' && window.autoRefreshInterval) {
|
||
clearInterval(window.autoRefreshInterval);
|
||
window.autoRefreshInterval = null;
|
||
const button = document.querySelector('#autoRefreshText');
|
||
const icon = button?.previousElementSibling;
|
||
if (button) {
|
||
button.textContent = '开启自动刷新';
|
||
if (icon) icon.className = 'bi bi-play-circle me-1';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 移动端侧边栏切换
|
||
function toggleSidebar() {
|
||
document.getElementById('sidebar').classList.toggle('show');
|
||
}
|
||
|
||
// 加载仪表盘数据
|
||
async function loadDashboard() {
|
||
try {
|
||
toggleLoading(true);
|
||
|
||
// 获取账号列表
|
||
const cookiesResponse = await fetch(`${apiBase}/cookies/details`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (cookiesResponse.ok) {
|
||
const cookiesData = await cookiesResponse.json();
|
||
|
||
// 为每个账号获取关键词信息
|
||
const accountsWithKeywords = await Promise.all(
|
||
cookiesData.map(async (account) => {
|
||
try {
|
||
const keywordsResponse = await fetch(`${apiBase}/keywords/${account.id}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (keywordsResponse.ok) {
|
||
const keywordsData = await keywordsResponse.json();
|
||
return {
|
||
...account,
|
||
keywords: keywordsData
|
||
};
|
||
} else {
|
||
return {
|
||
...account,
|
||
keywords: []
|
||
};
|
||
}
|
||
} catch (error) {
|
||
console.error(`获取账号 ${account.id} 关键词失败:`, error);
|
||
return {
|
||
...account,
|
||
keywords: []
|
||
};
|
||
}
|
||
})
|
||
);
|
||
|
||
dashboardData.accounts = accountsWithKeywords;
|
||
|
||
// 计算统计数据
|
||
let totalKeywords = 0;
|
||
let activeAccounts = 0;
|
||
let enabledAccounts = 0;
|
||
|
||
accountsWithKeywords.forEach(account => {
|
||
const keywordCount = account.keywords ? account.keywords.length : 0;
|
||
const isEnabled = account.enabled === undefined ? true : account.enabled;
|
||
|
||
if (isEnabled) {
|
||
enabledAccounts++;
|
||
totalKeywords += keywordCount;
|
||
if (keywordCount > 0) {
|
||
activeAccounts++;
|
||
}
|
||
}
|
||
});
|
||
|
||
dashboardData.totalKeywords = totalKeywords;
|
||
|
||
// 更新仪表盘显示
|
||
updateDashboardStats(accountsWithKeywords.length, totalKeywords, enabledAccounts);
|
||
updateDashboardAccountsList(accountsWithKeywords);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载仪表盘数据失败:', error);
|
||
showToast('加载仪表盘数据失败', 'danger');
|
||
} finally {
|
||
toggleLoading(false);
|
||
}
|
||
}
|
||
|
||
// 更新仪表盘统计数据
|
||
function updateDashboardStats(totalAccounts, totalKeywords, enabledAccounts) {
|
||
document.getElementById('totalAccounts').textContent = totalAccounts;
|
||
document.getElementById('totalKeywords').textContent = totalKeywords;
|
||
document.getElementById('activeAccounts').textContent = enabledAccounts;
|
||
}
|
||
|
||
// 更新仪表盘账号列表
|
||
function updateDashboardAccountsList(accounts) {
|
||
const tbody = document.getElementById('dashboardAccountsList');
|
||
tbody.innerHTML = '';
|
||
|
||
if (accounts.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="4" class="text-center text-muted py-4">
|
||
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
|
||
暂无账号数据
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
accounts.forEach(account => {
|
||
const keywordCount = account.keywords ? account.keywords.length : 0;
|
||
const isEnabled = account.enabled === undefined ? true : account.enabled;
|
||
|
||
let status = '';
|
||
if (!isEnabled) {
|
||
status = '<span class="badge bg-danger">已禁用</span>';
|
||
} else if (keywordCount > 0) {
|
||
status = '<span class="badge bg-success">活跃</span>';
|
||
} else {
|
||
status = '<span class="badge bg-secondary">未配置</span>';
|
||
}
|
||
|
||
const row = document.createElement('tr');
|
||
row.className = isEnabled ? '' : 'table-secondary';
|
||
row.innerHTML = `
|
||
<td>
|
||
<strong class="text-primary ${!isEnabled ? 'text-muted' : ''}">${account.id}</strong>
|
||
${!isEnabled ? '<i class="bi bi-pause-circle-fill text-danger ms-1" title="已禁用"></i>' : ''}
|
||
</td>
|
||
<td>
|
||
<span class="badge ${isEnabled ? 'bg-primary' : 'bg-secondary'}">${keywordCount} 个关键词</span>
|
||
</td>
|
||
<td>${status}</td>
|
||
<td>
|
||
<small class="text-muted">${new Date().toLocaleString()}</small>
|
||
</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// 获取账号关键词数量(带缓存)
|
||
async function getAccountKeywordCount(accountId) {
|
||
const now = Date.now();
|
||
|
||
// 检查缓存
|
||
if (accountKeywordCache[accountId] && (now - cacheTimestamp) < CACHE_DURATION) {
|
||
return accountKeywordCache[accountId];
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/keywords/${accountId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const keywordsData = await response.json();
|
||
const count = keywordsData.length;
|
||
|
||
// 更新缓存
|
||
accountKeywordCache[accountId] = count;
|
||
cacheTimestamp = now;
|
||
|
||
return count;
|
||
} else {
|
||
return 0;
|
||
}
|
||
} catch (error) {
|
||
console.error(`获取账号 ${accountId} 关键词失败:`, error);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// 清除关键词缓存
|
||
function clearKeywordCache() {
|
||
accountKeywordCache = {};
|
||
cacheTimestamp = 0;
|
||
}
|
||
|
||
// 刷新账号列表(用于自动回复页面)
|
||
async function refreshAccountList() {
|
||
try {
|
||
toggleLoading(true);
|
||
|
||
// 获取账号列表
|
||
const response = await fetch(`${apiBase}/cookies/details`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const accounts = await response.json();
|
||
const select = document.getElementById('accountSelect');
|
||
select.innerHTML = '<option value="">🔍 请选择一个账号开始配置...</option>';
|
||
|
||
// 为每个账号获取关键词数量
|
||
const accountsWithKeywords = await Promise.all(
|
||
accounts.map(async (account) => {
|
||
try {
|
||
const keywordsResponse = await fetch(`${apiBase}/keywords/${account.id}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (keywordsResponse.ok) {
|
||
const keywordsData = await keywordsResponse.json();
|
||
return {
|
||
...account,
|
||
keywordCount: keywordsData.length
|
||
};
|
||
} else {
|
||
return {
|
||
...account,
|
||
keywordCount: 0
|
||
};
|
||
}
|
||
} catch (error) {
|
||
console.error(`获取账号 ${account.id} 关键词失败:`, error);
|
||
return {
|
||
...account,
|
||
keywordCount: 0
|
||
};
|
||
}
|
||
})
|
||
);
|
||
|
||
// 渲染账号选项(显示所有账号,但标识禁用状态)
|
||
if (accountsWithKeywords.length === 0) {
|
||
select.innerHTML = '<option value="">❌ 暂无账号,请先添加账号</option>';
|
||
return;
|
||
}
|
||
|
||
// 分组显示:先显示启用的账号,再显示禁用的账号
|
||
const enabledAccounts = accountsWithKeywords.filter(account => {
|
||
const enabled = account.enabled === undefined ? true : account.enabled;
|
||
console.log(`账号 ${account.id} 过滤状态: enabled=${account.enabled}, 判断为启用=${enabled}`); // 调试信息
|
||
return enabled;
|
||
});
|
||
const disabledAccounts = accountsWithKeywords.filter(account => {
|
||
const enabled = account.enabled === undefined ? true : account.enabled;
|
||
return !enabled;
|
||
});
|
||
|
||
// 渲染启用的账号
|
||
enabledAccounts.forEach(account => {
|
||
const option = document.createElement('option');
|
||
option.value = account.id;
|
||
|
||
// 根据关键词数量显示不同的图标和样式
|
||
let icon = '📝';
|
||
let status = '';
|
||
if (account.keywordCount === 0) {
|
||
icon = '⚪';
|
||
status = ' (未配置)';
|
||
} else if (account.keywordCount >= 5) {
|
||
icon = '🟢';
|
||
status = ` (${account.keywordCount} 个关键词)`;
|
||
} else {
|
||
icon = '🟡';
|
||
status = ` (${account.keywordCount} 个关键词)`;
|
||
}
|
||
|
||
option.textContent = `${icon} ${account.id}${status}`;
|
||
select.appendChild(option);
|
||
});
|
||
|
||
// 如果有禁用的账号,添加分隔线和禁用账号
|
||
if (disabledAccounts.length > 0) {
|
||
// 添加分隔线
|
||
const separatorOption = document.createElement('option');
|
||
separatorOption.disabled = true;
|
||
separatorOption.textContent = `--- 禁用账号 (${disabledAccounts.length} 个) ---`;
|
||
select.appendChild(separatorOption);
|
||
|
||
// 渲染禁用的账号
|
||
disabledAccounts.forEach(account => {
|
||
const option = document.createElement('option');
|
||
option.value = account.id;
|
||
|
||
// 禁用账号使用特殊图标和样式
|
||
let icon = '🔴';
|
||
let status = '';
|
||
if (account.keywordCount === 0) {
|
||
status = ' (未配置) [已禁用]';
|
||
} else {
|
||
status = ` (${account.keywordCount} 个关键词) [已禁用]`;
|
||
}
|
||
|
||
option.textContent = `${icon} ${account.id}${status}`;
|
||
option.style.color = '#6b7280';
|
||
option.style.fontStyle = 'italic';
|
||
select.appendChild(option);
|
||
});
|
||
}
|
||
|
||
console.log('账号列表刷新完成,关键词统计:', accountsWithKeywords.map(a => ({id: a.id, keywords: a.keywordCount})));
|
||
} else {
|
||
showToast('获取账号列表失败', 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('刷新账号列表失败:', error);
|
||
showToast('刷新账号列表失败', 'danger');
|
||
} finally {
|
||
toggleLoading(false);
|
||
}
|
||
}
|
||
|
||
// 加载账号关键词
|
||
async function loadAccountKeywords() {
|
||
const accountId = document.getElementById('accountSelect').value;
|
||
const keywordManagement = document.getElementById('keywordManagement');
|
||
|
||
if (!accountId) {
|
||
keywordManagement.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
toggleLoading(true);
|
||
currentCookieId = accountId;
|
||
|
||
// 获取账号详情以检查状态
|
||
const accountResponse = await fetch(`${apiBase}/cookies/details`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
let accountStatus = true; // 默认启用
|
||
if (accountResponse.ok) {
|
||
const accounts = await accountResponse.json();
|
||
const currentAccount = accounts.find(acc => acc.id === accountId);
|
||
accountStatus = currentAccount ? (currentAccount.enabled === undefined ? true : currentAccount.enabled) : true;
|
||
console.log(`加载关键词时账号 ${accountId} 状态: enabled=${currentAccount?.enabled}, accountStatus=${accountStatus}`); // 调试信息
|
||
}
|
||
|
||
const response = await fetch(`${apiBase}/keywords-with-item-id/${accountId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
console.log('从服务器获取的关键词数据:', data); // 调试信息
|
||
|
||
// 后端返回的是 [{keyword, reply, item_id}, ...] 格式,直接使用
|
||
const formattedData = data;
|
||
|
||
console.log('格式化后的关键词数据:', formattedData); // 调试信息
|
||
keywordsData[accountId] = formattedData;
|
||
renderKeywordsList(formattedData);
|
||
|
||
// 加载商品列表
|
||
await loadItemsList(accountId);
|
||
|
||
// 更新账号徽章显示
|
||
updateAccountBadge(accountId, accountStatus);
|
||
|
||
keywordManagement.style.display = 'block';
|
||
} else {
|
||
showToast('加载关键词失败', 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('加载关键词失败:', error);
|
||
showToast('加载关键词失败', 'danger');
|
||
} finally {
|
||
toggleLoading(false);
|
||
}
|
||
}
|
||
|
||
// 更新账号徽章显示
|
||
function updateAccountBadge(accountId, isEnabled) {
|
||
const badge = document.getElementById('currentAccountBadge');
|
||
if (!badge) return;
|
||
|
||
const statusIcon = isEnabled ? '🟢' : '🔴';
|
||
const statusText = isEnabled ? '启用' : '禁用';
|
||
const statusClass = isEnabled ? 'bg-success' : 'bg-warning';
|
||
|
||
badge.innerHTML = `
|
||
<span class="badge ${statusClass} me-2">
|
||
${statusIcon} ${accountId}
|
||
</span>
|
||
<small class="text-muted">
|
||
状态: ${statusText}
|
||
${!isEnabled ? ' (配置的关键词不会参与自动回复)' : ''}
|
||
</small>
|
||
`;
|
||
}
|
||
|
||
// 显示添加关键词表单
|
||
function showAddKeywordForm() {
|
||
const form = document.getElementById('addKeywordForm');
|
||
form.style.display = form.style.display === 'none' ? 'block' : 'none';
|
||
|
||
if (form.style.display === 'block') {
|
||
document.getElementById('newKeyword').focus();
|
||
}
|
||
}
|
||
|
||
// 加载商品列表
|
||
async function loadItemsList(accountId) {
|
||
try {
|
||
const response = await fetch(`${apiBase}/items/${accountId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
const items = data.items || [];
|
||
|
||
// 更新商品选择下拉框
|
||
const selectElement = document.getElementById('newItemIdSelect');
|
||
if (selectElement) {
|
||
// 清空现有选项(保留第一个默认选项)
|
||
selectElement.innerHTML = '<option value="">选择商品或留空表示通用关键词</option>';
|
||
|
||
// 添加商品选项
|
||
items.forEach(item => {
|
||
const option = document.createElement('option');
|
||
option.value = item.item_id;
|
||
option.textContent = `${item.item_id} - ${item.item_title}`;
|
||
selectElement.appendChild(option);
|
||
});
|
||
}
|
||
|
||
console.log(`加载了 ${items.length} 个商品到选择列表`);
|
||
} else {
|
||
console.warn('加载商品列表失败:', response.status);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载商品列表时发生错误:', error);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// 添加或更新关键词
|
||
async function addKeyword() {
|
||
const keyword = document.getElementById('newKeyword').value.trim();
|
||
const reply = document.getElementById('newReply').value.trim();
|
||
const itemId = document.getElementById('newItemIdSelect').value.trim();
|
||
|
||
if (!keyword || !reply) {
|
||
showToast('请填写关键词和回复内容', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!currentCookieId) {
|
||
showToast('请先选择账号', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 检查是否为编辑模式
|
||
const isEditMode = typeof window.editingIndex !== 'undefined';
|
||
const actionText = isEditMode ? '更新' : '添加';
|
||
|
||
try {
|
||
toggleLoading(true);
|
||
|
||
// 获取当前关键词列表
|
||
let currentKeywords = [...(keywordsData[currentCookieId] || [])];
|
||
|
||
// 如果是编辑模式,先移除原关键词
|
||
if (isEditMode) {
|
||
currentKeywords.splice(window.editingIndex, 1);
|
||
}
|
||
|
||
// 准备要保存的关键词列表
|
||
let keywordsToSave = [...currentKeywords];
|
||
|
||
// 如果是编辑模式,先移除原关键词
|
||
if (isEditMode && typeof window.editingIndex !== 'undefined') {
|
||
keywordsToSave.splice(window.editingIndex, 1);
|
||
}
|
||
|
||
// 检查关键词是否已存在(考虑商品ID)
|
||
const existingKeyword = keywordsToSave.find(item =>
|
||
item.keyword === keyword &&
|
||
(item.item_id || '') === (itemId || '')
|
||
);
|
||
if (existingKeyword) {
|
||
const itemIdText = itemId ? `(商品ID: ${itemId})` : '(通用关键词)';
|
||
showToast(`关键词 "${keyword}" ${itemIdText} 已存在,请使用其他关键词或商品ID`, 'warning');
|
||
toggleLoading(false);
|
||
return;
|
||
}
|
||
|
||
// 添加新关键词或更新的关键词
|
||
const newKeyword = {
|
||
keyword: keyword,
|
||
reply: reply,
|
||
item_id: itemId || ''
|
||
};
|
||
keywordsToSave.push(newKeyword);
|
||
|
||
const response = await fetch(`${apiBase}/keywords-with-item-id/${currentCookieId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify({
|
||
keywords: keywordsToSave
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast(`✨ 关键词 "${keyword}" ${actionText}成功!`, 'success');
|
||
|
||
// 清空输入框并重置样式
|
||
const keywordInput = document.getElementById('newKeyword');
|
||
const replyInput = document.getElementById('newReply');
|
||
const selectElement = document.getElementById('newItemIdSelect');
|
||
const addBtn = document.querySelector('.add-btn');
|
||
|
||
keywordInput.value = '';
|
||
replyInput.value = '';
|
||
if (selectElement) {
|
||
selectElement.value = '';
|
||
}
|
||
keywordInput.style.borderColor = '#e5e7eb';
|
||
replyInput.style.borderColor = '#e5e7eb';
|
||
addBtn.style.opacity = '0.7';
|
||
addBtn.style.transform = 'scale(0.95)';
|
||
|
||
// 如果是编辑模式,重置编辑状态
|
||
if (isEditMode) {
|
||
delete window.editingIndex;
|
||
delete window.originalKeyword;
|
||
|
||
// 恢复添加按钮
|
||
addBtn.innerHTML = '<i class="bi bi-plus-lg"></i>添加';
|
||
addBtn.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)';
|
||
|
||
// 移除取消按钮
|
||
const cancelBtn = document.getElementById('cancelEditBtn');
|
||
if (cancelBtn) {
|
||
cancelBtn.remove();
|
||
}
|
||
}
|
||
|
||
// 聚焦到关键词输入框,方便连续添加
|
||
setTimeout(() => {
|
||
keywordInput.focus();
|
||
}, 100);
|
||
|
||
loadAccountKeywords(); // 重新加载关键词列表
|
||
clearKeywordCache(); // 清除缓存
|
||
} else {
|
||
const errorText = await response.text();
|
||
console.error('关键词添加失败:', errorText);
|
||
showToast('关键词添加失败', 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('添加关键词失败:', error);
|
||
showToast('添加关键词失败', 'danger');
|
||
} finally {
|
||
toggleLoading(false);
|
||
}
|
||
}
|
||
|
||
// 渲染现代化关键词列表
|
||
function renderKeywordsList(keywords) {
|
||
console.log('渲染关键词列表:', keywords); // 调试信息
|
||
const container = document.getElementById('keywordsList');
|
||
|
||
if (!container) {
|
||
console.error('找不到关键词列表容器元素');
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = '';
|
||
|
||
if (!keywords || keywords.length === 0) {
|
||
console.log('关键词列表为空,显示空状态');
|
||
container.innerHTML = `
|
||
<div class="empty-state">
|
||
<i class="bi bi-chat-dots"></i>
|
||
<h3>还没有关键词</h3>
|
||
<p>添加第一个关键词,让您的闲鱼店铺自动回复客户消息</p>
|
||
<button class="quick-add-btn" onclick="focusKeywordInput()">
|
||
<i class="bi bi-plus-lg me-2"></i>立即添加
|
||
</button>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
console.log(`开始渲染 ${keywords.length} 个关键词`);
|
||
|
||
keywords.forEach((item, index) => {
|
||
console.log(`渲染关键词 ${index + 1}:`, item); // 调试信息
|
||
|
||
const keywordItem = document.createElement('div');
|
||
keywordItem.className = 'keyword-item';
|
||
// 商品ID显示
|
||
const itemIdDisplay = item.item_id ?
|
||
`<small class="text-muted d-block"><i class="bi bi-box"></i> 商品ID: ${item.item_id}</small>` :
|
||
'<small class="text-muted d-block"><i class="bi bi-globe"></i> 通用关键词</small>';
|
||
|
||
keywordItem.innerHTML = `
|
||
<div class="keyword-item-header">
|
||
<div class="keyword-tag">
|
||
<i class="bi bi-tag-fill"></i>
|
||
${item.keyword}
|
||
${itemIdDisplay}
|
||
</div>
|
||
<div class="keyword-actions">
|
||
<button class="action-btn edit-btn" onclick="editKeyword(${index})" title="编辑">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
<button class="action-btn delete-btn" onclick="deleteKeyword('${currentCookieId}', ${index})" title="删除">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="keyword-content">
|
||
<p class="reply-text">${item.reply}</p>
|
||
</div>
|
||
`;
|
||
container.appendChild(keywordItem);
|
||
});
|
||
|
||
console.log('关键词列表渲染完成');
|
||
}
|
||
|
||
// 聚焦到关键词输入框
|
||
function focusKeywordInput() {
|
||
document.getElementById('newKeyword').focus();
|
||
}
|
||
|
||
// 编辑关键词 - 改进版本
|
||
function editKeyword(index) {
|
||
const keywords = keywordsData[currentCookieId] || [];
|
||
const keyword = keywords[index];
|
||
|
||
if (!keyword) {
|
||
showToast('关键词不存在', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 将关键词信息填入输入框
|
||
document.getElementById('newKeyword').value = keyword.keyword;
|
||
document.getElementById('newReply').value = keyword.reply;
|
||
|
||
// 设置商品ID选择框
|
||
const selectElement = document.getElementById('newItemIdSelect');
|
||
if (selectElement) {
|
||
selectElement.value = keyword.item_id || '';
|
||
}
|
||
|
||
// 设置编辑模式标识
|
||
window.editingIndex = index;
|
||
window.originalKeyword = keyword.keyword;
|
||
window.originalItemId = keyword.item_id || '';
|
||
|
||
// 更新按钮文本和样式
|
||
const addBtn = document.querySelector('.add-btn');
|
||
addBtn.innerHTML = '<i class="bi bi-check-lg"></i>更新';
|
||
addBtn.style.background = 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)';
|
||
|
||
// 显示取消按钮
|
||
showCancelEditButton();
|
||
|
||
// 聚焦到关键词输入框并选中文本
|
||
setTimeout(() => {
|
||
const keywordInput = document.getElementById('newKeyword');
|
||
keywordInput.focus();
|
||
keywordInput.select();
|
||
}, 100);
|
||
|
||
showToast('📝 编辑模式:修改后点击"更新"按钮保存', 'info');
|
||
}
|
||
|
||
// 显示取消编辑按钮
|
||
function showCancelEditButton() {
|
||
// 检查是否已存在取消按钮
|
||
if (document.getElementById('cancelEditBtn')) {
|
||
return;
|
||
}
|
||
|
||
const addBtn = document.querySelector('.add-btn');
|
||
const cancelBtn = document.createElement('button');
|
||
cancelBtn.id = 'cancelEditBtn';
|
||
cancelBtn.className = 'btn btn-outline-secondary';
|
||
cancelBtn.style.marginLeft = '0.5rem';
|
||
cancelBtn.innerHTML = '<i class="bi bi-x-lg"></i>取消';
|
||
cancelBtn.onclick = cancelEdit;
|
||
|
||
addBtn.parentNode.appendChild(cancelBtn);
|
||
}
|
||
|
||
// 取消编辑
|
||
function cancelEdit() {
|
||
// 清空输入框
|
||
document.getElementById('newKeyword').value = '';
|
||
document.getElementById('newReply').value = '';
|
||
|
||
// 清空商品ID选择框
|
||
const selectElement = document.getElementById('newItemIdSelect');
|
||
if (selectElement) {
|
||
selectElement.value = '';
|
||
}
|
||
|
||
// 重置编辑状态
|
||
delete window.editingIndex;
|
||
delete window.originalKeyword;
|
||
delete window.originalItemId;
|
||
|
||
// 恢复添加按钮
|
||
const addBtn = document.querySelector('.add-btn');
|
||
addBtn.innerHTML = '<i class="bi bi-plus-lg"></i>添加';
|
||
addBtn.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)';
|
||
|
||
// 移除取消按钮
|
||
const cancelBtn = document.getElementById('cancelEditBtn');
|
||
if (cancelBtn) {
|
||
cancelBtn.remove();
|
||
}
|
||
|
||
showToast('已取消编辑', 'info');
|
||
}
|
||
|
||
// 删除关键词
|
||
async function deleteKeyword(cookieId, index) {
|
||
if (!confirm('确定要删除这个关键词吗?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
toggleLoading(true);
|
||
|
||
// 获取当前关键词列表
|
||
const currentKeywords = keywordsData[cookieId] || [];
|
||
// 移除指定索引的关键词
|
||
currentKeywords.splice(index, 1);
|
||
|
||
// 更新服务器
|
||
const response = await fetch(`${apiBase}/keywords-with-item-id/${cookieId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify({
|
||
keywords: currentKeywords
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('关键词删除成功', 'success');
|
||
keywordsData[cookieId] = currentKeywords;
|
||
renderKeywordsList(currentKeywords);
|
||
clearKeywordCache(); // 清除缓存
|
||
} else {
|
||
const errorText = await response.text();
|
||
console.error('关键词删除失败:', errorText);
|
||
showToast('关键词删除失败', 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('删除关键词失败:', error);
|
||
showToast('删除关键词删除失败', 'danger');
|
||
} finally {
|
||
toggleLoading(false);
|
||
}
|
||
}
|
||
|
||
// 显示/隐藏加载动画
|
||
function toggleLoading(show) {
|
||
document.getElementById('loading').classList.toggle('d-none', !show);
|
||
}
|
||
|
||
// 显示提示消息
|
||
function showToast(message, type = 'success') {
|
||
const toastContainer = document.querySelector('.toast-container');
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast align-items-center text-white bg-${type} border-0`;
|
||
toast.setAttribute('role', 'alert');
|
||
toast.setAttribute('aria-live', 'assertive');
|
||
toast.setAttribute('aria-atomic', 'true');
|
||
|
||
toast.innerHTML = `
|
||
<div class="d-flex">
|
||
<div class="toast-body">
|
||
${message}
|
||
</div>
|
||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||
</div>
|
||
`;
|
||
|
||
toastContainer.appendChild(toast);
|
||
const bsToast = new bootstrap.Toast(toast, { delay: 3000 });
|
||
bsToast.show();
|
||
|
||
// 自动移除
|
||
toast.addEventListener('hidden.bs.toast', () => {
|
||
toast.remove();
|
||
});
|
||
}
|
||
|
||
// 错误处理
|
||
async function handleApiError(err) {
|
||
console.error(err);
|
||
showToast(err.message || '操作失败', 'danger');
|
||
toggleLoading(false);
|
||
}
|
||
|
||
// API请求包装
|
||
async function fetchJSON(url, opts = {}) {
|
||
toggleLoading(true);
|
||
try {
|
||
// 添加认证头
|
||
if (authToken) {
|
||
opts.headers = opts.headers || {};
|
||
opts.headers['Authorization'] = `Bearer ${authToken}`;
|
||
}
|
||
|
||
const res = await fetch(url, opts);
|
||
if (res.status === 401) {
|
||
// 未授权,跳转到登录页面
|
||
localStorage.removeItem('auth_token');
|
||
window.location.href = '/';
|
||
return;
|
||
}
|
||
if (!res.ok) {
|
||
let errorMessage = `HTTP ${res.status}`;
|
||
try {
|
||
const errorText = await res.text();
|
||
if (errorText) {
|
||
// 尝试解析JSON错误信息
|
||
try {
|
||
const errorJson = JSON.parse(errorText);
|
||
errorMessage = errorJson.detail || errorJson.message || errorText;
|
||
} catch {
|
||
errorMessage = errorText;
|
||
}
|
||
}
|
||
} catch {
|
||
errorMessage = `HTTP ${res.status} ${res.statusText}`;
|
||
}
|
||
throw new Error(errorMessage);
|
||
}
|
||
const data = await res.json();
|
||
toggleLoading(false);
|
||
return data;
|
||
} catch (err) {
|
||
handleApiError(err);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
// 加载Cookie列表
|
||
async function loadCookies() {
|
||
try {
|
||
toggleLoading(true);
|
||
const tbody = document.querySelector('#cookieTable tbody');
|
||
tbody.innerHTML = '';
|
||
|
||
const cookieDetails = await fetchJSON(apiBase + '/cookies/details');
|
||
|
||
if (cookieDetails.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="7" class="text-center py-4 text-muted empty-state">
|
||
<i class="bi bi-inbox fs-1 d-block mb-3"></i>
|
||
<h5>暂无账号</h5>
|
||
<p class="mb-0">请添加新的闲鱼账号开始使用</p>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// 为每个账号获取关键词数量和默认回复设置并渲染
|
||
const accountsWithKeywords = await Promise.all(
|
||
cookieDetails.map(async (cookie) => {
|
||
try {
|
||
// 获取关键词数量
|
||
const keywordsResponse = await fetch(`${apiBase}/keywords/${cookie.id}`, {
|
||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||
});
|
||
|
||
let keywordCount = 0;
|
||
if (keywordsResponse.ok) {
|
||
const keywordsData = await keywordsResponse.json();
|
||
keywordCount = keywordsData.length;
|
||
}
|
||
|
||
// 获取默认回复设置
|
||
const defaultReplyResponse = await fetch(`${apiBase}/default-replies/${cookie.id}`, {
|
||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||
});
|
||
|
||
let defaultReply = { enabled: false, reply_content: '' };
|
||
if (defaultReplyResponse.ok) {
|
||
defaultReply = await defaultReplyResponse.json();
|
||
}
|
||
|
||
// 获取AI回复设置
|
||
const aiReplyResponse = await fetch(`${apiBase}/ai-reply-settings/${cookie.id}`, {
|
||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||
});
|
||
|
||
let aiReply = { ai_enabled: false, model_name: 'qwen-plus' };
|
||
if (aiReplyResponse.ok) {
|
||
aiReply = await aiReplyResponse.json();
|
||
}
|
||
|
||
return {
|
||
...cookie,
|
||
keywordCount: keywordCount,
|
||
defaultReply: defaultReply,
|
||
aiReply: aiReply
|
||
};
|
||
} catch (error) {
|
||
return {
|
||
...cookie,
|
||
keywordCount: 0,
|
||
defaultReply: { enabled: false, reply_content: '' },
|
||
aiReply: { ai_enabled: false, model_name: 'qwen-plus' }
|
||
};
|
||
}
|
||
})
|
||
);
|
||
|
||
accountsWithKeywords.forEach(cookie => {
|
||
// 使用数据库中的实际状态,默认为启用
|
||
const isEnabled = cookie.enabled === undefined ? true : cookie.enabled;
|
||
|
||
console.log(`账号 ${cookie.id} 状态: enabled=${cookie.enabled}, isEnabled=${isEnabled}`); // 调试信息
|
||
|
||
const tr = document.createElement('tr');
|
||
tr.className = `account-row ${isEnabled ? 'enabled' : 'disabled'}`;
|
||
// 默认回复状态标签
|
||
const defaultReplyBadge = cookie.defaultReply.enabled ?
|
||
'<span class="badge bg-success">启用</span>' :
|
||
'<span class="badge bg-secondary">禁用</span>';
|
||
|
||
// AI回复状态标签
|
||
const aiReplyBadge = cookie.aiReply.ai_enabled ?
|
||
'<span class="badge bg-primary">AI启用</span>' :
|
||
'<span class="badge bg-secondary">AI禁用</span>';
|
||
|
||
// 自动确认发货状态(默认开启)
|
||
const autoConfirm = cookie.auto_confirm === undefined ? true : cookie.auto_confirm;
|
||
|
||
tr.innerHTML = `
|
||
<td class="align-middle">
|
||
<div class="cookie-id">
|
||
<strong class="text-primary">${cookie.id}</strong>
|
||
</div>
|
||
</td>
|
||
<td class="align-middle">
|
||
<div class="cookie-value" title="点击复制Cookie" style="font-family: monospace; font-size: 0.875rem; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||
${cookie.value || '未设置'}
|
||
</div>
|
||
</td>
|
||
<td class="align-middle">
|
||
<span class="badge ${cookie.keywordCount > 0 ? 'bg-success' : 'bg-secondary'}">
|
||
${cookie.keywordCount} 个关键词
|
||
</span>
|
||
</td>
|
||
<td class="align-middle">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<label class="status-toggle" title="${isEnabled ? '点击禁用' : '点击启用'}">
|
||
<input type="checkbox" ${isEnabled ? 'checked' : ''} onchange="toggleAccountStatus('${cookie.id}', this.checked)">
|
||
<span class="status-slider"></span>
|
||
</label>
|
||
<span class="status-badge ${isEnabled ? 'enabled' : 'disabled'}" title="${isEnabled ? '账号已启用' : '账号已禁用'}">
|
||
<i class="bi bi-${isEnabled ? 'check-circle-fill' : 'x-circle-fill'}"></i>
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td class="align-middle">
|
||
${defaultReplyBadge}
|
||
</td>
|
||
<td class="align-middle">
|
||
${aiReplyBadge}
|
||
</td>
|
||
<td class="align-middle">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<label class="status-toggle" title="${autoConfirm ? '点击关闭自动确认发货' : '点击开启自动确认发货'}">
|
||
<input type="checkbox" ${autoConfirm ? 'checked' : ''} onchange="toggleAutoConfirm('${cookie.id}', this.checked)">
|
||
<span class="status-slider"></span>
|
||
</label>
|
||
<span class="status-badge ${autoConfirm ? 'enabled' : 'disabled'}" title="${autoConfirm ? '自动确认发货已开启' : '自动确认发货已关闭'}">
|
||
<i class="bi bi-${autoConfirm ? 'truck' : 'truck-flatbed'}"></i>
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td class="align-middle">
|
||
<div class="btn-group" role="group">
|
||
<button class="btn btn-sm btn-outline-primary" onclick="editCookieInline('${cookie.id}', '${cookie.value}')" title="修改Cookie" ${!isEnabled ? 'disabled' : ''}>
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-success" onclick="goToAutoReply('${cookie.id}')" title="${isEnabled ? '设置自动回复' : '配置关键词 (账号已禁用)'}">
|
||
<i class="bi bi-arrow-right-circle"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-warning" onclick="configAIReply('${cookie.id}')" title="配置AI回复" ${!isEnabled ? 'disabled' : ''}>
|
||
<i class="bi bi-robot"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-info" onclick="copyCookie('${cookie.id}', '${cookie.value}')" title="复制Cookie">
|
||
<i class="bi bi-clipboard"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="delCookie('${cookie.id}')" title="删除账号">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
|
||
// 为Cookie值添加点击复制功能
|
||
document.querySelectorAll('.cookie-value').forEach(element => {
|
||
element.style.cursor = 'pointer';
|
||
element.addEventListener('click', function() {
|
||
const cookieValue = this.textContent;
|
||
if (cookieValue && cookieValue !== '未设置') {
|
||
navigator.clipboard.writeText(cookieValue).then(() => {
|
||
showToast('Cookie已复制到剪贴板', 'success');
|
||
}).catch(() => {
|
||
showToast('复制失败,请手动复制', 'error');
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
} catch (err) {
|
||
// 错误已在fetchJSON中处理
|
||
} finally {
|
||
toggleLoading(false);
|
||
}
|
||
}
|
||
|
||
// 复制Cookie
|
||
function copyCookie(id, value) {
|
||
if (!value || value === '未设置') {
|
||
showToast('该账号暂无Cookie值', 'warning');
|
||
return;
|
||
}
|
||
|
||
navigator.clipboard.writeText(value).then(() => {
|
||
showToast(`账号 "${id}" 的Cookie已复制到剪贴板`, 'success');
|
||
}).catch(() => {
|
||
// 降级方案:创建临时文本框
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = value;
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
try {
|
||
document.execCommand('copy');
|
||
showToast(`账号 "${id}" 的Cookie已复制到剪贴板`, 'success');
|
||
} catch (err) {
|
||
showToast('复制失败,请手动复制', 'error');
|
||
}
|
||
document.body.removeChild(textArea);
|
||
});
|
||
}
|
||
|
||
// 删除Cookie
|
||
async function delCookie(id) {
|
||
if (!confirm(`确定要删除账号 "${id}" 吗?此操作不可恢复。`)) return;
|
||
|
||
try {
|
||
await fetchJSON(apiBase + `/cookies/${id}`, { method: 'DELETE' });
|
||
showToast(`账号 "${id}" 已删除`, 'success');
|
||
loadCookies();
|
||
} catch (err) {
|
||
// 错误已在fetchJSON中处理
|
||
}
|
||
}
|
||
|
||
// 内联编辑Cookie
|
||
function editCookieInline(id, currentValue) {
|
||
const row = event.target.closest('tr');
|
||
const cookieValueCell = row.querySelector('.cookie-value');
|
||
const originalContent = cookieValueCell.innerHTML;
|
||
|
||
// 存储原始数据到全局变量,避免HTML注入问题
|
||
window.editingCookieData = {
|
||
id: id,
|
||
originalContent: originalContent,
|
||
originalValue: currentValue || ''
|
||
};
|
||
|
||
// 创建编辑界面容器
|
||
const editContainer = document.createElement('div');
|
||
editContainer.className = 'd-flex gap-2';
|
||
|
||
// 创建输入框
|
||
const input = document.createElement('input');
|
||
input.type = 'text';
|
||
input.className = 'form-control form-control-sm';
|
||
input.id = `edit-${id}`;
|
||
input.value = currentValue || '';
|
||
input.placeholder = '输入新的Cookie值';
|
||
|
||
// 创建保存按钮
|
||
const saveBtn = document.createElement('button');
|
||
saveBtn.className = 'btn btn-sm btn-success';
|
||
saveBtn.title = '保存';
|
||
saveBtn.innerHTML = '<i class="bi bi-check"></i>';
|
||
saveBtn.onclick = () => saveCookieInline(id);
|
||
|
||
// 创建取消按钮
|
||
const cancelBtn = document.createElement('button');
|
||
cancelBtn.className = 'btn btn-sm btn-secondary';
|
||
cancelBtn.title = '取消';
|
||
cancelBtn.innerHTML = '<i class="bi bi-x"></i>';
|
||
cancelBtn.onclick = () => cancelCookieEdit(id);
|
||
|
||
// 组装编辑界面
|
||
editContainer.appendChild(input);
|
||
editContainer.appendChild(saveBtn);
|
||
editContainer.appendChild(cancelBtn);
|
||
|
||
// 替换原内容
|
||
cookieValueCell.innerHTML = '';
|
||
cookieValueCell.appendChild(editContainer);
|
||
|
||
// 聚焦输入框
|
||
input.focus();
|
||
input.select();
|
||
|
||
// 添加键盘事件监听
|
||
input.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
saveCookieInline(id);
|
||
} else if (e.key === 'Escape') {
|
||
e.preventDefault();
|
||
cancelCookieEdit(id);
|
||
}
|
||
});
|
||
|
||
// 禁用该行的其他按钮
|
||
const actionButtons = row.querySelectorAll('.btn-group button');
|
||
actionButtons.forEach(btn => btn.disabled = true);
|
||
}
|
||
|
||
// 保存内联编辑的Cookie
|
||
async function saveCookieInline(id) {
|
||
const input = document.getElementById(`edit-${id}`);
|
||
const newValue = input.value.trim();
|
||
|
||
if (!newValue) {
|
||
showToast('Cookie值不能为空', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
toggleLoading(true);
|
||
|
||
await fetchJSON(apiBase + `/cookies/${id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
id: id,
|
||
value: newValue
|
||
})
|
||
});
|
||
|
||
showToast(`账号 "${id}" Cookie已更新`, 'success');
|
||
loadCookies(); // 重新加载列表
|
||
|
||
} catch (err) {
|
||
console.error('Cookie更新失败:', err);
|
||
showToast(`Cookie更新失败: ${err.message || '未知错误'}`, 'danger');
|
||
// 恢复原内容
|
||
cancelCookieEdit(id);
|
||
} finally {
|
||
toggleLoading(false);
|
||
}
|
||
}
|
||
|
||
// 取消Cookie编辑
|
||
function cancelCookieEdit(id) {
|
||
if (!window.editingCookieData || window.editingCookieData.id !== id) {
|
||
console.error('编辑数据不存在');
|
||
return;
|
||
}
|
||
|
||
const row = document.querySelector(`#edit-${id}`).closest('tr');
|
||
const cookieValueCell = row.querySelector('.cookie-value');
|
||
|
||
// 恢复原内容
|
||
cookieValueCell.innerHTML = window.editingCookieData.originalContent;
|
||
|
||
// 恢复按钮状态
|
||
const actionButtons = row.querySelectorAll('.btn-group button');
|
||
actionButtons.forEach(btn => btn.disabled = false);
|
||
|
||
// 清理全局数据
|
||
delete window.editingCookieData;
|
||
}
|
||
|
||
|
||
|
||
// 切换账号启用/禁用状态
|
||
async function toggleAccountStatus(accountId, enabled) {
|
||
try {
|
||
toggleLoading(true);
|
||
|
||
// 这里需要调用后端API来更新账号状态
|
||
// 由于当前后端可能没有enabled字段,我们先在前端模拟
|
||
// 实际项目中需要后端支持
|
||
|
||
const response = await fetch(`${apiBase}/cookies/${accountId}/status`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify({ enabled: enabled })
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast(`账号 "${accountId}" 已${enabled ? '启用' : '禁用'}`, 'success');
|
||
|
||
// 清除相关缓存,确保数据一致性
|
||
clearKeywordCache();
|
||
|
||
// 更新界面显示
|
||
updateAccountRowStatus(accountId, enabled);
|
||
|
||
// 刷新自动回复页面的账号列表
|
||
refreshAccountList();
|
||
|
||
// 如果禁用的账号在自动回复页面被选中,更新显示
|
||
const accountSelect = document.getElementById('accountSelect');
|
||
if (accountSelect && accountSelect.value === accountId) {
|
||
if (!enabled) {
|
||
// 更新徽章显示禁用状态
|
||
updateAccountBadge(accountId, false);
|
||
showToast('账号已禁用,配置的关键词不会参与自动回复', 'warning');
|
||
} else {
|
||
// 更新徽章显示启用状态
|
||
updateAccountBadge(accountId, true);
|
||
showToast('账号已启用,配置的关键词将参与自动回复', 'success');
|
||
}
|
||
}
|
||
|
||
} else {
|
||
// 如果后端不支持,先在前端模拟
|
||
console.warn('后端暂不支持账号状态切换,使用前端模拟');
|
||
showToast(`账号 "${accountId}" 已${enabled ? '启用' : '禁用'} (前端模拟)`, enabled ? 'success' : 'warning');
|
||
updateAccountRowStatus(accountId, enabled);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('切换账号状态失败:', error);
|
||
|
||
// 后端不支持时的降级处理
|
||
showToast(`账号 "${accountId}" 已${enabled ? '启用' : '禁用'} (本地模拟)`, enabled ? 'success' : 'warning');
|
||
updateAccountRowStatus(accountId, enabled);
|
||
|
||
// 恢复切换按钮状态
|
||
const toggle = document.querySelector(`input[onchange*="${accountId}"]`);
|
||
if (toggle) {
|
||
toggle.checked = enabled;
|
||
}
|
||
} finally {
|
||
toggleLoading(false);
|
||
}
|
||
}
|
||
|
||
// 更新账号行的状态显示
|
||
function updateAccountRowStatus(accountId, enabled) {
|
||
const toggle = document.querySelector(`input[onchange*="${accountId}"]`);
|
||
if (!toggle) return;
|
||
|
||
const row = toggle.closest('tr');
|
||
const statusBadge = row.querySelector('.status-badge');
|
||
const actionButtons = row.querySelectorAll('.btn-group .btn:not(.btn-outline-info):not(.btn-outline-danger)');
|
||
|
||
// 更新行样式
|
||
row.className = `account-row ${enabled ? 'enabled' : 'disabled'}`;
|
||
|
||
// 更新状态徽章
|
||
statusBadge.className = `status-badge ${enabled ? 'enabled' : 'disabled'}`;
|
||
statusBadge.title = enabled ? '账号已启用' : '账号已禁用';
|
||
statusBadge.innerHTML = `
|
||
<i class="bi bi-${enabled ? 'check-circle-fill' : 'x-circle-fill'}"></i>
|
||
`;
|
||
|
||
// 更新按钮状态(只禁用编辑Cookie按钮,其他按钮保持可用)
|
||
actionButtons.forEach(btn => {
|
||
if (btn.onclick && btn.onclick.toString().includes('editCookieInline')) {
|
||
btn.disabled = !enabled;
|
||
}
|
||
// 设置自动回复按钮始终可用,但更新提示文本
|
||
if (btn.onclick && btn.onclick.toString().includes('goToAutoReply')) {
|
||
btn.title = enabled ? '设置自动回复' : '配置关键词 (账号已禁用)';
|
||
}
|
||
});
|
||
|
||
// 更新切换按钮的提示
|
||
const label = toggle.closest('.status-toggle');
|
||
label.title = enabled ? '点击禁用' : '点击启用';
|
||
}
|
||
|
||
// 切换自动确认发货状态
|
||
async function toggleAutoConfirm(accountId, enabled) {
|
||
try {
|
||
toggleLoading(true);
|
||
|
||
const response = await fetch(`${apiBase}/cookies/${accountId}/auto-confirm`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify({ auto_confirm: enabled })
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
showToast(result.message, 'success');
|
||
|
||
// 更新界面显示
|
||
updateAutoConfirmRowStatus(accountId, enabled);
|
||
} else {
|
||
const error = await response.json();
|
||
showToast(error.detail || '更新自动确认发货设置失败', 'error');
|
||
|
||
// 恢复切换按钮状态
|
||
const toggle = document.querySelector(`input[onchange*="toggleAutoConfirm('${accountId}'"]`);
|
||
if (toggle) {
|
||
toggle.checked = !enabled;
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('切换自动确认发货状态失败:', error);
|
||
showToast('网络错误,请稍后重试', 'error');
|
||
|
||
// 恢复切换按钮状态
|
||
const toggle = document.querySelector(`input[onchange*="toggleAutoConfirm('${accountId}'"]`);
|
||
if (toggle) {
|
||
toggle.checked = !enabled;
|
||
}
|
||
} finally {
|
||
toggleLoading(false);
|
||
}
|
||
}
|
||
|
||
// 更新自动确认发货行状态
|
||
function updateAutoConfirmRowStatus(accountId, enabled) {
|
||
const row = document.querySelector(`tr:has(input[onchange*="toggleAutoConfirm('${accountId}'"])`);
|
||
if (!row) return;
|
||
|
||
const statusBadge = row.querySelector('.status-badge:has(i.bi-truck, i.bi-truck-flatbed)');
|
||
const toggle = row.querySelector(`input[onchange*="toggleAutoConfirm('${accountId}'"]`);
|
||
|
||
if (statusBadge && toggle) {
|
||
// 更新状态徽章
|
||
statusBadge.className = `status-badge ${enabled ? 'enabled' : 'disabled'}`;
|
||
statusBadge.title = enabled ? '自动确认发货已开启' : '自动确认发货已关闭';
|
||
statusBadge.innerHTML = `
|
||
<i class="bi bi-${enabled ? 'truck' : 'truck-flatbed'}"></i>
|
||
`;
|
||
|
||
// 更新切换按钮的提示
|
||
const label = toggle.closest('.status-toggle');
|
||
label.title = enabled ? '点击关闭自动确认发货' : '点击开启自动确认发货';
|
||
}
|
||
}
|
||
|
||
// 跳转到自动回复页面并选择指定账号
|
||
function goToAutoReply(accountId) {
|
||
// 切换到自动回复页面
|
||
showSection('auto-reply');
|
||
|
||
// 设置账号选择器的值
|
||
setTimeout(() => {
|
||
const accountSelect = document.getElementById('accountSelect');
|
||
if (accountSelect) {
|
||
accountSelect.value = accountId;
|
||
// 触发change事件来加载关键词
|
||
loadAccountKeywords();
|
||
}
|
||
}, 100);
|
||
|
||
showToast(`已切换到自动回复页面,账号 "${accountId}" 已选中`, 'info');
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
// 登出功能
|
||
async function logout() {
|
||
try {
|
||
if (authToken) {
|
||
await fetch('/logout', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
}
|
||
localStorage.removeItem('auth_token');
|
||
window.location.href = '/';
|
||
} catch (err) {
|
||
console.error('登出失败:', err);
|
||
localStorage.removeItem('auth_token');
|
||
window.location.href = '/';
|
||
}
|
||
}
|
||
|
||
// 检查认证状态
|
||
async function checkAuth() {
|
||
if (!authToken) {
|
||
window.location.href = '/';
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/verify', {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (!result.authenticated) {
|
||
localStorage.removeItem('auth_token');
|
||
window.location.href = '/';
|
||
return false;
|
||
}
|
||
|
||
// 检查是否为管理员,显示管理员菜单和功能
|
||
if (result.username === 'admin') {
|
||
const adminMenuSection = document.getElementById('adminMenuSection');
|
||
if (adminMenuSection) {
|
||
adminMenuSection.style.display = 'block';
|
||
}
|
||
|
||
// 显示备份管理功能
|
||
const backupManagement = document.getElementById('backup-management');
|
||
if (backupManagement) {
|
||
backupManagement.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
return true;
|
||
} catch (err) {
|
||
localStorage.removeItem('auth_token');
|
||
window.location.href = '/';
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 初始化事件监听
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
// 首先检查认证状态
|
||
const isAuthenticated = await checkAuth();
|
||
if (!isAuthenticated) return;
|
||
// 添加Cookie表单提交
|
||
document.getElementById('addForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const id = document.getElementById('cookieId').value.trim();
|
||
const value = document.getElementById('cookieValue').value.trim();
|
||
|
||
if (!id || !value) return;
|
||
|
||
try {
|
||
await fetchJSON(apiBase + '/cookies', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id, value })
|
||
});
|
||
|
||
document.getElementById('cookieId').value = '';
|
||
document.getElementById('cookieValue').value = '';
|
||
showToast(`账号 "${id}" 添加成功`);
|
||
loadCookies();
|
||
} catch (err) {
|
||
// 错误已在fetchJSON中处理
|
||
}
|
||
});
|
||
|
||
// 增强的键盘快捷键和用户体验
|
||
document.getElementById('newKeyword')?.addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
document.getElementById('newReply').focus();
|
||
}
|
||
});
|
||
|
||
document.getElementById('newReply')?.addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
addKeyword();
|
||
}
|
||
});
|
||
|
||
// ESC键取消编辑
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape' && typeof window.editingIndex !== 'undefined') {
|
||
e.preventDefault();
|
||
cancelEdit();
|
||
}
|
||
});
|
||
|
||
// 输入框实时验证和提示
|
||
document.getElementById('newKeyword')?.addEventListener('input', function(e) {
|
||
const value = e.target.value.trim();
|
||
const addBtn = document.querySelector('.add-btn');
|
||
const replyInput = document.getElementById('newReply');
|
||
|
||
if (value.length > 0) {
|
||
e.target.style.borderColor = '#10b981';
|
||
if (replyInput.value.trim().length > 0) {
|
||
addBtn.style.opacity = '1';
|
||
addBtn.style.transform = 'scale(1)';
|
||
}
|
||
} else {
|
||
e.target.style.borderColor = '#e5e7eb';
|
||
addBtn.style.opacity = '0.7';
|
||
addBtn.style.transform = 'scale(0.95)';
|
||
}
|
||
});
|
||
|
||
document.getElementById('newReply')?.addEventListener('input', function(e) {
|
||
const value = e.target.value.trim();
|
||
const addBtn = document.querySelector('.add-btn');
|
||
const keywordInput = document.getElementById('newKeyword');
|
||
|
||
if (value.length > 0) {
|
||
e.target.style.borderColor = '#10b981';
|
||
if (keywordInput.value.trim().length > 0) {
|
||
addBtn.style.opacity = '1';
|
||
addBtn.style.transform = 'scale(1)';
|
||
}
|
||
} else {
|
||
e.target.style.borderColor = '#e5e7eb';
|
||
addBtn.style.opacity = '0.7';
|
||
addBtn.style.transform = 'scale(0.95)';
|
||
}
|
||
});
|
||
|
||
// 初始加载仪表盘
|
||
loadDashboard();
|
||
|
||
// 点击侧边栏外部关闭移动端菜单
|
||
document.addEventListener('click', function(e) {
|
||
const sidebar = document.getElementById('sidebar');
|
||
const toggle = document.querySelector('.mobile-toggle');
|
||
|
||
if (window.innerWidth <= 768 &&
|
||
!sidebar.contains(e.target) &&
|
||
!toggle.contains(e.target) &&
|
||
sidebar.classList.contains('show')) {
|
||
sidebar.classList.remove('show');
|
||
}
|
||
});
|
||
});
|
||
|
||
// ==================== 默认回复管理功能 ====================
|
||
|
||
// 打开默认回复管理器
|
||
async function openDefaultReplyManager() {
|
||
try {
|
||
await loadDefaultReplies();
|
||
const modal = new bootstrap.Modal(document.getElementById('defaultReplyModal'));
|
||
modal.show();
|
||
} catch (error) {
|
||
console.error('打开默认回复管理器失败:', error);
|
||
showToast('打开默认回复管理器失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 加载默认回复列表
|
||
async function loadDefaultReplies() {
|
||
try {
|
||
// 获取所有账号
|
||
const accountsResponse = await fetch(`${apiBase}/cookies`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (!accountsResponse.ok) {
|
||
throw new Error('获取账号列表失败');
|
||
}
|
||
|
||
const accounts = await accountsResponse.json();
|
||
|
||
// 获取所有默认回复设置
|
||
const repliesResponse = await fetch(`${apiBase}/default-replies`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
let defaultReplies = {};
|
||
if (repliesResponse.ok) {
|
||
defaultReplies = await repliesResponse.json();
|
||
}
|
||
|
||
renderDefaultRepliesList(accounts, defaultReplies);
|
||
} catch (error) {
|
||
console.error('加载默认回复列表失败:', error);
|
||
showToast('加载默认回复列表失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 渲染默认回复列表
|
||
function renderDefaultRepliesList(accounts, defaultReplies) {
|
||
const tbody = document.getElementById('defaultReplyTableBody');
|
||
tbody.innerHTML = '';
|
||
|
||
if (accounts.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="4" class="text-center py-4 text-muted">
|
||
<i class="bi bi-chat-text fs-1 d-block mb-3"></i>
|
||
<h5>暂无账号数据</h5>
|
||
<p class="mb-0">请先添加账号</p>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
accounts.forEach(accountId => {
|
||
const replySettings = defaultReplies[accountId] || { enabled: false, reply_content: '' };
|
||
const tr = document.createElement('tr');
|
||
|
||
// 状态标签
|
||
const statusBadge = replySettings.enabled ?
|
||
'<span class="badge bg-success">启用</span>' :
|
||
'<span class="badge bg-secondary">禁用</span>';
|
||
|
||
// 回复内容预览
|
||
let contentPreview = replySettings.reply_content || '未设置';
|
||
if (contentPreview.length > 50) {
|
||
contentPreview = contentPreview.substring(0, 50) + '...';
|
||
}
|
||
|
||
tr.innerHTML = `
|
||
<td>
|
||
<strong class="text-primary">${accountId}</strong>
|
||
</td>
|
||
<td>${statusBadge}</td>
|
||
<td>
|
||
<div class="text-truncate" style="max-width: 300px;" title="${replySettings.reply_content || ''}">
|
||
${contentPreview}
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="btn-group" role="group">
|
||
<button class="btn btn-sm btn-outline-primary" onclick="editDefaultReply('${accountId}')" title="编辑">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-info" onclick="testDefaultReply('${accountId}')" title="测试">
|
||
<i class="bi bi-play"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
`;
|
||
|
||
tbody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
// 编辑默认回复
|
||
async function editDefaultReply(accountId) {
|
||
try {
|
||
// 获取当前设置
|
||
const response = await fetch(`${apiBase}/default-replies/${accountId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
let settings = { enabled: false, reply_content: '' };
|
||
if (response.ok) {
|
||
settings = await response.json();
|
||
}
|
||
|
||
// 填充编辑表单
|
||
document.getElementById('editAccountId').value = accountId;
|
||
document.getElementById('editAccountIdDisplay').value = accountId;
|
||
document.getElementById('editDefaultReplyEnabled').checked = settings.enabled;
|
||
document.getElementById('editReplyContent').value = settings.reply_content || '';
|
||
|
||
// 根据启用状态显示/隐藏内容输入框
|
||
toggleReplyContentVisibility();
|
||
|
||
// 显示编辑模态框
|
||
const modal = new bootstrap.Modal(document.getElementById('editDefaultReplyModal'));
|
||
modal.show();
|
||
} catch (error) {
|
||
console.error('获取默认回复设置失败:', error);
|
||
showToast('获取默认回复设置失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 切换回复内容输入框的显示/隐藏
|
||
function toggleReplyContentVisibility() {
|
||
const enabled = document.getElementById('editDefaultReplyEnabled').checked;
|
||
const contentGroup = document.getElementById('editReplyContentGroup');
|
||
contentGroup.style.display = enabled ? 'block' : 'none';
|
||
}
|
||
|
||
// 保存默认回复设置
|
||
async function saveDefaultReply() {
|
||
try {
|
||
const accountId = document.getElementById('editAccountId').value;
|
||
const enabled = document.getElementById('editDefaultReplyEnabled').checked;
|
||
const replyContent = document.getElementById('editReplyContent').value;
|
||
|
||
if (enabled && !replyContent.trim()) {
|
||
showToast('启用默认回复时必须设置回复内容', 'warning');
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
enabled: enabled,
|
||
reply_content: enabled ? replyContent : null
|
||
};
|
||
|
||
const response = await fetch(`${apiBase}/default-replies/${accountId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('默认回复设置保存成功', 'success');
|
||
bootstrap.Modal.getInstance(document.getElementById('editDefaultReplyModal')).hide();
|
||
loadDefaultReplies(); // 刷新列表
|
||
loadCookies(); // 刷新账号列表以更新默认回复状态显示
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`保存失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('保存默认回复设置失败:', error);
|
||
showToast('保存默认回复设置失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 测试默认回复(占位函数)
|
||
function testDefaultReply(accountId) {
|
||
showToast('测试功能开发中...', 'info');
|
||
}
|
||
|
||
// ==================== AI回复配置相关函数 ====================
|
||
|
||
// 配置AI回复
|
||
async function configAIReply(accountId) {
|
||
try {
|
||
// 获取当前AI回复设置
|
||
const settings = await fetchJSON(`${apiBase}/ai-reply-settings/${accountId}`);
|
||
|
||
// 填充表单
|
||
document.getElementById('aiConfigAccountId').value = accountId;
|
||
document.getElementById('aiConfigAccountIdDisplay').value = accountId;
|
||
document.getElementById('aiReplyEnabled').checked = settings.ai_enabled;
|
||
document.getElementById('aiModelName').value = settings.model_name;
|
||
document.getElementById('aiBaseUrl').value = settings.base_url;
|
||
document.getElementById('aiApiKey').value = settings.api_key;
|
||
document.getElementById('maxDiscountPercent').value = settings.max_discount_percent;
|
||
document.getElementById('maxDiscountAmount').value = settings.max_discount_amount;
|
||
document.getElementById('maxBargainRounds').value = settings.max_bargain_rounds;
|
||
document.getElementById('customPrompts').value = settings.custom_prompts;
|
||
|
||
// 切换设置显示状态
|
||
toggleAIReplySettings();
|
||
|
||
// 显示模态框
|
||
const modal = new bootstrap.Modal(document.getElementById('aiReplyConfigModal'));
|
||
modal.show();
|
||
|
||
} catch (error) {
|
||
console.error('获取AI回复设置失败:', error);
|
||
showToast('获取AI回复设置失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 切换AI回复设置显示
|
||
function toggleAIReplySettings() {
|
||
const enabled = document.getElementById('aiReplyEnabled').checked;
|
||
const settingsDiv = document.getElementById('aiReplySettings');
|
||
const bargainSettings = document.getElementById('bargainSettings');
|
||
const promptSettings = document.getElementById('promptSettings');
|
||
const testArea = document.getElementById('testArea');
|
||
|
||
if (enabled) {
|
||
settingsDiv.style.display = 'block';
|
||
bargainSettings.style.display = 'block';
|
||
promptSettings.style.display = 'block';
|
||
testArea.style.display = 'block';
|
||
} else {
|
||
settingsDiv.style.display = 'none';
|
||
bargainSettings.style.display = 'none';
|
||
promptSettings.style.display = 'none';
|
||
testArea.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 保存AI回复配置
|
||
async function saveAIReplyConfig() {
|
||
try {
|
||
const accountId = document.getElementById('aiConfigAccountId').value;
|
||
const enabled = document.getElementById('aiReplyEnabled').checked;
|
||
|
||
// 如果启用AI回复,验证必填字段
|
||
if (enabled) {
|
||
const apiKey = document.getElementById('aiApiKey').value.trim();
|
||
if (!apiKey) {
|
||
showToast('请输入API密钥', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 验证自定义提示词格式
|
||
const customPrompts = document.getElementById('customPrompts').value.trim();
|
||
if (customPrompts) {
|
||
try {
|
||
JSON.parse(customPrompts);
|
||
} catch (e) {
|
||
showToast('自定义提示词格式错误,请检查JSON格式', 'warning');
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 构建设置对象
|
||
const settings = {
|
||
ai_enabled: enabled,
|
||
model_name: document.getElementById('aiModelName').value,
|
||
api_key: document.getElementById('aiApiKey').value,
|
||
base_url: document.getElementById('aiBaseUrl').value,
|
||
max_discount_percent: parseInt(document.getElementById('maxDiscountPercent').value),
|
||
max_discount_amount: parseInt(document.getElementById('maxDiscountAmount').value),
|
||
max_bargain_rounds: parseInt(document.getElementById('maxBargainRounds').value),
|
||
custom_prompts: document.getElementById('customPrompts').value
|
||
};
|
||
|
||
// 保存设置
|
||
const response = await fetch(`${apiBase}/ai-reply-settings/${accountId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify(settings)
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('AI回复配置保存成功', 'success');
|
||
bootstrap.Modal.getInstance(document.getElementById('aiReplyConfigModal')).hide();
|
||
loadCookies(); // 刷新账号列表以更新AI回复状态显示
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`保存失败: ${error}`, 'danger');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('保存AI回复配置失败:', error);
|
||
showToast('保存AI回复配置失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 测试AI回复
|
||
async function testAIReply() {
|
||
try {
|
||
const accountId = document.getElementById('aiConfigAccountId').value;
|
||
const testMessage = document.getElementById('testMessage').value.trim();
|
||
const testItemPrice = document.getElementById('testItemPrice').value;
|
||
|
||
if (!testMessage) {
|
||
showToast('请输入测试消息', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 构建测试数据
|
||
const testData = {
|
||
message: testMessage,
|
||
item_title: '测试商品',
|
||
item_price: parseFloat(testItemPrice) || 100,
|
||
item_desc: '这是一个用于测试AI回复功能的商品'
|
||
};
|
||
|
||
// 显示加载状态
|
||
const testResult = document.getElementById('testResult');
|
||
const testReplyContent = document.getElementById('testReplyContent');
|
||
testResult.style.display = 'block';
|
||
testReplyContent.innerHTML = '<i class="bi bi-hourglass-split"></i> 正在生成AI回复...';
|
||
|
||
// 调用测试API
|
||
const response = await fetch(`${apiBase}/ai-reply-test/${accountId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify(testData)
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
testReplyContent.innerHTML = result.reply;
|
||
showToast('AI回复测试成功', 'success');
|
||
} else {
|
||
const error = await response.text();
|
||
testReplyContent.innerHTML = `<span class="text-danger">测试失败: ${error}</span>`;
|
||
showToast(`测试失败: ${error}`, 'danger');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('测试AI回复失败:', error);
|
||
const testReplyContent = document.getElementById('testReplyContent');
|
||
testReplyContent.innerHTML = `<span class="text-danger">测试失败: ${error.message}</span>`;
|
||
showToast('测试AI回复失败', 'danger');
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// 监听默认回复启用状态变化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const enabledCheckbox = document.getElementById('editDefaultReplyEnabled');
|
||
if (enabledCheckbox) {
|
||
enabledCheckbox.addEventListener('change', toggleReplyContentVisibility);
|
||
}
|
||
});
|
||
|
||
// ==================== 通知渠道管理功能 ====================
|
||
|
||
// 加载通知渠道列表
|
||
async function loadNotificationChannels() {
|
||
try {
|
||
const response = await fetch(`${apiBase}/notification-channels`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('获取通知渠道失败');
|
||
}
|
||
|
||
const channels = await response.json();
|
||
renderNotificationChannels(channels);
|
||
} catch (error) {
|
||
console.error('加载通知渠道失败:', error);
|
||
showToast('加载通知渠道失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 渲染通知渠道列表
|
||
function renderNotificationChannels(channels) {
|
||
const tbody = document.getElementById('channelsTableBody');
|
||
tbody.innerHTML = '';
|
||
|
||
if (channels.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="6" class="text-center py-4 text-muted">
|
||
<i class="bi bi-bell fs-1 d-block mb-3"></i>
|
||
<h5>暂无通知渠道</h5>
|
||
<p class="mb-0">请添加QQ通知渠道</p>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
channels.forEach(channel => {
|
||
const tr = document.createElement('tr');
|
||
|
||
const statusBadge = channel.enabled ?
|
||
'<span class="badge bg-success">启用</span>' :
|
||
'<span class="badge bg-secondary">禁用</span>';
|
||
|
||
tr.innerHTML = `
|
||
<td><strong class="text-primary">${channel.id}</strong></td>
|
||
<td>${channel.name}</td>
|
||
<td><span class="badge bg-info">QQ</span></td>
|
||
<td><code>${channel.config}</code></td>
|
||
<td>${statusBadge}</td>
|
||
<td>
|
||
<div class="btn-group" role="group">
|
||
<button class="btn btn-sm btn-outline-primary" onclick="editNotificationChannel(${channel.id})" title="编辑">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="deleteNotificationChannel(${channel.id})" title="删除">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
`;
|
||
|
||
tbody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
// 添加通知渠道表单提交
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const addChannelForm = document.getElementById('addChannelForm');
|
||
if (addChannelForm) {
|
||
addChannelForm.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
const name = document.getElementById('channelName').value;
|
||
const qq = document.getElementById('channelQQ').value;
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/notification-channels`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
name: name,
|
||
type: 'qq',
|
||
config: qq
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('通知渠道添加成功', 'success');
|
||
addChannelForm.reset();
|
||
loadNotificationChannels();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`添加失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('添加通知渠道失败:', error);
|
||
showToast('添加通知渠道失败', 'danger');
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 删除通知渠道
|
||
async function deleteNotificationChannel(channelId) {
|
||
if (!confirm('确定要删除这个通知渠道吗?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/notification-channels/${channelId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('通知渠道删除成功', 'success');
|
||
loadNotificationChannels();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`删除失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('删除通知渠道失败:', error);
|
||
showToast('删除通知渠道失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 编辑通知渠道
|
||
async function editNotificationChannel(channelId) {
|
||
try {
|
||
// 获取渠道详情
|
||
const response = await fetch(`${apiBase}/notification-channels`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('获取通知渠道失败');
|
||
}
|
||
|
||
const channels = await response.json();
|
||
const channel = channels.find(c => c.id === channelId);
|
||
|
||
if (!channel) {
|
||
showToast('通知渠道不存在', 'danger');
|
||
return;
|
||
}
|
||
|
||
// 填充编辑表单
|
||
document.getElementById('editChannelId').value = channel.id;
|
||
document.getElementById('editChannelName').value = channel.name;
|
||
document.getElementById('editChannelQQ').value = channel.config;
|
||
document.getElementById('editChannelEnabled').checked = channel.enabled;
|
||
|
||
// 显示编辑模态框
|
||
const modal = new bootstrap.Modal(document.getElementById('editChannelModal'));
|
||
modal.show();
|
||
} catch (error) {
|
||
console.error('编辑通知渠道失败:', error);
|
||
showToast('编辑通知渠道失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 更新通知渠道
|
||
async function updateNotificationChannel() {
|
||
const channelId = document.getElementById('editChannelId').value;
|
||
const name = document.getElementById('editChannelName').value;
|
||
const qq = document.getElementById('editChannelQQ').value;
|
||
const enabled = document.getElementById('editChannelEnabled').checked;
|
||
|
||
if (!name || !qq) {
|
||
showToast('请填写完整信息', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/notification-channels/${channelId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
name: name,
|
||
config: qq,
|
||
enabled: enabled
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('通知渠道更新成功', 'success');
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('editChannelModal'));
|
||
modal.hide();
|
||
loadNotificationChannels();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`更新失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('更新通知渠道失败:', error);
|
||
showToast('更新通知渠道失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// ==================== 消息通知配置功能 ====================
|
||
|
||
// 加载消息通知配置
|
||
async function loadMessageNotifications() {
|
||
try {
|
||
// 获取所有账号
|
||
const accountsResponse = await fetch(`${apiBase}/cookies`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (!accountsResponse.ok) {
|
||
throw new Error('获取账号列表失败');
|
||
}
|
||
|
||
const accounts = await accountsResponse.json();
|
||
|
||
// 获取所有通知配置
|
||
const notificationsResponse = await fetch(`${apiBase}/message-notifications`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
let notifications = {};
|
||
if (notificationsResponse.ok) {
|
||
notifications = await notificationsResponse.json();
|
||
}
|
||
|
||
renderMessageNotifications(accounts, notifications);
|
||
} catch (error) {
|
||
console.error('加载消息通知配置失败:', error);
|
||
showToast('加载消息通知配置失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 渲染消息通知配置
|
||
function renderMessageNotifications(accounts, notifications) {
|
||
const tbody = document.getElementById('notificationsTableBody');
|
||
tbody.innerHTML = '';
|
||
|
||
if (accounts.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="4" class="text-center py-4 text-muted">
|
||
<i class="bi bi-chat-dots fs-1 d-block mb-3"></i>
|
||
<h5>暂无账号数据</h5>
|
||
<p class="mb-0">请先添加账号</p>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
accounts.forEach(accountId => {
|
||
const accountNotifications = notifications[accountId] || [];
|
||
const tr = document.createElement('tr');
|
||
|
||
let channelsList = '';
|
||
if (accountNotifications.length > 0) {
|
||
channelsList = accountNotifications.map(n =>
|
||
`<span class="badge bg-${n.enabled ? 'success' : 'secondary'} me-1">${n.channel_name}</span>`
|
||
).join('');
|
||
} else {
|
||
channelsList = '<span class="text-muted">未配置</span>';
|
||
}
|
||
|
||
const status = accountNotifications.some(n => n.enabled) ?
|
||
'<span class="badge bg-success">启用</span>' :
|
||
'<span class="badge bg-secondary">禁用</span>';
|
||
|
||
tr.innerHTML = `
|
||
<td><strong class="text-primary">${accountId}</strong></td>
|
||
<td>${channelsList}</td>
|
||
<td>${status}</td>
|
||
<td>
|
||
<div class="btn-group" role="group">
|
||
<button class="btn btn-sm btn-outline-primary" onclick="configAccountNotification('${accountId}')" title="配置">
|
||
<i class="bi bi-gear"></i> 配置
|
||
</button>
|
||
${accountNotifications.length > 0 ? `
|
||
<button class="btn btn-sm btn-outline-danger" onclick="deleteAccountNotification('${accountId}')" title="删除配置">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
` : ''}
|
||
</div>
|
||
</td>
|
||
`;
|
||
|
||
tbody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
// 配置账号通知
|
||
async function configAccountNotification(accountId) {
|
||
try {
|
||
// 获取所有通知渠道
|
||
const channelsResponse = await fetch(`${apiBase}/notification-channels`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (!channelsResponse.ok) {
|
||
throw new Error('获取通知渠道失败');
|
||
}
|
||
|
||
const channels = await channelsResponse.json();
|
||
|
||
if (channels.length === 0) {
|
||
showToast('请先添加通知渠道', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 获取当前账号的通知配置
|
||
const notificationResponse = await fetch(`${apiBase}/message-notifications/${accountId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
let currentNotifications = [];
|
||
if (notificationResponse.ok) {
|
||
currentNotifications = await notificationResponse.json();
|
||
}
|
||
|
||
// 填充表单
|
||
document.getElementById('configAccountId').value = accountId;
|
||
document.getElementById('displayAccountId').value = accountId;
|
||
|
||
// 填充通知渠道选项
|
||
const channelSelect = document.getElementById('notificationChannel');
|
||
channelSelect.innerHTML = '<option value="">请选择通知渠道</option>';
|
||
|
||
// 获取当前配置的第一个通知渠道(如果存在)
|
||
const currentNotification = currentNotifications.length > 0 ? currentNotifications[0] : null;
|
||
|
||
channels.forEach(channel => {
|
||
if (channel.enabled) {
|
||
const option = document.createElement('option');
|
||
option.value = channel.id;
|
||
option.textContent = `${channel.name} (${channel.config})`;
|
||
if (currentNotification && currentNotification.channel_id === channel.id) {
|
||
option.selected = true;
|
||
}
|
||
channelSelect.appendChild(option);
|
||
}
|
||
});
|
||
|
||
// 设置启用状态
|
||
document.getElementById('notificationEnabled').checked =
|
||
currentNotification ? currentNotification.enabled : true;
|
||
|
||
// 显示配置模态框
|
||
const modal = new bootstrap.Modal(document.getElementById('configNotificationModal'));
|
||
modal.show();
|
||
} catch (error) {
|
||
console.error('配置账号通知失败:', error);
|
||
showToast('配置账号通知失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 删除账号通知配置
|
||
async function deleteAccountNotification(accountId) {
|
||
if (!confirm(`确定要删除账号 ${accountId} 的通知配置吗?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/message-notifications/account/${accountId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('通知配置删除成功', 'success');
|
||
loadMessageNotifications();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`删除失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('删除通知配置失败:', error);
|
||
showToast('删除通知配置失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 保存账号通知配置
|
||
async function saveAccountNotification() {
|
||
const accountId = document.getElementById('configAccountId').value;
|
||
const channelId = document.getElementById('notificationChannel').value;
|
||
const enabled = document.getElementById('notificationEnabled').checked;
|
||
|
||
if (!channelId) {
|
||
showToast('请选择通知渠道', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/message-notifications/${accountId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
channel_id: parseInt(channelId),
|
||
enabled: enabled
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('通知配置保存成功', 'success');
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('configNotificationModal'));
|
||
modal.hide();
|
||
loadMessageNotifications();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`保存失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('保存通知配置失败:', error);
|
||
showToast('保存通知配置失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// ==================== 卡券管理功能 ====================
|
||
|
||
// 加载卡券列表
|
||
async function loadCards() {
|
||
try {
|
||
const response = await fetch(`${apiBase}/cards`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const cards = await response.json();
|
||
renderCardsList(cards);
|
||
updateCardsStats(cards);
|
||
} else {
|
||
showToast('加载卡券列表失败', 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('加载卡券列表失败:', error);
|
||
showToast('加载卡券列表失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 渲染卡券列表
|
||
function renderCardsList(cards) {
|
||
const tbody = document.getElementById('cardsTableBody');
|
||
|
||
if (cards.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="8" class="text-center py-4 text-muted">
|
||
<i class="bi bi-credit-card fs-1 d-block mb-3"></i>
|
||
<h5>暂无卡券数据</h5>
|
||
<p class="mb-0">点击"添加卡券"开始创建您的第一个卡券</p>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = '';
|
||
|
||
cards.forEach(card => {
|
||
const tr = document.createElement('tr');
|
||
|
||
// 类型标签
|
||
let typeBadge = '';
|
||
switch(card.type) {
|
||
case 'api':
|
||
typeBadge = '<span class="badge bg-info">API接口</span>';
|
||
break;
|
||
case 'text':
|
||
typeBadge = '<span class="badge bg-success">固定文字</span>';
|
||
break;
|
||
case 'data':
|
||
typeBadge = '<span class="badge bg-warning">批量数据</span>';
|
||
break;
|
||
}
|
||
|
||
// 状态标签
|
||
const statusBadge = card.enabled ?
|
||
'<span class="badge bg-success">启用</span>' :
|
||
'<span class="badge bg-secondary">禁用</span>';
|
||
|
||
// 数据量显示
|
||
let dataCount = '-';
|
||
if (card.type === 'data' && card.data_content) {
|
||
const lines = card.data_content.split('\n').filter(line => line.trim());
|
||
dataCount = lines.length;
|
||
} else if (card.type === 'api') {
|
||
dataCount = '∞';
|
||
} else if (card.type === 'text') {
|
||
dataCount = '1';
|
||
}
|
||
|
||
// 延时时间显示
|
||
const delayDisplay = card.delay_seconds > 0 ?
|
||
`${card.delay_seconds}秒` :
|
||
'<span class="text-muted">立即</span>';
|
||
|
||
// 规格信息显示
|
||
let specDisplay = '<span class="text-muted">普通卡券</span>';
|
||
if (card.is_multi_spec && card.spec_name && card.spec_value) {
|
||
specDisplay = `<span class="badge bg-primary">${card.spec_name}: ${card.spec_value}</span>`;
|
||
}
|
||
|
||
tr.innerHTML = `
|
||
<td>
|
||
<div class="fw-bold">${card.name}</div>
|
||
${card.description ? `<small class="text-muted">${card.description}</small>` : ''}
|
||
</td>
|
||
<td>${typeBadge}</td>
|
||
<td>${specDisplay}</td>
|
||
<td>${dataCount}</td>
|
||
<td>${delayDisplay}</td>
|
||
<td>${statusBadge}</td>
|
||
<td>
|
||
<small class="text-muted">${new Date(card.created_at).toLocaleString('zh-CN')}</small>
|
||
</td>
|
||
<td>
|
||
<div class="btn-group" role="group">
|
||
<button class="btn btn-sm btn-outline-primary" onclick="editCard(${card.id})" title="编辑">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-info" onclick="testCard(${card.id})" title="测试">
|
||
<i class="bi bi-play"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="deleteCard(${card.id})" title="删除">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
`;
|
||
|
||
tbody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
// 更新卡券统计
|
||
function updateCardsStats(cards) {
|
||
const totalCards = cards.length;
|
||
const apiCards = cards.filter(card => card.type === 'api').length;
|
||
const textCards = cards.filter(card => card.type === 'text').length;
|
||
const dataCards = cards.filter(card => card.type === 'data').length;
|
||
|
||
document.getElementById('totalCards').textContent = totalCards;
|
||
document.getElementById('apiCards').textContent = apiCards;
|
||
document.getElementById('textCards').textContent = textCards;
|
||
document.getElementById('dataCards').textContent = dataCards;
|
||
}
|
||
|
||
// 显示添加卡券模态框
|
||
function showAddCardModal() {
|
||
document.getElementById('addCardForm').reset();
|
||
toggleCardTypeFields();
|
||
const modal = new bootstrap.Modal(document.getElementById('addCardModal'));
|
||
modal.show();
|
||
}
|
||
|
||
// 切换卡券类型字段显示
|
||
function toggleCardTypeFields() {
|
||
const cardType = document.getElementById('cardType').value;
|
||
|
||
document.getElementById('apiFields').style.display = cardType === 'api' ? 'block' : 'none';
|
||
document.getElementById('textFields').style.display = cardType === 'text' ? 'block' : 'none';
|
||
document.getElementById('dataFields').style.display = cardType === 'data' ? 'block' : 'none';
|
||
}
|
||
|
||
// 切换多规格字段显示
|
||
function toggleMultiSpecFields() {
|
||
const isMultiSpec = document.getElementById('isMultiSpec').checked;
|
||
document.getElementById('multiSpecFields').style.display = isMultiSpec ? 'block' : 'none';
|
||
}
|
||
|
||
// 切换编辑多规格字段显示
|
||
function toggleEditMultiSpecFields() {
|
||
const checkbox = document.getElementById('editIsMultiSpec');
|
||
const fieldsDiv = document.getElementById('editMultiSpecFields');
|
||
|
||
if (!checkbox) {
|
||
console.error('编辑多规格开关元素未找到');
|
||
return;
|
||
}
|
||
|
||
if (!fieldsDiv) {
|
||
console.error('编辑多规格字段容器未找到');
|
||
return;
|
||
}
|
||
|
||
const isMultiSpec = checkbox.checked;
|
||
const displayStyle = isMultiSpec ? 'block' : 'none';
|
||
|
||
console.log('toggleEditMultiSpecFields - 多规格状态:', isMultiSpec);
|
||
console.log('toggleEditMultiSpecFields - 设置显示样式:', displayStyle);
|
||
|
||
fieldsDiv.style.display = displayStyle;
|
||
|
||
// 验证设置是否生效
|
||
console.log('toggleEditMultiSpecFields - 实际显示样式:', fieldsDiv.style.display);
|
||
}
|
||
|
||
// 清空添加卡券表单
|
||
function clearAddCardForm() {
|
||
try {
|
||
// 安全地清空表单字段
|
||
const setElementValue = (id, value) => {
|
||
const element = document.getElementById(id);
|
||
if (element) {
|
||
if (element.type === 'checkbox') {
|
||
element.checked = value;
|
||
} else {
|
||
element.value = value;
|
||
}
|
||
} else {
|
||
console.warn(`Element with id '${id}' not found`);
|
||
}
|
||
};
|
||
|
||
const setElementDisplay = (id, display) => {
|
||
const element = document.getElementById(id);
|
||
if (element) {
|
||
element.style.display = display;
|
||
} else {
|
||
console.warn(`Element with id '${id}' not found`);
|
||
}
|
||
};
|
||
|
||
// 清空基本字段
|
||
setElementValue('cardName', '');
|
||
setElementValue('cardType', 'text');
|
||
setElementValue('cardDescription', '');
|
||
setElementValue('cardDelaySeconds', '0');
|
||
setElementValue('isMultiSpec', false);
|
||
setElementValue('specName', '');
|
||
setElementValue('specValue', '');
|
||
|
||
// 隐藏多规格字段
|
||
setElementDisplay('multiSpecFields', 'none');
|
||
|
||
// 清空类型相关字段
|
||
setElementValue('textContent', '');
|
||
setElementValue('dataContent', '');
|
||
setElementValue('apiUrl', '');
|
||
setElementValue('apiMethod', 'GET');
|
||
setElementValue('apiHeaders', '');
|
||
setElementValue('apiParams', '');
|
||
setElementValue('apiTimeout', '10');
|
||
|
||
// 重置字段显示
|
||
toggleCardTypeFields();
|
||
} catch (error) {
|
||
console.error('清空表单时出错:', error);
|
||
}
|
||
}
|
||
|
||
// 保存卡券
|
||
async function saveCard() {
|
||
try {
|
||
const cardType = document.getElementById('cardType').value;
|
||
const cardName = document.getElementById('cardName').value;
|
||
|
||
if (!cardType || !cardName) {
|
||
showToast('请填写必填字段', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 检查多规格设置
|
||
const isMultiSpec = document.getElementById('isMultiSpec').checked;
|
||
const specName = document.getElementById('specName').value;
|
||
const specValue = document.getElementById('specValue').value;
|
||
|
||
// 验证多规格字段
|
||
if (isMultiSpec && (!specName || !specValue)) {
|
||
showToast('多规格卡券必须填写规格名称和规格值', 'warning');
|
||
return;
|
||
}
|
||
|
||
const cardData = {
|
||
name: cardName,
|
||
type: cardType,
|
||
description: document.getElementById('cardDescription').value,
|
||
delay_seconds: parseInt(document.getElementById('cardDelaySeconds').value) || 0,
|
||
enabled: true,
|
||
is_multi_spec: isMultiSpec,
|
||
spec_name: isMultiSpec ? specName : null,
|
||
spec_value: isMultiSpec ? specValue : null
|
||
};
|
||
|
||
// 根据类型添加特定配置
|
||
switch(cardType) {
|
||
case 'api':
|
||
// 验证和解析JSON字段
|
||
let headers = '{}';
|
||
let params = '{}';
|
||
|
||
try {
|
||
const headersInput = document.getElementById('apiHeaders').value.trim();
|
||
if (headersInput) {
|
||
JSON.parse(headersInput); // 验证JSON格式
|
||
headers = headersInput;
|
||
}
|
||
} catch (e) {
|
||
showToast('请求头格式错误,请输入有效的JSON', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const paramsInput = document.getElementById('apiParams').value.trim();
|
||
if (paramsInput) {
|
||
JSON.parse(paramsInput); // 验证JSON格式
|
||
params = paramsInput;
|
||
}
|
||
} catch (e) {
|
||
showToast('请求参数格式错误,请输入有效的JSON', 'warning');
|
||
return;
|
||
}
|
||
|
||
cardData.api_config = {
|
||
url: document.getElementById('apiUrl').value,
|
||
method: document.getElementById('apiMethod').value,
|
||
timeout: parseInt(document.getElementById('apiTimeout').value),
|
||
headers: headers,
|
||
params: params
|
||
};
|
||
break;
|
||
case 'text':
|
||
cardData.text_content = document.getElementById('textContent').value;
|
||
break;
|
||
case 'data':
|
||
cardData.data_content = document.getElementById('dataContent').value;
|
||
break;
|
||
}
|
||
|
||
const response = await fetch(`${apiBase}/cards`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(cardData)
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('卡券保存成功', 'success');
|
||
bootstrap.Modal.getInstance(document.getElementById('addCardModal')).hide();
|
||
// 清空表单
|
||
clearAddCardForm();
|
||
loadCards();
|
||
} else {
|
||
let errorMessage = '保存失败';
|
||
try {
|
||
const errorData = await response.json();
|
||
errorMessage = errorData.error || errorData.detail || errorMessage;
|
||
} catch (e) {
|
||
// 如果不是JSON格式,尝试获取文本
|
||
try {
|
||
const errorText = await response.text();
|
||
errorMessage = errorText || errorMessage;
|
||
} catch (e2) {
|
||
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||
}
|
||
}
|
||
showToast(`保存失败: ${errorMessage}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('保存卡券失败:', error);
|
||
showToast(`网络错误: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
// ==================== 自动发货功能 ====================
|
||
|
||
// 加载发货规则列表
|
||
async function loadDeliveryRules() {
|
||
try {
|
||
const response = await fetch(`${apiBase}/delivery-rules`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const rules = await response.json();
|
||
renderDeliveryRulesList(rules);
|
||
updateDeliveryStats(rules);
|
||
|
||
// 同时加载卡券列表用于下拉选择
|
||
loadCardsForSelect();
|
||
} else {
|
||
showToast('加载发货规则失败', 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('加载发货规则失败:', error);
|
||
showToast('加载发货规则失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 渲染发货规则列表
|
||
function renderDeliveryRulesList(rules) {
|
||
const tbody = document.getElementById('deliveryRulesTableBody');
|
||
|
||
if (rules.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="7" class="text-center py-4 text-muted">
|
||
<i class="bi bi-truck fs-1 d-block mb-3"></i>
|
||
<h5>暂无发货规则</h5>
|
||
<p class="mb-0">点击"添加规则"开始配置自动发货规则</p>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = '';
|
||
|
||
rules.forEach(rule => {
|
||
const tr = document.createElement('tr');
|
||
|
||
// 状态标签
|
||
const statusBadge = rule.enabled ?
|
||
'<span class="badge bg-success">启用</span>' :
|
||
'<span class="badge bg-secondary">禁用</span>';
|
||
|
||
// 卡券类型标签
|
||
let cardTypeBadge = '<span class="badge bg-secondary">未知</span>';
|
||
if (rule.card_type) {
|
||
switch(rule.card_type) {
|
||
case 'api':
|
||
cardTypeBadge = '<span class="badge bg-info">API接口</span>';
|
||
break;
|
||
case 'text':
|
||
cardTypeBadge = '<span class="badge bg-success">固定文字</span>';
|
||
break;
|
||
case 'data':
|
||
cardTypeBadge = '<span class="badge bg-warning">批量数据</span>';
|
||
break;
|
||
}
|
||
}
|
||
|
||
tr.innerHTML = `
|
||
<td>
|
||
<div class="fw-bold">${rule.keyword}</div>
|
||
${rule.description ? `<small class="text-muted">${rule.description}</small>` : ''}
|
||
</td>
|
||
<td>
|
||
<div>
|
||
<span class="badge bg-primary">${rule.card_name || '未知卡券'}</span>
|
||
${rule.is_multi_spec && rule.spec_name && rule.spec_value ?
|
||
`<br><small class="text-muted mt-1 d-block"><i class="bi bi-tags"></i> ${rule.spec_name}: ${rule.spec_value}</small>` :
|
||
''}
|
||
</div>
|
||
</td>
|
||
<td>${cardTypeBadge}</td>
|
||
<td>
|
||
<span class="badge bg-info">${rule.delivery_count || 1}</span>
|
||
</td>
|
||
<td>${statusBadge}</td>
|
||
<td>
|
||
<span class="badge bg-warning">${rule.delivery_times || 0}</span>
|
||
</td>
|
||
<td>
|
||
<div class="btn-group" role="group">
|
||
<button class="btn btn-sm btn-outline-primary" onclick="editDeliveryRule(${rule.id})" title="编辑">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-info" onclick="testDeliveryRule(${rule.id})" title="测试">
|
||
<i class="bi bi-play"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="deleteDeliveryRule(${rule.id})" title="删除">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
`;
|
||
|
||
tbody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
// 更新发货统计
|
||
function updateDeliveryStats(rules) {
|
||
const totalRules = rules.length;
|
||
const activeRules = rules.filter(rule => rule.enabled).length;
|
||
const todayDeliveries = 0; // 需要从后端获取今日发货统计
|
||
const totalDeliveries = rules.reduce((sum, rule) => sum + (rule.delivery_times || 0), 0);
|
||
|
||
document.getElementById('totalRules').textContent = totalRules;
|
||
document.getElementById('activeRules').textContent = activeRules;
|
||
document.getElementById('todayDeliveries').textContent = todayDeliveries;
|
||
document.getElementById('totalDeliveries').textContent = totalDeliveries;
|
||
}
|
||
|
||
// 显示添加发货规则模态框
|
||
function showAddDeliveryRuleModal() {
|
||
document.getElementById('addDeliveryRuleForm').reset();
|
||
loadCardsForSelect(); // 加载卡券选项
|
||
const modal = new bootstrap.Modal(document.getElementById('addDeliveryRuleModal'));
|
||
modal.show();
|
||
}
|
||
|
||
// 加载卡券列表用于下拉选择
|
||
async function loadCardsForSelect() {
|
||
try {
|
||
const response = await fetch(`${apiBase}/cards`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const cards = await response.json();
|
||
const select = document.getElementById('selectedCard');
|
||
|
||
// 清空现有选项
|
||
select.innerHTML = '<option value="">请选择卡券</option>';
|
||
|
||
cards.forEach(card => {
|
||
if (card.enabled) { // 只显示启用的卡券
|
||
const option = document.createElement('option');
|
||
option.value = card.id;
|
||
|
||
// 构建显示文本
|
||
let displayText = card.name;
|
||
|
||
// 添加类型信息
|
||
const typeText = card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据';
|
||
displayText += ` (${typeText})`;
|
||
|
||
// 添加规格信息
|
||
if (card.is_multi_spec && card.spec_name && card.spec_value) {
|
||
displayText += ` [${card.spec_name}:${card.spec_value}]`;
|
||
}
|
||
|
||
option.textContent = displayText;
|
||
select.appendChild(option);
|
||
}
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('加载卡券选项失败:', error);
|
||
}
|
||
}
|
||
|
||
// 保存发货规则
|
||
async function saveDeliveryRule() {
|
||
try {
|
||
const keyword = document.getElementById('productKeyword').value;
|
||
const cardId = document.getElementById('selectedCard').value;
|
||
const deliveryCount = document.getElementById('deliveryCount').value;
|
||
const enabled = document.getElementById('ruleEnabled').checked;
|
||
const description = document.getElementById('ruleDescription').value;
|
||
|
||
if (!keyword || !cardId) {
|
||
showToast('请填写必填字段', 'warning');
|
||
return;
|
||
}
|
||
|
||
const ruleData = {
|
||
keyword: keyword,
|
||
card_id: parseInt(cardId),
|
||
delivery_count: parseInt(deliveryCount),
|
||
enabled: enabled,
|
||
description: description
|
||
};
|
||
|
||
const response = await fetch(`${apiBase}/delivery-rules`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(ruleData)
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('发货规则保存成功', 'success');
|
||
bootstrap.Modal.getInstance(document.getElementById('addDeliveryRuleModal')).hide();
|
||
loadDeliveryRules();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`保存失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('保存发货规则失败:', error);
|
||
showToast('保存发货规则失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 编辑卡券
|
||
async function editCard(cardId) {
|
||
try {
|
||
// 获取卡券详情
|
||
const response = await fetch(`${apiBase}/cards/${cardId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const card = await response.json();
|
||
|
||
// 填充编辑表单
|
||
document.getElementById('editCardId').value = card.id;
|
||
document.getElementById('editCardName').value = card.name;
|
||
document.getElementById('editCardType').value = card.type;
|
||
document.getElementById('editCardDescription').value = card.description || '';
|
||
document.getElementById('editCardDelaySeconds').value = card.delay_seconds || 0;
|
||
document.getElementById('editCardEnabled').checked = card.enabled;
|
||
|
||
// 填充多规格字段
|
||
const isMultiSpec = card.is_multi_spec || false;
|
||
document.getElementById('editIsMultiSpec').checked = isMultiSpec;
|
||
document.getElementById('editSpecName').value = card.spec_name || '';
|
||
document.getElementById('editSpecValue').value = card.spec_value || '';
|
||
|
||
// 添加调试日志
|
||
console.log('编辑卡券 - 多规格状态:', isMultiSpec);
|
||
console.log('编辑卡券 - 规格名称:', card.spec_name);
|
||
console.log('编辑卡券 - 规格值:', card.spec_value);
|
||
|
||
// 根据类型填充特定字段
|
||
if (card.type === 'api' && card.api_config) {
|
||
document.getElementById('editApiUrl').value = card.api_config.url || '';
|
||
document.getElementById('editApiMethod').value = card.api_config.method || 'GET';
|
||
document.getElementById('editApiTimeout').value = card.api_config.timeout || 10;
|
||
document.getElementById('editApiHeaders').value = card.api_config.headers || '{}';
|
||
document.getElementById('editApiParams').value = card.api_config.params || '{}';
|
||
} else if (card.type === 'text') {
|
||
document.getElementById('editTextContent').value = card.text_content || '';
|
||
} else if (card.type === 'data') {
|
||
document.getElementById('editDataContent').value = card.data_content || '';
|
||
}
|
||
|
||
// 显示对应的字段
|
||
toggleEditCardTypeFields();
|
||
|
||
// 使用延迟调用确保DOM更新完成后再显示多规格字段
|
||
setTimeout(() => {
|
||
console.log('延迟调用 toggleEditMultiSpecFields');
|
||
toggleEditMultiSpecFields();
|
||
|
||
// 验证多规格字段是否正确显示
|
||
const multiSpecElement = document.getElementById('editMultiSpecFields');
|
||
const isChecked = document.getElementById('editIsMultiSpec').checked;
|
||
console.log('多规格元素存在:', !!multiSpecElement);
|
||
console.log('多规格开关状态:', isChecked);
|
||
console.log('多规格字段显示状态:', multiSpecElement ? multiSpecElement.style.display : 'element not found');
|
||
}, 100);
|
||
|
||
// 显示模态框
|
||
const modal = new bootstrap.Modal(document.getElementById('editCardModal'));
|
||
modal.show();
|
||
} else {
|
||
showToast('获取卡券详情失败', 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取卡券详情失败:', error);
|
||
showToast('获取卡券详情失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 切换编辑卡券类型字段显示
|
||
function toggleEditCardTypeFields() {
|
||
const cardType = document.getElementById('editCardType').value;
|
||
|
||
document.getElementById('editApiFields').style.display = cardType === 'api' ? 'block' : 'none';
|
||
document.getElementById('editTextFields').style.display = cardType === 'text' ? 'block' : 'none';
|
||
document.getElementById('editDataFields').style.display = cardType === 'data' ? 'block' : 'none';
|
||
}
|
||
|
||
// 更新卡券
|
||
async function updateCard() {
|
||
try {
|
||
const cardId = document.getElementById('editCardId').value;
|
||
const cardType = document.getElementById('editCardType').value;
|
||
const cardName = document.getElementById('editCardName').value;
|
||
|
||
if (!cardType || !cardName) {
|
||
showToast('请填写必填字段', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 检查多规格设置
|
||
const isMultiSpec = document.getElementById('editIsMultiSpec').checked;
|
||
const specName = document.getElementById('editSpecName').value;
|
||
const specValue = document.getElementById('editSpecValue').value;
|
||
|
||
// 验证多规格字段
|
||
if (isMultiSpec && (!specName || !specValue)) {
|
||
showToast('多规格卡券必须填写规格名称和规格值', 'warning');
|
||
return;
|
||
}
|
||
|
||
const cardData = {
|
||
name: cardName,
|
||
type: cardType,
|
||
description: document.getElementById('editCardDescription').value,
|
||
delay_seconds: parseInt(document.getElementById('editCardDelaySeconds').value) || 0,
|
||
enabled: document.getElementById('editCardEnabled').checked,
|
||
is_multi_spec: isMultiSpec,
|
||
spec_name: isMultiSpec ? specName : null,
|
||
spec_value: isMultiSpec ? specValue : null
|
||
};
|
||
|
||
// 根据类型添加特定配置
|
||
switch(cardType) {
|
||
case 'api':
|
||
// 验证和解析JSON字段
|
||
let headers = '{}';
|
||
let params = '{}';
|
||
|
||
try {
|
||
const headersInput = document.getElementById('editApiHeaders').value.trim();
|
||
if (headersInput) {
|
||
JSON.parse(headersInput);
|
||
headers = headersInput;
|
||
}
|
||
} catch (e) {
|
||
showToast('请求头格式错误,请输入有效的JSON', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const paramsInput = document.getElementById('editApiParams').value.trim();
|
||
if (paramsInput) {
|
||
JSON.parse(paramsInput);
|
||
params = paramsInput;
|
||
}
|
||
} catch (e) {
|
||
showToast('请求参数格式错误,请输入有效的JSON', 'warning');
|
||
return;
|
||
}
|
||
|
||
cardData.api_config = {
|
||
url: document.getElementById('editApiUrl').value,
|
||
method: document.getElementById('editApiMethod').value,
|
||
timeout: parseInt(document.getElementById('editApiTimeout').value),
|
||
headers: headers,
|
||
params: params
|
||
};
|
||
break;
|
||
case 'text':
|
||
cardData.text_content = document.getElementById('editTextContent').value;
|
||
break;
|
||
case 'data':
|
||
cardData.data_content = document.getElementById('editDataContent').value;
|
||
break;
|
||
}
|
||
|
||
const response = await fetch(`${apiBase}/cards/${cardId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(cardData)
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('卡券更新成功', 'success');
|
||
bootstrap.Modal.getInstance(document.getElementById('editCardModal')).hide();
|
||
loadCards();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`更新失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('更新卡券失败:', error);
|
||
showToast('更新卡券失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 测试卡券(占位函数)
|
||
function testCard(cardId) {
|
||
showToast('测试功能开发中...', 'info');
|
||
}
|
||
|
||
// 删除卡券
|
||
async function deleteCard(cardId) {
|
||
if (confirm('确定要删除这个卡券吗?删除后无法恢复!')) {
|
||
try {
|
||
const response = await fetch(`${apiBase}/cards/${cardId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('卡券删除成功', 'success');
|
||
loadCards();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`删除失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('删除卡券失败:', error);
|
||
showToast('删除卡券失败', 'danger');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 编辑发货规则
|
||
async function editDeliveryRule(ruleId) {
|
||
try {
|
||
// 获取发货规则详情
|
||
const response = await fetch(`${apiBase}/delivery-rules/${ruleId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const rule = await response.json();
|
||
|
||
// 填充编辑表单
|
||
document.getElementById('editRuleId').value = rule.id;
|
||
document.getElementById('editProductKeyword').value = rule.keyword;
|
||
document.getElementById('editDeliveryCount').value = rule.delivery_count || 1;
|
||
document.getElementById('editRuleEnabled').checked = rule.enabled;
|
||
document.getElementById('editRuleDescription').value = rule.description || '';
|
||
|
||
// 加载卡券选项并设置当前选中的卡券
|
||
await loadCardsForEditSelect();
|
||
document.getElementById('editSelectedCard').value = rule.card_id;
|
||
|
||
// 显示模态框
|
||
const modal = new bootstrap.Modal(document.getElementById('editDeliveryRuleModal'));
|
||
modal.show();
|
||
} else {
|
||
showToast('获取发货规则详情失败', 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取发货规则详情失败:', error);
|
||
showToast('获取发货规则详情失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 加载卡券列表用于编辑时的下拉选择
|
||
async function loadCardsForEditSelect() {
|
||
try {
|
||
const response = await fetch(`${apiBase}/cards`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const cards = await response.json();
|
||
const select = document.getElementById('editSelectedCard');
|
||
|
||
// 清空现有选项
|
||
select.innerHTML = '<option value="">请选择卡券</option>';
|
||
|
||
cards.forEach(card => {
|
||
if (card.enabled) { // 只显示启用的卡券
|
||
const option = document.createElement('option');
|
||
option.value = card.id;
|
||
|
||
// 构建显示文本
|
||
let displayText = card.name;
|
||
|
||
// 添加类型信息
|
||
const typeText = card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据';
|
||
displayText += ` (${typeText})`;
|
||
|
||
// 添加规格信息
|
||
if (card.is_multi_spec && card.spec_name && card.spec_value) {
|
||
displayText += ` [${card.spec_name}:${card.spec_value}]`;
|
||
}
|
||
|
||
option.textContent = displayText;
|
||
select.appendChild(option);
|
||
}
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('加载卡券选项失败:', error);
|
||
}
|
||
}
|
||
|
||
// 更新发货规则
|
||
async function updateDeliveryRule() {
|
||
try {
|
||
const ruleId = document.getElementById('editRuleId').value;
|
||
const keyword = document.getElementById('editProductKeyword').value;
|
||
const cardId = document.getElementById('editSelectedCard').value;
|
||
const deliveryCount = document.getElementById('editDeliveryCount').value;
|
||
const enabled = document.getElementById('editRuleEnabled').checked;
|
||
const description = document.getElementById('editRuleDescription').value;
|
||
|
||
if (!keyword || !cardId) {
|
||
showToast('请填写必填字段', 'warning');
|
||
return;
|
||
}
|
||
|
||
const ruleData = {
|
||
keyword: keyword,
|
||
card_id: parseInt(cardId),
|
||
delivery_count: parseInt(deliveryCount),
|
||
enabled: enabled,
|
||
description: description
|
||
};
|
||
|
||
const response = await fetch(`${apiBase}/delivery-rules/${ruleId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(ruleData)
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('发货规则更新成功', 'success');
|
||
bootstrap.Modal.getInstance(document.getElementById('editDeliveryRuleModal')).hide();
|
||
loadDeliveryRules();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`更新失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('更新发货规则失败:', error);
|
||
showToast('更新发货规则失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 测试发货规则(占位函数)
|
||
function testDeliveryRule(ruleId) {
|
||
showToast('测试功能开发中...', 'info');
|
||
}
|
||
|
||
// 删除发货规则
|
||
async function deleteDeliveryRule(ruleId) {
|
||
if (confirm('确定要删除这个发货规则吗?删除后无法恢复!')) {
|
||
try {
|
||
const response = await fetch(`${apiBase}/delivery-rules/${ruleId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('发货规则删除成功', 'success');
|
||
loadDeliveryRules();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`删除失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('删除发货规则失败:', error);
|
||
showToast('删除发货规则失败', 'danger');
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
</script>
|
||
|
||
<!-- 默认回复管理模态框 -->
|
||
<div class="modal fade" id="defaultReplyModal" tabindex="-1" aria-labelledby="defaultReplyModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-xl">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="defaultReplyModalLabel">
|
||
<i class="bi bi-chat-text me-2"></i>默认回复管理
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-info">
|
||
<i class="bi bi-info-circle me-2"></i>
|
||
<strong>功能说明:</strong>当没有匹配到关键词时,系统会使用默认回复。支持以下变量:
|
||
<code>{send_user_name}</code> 用户昵称、
|
||
<code>{send_user_id}</code> 用户ID、
|
||
<code>{send_message}</code> 用户消息
|
||
</div>
|
||
|
||
<div class="table-responsive">
|
||
<table class="table table-hover" id="defaultReplyTable">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 15%">账号ID</th>
|
||
<th style="width: 15%">状态</th>
|
||
<th style="width: 50%">默认回复内容</th>
|
||
<th style="width: 20%">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="defaultReplyTableBody">
|
||
<!-- 动态生成 -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div class="text-muted me-auto">
|
||
<small><i class="bi bi-info-circle me-1"></i>请点击每个账号的"编辑"按钮进行单独设置</small>
|
||
</div>
|
||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">
|
||
<i class="bi bi-check-circle me-1"></i>完成
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑默认回复模态框 -->
|
||
<div class="modal fade" id="editDefaultReplyModal" tabindex="-1" aria-labelledby="editDefaultReplyModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="editDefaultReplyModalLabel">
|
||
<i class="bi bi-pencil-square me-2"></i>编辑默认回复
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="editDefaultReplyForm">
|
||
<input type="hidden" id="editAccountId" />
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">账号ID</label>
|
||
<input type="text" class="form-control" id="editAccountIdDisplay" readonly />
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" id="editDefaultReplyEnabled" onchange="toggleReplyContentVisibility()" />
|
||
<label class="form-check-label" for="editDefaultReplyEnabled">
|
||
启用默认回复
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3" id="editReplyContentGroup">
|
||
<label for="editReplyContent" class="form-label">默认回复内容</label>
|
||
<textarea class="form-control" id="editReplyContent" rows="4"
|
||
placeholder="请输入默认回复内容,支持变量:{send_user_name} {send_user_id} {send_message}"></textarea>
|
||
<div class="form-text">
|
||
<strong>可用变量:</strong>
|
||
<span class="badge bg-secondary me-1">{send_user_name}</span>
|
||
<span class="badge bg-secondary me-1">{send_user_id}</span>
|
||
<span class="badge bg-secondary me-1">{send_message}</span>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveDefaultReply()">
|
||
<i class="bi bi-check-circle me-1"></i>保存
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑商品详情模态框 -->
|
||
<div class="modal fade" id="editItemModal" tabindex="-1">
|
||
<div class="modal-dialog modal-xl">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-pencil me-2"></i>编辑商品详情
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="editItemForm">
|
||
<input type="hidden" id="editItemCookieId">
|
||
<input type="hidden" id="editItemId">
|
||
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label">账号ID</label>
|
||
<input type="text" class="form-control" id="editItemCookieIdDisplay" readonly>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">商品ID</label>
|
||
<input type="text" class="form-control" id="editItemIdDisplay" readonly>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="editItemDetail" class="form-label">商品详情 <span class="text-danger">*</span></label>
|
||
<textarea class="form-control" id="editItemDetail" rows="20"
|
||
placeholder="请输入商品详情内容..."></textarea>
|
||
<div class="form-text">
|
||
<i class="bi bi-info-circle me-1"></i>
|
||
请输入商品详情内容,这些内容将用于自动发货时的关键词匹配。
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveItemDetail()">
|
||
<i class="bi bi-check-circle me-1"></i>保存
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ==================== 系统设置功能 ====================
|
||
|
||
// 主题颜色映射
|
||
const themeColors = {
|
||
blue: '#4f46e5',
|
||
green: '#10b981',
|
||
purple: '#8b5cf6',
|
||
red: '#ef4444',
|
||
orange: '#f59e0b'
|
||
};
|
||
|
||
// 加载用户设置
|
||
async function loadUserSettings() {
|
||
try {
|
||
const response = await fetch(`${apiBase}/user-settings`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const settings = await response.json();
|
||
|
||
// 设置主题颜色
|
||
if (settings.theme_color && settings.theme_color.value) {
|
||
document.getElementById('themeColor').value = settings.theme_color.value;
|
||
applyThemeColor(settings.theme_color.value);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载用户设置失败:', error);
|
||
}
|
||
}
|
||
|
||
// 应用主题颜色
|
||
function applyThemeColor(colorName) {
|
||
const color = themeColors[colorName];
|
||
if (color) {
|
||
document.documentElement.style.setProperty('--primary-color', color);
|
||
|
||
// 计算hover颜色(稍微深一点)
|
||
const hoverColor = adjustBrightness(color, -20);
|
||
document.documentElement.style.setProperty('--primary-hover', hoverColor);
|
||
|
||
// 计算浅色版本(用于某些UI元素)
|
||
const lightColor = adjustBrightness(color, 10);
|
||
document.documentElement.style.setProperty('--primary-light', lightColor);
|
||
}
|
||
}
|
||
|
||
// 调整颜色亮度
|
||
function adjustBrightness(hex, percent) {
|
||
const num = parseInt(hex.replace("#", ""), 16);
|
||
const amt = Math.round(2.55 * percent);
|
||
const R = (num >> 16) + amt;
|
||
const G = (num >> 8 & 0x00FF) + amt;
|
||
const B = (num & 0x0000FF) + amt;
|
||
return "#" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
|
||
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
|
||
(B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1);
|
||
}
|
||
|
||
// 主题表单提交处理
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const themeForm = document.getElementById('themeForm');
|
||
if (themeForm) {
|
||
themeForm.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
const selectedColor = document.getElementById('themeColor').value;
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/user-settings/theme_color`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
value: selectedColor,
|
||
description: '主题颜色'
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
applyThemeColor(selectedColor);
|
||
showToast('主题颜色应用成功', 'success');
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`主题设置失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('主题设置失败:', error);
|
||
showToast('主题设置失败', 'danger');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 密码表单提交处理
|
||
const passwordForm = document.getElementById('passwordForm');
|
||
if (passwordForm) {
|
||
passwordForm.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
const currentPassword = document.getElementById('currentPassword').value;
|
||
const newPassword = document.getElementById('newPassword').value;
|
||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||
|
||
if (newPassword !== confirmPassword) {
|
||
showToast('新密码和确认密码不匹配', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (newPassword.length < 6) {
|
||
showToast('新密码长度至少6位', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/change-admin-password`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
current_password: currentPassword,
|
||
new_password: newPassword
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
showToast('密码更新成功,请重新登录', 'success');
|
||
passwordForm.reset();
|
||
// 3秒后跳转到登录页面
|
||
setTimeout(() => {
|
||
localStorage.removeItem('auth_token');
|
||
window.location.href = '/login.html';
|
||
}, 3000);
|
||
} else {
|
||
showToast(`密码更新失败: ${result.message}`, 'danger');
|
||
}
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`密码更新失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('密码更新失败:', error);
|
||
showToast('密码更新失败', 'danger');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 页面加载时加载用户设置
|
||
loadUserSettings();
|
||
});
|
||
|
||
// ==================== 备份管理功能 ====================
|
||
|
||
// 下载数据库备份
|
||
async function downloadDatabaseBackup() {
|
||
try {
|
||
showToast('正在准备数据库备份,请稍候...', 'info');
|
||
|
||
const response = await fetch(`${apiBase}/admin/backup/download`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
// 获取文件名
|
||
const contentDisposition = response.headers.get('content-disposition');
|
||
let filename = 'xianyu_backup.db';
|
||
if (contentDisposition) {
|
||
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
|
||
if (filenameMatch) {
|
||
filename = filenameMatch[1];
|
||
}
|
||
}
|
||
|
||
// 下载文件
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
window.URL.revokeObjectURL(url);
|
||
|
||
showToast('数据库备份下载成功', 'success');
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`下载失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('下载数据库备份失败:', error);
|
||
showToast('下载数据库备份失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 上传数据库备份
|
||
async function uploadDatabaseBackup() {
|
||
const fileInput = document.getElementById('databaseFile');
|
||
const file = fileInput.files[0];
|
||
|
||
if (!file) {
|
||
showToast('请选择数据库文件', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!file.name.endsWith('.db')) {
|
||
showToast('只支持.db格式的数据库文件', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 文件大小检查(限制100MB)
|
||
if (file.size > 100 * 1024 * 1024) {
|
||
showToast('数据库文件大小不能超过100MB', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!confirm('恢复数据库将完全替换当前所有数据,包括所有用户、Cookie、卡券等信息。\n\n此操作不可撤销!\n\n确定要继续吗?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showToast('正在上传并恢复数据库,请稍候...', 'info');
|
||
|
||
const formData = new FormData();
|
||
formData.append('backup_file', file);
|
||
|
||
const response = await fetch(`${apiBase}/admin/backup/upload`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
showToast(`数据库恢复成功!包含 ${result.user_count} 个用户`, 'success');
|
||
|
||
// 清空文件选择
|
||
fileInput.value = '';
|
||
|
||
// 提示用户刷新页面
|
||
setTimeout(() => {
|
||
if (confirm('数据库已恢复,建议刷新页面以加载新数据。是否立即刷新?')) {
|
||
window.location.reload();
|
||
}
|
||
}, 2000);
|
||
|
||
} else {
|
||
const error = await response.json();
|
||
showToast(`恢复失败: ${error.detail}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('上传数据库备份失败:', error);
|
||
showToast('上传数据库备份失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 导出备份(JSON格式,兼容旧版本)
|
||
async function exportBackup() {
|
||
try {
|
||
showToast('正在导出备份,请稍候...', 'info');
|
||
|
||
const response = await fetch(`${apiBase}/backup/export`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const backupData = await response.json();
|
||
|
||
// 生成文件名
|
||
const now = new Date();
|
||
const timestamp = now.getFullYear() +
|
||
String(now.getMonth() + 1).padStart(2, '0') +
|
||
String(now.getDate()).padStart(2, '0') + '_' +
|
||
String(now.getHours()).padStart(2, '0') +
|
||
String(now.getMinutes()).padStart(2, '0') +
|
||
String(now.getSeconds()).padStart(2, '0');
|
||
const filename = `xianyu_backup_${timestamp}.json`;
|
||
|
||
// 创建下载链接
|
||
const blob = new Blob([JSON.stringify(backupData, null, 2)], { type: 'application/json' });
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
window.URL.revokeObjectURL(url);
|
||
|
||
showToast('备份导出成功', 'success');
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`导出失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('导出备份失败:', error);
|
||
showToast('导出备份失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 导入备份
|
||
async function importBackup() {
|
||
const fileInput = document.getElementById('backupFile');
|
||
const file = fileInput.files[0];
|
||
|
||
if (!file) {
|
||
showToast('请选择备份文件', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!file.name.endsWith('.json')) {
|
||
showToast('只支持JSON格式的备份文件', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!confirm('导入备份将覆盖当前所有数据,确定要继续吗?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showToast('正在导入备份,请稍候...', 'info');
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const response = await fetch(`${apiBase}/backup/import`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('备份导入成功!正在刷新数据...', 'success');
|
||
|
||
// 清空文件选择
|
||
fileInput.value = '';
|
||
|
||
// 清除前端缓存
|
||
clearKeywordCache();
|
||
|
||
// 延迟一下再刷新数据,确保后端缓存已更新
|
||
setTimeout(async () => {
|
||
try {
|
||
// 如果当前在关键字管理页面,重新加载数据
|
||
if (currentCookieId) {
|
||
await loadAccountKeywords();
|
||
}
|
||
|
||
// 刷新仪表盘数据
|
||
if (document.getElementById('dashboard-section').classList.contains('active')) {
|
||
await loadDashboard();
|
||
}
|
||
|
||
// 刷新账号列表
|
||
if (document.getElementById('accounts-section').classList.contains('active')) {
|
||
await loadCookies();
|
||
}
|
||
|
||
showToast('数据刷新完成!', 'success');
|
||
} catch (error) {
|
||
console.error('刷新数据失败:', error);
|
||
showToast('备份导入成功,但数据刷新失败,请手动刷新页面', 'warning');
|
||
}
|
||
}, 1000);
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`导入失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('导入备份失败:', error);
|
||
showToast('导入备份失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 刷新系统缓存
|
||
async function reloadSystemCache() {
|
||
try {
|
||
showToast('正在刷新系统缓存...', 'info');
|
||
|
||
const response = await fetch(`${apiBase}/system/reload-cache`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
showToast('系统缓存刷新成功!关键字等数据已更新', 'success');
|
||
|
||
// 清除前端缓存
|
||
clearKeywordCache();
|
||
|
||
// 如果当前在关键字管理页面,重新加载数据
|
||
if (currentCookieId) {
|
||
setTimeout(() => {
|
||
loadAccountKeywords();
|
||
}, 500);
|
||
}
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`刷新缓存失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('刷新系统缓存失败:', error);
|
||
showToast('刷新系统缓存失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// ==================== 商品管理功能 ====================
|
||
|
||
// 切换商品多规格状态
|
||
async function toggleItemMultiSpec(cookieId, itemId, isMultiSpec) {
|
||
try {
|
||
const response = await fetch(`${apiBase}/items/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}/multi-spec`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify({
|
||
is_multi_spec: isMultiSpec
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast(`${isMultiSpec ? '开启' : '关闭'}多规格成功`, 'success');
|
||
// 刷新商品列表
|
||
await refreshItemsData();
|
||
} else {
|
||
const errorData = await response.json();
|
||
throw new Error(errorData.error || '操作失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('切换多规格状态失败:', error);
|
||
showToast(`切换多规格状态失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
// 加载商品列表
|
||
async function loadItems() {
|
||
try {
|
||
// 先加载Cookie列表用于筛选
|
||
await loadCookieFilter();
|
||
|
||
// 加载商品列表
|
||
await refreshItemsData();
|
||
} catch (error) {
|
||
console.error('加载商品列表失败:', error);
|
||
showToast('加载商品列表失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 只刷新商品数据,不重新加载筛选器
|
||
async function refreshItemsData() {
|
||
try {
|
||
const selectedCookie = document.getElementById('itemCookieFilter').value;
|
||
if (selectedCookie) {
|
||
await loadItemsByCookie();
|
||
} else {
|
||
await loadAllItems();
|
||
}
|
||
} catch (error) {
|
||
console.error('刷新商品数据失败:', error);
|
||
showToast('刷新商品数据失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 加载Cookie筛选选项
|
||
async function loadCookieFilter() {
|
||
try {
|
||
const response = await fetch(`${apiBase}/cookies/details`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const accounts = await response.json();
|
||
const select = document.getElementById('itemCookieFilter');
|
||
|
||
// 保存当前选择的值
|
||
const currentValue = select.value;
|
||
|
||
// 清空现有选项(保留"所有账号")
|
||
select.innerHTML = '<option value="">所有账号</option>';
|
||
|
||
if (accounts.length === 0) {
|
||
const option = document.createElement('option');
|
||
option.value = '';
|
||
option.textContent = '❌ 暂无账号';
|
||
option.disabled = true;
|
||
select.appendChild(option);
|
||
return;
|
||
}
|
||
|
||
// 分组显示:先显示启用的账号,再显示禁用的账号
|
||
const enabledAccounts = accounts.filter(account => {
|
||
const enabled = account.enabled === undefined ? true : account.enabled;
|
||
return enabled;
|
||
});
|
||
const disabledAccounts = accounts.filter(account => {
|
||
const enabled = account.enabled === undefined ? true : account.enabled;
|
||
return !enabled;
|
||
});
|
||
|
||
// 添加启用的账号
|
||
enabledAccounts.forEach(account => {
|
||
const option = document.createElement('option');
|
||
option.value = account.id;
|
||
option.textContent = `🟢 ${account.id}`;
|
||
select.appendChild(option);
|
||
});
|
||
|
||
// 添加禁用的账号
|
||
if (disabledAccounts.length > 0) {
|
||
// 添加分隔线
|
||
if (enabledAccounts.length > 0) {
|
||
const separator = document.createElement('option');
|
||
separator.value = '';
|
||
separator.textContent = '────────────────';
|
||
separator.disabled = true;
|
||
select.appendChild(separator);
|
||
}
|
||
|
||
disabledAccounts.forEach(account => {
|
||
const option = document.createElement('option');
|
||
option.value = account.id;
|
||
option.textContent = `🔴 ${account.id} (已禁用)`;
|
||
select.appendChild(option);
|
||
});
|
||
}
|
||
|
||
// 恢复之前选择的值
|
||
if (currentValue) {
|
||
select.value = currentValue;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载Cookie列表失败:', error);
|
||
showToast('加载账号列表失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 加载所有商品
|
||
async function loadAllItems() {
|
||
try {
|
||
const response = await fetch(`${apiBase}/items`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
displayItems(data.items);
|
||
} else {
|
||
throw new Error('获取商品列表失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('加载商品列表失败:', error);
|
||
showToast('加载商品列表失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 按Cookie加载商品
|
||
async function loadItemsByCookie() {
|
||
const cookieId = document.getElementById('itemCookieFilter').value;
|
||
|
||
if (!cookieId) {
|
||
await loadAllItems();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/items/cookie/${encodeURIComponent(cookieId)}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
displayItems(data.items);
|
||
} else {
|
||
throw new Error('获取商品列表失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('加载商品列表失败:', error);
|
||
showToast('加载商品列表失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 显示商品列表
|
||
function displayItems(items) {
|
||
const tbody = document.getElementById('itemsTableBody');
|
||
|
||
if (!items || items.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无商品数据</td></tr>';
|
||
// 重置选择状态
|
||
const selectAllCheckbox = document.getElementById('selectAllItems');
|
||
if (selectAllCheckbox) {
|
||
selectAllCheckbox.checked = false;
|
||
selectAllCheckbox.indeterminate = false;
|
||
}
|
||
updateBatchDeleteButton();
|
||
return;
|
||
}
|
||
|
||
const itemsHtml = items.map(item => {
|
||
// 处理商品标题显示
|
||
let itemTitleDisplay = item.item_title || '未设置';
|
||
if (itemTitleDisplay.length > 30) {
|
||
itemTitleDisplay = itemTitleDisplay.substring(0, 30) + '...';
|
||
}
|
||
|
||
// 处理商品详情显示
|
||
let itemDetailDisplay = '未设置';
|
||
if (item.item_detail) {
|
||
try {
|
||
// 尝试解析JSON并提取有用信息
|
||
const detail = JSON.parse(item.item_detail);
|
||
if (detail.content) {
|
||
itemDetailDisplay = detail.content.substring(0, 50) + (detail.content.length > 50 ? '...' : '');
|
||
} else {
|
||
// 如果是纯文本或其他格式,直接显示前50个字符
|
||
itemDetailDisplay = item.item_detail.substring(0, 50) + (item.item_detail.length > 50 ? '...' : '');
|
||
}
|
||
} catch (e) {
|
||
// 如果不是JSON格式,直接显示前50个字符
|
||
itemDetailDisplay = item.item_detail.substring(0, 50) + (item.item_detail.length > 50 ? '...' : '');
|
||
}
|
||
}
|
||
|
||
// 多规格状态显示
|
||
const isMultiSpec = item.is_multi_spec;
|
||
const multiSpecDisplay = isMultiSpec ?
|
||
'<span class="badge bg-success">多规格</span>' :
|
||
'<span class="badge bg-secondary">普通</span>';
|
||
|
||
return `
|
||
<tr>
|
||
<td>
|
||
<input type="checkbox" name="itemCheckbox"
|
||
data-cookie-id="${escapeHtml(item.cookie_id)}"
|
||
data-item-id="${escapeHtml(item.item_id)}"
|
||
onchange="updateSelectAllState()">
|
||
</td>
|
||
<td>${escapeHtml(item.cookie_id)}</td>
|
||
<td>${escapeHtml(item.item_id)}</td>
|
||
<td title="${escapeHtml(item.item_title || '未设置')}">${escapeHtml(itemTitleDisplay)}</td>
|
||
<td title="${escapeHtml(item.item_detail || '未设置')}">${escapeHtml(itemDetailDisplay)}</td>
|
||
<td>${multiSpecDisplay}</td>
|
||
<td>${formatDateTime(item.updated_at)}</td>
|
||
<td>
|
||
<div class="btn-group" role="group">
|
||
<button class="btn btn-sm btn-outline-primary" onclick="editItem('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}')" title="编辑详情">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}', '${escapeHtml(item.item_title || item.item_id)}')" title="删除">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
<button class="btn btn-sm ${isMultiSpec ? 'btn-warning' : 'btn-success'}" onclick="toggleItemMultiSpec('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}', ${!isMultiSpec})" title="${isMultiSpec ? '关闭多规格' : '开启多规格'}">
|
||
<i class="bi ${isMultiSpec ? 'bi-toggle-on' : 'bi-toggle-off'}"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
|
||
// 更新表格内容
|
||
tbody.innerHTML = itemsHtml;
|
||
|
||
// 重置选择状态
|
||
const selectAllCheckbox = document.getElementById('selectAllItems');
|
||
if (selectAllCheckbox) {
|
||
selectAllCheckbox.checked = false;
|
||
selectAllCheckbox.indeterminate = false;
|
||
}
|
||
updateBatchDeleteButton();
|
||
}
|
||
|
||
// 刷新商品列表
|
||
async function refreshItems() {
|
||
await refreshItemsData();
|
||
showToast('商品列表已刷新', 'success');
|
||
}
|
||
|
||
// 获取商品信息
|
||
async function getAllItemsFromAccount() {
|
||
const cookieSelect = document.getElementById('itemCookieFilter');
|
||
const selectedCookieId = cookieSelect.value;
|
||
const pageNumber = parseInt(document.getElementById('pageNumber').value) || 1;
|
||
|
||
if (!selectedCookieId) {
|
||
showToast('请先选择一个账号', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (pageNumber < 1) {
|
||
showToast('页码必须大于0', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 显示加载状态
|
||
const button = event.target;
|
||
const originalText = button.innerHTML;
|
||
button.innerHTML = '<i class="bi bi-hourglass-split me-1"></i>获取中...';
|
||
button.disabled = true;
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/items/get-by-page`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify({
|
||
cookie_id: selectedCookieId,
|
||
page_number: pageNumber,
|
||
page_size: 20
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showToast(`成功获取第${pageNumber}页 ${data.current_count} 个商品,请查看控制台日志`, 'success');
|
||
// 刷新商品列表(保持筛选器选择)
|
||
await refreshItemsData();
|
||
} else {
|
||
showToast(data.message || '获取商品信息失败', 'danger');
|
||
}
|
||
} else {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取商品信息失败:', error);
|
||
showToast('获取商品信息失败', 'danger');
|
||
} finally {
|
||
// 恢复按钮状态
|
||
button.innerHTML = originalText;
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
|
||
// 获取所有页商品信息
|
||
async function getAllItemsFromAccountAll() {
|
||
const cookieSelect = document.getElementById('itemCookieFilter');
|
||
const selectedCookieId = cookieSelect.value;
|
||
|
||
if (!selectedCookieId) {
|
||
showToast('请先选择一个账号', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 显示加载状态
|
||
const button = event.target;
|
||
const originalText = button.innerHTML;
|
||
button.innerHTML = '<i class="bi bi-hourglass-split me-1"></i>获取中...';
|
||
button.disabled = true;
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/items/get-all-from-account`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify({
|
||
cookie_id: selectedCookieId
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
const message = data.total_pages ?
|
||
`成功获取 ${data.total_count} 个商品(共${data.total_pages}页),请查看控制台日志` :
|
||
`成功获取商品信息,请查看控制台日志`;
|
||
showToast(message, 'success');
|
||
// 刷新商品列表(保持筛选器选择)
|
||
await refreshItemsData();
|
||
} else {
|
||
showToast(data.message || '获取商品信息失败', 'danger');
|
||
}
|
||
} else {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取商品信息失败:', error);
|
||
showToast('获取商品信息失败', 'danger');
|
||
} finally {
|
||
// 恢复按钮状态
|
||
button.innerHTML = originalText;
|
||
button.disabled = false;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// 编辑商品详情
|
||
async function editItem(cookieId, itemId) {
|
||
try {
|
||
const response = await fetch(`${apiBase}/items/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
const item = data.item;
|
||
|
||
// 填充表单
|
||
document.getElementById('editItemCookieId').value = item.cookie_id;
|
||
document.getElementById('editItemId').value = item.item_id;
|
||
document.getElementById('editItemCookieIdDisplay').value = item.cookie_id;
|
||
document.getElementById('editItemIdDisplay').value = item.item_id;
|
||
document.getElementById('editItemDetail').value = item.item_detail || '';
|
||
|
||
// 显示模态框
|
||
const modal = new bootstrap.Modal(document.getElementById('editItemModal'));
|
||
modal.show();
|
||
} else {
|
||
throw new Error('获取商品详情失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取商品详情失败:', error);
|
||
showToast('获取商品详情失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 保存商品详情
|
||
async function saveItemDetail() {
|
||
const cookieId = document.getElementById('editItemCookieId').value;
|
||
const itemId = document.getElementById('editItemId').value;
|
||
const itemDetail = document.getElementById('editItemDetail').value.trim();
|
||
|
||
if (!itemDetail) {
|
||
showToast('请输入商品详情', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/items/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify({
|
||
item_detail: itemDetail
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('商品详情更新成功', 'success');
|
||
|
||
// 关闭模态框
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('editItemModal'));
|
||
modal.hide();
|
||
|
||
// 刷新列表(保持筛选器选择)
|
||
await refreshItemsData();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`更新失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('更新商品详情失败:', error);
|
||
showToast('更新商品详情失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 删除商品信息
|
||
async function deleteItem(cookieId, itemId, itemTitle) {
|
||
try {
|
||
// 确认删除
|
||
const confirmed = confirm(`确定要删除商品信息吗?\n\n商品ID: ${itemId}\n商品标题: ${itemTitle || '未设置'}\n\n此操作不可撤销!`);
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
|
||
const response = await fetch(`${apiBase}/items/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast('商品信息删除成功', 'success');
|
||
// 刷新列表(保持筛选器选择)
|
||
await refreshItemsData();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`删除失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('删除商品信息失败:', error);
|
||
showToast('删除商品信息失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 批量删除商品信息
|
||
async function batchDeleteItems() {
|
||
try {
|
||
// 获取所有选中的复选框
|
||
const checkboxes = document.querySelectorAll('input[name="itemCheckbox"]:checked');
|
||
if (checkboxes.length === 0) {
|
||
showToast('请选择要删除的商品', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 确认删除
|
||
const confirmed = confirm(`确定要删除选中的 ${checkboxes.length} 个商品信息吗?\n\n此操作不可撤销!`);
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
|
||
// 构造删除列表
|
||
const itemsToDelete = Array.from(checkboxes).map(checkbox => {
|
||
const row = checkbox.closest('tr');
|
||
return {
|
||
cookie_id: checkbox.dataset.cookieId,
|
||
item_id: checkbox.dataset.itemId
|
||
};
|
||
});
|
||
|
||
const response = await fetch(`${apiBase}/items/batch`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify({ items: itemsToDelete })
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
showToast(`批量删除完成: 成功 ${result.success_count} 个,失败 ${result.failed_count} 个`, 'success');
|
||
// 刷新列表(保持筛选器选择)
|
||
await refreshItemsData();
|
||
} else {
|
||
const error = await response.text();
|
||
showToast(`批量删除失败: ${error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
console.error('批量删除商品信息失败:', error);
|
||
showToast('批量删除商品信息失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 全选/取消全选
|
||
function toggleSelectAll(selectAllCheckbox) {
|
||
const checkboxes = document.querySelectorAll('input[name="itemCheckbox"]');
|
||
checkboxes.forEach(checkbox => {
|
||
checkbox.checked = selectAllCheckbox.checked;
|
||
});
|
||
updateBatchDeleteButton();
|
||
}
|
||
|
||
// 更新全选状态
|
||
function updateSelectAllState() {
|
||
const checkboxes = document.querySelectorAll('input[name="itemCheckbox"]');
|
||
const checkedCheckboxes = document.querySelectorAll('input[name="itemCheckbox"]:checked');
|
||
const selectAllCheckbox = document.getElementById('selectAllItems');
|
||
|
||
if (checkboxes.length === 0) {
|
||
selectAllCheckbox.checked = false;
|
||
selectAllCheckbox.indeterminate = false;
|
||
} else if (checkedCheckboxes.length === checkboxes.length) {
|
||
selectAllCheckbox.checked = true;
|
||
selectAllCheckbox.indeterminate = false;
|
||
} else if (checkedCheckboxes.length > 0) {
|
||
selectAllCheckbox.checked = false;
|
||
selectAllCheckbox.indeterminate = true;
|
||
} else {
|
||
selectAllCheckbox.checked = false;
|
||
selectAllCheckbox.indeterminate = false;
|
||
}
|
||
|
||
updateBatchDeleteButton();
|
||
}
|
||
|
||
// 更新批量删除按钮状态
|
||
function updateBatchDeleteButton() {
|
||
const checkedCheckboxes = document.querySelectorAll('input[name="itemCheckbox"]:checked');
|
||
const batchDeleteBtn = document.getElementById('batchDeleteBtn');
|
||
|
||
if (checkedCheckboxes.length > 0) {
|
||
batchDeleteBtn.disabled = false;
|
||
batchDeleteBtn.innerHTML = `<i class="bi bi-trash"></i> 批量删除 (${checkedCheckboxes.length})`;
|
||
} else {
|
||
batchDeleteBtn.disabled = true;
|
||
batchDeleteBtn.innerHTML = '<i class="bi bi-trash"></i> 批量删除';
|
||
}
|
||
}
|
||
|
||
// 格式化日期时间
|
||
function formatDateTime(dateString) {
|
||
if (!dateString) return '未知';
|
||
const date = new Date(dateString);
|
||
return date.toLocaleString('zh-CN');
|
||
}
|
||
|
||
// HTML转义函数
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// ==================== 日志管理功能 ====================
|
||
|
||
window.autoRefreshInterval = null;
|
||
window.allLogs = [];
|
||
window.filteredLogs = [];
|
||
|
||
// 刷新日志
|
||
async function refreshLogs() {
|
||
try {
|
||
const lines = document.getElementById('logLines').value;
|
||
|
||
const response = await fetch(`${apiBase}/logs?lines=${lines}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
window.allLogs = data.logs || [];
|
||
window.filteredLogs = window.allLogs; // 不再过滤,直接显示所有日志
|
||
displayLogs();
|
||
updateLogStats();
|
||
showToast('日志已刷新', 'success');
|
||
} else {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('刷新日志失败:', error);
|
||
showToast('刷新日志失败', 'danger');
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// 显示日志
|
||
function displayLogs() {
|
||
const container = document.getElementById('logContainer');
|
||
|
||
if (window.filteredLogs.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="text-center p-4 text-muted">
|
||
<i class="bi bi-file-text fs-1"></i>
|
||
<p class="mt-2">暂无日志数据</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
const logsHtml = window.filteredLogs.map(log => {
|
||
const timestamp = formatLogTimestamp(log.timestamp);
|
||
const levelClass = log.level || 'INFO';
|
||
|
||
return `
|
||
<div class="log-entry ${levelClass}">
|
||
<span class="log-timestamp">${timestamp}</span>
|
||
<span class="log-level">[${log.level}]</span>
|
||
<span class="log-source">${log.source}:</span>
|
||
<span class="log-message">${escapeHtml(log.message)}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
container.innerHTML = logsHtml;
|
||
|
||
// 滚动到底部
|
||
container.scrollTop = container.scrollHeight;
|
||
}
|
||
|
||
// 格式化日志时间戳
|
||
function formatLogTimestamp(timestamp) {
|
||
if (!timestamp) return '';
|
||
const date = new Date(timestamp);
|
||
return date.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
fractionalSecondDigits: 3
|
||
});
|
||
}
|
||
|
||
// 更新日志统计信息
|
||
function updateLogStats() {
|
||
document.getElementById('logCount').textContent = `${window.filteredLogs.length} 条日志`;
|
||
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('zh-CN');
|
||
}
|
||
|
||
// 清空日志显示
|
||
function clearLogsDisplay() {
|
||
window.allLogs = [];
|
||
window.filteredLogs = [];
|
||
document.getElementById('logContainer').innerHTML = `
|
||
<div class="text-center p-4 text-muted">
|
||
<i class="bi bi-file-text fs-1"></i>
|
||
<p class="mt-2">日志显示已清空</p>
|
||
</div>
|
||
`;
|
||
updateLogStats();
|
||
showToast('日志显示已清空', 'info');
|
||
}
|
||
|
||
// 切换自动刷新
|
||
function toggleAutoRefresh() {
|
||
const button = document.querySelector('#autoRefreshText');
|
||
const icon = button.previousElementSibling;
|
||
|
||
if (window.autoRefreshInterval) {
|
||
// 停止自动刷新
|
||
clearInterval(window.autoRefreshInterval);
|
||
window.autoRefreshInterval = null;
|
||
button.textContent = '开启自动刷新';
|
||
icon.className = 'bi bi-play-circle me-1';
|
||
showToast('自动刷新已停止', 'info');
|
||
} else {
|
||
// 开启自动刷新
|
||
window.autoRefreshInterval = setInterval(refreshLogs, 5000); // 每5秒刷新一次
|
||
button.textContent = '停止自动刷新';
|
||
icon.className = 'bi bi-pause-circle me-1';
|
||
showToast('自动刷新已开启(每5秒)', 'success');
|
||
|
||
// 立即刷新一次
|
||
refreshLogs();
|
||
}
|
||
}
|
||
|
||
// 清空服务器日志
|
||
async function clearLogsServer() {
|
||
if (!confirm('确定要清空服务器端的所有日志吗?此操作不可恢复!')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/logs/clear`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
window.allLogs = [];
|
||
window.filteredLogs = [];
|
||
displayLogs();
|
||
updateLogStats();
|
||
showToast('服务器日志已清空', 'success');
|
||
} else {
|
||
showToast(data.message || '清空失败', 'danger');
|
||
}
|
||
} else {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('清空服务器日志失败:', error);
|
||
showToast('清空服务器日志失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// 显示日志统计信息
|
||
async function showLogStats() {
|
||
try {
|
||
const response = await fetch(`${apiBase}/logs/stats`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
const stats = data.stats;
|
||
|
||
let statsHtml = `
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<h6>总体统计</h6>
|
||
<ul class="list-unstyled">
|
||
<li>总日志数: <strong>${stats.total_logs}</strong></li>
|
||
<li>最大容量: <strong>${stats.max_capacity}</strong></li>
|
||
<li>使用率: <strong>${((stats.total_logs / stats.max_capacity) * 100).toFixed(1)}%</strong></li>
|
||
</ul>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<h6>级别分布</h6>
|
||
<ul class="list-unstyled">
|
||
`;
|
||
|
||
for (const [level, count] of Object.entries(stats.level_counts || {})) {
|
||
const percentage = ((count / stats.total_logs) * 100).toFixed(1);
|
||
statsHtml += `<li>${level}: <strong>${count}</strong> (${percentage}%)</li>`;
|
||
}
|
||
|
||
statsHtml += `
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="row mt-3">
|
||
<div class="col-12">
|
||
<h6>来源分布</h6>
|
||
<div class="row">
|
||
`;
|
||
|
||
const sources = Object.entries(stats.source_counts || {});
|
||
sources.forEach(([source, count], index) => {
|
||
if (index % 2 === 0) statsHtml += '<div class="col-md-6"><ul class="list-unstyled">';
|
||
const percentage = ((count / stats.total_logs) * 100).toFixed(1);
|
||
statsHtml += `<li>${source}: <strong>${count}</strong> (${percentage}%)</li>`;
|
||
if (index % 2 === 1 || index === sources.length - 1) statsHtml += '</ul></div>';
|
||
});
|
||
|
||
statsHtml += `
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 显示模态框
|
||
const modalHtml = `
|
||
<div class="modal fade" id="logStatsModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">日志统计信息</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
${statsHtml}
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 移除旧的模态框
|
||
const oldModal = document.getElementById('logStatsModal');
|
||
if (oldModal) oldModal.remove();
|
||
|
||
// 添加新的模态框
|
||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||
|
||
// 显示模态框
|
||
const modal = new bootstrap.Modal(document.getElementById('logStatsModal'));
|
||
modal.show();
|
||
|
||
} else {
|
||
showToast(data.message || '获取统计信息失败', 'danger');
|
||
}
|
||
} else {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取日志统计失败:', error);
|
||
showToast('获取日志统计失败', 'danger');
|
||
}
|
||
}
|
||
|
||
// ==================== 导入导出功能 ====================
|
||
|
||
// 导出关键词
|
||
async function exportKeywords() {
|
||
if (!currentCookieId) {
|
||
showToast('请先选择账号', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/keywords-export/${currentCookieId}`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
// 创建下载链接
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
|
||
// 根据当前账号是否有数据来设置文件名和提示
|
||
const currentKeywords = keywordsData[currentCookieId] || [];
|
||
const hasData = currentKeywords.length > 0;
|
||
|
||
if (hasData) {
|
||
a.download = `keywords_${currentCookieId}_${new Date().getTime()}.xlsx`;
|
||
showToast('关键词导出成功!', 'success');
|
||
} else {
|
||
a.download = `keywords_template_${currentCookieId}_${new Date().getTime()}.xlsx`;
|
||
showToast('导入模板导出成功!模板中包含示例数据供参考', 'success');
|
||
}
|
||
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
window.URL.revokeObjectURL(url);
|
||
} else {
|
||
const error = await response.json();
|
||
showToast(`导出失败: ${error.detail}`, 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('导出关键词失败:', error);
|
||
showToast('导出关键词失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 显示导入模态框
|
||
function showImportModal() {
|
||
if (!currentCookieId) {
|
||
showToast('请先选择账号', 'warning');
|
||
return;
|
||
}
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('importKeywordsModal'));
|
||
modal.show();
|
||
}
|
||
|
||
// 导入关键词
|
||
async function importKeywords() {
|
||
if (!currentCookieId) {
|
||
showToast('请先选择账号', 'warning');
|
||
return;
|
||
}
|
||
|
||
const fileInput = document.getElementById('importFileInput');
|
||
const file = fileInput.files[0];
|
||
|
||
if (!file) {
|
||
showToast('请选择要导入的Excel文件', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 显示进度条
|
||
const progressDiv = document.getElementById('importProgress');
|
||
const progressBar = progressDiv.querySelector('.progress-bar');
|
||
progressDiv.style.display = 'block';
|
||
progressBar.style.width = '30%';
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const response = await fetch(`${apiBase}/keywords-import/${currentCookieId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
progressBar.style.width = '70%';
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
progressBar.style.width = '100%';
|
||
|
||
setTimeout(() => {
|
||
progressDiv.style.display = 'none';
|
||
progressBar.style.width = '0%';
|
||
|
||
// 关闭模态框
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('importKeywordsModal'));
|
||
modal.hide();
|
||
|
||
// 清空文件输入
|
||
fileInput.value = '';
|
||
|
||
// 重新加载关键词列表
|
||
loadAccountKeywords(currentCookieId);
|
||
|
||
showToast(`导入成功!新增: ${result.added}, 更新: ${result.updated}`, 'success');
|
||
}, 500);
|
||
} else {
|
||
const error = await response.json();
|
||
progressDiv.style.display = 'none';
|
||
progressBar.style.width = '0%';
|
||
showToast(`导入失败: ${error.detail}`, 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('导入关键词失败:', error);
|
||
document.getElementById('importProgress').style.display = 'none';
|
||
document.querySelector('#importProgress .progress-bar').style.width = '0%';
|
||
showToast('导入关键词失败', 'error');
|
||
}
|
||
}
|
||
|
||
</script>
|
||
|
||
<!-- AI回复配置模态框 -->
|
||
<div class="modal fade" id="aiReplyConfigModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-robot me-2"></i>AI回复配置
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="aiReplyConfigForm">
|
||
<input type="hidden" id="aiConfigAccountId">
|
||
|
||
<!-- 基本设置 -->
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
<h6 class="mb-0"><i class="bi bi-gear me-2"></i>基本设置</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label">账号ID</label>
|
||
<input type="text" class="form-control" id="aiConfigAccountIdDisplay" readonly>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="form-check form-switch mt-4">
|
||
<input class="form-check-input" type="checkbox" id="aiReplyEnabled" onchange="toggleAIReplySettings()">
|
||
<label class="form-check-label" for="aiReplyEnabled">
|
||
<strong>启用AI回复</strong>
|
||
</label>
|
||
<small class="form-text text-muted d-block">启用后将自动禁用关键词匹配和默认回复</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="aiReplySettings" style="display: none;">
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<label for="aiModelName" class="form-label">AI模型</label>
|
||
<select class="form-select" id="aiModelName">
|
||
<option value="qwen-plus">通义千问Plus</option>
|
||
<option value="qwen-turbo">通义千问Turbo</option>
|
||
<option value="qwen-max">通义千问Max</option>
|
||
<option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
|
||
<option value="gpt-4">GPT-4</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label for="aiBaseUrl" class="form-label">API地址</label>
|
||
<input type="url" class="form-control" id="aiBaseUrl"
|
||
value="https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||
placeholder="API Base URL">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="aiApiKey" class="form-label">API密钥 <span class="text-danger">*</span></label>
|
||
<input type="password" class="form-control" id="aiApiKey"
|
||
placeholder="请输入API密钥" required>
|
||
<small class="form-text text-muted">
|
||
通义千问请使用DashScope API Key,GPT请使用OpenAI API Key
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 议价设置 -->
|
||
<div class="card mb-3" id="bargainSettings" style="display: none;">
|
||
<div class="card-header">
|
||
<h6 class="mb-0"><i class="bi bi-currency-dollar me-2"></i>议价设置</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row mb-3">
|
||
<div class="col-md-4">
|
||
<label for="maxDiscountPercent" class="form-label">最大优惠百分比</label>
|
||
<div class="input-group">
|
||
<input type="number" class="form-control" id="maxDiscountPercent"
|
||
min="0" max="50" value="10">
|
||
<span class="input-group-text">%</span>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label for="maxDiscountAmount" class="form-label">最大优惠金额</label>
|
||
<div class="input-group">
|
||
<input type="number" class="form-control" id="maxDiscountAmount"
|
||
min="0" value="100">
|
||
<span class="input-group-text">元</span>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label for="maxBargainRounds" class="form-label">最大议价轮数</label>
|
||
<input type="number" class="form-control" id="maxBargainRounds"
|
||
min="1" max="10" value="3">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 提示词设置 -->
|
||
<div class="card mb-3" id="promptSettings" style="display: none;">
|
||
<div class="card-header">
|
||
<h6 class="mb-0"><i class="bi bi-chat-quote me-2"></i>提示词设置</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<label class="form-label">自定义提示词 (JSON格式)</label>
|
||
<textarea class="form-control" id="customPrompts" rows="8"
|
||
placeholder='{"classify": "分类提示词", "price": "议价提示词", "tech": "技术提示词", "default": "默认提示词"}'></textarea>
|
||
<small class="form-text text-muted">
|
||
留空使用系统默认提示词。格式:{"classify": "...", "price": "...", "tech": "...", "default": "..."}
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 测试区域 -->
|
||
<div class="card" id="testArea" style="display: none;">
|
||
<div class="card-header">
|
||
<h6 class="mb-0"><i class="bi bi-play-circle me-2"></i>功能测试</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<label for="testMessage" class="form-label">测试消息</label>
|
||
<input type="text" class="form-control" id="testMessage"
|
||
value="你好,这个商品能便宜点吗?" placeholder="输入测试消息">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label for="testItemPrice" class="form-label">商品价格</label>
|
||
<input type="number" class="form-control" id="testItemPrice"
|
||
value="100" placeholder="商品价格">
|
||
</div>
|
||
</div>
|
||
<button type="button" class="btn btn-outline-primary" onclick="testAIReply()">
|
||
<i class="bi bi-play me-1"></i>测试AI回复
|
||
</button>
|
||
<div id="testResult" class="mt-3" style="display: none;">
|
||
<div class="alert alert-info">
|
||
<strong>AI回复:</strong>
|
||
<div id="testReplyContent"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveAIReplyConfig()">
|
||
<i class="bi bi-check-lg me-1"></i>保存配置
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 导入关键词模态框 -->
|
||
<div class="modal fade" id="importKeywordsModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi bi-upload me-2"></i>导入关键词
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="mb-3">
|
||
<label class="form-label">选择Excel文件</label>
|
||
<input type="file" class="form-control" id="importFileInput" accept=".xlsx,.xls">
|
||
<div class="form-text">
|
||
<i class="bi bi-info-circle me-1"></i>
|
||
请上传包含"关键词"、"商品ID"、"关键词内容"三列的Excel文件
|
||
</div>
|
||
</div>
|
||
<div class="alert alert-warning">
|
||
<h6><i class="bi bi-exclamation-triangle me-1"></i>导入说明:</h6>
|
||
<ul class="mb-0">
|
||
<li>Excel文件必须包含三列:关键词、商品ID、关键词内容</li>
|
||
<li>商品ID可以为空,表示通用关键词</li>
|
||
<li>如果关键词+商品ID组合已存在,将更新关键词内容</li>
|
||
<li>导入将覆盖当前账号的所有关键词数据</li>
|
||
</ul>
|
||
</div>
|
||
<div id="importProgress" style="display: none;">
|
||
<div class="progress mb-2">
|
||
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
|
||
</div>
|
||
<small class="text-muted">正在导入...</small>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" onclick="importKeywords()">
|
||
<i class="bi bi-upload me-1"></i>开始导入
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</body>
|
||
</html> |