import { useState, useEffect, useCallback } from 'react' import { Card, CardContent } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Badge } from '@/components/ui/badge' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' import { Search, UserPlus, Trash2, Edit3, Key, Save, X, RefreshCw, Users, Eye, Crown, Plus, BookOpen, Settings, PenLine, Navigation, TrendingUp, ArrowUpDown, ChevronDown, ChevronUp, } from 'lucide-react' import { UserDetailModal } from '@/components/modules/user/UserDetailModal' import { SetVipModal } from '@/components/modules/user/SetVipModal' import { Pagination } from '@/components/ui/Pagination' import { useDebounce } from '@/hooks/useDebounce' import { get, del, post, put } from '@/api/client' interface User { id: string openId?: string | null phone?: string | null nickname: string wechatId?: string | null avatar?: string | null isAdmin?: boolean | number hasFullBook?: boolean | number referralCode?: string earnings: number | string pendingEarnings?: number | string withdrawnEarnings?: number | string referralCount?: number createdAt: string updatedAt?: string | null // RFM(排序模式时有值) rfmScore?: number rfmLevel?: string } interface UserRule { id: number title: string description: string trigger: string sort: number enabled: boolean createdAt?: string } interface VipRole { id: number name: string sort: number createdAt?: string } // 用户旅程阶段定义 const JOURNEY_STAGES = [ { id: 'register', label: '注册/登录', icon: '👤', color: 'bg-blue-500/20 border-blue-500/40 text-blue-400', desc: '微信授权登录或手机号注册' }, { id: 'browse', label: '浏览章节', icon: '📖', color: 'bg-purple-500/20 border-purple-500/40 text-purple-400', desc: '点击免费/付费章节预览' }, { id: 'bind_phone', label: '绑定手机', icon: '📱', color: 'bg-cyan-500/20 border-cyan-500/40 text-cyan-400', desc: '触发付费章节后绑定手机' }, { id: 'first_pay', label: '首次付款', icon: '💳', color: 'bg-green-500/20 border-green-500/40 text-green-400', desc: '购买单章或全书' }, { id: 'fill_profile', label: '完善资料', icon: '✍️', color: 'bg-yellow-500/20 border-yellow-500/40 text-yellow-400', desc: '填写头像、MBTI、行业等' }, { id: 'match', label: '派对房匹配', icon: '🤝', color: 'bg-orange-500/20 border-orange-500/40 text-orange-400', desc: '参与 Soul 派对房' }, { id: 'vip', label: '升级 VIP', icon: '👑', color: 'bg-amber-500/20 border-amber-500/40 text-amber-400', desc: '付款 ¥1980 购买全书' }, { id: 'distribution', label: '开启分销', icon: '🔗', color: 'bg-[#38bdac]/20 border-[#38bdac]/40 text-[#38bdac]', desc: '生成推广码并推荐好友' }, ] export function UsersPage() { // ===== 用户列表 state ===== const [users, setUsers] = useState([]) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) const [searchTerm, setSearchTerm] = useState('') const debouncedSearch = useDebounce(searchTerm, 300) const [vipFilter, setVipFilter] = useState<'all' | 'vip'>('all') const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [rfmSortMode, setRfmSortMode] = useState(false) // RFM 排序模式 const [rfmSortDir, setRfmSortDir] = useState<'desc' | 'asc'>('desc') // 弹框 const [showUserModal, setShowUserModal] = useState(false) const [showPasswordModal, setShowPasswordModal] = useState(false) const [editingUser, setEditingUser] = useState(null) const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [isSaving, setIsSaving] = useState(false) const [showReferralsModal, setShowReferralsModal] = useState(false) const [referralsData, setReferralsData] = useState<{ referrals?: unknown[]; stats?: Record }>({ referrals: [], stats: {} }) const [referralsLoading, setReferralsLoading] = useState(false) const [selectedUserForReferrals, setSelectedUserForReferrals] = useState(null) const [showDetailModal, setShowDetailModal] = useState(false) const [selectedUserIdForDetail, setSelectedUserIdForDetail] = useState(null) const [showSetVipModal, setShowSetVipModal] = useState(false) const [selectedUserForVip, setSelectedUserForVip] = useState(null) const [formData, setFormData] = useState({ phone: '', nickname: '', password: '', isAdmin: false, hasFullBook: false }) // ===== 规则管理 ===== const [rules, setRules] = useState([]) const [rulesLoading, setRulesLoading] = useState(false) const [showRuleModal, setShowRuleModal] = useState(false) const [editingRule, setEditingRule] = useState(null) const [ruleForm, setRuleForm] = useState({ title: '', description: '', trigger: '', sort: 0, enabled: true }) // ===== VIP 角色 ===== const [vipRoles, setVipRoles] = useState([]) const [vipRolesLoading, setVipRolesLoading] = useState(false) const [showVipRoleModal, setShowVipRoleModal] = useState(false) const [editingVipRole, setEditingVipRole] = useState(null) const [vipRoleForm, setVipRoleForm] = useState({ name: '', sort: 0 }) // ===== 用户旅程总览 ===== const [journeyStats, setJourneyStats] = useState>({}) const [journeyLoading, setJourneyLoading] = useState(false) // ===== 用户列表 ===== async function loadUsers() { setIsLoading(true) setError(null) try { if (rfmSortMode) { // RFM 排序模式:从 RFM 接口获取 const params = new URLSearchParams({ search: debouncedSearch, limit: String(pageSize * 5) }) const data = await get<{ success?: boolean; users?: User[]; error?: string }>(`/api/db/users/rfm?${params}`) if (data?.success) { let list = data.users || [] if (rfmSortDir === 'asc') list = [...list].reverse() const start = (page - 1) * pageSize setUsers(list.slice(start, start + pageSize)) setTotal(data.users?.length ?? 0) if (list.length === 0) { // 暂无订单数据,退回普通模式 setRfmSortMode(false) setError('暂无订单数据,RFM 排序需要用户有购买记录后才能生效') } } else { setRfmSortMode(false) setError(data?.error || 'RFM 加载失败,已切回普通模式') } } else { const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize), search: debouncedSearch, ...(vipFilter === 'vip' && { vip: 'true' }), }) const data = await get<{ success?: boolean; users?: User[]; total?: number; error?: string }>(`/api/db/users?${params}`) if (data?.success) { setUsers(data.users || []) setTotal(data.total ?? 0) } else { setError(data?.error || '加载失败') } } } catch (err) { console.error('Load users error:', err) setError('网络错误') } finally { setIsLoading(false) } } useEffect(() => { setPage(1) }, [debouncedSearch, vipFilter, rfmSortMode]) useEffect(() => { loadUsers() }, [page, pageSize, debouncedSearch, vipFilter, rfmSortMode, rfmSortDir]) const totalPages = Math.ceil(total / pageSize) || 1 const toggleRfmSort = () => { if (rfmSortMode) { if (rfmSortDir === 'desc') setRfmSortDir('asc') else { setRfmSortMode(false); setRfmSortDir('desc') } } else { setRfmSortMode(true) setRfmSortDir('desc') } } const getRFMLevelColor = (level?: string) => { const map: Record = { 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' } async function handleDelete(userId: string) { if (!confirm('确定要删除这个用户吗?')) return try { const data = await del<{ success?: boolean; error?: string }>(`/api/db/users?id=${encodeURIComponent(userId)}`) if (data?.success) loadUsers() else alert('删除失败: ' + (data?.error || '')) } catch { alert('删除失败') } } const handleEditUser = (user: User) => { setEditingUser(user) setFormData({ phone: user.phone || '', nickname: user.nickname || '', password: '', isAdmin: !!(user.isAdmin ?? false), hasFullBook: !!(user.hasFullBook ?? false) }) setShowUserModal(true) } const handleAddUser = () => { setEditingUser(null) setFormData({ phone: '', nickname: '', password: '', isAdmin: false, hasFullBook: false }) setShowUserModal(true) } async function handleSaveUser() { if (!formData.phone || !formData.nickname) { alert('请填写手机号和昵称'); return } setIsSaving(true) try { if (editingUser) { const data = await put<{ success?: boolean; error?: string }>('/api/db/users', { id: editingUser.id, nickname: formData.nickname, isAdmin: formData.isAdmin, hasFullBook: formData.hasFullBook, ...(formData.password && { password: formData.password }) }) if (!data?.success) { alert('更新失败: ' + (data?.error || '')); return } } else { const data = await post<{ success?: boolean; error?: string }>('/api/db/users', { phone: formData.phone, nickname: formData.nickname, password: formData.password, isAdmin: formData.isAdmin }) if (!data?.success) { alert('创建失败: ' + (data?.error || '')); return } } setShowUserModal(false) loadUsers() } catch { alert('保存失败') } finally { setIsSaving(false) } } const handleChangePassword = (user: User) => { setEditingUser(user); setNewPassword(''); setConfirmPassword(''); setShowPasswordModal(true) } async function handleViewReferrals(user: User) { setSelectedUserForReferrals(user); setShowReferralsModal(true); setReferralsLoading(true) try { const data = await get<{ success?: boolean; referrals?: unknown[]; stats?: Record }>(`/api/db/users/referrals?userId=${encodeURIComponent(user.id)}`) if (data?.success) setReferralsData({ referrals: data.referrals || [], stats: data.stats || {} }) else setReferralsData({ referrals: [], stats: {} }) } catch { setReferralsData({ referrals: [], stats: {} }) } finally { setReferralsLoading(false) } } async function handleSavePassword() { if (!newPassword) { alert('请输入新密码'); return } if (newPassword !== confirmPassword) { alert('两次密码不一致'); return } if (newPassword.length < 6) { alert('密码至少6位'); return } setIsSaving(true) try { const data = await put<{ success?: boolean; error?: string }>('/api/db/users', { id: editingUser?.id, password: newPassword }) if (data?.success) { alert('修改成功'); setShowPasswordModal(false) } else alert('修改失败: ' + (data?.error || '')) } catch { alert('修改失败') } finally { setIsSaving(false) } } // ===== 规则管理 ===== const loadRules = useCallback(async () => { setRulesLoading(true) try { const data = await get<{ success?: boolean; rules?: UserRule[] }>('/api/db/user-rules') if (data?.success) setRules(data.rules || []) } catch { } finally { setRulesLoading(false) } }, []) async function handleSaveRule() { if (!ruleForm.title) { alert('请填写规则标题'); return } setIsSaving(true) try { if (editingRule) { const data = await put<{ success?: boolean; error?: string }>('/api/db/user-rules', { id: editingRule.id, ...ruleForm }) if (!data?.success) { alert('更新失败: ' + (data?.error || '')); return } } else { const data = await post<{ success?: boolean; error?: string }>('/api/db/user-rules', ruleForm) if (!data?.success) { alert('创建失败: ' + (data?.error || '')); return } } setShowRuleModal(false); loadRules() } catch { alert('保存失败') } finally { setIsSaving(false) } } async function handleDeleteRule(id: number) { if (!confirm('确定删除?')) return try { const data = await del<{ success?: boolean }>(`/api/db/user-rules?id=${id}`) if (data?.success) loadRules() } catch { } } async function handleToggleRule(rule: UserRule) { try { await put('/api/db/user-rules', { id: rule.id, enabled: !rule.enabled }); loadRules() } catch { } } // ===== VIP 角色 ===== const loadVipRoles = useCallback(async () => { setVipRolesLoading(true) try { const data = await get<{ success?: boolean; roles?: VipRole[] }>('/api/db/vip-roles') if (data?.success) setVipRoles(data.roles || []) } catch { } finally { setVipRolesLoading(false) } }, []) async function handleSaveVipRole() { if (!vipRoleForm.name) { alert('请填写角色名称'); return } setIsSaving(true) try { if (editingVipRole) { const data = await put<{ success?: boolean; error?: string }>('/api/db/vip-roles', { id: editingVipRole.id, ...vipRoleForm }) if (!data?.success) { alert('更新失败'); return } } else { const data = await post<{ success?: boolean; error?: string }>('/api/db/vip-roles', vipRoleForm) if (!data?.success) { alert('创建失败'); return } } setShowVipRoleModal(false); loadVipRoles() } catch { alert('保存失败') } finally { setIsSaving(false) } } async function handleDeleteVipRole(id: number) { if (!confirm('确定删除?')) return try { const data = await del<{ success?: boolean }>(`/api/db/vip-roles?id=${id}`) if (data?.success) loadVipRoles() } catch { } } // ===== 用户旅程总览 ===== const loadJourneyStats = useCallback(async () => { setJourneyLoading(true) try { const data = await get<{ success?: boolean; stats?: Record }>('/api/db/users/journey-stats') if (data?.success && data.stats) setJourneyStats(data.stats) } catch { } finally { setJourneyLoading(false) } }, []) return (
{error && (
{error}
)}

用户管理

共 {total} 位注册用户{rfmSortMode && ' · RFM 排序中'}

用户列表 用户旅程总览 规则配置 VIP 角色 {/* ===== 用户列表 ===== */}
setSearchTerm(e.target.value)} />
{isLoading ? (
加载中...
) : (
用户信息 绑定信息 购买状态 分销收益
RFM分值 {rfmSortMode ? ( rfmSortDir === 'desc' ? : ) : ( )}
{rfmSortMode && (
点击切换方向/关闭
)}
注册时间 操作
{users.map((user) => (
{user.avatar ? ( ) : user.nickname?.charAt(0) || '?'}

{user.nickname}

{user.isAdmin && 管理员} {user.openId && !user.id?.startsWith('user_') && 微信}

{user.openId ? user.openId.slice(0, 12) + '...' : user.id?.slice(0, 12)}

{user.phone &&
📱{user.phone}
} {user.wechatId &&
💬{user.wechatId}
} {user.openId &&
🔗{user.openId.slice(0, 12)}...
} {!user.phone && !user.wechatId && !user.openId && 未绑定}
{user.hasFullBook ? ( VIP ) : ( 未购买 )}
¥{parseFloat(String(user.earnings || 0)).toFixed(2)}
{parseFloat(String(user.pendingEarnings || 0)) > 0 && (
待提现: ¥{parseFloat(String(user.pendingEarnings || 0)).toFixed(2)}
)}
handleViewReferrals(user)} role="button" tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && handleViewReferrals(user)}> 绑定{user.referralCount || 0}人
{/* RFM 分值列 */} {user.rfmScore !== undefined ? (
{user.rfmScore} {user.rfmLevel}
) : ( 点列头排序 )}
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
))} {users.length === 0 && ( 暂无用户数据 )}
{ setPageSize(n); setPage(1) }} />
)}
{/* ===== 用户旅程总览 ===== */}

