登录页面
This commit is contained in:
@@ -1,49 +1,119 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { View, Text, Input, Button } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { SmsScene } from '@dukang/shared-types';
|
||||
import { useRouter } from '@tarojs/taro';
|
||||
import {
|
||||
SmsScene,
|
||||
isWxAuthorizeEnabled,
|
||||
type ClientRuntimeConfig,
|
||||
type WechatLoginResult,
|
||||
} from '@dukang/shared-types';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import { finishLoginNavigate } from '../../lib/auth-nav';
|
||||
import { request, saveAuth, toast, type SessionPayload } from '../../lib/api';
|
||||
import './index.css';
|
||||
import { loginWithWechat } from '../../lib/wechat-auth';
|
||||
|
||||
function normalizePhone(value: string) {
|
||||
return value.replace(/\D/g, '').slice(0, 11);
|
||||
}
|
||||
|
||||
function isValidPhone(phone: string) {
|
||||
return /^1[3-9]\d{9}$/.test(phone);
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const [phone, setPhone] = useState('');
|
||||
const [code, setCode] = useState(process.env.TARO_ENV === 'h5' ? '123456' : '');
|
||||
const [cooldown, setCooldown] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
const router = useRouter();
|
||||
const returnTo = router.params.return || '';
|
||||
|
||||
async function sendCode() {
|
||||
if (cooldown > 0) return;
|
||||
const [phone, setPhone] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [wxLoading, setWxLoading] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [cooldown, setCooldown] = useState(0);
|
||||
const [agreed, setAgreed] = useState(true);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [sentHint, setSentHint] = useState('');
|
||||
const [bindMode, setBindMode] = useState(false);
|
||||
const [wxSessionKey, setWxSessionKey] = useState<string | null>(null);
|
||||
const [wxAuthorize, setWxAuthorize] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
request<ClientRuntimeConfig>('/common/client-config')
|
||||
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
||||
.catch(() => setWxAuthorize(true));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (cooldown <= 0) return;
|
||||
const timer = setTimeout(() => setCooldown((c) => Math.max(0, c - 1)), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [cooldown]);
|
||||
|
||||
function ensureAgreed() {
|
||||
if (!agreed) {
|
||||
setMsg('请先勾选并同意用户协议');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function applySessionAndLeave(data: SessionPayload | WechatLoginResult) {
|
||||
if (!data.accessToken) return;
|
||||
saveAuth({
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
});
|
||||
toast('登录成功', 'success');
|
||||
finishLoginNavigate(returnTo);
|
||||
}
|
||||
|
||||
function handleWechatLoginResult(result: WechatLoginResult) {
|
||||
if (result.needBindPhone && result.wxSessionKey) {
|
||||
setBindMode(true);
|
||||
setWxSessionKey(result.wxSessionKey);
|
||||
setMsg('微信授权成功,请绑定手机号完成登录');
|
||||
setSentHint('');
|
||||
return;
|
||||
}
|
||||
if (result.accessToken) {
|
||||
applySessionAndLeave(result);
|
||||
return;
|
||||
}
|
||||
setMsg('微信登录未完成,请重试或使用手机号登录');
|
||||
}
|
||||
|
||||
async function onSendCode() {
|
||||
if (!ensureAgreed()) return;
|
||||
if (cooldown > 0 || sending) return;
|
||||
const normalized = phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(normalized)) {
|
||||
if (!isValidPhone(normalized)) {
|
||||
setMsg('请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
setMsg('');
|
||||
setSentHint('');
|
||||
setSending(true);
|
||||
try {
|
||||
await request('/auth/sms/send', {
|
||||
method: 'POST',
|
||||
data: { phone: normalized, scene: SmsScene.USER_LOGIN },
|
||||
data: {
|
||||
phone: normalized,
|
||||
scene: bindMode ? SmsScene.BIND_PHONE : SmsScene.USER_LOGIN,
|
||||
},
|
||||
});
|
||||
toast('验证码已发送');
|
||||
setCooldown(60);
|
||||
const timer = setInterval(() => {
|
||||
setCooldown((c) => {
|
||||
if (c <= 1) {
|
||||
clearInterval(timer);
|
||||
return 0;
|
||||
}
|
||||
return c - 1;
|
||||
});
|
||||
}, 1000);
|
||||
setSentHint('验证码已发送');
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '发送失败');
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function login() {
|
||||
if (!ensureAgreed()) return;
|
||||
const normalized = phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(normalized)) {
|
||||
if (!isValidPhone(normalized)) {
|
||||
setMsg('请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
@@ -53,14 +123,21 @@ export default function LoginPage() {
|
||||
}
|
||||
setLoading(true);
|
||||
setMsg('');
|
||||
setSentHint('');
|
||||
try {
|
||||
if (bindMode && wxSessionKey) {
|
||||
const data = await request<WechatLoginResult>('/auth/wechat/bind-phone', {
|
||||
method: 'POST',
|
||||
data: { wxSessionKey, phone: normalized, code: code.trim() },
|
||||
});
|
||||
handleWechatLoginResult(data);
|
||||
return;
|
||||
}
|
||||
const data = await request<SessionPayload>('/auth/login/sms', {
|
||||
method: 'POST',
|
||||
data: { phone: normalized, code: code.trim() },
|
||||
});
|
||||
saveAuth(data);
|
||||
toast('登录成功', 'success');
|
||||
Taro.switchTab({ url: '/pages/home/index' });
|
||||
applySessionAndLeave(data);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '登录失败');
|
||||
} finally {
|
||||
@@ -68,47 +145,127 @@ export default function LoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function wechatLogin() {
|
||||
if (!ensureAgreed()) return;
|
||||
setMsg('');
|
||||
setSentHint('');
|
||||
setWxLoading(true);
|
||||
try {
|
||||
const result = await loginWithWechat();
|
||||
handleWechatLoginResult(result);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '微信登录失败');
|
||||
} finally {
|
||||
setWxLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const displayMsg = msg || sentHint;
|
||||
const codeDisabled = cooldown > 0 || sending;
|
||||
|
||||
return (
|
||||
<View className="login-page">
|
||||
<View className="login-brand">
|
||||
<Text className="login-title">杜康好客</Text>
|
||||
<Text className="login-subtitle">用户端小程序</Text>
|
||||
<PageShell variant="plain" className="login-page">
|
||||
<View className="login-header">
|
||||
<View className="login-logo-wrap">
|
||||
<View className="login-logo">
|
||||
<Text>康</Text>
|
||||
</View>
|
||||
<Text className="login-logo-badge">官方</Text>
|
||||
</View>
|
||||
<View className="login-welcome">
|
||||
<Text className="login-welcome-title">欢迎来到杜康好客</Text>
|
||||
<Text className="login-welcome-sub">买美酒,享好礼</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="login-card">
|
||||
<Text className="login-card-title">手机号登录</Text>
|
||||
<Input
|
||||
className="login-input"
|
||||
type="number"
|
||||
maxlength={11}
|
||||
placeholder="请输入手机号"
|
||||
value={phone}
|
||||
onInput={(e) => setPhone(e.detail.value)}
|
||||
/>
|
||||
<View className="login-code-row">
|
||||
<Input
|
||||
className="login-input login-input--code"
|
||||
type="number"
|
||||
maxlength={6}
|
||||
placeholder="验证码"
|
||||
value={code}
|
||||
onInput={(e) => setCode(e.detail.value)}
|
||||
/>
|
||||
<Button className="login-send" disabled={cooldown > 0} onClick={() => void sendCode()}>
|
||||
{cooldown > 0 ? `${cooldown}s` : '获取验证码'}
|
||||
|
||||
<View className="login-main">
|
||||
<View className="login-card">
|
||||
<Text className="login-card-title">
|
||||
{bindMode ? '绑定手机号' : '手机验证码登录'}
|
||||
</Text>
|
||||
|
||||
<View className="login-field">
|
||||
<Text className="login-field-prefix">+86</Text>
|
||||
<Input
|
||||
className="login-field-input"
|
||||
type="number"
|
||||
maxlength={11}
|
||||
placeholder="请输入手机号"
|
||||
value={phone}
|
||||
onInput={(e) => {
|
||||
setPhone(normalizePhone(e.detail.value));
|
||||
setMsg('');
|
||||
setSentHint('');
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="login-field">
|
||||
<Input
|
||||
className="login-field-input"
|
||||
type="number"
|
||||
maxlength={6}
|
||||
placeholder="请输入验证码"
|
||||
value={code}
|
||||
onInput={(e) => setCode(e.detail.value.replace(/\D/g, '').slice(0, 6))}
|
||||
/>
|
||||
<Text
|
||||
className={`login-get-code${codeDisabled ? ' login-get-code--disabled' : ''}`}
|
||||
onClick={() => void onSendCode()}
|
||||
>
|
||||
{sending ? '发送中...' : cooldown > 0 ? `${cooldown}s 后重新获取` : '获取验证码'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{displayMsg ? (
|
||||
<Text className={`login-msg${sentHint && !msg ? ' login-msg--hint' : ''}`}>
|
||||
{displayMsg}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
className="login-sms-btn"
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
onClick={() => void login()}
|
||||
>
|
||||
{loading ? '登录中...' : bindMode ? '绑定并登录' : '登录'}
|
||||
</Button>
|
||||
</View>
|
||||
{msg ? <Text className="login-msg">{msg}</Text> : null}
|
||||
<Button
|
||||
className="u-btn u-btn--primary u-btn--block"
|
||||
loading={loading}
|
||||
onClick={() => void login()}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
<Text className="u-muted" style={{ textAlign: 'center', marginTop: 8 }}>
|
||||
Mock 环境验证码一般为 123456;微信一键登录后续接入
|
||||
</Text>
|
||||
|
||||
{!bindMode && wxAuthorize ? (
|
||||
<>
|
||||
<View className="login-divider">
|
||||
<View className="login-divider-line" />
|
||||
<Text className="login-divider-text">或者</Text>
|
||||
<View className="login-divider-line" />
|
||||
</View>
|
||||
<Button
|
||||
className="login-wechat-btn"
|
||||
loading={wxLoading}
|
||||
disabled={wxLoading}
|
||||
onClick={() => void wechatLogin()}
|
||||
>
|
||||
<Text className="login-wechat-icon">微</Text>
|
||||
<Text>微信一键授权</Text>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="login-footer">
|
||||
<View className="login-agreement" onClick={() => setAgreed((v) => !v)}>
|
||||
<View className={`login-agreement-check${agreed ? ' login-agreement-check--on' : ''}`}>
|
||||
{agreed ? <Text>✓</Text> : null}
|
||||
</View>
|
||||
<Text className="login-agreement-text">
|
||||
我已阅读并同意
|
||||
<Text className="login-agreement-link">《用户协议》</Text>
|
||||
和
|
||||
<Text className="login-agreement-link">《隐私政策》</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user