import md5 from 'md5' import QRCode from 'qrcode' import { checkLoginQrCodeStatus, fetchLoginQrCodeKey, loginWithEmail, loginWithPhone, } from '@/web/api/auth' import Icon from '@/web/components/Icon' import { state } from '@/web/store' import { setCookies } from '@/web/utils/cookie' import { useInterval } from 'react-use' import { cx } from '@emotion/css' import { useState, useMemo, useEffect } from 'react' import toast from 'react-hot-toast' import { useMutation, useQuery } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { useSnapshot } from 'valtio' enum Method { QrCode = 'qrcode', Email = 'email', Phone = 'phone', } const domParser = new DOMParser() // Shared components and methods const EmailInput = ({ email, setEmail, }: { email: string setEmail: (email: string) => void }) => { return (
邮箱
setEmail(e.target.value)} className='w-full rounded-md border border-gray-300 px-2 py-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white' type='email' />
) } const PhoneInput = ({ countryCode, setCountryCode, phone, setPhone, }: { countryCode: string setCountryCode: (code: string) => void phone: string setPhone: (phone: string) => void }) => { return (
手机
= 5 && 'w-20' )} type='text' placeholder='+86' value={countryCode} onChange={e => setCountryCode(e.target.value)} /> setPhone(e.target.value)} />
) } const PasswordInput = ({ password, setPassword, }: { password: string setPassword: (password: string) => void }) => { const [showPassword, setShowPassword] = useState(false) return (
密码
setPassword(e.target.value)} className='w-full rounded-md rounded-r-none border border-r-0 border-gray-300 px-2 py-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white' type={showPassword ? 'text' : 'password'} />
) } const LoginButton = ({ onClick, disabled, }: { onClick: () => void disabled: boolean }) => { // TODO: Add loading indicator return ( ) } const OtherLoginMethods = ({ method, setMethod, }: { method: Method setMethod: (method: Method) => void }) => { const otherLoginMethods: { id: Method name: string }[] = [ { id: Method.QrCode, name: '二维码', }, { id: Method.Email, name: '邮箱', }, { id: Method.Phone, name: '手机', }, ] return ( <>
or
{otherLoginMethods.map( ({ id, name }) => method !== id && ( ) )}
) } const saveCookie = (cookies: string) => { setCookies(cookies) } // Login with Email const LoginWithEmail = () => { const [password, setPassword] = useState('') const [email, setEmail] = useState('') const navigate = useNavigate() const doLogin = useMutation( () => loginWithEmail({ email: email.trim(), md5_password: md5(password.trim()), }), { onSuccess: result => { if (result?.code !== 200) { toast(`Login failed: ${result.code}`) return } saveCookie(result.cookie) navigate(-1) }, onError: error => { toast(`Login failed: ${error}`) }, } ) const handleLogin = () => { if (!email) { toast.error('Please enter email') return } if (!password) { toast.error('Please enter password') return } if ( email.match( /^[^\s@]+@(126|163|yeah|188|vip\.163|vip\.126)\.(com|net)$/ ) == null ) { toast.error('Please use netease email') return } doLogin.mutate() } return ( <> ) } // Login with Phone const LoginWithPhone = () => { const [password, setPassword] = useState('') const [phone, setPhone] = useState('') const countryCode = useSnapshot(state).uiStates.loginPhoneCountryCode const setCountryCode = (countryCode: string) => { state.uiStates.loginPhoneCountryCode = countryCode } const navigate = useNavigate() const doLogin = useMutation( () => { return loginWithPhone({ countrycode: Number(countryCode.replace('+', '').trim()) || 86, phone: phone.trim(), md5_password: md5(password.trim()), }) }, { onSuccess: result => { if (result?.code !== 200) { toast(`Login failed: ${result.code}`) return } saveCookie(result.cookie) navigate(-1) }, onError: error => { toast(`Login failed: ${error}`) }, } ) const handleLogin = () => { if (!countryCode || !Number(countryCode.replace('+', '').trim())) { toast.error('Please enter country code') return } if (!phone) { toast.error('Please enter phone number') return } if (!password) { toast.error('Please enter password') return } doLogin.mutate() } return ( <> ) } // Login with QRCode const LoginWithQRCode = () => { const [qrCodeMessage, setQrCodeMessage] = useState('打开网易云音乐,扫码登录') const [qrCodeImage, setQrCodeImage] = useState('') const navigate = useNavigate() const { data: key = { code: 200, data: { code: 200, unikey: 'Not Ready' } }, status: keyStatus, refetch: refetchKey, } = useQuery( ['qrCodeKey'], async () => { const result = await fetchLoginQrCodeKey() if (result.data.code !== 200) { throw Error(`Failed to fetch QR code key: ${result.data.code}`) } return result }, { retry: true, retryDelay: 500, } ) useInterval(async () => { if (keyStatus !== 'success') return const qrCodeStatus = await checkLoginQrCodeStatus({ key: key.data.unikey }) switch (qrCodeStatus.code) { case 800: refetchKey() break case 801: setQrCodeMessage('打开网易云音乐,扫码登录') break case 802: setQrCodeMessage('等待确认') break case 803: if (qrCodeStatus.cookie === undefined) { toast('checkLoginQrCodeStatus returned 803 without cookie') break } saveCookie(qrCodeStatus.cookie) navigate(-1) break } }, 1000) const qrCodeUrl = useMemo( () => `https://music.163.com/login?codekey=${key.data.unikey}`, [key] ) useEffect(() => { const updateImage = async () => { const svg = await QRCode.toString(qrCodeUrl, { margin: 0, color: { light: '#ffffff00', }, type: 'svg', }) const path = domParser .parseFromString(svg, 'text/xml') .getElementsByTagName('path')[0] setQrCodeImage(path?.getAttribute('d') ?? '') } updateImage() }, [qrCodeUrl]) return (
{qrCodeMessage}
) } export default function Login() { const [method, setMethod] = useState(Method.Phone) return (
{method === Method.Email && } {method === Method.Phone && } {method === Method.QrCode && }
) }