Files
dukang/apps/mini-hq/src/pages/login/index.tsx
T
jacy 14b867a3a5
CI / verify (pull_request) Has been cancelled
fix(auth): show user agreement before login actions
Quick login required agreement without a checkbox; move the consent row above CTAs across partner/shop/user/mini login pages so it stays visible on short screens.

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

267 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { View, Text, Input, Button } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { request, saveToken, toast } from '../../lib/api';
import {
bindHqWechatAfterSmsLogin,
handleHqWechatCallback,
handleHqWechatLoginResult,
loginHqWithWechat,
} from '../../lib/wechat';
import { isWechatEnv } from '../../lib/weixin';
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
import { clearHqAccountCache } from '../../lib/session';
import './index.css';
const DEMO_PHONE = '13600000001';
const REMEMBER_PHONE_KEY = 'hq_remember_phone';
const REMEMBER_FLAG_KEY = 'hq_remember_account';
function loadRememberedPhone(): { phone: string; remember: boolean } {
try {
const remember = Taro.getStorageSync(REMEMBER_FLAG_KEY) === '1';
const phone = remember ? Taro.getStorageSync(REMEMBER_PHONE_KEY) || '' : '';
return { phone, remember };
} catch {
return { phone: '', remember: false };
}
}
function WechatIcon() {
if (process.env.TARO_ENV === 'h5') {
return (
<svg className="login-wechat-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>
);
}
return <Text className="login-wechat-fallback"></Text>;
}
export default function LoginPage() {
const remembered = loadRememberedPhone();
const [phone, setPhone] = useState(remembered.phone || DEMO_PHONE);
const [code, setCode] = useState('123456');
const [rememberAccount, setRememberAccount] = useState(remembered.remember);
const [agreed, setAgreed] = useState(false);
const [cooldown, setCooldown] = useState(0);
const [loading, setLoading] = useState(false);
const [wxLoading, setWxLoading] = useState(false);
const [msg, setMsg] = useState('');
useEffect(() => {
if (!isWechatEnv()) return;
void handleHqWechatCallback()
.then((result) => {
if (!result) return;
if (handleHqWechatLoginResult(result)) enterApp();
})
.catch((e) => setMsg(formatWechatError(e)));
}, []);
function formatWechatError(e: unknown): string {
const text = e instanceof Error ? e.message : '微信登录失败';
if (text.includes('首次登录') || text.includes('手机验证码')) {
return '该微信尚未绑定管理员账号,请先使用手机验证码登录,登录后将自动关联微信';
}
return text;
}
function ensureAgreed(): boolean {
if (!agreed) {
setMsg('请先勾选并同意用户协议');
return false;
}
return true;
}
function persistRememberAccount(nextPhone: string) {
try {
if (rememberAccount) {
Taro.setStorageSync(REMEMBER_FLAG_KEY, '1');
Taro.setStorageSync(REMEMBER_PHONE_KEY, nextPhone);
} else {
Taro.removeStorageSync(REMEMBER_FLAG_KEY);
Taro.removeStorageSync(REMEMBER_PHONE_KEY);
}
} catch {
/* ignore */
}
}
function enterApp() {
clearHqAccountCache();
Taro.reLaunch({ url: '/pages/dashboard/index' });
}
async function sendCode() {
if (!ensureAgreed()) return;
if (cooldown > 0) return;
setMsg('');
try {
await request('/admin/auth/sms/send', {
method: 'POST',
data: { phone, scene: 'HQ_LOGIN' },
});
toast('验证码已发送(Mock:123456', 'success');
setCooldown(60);
const t = setInterval(() => {
setCooldown((s) => {
if (s <= 1) {
clearInterval(t);
return 0;
}
return s - 1;
});
}, 1000);
} catch (e) {
setMsg(e instanceof Error ? e.message : '发送失败');
}
}
async function smsLogin() {
if (!ensureAgreed()) return;
setLoading(true);
setMsg('');
try {
const data = await request<{ accessToken: string }>('/admin/auth/login/sms', {
method: 'POST',
data: { phone, code },
});
saveToken(data.accessToken);
persistRememberAccount(phone);
if (isWechatEnv() || process.env.TARO_ENV === 'weapp') {
setMsg('登录成功,正在关联微信…');
await bindHqWechatAfterSmsLogin();
if (process.env.TARO_ENV === 'weapp') {
enterApp();
}
return;
}
enterApp();
} catch (e) {
setMsg(e instanceof Error ? e.message : '登录失败');
} finally {
setLoading(false);
}
}
async function wechatLogin() {
if (!ensureAgreed()) return;
setMsg('');
if (!isWechatEnv()) {
setMsg(WECHAT_INAPP_REQUIRED_MSG);
return;
}
setWxLoading(true);
try {
const ok = await loginHqWithWechat();
if (ok) enterApp();
} catch (e) {
setMsg(formatWechatError(e));
} finally {
setWxLoading(false);
}
}
return (
<View className="login-page">
<View className="login-brand">
<View className="login-logo">
<Text className="material-symbols-outlined">local_bar</Text>
</View>
<Text className="login-title">杜康好客</Text>
<Text className="login-subtitle">总部管理中心</Text>
</View>
<View className="login-card">
<Text className="login-card-title">管理员登录</Text>
<View className="login-input-wrap">
<Text className="material-symbols-outlined login-input-icon">smartphone</Text>
<Input
className="login-input"
type="number"
placeholder="请输入手机号"
value={phone}
onInput={(e) => setPhone(e.detail.value)}
/>
</View>
<View className="login-input-row">
<View className="login-input-wrap">
<Text className="material-symbols-outlined login-input-icon">shield</Text>
<Input
className="login-input"
type="number"
placeholder="验证码"
value={code}
onInput={(e) => setCode(e.detail.value)}
/>
</View>
<View
className={`login-code-btn${cooldown > 0 ? ' is-disabled' : ''}`}
onClick={sendCode}
>
<Text>{cooldown > 0 ? `${cooldown}s 后重发` : '获取验证码'}</Text>
</View>
</View>
<View className="login-remember" onClick={() => setRememberAccount((v) => !v)}>
<View className={`login-checkbox${rememberAccount ? ' is-checked' : ''}`} />
<Text>记住账号</Text>
</View>
<View className="login-agreement" onClick={() => setAgreed((v) => !v)}>
<View className={`login-checkbox${agreed ? ' is-checked' : ''}`} />
<Text className="login-agreement-text">
我已阅读并同意
<Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/user-agreement/index' });
}}
>
《用户协议》
</Text>
<Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/privacy-policy/index' });
}}
>
《隐私政策》
</Text>
</Text>
</View>
{msg ? <Text className="login-msg">{msg}</Text> : null}
<Button className="hq-btn hq-btn--primary hq-btn--block login-submit" loading={loading} onClick={smsLogin}>
登录
</Button>
<View className="login-divider">
<Text>其他登录方式</Text>
</View>
<View
className={`login-wechat-btn${wxLoading ? ' is-disabled' : ''}`}
onClick={wxLoading ? undefined : wechatLogin}
>
<WechatIcon />
<Text>{wxLoading ? '登录中...' : '微信一键授权'}</Text>
</View>
</View>
<View className="login-footer">
<Text className="material-symbols-outlined" style="font-size:16px">verified_user</Text>
<Text>杜康好客 · 传承千年</Text>
</View>
</View>
);
}