Files
users/app/data-market/open-api/page.tsx

1461 lines
59 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { Label } from "@/components/ui/label"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
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,
Settings,
ArrowDownToLine,
ArrowUpFromLine,
Shield,
Clock,
Eye,
EyeOff,
Trash2,
Edit,
BarChart3,
AlertCircle,
CheckCircle,
XCircle,
Play,
} from "lucide-react"
// 接入方类型
interface Partner {
id: string
name: string
type: 'external' | 'internal' // 外部系统/内部系统
description: string
apiKey: string
apiSecret: string
status: 'active' | 'inactive' | 'pending'
permissions: {
dataIngest: boolean // 数据流入权限(传入数据)
dataQuery: boolean // 数据查询权限(获取标签)
tagEnrich: boolean // 标签完善权限
batchProcess: boolean // 批量处理权限
}
quotaConfig: {
dailyLimit: number // 每日限额
monthlyLimit: number // 每月限额
rateLimit: number // 速率限制(次/秒)
}
statistics: {
totalCalls: number
todayCalls: number
monthCalls: number
lastCallTime: string
}
createdAt: string
updatedAt: string
}
// 调用记录类型
interface CallLog {
id: string
partnerId: string
partnerName: string
endpoint: string
method: string
status: 'success' | 'error' | 'pending'
responseTime: number
requestSize: number
responseSize: number
timestamp: string
errorMessage?: string
}
// 开放API端点定义
interface OpenAPIEndpoint {
id: string
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
path: string
name: string
description: string
category: 'ingest' | 'query' | 'tag' | 'batch'
params: { name: string; type: string; required: boolean; desc: string }[]
response: string
example: {
request: string
response: string
}
}
// 预定义接入方
const DEFAULT_PARTNERS: Partner[] = [
{
id: 'ckb_001',
name: '存客宝',
type: 'external',
description: '私域流量管理系统,提供流量词上报和用户画像获取',
apiKey: 'sk-ckb-xxxxxxxxxx',
apiSecret: 'sec-ckb-xxxxxxxxxx',
status: 'active',
permissions: {
dataIngest: true,
dataQuery: true,
tagEnrich: true,
batchProcess: true,
},
quotaConfig: {
dailyLimit: 10000,
monthlyLimit: 300000,
rateLimit: 100,
},
statistics: {
totalCalls: 158420,
todayCalls: 1520,
monthCalls: 45800,
lastCallTime: '2026-01-31 18:45:22',
},
createdAt: '2025-06-15',
updatedAt: '2026-01-31',
},
{
id: 'dlm_001',
name: '点了码',
type: 'external',
description: '线下扫码营销系统,同步用户行为数据',
apiKey: 'sk-dlm-xxxxxxxxxx',
apiSecret: 'sec-dlm-xxxxxxxxxx',
status: 'active',
permissions: {
dataIngest: true,
dataQuery: true,
tagEnrich: false,
batchProcess: false,
},
quotaConfig: {
dailyLimit: 5000,
monthlyLimit: 150000,
rateLimit: 50,
},
statistics: {
totalCalls: 89200,
todayCalls: 820,
monthCalls: 28500,
lastCallTime: '2026-01-31 18:30:15',
},
createdAt: '2025-08-20',
updatedAt: '2026-01-31',
},
{
id: 'feishu_001',
name: '飞书机器人',
type: 'internal',
description: '企业内部飞书机器人,用于用户查询和报告推送',
apiKey: 'sk-fs-xxxxxxxxxx',
apiSecret: 'sec-fs-xxxxxxxxxx',
status: 'active',
permissions: {
dataIngest: false,
dataQuery: true,
tagEnrich: false,
batchProcess: false,
},
quotaConfig: {
dailyLimit: 1000,
monthlyLimit: 30000,
rateLimit: 10,
},
statistics: {
totalCalls: 12500,
todayCalls: 156,
monthCalls: 4200,
lastCallTime: '2026-01-31 18:50:00',
},
createdAt: '2025-10-10',
updatedAt: '2026-01-31',
},
]
// 开放API端点列表
const OPEN_API_ENDPOINTS: OpenAPIEndpoint[] = [
// 数据流入接口
{
id: 'ingest_1',
method: 'POST',
path: '/api/open-api/ingest/user',
name: '用户数据流入',
description: '第三方系统传入用户数据,神射手自动完善标签',
category: 'ingest',
params: [
{ name: 'phone', type: 'string', required: true, desc: '手机号11位' },
{ name: 'name', type: 'string', required: false, desc: '用户姓名' },
{ name: 'source', type: 'string', required: true, desc: '数据来源标识' },
{ name: 'tags', type: 'array', required: false, desc: '原始标签数组' },
{ name: 'extra', type: 'object', required: false, desc: '扩展字段' },
],
response: '{ "success": true, "data": { "userId": "xxx", "enriched": true, "tags": [...] } }',
example: {
request: `{
"phone": "13800138000",
"name": "张三",
"source": "cunkebao",
"tags": ["高意向", "电商用户"],
"extra": { "wechatId": "wxid_xxx" }
}`,
response: `{
"success": true,
"data": {
"userId": "usr_xxx",
"enriched": true,
"tags": ["高意向", "电商用户", "A级用户", "一线城市"],
"rfm_score": 85,
"user_level": "A"
}
}`,
},
},
{
id: 'ingest_2',
method: 'POST',
path: '/api/open-api/ingest/batch',
name: '批量数据流入',
description: '批量传入用户数据支持最多1000条/次',
category: 'ingest',
params: [
{ name: 'users', type: 'array', required: true, desc: '用户数组' },
{ name: 'source', type: 'string', required: true, desc: '数据来源标识' },
{ name: 'async', type: 'boolean', required: false, desc: '是否异步处理' },
],
response: '{ "success": true, "data": { "taskId": "xxx", "total": 1000, "processed": 1000 } }',
example: {
request: `{
"source": "cunkebao",
"async": true,
"users": [
{ "phone": "13800138001", "name": "张三" },
{ "phone": "13800138002", "name": "李四" }
]
}`,
response: `{
"success": true,
"data": {
"taskId": "task_xxx",
"total": 2,
"processed": 0,
"status": "processing"
}
}`,
},
},
// 数据查询接口
{
id: 'query_1',
method: 'GET',
path: '/api/open-api/query/user',
name: '用户画像查询',
description: '根据手机号/QQ获取完整用户画像和标签',
category: 'query',
params: [
{ name: 'phone', type: 'string', required: false, desc: '手机号' },
{ name: 'qq', type: 'string', required: false, desc: 'QQ号' },
{ name: 'fields', type: 'string', required: false, desc: '返回字段(逗号分隔)' },
],
response: '{ "success": true, "data": { "phone": "xxx", "tags": [...], "rfm": {...} } }',
example: {
request: `GET /api/open-api/query/user?phone=13800138000&fields=tags,rfm,level`,
response: `{
"success": true,
"data": {
"phone": "138****8000",
"name": "张*",
"tags": ["高价值用户", "电商活跃", "一线城市"],
"rfm": { "r": 5, "f": 4, "m": 5, "score": 93 },
"level": "S",
"traffic_pool": "钻石池"
}
}`,
},
},
{
id: 'query_2',
method: 'POST',
path: '/api/open-api/query/batch',
name: '批量用户查询',
description: '批量查询用户画像支持最多500个/次',
category: 'query',
params: [
{ name: 'phones', type: 'array', required: false, desc: '手机号数组' },
{ name: 'qqs', type: 'array', required: false, desc: 'QQ号数组' },
{ name: 'fields', type: 'array', required: false, desc: '返回字段数组' },
],
response: '{ "success": true, "data": { "users": [...], "notFound": [...] } }',
example: {
request: `{
"phones": ["13800138001", "13800138002"],
"fields": ["tags", "level"]
}`,
response: `{
"success": true,
"data": {
"users": [
{ "phone": "138****8001", "tags": [...], "level": "A" },
{ "phone": "138****8002", "tags": [...], "level": "B" }
],
"notFound": []
}
}`,
},
},
// 标签服务接口
{
id: 'tag_1',
method: 'POST',
path: '/api/open-api/tag/enrich',
name: '标签完善',
description: '对用户进行AI标签完善返回新增标签',
category: 'tag',
params: [
{ name: 'phone', type: 'string', required: true, desc: '手机号' },
{ name: 'strategy', type: 'string', required: false, desc: '打标策略rfm/behavior/preference' },
{ name: 'force', type: 'boolean', required: false, desc: '是否强制刷新' },
],
response: '{ "success": true, "data": { "newTags": [...], "allTags": [...] } }',
example: {
request: `{
"phone": "13800138000",
"strategy": "rfm",
"force": false
}`,
response: `{
"success": true,
"data": {
"newTags": ["高价值用户", "活跃用户"],
"allTags": ["高价值用户", "活跃用户", "电商用户", "一线城市"],
"rfm_score": 89
}
}`,
},
},
{
id: 'tag_2',
method: 'GET',
path: '/api/open-api/tag/list',
name: '标签列表',
description: '获取所有可用标签及其统计',
category: 'tag',
params: [
{ name: 'category', type: 'string', required: false, desc: '标签分类' },
{ name: 'source', type: 'string', required: false, desc: '来源筛选' },
],
response: '{ "success": true, "data": { "tags": [...], "total": 100 } }',
example: {
request: `GET /api/open-api/tag/list?category=value`,
response: `{
"success": true,
"data": {
"tags": [
{ "id": "tag_001", "name": "高价值用户", "count": 125000 },
{ "id": "tag_002", "name": "优质用户", "count": 850000 }
],
"total": 45
}
}`,
},
},
// 批量处理接口
{
id: 'batch_1',
method: 'POST',
path: '/api/open-api/batch/task',
name: '创建批量任务',
description: '创建异步批量处理任务',
category: 'batch',
params: [
{ name: 'type', type: 'string', required: true, desc: '任务类型enrich/export/sync' },
{ name: 'config', type: 'object', required: true, desc: '任务配置' },
{ name: 'callback', type: 'string', required: false, desc: '完成回调URL' },
],
response: '{ "success": true, "data": { "taskId": "xxx", "status": "pending" } }',
example: {
request: `{
"type": "enrich",
"config": {
"source": "cunkebao",
"strategy": "rfm",
"dateRange": "2026-01"
},
"callback": "https://your-domain.com/webhook"
}`,
response: `{
"success": true,
"data": {
"taskId": "task_xxx",
"status": "pending",
"estimatedTime": "10分钟"
}
}`,
},
},
{
id: 'batch_2',
method: 'GET',
path: '/api/open-api/batch/status',
name: '查询任务状态',
description: '查询批量任务执行状态',
category: 'batch',
params: [
{ name: 'taskId', type: 'string', required: true, desc: '任务ID' },
],
response: '{ "success": true, "data": { "status": "completed", "progress": 100 } }',
example: {
request: `GET /api/open-api/batch/status?taskId=task_xxx`,
response: `{
"success": true,
"data": {
"taskId": "task_xxx",
"status": "completed",
"progress": 100,
"total": 10000,
"processed": 10000,
"failed": 5,
"completedAt": "2026-01-31T18:45:00Z"
}
}`,
},
},
]
// 调用日志数据
const DEFAULT_CALL_LOGS: CallLog[] = [
{ id: 'log_1', partnerId: 'ckb_001', partnerName: '存客宝', endpoint: '/api/open-api/ingest/user', method: 'POST', status: 'success', responseTime: 45, requestSize: 256, responseSize: 512, timestamp: '2026-01-31 18:50:22' },
{ id: 'log_2', partnerId: 'ckb_001', partnerName: '存客宝', endpoint: '/api/open-api/query/user', method: 'GET', status: 'success', responseTime: 32, requestSize: 64, responseSize: 1024, timestamp: '2026-01-31 18:49:55' },
{ id: 'log_3', partnerId: 'dlm_001', partnerName: '点了码', endpoint: '/api/open-api/ingest/batch', method: 'POST', status: 'success', responseTime: 1250, requestSize: 45000, responseSize: 2048, timestamp: '2026-01-31 18:48:30' },
{ id: 'log_4', partnerId: 'ckb_001', partnerName: '存客宝', endpoint: '/api/open-api/tag/enrich', method: 'POST', status: 'success', responseTime: 89, requestSize: 128, responseSize: 768, timestamp: '2026-01-31 18:47:10' },
{ id: 'log_5', partnerId: 'feishu_001', partnerName: '飞书机器人', endpoint: '/api/open-api/query/user', method: 'GET', status: 'error', responseTime: 15, requestSize: 32, responseSize: 128, timestamp: '2026-01-31 18:46:00', errorMessage: '用户不存在' },
]
export default function OpenAPIPage() {
const [partners, setPartners] = useState<Partner[]>(DEFAULT_PARTNERS)
const [callLogs, setCallLogs] = useState<CallLog[]>(DEFAULT_CALL_LOGS)
const [activeTab, setActiveTab] = useState('guide')
const [selectedCategory, setSelectedCategory] = useState('all')
const [showAddDialog, setShowAddDialog] = useState(false)
const [showKeyDialog, setShowKeyDialog] = useState(false)
const [showDetailDialog, setShowDetailDialog] = useState(false)
const [selectedPartner, setSelectedPartner] = useState<Partner | null>(null)
const [selectedEndpoint, setSelectedEndpoint] = useState<OpenAPIEndpoint | null>(null)
const [copiedId, setCopiedId] = useState<string | null>(null)
const [apiBaseUrl, setApiBaseUrl] = useState('')
const [newPartner, setNewPartner] = useState({
name: '',
type: 'external' as 'external' | 'internal',
description: '',
permissions: {
dataIngest: true,
dataQuery: true,
tagEnrich: false,
batchProcess: false,
},
quotaConfig: {
dailyLimit: 5000,
monthlyLimit: 150000,
rateLimit: 50,
},
})
useEffect(() => {
const host = typeof window !== 'undefined' ? window.location.origin : ''
setApiBaseUrl(host)
}, [])
const copyToClipboard = (text: string, id: string) => {
navigator.clipboard.writeText(text)
setCopiedId(id)
setTimeout(() => setCopiedId(null), 2000)
}
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 getCategoryIcon = (category: string) => {
switch (category) {
case 'ingest': return <ArrowDownToLine className="h-4 w-4" />
case 'query': return <Search className="h-4 w-4" />
case 'tag': return <Tags className="h-4 w-4" />
case 'batch': return <Package className="h-4 w-4" />
default: return <Code className="h-4 w-4" />
}
}
const getCategoryName = (category: string) => {
switch (category) {
case 'ingest': return '数据流入'
case 'query': return '数据查询'
case 'tag': return '标签服务'
case 'batch': return '批量处理'
default: return category
}
}
const getStatusBadge = (status: string) => {
switch (status) {
case 'active':
return <Badge className="bg-green-100 text-green-700"></Badge>
case 'inactive':
return <Badge className="bg-gray-100 text-gray-700"></Badge>
case 'pending':
return <Badge className="bg-yellow-100 text-yellow-700"></Badge>
case 'success':
return <Badge className="bg-green-100 text-green-700"></Badge>
case 'error':
return <Badge className="bg-red-100 text-red-700"></Badge>
default:
return <Badge>{status}</Badge>
}
}
const filteredEndpoints = selectedCategory === 'all'
? OPEN_API_ENDPOINTS
: OPEN_API_ENDPOINTS.filter(e => e.category === selectedCategory)
const handleAddPartner = () => {
const newId = `partner_${Date.now()}`
const apiKey = `sk-${newPartner.type.slice(0, 3)}-${Math.random().toString(36).slice(2, 12)}`
const apiSecret = `sec-${newPartner.type.slice(0, 3)}-${Math.random().toString(36).slice(2, 12)}`
const partner: Partner = {
id: newId,
name: newPartner.name,
type: newPartner.type,
description: newPartner.description,
apiKey,
apiSecret,
status: 'pending',
permissions: newPartner.permissions,
quotaConfig: newPartner.quotaConfig,
statistics: {
totalCalls: 0,
todayCalls: 0,
monthCalls: 0,
lastCallTime: '-',
},
createdAt: new Date().toISOString().split('T')[0],
updatedAt: new Date().toISOString().split('T')[0],
}
setPartners([...partners, partner])
setShowAddDialog(false)
setNewPartner({
name: '',
type: 'external',
description: '',
permissions: {
dataIngest: true,
dataQuery: true,
tagEnrich: false,
batchProcess: false,
},
quotaConfig: {
dailyLimit: 5000,
monthlyLimit: 150000,
rateLimit: 50,
},
})
}
const togglePartnerStatus = (partnerId: string) => {
setPartners(partners.map(p => {
if (p.id === partnerId) {
return { ...p, status: p.status === 'active' ? 'inactive' : 'active' }
}
return p
}))
}
const togglePermission = (partnerId: string, permission: keyof Partner['permissions']) => {
setPartners(partners.map(p => {
if (p.id === partnerId) {
return {
...p,
permissions: {
...p.permissions,
[permission]: !p.permissions[permission],
},
}
}
return p
}))
}
// 计算统计数据
const totalCalls = partners.reduce((sum, p) => sum + p.statistics.todayCalls, 0)
const activePartners = partners.filter(p => p.status === 'active').length
const totalQuota = partners.reduce((sum, p) => sum + p.quotaConfig.dailyLimit, 0)
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"></p>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" onClick={() => setShowAddDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
</Button>
<Button>
<ExternalLink className="h-4 w-4 mr-2" />
API文档
</Button>
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-2xl font-bold text-gray-900">{partners.length}</p>
<p className="text-xs text-green-600">{activePartners} </p>
</div>
<div className="h-12 w-12 rounded-xl bg-purple-100 flex items-center justify-center">
<Users className="h-6 w-6 text-purple-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-2xl font-bold text-gray-900">{totalCalls.toLocaleString()}</p>
<p className="text-xs text-gray-500"> {totalQuota.toLocaleString()}</p>
</div>
<div className="h-12 w-12 rounded-xl bg-blue-100 flex items-center justify-center">
<Activity className="h-6 w-6 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-2xl font-bold text-gray-900">1,520</p>
<p className="text-xs text-green-600">+12.5% </p>
</div>
<div className="h-12 w-12 rounded-xl bg-green-100 flex items-center justify-center">
<ArrowDownToLine className="h-6 w-6 text-green-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-2xl font-bold text-gray-900">856</p>
<p className="text-xs text-purple-600">98.5% </p>
</div>
<div className="h-12 w-12 rounded-xl bg-orange-100 flex items-center justify-center">
<Tags className="h-6 w-6 text-orange-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* 快速开始指南 */}
<Card className="border-0 shadow-sm bg-gradient-to-r from-blue-50 to-purple-50">
<CardContent className="p-5">
<div className="flex items-start gap-6">
<div className="flex-1">
<h3 className="font-semibold text-gray-900 mb-3"></h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div className="flex items-start gap-2">
<div className="h-6 w-6 rounded-full bg-blue-500 text-white flex items-center justify-center text-xs font-bold shrink-0">1</div>
<div>
<p className="font-medium text-gray-900"></p>
<p className="text-gray-500">"密钥" API Key Secret</p>
</div>
</div>
<div className="flex items-start gap-2">
<div className="h-6 w-6 rounded-full bg-purple-500 text-white flex items-center justify-center text-xs font-bold shrink-0">2</div>
<div>
<p className="font-medium text-gray-900"></p>
<p className="text-gray-500">"配置"</p>
</div>
</div>
<div className="flex items-start gap-2">
<div className="h-6 w-6 rounded-full bg-green-500 text-white flex items-center justify-center text-xs font-bold shrink-0">3</div>
<div>
<p className="font-medium text-gray-900"></p>
<p className="text-gray-500"> Authorization: Bearer API_KEY </p>
</div>
</div>
</div>
</div>
<div className="hidden lg:block p-3 rounded-lg bg-gray-900 text-xs font-mono">
<p className="text-gray-400 mb-1"># </p>
<p className="text-green-400">curl -X POST "{apiBaseUrl}/api/open-api?endpoint=ingest/user" \</p>
<p className="text-blue-400 pl-4">-H "Authorization: Bearer sk-xxx" \</p>
<p className="text-yellow-400 pl-4">-d '{`{"phone":"138xxx","source":"ckb"}`}'</p>
</div>
</div>
</CardContent>
</Card>
{/* 主体内容 */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="bg-white/80">
<TabsTrigger value="guide">
<FileText className="h-4 w-4 mr-2" />
使
</TabsTrigger>
<TabsTrigger value="partners">
<Users className="h-4 w-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="endpoints">
<Code className="h-4 w-4 mr-2" />
API端点
</TabsTrigger>
<TabsTrigger value="logs">
<Clock className="h-4 w-4 mr-2" />
</TabsTrigger>
</TabsList>
{/* 使用说明 */}
<TabsContent value="guide" className="mt-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* 接口概述 */}
<Card className="border-0 shadow-sm bg-white/80">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<p className="text-gray-600">
API为第三方系统提供用户数据对接能力
</p>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Badge className="bg-green-100 text-green-700"></Badge>
<span className="text-gray-600"></span>
</div>
<div className="flex items-center gap-2">
<Badge className="bg-blue-100 text-blue-700"></Badge>
<span className="text-gray-600">RFM等</span>
</div>
<div className="flex items-center gap-2">
<Badge className="bg-purple-100 text-purple-700"></Badge>
<span className="text-gray-600">AI智能标签完善</span>
</div>
<div className="flex items-center gap-2">
<Badge className="bg-orange-100 text-orange-700"></Badge>
<span className="text-gray-600"></span>
</div>
</div>
</CardContent>
</Card>
{/* 认证方式 */}
<Card className="border-0 shadow-sm bg-white/80">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<p className="text-gray-600">API请求需在Header中携带API密钥</p>
<div className="p-3 rounded-lg bg-gray-900 text-xs font-mono space-y-1">
<p className="text-blue-400">Authorization: Bearer YOUR_API_KEY</p>
<p className="text-gray-500"># </p>
<p className="text-purple-400">X-API-Secret: YOUR_API_SECRET</p>
</div>
<p className="text-gray-500 text-xs">API Key Secret </p>
</CardContent>
</Card>
{/* 数据流入示例 */}
<Card className="border-0 shadow-sm bg-white/80">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="text-sm">
<div className="p-3 rounded-lg bg-gray-900 text-xs font-mono space-y-1">
<p className="text-gray-400"># </p>
<p className="text-green-400">POST /api/open-api?endpoint=ingest/user</p>
<p className="text-yellow-400">{`{`}</p>
<p className="text-white pl-4">"phone": "13800138000",</p>
<p className="text-white pl-4">"name": "张三",</p>
<p className="text-white pl-4">"source": "cunkebao",</p>
<p className="text-white pl-4">"tags": ["高意向", "电商用户"]</p>
<p className="text-yellow-400">{`}`}</p>
</div>
<p className="text-gray-500 text-xs mt-2">A级用户线...</p>
</CardContent>
</Card>
{/* 数据查询示例 */}
<Card className="border-0 shadow-sm bg-white/80">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="text-sm">
<div className="p-3 rounded-lg bg-gray-900 text-xs font-mono space-y-1">
<p className="text-gray-400"># </p>
<p className="text-green-400">GET /api/open-api?endpoint=query/user&phone=13800138000</p>
<p className="text-gray-400 mt-2"># </p>
<p className="text-yellow-400">{`{`}</p>
<p className="text-white pl-4">"phone": "138****8000",</p>
<p className="text-white pl-4">"tags": ["高价值用户", "电商活跃"],</p>
<p className="text-white pl-4">"level": "A",</p>
<p className="text-white pl-4">"rfm": {`{ "r": 5, "f": 4, "m": 5, "score": 93 }`}</p>
<p className="text-yellow-400">{`}`}</p>
</div>
</CardContent>
</Card>
{/* 常用端点 */}
<Card className="border-0 shadow-sm bg-white/80 lg:col-span-2">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<div className="p-3 rounded-lg bg-gray-50">
<Badge className="bg-blue-100 text-blue-700 mb-2">POST</Badge>
<p className="font-mono text-xs text-gray-700">ingest/user</p>
<p className="text-xs text-gray-500 mt-1"></p>
</div>
<div className="p-3 rounded-lg bg-gray-50">
<Badge className="bg-blue-100 text-blue-700 mb-2">POST</Badge>
<p className="font-mono text-xs text-gray-700">ingest/batch</p>
<p className="text-xs text-gray-500 mt-1"></p>
</div>
<div className="p-3 rounded-lg bg-gray-50">
<Badge className="bg-green-100 text-green-700 mb-2">GET</Badge>
<p className="font-mono text-xs text-gray-700">query/user</p>
<p className="text-xs text-gray-500 mt-1"></p>
</div>
<div className="p-3 rounded-lg bg-gray-50">
<Badge className="bg-blue-100 text-blue-700 mb-2">POST</Badge>
<p className="font-mono text-xs text-gray-700">tag/enrich</p>
<p className="text-xs text-gray-500 mt-1">AI标签完善</p>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* 接入方管理 */}
<TabsContent value="partners" className="mt-4">
<div className="space-y-4">
{partners.map(partner => (
<Card key={partner.id} className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<div className={`h-12 w-12 rounded-xl flex items-center justify-center ${
partner.type === 'external' ? 'bg-purple-100' : 'bg-blue-100'
}`}>
{partner.type === 'external' ? (
<Server className={`h-6 w-6 ${partner.type === 'external' ? 'text-purple-600' : 'text-blue-600'}`} />
) : (
<Shield className="h-6 w-6 text-blue-600" />
)}
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="font-semibold text-gray-900">{partner.name}</h3>
{getStatusBadge(partner.status)}
<Badge variant="outline" className="text-xs">
{partner.type === 'external' ? '外部系统' : '内部系统'}
</Badge>
</div>
<p className="text-sm text-gray-500 mt-1">{partner.description}</p>
<div className="flex items-center gap-4 mt-2 text-xs text-gray-400">
<span> {partner.createdAt}</span>
<span> {partner.statistics.lastCallTime}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedPartner(partner)
setShowKeyDialog(true)
}}
>
<Key className="h-4 w-4 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedPartner(partner)
setShowDetailDialog(true)
}}
>
<Settings className="h-4 w-4 mr-1" />
</Button>
<Switch
checked={partner.status === 'active'}
onCheckedChange={() => togglePartnerStatus(partner.id)}
/>
</div>
</div>
{/* 权限和配额 */}
<div className="mt-4 pt-4 border-t grid grid-cols-2 gap-4">
{/* 权限 */}
<div>
<h4 className="text-xs font-medium text-gray-500 mb-2"></h4>
<div className="flex flex-wrap gap-2">
<Badge
className={`cursor-pointer ${partner.permissions.dataIngest ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'}`}
onClick={() => togglePermission(partner.id, 'dataIngest')}
>
<ArrowDownToLine className="h-3 w-3 mr-1" />
</Badge>
<Badge
className={`cursor-pointer ${partner.permissions.dataQuery ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-500'}`}
onClick={() => togglePermission(partner.id, 'dataQuery')}
>
<Search className="h-3 w-3 mr-1" />
</Badge>
<Badge
className={`cursor-pointer ${partner.permissions.tagEnrich ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-500'}`}
onClick={() => togglePermission(partner.id, 'tagEnrich')}
>
<Tags className="h-3 w-3 mr-1" />
</Badge>
<Badge
className={`cursor-pointer ${partner.permissions.batchProcess ? 'bg-orange-100 text-orange-700' : 'bg-gray-100 text-gray-500'}`}
onClick={() => togglePermission(partner.id, 'batchProcess')}
>
<Package className="h-3 w-3 mr-1" />
</Badge>
</div>
</div>
{/* 统计 */}
<div>
<h4 className="text-xs font-medium text-gray-500 mb-2"></h4>
<div className="flex items-center gap-4 text-sm">
<div>
<span className="text-gray-500"></span>
<span className="ml-1 font-medium">{partner.statistics.todayCalls.toLocaleString()}</span>
<span className="text-gray-400 text-xs">/{partner.quotaConfig.dailyLimit.toLocaleString()}</span>
</div>
<div>
<span className="text-gray-500"></span>
<span className="ml-1 font-medium">{partner.statistics.monthCalls.toLocaleString()}</span>
</div>
<div>
<span className="text-gray-500"></span>
<span className="ml-1 font-medium">{partner.statistics.totalCalls.toLocaleString()}</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
{/* API端点 */}
<TabsContent value="endpoints" className="mt-4">
{/* 分类筛选 */}
<div className="flex items-center gap-2 mb-4">
<Button
variant={selectedCategory === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedCategory('all')}
>
({OPEN_API_ENDPOINTS.length})
</Button>
{['ingest', 'query', 'tag', 'batch'].map(cat => {
const count = OPEN_API_ENDPOINTS.filter(e => e.category === cat).length
return (
<Button
key={cat}
variant={selectedCategory === cat ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedCategory(cat)}
>
{getCategoryIcon(cat)}
<span className="ml-1">{getCategoryName(cat)} ({count})</span>
</Button>
)
})}
</div>
{/* 端点列表 */}
<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>
<Badge variant="outline" className="text-xs">
{getCategoryIcon(endpoint.category)}
<span className="ml-1">{getCategoryName(endpoint.category)}</span>
</Badge>
</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>
)}
{/* 示例展开 */}
<Button
variant="ghost"
size="sm"
className="w-full justify-start text-gray-500"
onClick={() => setSelectedEndpoint(selectedEndpoint?.id === endpoint.id ? null : endpoint)}
>
<Code className="h-4 w-4 mr-2" />
{selectedEndpoint?.id === endpoint.id ? '收起示例' : '查看请求/响应示例'}
</Button>
{selectedEndpoint?.id === endpoint.id && (
<div className="mt-3 space-y-3">
<div>
<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 overflow-x-auto">
{endpoint.example.request}
</pre>
</div>
<div>
<h4 className="text-xs font-medium text-gray-500 mb-2"></h4>
<pre className="bg-gray-900 text-blue-400 p-3 rounded-lg text-xs overflow-x-auto">
{endpoint.example.response}
</pre>
</div>
</div>
)}
</CardContent>
</Card>
))}
</div>
</TabsContent>
{/* 调用日志 */}
<TabsContent value="logs" className="mt-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium"></CardTitle>
<Button variant="outline" size="sm">
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></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.partnerName}</TableCell>
<TableCell className="font-mono text-xs">{log.endpoint}</TableCell>
<TableCell>
<Badge className={`${getMethodColor(log.method)} font-mono text-xs`}>
{log.method}
</Badge>
</TableCell>
<TableCell>{getStatusBadge(log.status)}</TableCell>
<TableCell className="text-sm">{log.responseTime}ms</TableCell>
<TableCell className="text-sm text-gray-500">
{(log.requestSize / 1024).toFixed(1)}KB / {(log.responseSize / 1024).toFixed(1)}KB
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 添加接入方弹窗 */}
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label> *</Label>
<Input
placeholder="例如:存客宝、点了码"
value={newPartner.name}
onChange={e => setNewPartner({ ...newPartner, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select
value={newPartner.type}
onValueChange={(v: 'external' | 'internal') => setNewPartner({ ...newPartner, type: v })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="external"></SelectItem>
<SelectItem value="internal"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Textarea
placeholder="描述接入方用途和数据来源"
value={newPartner.description}
onChange={e => setNewPartner({ ...newPartner, description: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex flex-wrap gap-2">
{[
{ key: 'dataIngest', label: '数据流入', icon: ArrowDownToLine },
{ key: 'dataQuery', label: '数据查询', icon: Search },
{ key: 'tagEnrich', label: '标签完善', icon: Tags },
{ key: 'batchProcess', label: '批量处理', icon: Package },
].map(({ key, label, icon: Icon }) => (
<Badge
key={key}
className={`cursor-pointer ${
newPartner.permissions[key as keyof typeof newPartner.permissions]
? 'bg-purple-100 text-purple-700'
: 'bg-gray-100 text-gray-500'
}`}
onClick={() => setNewPartner({
...newPartner,
permissions: {
...newPartner.permissions,
[key]: !newPartner.permissions[key as keyof typeof newPartner.permissions],
},
})}
>
<Icon className="h-3 w-3 mr-1" />
{label}
</Badge>
))}
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label></Label>
<Input
type="number"
value={newPartner.quotaConfig.dailyLimit}
onChange={e => setNewPartner({
...newPartner,
quotaConfig: { ...newPartner.quotaConfig, dailyLimit: parseInt(e.target.value) },
})}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
type="number"
value={newPartner.quotaConfig.monthlyLimit}
onChange={e => setNewPartner({
...newPartner,
quotaConfig: { ...newPartner.quotaConfig, monthlyLimit: parseInt(e.target.value) },
})}
/>
</div>
<div className="space-y-2">
<Label>(/)</Label>
<Input
type="number"
value={newPartner.quotaConfig.rateLimit}
onChange={e => setNewPartner({
...newPartner,
quotaConfig: { ...newPartner.quotaConfig, rateLimit: parseInt(e.target.value) },
})}
/>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAddDialog(false)}></Button>
<Button onClick={handleAddPartner} disabled={!newPartner.name}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* API密钥弹窗 */}
<Dialog open={showKeyDialog} onOpenChange={setShowKeyDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>API密钥 - {selectedPartner?.name}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedPartner && (
<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={selectedPartner.apiKey} readOnly className="font-mono" />
<Button
variant="outline"
size="icon"
onClick={() => copyToClipboard(selectedPartner.apiKey, 'apiKey')}
>
{copiedId === 'apiKey' ? <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={selectedPartner.apiSecret} readOnly className="font-mono" type="password" />
<Button
variant="outline"
size="icon"
onClick={() => copyToClipboard(selectedPartner.apiSecret, 'apiSecret')}
>
{copiedId === 'apiSecret' ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="p-3 rounded-lg bg-yellow-50 text-sm text-yellow-700">
<AlertCircle className="h-4 w-4 inline mr-2" />
</div>
<div className="p-4 rounded-lg bg-gray-50">
<h4 className="text-sm font-medium mb-2"></h4>
<pre className="bg-gray-900 text-green-400 p-3 rounded-lg text-xs overflow-x-auto">
{`curl -X POST "${apiBaseUrl}/api/open-api/ingest/user" \\
-H "Authorization: Bearer ${selectedPartner.apiKey}" \\
-H "X-API-Secret: ${selectedPartner.apiSecret}" \\
-H "Content-Type: application/json" \\
-d '{"phone": "13800138000", "source": "${selectedPartner.name.toLowerCase()}"}'`}
</pre>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setShowKeyDialog(false)}></Button>
<Button variant="destructive">
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 详细配置弹窗 */}
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle> - {selectedPartner?.name}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedPartner && (
<div className="space-y-6 py-4">
{/* 基本信息 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input value={selectedPartner.name} readOnly />
</div>
<div className="space-y-2">
<Label></Label>
<Input value={selectedPartner.type === 'external' ? '外部系统' : '内部系统'} readOnly />
</div>
</div>
{/* 权限配置 */}
<div className="space-y-3">
<Label></Label>
<div className="space-y-2">
{[
{ key: 'dataIngest', label: '数据流入', desc: '允许向神射手传入用户数据', icon: ArrowDownToLine },
{ key: 'dataQuery', label: '数据查询', desc: '允许查询用户画像和标签', icon: Search },
{ key: 'tagEnrich', label: '标签完善', desc: '允许触发AI标签完善', icon: Tags },
{ key: 'batchProcess', label: '批量处理', desc: '允许创建批量处理任务', icon: Package },
].map(({ key, label, desc, icon: Icon }) => (
<div key={key} className="flex items-center justify-between p-3 rounded-lg bg-gray-50">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-lg bg-white flex items-center justify-center">
<Icon className="h-4 w-4 text-gray-600" />
</div>
<div>
<p className="font-medium text-sm">{label}</p>
<p className="text-xs text-gray-500">{desc}</p>
</div>
</div>
<Switch
checked={selectedPartner.permissions[key as keyof Partner['permissions']]}
onCheckedChange={() => togglePermission(selectedPartner.id, key as keyof Partner['permissions'])}
/>
</div>
))}
</div>
</div>
{/* 配额配置 */}
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label></Label>
<Input type="number" defaultValue={selectedPartner.quotaConfig.dailyLimit} />
</div>
<div className="space-y-2">
<Label></Label>
<Input type="number" defaultValue={selectedPartner.quotaConfig.monthlyLimit} />
</div>
<div className="space-y-2">
<Label>(/)</Label>
<Input type="number" defaultValue={selectedPartner.quotaConfig.rateLimit} />
</div>
</div>
{/* 调用统计 */}
<div className="p-4 rounded-lg bg-gray-50">
<h4 className="text-sm font-medium mb-3"></h4>
<div className="grid grid-cols-4 gap-4 text-center">
<div>
<p className="text-2xl font-bold text-gray-900">{selectedPartner.statistics.todayCalls.toLocaleString()}</p>
<p className="text-xs text-gray-500"></p>
</div>
<div>
<p className="text-2xl font-bold text-gray-900">{selectedPartner.statistics.monthCalls.toLocaleString()}</p>
<p className="text-xs text-gray-500"></p>
</div>
<div>
<p className="text-2xl font-bold text-gray-900">{selectedPartner.statistics.totalCalls.toLocaleString()}</p>
<p className="text-xs text-gray-500"></p>
</div>
<div>
<p className="text-sm font-medium text-gray-900">{selectedPartner.statistics.lastCallTime}</p>
<p className="text-xs text-gray-500"></p>
</div>
</div>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setShowDetailDialog(false)}></Button>
<Button></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
)
}