import toast from '@/utils/toast' import { normalizeImageUrl } from '@/lib/utils' import { useState, useEffect, useMemo } from 'react' import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Switch } from '@/components/ui/switch' import { User, Phone, MapPin, RefreshCw, Link2, BookOpen, ShoppingBag, Users, MessageCircle, Clock, Save, X, Tag, Zap, Search, CheckCircle2, Crown, Key, Navigation, } from 'lucide-react' import { get, put, post } from '@/api/client' interface UserDetailModalProps { open: boolean onClose: () => void userId: string | null onUserUpdated?: () => void } interface UserDetail { id: string phone?: string nickname: string avatar?: string wechatId?: string openId?: string referralCode?: string referredBy?: string hasFullBook?: boolean isAdmin?: boolean earnings?: number pendingEarnings?: number referralCount?: number createdAt?: string updatedAt?: string tags?: string ckbTags?: string ckbSyncedAt?: string isVip?: boolean vipExpireDate?: string | null vipName?: string | null vipAvatar?: string | null vipProject?: string | null vipContact?: string | null vipBio?: string | null vipRole?: string | null // 扩展字段 mbti?: string region?: string industry?: string position?: string } interface UserTrack { id: string action: string actionLabel?: string target?: string chapterTitle?: string module?: string moduleLabel?: string createdAt: string timeAgo?: string } interface InboundVisitItem { seq: number visitedAt?: string referrerId?: string referrerNickname?: string referrerAvatar?: string source?: string page?: string } interface InboundSourceData { totalVisits?: number firstVisit?: InboundVisitItem latestVisit?: InboundVisitItem activeBinding?: { referrerId?: string referrerNickname?: string referrerAvatar?: string referralCode?: string bindingDate?: string expiryDate?: string } visits?: InboundVisitItem[] } interface ShensheShouData { rfm_score?: number user_level?: string tags?: string[] last_active?: string phone?: string } const TRACK_ACTION_LABELS: Record = { view_chapter: '浏览章节', purchase: '购买', match: '派对匹配', login: '登录', register: '注册', share: '分享', bind_phone: '绑定手机', bind_wechat: '绑定微信', fill_profile: '完善资料', fill_avatar: '设置头像', visit_page: '访问页面', first_pay: '首次付款', vip_activate: '开通会员', click_super: '点击超级个体', lead_submit: '提交留资', withdraw: '申请提现', referral_bind: '绑定推荐人', card_click: '点击名片', btn_click: '按钮点击', tab_click: '切换标签', nav_click: '导航点击', page_view: '页面浏览', search: '搜索', } function labelTrackAction(action: string) { return TRACK_ACTION_LABELS[action] || action || '行为' } /** 根据行为轨迹统计推断运营标签(供标签体系 Tab 展示,可一键合并到已选) */ function inferTagsFromJourney(trackStats: Record, user: UserDetail | null): string[] { const set = new Set() const has = (a: string) => (trackStats[a] ?? 0) > 0 if (has('purchase') || has('first_pay') || has('vip_activate')) set.add('已付费') if (has('lead_submit') || has('click_super')) set.add('高意向') if (has('view_chapter')) set.add('想学习') if (has('match')) set.add('找合伙人') if (has('withdraw')) set.add('有提现行为') if (has('referral_bind')) set.add('推广参与') if (has('fill_profile') || has('fill_avatar') || has('bind_phone')) set.add('资料完善中') if (user?.hasFullBook) set.add('全书读者') if (user?.isVip) set.add('VIP会员') if (user?.mbti && /^[EI][NS][FT][JP]$/i.test(user.mbti)) set.add(String(user.mbti).toUpperCase()) return Array.from(set) } export function UserDetailModal({ open, onClose, userId, onUserUpdated, }: UserDetailModalProps) { const [user, setUser] = useState(null) const [tracks, setTracks] = useState([]) const [trackStats, setTrackStats] = useState>({}) const [referrals, setReferrals] = useState([]) const [inboundSource, setInboundSource] = useState(null) const [balanceData, setBalanceData] = useState<{ balance: number; transactions: Array<{ id: string; type: string; amount: number; orderId?: string; createdAt: string }> } | null>(null) const [loading, setLoading] = useState(false) const [syncing, setSyncing] = useState(false) const [saving, setSaving] = useState(false) const [activeTab, setActiveTab] = useState('info') const [editPhone, setEditPhone] = useState('') const [editWechatId, setEditWechatId] = useState('') const [editNickname, setEditNickname] = useState('') const [editTags, setEditTags] = useState([]) const [newTag, setNewTag] = useState('') // 修改密码 const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [passwordSaving, setPasswordSaving] = useState(false) // 设成超级个体(VIP) const [vipForm, setVipForm] = useState({ isVip: false, vipExpireDate: '', vipRole: '', vipName: '', vipProject: '', vipContact: '', vipBio: '' }) const [vipRoles, setVipRoles] = useState<{ id: number; name: string }[]>([]) // 调整余额 const [adjustBalanceOpen, setAdjustBalanceOpen] = useState(false) const [adjustAmount, setAdjustAmount] = useState('') const [adjustRemark, setAdjustRemark] = useState('') const [adjustLoading, setAdjustLoading] = useState(false) // 用户资料完善(神射手) const [sssLoading, setSssLoading] = useState(false) const [sssData, setSssData] = useState(null) const [sssError, setSssError] = useState(null) const [sssQueryPhone, setSssQueryPhone] = useState('') const [sssQueryWechatId, setSssQueryWechatId] = useState('') const [sssQueryOpenId, setSssQueryOpenId] = useState('') const [batchIngestLoading, setBatchIngestLoading] = useState(false) const [batchIngestResult, setBatchIngestResult] = useState | null>(null) const [avatarBroken, setAvatarBroken] = useState(false) const [mbtiAvatarsMap, setMbtiAvatarsMap] = useState>({}) const [purchaseList, setPurchaseList] = useState<{ orderSn: string; productType: string; productId?: string; amount: number; createdAt: string }[]>([]) useEffect(() => { if (open && userId) { setAvatarBroken(false) setActiveTab('info') setSssData(null) setSssError(null) setBatchIngestResult(null) setNewPassword('') setConfirmPassword('') loadUserDetail() get<{ success?: boolean; data?: { id: number; name: string }[] }>('/api/db/vip-roles').then((r) => { if (r?.success && (r as { data?: { id: number; name: string }[] }).data) setVipRoles((r as { data: { id: number; name: string }[] }).data) }).catch(() => {}) } }, [open, userId]) useEffect(() => { if (!open) return get<{ success?: boolean; avatars?: Record }>('/api/admin/mbti-avatars') .then((r) => { if (r?.avatars && typeof r.avatars === 'object') setMbtiAvatarsMap(r.avatars) else setMbtiAvatarsMap({}) }) .catch(() => setMbtiAvatarsMap({})) }, [open]) const resolveAvatarByMbti = (avatar?: string | null, mbti?: string | null): string => { const av = (avatar || '').trim() if (av) return normalizeImageUrl(av) const key = (mbti || '').trim().toUpperCase() if (!/^[EI][NS][FT][JP]$/.test(key)) return '' return (mbtiAvatarsMap[key] || '').trim() } async function loadUserDetail() { if (!userId) return setLoading(true) try { const userData = await get<{ success?: boolean; user?: UserDetail }>( `/api/db/users?id=${encodeURIComponent(userId)}`, ) if (userData?.success && userData.user) { const u = userData.user setUser(u) setEditPhone(u.phone || '') setEditWechatId(u.wechatId || '') setEditNickname(u.nickname || '') setSssQueryPhone(u.phone || '') setSssQueryWechatId(u.wechatId || '') setSssQueryOpenId(u.openId || '') try { setEditTags(typeof u.tags === 'string' ? (JSON.parse(u.tags || '[]') as string[]) : []) } catch { setEditTags([]) } setVipForm({ isVip: !!(u.isVip ?? false), vipExpireDate: u.vipExpireDate ? String(u.vipExpireDate).slice(0, 10) : '', vipRole: String(u.vipRole ?? ''), vipName: String(u.vipName ?? ''), vipProject: String(u.vipProject ?? ''), vipContact: String(u.vipContact ?? ''), vipBio: String(u.vipBio ?? ''), }) } // 行为轨迹(用户旅程) try { const trackData = await get<{ success?: boolean; tracks?: UserTrack[]; stats?: Record }>( `/api/admin/user/track?userId=${encodeURIComponent(userId)}&limit=100`, ) if (trackData?.success) { setTrackStats(trackData.stats && typeof trackData.stats === 'object' ? trackData.stats : {}) const list = trackData.tracks || [] setTracks( list.map((t) => ({ ...t, actionLabel: t.actionLabel || t.action, timeAgo: t.timeAgo || '', })), ) } else { setTrackStats({}) setTracks([]) } } catch { setTrackStats({}) setTracks([]) } // 关系链路 try { const refData = await get<{ success?: boolean; referrals?: unknown[]; inboundSource?: InboundSourceData }>( `/api/db/users/referrals?userId=${encodeURIComponent(userId)}`, ) if (refData?.success) { setReferrals(refData.referrals || []) setInboundSource(refData.inboundSource || null) } else { setReferrals([]) setInboundSource(null) } } catch { setReferrals([]) setInboundSource(null) } try { const balData = await get<{ success?: boolean; data?: { balance: number; transactions: Array<{ id: string; type: string; amount: number; orderId?: string; createdAt: string }> } }>( `/api/admin/users/${encodeURIComponent(userId)}/balance`, ) if (balData?.success && balData.data) setBalanceData(balData.data) else setBalanceData(null) } catch { setBalanceData(null) } try { const ordersData = await get<{ success?: boolean; orders?: { orderSn: string; productType: string; productId?: string; amount: number; createdAt: string }[] }>( `/api/orders?userId=${encodeURIComponent(userId)}&status=paid&pageSize=50`, ) if (ordersData?.success && ordersData.orders) setPurchaseList(ordersData.orders) else setPurchaseList([]) } catch { setPurchaseList([]) } } catch (e) { console.error('Load user detail error:', e) } finally { setLoading(false) } } async function handleSyncCKB() { if (!user?.phone) { toast.info('用户未绑定手机号,无法同步'); return } setSyncing(true) try { const data = await post<{ success?: boolean; error?: string }>('/api/ckb/sync', { action: 'full_sync', phone: user.phone, userId: user.id, }) if (data?.success) { toast.success('同步成功'); loadUserDetail() } else toast.error('同步失败: ' + (data as { error?: string })?.error) } catch (e) { console.error('Sync CKB error:', e) toast.error('同步失败') } finally { setSyncing(false) } } async function handleSave() { if (!user) return if (vipForm.isVip && !vipForm.vipExpireDate.trim()) { toast.error('开启 VIP 请填写有效到期日') return } setSaving(true) try { const payload: Record = { id: user.id, phone: editPhone.trim() || undefined, wechatId: editWechatId.trim(), nickname: editNickname || undefined, tags: JSON.stringify(editTags), // 超级个体/VIP 相关字段一并保存 isVip: vipForm.isVip, vipExpireDate: vipForm.isVip ? vipForm.vipExpireDate : undefined, vipRole: vipForm.vipRole || undefined, vipName: vipForm.vipName || undefined, vipProject: vipForm.vipProject || undefined, vipContact: vipForm.vipContact || undefined, vipBio: vipForm.vipBio || undefined, } const data = await put<{ success?: boolean; error?: string }>('/api/db/users', payload) if (data?.success) { toast.success('保存成功') loadUserDetail() onUserUpdated?.() } else { toast.error('保存失败: ' + (data as { error?: string })?.error) } } catch (e) { console.error('Save user error:', e) toast.error('保存失败') } finally { setSaving(false) } } const addTag = () => { if (newTag && !editTags.includes(newTag)) { setEditTags([...editTags, newTag]) setNewTag('') } } const removeTag = (tag: string) => setEditTags(editTags.filter((t) => t !== tag)) async function handleSavePassword() { if (!user) return if (!newPassword) { toast.error('请输入新密码'); return } if (newPassword !== confirmPassword) { toast.error('两次密码不一致'); return } if (newPassword.length < 6) { toast.error('密码至少 6 位'); return } setPasswordSaving(true) try { const data = await put<{ success?: boolean; error?: string }>('/api/db/users', { id: user.id, password: newPassword }) if (data?.success) { toast.success('修改成功'); setNewPassword(''); setConfirmPassword('') } else toast.error('修改失败: ' + (data?.error || '')) } catch { toast.error('修改失败') } finally { setPasswordSaving(false) } } async function handleAdjustBalance() { if (!user) return const amt = parseFloat(adjustAmount) if (Number.isNaN(amt) || amt === 0) { toast.error('请输入有效金额(正数增加、负数扣减)'); return } setAdjustLoading(true) try { const res = await post<{ success?: boolean; error?: string }>(`/api/admin/users/${user.id}/balance/adjust`, { amount: amt, remark: adjustRemark || undefined, }) if (res?.success) { toast.success('余额已调整') setAdjustBalanceOpen(false) setAdjustAmount('') setAdjustRemark('') loadUserDetail() onUserUpdated?.() } else { toast.error('调整失败: ' + (res?.error || '')) } } catch { toast.error('调整失败') } finally { setAdjustLoading(false) } } // 用户资料完善查询(支持多维度) async function handleSSSQuery() { if (!sssQueryPhone && !sssQueryOpenId && !sssQueryWechatId) { setSssError('请至少输入手机号、微信号或 OpenID 中的一项') return } setSssLoading(true) setSssError(null) setSssData(null) try { const params = new URLSearchParams() if (sssQueryPhone) params.set('phone', sssQueryPhone) if (sssQueryOpenId) params.set('openId', sssQueryOpenId) if (sssQueryWechatId) params.set('wechatId', sssQueryWechatId) const data = await get<{ success?: boolean; data?: ShensheShouData; error?: string }>( `/api/admin/shensheshou/query?${params}`, ) if (data?.success && data.data) { setSssData(data.data) // 自动回填到用户信息 if (user) await handleSSSEnrich(data.data) } else setSssError(data?.error || '未查询到数据,该用户可能未在神射手收录') } catch (e) { console.error('SSS query error:', e) setSssError('请求失败,请检查神射手接口配置') } finally { setSssLoading(false) } } // 查询后自动回填用户基础信息 async function handleSSSEnrich(_sssResult?: ShensheShouData) { if (!user) return try { await post('/api/admin/shensheshou/enrich', { userId: user.id, phone: sssQueryPhone || user.phone || '', openId: sssQueryOpenId || user.openId || '', wechatId: sssQueryWechatId || user.wechatId || '', }) loadUserDetail() } catch (e) { console.error('SSS enrich error:', e) } } // 神射手 - 将当前用户信息推送/同步到神射手 async function handleSSSIngest() { if (!user) return setBatchIngestLoading(true) setBatchIngestResult(null) try { // 购买意向:看浏览/购买轨迹,把章节名备注给神射手(需求:便于 wepop/外部侧识别“想买哪章”) const purchaseIntentChapterTitles = Array.from( new Set( tracks .filter((t) => t.action === 'view_chapter' || t.action === 'purchase' || t.action === 'first_pay') .map((t) => (t.chapterTitle || t.target || '').trim()) .filter(Boolean), ), ).slice(0, 12) const purchaseIntentActions = { viewChapter: trackStats.view_chapter || 0, purchase: trackStats.purchase || 0, firstPay: trackStats.first_pay || 0, } const purchaseIntentRemark = purchaseIntentChapterTitles.length > 0 ? `意向章节:${purchaseIntentChapterTitles.join('、')}` : '' const payload = { users: [{ phone: user.phone || '', name: user.nickname || '', openId: user.openId || '', tags: editTags, purchaseIntent: purchaseIntentActions, purchaseIntentChapters: purchaseIntentChapterTitles, remark: purchaseIntentRemark, }] } const data = await post<{ success?: boolean; data?: Record; 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 icons: Record> = { view_chapter: BookOpen, purchase: ShoppingBag, match: Users, login: User, register: User, share: Link2, bind_phone: Phone, bind_wechat: MessageCircle, fill_profile: Tag, fill_avatar: User, visit_page: Navigation, first_pay: ShoppingBag, vip_activate: Crown, click_super: Users, lead_submit: Phone, withdraw: Key, referral_bind: Link2, card_click: User, btn_click: Zap, tab_click: Navigation, nav_click: Navigation, page_view: Navigation, search: Navigation, } const Icon = icons[action] || Clock return } function trackTargetIsOpaqueId(s: string) { const t = String(s || '').trim() return t.length > 22 && /^[a-zA-Z0-9_-]+$/.test(t) } const journeyInferredTags = useMemo( () => inferTagsFromJourney(trackStats, user), [trackStats, user], ) function mergeJourneyTagsIntoSelected() { const next = [...editTags] for (const t of journeyInferredTags) { if (!next.includes(t)) next.push(t) } setEditTags(next) toast.success('已将旅程推断标签合并到已选') } if (!open) return null return ( <> onClose()}> 用户详情 {user?.phone && 已绑定手机} {user?.isVip && VIP} {loading ? (
加载中...
) : user ? (
{/* 头部:身份 + 收益(紧凑) */}
{resolveAvatarByMbti(user.avatar, user.mbti) && !avatarBroken ? ( setAvatarBroken(true)} /> ) : ( user.nickname?.charAt(0) || '?' )}

