diff --git a/soul-admin/src/pages/users/UsersPage.tsx b/soul-admin/src/pages/users/UsersPage.tsx index 11ace343..68d8cf3c 100644 --- a/soul-admin/src/pages/users/UsersPage.tsx +++ b/soul-admin/src/pages/users/UsersPage.tsx @@ -36,9 +36,13 @@ import { Crown, Plus, BookOpen, - TrendingUp, Settings, PenLine, + Navigation, + TrendingUp, + ArrowUpDown, + ChevronDown, + ChevronUp, } from 'lucide-react' import { UserDetailModal } from '@/components/modules/user/UserDetailModal' import { SetVipModal } from '@/components/modules/user/SetVipModal' @@ -62,6 +66,9 @@ interface User { referralCount?: number createdAt: string updatedAt?: string | null + // RFM(排序模式时有值) + rfmScore?: number + rfmLevel?: string } interface UserRule { @@ -72,22 +79,27 @@ interface UserRule { sort: number enabled: boolean createdAt?: string - updatedAt?: string } -interface RFMUser { - id: string - nickname: string - phone?: string - avatar?: string - rfmScore: number - rfmLevel: string - recency: number - frequency: number - monetary: number - lastOrderAt?: 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([]) @@ -99,6 +111,10 @@ export function UsersPage() { 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) @@ -106,101 +122,110 @@ export function UsersPage() { 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 [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 [formData, setFormData] = useState({ phone: '', nickname: '', password: '', isAdmin: false, hasFullBook: false }) - // ===== 规则管理 state ===== + // ===== 规则管理 ===== 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 }) - // ===== RFM 排行榜 state ===== - const [rfmList, setRfmList] = useState([]) - const [rfmLoading, setRfmLoading] = useState(false) - const [rfmSearch, setRfmSearch] = useState('') - const debouncedRfmSearch = useDebounce(rfmSearch, 300) + // ===== 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 { - 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) + if (rfmSortMode) { + // RFM 排序模式:从 RFM 接口获取 + const params = new URLSearchParams({ search: debouncedSearch, limit: String(pageSize * 5) }) + const data = await get<{ success?: boolean; users?: User[] }>(`/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) + } } else { - setError(data?.error || '加载失败') + 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('网络错误,请检查连接') + setError('网络错误') } finally { setIsLoading(false) } } - useEffect(() => { - setPage(1) - }, [debouncedSearch, vipFilter]) - - useEffect(() => { - loadUsers() - }, [page, pageSize, debouncedSearch, vipFilter]) + 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)}`, - ) + const data = await del<{ success?: boolean; error?: string }>(`/api/db/users?id=${encodeURIComponent(userId)}`) if (data?.success) loadUsers() - else alert('删除失败: ' + (data?.error || '未知错误')) - } catch (err) { - console.error('Delete user error:', err) - alert('删除失败') - } + 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), - }) + setFormData({ phone: user.phone || '', nickname: user.nickname || '', password: '', isAdmin: !!(user.isAdmin ?? false), hasFullBook: !!(user.hasFullBook ?? false) }) setShowUserModal(true) } @@ -211,93 +236,42 @@ export function UsersPage() { } async function handleSaveUser() { - if (!formData.phone || !formData.nickname) { - alert('请填写手机号和昵称') - return - } + 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 } + 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 } + 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 (err) { - console.error('Save user error:', err) - alert('保存失败') - } finally { - setIsSaving(false) - } + } catch { alert('保存失败') } finally { setIsSaving(false) } } - const handleChangePassword = (user: User) => { - setEditingUser(user) - setNewPassword('') - setConfirmPassword('') - setShowPasswordModal(true) - } + const handleChangePassword = (user: User) => { setEditingUser(user); setNewPassword(''); setConfirmPassword(''); setShowPasswordModal(true) } async function handleViewReferrals(user: User) { - setSelectedUserForReferrals(user) - setShowReferralsModal(true) - setReferralsLoading(true) + 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)}`, - ) + 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 (err) { - console.error('Load referrals error:', err) - setReferralsData({ referrals: [], stats: {} }) - } finally { - setReferralsLoading(false) - } - } - - const handleViewDetail = (user: User) => { - setSelectedUserIdForDetail(user.id) - setShowDetailModal(true) - } - - const handleSetVip = (user: User) => { - setSelectedUserForVip(user) - setShowSetVipModal(true) + } 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 } + 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 (err) { - console.error('Change password error:', err) - alert('密码修改失败') - } finally { - setIsSaving(false) - } + 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) } } // ===== 规则管理 ===== @@ -306,159 +280,125 @@ export function UsersPage() { try { const data = await get<{ success?: boolean; rules?: UserRule[] }>('/api/db/user-rules') if (data?.success) setRules(data.rules || []) - } catch (err) { - console.error('Load rules error:', err) - } finally { - setRulesLoading(false) - } + } catch { } finally { setRulesLoading(false) } }, []) - const handleAddRule = () => { - setEditingRule(null) - setRuleForm({ title: '', description: '', trigger: '', sort: 0, enabled: true }) - setShowRuleModal(true) - } - - const handleEditRule = (rule: UserRule) => { - setEditingRule(rule) - setRuleForm({ - title: rule.title, - description: rule.description, - trigger: rule.trigger, - sort: rule.sort, - enabled: rule.enabled, - }) - setShowRuleModal(true) - } - 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, - }) + 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 (err) { - console.error('Save rule error:', err) - alert('保存失败') - } finally { - setIsSaving(false) - } + setShowRuleModal(false); loadRules() + } catch { alert('保存失败') } finally { setIsSaving(false) } } - async function handleDeleteRule(ruleId: number) { - if (!confirm('确定删除该规则?')) return + async function handleDeleteRule(id: number) { + if (!confirm('确定删除?')) return try { - const data = await del<{ success?: boolean; error?: string }>(`/api/db/user-rules?id=${ruleId}`) + const data = await del<{ success?: boolean }>(`/api/db/user-rules?id=${id}`) if (data?.success) loadRules() - else alert('删除失败: ' + (data?.error || '')) - } catch (err) { - console.error('Delete rule error:', err) - } + } catch { } } async function handleToggleRule(rule: UserRule) { - try { - await put('/api/db/user-rules', { id: rule.id, enabled: !rule.enabled }) - loadRules() - } catch (err) { - console.error('Toggle rule error:', err) - } + try { await put('/api/db/user-rules', { id: rule.id, enabled: !rule.enabled }); loadRules() } catch { } } - // ===== RFM 排行榜 ===== - const loadRFM = useCallback(async () => { - setRfmLoading(true) + // ===== VIP 角色 ===== + const loadVipRoles = useCallback(async () => { + setVipRolesLoading(true) try { - const params = new URLSearchParams({ search: debouncedRfmSearch, limit: '50' }) - const data = await get<{ success?: boolean; users?: RFMUser[] }>(`/api/db/users/rfm?${params}`) - if (data?.success) setRfmList(data.users || []) - } catch (err) { - console.error('Load RFM error:', err) - } finally { - setRfmLoading(false) - } - }, [debouncedRfmSearch]) + const data = await get<{ success?: boolean; roles?: VipRole[] }>('/api/db/vip-roles') + if (data?.success) setVipRoles(data.roles || []) + } catch { } finally { setVipRolesLoading(false) } + }, []) - 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 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} 位注册用户

+

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

- - - - 用户列表 + + + 用户列表 - - - RFM 估值排行 + + 用户旅程总览 - - - 规则配置 + + 规则配置 + + + VIP 角色 - {/* ===== 用户列表 Tab ===== */} + {/* ===== 用户列表 ===== */} -
+
setSearchTerm(e.target.value)} />
@@ -495,6 +434,22 @@ export function UsersPage() { 绑定信息 购买状态 分销收益 + +
+ + RFM分值 + {rfmSortMode ? ( + rfmSortDir === 'desc' + ? + : + ) : ( + + )} +
+ {rfmSortMode && ( +
点击切换方向/关闭
+ )} +
注册时间 操作 @@ -507,23 +462,13 @@ export function UsersPage() {
{user.avatar ? ( - ) : ( - user.nickname?.charAt(0) || '?' - )} + ) : user.nickname?.charAt(0) || '?'}
-
+

