chore: 以本地为准,上传全部并替换 GitHub
This commit is contained in:
452
app/data-market/api/page.tsx
Normal file
452
app/data-market/api/page.tsx
Normal file
@@ -0,0 +1,452 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } 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 {
|
||||
Server,
|
||||
Copy,
|
||||
CheckCircle2,
|
||||
Key,
|
||||
RefreshCw,
|
||||
ExternalLink,
|
||||
Code,
|
||||
Zap,
|
||||
Users,
|
||||
Search,
|
||||
Tags,
|
||||
Brain,
|
||||
Database,
|
||||
FileText,
|
||||
Package,
|
||||
Activity,
|
||||
} from "lucide-react"
|
||||
|
||||
// API端点接口
|
||||
interface APIEndpoint {
|
||||
id: string
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
path: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
auth: boolean
|
||||
params?: { name: string; type: string; required: boolean; desc: string }[]
|
||||
response?: string
|
||||
}
|
||||
|
||||
// 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查询完整用户画像',
|
||||
category: 'query',
|
||||
auth: true,
|
||||
params: [
|
||||
{ name: 'phone', type: 'string', required: false, desc: '11位手机号' },
|
||||
{ name: 'qq', type: 'string', required: false, desc: 'QQ号码' },
|
||||
],
|
||||
response: '{ "user": { "phone": "138xxx", "rfm": 85, "level": "A", "tags": [...] } }',
|
||||
},
|
||||
{
|
||||
id: 'api_2',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/users/batch',
|
||||
name: '批量用户查询',
|
||||
description: '批量查询多个用户的画像信息',
|
||||
category: 'query',
|
||||
auth: true,
|
||||
params: [
|
||||
{ name: 'phones', type: 'array', required: false, desc: '手机号数组' },
|
||||
{ name: 'qqs', type: 'array', required: false, desc: 'QQ号数组' },
|
||||
],
|
||||
},
|
||||
// 标签服务
|
||||
{
|
||||
id: 'api_3',
|
||||
method: 'GET',
|
||||
path: '/api/shensheshou/tags',
|
||||
name: '标签列表',
|
||||
description: '获取所有可用标签',
|
||||
category: 'tag',
|
||||
auth: true,
|
||||
params: [
|
||||
{ name: 'category', type: 'string', required: false, desc: '标签分类' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'api_4',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/tags/apply',
|
||||
name: '应用标签',
|
||||
description: '为用户应用指定标签',
|
||||
category: 'tag',
|
||||
auth: true,
|
||||
params: [
|
||||
{ name: 'userId', type: 'string', required: true, desc: '用户ID' },
|
||||
{ name: 'tags', type: 'array', required: true, desc: '标签ID数组' },
|
||||
],
|
||||
},
|
||||
// AI服务
|
||||
{
|
||||
id: 'api_5',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/chat',
|
||||
name: 'AI对话',
|
||||
description: '与神射手AI进行对话,支持自然语言查询',
|
||||
category: 'ai',
|
||||
auth: true,
|
||||
params: [
|
||||
{ name: 'message', type: 'string', required: true, desc: '对话内容' },
|
||||
],
|
||||
response: '{ "success": true, "response": { "content": "查询结果...", "data": {...} } }',
|
||||
},
|
||||
{
|
||||
id: 'api_6',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/ai/analyze',
|
||||
name: 'AI数据分析',
|
||||
description: 'AI自动分析数据并生成洞察',
|
||||
category: 'ai',
|
||||
auth: true,
|
||||
params: [
|
||||
{ name: 'type', type: 'string', required: true, desc: '分析类型: rfm/behavior/preference' },
|
||||
{ name: 'filters', type: 'object', required: false, desc: '筛选条件' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'api_7',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/ai/tag',
|
||||
name: 'AI智能打标',
|
||||
description: 'AI自动为用户打标签',
|
||||
category: 'ai',
|
||||
auth: true,
|
||||
params: [
|
||||
{ name: 'source', type: 'string', required: true, desc: '数据源' },
|
||||
{ name: 'model', type: 'string', required: false, desc: 'AI模型: qwen/deepseek' },
|
||||
],
|
||||
},
|
||||
// 数据服务
|
||||
{
|
||||
id: 'api_8',
|
||||
method: 'GET',
|
||||
path: '/api/shensheshou/sources',
|
||||
name: '数据源列表',
|
||||
description: '获取所有数据源连接状态',
|
||||
category: 'data',
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
id: 'api_9',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/ingest',
|
||||
name: '数据导入',
|
||||
description: '导入外部数据并通过AI标签引擎处理',
|
||||
category: 'data',
|
||||
auth: true,
|
||||
params: [
|
||||
{ name: 'source', type: 'string', required: true, desc: '数据源标识' },
|
||||
{ name: 'target', type: 'string', required: true, desc: '目标表' },
|
||||
],
|
||||
},
|
||||
// 报告服务
|
||||
{
|
||||
id: 'api_10',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/report/generate',
|
||||
name: '生成报告',
|
||||
description: 'AI自动生成数据分析报告',
|
||||
category: 'report',
|
||||
auth: true,
|
||||
params: [
|
||||
{ name: 'template', type: 'string', required: true, desc: '报告模板ID' },
|
||||
{ name: 'dateRange', type: 'string', required: false, desc: '日期范围' },
|
||||
],
|
||||
},
|
||||
// 流量包
|
||||
{
|
||||
id: 'api_11',
|
||||
method: 'GET',
|
||||
path: '/api/shensheshou/packages',
|
||||
name: '流量包列表',
|
||||
description: '获取所有流量包',
|
||||
category: 'package',
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
id: 'api_12',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/packages/create',
|
||||
name: '创建流量包',
|
||||
description: '根据筛选条件创建流量包',
|
||||
category: 'package',
|
||||
auth: true,
|
||||
params: [
|
||||
{ name: 'name', type: 'string', required: true, desc: '流量包名称' },
|
||||
{ name: 'criteria', type: 'object', required: true, desc: '筛选条件' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'api_13',
|
||||
method: 'POST',
|
||||
path: '/api/shensheshou/packages/send',
|
||||
name: '发送流量包',
|
||||
description: '发送流量包到邮箱/飞书/微信',
|
||||
category: 'package',
|
||||
auth: true,
|
||||
params: [
|
||||
{ name: 'packageId', type: 'string', required: true, desc: '流量包ID' },
|
||||
{ name: 'targets', type: 'array', required: true, desc: '发送目标' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default function APIServicePage() {
|
||||
const [activeCategory, setActiveCategory] = useState('all')
|
||||
const [apiBaseUrl, setApiBaseUrl] = useState('')
|
||||
const [showKeyDialog, setShowKeyDialog] = useState(false)
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
|
||||
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 filteredEndpoints = activeCategory === 'all'
|
||||
? API_ENDPOINTS
|
||||
: API_ENDPOINTS.filter(e => e.category === activeCategory)
|
||||
|
||||
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">
|
||||
<Button variant="outline" onClick={() => setShowKeyDialog(true)}>
|
||||
<Key className="h-4 w-4 mr-2" />
|
||||
API密钥
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
API文档
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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-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密钥:</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 "Content-Type: application/json"`}
|
||||
</pre>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 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>
|
||||
{endpoint.auth && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Key className="h-3 w-3 mr-1" />
|
||||
需认证
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* 响应示例 */}
|
||||
{endpoint.response && (
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-gray-500 mb-2">响应示例</h4>
|
||||
<pre className="bg-gray-100 p-2 rounded text-xs font-mono text-gray-700 overflow-x-auto">
|
||||
{endpoint.response}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* API密钥弹窗 */}
|
||||
<Dialog open={showKeyDialog} onOpenChange={setShowKeyDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>API密钥管理</DialogTitle>
|
||||
<DialogDescription>用于第三方系统调用神射手API</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>当前API密钥</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input value="sk-archer-xxxxxxxxxxxxx" readOnly className="font-mono" />
|
||||
<Button variant="outline" size="icon">
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">密钥创建于 2024-01-15,永不过期</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-yellow-50 text-sm text-yellow-700">
|
||||
⚠️ 请妥善保管API密钥,不要在客户端代码中暴露
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1460
app/data-market/open-api/page.tsx
Normal file
1460
app/data-market/open-api/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
522
app/data-market/packages/page.tsx
Normal file
522
app/data-market/packages/page.tsx
Normal file
@@ -0,0 +1,522 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import {
|
||||
Package,
|
||||
Plus,
|
||||
Download,
|
||||
Send,
|
||||
Mail,
|
||||
MessageSquare,
|
||||
Users,
|
||||
Calendar,
|
||||
RefreshCw,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Target,
|
||||
Loader2,
|
||||
Settings,
|
||||
Trash2,
|
||||
Copy,
|
||||
} from "lucide-react"
|
||||
|
||||
// 流量包接口
|
||||
interface TrafficPackage {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
pool: string
|
||||
userCount: number
|
||||
criteria: string[]
|
||||
createdAt: string
|
||||
status: 'active' | 'expired' | 'processing'
|
||||
downloadCount: number
|
||||
lastSentAt?: string
|
||||
sentTo?: string[]
|
||||
}
|
||||
|
||||
// 发送目标
|
||||
interface SendTarget {
|
||||
type: 'email' | 'wechat' | 'feishu'
|
||||
value: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// 预定义流量包
|
||||
const DEFAULT_PACKAGES: TrafficPackage[] = [
|
||||
{
|
||||
id: 'pkg_1',
|
||||
name: '钻石用户包-2024Q1',
|
||||
description: 'RFM≥90的高价值用户,适合高端产品推广',
|
||||
pool: 'diamond',
|
||||
userCount: 12500000,
|
||||
criteria: ['RFM≥90', '月活跃≥20天', '消费≥10次'],
|
||||
createdAt: '2024-01-15',
|
||||
status: 'active',
|
||||
downloadCount: 15,
|
||||
lastSentAt: '2024-01-28',
|
||||
sentTo: ['marketing@company.com', '飞书-营销群'],
|
||||
},
|
||||
{
|
||||
id: 'pkg_2',
|
||||
name: '厦门本地高价值用户',
|
||||
description: '厦门地区A级以上用户',
|
||||
pool: 'gold',
|
||||
userCount: 850000,
|
||||
criteria: ['地区=厦门', 'RFM≥80'],
|
||||
createdAt: '2024-01-20',
|
||||
status: 'active',
|
||||
downloadCount: 8,
|
||||
},
|
||||
{
|
||||
id: 'pkg_3',
|
||||
name: '电商活跃用户包',
|
||||
description: '京东、淘宝高频购买用户',
|
||||
pool: 'silver',
|
||||
userCount: 45000000,
|
||||
criteria: ['电商消费≥5次/月', '客单价≥200'],
|
||||
createdAt: '2024-01-22',
|
||||
status: 'active',
|
||||
downloadCount: 22,
|
||||
lastSentAt: '2024-01-29',
|
||||
sentTo: ['sales@company.com'],
|
||||
},
|
||||
{
|
||||
id: 'pkg_4',
|
||||
name: '沉默用户唤醒包',
|
||||
description: '30天未活跃但曾有高价值行为的用户',
|
||||
pool: 'potential',
|
||||
userCount: 28000000,
|
||||
criteria: ['30天未活跃', '历史RFM≥60'],
|
||||
createdAt: '2024-01-25',
|
||||
status: 'active',
|
||||
downloadCount: 5,
|
||||
},
|
||||
]
|
||||
|
||||
// 预定义发送目标
|
||||
const SAVED_TARGETS: SendTarget[] = [
|
||||
{ type: 'email', value: 'marketing@company.com', name: '营销部邮箱' },
|
||||
{ type: 'email', value: 'sales@company.com', name: '销售部邮箱' },
|
||||
{ type: 'feishu', value: 'oc_xxx', name: '飞书-营销群' },
|
||||
{ type: 'feishu', value: 'oc_yyy', name: '飞书-运营群' },
|
||||
{ type: 'wechat', value: 'wxid_xxx', name: '企微-客户群' },
|
||||
]
|
||||
|
||||
export default function PackagesPage() {
|
||||
const [packages, setPackages] = useState<TrafficPackage[]>(DEFAULT_PACKAGES)
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const [showSendDialog, setShowSendDialog] = useState(false)
|
||||
const [selectedPackage, setSelectedPackage] = useState<TrafficPackage | null>(null)
|
||||
const [sending, setSending] = useState(false)
|
||||
const [selectedTargets, setSelectedTargets] = useState<string[]>([])
|
||||
const [customEmail, setCustomEmail] = useState('')
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
|
||||
return num.toLocaleString()
|
||||
}
|
||||
|
||||
const getPoolConfig = (pool: string) => {
|
||||
switch (pool) {
|
||||
case 'diamond': return { name: '钻石池', icon: '💎', color: 'from-blue-500 to-purple-600' }
|
||||
case 'gold': return { name: '黄金池', icon: '🏆', color: 'from-yellow-400 to-orange-500' }
|
||||
case 'silver': return { name: '白银池', icon: '🥈', color: 'from-gray-300 to-gray-500' }
|
||||
case 'bronze': return { name: '青铜池', icon: '🥉', color: 'from-orange-300 to-orange-500' }
|
||||
default: return { name: '潜力池', icon: '🌱', color: 'from-green-300 to-green-500' }
|
||||
}
|
||||
}
|
||||
|
||||
const getTargetIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'email': return <Mail className="h-4 w-4" />
|
||||
case 'feishu': return <span className="text-sm">🪶</span>
|
||||
case 'wechat': return <MessageSquare className="h-4 w-4" />
|
||||
default: return <Send className="h-4 w-4" />
|
||||
}
|
||||
}
|
||||
|
||||
const toggleTarget = (value: string) => {
|
||||
setSelectedTargets(prev =>
|
||||
prev.includes(value)
|
||||
? prev.filter(t => t !== value)
|
||||
: [...prev, value]
|
||||
)
|
||||
}
|
||||
|
||||
const openSendDialog = (pkg: TrafficPackage) => {
|
||||
setSelectedPackage(pkg)
|
||||
setSelectedTargets([])
|
||||
setCustomEmail('')
|
||||
setShowSendDialog(true)
|
||||
}
|
||||
|
||||
// 下载流量包为CSV
|
||||
const downloadPackage = async (pkg: TrafficPackage) => {
|
||||
try {
|
||||
// 从API获取用户数据
|
||||
const res = await fetch(`/api/traffic-packages?action=export&packageId=${pkg.id}`)
|
||||
const data = await res.json()
|
||||
|
||||
if (data.success && data.users) {
|
||||
// 生成CSV内容
|
||||
const headers = ['手机号', '姓名', '等级', '估值分', '标签']
|
||||
const rows = data.users.map((u: any) => [
|
||||
u.phone || '',
|
||||
u.name || '',
|
||||
u.level || '',
|
||||
u.score || '',
|
||||
(u.tags || []).join(';')
|
||||
])
|
||||
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...rows.map((r: string[]) => r.join(','))
|
||||
].join('\n')
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${pkg.name}_${new Date().toISOString().split('T')[0]}.csv`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
// 更新下载次数
|
||||
setPackages(prev => prev.map(p =>
|
||||
p.id === pkg.id ? { ...p, downloadCount: p.downloadCount + 1 } : p
|
||||
))
|
||||
} else {
|
||||
alert('导出失败:' + (data.error || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
alert('下载失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
const sendPackage = async () => {
|
||||
if (!selectedPackage) return
|
||||
setSending(true)
|
||||
|
||||
try {
|
||||
// 调用API发送流量包
|
||||
const targets = [...selectedTargets]
|
||||
if (customEmail) targets.push(customEmail)
|
||||
|
||||
await fetch('/api/traffic-packages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'send',
|
||||
packageId: selectedPackage.id,
|
||||
targets,
|
||||
})
|
||||
})
|
||||
|
||||
// 更新包状态
|
||||
setPackages(packages.map(p => p.id === selectedPackage.id ? {
|
||||
...p,
|
||||
lastSentAt: new Date().toLocaleString(),
|
||||
sentTo: targets,
|
||||
} : p))
|
||||
|
||||
} catch (error) {
|
||||
console.error('发送失败:', error)
|
||||
} finally {
|
||||
setSending(false)
|
||||
setShowSendDialog(false)
|
||||
}
|
||||
}
|
||||
|
||||
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">流量包</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">管理和分发用户流量包</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
<Button onClick={() => setShowCreateDialog(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
创建流量包
|
||||
</Button>
|
||||
</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 justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">流量包总数</p>
|
||||
<p className="text-2xl font-bold">{packages.length}</p>
|
||||
</div>
|
||||
<Package className="h-8 w-8 text-purple-500 opacity-50" />
|
||||
</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">{formatNumber(packages.reduce((sum, p) => sum + p.userCount, 0))}</p>
|
||||
</div>
|
||||
<Users className="h-8 w-8 text-blue-500 opacity-50" />
|
||||
</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">{packages.reduce((sum, p) => sum + p.downloadCount, 0)}</p>
|
||||
</div>
|
||||
<Download className="h-8 w-8 text-green-500 opacity-50" />
|
||||
</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">12</p>
|
||||
</div>
|
||||
<Send className="h-8 w-8 text-orange-500 opacity-50" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 流量包列表 */}
|
||||
<div className="space-y-3">
|
||||
{packages.map(pkg => {
|
||||
const poolConfig = getPoolConfig(pkg.pool)
|
||||
|
||||
return (
|
||||
<Card key={pkg.id} className="border-0 shadow-sm bg-white/80 hover:shadow-md transition-all">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-14 h-14 rounded-xl bg-gradient-to-r ${poolConfig.color} flex items-center justify-center text-2xl`}>
|
||||
{poolConfig.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-gray-900">{pkg.name}</h3>
|
||||
<Badge className="bg-green-100 text-green-700">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />可用
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mb-2">{pkg.description}</p>
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{pkg.criteria.map((c, i) => (
|
||||
<Badge key={i} variant="outline" className="text-xs">{c}</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="h-3 w-3" />
|
||||
{formatNumber(pkg.userCount)} 用户
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
创建于 {pkg.createdAt}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Download className="h-3 w-3" />
|
||||
下载 {pkg.downloadCount} 次
|
||||
</span>
|
||||
{pkg.lastSentAt && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Send className="h-3 w-3" />
|
||||
上次发送: {pkg.lastSentAt}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => downloadPackage(pkg)}>
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
下载
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => openSendDialog(pkg)}>
|
||||
<Send className="h-4 w-4 mr-1" />
|
||||
发送
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 发送流量包弹窗 */}
|
||||
<Dialog open={showSendDialog} onOpenChange={setShowSendDialog}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>发送流量包</DialogTitle>
|
||||
<DialogDescription>选择发送目标,支持邮箱、微信群、飞书群</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedPackage && (
|
||||
<div className="space-y-4 py-4">
|
||||
{/* 流量包信息 */}
|
||||
<div className="p-3 rounded-lg bg-purple-50">
|
||||
<div className="font-medium text-gray-900">{selectedPackage.name}</div>
|
||||
<div className="text-sm text-gray-500">{formatNumber(selectedPackage.userCount)} 用户</div>
|
||||
</div>
|
||||
|
||||
{/* 已保存的目标 */}
|
||||
<div className="space-y-2">
|
||||
<Label>选择发送目标</Label>
|
||||
<div className="space-y-2 max-h-[200px] overflow-y-auto">
|
||||
{SAVED_TARGETS.map(target => (
|
||||
<div
|
||||
key={target.value}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-all ${
|
||||
selectedTargets.includes(target.value)
|
||||
? 'border-purple-500 bg-purple-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
onClick={() => toggleTarget(target.value)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{getTargetIcon(target.type)}
|
||||
<div>
|
||||
<div className="font-medium text-sm">{target.name}</div>
|
||||
<div className="text-xs text-gray-500">{target.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Checkbox checked={selectedTargets.includes(target.value)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 自定义邮箱 */}
|
||||
<div className="space-y-2">
|
||||
<Label>或输入自定义邮箱</Label>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="example@email.com"
|
||||
value={customEmail}
|
||||
onChange={(e) => setCustomEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 发送说明 */}
|
||||
<div className="p-3 rounded-lg bg-blue-50 text-sm text-blue-700">
|
||||
<p>📌 发送说明:</p>
|
||||
<ul className="list-disc list-inside text-xs mt-1 space-y-1">
|
||||
<li>邮箱发送:将生成CSV附件发送到指定邮箱</li>
|
||||
<li>飞书群:通过飞书机器人发送流量包链接</li>
|
||||
<li>微信群:通过企微机器人发送流量包链接</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowSendDialog(false)}>取消</Button>
|
||||
<Button
|
||||
onClick={sendPackage}
|
||||
disabled={sending || (selectedTargets.length === 0 && !customEmail)}
|
||||
>
|
||||
{sending ? (
|
||||
<><Loader2 className="h-4 w-4 animate-spin mr-2" />发送中...</>
|
||||
) : (
|
||||
<><Send className="h-4 w-4 mr-2" />确认发送</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 创建流量包弹窗 */}
|
||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建流量包</DialogTitle>
|
||||
<DialogDescription>从人群圈选创建流量包</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>流量包名称</Label>
|
||||
<Input placeholder="例如:高价值用户包-2024Q1" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>来源</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择来源" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="crowd">从人群圈选导入</SelectItem>
|
||||
<SelectItem value="pool">从流量池导入</SelectItem>
|
||||
<SelectItem value="tag">从标签筛选导入</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>描述</Label>
|
||||
<Textarea placeholder="描述该流量包的用途..." />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowCreateDialog(false)}>取消</Button>
|
||||
<Button onClick={() => {
|
||||
setShowCreateDialog(false)
|
||||
// 跳转到人群圈选页面
|
||||
window.location.href = '/tag-portrait/crowd'
|
||||
}}>
|
||||
<Target className="h-4 w-4 mr-2" />
|
||||
去圈选
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
326
app/data-market/page.tsx
Normal file
326
app/data-market/page.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
Package,
|
||||
Server,
|
||||
Webhook,
|
||||
ArrowUpRight,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Zap,
|
||||
DollarSign,
|
||||
Loader2,
|
||||
Diamond,
|
||||
Award,
|
||||
Medal,
|
||||
Shield,
|
||||
Target,
|
||||
BarChart3,
|
||||
Globe,
|
||||
ArrowDownToLine,
|
||||
ArrowUpFromLine,
|
||||
} from "lucide-react"
|
||||
|
||||
export default function DataMarketPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [pools, setPools] = useState<any[]>([])
|
||||
const [stats, setStats] = useState({
|
||||
totalPackages: 0,
|
||||
totalApis: 8,
|
||||
totalSubscriptions: 45,
|
||||
monthlyRevenue: 125000,
|
||||
apiCalls: 8956234
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/traffic-packages?action=pools')
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
setPools(data.pools)
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
totalPackages: data.pools.length
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
|
||||
return num.toLocaleString()
|
||||
}
|
||||
|
||||
// 流量池图标映射
|
||||
const poolIcons: Record<string, any> = {
|
||||
diamond: Diamond,
|
||||
gold: Award,
|
||||
silver: Medal,
|
||||
bronze: Shield,
|
||||
potential: Target
|
||||
}
|
||||
|
||||
// 流量池颜色映射
|
||||
const poolColors: Record<string, string> = {
|
||||
diamond: 'from-purple-500 to-pink-500',
|
||||
gold: 'from-yellow-500 to-orange-500',
|
||||
silver: 'from-gray-400 to-gray-500',
|
||||
bronze: 'from-orange-400 to-orange-600',
|
||||
potential: 'from-blue-400 to-cyan-500'
|
||||
}
|
||||
|
||||
const modules = [
|
||||
{
|
||||
title: "流量包",
|
||||
icon: Package,
|
||||
color: "text-purple-600",
|
||||
bg: "bg-purple-100",
|
||||
href: "/data-market/packages",
|
||||
desc: "用户人群包创建与导出,按流量池分层管理",
|
||||
stats: `${pools.length}个流量池`,
|
||||
},
|
||||
{
|
||||
title: "API市场",
|
||||
icon: Server,
|
||||
color: "text-blue-600",
|
||||
bg: "bg-blue-100",
|
||||
href: "/data-market/api",
|
||||
desc: "开放API接口服务,支持用户查询、画像等",
|
||||
stats: `${stats.totalApis}个API`,
|
||||
},
|
||||
{
|
||||
title: "开放接口",
|
||||
icon: Globe,
|
||||
color: "text-orange-600",
|
||||
bg: "bg-orange-100",
|
||||
href: "/data-market/open-api",
|
||||
desc: "第三方系统对接,存客宝/点了码数据流入流出",
|
||||
stats: "3个接入方",
|
||||
},
|
||||
{
|
||||
title: "数据订阅",
|
||||
icon: Webhook,
|
||||
color: "text-green-600",
|
||||
bg: "bg-green-100",
|
||||
href: "/data-market/subscription",
|
||||
desc: "实时数据推送服务,支持Webhook订阅",
|
||||
stats: `${stats.totalSubscriptions}个订阅`,
|
||||
},
|
||||
]
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20 flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
<span className="ml-3 text-gray-500">加载数据市场...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">数据市场</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
用户数据资产变现与开放服务平台
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<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">{pools.length}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-purple-100 flex items-center justify-center">
|
||||
<Package className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">API接口</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.totalApis}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center">
|
||||
<Server className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">API调用</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{formatNumber(stats.apiCalls)}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-green-100 flex items-center justify-center">
|
||||
<Zap className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||||
<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">¥{formatNumber(stats.monthlyRevenue)}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-orange-100 flex items-center justify-center">
|
||||
<DollarSign className="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 功能模块 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{modules.map((module, index) => (
|
||||
<Link key={index} href={module.href}>
|
||||
<Card className="h-full cursor-pointer hover:shadow-lg transition-all duration-300 border-none shadow-sm bg-white/70 backdrop-blur group hover:-translate-y-1">
|
||||
<CardContent className="p-6">
|
||||
<div className={`w-14 h-14 rounded-2xl ${module.bg} flex items-center justify-center mb-4 group-hover:scale-110 transition-transform`}>
|
||||
<module.icon className={`w-7 h-7 ${module.color}`} />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-2">{module.title}</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">{module.desc}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="secondary">{module.stats}</Badge>
|
||||
<ArrowUpRight className="w-5 h-5 text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 流量池资产 */}
|
||||
<Card className="border-none shadow-lg bg-white/80 backdrop-blur">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Target className="h-5 w-5 text-purple-500" />
|
||||
可变现数据资产
|
||||
<Badge variant="outline" className="ml-2">按价值分层</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
{pools.map((pool) => {
|
||||
const PoolIcon = poolIcons[pool.id] || Package
|
||||
const color = poolColors[pool.id] || 'from-gray-400 to-gray-500'
|
||||
|
||||
return (
|
||||
<Link key={pool.id} href={`/data-market/packages?pool=${pool.id}`}>
|
||||
<Card className="cursor-pointer hover:shadow-lg transition-all duration-300 border-0 overflow-hidden group">
|
||||
<div className={`h-2 bg-gradient-to-r ${color}`} />
|
||||
<CardContent className="p-4">
|
||||
<div className={`w-12 h-12 rounded-xl bg-gradient-to-r ${color} flex items-center justify-center mb-3 group-hover:scale-110 transition-transform`}>
|
||||
<PoolIcon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold text-gray-900 mb-1">{pool.name}</h3>
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<span className="text-xl font-bold text-gray-900">
|
||||
{formatNumber(pool.count || 0)}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">用户</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 数据变现趋势 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="border-none shadow-md bg-white/80">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4 text-green-500" />
|
||||
API调用趋势
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ name: '用户画像API', calls: 3500000, growth: 12.5 },
|
||||
{ name: '智能搜索API', calls: 2800000, growth: 8.3 },
|
||||
{ name: '人群圈选API', calls: 1500000, growth: 25.6 },
|
||||
{ name: 'RFM评分API', calls: 1156234, growth: 15.2 },
|
||||
].map((api, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 rounded-lg bg-gray-50">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{api.name}</div>
|
||||
<div className="text-sm text-gray-500">{formatNumber(api.calls)} 次调用</div>
|
||||
</div>
|
||||
<Badge className="bg-green-100 text-green-700">
|
||||
+{api.growth}%
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-none shadow-md bg-white/80">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<BarChart3 className="h-4 w-4 text-blue-500" />
|
||||
数据订阅分布
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ name: '用户行为数据', percentage: 35 },
|
||||
{ name: '交易数据', percentage: 28 },
|
||||
{ name: '标签更新', percentage: 22 },
|
||||
{ name: '用户画像变更', percentage: 15 },
|
||||
].map((sub, i) => (
|
||||
<div key={i}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm text-gray-700">{sub.name}</span>
|
||||
<span className="text-sm font-medium">{sub.percentage}%</span>
|
||||
</div>
|
||||
<Progress value={sub.percentage} className="h-2" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user