sync: soul-admin 页面 | 原因: 前端页面修改

This commit is contained in:
卡若
2026-03-08 10:24:41 +08:00
parent 1e2e1c73ff
commit 9faab51394

View File

@@ -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>
)
}