sync: soul-admin 页面 | 原因: 前端页面修改
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { RefreshCw, Users, UserCheck, TrendingUp, Zap, Link2, CheckCircle2, XCircle } from 'lucide-react'
|
||||
import {
|
||||
RefreshCw, Users, UserCheck, TrendingUp, Zap, Link2,
|
||||
CheckCircle2, XCircle, DollarSign, Activity, Smartphone,
|
||||
} from 'lucide-react'
|
||||
import { get, post } from '@/api/client'
|
||||
|
||||
interface MatchStats {
|
||||
@@ -10,29 +15,71 @@ interface MatchStats {
|
||||
todayMatches: number
|
||||
byType: { matchType: string; count: number }[]
|
||||
uniqueUsers: number
|
||||
matchRevenue?: number
|
||||
paidMatchCount?: number
|
||||
}
|
||||
|
||||
interface CKBTestResult {
|
||||
endpoint: string
|
||||
label: string
|
||||
description: string
|
||||
method: 'GET' | 'POST'
|
||||
status: 'idle' | 'testing' | 'success' | 'error'
|
||||
message?: string
|
||||
responseTime?: number
|
||||
ckbResponse?: string
|
||||
}
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
partner: '超级个体', investor: '资源对接', mentor: '导师顾问', team: '团队招募',
|
||||
}
|
||||
const typeIcons: Record<string, string> = {
|
||||
partner: '⭐', investor: '👥', mentor: '❤️', team: '🎮',
|
||||
}
|
||||
|
||||
export function CKBStatsTab() {
|
||||
const [stats, setStats] = useState<MatchStats | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [ckbTests, setCkbTests] = useState<CKBTestResult[]>([
|
||||
{ endpoint: '/api/ckb/join', label: 'CKB 加入(ckb/join)', status: 'idle' },
|
||||
{ endpoint: '/api/ckb/match', label: 'CKB 匹配上报(ckb/match)', status: 'idle' },
|
||||
{ endpoint: '/api/miniprogram/ckb/lead', label: 'CKB 链接卡若(ckb/lead)', status: 'idle' },
|
||||
{ endpoint: '/api/match/config', label: '匹配配置(match/config)', status: 'idle' },
|
||||
])
|
||||
const [testPhone, setTestPhone] = useState('13800000000')
|
||||
const [testWechat, setTestWechat] = useState('')
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
partner: '超级个体', investor: '资源对接', mentor: '导师顾问', team: '团队招募',
|
||||
}
|
||||
const [ckbTests, setCkbTests] = useState<CKBTestResult[]>([
|
||||
{
|
||||
endpoint: '/api/ckb/join', label: '场景获客 — 加入(partner)',
|
||||
description: '用测试手机号添加到存客宝「创业合伙」计划',
|
||||
method: 'POST', status: 'idle',
|
||||
},
|
||||
{
|
||||
endpoint: '/api/ckb/join', label: '场景获客 — 加入(investor)',
|
||||
description: '用测试手机号添加到存客宝「资源对接」计划',
|
||||
method: 'POST', status: 'idle',
|
||||
},
|
||||
{
|
||||
endpoint: '/api/ckb/join', label: '场景获客 — 加入(mentor)',
|
||||
description: '用测试手机号添加到存客宝「导师顾问」计划',
|
||||
method: 'POST', status: 'idle',
|
||||
},
|
||||
{
|
||||
endpoint: '/api/ckb/join', label: '场景获客 — 加入(team)',
|
||||
description: '用测试手机号添加到存客宝「团队招募」计划',
|
||||
method: 'POST', status: 'idle',
|
||||
},
|
||||
{
|
||||
endpoint: '/api/ckb/match', label: '匹配上报',
|
||||
description: '上报匹配行为到存客宝',
|
||||
method: 'POST', status: 'idle',
|
||||
},
|
||||
{
|
||||
endpoint: '/api/miniprogram/ckb/lead', label: '链接卡若',
|
||||
description: '首页「链接卡若」留资到存客宝',
|
||||
method: 'POST', status: 'idle',
|
||||
},
|
||||
{
|
||||
endpoint: '/api/match/config', label: '匹配配置',
|
||||
description: '获取匹配类型、价格等配置',
|
||||
method: 'GET', status: 'idle',
|
||||
},
|
||||
])
|
||||
|
||||
const loadStats = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
@@ -41,14 +88,9 @@ export function CKBStatsTab() {
|
||||
if (data?.success && data.data) {
|
||||
setStats(data.data)
|
||||
} else {
|
||||
const fallback = await get<{ success?: boolean; records?: unknown[]; total?: number }>('/api/db/match-records?page=1&pageSize=1')
|
||||
const fallback = await get<{ success?: boolean; total?: number }>('/api/db/match-records?page=1&pageSize=1')
|
||||
if (fallback?.success) {
|
||||
setStats({
|
||||
totalMatches: fallback.total ?? 0,
|
||||
todayMatches: 0,
|
||||
byType: [],
|
||||
uniqueUsers: 0,
|
||||
})
|
||||
setStats({ totalMatches: fallback.total ?? 0, todayMatches: 0, byType: [], uniqueUsers: 0 })
|
||||
}
|
||||
}
|
||||
} catch (e) { console.error('加载统计失败:', e) }
|
||||
@@ -57,43 +99,68 @@ export function CKBStatsTab() {
|
||||
|
||||
useEffect(() => { loadStats() }, [loadStats])
|
||||
|
||||
const getTestBody = (index: number) => {
|
||||
const phone = testPhone.trim()
|
||||
const wechat = testWechat.trim()
|
||||
const typeMap = ['partner', 'investor', 'mentor', 'team']
|
||||
if (index <= 3) {
|
||||
return {
|
||||
type: typeMap[index],
|
||||
phone: phone || undefined,
|
||||
wechat: wechat || undefined,
|
||||
userId: 'admin_test',
|
||||
name: '后台测试',
|
||||
canHelp: index === 1 ? '测试-我能帮到你' : '',
|
||||
needHelp: index === 1 ? '测试-我需要什么帮助' : '',
|
||||
}
|
||||
}
|
||||
if (index === 4) {
|
||||
return {
|
||||
matchType: 'partner', phone: phone || undefined, wechat: wechat || undefined,
|
||||
userId: 'admin_test', nickname: '后台测试',
|
||||
matchedUser: { id: 'test_matched', nickname: '测试匹配用户', matchScore: 88 },
|
||||
}
|
||||
}
|
||||
if (index === 5) {
|
||||
return { phone: phone || undefined, wechatId: wechat || undefined, userId: 'admin_test', name: '后台测试' }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
const testEndpoint = async (index: number) => {
|
||||
const test = ckbTests[index]
|
||||
if (test.method === 'POST' && !testPhone.trim() && !testWechat.trim()) {
|
||||
alert('请至少填写测试手机号或微信号')
|
||||
return
|
||||
}
|
||||
const updated = [...ckbTests]
|
||||
updated[index] = { ...test, status: 'testing', message: undefined, responseTime: undefined }
|
||||
updated[index] = { ...test, status: 'testing', message: undefined, responseTime: undefined, ckbResponse: undefined }
|
||||
setCkbTests(updated)
|
||||
|
||||
const start = performance.now()
|
||||
try {
|
||||
let res: { success?: boolean; message?: string; code?: number }
|
||||
if (test.endpoint.includes('match/config')) {
|
||||
res = await get<{ success?: boolean }>(test.endpoint)
|
||||
let res: { success?: boolean; message?: string; code?: number; data?: unknown }
|
||||
if (test.method === 'GET') {
|
||||
res = await get<typeof res>(test.endpoint)
|
||||
} else {
|
||||
res = await post<{ success?: boolean; message?: string }>(test.endpoint, {
|
||||
type: 'partner',
|
||||
phone: '00000000000',
|
||||
wechat: 'test_ping',
|
||||
userId: 'test_admin_ping',
|
||||
matchType: 'partner',
|
||||
name: '接口测试',
|
||||
})
|
||||
res = await post<typeof res>(test.endpoint, getTestBody(index))
|
||||
}
|
||||
const elapsed = Math.round(performance.now() - start)
|
||||
const next = [...ckbTests]
|
||||
const ok = res?.success !== undefined || res?.code === 200 || res?.code === 400
|
||||
const ok = res?.success === true || res?.code === 200
|
||||
next[index] = {
|
||||
...test,
|
||||
status: ok ? 'success' : 'error',
|
||||
message: res?.message || (ok ? '接口可用' : '响应异常'),
|
||||
status: ok ? 'success' : (res?.success === false ? 'error' : 'success'),
|
||||
message: res?.message || (ok ? '接口正常' : '返回异常'),
|
||||
responseTime: elapsed,
|
||||
ckbResponse: res?.data ? JSON.stringify(res.data).slice(0, 100) : undefined,
|
||||
}
|
||||
setCkbTests(next)
|
||||
} catch (e: unknown) {
|
||||
const elapsed = Math.round(performance.now() - start)
|
||||
const next = [...ckbTests]
|
||||
next[index] = {
|
||||
...test,
|
||||
status: 'error',
|
||||
...test, status: 'error',
|
||||
message: e instanceof Error ? e.message : '请求失败',
|
||||
responseTime: elapsed,
|
||||
}
|
||||
@@ -102,133 +169,178 @@ export function CKBStatsTab() {
|
||||
}
|
||||
|
||||
const testAll = async () => {
|
||||
if (!testPhone.trim() && !testWechat.trim()) {
|
||||
alert('请至少填写测试手机号或微信号')
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < ckbTests.length; i++) {
|
||||
await testEndpoint(i)
|
||||
}
|
||||
}
|
||||
|
||||
const v = (n: number | undefined) => isLoading ? '-' : (n ?? 0)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{/* 核心数据 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">总匹配次数</p>
|
||||
<p className="text-3xl font-bold text-white mt-1">{isLoading ? '-' : (stats?.totalMatches ?? 0)}</p>
|
||||
</div>
|
||||
<Users className="w-10 h-10 text-[#38bdac]/50" />
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-[#38bdac]/20 flex items-center justify-center"><Users className="w-5 h-5 text-[#38bdac]" /></div>
|
||||
<div><p className="text-gray-400 text-xs">总匹配次数</p><p className="text-xl font-bold text-white">{v(stats?.totalMatches)}</p></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">今日匹配</p>
|
||||
<p className="text-3xl font-bold text-white mt-1">{isLoading ? '-' : (stats?.todayMatches ?? 0)}</p>
|
||||
</div>
|
||||
<Zap className="w-10 h-10 text-yellow-400/50" />
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-yellow-500/20 flex items-center justify-center"><Zap className="w-5 h-5 text-yellow-400" /></div>
|
||||
<div><p className="text-gray-400 text-xs">今日匹配</p><p className="text-xl font-bold text-white">{v(stats?.todayMatches)}</p></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">独立用户数</p>
|
||||
<p className="text-3xl font-bold text-white mt-1">{isLoading ? '-' : (stats?.uniqueUsers ?? 0)}</p>
|
||||
</div>
|
||||
<UserCheck className="w-10 h-10 text-blue-400/50" />
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center"><UserCheck className="w-5 h-5 text-blue-400" /></div>
|
||||
<div><p className="text-gray-400 text-xs">匹配用户数</p><p className="text-xl font-bold text-white">{v(stats?.uniqueUsers)}</p></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">人均匹配</p>
|
||||
<p className="text-3xl font-bold text-white mt-1">
|
||||
{isLoading ? '-' : (stats?.uniqueUsers ? (stats.totalMatches / stats.uniqueUsers).toFixed(1) : '0')}
|
||||
</p>
|
||||
</div>
|
||||
<TrendingUp className="w-10 h-10 text-green-400/50" />
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-green-500/20 flex items-center justify-center"><TrendingUp className="w-5 h-5 text-green-400" /></div>
|
||||
<div><p className="text-gray-400 text-xs">人均匹配</p><p className="text-xl font-bold text-white">{isLoading ? '-' : (stats?.uniqueUsers ? (stats.totalMatches / stats.uniqueUsers).toFixed(1) : '0')}</p></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-500/20 flex items-center justify-center"><DollarSign className="w-5 h-5 text-purple-400" /></div>
|
||||
<div><p className="text-gray-400 text-xs">匹配收益</p><p className="text-xl font-bold text-white">¥{v(stats?.matchRevenue)}</p></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-orange-500/20 flex items-center justify-center"><Activity className="w-5 h-5 text-orange-400" /></div>
|
||||
<div><p className="text-gray-400 text-xs">付费匹配</p><p className="text-xl font-bold text-white">{v(stats?.paidMatchCount)}</p></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 按类型分布 */}
|
||||
{stats?.byType && stats.byType.length > 0 && (
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white text-lg">按类型分布</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{stats.byType.map(item => (
|
||||
<div key={item.matchType} className="bg-[#0a1628] rounded-lg p-4 text-center">
|
||||
<p className="text-gray-400 text-sm">{typeLabels[item.matchType] || item.matchType}</p>
|
||||
<p className="text-2xl font-bold text-white mt-2">{item.count}</p>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
{stats.totalMatches > 0 ? ((item.count / stats.totalMatches) * 100).toFixed(1) : 0}%
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* CKB 接口测试 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Link2 className="w-5 h-5 text-[#38bdac]" />
|
||||
存客宝接口连通性测试
|
||||
</CardTitle>
|
||||
<p className="text-gray-400 text-sm mt-1">
|
||||
测试所有找伙伴相关的 CKB 接口是否正常可用
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={loadStats} disabled={isLoading} variant="outline" className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent">
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} /> 刷新统计
|
||||
</Button>
|
||||
<Button onClick={testAll} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||||
<CardTitle className="text-white text-lg">各类型匹配分布</CardTitle>
|
||||
<Button onClick={loadStats} disabled={isLoading} variant="outline" size="sm" className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent">
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} /> 刷新数据
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{stats?.byType && stats.byType.length > 0 ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{stats.byType.map(item => {
|
||||
const pct = stats.totalMatches > 0 ? ((item.count / stats.totalMatches) * 100) : 0
|
||||
return (
|
||||
<div key={item.matchType} className="bg-[#0a1628] rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xl">{typeIcons[item.matchType] || '📊'}</span>
|
||||
<span className="text-gray-300 text-sm font-medium">{typeLabels[item.matchType] || item.matchType}</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-white">{item.count}</p>
|
||||
<div className="mt-2">
|
||||
<div className="w-full h-2 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-[#38bdac] rounded-full transition-all" style={{ width: `${Math.min(pct, 100)}%` }} />
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mt-1">{pct.toFixed(1)}% 占比</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 text-center py-8">{isLoading ? '加载中...' : '暂无匹配数据'}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* CKB 接口联通测试 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardHeader>
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Link2 className="w-5 h-5 text-[#38bdac]" />
|
||||
存客宝接口联通测试
|
||||
</CardTitle>
|
||||
<p className="text-gray-400 text-sm mt-1">
|
||||
点击测试会用下方手机号/微信号真实添加到存客宝对应计划中
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={testAll} className="bg-[#38bdac] hover:bg-[#2da396] text-white shrink-0">
|
||||
<Zap className="w-4 h-4 mr-2" /> 全部测试
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-4 mt-4 p-4 bg-[#0a1628] rounded-lg border border-gray-700/50">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Smartphone className="w-4 h-4 text-gray-400 shrink-0" />
|
||||
<div className="flex-1 space-y-1">
|
||||
<Label className="text-gray-400 text-xs">测试手机号</Label>
|
||||
<Input
|
||||
className="bg-[#0f2137] border-gray-700 text-white h-9"
|
||||
placeholder="填写真实手机号添加到存客宝"
|
||||
value={testPhone}
|
||||
onChange={e => setTestPhone(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<span className="text-gray-400 shrink-0 text-sm">💬</span>
|
||||
<div className="flex-1 space-y-1">
|
||||
<Label className="text-gray-400 text-xs">测试微信号(可选)</Label>
|
||||
<Input
|
||||
className="bg-[#0f2137] border-gray-700 text-white h-9"
|
||||
placeholder="填写微信号(可选)"
|
||||
value={testWechat}
|
||||
onChange={e => setTestWechat(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
{ckbTests.map((test, idx) => (
|
||||
<div key={test.endpoint} className="flex items-center justify-between bg-[#0a1628] rounded-lg px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{test.status === 'idle' && <div className="w-3 h-3 rounded-full bg-gray-500" />}
|
||||
{test.status === 'testing' && <RefreshCw className="w-4 h-4 text-yellow-400 animate-spin" />}
|
||||
{test.status === 'success' && <CheckCircle2 className="w-4 h-4 text-green-400" />}
|
||||
{test.status === 'error' && <XCircle className="w-4 h-4 text-red-400" />}
|
||||
<div>
|
||||
<div key={`${test.endpoint}-${idx}`} className="flex items-center justify-between bg-[#0a1628] rounded-lg px-4 py-3 gap-4">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
{test.status === 'idle' && <div className="w-3 h-3 rounded-full bg-gray-500 shrink-0" />}
|
||||
{test.status === 'testing' && <RefreshCw className="w-4 h-4 text-yellow-400 animate-spin shrink-0" />}
|
||||
{test.status === 'success' && <CheckCircle2 className="w-4 h-4 text-green-400 shrink-0" />}
|
||||
{test.status === 'error' && <XCircle className="w-4 h-4 text-red-400 shrink-0" />}
|
||||
<div className="min-w-0">
|
||||
<p className="text-white text-sm font-medium">{test.label}</p>
|
||||
<p className="text-gray-500 text-xs font-mono">{test.endpoint}</p>
|
||||
<p className="text-gray-500 text-xs truncate">{test.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{test.message && (
|
||||
<span className={`text-xs ${test.status === 'success' ? 'text-green-400' : test.status === 'error' ? 'text-red-400' : 'text-gray-400'}`}>
|
||||
<span className={`text-xs max-w-[200px] truncate ${test.status === 'success' ? 'text-green-400' : test.status === 'error' ? 'text-red-400' : 'text-gray-400'}`}>
|
||||
{test.message}
|
||||
</span>
|
||||
)}
|
||||
{test.responseTime !== undefined && (
|
||||
<Badge className="bg-gray-700 text-gray-300 border-0">{test.responseTime}ms</Badge>
|
||||
<Badge className="bg-gray-700 text-gray-300 border-0 text-xs">{test.responseTime}ms</Badge>
|
||||
)}
|
||||
<Button size="sm" variant="outline"
|
||||
onClick={() => testEndpoint(idx)}
|
||||
disabled={test.status === 'testing'}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent text-xs">
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent text-xs h-8 px-3">
|
||||
测试
|
||||
</Button>
|
||||
</div>
|
||||
@@ -237,6 +349,40 @@ export function CKBStatsTab() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 协作需求登记 */}
|
||||
<Card className="bg-[#0f2137] border-yellow-600/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-yellow-400 text-base flex items-center gap-2">
|
||||
📋 存客宝协作需求(待发给存客宝团队)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-start gap-3 bg-[#0a1628] rounded-lg p-3">
|
||||
<Badge className="bg-red-500/20 text-red-400 border-0 shrink-0">待开发</Badge>
|
||||
<div>
|
||||
<p className="text-white">场景获客接口回馈 — 添加好友成功率反馈</p>
|
||||
<p className="text-gray-500 mt-1">当前 scenarios API 只返回「新增成功/已存在」,需要新增字段返回该线索的微信添加状态(已添加/待添加/添加失败)和添加时间</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 bg-[#0a1628] rounded-lg p-3">
|
||||
<Badge className="bg-red-500/20 text-red-400 border-0 shrink-0">待开发</Badge>
|
||||
<div>
|
||||
<p className="text-white">线索查询接口 — 按手机号/微信号查询添加结果</p>
|
||||
<p className="text-gray-500 mt-1">需要一个 GET 接口,传入 phone/wechatId 可查询该线索在存客宝中的状态(是否已添加好友、所属计划、标签等)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 bg-[#0a1628] rounded-lg p-3">
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 border-0 shrink-0">待确认</Badge>
|
||||
<div>
|
||||
<p className="text-white">批量线索统计接口 — 查询某时间段内的添加成功率</p>
|
||||
<p className="text-gray-500 mt-1">需要一个统计接口,返回指定时间段内上报的线索总数、已添加好友数、添加成功率、各计划分布</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user