2025-07-24 14:16:18 +08:00

6723 lines
225 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>闲鱼自动回复管理系统</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/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);
}
.sidebar-header {
padding: 1.5rem 1rem;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
color: white;
text-decoration: none;
display: flex;
align-items: center;
}
.sidebar-nav {
padding: 1rem 0;
}
.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;
}
/* 主内容区域 */
.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 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 {
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="#" class="nav-link" onclick="showSection('logs')">
<i class="bi bi-file-text"></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 class="nav-item">
<a href="#" class="nav-link" onclick="showSection('about')">
<i class="bi bi-info-circle"></i>
关于
</a>
</div>
<div class="nav-item mt-auto" style="position: absolute; bottom: 2rem; width: 100%;">
<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: 20%">Cookie值</th>
<th style="width: 8%">关键词</th>
<th style="width: 8%">状态</th>
<th style="width: 10%">默认回复</th>
<th style="width: 10%">AI回复</th>
<th style="width: 34%">操作</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-center">
<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 h-100 gap-2">
<button class="btn btn-success" onclick="getAllItemsFromAccount()">
<i class="bi bi-download 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 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: 15%">账号ID</th>
<th style="width: 15%">商品ID</th>
<th style="width: 20%">商品标题</th>
<th style="width: 25%">商品详情</th>
<th style="width: 12%">更新时间</th>
<th style="width: 8%">操作</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-outline-primary w-100" onclick="refreshAccountList()">
<i class="bi bi-arrow-clockwise me-2"></i>刷新列表
</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="account-badge" id="currentAccountBadge">
<!-- 动态显示当前账号 -->
</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>
<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>
</tr>
</thead>
<tbody id="cardsTableBody">
<tr>
<td colspan="6" 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">&nbsp;</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 class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-archive me-2"></i>备份管理
</div>
<div class="card-body">
<div class="row">
<!-- 导出备份 -->
<div class="col-md-6">
<h6 class="mb-3">
<i class="bi bi-download me-2"></i>导出备份
</h6>
<p class="text-muted mb-3">导出系统的所有设置、账号管理、自动回复、卡券管理等数据</p>
<button type="button" class="btn btn-success" onclick="exportBackup()">
<i class="bi bi-download me-1"></i>导出备份
</button>
</div>
<!-- 导入备份 -->
<div class="col-md-6">
<h6 class="mb-3">
<i class="bi bi-upload me-2"></i>导入备份
</h6>
<p class="text-muted mb-3">导入之前导出的备份文件,将覆盖当前所有数据</p>
<div class="mb-3">
<input type="file" class="form-control" id="backupFile" accept=".json">
</div>
<button type="button" class="btn btn-warning" onclick="importBackup()">
<i class="bi bi-upload 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="https://img.zhinianboke.com/img/5527" 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="https://img.zhinianboke.com/img/5526" 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">
<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="请输入数据,每行一个:&#10;卡号1:密码1&#10;卡号2:密码2&#10;或者&#10;兑换码1&#10;兑换码2"></textarea>
<small class="form-text text-muted">支持格式:卡号:密码 或 单独的兑换码</small>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">备注</label>
<textarea class="form-control" id="cardDescription" 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="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>
<textarea class="form-control" id="editCardDescription" 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="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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/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/${accountId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
console.log('从服务器获取的关键词数据:', data); // 调试信息
// 后端返回的是 [[keyword, reply], ...] 格式,转换为前端需要的格式
const formattedData = data.map(([keyword, reply]) => ({
keyword: keyword,
reply: reply
}));
console.log('格式化后的关键词数据:', formattedData); // 调试信息
keywordsData[accountId] = formattedData;
renderKeywordsList(formattedData);
// 更新账号徽章显示
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 addKeyword() {
const keyword = document.getElementById('newKeyword').value.trim();
const reply = document.getElementById('newReply').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);
}
// 检查关键词是否已存在(排除编辑的原关键词)
const existingKeyword = currentKeywords.find(item => item.keyword === keyword);
if (existingKeyword && (!isEditMode || keyword !== window.originalKeyword)) {
showToast(`关键词 "${keyword}" 已存在,请使用其他关键词`, 'warning');
toggleLoading(false);
return;
}
// 转换为后端期望的格式
const keywordsObj = {};
currentKeywords.forEach(item => {
keywordsObj[item.keyword] = item.reply;
});
// 添加新关键词或更新的关键词
keywordsObj[keyword] = reply;
const response = await fetch(`${apiBase}/keywords/${currentCookieId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
keywords: keywordsObj
})
});
if (response.ok) {
showToast(`✨ 关键词 "${keyword}" ${actionText}成功!`, 'success');
// 清空输入框并重置样式
const keywordInput = document.getElementById('newKeyword');
const replyInput = document.getElementById('newReply');
const addBtn = document.querySelector('.add-btn');
keywordInput.value = '';
replyInput.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';
keywordItem.innerHTML = `
<div class="keyword-item-header">
<div class="keyword-tag">
<i class="bi bi-tag-fill"></i>
${item.keyword}
</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;
// 设置编辑模式标识
window.editingIndex = index;
window.originalKeyword = keyword.keyword;
// 更新按钮文本和样式
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 = '';
// 重置编辑状态
delete window.editingIndex;
delete window.originalKeyword;
// 恢复添加按钮
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 keywordsObj = {};
currentKeywords.forEach(item => {
keywordsObj[item.keyword] = item.reply;
});
// 更新服务器
const response = await fetch(`${apiBase}/keywords/${cookieId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
keywords: keywordsObj
})
});
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>';
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="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 ? '点击禁用' : '点击启用';
}
// 跳转到自动回复页面并选择指定账号
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;
}
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="6" 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';
}
tr.innerHTML = `
<td>
<div class="fw-bold">${card.name}</div>
${card.description ? `<small class="text-muted">${card.description}</small>` : ''}
</td>
<td>${typeBadge}</td>
<td>${dataCount}</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';
}
// 保存卡券
async function saveCard() {
try {
const cardType = document.getElementById('cardType').value;
const cardName = document.getElementById('cardName').value;
if (!cardType || !cardName) {
showToast('请填写必填字段', 'warning');
return;
}
const cardData = {
name: cardName,
type: cardType,
description: document.getElementById('cardDescription').value,
enabled: true
};
// 根据类型添加特定配置
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();
loadCards();
} else {
const error = await response.text();
showToast(`保存失败: ${error}`, 'danger');
}
} catch (error) {
console.error('保存卡券失败:', error);
showToast('保存卡券失败', '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>
<span class="badge bg-primary">${rule.card_name || '未知卡券'}</span>
</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;
option.textContent = `${card.name} (${card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据'})`;
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('editCardEnabled').checked = card.enabled;
// 根据类型填充特定字段
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();
// 显示模态框
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 cardData = {
name: cardName,
type: cardType,
description: document.getElementById('editCardDescription').value,
enabled: document.getElementById('editCardEnabled').checked
};
// 根据类型添加特定配置
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;
option.textContent = `${card.name} (${card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据'})`;
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 loadSystemSettings() {
try {
const response = await fetch(`${apiBase}/system-settings`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const settings = await response.json();
// 设置主题颜色
if (settings.theme_color) {
document.getElementById('themeColor').value = settings.theme_color;
applyThemeColor(settings.theme_color);
}
}
} 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}/system-settings/theme_color`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
key: 'theme_color',
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}/system-settings/password`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword
})
});
if (response.ok) {
showToast('密码更新成功,请重新登录', 'success');
passwordForm.reset();
// 3秒后跳转到登录页面
setTimeout(() => {
localStorage.removeItem('auth_token');
window.location.href = '/login.html';
}, 3000);
} else {
const error = await response.text();
showToast(`密码更新失败: ${error}`, 'danger');
}
} catch (error) {
console.error('密码更新失败:', error);
showToast('密码更新失败', 'danger');
}
});
}
// 页面加载时加载系统设置
loadSystemSettings();
});
// ==================== 备份管理功能 ====================
// 导出备份
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('备份导入成功页面将在3秒后刷新', 'success');
// 清空文件选择
fileInput.value = '';
// 3秒后刷新页面
setTimeout(() => {
window.location.reload();
}, 3000);
} else {
const error = await response.text();
showToast(`导入失败: ${error}`, 'danger');
}
} catch (error) {
console.error('导入备份失败:', error);
showToast('导入备份失败', 'danger');
}
}
// ==================== 商品管理功能 ====================
// 加载商品列表
async function loadItems() {
try {
// 先加载Cookie列表用于筛选
await loadCookieFilter();
// 加载商品列表
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');
// 清空现有选项(保留"所有账号"
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);
});
}
}
} 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="7" 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 ? '...' : '');
}
}
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>${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>
</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 loadItems();
showToast('商品列表已刷新', 'success');
}
// 获取所有商品信息
async function getAllItemsFromAccount() {
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) {
showToast(`成功获取商品信息,请查看控制台日志`, 'success');
// 刷新商品列表
await loadItems();
} 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 loadItems();
} 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 loadItems();
} 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 loadItems();
} 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');
}
}
</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 KeyGPT请使用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>
</body>
</html>