2026-01-09 11:58:08 +08:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, Suspense } from "react"
|
|
|
|
|
|
import { Card, CardContent } from "@/components/ui/card"
|
|
|
|
|
|
import { Input } from "@/components/ui/input"
|
|
|
|
|
|
import { Button } from "@/components/ui/button"
|
2026-01-21 15:49:12 +08:00
|
|
|
|
import { Label } from "@/components/ui/label"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
|
|
|
|
|
import { Badge } from "@/components/ui/badge"
|
2026-01-21 15:49:12 +08:00
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
|
interface User {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
phone: string
|
|
|
|
|
|
nickname: string
|
|
|
|
|
|
password?: string
|
|
|
|
|
|
is_admin?: boolean
|
|
|
|
|
|
has_full_book?: boolean
|
|
|
|
|
|
referral_code: string
|
|
|
|
|
|
referred_by?: string
|
|
|
|
|
|
earnings: number
|
|
|
|
|
|
pending_earnings: number
|
|
|
|
|
|
withdrawn_earnings: number
|
|
|
|
|
|
referral_count: number
|
|
|
|
|
|
match_count_today?: number
|
|
|
|
|
|
last_match_date?: string
|
|
|
|
|
|
created_at: string
|
|
|
|
|
|
}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
|
|
|
|
|
function UsersContent() {
|
|
|
|
|
|
const [users, setUsers] = useState<User[]>([])
|
|
|
|
|
|
const [searchTerm, setSearchTerm] = useState("")
|
2026-01-21 15:49:12 +08:00
|
|
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
|
|
|
|
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 [formData, setFormData] = useState({
|
|
|
|
|
|
phone: "",
|
|
|
|
|
|
nickname: "",
|
|
|
|
|
|
password: "",
|
|
|
|
|
|
is_admin: false,
|
|
|
|
|
|
has_full_book: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 加载用户列表
|
|
|
|
|
|
const loadUsers = async () => {
|
|
|
|
|
|
setIsLoading(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch('/api/db/users')
|
|
|
|
|
|
const data = await res.json()
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
setUsers(data.users || [])
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Load users error:', error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-01-21 15:49:12 +08:00
|
|
|
|
loadUsers()
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
const filteredUsers = users.filter((u) =>
|
|
|
|
|
|
u.nickname?.includes(searchTerm) || u.phone?.includes(searchTerm)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 删除用户
|
|
|
|
|
|
const handleDelete = async (userId: string) => {
|
|
|
|
|
|
if (!confirm("确定要删除这个用户吗?")) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`/api/db/users?id=${userId}`, { method: 'DELETE' })
|
|
|
|
|
|
const data = await res.json()
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
loadUsers()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
alert("删除失败: " + (data.error || "未知错误"))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Delete user error:', error)
|
|
|
|
|
|
alert("删除失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打开编辑用户弹窗
|
|
|
|
|
|
const handleEditUser = (user: User) => {
|
|
|
|
|
|
setEditingUser(user)
|
|
|
|
|
|
setFormData({
|
|
|
|
|
|
phone: user.phone,
|
|
|
|
|
|
nickname: user.nickname,
|
|
|
|
|
|
password: "",
|
|
|
|
|
|
is_admin: user.is_admin || false,
|
|
|
|
|
|
has_full_book: user.has_full_book || false,
|
|
|
|
|
|
})
|
|
|
|
|
|
setShowUserModal(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打开新建用户弹窗
|
|
|
|
|
|
const handleAddUser = () => {
|
|
|
|
|
|
setEditingUser(null)
|
|
|
|
|
|
setFormData({
|
|
|
|
|
|
phone: "",
|
|
|
|
|
|
nickname: "",
|
|
|
|
|
|
password: "",
|
|
|
|
|
|
is_admin: false,
|
|
|
|
|
|
has_full_book: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
setShowUserModal(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存用户
|
|
|
|
|
|
const handleSaveUser = async () => {
|
|
|
|
|
|
if (!formData.phone || !formData.nickname) {
|
|
|
|
|
|
alert("请填写手机号和昵称")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
setIsSaving(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (editingUser) {
|
|
|
|
|
|
// 更新用户
|
|
|
|
|
|
const res = await fetch('/api/db/users', {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
id: editingUser.id,
|
|
|
|
|
|
nickname: formData.nickname,
|
|
|
|
|
|
is_admin: formData.is_admin,
|
|
|
|
|
|
has_full_book: formData.has_full_book,
|
|
|
|
|
|
...(formData.password && { password: formData.password }),
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
const data = await res.json()
|
|
|
|
|
|
if (!data.success) {
|
|
|
|
|
|
alert("更新失败: " + (data.error || "未知错误"))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 创建用户
|
|
|
|
|
|
const res = await fetch('/api/db/users', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
phone: formData.phone,
|
|
|
|
|
|
nickname: formData.nickname,
|
|
|
|
|
|
password: formData.password,
|
|
|
|
|
|
is_admin: formData.is_admin,
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
const data = await res.json()
|
|
|
|
|
|
if (!data.success) {
|
|
|
|
|
|
alert("创建失败: " + (data.error || "未知错误"))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
setShowUserModal(false)
|
|
|
|
|
|
loadUsers()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Save user error:', error)
|
|
|
|
|
|
alert("保存失败")
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSaving(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打开修改密码弹窗
|
|
|
|
|
|
const handleChangePassword = (user: User) => {
|
|
|
|
|
|
setEditingUser(user)
|
|
|
|
|
|
setNewPassword("")
|
|
|
|
|
|
setConfirmPassword("")
|
|
|
|
|
|
setShowPasswordModal(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存密码
|
|
|
|
|
|
const handleSavePassword = async () => {
|
|
|
|
|
|
if (!newPassword) {
|
|
|
|
|
|
alert("请输入新密码")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
|
|
|
|
alert("两次输入的密码不一致")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (newPassword.length < 6) {
|
|
|
|
|
|
alert("密码长度不能少于6位")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setIsSaving(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch('/api/db/users', {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
id: editingUser?.id,
|
|
|
|
|
|
password: newPassword,
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
const data = await res.json()
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
alert("密码修改成功")
|
|
|
|
|
|
setShowPasswordModal(false)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
alert("密码修改失败: " + (data.error || "未知错误"))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Change password error:', error)
|
|
|
|
|
|
alert("密码修改失败")
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSaving(false)
|
2026-01-09 11:58:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="p-8 max-w-7xl mx-auto">
|
|
|
|
|
|
<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">共 {users.length} 位注册用户</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-4">
|
2026-01-21 15:49:12 +08:00
|
|
|
|
<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>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
<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>
|
2026-01-21 15:49:12 +08:00
|
|
|
|
<Button onClick={handleAddUser} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
2026-01-09 11:58:08 +08:00
|
|
|
|
<UserPlus className="w-4 h-4 mr-2" />
|
|
|
|
|
|
添加用户
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
{/* 用户编辑弹窗 */}
|
|
|
|
|
|
<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.is_admin}
|
|
|
|
|
|
onCheckedChange={(checked) => setFormData({ ...formData, is_admin: checked })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<Label className="text-gray-300">已购全书</Label>
|
|
|
|
|
|
<Switch
|
|
|
|
|
|
checked={formData.has_full_book}
|
|
|
|
|
|
onCheckedChange={(checked) => setFormData({ ...formData, has_full_book: 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>
|
|
|
|
|
|
|
2026-01-09 11:58:08 +08:00
|
|
|
|
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
|
|
|
|
|
<CardContent className="p-0">
|
2026-01-21 15:49:12 +08:00
|
|
|
|
{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>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<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>
|
|
|
|
|
|
{filteredUsers.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.nickname?.charAt(0) || "?"}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<p className="font-medium text-white">{user.nickname}</p>
|
|
|
|
|
|
{user.is_admin && (
|
|
|
|
|
|
<Badge className="bg-purple-500/20 text-purple-400 hover:bg-purple-500/20 border-0 text-xs">
|
|
|
|
|
|
管理员
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-xs text-gray-500">ID: {user.id?.slice(0, 8)}</p>
|
|
|
|
|
|
</div>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
2026-01-21 15:49:12 +08:00
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell className="text-gray-300">{user.phone}</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
{user.has_full_book ? (
|
|
|
|
|
|
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">全书已购</Badge>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Badge variant="outline" className="text-gray-500 border-gray-600">
|
|
|
|
|
|
未购买
|
|
|
|
|
|
</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 className="text-gray-400">
|
|
|
|
|
|
{user.created_at ? new Date(user.created_at).toLocaleDateString() : "-"}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell className="text-right">
|
|
|
|
|
|
<div className="flex items-center justify-end gap-1">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => handleEditUser(user)}
|
|
|
|
|
|
className="text-gray-400 hover:text-[#38bdac] hover:bg-[#38bdac]/10"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="w-4 h-4" />
|
|
|
|
|
|
</Button>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
2026-01-21 15:49:12 +08:00
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{filteredUsers.length === 0 && (
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableCell colSpan={7} className="text-center py-12 text-gray-500">
|
|
|
|
|
|
暂无用户数据
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
)}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function UsersPage() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Suspense fallback={null}>
|
|
|
|
|
|
<UsersContent />
|
|
|
|
|
|
</Suspense>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|