整合資料庫、完成登入註冊忘記密碼功能
This commit is contained in:
@@ -77,17 +77,27 @@ const mockNotifications: Notification[] = []
|
||||
const mockSearchData: SearchResult[] = []
|
||||
|
||||
export function AdminLayout({ children, currentPage, onPageChange }: AdminLayoutProps) {
|
||||
const { user, logout, isLoading } = useAuth()
|
||||
|
||||
// Move ALL hooks to the top, before any conditional logic
|
||||
const { user, logout, isLoading, isInitialized } = useAuth()
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||||
const [isClient, setIsClient] = useState(false)
|
||||
|
||||
// Search state
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
|
||||
const [showSearchResults, setShowSearchResults] = useState(false)
|
||||
|
||||
// Notification state
|
||||
const [notifications, setNotifications] = useState<Notification[]>(mockNotifications)
|
||||
const [showNotifications, setShowNotifications] = useState(false)
|
||||
|
||||
// Logout confirmation state
|
||||
const [showLogoutDialog, setShowLogoutDialog] = useState(false)
|
||||
|
||||
// Set client state after hydration
|
||||
useEffect(() => {
|
||||
setIsClient(true)
|
||||
}, [])
|
||||
|
||||
// Handle search
|
||||
useEffect(() => {
|
||||
if (searchQuery.trim()) {
|
||||
@@ -104,46 +114,6 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
|
||||
}
|
||||
}, [searchQuery])
|
||||
|
||||
// 認證檢查 - moved after all hooks
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span className="text-gray-600">載入中...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">需要登入</h1>
|
||||
<p className="text-gray-600 mb-6">請先登入才能訪問管理員頁面</p>
|
||||
<Button onClick={() => window.location.href = '/'}>
|
||||
返回首頁
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (user.role !== 'admin') {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">權限不足</h1>
|
||||
<p className="text-gray-600 mb-6">您沒有訪問管理員頁面的權限</p>
|
||||
<Button onClick={() => window.location.href = '/'}>
|
||||
返回首頁
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Get unread notification count
|
||||
const unreadCount = notifications.filter((n) => !n.read).length
|
||||
|
||||
@@ -207,13 +177,15 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
|
||||
setShowLogoutDialog(false)
|
||||
|
||||
// Check if this is a popup/new tab opened from main site
|
||||
if (typeof window !== 'undefined' && window.opener && !window.opener.closed) {
|
||||
// If opened from another window, close this tab and focus parent
|
||||
window.opener.focus()
|
||||
window.close()
|
||||
} else {
|
||||
// If this is the main window or standalone, redirect to homepage
|
||||
window.location.href = "/"
|
||||
if (isClient) {
|
||||
if (window.opener && !window.opener.closed) {
|
||||
// If opened from another window, close this tab and focus parent
|
||||
window.opener.focus()
|
||||
window.close()
|
||||
} else {
|
||||
// If this is the main window or standalone, redirect to homepage
|
||||
window.location.href = "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +208,19 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
|
||||
}
|
||||
}
|
||||
|
||||
// 如果還在載入中,顯示載入畫面
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="text-gray-600">載入中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 檢查用戶權限
|
||||
if (!user || user.role !== "admin") {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
@@ -246,11 +231,20 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">存取被拒</h2>
|
||||
<p className="text-gray-600 mb-4">您沒有管理員權限訪問此頁面</p>
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<div className="text-xs text-gray-500 mb-4">
|
||||
調試信息: 用戶={user ? '已登入' : '未登入'}, 角色={user?.role || '無'}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-x-3">
|
||||
<Button onClick={() => (window.location.href = "/")} variant="outline">
|
||||
<Button onClick={() => {
|
||||
if (isClient) {
|
||||
window.location.href = "/"
|
||||
}
|
||||
}} variant="outline">
|
||||
返回首頁
|
||||
</Button>
|
||||
{typeof window !== 'undefined' && window.opener && !window.opener.closed && (
|
||||
{isClient && window.opener && !window.opener.closed && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.opener.focus()
|
||||
|
@@ -15,7 +15,7 @@ export function AdminPanel() {
|
||||
const renderPage = () => {
|
||||
switch (currentPage) {
|
||||
case "dashboard":
|
||||
return <AdminDashboard onPageChange={setCurrentPage} />
|
||||
return <AdminDashboard />
|
||||
case "users":
|
||||
return <UserManagement />
|
||||
case "apps":
|
||||
@@ -27,7 +27,7 @@ export function AdminPanel() {
|
||||
case "settings":
|
||||
return <SystemSettings />
|
||||
default:
|
||||
return <AdminDashboard onPageChange={setCurrentPage} />
|
||||
return <AdminDashboard />
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -20,19 +20,9 @@ const recentActivities: any[] = []
|
||||
|
||||
const topApps: any[] = []
|
||||
|
||||
interface AdminDashboardProps {
|
||||
onPageChange?: (page: string) => void
|
||||
}
|
||||
|
||||
export function AdminDashboard({ onPageChange }: AdminDashboardProps) {
|
||||
export function AdminDashboard() {
|
||||
const { competitions } = useCompetition()
|
||||
|
||||
const handleManageUsers = () => {
|
||||
if (onPageChange) {
|
||||
onPageChange("users")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Welcome Section */}
|
||||
@@ -160,7 +150,7 @@ export function AdminDashboard({ onPageChange }: AdminDashboardProps) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Button className="h-20 flex flex-col space-y-2" onClick={handleManageUsers}>
|
||||
<Button className="h-20 flex flex-col space-y-2">
|
||||
<Users className="w-6 h-6" />
|
||||
<span>管理用戶</span>
|
||||
</Button>
|
||||
|
@@ -18,6 +18,9 @@ import {
|
||||
Users,
|
||||
Bell,
|
||||
Save,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Lock,
|
||||
TestTube,
|
||||
CheckCircle,
|
||||
HardDrive,
|
||||
@@ -67,6 +70,7 @@ export function SystemSettings() {
|
||||
|
||||
const [activeTab, setActiveTab] = useState("general")
|
||||
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle")
|
||||
const [showSmtpPassword, setShowSmtpPassword] = useState(false)
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaveStatus("saving")
|
||||
@@ -78,8 +82,8 @@ export function SystemSettings() {
|
||||
}
|
||||
|
||||
const handleTestEmail = () => {
|
||||
// 測試郵件功能 - 僅用於開發測試
|
||||
console.log("測試郵件功能")
|
||||
// 測試郵件功能
|
||||
alert("測試郵件已發送!")
|
||||
}
|
||||
|
||||
const updateSetting = (key: string, value: any) => {
|
||||
@@ -286,12 +290,23 @@ export function SystemSettings() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="smtpPassword">SMTP 密碼</Label>
|
||||
<Input
|
||||
id="smtpPassword"
|
||||
type="password"
|
||||
value={settings.smtpPassword}
|
||||
onChange={(e) => updateSetting("smtpPassword", e.target.value)}
|
||||
/>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
id="smtpPassword"
|
||||
type={showSmtpPassword ? "text" : "password"}
|
||||
value={settings.smtpPassword}
|
||||
onChange={(e) => updateSetting("smtpPassword", e.target.value)}
|
||||
className="pl-10 pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSmtpPassword(!showSmtpPassword)}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showSmtpPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -13,7 +13,6 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination"
|
||||
import {
|
||||
Search,
|
||||
MoreHorizontal,
|
||||
@@ -39,11 +38,8 @@ import {
|
||||
Users,
|
||||
} from "lucide-react"
|
||||
|
||||
// User data - empty for production
|
||||
const initialMockUsers: any[] = []
|
||||
|
||||
export function UserManagement() {
|
||||
const [users, setUsers] = useState(initialMockUsers)
|
||||
const [users, setUsers] = useState<any[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [selectedDepartment, setSelectedDepartment] = useState("all")
|
||||
const [selectedRole, setSelectedRole] = useState("all")
|
||||
@@ -53,94 +49,70 @@ export function UserManagement() {
|
||||
const [showInviteUser, setShowInviteUser] = useState(false)
|
||||
const [showEditUser, setShowEditUser] = useState(false)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const [isClient, setIsClient] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [stats, setStats] = useState({
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
adminCount: 0,
|
||||
developerCount: 0,
|
||||
inactiveUsers: 0,
|
||||
newThisMonth: 0
|
||||
})
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
})
|
||||
|
||||
// Set client state after hydration
|
||||
useEffect(() => {
|
||||
setIsClient(true)
|
||||
}, [])
|
||||
|
||||
// 載入用戶數據
|
||||
const loadUsers = async () => {
|
||||
if (!isClient) return
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: pagination.page.toString(),
|
||||
limit: pagination.limit.toString(),
|
||||
search: searchTerm,
|
||||
department: selectedDepartment,
|
||||
role: selectedRole,
|
||||
status: selectedStatus
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/admin/users?${params}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setUsers(data.data.users)
|
||||
setStats(data.data.stats)
|
||||
setPagination(data.data.pagination)
|
||||
} else {
|
||||
console.error('載入用戶失敗:', data.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入用戶錯誤:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 當篩選條件改變時重新載入
|
||||
useEffect(() => {
|
||||
loadUsers()
|
||||
}, [isClient, searchTerm, selectedDepartment, selectedRole, selectedStatus, pagination.page])
|
||||
|
||||
const [showInvitationLink, setShowInvitationLink] = useState(false)
|
||||
const [userToDelete, setUserToDelete] = useState<any>(null)
|
||||
const [generatedInvitation, setGeneratedInvitation] = useState<any>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [success, setSuccess] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [stats, setStats] = useState({
|
||||
total: 0,
|
||||
admin: 0,
|
||||
developer: 0,
|
||||
user: 0,
|
||||
today: 0,
|
||||
totalApps: 0,
|
||||
totalReviews: 0
|
||||
})
|
||||
|
||||
// Pagination state
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [totalUsers, setTotalUsers] = useState(0)
|
||||
const [itemsPerPage] = useState(10) // Default to 10 items per page
|
||||
|
||||
// 載入用戶資料
|
||||
useEffect(() => {
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
|
||||
// 獲取用戶列表 with pagination
|
||||
const usersResponse = await fetch(`/api/users?page=${currentPage}&limit=${itemsPerPage}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
if (usersResponse.ok) {
|
||||
const usersData = await usersResponse.json()
|
||||
setUsers(usersData.users || [])
|
||||
setTotalPages(usersData.pagination?.totalPages || 1)
|
||||
setTotalUsers(usersData.pagination?.total || 0)
|
||||
} else {
|
||||
const errorData = await usersResponse.json().catch(() => ({}))
|
||||
console.error('獲取用戶列表失敗:', errorData.error || usersResponse.statusText)
|
||||
setError(errorData.error || '獲取用戶列表失敗')
|
||||
}
|
||||
|
||||
// 獲取統計資料
|
||||
const statsResponse = await fetch('/api/users/stats', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
if (statsResponse.ok) {
|
||||
const statsData = await statsResponse.json()
|
||||
setStats(statsData)
|
||||
} else {
|
||||
const errorData = await statsResponse.json().catch(() => ({}))
|
||||
console.error('獲取統計資料失敗:', errorData.error || statsResponse.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入用戶資料失敗:', error)
|
||||
setError('載入用戶資料失敗')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchUsers()
|
||||
}, [currentPage, itemsPerPage]) // Re-fetch when page changes
|
||||
|
||||
// 重新獲取統計數據的函數
|
||||
const refreshStats = async () => {
|
||||
try {
|
||||
const statsResponse = await fetch('/api/users/stats', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
if (statsResponse.ok) {
|
||||
const statsData = await statsResponse.json()
|
||||
setStats(statsData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新獲取統計資料失敗:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 邀請用戶表單狀態 - 包含電子郵件和預設角色
|
||||
const [inviteEmail, setInviteEmail] = useState("")
|
||||
@@ -156,65 +128,11 @@ export function UserManagement() {
|
||||
status: "",
|
||||
})
|
||||
|
||||
const filteredUsers = users.filter((user) => {
|
||||
const matchesSearch =
|
||||
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesDepartment = selectedDepartment === "all" || user.department === selectedDepartment
|
||||
const matchesRole =
|
||||
selectedRole === "all" ||
|
||||
user.role === selectedRole ||
|
||||
(user.status === "invited" && (user as any).invitedRole === selectedRole)
|
||||
const matchesStatus = selectedStatus === "all" || user.status === selectedStatus
|
||||
// 篩選現在由 API 處理,不需要前端篩選
|
||||
|
||||
return matchesSearch && matchesDepartment && matchesRole && matchesStatus
|
||||
})
|
||||
|
||||
const handleViewUser = async (user: any) => {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/users/${user.id}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const userData = await response.json()
|
||||
|
||||
// 獲取用戶活動記錄
|
||||
const activityResponse = await fetch(`/api/users/${user.id}/activity`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
let activityData = []
|
||||
if (activityResponse.ok) {
|
||||
activityData = await activityResponse.json()
|
||||
}
|
||||
|
||||
// 合併用戶資料和活動記錄
|
||||
const userWithActivity = {
|
||||
...userData,
|
||||
activities: activityData
|
||||
}
|
||||
|
||||
setSelectedUser(userWithActivity)
|
||||
setShowUserDetail(true)
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.error || "獲取用戶詳情失敗")
|
||||
setTimeout(() => setError(""), 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching user details:', error)
|
||||
setError("獲取用戶詳情失敗")
|
||||
setTimeout(() => setError(""), 3000)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
const handleViewUser = (user: any) => {
|
||||
setSelectedUser(user)
|
||||
setShowUserDetail(true)
|
||||
}
|
||||
|
||||
const handleEditUser = (user: any) => {
|
||||
@@ -237,39 +155,18 @@ export function UserManagement() {
|
||||
const handleToggleUserStatus = async (userId: string) => {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const newStatus = users.find(user => user.id === userId)?.status === "active" ? "inactive" : "active"
|
||||
|
||||
const response = await fetch(`/api/users/${userId}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify({ status: newStatus })
|
||||
})
|
||||
// 模擬 API 調用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
if (response.ok) {
|
||||
setUsers(
|
||||
users.map((user) =>
|
||||
user.id === userId ? { ...user, status: newStatus } : user,
|
||||
),
|
||||
)
|
||||
setSuccess("用戶狀態更新成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
refreshStats() // 更新統計數據
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.error || "更新用戶狀態失敗")
|
||||
setTimeout(() => setError(""), 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user status:', error)
|
||||
setError("更新用戶狀態失敗")
|
||||
setTimeout(() => setError(""), 3000)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
setUsers(
|
||||
users.map((user) =>
|
||||
user.id === userId ? { ...user, status: user.status === "active" ? "inactive" : "active" } : user,
|
||||
),
|
||||
)
|
||||
|
||||
setIsLoading(false)
|
||||
setSuccess("用戶狀態更新成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
const handleChangeUserRole = async (userId: string, newRole: string) => {
|
||||
@@ -283,7 +180,6 @@ export function UserManagement() {
|
||||
setIsLoading(false)
|
||||
setSuccess(`用戶權限已更新為${getRoleText(newRole)}!`)
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
refreshStats() // 更新統計數據
|
||||
}
|
||||
|
||||
const handleGenerateInvitation = async () => {
|
||||
@@ -310,43 +206,62 @@ export function UserManagement() {
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
// 模擬生成邀請連結
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
try {
|
||||
const response = await fetch('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: inviteEmail,
|
||||
role: inviteRole
|
||||
})
|
||||
})
|
||||
|
||||
// 生成邀請 token(實際應用中會由後端生成)
|
||||
const invitationToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
||||
const invitationLink = `${window.location.origin}/register?token=${invitationToken}&email=${encodeURIComponent(inviteEmail)}&role=${inviteRole}`
|
||||
const data = await response.json()
|
||||
|
||||
const newInvitation = {
|
||||
id: Date.now().toString(),
|
||||
name: "",
|
||||
email: inviteEmail,
|
||||
department: "",
|
||||
role: "",
|
||||
status: "invited",
|
||||
joinDate: "",
|
||||
lastLogin: "",
|
||||
totalApps: 0,
|
||||
totalReviews: 0,
|
||||
totalLikes: 0,
|
||||
invitationSentAt: new Date().toLocaleString("zh-TW", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
invitationLink: invitationLink,
|
||||
invitedRole: inviteRole, // 記錄邀請時的預設角色
|
||||
if (data.success) {
|
||||
const newInvitation = {
|
||||
id: Date.now().toString(),
|
||||
name: "",
|
||||
email: inviteEmail,
|
||||
department: "",
|
||||
role: "",
|
||||
status: "invited",
|
||||
joinDate: "",
|
||||
lastLogin: "",
|
||||
totalApps: 0,
|
||||
totalReviews: 0,
|
||||
totalLikes: 0,
|
||||
invitationSentAt: new Date().toLocaleString("zh-TW", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
invitationLink: data.data.invitationLink,
|
||||
invitedRole: inviteRole, // 記錄邀請時的預設角色
|
||||
}
|
||||
|
||||
setGeneratedInvitation(newInvitation)
|
||||
setInviteEmail("")
|
||||
setInviteRole("user")
|
||||
setShowInviteUser(false)
|
||||
setShowInvitationLink(true)
|
||||
setSuccess("邀請連結已生成")
|
||||
|
||||
// 重新載入用戶列表
|
||||
loadUsers()
|
||||
} else {
|
||||
setError(data.error || "生成邀請連結失敗")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('邀請用戶錯誤:', error)
|
||||
setError("生成邀請連結時發生錯誤")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
setUsers([...users, newInvitation])
|
||||
setGeneratedInvitation(newInvitation)
|
||||
setInviteEmail("")
|
||||
setInviteRole("user")
|
||||
setIsLoading(false)
|
||||
setShowInviteUser(false)
|
||||
setShowInvitationLink(true)
|
||||
}
|
||||
|
||||
const handleCopyInvitationLink = async (link: string) => {
|
||||
@@ -369,7 +284,9 @@ export function UserManagement() {
|
||||
const newToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
||||
const user = users.find((u) => u.id === userId)
|
||||
const role = (user as any)?.invitedRole || "user"
|
||||
const newInvitationLink = `${window.location.origin}/register?token=${newToken}&email=${encodeURIComponent(email)}&role=${role}`
|
||||
const newInvitationLink = isClient
|
||||
? `${window.location.origin}/register?token=${newToken}&email=${encodeURIComponent(email)}&role=${role}`
|
||||
: `/register?token=${newToken}&email=${encodeURIComponent(email)}&role=${role}`
|
||||
|
||||
setUsers(
|
||||
users.map((user) =>
|
||||
@@ -402,42 +319,23 @@ export function UserManagement() {
|
||||
return
|
||||
}
|
||||
|
||||
// 檢查電子郵件是否被其他用戶使用
|
||||
if (users.some((user) => user.email === editUser.email && user.id !== editUser.id)) {
|
||||
setError("此電子郵件已被其他用戶使用")
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/users/${editUser.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: editUser.name,
|
||||
email: editUser.email,
|
||||
department: editUser.department,
|
||||
role: editUser.role
|
||||
})
|
||||
})
|
||||
// 模擬 API 調用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
|
||||
setShowEditUser(false)
|
||||
setSuccess("用戶資料更新成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
refreshStats() // 更新統計數據
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.error || "更新用戶資料失敗")
|
||||
setTimeout(() => setError(""), 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error)
|
||||
setError("更新用戶資料失敗")
|
||||
setTimeout(() => setError(""), 3000)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
|
||||
|
||||
setIsLoading(false)
|
||||
setShowEditUser(false)
|
||||
setSuccess("用戶資料更新成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
const confirmDeleteUser = async () => {
|
||||
@@ -445,33 +343,16 @@ export function UserManagement() {
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userToDelete.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
// 模擬 API 調用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
if (response.ok) {
|
||||
setUsers(users.filter((user) => user.id !== userToDelete.id))
|
||||
setShowDeleteConfirm(false)
|
||||
setUserToDelete(null)
|
||||
setSuccess("用戶刪除成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
refreshStats() // 更新統計數據
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.error || "刪除用戶失敗")
|
||||
setTimeout(() => setError(""), 3000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error)
|
||||
setError("刪除用戶失敗")
|
||||
setTimeout(() => setError(""), 3000)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
setUsers(users.filter((user) => user.id !== userToDelete.id))
|
||||
|
||||
setIsLoading(false)
|
||||
setShowDeleteConfirm(false)
|
||||
setUserToDelete(null)
|
||||
setSuccess("用戶刪除成功!")
|
||||
setTimeout(() => setSuccess(""), 3000)
|
||||
}
|
||||
|
||||
const getRoleColor = (role: string) => {
|
||||
@@ -572,13 +453,13 @@ export function UserManagement() {
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-8 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">總用戶數</p>
|
||||
<p className="text-2xl font-bold">{stats.total}</p>
|
||||
<p className="text-2xl font-bold">{stats.totalUsers}</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<Users className="w-4 h-4 text-blue-600" />
|
||||
@@ -592,7 +473,7 @@ export function UserManagement() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">活躍用戶</p>
|
||||
<p className="text-2xl font-bold">{stats.total}</p>
|
||||
<p className="text-2xl font-bold">{stats.activeUsers}</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Activity className="w-4 h-4 text-green-600" />
|
||||
@@ -606,7 +487,7 @@ export function UserManagement() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">管理員</p>
|
||||
<p className="text-2xl font-bold">{stats.admin}</p>
|
||||
<p className="text-2xl font-bold">{stats.adminCount}</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<Shield className="w-4 h-4 text-purple-600" />
|
||||
@@ -620,7 +501,7 @@ export function UserManagement() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">開發者</p>
|
||||
<p className="text-2xl font-bold">{stats.developer}</p>
|
||||
<p className="text-2xl font-bold">{stats.developerCount}</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Code className="w-4 h-4 text-green-600" />
|
||||
@@ -633,11 +514,11 @@ export function UserManagement() {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">一般用戶</p>
|
||||
<p className="text-2xl font-bold">{stats.user}</p>
|
||||
<p className="text-sm text-gray-600">待註冊</p>
|
||||
<p className="text-2xl font-bold">{stats.inactiveUsers}</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-yellow-600" />
|
||||
<Clock className="w-4 h-4 text-yellow-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -647,8 +528,8 @@ export function UserManagement() {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">今日新增</p>
|
||||
<p className="text-2xl font-bold">{stats.today}</p>
|
||||
<p className="text-sm text-gray-600">本月新增</p>
|
||||
<p className="text-2xl font-bold">{stats.newThisMonth}</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<UserPlus className="w-4 h-4 text-orange-600" />
|
||||
@@ -656,34 +537,6 @@ export function UserManagement() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">應用</p>
|
||||
<p className="text-2xl font-bold">{stats.totalApps}</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<Code className="w-4 h-4 text-indigo-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">評價</p>
|
||||
<p className="text-2xl font-bold">{stats.totalReviews}</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-pink-100 rounded-full flex items-center justify-center">
|
||||
<Activity className="w-4 h-4 text-pink-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
@@ -744,40 +597,25 @@ export function UserManagement() {
|
||||
{/* Users Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>用戶列表 ({totalUsers} 總用戶)</CardTitle>
|
||||
<CardDescription>
|
||||
管理所有平台用戶 - 第 {currentPage} 頁,共 {totalPages} 頁
|
||||
{totalPages > 1 && ` (每頁 ${itemsPerPage} 筆)`}
|
||||
</CardDescription>
|
||||
<CardTitle>用戶列表 ({pagination.total})</CardTitle>
|
||||
<CardDescription>管理所有平台用戶</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
|
||||
<span className="ml-2 text-gray-600">載入用戶資料中...</span>
|
||||
</div>
|
||||
) : filteredUsers.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Users className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600">沒有找到用戶</p>
|
||||
<p className="text-sm text-gray-500 mt-1">嘗試調整搜尋條件或篩選器</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用戶</TableHead>
|
||||
<TableHead>部門</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>狀態</TableHead>
|
||||
<TableHead>加入日期</TableHead>
|
||||
<TableHead>最後登入</TableHead>
|
||||
<TableHead>統計</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.map((user) => (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用戶</TableHead>
|
||||
<TableHead>部門</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>狀態</TableHead>
|
||||
<TableHead>加入日期</TableHead>
|
||||
<TableHead>最後登入</TableHead>
|
||||
<TableHead>統計</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-3">
|
||||
@@ -868,70 +706,9 @@ export function UserManagement() {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center items-center py-4">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
if (currentPage > 1) setCurrentPage(currentPage - 1)
|
||||
}}
|
||||
className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{/* Page numbers */}
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
let pageNum
|
||||
if (totalPages <= 5) {
|
||||
pageNum = i + 1
|
||||
} else if (currentPage <= 3) {
|
||||
pageNum = i + 1
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
pageNum = totalPages - 4 + i
|
||||
} else {
|
||||
pageNum = currentPage - 2 + i
|
||||
}
|
||||
|
||||
return (
|
||||
<PaginationItem key={pageNum}>
|
||||
<PaginationLink
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage(pageNum)
|
||||
}}
|
||||
isActive={currentPage === pageNum}
|
||||
>
|
||||
{pageNum}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
})}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
if (currentPage < totalPages) setCurrentPage(currentPage + 1)
|
||||
}}
|
||||
className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Invite User Dialog - 包含角色選擇 */}
|
||||
<Dialog open={showInviteUser} onOpenChange={setShowInviteUser}>
|
||||
<DialogContent className="max-w-md">
|
||||
@@ -1106,7 +883,7 @@ export function UserManagement() {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<Button variant="outline" onClick={() => window.open(generatedInvitation.invitationLink, "_blank")}>
|
||||
<Button variant="outline" onClick={() => isClient && window.open(generatedInvitation.invitationLink, "_blank")}>
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
預覽連結
|
||||
</Button>
|
||||
@@ -1362,32 +1139,22 @@ export function UserManagement() {
|
||||
<Clock className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">用戶尚未註冊,暫無活動記錄</p>
|
||||
</div>
|
||||
) : selectedUser.activities && selectedUser.activities.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{selectedUser.activities.map((activity: any, index: number) => (
|
||||
<div key={index} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
|
||||
{activity.type === 'login' ? (
|
||||
<Calendar className="w-4 h-4 text-blue-600" />
|
||||
) : activity.type === 'view_app' ? (
|
||||
<Eye className="w-4 h-4 text-green-600" />
|
||||
) : activity.type === 'create_app' ? (
|
||||
<Code className="w-4 h-4 text-purple-600" />
|
||||
) : activity.type === 'review' ? (
|
||||
<Activity className="w-4 h-4 text-orange-600" />
|
||||
) : (
|
||||
<Activity className="w-4 h-4 text-gray-600" />
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium">{activity.description}</p>
|
||||
<p className="text-xs text-gray-500">{activity.timestamp}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<Activity className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">暫無活動記錄</p>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
@@ -1422,7 +1189,7 @@ export function UserManagement() {
|
||||
<CardContent className="p-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-orange-600">
|
||||
{selectedUser.status === "invited" ? 0 : (selectedUser.loginDays || 0)}
|
||||
{selectedUser.status === "invited" ? 0 : 15}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">登入天數</p>
|
||||
</div>
|
||||
|
@@ -121,107 +121,33 @@ export function AppSubmissionDialog({ open, onOpenChange }: AppSubmissionDialogP
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!user) {
|
||||
console.error('用戶未登入')
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
// 準備應用程式資料
|
||||
const appData = {
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
type: mapTypeToApiType(formData.type),
|
||||
demoUrl: formData.appUrl || undefined,
|
||||
githubUrl: formData.sourceCodeUrl || undefined,
|
||||
docsUrl: formData.documentation || undefined,
|
||||
techStack: formData.technicalDetails ? [formData.technicalDetails] : undefined,
|
||||
tags: formData.features ? [formData.features] : undefined,
|
||||
version: '1.0.0'
|
||||
}
|
||||
// 模擬提交過程
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// 調用 API 創建應用程式
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await fetch('/api/apps', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(appData)
|
||||
setIsSubmitting(false)
|
||||
setIsSubmitted(true)
|
||||
|
||||
// 3秒後關閉對話框
|
||||
setTimeout(() => {
|
||||
onOpenChange(false)
|
||||
setIsSubmitted(false)
|
||||
setStep(1)
|
||||
setFormData({
|
||||
name: "",
|
||||
type: "文字處理",
|
||||
description: "",
|
||||
appUrl: "",
|
||||
demoFile: null,
|
||||
sourceCodeUrl: "",
|
||||
documentation: "",
|
||||
features: "",
|
||||
technicalDetails: "",
|
||||
requestFeatured: false,
|
||||
agreeTerms: false,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || '創建應用程式失敗')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
console.log('應用程式創建成功:', result)
|
||||
|
||||
setIsSubmitting(false)
|
||||
setIsSubmitted(true)
|
||||
|
||||
// 3秒後關閉對話框
|
||||
setTimeout(() => {
|
||||
onOpenChange(false)
|
||||
setIsSubmitted(false)
|
||||
setStep(1)
|
||||
setFormData({
|
||||
name: "",
|
||||
type: "文字處理",
|
||||
description: "",
|
||||
appUrl: "",
|
||||
demoFile: null,
|
||||
sourceCodeUrl: "",
|
||||
documentation: "",
|
||||
features: "",
|
||||
technicalDetails: "",
|
||||
requestFeatured: false,
|
||||
agreeTerms: false,
|
||||
})
|
||||
}, 3000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('創建應用程式失敗:', error)
|
||||
setIsSubmitting(false)
|
||||
// 這裡可以添加錯誤提示
|
||||
alert(`創建應用程式失敗: ${error instanceof Error ? error.message : '未知錯誤'}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 將前端類型映射到 API 類型
|
||||
const mapTypeToApiType = (frontendType: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'文字處理': 'productivity',
|
||||
'圖像生成': 'ai_model',
|
||||
'圖像處理': 'ai_model',
|
||||
'語音辨識': 'ai_model',
|
||||
'推薦系統': 'ai_model',
|
||||
'音樂生成': 'ai_model',
|
||||
'程式開發': 'automation',
|
||||
'影像處理': 'ai_model',
|
||||
'對話系統': 'ai_model',
|
||||
'數據分析': 'data_analysis',
|
||||
'設計工具': 'productivity',
|
||||
'語音技術': 'ai_model',
|
||||
'教育工具': 'educational',
|
||||
'健康醫療': 'healthcare',
|
||||
'金融科技': 'finance',
|
||||
'物聯網': 'iot_device',
|
||||
'區塊鏈': 'blockchain',
|
||||
'AR/VR': 'ar_vr',
|
||||
'機器學習': 'machine_learning',
|
||||
'電腦視覺': 'computer_vision',
|
||||
'自然語言處理': 'nlp',
|
||||
'機器人': 'robotics',
|
||||
'網路安全': 'cybersecurity',
|
||||
'雲端服務': 'cloud_service',
|
||||
'其他': 'other'
|
||||
}
|
||||
return typeMap[frontendType] || 'other'
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const isStep1Valid = formData.name && formData.description && formData.appUrl
|
||||
@@ -319,28 +245,9 @@ export function AppSubmissionDialog({ open, onOpenChange }: AppSubmissionDialogP
|
||||
<SelectContent>
|
||||
<SelectItem value="文字處理">文字處理</SelectItem>
|
||||
<SelectItem value="圖像生成">圖像生成</SelectItem>
|
||||
<SelectItem value="圖像處理">圖像處理</SelectItem>
|
||||
<SelectItem value="語音辨識">語音辨識</SelectItem>
|
||||
<SelectItem value="推薦系統">推薦系統</SelectItem>
|
||||
<SelectItem value="音樂生成">音樂生成</SelectItem>
|
||||
<SelectItem value="程式開發">程式開發</SelectItem>
|
||||
<SelectItem value="影像處理">影像處理</SelectItem>
|
||||
<SelectItem value="對話系統">對話系統</SelectItem>
|
||||
<SelectItem value="數據分析">數據分析</SelectItem>
|
||||
<SelectItem value="設計工具">設計工具</SelectItem>
|
||||
<SelectItem value="語音技術">語音技術</SelectItem>
|
||||
<SelectItem value="教育工具">教育工具</SelectItem>
|
||||
<SelectItem value="健康醫療">健康醫療</SelectItem>
|
||||
<SelectItem value="金融科技">金融科技</SelectItem>
|
||||
<SelectItem value="物聯網">物聯網</SelectItem>
|
||||
<SelectItem value="區塊鏈">區塊鏈</SelectItem>
|
||||
<SelectItem value="AR/VR">AR/VR</SelectItem>
|
||||
<SelectItem value="機器學習">機器學習</SelectItem>
|
||||
<SelectItem value="電腦視覺">電腦視覺</SelectItem>
|
||||
<SelectItem value="自然語言處理">自然語言處理</SelectItem>
|
||||
<SelectItem value="機器人">機器人</SelectItem>
|
||||
<SelectItem value="網路安全">網路安全</SelectItem>
|
||||
<SelectItem value="雲端服務">雲端服務</SelectItem>
|
||||
<SelectItem value="其他">其他</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
@@ -6,24 +6,134 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { BarChart3, Clock, Heart, ImageIcon, MessageSquare, FileText, TrendingUp } from "lucide-react"
|
||||
import { BarChart3, Clock, Heart, ImageIcon, MessageSquare, FileText, TrendingUp, Trash2, RefreshCw } from "lucide-react"
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
interface ActivityRecordsDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
// Recent apps data - empty for production
|
||||
const recentApps: any[] = []
|
||||
// Mock data for demonstration - will be replaced with real data
|
||||
const mockRecentApps: any[] = []
|
||||
|
||||
// Category data - empty for production
|
||||
const categoryData: any[] = []
|
||||
const mockCategoryData: any[] = []
|
||||
|
||||
export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDialogProps) {
|
||||
const { user } = useAuth()
|
||||
const {
|
||||
user,
|
||||
getViewCount,
|
||||
getAppLikes,
|
||||
getUserLikeHistory,
|
||||
getAppLikesInPeriod
|
||||
} = useAuth()
|
||||
|
||||
const [recentApps, setRecentApps] = useState<any[]>([])
|
||||
const [categoryData, setCategoryData] = useState<any[]>([])
|
||||
const [isResetting, setIsResetting] = useState(false)
|
||||
|
||||
if (!user) return null
|
||||
|
||||
// Calculate user statistics
|
||||
const calculateUserStats = () => {
|
||||
if (!user) return {
|
||||
totalUsage: 0,
|
||||
totalDuration: 0,
|
||||
favoriteApps: 0,
|
||||
daysJoined: 0
|
||||
}
|
||||
|
||||
// Calculate total usage count (views)
|
||||
const totalUsage = Object.values(user.recentApps || []).length
|
||||
|
||||
// Calculate total duration (simplified - 5 minutes per app view)
|
||||
const totalDuration = totalUsage * 5 // minutes
|
||||
|
||||
// Get favorite apps count
|
||||
const favoriteApps = user.favoriteApps?.length || 0
|
||||
|
||||
// Calculate days joined
|
||||
const joinDate = new Date(user.joinDate)
|
||||
const now = new Date()
|
||||
|
||||
// Check if joinDate is valid
|
||||
let daysJoined = 0
|
||||
if (!isNaN(joinDate.getTime())) {
|
||||
daysJoined = Math.floor((now.getTime() - joinDate.getTime()) / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
return {
|
||||
totalUsage,
|
||||
totalDuration,
|
||||
favoriteApps,
|
||||
daysJoined: Math.max(0, daysJoined)
|
||||
}
|
||||
}
|
||||
|
||||
const stats = calculateUserStats()
|
||||
|
||||
// Load recent apps from user's recent apps
|
||||
useEffect(() => {
|
||||
if (user?.recentApps) {
|
||||
// Convert recent app IDs to app objects (simplified)
|
||||
const recentAppsData = user.recentApps.slice(0, 10).map((appId, index) => ({
|
||||
id: appId,
|
||||
name: `應用 ${appId}`,
|
||||
author: "系統",
|
||||
category: "AI應用",
|
||||
usageCount: getViewCount(appId),
|
||||
timeSpent: "5分鐘",
|
||||
lastUsed: `${index + 1}天前`,
|
||||
icon: MessageSquare,
|
||||
color: "bg-blue-500"
|
||||
}))
|
||||
setRecentApps(recentAppsData)
|
||||
} else {
|
||||
setRecentApps([])
|
||||
}
|
||||
}, [user, getViewCount])
|
||||
|
||||
// Load category data (simplified)
|
||||
useEffect(() => {
|
||||
// This would normally be calculated from actual usage data
|
||||
setCategoryData([])
|
||||
}, [user])
|
||||
|
||||
// Reset user activity data
|
||||
const resetActivityData = async () => {
|
||||
setIsResetting(true)
|
||||
|
||||
try {
|
||||
// Clear localStorage data
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem("appViews")
|
||||
localStorage.removeItem("appLikes")
|
||||
localStorage.removeItem("userLikes")
|
||||
localStorage.removeItem("appLikesOld")
|
||||
localStorage.removeItem("appRatings")
|
||||
}
|
||||
|
||||
// Reset user's recent apps and favorites
|
||||
if (user) {
|
||||
const updatedUser = {
|
||||
...user,
|
||||
recentApps: [],
|
||||
favoriteApps: [],
|
||||
totalLikes: 0,
|
||||
totalViews: 0
|
||||
}
|
||||
localStorage.setItem("user", JSON.stringify(updatedUser))
|
||||
|
||||
// Reload the page to refresh all data
|
||||
window.location.reload()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error resetting activity data:", error)
|
||||
} finally {
|
||||
setIsResetting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-5xl max-h-[85vh] overflow-hidden">
|
||||
@@ -45,52 +155,86 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
|
||||
<h3 className="text-lg font-semibold mb-2">最近使用的應用</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">您最近體驗過的 AI 應用</p>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{recentApps.map((app) => {
|
||||
const IconComponent = app.icon
|
||||
return (
|
||||
<Card key={app.id} className="hover:shadow-md transition-shadow">
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`p-3 rounded-lg ${app.color}`}>
|
||||
<IconComponent className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">{app.name}</h4>
|
||||
<p className="text-sm text-muted-foreground">by {app.author}</p>
|
||||
<div className="flex items-center gap-4 mt-1">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{app.category}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<BarChart3 className="w-3 h-3" />
|
||||
使用 {app.usageCount} 次
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{app.timeSpent}
|
||||
</span>
|
||||
{recentApps.length > 0 ? (
|
||||
<div className="grid gap-4">
|
||||
{recentApps.map((app) => {
|
||||
const IconComponent = app.icon
|
||||
return (
|
||||
<Card key={app.id} className="hover:shadow-md transition-shadow">
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`p-3 rounded-lg ${app.color}`}>
|
||||
<IconComponent className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">{app.name}</h4>
|
||||
<p className="text-sm text-muted-foreground">by {app.author}</p>
|
||||
<div className="flex items-center gap-4 mt-1">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{app.category}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<BarChart3 className="w-3 h-3" />
|
||||
使用 {app.usageCount} 次
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{app.timeSpent}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-muted-foreground mb-2">{app.lastUsed}</p>
|
||||
<Button size="sm" variant="outline">
|
||||
再次體驗
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-muted-foreground mb-2">{app.lastUsed}</p>
|
||||
<Button size="sm" variant="outline">
|
||||
再次體驗
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="text-center py-12">
|
||||
<MessageSquare className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-gray-600 mb-2">尚未使用任何應用</h3>
|
||||
<p className="text-gray-500 mb-4">開始探索平台上的 AI 應用,您的使用記錄將顯示在這裡</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
開始探索
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="statistics" className="space-y-6 max-h-[60vh] overflow-y-auto">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">個人統計</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">您在平台上的活動概覽</p>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">個人統計</h3>
|
||||
<p className="text-sm text-muted-foreground">您在平台上的活動概覽</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={resetActivityData}
|
||||
disabled={isResetting}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
{isResetting ? (
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{isResetting ? "重置中..." : "清空數據"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
@@ -100,8 +244,10 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">41</div>
|
||||
<p className="text-xs text-muted-foreground">比上週增加 12%</p>
|
||||
<div className="text-2xl font-bold">{isNaN(stats.totalUsage) ? 0 : stats.totalUsage}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(isNaN(stats.totalUsage) ? 0 : stats.totalUsage) > 0 ? "累計使用" : "尚未使用任何應用"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -111,8 +257,16 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">9.2小時</div>
|
||||
<p className="text-xs text-muted-foreground">本月累計</p>
|
||||
<div className="text-2xl font-bold">
|
||||
{isNaN(stats.totalDuration) ? "0分鐘" : (
|
||||
stats.totalDuration >= 60
|
||||
? `${(stats.totalDuration / 60).toFixed(1)}小時`
|
||||
: `${stats.totalDuration}分鐘`
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(isNaN(stats.totalDuration) ? 0 : stats.totalDuration) > 0 ? "累計時長" : "尚未開始使用"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -122,8 +276,10 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
|
||||
<Heart className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">6</div>
|
||||
<p className="text-xs text-muted-foreground">個人收藏</p>
|
||||
<div className="text-2xl font-bold">{isNaN(stats.favoriteApps) ? 0 : stats.favoriteApps}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(isNaN(stats.favoriteApps) ? 0 : stats.favoriteApps) > 0 ? "個人收藏" : "尚未收藏任何應用"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -133,7 +289,10 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
|
||||
<CardDescription>天</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">0</div>
|
||||
<div className="text-2xl font-bold">{isNaN(stats.daysJoined) ? 0 : stats.daysJoined}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(isNaN(stats.daysJoined) ? 0 : stats.daysJoined) > 0 ? "已加入平台" : "今天剛加入"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -145,18 +304,26 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
|
||||
<CardDescription>根據您的使用頻率統計的應用類別分布</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{categoryData.map((category, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${category.color}`} />
|
||||
<span className="font-medium">{category.name}</span>
|
||||
{categoryData.length > 0 ? (
|
||||
categoryData.map((category, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${category.color}`} />
|
||||
<span className="font-medium">{category.name}</span>
|
||||
</div>
|
||||
<span className="text-muted-foreground">{category.usage}%</span>
|
||||
</div>
|
||||
<span className="text-muted-foreground">{category.usage}%</span>
|
||||
<Progress value={category.usage} className="h-2" />
|
||||
</div>
|
||||
<Progress value={category.usage} className="h-2" />
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<BarChart3 className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-gray-500 text-sm">尚未有使用數據</p>
|
||||
<p className="text-gray-400 text-xs mt-1">開始使用應用後,類別分布將顯示在這裡</p>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
@@ -21,6 +21,8 @@ export function ForgotPasswordDialog({ open, onOpenChange, onBackToLogin }: Forg
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [resetUrl, setResetUrl] = useState("")
|
||||
const [expiresAt, setExpiresAt] = useState("")
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -38,59 +40,101 @@ export function ForgotPasswordDialog({ open, onOpenChange, onBackToLogin }: Forg
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
try {
|
||||
const response = await fetch('/api/auth/forgot-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
|
||||
setIsLoading(false)
|
||||
setIsSuccess(true)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setResetUrl(data.resetUrl)
|
||||
setExpiresAt(data.expiresAt)
|
||||
setIsSuccess(true)
|
||||
} else {
|
||||
setError(data.error || "生成重設連結失敗")
|
||||
}
|
||||
} catch (err) {
|
||||
setError("生成重設連結時發生錯誤")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setEmail("")
|
||||
setError("")
|
||||
setIsSuccess(false)
|
||||
setResetUrl("")
|
||||
setExpiresAt("")
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const handleResend = async () => {
|
||||
setIsLoading(true)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<DialogTitle className="text-2xl font-bold text-center">重設密碼郵件已發送</DialogTitle>
|
||||
<DialogDescription className="text-center">我們已將重設密碼的連結發送到您的電子郵件</DialogDescription>
|
||||
<DialogTitle className="text-2xl font-bold text-center">密碼重設連結已生成</DialogTitle>
|
||||
<DialogDescription className="text-center">請複製以下連結並在新視窗中開啟以重設密碼</DialogDescription>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<p className="text-sm text-blue-800 mb-2">請檢查您的電子郵件:</p>
|
||||
<p className="text-sm text-blue-800 mb-2">重設連結已為以下電子郵件生成:</p>
|
||||
<p className="text-sm text-blue-700 font-medium">{email}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600 space-y-2">
|
||||
<p>• 請檢查您的收件匣和垃圾郵件資料夾</p>
|
||||
<p>• 重設連結將在 24 小時後過期</p>
|
||||
<p>• 如果您沒有收到郵件,請點擊下方重新發送</p>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reset-link">重設連結</Label>
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
id="reset-link"
|
||||
value={resetUrl}
|
||||
readOnly
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(resetUrl)
|
||||
// 可以添加 toast 提示
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
複製
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
此連結將在 {new Date(expiresAt).toLocaleString('zh-TW')} 過期
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Button
|
||||
onClick={() => window.open(resetUrl, '_blank')}
|
||||
className="w-full"
|
||||
>
|
||||
在新視窗中開啟重設頁面
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<Button onClick={onBackToLogin} variant="outline" className="flex-1 bg-transparent">
|
||||
<Button onClick={onBackToLogin} variant="outline" className="flex-1">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回登入
|
||||
</Button>
|
||||
<Button onClick={handleResend} disabled={isLoading} className="flex-1">
|
||||
{isLoading ? "發送中..." : "重新發送"}
|
||||
<Button onClick={handleClose} className="flex-1">
|
||||
關閉
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,7 +149,7 @@ export function ForgotPasswordDialog({ open, onOpenChange, onBackToLogin }: Forg
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold text-center">忘記密碼</DialogTitle>
|
||||
<DialogDescription className="text-center">
|
||||
請輸入您的電子郵件地址,我們將發送重設密碼的連結給您
|
||||
請輸入您的電子郵件地址,我們將生成密碼重設連結
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -138,11 +182,11 @@ export function ForgotPasswordDialog({ open, onOpenChange, onBackToLogin }: Forg
|
||||
返回登入
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading} className="flex-1">
|
||||
{isLoading ? "發送中..." : "發送重設連結"}
|
||||
{isLoading ? "生成中..." : "生成重設連結"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
@@ -107,8 +107,6 @@ export function LoginDialog({ open, onOpenChange, onSwitchToRegister, onSwitchTo
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? "登入中..." : "登入"}
|
||||
</Button>
|
||||
|
@@ -10,7 +10,7 @@ import { Label } from "@/components/ui/label"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { CheckCircle, AlertTriangle, Loader2 } from "lucide-react"
|
||||
import { CheckCircle, AlertTriangle, Loader2, Eye, EyeOff, Lock } from "lucide-react"
|
||||
|
||||
interface RegisterDialogProps {
|
||||
open: boolean
|
||||
@@ -28,6 +28,8 @@ export function RegisterDialog({ open, onOpenChange }: RegisterDialogProps) {
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
const departments = ["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "財務部", "人資部", "法務部"]
|
||||
@@ -86,7 +88,7 @@ export function RegisterDialog({ open, onOpenChange }: RegisterDialogProps) {
|
||||
<div className="text-center py-6">
|
||||
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-green-700 mb-2">註冊成功!</h3>
|
||||
<p className="text-gray-600">您的帳號已創建,現在可以登入使用。</p>
|
||||
<p className="text-gray-600">您的帳號已創建,請等待管理員審核。</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
@@ -140,24 +142,46 @@ export function RegisterDialog({ open, onOpenChange }: RegisterDialogProps) {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">密碼</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
className="pl-10 pr-10"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">確認密碼</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
||||
className="pl-10 pr-10"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
|
@@ -24,7 +24,7 @@ interface Message {
|
||||
quickQuestions?: string[]
|
||||
}
|
||||
|
||||
const DEEPSEEK_API_KEY = process.env.NEXT_PUBLIC_DEEPSEEK_API_KEY || ""
|
||||
const DEEPSEEK_API_KEY = process.env.NEXT_PUBLIC_DEEPSEEK_API_KEY || "sk-3640dcff23fe4a069a64f536ac538d75"
|
||||
const DEEPSEEK_API_URL = process.env.NEXT_PUBLIC_DEEPSEEK_API_URL || "https://api.deepseek.com/v1/chat/completions"
|
||||
|
||||
const systemPrompt = `你是一個競賽管理系統的AI助手,專門幫助用戶了解如何使用這個系統。
|
||||
|
Reference in New Issue
Block a user