mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:16:17 +00:00
Merge branch 'main' of https://github.com/Wei-Shaw/claude-relay-service
# Conflicts: # web/admin-spa/dist/assets/LoginView-BJ0LLv16.js # web/admin-spa/dist/assets/LogoTitle-DHj-MjwS.js # web/admin-spa/dist/assets/MainLayout-CLydIeqJ.js # web/admin-spa/dist/assets/SettingsView-DicW12bL.js # web/admin-spa/dist/assets/index-HYE9xPuR.js # web/admin-spa/dist/index.html # web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue # web/admin-spa/src/components/apikeys/EditApiKeyModal.vue # web/admin-spa/src/views/ApiKeysView.vue
This commit is contained in:
@@ -45,6 +45,7 @@ TOKEN_USAGE_RETENTION=2592000000
|
|||||||
HEALTH_CHECK_INTERVAL=60000
|
HEALTH_CHECK_INTERVAL=60000
|
||||||
SYSTEM_TIMEZONE=Asia/Shanghai
|
SYSTEM_TIMEZONE=Asia/Shanghai
|
||||||
TIMEZONE_OFFSET=8
|
TIMEZONE_OFFSET=8
|
||||||
|
METRICS_WINDOW=5 # 实时指标统计窗口(分钟),可选1-60,默认5分钟
|
||||||
|
|
||||||
# 🎨 Web 界面配置
|
# 🎨 Web 界面配置
|
||||||
WEB_TITLE=Claude Relay Service
|
WEB_TITLE=Claude Relay Service
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/app.js",
|
"start": "node src/app.js",
|
||||||
"dev": "nodemon src/app.js",
|
"dev": "nodemon src/app.js",
|
||||||
"build:web": "cd web && npm run build",
|
"build:web": "cd web/admin-spa && npm run build",
|
||||||
"install:web": "cd web && npm install",
|
"install:web": "cd web/admin-spa && npm install",
|
||||||
"setup": "node scripts/setup.js",
|
"setup": "node scripts/setup.js",
|
||||||
"cli": "node cli/index.js",
|
"cli": "node cli/index.js",
|
||||||
"init:costs": "node src/cli/initCosts.js",
|
"init:costs": "node src/cli/initCosts.js",
|
||||||
|
|||||||
@@ -186,6 +186,10 @@ class RedisClient {
|
|||||||
const keyModelMonthly = `usage:${keyId}:model:monthly:${model}:${currentMonth}`;
|
const keyModelMonthly = `usage:${keyId}:model:monthly:${model}:${currentMonth}`;
|
||||||
const keyModelHourly = `usage:${keyId}:model:hourly:${model}:${currentHour}`; // 新增API Key模型小时级别
|
const keyModelHourly = `usage:${keyId}:model:hourly:${model}:${currentHour}`; // 新增API Key模型小时级别
|
||||||
|
|
||||||
|
// 新增:系统级分钟统计
|
||||||
|
const minuteTimestamp = Math.floor(now.getTime() / 60000);
|
||||||
|
const systemMinuteKey = `system:metrics:minute:${minuteTimestamp}`;
|
||||||
|
|
||||||
// 智能处理输入输出token分配
|
// 智能处理输入输出token分配
|
||||||
const finalInputTokens = inputTokens || 0;
|
const finalInputTokens = inputTokens || 0;
|
||||||
const finalOutputTokens = outputTokens || (finalInputTokens > 0 ? 0 : tokens);
|
const finalOutputTokens = outputTokens || (finalInputTokens > 0 ? 0 : tokens);
|
||||||
@@ -197,96 +201,122 @@ class RedisClient {
|
|||||||
// 核心token(不包括缓存)- 用于与历史数据兼容
|
// 核心token(不包括缓存)- 用于与历史数据兼容
|
||||||
const coreTokens = finalInputTokens + finalOutputTokens;
|
const coreTokens = finalInputTokens + finalOutputTokens;
|
||||||
|
|
||||||
await Promise.all([
|
// 使用Pipeline优化性能
|
||||||
|
const pipeline = this.client.pipeline();
|
||||||
|
|
||||||
|
// 现有的统计保持不变
|
||||||
// 核心token统计(保持向后兼容)
|
// 核心token统计(保持向后兼容)
|
||||||
this.client.hincrby(key, 'totalTokens', coreTokens),
|
pipeline.hincrby(key, 'totalTokens', coreTokens);
|
||||||
this.client.hincrby(key, 'totalInputTokens', finalInputTokens),
|
pipeline.hincrby(key, 'totalInputTokens', finalInputTokens);
|
||||||
this.client.hincrby(key, 'totalOutputTokens', finalOutputTokens),
|
pipeline.hincrby(key, 'totalOutputTokens', finalOutputTokens);
|
||||||
// 缓存token统计(新增)
|
// 缓存token统计(新增)
|
||||||
this.client.hincrby(key, 'totalCacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(key, 'totalCacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(key, 'totalCacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(key, 'totalCacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(key, 'totalAllTokens', totalTokens), // 包含所有类型的总token
|
pipeline.hincrby(key, 'totalAllTokens', totalTokens); // 包含所有类型的总token
|
||||||
// 请求计数
|
// 请求计数
|
||||||
this.client.hincrby(key, 'totalRequests', 1),
|
pipeline.hincrby(key, 'totalRequests', 1);
|
||||||
|
|
||||||
// 每日统计
|
// 每日统计
|
||||||
this.client.hincrby(daily, 'tokens', coreTokens),
|
pipeline.hincrby(daily, 'tokens', coreTokens);
|
||||||
this.client.hincrby(daily, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(daily, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(daily, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(daily, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(daily, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(daily, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(daily, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(daily, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(daily, 'allTokens', totalTokens),
|
pipeline.hincrby(daily, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(daily, 'requests', 1),
|
pipeline.hincrby(daily, 'requests', 1);
|
||||||
|
|
||||||
// 每月统计
|
// 每月统计
|
||||||
this.client.hincrby(monthly, 'tokens', coreTokens),
|
pipeline.hincrby(monthly, 'tokens', coreTokens);
|
||||||
this.client.hincrby(monthly, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(monthly, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(monthly, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(monthly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(monthly, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(monthly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(monthly, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(monthly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(monthly, 'allTokens', totalTokens),
|
pipeline.hincrby(monthly, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(monthly, 'requests', 1),
|
pipeline.hincrby(monthly, 'requests', 1);
|
||||||
|
|
||||||
// 按模型统计 - 每日
|
// 按模型统计 - 每日
|
||||||
this.client.hincrby(modelDaily, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(modelDaily, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(modelDaily, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(modelDaily, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(modelDaily, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(modelDaily, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(modelDaily, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(modelDaily, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(modelDaily, 'allTokens', totalTokens),
|
pipeline.hincrby(modelDaily, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(modelDaily, 'requests', 1),
|
pipeline.hincrby(modelDaily, 'requests', 1);
|
||||||
|
|
||||||
// 按模型统计 - 每月
|
// 按模型统计 - 每月
|
||||||
this.client.hincrby(modelMonthly, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(modelMonthly, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(modelMonthly, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(modelMonthly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(modelMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(modelMonthly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(modelMonthly, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(modelMonthly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(modelMonthly, 'allTokens', totalTokens),
|
pipeline.hincrby(modelMonthly, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(modelMonthly, 'requests', 1),
|
pipeline.hincrby(modelMonthly, 'requests', 1);
|
||||||
|
|
||||||
// API Key级别的模型统计 - 每日
|
// API Key级别的模型统计 - 每日
|
||||||
this.client.hincrby(keyModelDaily, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(keyModelDaily, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(keyModelDaily, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(keyModelDaily, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(keyModelDaily, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(keyModelDaily, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(keyModelDaily, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(keyModelDaily, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(keyModelDaily, 'allTokens', totalTokens),
|
pipeline.hincrby(keyModelDaily, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(keyModelDaily, 'requests', 1),
|
pipeline.hincrby(keyModelDaily, 'requests', 1);
|
||||||
|
|
||||||
// API Key级别的模型统计 - 每月
|
// API Key级别的模型统计 - 每月
|
||||||
this.client.hincrby(keyModelMonthly, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(keyModelMonthly, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(keyModelMonthly, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(keyModelMonthly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(keyModelMonthly, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(keyModelMonthly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(keyModelMonthly, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(keyModelMonthly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(keyModelMonthly, 'allTokens', totalTokens),
|
pipeline.hincrby(keyModelMonthly, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(keyModelMonthly, 'requests', 1),
|
pipeline.hincrby(keyModelMonthly, 'requests', 1);
|
||||||
|
|
||||||
// 小时级别统计
|
// 小时级别统计
|
||||||
this.client.hincrby(hourly, 'tokens', coreTokens),
|
pipeline.hincrby(hourly, 'tokens', coreTokens);
|
||||||
this.client.hincrby(hourly, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(hourly, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(hourly, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(hourly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(hourly, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(hourly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(hourly, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(hourly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(hourly, 'allTokens', totalTokens),
|
pipeline.hincrby(hourly, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(hourly, 'requests', 1),
|
pipeline.hincrby(hourly, 'requests', 1);
|
||||||
|
|
||||||
// 按模型统计 - 每小时
|
// 按模型统计 - 每小时
|
||||||
this.client.hincrby(modelHourly, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(modelHourly, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(modelHourly, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(modelHourly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(modelHourly, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(modelHourly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(modelHourly, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(modelHourly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(modelHourly, 'allTokens', totalTokens),
|
pipeline.hincrby(modelHourly, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(modelHourly, 'requests', 1),
|
pipeline.hincrby(modelHourly, 'requests', 1);
|
||||||
|
|
||||||
// API Key级别的模型统计 - 每小时
|
// API Key级别的模型统计 - 每小时
|
||||||
this.client.hincrby(keyModelHourly, 'inputTokens', finalInputTokens),
|
pipeline.hincrby(keyModelHourly, 'inputTokens', finalInputTokens);
|
||||||
this.client.hincrby(keyModelHourly, 'outputTokens', finalOutputTokens),
|
pipeline.hincrby(keyModelHourly, 'outputTokens', finalOutputTokens);
|
||||||
this.client.hincrby(keyModelHourly, 'cacheCreateTokens', finalCacheCreateTokens),
|
pipeline.hincrby(keyModelHourly, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
this.client.hincrby(keyModelHourly, 'cacheReadTokens', finalCacheReadTokens),
|
pipeline.hincrby(keyModelHourly, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
this.client.hincrby(keyModelHourly, 'allTokens', totalTokens),
|
pipeline.hincrby(keyModelHourly, 'allTokens', totalTokens);
|
||||||
this.client.hincrby(keyModelHourly, 'requests', 1),
|
pipeline.hincrby(keyModelHourly, 'requests', 1);
|
||||||
|
|
||||||
|
// 新增:系统级分钟统计
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'requests', 1);
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'totalTokens', totalTokens);
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'inputTokens', finalInputTokens);
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'outputTokens', finalOutputTokens);
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'cacheCreateTokens', finalCacheCreateTokens);
|
||||||
|
pipeline.hincrby(systemMinuteKey, 'cacheReadTokens', finalCacheReadTokens);
|
||||||
|
|
||||||
// 设置过期时间
|
// 设置过期时间
|
||||||
this.client.expire(daily, 86400 * 32), // 32天过期
|
pipeline.expire(daily, 86400 * 32); // 32天过期
|
||||||
this.client.expire(monthly, 86400 * 365), // 1年过期
|
pipeline.expire(monthly, 86400 * 365); // 1年过期
|
||||||
this.client.expire(hourly, 86400 * 7), // 小时统计7天过期
|
pipeline.expire(hourly, 86400 * 7); // 小时统计7天过期
|
||||||
this.client.expire(modelDaily, 86400 * 32), // 模型每日统计32天过期
|
pipeline.expire(modelDaily, 86400 * 32); // 模型每日统计32天过期
|
||||||
this.client.expire(modelMonthly, 86400 * 365), // 模型每月统计1年过期
|
pipeline.expire(modelMonthly, 86400 * 365); // 模型每月统计1年过期
|
||||||
this.client.expire(modelHourly, 86400 * 7), // 模型小时统计7天过期
|
pipeline.expire(modelHourly, 86400 * 7); // 模型小时统计7天过期
|
||||||
this.client.expire(keyModelDaily, 86400 * 32), // API Key模型每日统计32天过期
|
pipeline.expire(keyModelDaily, 86400 * 32); // API Key模型每日统计32天过期
|
||||||
this.client.expire(keyModelMonthly, 86400 * 365), // API Key模型每月统计1年过期
|
pipeline.expire(keyModelMonthly, 86400 * 365); // API Key模型每月统计1年过期
|
||||||
this.client.expire(keyModelHourly, 86400 * 7) // API Key模型小时统计7天过期
|
pipeline.expire(keyModelHourly, 86400 * 7); // API Key模型小时统计7天过期
|
||||||
]);
|
|
||||||
|
// 系统级分钟统计的过期时间(窗口时间的2倍)
|
||||||
|
const config = require('../../config/config');
|
||||||
|
const metricsWindow = config.system.metricsWindow;
|
||||||
|
pipeline.expire(systemMinuteKey, metricsWindow * 60 * 2);
|
||||||
|
|
||||||
|
// 执行Pipeline
|
||||||
|
await pipeline.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📊 记录账户级别的使用统计
|
// 📊 记录账户级别的使用统计
|
||||||
@@ -974,6 +1004,96 @@ class RedisClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 📊 获取实时系统指标(基于滑动窗口)
|
||||||
|
async getRealtimeSystemMetrics() {
|
||||||
|
try {
|
||||||
|
const config = require('../../config/config');
|
||||||
|
const windowMinutes = config.system.metricsWindow || 5;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const currentMinute = Math.floor(now.getTime() / 60000);
|
||||||
|
|
||||||
|
// 调试:打印当前时间和分钟时间戳
|
||||||
|
logger.debug(`🔍 Realtime metrics - Current time: ${now.toISOString()}, Minute timestamp: ${currentMinute}`);
|
||||||
|
|
||||||
|
// 使用Pipeline批量获取窗口内的所有分钟数据
|
||||||
|
const pipeline = this.client.pipeline();
|
||||||
|
const minuteKeys = [];
|
||||||
|
for (let i = 0; i < windowMinutes; i++) {
|
||||||
|
const minuteKey = `system:metrics:minute:${currentMinute - i}`;
|
||||||
|
minuteKeys.push(minuteKey);
|
||||||
|
pipeline.hgetall(minuteKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`🔍 Realtime metrics - Checking keys: ${minuteKeys.join(', ')}`);
|
||||||
|
|
||||||
|
const results = await pipeline.exec();
|
||||||
|
|
||||||
|
// 聚合计算
|
||||||
|
let totalRequests = 0;
|
||||||
|
let totalTokens = 0;
|
||||||
|
let totalInputTokens = 0;
|
||||||
|
let totalOutputTokens = 0;
|
||||||
|
let totalCacheCreateTokens = 0;
|
||||||
|
let totalCacheReadTokens = 0;
|
||||||
|
let validDataCount = 0;
|
||||||
|
|
||||||
|
results.forEach(([err, data], index) => {
|
||||||
|
if (!err && data && Object.keys(data).length > 0) {
|
||||||
|
validDataCount++;
|
||||||
|
totalRequests += parseInt(data.requests || 0);
|
||||||
|
totalTokens += parseInt(data.totalTokens || 0);
|
||||||
|
totalInputTokens += parseInt(data.inputTokens || 0);
|
||||||
|
totalOutputTokens += parseInt(data.outputTokens || 0);
|
||||||
|
totalCacheCreateTokens += parseInt(data.cacheCreateTokens || 0);
|
||||||
|
totalCacheReadTokens += parseInt(data.cacheReadTokens || 0);
|
||||||
|
|
||||||
|
logger.debug(`🔍 Realtime metrics - Key ${minuteKeys[index]} data:`, {
|
||||||
|
requests: data.requests,
|
||||||
|
totalTokens: data.totalTokens
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(`🔍 Realtime metrics - Valid data count: ${validDataCount}/${windowMinutes}, Total requests: ${totalRequests}, Total tokens: ${totalTokens}`);
|
||||||
|
|
||||||
|
// 计算平均值(每分钟)
|
||||||
|
const realtimeRPM = windowMinutes > 0 ? Math.round((totalRequests / windowMinutes) * 100) / 100 : 0;
|
||||||
|
const realtimeTPM = windowMinutes > 0 ? Math.round((totalTokens / windowMinutes) * 100) / 100 : 0;
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
realtimeRPM,
|
||||||
|
realtimeTPM,
|
||||||
|
windowMinutes,
|
||||||
|
totalRequests,
|
||||||
|
totalTokens,
|
||||||
|
totalInputTokens,
|
||||||
|
totalOutputTokens,
|
||||||
|
totalCacheCreateTokens,
|
||||||
|
totalCacheReadTokens
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.debug('🔍 Realtime metrics - Final result:', result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting realtime system metrics:', error);
|
||||||
|
// 如果出错,返回历史平均值作为降级方案
|
||||||
|
const historicalMetrics = await this.getSystemAverages();
|
||||||
|
return {
|
||||||
|
realtimeRPM: historicalMetrics.systemRPM,
|
||||||
|
realtimeTPM: historicalMetrics.systemTPM,
|
||||||
|
windowMinutes: 0, // 标识使用了历史数据
|
||||||
|
totalRequests: 0,
|
||||||
|
totalTokens: historicalMetrics.totalTokens,
|
||||||
|
totalInputTokens: historicalMetrics.totalInputTokens,
|
||||||
|
totalOutputTokens: historicalMetrics.totalOutputTokens,
|
||||||
|
totalCacheCreateTokens: 0,
|
||||||
|
totalCacheReadTokens: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🔗 会话sticky映射管理
|
// 🔗 会话sticky映射管理
|
||||||
async setSessionAccountMapping(sessionHash, accountId, ttl = 3600) {
|
async setSessionAccountMapping(sessionHash, accountId, ttl = 3600) {
|
||||||
const key = `sticky_session:${sessionHash}`;
|
const key = `sticky_session:${sessionHash}`;
|
||||||
|
|||||||
@@ -1268,13 +1268,15 @@ router.get('/accounts/:accountId/usage-stats', authenticateAdmin, async (req, re
|
|||||||
// 获取系统概览
|
// 获取系统概览
|
||||||
router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [, apiKeys, claudeAccounts, geminiAccounts, todayStats, systemAverages] = await Promise.all([
|
const [, apiKeys, claudeAccounts, claudeConsoleAccounts, geminiAccounts, todayStats, systemAverages, realtimeMetrics] = await Promise.all([
|
||||||
redis.getSystemStats(),
|
redis.getSystemStats(),
|
||||||
apiKeyService.getAllApiKeys(),
|
apiKeyService.getAllApiKeys(),
|
||||||
claudeAccountService.getAllAccounts(),
|
claudeAccountService.getAllAccounts(),
|
||||||
|
claudeConsoleAccountService.getAllAccounts(),
|
||||||
geminiAccountService.getAllAccounts(),
|
geminiAccountService.getAllAccounts(),
|
||||||
redis.getTodayStats(),
|
redis.getTodayStats(),
|
||||||
redis.getSystemAverages()
|
redis.getSystemAverages(),
|
||||||
|
redis.getRealtimeSystemMetrics()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 计算使用统计(统一使用allTokens)
|
// 计算使用统计(统一使用allTokens)
|
||||||
@@ -1289,6 +1291,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
const activeApiKeys = apiKeys.filter(key => key.isActive).length;
|
const activeApiKeys = apiKeys.filter(key => key.isActive).length;
|
||||||
const activeClaudeAccounts = claudeAccounts.filter(acc => acc.isActive && acc.status === 'active').length;
|
const activeClaudeAccounts = claudeAccounts.filter(acc => acc.isActive && acc.status === 'active').length;
|
||||||
const rateLimitedClaudeAccounts = claudeAccounts.filter(acc => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited).length;
|
const rateLimitedClaudeAccounts = claudeAccounts.filter(acc => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited).length;
|
||||||
|
const activeClaudeConsoleAccounts = claudeConsoleAccounts.filter(acc => acc.isActive && acc.status === 'active').length;
|
||||||
|
const rateLimitedClaudeConsoleAccounts = claudeConsoleAccounts.filter(acc => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited).length;
|
||||||
const activeGeminiAccounts = geminiAccounts.filter(acc => acc.isActive && acc.status === 'active').length;
|
const activeGeminiAccounts = geminiAccounts.filter(acc => acc.isActive && acc.status === 'active').length;
|
||||||
const rateLimitedGeminiAccounts = geminiAccounts.filter(acc => acc.rateLimitStatus === 'limited').length;
|
const rateLimitedGeminiAccounts = geminiAccounts.filter(acc => acc.rateLimitStatus === 'limited').length;
|
||||||
|
|
||||||
@@ -1296,9 +1300,9 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
overview: {
|
overview: {
|
||||||
totalApiKeys: apiKeys.length,
|
totalApiKeys: apiKeys.length,
|
||||||
activeApiKeys,
|
activeApiKeys,
|
||||||
totalClaudeAccounts: claudeAccounts.length,
|
totalClaudeAccounts: claudeAccounts.length + claudeConsoleAccounts.length,
|
||||||
activeClaudeAccounts: activeClaudeAccounts,
|
activeClaudeAccounts: activeClaudeAccounts + activeClaudeConsoleAccounts,
|
||||||
rateLimitedClaudeAccounts: rateLimitedClaudeAccounts,
|
rateLimitedClaudeAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts,
|
||||||
totalGeminiAccounts: geminiAccounts.length,
|
totalGeminiAccounts: geminiAccounts.length,
|
||||||
activeGeminiAccounts: activeGeminiAccounts,
|
activeGeminiAccounts: activeGeminiAccounts,
|
||||||
rateLimitedGeminiAccounts: rateLimitedGeminiAccounts,
|
rateLimitedGeminiAccounts: rateLimitedGeminiAccounts,
|
||||||
@@ -1323,9 +1327,15 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
|||||||
rpm: systemAverages.systemRPM,
|
rpm: systemAverages.systemRPM,
|
||||||
tpm: systemAverages.systemTPM
|
tpm: systemAverages.systemTPM
|
||||||
},
|
},
|
||||||
|
realtimeMetrics: {
|
||||||
|
rpm: realtimeMetrics.realtimeRPM,
|
||||||
|
tpm: realtimeMetrics.realtimeTPM,
|
||||||
|
windowMinutes: realtimeMetrics.windowMinutes,
|
||||||
|
isHistorical: realtimeMetrics.windowMinutes === 0 // 标识是否使用了历史数据
|
||||||
|
},
|
||||||
systemHealth: {
|
systemHealth: {
|
||||||
redisConnected: redis.isConnected,
|
redisConnected: redis.isConnected,
|
||||||
claudeAccountsHealthy: activeClaudeAccounts > 0,
|
claudeAccountsHealthy: (activeClaudeAccounts + activeClaudeConsoleAccounts) > 0,
|
||||||
geminiAccountsHealthy: activeGeminiAccounts > 0,
|
geminiAccountsHealthy: activeGeminiAccounts > 0,
|
||||||
uptime: process.uptime()
|
uptime: process.uptime()
|
||||||
},
|
},
|
||||||
@@ -1490,7 +1500,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
endTime = new Date(endDate);
|
endTime = new Date(endDate);
|
||||||
|
|
||||||
// 调试日志
|
// 调试日志
|
||||||
logger.info(`📊 Usage trend hour granularity - received times:`);
|
logger.info('📊 Usage trend hour granularity - received times:');
|
||||||
logger.info(` startDate (raw): ${startDate}`);
|
logger.info(` startDate (raw): ${startDate}`);
|
||||||
logger.info(` endDate (raw): ${endDate}`);
|
logger.info(` endDate (raw): ${endDate}`);
|
||||||
logger.info(` startTime (parsed): ${startTime.toISOString()}`);
|
logger.info(` startTime (parsed): ${startTime.toISOString()}`);
|
||||||
@@ -1978,6 +1988,8 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
apiKeys: {}
|
apiKeys: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 先收集基础数据
|
||||||
|
const apiKeyDataMap = new Map();
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const match = key.match(/usage:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/);
|
const match = key.match(/usage:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/);
|
||||||
if (!match) continue;
|
if (!match) continue;
|
||||||
@@ -1986,19 +1998,80 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const data = await client.hgetall(key);
|
const data = await client.hgetall(key);
|
||||||
|
|
||||||
if (data && apiKeyMap.has(apiKeyId)) {
|
if (data && apiKeyMap.has(apiKeyId)) {
|
||||||
const totalTokens = (parseInt(data.inputTokens) || 0) +
|
const inputTokens = parseInt(data.inputTokens) || 0;
|
||||||
(parseInt(data.outputTokens) || 0) +
|
const outputTokens = parseInt(data.outputTokens) || 0;
|
||||||
(parseInt(data.cacheCreateTokens) || 0) +
|
const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0;
|
||||||
(parseInt(data.cacheReadTokens) || 0);
|
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0;
|
||||||
|
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
||||||
|
|
||||||
hourData.apiKeys[apiKeyId] = {
|
apiKeyDataMap.set(apiKeyId, {
|
||||||
name: apiKeyMap.get(apiKeyId).name,
|
name: apiKeyMap.get(apiKeyId).name,
|
||||||
tokens: totalTokens,
|
tokens: totalTokens,
|
||||||
requests: parseInt(data.requests) || 0
|
requests: parseInt(data.requests) || 0,
|
||||||
};
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheCreateTokens,
|
||||||
|
cacheReadTokens
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取该小时的模型级别数据来计算准确费用
|
||||||
|
const modelPattern = `usage:*:model:hourly:*:${hourKey}`;
|
||||||
|
const modelKeys = await client.keys(modelPattern);
|
||||||
|
const apiKeyCostMap = new Map();
|
||||||
|
|
||||||
|
for (const modelKey of modelKeys) {
|
||||||
|
const match = modelKey.match(/usage:(.+?):model:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const apiKeyId = match[1];
|
||||||
|
const model = match[2];
|
||||||
|
const modelData = await client.hgetall(modelKey);
|
||||||
|
|
||||||
|
if (modelData && apiKeyDataMap.has(apiKeyId)) {
|
||||||
|
const usage = {
|
||||||
|
input_tokens: parseInt(modelData.inputTokens) || 0,
|
||||||
|
output_tokens: parseInt(modelData.outputTokens) || 0,
|
||||||
|
cache_creation_input_tokens: parseInt(modelData.cacheCreateTokens) || 0,
|
||||||
|
cache_read_input_tokens: parseInt(modelData.cacheReadTokens) || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const costResult = CostCalculator.calculateCost(usage, model);
|
||||||
|
const currentCost = apiKeyCostMap.get(apiKeyId) || 0;
|
||||||
|
apiKeyCostMap.set(apiKeyId, currentCost + costResult.costs.total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组合数据
|
||||||
|
for (const [apiKeyId, data] of apiKeyDataMap) {
|
||||||
|
const cost = apiKeyCostMap.get(apiKeyId) || 0;
|
||||||
|
|
||||||
|
// 如果没有模型级别数据,使用默认模型计算(降级方案)
|
||||||
|
let finalCost = cost;
|
||||||
|
let formattedCost = CostCalculator.formatCost(cost);
|
||||||
|
|
||||||
|
if (cost === 0 && data.tokens > 0) {
|
||||||
|
const usage = {
|
||||||
|
input_tokens: data.inputTokens,
|
||||||
|
output_tokens: data.outputTokens,
|
||||||
|
cache_creation_input_tokens: data.cacheCreateTokens,
|
||||||
|
cache_read_input_tokens: data.cacheReadTokens
|
||||||
|
};
|
||||||
|
const fallbackResult = CostCalculator.calculateCost(usage, 'claude-3-5-sonnet-20241022');
|
||||||
|
finalCost = fallbackResult.costs.total;
|
||||||
|
formattedCost = fallbackResult.formatted.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
hourData.apiKeys[apiKeyId] = {
|
||||||
|
name: data.name,
|
||||||
|
tokens: data.tokens,
|
||||||
|
requests: data.requests,
|
||||||
|
cost: finalCost,
|
||||||
|
formattedCost: formattedCost
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
trendData.push(hourData);
|
trendData.push(hourData);
|
||||||
currentHour.setHours(currentHour.getHours() + 1);
|
currentHour.setHours(currentHour.getHours() + 1);
|
||||||
}
|
}
|
||||||
@@ -2023,6 +2096,8 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
apiKeys: {}
|
apiKeys: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 先收集基础数据
|
||||||
|
const apiKeyDataMap = new Map();
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const match = key.match(/usage:daily:(.+?):\d{4}-\d{2}-\d{2}/);
|
const match = key.match(/usage:daily:(.+?):\d{4}-\d{2}-\d{2}/);
|
||||||
if (!match) continue;
|
if (!match) continue;
|
||||||
@@ -2031,19 +2106,80 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const data = await client.hgetall(key);
|
const data = await client.hgetall(key);
|
||||||
|
|
||||||
if (data && apiKeyMap.has(apiKeyId)) {
|
if (data && apiKeyMap.has(apiKeyId)) {
|
||||||
const totalTokens = (parseInt(data.inputTokens) || 0) +
|
const inputTokens = parseInt(data.inputTokens) || 0;
|
||||||
(parseInt(data.outputTokens) || 0) +
|
const outputTokens = parseInt(data.outputTokens) || 0;
|
||||||
(parseInt(data.cacheCreateTokens) || 0) +
|
const cacheCreateTokens = parseInt(data.cacheCreateTokens) || 0;
|
||||||
(parseInt(data.cacheReadTokens) || 0);
|
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0;
|
||||||
|
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
||||||
|
|
||||||
dayData.apiKeys[apiKeyId] = {
|
apiKeyDataMap.set(apiKeyId, {
|
||||||
name: apiKeyMap.get(apiKeyId).name,
|
name: apiKeyMap.get(apiKeyId).name,
|
||||||
tokens: totalTokens,
|
tokens: totalTokens,
|
||||||
requests: parseInt(data.requests) || 0
|
requests: parseInt(data.requests) || 0,
|
||||||
};
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheCreateTokens,
|
||||||
|
cacheReadTokens
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取该天的模型级别数据来计算准确费用
|
||||||
|
const modelPattern = `usage:*:model:daily:*:${dateStr}`;
|
||||||
|
const modelKeys = await client.keys(modelPattern);
|
||||||
|
const apiKeyCostMap = new Map();
|
||||||
|
|
||||||
|
for (const modelKey of modelKeys) {
|
||||||
|
const match = modelKey.match(/usage:(.+?):model:daily:(.+?):\d{4}-\d{2}-\d{2}/);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const apiKeyId = match[1];
|
||||||
|
const model = match[2];
|
||||||
|
const modelData = await client.hgetall(modelKey);
|
||||||
|
|
||||||
|
if (modelData && apiKeyDataMap.has(apiKeyId)) {
|
||||||
|
const usage = {
|
||||||
|
input_tokens: parseInt(modelData.inputTokens) || 0,
|
||||||
|
output_tokens: parseInt(modelData.outputTokens) || 0,
|
||||||
|
cache_creation_input_tokens: parseInt(modelData.cacheCreateTokens) || 0,
|
||||||
|
cache_read_input_tokens: parseInt(modelData.cacheReadTokens) || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const costResult = CostCalculator.calculateCost(usage, model);
|
||||||
|
const currentCost = apiKeyCostMap.get(apiKeyId) || 0;
|
||||||
|
apiKeyCostMap.set(apiKeyId, currentCost + costResult.costs.total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组合数据
|
||||||
|
for (const [apiKeyId, data] of apiKeyDataMap) {
|
||||||
|
const cost = apiKeyCostMap.get(apiKeyId) || 0;
|
||||||
|
|
||||||
|
// 如果没有模型级别数据,使用默认模型计算(降级方案)
|
||||||
|
let finalCost = cost;
|
||||||
|
let formattedCost = CostCalculator.formatCost(cost);
|
||||||
|
|
||||||
|
if (cost === 0 && data.tokens > 0) {
|
||||||
|
const usage = {
|
||||||
|
input_tokens: data.inputTokens,
|
||||||
|
output_tokens: data.outputTokens,
|
||||||
|
cache_creation_input_tokens: data.cacheCreateTokens,
|
||||||
|
cache_read_input_tokens: data.cacheReadTokens
|
||||||
|
};
|
||||||
|
const fallbackResult = CostCalculator.calculateCost(usage, 'claude-3-5-sonnet-20241022');
|
||||||
|
finalCost = fallbackResult.costs.total;
|
||||||
|
formattedCost = fallbackResult.formatted.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
dayData.apiKeys[apiKeyId] = {
|
||||||
|
name: data.name,
|
||||||
|
tokens: data.tokens,
|
||||||
|
requests: data.requests,
|
||||||
|
cost: finalCost,
|
||||||
|
formattedCost: formattedCost
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
trendData.push(dayData);
|
trendData.push(dayData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,11 +88,11 @@ class ClaudeConsoleRelayService {
|
|||||||
logger.debug(`[DEBUG] Adding beta header: ${options.betaHeader}`);
|
logger.debug(`[DEBUG] Adding beta header: ${options.betaHeader}`);
|
||||||
requestConfig.headers['anthropic-beta'] = options.betaHeader;
|
requestConfig.headers['anthropic-beta'] = options.betaHeader;
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`[DEBUG] No beta header to add`);
|
logger.debug('[DEBUG] No beta header to add');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
logger.debug(`📤 Sending request to Claude Console API with headers:`, JSON.stringify(requestConfig.headers, null, 2));
|
logger.debug('📤 Sending request to Claude Console API with headers:', JSON.stringify(requestConfig.headers, null, 2));
|
||||||
const response = await axios(requestConfig);
|
const response = await axios(requestConfig);
|
||||||
|
|
||||||
// 移除监听器(请求成功完成)
|
// 移除监听器(请求成功完成)
|
||||||
|
|||||||
1
web/admin-spa/dist/assets/ApiStatsView-C5BOZdu2.css
vendored
Normal file
1
web/admin-spa/dist/assets/ApiStatsView-C5BOZdu2.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
web/admin-spa/dist/assets/DashboardView-B39lq_zZ.css
vendored
Normal file
1
web/admin-spa/dist/assets/DashboardView-B39lq_zZ.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.custom-date-picker[data-v-e2cbd0e3] .el-input__inner{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.custom-date-picker[data-v-e2cbd0e3] .el-input__inner:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1));--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.custom-date-picker[data-v-e2cbd0e3] .el-input__inner{font-size:13px;padding:0 10px}.custom-date-picker[data-v-e2cbd0e3] .el-range-separator{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1));padding:0 2px}.custom-date-picker[data-v-e2cbd0e3] .el-range-input{font-size:13px}@keyframes spin-e2cbd0e3{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.animate-spin[data-v-e2cbd0e3]{animation:spin-e2cbd0e3 1s linear infinite}
|
||||||
@@ -1 +0,0 @@
|
|||||||
.custom-date-picker[data-v-d355f079] .el-input__inner{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.custom-date-picker[data-v-d355f079] .el-input__inner:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1));--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.custom-date-picker[data-v-d355f079] .el-input__inner{font-size:13px;padding:0 10px}.custom-date-picker[data-v-d355f079] .el-range-separator{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1));padding:0 2px}.custom-date-picker[data-v-d355f079] .el-range-input{font-size:13px}
|
|
||||||
1
web/admin-spa/dist/assets/LogoTitle-BiOf3Vkp.css
vendored
Normal file
1
web/admin-spa/dist/assets/LogoTitle-BiOf3Vkp.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@keyframes pulse-718feedc{0%{opacity:.7}50%{opacity:.4}to{opacity:.7}}.animate-pulse[data-v-718feedc]{animation:pulse-718feedc 2s cubic-bezier(.4,0,.6,1) infinite}.header-title[data-v-718feedc]{text-shadow:0 1px 2px rgba(0,0,0,.1)}
|
||||||
@@ -1 +0,0 @@
|
|||||||
@keyframes pulse-a75bf797{0%{opacity:.7}50%{opacity:.4}to{opacity:.7}}.animate-pulse[data-v-a75bf797]{animation:pulse-a75bf797 2s cubic-bezier(.4,0,.6,1) infinite}.header-title[data-v-a75bf797]{text-shadow:0 1px 2px rgba(0,0,0,.1)}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.user-menu-dropdown[data-v-9c2dcb55]{margin-top:8px}.fade-enter-active[data-v-9c2dcb55],.fade-leave-active[data-v-9c2dcb55]{transition:opacity .3s}.fade-enter-from[data-v-9c2dcb55],.fade-leave-to[data-v-9c2dcb55]{opacity:0}
|
|
||||||
1
web/admin-spa/dist/assets/MainLayout-tWrOHYRR.css
vendored
Normal file
1
web/admin-spa/dist/assets/MainLayout-tWrOHYRR.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.user-menu-dropdown[data-v-590374fc]{margin-top:8px}.fade-enter-active[data-v-590374fc],.fade-leave-active[data-v-590374fc]{transition:opacity .3s}.fade-enter-from[data-v-590374fc],.fade-leave-to[data-v-590374fc]{opacity:0}
|
||||||
@@ -1 +1 @@
|
|||||||
.settings-container[data-v-3508e0de]{min-height:calc(100vh - 300px)}.card[data-v-3508e0de]{background:#fff;border-radius:12px;box-shadow:0 2px 12px #0000001a;border:1px solid #e5e7eb}.table-container[data-v-3508e0de]{overflow:hidden;border-radius:8px;border:1px solid #f3f4f6}.table-row[data-v-3508e0de]{transition:background-color .2s ease}.table-row[data-v-3508e0de]:hover{background-color:#f9fafb}.form-input[data-v-3508e0de]{width:100%;border-radius:.5rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));padding:.5rem 1rem;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.form-input[data-v-3508e0de]:focus{border-color:transparent;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.btn[data-v-3508e0de]{display:inline-flex;align-items:center;justify-content:center;border-radius:.5rem;padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:600;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.btn[data-v-3508e0de]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-offset-width: 2px}.btn-primary[data-v-3508e0de]{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.btn-primary[data-v-3508e0de]:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.btn-primary[data-v-3508e0de]:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.btn-success[data-v-3508e0de]{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.btn-success[data-v-3508e0de]:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.btn-success[data-v-3508e0de]:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity, 1))}.loading-spinner[data-v-3508e0de]{height:1.25rem;width:1.25rem}@keyframes spin-3508e0de{to{transform:rotate(360deg)}}.loading-spinner[data-v-3508e0de]{animation:spin-3508e0de 1s linear infinite;border-radius:9999px;border-width:2px;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));--tw-border-opacity: 1;border-top-color:rgb(37 99 235 / var(--tw-border-opacity, 1))}
|
.settings-container[data-v-d29d5f49]{min-height:calc(100vh - 300px)}.card[data-v-d29d5f49]{background:#fff;border-radius:12px;box-shadow:0 2px 12px #0000001a;border:1px solid #e5e7eb}.table-container[data-v-d29d5f49]{overflow:hidden;border-radius:8px;border:1px solid #f3f4f6}.table-row[data-v-d29d5f49]{transition:background-color .2s ease}.table-row[data-v-d29d5f49]:hover{background-color:#f9fafb}.form-input[data-v-d29d5f49]{width:100%;border-radius:.5rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));padding:.5rem 1rem;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.form-input[data-v-d29d5f49]:focus{border-color:transparent;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.btn[data-v-d29d5f49]{display:inline-flex;align-items:center;justify-content:center;border-radius:.5rem;padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:600;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.btn[data-v-d29d5f49]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-offset-width: 2px}.btn-primary[data-v-d29d5f49]{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.btn-primary[data-v-d29d5f49]:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.btn-primary[data-v-d29d5f49]:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.btn-success[data-v-d29d5f49]{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.btn-success[data-v-d29d5f49]:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.btn-success[data-v-d29d5f49]:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity, 1))}.loading-spinner[data-v-d29d5f49]{height:1.25rem;width:1.25rem}@keyframes spin-d29d5f49{to{transform:rotate(360deg)}}.loading-spinner[data-v-d29d5f49]{animation:spin-d29d5f49 1s linear infinite;border-radius:9999px;border-width:2px;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));--tw-border-opacity: 1;border-top-color:rgb(37 99 235 / var(--tw-border-opacity, 1))}
|
||||||
1
web/admin-spa/dist/assets/TutorialView-BM6fz9TT.css
vendored
Normal file
1
web/admin-spa/dist/assets/TutorialView-BM6fz9TT.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.tutorial-container[data-v-508c8654]{min-height:calc(100vh - 300px)}.tutorial-content[data-v-508c8654]{animation:fadeIn-508c8654 .3s ease-in-out}@keyframes fadeIn-508c8654{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}code[data-v-508c8654]{font-family:Fira Code,Monaco,Menlo,Ubuntu Mono,monospace}.tutorial-content h4[data-v-508c8654]{scroll-margin-top:100px}.tutorial-content .bg-gradient-to-r[data-v-508c8654]{transition:all .2s ease}.tutorial-content .bg-gradient-to-r[data-v-508c8654]:hover{transform:translateY(-1px);box-shadow:0 4px 12px #0000001a}
|
||||||
@@ -1 +0,0 @@
|
|||||||
.tutorial-container[data-v-58e5d8f5]{min-height:calc(100vh - 300px)}.tutorial-content[data-v-58e5d8f5]{animation:fadeIn-58e5d8f5 .3s ease-in-out}@keyframes fadeIn-58e5d8f5{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}code[data-v-58e5d8f5]{font-family:Fira Code,Monaco,Menlo,Ubuntu Mono,monospace}.tutorial-content h4[data-v-58e5d8f5]{scroll-margin-top:100px}.tutorial-content .bg-gradient-to-r[data-v-58e5d8f5]{transition:all .2s ease}.tutorial-content .bg-gradient-to-r[data-v-58e5d8f5]:hover{transform:translateY(-1px);box-shadow:0 4px 12px #0000001a}
|
|
||||||
File diff suppressed because one or more lines are too long
4
web/admin-spa/dist/index.html
vendored
4
web/admin-spa/dist/index.html
vendored
@@ -18,12 +18,12 @@
|
|||||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
||||||
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
|
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
|
||||||
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
|
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
|
||||||
<script type="module" crossorigin src="/admin-next/assets/index-CxLQf0HX.js"></script>
|
<script type="module" crossorigin src="/admin-next/assets/index-COOF1SF1.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/admin-next/assets/vue-vendor-CKToUHZx.js">
|
<link rel="modulepreload" crossorigin href="/admin-next/assets/vue-vendor-CKToUHZx.js">
|
||||||
<link rel="modulepreload" crossorigin href="/admin-next/assets/vendor-BDiMbLwQ.js">
|
<link rel="modulepreload" crossorigin href="/admin-next/assets/vendor-BDiMbLwQ.js">
|
||||||
<link rel="modulepreload" crossorigin href="/admin-next/assets/element-plus-B8Fs_0jW.js">
|
<link rel="modulepreload" crossorigin href="/admin-next/assets/element-plus-B8Fs_0jW.js">
|
||||||
<link rel="stylesheet" crossorigin href="/admin-next/assets/element-plus-CPnoEkWW.css">
|
<link rel="stylesheet" crossorigin href="/admin-next/assets/element-plus-CPnoEkWW.css">
|
||||||
<link rel="stylesheet" crossorigin href="/admin-next/assets/index-C8t_nyXa.css">
|
<link rel="stylesheet" crossorigin href="/admin-next/assets/index-Ba0i43MQ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -1,36 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div v-if="show" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="fixed inset-0 modal z-50 flex items-center justify-center p-4"
|
||||||
|
>
|
||||||
<div class="modal-content w-full max-w-2xl p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar">
|
<div class="modal-content w-full max-w-2xl p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
<div class="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
||||||
<i class="fas fa-user-circle text-white"></i>
|
<i class="fas fa-user-circle text-white" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-bold text-gray-900">{{ isEdit ? '编辑账户' : '添加账户' }}</h3>
|
<h3 class="text-xl font-bold text-gray-900">
|
||||||
|
{{ isEdit ? '编辑账户' : '添加账户' }}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="$emit('close')"
|
|
||||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
@click="$emit('close')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times text-xl"></i>
|
<i class="fas fa-times text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 步骤指示器 -->
|
<!-- 步骤指示器 -->
|
||||||
<div v-if="!isEdit && form.addType === 'oauth'" class="flex items-center justify-center mb-8">
|
<div
|
||||||
|
v-if="!isEdit && form.addType === 'oauth'"
|
||||||
|
class="flex items-center justify-center mb-8"
|
||||||
|
>
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div :class="['w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold',
|
<div
|
||||||
oauthStep >= 1 ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-500']">
|
:class="['w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold',
|
||||||
|
oauthStep >= 1 ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-500']"
|
||||||
|
>
|
||||||
1
|
1
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-2 text-sm font-medium text-gray-700">基本信息</span>
|
<span class="ml-2 text-sm font-medium text-gray-700">基本信息</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-8 h-0.5 bg-gray-300"></div>
|
<div class="w-8 h-0.5 bg-gray-300" />
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div :class="['w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold',
|
<div
|
||||||
oauthStep >= 2 ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-500']">
|
:class="['w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold',
|
||||||
|
oauthStep >= 2 ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-500']"
|
||||||
|
>
|
||||||
2
|
2
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-2 text-sm font-medium text-gray-700">授权认证</span>
|
<span class="ml-2 text-sm font-medium text-gray-700">授权认证</span>
|
||||||
@@ -46,8 +58,8 @@
|
|||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.platform"
|
v-model="form.platform"
|
||||||
|
type="radio"
|
||||||
value="claude"
|
value="claude"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -55,8 +67,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.platform"
|
v-model="form.platform"
|
||||||
|
type="radio"
|
||||||
value="claude-console"
|
value="claude-console"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -64,8 +76,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.platform"
|
v-model="form.platform"
|
||||||
|
type="radio"
|
||||||
value="gemini"
|
value="gemini"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -79,8 +91,8 @@
|
|||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.addType"
|
v-model="form.addType"
|
||||||
|
type="radio"
|
||||||
value="oauth"
|
value="oauth"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -88,8 +100,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.addType"
|
v-model="form.addType"
|
||||||
|
type="radio"
|
||||||
value="manual"
|
value="manual"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -108,7 +120,12 @@
|
|||||||
:class="{ 'border-red-500': errors.name }"
|
:class="{ 'border-red-500': errors.name }"
|
||||||
placeholder="为账户设置一个易识别的名称"
|
placeholder="为账户设置一个易识别的名称"
|
||||||
>
|
>
|
||||||
<p v-if="errors.name" class="text-red-500 text-xs mt-1">{{ errors.name }}</p>
|
<p
|
||||||
|
v-if="errors.name"
|
||||||
|
class="text-red-500 text-xs mt-1"
|
||||||
|
>
|
||||||
|
{{ errors.name }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -118,7 +135,7 @@
|
|||||||
rows="3"
|
rows="3"
|
||||||
class="form-input w-full resize-none"
|
class="form-input w-full resize-none"
|
||||||
placeholder="账户用途说明..."
|
placeholder="账户用途说明..."
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -126,8 +143,8 @@
|
|||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.accountType"
|
v-model="form.accountType"
|
||||||
|
type="radio"
|
||||||
value="shared"
|
value="shared"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -135,8 +152,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.accountType"
|
v-model="form.accountType"
|
||||||
|
type="radio"
|
||||||
value="dedicated"
|
value="dedicated"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -159,26 +176,43 @@
|
|||||||
>
|
>
|
||||||
<div class="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
<div class="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<i class="fas fa-info-circle text-yellow-600 mt-0.5"></i>
|
<i class="fas fa-info-circle text-yellow-600 mt-0.5" />
|
||||||
<div class="text-xs text-yellow-700">
|
<div class="text-xs text-yellow-700">
|
||||||
<p class="font-medium mb-1">Google Cloud/Workspace 账号需要提供项目编号</p>
|
<p class="font-medium mb-1">
|
||||||
|
Google Cloud/Workspace 账号需要提供项目编号
|
||||||
|
</p>
|
||||||
<p>某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目编号。</p>
|
<p>某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目编号。</p>
|
||||||
<div class="mt-2 p-2 bg-white rounded border border-yellow-300">
|
<div class="mt-2 p-2 bg-white rounded border border-yellow-300">
|
||||||
<p class="font-medium mb-1">如何获取项目编号:</p>
|
<p class="font-medium mb-1">
|
||||||
|
如何获取项目编号:
|
||||||
|
</p>
|
||||||
<ol class="list-decimal list-inside space-y-1 ml-2">
|
<ol class="list-decimal list-inside space-y-1 ml-2">
|
||||||
<li>访问 <a href="https://console.cloud.google.com/welcome" target="_blank" class="text-blue-600 hover:underline font-medium">Google Cloud Console</a></li>
|
<li>
|
||||||
|
访问 <a
|
||||||
|
href="https://console.cloud.google.com/welcome"
|
||||||
|
target="_blank"
|
||||||
|
class="text-blue-600 hover:underline font-medium"
|
||||||
|
>Google Cloud Console</a>
|
||||||
|
</li>
|
||||||
<li>复制<span class="font-semibold text-red-600">项目编号(Project Number)</span>,通常是12位纯数字</li>
|
<li>复制<span class="font-semibold text-red-600">项目编号(Project Number)</span>,通常是12位纯数字</li>
|
||||||
<li class="text-red-600">⚠️ 注意:不要复制项目ID(Project ID),要复制项目编号!</li>
|
<li class="text-red-600">
|
||||||
|
⚠️ 注意:不要复制项目ID(Project ID),要复制项目编号!
|
||||||
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2"><strong>提示:</strong>如果您的账号是普通个人账号(未绑定 Google Cloud),请留空此字段。</p>
|
<p class="mt-2">
|
||||||
|
<strong>提示:</strong>如果您的账号是普通个人账号(未绑定 Google Cloud),请留空此字段。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Claude Console 特定字段 -->
|
<!-- Claude Console 特定字段 -->
|
||||||
<div v-if="form.platform === 'claude-console' && !isEdit" class="space-y-4">
|
<div
|
||||||
|
v-if="form.platform === 'claude-console' && !isEdit"
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">API URL *</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-3">API URL *</label>
|
||||||
<input
|
<input
|
||||||
@@ -189,7 +223,12 @@
|
|||||||
:class="{ 'border-red-500': errors.apiUrl }"
|
:class="{ 'border-red-500': errors.apiUrl }"
|
||||||
placeholder="例如:https://api.example.com"
|
placeholder="例如:https://api.example.com"
|
||||||
>
|
>
|
||||||
<p v-if="errors.apiUrl" class="text-red-500 text-xs mt-1">{{ errors.apiUrl }}</p>
|
<p
|
||||||
|
v-if="errors.apiUrl"
|
||||||
|
class="text-red-500 text-xs mt-1"
|
||||||
|
>
|
||||||
|
{{ errors.apiUrl }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -202,7 +241,12 @@
|
|||||||
:class="{ 'border-red-500': errors.apiKey }"
|
:class="{ 'border-red-500': errors.apiKey }"
|
||||||
placeholder="请输入API Key"
|
placeholder="请输入API Key"
|
||||||
>
|
>
|
||||||
<p v-if="errors.apiKey" class="text-red-500 text-xs mt-1">{{ errors.apiKey }}</p>
|
<p
|
||||||
|
v-if="errors.apiKey"
|
||||||
|
class="text-red-500 text-xs mt-1"
|
||||||
|
>
|
||||||
|
{{ errors.apiKey }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -210,22 +254,22 @@
|
|||||||
<div class="mb-2 flex gap-2">
|
<div class="mb-2 flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="addPresetModel('claude-sonnet-4-20250514')"
|
|
||||||
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"
|
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"
|
||||||
|
@click="addPresetModel('claude-sonnet-4-20250514')"
|
||||||
>
|
>
|
||||||
+ claude-sonnet-4-20250514
|
+ claude-sonnet-4-20250514
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="addPresetModel('claude-opus-4-20250514')"
|
|
||||||
class="px-3 py-1 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
|
class="px-3 py-1 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
|
||||||
|
@click="addPresetModel('claude-opus-4-20250514')"
|
||||||
>
|
>
|
||||||
+ claude-opus-4-20250514
|
+ claude-opus-4-20250514
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="addPresetModel('claude-3-5-haiku-20241022')"
|
|
||||||
class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-purple-200 transition-colors"
|
class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-purple-200 transition-colors"
|
||||||
|
@click="addPresetModel('claude-3-5-haiku-20241022')"
|
||||||
>
|
>
|
||||||
+ claude-3-5-haiku-20241022
|
+ claude-3-5-haiku-20241022
|
||||||
</button>
|
</button>
|
||||||
@@ -235,8 +279,10 @@
|
|||||||
rows="3"
|
rows="3"
|
||||||
class="form-input w-full resize-none"
|
class="form-input w-full resize-none"
|
||||||
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型!"
|
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型!"
|
||||||
></textarea>
|
/>
|
||||||
<p class="text-xs text-gray-500 mt-1">留空表示支持所有模型。如果指定模型,请求中的模型不在列表内将不会调度到此账号</p>
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
留空表示支持所有模型。如果指定模型,请求中的模型不在列表内将不会调度到此账号
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -258,7 +304,9 @@
|
|||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
placeholder="默认60分钟"
|
placeholder="默认60分钟"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-1">当账号返回429错误时,暂停调度的时间(分钟)</p>
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
当账号返回429错误时,暂停调度的时间(分钟)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -273,37 +321,58 @@
|
|||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
placeholder="数字越小优先级越高,默认50"
|
placeholder="数字越小优先级越高,默认50"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-1">数字越小优先级越高,建议范围:1-100</p>
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
数字越小优先级越高,建议范围:1-100
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 手动输入 Token 字段 -->
|
<!-- 手动输入 Token 字段 -->
|
||||||
<div v-if="form.addType === 'manual' && form.platform !== 'claude-console'" class="space-y-4 bg-blue-50 p-4 rounded-lg border border-blue-200">
|
<div
|
||||||
|
v-if="form.addType === 'manual' && form.platform !== 'claude-console'"
|
||||||
|
class="space-y-4 bg-blue-50 p-4 rounded-lg border border-blue-200"
|
||||||
|
>
|
||||||
<div class="flex items-start gap-3 mb-4">
|
<div class="flex items-start gap-3 mb-4">
|
||||||
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
|
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
|
||||||
<i class="fas fa-info text-white text-sm"></i>
|
<i class="fas fa-info text-white text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 class="font-semibold text-blue-900 mb-2">手动输入 Token</h5>
|
<h5 class="font-semibold text-blue-900 mb-2">
|
||||||
<p v-if="form.platform === 'claude'" class="text-sm text-blue-800 mb-2">
|
手动输入 Token
|
||||||
|
</h5>
|
||||||
|
<p
|
||||||
|
v-if="form.platform === 'claude'"
|
||||||
|
class="text-sm text-blue-800 mb-2"
|
||||||
|
>
|
||||||
请输入有效的 Claude Access Token。如果您有 Refresh Token,建议也一并填写以支持自动刷新。
|
请输入有效的 Claude Access Token。如果您有 Refresh Token,建议也一并填写以支持自动刷新。
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="form.platform === 'gemini'" class="text-sm text-blue-800 mb-2">
|
<p
|
||||||
|
v-else-if="form.platform === 'gemini'"
|
||||||
|
class="text-sm text-blue-800 mb-2"
|
||||||
|
>
|
||||||
请输入有效的 Gemini Access Token。如果您有 Refresh Token,建议也一并填写以支持自动刷新。
|
请输入有效的 Gemini Access Token。如果您有 Refresh Token,建议也一并填写以支持自动刷新。
|
||||||
</p>
|
</p>
|
||||||
<div class="bg-white/80 rounded-lg p-3 mt-2 mb-2 border border-blue-300">
|
<div class="bg-white/80 rounded-lg p-3 mt-2 mb-2 border border-blue-300">
|
||||||
<p class="text-sm text-blue-900 font-medium mb-1">
|
<p class="text-sm text-blue-900 font-medium mb-1">
|
||||||
<i class="fas fa-folder-open mr-1"></i>
|
<i class="fas fa-folder-open mr-1" />
|
||||||
获取 Access Token 的方法:
|
获取 Access Token 的方法:
|
||||||
</p>
|
</p>
|
||||||
<p v-if="form.platform === 'claude'" class="text-xs text-blue-800">
|
<p
|
||||||
|
v-if="form.platform === 'claude'"
|
||||||
|
class="text-xs text-blue-800"
|
||||||
|
>
|
||||||
请从已登录 Claude Code 的机器上获取 <code class="bg-blue-100 px-1 py-0.5 rounded font-mono">~/.claude/.credentials.json</code> 文件中的凭证,
|
请从已登录 Claude Code 的机器上获取 <code class="bg-blue-100 px-1 py-0.5 rounded font-mono">~/.claude/.credentials.json</code> 文件中的凭证,
|
||||||
请勿使用 Claude 官网 API Keys 页面的密钥。
|
请勿使用 Claude 官网 API Keys 页面的密钥。
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="form.platform === 'gemini'" class="text-xs text-blue-800">
|
<p
|
||||||
|
v-else-if="form.platform === 'gemini'"
|
||||||
|
class="text-xs text-blue-800"
|
||||||
|
>
|
||||||
请从已登录 Gemini CLI 的机器上获取 <code class="bg-blue-100 px-1 py-0.5 rounded font-mono">~/.config/gemini/credentials.json</code> 文件中的凭证。
|
请从已登录 Gemini CLI 的机器上获取 <code class="bg-blue-100 px-1 py-0.5 rounded font-mono">~/.config/gemini/credentials.json</code> 文件中的凭证。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-blue-600">💡 如果未填写 Refresh Token,Token 过期后需要手动更新。</p>
|
<p class="text-xs text-blue-600">
|
||||||
|
💡 如果未填写 Refresh Token,Token 过期后需要手动更新。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -316,8 +385,13 @@
|
|||||||
class="form-input w-full resize-none font-mono text-xs"
|
class="form-input w-full resize-none font-mono text-xs"
|
||||||
:class="{ 'border-red-500': errors.accessToken }"
|
:class="{ 'border-red-500': errors.accessToken }"
|
||||||
placeholder="请输入 Access Token..."
|
placeholder="请输入 Access Token..."
|
||||||
></textarea>
|
/>
|
||||||
<p v-if="errors.accessToken" class="text-red-500 text-xs mt-1">{{ errors.accessToken }}</p>
|
<p
|
||||||
|
v-if="errors.accessToken"
|
||||||
|
class="text-red-500 text-xs mt-1"
|
||||||
|
>
|
||||||
|
{{ errors.accessToken }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -327,7 +401,7 @@
|
|||||||
rows="4"
|
rows="4"
|
||||||
class="form-input w-full resize-none font-mono text-xs"
|
class="form-input w-full resize-none font-mono text-xs"
|
||||||
placeholder="请输入 Refresh Token..."
|
placeholder="请输入 Refresh Token..."
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -337,28 +411,31 @@
|
|||||||
<div class="flex gap-3 pt-4">
|
<div class="flex gap-3 pt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="$emit('close')"
|
|
||||||
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
||||||
|
@click="$emit('close')"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="form.addType === 'oauth' && form.platform !== 'claude-console'"
|
v-if="form.addType === 'oauth' && form.platform !== 'claude-console'"
|
||||||
type="button"
|
type="button"
|
||||||
@click="nextStep"
|
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||||
|
@click="nextStep"
|
||||||
>
|
>
|
||||||
下一步
|
下一步
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
type="button"
|
type="button"
|
||||||
@click="createAccount"
|
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||||
|
@click="createAccount"
|
||||||
>
|
>
|
||||||
<div v-if="loading" class="loading-spinner mr-2"></div>
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
{{ loading ? '创建中...' : '创建' }}
|
{{ loading ? '创建中...' : '创建' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -375,7 +452,10 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 编辑模式 -->
|
<!-- 编辑模式 -->
|
||||||
<div v-if="isEdit" class="space-y-6">
|
<div
|
||||||
|
v-if="isEdit"
|
||||||
|
class="space-y-6"
|
||||||
|
>
|
||||||
<!-- 基本信息 -->
|
<!-- 基本信息 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">账户名称</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-3">账户名称</label>
|
||||||
@@ -395,7 +475,7 @@
|
|||||||
rows="3"
|
rows="3"
|
||||||
class="form-input w-full resize-none"
|
class="form-input w-full resize-none"
|
||||||
placeholder="账户用途说明..."
|
placeholder="账户用途说明..."
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -403,8 +483,8 @@
|
|||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.accountType"
|
v-model="form.accountType"
|
||||||
|
type="radio"
|
||||||
value="shared"
|
value="shared"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -412,8 +492,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.accountType"
|
v-model="form.accountType"
|
||||||
|
type="radio"
|
||||||
value="dedicated"
|
value="dedicated"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -450,11 +530,16 @@
|
|||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
placeholder="数字越小优先级越高"
|
placeholder="数字越小优先级越高"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-1">数字越小优先级越高,建议范围:1-100</p>
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
数字越小优先级越高,建议范围:1-100
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Claude Console 特定字段(编辑模式)-->
|
<!-- Claude Console 特定字段(编辑模式)-->
|
||||||
<div v-if="form.platform === 'claude-console'" class="space-y-4">
|
<div
|
||||||
|
v-if="form.platform === 'claude-console'"
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">API URL</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-3">API URL</label>
|
||||||
<input
|
<input
|
||||||
@@ -474,7 +559,9 @@
|
|||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
placeholder="留空表示不更新"
|
placeholder="留空表示不更新"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-1">留空表示不更新 API Key</p>
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
留空表示不更新 API Key
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -482,22 +569,22 @@
|
|||||||
<div class="mb-2 flex gap-2">
|
<div class="mb-2 flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="addPresetModel('claude-sonnet-4-20250514')"
|
|
||||||
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"
|
class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"
|
||||||
|
@click="addPresetModel('claude-sonnet-4-20250514')"
|
||||||
>
|
>
|
||||||
+ claude-sonnet-4-20250514
|
+ claude-sonnet-4-20250514
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="addPresetModel('claude-opus-4-20250514')"
|
|
||||||
class="px-3 py-1 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
|
class="px-3 py-1 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
|
||||||
|
@click="addPresetModel('claude-opus-4-20250514')"
|
||||||
>
|
>
|
||||||
+ claude-opus-4-20250514
|
+ claude-opus-4-20250514
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="addPresetModel('claude-3-5-haiku-20241022')"
|
|
||||||
class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-purple-200 transition-colors"
|
class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-purple-200 transition-colors"
|
||||||
|
@click="addPresetModel('claude-3-5-haiku-20241022')"
|
||||||
>
|
>
|
||||||
+ claude-3-5-haiku-20241022
|
+ claude-3-5-haiku-20241022
|
||||||
</button>
|
</button>
|
||||||
@@ -507,7 +594,7 @@
|
|||||||
rows="3"
|
rows="3"
|
||||||
class="form-input w-full resize-none"
|
class="form-input w-full resize-none"
|
||||||
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型!"
|
placeholder="每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型!"
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -532,15 +619,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Token 更新 -->
|
<!-- Token 更新 -->
|
||||||
<div v-if="form.platform !== 'claude-console'" class="bg-amber-50 p-4 rounded-lg border border-amber-200">
|
<div
|
||||||
|
v-if="form.platform !== 'claude-console'"
|
||||||
|
class="bg-amber-50 p-4 rounded-lg border border-amber-200"
|
||||||
|
>
|
||||||
<div class="flex items-start gap-3 mb-4">
|
<div class="flex items-start gap-3 mb-4">
|
||||||
<div class="w-8 h-8 bg-amber-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
|
<div class="w-8 h-8 bg-amber-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
|
||||||
<i class="fas fa-key text-white text-sm"></i>
|
<i class="fas fa-key text-white text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 class="font-semibold text-amber-900 mb-2">更新 Token</h5>
|
<h5 class="font-semibold text-amber-900 mb-2">
|
||||||
<p class="text-sm text-amber-800 mb-2">可以更新 Access Token 和 Refresh Token。为了安全起见,不会显示当前的 Token 值。</p>
|
更新 Token
|
||||||
<p class="text-xs text-amber-600">💡 留空表示不更新该字段。</p>
|
</h5>
|
||||||
|
<p class="text-sm text-amber-800 mb-2">
|
||||||
|
可以更新 Access Token 和 Refresh Token。为了安全起见,不会显示当前的 Token 值。
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-amber-600">
|
||||||
|
💡 留空表示不更新该字段。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -552,7 +648,7 @@
|
|||||||
rows="4"
|
rows="4"
|
||||||
class="form-input w-full resize-none font-mono text-xs"
|
class="form-input w-full resize-none font-mono text-xs"
|
||||||
placeholder="留空表示不更新..."
|
placeholder="留空表示不更新..."
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -562,7 +658,7 @@
|
|||||||
rows="4"
|
rows="4"
|
||||||
class="form-input w-full resize-none font-mono text-xs"
|
class="form-input w-full resize-none font-mono text-xs"
|
||||||
placeholder="留空表示不更新..."
|
placeholder="留空表示不更新..."
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -573,18 +669,21 @@
|
|||||||
<div class="flex gap-3 pt-4">
|
<div class="flex gap-3 pt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="$emit('close')"
|
|
||||||
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
||||||
|
@click="$emit('close')"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="updateAccount"
|
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||||
|
@click="updateAccount"
|
||||||
>
|
>
|
||||||
<div v-if="loading" class="loading-spinner mr-2"></div>
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
{{ loading ? '更新中...' : '更新' }}
|
{{ loading ? '更新中...' : '更新' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
<div class="bg-blue-50 p-6 rounded-lg border border-blue-200">
|
<div class="bg-blue-50 p-6 rounded-lg border border-blue-200">
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div class="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
<div class="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
<i class="fas fa-link text-white"></i>
|
<i class="fas fa-link text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h4 class="font-semibold text-blue-900 mb-3">Claude 账户授权</h4>
|
<h4 class="font-semibold text-blue-900 mb-3">
|
||||||
|
Claude 账户授权
|
||||||
|
</h4>
|
||||||
<p class="text-sm text-blue-800 mb-4">
|
<p class="text-sm text-blue-800 mb-4">
|
||||||
请按照以下步骤完成 Claude 账户的授权:
|
请按照以下步骤完成 Claude 账户的授权:
|
||||||
</p>
|
</p>
|
||||||
@@ -17,20 +19,33 @@
|
|||||||
<!-- 步骤1: 生成授权链接 -->
|
<!-- 步骤1: 生成授权链接 -->
|
||||||
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
|
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">1</div>
|
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="font-medium text-blue-900 mb-2">点击下方按钮生成授权链接</p>
|
<p class="font-medium text-blue-900 mb-2">
|
||||||
|
点击下方按钮生成授权链接
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
v-if="!authUrl"
|
v-if="!authUrl"
|
||||||
@click="generateAuthUrl"
|
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="btn btn-primary px-4 py-2 text-sm"
|
class="btn btn-primary px-4 py-2 text-sm"
|
||||||
|
@click="generateAuthUrl"
|
||||||
>
|
>
|
||||||
<i v-if="!loading" class="fas fa-link mr-2"></i>
|
<i
|
||||||
<div v-else class="loading-spinner mr-2"></div>
|
v-if="!loading"
|
||||||
|
class="fas fa-link mr-2"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
{{ loading ? '生成中...' : '生成授权链接' }}
|
{{ loading ? '生成中...' : '生成授权链接' }}
|
||||||
</button>
|
</button>
|
||||||
<div v-else class="space-y-3">
|
<div
|
||||||
|
v-else
|
||||||
|
class="space-y-3"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -39,18 +54,18 @@
|
|||||||
class="form-input flex-1 text-xs font-mono bg-gray-50"
|
class="form-input flex-1 text-xs font-mono bg-gray-50"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@click="copyAuthUrl"
|
|
||||||
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
title="复制链接"
|
title="复制链接"
|
||||||
|
@click="copyAuthUrl"
|
||||||
>
|
>
|
||||||
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'"></i>
|
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="regenerateAuthUrl"
|
|
||||||
class="text-xs text-blue-600 hover:text-blue-700"
|
class="text-xs text-blue-600 hover:text-blue-700"
|
||||||
|
@click="regenerateAuthUrl"
|
||||||
>
|
>
|
||||||
<i class="fas fa-sync-alt mr-1"></i>重新生成
|
<i class="fas fa-sync-alt mr-1" />重新生成
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,15 +75,19 @@
|
|||||||
<!-- 步骤2: 访问链接并授权 -->
|
<!-- 步骤2: 访问链接并授权 -->
|
||||||
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
|
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">2</div>
|
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="font-medium text-blue-900 mb-2">在浏览器中打开链接并完成授权</p>
|
<p class="font-medium text-blue-900 mb-2">
|
||||||
|
在浏览器中打开链接并完成授权
|
||||||
|
</p>
|
||||||
<p class="text-sm text-blue-700 mb-2">
|
<p class="text-sm text-blue-700 mb-2">
|
||||||
请在新标签页中打开授权链接,登录您的 Claude 账户并授权。
|
请在新标签页中打开授权链接,登录您的 Claude 账户并授权。
|
||||||
</p>
|
</p>
|
||||||
<div class="bg-yellow-50 p-3 rounded border border-yellow-300">
|
<div class="bg-yellow-50 p-3 rounded border border-yellow-300">
|
||||||
<p class="text-xs text-yellow-800">
|
<p class="text-xs text-yellow-800">
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
<i class="fas fa-exclamation-triangle mr-1" />
|
||||||
<strong>注意:</strong>如果您设置了代理,请确保浏览器也使用相同的代理访问授权页面。
|
<strong>注意:</strong>如果您设置了代理,请确保浏览器也使用相同的代理访问授权页面。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,26 +98,30 @@
|
|||||||
<!-- 步骤3: 输入授权码 -->
|
<!-- 步骤3: 输入授权码 -->
|
||||||
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
|
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">3</div>
|
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||||
|
3
|
||||||
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="font-medium text-blue-900 mb-2">输入 Authorization Code</p>
|
<p class="font-medium text-blue-900 mb-2">
|
||||||
|
输入 Authorization Code
|
||||||
|
</p>
|
||||||
<p class="text-sm text-blue-700 mb-3">
|
<p class="text-sm text-blue-700 mb-3">
|
||||||
授权完成后,页面会显示一个 <strong>Authorization Code</strong>,请将其复制并粘贴到下方输入框:
|
授权完成后,页面会显示一个 <strong>Authorization Code</strong>,请将其复制并粘贴到下方输入框:
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
<i class="fas fa-key text-blue-500 mr-2"></i>Authorization Code
|
<i class="fas fa-key text-blue-500 mr-2" />Authorization Code
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="authCode"
|
v-model="authCode"
|
||||||
rows="3"
|
rows="3"
|
||||||
class="form-input w-full resize-none font-mono text-sm"
|
class="form-input w-full resize-none font-mono text-sm"
|
||||||
placeholder="粘贴从Claude页面获取的Authorization Code..."
|
placeholder="粘贴从Claude页面获取的Authorization Code..."
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-2">
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
<i class="fas fa-info-circle mr-1" />
|
||||||
请粘贴从Claude页面复制的Authorization Code
|
请粘贴从Claude页面复制的Authorization Code
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,10 +139,12 @@
|
|||||||
<div class="bg-green-50 p-6 rounded-lg border border-green-200">
|
<div class="bg-green-50 p-6 rounded-lg border border-green-200">
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div class="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
<div class="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
<i class="fas fa-robot text-white"></i>
|
<i class="fas fa-robot text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h4 class="font-semibold text-green-900 mb-3">Gemini 账户授权</h4>
|
<h4 class="font-semibold text-green-900 mb-3">
|
||||||
|
Gemini 账户授权
|
||||||
|
</h4>
|
||||||
<p class="text-sm text-green-800 mb-4">
|
<p class="text-sm text-green-800 mb-4">
|
||||||
请按照以下步骤完成 Gemini 账户的授权:
|
请按照以下步骤完成 Gemini 账户的授权:
|
||||||
</p>
|
</p>
|
||||||
@@ -128,20 +153,33 @@
|
|||||||
<!-- 步骤1: 生成授权链接 -->
|
<!-- 步骤1: 生成授权链接 -->
|
||||||
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
|
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">1</div>
|
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="font-medium text-green-900 mb-2">点击下方按钮生成授权链接</p>
|
<p class="font-medium text-green-900 mb-2">
|
||||||
|
点击下方按钮生成授权链接
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
v-if="!authUrl"
|
v-if="!authUrl"
|
||||||
@click="generateAuthUrl"
|
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="btn btn-primary px-4 py-2 text-sm"
|
class="btn btn-primary px-4 py-2 text-sm"
|
||||||
|
@click="generateAuthUrl"
|
||||||
>
|
>
|
||||||
<i v-if="!loading" class="fas fa-link mr-2"></i>
|
<i
|
||||||
<div v-else class="loading-spinner mr-2"></div>
|
v-if="!loading"
|
||||||
|
class="fas fa-link mr-2"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
{{ loading ? '生成中...' : '生成授权链接' }}
|
{{ loading ? '生成中...' : '生成授权链接' }}
|
||||||
</button>
|
</button>
|
||||||
<div v-else class="space-y-3">
|
<div
|
||||||
|
v-else
|
||||||
|
class="space-y-3"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -150,18 +188,18 @@
|
|||||||
class="form-input flex-1 text-xs font-mono bg-gray-50"
|
class="form-input flex-1 text-xs font-mono bg-gray-50"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@click="copyAuthUrl"
|
|
||||||
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
title="复制链接"
|
title="复制链接"
|
||||||
|
@click="copyAuthUrl"
|
||||||
>
|
>
|
||||||
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'"></i>
|
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="regenerateAuthUrl"
|
|
||||||
class="text-xs text-green-600 hover:text-green-700"
|
class="text-xs text-green-600 hover:text-green-700"
|
||||||
|
@click="regenerateAuthUrl"
|
||||||
>
|
>
|
||||||
<i class="fas fa-sync-alt mr-1"></i>重新生成
|
<i class="fas fa-sync-alt mr-1" />重新生成
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,9 +209,13 @@
|
|||||||
<!-- 步骤2: 操作说明 -->
|
<!-- 步骤2: 操作说明 -->
|
||||||
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
|
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">2</div>
|
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="font-medium text-green-900 mb-2">在浏览器中打开链接并完成授权</p>
|
<p class="font-medium text-green-900 mb-2">
|
||||||
|
在浏览器中打开链接并完成授权
|
||||||
|
</p>
|
||||||
<ol class="text-sm text-green-800 space-y-1 list-decimal list-inside mb-3">
|
<ol class="text-sm text-green-800 space-y-1 list-decimal list-inside mb-3">
|
||||||
<li>点击上方的授权链接,在新页面中完成Google账号登录</li>
|
<li>点击上方的授权链接,在新页面中完成Google账号登录</li>
|
||||||
<li>点击“登录”按钮后可能会加载很慢(这是正常的)</li>
|
<li>点击“登录”按钮后可能会加载很慢(这是正常的)</li>
|
||||||
@@ -182,7 +224,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
<div class="bg-green-100 p-3 rounded border border-green-300">
|
<div class="bg-green-100 p-3 rounded border border-green-300">
|
||||||
<p class="text-xs text-green-700">
|
<p class="text-xs text-green-700">
|
||||||
<i class="fas fa-lightbulb mr-1"></i>
|
<i class="fas fa-lightbulb mr-1" />
|
||||||
<strong>提示:</strong>如果页面一直无法跳转,可以打开浏览器开发者工具(F12),F5刷新一下授权页再点击页面的登录按钮,在“网络”标签中找到以 localhost:45462 开头的请求,复制其完整URL。
|
<strong>提示:</strong>如果页面一直无法跳转,可以打开浏览器开发者工具(F12),F5刷新一下授权页再点击页面的登录按钮,在“网络”标签中找到以 localhost:45462 开头的请求,复制其完整URL。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,31 +235,35 @@
|
|||||||
<!-- 步骤3: 输入授权码 -->
|
<!-- 步骤3: 输入授权码 -->
|
||||||
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
|
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">3</div>
|
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||||
|
3
|
||||||
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="font-medium text-green-900 mb-2">复制oauth后的链接</p>
|
<p class="font-medium text-green-900 mb-2">
|
||||||
|
复制oauth后的链接
|
||||||
|
</p>
|
||||||
<p class="text-sm text-green-700 mb-3">
|
<p class="text-sm text-green-700 mb-3">
|
||||||
复制浏览器地址栏的完整链接并粘贴到下方输入框:
|
复制浏览器地址栏的完整链接并粘贴到下方输入框:
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
<i class="fas fa-key text-green-500 mr-2"></i>复制oauth后的链接
|
<i class="fas fa-key text-green-500 mr-2" />复制oauth后的链接
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="authCode"
|
v-model="authCode"
|
||||||
rows="3"
|
rows="3"
|
||||||
class="form-input w-full resize-none font-mono text-sm"
|
class="form-input w-full resize-none font-mono text-sm"
|
||||||
placeholder="粘贴以 http://localhost:45462 开头的完整链接..."
|
placeholder="粘贴以 http://localhost:45462 开头的完整链接..."
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 space-y-1">
|
<div class="mt-2 space-y-1">
|
||||||
<p class="text-xs text-gray-600">
|
<p class="text-xs text-gray-600">
|
||||||
<i class="fas fa-check-circle text-green-500 mr-1"></i>
|
<i class="fas fa-check-circle text-green-500 mr-1" />
|
||||||
支持粘贴完整链接,系统会自动提取授权码
|
支持粘贴完整链接,系统会自动提取授权码
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-gray-600">
|
<p class="text-xs text-gray-600">
|
||||||
<i class="fas fa-check-circle text-green-500 mr-1"></i>
|
<i class="fas fa-check-circle text-green-500 mr-1" />
|
||||||
也可以直接粘贴授权码(code参数的值)
|
也可以直接粘贴授权码(code参数的值)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,18 +280,21 @@
|
|||||||
<div class="flex gap-3 pt-4">
|
<div class="flex gap-3 pt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="$emit('back')"
|
|
||||||
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
||||||
|
@click="$emit('back')"
|
||||||
>
|
>
|
||||||
上一步
|
上一步
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="exchangeCode"
|
|
||||||
:disabled="!canExchange || exchanging"
|
:disabled="!canExchange || exchanging"
|
||||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||||
|
@click="exchangeCode"
|
||||||
>
|
>
|
||||||
<div v-if="exchanging" class="loading-spinner mr-2"></div>
|
<div
|
||||||
|
v-if="exchanging"
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
{{ exchanging ? '验证中...' : '完成授权' }}
|
{{ exchanging ? '验证中...' : '完成授权' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h4 class="text-sm font-semibold text-gray-700">代理设置 (可选)</h4>
|
<h4 class="text-sm font-semibold text-gray-700">
|
||||||
|
代理设置 (可选)
|
||||||
|
</h4>
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
v-model="proxy.enabled"
|
v-model="proxy.enabled"
|
||||||
|
type="checkbox"
|
||||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<span class="ml-2 text-sm text-gray-700">启用代理</span>
|
<span class="ml-2 text-sm text-gray-700">启用代理</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="proxy.enabled" class="bg-gray-50 p-4 rounded-lg border border-gray-200 space-y-4">
|
<div
|
||||||
|
v-if="proxy.enabled"
|
||||||
|
class="bg-gray-50 p-4 rounded-lg border border-gray-200 space-y-4"
|
||||||
|
>
|
||||||
<div class="flex items-start gap-3 mb-3">
|
<div class="flex items-start gap-3 mb-3">
|
||||||
<div class="w-8 h-8 bg-gray-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
<div class="w-8 h-8 bg-gray-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
<i class="fas fa-server text-white text-sm"></i>
|
<i class="fas fa-server text-white text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm text-gray-700">
|
<p class="text-sm text-gray-700">
|
||||||
@@ -33,9 +38,15 @@
|
|||||||
v-model="proxy.type"
|
v-model="proxy.type"
|
||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
>
|
>
|
||||||
<option value="socks5">SOCKS5</option>
|
<option value="socks5">
|
||||||
<option value="http">HTTP</option>
|
SOCKS5
|
||||||
<option value="https">HTTPS</option>
|
</option>
|
||||||
|
<option value="http">
|
||||||
|
HTTP
|
||||||
|
</option>
|
||||||
|
<option value="https">
|
||||||
|
HTTPS
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -63,17 +74,23 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
v-model="showAuth"
|
|
||||||
id="proxyAuth"
|
id="proxyAuth"
|
||||||
|
v-model="showAuth"
|
||||||
|
type="checkbox"
|
||||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<label for="proxyAuth" class="ml-2 text-sm text-gray-700 cursor-pointer">
|
<label
|
||||||
|
for="proxyAuth"
|
||||||
|
class="ml-2 text-sm text-gray-700 cursor-pointer"
|
||||||
|
>
|
||||||
需要身份验证
|
需要身份验证
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showAuth" class="grid grid-cols-2 gap-4">
|
<div
|
||||||
|
v-if="showAuth"
|
||||||
|
class="grid grid-cols-2 gap-4"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">用户名</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">用户名</label>
|
||||||
<input
|
<input
|
||||||
@@ -94,10 +111,10 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="showPassword = !showPassword"
|
|
||||||
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||||
|
@click="showPassword = !showPassword"
|
||||||
>
|
>
|
||||||
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,7 +123,7 @@
|
|||||||
|
|
||||||
<div class="bg-blue-50 p-3 rounded-lg border border-blue-200">
|
<div class="bg-blue-50 p-3 rounded-lg border border-blue-200">
|
||||||
<p class="text-xs text-blue-700">
|
<p class="text-xs text-blue-700">
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
<i class="fas fa-info-circle mr-1" />
|
||||||
<strong>提示:</strong>代理设置将用于所有与此账户相关的API请求。请确保代理服务器支持HTTPS流量转发。
|
<strong>提示:</strong>代理设置将用于所有与此账户相关的API请求。请确保代理服务器支持HTTPS流量转发。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,19 +5,24 @@
|
|||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
||||||
<i class="fas fa-key text-white"></i>
|
<i class="fas fa-key text-white" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-bold text-gray-900">创建新的 API Key</h3>
|
<h3 class="text-xl font-bold text-gray-900">
|
||||||
|
创建新的 API Key
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="$emit('close')"
|
|
||||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
@click="$emit('close')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times text-xl"></i>
|
<i class="fas fa-times text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="createApiKey" class="space-y-4 modal-scroll-content custom-scrollbar flex-1">
|
<form
|
||||||
|
class="space-y-4 modal-scroll-content custom-scrollbar flex-1"
|
||||||
|
@submit.prevent="createApiKey"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">名称 <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-semibold text-gray-700 mb-2">名称 <span class="text-red-500">*</span></label>
|
||||||
<input
|
<input
|
||||||
@@ -29,7 +34,12 @@
|
|||||||
placeholder="为您的 API Key 取一个名称"
|
placeholder="为您的 API Key 取一个名称"
|
||||||
@input="errors.name = ''"
|
@input="errors.name = ''"
|
||||||
>
|
>
|
||||||
<p v-if="errors.name" class="text-red-500 text-xs mt-1">{{ errors.name }}</p>
|
<p
|
||||||
|
v-if="errors.name"
|
||||||
|
class="text-red-500 text-xs mt-1"
|
||||||
|
>
|
||||||
|
{{ errors.name }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标签 -->
|
<!-- 标签 -->
|
||||||
@@ -38,14 +48,22 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- 已选择的标签 -->
|
<!-- 已选择的标签 -->
|
||||||
<div v-if="form.tags.length > 0">
|
<div v-if="form.tags.length > 0">
|
||||||
<div class="text-xs font-medium text-gray-600 mb-2">已选择的标签:</div>
|
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||||
|
已选择的标签:
|
||||||
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<span v-for="(tag, index) in form.tags" :key="'selected-' + index"
|
<span
|
||||||
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full">
|
v-for="(tag, index) in form.tags"
|
||||||
|
:key="'selected-' + index"
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"
|
||||||
|
>
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
<button type="button" @click="removeTag(index)"
|
<button
|
||||||
class="ml-1 hover:text-blue-900">
|
type="button"
|
||||||
<i class="fas fa-times text-xs"></i>
|
class="ml-1 hover:text-blue-900"
|
||||||
|
@click="removeTag(index)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times text-xs" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,16 +71,18 @@
|
|||||||
|
|
||||||
<!-- 可选择的已有标签 -->
|
<!-- 可选择的已有标签 -->
|
||||||
<div v-if="unselectedTags.length > 0">
|
<div v-if="unselectedTags.length > 0">
|
||||||
<div class="text-xs font-medium text-gray-600 mb-2">点击选择已有标签:</div>
|
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||||
|
点击选择已有标签:
|
||||||
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
v-for="tag in unselectedTags"
|
v-for="tag in unselectedTags"
|
||||||
:key="'available-' + tag"
|
:key="'available-' + tag"
|
||||||
type="button"
|
type="button"
|
||||||
@click="selectTag(tag)"
|
|
||||||
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
|
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
|
||||||
|
@click="selectTag(tag)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-tag text-gray-500 text-xs"></i>
|
<i class="fas fa-tag text-gray-500 text-xs" />
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +90,9 @@
|
|||||||
|
|
||||||
<!-- 创建新标签 -->
|
<!-- 创建新标签 -->
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-medium text-gray-600 mb-2">创建新标签:</div>
|
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||||
|
创建新标签:
|
||||||
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
v-model="newTag"
|
v-model="newTag"
|
||||||
@@ -79,14 +101,19 @@
|
|||||||
placeholder="输入新标签名称"
|
placeholder="输入新标签名称"
|
||||||
@keypress.enter.prevent="addTag"
|
@keypress.enter.prevent="addTag"
|
||||||
>
|
>
|
||||||
<button type="button" @click="addTag"
|
<button
|
||||||
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
type="button"
|
||||||
<i class="fas fa-plus"></i>
|
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
|
||||||
|
@click="addTag"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-xs text-gray-500">用于标记不同团队或用途,方便筛选管理</p>
|
<p class="text-xs text-gray-500">
|
||||||
|
用于标记不同团队或用途,方便筛选管理
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -94,9 +121,11 @@
|
|||||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
|
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
|
||||||
<i class="fas fa-tachometer-alt text-white text-xs"></i>
|
<i class="fas fa-tachometer-alt text-white text-xs" />
|
||||||
</div>
|
</div>
|
||||||
<h4 class="font-semibold text-gray-800 text-sm">速率限制设置 (可选)</h4>
|
<h4 class="font-semibold text-gray-800 text-sm">
|
||||||
|
速率限制设置 (可选)
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -110,7 +139,9 @@
|
|||||||
placeholder="无限制"
|
placeholder="无限制"
|
||||||
class="form-input w-full text-sm"
|
class="form-input w-full text-sm"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">时间段单位</p>
|
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||||
|
时间段单位
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -122,7 +153,9 @@
|
|||||||
placeholder="无限制"
|
placeholder="无限制"
|
||||||
class="form-input w-full text-sm"
|
class="form-input w-full text-sm"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大请求</p>
|
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||||
|
窗口内最大请求
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -133,13 +166,17 @@
|
|||||||
placeholder="无限制"
|
placeholder="无限制"
|
||||||
class="form-input w-full text-sm"
|
class="form-input w-full text-sm"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大Token</p>
|
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||||
|
窗口内最大Token
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 示例说明 -->
|
<!-- 示例说明 -->
|
||||||
<div class="bg-blue-100 rounded-lg p-2">
|
<div class="bg-blue-100 rounded-lg p-2">
|
||||||
<h5 class="text-xs font-semibold text-blue-800 mb-1">💡 使用示例</h5>
|
<h5 class="text-xs font-semibold text-blue-800 mb-1">
|
||||||
|
💡 使用示例
|
||||||
|
</h5>
|
||||||
<div class="text-xs text-blue-700 space-y-0.5">
|
<div class="text-xs text-blue-700 space-y-0.5">
|
||||||
<div><strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求</div>
|
<div><strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求</div>
|
||||||
<div><strong>示例2:</strong> 时间窗口=1,Token=10000 → 每分钟最多10,000个Token</div>
|
<div><strong>示例2:</strong> 时间窗口=1,Token=10000 → 每分钟最多10,000个Token</div>
|
||||||
@@ -153,10 +190,34 @@
|
|||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">每日费用限制 (美元)</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-2">每日费用限制 (美元)</label>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button type="button" @click="form.dailyCostLimit = '50'" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">$50</button>
|
<button
|
||||||
<button type="button" @click="form.dailyCostLimit = '100'" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">$100</button>
|
type="button"
|
||||||
<button type="button" @click="form.dailyCostLimit = '200'" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">$200</button>
|
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
|
||||||
<button type="button" @click="form.dailyCostLimit = ''" class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium">自定义</button>
|
@click="form.dailyCostLimit = '50'"
|
||||||
|
>
|
||||||
|
$50
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
|
||||||
|
@click="form.dailyCostLimit = '100'"
|
||||||
|
>
|
||||||
|
$100
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
|
||||||
|
@click="form.dailyCostLimit = '200'"
|
||||||
|
>
|
||||||
|
$200
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"
|
||||||
|
@click="form.dailyCostLimit = ''"
|
||||||
|
>
|
||||||
|
自定义
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model="form.dailyCostLimit"
|
v-model="form.dailyCostLimit"
|
||||||
@@ -166,7 +227,9 @@
|
|||||||
placeholder="0 表示无限制"
|
placeholder="0 表示无限制"
|
||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500">设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制</p>
|
<p class="text-xs text-gray-500">
|
||||||
|
设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -179,7 +242,9 @@
|
|||||||
placeholder="0 表示无限制"
|
placeholder="0 表示无限制"
|
||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-2">设置此 API Key 可同时处理的最大请求数,0 或留空表示无限制</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
设置此 API Key 可同时处理的最大请求数,0 或留空表示无限制
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -189,26 +254,45 @@
|
|||||||
rows="2"
|
rows="2"
|
||||||
class="form-input w-full resize-none text-sm"
|
class="form-input w-full resize-none text-sm"
|
||||||
placeholder="描述此 API Key 的用途..."
|
placeholder="描述此 API Key 的用途..."
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">有效期限</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-2">有效期限</label>
|
||||||
<select
|
<select
|
||||||
v-model="form.expireDuration"
|
v-model="form.expireDuration"
|
||||||
@change="updateExpireAt"
|
|
||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
|
@change="updateExpireAt"
|
||||||
>
|
>
|
||||||
<option value="">永不过期</option>
|
<option value="">
|
||||||
<option value="1d">1 天</option>
|
永不过期
|
||||||
<option value="7d">7 天</option>
|
</option>
|
||||||
<option value="30d">30 天</option>
|
<option value="1d">
|
||||||
<option value="90d">90 天</option>
|
1 天
|
||||||
<option value="180d">180 天</option>
|
</option>
|
||||||
<option value="365d">365 天</option>
|
<option value="7d">
|
||||||
<option value="custom">自定义日期</option>
|
7 天
|
||||||
|
</option>
|
||||||
|
<option value="30d">
|
||||||
|
30 天
|
||||||
|
</option>
|
||||||
|
<option value="90d">
|
||||||
|
90 天
|
||||||
|
</option>
|
||||||
|
<option value="180d">
|
||||||
|
180 天
|
||||||
|
</option>
|
||||||
|
<option value="365d">
|
||||||
|
365 天
|
||||||
|
</option>
|
||||||
|
<option value="custom">
|
||||||
|
自定义日期
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div v-if="form.expireDuration === 'custom'" class="mt-3">
|
<div
|
||||||
|
v-if="form.expireDuration === 'custom'"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="form.customExpireDate"
|
v-model="form.customExpireDate"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
@@ -217,7 +301,10 @@
|
|||||||
@change="updateCustomExpireAt"
|
@change="updateCustomExpireAt"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="form.expiresAt" class="text-xs text-gray-500 mt-2">
|
<p
|
||||||
|
v-if="form.expiresAt"
|
||||||
|
class="text-xs text-gray-500 mt-2"
|
||||||
|
>
|
||||||
将于 {{ formatExpireDate(form.expiresAt) }} 过期
|
将于 {{ formatExpireDate(form.expiresAt) }} 过期
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,8 +314,8 @@
|
|||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.permissions"
|
v-model="form.permissions"
|
||||||
|
type="radio"
|
||||||
value="all"
|
value="all"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -236,8 +323,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.permissions"
|
v-model="form.permissions"
|
||||||
|
type="radio"
|
||||||
value="claude"
|
value="claude"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -245,15 +332,17 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.permissions"
|
v-model="form.permissions"
|
||||||
|
type="radio"
|
||||||
value="gemini"
|
value="gemini"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
<span class="text-sm text-gray-700">仅 Gemini</span>
|
<span class="text-sm text-gray-700">仅 Gemini</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-2">控制此 API Key 可以访问哪些服务</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
控制此 API Key 可以访问哪些服务
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -266,7 +355,9 @@
|
|||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
:disabled="form.permissions === 'gemini'"
|
:disabled="form.permissions === 'gemini'"
|
||||||
>
|
>
|
||||||
<option value="">使用共享账号池</option>
|
<option value="">
|
||||||
|
使用共享账号池
|
||||||
|
</option>
|
||||||
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
|
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
|
||||||
<option
|
<option
|
||||||
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
|
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
|
||||||
@@ -294,7 +385,9 @@
|
|||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
:disabled="form.permissions === 'claude'"
|
:disabled="form.permissions === 'claude'"
|
||||||
>
|
>
|
||||||
<option value="">使用共享账号池</option>
|
<option value="">
|
||||||
|
使用共享账号池
|
||||||
|
</option>
|
||||||
<option
|
<option
|
||||||
v-for="account in accounts.gemini.filter(a => a.isDedicated)"
|
v-for="account in accounts.gemini.filter(a => a.isDedicated)"
|
||||||
:key="account.id"
|
:key="account.id"
|
||||||
@@ -305,23 +398,31 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-2">选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
v-model="form.enableModelRestriction"
|
|
||||||
id="enableModelRestriction"
|
id="enableModelRestriction"
|
||||||
|
v-model="form.enableModelRestriction"
|
||||||
|
type="checkbox"
|
||||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<label for="enableModelRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
|
<label
|
||||||
|
for="enableModelRestriction"
|
||||||
|
class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer"
|
||||||
|
>
|
||||||
启用模型限制
|
启用模型限制
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="form.enableModelRestriction" class="space-y-2 bg-red-50 border border-red-200 rounded-lg p-3">
|
<div
|
||||||
|
v-if="form.enableModelRestriction"
|
||||||
|
class="space-y-2 bg-red-50 border border-red-200 rounded-lg p-3"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">限制的模型列表</label>
|
<label class="block text-xs font-medium text-gray-700 mb-1">限制的模型列表</label>
|
||||||
<div class="flex flex-wrap gap-1 mb-2 min-h-[24px]">
|
<div class="flex flex-wrap gap-1 mb-2 min-h-[24px]">
|
||||||
@@ -333,33 +434,38 @@
|
|||||||
{{ model }}
|
{{ model }}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="removeRestrictedModel(index)"
|
|
||||||
class="ml-1 text-red-600 hover:text-red-800"
|
class="ml-1 text-red-600 hover:text-red-800"
|
||||||
|
@click="removeRestrictedModel(index)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times text-xs"></i>
|
<i class="fas fa-times text-xs" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="form.restrictedModels.length === 0" class="text-gray-400 text-xs">
|
<span
|
||||||
|
v-if="form.restrictedModels.length === 0"
|
||||||
|
class="text-gray-400 text-xs"
|
||||||
|
>
|
||||||
暂无限制的模型
|
暂无限制的模型
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
v-model="form.modelInput"
|
v-model="form.modelInput"
|
||||||
@keydown.enter.prevent="addRestrictedModel"
|
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="输入模型名称,按回车添加"
|
placeholder="输入模型名称,按回车添加"
|
||||||
class="form-input flex-1 text-sm"
|
class="form-input flex-1 text-sm"
|
||||||
|
@keydown.enter.prevent="addRestrictedModel"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="addRestrictedModel"
|
|
||||||
class="px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm"
|
class="px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm"
|
||||||
|
@click="addRestrictedModel"
|
||||||
>
|
>
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-1">例如:claude-opus-4-20250514</p>
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
例如:claude-opus-4-20250514
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -368,29 +474,42 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
v-model="form.enableClientRestriction"
|
|
||||||
id="enableClientRestriction"
|
id="enableClientRestriction"
|
||||||
|
v-model="form.enableClientRestriction"
|
||||||
|
type="checkbox"
|
||||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<label for="enableClientRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
|
<label
|
||||||
|
for="enableClientRestriction"
|
||||||
|
class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer"
|
||||||
|
>
|
||||||
启用客户端限制
|
启用客户端限制
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="form.enableClientRestriction" class="bg-green-50 border border-green-200 rounded-lg p-3">
|
<div
|
||||||
|
v-if="form.enableClientRestriction"
|
||||||
|
class="bg-green-50 border border-green-200 rounded-lg p-3"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-2">允许的客户端</label>
|
<label class="block text-xs font-medium text-gray-700 mb-2">允许的客户端</label>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
|
<div
|
||||||
|
v-for="client in supportedClients"
|
||||||
|
:key="client.id"
|
||||||
|
class="flex items-start"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
:id="`client_${client.id}`"
|
:id="`client_${client.id}`"
|
||||||
:value="client.id"
|
|
||||||
v-model="form.allowedClients"
|
v-model="form.allowedClients"
|
||||||
|
type="checkbox"
|
||||||
|
:value="client.id"
|
||||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
|
||||||
>
|
>
|
||||||
<label :for="`client_${client.id}`" class="ml-2 flex-1 cursor-pointer">
|
<label
|
||||||
|
:for="`client_${client.id}`"
|
||||||
|
class="ml-2 flex-1 cursor-pointer"
|
||||||
|
>
|
||||||
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
||||||
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
|
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -403,8 +522,8 @@
|
|||||||
<div class="flex gap-3 pt-2">
|
<div class="flex gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="$emit('close')"
|
|
||||||
class="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg font-semibold hover:bg-gray-200 transition-colors text-sm"
|
class="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg font-semibold hover:bg-gray-200 transition-colors text-sm"
|
||||||
|
@click="$emit('close')"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
@@ -413,8 +532,14 @@
|
|||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="btn btn-primary flex-1 py-2.5 px-4 font-semibold text-sm"
|
class="btn btn-primary flex-1 py-2.5 px-4 font-semibold text-sm"
|
||||||
>
|
>
|
||||||
<div v-if="loading" class="loading-spinner mr-2"></div>
|
<div
|
||||||
<i v-else class="fas fa-plus mr-2"></i>
|
v-if="loading"
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-plus mr-2"
|
||||||
|
/>
|
||||||
{{ loading ? '创建中...' : '创建' }}
|
{{ loading ? '创建中...' : '创建' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,19 +5,24 @@
|
|||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
||||||
<i class="fas fa-edit text-white"></i>
|
<i class="fas fa-edit text-white" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-bold text-gray-900">编辑 API Key</h3>
|
<h3 class="text-xl font-bold text-gray-900">
|
||||||
|
编辑 API Key
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="$emit('close')"
|
|
||||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
@click="$emit('close')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times text-xl"></i>
|
<i class="fas fa-times text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="updateApiKey" class="space-y-6 modal-scroll-content custom-scrollbar flex-1">
|
<form
|
||||||
|
class="space-y-6 modal-scroll-content custom-scrollbar flex-1"
|
||||||
|
@submit.prevent="updateApiKey"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">名称</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-3">名称</label>
|
||||||
<input
|
<input
|
||||||
@@ -26,7 +31,9 @@
|
|||||||
disabled
|
disabled
|
||||||
class="form-input w-full bg-gray-100 cursor-not-allowed"
|
class="form-input w-full bg-gray-100 cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-2">名称不可修改</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
名称不可修改
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标签 -->
|
<!-- 标签 -->
|
||||||
@@ -35,14 +42,22 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- 已选择的标签 -->
|
<!-- 已选择的标签 -->
|
||||||
<div v-if="form.tags.length > 0">
|
<div v-if="form.tags.length > 0">
|
||||||
<div class="text-xs font-medium text-gray-600 mb-2">已选择的标签:</div>
|
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||||
|
已选择的标签:
|
||||||
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<span v-for="(tag, index) in form.tags" :key="'selected-' + index"
|
<span
|
||||||
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full">
|
v-for="(tag, index) in form.tags"
|
||||||
|
:key="'selected-' + index"
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"
|
||||||
|
>
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
<button type="button" @click="removeTag(index)"
|
<button
|
||||||
class="ml-1 hover:text-blue-900">
|
type="button"
|
||||||
<i class="fas fa-times text-xs"></i>
|
class="ml-1 hover:text-blue-900"
|
||||||
|
@click="removeTag(index)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times text-xs" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,16 +65,18 @@
|
|||||||
|
|
||||||
<!-- 可选择的已有标签 -->
|
<!-- 可选择的已有标签 -->
|
||||||
<div v-if="unselectedTags.length > 0">
|
<div v-if="unselectedTags.length > 0">
|
||||||
<div class="text-xs font-medium text-gray-600 mb-2">点击选择已有标签:</div>
|
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||||
|
点击选择已有标签:
|
||||||
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
v-for="tag in unselectedTags"
|
v-for="tag in unselectedTags"
|
||||||
:key="'available-' + tag"
|
:key="'available-' + tag"
|
||||||
type="button"
|
type="button"
|
||||||
@click="selectTag(tag)"
|
|
||||||
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
|
class="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-blue-100 hover:text-blue-700 transition-colors"
|
||||||
|
@click="selectTag(tag)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-tag text-gray-500 text-xs"></i>
|
<i class="fas fa-tag text-gray-500 text-xs" />
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +84,9 @@
|
|||||||
|
|
||||||
<!-- 创建新标签 -->
|
<!-- 创建新标签 -->
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-medium text-gray-600 mb-2">创建新标签:</div>
|
<div class="text-xs font-medium text-gray-600 mb-2">
|
||||||
|
创建新标签:
|
||||||
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
v-model="newTag"
|
v-model="newTag"
|
||||||
@@ -76,14 +95,19 @@
|
|||||||
placeholder="输入新标签名称"
|
placeholder="输入新标签名称"
|
||||||
@keypress.enter.prevent="addTag"
|
@keypress.enter.prevent="addTag"
|
||||||
>
|
>
|
||||||
<button type="button" @click="addTag"
|
<button
|
||||||
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
type="button"
|
||||||
<i class="fas fa-plus"></i>
|
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
|
||||||
|
@click="addTag"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-xs text-gray-500">用于标记不同团队或用途,方便筛选管理</p>
|
<p class="text-xs text-gray-500">
|
||||||
|
用于标记不同团队或用途,方便筛选管理
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,9 +115,11 @@
|
|||||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
|
<div class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0">
|
||||||
<i class="fas fa-tachometer-alt text-white text-xs"></i>
|
<i class="fas fa-tachometer-alt text-white text-xs" />
|
||||||
</div>
|
</div>
|
||||||
<h4 class="font-semibold text-gray-800 text-sm">速率限制设置 (可选)</h4>
|
<h4 class="font-semibold text-gray-800 text-sm">
|
||||||
|
速率限制设置 (可选)
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -107,7 +133,9 @@
|
|||||||
placeholder="无限制"
|
placeholder="无限制"
|
||||||
class="form-input w-full text-sm"
|
class="form-input w-full text-sm"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">时间段单位</p>
|
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||||
|
时间段单位
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -119,7 +147,9 @@
|
|||||||
placeholder="无限制"
|
placeholder="无限制"
|
||||||
class="form-input w-full text-sm"
|
class="form-input w-full text-sm"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大请求</p>
|
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||||
|
窗口内最大请求
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -130,13 +160,17 @@
|
|||||||
placeholder="无限制"
|
placeholder="无限制"
|
||||||
class="form-input w-full text-sm"
|
class="form-input w-full text-sm"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-0.5 ml-2">窗口内最大Token</p>
|
<p class="text-xs text-gray-500 mt-0.5 ml-2">
|
||||||
|
窗口内最大Token
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 示例说明 -->
|
<!-- 示例说明 -->
|
||||||
<div class="bg-blue-100 rounded-lg p-2">
|
<div class="bg-blue-100 rounded-lg p-2">
|
||||||
<h5 class="text-xs font-semibold text-blue-800 mb-1">💡 使用示例</h5>
|
<h5 class="text-xs font-semibold text-blue-800 mb-1">
|
||||||
|
💡 使用示例
|
||||||
|
</h5>
|
||||||
<div class="text-xs text-blue-700 space-y-0.5">
|
<div class="text-xs text-blue-700 space-y-0.5">
|
||||||
<div><strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求</div>
|
<div><strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求</div>
|
||||||
<div><strong>示例2:</strong> 时间窗口=1,Token=10000 → 每分钟最多10,000个Token</div>
|
<div><strong>示例2:</strong> 时间窗口=1,Token=10000 → 每分钟最多10,000个Token</div>
|
||||||
@@ -150,10 +184,34 @@
|
|||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">每日费用限制 (美元)</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-3">每日费用限制 (美元)</label>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button type="button" @click="form.dailyCostLimit = '50'" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">$50</button>
|
<button
|
||||||
<button type="button" @click="form.dailyCostLimit = '100'" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">$100</button>
|
type="button"
|
||||||
<button type="button" @click="form.dailyCostLimit = '200'" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">$200</button>
|
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
|
||||||
<button type="button" @click="form.dailyCostLimit = ''" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium">自定义</button>
|
@click="form.dailyCostLimit = '50'"
|
||||||
|
>
|
||||||
|
$50
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
|
||||||
|
@click="form.dailyCostLimit = '100'"
|
||||||
|
>
|
||||||
|
$100
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
|
||||||
|
@click="form.dailyCostLimit = '200'"
|
||||||
|
>
|
||||||
|
$200
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"
|
||||||
|
@click="form.dailyCostLimit = ''"
|
||||||
|
>
|
||||||
|
自定义
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model="form.dailyCostLimit"
|
v-model="form.dailyCostLimit"
|
||||||
@@ -163,7 +221,9 @@
|
|||||||
placeholder="0 表示无限制"
|
placeholder="0 表示无限制"
|
||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500">设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制</p>
|
<p class="text-xs text-gray-500">
|
||||||
|
设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -176,7 +236,9 @@
|
|||||||
placeholder="0 表示无限制"
|
placeholder="0 表示无限制"
|
||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-2">设置此 API Key 可同时处理的最大请求数</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
设置此 API Key 可同时处理的最大请求数
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -184,8 +246,8 @@
|
|||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.permissions"
|
v-model="form.permissions"
|
||||||
|
type="radio"
|
||||||
value="all"
|
value="all"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -193,8 +255,8 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.permissions"
|
v-model="form.permissions"
|
||||||
|
type="radio"
|
||||||
value="claude"
|
value="claude"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
@@ -202,15 +264,17 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="flex items-center cursor-pointer">
|
<label class="flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
|
||||||
v-model="form.permissions"
|
v-model="form.permissions"
|
||||||
|
type="radio"
|
||||||
value="gemini"
|
value="gemini"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
<span class="text-sm text-gray-700">仅 Gemini</span>
|
<span class="text-sm text-gray-700">仅 Gemini</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-2">控制此 API Key 可以访问哪些服务</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
控制此 API Key 可以访问哪些服务
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -223,7 +287,9 @@
|
|||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
:disabled="form.permissions === 'gemini'"
|
:disabled="form.permissions === 'gemini'"
|
||||||
>
|
>
|
||||||
<option value="">使用共享账号池</option>
|
<option value="">
|
||||||
|
使用共享账号池
|
||||||
|
</option>
|
||||||
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
|
<optgroup v-if="accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth').length > 0" label="Claude OAuth 账号">
|
||||||
<option
|
<option
|
||||||
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
|
v-for="account in accounts.claude.filter(a => a.isDedicated && a.platform === 'claude-oauth')"
|
||||||
@@ -251,9 +317,11 @@
|
|||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
:disabled="form.permissions === 'claude'"
|
:disabled="form.permissions === 'claude'"
|
||||||
>
|
>
|
||||||
<option value="">使用共享账号池</option>
|
<option value="">
|
||||||
|
使用共享账号池
|
||||||
|
</option>
|
||||||
<option
|
<option
|
||||||
v-for="account in accounts.gemini.filter(a => a.isDedicated)"
|
v-for="account in accounts.gemini"
|
||||||
:key="account.id"
|
:key="account.id"
|
||||||
:value="account.id"
|
:value="account.id"
|
||||||
>
|
>
|
||||||
@@ -262,23 +330,31 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-2">修改绑定账号将影响此API Key的请求路由</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
修改绑定账号将影响此API Key的请求路由
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
v-model="form.enableModelRestriction"
|
|
||||||
id="editEnableModelRestriction"
|
id="editEnableModelRestriction"
|
||||||
|
v-model="form.enableModelRestriction"
|
||||||
|
type="checkbox"
|
||||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<label for="editEnableModelRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
|
<label
|
||||||
|
for="editEnableModelRestriction"
|
||||||
|
class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer"
|
||||||
|
>
|
||||||
启用模型限制
|
启用模型限制
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="form.enableModelRestriction" class="space-y-3">
|
<div
|
||||||
|
v-if="form.enableModelRestriction"
|
||||||
|
class="space-y-3"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-600 mb-2">限制的模型列表</label>
|
<label class="block text-sm font-medium text-gray-600 mb-2">限制的模型列表</label>
|
||||||
<div class="flex flex-wrap gap-2 mb-3 min-h-[32px] p-2 bg-gray-50 rounded-lg border border-gray-200">
|
<div class="flex flex-wrap gap-2 mb-3 min-h-[32px] p-2 bg-gray-50 rounded-lg border border-gray-200">
|
||||||
@@ -290,33 +366,38 @@
|
|||||||
{{ model }}
|
{{ model }}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="removeRestrictedModel(index)"
|
|
||||||
class="ml-2 text-red-600 hover:text-red-800"
|
class="ml-2 text-red-600 hover:text-red-800"
|
||||||
|
@click="removeRestrictedModel(index)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times text-xs"></i>
|
<i class="fas fa-times text-xs" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="form.restrictedModels.length === 0" class="text-gray-400 text-sm">
|
<span
|
||||||
|
v-if="form.restrictedModels.length === 0"
|
||||||
|
class="text-gray-400 text-sm"
|
||||||
|
>
|
||||||
暂无限制的模型
|
暂无限制的模型
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
v-model="form.modelInput"
|
v-model="form.modelInput"
|
||||||
@keydown.enter.prevent="addRestrictedModel"
|
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="输入模型名称,按回车添加"
|
placeholder="输入模型名称,按回车添加"
|
||||||
class="form-input flex-1"
|
class="form-input flex-1"
|
||||||
|
@keydown.enter.prevent="addRestrictedModel"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="addRestrictedModel"
|
|
||||||
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||||
|
@click="addRestrictedModel"
|
||||||
>
|
>
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-2">设置此API Key无法访问的模型,例如:claude-opus-4-20250514</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
设置此API Key无法访问的模型,例如:claude-opus-4-20250514
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -325,30 +406,45 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
v-model="form.enableClientRestriction"
|
|
||||||
id="editEnableClientRestriction"
|
id="editEnableClientRestriction"
|
||||||
|
v-model="form.enableClientRestriction"
|
||||||
|
type="checkbox"
|
||||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<label for="editEnableClientRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
|
<label
|
||||||
|
for="editEnableClientRestriction"
|
||||||
|
class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer"
|
||||||
|
>
|
||||||
启用客户端限制
|
启用客户端限制
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="form.enableClientRestriction" class="space-y-3">
|
<div
|
||||||
|
v-if="form.enableClientRestriction"
|
||||||
|
class="space-y-3"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-600 mb-2">允许的客户端</label>
|
<label class="block text-sm font-medium text-gray-600 mb-2">允许的客户端</label>
|
||||||
<p class="text-xs text-gray-500 mb-3">勾选允许使用此API Key的客户端</p>
|
<p class="text-xs text-gray-500 mb-3">
|
||||||
|
勾选允许使用此API Key的客户端
|
||||||
|
</p>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
|
<div
|
||||||
|
v-for="client in supportedClients"
|
||||||
|
:key="client.id"
|
||||||
|
class="flex items-start"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
|
||||||
:id="`edit_client_${client.id}`"
|
:id="`edit_client_${client.id}`"
|
||||||
:value="client.id"
|
|
||||||
v-model="form.allowedClients"
|
v-model="form.allowedClients"
|
||||||
|
type="checkbox"
|
||||||
|
:value="client.id"
|
||||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
|
||||||
>
|
>
|
||||||
<label :for="`edit_client_${client.id}`" class="ml-2 flex-1 cursor-pointer">
|
<label
|
||||||
|
:for="`edit_client_${client.id}`"
|
||||||
|
class="ml-2 flex-1 cursor-pointer"
|
||||||
|
>
|
||||||
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
||||||
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
|
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -361,8 +457,8 @@
|
|||||||
<div class="flex gap-3 pt-4">
|
<div class="flex gap-3 pt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="$emit('close')"
|
|
||||||
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
||||||
|
@click="$emit('close')"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
@@ -371,8 +467,14 @@
|
|||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||||
>
|
>
|
||||||
<div v-if="loading" class="loading-spinner mr-2"></div>
|
<div
|
||||||
<i v-else class="fas fa-save mr-2"></i>
|
v-if="loading"
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-save mr-2"
|
||||||
|
/>
|
||||||
{{ loading ? '保存中...' : '保存修改' }}
|
{{ loading ? '保存中...' : '保存修改' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,7 +487,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
|
||||||
import { useClientsStore } from '@/stores/clients'
|
import { useClientsStore } from '@/stores/clients'
|
||||||
import { useApiKeysStore } from '@/stores/apiKeys'
|
import { useApiKeysStore } from '@/stores/apiKeys'
|
||||||
import { apiClient } from '@/config/api'
|
import { apiClient } from '@/config/api'
|
||||||
@@ -403,7 +504,6 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['close', 'success'])
|
const emit = defineEmits(['close', 'success'])
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
const clientsStore = useClientsStore()
|
const clientsStore = useClientsStore()
|
||||||
const apiKeysStore = useApiKeysStore()
|
const apiKeysStore = useApiKeysStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -558,8 +658,9 @@ onMounted(async () => {
|
|||||||
form.restrictedModels = props.apiKey.restrictedModels || []
|
form.restrictedModels = props.apiKey.restrictedModels || []
|
||||||
form.allowedClients = props.apiKey.allowedClients || []
|
form.allowedClients = props.apiKey.allowedClients || []
|
||||||
form.tags = props.apiKey.tags || []
|
form.tags = props.apiKey.tags || []
|
||||||
form.enableModelRestriction = form.restrictedModels.length > 0
|
// 从后端数据中获取实际的启用状态,而不是根据数组长度推断
|
||||||
form.enableClientRestriction = form.allowedClients.length > 0
|
form.enableModelRestriction = props.apiKey.enableModelRestriction || false
|
||||||
|
form.enableClientRestriction = props.apiKey.enableClientRestriction || false
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,23 @@
|
|||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
||||||
<i class="fas fa-check text-white text-lg"></i>
|
<i class="fas fa-check text-white text-lg" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-gray-900">API Key 创建成功</h3>
|
<h3 class="text-xl font-bold text-gray-900">
|
||||||
<p class="text-sm text-gray-600">请妥善保存您的 API Key</p>
|
API Key 创建成功
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
请妥善保存您的 API Key
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="handleDirectClose"
|
|
||||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
title="直接关闭(不推荐)"
|
title="直接关闭(不推荐)"
|
||||||
|
@click="handleDirectClose"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times text-xl"></i>
|
<i class="fas fa-times text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -25,10 +29,12 @@
|
|||||||
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 mb-6">
|
<div class="bg-amber-50 border-l-4 border-amber-400 p-4 mb-6">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<div class="w-6 h-6 bg-amber-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
<div class="w-6 h-6 bg-amber-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||||
<i class="fas fa-exclamation-triangle text-white text-sm"></i>
|
<i class="fas fa-exclamation-triangle text-white text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<h5 class="font-semibold text-amber-900 mb-1">重要提醒</h5>
|
<h5 class="font-semibold text-amber-900 mb-1">
|
||||||
|
重要提醒
|
||||||
|
</h5>
|
||||||
<p class="text-sm text-amber-800">
|
<p class="text-sm text-amber-800">
|
||||||
这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即复制并妥善保存。
|
这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即复制并妥善保存。
|
||||||
</p>
|
</p>
|
||||||
@@ -60,12 +66,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="absolute top-3 right-3">
|
<div class="absolute top-3 right-3">
|
||||||
<button
|
<button
|
||||||
@click="toggleKeyVisibility"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-icon-sm hover:bg-gray-800 bg-gray-700"
|
class="btn-icon-sm hover:bg-gray-800 bg-gray-700"
|
||||||
:title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
|
:title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
|
||||||
|
@click="toggleKeyVisibility"
|
||||||
>
|
>
|
||||||
<i :class="['fas', showFullKey ? 'fa-eye-slash' : 'fa-eye', 'text-gray-300']"></i>
|
<i :class="['fas', showFullKey ? 'fa-eye-slash' : 'fa-eye', 'text-gray-300']" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,15 +84,15 @@
|
|||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button
|
<button
|
||||||
@click="copyApiKey"
|
|
||||||
class="flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"
|
class="flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"
|
||||||
|
@click="copyApiKey"
|
||||||
>
|
>
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy" />
|
||||||
复制 API Key
|
复制 API Key
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="handleClose"
|
|
||||||
class="px-6 py-3 bg-gray-200 text-gray-800 rounded-xl font-semibold hover:bg-gray-300 transition-colors border border-gray-300"
|
class="px-6 py-3 bg-gray-200 text-gray-800 rounded-xl font-semibold hover:bg-gray-300 transition-colors border border-gray-300"
|
||||||
|
@click="handleClose"
|
||||||
>
|
>
|
||||||
我已保存
|
我已保存
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,15 +5,17 @@
|
|||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
<div class="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
||||||
<i class="fas fa-clock text-white"></i>
|
<i class="fas fa-clock text-white" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-bold text-gray-900">续期 API Key</h3>
|
<h3 class="text-xl font-bold text-gray-900">
|
||||||
|
续期 API Key
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="$emit('close')"
|
|
||||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
@click="$emit('close')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times text-xl"></i>
|
<i class="fas fa-times text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -21,11 +23,15 @@
|
|||||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
<i class="fas fa-info text-white text-sm"></i>
|
<i class="fas fa-info text-white text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-semibold text-gray-800 mb-1">API Key 信息</h4>
|
<h4 class="font-semibold text-gray-800 mb-1">
|
||||||
<p class="text-sm text-gray-700">{{ apiKey.name }}</p>
|
API Key 信息
|
||||||
|
</h4>
|
||||||
|
<p class="text-sm text-gray-700">
|
||||||
|
{{ apiKey.name }}
|
||||||
|
</p>
|
||||||
<p class="text-xs text-gray-600 mt-1">
|
<p class="text-xs text-gray-600 mt-1">
|
||||||
当前过期时间:{{ apiKey.expiresAt ? formatExpireDate(apiKey.expiresAt) : '永不过期' }}
|
当前过期时间:{{ apiKey.expiresAt ? formatExpireDate(apiKey.expiresAt) : '永不过期' }}
|
||||||
</p>
|
</p>
|
||||||
@@ -37,18 +43,35 @@
|
|||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">续期时长</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-3">续期时长</label>
|
||||||
<select
|
<select
|
||||||
v-model="form.renewDuration"
|
v-model="form.renewDuration"
|
||||||
@change="updateRenewExpireAt"
|
|
||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
|
@change="updateRenewExpireAt"
|
||||||
>
|
>
|
||||||
<option value="7d">延长 7 天</option>
|
<option value="7d">
|
||||||
<option value="30d">延长 30 天</option>
|
延长 7 天
|
||||||
<option value="90d">延长 90 天</option>
|
</option>
|
||||||
<option value="180d">延长 180 天</option>
|
<option value="30d">
|
||||||
<option value="365d">延长 365 天</option>
|
延长 30 天
|
||||||
<option value="custom">自定义日期</option>
|
</option>
|
||||||
<option value="permanent">设为永不过期</option>
|
<option value="90d">
|
||||||
|
延长 90 天
|
||||||
|
</option>
|
||||||
|
<option value="180d">
|
||||||
|
延长 180 天
|
||||||
|
</option>
|
||||||
|
<option value="365d">
|
||||||
|
延长 365 天
|
||||||
|
</option>
|
||||||
|
<option value="custom">
|
||||||
|
自定义日期
|
||||||
|
</option>
|
||||||
|
<option value="permanent">
|
||||||
|
设为永不过期
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div v-if="form.renewDuration === 'custom'" class="mt-3">
|
<div
|
||||||
|
v-if="form.renewDuration === 'custom'"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="form.customExpireDate"
|
v-model="form.customExpireDate"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
@@ -57,7 +80,10 @@
|
|||||||
@change="updateCustomRenewExpireAt"
|
@change="updateCustomRenewExpireAt"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="form.newExpiresAt" class="text-xs text-gray-500 mt-2">
|
<p
|
||||||
|
v-if="form.newExpiresAt"
|
||||||
|
class="text-xs text-gray-500 mt-2"
|
||||||
|
>
|
||||||
新的过期时间:{{ formatExpireDate(form.newExpiresAt) }}
|
新的过期时间:{{ formatExpireDate(form.newExpiresAt) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,19 +92,25 @@
|
|||||||
<div class="flex gap-3 pt-4">
|
<div class="flex gap-3 pt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="$emit('close')"
|
|
||||||
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
||||||
|
@click="$emit('close')"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="renewApiKey"
|
|
||||||
:disabled="loading || !form.renewDuration"
|
:disabled="loading || !form.renewDuration"
|
||||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||||
|
@click="renewApiKey"
|
||||||
>
|
>
|
||||||
<div v-if="loading" class="loading-spinner mr-2"></div>
|
<div
|
||||||
<i v-else class="fas fa-clock mr-2"></i>
|
v-if="loading"
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-clock mr-2"
|
||||||
|
/>
|
||||||
{{ loading ? '续期中...' : '确认续期' }}
|
{{ loading ? '续期中...' : '确认续期' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
<!-- 标题区域 -->
|
<!-- 标题区域 -->
|
||||||
<div class="wide-card-title text-center mb-6">
|
<div class="wide-card-title text-center mb-6">
|
||||||
<h2 class="text-2xl font-bold mb-2">
|
<h2 class="text-2xl font-bold mb-2">
|
||||||
<i class="fas fa-chart-line mr-3"></i>
|
<i class="fas fa-chart-line mr-3" />
|
||||||
使用统计查询
|
使用统计查询
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-base text-gray-600">查询您的 API Key 使用情况和统计数据</p>
|
<p class="text-base text-gray-600">
|
||||||
|
查询您的 API Key 使用情况和统计数据
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输入区域 -->
|
<!-- 输入区域 -->
|
||||||
@@ -15,7 +17,7 @@
|
|||||||
<!-- API Key 输入 -->
|
<!-- API Key 输入 -->
|
||||||
<div class="lg:col-span-3">
|
<div class="lg:col-span-3">
|
||||||
<label class="block text-sm font-medium mb-2 text-gray-700">
|
<label class="block text-sm font-medium mb-2 text-gray-700">
|
||||||
<i class="fas fa-key mr-2"></i>
|
<i class="fas fa-key mr-2" />
|
||||||
输入您的 API Key
|
输入您的 API Key
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -23,8 +25,8 @@
|
|||||||
type="password"
|
type="password"
|
||||||
placeholder="请输入您的 API Key (cr_...)"
|
placeholder="请输入您的 API Key (cr_...)"
|
||||||
class="wide-card-input w-full"
|
class="wide-card-input w-full"
|
||||||
@keyup.enter="queryStats"
|
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
|
@keyup.enter="queryStats"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,12 +36,18 @@
|
|||||||
|
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
@click="queryStats"
|
|
||||||
:disabled="loading || !apiKey.trim()"
|
:disabled="loading || !apiKey.trim()"
|
||||||
class="btn btn-primary btn-query w-full h-full flex items-center justify-center gap-2"
|
class="btn btn-primary btn-query w-full h-full flex items-center justify-center gap-2"
|
||||||
|
@click="queryStats"
|
||||||
>
|
>
|
||||||
<i v-if="loading" class="fas fa-spinner loading-spinner"></i>
|
<i
|
||||||
<i v-else class="fas fa-search"></i>
|
v-if="loading"
|
||||||
|
class="fas fa-spinner loading-spinner"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-search"
|
||||||
|
/>
|
||||||
{{ loading ? '查询中...' : '查询统计' }}
|
{{ loading ? '查询中...' : '查询统计' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +55,7 @@
|
|||||||
|
|
||||||
<!-- 安全提示 -->
|
<!-- 安全提示 -->
|
||||||
<div class="security-notice mt-4">
|
<div class="security-notice mt-4">
|
||||||
<i class="fas fa-shield-alt mr-2"></i>
|
<i class="fas fa-shield-alt mr-2" />
|
||||||
您的 API Key 仅用于查询自己的统计数据,不会被存储或用于其他用途
|
您的 API Key 仅用于查询自己的统计数据,不会被存储或用于其他用途
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- 限制配置 -->
|
<!-- 限制配置 -->
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
|
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
|
||||||
<i class="fas fa-shield-alt mr-3 text-red-500"></i>
|
<i class="fas fa-shield-alt mr-3 text-red-500" />
|
||||||
限制配置
|
限制配置
|
||||||
</h3>
|
</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
@@ -30,13 +30,18 @@
|
|||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-gray-600">模型限制</span>
|
<span class="text-gray-600">模型限制</span>
|
||||||
<span class="font-medium text-gray-900">
|
<span class="font-medium text-gray-900">
|
||||||
<span v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
|
<span
|
||||||
class="text-orange-600">
|
v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
class="text-orange-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1" />
|
||||||
限制 {{ statsData.restrictions.restrictedModels.length }} 个模型
|
限制 {{ statsData.restrictions.restrictedModels.length }} 个模型
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-green-600">
|
<span
|
||||||
<i class="fas fa-check-circle mr-1"></i>
|
v-else
|
||||||
|
class="text-green-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check-circle mr-1" />
|
||||||
允许所有模型
|
允许所有模型
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -44,13 +49,18 @@
|
|||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-gray-600">客户端限制</span>
|
<span class="text-gray-600">客户端限制</span>
|
||||||
<span class="font-medium text-gray-900">
|
<span class="font-medium text-gray-900">
|
||||||
<span v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
|
<span
|
||||||
class="text-orange-600">
|
v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
class="text-orange-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1" />
|
||||||
限制 {{ statsData.restrictions.allowedClients.length }} 个客户端
|
限制 {{ statsData.restrictions.allowedClients.length }} 个客户端
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-green-600">
|
<span
|
||||||
<i class="fas fa-check-circle mr-1"></i>
|
v-else
|
||||||
|
class="text-green-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check-circle mr-1" />
|
||||||
允许所有客户端
|
允许所有客户端
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -59,53 +69,63 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 详细限制信息 -->
|
<!-- 详细限制信息 -->
|
||||||
<div v-if="(statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0) ||
|
<div
|
||||||
|
v-if="(statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0) ||
|
||||||
(statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0)"
|
(statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0)"
|
||||||
class="card p-6 mt-6">
|
class="card p-6 mt-6"
|
||||||
|
>
|
||||||
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
|
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
|
||||||
<i class="fas fa-list-alt mr-3 text-amber-500"></i>
|
<i class="fas fa-list-alt mr-3 text-amber-500" />
|
||||||
详细限制信息
|
详细限制信息
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<!-- 模型限制详情 -->
|
<!-- 模型限制详情 -->
|
||||||
<div v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
|
<div
|
||||||
class="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
|
||||||
|
class="bg-amber-50 border border-amber-200 rounded-lg p-4"
|
||||||
|
>
|
||||||
<h4 class="font-bold text-amber-800 mb-3 flex items-center">
|
<h4 class="font-bold text-amber-800 mb-3 flex items-center">
|
||||||
<i class="fas fa-robot mr-2"></i>
|
<i class="fas fa-robot mr-2" />
|
||||||
受限模型列表
|
受限模型列表
|
||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div v-for="model in statsData.restrictions.restrictedModels"
|
<div
|
||||||
|
v-for="model in statsData.restrictions.restrictedModels"
|
||||||
:key="model"
|
:key="model"
|
||||||
class="bg-white rounded px-3 py-2 text-sm border border-amber-200">
|
class="bg-white rounded px-3 py-2 text-sm border border-amber-200"
|
||||||
<i class="fas fa-ban mr-2 text-red-500"></i>
|
>
|
||||||
|
<i class="fas fa-ban mr-2 text-red-500" />
|
||||||
<span class="text-gray-800">{{ model }}</span>
|
<span class="text-gray-800">{{ model }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-amber-700 mt-3">
|
<p class="text-xs text-amber-700 mt-3">
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
<i class="fas fa-info-circle mr-1" />
|
||||||
此 API Key 不能访问以上列出的模型
|
此 API Key 不能访问以上列出的模型
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 客户端限制详情 -->
|
<!-- 客户端限制详情 -->
|
||||||
<div v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
|
<div
|
||||||
class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
|
||||||
|
class="bg-blue-50 border border-blue-200 rounded-lg p-4"
|
||||||
|
>
|
||||||
<h4 class="font-bold text-blue-800 mb-3 flex items-center">
|
<h4 class="font-bold text-blue-800 mb-3 flex items-center">
|
||||||
<i class="fas fa-desktop mr-2"></i>
|
<i class="fas fa-desktop mr-2" />
|
||||||
允许的客户端
|
允许的客户端
|
||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div v-for="client in statsData.restrictions.allowedClients"
|
<div
|
||||||
|
v-for="client in statsData.restrictions.allowedClients"
|
||||||
:key="client"
|
:key="client"
|
||||||
class="bg-white rounded px-3 py-2 text-sm border border-blue-200">
|
class="bg-white rounded px-3 py-2 text-sm border border-blue-200"
|
||||||
<i class="fas fa-check mr-2 text-green-500"></i>
|
>
|
||||||
|
<i class="fas fa-check mr-2 text-green-500" />
|
||||||
<span class="text-gray-800">{{ client }}</span>
|
<span class="text-gray-800">{{ client }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-blue-700 mt-3">
|
<p class="text-xs text-blue-700 mt-3">
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
<i class="fas fa-info-circle mr-1" />
|
||||||
此 API Key 只能被以上列出的客户端使用
|
此 API Key 只能被以上列出的客户端使用
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,19 +2,27 @@
|
|||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h3 class="text-xl font-bold flex items-center text-gray-900">
|
<h3 class="text-xl font-bold flex items-center text-gray-900">
|
||||||
<i class="fas fa-robot mr-3 text-indigo-500"></i>
|
<i class="fas fa-robot mr-3 text-indigo-500" />
|
||||||
模型使用统计 <span class="text-sm font-normal text-gray-600 ml-2">({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span>
|
模型使用统计 <span class="text-sm font-normal text-gray-600 ml-2">({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模型统计加载状态 -->
|
<!-- 模型统计加载状态 -->
|
||||||
<div v-if="modelStatsLoading" class="text-center py-8">
|
<div
|
||||||
<i class="fas fa-spinner loading-spinner text-2xl mb-2 text-gray-600"></i>
|
v-if="modelStatsLoading"
|
||||||
<p class="text-gray-600">加载模型统计数据中...</p>
|
class="text-center py-8"
|
||||||
|
>
|
||||||
|
<i class="fas fa-spinner loading-spinner text-2xl mb-2 text-gray-600" />
|
||||||
|
<p class="text-gray-600">
|
||||||
|
加载模型统计数据中...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模型统计数据 -->
|
<!-- 模型统计数据 -->
|
||||||
<div v-else-if="modelStats.length > 0" class="space-y-4">
|
<div
|
||||||
|
v-else-if="modelStats.length > 0"
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(model, index) in modelStats"
|
v-for="(model, index) in modelStats"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -22,39 +30,66 @@
|
|||||||
>
|
>
|
||||||
<div class="flex justify-between items-start mb-3">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-bold text-lg text-gray-900">{{ model.model }}</h4>
|
<h4 class="font-bold text-lg text-gray-900">
|
||||||
<p class="text-gray-600 text-sm">{{ model.requests }} 次请求</p>
|
{{ model.model }}
|
||||||
|
</h4>
|
||||||
|
<p class="text-gray-600 text-sm">
|
||||||
|
{{ model.requests }} 次请求
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<div class="text-lg font-bold text-green-600">{{ model.formatted?.total || '$0.000000' }}</div>
|
<div class="text-lg font-bold text-green-600">
|
||||||
<div class="text-sm text-gray-600">总费用</div>
|
{{ model.formatted?.total || '$0.000000' }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
总费用
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
||||||
<div class="bg-gray-50 rounded p-2">
|
<div class="bg-gray-50 rounded p-2">
|
||||||
<div class="text-gray-600">输入 Token</div>
|
<div class="text-gray-600">
|
||||||
<div class="font-medium text-gray-900">{{ formatNumber(model.inputTokens) }}</div>
|
输入 Token
|
||||||
|
</div>
|
||||||
|
<div class="font-medium text-gray-900">
|
||||||
|
{{ formatNumber(model.inputTokens) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 rounded p-2">
|
<div class="bg-gray-50 rounded p-2">
|
||||||
<div class="text-gray-600">输出 Token</div>
|
<div class="text-gray-600">
|
||||||
<div class="font-medium text-gray-900">{{ formatNumber(model.outputTokens) }}</div>
|
输出 Token
|
||||||
|
</div>
|
||||||
|
<div class="font-medium text-gray-900">
|
||||||
|
{{ formatNumber(model.outputTokens) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 rounded p-2">
|
<div class="bg-gray-50 rounded p-2">
|
||||||
<div class="text-gray-600">缓存创建</div>
|
<div class="text-gray-600">
|
||||||
<div class="font-medium text-gray-900">{{ formatNumber(model.cacheCreateTokens) }}</div>
|
缓存创建
|
||||||
|
</div>
|
||||||
|
<div class="font-medium text-gray-900">
|
||||||
|
{{ formatNumber(model.cacheCreateTokens) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 rounded p-2">
|
<div class="bg-gray-50 rounded p-2">
|
||||||
<div class="text-gray-600">缓存读取</div>
|
<div class="text-gray-600">
|
||||||
<div class="font-medium text-gray-900">{{ formatNumber(model.cacheReadTokens) }}</div>
|
缓存读取
|
||||||
|
</div>
|
||||||
|
<div class="font-medium text-gray-900">
|
||||||
|
{{ formatNumber(model.cacheReadTokens) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 无模型数据 -->
|
<!-- 无模型数据 -->
|
||||||
<div v-else class="text-center py-8 text-gray-500">
|
<div
|
||||||
<i class="fas fa-chart-pie text-3xl mb-3"></i>
|
v-else
|
||||||
|
class="text-center py-8 text-gray-500"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chart-pie text-3xl mb-3" />
|
||||||
<p>暂无{{ statsPeriod === 'daily' ? '今日' : '本月' }}模型使用数据</p>
|
<p>暂无{{ statsPeriod === 'daily' ? '今日' : '本月' }}模型使用数据</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- API Key 基本信息 -->
|
<!-- API Key 基本信息 -->
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
|
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
|
||||||
<i class="fas fa-info-circle mr-3 text-blue-500"></i>
|
<i class="fas fa-info-circle mr-3 text-blue-500" />
|
||||||
API Key 信息
|
API Key 信息
|
||||||
</h3>
|
</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
@@ -13,8 +13,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-gray-600">状态</span>
|
<span class="text-gray-600">状态</span>
|
||||||
<span :class="statsData.isActive ? 'text-green-600' : 'text-red-600'" class="font-medium">
|
<span
|
||||||
<i :class="statsData.isActive ? 'fas fa-check-circle' : 'fas fa-times-circle'" class="mr-1"></i>
|
:class="statsData.isActive ? 'text-green-600' : 'text-red-600'"
|
||||||
|
class="font-medium"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="statsData.isActive ? 'fas fa-check-circle' : 'fas fa-times-circle'"
|
||||||
|
class="mr-1"
|
||||||
|
/>
|
||||||
{{ statsData.isActive ? '活跃' : '已停用' }}
|
{{ statsData.isActive ? '活跃' : '已停用' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,20 +35,32 @@
|
|||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-gray-600">过期时间</span>
|
<span class="text-gray-600">过期时间</span>
|
||||||
<div v-if="statsData.expiresAt">
|
<div v-if="statsData.expiresAt">
|
||||||
<div v-if="isApiKeyExpired(statsData.expiresAt)" class="text-red-600 font-medium">
|
<div
|
||||||
<i class="fas fa-exclamation-circle mr-1"></i>
|
v-if="isApiKeyExpired(statsData.expiresAt)"
|
||||||
|
class="text-red-600 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-circle mr-1" />
|
||||||
已过期
|
已过期
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="isApiKeyExpiringSoon(statsData.expiresAt)" class="text-orange-600 font-medium">
|
<div
|
||||||
<i class="fas fa-clock mr-1"></i>
|
v-else-if="isApiKeyExpiringSoon(statsData.expiresAt)"
|
||||||
|
class="text-orange-600 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-clock mr-1" />
|
||||||
{{ formatExpireDate(statsData.expiresAt) }}
|
{{ formatExpireDate(statsData.expiresAt) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-900 font-medium">
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-gray-900 font-medium"
|
||||||
|
>
|
||||||
{{ formatExpireDate(statsData.expiresAt) }}
|
{{ formatExpireDate(statsData.expiresAt) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-400 font-medium">
|
<div
|
||||||
<i class="fas fa-infinity mr-1"></i>
|
v-else
|
||||||
|
class="text-gray-400 font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-infinity mr-1" />
|
||||||
永不过期
|
永不过期
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,25 +70,41 @@
|
|||||||
<!-- 使用统计概览 -->
|
<!-- 使用统计概览 -->
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
|
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
|
||||||
<i class="fas fa-chart-bar mr-3 text-green-500"></i>
|
<i class="fas fa-chart-bar mr-3 text-green-500" />
|
||||||
使用统计概览 <span class="text-sm font-normal text-gray-600 ml-2">({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span>
|
使用统计概览 <span class="text-sm font-normal text-gray-600 ml-2">({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="stat-card text-center">
|
<div class="stat-card text-center">
|
||||||
<div class="text-3xl font-bold text-green-600">{{ formatNumber(currentPeriodData.requests) }}</div>
|
<div class="text-3xl font-bold text-green-600">
|
||||||
<div class="text-sm text-gray-600">{{ statsPeriod === 'daily' ? '今日' : '本月' }}请求数</div>
|
{{ formatNumber(currentPeriodData.requests) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
{{ statsPeriod === 'daily' ? '今日' : '本月' }}请求数
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card text-center">
|
<div class="stat-card text-center">
|
||||||
<div class="text-3xl font-bold text-blue-600">{{ formatNumber(currentPeriodData.allTokens) }}</div>
|
<div class="text-3xl font-bold text-blue-600">
|
||||||
<div class="text-sm text-gray-600">{{ statsPeriod === 'daily' ? '今日' : '本月' }}Token数</div>
|
{{ formatNumber(currentPeriodData.allTokens) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
{{ statsPeriod === 'daily' ? '今日' : '本月' }}Token数
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card text-center">
|
<div class="stat-card text-center">
|
||||||
<div class="text-3xl font-bold text-purple-600">{{ currentPeriodData.formattedCost || '$0.000000' }}</div>
|
<div class="text-3xl font-bold text-purple-600">
|
||||||
<div class="text-sm text-gray-600">{{ statsPeriod === 'daily' ? '今日' : '本月' }}费用</div>
|
{{ currentPeriodData.formattedCost || '$0.000000' }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
{{ statsPeriod === 'daily' ? '今日' : '本月' }}费用
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card text-center">
|
<div class="stat-card text-center">
|
||||||
<div class="text-3xl font-bold text-yellow-600">{{ formatNumber(currentPeriodData.inputTokens) }}</div>
|
<div class="text-3xl font-bold text-yellow-600">
|
||||||
<div class="text-sm text-gray-600">{{ statsPeriod === 'daily' ? '今日' : '本月' }}输入Token</div>
|
{{ formatNumber(currentPeriodData.inputTokens) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
{{ statsPeriod === 'daily' ? '今日' : '本月' }}输入Token
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
|
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-900">
|
||||||
<i class="fas fa-coins mr-3 text-yellow-500"></i>
|
<i class="fas fa-coins mr-3 text-yellow-500" />
|
||||||
Token 使用分布 <span class="text-sm font-normal text-gray-600 ml-2">({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span>
|
Token 使用分布 <span class="text-sm font-normal text-gray-600 ml-2">({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-gray-600 flex items-center">
|
<span class="text-gray-600 flex items-center">
|
||||||
<i class="fas fa-arrow-right mr-2 text-green-500"></i>
|
<i class="fas fa-arrow-right mr-2 text-green-500" />
|
||||||
输入 Token
|
输入 Token
|
||||||
</span>
|
</span>
|
||||||
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.inputTokens) }}</span>
|
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.inputTokens) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-gray-600 flex items-center">
|
<span class="text-gray-600 flex items-center">
|
||||||
<i class="fas fa-arrow-left mr-2 text-blue-500"></i>
|
<i class="fas fa-arrow-left mr-2 text-blue-500" />
|
||||||
输出 Token
|
输出 Token
|
||||||
</span>
|
</span>
|
||||||
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.outputTokens) }}</span>
|
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.outputTokens) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-gray-600 flex items-center">
|
<span class="text-gray-600 flex items-center">
|
||||||
<i class="fas fa-save mr-2 text-purple-500"></i>
|
<i class="fas fa-save mr-2 text-purple-500" />
|
||||||
缓存创建 Token
|
缓存创建 Token
|
||||||
</span>
|
</span>
|
||||||
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.cacheCreateTokens) }}</span>
|
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.cacheCreateTokens) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-gray-600 flex items-center">
|
<span class="text-gray-600 flex items-center">
|
||||||
<i class="fas fa-download mr-2 text-orange-500"></i>
|
<i class="fas fa-download mr-2 text-orange-500" />
|
||||||
缓存读取 Token
|
缓存读取 Token
|
||||||
</span>
|
</span>
|
||||||
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.cacheReadTokens) }}</span>
|
<span class="font-medium text-gray-900">{{ formatNumber(currentPeriodData.cacheReadTokens) }}</span>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="modal" appear>
|
<Transition
|
||||||
|
name="modal"
|
||||||
|
appear
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isVisible"
|
v-if="isVisible"
|
||||||
class="fixed inset-0 modal z-[100] flex items-center justify-center p-4"
|
class="fixed inset-0 modal z-[100] flex items-center justify-center p-4"
|
||||||
@@ -9,29 +12,36 @@
|
|||||||
<div class="modal-content w-full max-w-md p-6 mx-auto">
|
<div class="modal-content w-full max-w-md p-6 mx-auto">
|
||||||
<div class="flex items-start gap-4 mb-6">
|
<div class="flex items-start gap-4 mb-6">
|
||||||
<div class="flex-shrink-0 w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl flex items-center justify-center">
|
<div class="flex-shrink-0 w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl flex items-center justify-center">
|
||||||
<i class="fas fa-exclamation-triangle text-white text-lg"></i>
|
<i class="fas fa-exclamation-triangle text-white text-lg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">{{ title }}</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
||||||
<div class="text-gray-600 leading-relaxed whitespace-pre-line">{{ message }}</div>
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
<div class="text-gray-600 leading-relaxed whitespace-pre-line">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end gap-3">
|
<div class="flex items-center justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
@click="handleCancel"
|
|
||||||
class="btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3"
|
class="btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3"
|
||||||
:disabled="isProcessing"
|
:disabled="isProcessing"
|
||||||
|
@click="handleCancel"
|
||||||
>
|
>
|
||||||
{{ cancelText }}
|
{{ cancelText }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="handleConfirm"
|
|
||||||
class="btn btn-warning px-6 py-3"
|
class="btn btn-warning px-6 py-3"
|
||||||
:class="{ 'opacity-50 cursor-not-allowed': isProcessing }"
|
:class="{ 'opacity-50 cursor-not-allowed': isProcessing }"
|
||||||
:disabled="isProcessing"
|
:disabled="isProcessing"
|
||||||
|
@click="handleConfirm"
|
||||||
>
|
>
|
||||||
<div v-if="isProcessing" class="loading-spinner mr-2"></div>
|
<div
|
||||||
|
v-if="isProcessing"
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
{{ confirmText }}
|
{{ confirmText }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div v-if="show" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="fixed inset-0 modal z-50 flex items-center justify-center p-4"
|
||||||
|
>
|
||||||
<div class="modal-content w-full max-w-md p-6 mx-auto">
|
<div class="modal-content w-full max-w-md p-6 mx-auto">
|
||||||
<div class="flex items-start gap-4 mb-6">
|
<div class="flex items-start gap-4 mb-6">
|
||||||
<div class="w-12 h-12 bg-gradient-to-br from-yellow-400 to-yellow-500 rounded-full flex items-center justify-center flex-shrink-0">
|
<div class="w-12 h-12 bg-gradient-to-br from-yellow-400 to-yellow-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
<i class="fas fa-exclamation text-white text-xl"></i>
|
<i class="fas fa-exclamation text-white text-xl" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="text-lg font-bold text-gray-900 mb-2">{{ title }}</h3>
|
<h3 class="text-lg font-bold text-gray-900 mb-2">
|
||||||
<p class="text-gray-600 text-sm leading-relaxed whitespace-pre-line">{{ message }}</p>
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600 text-sm leading-relaxed whitespace-pre-line">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button
|
<button
|
||||||
@click="$emit('cancel')"
|
|
||||||
class="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors"
|
class="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors"
|
||||||
|
@click="$emit('cancel')"
|
||||||
>
|
>
|
||||||
{{ cancelText }}
|
{{ cancelText }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="$emit('confirm')"
|
|
||||||
class="flex-1 px-4 py-2.5 bg-gradient-to-r from-yellow-500 to-orange-500 text-white rounded-xl font-medium hover:from-yellow-600 hover:to-orange-600 transition-colors shadow-sm"
|
class="flex-1 px-4 py-2.5 bg-gradient-to-r from-yellow-500 to-orange-500 text-white rounded-xl font-medium hover:from-yellow-600 hover:to-orange-600 transition-colors shadow-sm"
|
||||||
|
@click="$emit('confirm')"
|
||||||
>
|
>
|
||||||
{{ confirmText }}
|
{{ confirmText }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,27 +3,45 @@
|
|||||||
<!-- Logo区域 -->
|
<!-- Logo区域 -->
|
||||||
<div class="w-12 h-12 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-xl flex items-center justify-center backdrop-blur-sm flex-shrink-0 overflow-hidden">
|
<div class="w-12 h-12 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-xl flex items-center justify-center backdrop-blur-sm flex-shrink-0 overflow-hidden">
|
||||||
<template v-if="!loading">
|
<template v-if="!loading">
|
||||||
<img v-if="logoSrc"
|
<img
|
||||||
|
v-if="logoSrc"
|
||||||
:src="logoSrc"
|
:src="logoSrc"
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
class="w-8 h-8 object-contain"
|
class="w-8 h-8 object-contain"
|
||||||
@error="handleLogoError">
|
@error="handleLogoError"
|
||||||
<i v-else class="fas fa-cloud text-xl text-gray-700"></i>
|
>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-cloud text-xl text-gray-700"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="w-8 h-8 bg-gray-300/50 rounded animate-pulse"></div>
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-8 h-8 bg-gray-300/50 rounded animate-pulse"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标题区域 -->
|
<!-- 标题区域 -->
|
||||||
<div class="flex flex-col justify-center min-h-[48px]">
|
<div class="flex flex-col justify-center min-h-[48px]">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<template v-if="!loading && title">
|
<template v-if="!loading && title">
|
||||||
<h1 :class="['text-2xl font-bold header-title leading-tight', titleClass]">{{ title }}</h1>
|
<h1 :class="['text-2xl font-bold header-title leading-tight', titleClass]">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="loading" class="h-8 w-64 bg-gray-300/50 rounded animate-pulse"></div>
|
<div
|
||||||
|
v-else-if="loading"
|
||||||
|
class="h-8 w-64 bg-gray-300/50 rounded animate-pulse"
|
||||||
|
/>
|
||||||
<!-- 插槽用于版本信息等额外内容 -->
|
<!-- 插槽用于版本信息等额外内容 -->
|
||||||
<slot name="after-title"></slot>
|
<slot name="after-title" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="subtitle" class="text-gray-600 text-sm leading-tight mt-0.5">{{ subtitle }}</p>
|
<p
|
||||||
|
v-if="subtitle"
|
||||||
|
class="text-gray-600 text-sm leading-tight mt-0.5"
|
||||||
|
>
|
||||||
|
{{ subtitle }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,12 +2,21 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium text-gray-600 mb-1">{{ title }}</p>
|
<p class="text-sm font-medium text-gray-600 mb-1">
|
||||||
<p class="text-3xl font-bold text-gray-800">{{ value }}</p>
|
{{ title }}
|
||||||
<p v-if="subtitle" class="text-sm text-gray-500 mt-2">{{ subtitle }}</p>
|
</p>
|
||||||
|
<p class="text-3xl font-bold text-gray-800">
|
||||||
|
{{ value }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-if="subtitle"
|
||||||
|
class="text-sm text-gray-500 mt-2"
|
||||||
|
>
|
||||||
|
{{ subtitle }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div :class="['stat-icon', iconBgClass]">
|
<div :class="['stat-icon', iconBgClass]">
|
||||||
<i :class="icon"></i>
|
<i :class="icon" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,24 +13,31 @@
|
|||||||
>
|
>
|
||||||
<div class="toast-content">
|
<div class="toast-content">
|
||||||
<div class="toast-icon">
|
<div class="toast-icon">
|
||||||
<i :class="getIconClass(toast.type)"></i>
|
<i :class="getIconClass(toast.type)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="toast-body">
|
<div class="toast-body">
|
||||||
<div v-if="toast.title" class="toast-title">{{ toast.title }}</div>
|
<div
|
||||||
<div class="toast-message">{{ toast.message }}</div>
|
v-if="toast.title"
|
||||||
|
class="toast-title"
|
||||||
|
>
|
||||||
|
{{ toast.title }}
|
||||||
|
</div>
|
||||||
|
<div class="toast-message">
|
||||||
|
{{ toast.message }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="toast-close"
|
class="toast-close"
|
||||||
@click.stop="removeToast(toast.id)"
|
@click.stop="removeToast(toast.id)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="toast.duration > 0"
|
v-if="toast.duration > 0"
|
||||||
class="toast-progress"
|
class="toast-progress"
|
||||||
:style="{ animationDuration: `${toast.duration}ms` }"
|
:style="{ animationDuration: `${toast.duration}ms` }"
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|||||||
@@ -2,38 +2,65 @@
|
|||||||
<div class="glass-strong rounded-3xl p-6">
|
<div class="glass-strong rounded-3xl p-6">
|
||||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
|
||||||
<h2 class="text-xl font-bold text-gray-800 flex items-center">
|
<h2 class="text-xl font-bold text-gray-800 flex items-center">
|
||||||
<i class="fas fa-robot mr-2 text-purple-500"></i>
|
<i class="fas fa-robot mr-2 text-purple-500" />
|
||||||
模型使用分布
|
模型使用分布
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<el-radio-group v-model="modelPeriod" size="small" @change="handlePeriodChange">
|
<el-radio-group
|
||||||
<el-radio-button label="daily">今日</el-radio-button>
|
v-model="modelPeriod"
|
||||||
<el-radio-button label="total">累计</el-radio-button>
|
size="small"
|
||||||
|
@change="handlePeriodChange"
|
||||||
|
>
|
||||||
|
<el-radio-button label="daily">
|
||||||
|
今日
|
||||||
|
</el-radio-button>
|
||||||
|
<el-radio-button label="total">
|
||||||
|
累计
|
||||||
|
</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="dashboardStore.dashboardModelStats.length === 0" class="text-center py-12 text-gray-500">
|
<div
|
||||||
<i class="fas fa-chart-pie text-4xl mb-3 opacity-30"></i>
|
v-if="dashboardStore.dashboardModelStats.length === 0"
|
||||||
|
class="text-center py-12 text-gray-500"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chart-pie text-4xl mb-3 opacity-30" />
|
||||||
<p>暂无模型使用数据</p>
|
<p>暂无模型使用数据</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div
|
||||||
|
v-else
|
||||||
|
class="grid grid-cols-1 lg:grid-cols-2 gap-6"
|
||||||
|
>
|
||||||
<!-- 饼图 -->
|
<!-- 饼图 -->
|
||||||
<div class="relative" style="height: 300px;">
|
<div
|
||||||
<canvas ref="chartCanvas"></canvas>
|
class="relative"
|
||||||
|
style="height: 300px;"
|
||||||
|
>
|
||||||
|
<canvas ref="chartCanvas" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 数据列表 -->
|
<!-- 数据列表 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div v-for="(stat, index) in sortedStats" :key="stat.model"
|
<div
|
||||||
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
v-for="(stat, index) in sortedStats"
|
||||||
|
:key="stat.model"
|
||||||
|
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-4 h-4 rounded" :style="`background-color: ${getColor(index)}`"></div>
|
<div
|
||||||
|
class="w-4 h-4 rounded"
|
||||||
|
:style="`background-color: ${getColor(index)}`"
|
||||||
|
/>
|
||||||
<span class="font-medium text-gray-700">{{ stat.model }}</span>
|
<span class="font-medium text-gray-700">{{ stat.model }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="font-semibold text-gray-800">{{ formatNumber(stat.requests) }} 请求</p>
|
<p class="font-semibold text-gray-800">
|
||||||
<p class="text-sm text-gray-500">{{ formatNumber(stat.totalTokens) }} tokens</p>
|
{{ formatNumber(stat.requests) }} 请求
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{{ formatNumber(stat.totalTokens) }} tokens
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,24 +2,45 @@
|
|||||||
<div class="glass-strong rounded-3xl p-6 mb-8">
|
<div class="glass-strong rounded-3xl p-6 mb-8">
|
||||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
|
||||||
<h2 class="text-xl font-bold text-gray-800 flex items-center">
|
<h2 class="text-xl font-bold text-gray-800 flex items-center">
|
||||||
<i class="fas fa-chart-area mr-2 text-blue-500"></i>
|
<i class="fas fa-chart-area mr-2 text-blue-500" />
|
||||||
使用趋势
|
使用趋势
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<el-radio-group v-model="granularity" size="small" @change="handleGranularityChange">
|
<el-radio-group
|
||||||
<el-radio-button label="day">按天</el-radio-button>
|
v-model="granularity"
|
||||||
<el-radio-button label="hour">按小时</el-radio-button>
|
size="small"
|
||||||
|
@change="handleGranularityChange"
|
||||||
|
>
|
||||||
|
<el-radio-button label="day">
|
||||||
|
按天
|
||||||
|
</el-radio-button>
|
||||||
|
<el-radio-button label="hour">
|
||||||
|
按小时
|
||||||
|
</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
|
|
||||||
<el-select v-model="trendPeriod" size="small" style="width: 120px" @change="handlePeriodChange">
|
<el-select
|
||||||
<el-option :label="`最近${period.days}天`" :value="period.days" v-for="period in periodOptions" :key="period.days" />
|
v-model="trendPeriod"
|
||||||
|
size="small"
|
||||||
|
style="width: 120px"
|
||||||
|
@change="handlePeriodChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="period in periodOptions"
|
||||||
|
:key="period.days"
|
||||||
|
:label="`最近${period.days}天`"
|
||||||
|
:value="period.days"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative" style="height: 300px;">
|
<div
|
||||||
<canvas ref="chartCanvas"></canvas>
|
class="relative"
|
||||||
|
style="height: 300px;"
|
||||||
|
>
|
||||||
|
<canvas ref="chartCanvas" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 顶部导航 -->
|
<!-- 顶部导航 -->
|
||||||
<div class="glass-strong rounded-3xl p-6 mb-8 shadow-xl" style="z-index: 10; position: relative;">
|
<div
|
||||||
|
class="glass-strong rounded-3xl p-6 mb-8 shadow-xl"
|
||||||
|
style="z-index: 10; position: relative;"
|
||||||
|
>
|
||||||
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
|
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<LogoTitle
|
<LogoTitle
|
||||||
@@ -22,7 +25,7 @@
|
|||||||
class="inline-flex items-center gap-1 px-2 py-0.5 bg-green-500 border border-green-600 rounded-full text-xs text-white hover:bg-green-600 transition-colors animate-pulse"
|
class="inline-flex items-center gap-1 px-2 py-0.5 bg-green-500 border border-green-600 rounded-full text-xs text-white hover:bg-green-600 transition-colors animate-pulse"
|
||||||
title="有新版本可用"
|
title="有新版本可用"
|
||||||
>
|
>
|
||||||
<i class="fas fa-arrow-up text-[10px]"></i>
|
<i class="fas fa-arrow-up text-[10px]" />
|
||||||
<span>新版本</span>
|
<span>新版本</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,12 +35,15 @@
|
|||||||
<!-- 用户菜单 -->
|
<!-- 用户菜单 -->
|
||||||
<div class="relative user-menu-container">
|
<div class="relative user-menu-container">
|
||||||
<button
|
<button
|
||||||
@click="userMenuOpen = !userMenuOpen"
|
|
||||||
class="btn btn-primary px-4 py-3 flex items-center gap-2 relative"
|
class="btn btn-primary px-4 py-3 flex items-center gap-2 relative"
|
||||||
|
@click="userMenuOpen = !userMenuOpen"
|
||||||
>
|
>
|
||||||
<i class="fas fa-user-circle"></i>
|
<i class="fas fa-user-circle" />
|
||||||
<span>{{ currentUser.username || 'Admin' }}</span>
|
<span>{{ currentUser.username || 'Admin' }}</span>
|
||||||
<i class="fas fa-chevron-down text-xs transition-transform duration-200" :class="{ 'rotate-180': userMenuOpen }"></i>
|
<i
|
||||||
|
class="fas fa-chevron-down text-xs transition-transform duration-200"
|
||||||
|
:class="{ 'rotate-180': userMenuOpen }"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 悬浮菜单 -->
|
<!-- 悬浮菜单 -->
|
||||||
@@ -53,10 +59,13 @@
|
|||||||
<span class="text-gray-500">当前版本</span>
|
<span class="text-gray-500">当前版本</span>
|
||||||
<span class="font-mono text-gray-700">v{{ versionInfo.current || '...' }}</span>
|
<span class="font-mono text-gray-700">v{{ versionInfo.current || '...' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="versionInfo.hasUpdate" class="mt-2">
|
<div
|
||||||
|
v-if="versionInfo.hasUpdate"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
<div class="flex items-center justify-between text-sm mb-2">
|
<div class="flex items-center justify-between text-sm mb-2">
|
||||||
<span class="text-green-600 font-medium">
|
<span class="text-green-600 font-medium">
|
||||||
<i class="fas fa-arrow-up mr-1"></i>有新版本
|
<i class="fas fa-arrow-up mr-1" />有新版本
|
||||||
</span>
|
</span>
|
||||||
<span class="font-mono text-green-600">v{{ versionInfo.latest }}</span>
|
<span class="font-mono text-green-600">v{{ versionInfo.latest }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,47 +74,60 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
class="block w-full text-center px-3 py-1.5 bg-green-500 text-white text-sm rounded-lg hover:bg-green-600 transition-colors"
|
class="block w-full text-center px-3 py-1.5 bg-green-500 text-white text-sm rounded-lg hover:bg-green-600 transition-colors"
|
||||||
>
|
>
|
||||||
<i class="fas fa-external-link-alt mr-1"></i>查看更新
|
<i class="fas fa-external-link-alt mr-1" />查看更新
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="versionInfo.checkingUpdate" class="mt-2 text-center text-xs text-gray-500">
|
<div
|
||||||
<i class="fas fa-spinner fa-spin mr-1"></i>检查更新中...
|
v-else-if="versionInfo.checkingUpdate"
|
||||||
|
class="mt-2 text-center text-xs text-gray-500"
|
||||||
|
>
|
||||||
|
<i class="fas fa-spinner fa-spin mr-1" />检查更新中...
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="mt-2 text-center">
|
<div
|
||||||
|
v-else
|
||||||
|
class="mt-2 text-center"
|
||||||
|
>
|
||||||
<!-- 已是最新版提醒 -->
|
<!-- 已是最新版提醒 -->
|
||||||
<transition name="fade" mode="out-in">
|
<transition
|
||||||
<div v-if="versionInfo.noUpdateMessage" key="message" class="px-3 py-1.5 bg-green-100 border border-green-200 rounded-lg inline-block">
|
name="fade"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="versionInfo.noUpdateMessage"
|
||||||
|
key="message"
|
||||||
|
class="px-3 py-1.5 bg-green-100 border border-green-200 rounded-lg inline-block"
|
||||||
|
>
|
||||||
<p class="text-xs text-green-700 font-medium">
|
<p class="text-xs text-green-700 font-medium">
|
||||||
<i class="fas fa-check-circle mr-1"></i>当前已是最新版本
|
<i class="fas fa-check-circle mr-1" />当前已是最新版本
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
key="button"
|
key="button"
|
||||||
@click="checkForUpdates()"
|
|
||||||
class="text-xs text-blue-500 hover:text-blue-700 transition-colors"
|
class="text-xs text-blue-500 hover:text-blue-700 transition-colors"
|
||||||
|
@click="checkForUpdates()"
|
||||||
>
|
>
|
||||||
<i class="fas fa-sync-alt mr-1"></i>检查更新
|
<i class="fas fa-sync-alt mr-1" />检查更新
|
||||||
</button>
|
</button>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="openChangePasswordModal"
|
|
||||||
class="w-full px-4 py-3 text-left text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-3"
|
class="w-full px-4 py-3 text-left text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-3"
|
||||||
|
@click="openChangePasswordModal"
|
||||||
>
|
>
|
||||||
<i class="fas fa-key text-blue-500"></i>
|
<i class="fas fa-key text-blue-500" />
|
||||||
<span>修改账户信息</span>
|
<span>修改账户信息</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<hr class="my-2 border-gray-200">
|
<hr class="my-2 border-gray-200">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="logout"
|
|
||||||
class="w-full px-4 py-3 text-left text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-3"
|
class="w-full px-4 py-3 text-left text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-3"
|
||||||
|
@click="logout"
|
||||||
>
|
>
|
||||||
<i class="fas fa-sign-out-alt text-red-500"></i>
|
<i class="fas fa-sign-out-alt text-red-500" />
|
||||||
<span>退出登录</span>
|
<span>退出登录</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,24 +136,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 修改账户信息模态框 -->
|
<!-- 修改账户信息模态框 -->
|
||||||
<div v-if="showChangePasswordModal" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
<div
|
||||||
|
v-if="showChangePasswordModal"
|
||||||
|
class="fixed inset-0 modal z-50 flex items-center justify-center p-4"
|
||||||
|
>
|
||||||
<div class="modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col">
|
<div class="modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
||||||
<i class="fas fa-key text-white"></i>
|
<i class="fas fa-key text-white" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-bold text-gray-900">修改账户信息</h3>
|
<h3 class="text-xl font-bold text-gray-900">
|
||||||
|
修改账户信息
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="closeChangePasswordModal"
|
|
||||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
@click="closeChangePasswordModal"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times text-xl"></i>
|
<i class="fas fa-times text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="changePassword" class="space-y-6 modal-scroll-content custom-scrollbar flex-1">
|
<form
|
||||||
|
class="space-y-6 modal-scroll-content custom-scrollbar flex-1"
|
||||||
|
@submit.prevent="changePassword"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">当前用户名</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-3">当前用户名</label>
|
||||||
<input
|
<input
|
||||||
@@ -140,7 +170,9 @@
|
|||||||
disabled
|
disabled
|
||||||
class="form-input w-full bg-gray-100 cursor-not-allowed"
|
class="form-input w-full bg-gray-100 cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-2">当前用户名,输入新用户名以修改</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
当前用户名,输入新用户名以修改
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -151,7 +183,9 @@
|
|||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
placeholder="输入新用户名(留空保持不变)"
|
placeholder="输入新用户名(留空保持不变)"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-2">留空表示不修改用户名</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
留空表示不修改用户名
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -174,7 +208,9 @@
|
|||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
placeholder="请输入新密码"
|
placeholder="请输入新密码"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-2">密码长度至少8位</p>
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
密码长度至少8位
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -191,8 +227,8 @@
|
|||||||
<div class="flex gap-3 pt-4">
|
<div class="flex gap-3 pt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="closeChangePasswordModal"
|
|
||||||
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
||||||
|
@click="closeChangePasswordModal"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
@@ -201,8 +237,14 @@
|
|||||||
:disabled="changePasswordLoading"
|
:disabled="changePasswordLoading"
|
||||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||||
>
|
>
|
||||||
<div v-if="changePasswordLoading" class="loading-spinner mr-2"></div>
|
<div
|
||||||
<i v-else class="fas fa-save mr-2"></i>
|
v-if="changePasswordLoading"
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-save mr-2"
|
||||||
|
/>
|
||||||
{{ changePasswordLoading ? '保存中...' : '保存修改' }}
|
{{ changePasswordLoading ? '保存中...' : '保存修改' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,14 +4,23 @@
|
|||||||
<AppHeader />
|
<AppHeader />
|
||||||
|
|
||||||
<!-- 主内容区域 -->
|
<!-- 主内容区域 -->
|
||||||
<div class="glass-strong rounded-3xl p-6 shadow-xl" style="z-index: 1; min-height: calc(100vh - 240px);">
|
<div
|
||||||
|
class="glass-strong rounded-3xl p-6 shadow-xl"
|
||||||
|
style="z-index: 1; min-height: calc(100vh - 240px);"
|
||||||
|
>
|
||||||
<!-- 标签栏 -->
|
<!-- 标签栏 -->
|
||||||
<TabBar :active-tab="activeTab" @tab-change="handleTabChange" />
|
<TabBar
|
||||||
|
:active-tab="activeTab"
|
||||||
|
@tab-change="handleTabChange"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<transition name="slide-up" mode="out-in">
|
<transition
|
||||||
|
name="slide-up"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
<keep-alive :include="['DashboardView', 'ApiKeysView']">
|
<keep-alive :include="['DashboardView', 'ApiKeysView']">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
<button
|
<button
|
||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
:key="tab.key"
|
:key="tab.key"
|
||||||
@click="$emit('tab-change', tab.key)"
|
|
||||||
:class="[
|
:class="[
|
||||||
'tab-btn flex-1 py-3 px-6 text-sm font-semibold transition-all duration-300',
|
'tab-btn flex-1 py-3 px-6 text-sm font-semibold transition-all duration-300',
|
||||||
activeTab === tab.key ? 'active' : 'text-gray-700 hover:bg-white/10 hover:text-gray-900'
|
activeTab === tab.key ? 'active' : 'text-gray-700 hover:bg-white/10 hover:text-gray-900'
|
||||||
]"
|
]"
|
||||||
|
@click="$emit('tab-change', tab.key)"
|
||||||
>
|
>
|
||||||
<i :class="tab.icon + ' mr-2'"></i>{{ tab.name }}
|
<i :class="tab.icon + ' mr-2'" />{{ tab.name }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|||||||
todayCacheReadTokens: 0,
|
todayCacheReadTokens: 0,
|
||||||
systemRPM: 0,
|
systemRPM: 0,
|
||||||
systemTPM: 0,
|
systemTPM: 0,
|
||||||
|
realtimeRPM: 0,
|
||||||
|
realtimeTPM: 0,
|
||||||
|
metricsWindow: 5,
|
||||||
|
isHistoricalMetrics: false,
|
||||||
systemStatus: '正常',
|
systemStatus: '正常',
|
||||||
uptime: 0,
|
uptime: 0,
|
||||||
systemTimezone: 8 // 默认 UTC+8
|
systemTimezone: 8 // 默认 UTC+8
|
||||||
@@ -129,6 +133,7 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|||||||
const overview = dashboardResponse.data.overview || {}
|
const overview = dashboardResponse.data.overview || {}
|
||||||
const recentActivity = dashboardResponse.data.recentActivity || {}
|
const recentActivity = dashboardResponse.data.recentActivity || {}
|
||||||
const systemAverages = dashboardResponse.data.systemAverages || {}
|
const systemAverages = dashboardResponse.data.systemAverages || {}
|
||||||
|
const realtimeMetrics = dashboardResponse.data.realtimeMetrics || {}
|
||||||
const systemHealth = dashboardResponse.data.systemHealth || {}
|
const systemHealth = dashboardResponse.data.systemHealth || {}
|
||||||
|
|
||||||
dashboardData.value = {
|
dashboardData.value = {
|
||||||
@@ -151,6 +156,10 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|||||||
todayCacheReadTokens: recentActivity.cacheReadTokensToday || 0,
|
todayCacheReadTokens: recentActivity.cacheReadTokensToday || 0,
|
||||||
systemRPM: systemAverages.rpm || 0,
|
systemRPM: systemAverages.rpm || 0,
|
||||||
systemTPM: systemAverages.tpm || 0,
|
systemTPM: systemAverages.tpm || 0,
|
||||||
|
realtimeRPM: realtimeMetrics.rpm || 0,
|
||||||
|
realtimeTPM: realtimeMetrics.tpm || 0,
|
||||||
|
metricsWindow: realtimeMetrics.windowMinutes || 5,
|
||||||
|
isHistoricalMetrics: realtimeMetrics.isHistorical || false,
|
||||||
systemStatus: systemHealth.redisConnected ? '正常' : '异常',
|
systemStatus: systemHealth.redisConnected ? '正常' : '异常',
|
||||||
uptime: systemHealth.uptime || 0,
|
uptime: systemHealth.uptime || 0,
|
||||||
systemTimezone: dashboardResponse.data.systemTimezone || 8
|
systemTimezone: dashboardResponse.data.systemTimezone || 8
|
||||||
|
|||||||
@@ -3,199 +3,355 @@
|
|||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
|
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-gray-900 mb-2">账户管理</h3>
|
<h3 class="text-xl font-bold text-gray-900 mb-2">
|
||||||
<p class="text-gray-600">管理您的 Claude 和 Gemini 账户及代理配置</p>
|
账户管理
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
管理您的 Claude 和 Gemini 账户及代理配置
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<select v-model="accountSortBy" @change="sortAccounts()" class="form-input px-3 py-2 text-sm">
|
<select
|
||||||
<option value="name">按名称排序</option>
|
v-model="accountSortBy"
|
||||||
<option value="dailyTokens">按今日Token排序</option>
|
class="form-input px-3 py-2 text-sm"
|
||||||
<option value="dailyRequests">按今日请求数排序</option>
|
@change="sortAccounts()"
|
||||||
<option value="totalTokens">按总Token排序</option>
|
>
|
||||||
<option value="lastUsed">按最后使用排序</option>
|
<option value="name">
|
||||||
|
按名称排序
|
||||||
|
</option>
|
||||||
|
<option value="dailyTokens">
|
||||||
|
按今日Token排序
|
||||||
|
</option>
|
||||||
|
<option value="dailyRequests">
|
||||||
|
按今日请求数排序
|
||||||
|
</option>
|
||||||
|
<option value="totalTokens">
|
||||||
|
按总Token排序
|
||||||
|
</option>
|
||||||
|
<option value="lastUsed">
|
||||||
|
按最后使用排序
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
@click.stop="openCreateAccountModal"
|
|
||||||
class="btn btn-success px-6 py-3 flex items-center gap-2"
|
class="btn btn-success px-6 py-3 flex items-center gap-2"
|
||||||
|
@click.stop="openCreateAccountModal"
|
||||||
>
|
>
|
||||||
<i class="fas fa-plus"></i>添加账户
|
<i class="fas fa-plus" />添加账户
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="accountsLoading" class="text-center py-12">
|
<div
|
||||||
<div class="loading-spinner mx-auto mb-4"></div>
|
v-if="accountsLoading"
|
||||||
<p class="text-gray-500">正在加载账户...</p>
|
class="text-center py-12"
|
||||||
|
>
|
||||||
|
<div class="loading-spinner mx-auto mb-4" />
|
||||||
|
<p class="text-gray-500">
|
||||||
|
正在加载账户...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="sortedAccounts.length === 0" class="text-center py-12">
|
<div
|
||||||
|
v-else-if="sortedAccounts.length === 0"
|
||||||
|
class="text-center py-12"
|
||||||
|
>
|
||||||
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
|
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
|
||||||
<i class="fas fa-user-circle text-gray-400 text-xl"></i>
|
<i class="fas fa-user-circle text-gray-400 text-xl" />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-500 text-lg">暂无账户</p>
|
<p class="text-gray-500 text-lg">
|
||||||
<p class="text-gray-400 text-sm mt-2">点击上方按钮添加您的第一个账户</p>
|
暂无账户
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-400 text-sm mt-2">
|
||||||
|
点击上方按钮添加您的第一个账户
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="table-container">
|
<div
|
||||||
|
v-else
|
||||||
|
class="table-container"
|
||||||
|
>
|
||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('name')">
|
<th
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||||
|
@click="sortAccounts('name')"
|
||||||
|
>
|
||||||
名称
|
名称
|
||||||
<i v-if="accountsSortBy === 'name'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
<i
|
||||||
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
v-if="accountsSortBy === 'name'"
|
||||||
|
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-sort ml-1 text-gray-400"
|
||||||
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('platform')">
|
<th
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||||
|
@click="sortAccounts('platform')"
|
||||||
|
>
|
||||||
平台
|
平台
|
||||||
<i v-if="accountsSortBy === 'platform'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
<i
|
||||||
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
v-if="accountsSortBy === 'platform'"
|
||||||
|
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-sort ml-1 text-gray-400"
|
||||||
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('accountType')">
|
<th
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||||
|
@click="sortAccounts('accountType')"
|
||||||
|
>
|
||||||
类型
|
类型
|
||||||
<i v-if="accountsSortBy === 'accountType'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
<i
|
||||||
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
v-if="accountsSortBy === 'accountType'"
|
||||||
|
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-sort ml-1 text-gray-400"
|
||||||
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('status')">
|
<th
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||||
|
@click="sortAccounts('status')"
|
||||||
|
>
|
||||||
状态
|
状态
|
||||||
<i v-if="accountsSortBy === 'status'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
<i
|
||||||
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
v-if="accountsSortBy === 'status'"
|
||||||
|
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-sort ml-1 text-gray-400"
|
||||||
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortAccounts('priority')">
|
<th
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||||
|
@click="sortAccounts('priority')"
|
||||||
|
>
|
||||||
优先级
|
优先级
|
||||||
<i v-if="accountsSortBy === 'priority'" :class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
<i
|
||||||
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
v-if="accountsSortBy === 'priority'"
|
||||||
|
:class="['fas', accountsSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-sort ml-1 text-gray-400"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
代理
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
今日使用
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
会话窗口
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
最后使用
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
操作
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">代理</th>
|
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">今日使用</th>
|
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">会话窗口</th>
|
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">最后使用</th>
|
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200/50">
|
<tbody class="divide-y divide-gray-200/50">
|
||||||
<tr v-for="account in sortedAccounts" :key="account.id" class="table-row">
|
<tr
|
||||||
|
v-for="account in sortedAccounts"
|
||||||
|
:key="account.id"
|
||||||
|
class="table-row"
|
||||||
|
>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-8 h-8 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center mr-3">
|
<div class="w-8 h-8 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center mr-3">
|
||||||
<i class="fas fa-user-circle text-white text-xs"></i>
|
<i class="fas fa-user-circle text-white text-xs" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-sm font-semibold text-gray-900">{{ account.name }}</div>
|
<div class="text-sm font-semibold text-gray-900">
|
||||||
<span v-if="account.accountType === 'dedicated'"
|
{{ account.name }}
|
||||||
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
</div>
|
||||||
<i class="fas fa-lock mr-1"></i>专属
|
<span
|
||||||
|
v-if="account.accountType === 'dedicated'"
|
||||||
|
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-lock mr-1" />专属
|
||||||
</span>
|
</span>
|
||||||
<span v-else
|
<span
|
||||||
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
v-else
|
||||||
<i class="fas fa-share-alt mr-1"></i>共享
|
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-share-alt mr-1" />共享
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-500">{{ account.id }}</div>
|
<div class="text-xs text-gray-500">
|
||||||
|
{{ account.id }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<span v-if="account.platform === 'gemini'"
|
<span
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800">
|
v-if="account.platform === 'gemini'"
|
||||||
<i class="fas fa-robot mr-1"></i>Gemini
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-robot mr-1" />Gemini
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="account.platform === 'claude-console'"
|
<span
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-purple-100 text-purple-800">
|
v-else-if="account.platform === 'claude-console'"
|
||||||
<i class="fas fa-terminal mr-1"></i>Claude Console
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-purple-100 text-purple-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-terminal mr-1" />Claude Console
|
||||||
</span>
|
</span>
|
||||||
<span v-else
|
<span
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-800">
|
v-else
|
||||||
<i class="fas fa-brain mr-1"></i>Claude
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-brain mr-1" />Claude
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<span v-if="account.platform === 'claude-console'"
|
<span
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800">
|
v-if="account.platform === 'claude-console'"
|
||||||
<i class="fas fa-key mr-1"></i>API Key
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-key mr-1" />API Key
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="account.scopes && account.scopes.length > 0"
|
<span
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800">
|
v-else-if="account.scopes && account.scopes.length > 0"
|
||||||
<i class="fas fa-lock mr-1"></i>OAuth
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-lock mr-1" />OAuth
|
||||||
</span>
|
</span>
|
||||||
<span v-else
|
<span
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 text-orange-800">
|
v-else
|
||||||
<i class="fas fa-key mr-1"></i>传统
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 text-orange-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-key mr-1" />传统
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<span :class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
|
<span
|
||||||
|
:class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
|
||||||
account.status === 'blocked' ? 'bg-orange-100 text-orange-800' :
|
account.status === 'blocked' ? 'bg-orange-100 text-orange-800' :
|
||||||
account.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']">
|
account.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']"
|
||||||
<div :class="['w-2 h-2 rounded-full mr-2',
|
>
|
||||||
|
<div
|
||||||
|
:class="['w-2 h-2 rounded-full mr-2',
|
||||||
account.status === 'blocked' ? 'bg-orange-500' :
|
account.status === 'blocked' ? 'bg-orange-500' :
|
||||||
account.isActive ? 'bg-green-500' : 'bg-red-500']"></div>
|
account.isActive ? 'bg-green-500' : 'bg-red-500']"
|
||||||
|
/>
|
||||||
{{ account.status === 'blocked' ? '已封锁' : account.isActive ? '正常' : '异常' }}
|
{{ account.status === 'blocked' ? '已封锁' : account.isActive ? '正常' : '异常' }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="account.rateLimitStatus && account.rateLimitStatus.isRateLimited"
|
<span
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800">
|
v-if="account.rateLimitStatus && account.rateLimitStatus.isRateLimited"
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1" />
|
||||||
限流中 ({{ account.rateLimitStatus.minutesRemaining }}分钟)
|
限流中 ({{ account.rateLimitStatus.minutesRemaining }}分钟)
|
||||||
</span>
|
</span>
|
||||||
<span v-if="account.schedulable === false"
|
<span
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-700">
|
v-if="account.schedulable === false"
|
||||||
<i class="fas fa-pause-circle mr-1"></i>
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-700"
|
||||||
|
>
|
||||||
|
<i class="fas fa-pause-circle mr-1" />
|
||||||
不可调度
|
不可调度
|
||||||
</span>
|
</span>
|
||||||
<span v-if="account.status === 'blocked' && account.errorMessage"
|
<span
|
||||||
|
v-if="account.status === 'blocked' && account.errorMessage"
|
||||||
class="text-xs text-gray-500 mt-1 max-w-xs truncate"
|
class="text-xs text-gray-500 mt-1 max-w-xs truncate"
|
||||||
:title="account.errorMessage">
|
:title="account.errorMessage"
|
||||||
|
>
|
||||||
{{ account.errorMessage }}
|
{{ account.errorMessage }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="account.accountType === 'dedicated'"
|
<span
|
||||||
class="text-xs text-gray-500">
|
v-if="account.accountType === 'dedicated'"
|
||||||
|
class="text-xs text-gray-500"
|
||||||
|
>
|
||||||
绑定: {{ account.boundApiKeysCount || 0 }} 个API Key
|
绑定: {{ account.boundApiKeysCount || 0 }} 个API Key
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div v-if="account.platform === 'claude' || account.platform === 'claude-console'" class="flex items-center gap-2">
|
<div
|
||||||
|
v-if="account.platform === 'claude' || account.platform === 'claude-console'"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
<div class="w-16 bg-gray-200 rounded-full h-2">
|
<div class="w-16 bg-gray-200 rounded-full h-2">
|
||||||
<div class="bg-gradient-to-r from-green-500 to-blue-600 h-2 rounded-full transition-all duration-300"
|
<div
|
||||||
:style="{ width: ((101 - (account.priority || 50)) + '%') }"></div>
|
class="bg-gradient-to-r from-green-500 to-blue-600 h-2 rounded-full transition-all duration-300"
|
||||||
|
:style="{ width: ((101 - (account.priority || 50)) + '%') }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs text-gray-700 font-medium min-w-[20px]">
|
<span class="text-xs text-gray-700 font-medium min-w-[20px]">
|
||||||
{{ account.priority || 50 }}
|
{{ account.priority || 50 }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-400 text-sm">
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-gray-400 text-sm"
|
||||||
|
>
|
||||||
<span class="text-xs">N/A</span>
|
<span class="text-xs">N/A</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||||
<div v-if="formatProxyDisplay(account.proxy)" class="text-xs bg-blue-50 px-2 py-1 rounded font-mono">
|
<div
|
||||||
|
v-if="formatProxyDisplay(account.proxy)"
|
||||||
|
class="text-xs bg-blue-50 px-2 py-1 rounded font-mono"
|
||||||
|
>
|
||||||
{{ formatProxyDisplay(account.proxy) }}
|
{{ formatProxyDisplay(account.proxy) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-400">无代理</div>
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-gray-400"
|
||||||
|
>
|
||||||
|
无代理
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
<div v-if="account.usage && account.usage.daily" class="space-y-1">
|
<div
|
||||||
|
v-if="account.usage && account.usage.daily"
|
||||||
|
class="space-y-1"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div class="w-2 h-2 bg-green-500 rounded-full" />
|
||||||
<span class="text-sm font-medium text-gray-900">{{ account.usage.daily.requests || 0 }} 次</span>
|
<span class="text-sm font-medium text-gray-900">{{ account.usage.daily.requests || 0 }} 次</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-2 h-2 bg-blue-500 rounded-full"></div>
|
<div class="w-2 h-2 bg-blue-500 rounded-full" />
|
||||||
<span class="text-xs text-gray-600">{{ formatNumber(account.usage.daily.allTokens || 0) }} tokens</span>
|
<span class="text-xs text-gray-600">{{ formatNumber(account.usage.daily.allTokens || 0) }} tokens</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="account.usage.averages && account.usage.averages.rpm > 0" class="text-xs text-gray-500">
|
<div
|
||||||
|
v-if="account.usage.averages && account.usage.averages.rpm > 0"
|
||||||
|
class="text-xs text-gray-500"
|
||||||
|
>
|
||||||
平均 {{ account.usage.averages.rpm.toFixed(2) }} RPM
|
平均 {{ account.usage.averages.rpm.toFixed(2) }} RPM
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-400 text-xs">暂无数据</div>
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-gray-400 text-xs"
|
||||||
|
>
|
||||||
|
暂无数据
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div v-if="account.platform === 'claude' && account.sessionWindow && account.sessionWindow.hasActiveWindow" class="space-y-2">
|
<div
|
||||||
|
v-if="account.platform === 'claude' && account.sessionWindow && account.sessionWindow.hasActiveWindow"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-24 bg-gray-200 rounded-full h-2">
|
<div class="w-24 bg-gray-200 rounded-full h-2">
|
||||||
<div class="bg-gradient-to-r from-blue-500 to-indigo-600 h-2 rounded-full transition-all duration-300"
|
<div
|
||||||
:style="{ width: account.sessionWindow.progress + '%' }"></div>
|
class="bg-gradient-to-r from-blue-500 to-indigo-600 h-2 rounded-full transition-all duration-300"
|
||||||
|
:style="{ width: account.sessionWindow.progress + '%' }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs text-gray-700 font-medium min-w-[32px]">
|
<span class="text-xs text-gray-700 font-medium min-w-[32px]">
|
||||||
{{ account.sessionWindow.progress }}%
|
{{ account.sessionWindow.progress }}%
|
||||||
@@ -203,15 +359,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-600">
|
<div class="text-xs text-gray-600">
|
||||||
<div>{{ formatSessionWindow(account.sessionWindow.windowStart, account.sessionWindow.windowEnd) }}</div>
|
<div>{{ formatSessionWindow(account.sessionWindow.windowStart, account.sessionWindow.windowEnd) }}</div>
|
||||||
<div v-if="account.sessionWindow.remainingTime > 0" class="text-indigo-600 font-medium">
|
<div
|
||||||
|
v-if="account.sessionWindow.remainingTime > 0"
|
||||||
|
class="text-indigo-600 font-medium"
|
||||||
|
>
|
||||||
剩余 {{ formatRemainingTime(account.sessionWindow.remainingTime) }}
|
剩余 {{ formatRemainingTime(account.sessionWindow.remainingTime) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="account.platform === 'claude'" class="text-gray-400 text-sm">
|
<div
|
||||||
<i class="fas fa-minus"></i>
|
v-else-if="account.platform === 'claude'"
|
||||||
|
class="text-gray-400 text-sm"
|
||||||
|
>
|
||||||
|
<i class="fas fa-minus" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-400 text-sm">
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-gray-400 text-sm"
|
||||||
|
>
|
||||||
<span class="text-xs">N/A</span>
|
<span class="text-xs">N/A</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -222,7 +387,6 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
v-if="account.platform === 'claude' && account.scopes"
|
v-if="account.platform === 'claude' && account.scopes"
|
||||||
@click="refreshToken(account)"
|
|
||||||
:disabled="account.isRefreshing"
|
:disabled="account.isRefreshing"
|
||||||
:class="[
|
:class="[
|
||||||
'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
|
'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
|
||||||
@@ -231,14 +395,16 @@
|
|||||||
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
|
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
|
||||||
]"
|
]"
|
||||||
:title="account.isRefreshing ? '刷新中...' : '刷新Token'"
|
:title="account.isRefreshing ? '刷新中...' : '刷新Token'"
|
||||||
|
@click="refreshToken(account)"
|
||||||
>
|
>
|
||||||
<i :class="[
|
<i
|
||||||
|
:class="[
|
||||||
'fas fa-sync-alt',
|
'fas fa-sync-alt',
|
||||||
account.isRefreshing ? 'animate-spin' : ''
|
account.isRefreshing ? 'animate-spin' : ''
|
||||||
]"></i>
|
]"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="toggleSchedulable(account)"
|
|
||||||
:disabled="account.isTogglingSchedulable"
|
:disabled="account.isTogglingSchedulable"
|
||||||
:class="[
|
:class="[
|
||||||
'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
|
'px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
|
||||||
@@ -249,23 +415,26 @@
|
|||||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||||
]"
|
]"
|
||||||
:title="account.schedulable ? '点击禁用调度' : '点击启用调度'"
|
:title="account.schedulable ? '点击禁用调度' : '点击启用调度'"
|
||||||
|
@click="toggleSchedulable(account)"
|
||||||
>
|
>
|
||||||
<i :class="[
|
<i
|
||||||
|
:class="[
|
||||||
'fas',
|
'fas',
|
||||||
account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off'
|
account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off'
|
||||||
]"></i>
|
]"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="editAccount(account)"
|
|
||||||
class="px-3 py-1.5 bg-blue-100 text-blue-700 rounded-lg text-xs font-medium hover:bg-blue-200 transition-colors"
|
class="px-3 py-1.5 bg-blue-100 text-blue-700 rounded-lg text-xs font-medium hover:bg-blue-200 transition-colors"
|
||||||
|
@click="editAccount(account)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="deleteAccount(account)"
|
|
||||||
class="px-3 py-1.5 bg-red-100 text-red-700 rounded-lg text-xs font-medium hover:bg-red-200 transition-colors"
|
class="px-3 py-1.5 bg-red-100 text-red-700 rounded-lg text-xs font-medium hover:bg-red-200 transition-colors"
|
||||||
|
@click="deleteAccount(account)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -3,107 +3,200 @@
|
|||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
|
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-gray-900 mb-2">API Keys 管理</h3>
|
<h3 class="text-xl font-bold text-gray-900 mb-2">
|
||||||
<p class="text-gray-600">管理和监控您的 API 密钥</p>
|
API Keys 管理
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
管理和监控您的 API 密钥
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<!-- Token统计时间范围选择 -->
|
<!-- Token统计时间范围选择 -->
|
||||||
<select
|
<select
|
||||||
v-model="apiKeyStatsTimeRange"
|
v-model="apiKeyStatsTimeRange"
|
||||||
@change="loadApiKeys()"
|
|
||||||
class="form-input px-3 py-2 text-sm"
|
class="form-input px-3 py-2 text-sm"
|
||||||
|
@change="loadApiKeys()"
|
||||||
>
|
>
|
||||||
<option value="today">今日</option>
|
<option value="today">
|
||||||
<option value="7days">最近7天</option>
|
今日
|
||||||
<option value="monthly">本月</option>
|
</option>
|
||||||
<option value="all">全部时间</option>
|
<option value="7days">
|
||||||
|
最近7天
|
||||||
|
</option>
|
||||||
|
<option value="monthly">
|
||||||
|
本月
|
||||||
|
</option>
|
||||||
|
<option value="all">
|
||||||
|
全部时间
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<!-- 标签筛选器 -->
|
<!-- 标签筛选器 -->
|
||||||
<select
|
<select
|
||||||
v-model="selectedTagFilter"
|
v-model="selectedTagFilter"
|
||||||
class="form-input px-3 py-2 text-sm"
|
class="form-input px-3 py-2 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">所有标签</option>
|
<option value="">
|
||||||
<option v-for="tag in availableTags" :key="tag" :value="tag">{{ tag }}</option>
|
所有标签
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="tag in availableTags"
|
||||||
|
:key="tag"
|
||||||
|
:value="tag"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
@click.stop="openCreateApiKeyModal"
|
|
||||||
class="btn btn-primary px-6 py-3 flex items-center gap-2"
|
class="btn btn-primary px-6 py-3 flex items-center gap-2"
|
||||||
|
@click.stop="openCreateApiKeyModal"
|
||||||
>
|
>
|
||||||
<i class="fas fa-plus"></i>创建新 Key
|
<i class="fas fa-plus" />创建新 Key
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="apiKeysLoading" class="text-center py-12">
|
<div
|
||||||
<div class="loading-spinner mx-auto mb-4"></div>
|
v-if="apiKeysLoading"
|
||||||
<p class="text-gray-500">正在加载 API Keys...</p>
|
class="text-center py-12"
|
||||||
|
>
|
||||||
|
<div class="loading-spinner mx-auto mb-4" />
|
||||||
|
<p class="text-gray-500">
|
||||||
|
正在加载 API Keys...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="apiKeys.length === 0" class="text-center py-12">
|
<div
|
||||||
|
v-else-if="apiKeys.length === 0"
|
||||||
|
class="text-center py-12"
|
||||||
|
>
|
||||||
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
|
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
|
||||||
<i class="fas fa-key text-gray-400 text-xl"></i>
|
<i class="fas fa-key text-gray-400 text-xl" />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-500 text-lg">暂无 API Keys</p>
|
<p class="text-gray-500 text-lg">
|
||||||
<p class="text-gray-400 text-sm mt-2">点击上方按钮创建您的第一个 API Key</p>
|
暂无 API Keys
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-400 text-sm mt-2">
|
||||||
|
点击上方按钮创建您的第一个 API Key
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="table-container">
|
<div
|
||||||
|
v-else
|
||||||
|
class="table-container"
|
||||||
|
>
|
||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('name')">
|
<th
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||||
|
@click="sortApiKeys('name')"
|
||||||
|
>
|
||||||
名称
|
名称
|
||||||
<i v-if="apiKeysSortBy === 'name'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
<i
|
||||||
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
v-if="apiKeysSortBy === 'name'"
|
||||||
|
:class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-sort ml-1 text-gray-400"
|
||||||
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">标签</th>
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">API Key</th>
|
标签
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('status')">
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
API Key
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||||
|
@click="sortApiKeys('status')"
|
||||||
|
>
|
||||||
状态
|
状态
|
||||||
<i v-if="apiKeysSortBy === 'status'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
<i
|
||||||
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
v-if="apiKeysSortBy === 'status'"
|
||||||
|
:class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-sort ml-1 text-gray-400"
|
||||||
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
使用统计
|
使用统计
|
||||||
<span class="cursor-pointer hover:bg-gray-100 px-2 py-1 rounded" @click="sortApiKeys('cost')">
|
<span
|
||||||
|
class="cursor-pointer hover:bg-gray-100 px-2 py-1 rounded"
|
||||||
|
@click="sortApiKeys('cost')"
|
||||||
|
>
|
||||||
(费用
|
(费用
|
||||||
<i v-if="apiKeysSortBy === 'cost'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
<i
|
||||||
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>)
|
v-if="apiKeysSortBy === 'cost'"
|
||||||
|
:class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-sort ml-1 text-gray-400"
|
||||||
|
/>)
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('createdAt')">
|
<th
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||||
|
@click="sortApiKeys('createdAt')"
|
||||||
|
>
|
||||||
创建时间
|
创建时间
|
||||||
<i v-if="apiKeysSortBy === 'createdAt'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
<i
|
||||||
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
v-if="apiKeysSortBy === 'createdAt'"
|
||||||
|
:class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-sort ml-1 text-gray-400"
|
||||||
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100" @click="sortApiKeys('expiresAt')">
|
<th
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||||
|
@click="sortApiKeys('expiresAt')"
|
||||||
|
>
|
||||||
过期时间
|
过期时间
|
||||||
<i v-if="apiKeysSortBy === 'expiresAt'" :class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"></i>
|
<i
|
||||||
<i v-else class="fas fa-sort ml-1 text-gray-400"></i>
|
v-if="apiKeysSortBy === 'expiresAt'"
|
||||||
|
:class="['fas', apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down', 'ml-1']"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-sort ml-1 text-gray-400"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
操作
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">操作</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200/50">
|
<tbody class="divide-y divide-gray-200/50">
|
||||||
<template v-for="key in sortedApiKeys" :key="key.id">
|
<template
|
||||||
|
v-for="key in sortedApiKeys"
|
||||||
|
:key="key.id"
|
||||||
|
>
|
||||||
<!-- API Key 主行 -->
|
<!-- API Key 主行 -->
|
||||||
<tr class="table-row">
|
<tr class="table-row">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3">
|
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3">
|
||||||
<i class="fas fa-key text-white text-xs"></i>
|
<i class="fas fa-key text-white text-xs" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-semibold text-gray-900">{{ key.name }}</div>
|
<div class="text-sm font-semibold text-gray-900">
|
||||||
<div class="text-xs text-gray-500">{{ key.id }}</div>
|
{{ key.name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{{ key.id }}
|
||||||
|
</div>
|
||||||
<div class="text-xs text-gray-500 mt-1">
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
<span v-if="key.claudeAccountId || key.claudeConsoleAccountId">
|
<span v-if="key.claudeAccountId || key.claudeConsoleAccountId">
|
||||||
<i class="fas fa-link mr-1"></i>
|
<i class="fas fa-link mr-1" />
|
||||||
绑定: {{ getBoundAccountName(key.claudeAccountId, key.claudeConsoleAccountId) }}
|
绑定: {{ getBoundAccountName(key.claudeAccountId, key.claudeConsoleAccountId) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<i class="fas fa-share-alt mr-1"></i>
|
<i class="fas fa-share-alt mr-1" />
|
||||||
使用共享池
|
使用共享池
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,12 +205,17 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
<span v-for="tag in (key.tags || [])" :key="tag"
|
<span
|
||||||
class="inline-flex items-center px-2 py-0.5 bg-blue-100 text-blue-800 text-xs rounded-full">
|
v-for="tag in (key.tags || [])"
|
||||||
|
:key="tag"
|
||||||
|
class="inline-flex items-center px-2 py-0.5 bg-blue-100 text-blue-800 text-xs rounded-full"
|
||||||
|
>
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!key.tags || key.tags.length === 0"
|
<span
|
||||||
class="text-xs text-gray-400">无标签</span>
|
v-if="!key.tags || key.tags.length === 0"
|
||||||
|
class="text-xs text-gray-400"
|
||||||
|
>无标签</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
@@ -126,10 +224,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<span :class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
|
<span
|
||||||
key.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']">
|
:class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
|
||||||
<div :class="['w-2 h-2 rounded-full mr-2',
|
key.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']"
|
||||||
key.isActive ? 'bg-green-500' : 'bg-red-500']"></div>
|
>
|
||||||
|
<div
|
||||||
|
:class="['w-2 h-2 rounded-full mr-2',
|
||||||
|
key.isActive ? 'bg-green-500' : 'bg-red-500']"
|
||||||
|
/>
|
||||||
{{ key.isActive ? '活跃' : '禁用' }}
|
{{ key.isActive ? '活跃' : '禁用' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -151,7 +253,10 @@
|
|||||||
<span class="font-medium text-green-600">{{ calculateApiKeyCost(key.usage) }}</span>
|
<span class="font-medium text-green-600">{{ calculateApiKeyCost(key.usage) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 每日费用限制 -->
|
<!-- 每日费用限制 -->
|
||||||
<div v-if="key.dailyCostLimit > 0" class="flex justify-between text-sm">
|
<div
|
||||||
|
v-if="key.dailyCostLimit > 0"
|
||||||
|
class="flex justify-between text-sm"
|
||||||
|
>
|
||||||
<span class="text-gray-600">今日费用:</span>
|
<span class="text-gray-600">今日费用:</span>
|
||||||
<span :class="['font-medium', (key.dailyCost || 0) >= key.dailyCostLimit ? 'text-red-600' : 'text-blue-600']">
|
<span :class="['font-medium', (key.dailyCost || 0) >= key.dailyCostLimit ? 'text-red-600' : 'text-blue-600']">
|
||||||
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }}
|
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }}
|
||||||
@@ -167,16 +272,25 @@
|
|||||||
<span class="text-gray-600">当前并发:</span>
|
<span class="text-gray-600">当前并发:</span>
|
||||||
<span :class="['font-medium', key.currentConcurrency > 0 ? 'text-orange-600' : 'text-gray-600']">
|
<span :class="['font-medium', key.currentConcurrency > 0 ? 'text-orange-600' : 'text-gray-600']">
|
||||||
{{ key.currentConcurrency || 0 }}
|
{{ key.currentConcurrency || 0 }}
|
||||||
<span v-if="key.concurrencyLimit > 0" class="text-xs text-gray-500">/ {{ key.concurrencyLimit }}</span>
|
<span
|
||||||
|
v-if="key.concurrencyLimit > 0"
|
||||||
|
class="text-xs text-gray-500"
|
||||||
|
>/ {{ key.concurrencyLimit }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 时间窗口限流 -->
|
<!-- 时间窗口限流 -->
|
||||||
<div v-if="key.rateLimitWindow > 0" class="flex justify-between text-sm">
|
<div
|
||||||
|
v-if="key.rateLimitWindow > 0"
|
||||||
|
class="flex justify-between text-sm"
|
||||||
|
>
|
||||||
<span class="text-gray-600">时间窗口:</span>
|
<span class="text-gray-600">时间窗口:</span>
|
||||||
<span class="font-medium text-indigo-600">{{ key.rateLimitWindow }} 分钟</span>
|
<span class="font-medium text-indigo-600">{{ key.rateLimitWindow }} 分钟</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 请求次数限制 -->
|
<!-- 请求次数限制 -->
|
||||||
<div v-if="key.rateLimitRequests > 0" class="flex justify-between text-sm">
|
<div
|
||||||
|
v-if="key.rateLimitRequests > 0"
|
||||||
|
class="flex justify-between text-sm"
|
||||||
|
>
|
||||||
<span class="text-gray-600">请求限制:</span>
|
<span class="text-gray-600">请求限制:</span>
|
||||||
<span class="font-medium text-indigo-600">{{ key.rateLimitRequests }} 次/窗口</span>
|
<span class="font-medium text-indigo-600">{{ key.rateLimitRequests }} 次/窗口</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,7 +300,10 @@
|
|||||||
<span>输出: {{ formatNumber((key.usage && key.usage.total && key.usage.total.outputTokens) || 0) }}</span>
|
<span>输出: {{ formatNumber((key.usage && key.usage.total && key.usage.total.outputTokens) || 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 缓存Token细节 -->
|
<!-- 缓存Token细节 -->
|
||||||
<div v-if="((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) > 0 || ((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) > 0" class="flex justify-between text-xs text-orange-500">
|
<div
|
||||||
|
v-if="((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) > 0 || ((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) > 0"
|
||||||
|
class="flex justify-between text-xs text-orange-500"
|
||||||
|
>
|
||||||
<span>缓存创建: {{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) }}</span>
|
<span>缓存创建: {{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) }}</span>
|
||||||
<span>缓存读取: {{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) }}</span>
|
<span>缓存读取: {{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,8 +321,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 模型分布按钮 -->
|
<!-- 模型分布按钮 -->
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
<button @click="toggleApiKeyModelStats(key.id)" v-if="key && key.id" class="text-xs text-indigo-600 hover:text-indigo-800 font-medium">
|
<button
|
||||||
<i :class="['fas', expandedApiKeys[key.id] ? 'fa-chevron-up' : 'fa-chevron-down', 'mr-1']"></i>
|
v-if="key && key.id"
|
||||||
|
class="text-xs text-indigo-600 hover:text-indigo-800 font-medium"
|
||||||
|
@click="toggleApiKeyModelStats(key.id)"
|
||||||
|
>
|
||||||
|
<i :class="['fas', expandedApiKeys[key.id] ? 'fa-chevron-up' : 'fa-chevron-down', 'mr-1']" />
|
||||||
模型使用分布
|
模型使用分布
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,50 +337,62 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
<div v-if="key.expiresAt">
|
<div v-if="key.expiresAt">
|
||||||
<div v-if="isApiKeyExpired(key.expiresAt)" class="text-red-600">
|
<div
|
||||||
<i class="fas fa-exclamation-circle mr-1"></i>
|
v-if="isApiKeyExpired(key.expiresAt)"
|
||||||
|
class="text-red-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-circle mr-1" />
|
||||||
已过期
|
已过期
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="isApiKeyExpiringSoon(key.expiresAt)" class="text-orange-600">
|
<div
|
||||||
<i class="fas fa-clock mr-1"></i>
|
v-else-if="isApiKeyExpiringSoon(key.expiresAt)"
|
||||||
|
class="text-orange-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-clock mr-1" />
|
||||||
{{ formatExpireDate(key.expiresAt) }}
|
{{ formatExpireDate(key.expiresAt) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-600">
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-gray-600"
|
||||||
|
>
|
||||||
{{ formatExpireDate(key.expiresAt) }}
|
{{ formatExpireDate(key.expiresAt) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-gray-400">
|
<div
|
||||||
<i class="fas fa-infinity mr-1"></i>
|
v-else
|
||||||
|
class="text-gray-400"
|
||||||
|
>
|
||||||
|
<i class="fas fa-infinity mr-1" />
|
||||||
永不过期
|
永不过期
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@click="copyApiStatsLink(key)"
|
|
||||||
class="text-purple-600 hover:text-purple-900 font-medium hover:bg-purple-50 px-3 py-1 rounded-lg transition-colors"
|
class="text-purple-600 hover:text-purple-900 font-medium hover:bg-purple-50 px-3 py-1 rounded-lg transition-colors"
|
||||||
title="复制统计页面链接"
|
title="复制统计页面链接"
|
||||||
|
@click="copyApiStatsLink(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-chart-bar mr-1"></i>统计
|
<i class="fas fa-chart-bar mr-1" />统计
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="openEditApiKeyModal(key)"
|
|
||||||
class="text-blue-600 hover:text-blue-900 font-medium hover:bg-blue-50 px-3 py-1 rounded-lg transition-colors"
|
class="text-blue-600 hover:text-blue-900 font-medium hover:bg-blue-50 px-3 py-1 rounded-lg transition-colors"
|
||||||
|
@click="openEditApiKeyModal(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-edit mr-1"></i>编辑
|
<i class="fas fa-edit mr-1" />编辑
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="key.expiresAt && (isApiKeyExpired(key.expiresAt) || isApiKeyExpiringSoon(key.expiresAt))"
|
v-if="key.expiresAt && (isApiKeyExpired(key.expiresAt) || isApiKeyExpiringSoon(key.expiresAt))"
|
||||||
@click="openRenewApiKeyModal(key)"
|
|
||||||
class="text-green-600 hover:text-green-900 font-medium hover:bg-green-50 px-3 py-1 rounded-lg transition-colors"
|
class="text-green-600 hover:text-green-900 font-medium hover:bg-green-50 px-3 py-1 rounded-lg transition-colors"
|
||||||
|
@click="openRenewApiKeyModal(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-clock mr-1"></i>续期
|
<i class="fas fa-clock mr-1" />续期
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="deleteApiKey(key.id)"
|
|
||||||
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
|
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
|
||||||
|
@click="deleteApiKey(key.id)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash mr-1"></i>删除
|
<i class="fas fa-trash mr-1" />删除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -267,20 +400,31 @@
|
|||||||
|
|
||||||
<!-- 模型统计展开区域 -->
|
<!-- 模型统计展开区域 -->
|
||||||
<tr v-if="key && key.id && expandedApiKeys[key.id]">
|
<tr v-if="key && key.id && expandedApiKeys[key.id]">
|
||||||
<td colspan="7" class="px-6 py-4 bg-gray-50">
|
<td
|
||||||
<div v-if="!apiKeyModelStats[key.id]" class="text-center py-4">
|
colspan="7"
|
||||||
<div class="loading-spinner mx-auto"></div>
|
class="px-6 py-4 bg-gray-50"
|
||||||
<p class="text-sm text-gray-500 mt-2">加载模型统计...</p>
|
>
|
||||||
|
<div
|
||||||
|
v-if="!apiKeyModelStats[key.id]"
|
||||||
|
class="text-center py-4"
|
||||||
|
>
|
||||||
|
<div class="loading-spinner mx-auto" />
|
||||||
|
<p class="text-sm text-gray-500 mt-2">
|
||||||
|
加载模型统计...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- 通用的标题和时间筛选器,无论是否有数据都显示 -->
|
<!-- 通用的标题和时间筛选器,无论是否有数据都显示 -->
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h5 class="text-sm font-semibold text-gray-700 flex items-center">
|
<h5 class="text-sm font-semibold text-gray-700 flex items-center">
|
||||||
<i class="fas fa-chart-pie text-indigo-500 mr-2"></i>
|
<i class="fas fa-chart-pie text-indigo-500 mr-2" />
|
||||||
模型使用分布
|
模型使用分布
|
||||||
</h5>
|
</h5>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
|
<span
|
||||||
|
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
|
||||||
|
class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full"
|
||||||
|
>
|
||||||
{{ apiKeyModelStats[key.id].length }} 个模型
|
{{ apiKeyModelStats[key.id].length }} 个模型
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -291,13 +435,13 @@
|
|||||||
<button
|
<button
|
||||||
v-for="option in getApiKeyDateFilter(key.id).presetOptions"
|
v-for="option in getApiKeyDateFilter(key.id).presetOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
@click="setApiKeyDateFilterPreset(option.value, key.id)"
|
|
||||||
:class="[
|
:class="[
|
||||||
'px-2 py-1 rounded text-xs font-medium transition-colors',
|
'px-2 py-1 rounded text-xs font-medium transition-colors',
|
||||||
getApiKeyDateFilter(key.id).preset === option.value && getApiKeyDateFilter(key.id).type === 'preset'
|
getApiKeyDateFilter(key.id).preset === option.value && getApiKeyDateFilter(key.id).type === 'preset'
|
||||||
? 'bg-white text-blue-600 shadow-sm'
|
? 'bg-white text-blue-600 shadow-sm'
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
]"
|
]"
|
||||||
|
@click="setApiKeyDateFilterPreset(option.value, key.id)"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</button>
|
</button>
|
||||||
@@ -306,7 +450,6 @@
|
|||||||
<!-- Element Plus 日期范围选择器 -->
|
<!-- Element Plus 日期范围选择器 -->
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
:model-value="getApiKeyDateFilter(key.id).customRange"
|
:model-value="getApiKeyDateFilter(key.id).customRange"
|
||||||
@update:model-value="(value) => onApiKeyCustomDateRangeChange(key.id, value)"
|
|
||||||
type="datetimerange"
|
type="datetimerange"
|
||||||
range-separator="至"
|
range-separator="至"
|
||||||
start-placeholder="开始日期"
|
start-placeholder="开始日期"
|
||||||
@@ -317,33 +460,47 @@
|
|||||||
:default-time="defaultTime"
|
:default-time="defaultTime"
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 280px;"
|
style="width: 280px;"
|
||||||
|
@update:model-value="(value) => onApiKeyCustomDateRangeChange(key.id, value)"
|
||||||
class="api-key-date-picker"
|
class="api-key-date-picker"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
:unlink-panels="false"
|
:unlink-panels="false"
|
||||||
></el-date-picker>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 数据展示区域 -->
|
<!-- 数据展示区域 -->
|
||||||
<div v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length === 0" class="text-center py-8">
|
<div
|
||||||
|
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length === 0"
|
||||||
|
class="text-center py-8"
|
||||||
|
>
|
||||||
<div class="flex items-center justify-center gap-2 mb-3">
|
<div class="flex items-center justify-center gap-2 mb-3">
|
||||||
<i class="fas fa-chart-line text-gray-400 text-lg"></i>
|
<i class="fas fa-chart-line text-gray-400 text-lg" />
|
||||||
<p class="text-sm text-gray-500">暂无模型使用数据</p>
|
<p class="text-sm text-gray-500">
|
||||||
|
暂无模型使用数据
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
@click="resetApiKeyDateFilter(key.id)"
|
|
||||||
class="text-blue-500 hover:text-blue-700 text-sm ml-2 flex items-center gap-1 transition-colors"
|
class="text-blue-500 hover:text-blue-700 text-sm ml-2 flex items-center gap-1 transition-colors"
|
||||||
title="重置筛选条件并刷新"
|
title="重置筛选条件并刷新"
|
||||||
|
@click="resetApiKeyDateFilter(key.id)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-sync-alt text-xs"></i>
|
<i class="fas fa-sync-alt text-xs" />
|
||||||
<span class="text-xs">刷新</span>
|
<span class="text-xs">刷新</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-400">尝试调整时间范围或点击刷新重新加载数据</p>
|
<p class="text-xs text-gray-400">
|
||||||
|
尝试调整时间范围或点击刷新重新加载数据
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div
|
||||||
<div v-for="stat in apiKeyModelStats[key.id]" :key="stat.model"
|
v-else-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
|
||||||
class="bg-gradient-to-br from-white to-gray-50 rounded-xl p-4 border border-gray-200 hover:border-indigo-300 hover:shadow-lg transition-all duration-200">
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="stat in apiKeyModelStats[key.id]"
|
||||||
|
:key="stat.model"
|
||||||
|
class="bg-gradient-to-br from-white to-gray-50 rounded-xl p-4 border border-gray-200 hover:border-indigo-300 hover:shadow-lg transition-all duration-200"
|
||||||
|
>
|
||||||
<div class="flex justify-between items-start mb-3">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<span class="text-sm font-semibold text-gray-800 block mb-1">{{ stat.model }}</span>
|
<span class="text-sm font-semibold text-gray-800 block mb-1">{{ stat.model }}</span>
|
||||||
@@ -354,14 +511,14 @@
|
|||||||
<div class="space-y-2 mb-3">
|
<div class="space-y-2 mb-3">
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<span class="text-gray-600 flex items-center">
|
<span class="text-gray-600 flex items-center">
|
||||||
<i class="fas fa-coins text-yellow-500 mr-1 text-xs"></i>
|
<i class="fas fa-coins text-yellow-500 mr-1 text-xs" />
|
||||||
总Token:
|
总Token:
|
||||||
</span>
|
</span>
|
||||||
<span class="font-semibold text-gray-900">{{ formatNumber(stat.allTokens) }}</span>
|
<span class="font-semibold text-gray-900">{{ formatNumber(stat.allTokens) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<span class="text-gray-600 flex items-center">
|
<span class="text-gray-600 flex items-center">
|
||||||
<i class="fas fa-dollar-sign text-green-500 mr-1 text-xs"></i>
|
<i class="fas fa-dollar-sign text-green-500 mr-1 text-xs" />
|
||||||
费用:
|
费用:
|
||||||
</span>
|
</span>
|
||||||
<span class="font-semibold text-green-600">{{ calculateModelCost(stat) }}</span>
|
<span class="font-semibold text-green-600">{{ calculateModelCost(stat) }}</span>
|
||||||
@@ -369,28 +526,34 @@
|
|||||||
<div class="pt-2 mt-2 border-t border-gray-100">
|
<div class="pt-2 mt-2 border-t border-gray-100">
|
||||||
<div class="flex justify-between items-center text-xs text-gray-500">
|
<div class="flex justify-between items-center text-xs text-gray-500">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<i class="fas fa-arrow-down text-green-500 mr-1"></i>
|
<i class="fas fa-arrow-down text-green-500 mr-1" />
|
||||||
输入:
|
输入:
|
||||||
</span>
|
</span>
|
||||||
<span class="font-medium">{{ formatNumber(stat.inputTokens) }}</span>
|
<span class="font-medium">{{ formatNumber(stat.inputTokens) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center text-xs text-gray-500">
|
<div class="flex justify-between items-center text-xs text-gray-500">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<i class="fas fa-arrow-up text-blue-500 mr-1"></i>
|
<i class="fas fa-arrow-up text-blue-500 mr-1" />
|
||||||
输出:
|
输出:
|
||||||
</span>
|
</span>
|
||||||
<span class="font-medium">{{ formatNumber(stat.outputTokens) }}</span>
|
<span class="font-medium">{{ formatNumber(stat.outputTokens) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="stat.cacheCreateTokens > 0" class="flex justify-between items-center text-xs text-purple-600">
|
<div
|
||||||
|
v-if="stat.cacheCreateTokens > 0"
|
||||||
|
class="flex justify-between items-center text-xs text-purple-600"
|
||||||
|
>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<i class="fas fa-save mr-1"></i>
|
<i class="fas fa-save mr-1" />
|
||||||
缓存创建:
|
缓存创建:
|
||||||
</span>
|
</span>
|
||||||
<span class="font-medium">{{ formatNumber(stat.cacheCreateTokens) }}</span>
|
<span class="font-medium">{{ formatNumber(stat.cacheCreateTokens) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="stat.cacheReadTokens > 0" class="flex justify-between items-center text-xs text-purple-600">
|
<div
|
||||||
|
v-if="stat.cacheReadTokens > 0"
|
||||||
|
class="flex justify-between items-center text-xs text-purple-600"
|
||||||
|
>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<i class="fas fa-download mr-1"></i>
|
<i class="fas fa-download mr-1" />
|
||||||
缓存读取:
|
缓存读取:
|
||||||
</span>
|
</span>
|
||||||
<span class="font-medium">{{ formatNumber(stat.cacheReadTokens) }}</span>
|
<span class="font-medium">{{ formatNumber(stat.cacheReadTokens) }}</span>
|
||||||
@@ -400,9 +563,10 @@
|
|||||||
|
|
||||||
<!-- 进度条 -->
|
<!-- 进度条 -->
|
||||||
<div class="w-full bg-gray-200 rounded-full h-2 mt-3">
|
<div class="w-full bg-gray-200 rounded-full h-2 mt-3">
|
||||||
<div class="bg-gradient-to-r from-indigo-500 to-purple-600 h-2 rounded-full transition-all duration-500"
|
<div
|
||||||
:style="{ width: calculateApiKeyModelPercentage(stat.allTokens, apiKeyModelStats[key.id]) + '%' }">
|
class="bg-gradient-to-r from-indigo-500 to-purple-600 h-2 rounded-full transition-all duration-500"
|
||||||
</div>
|
:style="{ width: calculateApiKeyModelPercentage(stat.allTokens, apiKeyModelStats[key.id]) + '%' }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right mt-1">
|
<div class="text-right mt-1">
|
||||||
<span class="text-xs font-medium text-indigo-600">
|
<span class="text-xs font-medium text-indigo-600">
|
||||||
@@ -413,10 +577,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 总计统计,仅在有数据时显示 -->
|
<!-- 总计统计,仅在有数据时显示 -->
|
||||||
<div v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" class="mt-4 p-3 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg border border-indigo-100">
|
<div
|
||||||
|
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
|
||||||
|
class="mt-4 p-3 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg border border-indigo-100"
|
||||||
|
>
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="font-semibold text-gray-700 flex items-center">
|
<span class="font-semibold text-gray-700 flex items-center">
|
||||||
<i class="fas fa-calculator text-indigo-500 mr-2"></i>
|
<i class="fas fa-calculator text-indigo-500 mr-2" />
|
||||||
总计统计
|
总计统计
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-4 text-xs">
|
<div class="flex gap-4 text-xs">
|
||||||
@@ -448,7 +615,7 @@
|
|||||||
|
|
||||||
<EditApiKeyModal
|
<EditApiKeyModal
|
||||||
v-if="showEditApiKeyModal"
|
v-if="showEditApiKeyModal"
|
||||||
:apiKey="editingApiKey"
|
:api-key="editingApiKey"
|
||||||
:accounts="accounts"
|
:accounts="accounts"
|
||||||
@close="showEditApiKeyModal = false"
|
@close="showEditApiKeyModal = false"
|
||||||
@success="handleEditSuccess"
|
@success="handleEditSuccess"
|
||||||
@@ -456,14 +623,14 @@
|
|||||||
|
|
||||||
<RenewApiKeyModal
|
<RenewApiKeyModal
|
||||||
v-if="showRenewApiKeyModal"
|
v-if="showRenewApiKeyModal"
|
||||||
:apiKey="renewingApiKey"
|
:api-key="renewingApiKey"
|
||||||
@close="showRenewApiKeyModal = false"
|
@close="showRenewApiKeyModal = false"
|
||||||
@success="handleRenewSuccess"
|
@success="handleRenewSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NewApiKeyModal
|
<NewApiKeyModal
|
||||||
v-if="showNewApiKeyModal"
|
v-if="showNewApiKeyModal"
|
||||||
:apiKey="newApiKeyData"
|
:api-key="newApiKeyData"
|
||||||
@close="showNewApiKeyModal = false"
|
@close="showNewApiKeyModal = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,8 +10,11 @@
|
|||||||
:logo-src="oemSettings.siteIconData || oemSettings.siteIcon"
|
:logo-src="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<router-link to="/dashboard" class="admin-button rounded-xl px-4 py-2 text-white transition-all duration-300 flex items-center gap-2">
|
<router-link
|
||||||
<i class="fas fa-cog text-sm"></i>
|
to="/dashboard"
|
||||||
|
class="admin-button rounded-xl px-4 py-2 text-white transition-all duration-300 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<i class="fas fa-cog text-sm" />
|
||||||
<span class="text-sm font-medium">管理后台</span>
|
<span class="text-sm font-medium">管理后台</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,23 +26,23 @@
|
|||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="inline-flex bg-white/10 backdrop-blur-xl rounded-full p-1 shadow-lg border border-white/20">
|
<div class="inline-flex bg-white/10 backdrop-blur-xl rounded-full p-1 shadow-lg border border-white/20">
|
||||||
<button
|
<button
|
||||||
@click="currentTab = 'stats'"
|
|
||||||
:class="[
|
:class="[
|
||||||
'tab-pill-button',
|
'tab-pill-button',
|
||||||
currentTab === 'stats' ? 'active' : ''
|
currentTab === 'stats' ? 'active' : ''
|
||||||
]"
|
]"
|
||||||
|
@click="currentTab = 'stats'"
|
||||||
>
|
>
|
||||||
<i class="fas fa-chart-line mr-2"></i>
|
<i class="fas fa-chart-line mr-2" />
|
||||||
<span>统计查询</span>
|
<span>统计查询</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="currentTab = 'tutorial'"
|
|
||||||
:class="[
|
:class="[
|
||||||
'tab-pill-button',
|
'tab-pill-button',
|
||||||
currentTab === 'tutorial' ? 'active' : ''
|
currentTab === 'tutorial' ? 'active' : ''
|
||||||
]"
|
]"
|
||||||
|
@click="currentTab = 'tutorial'"
|
||||||
>
|
>
|
||||||
<i class="fas fa-graduation-cap mr-2"></i>
|
<i class="fas fa-graduation-cap mr-2" />
|
||||||
<span>使用教程</span>
|
<span>使用教程</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,45 +50,54 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 统计内容 -->
|
<!-- 统计内容 -->
|
||||||
<div v-if="currentTab === 'stats'" class="tab-content">
|
<div
|
||||||
|
v-if="currentTab === 'stats'"
|
||||||
|
class="tab-content"
|
||||||
|
>
|
||||||
<!-- API Key 输入区域 -->
|
<!-- API Key 输入区域 -->
|
||||||
<ApiKeyInput />
|
<ApiKeyInput />
|
||||||
|
|
||||||
<!-- 错误提示 -->
|
<!-- 错误提示 -->
|
||||||
<div v-if="error" class="mb-8">
|
<div
|
||||||
|
v-if="error"
|
||||||
|
class="mb-8"
|
||||||
|
>
|
||||||
<div class="bg-red-500/20 border border-red-500/30 rounded-xl p-4 text-red-800 backdrop-blur-sm">
|
<div class="bg-red-500/20 border border-red-500/30 rounded-xl p-4 text-red-800 backdrop-blur-sm">
|
||||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
<i class="fas fa-exclamation-triangle mr-2" />
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 统计数据展示区域 -->
|
<!-- 统计数据展示区域 -->
|
||||||
<div v-if="statsData" class="fade-in">
|
<div
|
||||||
|
v-if="statsData"
|
||||||
|
class="fade-in"
|
||||||
|
>
|
||||||
<div class="glass-strong rounded-3xl p-6 shadow-xl">
|
<div class="glass-strong rounded-3xl p-6 shadow-xl">
|
||||||
<!-- 时间范围选择器 -->
|
<!-- 时间范围选择器 -->
|
||||||
<div class="mb-6 pb-6 border-b border-gray-200">
|
<div class="mb-6 pb-6 border-b border-gray-200">
|
||||||
<div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
<div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<i class="fas fa-clock text-blue-500 text-lg"></i>
|
<i class="fas fa-clock text-blue-500 text-lg" />
|
||||||
<span class="text-lg font-medium text-gray-700">统计时间范围</span>
|
<span class="text-lg font-medium text-gray-700">统计时间范围</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@click="switchPeriod('daily')"
|
|
||||||
:class="['period-btn', { 'active': statsPeriod === 'daily' }]"
|
:class="['period-btn', { 'active': statsPeriod === 'daily' }]"
|
||||||
class="px-6 py-2 text-sm font-medium flex items-center gap-2"
|
class="px-6 py-2 text-sm font-medium flex items-center gap-2"
|
||||||
:disabled="loading || modelStatsLoading"
|
:disabled="loading || modelStatsLoading"
|
||||||
|
@click="switchPeriod('daily')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-calendar-day"></i>
|
<i class="fas fa-calendar-day" />
|
||||||
今日
|
今日
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="switchPeriod('monthly')"
|
|
||||||
:class="['period-btn', { 'active': statsPeriod === 'monthly' }]"
|
:class="['period-btn', { 'active': statsPeriod === 'monthly' }]"
|
||||||
class="px-6 py-2 text-sm font-medium flex items-center gap-2"
|
class="px-6 py-2 text-sm font-medium flex items-center gap-2"
|
||||||
:disabled="loading || modelStatsLoading"
|
:disabled="loading || modelStatsLoading"
|
||||||
|
@click="switchPeriod('monthly')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-calendar-alt"></i>
|
<i class="fas fa-calendar-alt" />
|
||||||
本月
|
本月
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +120,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 教程内容 -->
|
<!-- 教程内容 -->
|
||||||
<div v-if="currentTab === 'tutorial'" class="tab-content">
|
<div
|
||||||
|
v-if="currentTab === 'tutorial'"
|
||||||
|
class="tab-content"
|
||||||
|
>
|
||||||
<div class="glass-strong rounded-3xl shadow-xl">
|
<div class="glass-strong rounded-3xl shadow-xl">
|
||||||
<TutorialView />
|
<TutorialView />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,12 +5,18 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-600 mb-1">总API Keys</p>
|
<p class="text-sm font-semibold text-gray-600 mb-1">
|
||||||
<p class="text-3xl font-bold text-gray-900">{{ dashboardData.totalApiKeys }}</p>
|
总API Keys
|
||||||
<p class="text-xs text-gray-500 mt-1">活跃: {{ dashboardData.activeApiKeys || 0 }}</p>
|
</p>
|
||||||
|
<p class="text-3xl font-bold text-gray-900">
|
||||||
|
{{ dashboardData.totalApiKeys }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
活跃: {{ dashboardData.activeApiKeys || 0 }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-blue-500 to-blue-600">
|
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-blue-500 to-blue-600">
|
||||||
<i class="fas fa-key"></i>
|
<i class="fas fa-key" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,17 +24,24 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-600 mb-1">服务账户</p>
|
<p class="text-sm font-semibold text-gray-600 mb-1">
|
||||||
<p class="text-3xl font-bold text-gray-900">{{ dashboardData.totalAccounts }}</p>
|
服务账户
|
||||||
|
</p>
|
||||||
|
<p class="text-3xl font-bold text-gray-900">
|
||||||
|
{{ dashboardData.totalAccounts }}
|
||||||
|
</p>
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
活跃: {{ dashboardData.activeAccounts || 0 }}
|
活跃: {{ dashboardData.activeAccounts || 0 }}
|
||||||
<span v-if="dashboardData.rateLimitedAccounts > 0" class="text-yellow-600">
|
<span
|
||||||
|
v-if="dashboardData.rateLimitedAccounts > 0"
|
||||||
|
class="text-yellow-600"
|
||||||
|
>
|
||||||
| 限流: {{ dashboardData.rateLimitedAccounts }}
|
| 限流: {{ dashboardData.rateLimitedAccounts }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-green-500 to-green-600">
|
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-green-500 to-green-600">
|
||||||
<i class="fas fa-user-circle"></i>
|
<i class="fas fa-user-circle" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,12 +49,18 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-600 mb-1">今日请求</p>
|
<p class="text-sm font-semibold text-gray-600 mb-1">
|
||||||
<p class="text-3xl font-bold text-gray-900">{{ dashboardData.todayRequests }}</p>
|
今日请求
|
||||||
<p class="text-xs text-gray-500 mt-1">总请求: {{ formatNumber(dashboardData.totalRequests || 0) }}</p>
|
</p>
|
||||||
|
<p class="text-3xl font-bold text-gray-900">
|
||||||
|
{{ dashboardData.todayRequests }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
总请求: {{ formatNumber(dashboardData.totalRequests || 0) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-purple-500 to-purple-600">
|
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-purple-500 to-purple-600">
|
||||||
<i class="fas fa-chart-line"></i>
|
<i class="fas fa-chart-line" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,12 +68,18 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-600 mb-1">系统状态</p>
|
<p class="text-sm font-semibold text-gray-600 mb-1">
|
||||||
<p class="text-3xl font-bold text-green-600">{{ dashboardData.systemStatus }}</p>
|
系统状态
|
||||||
<p class="text-xs text-gray-500 mt-1">运行时间: {{ formattedUptime }}</p>
|
</p>
|
||||||
|
<p class="text-3xl font-bold text-green-600">
|
||||||
|
{{ dashboardData.systemStatus }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
运行时间: {{ formattedUptime }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-yellow-500 to-orange-500">
|
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-yellow-500 to-orange-500">
|
||||||
<i class="fas fa-heartbeat"></i>
|
<i class="fas fa-heartbeat" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,22 +90,32 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex-1 mr-8">
|
<div class="flex-1 mr-8">
|
||||||
<p class="text-sm font-semibold text-gray-600 mb-1">今日Token</p>
|
<p class="text-sm font-semibold text-gray-600 mb-1">
|
||||||
|
今日Token
|
||||||
|
</p>
|
||||||
<div class="flex items-baseline gap-2 mb-2 flex-wrap">
|
<div class="flex items-baseline gap-2 mb-2 flex-wrap">
|
||||||
<p class="text-3xl font-bold text-blue-600">{{ formatNumber((dashboardData.todayInputTokens || 0) + (dashboardData.todayOutputTokens || 0) + (dashboardData.todayCacheCreateTokens || 0) + (dashboardData.todayCacheReadTokens || 0)) }}</p>
|
<p class="text-3xl font-bold text-blue-600">
|
||||||
|
{{ formatNumber((dashboardData.todayInputTokens || 0) + (dashboardData.todayOutputTokens || 0) + (dashboardData.todayCacheCreateTokens || 0) + (dashboardData.todayCacheReadTokens || 0)) }}
|
||||||
|
</p>
|
||||||
<span class="text-sm text-green-600 font-medium">/ {{ costsData.todayCosts.formatted.totalCost }}</span>
|
<span class="text-sm text-green-600 font-medium">/ {{ costsData.todayCosts.formatted.totalCost }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-500">
|
<div class="text-xs text-gray-500">
|
||||||
<div class="flex justify-between items-center flex-wrap gap-x-4">
|
<div class="flex justify-between items-center flex-wrap gap-x-4">
|
||||||
<span>输入: <span class="font-medium">{{ formatNumber(dashboardData.todayInputTokens || 0) }}</span></span>
|
<span>输入: <span class="font-medium">{{ formatNumber(dashboardData.todayInputTokens || 0) }}</span></span>
|
||||||
<span>输出: <span class="font-medium">{{ formatNumber(dashboardData.todayOutputTokens || 0) }}</span></span>
|
<span>输出: <span class="font-medium">{{ formatNumber(dashboardData.todayOutputTokens || 0) }}</span></span>
|
||||||
<span v-if="(dashboardData.todayCacheCreateTokens || 0) > 0" class="text-purple-600">缓存创建: <span class="font-medium">{{ formatNumber(dashboardData.todayCacheCreateTokens || 0) }}</span></span>
|
<span
|
||||||
<span v-if="(dashboardData.todayCacheReadTokens || 0) > 0" class="text-purple-600">缓存读取: <span class="font-medium">{{ formatNumber(dashboardData.todayCacheReadTokens || 0) }}</span></span>
|
v-if="(dashboardData.todayCacheCreateTokens || 0) > 0"
|
||||||
|
class="text-purple-600"
|
||||||
|
>缓存创建: <span class="font-medium">{{ formatNumber(dashboardData.todayCacheCreateTokens || 0) }}</span></span>
|
||||||
|
<span
|
||||||
|
v-if="(dashboardData.todayCacheReadTokens || 0) > 0"
|
||||||
|
class="text-purple-600"
|
||||||
|
>缓存读取: <span class="font-medium">{{ formatNumber(dashboardData.todayCacheReadTokens || 0) }}</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-indigo-500 to-indigo-600">
|
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-indigo-500 to-indigo-600">
|
||||||
<i class="fas fa-coins"></i>
|
<i class="fas fa-coins" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,22 +123,32 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex-1 mr-8">
|
<div class="flex-1 mr-8">
|
||||||
<p class="text-sm font-semibold text-gray-600 mb-1">总Token消耗</p>
|
<p class="text-sm font-semibold text-gray-600 mb-1">
|
||||||
|
总Token消耗
|
||||||
|
</p>
|
||||||
<div class="flex items-baseline gap-2 mb-2 flex-wrap">
|
<div class="flex items-baseline gap-2 mb-2 flex-wrap">
|
||||||
<p class="text-3xl font-bold text-emerald-600">{{ formatNumber((dashboardData.totalInputTokens || 0) + (dashboardData.totalOutputTokens || 0) + (dashboardData.totalCacheCreateTokens || 0) + (dashboardData.totalCacheReadTokens || 0)) }}</p>
|
<p class="text-3xl font-bold text-emerald-600">
|
||||||
|
{{ formatNumber((dashboardData.totalInputTokens || 0) + (dashboardData.totalOutputTokens || 0) + (dashboardData.totalCacheCreateTokens || 0) + (dashboardData.totalCacheReadTokens || 0)) }}
|
||||||
|
</p>
|
||||||
<span class="text-sm text-green-600 font-medium">/ {{ costsData.totalCosts.formatted.totalCost }}</span>
|
<span class="text-sm text-green-600 font-medium">/ {{ costsData.totalCosts.formatted.totalCost }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-500">
|
<div class="text-xs text-gray-500">
|
||||||
<div class="flex justify-between items-center flex-wrap gap-x-4">
|
<div class="flex justify-between items-center flex-wrap gap-x-4">
|
||||||
<span>输入: <span class="font-medium">{{ formatNumber(dashboardData.totalInputTokens || 0) }}</span></span>
|
<span>输入: <span class="font-medium">{{ formatNumber(dashboardData.totalInputTokens || 0) }}</span></span>
|
||||||
<span>输出: <span class="font-medium">{{ formatNumber(dashboardData.totalOutputTokens || 0) }}</span></span>
|
<span>输出: <span class="font-medium">{{ formatNumber(dashboardData.totalOutputTokens || 0) }}</span></span>
|
||||||
<span v-if="(dashboardData.totalCacheCreateTokens || 0) > 0" class="text-purple-600">缓存创建: <span class="font-medium">{{ formatNumber(dashboardData.totalCacheCreateTokens || 0) }}</span></span>
|
<span
|
||||||
<span v-if="(dashboardData.totalCacheReadTokens || 0) > 0" class="text-purple-600">缓存读取: <span class="font-medium">{{ formatNumber(dashboardData.totalCacheReadTokens || 0) }}</span></span>
|
v-if="(dashboardData.totalCacheCreateTokens || 0) > 0"
|
||||||
|
class="text-purple-600"
|
||||||
|
>缓存创建: <span class="font-medium">{{ formatNumber(dashboardData.totalCacheCreateTokens || 0) }}</span></span>
|
||||||
|
<span
|
||||||
|
v-if="(dashboardData.totalCacheReadTokens || 0) > 0"
|
||||||
|
class="text-purple-600"
|
||||||
|
>缓存读取: <span class="font-medium">{{ formatNumber(dashboardData.totalCacheReadTokens || 0) }}</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-emerald-500 to-emerald-600">
|
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-emerald-500 to-emerald-600">
|
||||||
<i class="fas fa-database"></i>
|
<i class="fas fa-database" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,12 +156,25 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-600 mb-1">平均RPM</p>
|
<p class="text-sm font-semibold text-gray-600 mb-1">
|
||||||
<p class="text-3xl font-bold text-orange-600">{{ dashboardData.systemRPM || 0 }}</p>
|
实时RPM
|
||||||
<p class="text-xs text-gray-500 mt-1">每分钟请求数</p>
|
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-3xl font-bold text-orange-600">
|
||||||
|
{{ dashboardData.realtimeRPM || 0 }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
每分钟请求数
|
||||||
|
<span
|
||||||
|
v-if="dashboardData.isHistoricalMetrics"
|
||||||
|
class="text-yellow-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-circle" /> 历史数据
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-orange-500 to-orange-600">
|
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-orange-500 to-orange-600">
|
||||||
<i class="fas fa-tachometer-alt"></i>
|
<i class="fas fa-tachometer-alt" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,12 +182,25 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-600 mb-1">平均TPM</p>
|
<p class="text-sm font-semibold text-gray-600 mb-1">
|
||||||
<p class="text-3xl font-bold text-rose-600">{{ dashboardData.systemTPM || 0 }}</p>
|
实时TPM
|
||||||
<p class="text-xs text-gray-500 mt-1">每分钟Token数</p>
|
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-3xl font-bold text-rose-600">
|
||||||
|
{{ formatNumber(dashboardData.realtimeTPM || 0) }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
每分钟Token数
|
||||||
|
<span
|
||||||
|
v-if="dashboardData.isHistoricalMetrics"
|
||||||
|
class="text-yellow-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-circle" /> 历史数据
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-rose-500 to-rose-600">
|
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-rose-500 to-rose-600">
|
||||||
<i class="fas fa-rocket"></i>
|
<i class="fas fa-rocket" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,21 +208,23 @@
|
|||||||
|
|
||||||
<!-- 模型消费统计 -->
|
<!-- 模型消费统计 -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
|
||||||
<h3 class="text-xl font-bold text-gray-900">模型使用分布与Token使用趋势</h3>
|
<h3 class="text-xl font-bold text-gray-900">
|
||||||
<div class="flex gap-2 items-center">
|
模型使用分布与Token使用趋势
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
<!-- 快捷日期选择 -->
|
<!-- 快捷日期选择 -->
|
||||||
<div class="flex gap-1 bg-gray-100 rounded-lg p-1">
|
<div class="flex gap-1 bg-gray-100 rounded-lg p-1">
|
||||||
<button
|
<button
|
||||||
v-for="option in dateFilter.presetOptions"
|
v-for="option in dateFilter.presetOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
@click="setDateFilterPreset(option.value)"
|
|
||||||
:class="[
|
:class="[
|
||||||
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
|
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
|
||||||
dateFilter.preset === option.value && dateFilter.type === 'preset'
|
dateFilter.preset === option.value && dateFilter.type === 'preset'
|
||||||
? 'bg-white text-blue-600 shadow-sm'
|
? 'bg-white text-blue-600 shadow-sm'
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
]"
|
]"
|
||||||
|
@click="setDateFilterPreset(option.value)"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</button>
|
</button>
|
||||||
@@ -160,89 +233,163 @@
|
|||||||
<!-- 粒度切换按钮 -->
|
<!-- 粒度切换按钮 -->
|
||||||
<div class="flex gap-1 bg-gray-100 rounded-lg p-1">
|
<div class="flex gap-1 bg-gray-100 rounded-lg p-1">
|
||||||
<button
|
<button
|
||||||
@click="setTrendGranularity('day')"
|
|
||||||
:class="[
|
:class="[
|
||||||
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
|
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
|
||||||
trendGranularity === 'day'
|
trendGranularity === 'day'
|
||||||
? 'bg-white text-blue-600 shadow-sm'
|
? 'bg-white text-blue-600 shadow-sm'
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
]"
|
]"
|
||||||
|
@click="setTrendGranularity('day')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-calendar-day mr-1"></i>按天
|
<i class="fas fa-calendar-day mr-1" />按天
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="setTrendGranularity('hour')"
|
|
||||||
:class="[
|
:class="[
|
||||||
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
|
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
|
||||||
trendGranularity === 'hour'
|
trendGranularity === 'hour'
|
||||||
? 'bg-white text-blue-600 shadow-sm'
|
? 'bg-white text-blue-600 shadow-sm'
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
]"
|
]"
|
||||||
|
@click="setTrendGranularity('hour')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-clock mr-1"></i>按小时
|
<i class="fas fa-clock mr-1" />按小时
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Element Plus 日期范围选择器 -->
|
<!-- Element Plus 日期范围选择器 -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
:default-time="defaultTime"
|
|
||||||
v-model="dateFilter.customRange"
|
v-model="dateFilter.customRange"
|
||||||
|
:default-time="defaultTime"
|
||||||
type="datetimerange"
|
type="datetimerange"
|
||||||
range-separator="至"
|
range-separator="至"
|
||||||
start-placeholder="开始日期"
|
start-placeholder="开始日期"
|
||||||
end-placeholder="结束日期"
|
end-placeholder="结束日期"
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
@change="onCustomDateRangeChange"
|
|
||||||
:disabled-date="disabledDate"
|
:disabled-date="disabledDate"
|
||||||
size="default"
|
size="default"
|
||||||
style="width: 400px;"
|
style="width: 400px;"
|
||||||
class="custom-date-picker"
|
class="custom-date-picker"
|
||||||
></el-date-picker>
|
@change="onCustomDateRangeChange"
|
||||||
<span v-if="trendGranularity === 'hour'" class="text-xs text-orange-600">
|
/>
|
||||||
<i class="fas fa-info-circle"></i> 最多24小时
|
<span
|
||||||
|
v-if="trendGranularity === 'hour'"
|
||||||
|
class="text-xs text-orange-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-info-circle" /> 最多24小时
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button @click="refreshChartsData()" class="btn btn-primary px-4 py-2 flex items-center gap-2">
|
<!-- 刷新控制 -->
|
||||||
<i class="fas fa-sync-alt"></i>刷新
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- 自动刷新控制 -->
|
||||||
|
<div class="flex items-center bg-gray-100 rounded-lg px-3 py-1">
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
v-model="autoRefreshEnabled"
|
||||||
|
type="checkbox"
|
||||||
|
class="sr-only peer"
|
||||||
|
>
|
||||||
|
<!-- 更小的开关 -->
|
||||||
|
<div class="relative w-9 h-5 bg-gray-300 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300 rounded-full peer peer-checked:bg-blue-500 transition-all duration-200 after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:w-4 after:h-4 after:rounded-full after:shadow-sm after:transition-transform after:duration-200 peer-checked:after:translate-x-4" />
|
||||||
|
<span class="ml-2.5 text-sm font-medium text-gray-600 select-none flex items-center gap-1">
|
||||||
|
<i class="fas fa-redo-alt text-xs text-gray-500" />
|
||||||
|
<span>自动刷新</span>
|
||||||
|
<span
|
||||||
|
v-if="autoRefreshEnabled"
|
||||||
|
class="ml-1 text-xs text-blue-600 font-mono transition-opacity"
|
||||||
|
:class="refreshCountdown > 0 ? 'opacity-100' : 'opacity-0'"
|
||||||
|
>
|
||||||
|
{{ refreshCountdown }}s
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 刷新按钮 -->
|
||||||
|
<button
|
||||||
|
:disabled="isRefreshing"
|
||||||
|
class="px-3 py-1 rounded-md text-sm font-medium transition-colors bg-white text-blue-600 shadow-sm hover:bg-gray-50 border border-gray-300 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="立即刷新数据"
|
||||||
|
@click="refreshAllData()"
|
||||||
|
>
|
||||||
|
<i :class="['fas fa-sync-alt text-xs', { 'animate-spin': isRefreshing }]" />
|
||||||
|
<span>{{ isRefreshing ? '刷新中' : '刷新' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<!-- 饼图 -->
|
<!-- 饼图 -->
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<h4 class="text-lg font-semibold text-gray-800 mb-4">Token使用分布</h4>
|
<h4 class="text-lg font-semibold text-gray-800 mb-4">
|
||||||
<div class="relative" style="height: 300px;">
|
Token使用分布
|
||||||
<canvas ref="modelUsageChart"></canvas>
|
</h4>
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
style="height: 300px;"
|
||||||
|
>
|
||||||
|
<canvas ref="modelUsageChart" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 详细数据表格 -->
|
<!-- 详细数据表格 -->
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<h4 class="text-lg font-semibold text-gray-800 mb-4">详细统计数据</h4>
|
<h4 class="text-lg font-semibold text-gray-800 mb-4">
|
||||||
<div v-if="dashboardModelStats.length === 0" class="text-center py-8">
|
详细统计数据
|
||||||
<p class="text-gray-500">暂无模型使用数据</p>
|
</h4>
|
||||||
|
<div
|
||||||
|
v-if="dashboardModelStats.length === 0"
|
||||||
|
class="text-center py-8"
|
||||||
|
>
|
||||||
|
<p class="text-gray-500">
|
||||||
|
暂无模型使用数据
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="overflow-auto max-h-[300px]">
|
<div
|
||||||
|
v-else
|
||||||
|
class="overflow-auto max-h-[300px]"
|
||||||
|
>
|
||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
<thead class="bg-gray-50 sticky top-0">
|
<thead class="bg-gray-50 sticky top-0">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-700">模型</th>
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-700">
|
||||||
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">请求数</th>
|
模型
|
||||||
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">总Token</th>
|
</th>
|
||||||
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">费用</th>
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">
|
||||||
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">占比</th>
|
请求数
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">
|
||||||
|
总Token
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">
|
||||||
|
费用
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-700">
|
||||||
|
占比
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200">
|
<tbody class="divide-y divide-gray-200">
|
||||||
<tr v-for="stat in dashboardModelStats" :key="stat.model" class="hover:bg-gray-50">
|
<tr
|
||||||
<td class="px-4 py-2 text-sm text-gray-900">{{ stat.model }}</td>
|
v-for="stat in dashboardModelStats"
|
||||||
<td class="px-4 py-2 text-sm text-gray-600 text-right">{{ formatNumber(stat.requests) }}</td>
|
:key="stat.model"
|
||||||
<td class="px-4 py-2 text-sm text-gray-600 text-right">{{ formatNumber(stat.allTokens) }}</td>
|
class="hover:bg-gray-50"
|
||||||
<td class="px-4 py-2 text-sm text-green-600 text-right font-medium">{{ stat.formatted ? stat.formatted.total : '$0.000000' }}</td>
|
>
|
||||||
|
<td class="px-4 py-2 text-sm text-gray-900">
|
||||||
|
{{ stat.model }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-gray-600 text-right">
|
||||||
|
{{ formatNumber(stat.requests) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-gray-600 text-right">
|
||||||
|
{{ formatNumber(stat.allTokens) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-green-600 text-right font-medium">
|
||||||
|
{{ stat.formatted ? stat.formatted.total : '$0.000000' }}
|
||||||
|
</td>
|
||||||
<td class="px-4 py-2 text-sm font-medium text-right">
|
<td class="px-4 py-2 text-sm font-medium text-right">
|
||||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||||
{{ calculatePercentage(stat.allTokens, dashboardModelStats) }}%
|
{{ calculatePercentage(stat.allTokens, dashboardModelStats) }}%
|
||||||
@@ -260,7 +407,7 @@
|
|||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<div style="height: 300px;">
|
<div style="height: 300px;">
|
||||||
<canvas ref="usageTrendChart"></canvas>
|
<canvas ref="usageTrendChart" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -269,30 +416,32 @@
|
|||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h3 class="text-lg font-semibold text-gray-900">API Keys 使用趋势</h3>
|
<h3 class="text-lg font-semibold text-gray-900">
|
||||||
|
API Keys 使用趋势
|
||||||
|
</h3>
|
||||||
<!-- 维度切换按钮 -->
|
<!-- 维度切换按钮 -->
|
||||||
<div class="flex gap-1 bg-gray-100 rounded-lg p-1">
|
<div class="flex gap-1 bg-gray-100 rounded-lg p-1">
|
||||||
<button
|
<button
|
||||||
@click="apiKeysTrendMetric = 'requests'; updateApiKeysUsageTrendChart()"
|
|
||||||
:class="[
|
:class="[
|
||||||
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
|
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
|
||||||
apiKeysTrendMetric === 'requests'
|
apiKeysTrendMetric === 'requests'
|
||||||
? 'bg-white text-blue-600 shadow-sm'
|
? 'bg-white text-blue-600 shadow-sm'
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
]"
|
]"
|
||||||
|
@click="apiKeysTrendMetric = 'requests'; updateApiKeysUsageTrendChart()"
|
||||||
>
|
>
|
||||||
<i class="fas fa-exchange-alt mr-1"></i>请求次数
|
<i class="fas fa-exchange-alt mr-1" />请求次数
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="apiKeysTrendMetric = 'tokens'; updateApiKeysUsageTrendChart()"
|
|
||||||
:class="[
|
:class="[
|
||||||
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
|
'px-3 py-1 rounded-md text-sm font-medium transition-colors',
|
||||||
apiKeysTrendMetric === 'tokens'
|
apiKeysTrendMetric === 'tokens'
|
||||||
? 'bg-white text-blue-600 shadow-sm'
|
? 'bg-white text-blue-600 shadow-sm'
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
]"
|
]"
|
||||||
|
@click="apiKeysTrendMetric = 'tokens'; updateApiKeysUsageTrendChart()"
|
||||||
>
|
>
|
||||||
<i class="fas fa-coins mr-1"></i>Token 数量
|
<i class="fas fa-coins mr-1" />Token 数量
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,7 +454,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 350px;">
|
<div style="height: 350px;">
|
||||||
<canvas ref="apiKeysUsageTrendChart"></canvas>
|
<canvas ref="apiKeysUsageTrendChart" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -313,7 +462,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, nextTick } from 'vue'
|
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useDashboardStore } from '@/stores/dashboard'
|
import { useDashboardStore } from '@/stores/dashboard'
|
||||||
import Chart from 'chart.js/auto'
|
import Chart from 'chart.js/auto'
|
||||||
@@ -334,8 +483,6 @@ const {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
loadDashboardData,
|
loadDashboardData,
|
||||||
loadUsageTrend,
|
|
||||||
loadModelStats,
|
|
||||||
loadApiKeysTrend,
|
loadApiKeysTrend,
|
||||||
setDateFilterPreset,
|
setDateFilterPreset,
|
||||||
onCustomDateRangeChange,
|
onCustomDateRangeChange,
|
||||||
@@ -352,6 +499,20 @@ let modelUsageChartInstance = null
|
|||||||
let usageTrendChartInstance = null
|
let usageTrendChartInstance = null
|
||||||
let apiKeysUsageTrendChartInstance = null
|
let apiKeysUsageTrendChartInstance = null
|
||||||
|
|
||||||
|
// 自动刷新相关
|
||||||
|
const autoRefreshEnabled = ref(false)
|
||||||
|
const autoRefreshInterval = ref(30) // 秒
|
||||||
|
const autoRefreshTimer = ref(null)
|
||||||
|
const refreshCountdown = ref(0)
|
||||||
|
const countdownTimer = ref(null)
|
||||||
|
const isRefreshing = ref(false)
|
||||||
|
|
||||||
|
// 计算倒计时显示
|
||||||
|
const refreshCountdownDisplay = computed(() => {
|
||||||
|
if (!autoRefreshEnabled.value || refreshCountdown.value <= 0) return ''
|
||||||
|
return `${refreshCountdown.value}秒后刷新`
|
||||||
|
})
|
||||||
|
|
||||||
// 格式化数字
|
// 格式化数字
|
||||||
function formatNumber(num) {
|
function formatNumber(num) {
|
||||||
if (num >= 1000000) {
|
if (num >= 1000000) {
|
||||||
@@ -543,6 +704,22 @@ function createUsageTrendChart() {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
mode: 'index',
|
mode: 'index',
|
||||||
intersect: false,
|
intersect: false,
|
||||||
|
itemSort: function(a, b) {
|
||||||
|
// 按值倒序排列,费用和请求数特殊处理
|
||||||
|
const aLabel = a.dataset.label || ''
|
||||||
|
const bLabel = b.dataset.label || ''
|
||||||
|
|
||||||
|
// 费用和请求数使用不同的轴,单独处理
|
||||||
|
if (aLabel === '费用 (USD)' || bLabel === '费用 (USD)') {
|
||||||
|
return aLabel === '费用 (USD)' ? -1 : 1
|
||||||
|
}
|
||||||
|
if (aLabel === '请求数' || bLabel === '请求数') {
|
||||||
|
return aLabel === '请求数' ? 1 : -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他按token值倒序
|
||||||
|
return b.parsed.y - a.parsed.y
|
||||||
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: function(context) {
|
label: function(context) {
|
||||||
const label = context.dataset.label || ''
|
const label = context.dataset.label || ''
|
||||||
@@ -557,12 +734,19 @@ function createUsageTrendChart() {
|
|||||||
}
|
}
|
||||||
} else if (label === '请求数') {
|
} else if (label === '请求数') {
|
||||||
return label + ': ' + value.toLocaleString() + ' 次'
|
return label + ': ' + value.toLocaleString() + ' 次'
|
||||||
|
} else {
|
||||||
|
// 格式化token数显示
|
||||||
|
if (value >= 1000000) {
|
||||||
|
return label + ': ' + (value / 1000000).toFixed(2) + 'M tokens'
|
||||||
|
} else if (value >= 1000) {
|
||||||
|
return label + ': ' + (value / 1000).toFixed(2) + 'K tokens'
|
||||||
} else {
|
} else {
|
||||||
return label + ': ' + value.toLocaleString() + ' tokens'
|
return label + ': ' + value.toLocaleString() + ' tokens'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
@@ -706,12 +890,52 @@ function createApiKeysUsageTrendChart() {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
mode: 'index',
|
mode: 'index',
|
||||||
intersect: false,
|
intersect: false,
|
||||||
|
itemSort: function(a, b) {
|
||||||
|
// 按值倒序排列
|
||||||
|
return b.parsed.y - a.parsed.y
|
||||||
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: function(context) {
|
label: function(context) {
|
||||||
const label = context.dataset.label || ''
|
const label = context.dataset.label || ''
|
||||||
const value = context.parsed.y
|
const value = context.parsed.y
|
||||||
const unit = apiKeysTrendMetric.value === 'tokens' ? ' tokens' : ' 次'
|
const dataIndex = context.dataIndex
|
||||||
return label + ': ' + value.toLocaleString() + unit
|
const dataPoint = apiKeysTrendData.value.data[dataIndex]
|
||||||
|
|
||||||
|
// 获取所有数据集在这个时间点的值,用于排名
|
||||||
|
const allValues = context.chart.data.datasets.map((dataset, idx) => ({
|
||||||
|
value: dataset.data[dataIndex] || 0,
|
||||||
|
index: idx
|
||||||
|
})).sort((a, b) => b.value - a.value)
|
||||||
|
|
||||||
|
// 找出当前数据集的排名
|
||||||
|
const rank = allValues.findIndex(item => item.index === context.datasetIndex) + 1
|
||||||
|
|
||||||
|
// 准备排名标识
|
||||||
|
let rankIcon = ''
|
||||||
|
if (rank === 1) rankIcon = '🥇 '
|
||||||
|
else if (rank === 2) rankIcon = '🥈 '
|
||||||
|
else if (rank === 3) rankIcon = '🥉 '
|
||||||
|
|
||||||
|
if (apiKeysTrendMetric.value === 'tokens') {
|
||||||
|
// 格式化token显示
|
||||||
|
let formattedValue = ''
|
||||||
|
if (value >= 1000000) {
|
||||||
|
formattedValue = (value / 1000000).toFixed(2) + 'M'
|
||||||
|
} else if (value >= 1000) {
|
||||||
|
formattedValue = (value / 1000).toFixed(2) + 'K'
|
||||||
|
} else {
|
||||||
|
formattedValue = value.toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取对应API Key的费用信息
|
||||||
|
const apiKeyId = apiKeysTrendData.value.topApiKeys[context.datasetIndex]
|
||||||
|
const apiKeyData = dataPoint?.apiKeys?.[apiKeyId]
|
||||||
|
const cost = apiKeyData?.formattedCost || '$0.00'
|
||||||
|
|
||||||
|
return `${rankIcon}${label}: ${formattedValue} tokens (${cost})`
|
||||||
|
} else {
|
||||||
|
return `${rankIcon}${label}: ${value.toLocaleString()} 次`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -762,13 +986,90 @@ watch(apiKeysTrendData, () => {
|
|||||||
nextTick(() => createApiKeysUsageTrendChart())
|
nextTick(() => createApiKeysUsageTrendChart())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 刷新所有数据
|
||||||
|
async function refreshAllData() {
|
||||||
|
if (isRefreshing.value) return
|
||||||
|
|
||||||
|
isRefreshing.value = true
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
loadDashboardData(),
|
||||||
|
refreshChartsData()
|
||||||
|
])
|
||||||
|
} finally {
|
||||||
|
isRefreshing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动自动刷新
|
||||||
|
function startAutoRefresh() {
|
||||||
|
if (!autoRefreshEnabled.value) return
|
||||||
|
|
||||||
|
// 重置倒计时
|
||||||
|
refreshCountdown.value = autoRefreshInterval.value
|
||||||
|
|
||||||
|
// 清除现有定时器
|
||||||
|
if (countdownTimer.value) {
|
||||||
|
clearInterval(countdownTimer.value)
|
||||||
|
}
|
||||||
|
if (autoRefreshTimer.value) {
|
||||||
|
clearTimeout(autoRefreshTimer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动倒计时
|
||||||
|
countdownTimer.value = setInterval(() => {
|
||||||
|
refreshCountdown.value--
|
||||||
|
if (refreshCountdown.value <= 0) {
|
||||||
|
clearInterval(countdownTimer.value)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
// 设置刷新定时器
|
||||||
|
autoRefreshTimer.value = setTimeout(async () => {
|
||||||
|
await refreshAllData()
|
||||||
|
// 递归调用以继续自动刷新
|
||||||
|
if (autoRefreshEnabled.value) {
|
||||||
|
startAutoRefresh()
|
||||||
|
}
|
||||||
|
}, autoRefreshInterval.value * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止自动刷新
|
||||||
|
function stopAutoRefresh() {
|
||||||
|
if (countdownTimer.value) {
|
||||||
|
clearInterval(countdownTimer.value)
|
||||||
|
countdownTimer.value = null
|
||||||
|
}
|
||||||
|
if (autoRefreshTimer.value) {
|
||||||
|
clearTimeout(autoRefreshTimer.value)
|
||||||
|
autoRefreshTimer.value = null
|
||||||
|
}
|
||||||
|
refreshCountdown.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换自动刷新
|
||||||
|
function toggleAutoRefresh() {
|
||||||
|
autoRefreshEnabled.value = !autoRefreshEnabled.value
|
||||||
|
if (autoRefreshEnabled.value) {
|
||||||
|
startAutoRefresh()
|
||||||
|
} else {
|
||||||
|
stopAutoRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听自动刷新状态变化
|
||||||
|
watch(autoRefreshEnabled, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
startAutoRefresh()
|
||||||
|
} else {
|
||||||
|
stopAutoRefresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 加载所有数据
|
// 加载所有数据
|
||||||
await Promise.all([
|
await refreshAllData()
|
||||||
loadDashboardData(),
|
|
||||||
refreshChartsData() // 使用refreshChartsData来确保根据当前筛选条件加载数据
|
|
||||||
])
|
|
||||||
|
|
||||||
// 创建图表
|
// 创建图表
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@@ -776,6 +1077,21 @@ onMounted(async () => {
|
|||||||
createUsageTrendChart()
|
createUsageTrendChart()
|
||||||
createApiKeysUsageTrendChart()
|
createApiKeysUsageTrendChart()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAutoRefresh()
|
||||||
|
// 销毁图表实例
|
||||||
|
if (modelUsageChartInstance) {
|
||||||
|
modelUsageChartInstance.destroy()
|
||||||
|
}
|
||||||
|
if (usageTrendChartInstance) {
|
||||||
|
usageTrendChartInstance.destroy()
|
||||||
|
}
|
||||||
|
if (apiKeysUsageTrendChartInstance) {
|
||||||
|
apiKeysUsageTrendChartInstance.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -794,4 +1110,18 @@ onMounted(async () => {
|
|||||||
.custom-date-picker :deep(.el-range-input) {
|
.custom-date-picker :deep(.el-range-input) {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 旋转动画 */
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -5,23 +5,41 @@
|
|||||||
<!-- 使用自定义布局来保持登录页面的居中大logo样式 -->
|
<!-- 使用自定义布局来保持登录页面的居中大logo样式 -->
|
||||||
<div class="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-2xl flex items-center justify-center backdrop-blur-sm overflow-hidden">
|
<div class="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-2xl flex items-center justify-center backdrop-blur-sm overflow-hidden">
|
||||||
<template v-if="!oemLoading">
|
<template v-if="!oemLoading">
|
||||||
<img v-if="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon"
|
<img
|
||||||
|
v-if="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon"
|
||||||
:src="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon"
|
:src="authStore.oemSettings.siteIconData || authStore.oemSettings.siteIcon"
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
class="w-12 h-12 object-contain"
|
class="w-12 h-12 object-contain"
|
||||||
@error="(e) => e.target.style.display = 'none'">
|
@error="(e) => e.target.style.display = 'none'"
|
||||||
<i v-else class="fas fa-cloud text-3xl text-gray-700"></i>
|
>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-cloud text-3xl text-gray-700"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="w-12 h-12 bg-gray-300/50 rounded animate-pulse"></div>
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-12 h-12 bg-gray-300/50 rounded animate-pulse"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!oemLoading && authStore.oemSettings.siteName">
|
<template v-if="!oemLoading && authStore.oemSettings.siteName">
|
||||||
<h1 class="text-3xl font-bold text-white mb-2 header-title">{{ authStore.oemSettings.siteName }}</h1>
|
<h1 class="text-3xl font-bold text-white mb-2 header-title">
|
||||||
|
{{ authStore.oemSettings.siteName }}
|
||||||
|
</h1>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="oemLoading" class="h-9 w-64 bg-gray-300/50 rounded animate-pulse mx-auto mb-2"></div>
|
<div
|
||||||
<p class="text-gray-600 text-lg">管理后台</p>
|
v-else-if="oemLoading"
|
||||||
|
class="h-9 w-64 bg-gray-300/50 rounded animate-pulse mx-auto mb-2"
|
||||||
|
/>
|
||||||
|
<p class="text-gray-600 text-lg">
|
||||||
|
管理后台
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="handleLogin" class="space-y-6">
|
<form
|
||||||
|
class="space-y-6"
|
||||||
|
@submit.prevent="handleLogin"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-900 mb-3">用户名</label>
|
<label class="block text-sm font-semibold text-gray-900 mb-3">用户名</label>
|
||||||
<input
|
<input
|
||||||
@@ -49,14 +67,23 @@
|
|||||||
:disabled="authStore.loginLoading"
|
:disabled="authStore.loginLoading"
|
||||||
class="btn btn-primary w-full py-4 px-6 text-lg font-semibold"
|
class="btn btn-primary w-full py-4 px-6 text-lg font-semibold"
|
||||||
>
|
>
|
||||||
<i v-if="!authStore.loginLoading" class="fas fa-sign-in-alt mr-2"></i>
|
<i
|
||||||
<div v-if="authStore.loginLoading" class="loading-spinner mr-2"></div>
|
v-if="!authStore.loginLoading"
|
||||||
|
class="fas fa-sign-in-alt mr-2"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="authStore.loginLoading"
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
{{ authStore.loginLoading ? '登录中...' : '登录' }}
|
{{ authStore.loginLoading ? '登录中...' : '登录' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div v-if="authStore.loginError" class="mt-6 p-4 bg-red-500/20 border border-red-500/30 rounded-xl text-red-800 text-sm text-center backdrop-blur-sm">
|
<div
|
||||||
<i class="fas fa-exclamation-triangle mr-2"></i>{{ authStore.loginError }}
|
v-if="authStore.loginError"
|
||||||
|
class="mt-6 p-4 bg-red-500/20 border border-red-500/30 rounded-xl text-red-800 text-sm text-center backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2" />{{ authStore.loginError }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,17 +3,29 @@
|
|||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
|
<div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-gray-900 mb-2">其他设置</h3>
|
<h3 class="text-xl font-bold text-gray-900 mb-2">
|
||||||
<p class="text-gray-600">自定义网站名称和图标</p>
|
其他设置
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
自定义网站名称和图标
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loading" class="text-center py-12">
|
<div
|
||||||
<div class="loading-spinner mx-auto mb-4"></div>
|
v-if="loading"
|
||||||
<p class="text-gray-500">正在加载设置...</p>
|
class="text-center py-12"
|
||||||
|
>
|
||||||
|
<div class="loading-spinner mx-auto mb-4" />
|
||||||
|
<p class="text-gray-500">
|
||||||
|
正在加载设置...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="table-container">
|
<div
|
||||||
|
v-else
|
||||||
|
class="table-container"
|
||||||
|
>
|
||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
<tbody class="divide-y divide-gray-200/50">
|
<tbody class="divide-y divide-gray-200/50">
|
||||||
<!-- 网站名称 -->
|
<!-- 网站名称 -->
|
||||||
@@ -21,11 +33,15 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap w-48">
|
<td class="px-6 py-4 whitespace-nowrap w-48">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3">
|
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3">
|
||||||
<i class="fas fa-font text-white text-xs"></i>
|
<i class="fas fa-font text-white text-xs" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-semibold text-gray-900">网站名称</div>
|
<div class="text-sm font-semibold text-gray-900">
|
||||||
<div class="text-xs text-gray-500">品牌标识</div>
|
网站名称
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
品牌标识
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -37,7 +53,9 @@
|
|||||||
placeholder="Claude Relay Service"
|
placeholder="Claude Relay Service"
|
||||||
maxlength="100"
|
maxlength="100"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-1">将显示在浏览器标题和页面头部</p>
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
将显示在浏览器标题和页面头部
|
||||||
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -46,18 +64,25 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap w-48">
|
<td class="px-6 py-4 whitespace-nowrap w-48">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-8 h-8 bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg flex items-center justify-center mr-3">
|
<div class="w-8 h-8 bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg flex items-center justify-center mr-3">
|
||||||
<i class="fas fa-image text-white text-xs"></i>
|
<i class="fas fa-image text-white text-xs" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-semibold text-gray-900">网站图标</div>
|
<div class="text-sm font-semibold text-gray-900">
|
||||||
<div class="text-xs text-gray-500">Favicon</div>
|
网站图标
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
Favicon
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<!-- 图标预览 -->
|
<!-- 图标预览 -->
|
||||||
<div v-if="oemSettings.siteIconData || oemSettings.siteIcon" class="inline-flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
|
<div
|
||||||
|
v-if="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||||
|
class="inline-flex items-center gap-3 p-3 bg-gray-50 rounded-lg"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
:src="oemSettings.siteIconData || oemSettings.siteIcon"
|
:src="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||||
alt="图标预览"
|
alt="图标预览"
|
||||||
@@ -66,27 +91,27 @@
|
|||||||
>
|
>
|
||||||
<span class="text-sm text-gray-600">当前图标</span>
|
<span class="text-sm text-gray-600">当前图标</span>
|
||||||
<button
|
<button
|
||||||
@click="removeIcon"
|
|
||||||
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
|
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
|
||||||
|
@click="removeIcon"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash mr-1"></i>删除
|
<i class="fas fa-trash mr-1" />删除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文件上传 -->
|
<!-- 文件上传 -->
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="file"
|
|
||||||
ref="iconFileInput"
|
ref="iconFileInput"
|
||||||
@change="handleIconUpload"
|
type="file"
|
||||||
accept=".ico,.png,.jpg,.jpeg,.svg"
|
accept=".ico,.png,.jpg,.jpeg,.svg"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
|
@change="handleIconUpload"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@click="$refs.iconFileInput.click()"
|
|
||||||
class="btn btn-success px-4 py-2"
|
class="btn btn-success px-4 py-2"
|
||||||
|
@click="$refs.iconFileInput.click()"
|
||||||
>
|
>
|
||||||
<i class="fas fa-upload mr-2"></i>
|
<i class="fas fa-upload mr-2" />
|
||||||
上传图标
|
上传图标
|
||||||
</button>
|
</button>
|
||||||
<span class="text-xs text-gray-500 ml-3">支持 .ico, .png, .jpg, .svg 格式,最大 350KB</span>
|
<span class="text-xs text-gray-500 ml-3">支持 .ico, .png, .jpg, .svg 格式,最大 350KB</span>
|
||||||
@@ -97,32 +122,44 @@
|
|||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-6 py-6" colspan="2">
|
<td
|
||||||
|
class="px-6 py-6"
|
||||||
|
colspan="2"
|
||||||
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button
|
<button
|
||||||
@click="saveOemSettings"
|
|
||||||
:disabled="saving"
|
:disabled="saving"
|
||||||
class="btn btn-primary px-6 py-3"
|
class="btn btn-primary px-6 py-3"
|
||||||
:class="{ 'opacity-50 cursor-not-allowed': saving }"
|
:class="{ 'opacity-50 cursor-not-allowed': saving }"
|
||||||
|
@click="saveOemSettings"
|
||||||
>
|
>
|
||||||
<div v-if="saving" class="loading-spinner mr-2"></div>
|
<div
|
||||||
<i v-else class="fas fa-save mr-2"></i>
|
v-if="saving"
|
||||||
|
class="loading-spinner mr-2"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="fas fa-save mr-2"
|
||||||
|
/>
|
||||||
{{ saving ? '保存中...' : '保存设置' }}
|
{{ saving ? '保存中...' : '保存设置' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="resetOemSettings"
|
|
||||||
class="btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3"
|
class="btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3"
|
||||||
:disabled="saving"
|
:disabled="saving"
|
||||||
|
@click="resetOemSettings"
|
||||||
>
|
>
|
||||||
<i class="fas fa-undo mr-2"></i>
|
<i class="fas fa-undo mr-2" />
|
||||||
重置为默认
|
重置为默认
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="oemSettings.updatedAt" class="text-sm text-gray-500">
|
<div
|
||||||
<i class="fas fa-clock mr-1"></i>
|
v-if="oemSettings.updatedAt"
|
||||||
|
class="text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
<i class="fas fa-clock mr-1" />
|
||||||
最后更新:{{ formatDateTime(oemSettings.updatedAt) }}
|
最后更新:{{ formatDateTime(oemSettings.updatedAt) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user