用户从注册到 VIP 的完整行动路径,点击各阶段查看用户动态

{/* 流程图 */}
{/* 连接线 */}
{JOURNEY_STAGES.map((stage, idx) => (
{/* 阶段卡片 */}
{stage.icon}
c.startsWith('text-'))}`}>{stage.label}
{journeyStats[stage.id] !== undefined && (
{journeyStats[stage.id]}
)} {/* 序号 */}
{idx + 1}
{/* 箭头(非最后一个)*/} {idx < JOURNEY_STAGES.length - 1 && (
)} {/* 描述 */}

{stage.desc}

))}
{/* 旅程说明 */}
旅程关键节点
{[ { step: '① 注册', action: '微信 OAuth 或手机号注册', next: '引导填写头像' }, { step: '② 浏览', action: '点击章节/阅读免费内容', next: '触发绑定手机' }, { step: '③ 首付', action: '购买单章或全书', next: '推送分销功能' }, { step: '④ VIP', action: '¥1980 购买全书', next: '进入 VIP 私域群' }, { step: '⑤ 分销', action: '推广好友购买', next: '提现分销收益' }, ].map((item) => (
{item.step}

{item.action}

→ {item.next}

))}
行为锚点统计 实时更新
{journeyLoading ? (
) : Object.keys(journeyStats).length > 0 ? (
{JOURNEY_STAGES.map((stage) => { const count = journeyStats[stage.id] || 0 const maxCount = Math.max(...JOURNEY_STAGES.map(s => journeyStats[s.id] || 0), 1) const pct = Math.round((count / maxCount) * 100) return (
{stage.icon} {stage.label}
{count}
) })}
) : (

点击「刷新数据」加载统计

)}
{/* ===== 规则配置 ===== */}

用户旅程引导规则,定义各行为节点的触发条件与引导内容

{rulesLoading ? (
) : rules.length === 0 ? (

暂无规则(重启服务将自动写入10条默认规则)

) : (
{rules.map((rule) => (
{rule.title} {rule.trigger && 触发:{rule.trigger}} {rule.enabled ? '启用' : '禁用'}
{rule.description &&

{rule.description}

}
handleToggleRule(rule)} />
))}
)}
{/* ===== VIP 角色 ===== */}

管理用户 VIP 角色分类,这些角色将在用户详情和会员展示中使用

{vipRolesLoading ? (
) : vipRoles.length === 0 ? (

暂无 VIP 角色

) : (
{vipRoles.map((role) => (
{role.name}

排序: {role.sort}

))}
)}
{/* ===== 弹框组件 ===== */} {/* 添加/编辑用户 */} {editingUser ? : }{editingUser ? '编辑用户' : '添加用户'}
setFormData({ ...formData, phone: e.target.value })} disabled={!!editingUser} />
setFormData({ ...formData, nickname: e.target.value })} />
setFormData({ ...formData, password: e.target.value })} />
setFormData({ ...formData, isAdmin: c })} />
setFormData({ ...formData, hasFullBook: c })} />
{/* 修改密码 */} 修改密码

用户:{editingUser?.nickname}

手机号:{editingUser?.phone}

setNewPassword(e.target.value)} />
setConfirmPassword(e.target.value)} />
{/* 添加/编辑规则 */} {editingRule ? '编辑规则' : '添加规则'}
setRuleForm({ ...ruleForm, title: e.target.value })} />