{user.nickname}

{user.isAdmin && 管理员} {user.hasFullBook && 全书已购} {user.vipRole && {user.vipRole}}
{user.referralCode && (

推荐码 {user.referralCode}

)}
昵称

{editNickname || user.nickname || '—'}

手机号

{editPhone || '—'}

微信标识

{editWechatId || '—'}

画像

{[user.region, user.industry, user.position, user.mbti ? `MBTI ${user.mbti}` : ''].filter(Boolean).join(' · ') || '未完善'}

累计佣金

¥{(user.earnings ?? 0).toFixed(2)}

推广/分佣入账

待提现

¥{(user.pendingEarnings ?? 0).toFixed(2)}

未打款部分

账户余额

¥{(balanceData?.balance ?? 0).toFixed(2)}

可消费/抵扣

推荐人数

{user.referralCount ?? 0}

{user.createdAt ? `注册 ${new Date(user.createdAt).toLocaleDateString()}` : '—'}

用户信息 旅程与轨迹 关系链路 标签体系 {/* ===== 用户信息(紧凑单屏):基础字段 + 超级个体 + 外部同步合并为一屏滚动 ===== */}
技术标识 (用户ID / OpenID,默认折叠)

用户ID {user.id}

OpenID {user.openId || '—'}

OpenID 为微信用户标识;下方「微信标识」为微信号/wxid,供存客宝归属,与 OpenID 不同。

