sync: soul-admin 组件 | 原因: 前端组件修改
This commit is contained in:
@@ -13,7 +13,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
Phone,
|
Phone,
|
||||||
History,
|
MapPin,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Link2,
|
Link2,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
@@ -24,6 +24,12 @@ import {
|
|||||||
Save,
|
Save,
|
||||||
X,
|
X,
|
||||||
Tag,
|
Tag,
|
||||||
|
TrendingUp,
|
||||||
|
Zap,
|
||||||
|
Search,
|
||||||
|
CheckCircle2,
|
||||||
|
Crown,
|
||||||
|
Navigation,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { get, put, post } from '@/api/client'
|
import { get, put, post } from '@/api/client'
|
||||||
|
|
||||||
@@ -60,6 +66,12 @@ interface UserDetail {
|
|||||||
vipProject?: string | null
|
vipProject?: string | null
|
||||||
vipContact?: string | null
|
vipContact?: string | null
|
||||||
vipBio?: string | null
|
vipBio?: string | null
|
||||||
|
vipRole?: string | null
|
||||||
|
// 扩展字段
|
||||||
|
mbti?: string
|
||||||
|
region?: string
|
||||||
|
industry?: string
|
||||||
|
position?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserTrack {
|
interface UserTrack {
|
||||||
@@ -72,6 +84,23 @@ interface UserTrack {
|
|||||||
timeAgo: string
|
timeAgo: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RFMData {
|
||||||
|
rfmScore: number
|
||||||
|
rfmLevel: string
|
||||||
|
recency: number
|
||||||
|
frequency: number
|
||||||
|
monetary: number
|
||||||
|
lastOrderAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShensheShouData {
|
||||||
|
rfm_score?: number
|
||||||
|
user_level?: string
|
||||||
|
tags?: string[]
|
||||||
|
last_active?: string
|
||||||
|
phone?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function UserDetailModal({
|
export function UserDetailModal({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -81,6 +110,7 @@ export function UserDetailModal({
|
|||||||
const [user, setUser] = useState<UserDetail | null>(null)
|
const [user, setUser] = useState<UserDetail | null>(null)
|
||||||
const [tracks, setTracks] = useState<UserTrack[]>([])
|
const [tracks, setTracks] = useState<UserTrack[]>([])
|
||||||
const [referrals, setReferrals] = useState<unknown[]>([])
|
const [referrals, setReferrals] = useState<unknown[]>([])
|
||||||
|
const [rfmData, setRfmData] = useState<RFMData | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [syncing, setSyncing] = useState(false)
|
const [syncing, setSyncing] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
@@ -90,8 +120,23 @@ export function UserDetailModal({
|
|||||||
const [editTags, setEditTags] = useState<string[]>([])
|
const [editTags, setEditTags] = useState<string[]>([])
|
||||||
const [newTag, setNewTag] = useState('')
|
const [newTag, setNewTag] = useState('')
|
||||||
|
|
||||||
|
// 神射手
|
||||||
|
const [sssLoading, setSssLoading] = useState(false)
|
||||||
|
const [sssData, setSssData] = useState<ShensheShouData | null>(null)
|
||||||
|
const [sssError, setSssError] = useState<string | null>(null)
|
||||||
|
const [sssQueryPhone, setSssQueryPhone] = useState('')
|
||||||
|
const [batchIngestLoading, setBatchIngestLoading] = useState(false)
|
||||||
|
const [batchIngestResult, setBatchIngestResult] = useState<Record<string, unknown> | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && userId) loadUserDetail()
|
if (open && userId) {
|
||||||
|
setActiveTab('info')
|
||||||
|
setSssData(null)
|
||||||
|
setSssError(null)
|
||||||
|
setBatchIngestResult(null)
|
||||||
|
setRfmData(null)
|
||||||
|
loadUserDetail()
|
||||||
|
}
|
||||||
}, [open, userId])
|
}, [open, userId])
|
||||||
|
|
||||||
async function loadUserDetail() {
|
async function loadUserDetail() {
|
||||||
@@ -106,24 +151,34 @@ export function UserDetailModal({
|
|||||||
setUser(u)
|
setUser(u)
|
||||||
setEditPhone(u.phone || '')
|
setEditPhone(u.phone || '')
|
||||||
setEditNickname(u.nickname || '')
|
setEditNickname(u.nickname || '')
|
||||||
setEditTags(typeof u.tags === 'string' ? (JSON.parse(u.tags || '[]') as string[]) : [])
|
setSssQueryPhone(u.phone || '')
|
||||||
|
try {
|
||||||
|
setEditTags(typeof u.tags === 'string' ? (JSON.parse(u.tags || '[]') as string[]) : [])
|
||||||
|
} catch {
|
||||||
|
setEditTags([])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 行为轨迹(用户旅程)
|
||||||
try {
|
try {
|
||||||
const trackData = await get<{ success?: boolean; tracks?: UserTrack[] }>(
|
const trackData = await get<{ success?: boolean; tracks?: UserTrack[] }>(
|
||||||
`/api/user/track?userId=${encodeURIComponent(userId)}&limit=50`,
|
`/api/user/track?userId=${encodeURIComponent(userId)}&limit=50`,
|
||||||
)
|
)
|
||||||
if (trackData?.success && trackData.tracks) setTracks(trackData.tracks)
|
if (trackData?.success && trackData.tracks) setTracks(trackData.tracks)
|
||||||
} catch {
|
} catch { setTracks([]) }
|
||||||
setTracks([])
|
// 关系链路
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const refData = await get<{ success?: boolean; referrals?: unknown[] }>(
|
const refData = await get<{ success?: boolean; referrals?: unknown[] }>(
|
||||||
`/api/db/users/referrals?userId=${encodeURIComponent(userId)}`,
|
`/api/db/users/referrals?userId=${encodeURIComponent(userId)}`,
|
||||||
)
|
)
|
||||||
if (refData?.success && refData.referrals) setReferrals(refData.referrals)
|
if (refData?.success && refData.referrals) setReferrals(refData.referrals)
|
||||||
} catch {
|
} catch { setReferrals([]) }
|
||||||
setReferrals([])
|
// RFM 数据
|
||||||
}
|
try {
|
||||||
|
const rfm = await get<{ success?: boolean; rfm?: RFMData }>(
|
||||||
|
`/api/db/users/rfm-single?userId=${encodeURIComponent(userId)}`,
|
||||||
|
)
|
||||||
|
if (rfm?.success && rfm.rfm) setRfmData(rfm.rfm)
|
||||||
|
} catch { setRfmData(null) }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Load user detail error:', e)
|
console.error('Load user detail error:', e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -132,10 +187,7 @@ export function UserDetailModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSyncCKB() {
|
async function handleSyncCKB() {
|
||||||
if (!user?.phone) {
|
if (!user?.phone) { alert('用户未绑定手机号,无法同步'); return }
|
||||||
alert('用户未绑定手机号,无法同步')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setSyncing(true)
|
setSyncing(true)
|
||||||
try {
|
try {
|
||||||
const data = await post<{ success?: boolean; error?: string }>('/api/ckb/sync', {
|
const data = await post<{ success?: boolean; error?: string }>('/api/ckb/sync', {
|
||||||
@@ -143,12 +195,8 @@ export function UserDetailModal({
|
|||||||
phone: user.phone,
|
phone: user.phone,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
})
|
})
|
||||||
if (data?.success) {
|
if (data?.success) { alert('同步成功'); loadUserDetail() }
|
||||||
alert('同步成功')
|
else alert('同步失败: ' + (data as { error?: string })?.error)
|
||||||
loadUserDetail()
|
|
||||||
} else {
|
|
||||||
alert('同步失败: ' + (data as { error?: string })?.error)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Sync CKB error:', e)
|
console.error('Sync CKB error:', e)
|
||||||
alert('同步失败')
|
alert('同步失败')
|
||||||
@@ -190,8 +238,60 @@ export function UserDetailModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeTag = (tag: string) => {
|
const removeTag = (tag: string) => setEditTags(editTags.filter((t) => t !== tag))
|
||||||
setEditTags(editTags.filter((t) => t !== tag))
|
|
||||||
|
// 神射手查询
|
||||||
|
async function handleSSSQuery() {
|
||||||
|
if (!sssQueryPhone && !user?.openId) {
|
||||||
|
setSssError('请输入手机号或确保用户已绑定 openId')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSssLoading(true)
|
||||||
|
setSssError(null)
|
||||||
|
setSssData(null)
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (sssQueryPhone) params.set('phone', sssQueryPhone)
|
||||||
|
if (user?.openId) params.set('openId', user.openId)
|
||||||
|
const data = await get<{ success?: boolean; data?: ShensheShouData; error?: string }>(
|
||||||
|
`/api/admin/shensheshou/query?${params}`,
|
||||||
|
)
|
||||||
|
if (data?.success && data.data) setSssData(data.data)
|
||||||
|
else setSssError(data?.error || '未查询到数据,可能该用户未在神射手中收录')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('SSS query error:', e)
|
||||||
|
setSssError('请求失败,请检查神射手接口配置')
|
||||||
|
} finally {
|
||||||
|
setSssLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 神射手 - 将当前用户信息推送/同步到神射手
|
||||||
|
async function handleSSSIngest() {
|
||||||
|
if (!user) return
|
||||||
|
setBatchIngestLoading(true)
|
||||||
|
setBatchIngestResult(null)
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
users: [{
|
||||||
|
phone: user.phone || '',
|
||||||
|
name: user.nickname || '',
|
||||||
|
openId: user.openId || '',
|
||||||
|
tags: editTags,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
const data = await post<{ success?: boolean; data?: Record<string, unknown>; error?: string }>(
|
||||||
|
'/api/admin/shensheshou/ingest',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
if (data?.success && data.data) setBatchIngestResult(data.data)
|
||||||
|
else setBatchIngestResult({ error: data?.error || '推送失败' })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('SSS ingest error:', e)
|
||||||
|
setBatchIngestResult({ error: '请求失败' })
|
||||||
|
} finally {
|
||||||
|
setBatchIngestLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getActionIcon = (action: string) => {
|
const getActionIcon = (action: string) => {
|
||||||
@@ -204,11 +304,24 @@ export function UserDetailModal({
|
|||||||
share: Link2,
|
share: Link2,
|
||||||
bind_phone: Phone,
|
bind_phone: Phone,
|
||||||
bind_wechat: MessageCircle,
|
bind_wechat: MessageCircle,
|
||||||
|
fill_profile: Tag,
|
||||||
|
visit_page: Navigation,
|
||||||
}
|
}
|
||||||
const Icon = icons[action] || History
|
const Icon = icons[action] || Clock
|
||||||
return <Icon className="w-4 h-4" />
|
return <Icon className="w-4 h-4" />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRFMLevelColor = (level: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
S: 'bg-amber-500/20 text-amber-400',
|
||||||
|
A: 'bg-green-500/20 text-green-400',
|
||||||
|
B: 'bg-blue-500/20 text-blue-400',
|
||||||
|
C: 'bg-gray-500/20 text-gray-400',
|
||||||
|
D: 'bg-red-500/20 text-red-400',
|
||||||
|
}
|
||||||
|
return map[level] || 'bg-gray-500/20 text-gray-400'
|
||||||
|
}
|
||||||
|
|
||||||
if (!open) return null
|
if (!open) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -218,9 +331,8 @@ export function UserDetailModal({
|
|||||||
<DialogTitle className="text-white flex items-center gap-2">
|
<DialogTitle className="text-white flex items-center gap-2">
|
||||||
<User className="w-5 h-5 text-[#38bdac]" />
|
<User className="w-5 h-5 text-[#38bdac]" />
|
||||||
用户详情
|
用户详情
|
||||||
{user?.phone && (
|
{user?.phone && <Badge className="bg-green-500/20 text-green-400 border-0 ml-2">已绑定手机</Badge>}
|
||||||
<Badge className="bg-green-500/20 text-green-400 border-0 ml-2">已绑定手机</Badge>
|
{user?.isVip && <Badge className="bg-amber-500/20 text-amber-400 border-0">VIP</Badge>}
|
||||||
)}
|
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -230,55 +342,77 @@ export function UserDetailModal({
|
|||||||
<span className="ml-2 text-gray-400">加载中...</span>
|
<span className="ml-2 text-gray-400">加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
) : user ? (
|
) : user ? (
|
||||||
<div className="flex flex-col h-[70vh]">
|
<div className="flex flex-col h-[75vh]">
|
||||||
<div className="flex items-center gap-4 p-4 bg-[#0a1628] rounded-lg mb-4">
|
{/* 用户头部信息 */}
|
||||||
<div className="w-16 h-16 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-2xl text-[#38bdac]">
|
<div className="flex items-center gap-4 p-4 bg-[#0a1628] rounded-lg mb-3">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-2xl text-[#38bdac] shrink-0">
|
||||||
{user.avatar ? (
|
{user.avatar ? (
|
||||||
<img src={user.avatar} className="w-full h-full rounded-full object-cover" alt="" />
|
<img src={user.avatar} className="w-full h-full rounded-full object-cover" alt="" />
|
||||||
) : (
|
) : (
|
||||||
user.nickname?.charAt(0) || '?'
|
user.nickname?.charAt(0) || '?'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<h3 className="text-lg font-bold text-white">{user.nickname}</h3>
|
<h3 className="text-lg font-bold text-white">{user.nickname}</h3>
|
||||||
{user.isAdmin && (
|
{user.isAdmin && <Badge className="bg-purple-500/20 text-purple-400 border-0">管理员</Badge>}
|
||||||
<Badge className="bg-purple-500/20 text-purple-400 border-0">管理员</Badge>
|
{user.hasFullBook && <Badge className="bg-green-500/20 text-green-400 border-0">全书已购</Badge>}
|
||||||
)}
|
{user.vipRole && <Badge className="bg-amber-500/20 text-amber-400 border-0">{user.vipRole}</Badge>}
|
||||||
{user.hasFullBook && (
|
|
||||||
<Badge className="bg-green-500/20 text-green-400 border-0">全书已购</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-400 text-sm mt-1">
|
<p className="text-gray-400 text-sm mt-1">
|
||||||
{user.phone ? `📱 ${user.phone}` : '未绑定手机'}
|
{user.phone ? `📱 ${user.phone}` : '未绑定手机'}
|
||||||
{user.wechatId && ` · 💬 ${user.wechatId}`}
|
{user.wechatId && ` · 💬 ${user.wechatId}`}
|
||||||
|
{user.mbti && ` · ${user.mbti}`}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 text-xs mt-1">
|
<div className="flex items-center gap-4 mt-1">
|
||||||
ID: {user.id} · 推广码: {user.referralCode ?? '-'}
|
<p className="text-gray-600 text-xs">ID: {user.id.slice(0, 16)}…</p>
|
||||||
</p>
|
{user.referralCode && (
|
||||||
|
<p className="text-xs">
|
||||||
|
<span className="text-gray-500">推广码:</span>
|
||||||
|
<code className="text-[#38bdac] bg-[#38bdac]/10 px-1.5 py-0.5 rounded">{user.referralCode}</code>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right shrink-0">
|
||||||
<p className="text-[#38bdac] font-bold">¥{(user.earnings || 0).toFixed(2)}</p>
|
<p className="text-[#38bdac] font-bold text-lg">¥{(user.earnings || 0).toFixed(2)}</p>
|
||||||
<p className="text-gray-500 text-xs">累计收益</p>
|
<p className="text-gray-500 text-xs">累计收益</p>
|
||||||
|
{rfmData && (
|
||||||
|
<div className="mt-1">
|
||||||
|
<Badge className={`border-0 text-xs ${getRFMLevelColor(rfmData.rfmLevel)}`}>
|
||||||
|
RFM {rfmData.rfmLevel} 级 · {rfmData.rfmScore}分
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col overflow-hidden">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col overflow-hidden">
|
||||||
<TabsList className="bg-[#0a1628] border border-gray-700/50 p-1 mb-4">
|
<TabsList className="bg-[#0a1628] border border-gray-700/50 p-1 mb-3 flex-wrap h-auto gap-1">
|
||||||
<TabsTrigger value="info" className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac]">
|
<TabsTrigger value="info" className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-xs">
|
||||||
基础信息
|
基础信息
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="tags" className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac]">
|
<TabsTrigger value="rfm" className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-xs">
|
||||||
|
<TrendingUp className="w-3 h-3 mr-1" />
|
||||||
|
RFM估值
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="tags" className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-xs">
|
||||||
标签体系
|
标签体系
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="tracks" className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac]">
|
<TabsTrigger value="journey" className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-xs">
|
||||||
行为轨迹
|
<Navigation className="w-3 h-3 mr-1" />
|
||||||
|
用户旅程
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="relations" className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac]">
|
<TabsTrigger value="relations" className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-xs">
|
||||||
关系链路
|
关系链路
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="shensheshou" className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-xs">
|
||||||
|
<Zap className="w-3 h-3 mr-1" />
|
||||||
|
神射手
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
{/* ===== 基础信息 ===== */}
|
||||||
<TabsContent value="info" className="flex-1 overflow-auto space-y-4">
|
<TabsContent value="info" className="flex-1 overflow-auto space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -300,6 +434,36 @@ export function UserDetailModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* 详细信息展示 */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
{user.openId && (
|
||||||
|
<div className="p-3 bg-[#0a1628] rounded-lg">
|
||||||
|
<p className="text-gray-500 text-xs mb-1">微信 OpenID</p>
|
||||||
|
<p className="text-gray-300 font-mono text-xs break-all">{user.openId}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{user.region && (
|
||||||
|
<div className="p-3 bg-[#0a1628] rounded-lg flex items-center gap-2">
|
||||||
|
<MapPin className="w-4 h-4 text-gray-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-500 text-xs">地区</p>
|
||||||
|
<p className="text-white">{user.region}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{user.industry && (
|
||||||
|
<div className="p-3 bg-[#0a1628] rounded-lg">
|
||||||
|
<p className="text-gray-500 text-xs mb-1">行业</p>
|
||||||
|
<p className="text-white">{user.industry}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{user.position && (
|
||||||
|
<div className="p-3 bg-[#0a1628] rounded-lg">
|
||||||
|
<p className="text-gray-500 text-xs mb-1">职位</p>
|
||||||
|
<p className="text-white">{user.position}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||||
<p className="text-gray-400 text-sm">推荐人数</p>
|
<p className="text-gray-400 text-sm">推荐人数</p>
|
||||||
@@ -318,6 +482,24 @@ export function UserDetailModal({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* VIP 信息 */}
|
||||||
|
{user.isVip && (
|
||||||
|
<div className="p-4 bg-[#0a1628] rounded-lg border border-amber-500/20">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Crown className="w-4 h-4 text-amber-400" />
|
||||||
|
<span className="text-white font-medium">VIP 信息</span>
|
||||||
|
<Badge className="bg-amber-500/20 text-amber-400 border-0 text-xs">{user.vipRole || 'VIP'}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
{user.vipName && <div><span className="text-gray-500">展示名:</span><span className="text-white">{user.vipName}</span></div>}
|
||||||
|
{user.vipProject && <div><span className="text-gray-500">项目:</span><span className="text-white">{user.vipProject}</span></div>}
|
||||||
|
{user.vipContact && <div><span className="text-gray-500">联系方式:</span><span className="text-white">{user.vipContact}</span></div>}
|
||||||
|
{user.vipExpireDate && <div><span className="text-gray-500">到期时间:</span><span className="text-white">{new Date(user.vipExpireDate).toLocaleDateString()}</span></div>}
|
||||||
|
</div>
|
||||||
|
{user.vipBio && <p className="text-gray-400 text-sm mt-2">{user.vipBio}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* 存客宝同步 */}
|
||||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -331,13 +513,9 @@ export function UserDetailModal({
|
|||||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||||
>
|
>
|
||||||
{syncing ? (
|
{syncing ? (
|
||||||
<>
|
<><RefreshCw className="w-4 h-4 mr-1 animate-spin" /> 同步中...</>
|
||||||
<RefreshCw className="w-4 h-4 mr-1 animate-spin" /> 同步中...
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<><RefreshCw className="w-4 h-4 mr-1" /> 同步数据</>
|
||||||
<RefreshCw className="w-4 h-4 mr-1" /> 同步数据
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -360,54 +538,212 @@ export function UserDetailModal({
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* ===== RFM 估值 ===== */}
|
||||||
|
<TabsContent value="rfm" className="flex-1 overflow-auto space-y-4">
|
||||||
|
{rfmData ? (
|
||||||
|
<>
|
||||||
|
<div className="p-4 bg-[#0a1628] rounded-lg border border-gray-700/30">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-5 h-5 text-[#38bdac]" />
|
||||||
|
<span className="text-white font-medium text-lg">用户 RFM 估值</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-3xl font-bold text-white">{rfmData.rfmScore}</div>
|
||||||
|
<Badge className={`border-0 ${getRFMLevelColor(rfmData.rfmLevel)}`}>
|
||||||
|
{rfmData.rfmLevel} 级用户
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="p-3 bg-[#162840] rounded-lg text-center">
|
||||||
|
<div className="w-8 h-8 rounded bg-red-500/20 flex items-center justify-center text-sm font-bold text-red-400 mx-auto mb-2">R</div>
|
||||||
|
<div className="text-2xl font-bold text-red-400">{rfmData.recency}</div>
|
||||||
|
<div className="text-gray-500 text-xs mt-1">最近购买天数</div>
|
||||||
|
<div className="text-gray-600 text-xs">越低越好</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-[#162840] rounded-lg text-center">
|
||||||
|
<div className="w-8 h-8 rounded bg-blue-500/20 flex items-center justify-center text-sm font-bold text-blue-400 mx-auto mb-2">F</div>
|
||||||
|
<div className="text-2xl font-bold text-blue-400">{rfmData.frequency}</div>
|
||||||
|
<div className="text-gray-500 text-xs mt-1">购买频次</div>
|
||||||
|
<div className="text-gray-600 text-xs">越高越好</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-[#162840] rounded-lg text-center">
|
||||||
|
<div className="w-8 h-8 rounded bg-green-500/20 flex items-center justify-center text-sm font-bold text-green-400 mx-auto mb-2">M</div>
|
||||||
|
<div className="text-2xl font-bold text-green-400">¥{rfmData.monetary.toFixed(0)}</div>
|
||||||
|
<div className="text-gray-500 text-xs mt-1">消费金额</div>
|
||||||
|
<div className="text-gray-600 text-xs">越高越好</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{rfmData.lastOrderAt && (
|
||||||
|
<div className="mt-3 text-sm text-gray-500">
|
||||||
|
最近订单:{new Date(rfmData.lastOrderAt).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||||
|
<p className="text-gray-400 text-sm mb-3 font-medium">等级说明</p>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
{[
|
||||||
|
{ level: 'S', label: '超级VIP', desc: '高频高额近期活跃,顶级价值用户', color: 'text-amber-400' },
|
||||||
|
{ level: 'A', label: '高价值', desc: '消费能力强,复购率高', color: 'text-green-400' },
|
||||||
|
{ level: 'B', label: '潜力用户', desc: '有一定消费,需重点维护', color: 'text-blue-400' },
|
||||||
|
{ level: 'C', label: '普通用户', desc: '偶发购买,待激活', color: 'text-gray-400' },
|
||||||
|
{ level: 'D', label: '低活跃', desc: '长期未购买或无消费记录', color: 'text-red-400' },
|
||||||
|
].map((item) => (
|
||||||
|
<div key={item.level} className={`flex items-center gap-3 ${rfmData.rfmLevel === item.level ? 'opacity-100' : 'opacity-50'}`}>
|
||||||
|
<Badge className={`border-0 w-6 h-6 flex items-center justify-center p-0 ${getRFMLevelColor(item.level)}`}>
|
||||||
|
{item.level}
|
||||||
|
</Badge>
|
||||||
|
<span className={`font-medium w-16 ${item.color}`}>{item.label}</span>
|
||||||
|
<span className="text-gray-500">{item.desc}</span>
|
||||||
|
{rfmData.rfmLevel === item.level && (
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-[#38bdac] ml-auto" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<TrendingUp className="w-12 h-12 text-[#38bdac]/30 mx-auto mb-4" />
|
||||||
|
<p className="text-gray-400">暂无 RFM 数据</p>
|
||||||
|
<p className="text-gray-600 text-sm mt-1">用户需有至少一次购买记录才会生成 RFM 估值</p>
|
||||||
|
<Button
|
||||||
|
className="mt-4 bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||||
|
onClick={loadUserDetail}
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
重新加载
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* ===== 标签体系 ===== */}
|
||||||
<TabsContent value="tags" className="flex-1 overflow-auto space-y-4">
|
<TabsContent value="tags" className="flex-1 overflow-auto space-y-4">
|
||||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<Tag className="w-4 h-4 text-[#38bdac]" />
|
<Tag className="w-4 h-4 text-[#38bdac]" />
|
||||||
<span className="text-white font-medium">系统标签</span>
|
<span className="text-white font-medium">用户标签</span>
|
||||||
|
<span className="text-gray-500 text-xs">基于《一场 Soul 的创业实验》维度打标</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2 mb-3">
|
{/* 预设标签分类 */}
|
||||||
{editTags.map((tag, i) => (
|
<div className="mb-4 space-y-3">
|
||||||
<Badge key={i} className="bg-[#38bdac]/20 text-[#38bdac] border-0 pr-1">
|
{[
|
||||||
{tag}
|
{
|
||||||
<button type="button" onClick={() => removeTag(tag)} className="ml-1 hover:text-red-400">
|
category: '身份类型',
|
||||||
<X className="w-3 h-3" />
|
tags: ['创业者', '打工人', '自由职业', '学生', '投资人', '合伙人'],
|
||||||
</button>
|
},
|
||||||
</Badge>
|
{
|
||||||
|
category: '行业背景',
|
||||||
|
tags: ['电商', '内容', '传统行业', '科技/AI', '金融', '教育', '餐饮'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '痛点标签',
|
||||||
|
tags: ['找资源', '找方向', '找合伙人', '想赚钱', '想学习', '找情感出口'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '付费意愿',
|
||||||
|
tags: ['高意向', '已付费', '观望中', '薅羊毛'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'MBTI',
|
||||||
|
tags: ['ENTJ', 'INTJ', 'ENFP', 'INFP', 'ENTP', 'INTP', 'ESTJ', 'ISFJ'],
|
||||||
|
},
|
||||||
|
].map((group) => (
|
||||||
|
<div key={group.category}>
|
||||||
|
<p className="text-gray-500 text-xs mb-1.5">{group.category}</p>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{group.tags.map((tag) => (
|
||||||
|
<button
|
||||||
|
key={tag}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (editTags.includes(tag)) removeTag(tag)
|
||||||
|
else setEditTags([...editTags, tag])
|
||||||
|
}}
|
||||||
|
className={`px-2 py-0.5 rounded text-xs border transition-all ${
|
||||||
|
editTags.includes(tag)
|
||||||
|
? 'bg-[#38bdac]/20 border-[#38bdac]/50 text-[#38bdac]'
|
||||||
|
: 'bg-transparent border-gray-700 text-gray-500 hover:border-gray-500 hover:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{editTags.includes(tag) ? '✓ ' : ''}{tag}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
{editTags.length === 0 && <span className="text-gray-500 text-sm">暂无标签</span>}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="border-t border-gray-700/50 pt-3">
|
||||||
<Input
|
<p className="text-gray-500 text-xs mb-2">已选标签</p>
|
||||||
className="bg-[#162840] border-gray-700 text-white flex-1"
|
<div className="flex flex-wrap gap-2 mb-3 min-h-[32px]">
|
||||||
placeholder="添加新标签"
|
{editTags.map((tag, i) => (
|
||||||
value={newTag}
|
<Badge key={i} className="bg-[#38bdac]/20 text-[#38bdac] border-0 pr-1">
|
||||||
onChange={(e) => setNewTag(e.target.value)}
|
{tag}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && addTag()}
|
<button type="button" onClick={() => removeTag(tag)} className="ml-1 hover:text-red-400">
|
||||||
/>
|
<X className="w-3 h-3" />
|
||||||
<Button onClick={addTag} className="bg-[#38bdac] hover:bg-[#2da396]">
|
</button>
|
||||||
添加
|
</Badge>
|
||||||
</Button>
|
))}
|
||||||
|
{editTags.length === 0 && <span className="text-gray-600 text-sm">暂未选择标签</span>}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
className="bg-[#162840] border-gray-700 text-white flex-1"
|
||||||
|
placeholder="自定义标签(回车添加)"
|
||||||
|
value={newTag}
|
||||||
|
onChange={(e) => setNewTag(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && addTag()}
|
||||||
|
/>
|
||||||
|
<Button onClick={addTag} className="bg-[#38bdac] hover:bg-[#2da396]">添加</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* 存客宝标签 */}
|
||||||
|
{user.ckbTags && (
|
||||||
|
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Tag className="w-4 h-4 text-purple-400" />
|
||||||
|
<span className="text-white font-medium">存客宝标签</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(typeof user.ckbTags === 'string' ? user.ckbTags.split(',') : []).map((tag, i) => (
|
||||||
|
<Badge key={i} className="bg-purple-500/20 text-purple-400 border-0">{tag.trim()}</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="tracks" className="flex-1 overflow-auto">
|
{/* ===== 用户旅程(原行为轨迹)===== */}
|
||||||
|
<TabsContent value="journey" className="flex-1 overflow-auto">
|
||||||
|
<div className="mb-3 p-3 bg-[#0a1628] rounded-lg flex items-center gap-2">
|
||||||
|
<Navigation className="w-4 h-4 text-[#38bdac]" />
|
||||||
|
<span className="text-gray-400 text-sm">记录用户从注册到付费的完整行动路径,共 {tracks.length} 条记录</span>
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{tracks.length > 0 ? (
|
{tracks.length > 0 ? (
|
||||||
tracks.map((track) => (
|
tracks.map((track, idx) => (
|
||||||
<div key={track.id} className="flex items-start gap-3 p-3 bg-[#0a1628] rounded-lg">
|
<div key={track.id} className="flex items-start gap-3 p-3 bg-[#0a1628] rounded-lg">
|
||||||
<div className="w-8 h-8 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-[#38bdac]">
|
<div className="flex flex-col items-center">
|
||||||
{getActionIcon(track.action)}
|
<div className="w-8 h-8 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-[#38bdac]">
|
||||||
|
{getActionIcon(track.action)}
|
||||||
|
</div>
|
||||||
|
{idx < tracks.length - 1 && (
|
||||||
|
<div className="w-0.5 h-4 bg-gray-700/50 mt-1" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1 pb-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-white font-medium">{track.actionLabel}</span>
|
<span className="text-white font-medium">{track.actionLabel}</span>
|
||||||
{track.chapterTitle && (
|
{track.chapterTitle && (
|
||||||
<span className="text-gray-400 text-sm">- {track.chapterTitle}</span>
|
<span className="text-gray-400 text-sm">- {track.chapterTitle}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-500 text-xs mt-1">
|
<p className="text-gray-500 text-xs mt-0.5">
|
||||||
<Clock className="w-3 h-3 inline mr-1" />
|
<Clock className="w-3 h-3 inline mr-1" />
|
||||||
{track.timeAgo} · {new Date(track.createdAt).toLocaleString()}
|
{track.timeAgo} · {new Date(track.createdAt).toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
@@ -416,13 +752,15 @@ export function UserDetailModal({
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<History className="w-10 h-10 text-[#38bdac]/40 mx-auto mb-4" />
|
<Navigation className="w-10 h-10 text-[#38bdac]/40 mx-auto mb-4" />
|
||||||
<p className="text-gray-400">暂无行为轨迹</p>
|
<p className="text-gray-400">暂无用户旅程记录</p>
|
||||||
|
<p className="text-gray-600 text-sm mt-1">当用户浏览章节、购买或完善信息时会自动记录</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* ===== 关系链路 ===== */}
|
||||||
<TabsContent value="relations" className="flex-1 overflow-auto space-y-4">
|
<TabsContent value="relations" className="flex-1 overflow-auto space-y-4">
|
||||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
@@ -432,7 +770,7 @@ export function UserDetailModal({
|
|||||||
</div>
|
</div>
|
||||||
<Badge className="bg-[#38bdac]/20 text-[#38bdac] border-0">共 {referrals.length} 人</Badge>
|
<Badge className="bg-[#38bdac]/20 text-[#38bdac] border-0">共 {referrals.length} 人</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 max-h-[200px] overflow-y-auto">
|
<div className="space-y-2 max-h-[250px] overflow-y-auto">
|
||||||
{referrals.length > 0 ? (
|
{referrals.length > 0 ? (
|
||||||
referrals.map((ref: unknown, i: number) => {
|
referrals.map((ref: unknown, i: number) => {
|
||||||
const r = ref as { id?: string; nickname?: string; status?: string; createdAt?: string }
|
const r = ref as { id?: string; nickname?: string; status?: string; createdAt?: string }
|
||||||
@@ -461,9 +799,119 @@ export function UserDetailModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* ===== 神射手 ===== */}
|
||||||
|
<TabsContent value="shensheshou" className="flex-1 overflow-auto space-y-4">
|
||||||
|
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Zap className="w-5 h-5 text-[#38bdac]" />
|
||||||
|
<span className="text-white font-medium">神射手数据查询</span>
|
||||||
|
<span className="text-gray-500 text-xs">通过手机号 / OpenID 查询用户画像</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<Input
|
||||||
|
className="bg-[#162840] border-gray-700 text-white flex-1"
|
||||||
|
placeholder="手机号(自动填入已绑定手机)"
|
||||||
|
value={sssQueryPhone}
|
||||||
|
onChange={(e) => setSssQueryPhone(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleSSSQuery}
|
||||||
|
disabled={sssLoading}
|
||||||
|
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||||
|
>
|
||||||
|
{sssLoading ? (
|
||||||
|
<><RefreshCw className="w-4 h-4 mr-1 animate-spin" /> 查询中</>
|
||||||
|
) : (
|
||||||
|
<><Search className="w-4 h-4 mr-1" /> 查询</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{sssError && (
|
||||||
|
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm mb-3">
|
||||||
|
{sssError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sssData && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="p-3 bg-[#162840] rounded-lg">
|
||||||
|
<p className="text-gray-500 text-xs mb-1">神射手 RFM 分</p>
|
||||||
|
<p className="text-2xl font-bold text-[#38bdac]">{sssData.rfm_score ?? '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-[#162840] rounded-lg">
|
||||||
|
<p className="text-gray-500 text-xs mb-1">用户等级</p>
|
||||||
|
<p className="text-2xl font-bold text-white">{sssData.user_level ?? '-'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{sssData.tags && sssData.tags.length > 0 && (
|
||||||
|
<div className="p-3 bg-[#162840] rounded-lg">
|
||||||
|
<p className="text-gray-500 text-xs mb-2">神射手标签</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{sssData.tags.map((tag, i) => (
|
||||||
|
<Badge key={i} className="bg-[#38bdac]/10 text-[#38bdac] border border-[#38bdac]/20">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sssData.last_active && (
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
最近活跃:{sssData.last_active}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 推送到神射手 */}
|
||||||
|
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Zap className="w-4 h-4 text-purple-400" />
|
||||||
|
<span className="text-white font-medium">推送用户数据到神射手</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-500 text-xs">将本用户信息(手机号、昵称、标签等)同步至神射手,自动完善用户画像</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleSSSIngest}
|
||||||
|
disabled={batchIngestLoading || !user.phone}
|
||||||
|
variant="outline"
|
||||||
|
className="border-purple-500/40 text-purple-400 hover:bg-purple-500/10 bg-transparent shrink-0 ml-4"
|
||||||
|
>
|
||||||
|
{batchIngestLoading ? (
|
||||||
|
<><RefreshCw className="w-4 h-4 mr-1 animate-spin" /> 推送中</>
|
||||||
|
) : (
|
||||||
|
<><Zap className="w-4 h-4 mr-1" /> 推送</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{!user.phone && (
|
||||||
|
<p className="text-yellow-500/70 text-xs">⚠ 用户未绑定手机号,无法推送</p>
|
||||||
|
)}
|
||||||
|
{batchIngestResult && (
|
||||||
|
<div className="mt-3 p-3 bg-[#162840] rounded-lg text-sm">
|
||||||
|
{batchIngestResult.error ? (
|
||||||
|
<p className="text-red-400">{String(batchIngestResult.error)}</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-green-400 flex items-center gap-1">
|
||||||
|
<CheckCircle2 className="w-4 h-4" /> 推送成功
|
||||||
|
</p>
|
||||||
|
{batchIngestResult.enriched !== undefined && (
|
||||||
|
<p className="text-gray-400">自动补全标签数:{String(batchIngestResult.new_tags_added ?? 0)}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4 border-t border-gray-700 mt-4">
|
<div className="flex justify-end gap-2 pt-3 border-t border-gray-700 mt-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
|||||||
Reference in New Issue
Block a user