Files
dukang/apps/h5-partner/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

232 lines
7.6 KiB
TypeScript

import { useRef, useState, type RefObject } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import {
getLastPhone,
getPartnerProfile,
request,
saveRememberedSession,
type PartnerSessionPayload,
} from '../lib/api';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
import { partnerHomePath } from '../lib/partnerAccess';
const REMEMBER_PHONE_KEY = 'partner_remember_phone';
const REMEMBER_FLAG_KEY = 'partner_remember_account';
function AgreementCheckbox({
agreed,
onChange,
inputRef,
}: {
agreed: boolean;
onChange: (next: boolean) => void;
inputRef?: RefObject<HTMLLabelElement>;
}) {
return (
<label className="partner-checkbox-row partner-checkbox-row--agreement" ref={inputRef}>
<input type="checkbox" checked={agreed} onChange={(e) => onChange(e.target.checked)} />
<span>
我已阅读并同意{' '}
<Link to="/legal/user-agreement" className="text-primary" style={{ fontWeight: 600 }} onClick={(e) => e.stopPropagation()}>
《用户协议》
</Link>{' '}
{' '}
<Link to="/legal/privacy-policy" className="text-primary" style={{ fontWeight: 600 }} onClick={(e) => e.stopPropagation()}>
《隐私政策》
</Link>
</span>
</label>
);
}
function loadRememberedPhone(): { phone: string; remember: boolean } {
try {
const remember = localStorage.getItem(REMEMBER_FLAG_KEY) === '1';
const phone = remember ? localStorage.getItem(REMEMBER_PHONE_KEY) || '' : '';
return { phone, remember };
} catch {
return { phone: '', remember: false };
}
}
function formatPartnerError(e: unknown): string {
const text = e instanceof Error ? e.message : '操作失败';
if (text.includes('合伙人账号不存在') || text.includes('未找到合伙人账号')) {
return '未找到合伙人账号';
}
if (text.includes('合伙人账号已停用')) {
return '合伙人账号已停用';
}
return text;
}
export default function LoginPage() {
const navigate = useNavigate();
const { applySession, refresh, account } = usePartnerSession();
const savedProfile = getPartnerProfile();
const remembered = loadRememberedPhone();
const [phone, setPhone] = useState(remembered.phone || getLastPhone());
const [code, setCode] = useState('');
const [rememberAccount, setRememberAccount] = useState(remembered.remember);
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;
}
function persistRememberAccount(nextPhone: string) {
try {
if (rememberAccount) {
localStorage.setItem(REMEMBER_FLAG_KEY, '1');
localStorage.setItem(REMEMBER_PHONE_KEY, nextPhone);
} else {
localStorage.removeItem(REMEMBER_FLAG_KEY);
localStorage.removeItem(REMEMBER_PHONE_KEY);
}
} catch {
/* ignore */
}
}
async function sendCode() {
if (!ensureAgreed()) return;
setMsg('');
try {
await request('PARTNER_H5', '/partner/auth/sms/send', {
method: 'POST',
body: JSON.stringify({ phone, scene: 'PARTNER_LOGIN' }),
silent: true,
});
setMsg('验证码已发送');
setCodeCooldown(60);
const timer = setInterval(() => {
setCodeCooldown((s) => {
if (s <= 1) {
clearInterval(timer);
return 0;
}
return s - 1;
});
}, 1000);
} catch (e) {
setMsg(formatPartnerError(e));
}
}
async function finishLoginNavigate() {
const me = await refresh();
navigate(partnerHomePath(me ?? account ?? savedProfile));
}
async function login() {
if (!ensureAgreed()) return;
setLoading(true);
setMsg('');
try {
const data = await request<PartnerSessionPayload>('PARTNER_H5', '/partner/auth/login/sms', {
method: 'POST',
body: JSON.stringify({ phone, code }),
silent: true,
});
saveRememberedSession(data);
applySession(data);
persistRememberAccount(phone);
await finishLoginNavigate();
} catch (e) {
setMsg(formatPartnerError(e));
} finally {
setLoading(false);
}
}
return (
<div className="partner-auth-page">
<div className="partner-auth-brand">
<div className="partner-auth-logo-circle">
<span className="material-symbols-outlined">wine_bar</span>
</div>
<h1 className="partner-auth-title">杜康好客</h1>
<p className="partner-auth-subtitle">城市合伙人端</p>
</div>
<main className="partner-auth-card">
<h2 className="partner-auth-card-title">城市合伙人登录</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div className="partner-input-wrap">
<span className="material-symbols-outlined partner-input-icon">smartphone</span>
<input
className="partner-input"
type="tel"
maxLength={11}
placeholder="请输入手机号"
value={phone}
onChange={(e) => {
setPhone(e.target.value);
setMsg('');
}}
/>
</div>
<div className="partner-input-row">
<div className="partner-input-wrap">
<span className="material-symbols-outlined partner-input-icon">shield</span>
<input
className="partner-input"
type="text"
maxLength={6}
placeholder="验证码"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
</div>
<button type="button" className="partner-code-btn" onClick={sendCode} disabled={codeCooldown > 0}>
{codeCooldown > 0 ? `${codeCooldown}s` : '获取验证码'}
</button>
</div>
<label className="partner-remember-row">
<input
type="checkbox"
checked={rememberAccount}
onChange={(e) => setRememberAccount(e.target.checked)}
/>
<span>记住账号</span>
</label>
<AgreementCheckbox
agreed={agreed}
onChange={setAgreed}
inputRef={agreementRef}
/>
{msg && <p className="partner-auth-msg" style={{ color: 'var(--color-error, #d33)', fontSize: 13, textAlign: 'center' }}>{msg}</p>}
<button type="button" className="partner-btn-primary" onClick={() => void login()} disabled={loading}>
<span>{loading ? '登录中...' : '登录'}</span>
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
</button>
<p className="partner-auth-msg" style={{ textAlign: 'center' }}>
手机号验证成功后,7 天内无需再次输入验证码
</p>
</div>
</main>
<footer className="partner-auth-footer">
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span className="material-symbols-outlined">verified_user</span>
<span className="label-md" style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase' }}>SECURED BY DUKANG HERITAGE</span>
</div>
</footer>
</div>
);
}