pmc bd95ba470c feat: update admin panel, API modules, and add migrations
- Update food, outfits, props, home-decor pages and components
- Add permissions page and sidebar updates
- Update API client and all API modules (auth, food, dances, etc.)
- Add card model migrations for optional fields
- Update Django views, serializers, and authentication
- Add affinity level migrations and user app updates
- Add project documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 13:06:50 +08:00

276 lines
11 KiB
TypeScript

"use client"
import type React from "react"
import { useState } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import { Sparkles, Mail, Lock, Phone, ArrowRight, Loader2 } from "lucide-react"
import { emailLogin, saveAuthToken } from "@/lib/api/auth"
export default function LoginPage() {
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
const [loginMethod, setLoginMethod] = useState<"email" | "phone">("email")
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [phone, setPhone] = useState("")
const [verificationCode, setVerificationCode] = useState("")
const [isSendingCode, setIsSendingCode] = useState(false)
const [countdown, setCountdown] = useState(0)
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
try {
if (loginMethod === "email") {
// 使用真实的邮箱登录接口
const response = await emailLogin(email, password)
console.log(response)
// 保存登录凭证(包含角色信息)
saveAuthToken(response.data.token, response.data.is_superuser, response.data.role)
// 设置登录状态
localStorage.setItem("isLoggedIn", "true")
// 登录成功后跳转到首页
router.push("/")
} else {
// 手机号登录逻辑保持不变
// 模拟登录请求
await new Promise((resolve) => setTimeout(resolve, 1500))
// 设置登录状态
localStorage.setItem("isLoggedIn", "true")
// 登录成功后跳转到首页
router.push("/")
}
} catch (error) {
console.error("登录失败", error)
alert(error instanceof Error ? error.message : "登录失败,请重试")
} finally {
setIsLoading(false)
}
}
const handleSendVerificationCode = async () => {
if (!phone || phone.length !== 11 || isSendingCode) return
setIsSendingCode(true)
try {
// 模拟发送验证码请求
await new Promise((resolve) => setTimeout(resolve, 1000))
// 开始倒计时
setCountdown(60)
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer)
setIsSendingCode(false)
return 0
}
return prev - 1
})
}, 1000)
} catch (error) {
console.error("发送验证码失败", error)
setIsSendingCode(false)
}
}
return (
<div className="min-h-screen w-full flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-4">
<div className="absolute top-0 right-0 w-1/2 h-64 bg-gradient-to-bl from-pink-200 via-purple-200 to-transparent opacity-20 rounded-bl-full" />
<div className="absolute bottom-0 left-0 w-1/2 h-64 bg-gradient-to-tr from-blue-200 via-purple-200 to-transparent opacity-20 rounded-tr-full" />
<Card className="w-full max-w-md border-none shadow-xl bg-white/80 backdrop-blur-sm">
<CardHeader className="space-y-2 text-center">
<div className="flex justify-center mb-2">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-pink-500 to-purple-600 flex items-center justify-center">
<Sparkles className="h-6 w-6 text-white" />
</div>
</div>
<CardTitle className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-pink-600 to-purple-600">
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="email" onValueChange={(value) => setLoginMethod(value as "email" | "phone")}>
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger
value="email"
className="data-[state=active]:bg-gradient-to-r data-[state=active]:from-pink-500 data-[state=active]:to-purple-600 data-[state=active]:text-white"
>
</TabsTrigger>
<TabsTrigger
value="phone"
className="data-[state=active]:bg-gradient-to-r data-[state=active]:from-pink-500 data-[state=active]:to-purple-600 data-[state=active]:text-white"
>
</TabsTrigger>
</TabsList>
<TabsContent value="email">
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email"></Label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
id="email"
type="email"
placeholder="请输入邮箱地址"
className="pl-10 border-gray-300 focus-visible:ring-pink-500"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="password"></Label>
<Link href="/forgot-password" className="text-xs text-pink-600 hover:text-pink-700 hover:underline">
?
</Link>
</div>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
id="password"
type="password"
placeholder="请输入密码"
className="pl-10 border-gray-300 focus-visible:ring-pink-500"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="remember" />
<Label htmlFor="remember" className="text-sm font-normal cursor-pointer">
</Label>
</div>
<Button
type="submit"
className="w-full bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 transition-all duration-300 shadow-md hover:shadow-lg"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<ArrowRight className="ml-2 h-4 w-4" />
</>
)}
</Button>
</form>
</TabsContent>
<TabsContent value="phone">
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<div className="relative">
<Phone className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
id="phone"
type="tel"
placeholder="请输入手机号码"
className="pl-10 border-gray-300 focus-visible:ring-pink-500"
value={phone}
onChange={(e) => setPhone(e.target.value)}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="verificationCode"></Label>
<div className="flex space-x-2">
<div className="relative flex-1">
<Input
id="verificationCode"
type="text"
placeholder="请输入验证码"
className="border-gray-300 focus-visible:ring-pink-500"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
required
/>
</div>
<Button
type="button"
variant="outline"
className="min-w-[120px] hover:bg-pink-50 hover:text-pink-700 transition-all duration-200"
onClick={handleSendVerificationCode}
disabled={isSendingCode || !phone || phone.length !== 11}
>
{countdown > 0 ? `${countdown}秒后重发` : "获取验证码"}
</Button>
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="remember-phone" />
<Label htmlFor="remember-phone" className="text-sm font-normal cursor-pointer">
</Label>
</div>
<Button
type="submit"
className="w-full bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 transition-all duration-300 shadow-md hover:shadow-lg"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<ArrowRight className="ml-2 h-4 w-4" />
</>
)}
</Button>
</form>
</TabsContent>
</Tabs>
</CardContent>
<CardFooter className="flex flex-col space-y-4 pt-0">
<div className="text-center text-sm text-gray-500">
?{" "}
<Link href="/register" className="text-pink-600 hover:text-pink-700 hover:underline">
</Link>
</div>
</CardFooter>
</Card>
</div>
)
}