Files
dukang/apps/h5-shop/src/pages/LoginPage.tsx
T
jacy 83ed90ef67
CI / verify (pull_request) Has been cancelled
feat(h5-auth): remember SMS sessions and simplify media access
Hide WeChat login for shop and partner users, keep verified sessions for seven days, and allow JSSDK camera or album access without an OpenID binding.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 10:41:17 +08:00

190 lines
5.8 KiB
TypeScript

import { useRef, useState, type RefObject } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage';
import { useStoreSession } from '../contexts/StoreSessionContext';
import {
getLastPhone,
request,
saveRememberedSession,
type ShopSessionPayload,
} from '../lib/api';
import { routeAfterShopLogin } from './SelectStorePage';
function ShopAgreementCheckbox({
agreed,
onChange,
labelRef,
}: {
agreed: boolean;
onChange: (next: boolean) => void;
labelRef?: RefObject<HTMLLabelElement>;
}) {
return (
<label className="shop-login-agreement" ref={labelRef}>
<input
type="checkbox"
checked={agreed}
onChange={(e) => onChange(e.target.checked)}
/>
<span>
我已阅读并同意
<Link to="/legal/user-agreement" onClick={(e) => e.stopPropagation()}>
《用户协议》
</Link>
<Link to="/legal/privacy-policy" onClick={(e) => e.stopPropagation()}>
《隐私政策》
</Link>
</span>
</label>
);
}
export default function LoginPage() {
const navigate = useNavigate();
const { applySession } = useStoreSession();
const [phone, setPhone] = useState(getLastPhone());
const [code, setCode] = useState('');
const [agreed, setAgreed] = useState(false);
const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState('');
const [codeCooldown, setCodeCooldown] = useState(0);
const agreementRef = useRef<HTMLLabelElement>(null);
function ensureAgreed() {
if (!agreed) {
setMsg('请先阅读并同意用户协议');
agreementRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
return false;
}
return true;
}
async function sendCode() {
if (!ensureAgreed()) return;
setMsg('');
try {
await request('SHOP_H5', '/shop/auth/sms/send', {
method: 'POST',
body: JSON.stringify({ phone, scene: 'STORE_LOGIN' }),
});
setMsg('验证码已发送');
setCodeCooldown(60);
const timer = setInterval(() => {
setCodeCooldown((c) => {
if (c <= 1) {
clearInterval(timer);
return 0;
}
return c - 1;
});
}, 1000);
} catch (e) {
setMsg(e instanceof Error ? e.message : '发送失败');
}
}
async function login() {
if (!ensureAgreed()) return;
setLoading(true);
setMsg('');
try {
const data = await request<ShopSessionPayload>('SHOP_H5', '/shop/auth/login/sms', {
method: 'POST',
body: JSON.stringify({ phone, code }),
});
saveRememberedSession(data);
applySession(data);
routeAfterShopLogin(data, navigate);
} catch (e) {
setMsg(e instanceof Error ? e.message : '登录失败');
} finally {
setLoading(false);
}
}
return (
<div className="shop-login-page">
<header className="shop-login-hero">
<div className="shop-login-logo-wrap">
<AppImage src="/logo.png" alt="杜康好客" wrapperClassName="app-image--fill" fit="contain" />
</div>
<h1 className="shop-login-brand">杜康好客</h1>
<p className="shop-login-tagline">门店管理系统</p>
</header>
<main className="shop-login-main">
<div className="shop-login-card">
<div className="shop-login-field">
<label htmlFor="phone">手机号码</label>
<div className="shop-login-input-wrap">
<span className="material-symbols-outlined">phone_iphone</span>
<input
id="phone"
type="tel"
maxLength={11}
placeholder="请输入您的手机号"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</div>
</div>
<div className="shop-login-field">
<label htmlFor="code">验证码</label>
<div className="shop-login-code-row">
<div className="shop-login-input-wrap">
<span className="material-symbols-outlined">shield</span>
<input
id="code"
type="text"
maxLength={6}
placeholder="验证码"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
</div>
<button
type="button"
className="shop-login-code-btn"
disabled={codeCooldown > 0}
onClick={sendCode}
>
{codeCooldown > 0 ? `${codeCooldown}s` : '获取验证码'}
</button>
</div>
</div>
{msg && <p className="shop-login-msg">{msg}</p>}
<ShopAgreementCheckbox
agreed={agreed}
onChange={setAgreed}
labelRef={agreementRef}
/>
<button
type="button"
className="shop-login-submit"
disabled={loading}
onClick={() => void login()}
>
<span>{loading ? '登录中...' : '登录'}</span>
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
</button>
<p className="shop-login-msg" style={{ textAlign: 'center', marginTop: 12 }}>
手机号验证成功后,7 天内无需再次输入验证码
</p>
</div>
</main>
<footer className="shop-login-footer">
<p className="shop-login-footer-brand">Secured by DUKANG HERITAGE</p>
<span className="material-symbols-outlined shop-fill-icon" style={{ opacity: 0.3, fontSize: 20, color: 'var(--color-outline)' }}>
security
</span>
</footer>
</div>
);
}