sync: soul-admin 页面、开发文档 需求 | 原因: 前端页面修改、需求文档更新
This commit is contained in:
@@ -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<WorkspaceTab>('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<LeadRow[]>([])
|
||||
const [contactLeads, setContactLeads] = useState<LeadRow[]>([])
|
||||
const [routes, setRoutes] = useState<Record<string, RouteConfig>>({})
|
||||
const [tests, setTests] = useState<TestResult[]>([
|
||||
{ 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<string, RouteConfig> = {}
|
||||
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<string, RouteConfig>; 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<RouteConfig>) => {
|
||||
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) => (
|
||||
<div className="overflow-auto rounded-lg border border-gray-700/30">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-[#0a1628] text-gray-400">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3">发起人</th>
|
||||
<th className="text-left px-4 py-3">类型</th>
|
||||
<th className="text-left px-4 py-3">手机号</th>
|
||||
<th className="text-left px-4 py-3">微信号</th>
|
||||
<th className="text-left px-4 py-3">时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={5} className="text-center py-10 text-gray-500">{emptyText}</td></tr>
|
||||
) : rows.map((row) => (
|
||||
<tr key={row.id} className="border-t border-gray-700/30">
|
||||
<td className="px-4 py-3 text-white">{row.userNickname || row.userId}</td>
|
||||
<td className="px-4 py-3 text-gray-300">{row.matchType}</td>
|
||||
<td className="px-4 py-3 text-green-400">{row.phone || '—'}</td>
|
||||
<td className="px-4 py-3 text-blue-400">{row.wechatId || '—'}</td>
|
||||
<td className="px-4 py-3 text-gray-400">{row.createdAt ? new Date(row.createdAt).toLocaleString() : '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Card className="bg-[#0f2137] border-orange-500/30 mb-6">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-white font-semibold">存客宝配置与接口测试</h3>
|
||||
<h3 className="text-white font-semibold">存客宝工作台</h3>
|
||||
<Badge className="bg-orange-500/20 text-orange-400 border-0 text-xs">CKB</Badge>
|
||||
<a href="https://ckbapi.quwanzhi.com" target="_blank" rel="noreferrer"
|
||||
className="text-orange-400/60 text-xs hover:text-orange-400 flex items-center gap-1">
|
||||
<button type="button" onClick={() => setActiveTab('doc')} className="text-orange-400/60 text-xs hover:text-orange-400 flex items-center gap-1">
|
||||
<ExternalLink className="w-3 h-3" /> API 文档
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
<Button onClick={testAll} size="sm" className="bg-orange-500 hover:bg-orange-600 text-white">
|
||||
<Zap className="w-3.5 h-3.5 mr-1" /> 全部测试
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-5">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-gray-500 text-xs">API 地址</Label>
|
||||
<Input className="bg-[#0a1628] border-gray-700 text-white h-9 text-sm" value={apiUrl} onChange={e => setApiUrl(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-gray-500 text-xs">API Key</Label>
|
||||
<Input className="bg-[#0a1628] border-gray-700 text-white h-9 text-sm" value={apiKey} onChange={e => setApiKey(e.target.value)} />
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<Button onClick={saveConfig} disabled={isSaving} className="bg-[#38bdac] hover:bg-[#2da396] text-white w-full">
|
||||
<Save className="w-4 h-4 mr-2" /> {isSaving ? '保存中...' : '保存配置'}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={() => loadWorkspace()} variant="outline" size="sm" className="border-gray-700 text-gray-300 hover:bg-gray-700/50 bg-transparent">
|
||||
<RefreshCw className={`w-3.5 h-3.5 mr-1 ${loading ? 'animate-spin' : ''}`} /> 刷新
|
||||
</Button>
|
||||
<Button onClick={saveConfig} disabled={isSaving} size="sm" className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<Save className="w-3.5 h-3.5 mr-1" /> {isSaving ? '保存中...' : '保存配置'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mb-4">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Smartphone className="w-4 h-4 text-gray-500 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<Label className="text-gray-500 text-xs">测试手机号</Label>
|
||||
<Input className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm mt-0.5" value={testPhone} onChange={e => setTestPhone(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<span className="text-gray-500 text-sm shrink-0">💬</span>
|
||||
<div className="flex-1">
|
||||
<Label className="text-gray-500 text-xs">微信号(可选)</Label>
|
||||
<Input className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm mt-0.5" value={testWechat} onChange={e => setTestWechat(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
|
||||
{tests.map((t, idx) => (
|
||||
<div key={`${t.endpoint}-${idx}`} className="flex items-center justify-between bg-[#0a1628] rounded-lg px-3 py-2 border border-gray-700/30">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{t.status === 'idle' && <div className="w-2 h-2 rounded-full bg-gray-600 shrink-0" />}
|
||||
{t.status === 'testing' && <RefreshCw className="w-3 h-3 text-yellow-400 animate-spin shrink-0" />}
|
||||
{t.status === 'success' && <CheckCircle2 className="w-3 h-3 text-green-400 shrink-0" />}
|
||||
{t.status === 'error' && <XCircle className="w-3 h-3 text-red-400 shrink-0" />}
|
||||
<span className="text-white text-xs truncate">{t.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{t.responseTime !== undefined && <span className="text-gray-600 text-[10px]">{t.responseTime}ms</span>}
|
||||
<button type="button" onClick={() => testOne(idx)} disabled={t.status === 'testing'}
|
||||
className="text-orange-400/60 hover:text-orange-400 text-[10px] disabled:opacity-50">测试</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mb-5">
|
||||
{[
|
||||
['overview', '概览'],
|
||||
['submitted', '已提交线索'],
|
||||
['contact', '有联系方式'],
|
||||
['config', '场景配置'],
|
||||
['test', '接口测试'],
|
||||
['doc', 'API 文档'],
|
||||
].map(([id, label]) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(id as WorkspaceTab)}
|
||||
className={`px-4 py-2 rounded-lg text-sm transition-colors ${
|
||||
activeTab === id ? 'bg-orange-500 text-white' : 'bg-[#0a1628] text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-3 text-xs">
|
||||
<a href="https://ckbapi.quwanzhi.com/v1/api/scenarios" target="_blank" rel="noreferrer"
|
||||
className="text-gray-500 hover:text-orange-400 flex items-center gap-1">
|
||||
<FileText className="w-3 h-3" /> 场景获客API
|
||||
</a>
|
||||
<span className="text-gray-700">|</span>
|
||||
<span className="text-gray-600">Key: {apiKey ? `${apiKey.slice(0, 8)}...` : '未配置'}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<div className="bg-[#0a1628] rounded-lg border border-gray-700/30 p-4">
|
||||
<h4 className="text-white text-sm font-medium mb-3">场景获客接口摘要</h4>
|
||||
<div className="space-y-2 text-xs text-gray-400">
|
||||
<p><span className="text-gray-300">接口:</span><code className="text-orange-300">POST /v1/api/scenarios</code></p>
|
||||
<p><span className="text-gray-300">必填鉴权:</span><code>apiKey</code>、<code>sign</code>、<code>timestamp</code></p>
|
||||
<p><span className="text-gray-300">至少一项:</span><code>phone</code> 或 <code>wechatId</code></p>
|
||||
<p><span className="text-gray-300">可选字段:</span><code>name</code>、<code>source</code>、<code>remark</code>、<code>tags</code>、<code>siteTags</code>、<code>portrait</code></p>
|
||||
<p><span className="text-gray-300">签名:</span>排除 <code>sign/apiKey/portrait</code>,按键升序拼接值后双重 MD5</p>
|
||||
<p><span className="text-gray-300">成功返回:</span><code>{"{ code: 200, message: '新增成功|已存在' }"}</code></p>
|
||||
{activeTab === 'overview' && (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-[#0a1628] border border-gray-700/30 rounded-xl p-5">
|
||||
<p className="text-gray-400 text-xs mb-2">已提交线索</p>
|
||||
<p className="text-3xl font-bold text-white">{submittedLeads.length}</p>
|
||||
</div>
|
||||
<div className="bg-[#0a1628] border border-gray-700/30 rounded-xl p-5">
|
||||
<p className="text-gray-400 text-xs mb-2">有联系方式</p>
|
||||
<p className="text-3xl font-bold text-white">{contactLeads.length}</p>
|
||||
</div>
|
||||
<div className="bg-[#0a1628] border border-gray-700/30 rounded-xl p-5">
|
||||
<p className="text-gray-400 text-xs mb-2">场景配置数</p>
|
||||
<p className="text-3xl font-bold text-white">{routeDefs.length}</p>
|
||||
</div>
|
||||
<div className="bg-[#0a1628] border border-gray-700/30 rounded-xl p-5">
|
||||
<p className="text-gray-400 text-xs mb-2">文档备注</p>
|
||||
<p className="text-sm text-gray-300 line-clamp-3">{docNotes || '未填写'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#0a1628] rounded-lg border border-gray-700/30 p-4">
|
||||
<h4 className="text-white text-sm font-medium mb-3">说明备注(可编辑)</h4>
|
||||
<textarea
|
||||
className="w-full min-h-[140px] bg-[#0f2137] border border-gray-700 rounded-md text-sm text-gray-300 p-3 outline-none focus:border-orange-500/50 resize-y"
|
||||
value={docNotes}
|
||||
onChange={(e) => setDocNotes(e.target.value)}
|
||||
placeholder="这里可记录存客宝 Token、计划说明、接口对接约定、回复率统计规则等。"
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'submitted' && renderLeadTable(submittedLeads, '暂无已提交线索')}
|
||||
{activeTab === 'contact' && renderLeadTable(contactLeads, '暂无有联系方式线索')}
|
||||
|
||||
{activeTab === 'config' && (
|
||||
<div className="space-y-4">
|
||||
{routeDefs.map((item) => (
|
||||
<div key={item.key} className="bg-[#0a1628] border border-gray-700/30 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-white font-medium">{item.label}</h4>
|
||||
<Badge className="bg-orange-500/20 text-orange-300 border-0 text-xs">{item.key}</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-gray-500 text-xs">API 地址</Label>
|
||||
<Input className="bg-[#0f2137] border-gray-700 text-white h-9 text-sm" value={routeMap[item.key].apiUrl} onChange={(e) => updateRoute(item.key, { apiUrl: e.target.value })} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-gray-500 text-xs">API Key</Label>
|
||||
<Input className="bg-[#0f2137] border-gray-700 text-white h-9 text-sm" value={routeMap[item.key].apiKey} onChange={(e) => updateRoute(item.key, { apiKey: e.target.value })} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-gray-500 text-xs">Source</Label>
|
||||
<Input className="bg-[#0f2137] border-gray-700 text-white h-9 text-sm" value={routeMap[item.key].source} onChange={(e) => updateRoute(item.key, { source: e.target.value })} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-gray-500 text-xs">Tags</Label>
|
||||
<Input className="bg-[#0f2137] border-gray-700 text-white h-9 text-sm" value={routeMap[item.key].tags} onChange={(e) => updateRoute(item.key, { tags: e.target.value })} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-gray-500 text-xs">SiteTags</Label>
|
||||
<Input className="bg-[#0f2137] border-gray-700 text-white h-9 text-sm" value={routeMap[item.key].siteTags} onChange={(e) => updateRoute(item.key, { siteTags: e.target.value })} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-gray-500 text-xs">说明备注</Label>
|
||||
<Input className="bg-[#0f2137] border-gray-700 text-white h-9 text-sm" value={routeMap[item.key].notes} onChange={(e) => updateRoute(item.key, { notes: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'test' && (
|
||||
<>
|
||||
<div className="flex gap-3 mb-4">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Smartphone className="w-4 h-4 text-gray-500 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<Label className="text-gray-500 text-xs">测试手机号</Label>
|
||||
<Input className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm mt-0.5" value={testPhone} onChange={(e) => setTestPhone(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<span className="text-gray-500 text-sm shrink-0">💬</span>
|
||||
<div className="flex-1">
|
||||
<Label className="text-gray-500 text-xs">微信号(可选)</Label>
|
||||
<Input className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm mt-0.5" value={testWechat} onChange={(e) => setTestWechat(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<Button onClick={testAll} className="bg-orange-500 hover:bg-orange-600 text-white">
|
||||
<Zap className="w-3.5 h-3.5 mr-1" /> 全部测试
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
|
||||
{tests.map((t, idx) => (
|
||||
<div key={`${t.endpoint}-${idx}`} className="flex items-center justify-between bg-[#0a1628] rounded-lg px-3 py-2 border border-gray-700/30">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{t.status === 'idle' && <div className="w-2 h-2 rounded-full bg-gray-600 shrink-0" />}
|
||||
{t.status === 'testing' && <RefreshCw className="w-3 h-3 text-yellow-400 animate-spin shrink-0" />}
|
||||
{t.status === 'success' && <CheckCircle2 className="w-3 h-3 text-green-400 shrink-0" />}
|
||||
{t.status === 'error' && <XCircle className="w-3 h-3 text-red-400 shrink-0" />}
|
||||
<span className="text-white text-xs truncate">{t.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{t.responseTime !== undefined && <span className="text-gray-600 text-[10px]">{t.responseTime}ms</span>}
|
||||
<button type="button" onClick={() => testOne(idx)} disabled={t.status === 'testing'} className="text-orange-400/60 hover:text-orange-400 text-[10px] disabled:opacity-50">测试</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'doc' && (
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<div className="bg-[#0a1628] rounded-lg border border-gray-700/30 p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-white text-sm font-medium">场景获客 API 摘要</h4>
|
||||
<a href="https://ckbapi.quwanzhi.com/v1/api/scenarios" target="_blank" rel="noreferrer" className="text-orange-400/70 hover:text-orange-400 text-xs flex items-center gap-1">
|
||||
<ExternalLink className="w-3 h-3" /> 打开外链
|
||||
</a>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap text-xs text-gray-400 leading-6">{docContent || defaultDoc}</pre>
|
||||
</div>
|
||||
<div className="bg-[#0a1628] rounded-lg border border-gray-700/30 p-4">
|
||||
<h4 className="text-white text-sm font-medium mb-3">说明备注(可编辑)</h4>
|
||||
<textarea
|
||||
className="w-full min-h-[260px] bg-[#0f2137] border border-gray-700 rounded-md text-sm text-gray-300 p-3 outline-none focus:border-orange-500/50 resize-y"
|
||||
value={docNotes}
|
||||
onChange={(e) => setDocNotes(e.target.value)}
|
||||
placeholder="记录 Token、入口差异、回复率统计规则、对接约定等。"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user