diff --git a/soul-admin/src/pages/find-partner/tabs/CKBConfigPanel.tsx b/soul-admin/src/pages/find-partner/tabs/CKBConfigPanel.tsx index 89abfb5f..7ecd00d2 100644 --- a/soul-admin/src/pages/find-partner/tabs/CKBConfigPanel.tsx +++ b/soul-admin/src/pages/find-partner/tabs/CKBConfigPanel.tsx @@ -1,29 +1,71 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Card, CardContent } 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, Zap, CheckCircle2, XCircle, Smartphone, FileText, ExternalLink, Save, -} from 'lucide-react' +import { RefreshCw, Zap, CheckCircle2, XCircle, Smartphone, FileText, ExternalLink, Save } from 'lucide-react' import { get, post } from '@/api/client' +type WorkspaceTab = 'overview' | 'submitted' | 'contact' | 'config' | 'test' | 'doc' + interface TestResult { - endpoint: string; label: string - method: 'GET' | 'POST'; status: 'idle' | 'testing' | 'success' | 'error' - message?: string; responseTime?: number + endpoint: string + label: string + method: 'GET' | 'POST' + status: 'idle' | 'testing' | 'success' | 'error' + message?: string + responseTime?: number +} + +interface LeadRow { + id: string + userId: string + userNickname?: string + matchType: string + phone?: string + wechatId?: string + createdAt: string +} + +interface RouteConfig { + apiUrl: string + apiKey: string + source: string + tags: string + siteTags: string + notes: string } const typeMap = ['partner', 'investor', 'mentor', 'team'] +const routeDefs = [ + { key: 'join_partner', label: '找伙伴场景' }, + { key: 'join_investor', label: '资源对接场景' }, + { key: 'join_mentor', label: '导师顾问场景' }, + { key: 'join_team', label: '团队招募场景' }, + { key: 'match', label: '匹配上报' }, + { key: 'lead', label: '链接卡若' }, +] as const + +const defaultDoc = `# 场景获客接口摘要 +- 地址:POST /v1/api/scenarios +- 必填:apiKey、sign、timestamp +- 主标识:phone 或 wechatId 至少一项 +- 可选:name、source、remark、tags、siteTags、portrait +- 签名:排除 sign/apiKey/portrait,键名升序拼接值后双重 MD5 +- 成功:code=200,message=新增成功 或 已存在` export function CKBConfigPanel() { + const [activeTab, setActiveTab] = useState('overview') const [testPhone, setTestPhone] = useState('13800000000') const [testWechat, setTestWechat] = useState('') - const [apiUrl, setApiUrl] = useState('https://ckbapi.quwanzhi.com/v1/api/scenarios') - const [apiKey, setApiKey] = useState('fyngh-ecy9h-qkdae-epwd5-rz6kd') const [docNotes, setDocNotes] = useState('') + const [docContent, setDocContent] = useState(defaultDoc) const [isSaving, setIsSaving] = useState(false) + const [loading, setLoading] = useState(false) + const [submittedLeads, setSubmittedLeads] = useState([]) + const [contactLeads, setContactLeads] = useState([]) + const [routes, setRoutes] = useState>({}) const [tests, setTests] = useState([ { endpoint: '/api/ckb/join', label: '找伙伴', method: 'POST', status: 'idle' }, { endpoint: '/api/ckb/join', label: '资源对接', method: 'POST', status: 'idle' }, @@ -34,56 +76,57 @@ export function CKBConfigPanel() { { endpoint: '/api/match/config', label: '匹配配置', method: 'GET', status: 'idle' }, ]) + const routeMap = useMemo(() => { + const m: Record = {} + routeDefs.forEach((item) => { + m[item.key] = routes[item.key] || { + apiUrl: 'https://ckbapi.quwanzhi.com/v1/api/scenarios', + apiKey: 'fyngh-ecy9h-qkdae-epwd5-rz6kd', + source: '', + tags: '', + siteTags: '创业实验APP', + notes: '', + } + }) + return m + }, [routes]) + const getBody = (idx: number) => { - const phone = testPhone.trim(); const wechat = testWechat.trim() + const phone = testPhone.trim() + const wechat = testWechat.trim() if (idx <= 3) return { type: typeMap[idx], phone: phone || undefined, wechat: wechat || undefined, userId: 'admin_test', name: '后台测试' } if (idx === 4) return { matchType: 'partner', phone: phone || undefined, wechat: wechat || undefined, userId: 'admin_test', nickname: '后台测试', matchedUser: { id: 'test', nickname: '测试', matchScore: 88 } } if (idx === 5) return { phone: phone || undefined, wechatId: wechat || undefined, userId: 'admin_test', name: '后台测试' } return {} } - const testOne = async (idx: number) => { - const t = tests[idx] - if (t.method === 'POST' && !testPhone.trim() && !testWechat.trim()) { alert('请填写测试手机号'); return } - const next = [...tests]; next[idx] = { ...t, status: 'testing', message: undefined, responseTime: undefined }; setTests(next) - const start = performance.now() + async function loadWorkspace() { + setLoading(true) try { - const res = t.method === 'GET' - ? await get<{ success?: boolean; message?: string }>(t.endpoint) - : await post<{ success?: boolean; message?: string }>(t.endpoint, getBody(idx)) - const elapsed = Math.round(performance.now() - start) - const msg = res?.message || '' - const ok = res?.success === true || msg.includes('已存在') || msg.includes('已加入') || msg.includes('已提交') - const n = [...tests]; n[idx] = { ...t, status: ok ? 'success' : 'error', message: msg || (ok ? '正常' : '异常'), responseTime: elapsed }; setTests(n) - } catch (e: unknown) { - const elapsed = Math.round(performance.now() - start) - const n = [...tests]; n[idx] = { ...t, status: 'error', message: e instanceof Error ? e.message : '失败', responseTime: elapsed }; setTests(n) - } - } - - const testAll = async () => { - if (!testPhone.trim() && !testWechat.trim()) { alert('请填写测试手机号'); return } - for (let i = 0; i < tests.length; i++) await testOne(i) - } - - async function loadConfig() { - try { - const data = await get<{ data?: { apiUrl?: string; apiKey?: string; docNotes?: string } }>('/api/db/config/full?key=ckb_config') - const c = data?.data - if (c?.apiUrl) setApiUrl(c.apiUrl) - if (c?.apiKey) setApiKey(c.apiKey) + const [cfgRes, submittedRes, contactRes] = await Promise.all([ + get<{ data?: { routes?: Record; docNotes?: string; docContent?: string } }>('/api/db/config/full?key=ckb_config'), + get<{ success?: boolean; records?: LeadRow[] }>('/api/db/ckb-leads?mode=submitted&page=1&pageSize=50'), + get<{ success?: boolean; records?: LeadRow[] }>('/api/db/ckb-leads?mode=contact&page=1&pageSize=50'), + ]) + const c = cfgRes?.data + if (c?.routes) setRoutes(c.routes) if (c?.docNotes) setDocNotes(c.docNotes) - } catch { - // ignore + if (c?.docContent) setDocContent(c.docContent) + if (submittedRes?.success) setSubmittedLeads(submittedRes.records || []) + if (contactRes?.success) setContactLeads(contactRes.records || []) + } finally { + setLoading(false) } } + useEffect(() => { void loadWorkspace() }, []) + async function saveConfig() { setIsSaving(true) try { const res = await post<{ success?: boolean; error?: string }>('/api/db/config', { key: 'ckb_config', - value: { apiUrl, apiKey, docNotes }, + value: { routes: routeMap, docNotes, docContent }, description: '存客宝接口配置', }) alert(res?.success !== false ? '存客宝配置已保存' : `保存失败: ${res?.error || '未知错误'}`) @@ -94,108 +137,242 @@ export function CKBConfigPanel() { } } - useEffect(() => { void loadConfig() }, []) + const updateRoute = (key: string, patch: Partial) => { + setRoutes((prev) => ({ ...prev, [key]: { ...routeMap[key], ...patch } })) + } + + const testOne = async (idx: number) => { + const t = tests[idx] + if (t.method === 'POST' && !testPhone.trim() && !testWechat.trim()) { alert('请填写测试手机号'); return } + const next = [...tests] + next[idx] = { ...t, status: 'testing', message: undefined, responseTime: undefined } + setTests(next) + const start = performance.now() + try { + const res = t.method === 'GET' + ? await get<{ success?: boolean; message?: string }>(t.endpoint) + : await post<{ success?: boolean; message?: string }>(t.endpoint, getBody(idx)) + const elapsed = Math.round(performance.now() - start) + const msg = res?.message || '' + const ok = res?.success === true || msg.includes('已存在') || msg.includes('已加入') || msg.includes('已提交') + const out = [...tests] + out[idx] = { ...t, status: ok ? 'success' : 'error', message: msg || (ok ? '正常' : '异常'), responseTime: elapsed } + setTests(out) + await loadWorkspace() + } catch (e: unknown) { + const elapsed = Math.round(performance.now() - start) + const out = [...tests] + out[idx] = { ...t, status: 'error', message: e instanceof Error ? e.message : '失败', responseTime: elapsed } + setTests(out) + } + } + + const testAll = async () => { + if (!testPhone.trim() && !testWechat.trim()) { alert('请填写测试手机号'); return } + for (let i = 0; i < tests.length; i++) await testOne(i) + } + + const renderLeadTable = (rows: LeadRow[], emptyText: string) => ( +
+ + + + + + + + + + + + {rows.length === 0 ? ( + + ) : rows.map((row) => ( + + + + + + + + ))} + +
发起人类型手机号微信号时间
{emptyText}
{row.userNickname || row.userId}{row.matchType}{row.phone || '—'}{row.wechatId || '—'}{row.createdAt ? new Date(row.createdAt).toLocaleString() : '—'}
+
+ ) return ( - -
-
- - setApiUrl(e.target.value)} /> -
-
- - setApiKey(e.target.value)} /> -
-
- +
-
-
- -
- - setTestPhone(e.target.value)} /> -
-
-
- 💬 -
- - setTestWechat(e.target.value)} /> -
-
-
- -
- {tests.map((t, idx) => ( -
-
- {t.status === 'idle' &&
} - {t.status === 'testing' && } - {t.status === 'success' && } - {t.status === 'error' && } - {t.label} -
-
- {t.responseTime !== undefined && {t.responseTime}ms} - -
-
+
+ {[ + ['overview', '概览'], + ['submitted', '已提交线索'], + ['contact', '有联系方式'], + ['config', '场景配置'], + ['test', '接口测试'], + ['doc', 'API 文档'], + ].map(([id, label]) => ( + ))}
-
- - 场景获客API - - | - Key: {apiKey ? `${apiKey.slice(0, 8)}...` : '未配置'} -
- -
-
-

场景获客接口摘要

-
-

接口:POST /v1/api/scenarios

-

必填鉴权:apiKeysigntimestamp

-

至少一项:phonewechatId

-

可选字段:namesourceremarktagssiteTagsportrait

-

签名:排除 sign/apiKey/portrait,按键升序拼接值后双重 MD5

-

成功返回:{"{ code: 200, message: '新增成功|已存在' }"}

+ {activeTab === 'overview' && ( +
+
+

已提交线索

+

{submittedLeads.length}

+
+
+

有联系方式

+

{contactLeads.length}

+
+
+

场景配置数

+

{routeDefs.length}

+
+
+

文档备注

+

{docNotes || '未填写'}

-
-

说明备注(可编辑)

-