1618 lines
66 KiB
TypeScript
1618 lines
66 KiB
TypeScript
"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>
|
||
)
|
||
}
|