{user.nickname}

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

{user.openId ? user.openId.slice(0, 12) + '...' : user.id?.slice(0, 12)} @@ -533,365 +478,215 @@ export function UsersPage() {

- {user.phone && ( -
- 📱 - {user.phone} -
- )} - {user.wechatId && ( -
- 💬 - {user.wechatId} -
- )} - {user.openId && ( -
- 🔗 - - {user.openId.slice(0, 12)}... - -
- )} - {!user.phone && !user.wechatId && !user.openId && ( - 未绑定 - )} + {user.phone &&
📱{user.phone}
} + {user.wechatId &&
💬{user.wechatId}
} + {user.openId &&
🔗{user.openId.slice(0, 12)}...
} + {!user.phone && !user.wechatId && !user.openId && 未绑定}
{user.hasFullBook ? ( - - VIP - + VIP ) : ( - - 未购买 - + 未购买 )}
-
- ¥{parseFloat(String(user.earnings || 0)).toFixed(2)} -
+
¥{parseFloat(String(user.earnings || 0)).toFixed(2)}
{parseFloat(String(user.pendingEarnings || 0)) > 0 && ( -
- 待提现: ¥{parseFloat(String(user.pendingEarnings || 0)).toFixed(2)} -
+
待提现: ¥{parseFloat(String(user.pendingEarnings || 0)).toFixed(2)}
)} -
handleViewReferrals(user)} - onKeyDown={(e) => e.key === 'Enter' && handleViewReferrals(user)} - role="button" - tabIndex={0} - > - - 绑定{user.referralCount || 0}人 +
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) }} - /> + { setPageSize(n); setPage(1) }} />
)} - {/* ===== RFM 排行榜 Tab ===== */} - -
-
- - setRfmSearch(e.target.value)} - /> -
- -
- S 超级VIP - A 高价值 - B 潜力用户 - C 普通 - D 低活跃 -
- {/* RFM 说明卡片 */} -
-
-
-
R
- 最近购买(Recency) -
-

