登录页面

This commit is contained in:
2026-07-12 15:17:54 +08:00
parent becf91da52
commit bfac6c6663
63 changed files with 4662 additions and 431 deletions
+221 -64
View File
@@ -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>
);
}