diff --git a/frontend/src/components/layout/AdminLayout.tsx b/frontend/src/components/layout/AdminLayout.tsx index 7bd721d..aa6c349 100644 --- a/frontend/src/components/layout/AdminLayout.tsx +++ b/frontend/src/components/layout/AdminLayout.tsx @@ -160,7 +160,7 @@ const AdminLayout: React.FC = () => { }; const handleVisitFrontend = () => { - const url = window.location.origin + window.location.pathname + '#/portal'; + const url = window.location.origin + window.location.pathname + '#/chat'; window.open(url, '_blank'); }; diff --git a/frontend/src/pages/admin/Achievement.tsx b/frontend/src/pages/admin/Achievement.tsx index 4bcda45..4d8751a 100644 --- a/frontend/src/pages/admin/Achievement.tsx +++ b/frontend/src/pages/admin/Achievement.tsx @@ -1,644 +1,1288 @@ // @ts-nocheck + /** + * Phase 4 剩余 - 成就系统管理页面 + * 成就管理、用户进度、排行榜、奖励领取 + */ + import React, { useState, useEffect, useCallback } from 'react'; + import { adminAPI } from '@/api'; + + // ==================== 类型定义 ==================== + interface Achievement { + id: string; + name: string; + description: string; + icon: string; + category: string; + criteria_type: string; + criteria_key: string | null; + criteria_value: number; + reward_type: string; + reward_value: number; + badge_icon: string | null; + is_active: boolean; + display_order: number; + user_progress?: number; + is_completed?: boolean; + reward_claimed?: boolean; + completed_at?: string | null; + } + + interface AchievementStats { + total_achievements: number; + active_achievements: number; + completed_achievements: number; + rewards_claimed: number; + total_points: number; + by_category: Record; + } + + const CATEGORY_LABELS: Record = { + usage: '使用量', + social: '社交', + content: '内容创作', + payment: '付费', + special: '特殊', + }; + + const CATEGORY_COLORS: Record = { + usage: '#6C63FF', + social: '#00D4AA', + content: '#FFB800', + payment: '#FF6B6B', + special: '#C084FC', + }; + + const REWARD_LABELS: Record = { + points: '积分', + badge: '徽章', + discount: '折扣', + feature: '功能解锁', + credits: '额度', + }; + + const CRITERIA_LABELS: Record = { + message_count: '消息数量', + conversation_count: '对话数量', + token_usage: 'Token用量', + referral_count: '推荐人数', + kb_doc_count: '知识库文档数', + agent_count: 'Agent数量', + skill_count: '技能数量', + payment_amount: '付费金额', + days_active: '活跃天数', + workflow_count: '工作流数量', + }; + + // ==================== 主组件 ==================== + const Achievement: React.FC = () => { + const [activeTab, setActiveTab] = useState('overview'); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const [stats, setStats] = useState(null); + const [achievements, setAchievements] = useState([]); + const [userProgress, setUserProgress] = useState([]); + const [leaderboard, setLeaderboard] = useState([]); + const [categoryFilter, setCategoryFilter] = useState(''); + + // Modal states + const [showCreateModal, setShowCreateModal] = useState(false); + const [editingAch, setEditingAch] = useState(null); + const [formData, setFormData] = useState({ + name: '', + description: '', + category: 'usage', + criteria_type: 'message_count', + criteria_value: 100, + reward_type: 'points', + reward_value: 100, + icon: '', + }); + + const loadStats = useCallback(async () => { + try { + const allRes = await adminAPI.getAchievements(''); + const items = allRes.data?.items || []; + const active = items.filter((a: Achievement) => a.is_active); + const completed = items.filter((a: Achievement) => a.is_completed); + const claimed = items.filter((a: Achievement) => a.reward_claimed); + const totalPoints = items + .filter((a: Achievement) => a.is_completed) + .reduce((sum: number, a: Achievement) => sum + (a.reward_value || 0), 0); + const byCategory: Record = {}; + items.forEach((a: Achievement) => { + byCategory[a.category] = (byCategory[a.category] || 0) + 1; + }); + setStats({ + total_achievements: items.length, + active_achievements: active.length, + completed_achievements: completed.length, + rewards_claimed: claimed.length, + total_points: totalPoints, + by_category: byCategory, + }); + } catch (e: any) { + console.error('Failed to load stats:', e); + } + }, []); + + const loadAchievements = useCallback(async () => { + setLoading(true); + setError(''); + try { + const params = categoryFilter ? `?category=${categoryFilter}` : ''; + const res = await adminAPI.getAchievements(params); + setAchievements(res.data?.items || []); + } catch (e: any) { + setError(e.message || '加载失败'); + } finally { + setLoading(false); + } + }, [categoryFilter]); + + const loadUserProgress = useCallback(async () => { + setLoading(true); + try { + const res = await adminAPI.getUserAchievements(); + setUserProgress(res.data?.items || []); + } catch (e: any) { + setError(e.message || '加载失败'); + } finally { + setLoading(false); + } + }, []); + + const loadLeaderboard = useCallback(async () => { + setLoading(true); + try { + const res = await adminAPI.getAchievementLeaderboard('?period=all'); + setLeaderboard(res.data?.items || []); + } catch (e: any) { + setError(e.message || '加载失败'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadStats(); + }, [loadStats]); + + useEffect(() => { + if (activeTab === 'list') loadAchievements(); + else if (activeTab === 'progress') loadUserProgress(); + else if (activeTab === 'leaderboard') loadLeaderboard(); + }, [activeTab, loadAchievements, loadUserProgress, loadLeaderboard]); + + const handleSave = async () => { + if (!formData.name.trim()) { + setError('成就名称不能为空'); + return; + } + setLoading(true); + try { + const params = `?name=${encodeURIComponent(formData.name)}&category=${formData.category}&criteria_type=${formData.criteria_type}&criteria_value=${formData.criteria_value}&description=${encodeURIComponent(formData.description)}&reward_type=${formData.reward_type}&reward_value=${formData.reward_value}&icon=${encodeURIComponent(formData.icon)}`; + if (editingAch) { + const updateParams = `?name=${encodeURIComponent(formData.name)}&description=${encodeURIComponent(formData.description)}&reward_value=${formData.reward_value}`; + await adminAPI.updateAchievement(editingAch.id, updateParams); + } else { + await adminAPI.createAchievement(params); + } + setShowCreateModal(false); + setEditingAch(null); + setFormData({ + name: '', description: '', category: 'usage', criteria_type: 'message_count', + criteria_value: 100, reward_type: 'points', reward_value: 100, icon: '', + }); + loadAchievements(); + loadStats(); + } catch (e: any) { + setError(e.message || '保存失败'); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm('确定要删除这个成就吗?')) return; + try { + await adminAPI.deleteAchievement(id); + loadAchievements(); + loadStats(); + } catch (e: any) { + setError(e.message || '删除失败'); + } + }; + + const handleToggle = async (ach: Achievement) => { + try { + const params = `?is_active=${!ach.is_active}`; + await adminAPI.updateAchievement(ach.id, params); + loadAchievements(); + loadStats(); + } catch (e: any) { + setError(e.message || '操作失败'); + } + }; + + const handleClaim = async (id: string) => { + try { + await adminAPI.claimAchievementReward(id); + loadUserProgress(); + loadStats(); + } catch (e: any) { + setError(e.message || '领取失败'); + } + }; + + const handleCheckProgress = async () => { + try { + await adminAPI.checkAchievementProgress(); + loadUserProgress(); + loadStats(); + } catch (e: any) { + setError(e.message || '检查失败'); + } + }; + + const openEdit = (ach: Achievement) => { + setEditingAch(ach); + setFormData({ + name: ach.name, + description: ach.description || '', + category: ach.category, + criteria_type: ach.criteria_type, + criteria_value: ach.criteria_value, + reward_type: ach.reward_type, + reward_value: ach.reward_value, + icon: ach.icon || '', + }); + setShowCreateModal(true); + }; + + const getProgressPercent = (current: number, target: number) => { + if (target <= 0) return 0; + return Math.min(100, Math.round((current / target) * 100)); + }; + + // ==================== 渲染 ==================== + return ( +
+ + +
-

