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>
232 lines
7.6 KiB
TypeScript
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>
|
|
);
|
|
}
|