setEditNickname(e.target.value)} />
setEditPhone(e.target.value)} />
setEditWechatId(e.target.value)} />
{(user.region || user.industry || user.position || user.mbti) && (
{user.region && ( {user.region} )} {user.industry && 行业 {user.industry}} {user.position && 职位 {user.position}} {user.mbti && MBTI {user.mbti}}
)}
超级个体 {user.isVip && {user.vipRole || 'VIP'}}
setVipForm((f) => ({ ...f, isVip: c }))} />
{vipForm.isVip && (
setVipForm((f) => ({ ...f, vipExpireDate: e.target.value }))} />
setVipForm((f) => ({ ...f, vipName: e.target.value }))} />
setVipForm((f) => ({ ...f, vipProject: e.target.value }))} />
setVipForm((f) => ({ ...f, vipContact: e.target.value }))} />
setVipForm((f) => ({ ...f, vipBio: e.target.value }))} />
)}
外部资料 · 神射手 / 存客宝(与上方基础信息联动)
setSssQueryPhone(e.target.value)} /> setSssQueryWechatId(e.target.value)} /> setSssQueryOpenId(e.target.value)} />
{user.ckbSyncedAt &&

最近存客宝同步:{new Date(user.ckbSyncedAt).toLocaleString()}

} {sssError &&

{sssError}

} {sssData && (
RFM {sssData.rfm_score ?? '—'}
等级 {sssData.user_level ?? '—'}
)} {batchIngestResult && (

{batchIngestResult.error ? {String(batchIngestResult.error)} : 推送成功}

)}
修改密码
setNewPassword(e.target.value)} /> setConfirmPassword(e.target.value)} />
{/* ===== 用户旅程 + 行为轨迹 ===== */} {purchaseList.length > 0 && (
购买清单({purchaseList.length} 笔)
{purchaseList.map((o, i) => (
{o.productType === 'fullbook' || o.productType === 'vip' ? '全书/VIP' : `章节 ${o.productId || ''}`} ¥{Number(o.amount || 0).toFixed(2)}
{o.createdAt ? new Date(o.createdAt).toLocaleString('zh-CN') : ''}
))}
)}
全站埋点共 {tracks.length} 条;用于 RFM 与「标签体系」旅程推断
{Object.keys(trackStats).length > 0 && (
{Object.entries(trackStats) .sort((a, b) => b[1] - a[1]) .map(([act, n]) => ( {labelTrackAction(act)} ×{n} ))}
)}
{tracks.length > 0 ? ( tracks.map((track, idx) => (
{getActionIcon(track.action)}
{idx < tracks.length - 1 &&
}
{track.actionLabel || track.action} {track.moduleLabel && · {track.moduleLabel}} {track.chapterTitle && · {track.chapterTitle}}
{track.target && track.target !== track.chapterTitle && !trackTargetIsOpaqueId(track.target) && (

详情: {track.target}

)}

{track.timeAgo ? `${track.timeAgo} · ` : ''} {track.createdAt ? new Date(track.createdAt).toLocaleString() : ''}

)) ) : (
暂无旅程记录
)}
{/* ===== 关系链路 ===== */}
入站关系链路
点击 {inboundSource?.totalVisits || 0} 次

