feat: 旋转验证对接成功

This commit is contained in:
landaiqing
2024-06-05 01:53:04 +08:00
parent 60732f6142
commit af176c0e66
5 changed files with 449 additions and 335 deletions

View File

@@ -24,12 +24,16 @@ export const oauthLogin = (type: string) => {
/** /**
* 获取短信验证码 * 获取短信验证码
* @param phone * @param data
*/ */
export const getSms = (phone: string) => { export const getSms = (data: any) => {
return web.request({ return web.request({
url: "/sms/sendByTemplate/" + phone, url: "/sms/sendByTemplate/",
method: "post", method: "post",
headers: {
"Content-Type": "application/json;charset=UTF-8",
},
data: data,
}); });
}; };
@@ -44,3 +48,16 @@ export const register = (data: API.PhoneRegisterRequest) => {
data: data, data: data,
}); });
}; };
/**
* 注册
* @param data
*/
export const login = (data: API.LoginRequest) => {
return web.request({
url: "/auth/user/login",
method: "post",
data: data,
});
};

View File

@@ -9,6 +9,12 @@ declare namespace API {
confirmPassword?: string; confirmPassword?: string;
activeCode?: string; activeCode?: string;
}; };
type LoginRequest = {
userName?: string;
password?: string;
token: string;
deg: number;
};
// type ApiResponse<T> = { // type ApiResponse<T> = {
// success?: boolean; // success?: boolean;
// code?: number; // code?: number;

View File

