diff --git a/src/api.js b/src/api.js index 4dcbb5a..375523f 100644 --- a/src/api.js +++ b/src/api.js @@ -47,6 +47,7 @@ export const getSiteInfo = () => api.get('/api/dist/site/info'); export const getSiteModels = () => api.get('/api/dist/site/models'); export const getSitePricing = () => api.get('/api/dist/site/pricing'); export const getSitePackages = () => api.get('/api/dist/site/packages'); +export const getSiteKeyGroups = () => api.get('/api/dist/site/key-groups'); // ===== Auth ===== export const register = (data) => api.post('/api/dist/user/register', data); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a38ecb9..f8217b0 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -119,6 +119,17 @@ "tokens.tokenEnabled": "Token enabled", "tokens.tokenDeleted": "Token deleted", "tokens.enterName": "Please enter a name", + "tokens.selectGroup": "Create API Key", + "tokens.selectGroupSubtitle": "Choose a model group and rate, click a card to create", + "tokens.defaultGroup": "All Providers (Default)", + "tokens.defaultGroupDesc": "No provider restriction, uses all available channels", + "tokens.otherGroups": "Other", + "tokens.groupCount": "groups", + "tokens.recommended": "Recommended", + "tokens.unavailable": "Unavailable", + "tokens.rmbPerUsd": "RMB / $1", + "tokens.myKeys": "My API Keys", + "tokens.selectedGroup": "Selected Group", "tokens.quickStart": "Quick Start", "tokens.quickStartComment": "# Use your API key with OpenAI-compatible endpoint", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 364aa5e..e112515 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -119,6 +119,17 @@ "tokens.tokenEnabled": "令牌已启用", "tokens.tokenDeleted": "令牌已删除", "tokens.enterName": "请输入名称", + "tokens.selectGroup": "新建 API Key", + "tokens.selectGroupSubtitle": "选择模型与费率,点击卡片即可快速创建", + "tokens.defaultGroup": "全站调用(默认)", + "tokens.defaultGroupDesc": "不限制供应商,使用所有可用渠道", + "tokens.otherGroups": "其他", + "tokens.groupCount": "个分组", + "tokens.recommended": "推荐", + "tokens.unavailable": "暂不可用", + "tokens.rmbPerUsd": "块 / 1 美元", + "tokens.myKeys": "我的 API Keys", + "tokens.selectedGroup": "已选分组", "tokens.quickStart": "快速开始", "tokens.quickStartComment": "# 使用 OpenAI 兼容端点调用 API", diff --git a/src/pages/Tokens.jsx b/src/pages/Tokens.jsx index 283cbc2..9e11b9e 100644 --- a/src/pages/Tokens.jsx +++ b/src/pages/Tokens.jsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { getTokens, createToken, updateToken, deleteToken } from '../api'; +import { getTokens, createToken, updateToken, deleteToken, getSiteKeyGroups } from '../api'; import ConfigExporter from '../components/ConfigExporter'; import toast from 'react-hot-toast'; @@ -8,25 +8,58 @@ export default function Tokens() { const { t } = useTranslation(); const [tokens, setTokens] = useState([]); const [loading, setLoading] = useState(true); - const [showCreate, setShowCreate] = useState(false); - const [createName, setCreateName] = useState(''); - const [creating, setCreating] = useState(false); const [copiedId, setCopiedId] = useState(null); - // Newly created key — shown only once const [newKey, setNewKey] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState(null); + // Key groups + const [keyGroups, setKeyGroups] = useState([]); + + // Create modal + const [showCreate, setShowCreate] = useState(false); + const [createName, setCreateName] = useState(''); + const [selectedGroupId, setSelectedGroupId] = useState(0); + const [creating, setCreating] = useState(false); + const load = useCallback(async () => { setLoading(true); try { - const res = await getTokens(); - if (res.data.success) setTokens(res.data.data || []); + const [tokensRes, groupsRes] = await Promise.all([ + getTokens(), + getSiteKeyGroups().catch(() => ({ data: { success: false } })), + ]); + if (tokensRes.data.success) setTokens(tokensRes.data.data || []); + if (groupsRes.data.success) setKeyGroups(groupsRes.data.data || []); } catch (e) { /* interceptor */ } setLoading(false); }, []); useEffect(() => { load(); }, [load]); + // Group by vendor_category + const groupedByVendor = useMemo(() => { + const map = {}; + keyGroups.forEach((g) => { + const cat = g.vendor_category || t('tokens.otherGroups'); + if (!map[cat]) map[cat] = []; + map[cat].push(g); + }); + return map; + }, [keyGroups, t]); + + const openCreateFromGroup = (group) => { + if (group.is_unavailable) return; + setSelectedGroupId(group.id); + setCreateName(group.name); + setShowCreate(true); + }; + + const openCreateDefault = () => { + setSelectedGroupId(0); + setCreateName(''); + setShowCreate(true); + }; + const handleCreate = async (e) => { e.preventDefault(); if (!createName.trim()) { @@ -35,15 +68,15 @@ export default function Tokens() { } setCreating(true); try { - const res = await createToken({ name: createName.trim() }); + const payload = { name: createName.trim() }; + if (selectedGroupId > 0) payload.key_group_id = selectedGroupId; + const res = await createToken(payload); if (res.data.success) { setCreateName(''); setShowCreate(false); - // Show the key — it's only visible at creation time! + setSelectedGroupId(0); const createdKey = res.data.data?.key; - if (createdKey) { - setNewKey(createdKey); - } + if (createdKey) setNewKey(createdKey); await load(); } } catch (e) { /* interceptor */ } @@ -92,6 +125,10 @@ export default function Tokens() { setTimeout(() => setCopiedId(null), 2000); }; + const parseTags = (tagsStr) => { + try { return JSON.parse(tagsStr || '[]'); } catch { return []; } + }; + if (loading) { return (
{t('tokens.subtitle')}
+ + {/* ========== Section 1: Create Key with Groups ========== */} +{t('tokens.selectGroupSubtitle')}
+ )} + + {/* Default (All Providers) Card */} +{t('tokens.selectedGroup')}
+{g.name}
+