Files
dukang/apps/mini-user/src/pages/login/index.tsx
T

274 lines
8.4 KiB
TypeScript

import { useEffect, useState } from 'react';
import { View, Text, Input, Button } from '@tarojs/components';
import { useRouter } from '@tarojs/taro';
import {
SmsScene,
isWxAuthorizeEnabled,
type ClientRuntimeConfig,
type WechatLoginResult,
} from '@dukang/shared-types';
import PageShell from '../../components/PageShell';
import WechatLoginButton from '../../components/WechatLoginButton';
import { finishLoginNavigate } from '../../lib/auth-nav';
import { request, saveAuth, toast, type SessionPayload } from '../../lib/api';
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 router = useRouter();
const returnTo = router.params.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 (!isValidPhone(normalized)) {
setMsg('请输入正确的手机号');
return;
}
setMsg('');
setSentHint('');
setSending(true);
try {
await request('/auth/sms/send', {
method: 'POST',
data: {
phone: normalized,
scene: bindMode ? SmsScene.BIND_PHONE : SmsScene.USER_LOGIN,
},
});
setCooldown(60);
setSentHint('验证码已发送');
} catch (e) {
setMsg(e instanceof Error ? e.message : '发送失败');
} finally {
setSending(false);
}
}
async function login() {
if (!ensureAgreed()) return;
const normalized = phone.trim();
if (!isValidPhone(normalized)) {
setMsg('请输入正确的手机号');
return;
}
if (!code.trim()) {
setMsg('请输入验证码');
return;
}
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() },
});
applySessionAndLeave(data);
} catch (e) {
setMsg(e instanceof Error ? e.message : '登录失败');
} finally {
setLoading(false);
}
}
async function wechatLogin() {
if (!ensureAgreed()) return;
setMsg('');
setSentHint('');
setWxLoading(true);
try {
const result = await loginWithWechat();
handleWechatLoginResult(result);
} catch (e) {
const raw = e instanceof Error ? e.message : '微信登录失败';
const hint = /invalid code/i.test(raw)
? '微信 code 无效:请确认后端 WX_MINI_APP_ID 与小程序 appid 一致,或本地联调使用 MOCK_WECHAT'
: raw;
setMsg(hint);
} finally {
setWxLoading(false);
}
}
const displayMsg = msg || sentHint;
const codeDisabled = cooldown > 0 || sending;
const showWechatLogin =
!bindMode && (process.env.TARO_ENV === 'weapp' || wxAuthorize);
return (
<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-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>
{showWechatLogin ? (
<>
<View className="login-divider">
<View className="login-divider-line" />
<Text className="login-divider-text">或者</Text>
<View className="login-divider-line" />
</View>
<WechatLoginButton
loading={wxLoading}
onClick={() => void wechatLogin()}
/>
</>
) : null}
</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>
);
}