feat;提交管理端和城市合伙人端

This commit is contained in:
ljy
2026-07-05 23:48:20 +08:00
parent fecfc61ec5
commit 569becedaa
73 changed files with 10150 additions and 80 deletions
+245
View File
@@ -0,0 +1,245 @@
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 { 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(true);
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('');
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>
<Button className="hq-btn hq-btn--primary hq-btn--block login-submit" loading={loading} onClick={smsLogin}>
</Button>
{msg ? <Text className="login-msg">{msg}</Text> : null}
<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 className="login-agreement" onClick={() => setAgreed((v) => !v)}>
<View className={`login-checkbox${agreed ? ' is-checked' : ''}`} />
<Text className="login-agreement-text">
<Text className="login-agreement-link"></Text>
<Text className="login-agreement-link"></Text>
</Text>
</View>
</View>
<View className="login-footer">
<Text className="material-symbols-outlined" style="font-size:16px">verified_user</Text>
<Text> · </Text>
</View>
</View>
);
}