feat(project): init
This commit is contained in:
505
components/login/index.tsx
Normal file
505
components/login/index.tsx
Normal file
@@ -0,0 +1,505 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import {
|
||||
Box,
|
||||
TextInput,
|
||||
PasswordInput,
|
||||
Paper,
|
||||
Text,
|
||||
Group,
|
||||
Button,
|
||||
LoadingOverlay,
|
||||
Stack,
|
||||
Modal,
|
||||
Flex,
|
||||
BackgroundImage,
|
||||
Tabs,
|
||||
FloatingIndicator,
|
||||
Input,
|
||||
useComputedColorScheme,
|
||||
} from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { useDisclosure } from "@mantine/hooks"
|
||||
import { DigitalIcon } from "./resource/icons/digital"
|
||||
import tabClasses from "./Tab.module.css"
|
||||
import {
|
||||
getQueryVariable,
|
||||
validatePhoneNumber,
|
||||
} from "@/components/login/libs/common"
|
||||
import { getVerifyCode, passwdLogin, verificationLogin } from "./api"
|
||||
import QrLogin from "./feishu/qr-login"
|
||||
import { APP, PLATFORM, TENANT } from "./libs/common"
|
||||
import FingerprintJS from "@fingerprintjs/fingerprintjs"
|
||||
import { useLoginStore } from "./store"
|
||||
// import { useSearchParams } from "next/navigation"
|
||||
import Feishu from "./feishu/auto-login"
|
||||
import LightBgImage from "./resource/images/light-login-bg.png"
|
||||
import DarkBgImage from "./resource/images/dark-login-bg.png"
|
||||
import LogoDigImage from "./resource/images/logo-dig.png"
|
||||
import loginBg2Light from "./resource/images/login-bg2-light.png"
|
||||
import loginBg2Dark from "./resource/images/login-bg2-dark.png"
|
||||
|
||||
const HeaderIcon = DigitalIcon
|
||||
|
||||
const headerTitle = "管理系统平台"
|
||||
|
||||
export default function LoginPage({ useUserStore }: { useUserStore: any }) {
|
||||
const colorScheme = useComputedColorScheme()
|
||||
// const params = useSearchParams().get("auto")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [isClient, setIsClient] = useState(false)
|
||||
useEffect(() => {
|
||||
setIsClient(true)
|
||||
}, [])
|
||||
|
||||
// 创建 Mantine Form 实例
|
||||
const accountForm = useForm({
|
||||
// 初始值
|
||||
initialValues: {
|
||||
username: "", // 账户名
|
||||
password: "", // 密码
|
||||
},
|
||||
|
||||
// 验证规则 - 基于 rules 对象
|
||||
validate: {
|
||||
username: (value) => {
|
||||
if (!value) {
|
||||
return "账户名不能为空"
|
||||
}
|
||||
if (value.length < 1) {
|
||||
return "账户名至少1个字符"
|
||||
}
|
||||
// if (value.length > 20) {
|
||||
// return "账户名不能超过20个字符"
|
||||
// }
|
||||
// if (!/^[a-zA-Z0-9_]+$/.test(value)) {
|
||||
// return "账户名只能包含字母、数字和下划线"
|
||||
// }
|
||||
return null
|
||||
},
|
||||
|
||||
password: (value) => {
|
||||
if (!value) {
|
||||
return "密码不能为空"
|
||||
}
|
||||
// if (value.length < 6) {
|
||||
// return "密码至少6个字符"
|
||||
// }
|
||||
// if (value.length > 30) {
|
||||
// return "密码不能超过30个字符"
|
||||
// }
|
||||
// if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
|
||||
// return "密码需包含大小写字母和数字"
|
||||
// }
|
||||
return null
|
||||
},
|
||||
},
|
||||
|
||||
// 验证触发时机
|
||||
validateInputOnChange: true, // 输入时验证
|
||||
validateInputOnBlur: true, // 失焦时验证
|
||||
clearInputErrorOnChange: true, // 输入时清除错误
|
||||
})
|
||||
|
||||
const phoneForm = useForm({
|
||||
// 初始值
|
||||
initialValues: {
|
||||
phone: "", // 手机号
|
||||
code: "", // 验证码
|
||||
},
|
||||
// 验证规则 - 基于 rules 对象
|
||||
validate: {
|
||||
phone: (value) => {
|
||||
if (!value) {
|
||||
return "手机号不能为空"
|
||||
}
|
||||
if (value.length !== 11) {
|
||||
return "手机号必须为11位"
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
code: (value) => {
|
||||
if (!value) {
|
||||
return "验证码不能为空"
|
||||
}
|
||||
if (value.length !== 6) {
|
||||
return "验证码必须为6位"
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
// 验证触发时机
|
||||
validateInputOnChange: true, // 输入时验证
|
||||
validateInputOnBlur: true, // 失焦时验证
|
||||
clearInputErrorOnChange: true, // 输入时清除错误
|
||||
})
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false)
|
||||
const [activeTab, setActiveTab] = useState<string>("account")
|
||||
|
||||
// const [fingerprint, setFingerprint] = useState("")
|
||||
|
||||
const { setFingerprint } = useLoginStore()
|
||||
|
||||
const fingerprint = useLoginStore.getState().fingerprint
|
||||
|
||||
const fn = useCallback(async () => {
|
||||
const fpPromise = await FingerprintJS.load()
|
||||
fpPromise.get().then((result) => {
|
||||
const visitorId = result.visitorId
|
||||
setFingerprint(visitorId)
|
||||
})
|
||||
}, [setFingerprint])
|
||||
|
||||
useEffect(() => {
|
||||
fn()
|
||||
}, [fn])
|
||||
|
||||
useEffect(() => {
|
||||
if (getQueryVariable("code")) {
|
||||
setActiveTab("feishu")
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null)
|
||||
const [controlsRefs, setControlsRefs] = useState<
|
||||
Record<string, HTMLButtonElement | null>
|
||||
>({})
|
||||
const setControlRef = (val: string) => (node: HTMLButtonElement) => {
|
||||
controlsRefs[val] = node
|
||||
setControlsRefs(controlsRefs)
|
||||
}
|
||||
|
||||
const { setUserInfo, refreshToken } = useUserStore()
|
||||
|
||||
// 表单提交处理
|
||||
const handleAccountSubmit = accountForm.onSubmit(
|
||||
async (values) => {
|
||||
// 验证成功时的处理
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
let isAdmin = values.username === "admin"
|
||||
let phone = values.username.startsWith("+86")
|
||||
? values.username
|
||||
: `+86${values.username}`
|
||||
const params = {
|
||||
account: isAdmin ? values.username : phone,
|
||||
password: values.password,
|
||||
tenant: TENANT,
|
||||
platform: PLATFORM,
|
||||
app: APP,
|
||||
fingerprint,
|
||||
}
|
||||
try {
|
||||
const res = await passwdLogin(params)
|
||||
const info = res.message
|
||||
setUserInfo(info.tenant, info.user_info, info.rbac)
|
||||
refreshToken({
|
||||
access_token: info.access_token || "",
|
||||
refresh_token: info.refresh_token || "",
|
||||
})
|
||||
window.location.href = "/"
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "登录失败,请重试"
|
||||
setError(message)
|
||||
open() // 显示错误弹窗
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "登录失败,请重试"
|
||||
setError(message)
|
||||
open() // 显示错误弹窗
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
(errors) => {
|
||||
console.log("验证错误:", errors)
|
||||
}
|
||||
)
|
||||
|
||||
const [codeTime, setCodeTime] = useState<number>(0)
|
||||
const handleCountDown = () => {
|
||||
setCodeTime(60)
|
||||
let clean = setInterval(() => {
|
||||
setCodeTime((time) => {
|
||||
if (time === 0) {
|
||||
clearInterval(clean)
|
||||
return 0
|
||||
}
|
||||
return time - 1
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
const handleGetCode = async () => {
|
||||
if (!validatePhoneNumber(phoneForm.values.phone)) {
|
||||
setError("请输入正确的手机号")
|
||||
open() // 显示错误弹窗
|
||||
return
|
||||
} else {
|
||||
try {
|
||||
handleCountDown()
|
||||
getVerifyCode({
|
||||
phone: phoneForm.values.phone.startsWith("+86")
|
||||
? phoneForm.values.phone
|
||||
: `+86${phoneForm.values.phone}`,
|
||||
}).then((res) => {
|
||||
if (res?.code === 200) {
|
||||
setError("验证码已发送")
|
||||
open() // 显示错误弹窗
|
||||
} else {
|
||||
typeof res?.message === "string" &&
|
||||
setError(res?.message || "验证码发送失败")
|
||||
open() // 显示错误弹窗
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "获取验证码失败,请重试"
|
||||
setError(message)
|
||||
open() // 显示错误弹窗
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 手机号登录表单提交处理
|
||||
const handlePhoneSubmit = phoneForm.onSubmit(
|
||||
async (values) => {
|
||||
// 验证成功时的处理
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const params = {
|
||||
phone: values.phone.startsWith("+86")
|
||||
? values.phone
|
||||
: `+86${values.phone}`,
|
||||
verify_code: values.code,
|
||||
tenant: TENANT,
|
||||
platform: PLATFORM,
|
||||
app: APP,
|
||||
fingerprint,
|
||||
}
|
||||
const res = await verificationLogin(params)
|
||||
const info = res.message
|
||||
setUserInfo(info.tenant, info.user_info, info.rbac)
|
||||
refreshToken({
|
||||
access_token: info.access_token || "",
|
||||
refresh_token: info.refresh_token || "",
|
||||
})
|
||||
window.location.href = "/"
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "登录失败,请重试"
|
||||
setError(message)
|
||||
open() // 显示错误弹窗
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
(errors) => {
|
||||
console.log("验证错误:", errors)
|
||||
}
|
||||
)
|
||||
|
||||
const bgImage = useMemo(
|
||||
() => (colorScheme !== "dark" ? LightBgImage : DarkBgImage),
|
||||
[colorScheme]
|
||||
)
|
||||
|
||||
const stackBg2Image = useMemo(
|
||||
() => (colorScheme === "dark" ? loginBg2Dark : loginBg2Light),
|
||||
[colorScheme]
|
||||
)
|
||||
|
||||
const isFeishu = useMemo(() => {
|
||||
if (typeof window !== "undefined") return !!(window.h5sdk && window.tt)
|
||||
else return false
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={opened} onClose={close} title="消息提示">
|
||||
<Flex gap={12} align="center">
|
||||
<Text size="sm">{error}</Text>
|
||||
</Flex>
|
||||
|
||||
<Group mt="md">
|
||||
<Button variant="outline" size="sm" onClick={close}>
|
||||
知道了
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
<Stack align="center" justify="center" h={"100vh"} w="100%">
|
||||
{isClient && (
|
||||
<BackgroundImage
|
||||
src={bgImage.src}
|
||||
w="100%"
|
||||
h="100%"
|
||||
style={{ position: "absolute", top: 0, left: 0, zIndex: -1 }}
|
||||
/>
|
||||
)}
|
||||
<Paper
|
||||
withBorder
|
||||
shadow="md"
|
||||
radius="md"
|
||||
h={520}
|
||||
maw={"100%"}
|
||||
style={{ borderColor: "rgba(255, 255, 255, 0.5)" }}>
|
||||
<Flex w="100%" h={"100%"}>
|
||||
<Stack
|
||||
w={600}
|
||||
p={32}
|
||||
visibleFrom="md"
|
||||
style={{ position: "relative" }}>
|
||||
<Box
|
||||
w="100%"
|
||||
h="100%"
|
||||
style={{ position: "absolute", top: 0, left: 0 }}>
|
||||
{isClient && (
|
||||
<BackgroundImage src={stackBg2Image.src} w="100%" h="100%" />
|
||||
)}
|
||||
</Box>
|
||||
<Flex
|
||||
justify="center"
|
||||
align="center"
|
||||
h="100%"
|
||||
style={{ zIndex: 11 }}>
|
||||
<BackgroundImage
|
||||
src={LogoDigImage.src}
|
||||
radius="xs"
|
||||
w={432}
|
||||
h={310}
|
||||
visibleFrom="md"></BackgroundImage>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Stack h={"100%"} w={380} p={"54px 32px"} gap={0}>
|
||||
<Flex mb={"2rem"} gap={8} justify="center" align={"center"}>
|
||||
<HeaderIcon />
|
||||
<Text size="xxl">{headerTitle}</Text>
|
||||
</Flex>
|
||||
<Tabs
|
||||
variant="none"
|
||||
value={activeTab}
|
||||
onChange={(value) => setActiveTab(value || "")}>
|
||||
<Tabs.List ref={setRootRef} className={tabClasses.list}>
|
||||
<Tabs.Tab
|
||||
value="account"
|
||||
ref={setControlRef("account")}
|
||||
className={tabClasses.tab}>
|
||||
账户登录
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="phone"
|
||||
ref={setControlRef("phone")}
|
||||
className={""}>
|
||||
手机号登录
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="feishu"
|
||||
ref={setControlRef("feishu")}
|
||||
className={tabClasses.tab}>
|
||||
飞书扫码
|
||||
</Tabs.Tab>
|
||||
<FloatingIndicator
|
||||
target={activeTab ? controlsRefs[activeTab] : null}
|
||||
parent={rootRef}
|
||||
className={tabClasses.indicator}
|
||||
/>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="account">
|
||||
<form onSubmit={handleAccountSubmit}>
|
||||
<LoadingOverlay visible={loading} />
|
||||
{/* 账户名输入 */}
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label="账户名"
|
||||
placeholder="请输入账户名"
|
||||
{...accountForm.getInputProps("username")}
|
||||
/>
|
||||
|
||||
{/* 密码输入 */}
|
||||
<PasswordInput
|
||||
withAsterisk
|
||||
label="密码"
|
||||
placeholder="请输入密码"
|
||||
mt="md"
|
||||
{...accountForm.getInputProps("password")}
|
||||
/>
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<Button type="submit" fullWidth mt="xl" disabled={loading}>
|
||||
{loading ? "登录中..." : "登录"}
|
||||
</Button>
|
||||
</form>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="phone">
|
||||
<form onSubmit={handlePhoneSubmit}>
|
||||
<LoadingOverlay visible={loading} />
|
||||
{/* 手机号输入 */}
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label="手机号"
|
||||
placeholder="请输入手机号"
|
||||
{...phoneForm.getInputProps("phone")}
|
||||
/>
|
||||
|
||||
{/* 验证码输入 */}
|
||||
<Input.Wrapper
|
||||
label="验证码"
|
||||
withAsterisk
|
||||
mt="md"
|
||||
error={phoneForm.errors.code}>
|
||||
<Flex justify={"space-between"} align={"end"}>
|
||||
<Input
|
||||
placeholder="请输入验证码"
|
||||
{...phoneForm.getInputProps("code")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={
|
||||
!validatePhoneNumber(phoneForm.values.phone) ||
|
||||
!!codeTime
|
||||
}
|
||||
onClick={handleGetCode}>
|
||||
{codeTime !== 0
|
||||
? `${codeTime}秒后获取`
|
||||
: "获取验证码"}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Input.Wrapper>
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<Button type="submit" fullWidth mt="xl" disabled={loading}>
|
||||
{loading ? "登录中..." : "登录"}
|
||||
</Button>
|
||||
</form>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="feishu">
|
||||
{isClient && <QrLogin useUserStore={useUserStore}></QrLogin>}
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Paper>
|
||||
</Stack>
|
||||
{isFeishu && (
|
||||
<Stack
|
||||
h={"100vh"}
|
||||
w={"100vw"}
|
||||
bg="white"
|
||||
style={{ position: "absolute", top: 0, zIndex: 99999 }}>
|
||||
<Feishu useUserStore={useUserStore} />
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user