83ed90ef67
CI / verify (pull_request) Has been cancelled
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>
190 lines
5.8 KiB
TypeScript
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>
|
|
);
|
|
}
|