diff --git a/package.json b/package.json index 420790f..686df70 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "next": "14.1.3", "nookies": "^2.5.2", "qrcode": "^1.5.3", + "qrcode.react": "^3.1.0", "qs": "^6.12.0", "react": "^18", "react-dom": "^18", diff --git a/public/img/logo_qrcode.svg b/public/img/logo_qrcode.svg new file mode 100644 index 0000000..b8478f2 --- /dev/null +++ b/public/img/logo_qrcode.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/Header/Auth.tsx b/src/components/Header/Auth.tsx index 6924665..b4bf865 100644 --- a/src/components/Header/Auth.tsx +++ b/src/components/Header/Auth.tsx @@ -2,11 +2,11 @@ import { useShallow } from 'zustand/react/shallow'; -import { LoginProvider } from '@/components/Login/loginContext'; +import { LoginProvider } from '@/components/Login/LoginContext'; import HeaderAvatar from './HeaderAvatar'; -import { LoginModal } from '@/components'; +import { Login } from '@/components'; import { useUserStore } from '@/store'; export default function Auth() { @@ -18,7 +18,7 @@ export default function Auth() { ) : ( - + )} diff --git a/src/components/Login/Footer.tsx b/src/components/Login/Footer.tsx new file mode 100644 index 0000000..c629549 --- /dev/null +++ b/src/components/Login/Footer.tsx @@ -0,0 +1,21 @@ +export default function Footer() { + return ( +
+ 注册登录即代表同意 + + 《注册协议》 + + + 《隐私政策》 + +
+ ); +} diff --git a/src/components/Login/Login.tsx b/src/components/Login/Login.tsx new file mode 100644 index 0000000..4e01d0f --- /dev/null +++ b/src/components/Login/Login.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useState, useContext } from 'react'; + +import { Dialog, DialogContent, DialogTrigger, DialogClose } from '@/components/ui/dialog'; + +import Footer from './Footer'; +import { LoginContext } from './LoginContext'; +import LoginPhone from './LoginPhone'; +import LoginQrcode from './LoginQrcode'; +import Tab from './Tab'; + +import { IconClose } from '@/components'; + +export default function Login() { + const { state, dispatch } = useContext(LoginContext); + const [currentTab, setCurrent] = useState('qrCode'); + + return ( + dispatch({ type: 'SHOW_LOGIN', payload: open })}> + + + + + e.preventDefault()}> + setCurrent(value)} /> + + + + + +
+ {currentTab === 'qrCode' && } + {currentTab === 'phone' && } +
+ +
+ +
+ ); +} diff --git a/src/components/Login/loginContext.tsx b/src/components/Login/LoginContext.tsx similarity index 100% rename from src/components/Login/loginContext.tsx rename to src/components/Login/LoginContext.tsx diff --git a/src/components/Login/LoginForm.tsx b/src/components/Login/LoginForm.tsx index 4d63709..5b08008 100644 --- a/src/components/Login/LoginForm.tsx +++ b/src/components/Login/LoginForm.tsx @@ -124,7 +124,7 @@ export default function LoginForm({ className }: { className?: string }) {
-
+
@@ -159,7 +159,7 @@ export default function LoginForm({ className }: { className?: string }) { - - - e.preventDefault()}> - {/* title */} -
-

登录雀乐

-

- 输入手机号,开启雀乐;未注册手机,将自动注册 -

-
- - - - - - {/* form */} - - -
- 注册登录即代表同意 - - 《注册协议》 - - - 《隐私政策》 - -
-
- - ); -} diff --git a/src/components/Login/LoginPhone.tsx b/src/components/Login/LoginPhone.tsx new file mode 100644 index 0000000..90371d0 --- /dev/null +++ b/src/components/Login/LoginPhone.tsx @@ -0,0 +1,16 @@ +'use client'; +import LoginForm from './LoginForm'; + +export default function LoginPhone() { + return ( +
+ {/* title */} +

+ 输入手机号,开启雀乐;未注册手机,将自动注册 +

+ + {/* form */} + +
+ ); +} diff --git a/src/components/Login/LoginQrcode.tsx b/src/components/Login/LoginQrcode.tsx new file mode 100644 index 0000000..db3e10c --- /dev/null +++ b/src/components/Login/LoginQrcode.tsx @@ -0,0 +1,134 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; + +import { useRouter } from 'next/navigation'; +import { QRCodeSVG } from 'qrcode.react'; +import { useShallow } from 'zustand/react/shallow'; + +import { useToast } from '@/components/ui/use-toast'; + +import { saveToken, useLoginRedirect } from '@/hooks'; +import { apiGetUUID, apiCheckQr, apiGetLoginQr } from '@/services'; +import { useUserStore } from '@/store'; + +export default function LoginQrcode() { + const redirect = useLoginRedirect(); + const router = useRouter(); + const uuidRef = useRef(''); + const { toast } = useToast(); + + const [qrCodeState, setQrCodeState] = useState<0 | 1 | 2>(0); // 0-未扫码 1-已扫码 2-已过期 + const [qrCodeValue, setQrCodeValue] = useState(''); + const intervalIdRef = useRef(null); + + const { setShowLogin, getUserInfo } = useUserStore( + useShallow((state) => ({ + setShowLogin: state.setShowLogin, + getUserInfo: state.getUserInfo, + })), + ); + + const getUUID = async () => { + const result = await apiGetUUID(); + + if (result.code === 200) { + uuidRef.current = result.data; + getQrcode(result.data); + return result.data; + } + }; + + const getQrcode = async (id: string) => { + const res = await apiGetLoginQr(id); + if (res.code === 200) { + setQrCodeState(0); + setQrCodeValue(res.data); + handleCountDown(); + } + }; + + const handleCountDown = () => { + intervalIdRef.current = window.setInterval(() => { + checkLoginStatus(); + }, 3000); + }; + + const handleResetQrcode = () => { + setQrCodeValue(''); + setQrCodeState(0); + uuidRef.current = ''; + + getUUID(); + }; + + const checkLoginStatus = async () => { + const res = await apiCheckQr(uuidRef.current); + if (res.code === 10000) { + // 过期 + setQrCodeState(2); + intervalIdRef.current !== null && window.clearInterval(intervalIdRef.current); + } else if (res.code !== 200) { + // 请求出错 + uuidRef.current = ''; + setQrCodeValue(''); + uuidRef.current = ''; + return; + } else if (res.data === '0') { + // 未扫码 + return; + } else if (res.data === '1') { + setQrCodeState(1); + } else { + // 扫码成功 + saveToken(res.data); + setShowLogin(false); + await getUserInfo(); + toast({ + description: '登录成功', + duration: 1500, + type: 'background', + }); + if (redirect) router.replace(redirect); + } + }; + + useEffect(() => { + getUUID(); + + return () => { + if (intervalIdRef.current !== null) { + window.clearInterval(intervalIdRef.current); + } + }; + }, []); + + return ( +
+ {/* title */} +
通过雀乐APP扫码登录
+ +
+ {!qrCodeValue &&
加载中...
} + + {!!qrCodeValue && qrCodeState !== 2 && ( + + )} + + {!!qrCodeValue && qrCodeState === 2 && ( +
+
二维码已过期
+
+ 重新获取 +
+
+ )} +
+
+ ); +} diff --git a/src/components/Login/Tab.tsx b/src/components/Login/Tab.tsx new file mode 100644 index 0000000..3963b94 --- /dev/null +++ b/src/components/Login/Tab.tsx @@ -0,0 +1,30 @@ +export default function Tab({ + className, + value, + changeTab, +}: { + className?: string; + value: string; + changeTab: (value: string) => void; +}) { + const tabs = [ + { name: 'qrCode', label: '扫码登录' }, + { name: 'phone', label: '手机登录' }, + ]; + + return ( +
+ {tabs.map((tab) => { + return ( +
changeTab(tab.name)} + > + {tab.label} +
+ ); + })} +
+ ); +} diff --git a/src/components/index.ts b/src/components/index.ts index c2d5b95..71f7f67 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -15,7 +15,7 @@ export { default as Category } from './Category'; export { default as ContributorCard } from './ContributorCard'; // Login -export { default as LoginModal } from './Login/LoginModal'; +export { default as Login } from './Login/Login'; export { default as LoginForm } from './Login/LoginForm'; export { default as RedirectCheck } from './Login/RedirectCheck'; diff --git a/src/hooks/useLogin.ts b/src/hooks/useLogin.ts index 5199f73..c3748db 100644 --- a/src/hooks/useLogin.ts +++ b/src/hooks/useLogin.ts @@ -3,6 +3,11 @@ import { jwtDecode } from 'jwt-decode'; import { apiUserLogin, apiAutoLogin } from '@/services'; import { setAccessToken, getAuthorization, clearAccessToken, getDeviceId } from '@/utils'; +export const saveToken = async (token: string) => { + const { exp } = parseJWT(token); + setAccessToken({ token, expires: exp.toString() }); +}; + export const SmsLogin = async ({ phone, authCode, @@ -15,9 +20,7 @@ export const SmsLogin = async ({ // 登录成功后,将token存入localStorage const result = await apiUserLogin({ mobile: phone, mobileCheckCode: authCode, deviceId }); if (result.code === 200) { - const jwt = result.data; - const { exp } = parseJWT(jwt); - setAccessToken({ token: jwt, expires: exp.toString() }); + saveToken(result.data); } return result; }; diff --git a/src/services/client/user.ts b/src/services/client/user.ts index 321d92b..759f1eb 100644 --- a/src/services/client/user.ts +++ b/src/services/client/user.ts @@ -61,3 +61,27 @@ export const apiGetMyUserInfo = async () => { const res: FetchResponse = await clientHttp.get('/queyueapi/user/my/userInfo'); return res; }; + +/** + * @description 获取UUID + */ +export const apiGetUUID = async () => { + const res: FetchResponse = await clientHttp.get('/queyueapi/user/user/uuid'); + return res; +}; + +/** + * @description 校验uuid对应的请求认证状态 + */ +export const apiCheckQr = async (uuid: string) => { + const res: FetchResponse = await clientHttp.get(`/queyueapi/user/user/check/qr?uuid=${uuid}`); + return res; +}; + +/** + * @description 获取登录二维码 + */ +export const apiGetLoginQr = async (uuid: string) => { + const res: FetchResponse = await clientHttp.get(`/queyueapi/user/user/getLoginQr?uuid=${uuid}`); + return res; +}; diff --git a/src/store/audio.ts b/src/store/audio.ts index 7b21a6e..d9f0c15 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep'; import { create } from 'zustand'; import { devtools, persist, createJSONStorage } from 'zustand/middleware'; -import { openLogin } from '@/components/Login/loginContext'; +import { openLogin } from '@/components/Login/LoginContext'; import { apiGetSongRandom } from '@/services'; import { clearAccessToken, checkAuthOffline } from '@/utils'; diff --git a/src/types/login.d.ts b/src/types/login.d.ts new file mode 100644 index 0000000..5e5f2e3 --- /dev/null +++ b/src/types/login.d.ts @@ -0,0 +1,6 @@ +declare namespace Login { + export interface Tabs { + name: string; + label: string; + } +} diff --git a/src/utils/request/client/utils.ts b/src/utils/request/client/utils.ts index 92144a2..5abf0e5 100644 --- a/src/utils/request/client/utils.ts +++ b/src/utils/request/client/utils.ts @@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep'; import qs from 'qs'; -import { openLogin } from '@/components/Login/loginContext'; +import { openLogin } from '@/components/Login/LoginContext'; import { toast } from '@/components/ui/use-toast'; import { IQueryParams, IResponse } from '@/utils/request/type'; diff --git a/yarn.lock b/yarn.lock index 56b688a..14cb098 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5692,6 +5692,11 @@ punycode@^2.1.0: resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +qrcode.react@^3.1.0: + version "3.1.0" + resolved "https://mirrors.cloud.tencent.com/npm/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8" + integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q== + qrcode@^1.5.3: version "1.5.3" resolved "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.3.tgz#03afa80912c0dccf12bc93f615a535aad1066170"