Files
users/app/login/page.tsx

358 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { Eye, EyeOff, Mail, Phone } from "lucide-react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Checkbox } from "@/components/ui/checkbox"
import { useRouter } from "next/navigation"
import { WeChatIcon } from "@/components/icons/wechat-icon"
import { AppleIcon } from "@/components/icons/apple-icon"
import { useToast } from "@/components/ui/use-toast"
// 使用环境变量:不配置则用本地 API支持 zhiqun@qq.com / Zhiqun1984
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ""
// 定义登录响应类型
interface LoginResponse {
code: number
message: string
data?: {
token: string
}
}
interface LoginForm {
account: string // 邮箱或手机号
password: string
verificationCode: string
agreeToTerms: boolean
}
export default function LoginPage() {
const [showPassword, setShowPassword] = useState(false)
const [activeTab, setActiveTab] = useState<"password" | "verification">("password")
const [isLoading, setIsLoading] = useState(false)
const [form, setForm] = useState<LoginForm>({
account: "",
password: "",
verificationCode: "",
agreeToTerms: false,
})
const router = useRouter()
const { toast } = useToast()
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setForm((prev) => ({ ...prev, [name]: value }))
}
const handleCheckboxChange = (checked: boolean) => {
setForm((prev) => ({ ...prev, agreeToTerms: checked }))
}
const validateForm = () => {
if (!form.account.trim()) {
toast({
variant: "destructive",
title: "请输入账号",
description: "请输入邮箱或手机号",
})
return false
}
if (!form.agreeToTerms) {
toast({
variant: "destructive",
title: "请同意用户协议",
description: "需要同意用户协议和隐私政策才能继续",
})
return false
}
if (activeTab === "password" && !form.password) {
toast({
variant: "destructive",
title: "请输入密码",
description: "密码不能为空",
})
return false
}
if (activeTab === "verification" && !form.verificationCode) {
toast({
variant: "destructive",
title: "请输入验证码",
description: "验证码不能为空",
})
return false
}
return true
}
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
if (!validateForm()) return
setIsLoading(true)
try {
const formData = new FormData()
const isEmail = form.account.includes("@")
formData.append(isEmail ? "email" : "phone", form.account)
if (activeTab === "password") {
formData.append("password", form.password)
} else {
formData.append("verificationCode", form.verificationCode)
}
const apiUrl = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/api/auth/login"
const response = await fetch(apiUrl, {
method: "POST",
body: formData,
// 不需要设置Content-Type浏览器会自动设置为multipart/form-data并添加boundary
})
const result: LoginResponse = await response.json()
if (result.code === 10000 && result.data?.token) {
// 保存token到localStorage
localStorage.setItem("token", result.data.token)
// 成功后跳转
router.push("/")
toast({
title: "登录成功",
description: "欢迎回来!",
})
} else {
throw new Error(result.message || "登录失败")
}
} catch (error) {
toast({
variant: "destructive",
title: "登录失败",
description: error instanceof Error ? error.message : "请稍后重试",
})
} finally {
setIsLoading(false)
}
}
const handleSendVerificationCode = async () => {
if (!form.account.trim()) {
toast({
variant: "destructive",
title: "请输入手机号",
description: "发送验证码需要手机号",
})
return
}
setIsLoading(true)
try {
const formData = new FormData()
formData.append("phone", form.account)
const apiUrl = API_BASE_URL ? `${API_BASE_URL}/auth/send-code` : "/api/auth/send-code"
const response = await fetch(apiUrl, {
method: "POST",
body: formData,
})
const result = await response.json()
if (result.code === 10000) {
toast({
title: "验证码已发送",
description: "请查看手机短信",
})
} else {
throw new Error(result.message || "发送失败")
}
} catch (error) {
toast({
variant: "destructive",
title: "发送失败",
description: error instanceof Error ? error.message : "请稍后重试",
})
} finally {
setIsLoading(false)
}
}
useEffect(() => {
// 检查是否已登录
const token = localStorage.getItem("token")
if (token) {
router.push("/profile")
}
}, [router])
return (
<div className="min-h-screen bg-white text-gray-900 flex flex-col px-4 py-8">
<div className="max-w-md w-full mx-auto space-y-8">
<Tabs
defaultValue="password"
className="w-full"
onValueChange={(v) => setActiveTab(v as "password" | "verification")}
>
<TabsList className="w-full bg-transparent border-b border-gray-200">
<TabsTrigger
value="verification"
className="flex-1 data-[state=active]:border-blue-500 data-[state=active]:text-blue-500 border-b-2 border-transparent"
>
</TabsTrigger>
<TabsTrigger
value="password"
className="flex-1 data-[state=active]:border-blue-500 data-[state=active]:text-blue-500 border-b-2 border-transparent"
>
</TabsTrigger>
</TabsList>
<div className="mt-8">
<p className="text-gray-600 mb-6"> / / / Apple </p>
<form onSubmit={handleLogin} className="space-y-6">
<div className="relative">
<Input
type="text"
name="account"
value={form.account}
onChange={handleInputChange}
placeholder="邮箱或手机号"
className="pl-12 border-gray-300 text-gray-900 h-12"
disabled={isLoading}
/>
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500">
{form.account.includes("@") ? (
<Mail className="h-4 w-4" />
) : (
<Phone className="h-4 w-4" />
)}
</span>
</div>
<TabsContent value="password" className="m-0">
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
name="password"
value={form.password}
onChange={handleInputChange}
placeholder="密码"
className="pr-12 border-gray-300 text-gray-900 h-12"
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-500"
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</TabsContent>
<TabsContent value="verification" className="m-0">
<div className="flex gap-3">
<Input
type="text"
name="verificationCode"
value={form.verificationCode}
onChange={handleInputChange}
placeholder="验证码"
className="border-gray-300 text-gray-900 h-12"
disabled={isLoading}
/>
<Button
type="button"
variant="outline"
className="w-32 h-12 border-gray-300 text-gray-600 hover:text-gray-900"
onClick={handleSendVerificationCode}
disabled={isLoading}
>
</Button>
</div>
</TabsContent>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={form.agreeToTerms}
onCheckedChange={handleCheckboxChange}
className="border-gray-300 data-[state=checked]:bg-blue-500"
disabled={isLoading}
/>
<label htmlFor="terms" className="text-sm text-gray-600">
<a href="#" className="text-blue-500 mx-1">
</a>
<a href="#" className="text-blue-500 ml-1">
</a>
</label>
</div>
<Button
type="submit"
className="w-full h-12 bg-blue-500 hover:bg-blue-600 text-white"
disabled={isLoading}
>
{isLoading ? "登录中..." : "登录"}
</Button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-gray-300"></span>
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-gray-500"></span>
</div>
</div>
<div className="space-y-3">
<Button
type="button"
variant="outline"
className="w-full h-12 border-gray-300 text-gray-700 hover:bg-gray-50"
disabled={isLoading}
>
<WeChatIcon className="w-6 h-6 mr-2 text-[#07C160]" />
使
</Button>
<Button
type="button"
variant="outline"
className="w-full h-12 border-gray-300 text-gray-700 hover:bg-gray-50"
disabled={isLoading}
>
<AppleIcon className="w-6 h-6 mr-2" />
使 Apple
</Button>
</div>
</form>
<div className="mt-8 text-center">
<a href="#" className="text-sm text-gray-500">
</a>
</div>
</div>
</Tabs>
</div>
</div>
)
}