cc750dc1d6
- mini-user 门店座机电话中间四位脱敏(保留区号与前后位) - mini-user 套餐详情图增加 1/N 计数与自动轮播 - h5-partner 账号暂停(DISABLED)禁止登录并提示客服 - h5-shop 提现/筛选栏左右留白与页面一致 - h5-shop 休息中核销改为弹「是否开启营业」并支持开张后继续 Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
449 lines
16 KiB
TypeScript
449 lines
16 KiB
TypeScript
import { useEffect, useRef, useState, type RefObject } from 'react';
|
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
|
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
|
|
import {
|
|
getLastPhone,
|
|
getPartnerProfile,
|
|
hasPartnerWxSession,
|
|
request,
|
|
saveRememberedSession,
|
|
type PartnerSessionPayload,
|
|
} from '../lib/api';
|
|
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
|
import { partnerHomePath } from '../lib/partnerAccess';
|
|
import type { PartnerPhoneCheckResponse } from '@dukang/shared-types';
|
|
import {
|
|
bindPartnerWechatAfterSmsLogin,
|
|
checkPartnerPhoneForLogin,
|
|
fetchClientConfig,
|
|
loginPartnerWithWechat,
|
|
} from '../lib/wechat-auth';
|
|
import { toastError } from '../lib/toast';
|
|
import { isWechatEnv } from '../lib/weixin';
|
|
|
|
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 maskPhone(phone: string) {
|
|
if (phone.length < 7) return phone;
|
|
return `${phone.slice(0, 3)} **** ${phone.slice(-4)}`;
|
|
}
|
|
|
|
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('合伙人账号已停用') ||
|
|
text.includes('账号已停用') ||
|
|
text.includes('暂停使用')
|
|
) {
|
|
return '该账号已暂停使用,请联系客服人员';
|
|
}
|
|
return text;
|
|
}
|
|
|
|
function formatWechatError(e: unknown): string {
|
|
const text = e instanceof Error ? e.message : '微信登录失败';
|
|
if (
|
|
text.includes('合伙人账号已停用') ||
|
|
text.includes('账号已停用') ||
|
|
text.includes('暂停使用')
|
|
) {
|
|
return '该账号已暂停使用,请联系客服人员';
|
|
}
|
|
if (text.includes('首次登录') || text.includes('手机验证码')) {
|
|
return '该微信尚未绑定合伙人账号,请先使用手机验证码登录,登录后将自动关联微信';
|
|
}
|
|
return text;
|
|
}
|
|
|
|
export default function LoginPage() {
|
|
const navigate = useNavigate();
|
|
const { applySession, refresh, account } = usePartnerSession();
|
|
const [params] = useSearchParams();
|
|
const quick = params.get('quick') === '1';
|
|
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 [wxLoading, setWxLoading] = useState(false);
|
|
const [msg, setMsg] = useState('');
|
|
const [codeCooldown, setCodeCooldown] = useState(0);
|
|
const [wxAuthorize, setWxAuthorize] = useState(false);
|
|
const agreementRef = useRef<HTMLLabelElement>(null);
|
|
|
|
useEffect(() => {
|
|
fetchClientConfig()
|
|
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
|
.catch(() => setWxAuthorize(false));
|
|
}, []);
|
|
|
|
const quickName = savedProfile?.name ?? '城市合伙人';
|
|
const quickCompany = savedProfile?.companyName ?? '';
|
|
const quickPhone = savedProfile?.phone || phone;
|
|
|
|
function ensureAgreed() {
|
|
if (!agreed) {
|
|
const tip = '请先勾选并同意用户协议';
|
|
setMsg(tip);
|
|
toastError(tip);
|
|
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;
|
|
const trimmedPhone = phone.trim();
|
|
if (!trimmedPhone) {
|
|
setMsg('请输入手机号');
|
|
return;
|
|
}
|
|
if (!/^1\d{10}$/.test(trimmedPhone)) {
|
|
setMsg('请输入正确的手机号');
|
|
return;
|
|
}
|
|
setMsg('');
|
|
try {
|
|
await checkPartnerPhoneForLogin(trimmedPhone);
|
|
await request('PARTNER_H5', '/partner/auth/sms/send', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ phone: trimmedPhone, 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;
|
|
const trimmedPhone = phone.trim();
|
|
const trimmedCode = code.trim();
|
|
if (!trimmedPhone) {
|
|
setMsg('请输入手机号');
|
|
return;
|
|
}
|
|
if (!/^1\d{10}$/.test(trimmedPhone)) {
|
|
setMsg('请输入正确的手机号');
|
|
return;
|
|
}
|
|
if (!trimmedCode) {
|
|
setMsg('请输入验证码');
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
setMsg('');
|
|
try {
|
|
await checkPartnerPhoneForLogin(trimmedPhone);
|
|
const data = await request<PartnerSessionPayload>('PARTNER_H5', '/partner/auth/login/sms', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ phone: trimmedPhone, code: trimmedCode }),
|
|
silent: true,
|
|
});
|
|
saveRememberedSession(data);
|
|
applySession(data);
|
|
persistRememberAccount(phone);
|
|
if (isWechatEnv() && wxAuthorize) {
|
|
setMsg('登录成功,正在关联微信…');
|
|
await bindPartnerWechatAfterSmsLogin();
|
|
return;
|
|
}
|
|
await finishLoginNavigate();
|
|
} catch (e) {
|
|
setMsg(formatPartnerError(e));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function wechatLogin() {
|
|
if (!ensureAgreed()) return;
|
|
setMsg('');
|
|
if (!isWechatEnv()) {
|
|
setMsg(WECHAT_INAPP_REQUIRED_MSG);
|
|
toastError(WECHAT_INAPP_REQUIRED_MSG);
|
|
return;
|
|
}
|
|
setWxLoading(true);
|
|
try {
|
|
// 直接发起 OAuth;未绑定账号由后端返回「请先短信登录」提示
|
|
const session = await loginPartnerWithWechat();
|
|
if (session) {
|
|
applySession(session);
|
|
await finishLoginNavigate();
|
|
return;
|
|
}
|
|
if (session === null) {
|
|
const tip = '微信授权暂未开启,请使用验证码登录';
|
|
setMsg(tip);
|
|
toastError(tip);
|
|
}
|
|
// session === undefined:已跳转微信授权页
|
|
} catch (e) {
|
|
const tip = formatWechatError(e);
|
|
setMsg(tip);
|
|
toastError(tip);
|
|
} finally {
|
|
setWxLoading(false);
|
|
}
|
|
}
|
|
|
|
if (quick) {
|
|
const canWechatQuick =
|
|
wxAuthorize &&
|
|
isWechatEnv() &&
|
|
hasPartnerWxSession() &&
|
|
!!savedProfile &&
|
|
savedProfile.hasWechat === true;
|
|
|
|
return (
|
|
<div className="partner-auth-page partner-auth-page--quick">
|
|
<header className="partner-auth-brand">
|
|
<div className="partner-quick-avatar" style={{ width: 120, height: 120, margin: '0 auto 16px' }}>
|
|
<span className="material-symbols-outlined" style={{ fontSize: 48 }}>wine_bar</span>
|
|
</div>
|
|
<h1 className="partner-auth-title" style={{ fontSize: 20 }}>杜康好客</h1>
|
|
<p className="partner-auth-subtitle" style={{ fontSize: 12, letterSpacing: '0.2em', textTransform: 'uppercase' }}>城市合伙人端</p>
|
|
</header>
|
|
|
|
<section className="partner-glass-card">
|
|
<div className="partner-quick-badge">已识别账号</div>
|
|
<div className="partner-quick-avatar">
|
|
<span className="material-symbols-outlined">person</span>
|
|
</div>
|
|
<div style={{ textAlign: 'center' }}>
|
|
<h2 className="headline-md">
|
|
{quickName}
|
|
{quickCompany ? (
|
|
<span className="text-muted body-md" style={{ fontWeight: 400 }}> ({quickCompany})</span>
|
|
) : null}
|
|
</h2>
|
|
<p className="text-muted body-md" style={{ letterSpacing: '0.1em', marginTop: 4 }}>
|
|
{quickPhone ? maskPhone(quickPhone) : '暂无已保存账号'}
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<nav style={{ width: '100%', maxWidth: 384, marginTop: 16 }}>
|
|
{msg && <p className="partner-auth-msg" style={{ color: 'var(--color-error, #d33)', fontSize: 13, textAlign: 'center', marginBottom: 12 }}>{msg}</p>}
|
|
<AgreementCheckbox
|
|
agreed={agreed}
|
|
onChange={setAgreed}
|
|
inputRef={agreementRef}
|
|
/>
|
|
{canWechatQuick ? (
|
|
<button
|
|
type="button"
|
|
className="partner-btn-primary"
|
|
style={{ marginTop: 16 }}
|
|
disabled={wxLoading}
|
|
onClick={() => void wechatLogin()}
|
|
>
|
|
<span className="material-symbols-outlined">chat</span>
|
|
<span>{wxLoading ? '登录中...' : '微信一键登录'}</span>
|
|
</button>
|
|
) : (
|
|
<p className="partner-auth-msg" style={{ textAlign: 'center', marginTop: 12, marginBottom: 12 }}>
|
|
{wxAuthorize && !isWechatEnv()
|
|
? '请在微信内打开以使用一键登录'
|
|
: '请使用验证码登录并绑定微信后,即可 7 天内免登录'}
|
|
</p>
|
|
)}
|
|
{!canWechatQuick && (
|
|
<Link to="/login" className="partner-btn-primary" style={{ display: 'block', textAlign: 'center', textDecoration: 'none', marginTop: 12 }}>
|
|
验证码登录
|
|
</Link>
|
|
)}
|
|
<Link to="/login" className="partner-btn-ghost" style={{ display: 'block', marginTop: 12 }}>切换账号</Link>
|
|
</nav>
|
|
|
|
<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.1em', textTransform: 'uppercase' }}>
|
|
{canWechatQuick ? '微信验证 · 7 天内免登录' : 'Secured by Dukang Heritage'}
|
|
</span>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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>
|
|
|
|
{wxAuthorize && (
|
|
<>
|
|
<div className="partner-auth-divider"><span>其他登录方式</span></div>
|
|
<button type="button" className="partner-btn-wechat" onClick={() => void wechatLogin()} disabled={wxLoading}>
|
|
<svg viewBox="0 0 24 24" fill="#07C160" aria-hidden>
|
|
<path d="M8.25 4.5C4.52 4.5 1.5 7.04 1.5 10.17c0 1.78.98 3.37 2.5 4.48l-.63 1.88 2.19-1.09c.84.24 1.74.38 2.69.38.25 0 .5 0 .75-.03-.16-.53-.25-1.09-.25-1.66 0-3.13 3.02-5.67 6.75-5.67.57 0 1.13.06 1.66.17C15.17 6.13 12 4.5 8.25 4.5zm10.5 6.33c-3.11 0-5.62 2.12-5.62 4.73 0 2.61 2.51 4.73 5.62 4.73.79 0 1.54-.14 2.24-.38l1.83.91-.53-1.57c1.27-.92 2.08-2.25 2.08-3.73 0-2.61-2.51-4.73-5.62-4.73z" />
|
|
</svg>
|
|
<span>{wxLoading ? '跳转授权中...' : '微信一键登录'}</span>
|
|
</button>
|
|
{msg && (
|
|
<p className="partner-auth-msg" style={{ color: 'var(--color-error, #d33)', fontSize: 13, textAlign: 'center' }}>
|
|
{msg}
|
|
</p>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
<p className="partner-auth-msg" style={{ textAlign: 'center' }}>
|
|
请在微信内打开;首次请先用验证码登录,成功后将自动关联微信,之后可一键登录
|
|
</p>
|
|
</div>
|
|
</main>
|
|
|
|
{hasPartnerWxSession() && savedProfile?.hasWechat && (
|
|
<Link to="/login?quick=1" className="partner-link">微信快捷登录</Link>
|
|
)}
|
|
|
|
<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>
|
|
);
|
|
}
|