成就系统

+ +

成就系统

+
+ {activeTab === 'list' && ( + + )} + {activeTab === 'progress' && ( + + )} +
+
+ + {error &&
{error}
} + + {/* Tabs */} +
+ + + + +
+ + {/* Overview Tab */} + {activeTab === 'overview' && stats && ( +
+
+
+
总成就数
+
{stats.total_achievements}
+
+
+
已激活
+
{stats.active_achievements}
+
+
+
已完成
+
{stats.completed_achievements}
+
+
+
奖励已领取
+
{stats.rewards_claimed}
+
+
+
累计积分
+
{stats.total_points}
+
+
+ +
-

分类分布

+ +

分类分布

+ {Object.entries(stats.by_category).map(([cat, count]) => { + const pct = stats.total_achievements > 0 ? (count / stats.total_achievements) * 100 : 0; + return ( +
+ {CATEGORY_LABELS[cat] || cat} +
+
+
+ {count} +
+ ); + })} + {Object.keys(stats.by_category).length === 0 && ( +
暂无数据
+ )} +
+
+ )} + + {/* List Tab */} + {activeTab === 'list' && ( +
+
+ +
+ + {loading ? ( +
加载中...
+ ) : achievements.length === 0 ? ( +
暂无成就数据
+ ) : ( +
+ {achievements.map((ach) => { + const progressPct = getProgressPercent(ach.user_progress || 0, ach.criteria_value); + const color = CATEGORY_COLORS[ach.category] || '#6C63FF'; + return ( +
+
+ {ach.icon || CATEGORY_LABELS[ach.category]?.[0] || '★'} +
+
{ach.name}
+
{ach.description || '暂无描述'}
+
+ {CATEGORY_LABELS[ach.category] || ach.category} + + {CRITERIA_LABELS[ach.criteria_type] || ach.criteria_type}: {ach.criteria_value} + + + {REWARD_LABELS[ach.reward_type] || ach.reward_type}: {ach.reward_value} + + {!ach.is_active && ( + 已停用 + )} +
+ {ach.user_progress !== undefined && ( + <> +
+
+
+
+ {ach.user_progress || 0} / {ach.criteria_value} + {progressPct}% +
+ + )} +
+ + + +
+
+ ); + })} +
+ )} +
+ )} + + {/* Progress Tab */} + {activeTab === 'progress' && ( +
+ {loading ? ( +
加载中...
+ ) : userProgress.length === 0 ? ( +
暂无进度数据
+ ) : ( +
+ {userProgress.map((ach) => { + const progressPct = getProgressPercent(ach.progress || 0, ach.criteria_value); + const color = CATEGORY_COLORS[ach.category] || '#6C63FF'; + return ( +
+
+ {ach.icon || CATEGORY_LABELS[ach.category]?.[0] || '★'} +
+
{ach.name}
+
{ach.description || '暂无描述'}
+
+ {CATEGORY_LABELS[ach.category] || ach.category} + {ach.is_completed && ( + 已完成 + )} + {ach.reward_claimed && ( + 已领取 + )} +
+
+
+
+
+ {ach.progress || 0} / {ach.criteria_value} + {progressPct}% +
+ {ach.completed_at && ( -
+ +
+ 完成时间: {new Date(ach.completed_at).toLocaleString('zh-CN')} +
+ )} +
+ {ach.is_completed && !ach.reward_claimed && ( + + )} + {ach.reward_claimed && ( + 奖励已领取 + )} +
+
+ ); + })} +
+ )} +
+ )} + + {/* Leaderboard Tab */} + {activeTab === 'leaderboard' && ( +
+ {loading ? ( +
加载中...
+ ) : leaderboard.length === 0 ? ( +
+
排行榜暂无数据
+
用户完成成就后将出现在排行榜中
+
+ ) : ( + + + + + + + + + + + + {leaderboard.map((entry, idx) => ( + + + + + + + + ))} + +
排名用户完成成就累计积分最近完成
+ + {idx + 1} + + {entry.username || entry.user_id || '匿名用户'}{entry.achievement_count || 0}{entry.total_points || 0} + {entry.last_completed ? new Date(entry.last_completed).toLocaleDateString('zh-CN') : '-'} +
+ )} +
+ )} + + {/* Create/Edit Modal */} + {showCreateModal && ( +
setShowCreateModal(false)}> +
e.stopPropagation()}> +

{editingAch ? '编辑成就' : '创建成就'}

+
+ + setFormData({ ...formData, name: e.target.value })} placeholder="例如:对话达人" /> +
+
+ +