距离最近一次购买的天数,越近分值越高

-
-
-
-
F
- 购买频率(Frequency) -
-

累计购买次数,频次越高分值越高

-
-
-
-
M
- 消费金额(Monetary) -
-

累计消费总额,金额越高分值越高

-
-
- - - - {rfmLoading ? ( -
- - 计算 RFM 中... + {/* 流程图 */} +
+ {/* 连接线 */} +
+
+ {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}

- ) : rfmList.length === 0 ? ( -
- -

点击「刷新」加载 RFM 排行榜

+ ))} +
+
+ + {/* 旅程说明 */} +
+
+
+ + 旅程关键节点 +
+
+ {[ + { 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} +
+ ) + })}
) : ( - - - - 排名 - 用户 - 等级 - RFM总分 - R 最近购买 - F 购买频次 - M 消费金额 - 最近订单 - - - - {rfmList.map((u, i) => ( - - - - #{i + 1} - - - -
-
- {u.avatar ? ( - - ) : ( - u.nickname?.charAt(0) || '?' - )} -
-
-

{u.nickname}

- {u.phone &&

{u.phone}

} -
-
-
- - - {u.rfmLevel} 级 - - - - {u.rfmScore} - - - {u.recency} - - - {u.frequency} - - - ¥{u.monetary.toFixed(0)} - - - {u.lastOrderAt ? new Date(u.lastOrderAt).toLocaleDateString() : '-'} - -
- ))} -
-
+
+

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

+
)} - - +
+
- {/* ===== 规则配置 Tab ===== */} + {/* ===== 规则配置 ===== */}
-
-

- 配置用户旅程引导规则,包括填写头像、完善信息、付款节点等步骤触发规则,帮助用户了解完整路径。 -

-
+

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

- -
{rulesLoading ? ( -
- - 加载中... -
+
) : rules.length === 0 ? (
-

暂无规则配置

-

添加用户旅程规则,帮助引导用户完善个人信息

- +

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

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

{rule.description}

- )} + {rule.description &&

{rule.description}

}
- handleToggleRule(rule)} - /> - - + handleToggleRule(rule)} /> + +
@@ -899,286 +694,159 @@ export function UsersPage() {
)} + + {/* ===== VIP 角色 ===== */} + +
+

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

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

暂无 VIP 角色

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

排序: {role.sort}

+
+ ))} +
+ )} +
- {/* ===== 添加/编辑用户弹框 ===== */} + {/* ===== 弹框组件 ===== */} + + {/* 添加/编辑用户 */} - - - {editingUser ? : } - {editingUser ? '编辑用户' : '添加用户'} - - + {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: checked })} - /> -
-
- - setFormData({ ...formData, hasFullBook: checked })} - /> -
+
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)} - /> -
+

用户:{editingUser?.nickname}

手机号:{editingUser?.phone}

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