@@ -14,7 +14,7 @@ class Request {
(config) => { (config) => {
const token: string | null = getStorageFromKey("token"); const token: string | null = getStorageFromKey("token");
if (token) { if (token) {
config.headers.Authorization = `schisandra ${token}`; config.headers.Authorization = `${import.meta.env.VITE_APP_TOKEN_KEY} ${token}`;
} }
// if (config.method == "post") { // if (config.method == "post") {

View File

@@ -36,8 +36,7 @@ import { observer } from "mobx-react";
import FooterComponent from "@/components/Footer"; import FooterComponent from "@/components/Footer";
import RotateCaptcha, { CaptchaInstance, type TicketInfoType } from "react-rotate-captcha"; import RotateCaptcha, { CaptchaInstance, type TicketInfoType } from "react-rotate-captcha";
import { get, load } from "@/api/captcha/index.ts"; import { get, load } from "@/api/captcha/index.ts";
import { getSms, oauthLogin } from "@/api/user"; import { getSms, login, oauthLogin } from "@/api/user";
import { VerfiyCaptcha } from "@/api/captcha/api.ts";
import { TinyColor } from "@ctrl/tinycolor"; import { TinyColor } from "@ctrl/tinycolor";
type LoginType = "account" | "phone"; type LoginType = "account" | "phone";
@@ -51,7 +50,8 @@ const iconStyles: CSSProperties = {
export default observer(() => { export default observer(() => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const captcha = useRef<CaptchaInstance>(null); const smsCaptcha = useRef<CaptchaInstance>(null);
const loginCaptcha = useRef<CaptchaInstance>(null);
const captchaRef = useRef<CaptFieldRef | null | undefined>(); const captchaRef = useRef<CaptFieldRef | null | undefined>();
const colors = ["#40e495", "#30dd8a", "#2bb673"]; const colors = ["#40e495", "#30dd8a", "#2bb673"];
const getHoverColors = (colors: string[]) => const getHoverColors = (colors: string[]) =>
@@ -59,12 +59,53 @@ export default observer(() => {
const getActiveColors = (colors: string[]) => const getActiveColors = (colors: string[]) =>
colors.map((color) => new TinyColor(color).darken(5).toString()); colors.map((color) => new TinyColor(color).darken(5).toString());
async function verify(token: string, deg: number): Promise<TicketInfoType> { async function smsVerify(token: string, deg: number): Promise<TicketInfoType> {
const phone = form.getFieldValue("mobile");
const data: any = { const data: any = {
token: token, token: token,
deg: deg, deg: deg,
phone: phone,
}; };
const res = await VerfiyCaptcha(data); const res: any = await getSms(data);
if (res && res.code === 0) {
message.open({
content: res.data,
type: "success",
duration: 5,
});
} else {
message.open({
content: res.data,
type: "warning",
duration: 5,
});
}
return res;
}
async function loginVerify(token: string, deg: number): Promise<TicketInfoType> {
const userName = form.getFieldValue("username");
const password = form.getFieldValue("password");
const data: any = {
token: token,
deg: deg,
userName: userName,
password: password,
};
const res: any = await login(data);
if (res && res.success && res.code === 0) {
message.open({
content: "登录成功!",
type: "success",
duration: 5,
});
} else if (res.code === 0 && !res.success) {
message.open({
content: "登录失败!用户名或密码错误!",
type: "error",
duration: 5,
});
}
return res; return res;
} }
@@ -96,322 +137,317 @@ export default observer(() => {
const [loginType, setLoginType] = useState<LoginType>("account"); const [loginType, setLoginType] = useState<LoginType>("account");
async function openCaptcha() { async function openSmsCaptcha() {
captcha.current!.open(); smsCaptcha.current!.open();
}
async function openLoginCaptcha() {
loginCaptcha.current!.open();
} }
const onSubmit = async (formData: object) => {
openCaptcha().then(() => {
console.log("formData", formData);
});
};
return ( return (
<RotateCaptcha get={get} load={load} verify={verify} limit={2} ref={captcha}> <div className={styles.container}>
<div className={styles.container}> <RotateCaptcha get={get} load={load} verify={smsVerify} limit={2} ref={smsCaptcha} />
<div className={styles.content}> <RotateCaptcha get={get} load={load} verify={loginVerify} ref={loginCaptcha} />
<Space className={styles.content_content}> <div className={styles.content}>
<Space className={styles.login_content}> <Space className={styles.content_content}>
<Space align="center" className={styles.mp_code}> <Space className={styles.login_content}>
<Space direction="vertical" align="center"> <Space align="center" className={styles.mp_code}>
<span className={styles.mp_code_title}></span> <Space direction="vertical" align="center">
<Image <span className={styles.mp_code_title}></span>
preview={false} <Image
height={210} preview={false}
width={200} height={210}
className={styles.mp_code_img} width={200}
// src={generateMpRegCodeData.data?.qrCodeUrl} className={styles.mp_code_img}
src={qrCode} // src={generateMpRegCodeData.data?.qrCodeUrl}
/> src={qrCode}
<Alert />
// message={(<span>微信扫码<span>关注公众号</span></span>)} <Alert
description={ // message={(<span>微信扫码<span>关注公众号</span></span>)}
<div> description={
<span> <div>
<span>
<span className={styles.mp_tips}>
<span className={styles.mp_tips}></span>
</span> </span>
</span> <br />
<br />
</div>
</div> }
} // type="success"
// type="success" showIcon={true}
showIcon={true} className={styles.alert}
className={styles.alert} icon={<WechatOutlined />}
icon={<WechatOutlined />} />
/>
</Space>
</Space> </Space>
<Form
form={form}
className={styles.login_form}
initialValues={{
autoLogin: true,
}}>
<Space direction="vertical" align="center">
<Space className={styles.logo}>
<img
alt="logo"
src={logo}
style={{ width: "44px", height: "44px" }}
/>
<span></span>
</Space>
<div className={styles.subTitle}></div>
</Space>
<Tabs
centered={true}
items={items}
activeKey={loginType}
onChange={(activeKey) =>
setLoginType(activeKey as LoginType)
}></Tabs>
{loginType === "account" && (
<>
<ProFormText
name="username"
fieldProps={{
size: "large",
prefix: <UserOutlined className={"prefixIcon"} />,
}}
placeholder={"请输入邮箱或电话号码"}
rules={[
{
required: true,
message: "请输入用户名!",
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: "large",
prefix: <LockOutlined className={"prefixIcon"} />,
}}
placeholder={"请输入密码"}
rules={[
{
required: true,
message: "请输入密码!",
},
{
pattern:
/^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z\\W]{6,18}$/,
message:
"密码长度需在6~18位字符且必须包含字母和数字",
},
]}
/>
</>
)}
{loginType === "phone" && (
<>
<ProFormText
fieldProps={{
size: "large",
prefix: <MobileOutlined className={"prefixIcon"} />,
autoComplete: "off",
}}
name="mobile"
placeholder={"请输入手机号"}
rules={[
{
required: true,
message: "请输入手机号!",
},
{
pattern: /^1\d{10}$/,
message: "手机号格式错误!",
},
]}
/>
<ProFormCaptcha
fieldProps={{
size: "large",
prefix: <SafetyOutlined className={"prefixIcon"} />,
}}
captchaProps={{
size: "large",
}}
placeholder={"请输入验证码"}
captchaTextRender={(timing: boolean) => {
if (timing) {
// return `${count} ${"获取验证码"}`;
return `${"获取验证码"}`;
}
return "获取验证码";
}}
name="captcha"
phoneName={"mobile"}
rules={[
{
required: true,
message: "请输入验证码!",
},
]}
fieldRef={captchaRef}
countDown={300}
onGetCaptcha={async (mobile: string) => {
const res: any = getSms(mobile);
if (res && res.success) {
message.success(res.data, 3);
} else {
message.warning(res.data, 3);
}
}}
/>
</>
)}
<div style={{ marginBlockEnd: 14 }}>
<ProFormCheckbox noStyle name="autoLogin">
</ProFormCheckbox>
<a href={"/forget"} style={{ float: "right" }}>
</a>
</div>
<ConfigProvider
theme={{
components: {
Button: {
colorPrimary: `linear-gradient(116deg, ${colors.join(", ")})`,
colorPrimaryHover: `linear-gradient(116deg, ${getHoverColors(colors).join(", ")})`,
colorPrimaryActive: `linear-gradient(116deg, ${getActiveColors(colors).join(", ")})`,
lineWidth: 0,
},
},
}}>
<Button
type="primary"
block
size="large"
onClick={async () => {
let validateFields;
if (loginType === "account") {
validateFields = ["username", "password"];
} else {
validateFields = ["mobile", "captcha"];
}
await form
.validateFields(validateFields)
.then(async (values) => {
await onSubmit(values);
})
.catch((error) => {
console.error(error);
});
}}>
</Button>
</ConfigProvider>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
}}>
<Divider plain>
<span
style={{
color: "#CCC",
fontWeight: "normal",
fontSize: 14,
}}>
</span>
</Divider>
<Space align="center" size={24}>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: 40,
width: 40,
border: "1px solid #D4D8DD",
borderRadius: "50%",
}}>
<Tooltip title="QQ登录" color={"blue"}>
<QqOutlined
style={{ ...iconStyles, color: "#1677FF" }}
/>
</Tooltip>
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: 40,
width: 40,
border: "1px solid #D4D8DD",
borderRadius: "50%",
}}>
<Tooltip title="企业微信登录" color={"green"}>
<WechatOutlined
style={{ ...iconStyles, color: "#08a327" }}
/>
</Tooltip>
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: 40,
width: 40,
border: "1px solid #D4D8DD",
borderRadius: "50%",
}}>
<Tooltip title="github登录" color={"black"}>
<GithubOutlined
onClick={() => {
oAuthLogin("github").then();
}}
style={{ ...iconStyles, color: "#333333" }}
/>
</Tooltip>
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: 40,
width: 40,
border: "1px solid #D4D8DD",
borderRadius: "50%",
}}>
<Tooltip title="gitee登录" color={"orange"}>
<GitlabOutlined
onClick={() => {
oAuthLogin("gitee").then();
}}
style={{ ...iconStyles, color: "#FF6A10" }}
/>
</Tooltip>
</div>
</Space>
</div>
</Form>
<a href="/register" className={styles.go_to_register}>
<span></span>
</a>
</Space> </Space>
<Form
form={form}
className={styles.login_form}
initialValues={{
autoLogin: true,
}}>
<Space direction="vertical" align="center">
<Space className={styles.logo}>
<img
alt="logo"
src={logo}
style={{ width: "44px", height: "44px" }}
/>
<span></span>
</Space>
<div className={styles.subTitle}></div>
</Space>
<Tabs
centered={true}
items={items}
activeKey={loginType}
onChange={(activeKey) =>
setLoginType(activeKey as LoginType)
}></Tabs>
{loginType === "account" && (
<>
<ProFormText
name="username"
fieldProps={{
size: "large",
prefix: <UserOutlined className={"prefixIcon"} />,
}}
placeholder={"请输入邮箱或电话号码"}
rules={[
{
required: true,
message: "请输入用户名!",
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: "large",
prefix: <LockOutlined className={"prefixIcon"} />,
}}
placeholder={"请输入密码"}
rules={[
{
required: true,
message: "请输入密码!",
},
{
pattern:
/^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z\\W]{6,18}$/,
message:
"密码长度需在6~18位字符且必须包含字母和数字",
},
]}
/>
</>
)}
{loginType === "phone" && (
<>
<ProFormText
fieldProps={{
size: "large",
prefix: <MobileOutlined className={"prefixIcon"} />,
autoComplete: "off",
}}
name="mobile"
placeholder={"请输入手机号"}
rules={[
{
required: true,
message: "请输入手机号!",
},
{
pattern: /^1\d{10}$/,
message: "手机号格式错误!",
},
]}
/>
<ProFormCaptcha
fieldProps={{
size: "large",
prefix: <SafetyOutlined className={"prefixIcon"} />,
}}
captchaProps={{
size: "large",
}}
placeholder={"请输入验证码"}
captchaTextRender={(timing: boolean) => {
if (timing) {
// return `${count} ${"获取验证码"}`;
return `${"获取验证码"}`;
}
return "获取验证码";
}}
name="captcha"
phoneName={"mobile"}
rules={[
{
required: true,
message: "请输入验证码!",
},
]}
fieldRef={captchaRef}
countDown={300}
onGetCaptcha={async () => {
await openSmsCaptcha();
}}
/>
</>
)}
<div style={{ marginBlockEnd: 14 }}>
<ProFormCheckbox noStyle name="autoLogin">
</ProFormCheckbox>
<a href={"/forget"} style={{ float: "right" }}>
</a>
</div>
<ConfigProvider
theme={{
components: {
Button: {
colorPrimary: `linear-gradient(116deg, ${colors.join(", ")})`,
colorPrimaryHover: `linear-gradient(116deg, ${getHoverColors(colors).join(", ")})`,
colorPrimaryActive: `linear-gradient(116deg, ${getActiveColors(colors).join(", ")})`,
lineWidth: 0,
},
},
}}>
<Button
type="primary"
block
size="large"
onClick={async () => {
let validateFields;
if (loginType === "account") {
validateFields = ["username", "password"];
} else {
validateFields = ["mobile", "captcha"];
}
await form
.validateFields(validateFields)
.then(async () => {
// await onSubmit(values);
if (loginType === "account") {
await openLoginCaptcha();
}
})
.catch((error) => {
console.error(error);
});
}}>
</Button>
</ConfigProvider>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
}}>
<Divider plain>
<span
style={{
color: "#CCC",
fontWeight: "normal",
fontSize: 14,
}}>
</span>
</Divider>
<Space align="center" size={24}>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: 40,
width: 40,
border: "1px solid #D4D8DD",
borderRadius: "50%",
}}>
<Tooltip title="QQ登录" color={"blue"}>
<QqOutlined
style={{ ...iconStyles, color: "#1677FF" }}
/>
</Tooltip>
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: 40,
width: 40,
border: "1px solid #D4D8DD",
borderRadius: "50%",
}}>
<Tooltip title="企业微信登录" color={"green"}>
<WechatOutlined
style={{ ...iconStyles, color: "#08a327" }}
/>
</Tooltip>
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: 40,
width: 40,
border: "1px solid #D4D8DD",
borderRadius: "50%",
}}>
<Tooltip title="github登录" color={"black"}>
<GithubOutlined
onClick={() => {
oAuthLogin("github").then();
}}
style={{ ...iconStyles, color: "#333333" }}
/>
</Tooltip>
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: 40,
width: 40,
border: "1px solid #D4D8DD",
borderRadius: "50%",
}}>
<Tooltip title="gitee登录" color={"orange"}>
<GitlabOutlined
onClick={() => {
oAuthLogin("gitee").then();
}}
style={{ ...iconStyles, color: "#FF6A10" }}
/>
</Tooltip>
</div>
</Space>
</div>
</Form>
<a href="/register" className={styles.go_to_register}>
<span></span>
</a>
</Space> </Space>
</div> </Space>
<FooterComponent></FooterComponent>
</div> </div>
</RotateCaptcha> <FooterComponent></FooterComponent>
</div>
); );
}); });

