Files
users/app/data-market/api/page.tsx
卡若 7c72871a7a chore: 同步本地到 main 和 Gitea
Made-with: Cursor
2026-03-16 14:48:26 +08:00

1618 lines
66 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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

"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Checkbox } from "@/components/ui/checkbox"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Server,
Copy,
CheckCircle2,
Key,
RefreshCw,
ExternalLink,
Code,
Zap,
Users,
Search,
Tags,
Brain,
Database,
FileText,
Package,
Activity,
Plus,
Trash2,
Eye,
EyeOff,
Settings,
Shield,
CreditCard,
BarChart3,
Clock,
DollarSign,
AlertCircle,
CheckCircle,
XCircle,
ChevronDown,
ChevronRight,
Lock,
Unlock,
Download,
Filter,
} from "lucide-react"
import Link from "next/link"
// ==================== 类型定义 ====================
// API密钥接口
interface APIKey {
id: string
name: string
key: string
secret: string
status: 'active' | 'disabled' | 'expired'
createdAt: string
expiresAt: string | null
lastUsed: string | null
permissions: FieldPermission[]
rateLimit: {
requestsPerDay: number
requestsPerMonth: number
}
billing: {
plan: 'free' | 'basic' | 'pro' | 'enterprise'
usedCredits: number
totalCredits: number
}
callStats: {
today: number
thisMonth: number
total: number
}
}
// 字段权限接口
interface FieldPermission {
fieldGroup: string
fields: {
name: string
label: string
enabled: boolean
price: number // 每次调用价格(信用点)
}[]
}
// API端点接口
interface APIEndpoint {
id: string
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
path: string
name: string
description: string
category: string
auth: boolean
price: number // 每次调用价格(信用点)
params?: { name: string; type: string; required: boolean; desc: string }[]
response?: string
example?: string
}
// 计费套餐
interface BillingPlan {
id: 'free' | 'basic' | 'pro' | 'enterprise'
name: string
price: number
credits: number
features: string[]
rateLimit: {
requestsPerDay: number
requestsPerMonth: number
}
}
// 调用日志
interface CallLog {
id: string
keyId: string
keyName: string
endpoint: string
method: string
status: number
credits: number
responseTime: number
timestamp: string
ip: string
}
// ==================== 常量数据 ====================
// 计费套餐
const BILLING_PLANS: BillingPlan[] = [
{
id: 'free',
name: '免费版',
price: 0,
credits: 1000,
features: ['每日100次调用', '基础字段访问', '标准响应速度'],
rateLimit: { requestsPerDay: 100, requestsPerMonth: 3000 }
},
{
id: 'basic',
name: '基础版',
price: 99,
credits: 10000,
features: ['每日1000次调用', '全部字段访问', '优先响应'],
rateLimit: { requestsPerDay: 1000, requestsPerMonth: 30000 }
},
{
id: 'pro',
name: '专业版',
price: 299,
credits: 50000,
features: ['每日5000次调用', '全部字段+AI分析', '实时响应', '专属支持'],
rateLimit: { requestsPerDay: 5000, requestsPerMonth: 150000 }
},
{
id: 'enterprise',
name: '企业版',
price: 999,
credits: -1, // 无限
features: ['无限调用', '全部功能', '定制开发', '7x24支持'],
rateLimit: { requestsPerDay: -1, requestsPerMonth: -1 }
},
]
// 字段分组及权限
const FIELD_GROUPS: FieldPermission[] = [
{
fieldGroup: '基础信息',
fields: [
{ name: 'phone', label: '手机号', enabled: true, price: 1 },
{ name: 'qq', label: 'QQ号', enabled: true, price: 1 },
{ name: 'wechat', label: '微信号', enabled: false, price: 2 },
{ name: 'email', label: '邮箱', enabled: false, price: 1 },
{ name: 'nickname', label: '昵称', enabled: true, price: 0.5 },
]
},
{
fieldGroup: '用户画像',
fields: [
{ name: 'rfm_score', label: 'RFM评分', enabled: true, price: 2 },
{ name: 'user_level', label: '用户等级', enabled: true, price: 1 },
{ name: 'value_score', label: '价值评分', enabled: false, price: 3 },
{ name: 'activity_score', label: '活跃度评分', enabled: false, price: 2 },
{ name: 'loyalty_score', label: '忠诚度评分', enabled: false, price: 2 },
]
},
{
fieldGroup: '标签数据',
fields: [
{ name: 'basic_tags', label: '基础标签', enabled: true, price: 1 },
{ name: 'behavior_tags', label: '行为标签', enabled: false, price: 2 },
{ name: 'preference_tags', label: '偏好标签', enabled: false, price: 2 },
{ name: 'ai_tags', label: 'AI智能标签', enabled: false, price: 5 },
{ name: 'custom_tags', label: '自定义标签', enabled: true, price: 1 },
]
},
{
fieldGroup: '行为数据',
fields: [
{ name: 'last_active', label: '最后活跃时间', enabled: true, price: 0.5 },
{ name: 'visit_count', label: '访问次数', enabled: false, price: 1 },
{ name: 'purchase_history', label: '购买历史', enabled: false, price: 5 },
{ name: 'interaction_log', label: '交互记录', enabled: false, price: 3 },
{ name: 'channel_source', label: '渠道来源', enabled: true, price: 1 },
]
},
{
fieldGroup: '扩展数据',
fields: [
{ name: 'social_bindigns', label: '社交绑定', enabled: false, price: 3 },
{ name: 'device_info', label: '设备信息', enabled: false, price: 2 },
{ name: 'location_data', label: '位置数据', enabled: false, price: 4 },
{ name: 'risk_assessment', label: '风险评估', enabled: false, price: 5 },
{ name: 'ai_insights', label: 'AI洞察', enabled: false, price: 10 },
]
},
]
// API分类
const API_CATEGORIES = [
{ id: 'query', name: '用户查询', icon: Search },
{ id: 'tag', name: '标签服务', icon: Tags },
{ id: 'ai', name: 'AI服务', icon: Brain },
{ id: 'data', name: '数据服务', icon: Database },
{ id: 'report', name: '报告服务', icon: FileText },
{ id: 'package', name: '流量包', icon: Package },
]
// 预定义API端点
const API_ENDPOINTS: APIEndpoint[] = [
// 用户查询
{
id: 'api_1',
method: 'GET',
path: '/api/shensheshou/user',
name: '用户画像查询',
description: '根据手机号或QQ查询完整用户画像返回字段根据API密钥权限决定',
category: 'query',
auth: true,
price: 1,
params: [
{ name: 'phone', type: 'string', required: false, desc: '11位手机号' },
{ name: 'qq', type: 'string', required: false, desc: 'QQ号码' },
{ name: 'fields', type: 'string', required: false, desc: '指定返回字段,逗号分隔' },
],
response: `{
"success": true,
"data": {
"phone": "138****8000",
"rfm_score": 85,
"user_level": "A",
"tags": ["高价值", "活跃用户"],
"last_active": "2026-01-30"
},
"credits_used": 5,
"credits_remaining": 995
}`,
example: `curl -X GET "https://api.shensheshou.com/api/shensheshou/user?phone=13800138000&fields=rfm_score,tags" \\
-H "Authorization: Bearer sk-archer-xxxxx" \\
-H "X-API-Secret: sec-xxxxx"`,
},
{
id: 'api_2',
method: 'POST',
path: '/api/shensheshou/users/batch',
name: '批量用户查询',
description: '批量查询多个用户的画像信息最多支持100个用户/次',
category: 'query',
auth: true,
price: 0.8, // 批量折扣
params: [
{ name: 'phones', type: 'array', required: false, desc: '手机号数组最多100个' },
{ name: 'qqs', type: 'array', required: false, desc: 'QQ号数组最多100个' },
{ name: 'fields', type: 'array', required: false, desc: '指定返回字段数组' },
],
response: `{
"success": true,
"data": [
{ "phone": "138****8000", "rfm_score": 85, "user_level": "A" },
{ "phone": "139****9000", "rfm_score": 72, "user_level": "B" }
],
"total": 2,
"credits_used": 8,
"credits_remaining": 992
}`,
},
// 标签服务
{
id: 'api_3',
method: 'GET',
path: '/api/shensheshou/tags',
name: '标签列表',
description: '获取系统中所有可用标签及其分类',
category: 'tag',
auth: true,
price: 0.5,
params: [
{ name: 'category', type: 'string', required: false, desc: '标签分类筛选' },
],
},
{
id: 'api_4',
method: 'POST',
path: '/api/shensheshou/tags/apply',
name: '应用标签',
description: '为指定用户批量应用标签',
category: 'tag',
auth: true,
price: 2,
params: [
{ name: 'user_id', type: 'string', required: true, desc: '用户ID或手机号' },
{ name: 'tags', type: 'array', required: true, desc: '要应用的标签ID数组' },
{ name: 'overwrite', type: 'boolean', required: false, desc: '是否覆盖现有标签' },
],
},
// AI服务
{
id: 'api_5',
method: 'POST',
path: '/api/shensheshou/ai/chat',
name: 'AI智能对话',
description: '与神射手AI进行对话支持自然语言查询和数据分析',
category: 'ai',
auth: true,
price: 5,
params: [
{ name: 'message', type: 'string', required: true, desc: '对话内容' },
{ name: 'context', type: 'object', required: false, desc: '上下文信息' },
{ name: 'model', type: 'string', required: false, desc: 'AI模型: qwen/deepseek' },
],
response: `{
"success": true,
"response": {
"content": "根据查询,共有 1,234 位高价值用户...",
"data": { "count": 1234, "avg_rfm": 82 },
"suggestions": ["可以进一步筛选活跃度", "建议导出为流量包"]
},
"credits_used": 5
}`,
},
{
id: 'api_6',
method: 'POST',
path: '/api/shensheshou/ai/analyze',
name: 'AI数据分析',
description: 'AI自动分析用户群体特征并生成洞察报告',
category: 'ai',
auth: true,
price: 10,
params: [
{ name: 'type', type: 'string', required: true, desc: '分析类型: rfm/behavior/preference/churn' },
{ name: 'filters', type: 'object', required: false, desc: '用户筛选条件' },
{ name: 'depth', type: 'string', required: false, desc: '分析深度: quick/standard/deep' },
],
},
{
id: 'api_7',
method: 'POST',
path: '/api/shensheshou/ai/tag',
name: 'AI智能打标',
description: 'AI自动分析用户数据并智能打标签',
category: 'ai',
auth: true,
price: 8,
params: [
{ name: 'user_ids', type: 'array', required: true, desc: '用户ID数组' },
{ name: 'tag_types', type: 'array', required: false, desc: '指定标签类型' },
{ name: 'model', type: 'string', required: false, desc: 'AI模型选择' },
],
},
// 数据服务
{
id: 'api_8',
method: 'GET',
path: '/api/shensheshou/sources',
name: '数据源列表',
description: '获取所有已接入的数据源及其状态',
category: 'data',
auth: true,
price: 0.5,
},
{
id: 'api_9',
method: 'POST',
path: '/api/shensheshou/ingest',
name: '数据导入',
description: '导入外部数据到神射手平台自动触发AI标签引擎处理',
category: 'data',
auth: true,
price: 3,
params: [
{ name: 'source', type: 'string', required: true, desc: '数据源标识' },
{ name: 'data', type: 'array', required: true, desc: '用户数据数组' },
{ name: 'auto_tag', type: 'boolean', required: false, desc: '是否自动打标' },
],
},
// 报告服务
{
id: 'api_10',
method: 'POST',
path: '/api/shensheshou/report/generate',
name: '生成分析报告',
description: 'AI自动生成数据分析报告支持多种模板',
category: 'report',
auth: true,
price: 15,
params: [
{ name: 'template', type: 'string', required: true, desc: '报告模板: user_insight/rfm_analysis/trend_report' },
{ name: 'date_range', type: 'object', required: false, desc: '日期范围 {start, end}' },
{ name: 'format', type: 'string', required: false, desc: '输出格式: pdf/html/json' },
],
},
// 流量包
{
id: 'api_11',
method: 'GET',
path: '/api/shensheshou/packages',
name: '流量包列表',
description: '获取所有已创建的流量包',
category: 'package',
auth: true,
price: 0.5,
},
{
id: 'api_12',
method: 'POST',
path: '/api/shensheshou/packages/create',
name: '创建流量包',
description: '根据筛选条件创建用户流量包',
category: 'package',
auth: true,
price: 5,
params: [
{ name: 'name', type: 'string', required: true, desc: '流量包名称' },
{ name: 'criteria', type: 'object', required: true, desc: '筛选条件' },
{ name: 'export_fields', type: 'array', required: false, desc: '导出字段' },
],
},
{
id: 'api_13',
method: 'POST',
path: '/api/shensheshou/packages/export',
name: '导出流量包',
description: '导出流量包数据到指定目标',
category: 'package',
auth: true,
price: 10,
params: [
{ name: 'package_id', type: 'string', required: true, desc: '流量包ID' },
{ name: 'target', type: 'string', required: true, desc: '导出目标: email/feishu/webhook' },
{ name: 'config', type: 'object', required: false, desc: '导出配置' },
],
},
]
// 模拟API密钥数据
const MOCK_API_KEYS: APIKey[] = [
{
id: 'key_1',
name: '存客宝-生产环境',
key: 'sk-archer-ckb-prod-a1b2c3d4e5f6',
secret: 'sec-ckb-x9y8z7w6v5u4',
status: 'active',
createdAt: '2026-01-15',
expiresAt: null,
lastUsed: '2026-01-31 14:32:15',
permissions: JSON.parse(JSON.stringify(FIELD_GROUPS)),
rateLimit: { requestsPerDay: 5000, requestsPerMonth: 150000 },
billing: { plan: 'pro', usedCredits: 12580, totalCredits: 50000 },
callStats: { today: 342, thisMonth: 8956, total: 45678 }
},
{
id: 'key_2',
name: '点了码-测试环境',
key: 'sk-archer-dlm-test-g7h8i9j0k1l2',
secret: 'sec-dlm-m3n4o5p6q7r8',
status: 'active',
createdAt: '2026-01-20',
expiresAt: '2026-04-20',
lastUsed: '2026-01-30 09:15:42',
permissions: JSON.parse(JSON.stringify(FIELD_GROUPS)).map((g: FieldPermission) => ({
...g,
fields: g.fields.map(f => ({ ...f, enabled: f.price <= 2 }))
})),
rateLimit: { requestsPerDay: 1000, requestsPerMonth: 30000 },
billing: { plan: 'basic', usedCredits: 2340, totalCredits: 10000 },
callStats: { today: 56, thisMonth: 1234, total: 5678 }
},
{
id: 'key_3',
name: '内部测试密钥',
key: 'sk-archer-internal-s3t4u5v6w7x8',
secret: 'sec-int-y9z0a1b2c3d4',
status: 'disabled',
createdAt: '2026-01-10',
expiresAt: null,
lastUsed: '2026-01-25 16:45:30',
permissions: JSON.parse(JSON.stringify(FIELD_GROUPS)).map((g: FieldPermission) => ({
...g,
fields: g.fields.map(f => ({ ...f, enabled: true }))
})),
rateLimit: { requestsPerDay: -1, requestsPerMonth: -1 },
billing: { plan: 'enterprise', usedCredits: 0, totalCredits: -1 },
callStats: { today: 0, thisMonth: 567, total: 12345 }
},
]
// 模拟调用日志
const MOCK_CALL_LOGS: CallLog[] = [
{ id: 'log_1', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/user', method: 'GET', status: 200, credits: 5, responseTime: 123, timestamp: '2026-01-31 14:32:15', ip: '123.45.67.89' },
{ id: 'log_2', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/users/batch', method: 'POST', status: 200, credits: 40, responseTime: 856, timestamp: '2026-01-31 14:30:02', ip: '123.45.67.89' },
{ id: 'log_3', keyId: 'key_2', keyName: '点了码-测试环境', endpoint: '/api/shensheshou/ai/chat', method: 'POST', status: 200, credits: 5, responseTime: 2341, timestamp: '2026-01-31 14:28:45', ip: '98.76.54.32' },
{ id: 'log_4', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/tags', method: 'GET', status: 200, credits: 0.5, responseTime: 45, timestamp: '2026-01-31 14:25:18', ip: '123.45.67.89' },
{ id: 'log_5', keyId: 'key_2', keyName: '点了码-测试环境', endpoint: '/api/shensheshou/user', method: 'GET', status: 403, credits: 0, responseTime: 12, timestamp: '2026-01-31 14:20:33', ip: '98.76.54.32' },
{ id: 'log_6', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/ai/analyze', method: 'POST', status: 200, credits: 10, responseTime: 5623, timestamp: '2026-01-31 14:15:00', ip: '123.45.67.89' },
{ id: 'log_7', keyId: 'key_1', keyName: '存客宝-生产环境', endpoint: '/api/shensheshou/packages/create', method: 'POST', status: 200, credits: 5, responseTime: 1234, timestamp: '2026-01-31 14:10:22', ip: '123.45.67.89' },
{ id: 'log_8', keyId: 'key_2', keyName: '点了码-测试环境', endpoint: '/api/shensheshou/ingest', method: 'POST', status: 429, credits: 0, responseTime: 8, timestamp: '2026-01-31 14:05:11', ip: '98.76.54.32' },
]
// ==================== 主组件 ====================
export default function APIServicePage() {
const [activeTab, setActiveTab] = useState('keys')
const [activeCategory, setActiveCategory] = useState('all')
const [apiBaseUrl, setApiBaseUrl] = useState('')
const [apiKeys, setApiKeys] = useState<APIKey[]>([])
const [loading, setLoading] = useState(true)
const [creating, setCreating] = useState(false)
const [callLogs] = useState<CallLog[]>(MOCK_CALL_LOGS)
// 弹窗状态
const [showCreateKeyDialog, setShowCreateKeyDialog] = useState(false)
const [showKeyDetailDialog, setShowKeyDetailDialog] = useState(false)
const [showPermissionDialog, setShowPermissionDialog] = useState(false)
const [showBillingDialog, setShowBillingDialog] = useState(false)
const [selectedKey, setSelectedKey] = useState<APIKey | null>(null)
// 复制状态
const [copiedId, setCopiedId] = useState<string | null>(null)
// 新密钥表单
const [newKeyForm, setNewKeyForm] = useState<{
name: string
plan: 'free' | 'basic' | 'pro' | 'enterprise'
expiresAt: string
}>({
name: '',
plan: 'basic',
expiresAt: '',
})
// 显示/隐藏密钥
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set())
const fetchKeys = async () => {
try {
const res = await fetch('/api/api-keys')
const json = await res.json()
if (json.success && json.data) setApiKeys(json.data)
} catch (e) {
console.error('拉取API密钥失败:', e)
} finally {
setLoading(false)
}
}
useEffect(() => {
const host = typeof window !== 'undefined' ? window.location.origin : ''
setApiBaseUrl(host)
fetchKeys()
}, [])
const copyToClipboard = (text: string, id: string) => {
navigator.clipboard.writeText(text)
setCopiedId(id)
setTimeout(() => setCopiedId(null), 2000)
}
const toggleKeyVisibility = (keyId: string) => {
const newSet = new Set(visibleKeys)
if (newSet.has(keyId)) {
newSet.delete(keyId)
} else {
newSet.add(keyId)
}
setVisibleKeys(newSet)
}
const maskKey = (key: string) => {
return key.substring(0, 12) + '••••••••••••'
}
const getMethodColor = (method: string) => {
switch (method) {
case 'GET': return 'bg-green-100 text-green-700'
case 'POST': return 'bg-blue-100 text-blue-700'
case 'PUT': return 'bg-yellow-100 text-yellow-700'
case 'DELETE': return 'bg-red-100 text-red-700'
default: return 'bg-gray-100 text-gray-700'
}
}
const getStatusBadge = (status: string) => {
switch (status) {
case 'active': return <Badge className="bg-green-100 text-green-700"></Badge>
case 'disabled': return <Badge className="bg-gray-100 text-gray-600"></Badge>
case 'expired': return <Badge className="bg-red-100 text-red-700"></Badge>
default: return <Badge variant="outline">{status}</Badge>
}
}
const getPlanBadge = (plan: string) => {
switch (plan) {
case 'free': return <Badge variant="outline"></Badge>
case 'basic': return <Badge className="bg-blue-100 text-blue-700"></Badge>
case 'pro': return <Badge className="bg-purple-100 text-purple-700"></Badge>
case 'enterprise': return <Badge className="bg-orange-100 text-orange-700"></Badge>
default: return <Badge variant="outline">{plan}</Badge>
}
}
const filteredEndpoints = activeCategory === 'all'
? API_ENDPOINTS
: API_ENDPOINTS.filter(e => e.category === activeCategory)
// 创建新密钥
const handleCreateKey = async () => {
if (!newKeyForm.name.trim()) return
setCreating(true)
try {
const res = await fetch('/api/api-keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: newKeyForm.name.trim(),
plan: newKeyForm.plan,
expiresAt: newKeyForm.expiresAt || undefined,
}),
})
const json = await res.json()
if (json.success && json.data) {
setApiKeys([json.data, ...apiKeys])
setShowCreateKeyDialog(false)
setNewKeyForm({ name: '', plan: 'basic', expiresAt: '' })
} else {
alert(json.error || '创建失败')
}
} catch {
alert('创建失败')
} finally {
setCreating(false)
}
}
// 切换密钥状态
const toggleKeyStatus = async (keyId: string) => {
try {
const res = await fetch('/api/api-keys', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: keyId, action: 'toggleStatus' }),
})
const json = await res.json()
if (json.success && json.data) {
setApiKeys(apiKeys.map(k => k.id === keyId ? { ...k, ...json.data } : k))
}
} catch (e) {
console.error('切换状态失败:', e)
}
}
// 删除密钥
const deleteKey = async (keyId: string) => {
if (!confirm('确定要删除此API密钥吗此操作不可恢复。')) return
try {
const res = await fetch(`/api/api-keys?id=${keyId}`, { method: 'DELETE' })
const json = await res.json()
if (json.success) {
setApiKeys(apiKeys.filter(k => k.id !== keyId))
} else {
alert(json.error || '删除失败')
}
} catch {
alert('删除失败')
}
}
// 更新字段权限
const updateFieldPermission = (groupIndex: number, fieldIndex: number, enabled: boolean) => {
if (!selectedKey) return
const newPermissions = [...selectedKey.permissions]
newPermissions[groupIndex].fields[fieldIndex].enabled = enabled
setSelectedKey({ ...selectedKey, permissions: newPermissions })
}
// 保存权限设置
const savePermissions = async () => {
if (!selectedKey) return
try {
const res = await fetch('/api/api-keys', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: selectedKey.id, action: 'updatePermissions', permissions: selectedKey.permissions }),
})
const json = await res.json()
if (json.success) {
setApiKeys(apiKeys.map(k => k.id === selectedKey.id ? { ...k, permissions: selectedKey.permissions } : k))
setShowPermissionDialog(false)
}
} catch (e) {
console.error('保存权限失败:', e)
}
}
// 统计数据
const totalCalls = apiKeys.reduce((sum, k) => sum + k.callStats.today, 0)
const totalCreditsUsed = apiKeys.reduce((sum, k) => sum + k.billing.usedCredits, 0)
const activeKeysCount = apiKeys.filter(k => k.status === 'active').length
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
<div className="p-6 space-y-6">
{/* 顶部标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">API服务</h1>
<p className="text-sm text-gray-500 mt-1">API</p>
</div>
<div className="flex items-center gap-3">
<Link href="/data-market/api/keys">
<Button variant="outline">
<Key className="h-4 w-4 mr-2" />
API密钥
</Button>
</Link>
<Link href="/data-market/api/docs">
<Button variant="outline">
<ExternalLink className="h-4 w-4 mr-2" />
API文档
</Button>
</Link>
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-4 gap-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-purple-100">
<Key className="h-5 w-5 text-purple-600" />
</div>
<div>
<div className="text-2xl font-bold text-gray-900">{apiKeys.length}</div>
<div className="text-sm text-gray-500">API密钥</div>
</div>
</div>
<div className="mt-2 text-xs text-gray-400">{activeKeysCount} 使</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-100">
<Activity className="h-5 w-5 text-blue-600" />
</div>
<div>
<div className="text-2xl font-bold text-gray-900">{totalCalls.toLocaleString()}</div>
<div className="text-sm text-gray-500"></div>
</div>
</div>
<div className="mt-2 text-xs text-green-600"> 12.5% </div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-green-100">
<DollarSign className="h-5 w-5 text-green-600" />
</div>
<div>
<div className="text-2xl font-bold text-gray-900">{totalCreditsUsed.toLocaleString()}</div>
<div className="text-sm text-gray-500"></div>
</div>
</div>
<div className="mt-2 text-xs text-gray-400"></div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-orange-100">
<Server className="h-5 w-5 text-orange-600" />
</div>
<div>
<div className="text-2xl font-bold text-gray-900">{API_ENDPOINTS.length}</div>
<div className="text-sm text-gray-500">API端点</div>
</div>
</div>
<div className="mt-2 text-xs text-gray-400">{API_CATEGORIES.length} </div>
</CardContent>
</Card>
</div>
{/* 主内容区 */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="bg-white/80 border">
<TabsTrigger value="keys" className="flex items-center gap-2">
<Key className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger value="endpoints" className="flex items-center gap-2">
<Code className="h-4 w-4" />
API文档
</TabsTrigger>
<TabsTrigger value="logs" className="flex items-center gap-2">
<Activity className="h-4 w-4" />
</TabsTrigger>
<TabsTrigger value="billing" className="flex items-center gap-2">
<CreditCard className="h-4 w-4" />
</TabsTrigger>
</TabsList>
{/* 密钥管理 */}
<TabsContent value="keys" className="mt-4 space-y-4">
{/* API基础信息 */}
<Card className="border-0 shadow-sm bg-gradient-to-r from-purple-50 to-blue-50">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-gray-900 mb-1">API基础地址</h3>
<div className="flex items-center gap-2">
<code className="bg-white px-4 py-2 rounded-lg text-purple-600 font-mono">
{apiBaseUrl || 'https://your-domain.com'}
</code>
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(apiBaseUrl, 'base')}
>
{copiedId === 'base' ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="text-right">
<div className="text-sm text-gray-500">API版本</div>
<Badge>v1.0</Badge>
</div>
</div>
</CardContent>
</Card>
{/* 密钥列表 */}
<Card className="border-0 shadow-sm bg-white/80">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg">API密钥列表</CardTitle>
<CardDescription></CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="py-12 text-center text-gray-500">...</div>
) : apiKeys.length === 0 ? (
<div className="py-12 text-center text-gray-500">
API密钥
<Link href="/data-market/api/keys" className="text-purple-600 hover:underline ml-1">API密钥管理</Link>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead>API Key</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>/</TableHead>
<TableHead></TableHead>
<TableHead>使</TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiKeys.map(key => (
<TableRow key={key.id}>
<TableCell className="font-medium">{key.name}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<code className="text-xs bg-gray-100 px-2 py-1 rounded font-mono">
{visibleKeys.has(key.id) ? key.key : maskKey(key.key)}
</code>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => toggleKeyVisibility(key.id)}
>
{visibleKeys.has(key.id) ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => copyToClipboard(key.key, key.id)}
>
{copiedId === key.id ? <CheckCircle2 className="h-3 w-3 text-green-500" /> : <Copy className="h-3 w-3" />}
</Button>
</div>
</TableCell>
<TableCell>{getPlanBadge(key.billing.plan)}</TableCell>
<TableCell>{getStatusBadge(key.status)}</TableCell>
<TableCell>
<span className="text-sm">{key.callStats.today.toLocaleString()} / {key.callStats.thisMonth.toLocaleString()}</span>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="w-16 h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-purple-500 rounded-full"
style={{ width: key.billing.totalCredits === -1 ? '100%' : `${(key.billing.usedCredits / key.billing.totalCredits) * 100}%` }}
/>
</div>
<span className="text-xs text-gray-500">
{key.billing.totalCredits === -1 ? '无限' : `${key.billing.usedCredits}/${key.billing.totalCredits}`}
</span>
</div>
</TableCell>
<TableCell className="text-sm text-gray-500">
{key.lastUsed || '-'}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedKey(key)
setShowKeyDetailDialog(true)
}}
>
<Settings className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedKey(key)
setShowPermissionDialog(true)
}}
>
<Shield className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => toggleKeyStatus(key.id)}
>
{key.status === 'active' ? <Lock className="h-4 w-4" /> : <Unlock className="h-4 w-4" />}
</Button>
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-700"
onClick={() => deleteKey(key.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{/* 认证说明 */}
<Card className="border-0 shadow-sm bg-white/80">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="p-4 rounded-lg bg-gray-50">
<p className="text-sm text-gray-600 mb-3">API请求需要在Header中携带API密钥和密钥Secret</p>
<pre className="bg-gray-900 text-green-400 p-4 rounded-lg text-sm overflow-x-auto">
{`curl -X GET "${apiBaseUrl}/api/shensheshou/user?phone=13800138000" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-H "X-API-Secret: YOUR_API_SECRET" \\
-H "Content-Type: application/json"`}
</pre>
</div>
</CardContent>
</Card>
</TabsContent>
{/* API文档 */}
<TabsContent value="endpoints" className="mt-4 space-y-4">
{/* API分类筛选 */}
<div className="flex items-center gap-2 overflow-x-auto pb-2">
<Button
variant={activeCategory === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setActiveCategory('all')}
>
({API_ENDPOINTS.length})
</Button>
{API_CATEGORIES.map(cat => {
const count = API_ENDPOINTS.filter(e => e.category === cat.id).length
const Icon = cat.icon
return (
<Button
key={cat.id}
variant={activeCategory === cat.id ? 'default' : 'outline'}
size="sm"
onClick={() => setActiveCategory(cat.id)}
>
<Icon className="h-4 w-4 mr-1" />
{cat.name} ({count})
</Button>
)
})}
</div>
{/* API端点列表 */}
<div className="space-y-3">
{filteredEndpoints.map(endpoint => (
<Card key={endpoint.id} className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<Badge className={`${getMethodColor(endpoint.method)} font-mono`}>
{endpoint.method}
</Badge>
<code className="text-sm font-mono text-gray-700">{endpoint.path}</code>
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(`${apiBaseUrl}${endpoint.path}`, endpoint.id)}
>
{copiedId === endpoint.id ? (
<CheckCircle2 className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
<DollarSign className="h-3 w-3 mr-1" />
{endpoint.price} /
</Badge>
{endpoint.auth && (
<Badge variant="outline" className="text-xs">
<Key className="h-3 w-3 mr-1" />
</Badge>
)}
</div>
</div>
<h3 className="font-semibold text-gray-900 mb-1">{endpoint.name}</h3>
<p className="text-sm text-gray-500 mb-3">{endpoint.description}</p>
{/* 参数 */}
{endpoint.params && endpoint.params.length > 0 && (
<div className="mb-3">
<h4 className="text-xs font-medium text-gray-500 mb-2"></h4>
<div className="space-y-1">
{endpoint.params.map((param, i) => (
<div key={i} className="flex items-center gap-2 text-sm">
<code className="bg-gray-100 px-2 py-0.5 rounded text-purple-600">{param.name}</code>
<Badge variant="outline" className="text-xs">{param.type}</Badge>
{param.required && <Badge className="bg-red-100 text-red-700 text-xs"></Badge>}
<span className="text-gray-500">{param.desc}</span>
</div>
))}
</div>
</div>
)}
{/* 调用示例 */}
{endpoint.example && (
<div className="mb-3">
<h4 className="text-xs font-medium text-gray-500 mb-2"></h4>
<pre className="bg-gray-900 text-green-400 p-3 rounded-lg text-xs font-mono overflow-x-auto">
{endpoint.example}
</pre>
</div>
)}
{/* 响应示例 */}
{endpoint.response && (
<div>
<h4 className="text-xs font-medium text-gray-500 mb-2"></h4>
<pre className="bg-gray-100 p-3 rounded text-xs font-mono text-gray-700 overflow-x-auto">
{endpoint.response}
</pre>
</div>
)}
</CardContent>
</Card>
))}
</div>
</TabsContent>
{/* 调用日志 */}
<TabsContent value="logs" className="mt-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg"></CardTitle>
<CardDescription>API调用记录和状态</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Filter className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>IP地址</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{callLogs.map(log => (
<TableRow key={log.id}>
<TableCell className="text-sm text-gray-500">{log.timestamp}</TableCell>
<TableCell className="font-medium">{log.keyName}</TableCell>
<TableCell>
<code className="text-xs bg-gray-100 px-2 py-1 rounded">{log.endpoint}</code>
</TableCell>
<TableCell>
<Badge className={`${getMethodColor(log.method)} text-xs`}>{log.method}</Badge>
</TableCell>
<TableCell>
{log.status === 200 ? (
<Badge className="bg-green-100 text-green-700">{log.status}</Badge>
) : log.status === 403 ? (
<Badge className="bg-yellow-100 text-yellow-700">{log.status}</Badge>
) : log.status === 429 ? (
<Badge className="bg-red-100 text-red-700">{log.status}</Badge>
) : (
<Badge variant="outline">{log.status}</Badge>
)}
</TableCell>
<TableCell>{log.credits > 0 ? log.credits : '-'}</TableCell>
<TableCell className="text-sm text-gray-500">{log.responseTime}ms</TableCell>
<TableCell className="text-sm text-gray-400">{log.ip}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
{/* 计费明细 */}
<TabsContent value="billing" className="mt-4 space-y-4">
{/* 套餐对比 */}
<div className="grid grid-cols-4 gap-4">
{BILLING_PLANS.map(plan => (
<Card key={plan.id} className={`border-0 shadow-sm ${plan.id === 'pro' ? 'ring-2 ring-purple-500' : 'bg-white/80'}`}>
<CardContent className="p-4">
{plan.id === 'pro' && (
<Badge className="mb-2 bg-purple-500"></Badge>
)}
<h3 className="text-lg font-bold text-gray-900">{plan.name}</h3>
<div className="mt-2 mb-4">
<span className="text-3xl font-bold text-gray-900">¥{plan.price}</span>
<span className="text-gray-500">/</span>
</div>
<div className="text-sm text-gray-600 mb-4">
{plan.credits === -1 ? '无限信用点' : `${plan.credits.toLocaleString()} 信用点/月`}
</div>
<ul className="space-y-2 mb-4">
{plan.features.map((f, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-gray-600">
<CheckCircle className="h-4 w-4 text-green-500" />
{f}
</li>
))}
</ul>
<Button className="w-full" variant={plan.id === 'pro' ? 'default' : 'outline'}>
{plan.id === 'free' ? '当前套餐' : '升级'}
</Button>
</CardContent>
</Card>
))}
</div>
{/* 消费明细 */}
<Card className="border-0 shadow-sm bg-white/80">
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>API分类</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>5,234</TableCell>
<TableCell>1 /</TableCell>
<TableCell>5,234</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="h-full bg-blue-500 rounded-full" style={{ width: '45%' }} />
</div>
<span className="text-sm">45%</span>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">AI服务</TableCell>
<TableCell>423</TableCell>
<TableCell>5-10 /</TableCell>
<TableCell>3,156</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="h-full bg-purple-500 rounded-full" style={{ width: '27%' }} />
</div>
<span className="text-sm">27%</span>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>1,567</TableCell>
<TableCell>0.5-2 /</TableCell>
<TableCell>1,890</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="h-full bg-green-500 rounded-full" style={{ width: '16%' }} />
</div>
<span className="text-sm">16%</span>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>234</TableCell>
<TableCell>0.5-3 /</TableCell>
<TableCell>456</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="h-full bg-orange-500 rounded-full" style={{ width: '4%' }} />
</div>
<span className="text-sm">4%</span>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>89</TableCell>
<TableCell>5-10 /</TableCell>
<TableCell>844</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="h-full bg-pink-500 rounded-full" style={{ width: '7%' }} />
</div>
<span className="text-sm">7%</span>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
<div className="mt-4 pt-4 border-t flex justify-between items-center">
<span className="font-medium text-gray-900"></span>
<span className="text-xl font-bold text-purple-600">11,580 </span>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* ==================== 弹窗 ==================== */}
{/* 创建密钥弹窗 */}
<Dialog open={showCreateKeyDialog} onOpenChange={setShowCreateKeyDialog}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>API密钥</DialogTitle>
<DialogDescription>API访问密钥</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label></Label>
<Input
placeholder="例如:存客宝-生产环境"
value={newKeyForm.name}
onChange={e => setNewKeyForm({ ...newKeyForm, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select
value={newKeyForm.plan}
onValueChange={(v) => setNewKeyForm({ ...newKeyForm, plan: v as 'free' | 'basic' | 'pro' | 'enterprise' })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{BILLING_PLANS.map(plan => (
<SelectItem key={plan.id} value={plan.id}>
{plan.name} - ¥{plan.price}/ ({plan.credits === -1 ? '无限' : plan.credits.toLocaleString()} )
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Input
type="date"
value={newKeyForm.expiresAt}
onChange={e => setNewKeyForm({ ...newKeyForm, expiresAt: e.target.value })}
/>
<p className="text-xs text-gray-500"></p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowCreateKeyDialog(false)}></Button>
<Button onClick={handleCreateKey} disabled={!newKeyForm.name.trim() || creating}>
{creating ? '创建中...' : '创建密钥'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 密钥详情弹窗 */}
<Dialog open={showKeyDetailDialog} onOpenChange={setShowKeyDetailDialog}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>{selectedKey?.name}</DialogDescription>
</DialogHeader>
{selectedKey && (
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>API Key</Label>
<div className="flex items-center gap-2">
<Input
value={visibleKeys.has(selectedKey.id + '_detail') ? selectedKey.key : maskKey(selectedKey.key)}
readOnly
className="font-mono"
/>
<Button
variant="outline"
size="icon"
onClick={() => toggleKeyVisibility(selectedKey.id + '_detail')}
>
{visibleKeys.has(selectedKey.id + '_detail') ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
<Button
variant="outline"
size="icon"
onClick={() => copyToClipboard(selectedKey.key, selectedKey.id + '_key')}
>
{copiedId === selectedKey.id + '_key' ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="space-y-2">
<Label>API Secret</Label>
<div className="flex items-center gap-2">
<Input
value={visibleKeys.has(selectedKey.id + '_secret') ? selectedKey.secret : '••••••••••••••••'}
readOnly
className="font-mono"
/>
<Button
variant="outline"
size="icon"
onClick={() => toggleKeyVisibility(selectedKey.id + '_secret')}
>
{visibleKeys.has(selectedKey.id + '_secret') ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
<Button
variant="outline"
size="icon"
onClick={() => copyToClipboard(selectedKey.secret, selectedKey.id + '_sec')}
>
{copiedId === selectedKey.id + '_sec' ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-gray-500"></Label>
<p className="font-medium">{selectedKey.createdAt}</p>
</div>
<div>
<Label className="text-gray-500"></Label>
<p className="font-medium">{selectedKey.expiresAt || '永不过期'}</p>
</div>
<div>
<Label className="text-gray-500"></Label>
<p>{getPlanBadge(selectedKey.billing.plan)}</p>
</div>
<div>
<Label className="text-gray-500"></Label>
<p>{getStatusBadge(selectedKey.status)}</p>
</div>
</div>
<div className="p-3 rounded-lg bg-gray-50">
<h4 className="font-medium mb-2"></h4>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-xl font-bold text-gray-900">{selectedKey.callStats.today.toLocaleString()}</div>
<div className="text-xs text-gray-500"></div>
</div>
<div>
<div className="text-xl font-bold text-gray-900">{selectedKey.callStats.thisMonth.toLocaleString()}</div>
<div className="text-xs text-gray-500"></div>
</div>
<div>
<div className="text-xl font-bold text-gray-900">{selectedKey.callStats.total.toLocaleString()}</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
</div>
<div className="p-3 rounded-lg bg-yellow-50 text-sm text-yellow-700">
API密钥
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setShowKeyDetailDialog(false)}></Button>
<Button variant="destructive">
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 字段权限配置弹窗 */}
<Dialog open={showPermissionDialog} onOpenChange={setShowPermissionDialog}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription> {selectedKey?.name} 访</DialogDescription>
</DialogHeader>
{selectedKey && (
<div className="space-y-6 py-4">
{selectedKey.permissions.map((group, groupIndex) => (
<div key={group.fieldGroup} className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-900">{group.fieldGroup}</h4>
<Button
variant="ghost"
size="sm"
onClick={() => {
const allEnabled = group.fields.every(f => f.enabled)
const newPermissions = [...selectedKey.permissions]
newPermissions[groupIndex].fields = newPermissions[groupIndex].fields.map(f => ({ ...f, enabled: !allEnabled }))
setSelectedKey({ ...selectedKey, permissions: newPermissions })
}}
>
{group.fields.every(f => f.enabled) ? '取消全选' : '全选'}
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
{group.fields.map((field, fieldIndex) => (
<div
key={field.name}
className={`flex items-center justify-between p-3 rounded-lg border ${field.enabled ? 'bg-purple-50 border-purple-200' : 'bg-gray-50 border-gray-200'}`}
>
<div className="flex items-center gap-3">
<Checkbox
checked={field.enabled}
onCheckedChange={(checked) => updateFieldPermission(groupIndex, fieldIndex, checked as boolean)}
/>
<div>
<div className="font-medium text-sm">{field.label}</div>
<div className="text-xs text-gray-500">{field.name}</div>
</div>
</div>
<Badge variant="outline" className="text-xs">
{field.price} /
</Badge>
</div>
))}
</div>
</div>
))}
<div className="p-4 rounded-lg bg-blue-50 text-sm text-blue-700">
💡 API时访API响应中将被过滤
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setShowPermissionDialog(false)}></Button>
<Button onClick={savePermissions}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 计费套餐弹窗 */}
<Dialog open={showBillingDialog} onOpenChange={setShowBillingDialog}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>API调用套餐</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-4 gap-4 py-4">
{BILLING_PLANS.map(plan => (
<Card key={plan.id} className={`${plan.id === 'pro' ? 'ring-2 ring-purple-500' : ''}`}>
<CardContent className="p-4">
{plan.id === 'pro' && (
<Badge className="mb-2 bg-purple-500"></Badge>
)}
<h3 className="text-lg font-bold text-gray-900">{plan.name}</h3>
<div className="mt-2 mb-4">
<span className="text-3xl font-bold text-gray-900">¥{plan.price}</span>
<span className="text-gray-500">/</span>
</div>
<div className="text-sm text-gray-600 mb-4">
{plan.credits === -1 ? '无限信用点' : `${plan.credits.toLocaleString()} 信用点/月`}
</div>
<ul className="space-y-2 mb-4">
{plan.features.map((f, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-gray-600">
<CheckCircle className="h-4 w-4 text-green-500 flex-shrink-0" />
{f}
</li>
))}
</ul>
<div className="text-xs text-gray-500 mb-4">
<div>: {plan.rateLimit.requestsPerDay === -1 ? '无限' : plan.rateLimit.requestsPerDay.toLocaleString()}</div>
<div>: {plan.rateLimit.requestsPerMonth === -1 ? '无限' : plan.rateLimit.requestsPerMonth.toLocaleString()}</div>
</div>
<Button className="w-full" variant={plan.id === 'pro' ? 'default' : 'outline'}>
</Button>
</CardContent>
</Card>
))}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowBillingDialog(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
)
}