506 lines
15 KiB
TypeScript
506 lines
15 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
getQueryVariable,
|
|
validatePhoneNumber,
|
|
} from "@/components/login/libs/common"
|
|
import FingerprintJS from "@fingerprintjs/fingerprintjs"
|
|
import {
|
|
BackgroundImage,
|
|
Box,
|
|
Button,
|
|
Flex,
|
|
FloatingIndicator,
|
|
Group,
|
|
Input,
|
|
LoadingOverlay,
|
|
Modal,
|
|
Paper,
|
|
PasswordInput,
|
|
Stack,
|
|
Tabs,
|
|
Text,
|
|
TextInput,
|
|
useComputedColorScheme,
|
|
} from "@mantine/core"
|
|
import { useForm } from "@mantine/form"
|
|
import { useDisclosure } from "@mantine/hooks"
|
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
|
import { getVerifyCode, passwdLogin, verificationLogin } from "./api"
|
|
import QrLogin from "./feishu/qr-login"
|
|
import { APP, PLATFORM, TENANT } from "./libs/common"
|
|
import { DigitalIcon } from "./resource/icons/digital"
|
|
import { useLoginStore } from "./store"
|
|
import tabClasses from "./Tab.module.css"
|
|
// import { useSearchParams } from "next/navigation"
|
|
import Feishu from "./feishu/auto-login"
|
|
import DarkBgImage from "./resource/images/dark-login-bg.png"
|
|
import LightBgImage from "./resource/images/light-login-bg.png"
|
|
import loginBg2Dark from "./resource/images/login-bg2-dark.png"
|
|
import loginBg2Light from "./resource/images/login-bg2-light.png"
|
|
import LogoDigImage from "./resource/images/logo-dig.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>
|
|
)}
|
|
</>
|
|
)
|
|
}
|