View File

@@ -18,11 +18,15 @@ import { observer } from "mobx-react";
import FooterComponent from "@/components/Footer"; import FooterComponent from "@/components/Footer";
import { getSms, register } from "@/api/user"; import { getSms, register } from "@/api/user";
import { TinyColor } from "@ctrl/tinycolor"; import { TinyColor } from "@ctrl/tinycolor";
import { get, load } from "@/api/captcha";
import RotateCaptcha, { CaptchaInstance, type TicketInfoType } from "react-rotate-captcha";
// import useStore from '@/utils/store/useStore.tsx' // import useStore from '@/utils/store/useStore.tsx'
type LoginType = "phone"; type LoginType = "phone";
export default observer(() => { export default observer(() => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const registerCaptcha = useRef<CaptchaInstance>(null);
const smsCaptcha = useRef<CaptchaInstance>(null);
const captchaRef = useRef<CaptFieldRef | null | undefined>(); const captchaRef = useRef<CaptFieldRef | null | undefined>();
const colors = ["#6253E1", "#04BEFE"]; const colors = ["#6253E1", "#04BEFE"];
const getHoverColors = (colors: string[]) => const getHoverColors = (colors: string[]) =>
@@ -43,16 +47,72 @@ export default observer(() => {
]; ];
const [loginType, setLoginType] = useState<LoginType>("phone"); const [loginType, setLoginType] = useState<LoginType>("phone");
const onSubmit = async (formData: object) => { async function smsVerify(token: string, deg: number): Promise<TicketInfoType> {
const res: any = await register(formData); const phone = form.getFieldValue("phone");
if (res && res.success) { const data: any = {
message.success(res.data); token: token,
deg: deg,
phone: phone,
};
const res: any = await getSms(data);
if (res && res.code === 0) {
message.open({
content: res.data,
type: "success",
duration: 5,
});
} else { } else {
message.error(res.data); message.open({
content: res.data,
type: "warning",
duration: 5,
});
} }
}; return res;
}
async function openRegisterCaptcha() {
registerCaptcha.current!.open();
}
async function openSmsCaptcha() {
smsCaptcha.current!.open();
}
async function registerVerify(token: string, deg: number): Promise<TicketInfoType> {
const userName = form.getFieldValue("username");
const password = form.getFieldValue("password");
const phone = form.getFieldValue("phone");
const activeCode = form.getFieldValue("activeCode");
const data: any = {
token: token,
deg: deg,
userName: userName,
password: password,
phone: phone,
activeCode: activeCode,
};
const res: any = await register(data);
if (res && res.success && res.code === 0) {
message.open({
content: res.data,
type: "success",
duration: 5,
});
} else if (res.code === 0 && !res.success) {
message.open({
content: res.data,
type: "error",
duration: 5,
});
}
return res;
}
return ( return (
<div className={styles.container}> <div className={styles.container}>
<RotateCaptcha get={get} load={load} verify={registerVerify} ref={registerCaptcha} />
<RotateCaptcha get={get} load={load} verify={smsVerify} ref={smsCaptcha} />
<div className={styles.content}> <div className={styles.content}>
<Space> <Space>
<Space className={styles.login_content}> <Space className={styles.login_content}>
@@ -119,7 +179,7 @@ export default observer(() => {
prefix: <UserOutlined className={"prefixIcon"} />, prefix: <UserOutlined className={"prefixIcon"} />,
autoComplete: "off", autoComplete: "off",
}} }}
name="userName" name="username"
placeholder="请输入用户名" placeholder="请输入用户名"
rules={[ rules={[
{ {
@@ -179,13 +239,8 @@ export default observer(() => {
}, },
]} ]}
fieldRef={captchaRef} fieldRef={captchaRef}
onGetCaptcha={async (phone: string) => { onGetCaptcha={async () => {
const res: any = await getSms(phone); await openSmsCaptcha();
if (res && res.success) {
message.success(res.data, 3);
} else {
message.warning(res.data, 3);
}
}} }}
/> />
@@ -261,8 +316,8 @@ export default observer(() => {
]; ];
await form await form
.validateFields(validateFields) .validateFields(validateFields)
.then(async (values) => { .then(async () => {
await onSubmit(values as API.PhoneRegisterRequest); await openRegisterCaptcha();
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);