首次来自

{inboundSource?.firstVisit?.referrerNickname || '—'} {inboundSource?.firstVisit?.referrerId ? `(${inboundSource.firstVisit.referrerId})` : ''}

最近来自

{inboundSource?.latestVisit?.referrerNickname || '—'} {inboundSource?.latestVisit?.referrerId ? `(${inboundSource.latestVisit.referrerId})` : ''}

{inboundSource?.activeBinding?.referrerId ? (

当前绑定: {inboundSource.activeBinding.referrerNickname || '微信用户'} {`(${inboundSource.activeBinding.referrerId})`}

) : null}
{(inboundSource?.visits || []).length > 0 ? ( (inboundSource?.visits || []).map((v, i) => (

第 {v.seq || i + 1} 次 · {v.referrerNickname || '微信用户'} {v.referrerId ? `(${v.referrerId})` : ''}

{v.page ?

{v.page}

: null}
{v.visitedAt ? new Date(v.visitedAt).toLocaleString() : ''}
)) ) : (

暂无来源点击记录

)}
推荐的用户
共 {referrals.length} 人
{referrals.length > 0 ? ( referrals.map((ref: unknown, i: number) => { const r = ref as { id?: string; nickname?: string; status?: string; createdAt?: string } return (
{r.nickname?.charAt(0) || '?'}
{r.nickname}
{r.status === 'vip' && 已购} {r.createdAt ? new Date(r.createdAt).toLocaleDateString() : ''}
) }) ) : (

暂无推荐用户

)}
{/* ===== 标签体系 ===== */}
用户标签 《一场 Soul 的创业实验》维度
预设可点选;下方「旅程推断」由轨迹+资料自动算出,可一键并入已选后点弹窗底部保存。
旅程推断标签
{journeyInferredTags.length > 0 ? (
{journeyInferredTags.map((t) => ( {editTags.includes(t) ? '✓ ' : ''} {t} ))}
) : (

暂无推断(无轨迹或行为未命中规则)

)}
{[ { category: '身份类型', tags: ['创业者', '打工人', '自由职业', '学生', '投资人', '合伙人'], }, { category: '行业背景', tags: ['电商', '内容', '传统行业', '科技/AI', '金融', '教育', '餐饮'], }, { category: '痛点标签', tags: ['找资源', '找方向', '找合伙人', '想赚钱', '想学习', '找情感出口'], }, { category: '付费意愿', tags: ['高意向', '已付费', '观望中', '薅羊毛'], }, { category: 'MBTI', tags: ['ENTJ', 'INTJ', 'ENFP', 'INFP', 'ENTP', 'INTP', 'ESTJ', 'ISFJ'], }, ].map((group) => (

{group.category}

{group.tags.map((tag) => ( ))}
))}

已选标签(需保存修改写入库)

{editTags.map((tag, i) => ( {tag} ))} {editTags.length === 0 && 暂未选择}
setNewTag(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addTag()} />
{/* 存客宝标签(与用户标签共用 ckb_tags,兼容 JSON 与逗号分隔) */} {(() => { const raw = user.tags || user.ckbTags || '' let arr: string[] = [] try { const parsed = typeof raw === 'string' ? JSON.parse(raw || '[]') : [] arr = Array.isArray(parsed) ? parsed : (typeof raw === 'string' ? raw.split(',') : []) } catch { arr = typeof raw === 'string' ? raw.split(',') : [] } const tags = arr.map((t) => String(t).trim()).filter(Boolean) if (tags.length === 0) return null return (
存客宝标签
{tags.map((tag, i) => ( {tag} ))}
) })()}
) : (
用户不存在
)}
调整余额
setAdjustAmount(e.target.value)} />
setAdjustRemark(e.target.value)} />
) }