feat: 完善后台管理+搜索功能+分销系统
主要更新: - 后台菜单精简(9项→6项) - 新增搜索功能(敏感信息过滤) - 分销绑定和提现系统完善 - 数据库初始化API(自动修复表结构) - 用户管理:显示绑定关系详情 - 小程序:上下章导航优化、匹配页面重构 - 修复hydration和数据类型问题
This commit is contained in:
@@ -9,36 +9,48 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Search, UserPlus, Eye, Trash2, Edit3, Key, Save, X, RefreshCw } from "lucide-react"
|
||||
import { Search, UserPlus, Trash2, Edit3, Key, Save, X, RefreshCw, Users, Eye } from "lucide-react"
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
phone: string
|
||||
open_id?: string | null
|
||||
phone?: string | null
|
||||
nickname: string
|
||||
password?: string
|
||||
is_admin?: boolean
|
||||
has_full_book?: boolean
|
||||
password?: string | null
|
||||
wechat_id?: string | null
|
||||
avatar?: string | null
|
||||
is_admin?: boolean | number
|
||||
has_full_book?: boolean | number
|
||||
referral_code: string
|
||||
referred_by?: string
|
||||
earnings: number
|
||||
pending_earnings: number
|
||||
withdrawn_earnings: number
|
||||
referred_by?: string | null
|
||||
earnings: number | string
|
||||
pending_earnings: number | string
|
||||
withdrawn_earnings?: number | string
|
||||
referral_count: number
|
||||
match_count_today?: number
|
||||
last_match_date?: string
|
||||
last_match_date?: string | null
|
||||
purchased_sections?: string[] | string | null
|
||||
created_at: string
|
||||
updated_at?: string | null
|
||||
}
|
||||
|
||||
function UsersContent() {
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showUserModal, setShowUserModal] = useState(false)
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false)
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null)
|
||||
const [newPassword, setNewPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
// 绑定关系弹窗
|
||||
const [showReferralsModal, setShowReferralsModal] = useState(false)
|
||||
const [referralsData, setReferralsData] = useState<any>({ referrals: [], stats: {} })
|
||||
const [referralsLoading, setReferralsLoading] = useState(false)
|
||||
const [selectedUserForReferrals, setSelectedUserForReferrals] = useState<User | null>(null)
|
||||
|
||||
// 初始表单状态
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -52,14 +64,18 @@ function UsersContent() {
|
||||
// 加载用户列表
|
||||
const loadUsers = async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch('/api/db/users')
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
setUsers(data.users || [])
|
||||
} else {
|
||||
setError(data.error || '加载失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load users error:', error)
|
||||
} catch (err) {
|
||||
console.error('Load users error:', err)
|
||||
setError('网络错误,请检查连接')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -95,8 +111,8 @@ function UsersContent() {
|
||||
const handleEditUser = (user: User) => {
|
||||
setEditingUser(user)
|
||||
setFormData({
|
||||
phone: user.phone,
|
||||
nickname: user.nickname,
|
||||
phone: user.phone || "",
|
||||
nickname: user.nickname || "",
|
||||
password: "",
|
||||
is_admin: user.is_admin || false,
|
||||
has_full_book: user.has_full_book || false,
|
||||
@@ -181,6 +197,28 @@ function UsersContent() {
|
||||
setShowPasswordModal(true)
|
||||
}
|
||||
|
||||
// 查看绑定关系
|
||||
const handleViewReferrals = async (user: User) => {
|
||||
setSelectedUserForReferrals(user)
|
||||
setShowReferralsModal(true)
|
||||
setReferralsLoading(true)
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/db/users/referrals?userId=${user.id}`)
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
setReferralsData(data)
|
||||
} else {
|
||||
setReferralsData({ referrals: [], stats: {} })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Load referrals error:', err)
|
||||
setReferralsData({ referrals: [], stats: {} })
|
||||
} finally {
|
||||
setReferralsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存密码
|
||||
const handleSavePassword = async () => {
|
||||
if (!newPassword) {
|
||||
@@ -384,6 +422,92 @@ function UsersContent() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 绑定关系弹窗 */}
|
||||
<Dialog open={showReferralsModal} onOpenChange={setShowReferralsModal}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-2xl max-h-[80vh] overflow-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-[#38bdac]" />
|
||||
绑定关系详情 - {selectedUserForReferrals?.nickname}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
{/* 统计信息 */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="bg-[#0a1628] rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-[#38bdac]">{referralsData.stats?.total || 0}</div>
|
||||
<div className="text-xs text-gray-400">绑定总数</div>
|
||||
</div>
|
||||
<div className="bg-[#0a1628] rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-green-400">{referralsData.stats?.purchased || 0}</div>
|
||||
<div className="text-xs text-gray-400">已付费</div>
|
||||
</div>
|
||||
<div className="bg-[#0a1628] rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-yellow-400">¥{(referralsData.stats?.earnings || 0).toFixed(2)}</div>
|
||||
<div className="text-xs text-gray-400">累计收益</div>
|
||||
</div>
|
||||
<div className="bg-[#0a1628] rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-orange-400">¥{(referralsData.stats?.pendingEarnings || 0).toFixed(2)}</div>
|
||||
<div className="text-xs text-gray-400">待提现</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 绑定用户列表 */}
|
||||
{referralsLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<RefreshCw className="w-5 h-5 text-[#38bdac] animate-spin" />
|
||||
<span className="ml-2 text-gray-400">加载中...</span>
|
||||
</div>
|
||||
) : referralsData.referrals?.length > 0 ? (
|
||||
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
{referralsData.referrals.map((ref: any) => (
|
||||
<div key={ref.id} className="flex items-center justify-between bg-[#0a1628] rounded-lg p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm text-[#38bdac]">
|
||||
{ref.nickname?.charAt(0) || "?"}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white text-sm">{ref.nickname}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{ref.phone || (ref.hasOpenId ? '微信用户' : '未绑定')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{ref.status === 'vip' && (
|
||||
<Badge className="bg-green-500/20 text-green-400 border-0 text-xs">全书已购</Badge>
|
||||
)}
|
||||
{ref.status === 'paid' && (
|
||||
<Badge className="bg-blue-500/20 text-blue-400 border-0 text-xs">已付费{ref.purchasedSections}章</Badge>
|
||||
)}
|
||||
{ref.status === 'free' && (
|
||||
<Badge className="bg-gray-500/20 text-gray-400 border-0 text-xs">未付费</Badge>
|
||||
)}
|
||||
<span className="text-xs text-gray-500">
|
||||
{ref.createdAt ? new Date(ref.createdAt).toLocaleDateString() : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
暂无绑定用户
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowReferralsModal(false)}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardContent className="p-0">
|
||||
{isLoading ? (
|
||||
@@ -396,10 +520,10 @@ function UsersContent() {
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#0a1628] hover:bg-[#0a1628] border-gray-700">
|
||||
<TableHead className="text-gray-400">用户信息</TableHead>
|
||||
<TableHead className="text-gray-400">手机号</TableHead>
|
||||
<TableHead className="text-gray-400">绑定信息</TableHead>
|
||||
<TableHead className="text-gray-400">购买状态</TableHead>
|
||||
<TableHead className="text-gray-400">分销收益</TableHead>
|
||||
<TableHead className="text-gray-400">今日匹配</TableHead>
|
||||
<TableHead className="text-gray-400">推广码</TableHead>
|
||||
<TableHead className="text-gray-400">注册时间</TableHead>
|
||||
<TableHead className="text-right text-gray-400">操作</TableHead>
|
||||
</TableRow>
|
||||
@@ -410,7 +534,11 @@ function UsersContent() {
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac]">
|
||||
{user.nickname?.charAt(0) || "?"}
|
||||
{user.avatar ? (
|
||||
<img src={user.avatar} className="w-full h-full rounded-full object-cover" alt="" />
|
||||
) : (
|
||||
user.nickname?.charAt(0) || "?"
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -420,12 +548,45 @@ function UsersContent() {
|
||||
管理员
|
||||
</Badge>
|
||||
)}
|
||||
{user.open_id && !user.id?.startsWith('user_') && (
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0 text-xs">
|
||||
微信
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">ID: {user.id?.slice(0, 8)}</p>
|
||||
<p className="text-xs text-gray-500 font-mono">
|
||||
{user.open_id ? user.open_id.slice(0, 12) + '...' : user.id?.slice(0, 12)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-300">{user.phone}</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
{user.phone && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<span className="text-gray-500">📱</span>
|
||||
<span className="text-gray-300">{user.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
{user.wechat_id && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<span className="text-gray-500">💬</span>
|
||||
<span className="text-gray-300">{user.wechat_id}</span>
|
||||
</div>
|
||||
)}
|
||||
{user.open_id && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<span className="text-gray-500">🔗</span>
|
||||
<span className="text-gray-500 truncate max-w-[100px]" title={user.open_id}>
|
||||
{user.open_id.slice(0, 12)}...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!user.phone && !user.wechat_id && !user.open_id && (
|
||||
<span className="text-gray-600 text-xs">未绑定</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.has_full_book ? (
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">全书已购</Badge>
|
||||
@@ -435,8 +596,31 @@ function UsersContent() {
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-white font-medium">¥{(user.earnings || 0).toFixed(2)}</TableCell>
|
||||
<TableCell className="text-gray-300">{user.match_count_today || 0}/3</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className="text-white font-medium">¥{parseFloat(String(user.earnings || 0)).toFixed(2)}</div>
|
||||
{parseFloat(String(user.pending_earnings || 0)) > 0 && (
|
||||
<div className="text-xs text-yellow-400">待提现: ¥{parseFloat(String(user.pending_earnings || 0)).toFixed(2)}</div>
|
||||
)}
|
||||
<div
|
||||
className="text-xs text-[#38bdac] cursor-pointer hover:underline flex items-center gap-1"
|
||||
onClick={() => handleViewReferrals(user)}
|
||||
>
|
||||
<Users className="w-3 h-3" />
|
||||
绑定{user.referral_count || 0}人
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<code className="text-[#38bdac] text-xs bg-[#38bdac]/10 px-2 py-0.5 rounded">
|
||||
{user.referral_code || '-'}
|
||||
</code>
|
||||
{user.referred_by && (
|
||||
<div className="text-xs text-gray-500">来自: {user.referred_by.slice(0, 8)}</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-400">
|
||||
{user.created_at ? new Date(user.created_at).toLocaleDateString() : "-"}
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user