787 lines
30 KiB
TypeScript
787 lines
30 KiB
TypeScript
import { useState, useEffect } 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 {
|
||
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 {
|
||
Search,
|
||
UserPlus,
|
||
Trash2,
|
||
Edit3,
|
||
Key,
|
||
Save,
|
||
X,
|
||
RefreshCw,
|
||
Users,
|
||
Eye,
|
||
Crown,
|
||
} 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
|
||
}
|
||
|
||
export function UsersPage() {
|
||
const [users, setUsers] = useState<User[]>([])
|
||
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<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<{ referrals?: unknown[]; stats?: Record<string, unknown> }>({
|
||
referrals: [],
|
||
stats: {},
|
||
})
|
||
const [referralsLoading, setReferralsLoading] = useState(false)
|
||
const [selectedUserForReferrals, setSelectedUserForReferrals] = useState<User | null>(null)
|
||
const [showDetailModal, setShowDetailModal] = useState(false)
|
||
const [selectedUserIdForDetail, setSelectedUserIdForDetail] = useState<string | null>(null)
|
||
const [showSetVipModal, setShowSetVipModal] = useState(false)
|
||
const [selectedUserForVip, setSelectedUserForVip] = useState<User | null>(null)
|
||
const [formData, setFormData] = useState({
|
||
phone: '',
|
||
nickname: '',
|
||
password: '',
|
||
isAdmin: false,
|
||
hasFullBook: 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)
|
||
} else {
|
||
setError(data?.error || '加载失败')
|
||
}
|
||
} catch (err) {
|
||
console.error('Load users error:', err)
|
||
setError('网络错误,请检查连接')
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
setPage(1)
|
||
}, [debouncedSearch, vipFilter])
|
||
|
||
useEffect(() => {
|
||
loadUsers()
|
||
}, [page, pageSize, debouncedSearch, vipFilter])
|
||
|
||
const totalPages = Math.ceil(total / pageSize) || 1
|
||
|
||
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 (err) {
|
||
console.error('Delete user error:', err)
|
||
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 (err) {
|
||
console.error('Save user error:', err)
|
||
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<string, unknown> }>(
|
||
`/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)
|
||
}
|
||
|
||
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 (err) {
|
||
console.error('Change password error:', err)
|
||
alert('密码修改失败')
|
||
} finally {
|
||
setIsSaving(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="p-8 w-full">
|
||
{error && (
|
||
<div className="mb-4 px-4 py-3 rounded-lg bg-red-500/20 border border-red-500/50 text-red-400 text-sm flex items-center justify-between">
|
||
<span>{error}</span>
|
||
<button type="button" onClick={() => setError(null)} className="hover:text-red-300">
|
||
×
|
||
</button>
|
||
</div>
|
||
)}
|
||
<div className="flex justify-between items-center mb-8">
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-white">用户管理</h2>
|
||
<p className="text-gray-400 mt-1">
|
||
共 {total} 位注册用户
|
||
{vipFilter === 'vip' && `,当前筛选 VIP`}
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-4">
|
||
<Button
|
||
variant="outline"
|
||
onClick={loadUsers}
|
||
disabled={isLoading}
|
||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||
>
|
||
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
||
刷新
|
||
</Button>
|
||
<select
|
||
value={vipFilter}
|
||
onChange={(e) => {
|
||
setVipFilter(e.target.value as 'all' | 'vip')
|
||
setPage(1)
|
||
}}
|
||
className="bg-[#0f2137] border border-gray-700 text-white rounded-lg px-3 py-2 text-sm"
|
||
>
|
||
<option value="all">全部用户</option>
|
||
<option value="vip">VIP会员</option>
|
||
</select>
|
||
<div className="relative">
|
||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||
<Input
|
||
type="text"
|
||
placeholder="搜索用户..."
|
||
className="pl-10 bg-[#0f2137] border-gray-700 text-white placeholder:text-gray-500 w-64"
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
/>
|
||
</div>
|
||
<Button onClick={handleAddUser} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||
<UserPlus className="w-4 h-4 mr-2" />
|
||
添加用户
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<Dialog open={showUserModal} onOpenChange={setShowUserModal}>
|
||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-lg">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-white flex items-center gap-2">
|
||
{editingUser ? (
|
||
<Edit3 className="w-5 h-5 text-[#38bdac]" />
|
||
) : (
|
||
<UserPlus className="w-5 h-5 text-[#38bdac]" />
|
||
)}
|
||
{editingUser ? '编辑用户' : '添加用户'}
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="space-y-4 py-4">
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300">手机号</Label>
|
||
<Input
|
||
className="bg-[#0a1628] border-gray-700 text-white"
|
||
placeholder="请输入手机号"
|
||
value={formData.phone}
|
||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||
disabled={!!editingUser}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300">昵称</Label>
|
||
<Input
|
||
className="bg-[#0a1628] border-gray-700 text-white"
|
||
placeholder="请输入昵称"
|
||
value={formData.nickname}
|
||
onChange={(e) => setFormData({ ...formData, nickname: e.target.value })}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300">
|
||
{editingUser ? '新密码 (留空则不修改)' : '密码'}
|
||
</Label>
|
||
<Input
|
||
type="password"
|
||
className="bg-[#0a1628] border-gray-700 text-white"
|
||
placeholder={editingUser ? '留空则不修改' : '请输入密码'}
|
||
value={formData.password}
|
||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||
/>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<Label className="text-gray-300">管理员权限</Label>
|
||
<Switch
|
||
checked={formData.isAdmin}
|
||
onCheckedChange={(checked) => setFormData({ ...formData, isAdmin: checked })}
|
||
/>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<Label className="text-gray-300">已购全书</Label>
|
||
<Switch
|
||
checked={formData.hasFullBook}
|
||
onCheckedChange={(checked) => setFormData({ ...formData, hasFullBook: checked })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => setShowUserModal(false)}
|
||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||
>
|
||
<X className="w-4 h-4 mr-2" />
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={handleSaveUser}
|
||
disabled={isSaving}
|
||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||
>
|
||
<Save className="w-4 h-4 mr-2" />
|
||
{isSaving ? '保存中...' : '保存'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
<Dialog open={showPasswordModal} onOpenChange={setShowPasswordModal}>
|
||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-md">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-white flex items-center gap-2">
|
||
<Key className="w-5 h-5 text-[#38bdac]" />
|
||
修改密码
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="space-y-4 py-4">
|
||
<div className="bg-[#0a1628] rounded-lg p-3">
|
||
<p className="text-gray-400 text-sm">用户:{editingUser?.nickname}</p>
|
||
<p className="text-gray-400 text-sm">手机号:{editingUser?.phone}</p>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300">新密码</Label>
|
||
<Input
|
||
type="password"
|
||
className="bg-[#0a1628] border-gray-700 text-white"
|
||
placeholder="请输入新密码 (至少6位)"
|
||
value={newPassword}
|
||
onChange={(e) => setNewPassword(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300">确认密码</Label>
|
||
<Input
|
||
type="password"
|
||
className="bg-[#0a1628] border-gray-700 text-white"
|
||
placeholder="请再次输入新密码"
|
||
value={confirmPassword}
|
||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => setShowPasswordModal(false)}
|
||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={handleSavePassword}
|
||
disabled={isSaving}
|
||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||
>
|
||
{isSaving ? '保存中...' : '确认修改'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
<UserDetailModal
|
||
open={showDetailModal}
|
||
onClose={() => setShowDetailModal(false)}
|
||
userId={selectedUserIdForDetail}
|
||
onUserUpdated={loadUsers}
|
||
/>
|
||
|
||
<SetVipModal
|
||
open={showSetVipModal}
|
||
onClose={() => { setShowSetVipModal(false); setSelectedUserForVip(null) }}
|
||
userId={selectedUserForVip?.id ?? null}
|
||
userNickname={selectedUserForVip?.nickname}
|
||
onSaved={loadUsers}
|
||
/>
|
||
|
||
<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 as number) || 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 as number) || 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 as number) || 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 as number) || 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) > 0 ? (
|
||
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
||
{(referralsData.referrals ?? []).map((ref: unknown, i: number) => {
|
||
const r = ref as {
|
||
id?: string
|
||
nickname?: string
|
||
phone?: string
|
||
hasOpenId?: boolean
|
||
status?: string
|
||
purchasedSections?: number
|
||
createdAt?: string
|
||
}
|
||
return (
|
||
<div key={r.id || i} 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]">
|
||
{r.nickname?.charAt(0) || '?'}
|
||
</div>
|
||
<div>
|
||
<div className="text-white text-sm">{r.nickname}</div>
|
||
<div className="text-xs text-gray-500">
|
||
{r.phone || (r.hasOpenId ? '微信用户' : '未绑定')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
{r.status === 'vip' && (
|
||
<Badge className="bg-green-500/20 text-green-400 border-0 text-xs">全书已购</Badge>
|
||
)}
|
||
{r.status === 'paid' && (
|
||
<Badge className="bg-blue-500/20 text-blue-400 border-0 text-xs">
|
||
已付费{r.purchasedSections}章
|
||
</Badge>
|
||
)}
|
||
{r.status === 'free' && (
|
||
<Badge className="bg-gray-500/20 text-gray-400 border-0 text-xs">未付费</Badge>
|
||
)}
|
||
<span className="text-xs text-gray-500">
|
||
{r.createdAt ? new Date(r.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 ? (
|
||
<div className="flex items-center justify-center py-12">
|
||
<RefreshCw className="w-6 h-6 text-[#38bdac] animate-spin" />
|
||
<span className="ml-2 text-gray-400">加载中...</span>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<Table>
|
||
<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-right text-gray-400">操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{users.map((user) => (
|
||
<TableRow key={user.id} className="hover:bg-[#0a1628] border-gray-700/50">
|
||
<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.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">
|
||
<p className="font-medium text-white">{user.nickname}</p>
|
||
{user.isAdmin && (
|
||
<Badge className="bg-purple-500/20 text-purple-400 hover:bg-purple-500/20 border-0 text-xs">
|
||
管理员
|
||
</Badge>
|
||
)}
|
||
{user.openId && !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 font-mono">
|
||
{user.openId ? user.openId.slice(0, 12) + '...' : user.id?.slice(0, 12)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</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.wechatId && (
|
||
<div className="flex items-center gap-1 text-xs">
|
||
<span className="text-gray-500">💬</span>
|
||
<span className="text-gray-300">{user.wechatId}</span>
|
||
</div>
|
||
)}
|
||
{user.openId && (
|
||
<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.openId}>
|
||
{user.openId.slice(0, 12)}...
|
||
</span>
|
||
</div>
|
||
)}
|
||
{!user.phone && !user.wechatId && !user.openId && (
|
||
<span className="text-gray-600 text-xs">未绑定</span>
|
||
)}
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
{user.hasFullBook ? (
|
||
<Badge className="bg-amber-500/20 text-amber-400 hover:bg-amber-500/20 border-0">
|
||
VIP
|
||
</Badge>
|
||
) : (
|
||
<Badge variant="outline" className="text-gray-500 border-gray-600">
|
||
未购买
|
||
</Badge>
|
||
)}
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="space-y-1">
|
||
<div className="text-white font-medium">
|
||
¥{parseFloat(String(user.earnings || 0)).toFixed(2)}
|
||
</div>
|
||
{parseFloat(String(user.pendingEarnings || 0)) > 0 && (
|
||
<div className="text-xs text-yellow-400">
|
||
待提现: ¥{parseFloat(String(user.pendingEarnings || 0)).toFixed(2)}
|
||
</div>
|
||
)}
|
||
<div
|
||
className="text-xs text-[#38bdac] cursor-pointer hover:underline flex items-center gap-1"
|
||
onClick={() => handleViewReferrals(user)}
|
||
onKeyDown={(e) => e.key === 'Enter' && handleViewReferrals(user)}
|
||
role="button"
|
||
tabIndex={0}
|
||
>
|
||
<Users className="w-3 h-3" />
|
||
绑定{user.referralCount || 0}人
|
||
</div>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<code className="text-[#38bdac] text-xs bg-[#38bdac]/10 px-2 py-0.5 rounded">
|
||
{user.referralCode || '-'}
|
||
</code>
|
||
</TableCell>
|
||
<TableCell className="text-gray-400">
|
||
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
|
||
</TableCell>
|
||
<TableCell className="text-right">
|
||
<div className="flex items-center justify-end gap-1">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleSetVip(user)}
|
||
className="text-gray-400 hover:text-amber-400 hover:bg-amber-400/10"
|
||
title="设置 VIP"
|
||
>
|
||
<Crown className="w-4 h-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleViewDetail(user)}
|
||
className="text-gray-400 hover:text-blue-400 hover:bg-blue-400/10"
|
||
title="查看详情"
|
||
>
|
||
<Eye className="w-4 h-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleEditUser(user)}
|
||
className="text-gray-400 hover:text-[#38bdac] hover:bg-[#38bdac]/10"
|
||
title="编辑"
|
||
>
|
||
<Edit3 className="w-4 h-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleChangePassword(user)}
|
||
className="text-gray-400 hover:text-yellow-400 hover:bg-yellow-400/10"
|
||
title="修改密码"
|
||
>
|
||
<Key className="w-4 h-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||
onClick={() => handleDelete(user.id)}
|
||
title="删除"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
{users.length === 0 && (
|
||
<TableRow>
|
||
<TableCell colSpan={7} className="text-center py-12 text-gray-500">
|
||
暂无用户数据
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
<Pagination
|
||
page={page}
|
||
totalPages={totalPages}
|
||
total={total}
|
||
pageSize={pageSize}
|
||
onPageChange={setPage}
|
||
onPageSizeChange={(n) => {
|
||
setPageSize(n)
|
||
setPage(1)
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|