<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SiliconFlow - 批量余额查询</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #0062ff;
--primary-dark: #0050d0;
--primary-light: #cce0ff;
--primary-bg: #f0f6ff;
--success: #05a660;
--success-light: #e6f6ef;
--warning: #ff9800;
--warning-light: #fff4e6;
--danger: #e53935;
--danger-light: #ffebee;
--info: #0288d1;
--info-light: #e1f5fe;
--gray-50: #fafafa;
--gray-100: #f5f5f5;
--gray-200: #eeeeee;
--gray-300: #e0e0e0;
--gray-400: #bdbdbd;
--gray-500: #9e9e9e;
--gray-600: #757575;
--gray-700: #616161;
--gray-800: #424242;
--gray-900: #212121;
--black: #000000;
--white: #ffffff;
--radius-xs: 2px;
--radius-sm: 4px;
--radius: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 24px;
--radius-full: 9999px;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
--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);
--shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.5;
color: var(--gray-800);
background-color: #f8fafc;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 1.5rem;
}
.navbar {
background-color: var(--white);
box-shadow: var(--shadow);
position: sticky;
top: 0;
z-index: 100;
padding: 1rem 0;
}
.navbar-container {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
.navbar-brand {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
}
.brand-logo {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--primary), #3d5afe);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
color: var(--white);
font-weight: 600;
box-shadow: var(--shadow);
}
.brand-text {
font-size: 1.25rem;
font-weight: 600;
color: var(--gray-900);
}
.page-header {
margin: 2rem 0 2.5rem;
text-align: center;
}
.page-title {
font-size: 2rem;
font-weight: 700;
color: var(--gray-900);
margin-bottom: 0.75rem;
}
.page-description {
color: var(--gray-600);
font-size: 1.125rem;
max-width: 640px;
margin: 0 auto;
}
.card {
background-color: var(--white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
overflow: hidden;
transition: box-shadow 0.2s;
margin-bottom: 1.5rem;
}
.card:hover {
box-shadow: var(--shadow-lg);
}
.card-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--gray-200);
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--white);
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--gray-900);
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-title i {
color: var(--primary);
font-size: 1.125rem;
}
.card-body {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--gray-800);
}
.form-helper {
color: var(--gray-600);
font-size: 0.875rem;
margin-top: 0.375rem;
display: flex;
align-items: center;
gap: 0.25rem;
}
.form-control {
width: 100%;
padding: 0.875rem 1rem;
font-size: 0.875rem;
line-height: 1.5;
color: var(--gray-900);
background-color: var(--white);
border: 1px solid var(--gray-300);
border-radius: var(--radius);
transition: all 0.2s;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 98, 255, 0.15);
}
textarea.form-control {
min-height: 120px;
resize: vertical;
font-family: inherit;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 500;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
user-select: none;
border: 1px solid transparent;
border-radius: var(--radius);
transition: all 0.2s;
gap: 0.5rem;
}
.btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(0, 98, 255, 0.25);
}
.btn-primary {
color: var(--white);
background-color: var(--primary);
border-color: var(--primary);
}
.btn-primary:hover {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
.btn-outline {
color: var(--gray-700);
background-color: var(--white);
border-color: var(--gray-300);
}
.btn-outline:hover {
background-color: var(--gray-100);
border-color: var(--gray-400);
}
.btn-success {
color: var(--white);
background-color: var(--success);
border-color: var(--success);
}
.btn-success:hover {
background-color: #048f53;
border-color: #048f53;
}
.btn-lg {
padding: 0.875rem 2rem;
font-size: 1.125rem;
}
.btn-sm {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
border-radius: var(--radius-sm);
}
.btn-icon {
padding: 0.5rem;
border-radius: var(--radius-full);
}
.btn-block {
display: flex;
width: 100%;
}
.alert {
padding: 1rem;
border-radius: var(--radius);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
display: none;
}
.alert.show {
display: flex;
}
.alert-content {
flex: 1;
}
.alert-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: var(--radius-full);
flex-shrink: 0;
}
.alert-danger {
background-color: var(--danger-light);
color: var(--danger);
}
.alert-danger .alert-icon {
background-color: var(--danger);
color: var(--white);
}
.alert-success {
background-color: var(--success-light);
color: var(--success);
}
.alert-success .alert-icon {
background-color: var(--success);
color: var(--white);
}
.loading-container {
padding: 3rem 1.5rem;
display: none;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.loading-container.show {
display: flex;
}
.loading-animation {
position: relative;
width: 64px;
height: 64px;
}
.loading-circle {
position: absolute;
width: 64px;
height: 64px;
border: 4px solid rgba(0, 98, 255, 0.1);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
font-size: 1.25rem;
font-weight: 600;
color: var(--gray-800);
}
.loading-subtext {
color: var(--gray-600);
margin-top: 0.25rem;
}
.progress-container {
width: 100%;
max-width: 360px;
}
.progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.875rem;
color: var(--gray-600);
}
.progress-bar-bg {
width: 100%;
height: 6px;
background-color: var(--gray-200);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: var(--primary);
width: 0%;
transition: width 0.3s;
}
.results-section {
display: none;
}
.results-section.show {
display: block;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.summary-card {
background-color: var(--white);
border-radius: var(--radius);
padding: 1rem 1.25rem;
box-shadow: var(--shadow-sm);
display: flex;
align-items: center;
gap: 1rem;
}
.summary-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: var(--radius);
flex-shrink: 0;
}
.summary-data {
flex: 1;
}
.summary-value {
font-size: 1.5rem;
font-weight: 700;
line-height: 1.2;
margin-bottom: 0.25rem;
}
.summary-label {
font-size: 0.875rem;
color: var(--gray-600);
}
.summary-total .summary-icon {
background-color: var(--primary-bg);
color: var(--primary);
}
.summary-total .summary-value {
color: var(--primary);
}
.summary-valid .summary-icon {
background-color: var(--success-light);
color: var(--success);
}
.summary-valid .summary-value {
color: var(--success);
}
.summary-invalid .summary-icon {
background-color: var(--danger-light);
color: var(--danger);
}
.summary-invalid .summary-value {
color: var(--danger);
}
.summary-balance .summary-icon {
background-color: var(--info-light);
color: var(--info);
}
.summary-balance .summary-value {
color: var(--info);
}
.actions-bar {
background-color: var(--gray-50);
border-radius: var(--radius);
padding: 0.75rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.actions-group {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.actions-title {
font-weight: 500;
color: var(--gray-700);
margin-right: 0.75rem;
}
.badge-count {
display: inline-flex;
align-items: center;
justify-content: center;
background-color: var(--primary-bg);
color: var(--primary);
font-size: 0.75rem;
font-weight: 600;
height: 20px;
min-width: 20px;
padding: 0 6px;
border-radius: var(--radius-full);
margin-left: 0.375rem;
}
.table-container {
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-sm);
margin-bottom: 1.5rem;
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background-color: var(--gray-50);
border-bottom: 1px solid var(--gray-200);
}
th {
color: var(--gray-700);
font-weight: 600;
font-size: 0.875rem;
padding: 0.875rem 1rem;
text-align: left;
white-space: nowrap;
}
th.sortable {
cursor: pointer;
user-select: none;
}
th.sortable:hover {
background-color: var(--gray-100);
}
th.sortable .sort-icon {
display: inline-block;
margin-left: 0.25rem;
font-size: 0.75rem;
}
td {
padding: 1rem;
vertical-align: middle;
border-bottom: 1px solid var(--gray-200);
}
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:hover {
background-color: var(--gray-50);
}
.valid-entry:hover {
background-color: rgba(5, 166, 96, 0.04);
}
.invalid-entry:hover {
background-color: rgba(229, 57, 53, 0.04);
}
.user-cell {
display: flex;
align-items: center;
gap: 1rem;
min-width: 240px;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: var(--radius-full);
background-color: var(--primary-bg);
display: flex;
align-items: center;
justify-content: center;
color: var(--primary);
font-weight: 600;
position: relative;
flex-shrink: 0;
overflow: hidden;
}
.user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.admin-badge {
position: absolute;
bottom: 0;
right: 0;
width: 14px;
height: 14px;
border-radius: var(--radius-full);
background-color: var(--primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
border: 1.5px solid white;
}
.user-info {
display: flex;
flex-direction: column;
min-width: 0;
}
.user-name {
font-weight: 500;
color: var(--gray-900);
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-meta {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--gray-600);
font-size: 0.75rem;
}
.user-contact {
display: flex;
align-items: center;
gap: 0.25rem;
}
.user-role {
display: inline-flex;
align-items: center;
padding: 0 6px;
height: 18px;
background-color: var(--primary-bg);
color: var(--primary);
border-radius: var(--radius-full);
font-size: 0.75rem;
font-weight: 500;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.75rem;
border-radius: var(--radius-full);
font-size: 0.75rem;
font-weight: 500;
line-height: 1.5;
}
.status-active {
background-color: var(--success-light);
color: var(--success);
}
.status-warning {
background-color: var(--warning-light);
color: var(--warning);
}
.status-danger {
background-color: var(--danger-light);
color: var(--danger);
}
.api-key-cell {
display: flex;
align-items: center;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 0.813rem;
color: var(--gray-800);
gap: 0.5rem;
max-width: 320px;
}
.key-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.copy-btn {
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
background-color: var(--gray-100);
color: var(--gray-700);
border: 1px solid var(--gray-200);
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.25rem;
white-space: nowrap;
flex-shrink: 0;
}
.copy-btn:hover {
background-color: var(--gray-200);
color: var(--gray-800);
}
.copy-btn.copied {
background-color: var(--success-light);
color: var(--success);
border-color: var(--success);
}
.amount-cell {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 0.875rem;
font-weight: 500;
text-align: center;
white-space: nowrap;
width: 100%;
display: block;
margin-left: auto;
margin-right: auto;
}
.amount-total {
color: var(--success);
font-weight: 600;
}
.error-cell {
color: var(--danger);
font-size: 0.875rem;
max-width: 320px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 1rem;
text-align: center;
display: none;
}
.empty-state.show {
display: flex;
}
.empty-icon {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-full);
margin-bottom: 1.25rem;
font-size: 2rem;
}
.empty-valid .empty-icon {
background-color: var(--success-light);
color: var(--success);
}
.empty-invalid .empty-icon {
background-color: var(--danger-light);
color: var(--danger);
}
.empty-title {
font-weight: 600;
font-size: 1.125rem;
color: var(--gray-800);
margin-bottom: 0.5rem;
}
.empty-message {
color: var(--gray-600);
max-width: 300px;
}
.footer {
margin-top: 3rem;
padding: 1.5rem;
text-align: center;
color: var(--gray-500);
}
.footer-text {
font-size: 0.875rem;
}
.toast-container {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 9999;
}
.toast {
background-color: var(--gray-900);
color: var(--white);
border-radius: var(--radius);
padding: 0.75rem 1rem;
margin-top: 0.5rem;
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 200px;
max-width: 300px;
transform: translateY(10px);
opacity: 0;
transition: transform 0.3s, opacity 0.3s;
}
.toast.show {
transform: translateY(0);
opacity: 1;
}
.toast-success {
border-left: 4px solid var(--success);
}
.toast-error {
border-left: 4px solid var(--danger);
}
.toast-info {
border-left: 4px solid var(--primary);
}
.toast-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
flex-shrink: 0;
}
.toast-content {
flex: 1;
}
.detected-keys {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: var(--primary-bg);
border-radius: var(--radius);
margin-top: 0.75rem;
font-size: 0.875rem;
color: var(--primary);
}
.detected-keys i {
margin-right: 0.5rem;
}
.detected-count {
font-weight: 600;
margin: 0 0.25rem;
}
.sort-button {
display: flex;
align-items: center;
gap: 0.375rem;
margin-right: 0.5rem;
}
.sort-button .sort-icon {
transition: transform 0.2s ease;
}
.sort-active .sort-icon {
color: var(--primary);
}
.sort-desc .sort-icon {
transform: rotate(180deg);
}
@media (max-width: 992px) {
.summary-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.actions-bar {
flex-direction: column;
align-items: flex-start;
}
.table-container {
display: none;
}
.cards-view {
display: block;
}
.actions-group {
display: flex;
flex-direction: column;
width: 100%;
gap: 0.75rem;
}
.actions-group .btn {
width: 100%;
justify-content: center;
}
}
@media (min-width: 769px) {
.table-container {
display: block;
}
.cards-view {
display: none;
}
}
.result-card {
background-color: var(--white);
border-radius: var(--radius);
padding: 1rem;
margin-bottom: 1rem;
box-shadow: var(--shadow-sm);
border-left: 4px solid transparent;
}
.valid-card {
border-left-color: var(--success);
}
.invalid-card {
border-left-color: var(--danger);
}
.card-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.75rem;
align-items: center;
}
.card-row:last-child {
margin-bottom: 0;
}
.card-label {
font-size: 0.75rem;
color: var(--gray-600);
margin-bottom: 0.25rem;
text-align: center;
}
.card-value {
font-weight: 500;
}
.card-divider {
height: 1px;
background-color: var(--gray-200);
margin: 0.75rem 0;
}
.found-keys-badge {
display: inline-flex;
align-items: center;
padding: 0.375rem 0.75rem;
background-color: var(--primary-bg);
color: var(--primary);
border-radius: var(--radius-full);
font-size: 0.875rem;
font-weight: 500;
margin-top: 0.75rem;
}
.found-keys-count {
margin: 0 0.25rem;
font-weight: 600;
}
.card-value.amount-cell {
text-align: center;
width: 100%;
margin: 0 auto;
}
.balance-group {
display: flex;
flex-direction: column;
align-items: center;
width: 33.33%;
text-align: center;
}
</style>
</head>
<body>
<nav class="navbar">
<div class="navbar-container">
<a href="#" class="navbar-brand">
<div class="brand-logo">SF</div>
<div class="brand-text">SiliconFlow</div>
</a>
</div>
</nav>
<div class="container">
<header class="page-header">
<h1 class="page-title">批量账户余额查询</h1>
<p class="page-description">快速检索多个API密钥,获取账户余额和详细信息</p>
</header>
<div class="card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-key"></i>
输入API密钥
</h2>
</div>
<div class="card-body">
<div id="errorAlert" class="alert alert-danger">
<div class="alert-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="alert-content" id="errorMessage"></div>
</div>
<div id="successAlert" class="alert alert-success">
<div class="alert-icon">
<i class="fas fa-check"></i>
</div>
<div class="alert-content" id="successMessage"></div>
</div>
<form id="apiKeysForm">
<div class="form-group">
<label for="apiKeysInput" class="form-label">粘贴任意文本</label>
<textarea id="apiKeysInput" class="form-control" placeholder="粘贴任何包含API密钥的文本,系统会自动提取以sk-开头的密钥..."></textarea>
<div class="form-helper">
<i class="fas fa-info-circle"></i>
智能识别:自动从粘贴文本中提取有效密钥,无需手动格式化
</div>
</div>
<div id="detectedKeysInfo" class="detected-keys" style="display: none;">
<i class="fas fa-search"></i>
已检测到 <span id="detectedKeysCount" class="detected-count">0</span> 个API密钥
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg">
<i class="fas fa-search"></i>
开始查询
</button>
</form>
<div id="loadingContainer" class="loading-container">
<div class="loading-animation">
<div class="loading-circle"></div>
</div>
<div>
<div class="loading-text">正在查询账户信息</div>
<div class="loading-subtext">请稍候,这可能需要一些时间...</div>
</div>
<div class="progress-container">
<div class="progress-info">
<span id="progressStatus">已查询 <span id="currentProgress">0</span>/<span id="totalKeys">0</span> 个</span>
<span id="progressPercentage">0%</span>
</div>
<div class="progress-bar-bg">
<div id="progressBar" class="progress-bar"></div>
</div>
</div>
</div>
</div>
</div>
<div id="resultsSection" class="results-section">
<div class="summary-grid">
<div class="summary-card summary-total">
<div class="summary-icon">
<i class="fas fa-search"></i>
</div>
<div class="summary-data">
<div class="summary-value" id="totalQueries">0</div>
<div class="summary-label">总查询数</div>
</div>
</div>
<div class="summary-card summary-valid">
<div class="summary-icon">
<i class="fas fa-check-circle"></i>
</div>
<div class="summary-data">
<div class="summary-value" id="validQueries">0</div>
<div class="summary-label">有效密钥</div>
</div>
</div>
<div class="summary-card summary-invalid">
<div class="summary-icon">
<i class="fas fa-times-circle"></i>
</div>
<div class="summary-data">
<div class="summary-value" id="invalidQueries">0</div>
<div class="summary-label">无效密钥</div>
</div>
</div>
<div class="summary-card summary-balance">
<div class="summary-icon">
<i class="fas fa-wallet"></i>
</div>
<div class="summary-data">
<div class="summary-value" id="totalBalance">¥0.00</div>
<div class="summary-label">总余额</div>
</div>
</div>
</div>
<div class="card" id="validKeysCard">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-check-circle"></i>
有效密钥 <span class="badge-count" id="validKeyCount">0</span>
</h2>
<div class="actions-group">
<button id="sortBalanceBtn" class="btn btn-outline btn-sm sort-button">
<i class="fas fa-sort-amount-down sort-icon"></i>
余额排序
</button>
<button id="copyCommaBtn" class="btn btn-outline btn-sm">
<i class="fas fa-copy"></i>
逗号分隔复制
</button>
<button id="copyLineBtn" class="btn btn-outline btn-sm">
<i class="fas fa-copy"></i>
换行分隔复制
</button>
<button id="exportCsvBtn" class="btn btn-outline btn-sm">
<i class="fas fa-download"></i>
导出CSV
</button>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>用户信息</th>
<th>账户状态</th>
<th>赠送余额</th>
<th>充值余额</th>
<th class="sortable" data-sort="totalBalance">
总余额
<span class="sort-icon"><i class="fas fa-sort"></i></span>
</th>
<th>API密钥</th>
</tr>
</thead>
<tbody id="validResultsBody">
</tbody>
</table>
</div>
<div class="cards-view" id="validCardsView">
</div>
<div id="emptyValidState" class="empty-state empty-valid">
<div class="empty-icon">
<i class="fas fa-check-circle"></i>
</div>
<h3 class="empty-title">暂无有效密钥</h3>
<p class="empty-message">查询完成后,有效的API密钥将显示在这里</p>
</div>
</div>
<div class="card" id="invalidKeysCard">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-times-circle"></i>
无效密钥 <span class="badge-count" id="invalidKeyCount">0</span>
</h2>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>API密钥</th>
<th>错误信息</th>
</tr>
</thead>
<tbody id="invalidResultsBody">
</tbody>
</table>
</div>
<div class="cards-view" id="invalidCardsView">
</div>
<div id="emptyInvalidState" class="empty-state empty-invalid">
<div class="empty-icon">
<i class="fas fa-times-circle"></i>
</div>
<h3 class="empty-title">暂无无效密钥</h3>
<p class="empty-message">查询完成后,无效或失败的API密钥将显示在这里</p>
</div>
</div>
</div>
<footer class="footer">
<div class="footer-text">
© <span id="currentYear"></span> SiliconFlow - 批量账户查询工具
</div>
</footer>
</div>
<div class="toast-container" id="toastContainer"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('currentYear').textContent = new Date().getFullYear();
const form = document.getElementById('apiKeysForm');
const apiKeysInput = document.getElementById('apiKeysInput');
const errorAlert = document.getElementById('errorAlert');
const errorMessage = document.getElementById('errorMessage');
const successAlert = document.getElementById('successAlert');
const successMessage = document.getElementById('successMessage');
const loadingContainer = document.getElementById('loadingContainer');
const currentProgress = document.getElementById('currentProgress');
const totalKeys = document.getElementById('totalKeys');
const progressBar = document.getElementById('progressBar');
const progressPercentage = document.getElementById('progressPercentage');
const resultsSection = document.getElementById('resultsSection');
const totalQueriesEl = document.getElementById('totalQueries');
const validQueriesEl = document.getElementById('validQueries');
const invalidQueriesEl = document.getElementById('invalidQueries');
const totalBalanceEl = document.getElementById('totalBalance');
const validKeyCount = document.getElementById('validKeyCount');
const invalidKeyCount = document.getElementById('invalidKeyCount');
const validResultsBody = document.getElementById('validResultsBody');
const invalidResultsBody = document.getElementById('invalidResultsBody');
const validCardsView = document.getElementById('validCardsView');
const invalidCardsView = document.getElementById('invalidCardsView');
const emptyValidState = document.getElementById('emptyValidState');
const emptyInvalidState = document.getElementById('emptyInvalidState');
const sortBalanceBtn = document.getElementById('sortBalanceBtn');
const copyCommaBtn = document.getElementById('copyCommaBtn');
const copyLineBtn = document.getElementById('copyLineBtn');
const exportCsvBtn = document.getElementById('exportCsvBtn');
const detectedKeysInfo = document.getElementById('detectedKeysInfo');
const detectedKeysCount = document.getElementById('detectedKeysCount');
const toastContainer = document.getElementById('toastContainer');
let validResults = [];
let invalidResults = [];
let extractedKeys = [];
let sortDirection = 'desc';
function showError(message) {
errorMessage.textContent = message;
errorAlert.classList.add('show');
successAlert.classList.remove('show');
setTimeout(() => {
errorAlert.classList.remove('show');
}, 5000);
}
function showSuccess(message) {
successMessage.textContent = message;
successAlert.classList.add('show');
errorAlert.classList.remove('show');
setTimeout(() => {
successAlert.classList.remove('show');
}, 5000);
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
const iconMap = {
success: 'check-circle',
error: 'exclamation-circle',
info: 'info-circle'
};
toast.innerHTML = `
<div class="toast-icon">
<i class="fas fa-${iconMap[type] || 'info-circle'}"></i>
</div>
<div class="toast-content">${message}</div>
`;
toastContainer.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
}, 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => {
toastContainer.removeChild(toast);
}, 300);
}, 3000);
}
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
showToast('复制成功', 'success');
return true;
} catch (err) {
console.error('复制失败:', err);
showToast('复制失败,请手动复制', 'error');
return false;
}
}
function extractApiKeys(text) {
if (!text.trim()) return [];
const regex = /sk-[a-zA-Z0-9]{48}/g;
const matches = text.match(regex) || [];
return [...new Set(matches)];
}
apiKeysInput.addEventListener('input', function() {
const text = this.value;
extractedKeys = extractApiKeys(text);
if (extractedKeys.length > 0) {
detectedKeysCount.textContent = extractedKeys.length;
detectedKeysInfo.style.display = 'flex';
} else {
detectedKeysInfo.style.display = 'none';
}
});
function formatCurrency(value) {
return `¥${parseFloat(value || 0).toFixed(4)}`;
}
function isPhoneEmail(email) {
return email && email.endsWith('@sf.cn');
}
function getRoleDisplay(role) {
if (!role || role === 'user' || role === 'user_role') {
return '';
}
const roleMap = {
'admin': { text: '管理员' },
'vip': { text: 'VIP用户' }
};
const roleInfo = roleMap[role.toLowerCase()] || { text: role };
return `<div class="user-role">${roleInfo.text}</div>`;
}
async function queryApiKey(apiKey) {
try {
const response = await fetch('https://api.siliconflow.cn/v1/user/info', {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`
}
});
const data = await response.json();
if (!response.ok || !data.status) {
throw new Error(data.message || '获取账户信息失败');
}
return {
success: true,
data: data.data,
apiKey
};
} catch (error) {
return {
success: false,
error: error.message || '查询失败',
apiKey
};
}
}
function createValidTableRow(result) {
const row = document.createElement('tr');
row.className = 'valid-entry';
row.dataset.totalBalance = parseFloat(result.data.totalBalance || 0);
const user = result.data;
const initial = ((user.name || '用户')[0] || 'U').toUpperCase();
let contactHTML = '';
if (user.email) {
if (isPhoneEmail(user.email)) {
contactHTML = `<div class="user-contact"><i class="fas fa-mobile-alt"></i> ${user.email.split('@')[0]}</div>`;
} else {
contactHTML = `<div class="user-contact"><i class="fas fa-envelope"></i> ${user.email}</div>`;
}
}
const isNormalStatus = user.status === 'normal';
const statusClass = isNormalStatus ? 'status-active' : 'status-warning';
const statusText = isNormalStatus ? '正常' : user.status;
const statusIcon = isNormalStatus ? 'check-circle' : 'exclamation-circle';
row.innerHTML = `
<td>
<div class="user-cell">
<div class="user-avatar">
${user.image ? `<img src="${user.image}" alt="${user.name || '用户'}" />` : initial}
${user.isAdmin ? '<div class="admin-badge"><i class="fas fa-crown"></i></div>' : ''}
</div>
<div class="user-info">
<div class="user-name">${user.name || '未命名用户'}</div>
<div class="user-meta">
${contactHTML}
${getRoleDisplay(user.role)}
</div>
</div>
</div>
</td>
<td>
<span class="status-badge ${statusClass}">
<i class="fas fa-${statusIcon}"></i>
${statusText}
</span>
</td>
<td><span class="amount-cell">${formatCurrency(user.balance)}</span></td>
<td><span class="amount-cell">${formatCurrency(user.chargeBalance)}</span></td>
<td><span class="amount-cell amount-total">${formatCurrency(user.totalBalance)}</span></td>
<td>
<div class="api-key-cell">
<span class="key-text">${result.apiKey}</span>
<button class="copy-btn" data-key="${result.apiKey}">
<i class="fas fa-copy"></i> 复制
</button>
</div>
</td>
`;
const copyBtn = row.querySelector('.copy-btn');
copyBtn.addEventListener('click', async function() {
const key = this.getAttribute('data-key');
const success = await copyToClipboard(key);
if (success) {
this.innerHTML = '<i class="fas fa-check"></i> 已复制';
this.classList.add('copied');
setTimeout(() => {
this.innerHTML = '<i class="fas fa-copy"></i> 复制';
this.classList.remove('copied');
}, 2000);
}
});
return row;
}
function createValidCard(result) {
const card = document.createElement('div');
card.className = 'result-card valid-card';
card.dataset.totalBalance = parseFloat(result.data.totalBalance || 0);
const user = result.data;
const initial = ((user.name || '用户')[0] || 'U').toUpperCase();
let contactText = '未设置';
let contactIcon = 'user';
if (user.email) {
if (isPhoneEmail(user.email)) {
contactText = user.email.split('@')[0];
contactIcon = 'mobile-alt';
} else {
contactText = user.email;
contactIcon = 'envelope';
}
}
const isNormalStatus = user.status === 'normal';
const statusClass = isNormalStatus ? 'status-active' : 'status-warning';
const statusText = isNormalStatus ? '正常' : user.status;
const statusIcon = isNormalStatus ? 'check-circle' : 'exclamation-circle';
card.innerHTML = `
<div class="card-row">
<div class="user-cell">
<div class="user-avatar">
${user.image ? `<img src="${user.image}" alt="${user.name || '用户'}" />` : initial}
${user.isAdmin ? '<div class="admin-badge"><i class="fas fa-crown"></i></div>' : ''}
</div>
<div class="user-info">
<div class="user-name">${user.name || '未命名用户'}</div>
<div class="user-meta">
<div class="user-contact">
<i class="fas fa-${contactIcon}"></i> ${contactText}
</div>
${getRoleDisplay(user.role)}
</div>
</div>
</div>
<span class="status-badge ${statusClass}">
<i class="fas fa-${statusIcon}"></i>
${statusText}
</span>
</div>
<div class="card-divider"></div>
<div class="card-row" style="justify-content: center;">
<div class="balance-group">
<div class="card-label">赠送余额</div>
<div class="card-value amount-cell">${formatCurrency(user.balance)}</div>
</div>
<div class="balance-group">
<div class="card-label">充值余额</div>
<div class="card-value amount-cell">${formatCurrency(user.chargeBalance)}</div>
</div>
<div class="balance-group">
<div class="card-label">总余额</div>
<div class="card-value amount-cell amount-total">${formatCurrency(user.totalBalance)}</div>
</div>
</div>
<div class="card-divider"></div>
<div class="card-row">
<div style="flex: 1; min-width: 0;">
<div class="card-label">API密钥</div>
<div class="card-value api-key-cell">
<span class="key-text">${result.apiKey}</span>
</div>
</div>
<button class="copy-btn" data-key="${result.apiKey}">
<i class="fas fa-copy"></i> 复制
</button>
</div>
`;
const copyBtn = card.querySelector('.copy-btn');
copyBtn.addEventListener('click', async function() {
const key = this.getAttribute('data-key');
const success = await copyToClipboard(key);
if (success) {
this.innerHTML = '<i class="fas fa-check"></i> 已复制';
this.classList.add('copied');
setTimeout(() => {
this.innerHTML = '<i class="fas fa-copy"></i> 复制';
this.classList.remove('copied');
}, 2000);
}
});
return card;
}
function createInvalidTableRow(result) {
const row = document.createElement('tr');
row.className = 'invalid-entry';
row.innerHTML = `
<td>
<div class="api-key-cell">
<span class="key-text">${result.apiKey}</span>
<button class="copy-btn" data-key="${result.apiKey}">
<i class="fas fa-copy"></i> 复制
</button>
</div>
</td>
<td>
<div class="error-cell">${result.error}</div>
</td>
`;
const copyBtn = row.querySelector('.copy-btn');
copyBtn.addEventListener('click', async function() {
const key = this.getAttribute('data-key');
const success = await copyToClipboard(key);
if (success) {
this.innerHTML = '<i class="fas fa-check"></i> 已复制';
this.classList.add('copied');
setTimeout(() => {
this.innerHTML = '<i class="fas fa-copy"></i> 复制';
this.classList.remove('copied');
}, 2000);
}
});
return row;
}
function createInvalidCard(result) {
const card = document.createElement('div');
card.className = 'result-card invalid-card';
card.innerHTML = `
<div class="card-row">
<div style="flex: 1; min-width: 0;">
<div class="card-label">API密钥</div>
<div class="card-value api-key-cell">
<span class="key-text">${result.apiKey}</span>
</div>
</div>
<button class="copy-btn" data-key="${result.apiKey}">
<i class="fas fa-copy"></i> 复制
</button>
</div>
<div class="card-divider"></div>
<div>
<div class="card-label">错误信息</div>
<div class="card-value error-cell">${result.error}</div>
</div>
`;
const copyBtn = card.querySelector('.copy-btn');
copyBtn.addEventListener('click', async function() {
const key = this.getAttribute('data-key');
const success = await copyToClipboard(key);
if (success) {
this.innerHTML = '<i class="fas fa-check"></i> 已复制';
this.classList.add('copied');
setTimeout(() => {
this.innerHTML = '<i class="fas fa-copy"></i> 复制';
this.classList.remove('copied');
}, 2000);
}
});
return card;
}
function sortResultsByBalance() {
sortDirection = sortDirection === 'desc' ? 'asc' : 'desc';
const sortIcon = sortBalanceBtn.querySelector('.sort-icon');
sortIcon.className = `fas fa-sort-amount-${sortDirection === 'desc' ? 'down' : 'up'} sort-icon`;
const tableHeaderIcon = document.querySelector('th[data-sort="totalBalance"] .sort-icon i');
tableHeaderIcon.className = `fas fa-sort-${sortDirection === 'desc' ? 'down' : 'up'}`;
const sortedResults = [...validResults];
sortedResults.sort((a, b) => {
const balanceA = parseFloat(a.data.totalBalance || 0);
const balanceB = parseFloat(b.data.totalBalance || 0);
if (sortDirection === 'desc') {
return balanceB - balanceA;
} else {
return balanceA - balanceB;
}
});
validResultsBody.innerHTML = '';
validCardsView.innerHTML = '';
sortedResults.forEach(result => {
validResultsBody.appendChild(createValidTableRow(result));
validCardsView.appendChild(createValidCard(result));
});
showToast(`已按总余额${sortDirection === 'desc' ? '从高到低' : '从低到高'}排序`, 'success');
}
async function batchQueryApiKeys(apiKeys) {
validResults = [];
invalidResults = [];
validResultsBody.innerHTML = '';
invalidResultsBody.innerHTML = '';
validCardsView.innerHTML = '';
invalidCardsView.innerHTML = '';
totalKeys.textContent = apiKeys.length;
let totalBalanceSum = 0;
for (let i = 0; i < apiKeys.length; i++) {
const apiKey = apiKeys[i];
const progress = Math.round(((i + 1) / apiKeys.length) * 100);
currentProgress.textContent = i + 1;
progressBar.style.width = `${progress}%`;
progressPercentage.textContent = `${progress}%`;
const result = await queryApiKey(apiKey);
if (result.success) {
validResults.push(result);
totalBalanceSum += parseFloat(result.data.totalBalance || 0);
validResultsBody.appendChild(createValidTableRow(result));
validCardsView.appendChild(createValidCard(result));
} else {
invalidResults.push(result);
invalidResultsBody.appendChild(createInvalidTableRow(result));
invalidCardsView.appendChild(createInvalidCard(result));
}
await new Promise(resolve => setTimeout(resolve, 50));
}
totalQueriesEl.textContent = apiKeys.length;
validQueriesEl.textContent = validResults.length;
invalidQueriesEl.textContent = invalidResults.length;
totalBalanceEl.textContent = formatCurrency(totalBalanceSum);
validKeyCount.textContent = validResults.length;
invalidKeyCount.textContent = invalidResults.length;
emptyValidState.style.display = validResults.length > 0 ? 'none' : 'flex';
emptyInvalidState.style.display = invalidResults.length > 0 ? 'none' : 'flex';
if (validResults.length > 0) {
const sortedResults = [...validResults].sort((a, b) => {
const balanceA = parseFloat(a.data.totalBalance || 0);
const balanceB = parseFloat(b.data.totalBalance || 0);
return balanceB - balanceA;
});
validResultsBody.innerHTML = '';
validCardsView.innerHTML = '';
sortedResults.forEach(result => {
validResultsBody.appendChild(createValidTableRow(result));
validCardsView.appendChild(createValidCard(result));
});
const sortIcon = sortBalanceBtn.querySelector('.sort-icon');
if (sortIcon) sortIcon.className = 'fas fa-sort-amount-down sort-icon';
const tableHeaderIcon = document.querySelector('th[data-sort="totalBalance"] .sort-icon i');
if (tableHeaderIcon) tableHeaderIcon.className = 'fas fa-sort-down';
}
return {
totalQueries: apiKeys.length,
validResults,
invalidResults,
totalBalanceSum
};
}
form.addEventListener('submit', async function(e) {
e.preventDefault();
const text = apiKeysInput.value.trim();
if (!text) {
showError('请输入包含API密钥的文本');
return;
}
const apiKeys = extractApiKeys(text);
if (apiKeys.length === 0) {
showError('未检测到有效的API密钥(格式:sk-开头加48个字符)');
return;
}
errorAlert.classList.remove('show');
successAlert.classList.remove('show');
form.style.display = 'none';
loadingContainer.classList.add('show');
try {
await batchQueryApiKeys(apiKeys);
resultsSection.classList.add('show');
showSuccess(`成功查询 ${apiKeys.length} 个API密钥,其中 ${validResults.length} 个有效`);
} catch (error) {
showError('批量查询过程中发生错误');
console.error('批量查询错误:', error);
} finally {
loadingContainer.classList.remove('show');
form.style.display = 'block';
}
});
sortBalanceBtn.addEventListener('click', sortResultsByBalance);
document.querySelector('th[data-sort="totalBalance"]').addEventListener('click', sortResultsByBalance);
copyCommaBtn.addEventListener('click', function() {
if (validResults.length === 0) {
showToast('没有可复制的有效密钥', 'error');
return;
}
const keys = validResults.map(result => result.apiKey);
copyToClipboard(keys.join(','));
});
copyLineBtn.addEventListener('click', function() {
if (validResults.length === 0) {
showToast('没有可复制的有效密钥', 'error');
return;
}
const keys = validResults.map(result => result.apiKey);
copyToClipboard(keys.join('\n'));
});
exportCsvBtn.addEventListener('click', function() {
if (validResults.length === 0 && invalidResults.length === 0) {
showToast('没有可导出的查询结果', 'error');
return;
}
const headers = ['"状态","API密钥","用户名","联系方式","账户状态","赠送余额","充值余额","总余额","错误信息"'];
const rows = [];
validResults.forEach(result => {
const user = result.data;
const userName = user.name || '未命名用户';
let contact = '';
if (user.email) {
contact = isPhoneEmail(user.email) ? user.email.split('@')[0] : user.email;
} else {
contact = '未设置';
}
const status = user.status === 'normal' ? '正常' : user.status;
const balance = parseFloat(user.balance || 0).toFixed(4);
const chargeBalance = parseFloat(user.chargeBalance || 0).toFixed(4);
const totalBalance = parseFloat(user.totalBalance || 0).toFixed(4);
rows.push(`"有效","${result.apiKey}","${userName}","${contact}","${status}","${balance}","${chargeBalance}","${totalBalance}",""`);
});
invalidResults.forEach(result => {
rows.push(`"无效","${result.apiKey}","","","","","","","${result.error}"`);
});
const csvData = headers.concat(rows).join('\n');
const blob = new Blob(["\uFEFF" + csvData], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `SiliconFlow_查询结果_${timestamp}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showToast('导出成功', 'success');
});
apiKeysInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.ctrlKey) {
form.dispatchEvent(new Event('submit'));
}
});
});
</script>
93 Likes
@yeahhe 还不快试试
8 Likes
优秀,太优秀了
6 Likes
已使用,牛叉~
5 Likes
太酷啦~
3 Likes
感谢楼主的分享~用上了
2 Likes
拿来主义,发动!
2 Likes
把余额不够的区分开来比较好吧
佬可以加个设置最低余额吗
好用,好看,顶一个
1 Like
支持批量吗佬
为啥这么帅
真的是很帅
连着贴了好几个
2 Likes
帅。省事了
确实帅!
已使用 确实帅
确实好看
用佬的代码改了一下,一个cloudflare worker版本,在线更方便
贴一个demo: SiliconFlow - 批量余额查询
额外加了这两个:
@Baby1 快来
cf-worker-siliconflow-checker.txt (75.7 KB)
8 Likes