admin.html•88.7 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SOAR MCP Server 管理后台</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body, html {
height: 100%;
font-family: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow-x: hidden;
color: #333;
}
/* 动态背景 */
.background {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: linear-gradient(135deg,
#22c55e 0%,
#16a34a 15%,
#15803d 30%,
#14532d 45%,
#059669 60%,
#0d9488 75%,
#10b981 90%,
#34d399 100%);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
z-index: -2;
}
.background::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at 20% 80%, rgba(34, 197, 94, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(52, 211, 153, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(16, 185, 129, 0.3) 0%, transparent 50%);
}
@keyframes gradientShift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
/* 浮动装饰元素 */
.floating-elements {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: -1;
}
.floating-shape {
position: absolute;
opacity: 0.1;
animation: float 20s infinite ease-in-out;
}
.shape-1 {
width: 100px;
height: 100px;
background: linear-gradient(45deg, #22c55e, #16a34a);
border-radius: 50%;
top: 10%;
left: 10%;
animation-delay: 0s;
}
.shape-2 {
width: 150px;
height: 150px;
background: linear-gradient(45deg, #34d399, #10b981);
border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;
top: 60%;
right: 10%;
animation-delay: -5s;
}
.shape-3 {
width: 80px;
height: 80px;
background: linear-gradient(45deg, #059669, #0d9488);
border-radius: 20px;
top: 30%;
right: 30%;
animation-delay: -10s;
transform: rotate(45deg);
}
@keyframes float {
0%, 100% { transform: translateY(0) rotate(0deg) scale(1); }
25% { transform: translateY(-20px) rotate(90deg) scale(1.1); }
50% { transform: translateY(-40px) rotate(180deg) scale(0.9); }
75% { transform: translateY(-20px) rotate(270deg) scale(1.1); }
}
.header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(30px);
padding: 2rem 4rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 10;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
z-index: -1;
}
.header h1 {
color: #f8fffe;
font-size: 1.8rem;
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 0.5rem;
text-shadow: 0 2px 8px rgba(34, 197, 94, 0.3);
}
.nav {
margin-top: 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-left {
display: flex;
}
.nav-right {
display: flex;
}
.nav-item {
display: inline-block;
color: #e8f5f3;
text-decoration: none;
padding: 0.5rem 1rem;
margin-right: 1rem;
border-radius: 8px;
background: rgba(34, 197, 94, 0.15);
border: 1px solid rgba(52, 211, 153, 0.2);
transition: all 0.3s ease;
font-weight: 500;
backdrop-filter: blur(10px);
}
.nav-item.active, .nav-item:hover {
background: rgba(34, 197, 94, 0.25);
color: #f0fdfa;
border-color: rgba(52, 211, 153, 0.4);
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2);
transform: translateY(-1px);
}
.nav-item.logout {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(248, 113, 113, 0.3);
color: #fef2f2;
}
.nav-item.logout:hover {
background: rgba(239, 68, 68, 0.25);
color: #fef2f2;
border-color: rgba(248, 113, 113, 0.5);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
}
.container {
max-width: 95%;
margin: 2rem auto;
padding: 0 2rem;
}
.card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(30px);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
overflow: hidden;
margin-bottom: 2rem;
position: relative;
animation: cardFloat 6s ease-in-out infinite;
color: #ffffff;
}
@keyframes cardFloat {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
}
.card::before {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
background: linear-gradient(45deg, #22c55e, #16a34a, #059669, #10b981, #34d399);
border-radius: 21px;
z-index: -1;
animation: borderGlow 3s ease-in-out infinite alternate;
opacity: 0.3;
}
@keyframes borderGlow {
0% { opacity: 0.2; }
100% { opacity: 0.5; }
}
.card-header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
padding: 1.5rem 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: #ffffff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.stats {
display: flex;
gap: 1rem;
}
.stat-item {
background: linear-gradient(135deg, #10b981, #059669);
color: #f0fdf9;
padding: 0.6rem 1.2rem;
border-radius: 10px;
min-width: fit-content;
white-space: nowrap;
font-size: 0.875rem;
font-weight: 600;
border: 1px solid rgba(52, 211, 153, 0.3);
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2);
backdrop-filter: blur(5px);
}
.table-container {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th {
background: rgba(34, 197, 94, 0.2);
color: #f0fdf9;
font-weight: 600;
padding: 1rem 1.5rem;
text-align: left;
border-bottom: 2px solid rgba(52, 211, 153, 0.3);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.table td {
padding: 1rem 1.5rem;
border-bottom: 1px solid rgba(52, 211, 153, 0.1);
vertical-align: middle;
color: #ffffff;
font-size: 0.9rem;
}
.table tbody tr:hover {
background: rgba(34, 197, 94, 0.15);
backdrop-filter: blur(10px);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 28px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #e2e8f0, #cbd5e0);
transition: all 0.3s ease;
border-radius: 28px;
border: 1px solid rgba(203, 213, 224, 0.5);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background: linear-gradient(135deg, #ffffff, #f7fafc);
transition: all 0.3s ease;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
input:checked + .slider {
background: linear-gradient(135deg, #10b981, #059669);
border-color: rgba(16, 185, 129, 0.5);
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2),
inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
input:checked + .slider:before {
transform: translateX(22px);
background: linear-gradient(135deg, #f0fdf9, #ecfdf5);
}
.category-badge {
display: inline-block;
background: #e2e8f0;
color: #4a5568;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 500;
}
.playbook-id {
font-family: 'Monaco', 'Menlo', monospace;
color: #805ad5;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
}
.playbook-id:hover {
color: #553c9a;
text-decoration: underline;
}
.playbook-display-name {
color: #2d3748;
font-weight: 500;
font-size: 0.95rem;
}
.loading {
text-align: center;
padding: 3rem;
color: #718096;
}
.error {
background: #fed7d7;
color: #c53030;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.success {
background: #c6f6d5;
color: #2f855a;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
/* Modal 样式 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(20px);
}
.modal-content {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(30px);
border: 1px solid rgba(255, 255, 255, 0.2);
margin: 5% auto;
padding: 0;
border-radius: 20px;
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.modal-header {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
padding: 1.5rem 2rem;
border-radius: 12px 12px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.modal-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.close {
color: rgba(255, 255, 255, 0.8);
font-size: 2rem;
font-weight: bold;
cursor: pointer;
background: none;
border: none;
padding: 0;
line-height: 1;
transition: color 0.2s ease;
}
.close:hover {
color: white;
}
.modal-body {
padding: 2rem;
background: rgba(255, 255, 255, 0.95);
border-radius: 0 0 12px 12px;
}
.detail-section {
margin-bottom: 2rem;
}
.detail-section:last-child {
margin-bottom: 0;
}
.detail-label {
font-weight: 600;
color: #059669;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.5rem;
}
.detail-value {
color: #1f2937;
font-size: 0.95rem;
line-height: 1.5;
font-weight: 500;
}
.param-item {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.param-item:last-child {
margin-bottom: 0;
}
.param-name {
font-weight: 600;
color: #805ad5;
margin-bottom: 0.5rem;
}
.param-type {
display: inline-block;
background: #4c51bf;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
margin-right: 0.5rem;
}
.param-required {
color: #e53e3e;
font-size: 0.75rem;
font-weight: 500;
}
.param-description {
color: #718096;
font-size: 0.875rem;
margin-top: 0.5rem;
}
/* 底部信息样式 */
.footer {
margin-top: 3rem;
padding: 2rem 0;
text-align: center;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(52, 211, 153, 0.2);
}
.footer-content {
color: #ffffff;
font-size: 0.9rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.footer-content a {
color: #34d399;
text-decoration: none;
font-weight: 600;
margin: 0 0.5rem;
transition: all 0.3s ease;
}
.footer-content a:hover {
color: #10b981;
text-shadow: 0 0 8px rgba(52, 211, 153, 0.6);
}
@media (max-width: 768px) {
.container {
padding: 0 1rem;
max-width: 100%;
}
.header {
padding: 1rem;
}
.card-header {
padding: 1rem;
}
.table th,
.table td {
padding: 0.75rem 0.5rem;
}
.stats {
flex-wrap: wrap;
}
.modal-content {
width: 95%;
margin: 2% auto;
}
.modal-header {
padding: 1rem;
}
.modal-body {
padding: 1rem;
}
}
/* 分页控件样式 */
.pagination-container {
margin: 2rem 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(20px);
border: 1px solid rgba(52, 211, 153, 0.2);
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.pagination-info {
font-size: 0.9rem;
color: #ffffff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.pagination-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.pagination-btn {
padding: 0.6rem 1rem;
border: 1px solid rgba(34, 197, 94, 0.3);
background: rgba(240, 253, 250, 0.8);
color: #065f46;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
font-weight: 500;
backdrop-filter: blur(10px);
}
.pagination-btn:hover:not(:disabled) {
background: rgba(34, 197, 94, 0.15);
border-color: #10b981;
color: #047857;
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.2);
transform: translateY(-1px);
}
.pagination-btn:disabled {
background: rgba(156, 163, 175, 0.1);
color: #9ca3af;
cursor: not-allowed;
border-color: rgba(156, 163, 175, 0.3);
}
.page-numbers {
display: flex;
gap: 0.25rem;
}
.page-number {
padding: 0.5rem 0.75rem;
border: 1px solid #ddd;
background: white;
color: #333;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
min-width: 36px;
text-align: center;
}
.page-number:hover {
background: #f8f9fa;
border-color: #007bff;
color: #007bff;
}
.page-number.active {
background: #007bff;
border-color: #007bff;
color: white;
}
.page-number.ellipsis {
cursor: default;
border: none;
background: transparent;
}
.page-number.ellipsis:hover {
background: transparent;
color: #333;
}
/* 系统配置样式 */
.config-actions {
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.6rem 1.2rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
}
.btn-primary {
background: linear-gradient(135deg, #10b981, #059669);
color: #f0fdf9;
border: 1px solid rgba(52, 211, 153, 0.3);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.25);
}
.btn-primary:hover {
background: linear-gradient(135deg, #059669, #047857);
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.35);
transform: translateY(-2px);
}
.btn-secondary {
background: rgba(34, 197, 94, 0.15);
color: #065f46;
border: 1px solid rgba(34, 197, 94, 0.3);
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.1);
}
.btn-secondary:hover {
background: rgba(34, 197, 94, 0.25);
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2);
transform: translateY(-1px);
}
.config-container {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
padding: 2rem;
}
.config-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group .label-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
}
.form-group label {
font-weight: 600;
color: #333;
margin: 0;
}
.form-group .form-help {
font-size: 0.85em;
color: #666;
margin: 0;
font-style: italic;
}
/* 同步周期选择框样式 */
#sync-interval {
max-width: 150px;
}
.form-input {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
transition: border-color 0.2s;
}
.form-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.form-help {
font-size: 0.8rem;
color: #666;
}
.password-input-container {
position: relative;
display: flex;
align-items: center;
}
.password-input-container .form-input {
padding-right: 3rem;
width: 100%;
min-width: 400px;
}
.password-toggle {
position: absolute;
right: 0.75rem;
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
font-size: 1rem;
color: #666;
z-index: 1;
}
.password-toggle:hover {
color: #333;
}
.status-success {
color: #28a745;
font-weight: bold;
}
.status-error {
color: #dc3545;
font-weight: bold;
}
.status-warning {
color: #ffc107;
font-weight: bold;
}
/* 限制特定输入框的宽度 */
#soar-timeout {
max-width: 120px;
}
.labels-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
.labels-input {
display: flex;
gap: 0.5rem;
}
.labels-input .form-input {
flex: 1;
}
.labels-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
min-height: 2rem;
}
.label-tag {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.75rem;
background: #e3f2fd;
color: #1976d2;
border-radius: 16px;
font-size: 0.8rem;
border: 1px solid #bbdefb;
}
.label-tag .remove-label {
background: none;
border: none;
color: #1976d2;
cursor: pointer;
font-size: 1rem;
line-height: 1;
padding: 0;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.label-tag .remove-label:hover {
background: #1976d2;
color: white;
border-radius: 50%;
}
.config-status {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.config-status h3 {
margin-bottom: 1rem;
color: #495057;
font-size: 1.1rem;
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
}
.status-item:last-child {
border-bottom: none;
}
.status-label {
font-weight: 600;
color: #495057;
}
.status-value {
color: #6c757d;
}
.status-success {
color: #28a745 !important;
}
.status-error {
color: #dc3545 !important;
}
.status-warning {
color: #ffc107 !important;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2rem;
margin: 2rem 0;
padding: 0 1rem;
}
.stat-card {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(20px);
border: 1px solid rgba(52, 211, 153, 0.2);
padding: 2rem;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1),
0 4px 16px rgba(34, 197, 94, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #10b981, #059669, #34d399);
opacity: 0.8;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15),
0 6px 24px rgba(34, 197, 94, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
border-color: rgba(52, 211, 153, 0.3);
}
.stat-card h3 {
margin: 0 0 1.5rem 0;
color: #f0fdf9;
font-size: 1.2rem;
font-weight: 700;
border-bottom: 2px solid rgba(52, 211, 153, 0.3);
padding-bottom: 0.75rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.stats-grid .stat-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding: 1rem;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
transition: all 0.2s ease;
}
.stats-grid .stat-item:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(52, 211, 153, 0.3);
transform: translateX(2px);
}
.stats-grid .stat-item:last-child {
margin-bottom: 0;
}
.stat-label {
font-weight: 500;
color: #f0fdf9;
}
.stat-value {
font-weight: 700;
color: #ffffff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
background: linear-gradient(135deg, #ffffff, #f0fdf9);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
@media (max-width: 768px) {
.config-container {
grid-template-columns: 1fr;
gap: 1rem;
padding: 1rem;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.labels-input {
flex-direction: column;
}
.config-actions {
flex-direction: column;
}
}
</style>
</head>
<body>
<!-- 动态背景 -->
<div class="background"></div>
<!-- 浮动装饰元素 -->
<div class="floating-elements">
<div class="floating-shape shape-1"></div>
<div class="floating-shape shape-2"></div>
<div class="floating-shape shape-3"></div>
</div>
<div class="header">
<h1 style="display: flex; align-items: center; gap: 12px;">
<img src="/static/logo.webp"
alt="SOAR MCP Server Logo"
style="width: 48px; height: 48px; border-radius: 8px; object-fit: cover;">
SOAR MCP Server 管理后台
</h1>
<nav class="nav">
<div class="nav-left">
<a href="#" class="nav-item active" id="nav-playbooks">剧本管理</a>
<a href="#" class="nav-item" id="nav-tokens">Token管理</a>
<a href="#" class="nav-item" id="nav-config">系统配置</a>
<a href="#" class="nav-item" id="nav-stats">统计信息</a>
</div>
<div class="nav-right">
<a href="#" class="nav-item logout" id="logout-btn">注销</a>
</div>
</nav>
</div>
<div class="container">
<div id="alert-container"></div>
<!-- 剧本详情 Modal -->
<div id="playbook-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">剧本详情</h3>
<button class="close" id="close-modal-btn">×</button>
</div>
<div class="modal-body" id="modal-body">
<div class="loading">加载中...</div>
</div>
</div>
</div>
<div class="card" id="playbooks-section">
<div class="card-header">
<h2 class="card-title">剧本管理</h2>
<div class="stats" id="playbook-stats">
<div class="stat-item">总计: <span id="total-count">-</span></div>
<div class="stat-item">启用: <span id="enabled-count">-</span></div>
<div class="stat-item">禁用: <span id="disabled-count">-</span></div>
</div>
</div>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th width="25%">剧本ID</th>
<th width="50%">显示名称</th>
<th width="15%">最后同步</th>
<th width="10%">状态</th>
</tr>
</thead>
<tbody id="playbooks-tbody">
<tr>
<td colspan="4" class="loading">
<div>正在加载剧本数据...</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控件 -->
<div class="pagination-container">
<div class="pagination-info">
<span id="pagination-info">显示第 1-20 条,共 0 条</span>
</div>
<div class="pagination-controls">
<button id="prev-page" class="pagination-btn" disabled>上一页</button>
<div id="page-numbers" class="page-numbers"></div>
<button id="next-page" class="pagination-btn" disabled>下一页</button>
</div>
</div>
</div>
<!-- Token管理区域 -->
<div class="card" id="tokens-section" style="display: none;">
<div class="card-header">
<h2 class="card-title">Token管理</h2>
<div class="config-actions">
<button id="create-token-btn" class="btn btn-primary" onclick="showCreateTokenModal()">创建Token</button>
</div>
</div>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th width="18%">Token名称</th>
<th width="20%">Token值</th>
<th width="14%">创建时间</th>
<th width="14%">过期时间</th>
<th width="16%">最后使用</th>
<th width="10%">状态</th>
<th width="8%">操作</th>
</tr>
</thead>
<tbody id="tokens-tbody">
<tr>
<td colspan="7" class="loading">
<div>正在加载Token数据...</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 创建Token Modal -->
<div id="create-token-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">创建新Token</h3>
<button class="close" onclick="closeCreateTokenModal()">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="token-name">Token名称</label>
<input type="text" id="token-name" class="form-input" placeholder="输入Token描述名称" required>
<small class="form-help">用于标识此Token的描述性名称</small>
</div>
<div class="form-group">
<label for="token-expires">过期时间</label>
<select id="token-expires" class="form-input">
<option value="">永不过期</option>
<option value="7">7天后过期</option>
<option value="30">30天后过期</option>
<option value="90">90天后过期</option>
</select>
<small class="form-help">Token的有效期,过期后将自动失效</small>
</div>
<div class="config-actions">
<button type="button" class="btn btn-secondary" onclick="closeCreateTokenModal()">取消</button>
<button type="button" class="btn btn-primary" onclick="createToken()">创建Token</button>
</div>
</div>
</div>
</div>
<!-- 显示Token值 Modal -->
<div id="show-token-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Token创建成功</h3>
<button class="close" onclick="closeShowTokenModal()">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Token值</label>
<div class="password-input-container">
<input type="text" id="new-token-value" class="form-input" readonly>
<button type="button" class="password-toggle" onclick="copyTokenToClipboard()">
<span>📋</span>
</button>
</div>
<small class="form-help" style="color: #e53e3e; font-weight: 600;">
请立即复制并保存此Token值,关闭窗口后将无法再次查看完整Token!
</small>
</div>
<div class="config-actions">
<button type="button" class="btn btn-primary" onclick="copyTokenToClipboard()">复制Token</button>
<button type="button" class="btn btn-secondary" onclick="closeShowTokenModal()">关闭</button>
</div>
</div>
</div>
</div>
<!-- 系统配置区域 -->
<div class="card" id="config-section" style="display: none;">
<div class="card-header">
<h2 class="card-title">系统配置</h2>
<div class="config-actions">
<button id="test-connection-btn" class="btn btn-primary" onclick="testConnection()">测试连接</button>
<button id="save-config-btn" class="btn btn-primary" onclick="saveConfiguration()">保存配置</button>
</div>
</div>
<div class="config-container">
<div class="config-form">
<div class="form-group">
<div class="label-row">
<label for="soar-api-url">SOAR服务器API地址</label>
<small class="form-help">SOAR服务器的API基础地址</small>
</div>
<input type="url" id="soar-api-url" class="form-input" placeholder="https://example.com" required>
</div>
<div class="form-group">
<div class="label-row">
<label for="soar-api-token">API Token</label>
<small class="form-help">用于API认证的Token</small>
</div>
<div class="password-input-container">
<input type="password" id="soar-api-token" class="form-input" placeholder="输入API Token" required>
<button type="button" class="password-toggle" onclick="togglePasswordVisibility('soar-api-token')">
<span class="eye-icon" id="soar-api-token-eye">👁️</span>
</button>
</div>
</div>
<div class="form-group">
<div class="label-row">
<label for="soar-timeout">超时时间(秒)</label>
<small class="form-help">API请求的超时时间,建议30-60秒</small>
</div>
<input type="number" id="soar-timeout" class="form-input" min="1" max="300" value="30" required>
</div>
<div class="form-group">
<div class="label-row">
<label for="sync-interval">同步周期</label>
<small class="form-help">剧本和应用的自动同步周期,默认4小时</small>
</div>
<select id="sync-interval" class="form-input" required>
<option value="3600">1小时</option>
<option value="14400" selected>4小时</option>
<option value="43200">12小时</option>
<option value="86400">24小时</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="label-input">剧本抓取标签</label>
<small class="form-help">用于过滤同步的剧本标签,至少需要一个标签</small>
</div>
<div class="labels-container">
<div class="labels-input">
<input type="text" id="label-input" class="form-input" placeholder="输入标签名称" maxlength="50">
<button type="button" id="add-label-btn" class="btn btn-primary" onclick="addLabel()">添加</button>
</div>
<div class="labels-list" id="labels-list">
<!-- 标签列表将通过JavaScript动态生成 -->
</div>
</div>
</div>
</div>
<div class="config-status" id="config-status">
<h3>配置状态</h3>
<div class="status-item">
<span class="status-label">配置验证:</span>
<span class="status-value" id="config-valid">未验证</span>
</div>
<div class="status-item">
<span class="status-label">连接测试:</span>
<span class="status-value" id="connection-status">未测试</span>
</div>
<div class="status-item">
<span class="status-label">最后更新:</span>
<span class="status-value" id="config-updated">-</span>
</div>
</div>
</div>
</div>
<!-- 统计信息页面 -->
<div class="card" id="stats-section" style="display: none;">
<div class="card-header">
<h2 class="card-title">统计信息</h2>
<div class="card-actions">
<button class="btn btn-primary" onclick="refreshStats()">刷新统计</button>
</div>
</div>
<div class="card-content">
<div class="stats-grid">
<div class="stat-card">
<h3>剧本统计</h3>
<div class="stat-item">
<span class="stat-label">总剧本数:</span>
<span class="stat-value" id="total-playbooks">-</span>
</div>
<div class="stat-item">
<span class="stat-label">已启用:</span>
<span class="stat-value" id="enabled-playbooks">-</span>
</div>
<div class="stat-item">
<span class="stat-label">已禁用:</span>
<span class="stat-value" id="disabled-playbooks">-</span>
</div>
</div>
<div class="stat-card">
<h3>应用统计</h3>
<div class="stat-item">
<span class="stat-label">总应用数:</span>
<span class="stat-value" id="total-apps">-</span>
</div>
</div>
<div class="stat-card">
<h3>系统状态</h3>
<div class="stat-item">
<span class="stat-label">服务状态:</span>
<span class="stat-value status-success" id="service-status">运行中</span>
</div>
<div class="stat-item">
<span class="stat-label">最后同步:</span>
<span class="stat-value" id="last-sync">-</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 检查认证状态
function checkAuth() {
const jwt = localStorage.getItem('jwt_token');
if (!jwt) {
// 如果没有token,跳转到登录页
window.location.href = '/login';
return false;
}
return jwt;
}
// 注销函数
function logout() {
// 确认注销
if (confirm('确定要注销吗?')) {
// 清除本地存储的JWT token
localStorage.removeItem('jwt_token');
// 跳转到登录页面
window.location.href = '/login';
}
}
// 通用API调用函数,自动添加JWT认证头
async function apiCall(url, options = {}) {
const jwt = checkAuth();
if (!jwt) return null;
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwt}`
}
};
const mergedOptions = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...(options.headers || {})
}
};
try {
const response = await fetch(url, mergedOptions);
// 如果返回401,清除token并跳转到登录页
if (response.status === 401) {
localStorage.removeItem('jwt_token');
window.location.href = '/login';
return null;
}
return response;
} catch (error) {
console.error('API调用失败:', error);
throw error;
}
}
// 全局状态
let playbooks = [];
let currentPage = 1;
const itemsPerPage = 20;
// DOM 元素
const alertContainer = document.getElementById('alert-container');
const playbooksTable = document.getElementById('playbooks-tbody');
const totalCount = document.getElementById('total-count');
const enabledCount = document.getElementById('enabled-count');
const disabledCount = document.getElementById('disabled-count');
const paginationInfo = document.getElementById('pagination-info');
const prevPageBtn = document.getElementById('prev-page');
const nextPageBtn = document.getElementById('next-page');
const pageNumbers = document.getElementById('page-numbers');
// 显示通知
function showAlert(message, type = 'success') {
const alert = document.createElement('div');
alert.className = type === 'error' ? 'error' : 'success';
alert.textContent = message;
alertContainer.innerHTML = '';
alertContainer.appendChild(alert);
setTimeout(() => {
alert.remove();
}, 3000);
}
// 格式化日期
function formatDate(dateStr) {
if (!dateStr) return '-';
try {
const date = new Date(dateStr);
return date.toLocaleString('zh-CN');
} catch {
return '-';
}
}
// 切换剧本状态
async function togglePlaybook(playbookId, enabled) {
try {
const response = await apiCall(`/api/admin/playbooks/${playbookId}/toggle`, {
method: 'POST',
body: JSON.stringify({ enabled: enabled })
});
if (!response) return;
const result = await response.json();
if (result.success) {
showAlert(`剧本 ${playbookId} 已${enabled ? '启用' : '禁用'}`, 'success');
// 更新本地状态 (注意:比较时统一转换为字符串)
const playbook = playbooks.find(p => String(p.id) === String(playbookId));
if (playbook) {
playbook.enabled = enabled;
updateStats();
}
} else {
showAlert(result.error || '操作失败', 'error');
// 回滚开关状态
const toggle = document.querySelector(`input[data-id="${playbookId}"]`);
if (toggle) {
toggle.checked = !enabled;
}
}
} catch (error) {
showAlert('网络错误: ' + error.message, 'error');
// 回滚开关状态
const toggle = document.querySelector(`input[data-id="${playbookId}"]`);
if (toggle) {
toggle.checked = !enabled;
}
}
}
// 更新统计
function updateStats() {
const total = playbooks.length;
const enabled = playbooks.filter(p => p.enabled).length;
const disabled = total - enabled;
totalCount.textContent = total;
enabledCount.textContent = enabled;
disabledCount.textContent = disabled;
}
// 渲染剧本表格
function renderPlaybooks() {
if (playbooks.length === 0) {
playbooksTable.innerHTML = `
<tr>
<td colspan="4" class="loading">
<div>暂无剧本数据</div>
</td>
</tr>
`;
updatePagination();
return;
}
// 计算分页数据
const totalPages = Math.ceil(playbooks.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentPlaybooks = playbooks.slice(startIndex, endIndex);
playbooksTable.innerHTML = currentPlaybooks.map(playbook => `
<tr>
<td>
<span class="playbook-id" data-id="${playbook.id}">${String(playbook.id)}</span>
</td>
<td>
<div class="playbook-display-name">${playbook.displayName || playbook.name}</div>
</td>
<td>${formatDate(playbook.syncTime)}</td>
<td>
<label class="switch">
<input type="checkbox" data-id="${playbook.id}" ${playbook.enabled ? 'checked' : ''}>
<span class="slider"></span>
</label>
</td>
</tr>
`).join('');
// 绑定开关事件
playbooksTable.querySelectorAll('input[type="checkbox"]').forEach(toggle => {
toggle.addEventListener('change', (e) => {
const playbookId = e.target.dataset.id; // 保持字符串格式避免精度丢失
const enabled = e.target.checked;
togglePlaybook(playbookId, enabled);
});
});
// 绑定剧本ID点击事件
playbooksTable.querySelectorAll('.playbook-id').forEach(playbookIdElement => {
playbookIdElement.addEventListener('click', (e) => {
const playbookId = e.target.dataset.id; // 保持字符串格式避免精度丢失
showPlaybookDetail(playbookId);
});
});
updateStats();
updatePagination();
}
// 加载剧本数据
async function loadPlaybooks() {
try {
const response = await apiCall('/api/admin/playbooks');
if (!response) return;
const result = await response.json();
if (result.success) {
playbooks = result.data;
renderPlaybooks();
} else {
showAlert(result.error || '加载剧本数据失败', 'error');
playbooksTable.innerHTML = `
<tr>
<td colspan="4" class="loading">
<div>加载失败,请刷新页面重试</div>
</td>
</tr>
`;
}
} catch (error) {
showAlert('网络错误: ' + error.message, 'error');
playbooksTable.innerHTML = `
<tr>
<td colspan="4" class="loading">
<div>网络错误,请检查连接</div>
</td>
</tr>
`;
}
}
// 显示剧本详情
async function showPlaybookDetail(playbookId) {
const modal = document.getElementById('playbook-modal');
const modalBody = document.getElementById('modal-body');
modal.style.display = 'block';
modalBody.innerHTML = '<div class="loading">加载中...</div>';
try {
const response = await apiCall(`/api/admin/playbooks/${playbookId}`);
if (!response) {
modal.style.display = 'none';
return;
}
const result = await response.json();
if (result.success) {
const playbook = result.data;
let params = [];
// 解析参数
if (playbook.playbookParams) {
try {
params = JSON.parse(playbook.playbookParams);
} catch (e) {
console.warn('参数解析失败:', e);
}
}
modalBody.innerHTML = `
<div class="detail-section">
<div class="detail-label">剧本ID</div>
<div class="detail-value">${String(playbook.id)}</div>
</div>
<div class="detail-section">
<div class="detail-label">剧本名称</div>
<div class="detail-value">${playbook.name}</div>
</div>
<div class="detail-section">
<div class="detail-label">显示名称</div>
<div class="detail-value">${playbook.displayName || playbook.name}</div>
</div>
<div class="detail-section">
<div class="detail-label">分类</div>
<div class="detail-value">${playbook.playbookCategory}</div>
</div>
<div class="detail-section">
<div class="detail-label">描述</div>
<div class="detail-value">${playbook.description || '无描述'}</div>
</div>
<div class="detail-section">
<div class="detail-label">创建时间</div>
<div class="detail-value">${formatDate(playbook.createTime)}</div>
</div>
<div class="detail-section">
<div class="detail-label">更新时间</div>
<div class="detail-value">${formatDate(playbook.updateTime)}</div>
</div>
<div class="detail-section">
<div class="detail-label">最后同步</div>
<div class="detail-value">${formatDate(playbook.syncTime)}</div>
</div>
<div class="detail-section">
<div class="detail-label">状态</div>
<div class="detail-value">${playbook.enabled ? '已启用' : '已禁用'}</div>
</div>
<div class="detail-section">
<div class="detail-label">参数信息 (${params.length} 个)</div>
<div class="detail-value">
${params.length === 0 ? '无参数' : params.map(param => `
<div class="param-item">
<div class="param-name">
${param.cefColumn || param.key || '未知参数'}
<span class="param-type">${param.valueType || param.type || 'string'}</span>
${param.required ? '<span class="param-required">必填</span>' : ''}
</div>
${param.cefDesc ? `<div class="param-description">${param.cefDesc}</div>` : ''}
${param.description ? `<div class="param-description">${param.description}</div>` : ''}
${param.defaultValue ? `<div class="param-description">默认值: ${param.defaultValue}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`;
} else {
modalBody.innerHTML = `<div class="error">加载失败: ${result.error || '未知错误'}</div>`;
}
} catch (error) {
modalBody.innerHTML = `<div class="error">网络错误: ${error.message}</div>`;
}
}
// 关闭Modal
function closeModal() {
document.getElementById('playbook-modal').style.display = 'none';
}
// 更新分页信息和控件
function updatePagination() {
const totalPages = Math.ceil(playbooks.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, playbooks.length);
// 更新分页信息
if (playbooks.length === 0) {
paginationInfo.textContent = '显示第 0-0 条,共 0 条';
} else {
paginationInfo.textContent = `显示第 ${startIndex + 1}-${endIndex} 条,共 ${playbooks.length} 条`;
}
// 更新按钮状态
prevPageBtn.disabled = currentPage === 1;
nextPageBtn.disabled = currentPage === totalPages || totalPages === 0;
// 生成页码
renderPageNumbers(totalPages);
}
// 渲染页码
function renderPageNumbers(totalPages) {
pageNumbers.innerHTML = '';
if (totalPages <= 1) {
return;
}
const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage < maxVisiblePages - 1) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
// 显示第一页和省略号
if (startPage > 1) {
pageNumbers.appendChild(createPageButton(1));
if (startPage > 2) {
pageNumbers.appendChild(createEllipsis());
}
}
// 显示页码
for (let page = startPage; page <= endPage; page++) {
pageNumbers.appendChild(createPageButton(page));
}
// 显示省略号和最后一页
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
pageNumbers.appendChild(createEllipsis());
}
pageNumbers.appendChild(createPageButton(totalPages));
}
}
// 创建页码按钮
function createPageButton(page) {
const button = document.createElement('div');
button.className = `page-number ${page === currentPage ? 'active' : ''}`;
button.textContent = page;
button.onclick = () => goToPage(page);
return button;
}
// 创建省略号
function createEllipsis() {
const ellipsis = document.createElement('div');
ellipsis.className = 'page-number ellipsis';
ellipsis.textContent = '...';
return ellipsis;
}
// 跳转到指定页
function goToPage(page) {
if (page >= 1 && page <= Math.ceil(playbooks.length / itemsPerPage)) {
currentPage = page;
renderPlaybooks();
}
}
// 点击Modal背景关闭
function setupModalEvents() {
const modal = document.getElementById('playbook-modal');
const closeBtn = document.getElementById('close-modal-btn');
// 关闭按钮事件
closeBtn.addEventListener('click', closeModal);
// 点击背景关闭
modal.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
}
// 密码显示/隐藏功能
function togglePasswordVisibility(inputId) {
const input = document.getElementById(inputId);
const eyeIcon = document.getElementById(inputId + '-eye');
if (input.type === 'password') {
input.type = 'text';
eyeIcon.textContent = '🙈';
} else {
input.type = 'password';
eyeIcon.textContent = '👁️';
}
}
// 测试连接功能
async function testConnection() {
const testBtn = document.getElementById('test-connection-btn');
const connectionStatus = document.getElementById('connection-status');
testBtn.disabled = true;
testBtn.textContent = '测试中...';
connectionStatus.textContent = '测试中...';
connectionStatus.className = 'status-warning';
try {
const response = await apiCall('/api/admin/config/test', {
method: 'POST',
body: JSON.stringify({}) // 发送空对象,使用当前数据库配置
});
if (!response) return;
const data = await response.json();
if (data.success) {
connectionStatus.textContent = '连接成功';
connectionStatus.className = 'status-success';
showAlert('API连接测试成功!', 'success');
} else {
connectionStatus.textContent = '连接失败';
connectionStatus.className = 'status-error';
showAlert(`连接测试失败: ${data.message}`, 'error');
}
} catch (error) {
connectionStatus.textContent = '连接失败';
connectionStatus.className = 'status-error';
showAlert(`连接测试失败: ${error.message}`, 'error');
}
testBtn.disabled = false;
testBtn.textContent = '测试连接';
}
// 保存配置功能
async function saveConfiguration() {
const saveBtn = document.getElementById('save-config-btn');
const configValid = document.getElementById('config-valid');
const configUpdated = document.getElementById('config-updated');
saveBtn.disabled = true;
saveBtn.textContent = '保存中...';
// 收集配置数据
const apiUrl = document.getElementById('soar-api-url').value.trim();
const apiToken = document.getElementById('soar-api-token').value.trim();
const timeout = parseInt(document.getElementById('soar-timeout').value);
const syncInterval = parseInt(document.getElementById('sync-interval').value);
// 收集标签
const labelElements = document.querySelectorAll('.label-tag .label-name');
const labels = Array.from(labelElements).map(el => el.textContent.trim()).filter(label => label);
// 构建配置数据
const configData = {
soar_api_url: apiUrl,
soar_timeout: timeout,
sync_interval: syncInterval,
soar_labels: labels
};
// 检查Token是否被修改(如果不是打码格式,说明用户输入了新Token)
if (!apiToken.includes('***')) {
configData.soar_api_token = apiToken;
}
// 如果是打码格式,则不包含Token字段,后端会保持原有Token
// 验证必填项
if (!apiUrl || !apiToken || !timeout || labels.length === 0) {
showAlert('请填写所有必填项并至少添加一个标签', 'error');
saveBtn.disabled = false;
saveBtn.textContent = '保存配置';
return;
}
try {
const response = await apiCall('/api/admin/config', {
method: 'POST',
body: JSON.stringify(configData)
});
if (!response) return;
const data = await response.json();
if (data.success) {
configValid.textContent = '配置有效';
configValid.className = 'status-success';
configUpdated.textContent = new Date().toLocaleString('zh-CN');
showAlert('配置保存成功!', 'success');
} else {
configValid.textContent = '配置无效';
configValid.className = 'status-error';
showAlert(`配置保存失败: ${data.message}`, 'error');
}
} catch (error) {
configValid.textContent = '配置无效';
configValid.className = 'status-error';
showAlert(`配置保存失败: ${error.message}`, 'error');
}
saveBtn.disabled = false;
saveBtn.textContent = '保存配置';
}
// 标签管理功能
function addLabel() {
const labelInput = document.getElementById('label-input');
const labelsList = document.getElementById('labels-list');
const labelName = labelInput.value.trim();
if (!labelName) {
showAlert('请输入标签名称', 'warning');
return;
}
// 检查重复
const existingLabels = Array.from(document.querySelectorAll('.label-tag .label-name'))
.map(el => el.textContent.trim());
if (existingLabels.includes(labelName)) {
showAlert('标签已存在', 'warning');
return;
}
// 创建标签元素
const labelElement = document.createElement('div');
labelElement.className = 'label-tag';
labelElement.innerHTML = `
<span class="label-name">${labelName}</span>
<button type="button" class="label-remove" onclick="this.parentElement.remove()">×</button>
`;
labelsList.appendChild(labelElement);
labelInput.value = '';
}
// 初始化
// 加载系统配置
async function loadSystemConfig() {
try {
const response = await apiCall('/api/admin/config');
if (!response) return;
const data = await response.json();
if (data.success && data.data) {
// 填充表单字段
document.getElementById('soar-api-url').value = data.data.soar_api_url || '';
document.getElementById('soar-api-token').value = data.data.soar_api_token || '';
document.getElementById('soar-timeout').value = data.data.soar_timeout || 30;
document.getElementById('sync-interval').value = data.data.sync_interval || 14400;
// 填充标签列表
const labelsList = document.getElementById('labels-list');
labelsList.innerHTML = '';
const labels = data.data.soar_labels && Array.isArray(data.data.soar_labels)
? data.data.soar_labels
: ['MCP']; // 默认显示MCP标签
labels.forEach(label => {
const labelElement = document.createElement('div');
labelElement.className = 'label-tag';
labelElement.innerHTML = `
<span class="label-name">${label}</span>
<button type="button" class="label-remove" onclick="this.parentElement.remove()">×</button>
`;
labelsList.appendChild(labelElement);
});
}
} catch (error) {
console.error('加载系统配置失败:', error);
showAlert('加载系统配置失败', 'error');
// 即使加载失败,也显示默认的MCP标签
const labelsList = document.getElementById('labels-list');
labelsList.innerHTML = '';
const labelElement = document.createElement('div');
labelElement.className = 'label-tag';
labelElement.innerHTML = `
<span class="label-name">MCP</span>
<button type="button" class="label-remove" onclick="this.parentElement.remove()">×</button>
`;
labelsList.appendChild(labelElement);
}
}
// Token管理功能
let tokens = [];
// 加载Token列表
async function loadTokens() {
try {
const response = await apiCall('/api/admin/tokens');
if (!response) return;
const result = await response.json();
if (result.success) {
tokens = result.data;
renderTokens();
} else {
showAlert(result.error || '加载Token数据失败', 'error');
document.getElementById('tokens-tbody').innerHTML = `
<tr>
<td colspan="7" class="loading">
<div>加载失败,请刷新页面重试</div>
</td>
</tr>
`;
}
} catch (error) {
showAlert('网络错误: ' + error.message, 'error');
document.getElementById('tokens-tbody').innerHTML = `
<tr>
<td colspan="7" class="loading">
<div>网络错误,请检查连接</div>
</td>
</tr>
`;
}
}
// 渲染Token表格
function renderTokens() {
const tokensTable = document.getElementById('tokens-tbody');
if (tokens.length === 0) {
tokensTable.innerHTML = `
<tr>
<td colspan="7" class="loading">
<div>暂无Token数据</div>
</td>
</tr>
`;
return;
}
tokensTable.innerHTML = tokens.map(token => {
const isExpired = token.expires_at && new Date(token.expires_at) < new Date();
const maskedToken = token.token.substring(0, 8) + '***' + token.token.substring(token.token.length - 4);
return `
<tr>
<td>${token.name}</td>
<td>
<span class="playbook-id" style="font-size: 0.8rem; cursor: default;">${maskedToken}</span>
</td>
<td style="white-space: nowrap; font-size: 0.85rem;">${formatDate(token.created_at)}</td>
<td style="white-space: nowrap; font-size: 0.85rem;">${token.expires_at ? formatDate(token.expires_at) : '永不过期'}</td>
<td style="white-space: nowrap; font-size: 0.85rem;">${formatDate(token.last_used_at)}</td>
<td>
<span style="color: ${isExpired ? '#fca5a5' : (token.is_active ? '#ffffff' : '#d1d5db')}; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);">
${isExpired ? '已过期' : (token.is_active ? '活跃' : '禁用')}
</span>
</td>
<td>
<button class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;"
onclick="deleteToken(${token.id})">删除</button>
</td>
</tr>
`;
}).join('');
}
// 显示创建Token模态框
function showCreateTokenModal() {
document.getElementById('create-token-modal').style.display = 'block';
document.getElementById('token-name').value = '';
document.getElementById('token-expires').value = '';
}
// 关闭创建Token模态框
function closeCreateTokenModal() {
document.getElementById('create-token-modal').style.display = 'none';
}
// 创建Token
async function createToken() {
const name = document.getElementById('token-name').value.trim();
const expiresInDays = document.getElementById('token-expires').value;
if (!name) {
showAlert('请输入Token名称', 'error');
return;
}
try {
const response = await apiCall('/api/admin/tokens', {
method: 'POST',
body: JSON.stringify({
name: name,
expires_in_days: expiresInDays ? parseInt(expiresInDays) : null
})
});
if (!response) return;
const result = await response.json();
if (result.success) {
closeCreateTokenModal();
// 显示新创建的Token值
document.getElementById('new-token-value').value = result.token;
document.getElementById('show-token-modal').style.display = 'block';
// 重新加载Token列表
loadTokens();
showAlert('Token创建成功!', 'success');
} else {
showAlert(result.error || 'Token创建失败', 'error');
}
} catch (error) {
showAlert('网络错误: ' + error.message, 'error');
}
}
// 关闭显示Token值模态框
function closeShowTokenModal() {
document.getElementById('show-token-modal').style.display = 'none';
document.getElementById('new-token-value').value = '';
}
// 复制Token到剪贴板
async function copyTokenToClipboard() {
const tokenInput = document.getElementById('new-token-value');
try {
await navigator.clipboard.writeText(tokenInput.value);
showAlert('Token已复制到剪贴板', 'success');
} catch (error) {
// 兼容旧浏览器
tokenInput.select();
document.execCommand('copy');
showAlert('Token已复制到剪贴板', 'success');
}
}
// 删除Token
async function deleteToken(tokenId) {
if (!confirm('确定要删除这个Token吗?删除后无法恢复!')) {
return;
}
try {
const response = await apiCall(`/api/admin/tokens/${tokenId}`, {
method: 'DELETE'
});
if (!response) return;
const result = await response.json();
if (result.success) {
showAlert('Token删除成功', 'success');
loadTokens(); // 重新加载Token列表
} else {
showAlert(result.error || 'Token删除失败', 'error');
}
} catch (error) {
showAlert('网络错误: ' + error.message, 'error');
}
}
// 将refreshStats定义为全局函数
function refreshStats() {
// 获取所有统计信息(包括剧本和应用统计)
apiCall('/api/admin/stats')
.then(response => {
if (!response) return;
return response.json();
})
.then(data => {
if (!data) return;
if (data.success && data.stats) {
// 更新剧本统计
document.getElementById('total-playbooks').textContent = data.stats.total_playbooks || '-';
document.getElementById('enabled-playbooks').textContent = data.stats.enabled_playbooks || '-';
document.getElementById('disabled-playbooks').textContent = data.stats.disabled_playbooks || '-';
// 更新应用统计
document.getElementById('total-apps').textContent = data.stats.total_apps || '-';
// 更新最后同步时间
if (data.stats.last_sync_time) {
const lastSync = new Date(data.stats.last_sync_time).toLocaleString('zh-CN');
document.getElementById('last-sync').textContent = lastSync;
}
}
})
.catch(error => console.error('Error fetching stats:', error));
}
// Tab切换功能
function showTab(tabName) {
// 隐藏所有section
document.getElementById('playbooks-section').style.display = 'none';
document.getElementById('tokens-section').style.display = 'none';
document.getElementById('config-section').style.display = 'none';
document.getElementById('stats-section').style.display = 'none';
// 移除所有导航项的active类
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
// 显示对应的section
if (tabName === 'playbooks') {
document.getElementById('playbooks-section').style.display = 'block';
document.getElementById('nav-playbooks').classList.add('active');
} else if (tabName === 'tokens') {
document.getElementById('tokens-section').style.display = 'block';
document.getElementById('nav-tokens').classList.add('active');
loadTokens(); // 加载Token列表
} else if (tabName === 'config') {
document.getElementById('config-section').style.display = 'block';
document.getElementById('nav-config').classList.add('active');
loadSystemConfig(); // 加载系统配置
} else if (tabName === 'stats') {
document.getElementById('stats-section').style.display = 'block';
document.getElementById('nav-stats').classList.add('active');
refreshStats(); // 刷新统计信息
}
}
document.addEventListener('DOMContentLoaded', function() {
// 页面加载时检查认证
if (!checkAuth()) {
return; // checkAuth会处理跳转
}
// 添加导航事件监听器
document.getElementById('nav-playbooks').addEventListener('click', (e) => {
e.preventDefault();
showTab('playbooks');
});
document.getElementById('nav-tokens').addEventListener('click', (e) => {
e.preventDefault();
showTab('tokens');
});
document.getElementById('nav-config').addEventListener('click', (e) => {
e.preventDefault();
showTab('config');
});
document.getElementById('nav-stats').addEventListener('click', (e) => {
e.preventDefault();
showTab('stats');
});
// 注销按钮事件监听器
document.getElementById('logout-btn').addEventListener('click', (e) => {
e.preventDefault();
logout();
});
setupModalEvents();
// 设置分页按钮事件
prevPageBtn.addEventListener('click', () => {
if (currentPage > 1) {
goToPage(currentPage - 1);
}
});
nextPageBtn.addEventListener('click', () => {
const totalPages = Math.ceil(playbooks.length / itemsPerPage);
if (currentPage < totalPages) {
goToPage(currentPage + 1);
}
});
loadPlaybooks();
// 定期刷新(每30秒)
setInterval(loadPlaybooks, 30000);
// 添加玻璃拟态交互效果
initGlassmorphismEffects();
});
// 玻璃拟态交互效果
function initGlassmorphismEffects() {
// 添加鼠标跟踪效果
document.addEventListener('mousemove', (e) => {
const shapes = document.querySelectorAll('.floating-shape');
const x = e.clientX / window.innerWidth;
const y = e.clientY / window.innerHeight;
shapes.forEach((shape, index) => {
const speed = (index + 1) * 0.3;
const xOffset = (x - 0.5) * speed * 30;
const yOffset = (y - 0.5) * speed * 30;
shape.style.transform += ` translate(${xOffset}px, ${yOffset}px)`;
});
});
// 为卡片添加悬浮效果
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.addEventListener('mouseenter', (e) => {
e.target.style.transform = 'translateY(-8px)';
e.target.style.boxShadow = '0 30px 80px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.3)';
});
card.addEventListener('mouseleave', (e) => {
e.target.style.transform = 'translateY(0px)';
e.target.style.boxShadow = '0 20px 60px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.2)';
});
});
// 为按钮添加波纹效果
const buttons = document.querySelectorAll('.btn, .nav-item');
buttons.forEach(button => {
button.addEventListener('click', function(e) {
const ripple = document.createElement('span');
const rect = this.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: scale(0);
animation: ripple 0.6s linear;
pointer-events: none;
`;
this.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 600);
});
});
// 添加ripple动画样式
if (!document.getElementById('ripple-style')) {
const style = document.createElement('style');
style.id = 'ripple-style';
style.textContent = `
@keyframes ripple {
to {
transform: scale(2);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
}
</script>
<!-- 底部信息 -->
<footer class="footer">
<div class="footer-content">
雾帜智能@2025 <a href="https://flagify.com" target="_blank">最牛的SOAR</a> <a href="https://github.com/flagify-com/OctoMation/wiki" target="_blank">OctoMation</a>
</div>
</footer>
</body>
</html>