實作管理者用戶管理、邀請註冊功能

This commit is contained in:
2025-09-09 15:15:26 +08:00
parent 32b19e9a0f
commit 46bd9db2e3
11 changed files with 1297 additions and 214 deletions

View File

@@ -33,9 +33,17 @@ import {
RefreshCw,
Copy,
Link,
Heart,
ThumbsUp,
Star,
Plus,
ExternalLink,
Code,
Users,
ChevronLeft,
ChevronRight,
Filter,
X,
} from "lucide-react"
export function UserManagement() {
@@ -45,6 +53,25 @@ export function UserManagement() {
const [selectedRole, setSelectedRole] = useState("all")
const [selectedStatus, setSelectedStatus] = useState("all")
const [selectedUser, setSelectedUser] = useState<any>(null)
const [userActivities, setUserActivities] = useState<any[]>([])
const [userStats, setUserStats] = useState<any>({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
const [activityPagination, setActivityPagination] = useState({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
const [activityFilters, setActivityFilters] = useState({
search: '',
startDate: '',
endDate: '',
activityType: 'all'
})
const [showUserDetail, setShowUserDetail] = useState(false)
const [showInviteUser, setShowInviteUser] = useState(false)
const [showEditUser, setShowEditUser] = useState(false)
@@ -71,6 +98,74 @@ export function UserManagement() {
setIsClient(true)
}, [])
// 載入用戶統計數據
const loadUserStats = async (userId: string) => {
try {
const response = await fetch(`/api/admin/users/${userId}/stats`)
const data = await response.json()
if (data.success) {
setUserStats(data.data)
} else {
console.error('載入用戶統計數據失敗:', data.error)
setUserStats({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
}
} catch (error) {
console.error('載入用戶統計數據錯誤:', error)
setUserStats({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
}
}
// 載入用戶活動記錄
const loadUserActivities = async (userId: string, page: number = 1, filters: any = {}) => {
try {
const params = new URLSearchParams({
page: page.toString(),
limit: activityPagination.limit.toString(),
search: filters.search || '',
startDate: filters.startDate || '',
endDate: filters.endDate || '',
activityType: filters.activityType || 'all'
})
const response = await fetch(`/api/admin/users/${userId}/activities?${params}`)
const data = await response.json()
if (data.success) {
setUserActivities(data.data.activities)
setActivityPagination(data.data.pagination)
} else {
console.error('載入活動記錄失敗:', data.error)
setUserActivities([])
setActivityPagination({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
}
} catch (error) {
console.error('載入活動記錄錯誤:', error)
setUserActivities([])
setActivityPagination({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
}
}
// 載入用戶數據
const loadUsers = async () => {
if (!isClient) return
@@ -133,6 +228,51 @@ export function UserManagement() {
const handleViewUser = (user: any) => {
setSelectedUser(user)
setShowUserDetail(true)
// 重置活動記錄狀態
setActivityPagination({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
setActivityFilters({
search: '',
startDate: '',
endDate: '',
activityType: 'all'
})
// 重置統計數據狀態
setUserStats({
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
loginDays: 0
})
// 載入用戶統計數據和活動記錄
loadUserStats(user.id)
loadUserActivities(user.id, 1, {
search: '',
startDate: '',
endDate: '',
activityType: 'all'
})
}
// 處理活動記錄篩選
const handleActivityFilter = (newFilters: any) => {
const updatedFilters = { ...activityFilters, ...newFilters }
setActivityFilters(updatedFilters)
if (selectedUser) {
loadUserActivities(selectedUser.id, 1, updatedFilters)
}
}
// 處理活動記錄分頁
const handleActivityPageChange = (page: number) => {
setActivityPagination(prev => ({ ...prev, page }))
if (selectedUser) {
loadUserActivities(selectedUser.id, page, activityFilters)
}
}
const handleEditUser = (user: any) => {
@@ -155,18 +295,51 @@ export function UserManagement() {
const handleToggleUserStatus = async (userId: string) => {
setIsLoading(true)
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1000))
try {
// 找到要切換狀態的用戶
const user = users.find(u => u.id === userId)
if (!user) {
setError("用戶不存在")
setIsLoading(false)
return
}
setUsers(
users.map((user) =>
user.id === userId ? { ...user, status: user.status === "active" ? "inactive" : "active" } : user,
),
)
const newStatus = user.status === "active" ? "inactive" : "active"
const response = await fetch(`/api/admin/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: user.name,
department: user.department,
role: user.role,
status: newStatus
})
})
const data = await response.json()
if (data.success) {
// 更新本地用戶列表
setUsers(
users.map((u) =>
u.id === userId ? { ...u, status: newStatus } : u
)
)
setSuccess("用戶狀態更新成功!")
setTimeout(() => setSuccess(""), 3000)
} else {
setError(data.error || "更新用戶狀態失敗")
}
} catch (error) {
console.error('更新用戶狀態錯誤:', error)
setError("更新用戶狀態時發生錯誤")
}
setIsLoading(false)
setSuccess("用戶狀態更新成功!")
setTimeout(() => setSuccess(""), 3000)
}
const handleChangeUserRole = async (userId: string, newRole: string) => {
@@ -222,13 +395,13 @@ export function UserManagement() {
if (data.success) {
const newInvitation = {
id: Date.now().toString(),
name: "",
email: inviteEmail,
department: "",
role: "",
id: data.data.user.id,
name: data.data.user.name || "",
email: data.data.user.email,
department: data.data.user.department || "",
role: data.data.user.role,
status: "invited",
joinDate: "",
joinDate: data.data.user.join_date ? new Date(data.data.user.join_date).toLocaleDateString('zh-TW') : "",
lastLogin: "",
totalApps: 0,
totalReviews: 0,
@@ -327,15 +500,38 @@ export function UserManagement() {
setIsLoading(true)
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1500))
try {
const response = await fetch(`/api/admin/users/${editUser.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: editUser.name,
department: editUser.department,
role: editUser.role,
status: editUser.status
})
})
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
const data = await response.json()
if (data.success) {
// 更新本地用戶列表
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
setShowEditUser(false)
setSuccess("用戶資料更新成功!")
setTimeout(() => setSuccess(""), 3000)
} else {
setError(data.error || "更新用戶失敗")
}
} catch (error) {
console.error('更新用戶錯誤:', error)
setError("更新用戶時發生錯誤")
}
setIsLoading(false)
setShowEditUser(false)
setSuccess("用戶資料更新成功!")
setTimeout(() => setSuccess(""), 3000)
}
const confirmDeleteUser = async () => {
@@ -343,16 +539,30 @@ export function UserManagement() {
setIsLoading(true)
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1500))
try {
const response = await fetch(`/api/admin/users/${userToDelete.id}`, {
method: 'DELETE'
})
setUsers(users.filter((user) => user.id !== userToDelete.id))
const data = await response.json()
if (data.success) {
// 從本地用戶列表中移除
setUsers(users.filter((user) => user.id !== userToDelete.id))
setShowDeleteConfirm(false)
setUserToDelete(null)
setSuccess("用戶刪除成功!")
setTimeout(() => setSuccess(""), 3000)
} else {
setError(data.error || "刪除用戶失敗")
}
} catch (error) {
console.error('刪除用戶錯誤:', error)
setError("刪除用戶時發生錯誤")
}
setIsLoading(false)
setShowDeleteConfirm(false)
setUserToDelete(null)
setSuccess("用戶刪除成功!")
setTimeout(() => setSuccess(""), 3000)
}
const getRoleColor = (role: string) => {
@@ -407,6 +617,44 @@ export function UserManagement() {
}
}
// 獲取活動圖標
const getActivityIcon = (iconName: string) => {
switch (iconName) {
case 'Calendar':
return <Calendar className="w-4 h-4" />
case 'Heart':
return <Heart className="w-4 h-4" />
case 'ThumbsUp':
return <ThumbsUp className="w-4 h-4" />
case 'Eye':
return <Eye className="w-4 h-4" />
case 'Star':
return <Star className="w-4 h-4" />
case 'Plus':
return <Plus className="w-4 h-4" />
default:
return <Calendar className="w-4 h-4" />
}
}
// 獲取活動顏色
const getActivityColor = (color: string) => {
switch (color) {
case 'blue':
return 'text-blue-600'
case 'red':
return 'text-red-600'
case 'green':
return 'text-green-600'
case 'purple':
return 'text-purple-600'
case 'yellow':
return 'text-yellow-600'
default:
return 'text-gray-600'
}
}
const getRoleIcon = (role: string) => {
switch (role) {
case "admin":
@@ -1140,22 +1388,141 @@ export function UserManagement() {
<p className="text-gray-500"></p>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Calendar className="w-4 h-4 text-blue-600" />
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500">2024-01-20 16:45</p>
<>
{/* 篩選控件 */}
<div className="space-y-3">
<div className="flex flex-wrap items-end justify-between gap-3">
<div className="flex flex-wrap gap-3 items-end">
<div className="flex-1 min-w-[180px]">
<Label htmlFor="activity-search" className="text-xs"></Label>
<Input
id="activity-search"
placeholder="搜尋活動內容..."
value={activityFilters.search}
onChange={(e) => handleActivityFilter({ search: e.target.value })}
className="mt-1 h-8 text-sm"
/>
</div>
<div className="min-w-[100px]">
<Label htmlFor="activity-type" className="text-xs"></Label>
<Select
value={activityFilters.activityType}
onValueChange={(value) => handleActivityFilter({ activityType: value })}
>
<SelectTrigger className="mt-1 h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="login"></SelectItem>
<SelectItem value="favorite"></SelectItem>
<SelectItem value="like"></SelectItem>
<SelectItem value="view"></SelectItem>
<SelectItem value="rating"></SelectItem>
<SelectItem value="create"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="min-w-[120px]">
<Label htmlFor="start-date" className="text-xs"></Label>
<Input
id="start-date"
type="date"
value={activityFilters.startDate}
onChange={(e) => handleActivityFilter({ startDate: e.target.value })}
className="mt-1 h-8 text-sm"
/>
</div>
<div className="min-w-[120px]">
<Label htmlFor="end-date" className="text-xs"></Label>
<Input
id="end-date"
type="date"
value={activityFilters.endDate}
onChange={(e) => handleActivityFilter({ endDate: e.target.value })}
className="mt-1 h-8 text-sm"
/>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handleActivityFilter({
search: '',
startDate: '',
endDate: '',
activityType: 'all'
})}
className="h-8 px-3 text-xs"
>
<X className="w-3 h-3 mr-1" />
</Button>
</div>
</div>
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Eye className="w-4 h-4 text-green-600" />
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500">2024-01-20 15:30</p>
{/* 活動記錄列表 */}
{userActivities.length === 0 ? (
<div className="text-center py-8">
<Calendar className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500"></p>
</div>
</div>
</div>
) : (
<>
<div className="space-y-3">
{userActivities.map((activity, index) => (
<div key={index} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<div className={`${getActivityColor(activity.color)}`}>
{getActivityIcon(activity.icon)}
</div>
<div className="flex-1">
<p className="text-sm font-medium">{activity.action_text}</p>
<p className="text-xs text-gray-500">
{new Date(activity.created_at).toLocaleString('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
</div>
))}
</div>
{/* 分頁控件 */}
{activityPagination.totalPages > 1 && (
<div className="flex items-center justify-between pt-4">
<div className="text-sm text-gray-500">
{((activityPagination.page - 1) * activityPagination.limit) + 1} - {Math.min(activityPagination.page * activityPagination.limit, activityPagination.total)} {activityPagination.total}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleActivityPageChange(activityPagination.page - 1)}
disabled={activityPagination.page <= 1}
>
<ChevronLeft className="w-4 h-4" />
</Button>
<span className="text-sm">
{activityPagination.page} {activityPagination.totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => handleActivityPageChange(activityPagination.page + 1)}
disabled={activityPagination.page >= activityPagination.totalPages}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
)}
</>
)}
</>
)}
</TabsContent>
@@ -1164,7 +1531,7 @@ export function UserManagement() {
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">{selectedUser.totalApps}</p>
<p className="text-2xl font-bold text-blue-600">{userStats.totalApps}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
@@ -1172,7 +1539,7 @@ export function UserManagement() {
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-green-600">{selectedUser.totalReviews}</p>
<p className="text-2xl font-bold text-green-600">{userStats.totalReviews}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
@@ -1180,7 +1547,7 @@ export function UserManagement() {
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-purple-600">{selectedUser.totalLikes}</p>
<p className="text-2xl font-bold text-purple-600">{userStats.totalLikes}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
@@ -1188,9 +1555,7 @@ export function UserManagement() {
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-orange-600">
{selectedUser.status === "invited" ? 0 : 15}
</p>
<p className="text-2xl font-bold text-orange-600">{userStats.loginDays}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>