Files
hms/apps/miniprogram/src/pages/login/index.tsx
iven 485b9bb926 feat(mp): 登录页 UX 优化 — 协议区域就近显示
- 协议勾选移至对应操作区域(微信登录 + 绑定区)更直觉
- 统一 SHOW_DEV_LOGIN 常量控制开发模式入口
- 抽取 requireAgreement() 复用协议检查逻辑
2026-05-25 13:44:35 +08:00

214 lines
7.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 { useState } from 'react';
import { View, Text, Button } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { safeNavigateTo } from '@/utils/navigate';
import { useAuthStore } from '../../stores/auth';
import './index.scss';
declare const __wxConfig: Record<string, unknown> | undefined;
const IS_DEV = process.env.NODE_ENV !== 'production';
const IS_SIMULATOR = typeof __wxConfig !== 'undefined' && (__wxConfig as Record<string, unknown>)?.envVersion === 'develop';
const SHOW_DEV_LOGIN = (IS_DEV || IS_SIMULATOR) && !!(process.env.TARO_APP_DEV_USER && process.env.TARO_APP_DEV_PASS);
export default function Login() {
const [agreed, setAgreed] = useState(false);
const [needBind, setNeedBind] = useState(false);
const credentialLogin = useAuthStore((s) => s.credentialLogin);
const login = useAuthStore((s) => s.login);
const bindPhone = useAuthStore((s) => s.bindPhone);
const loading = useAuthStore((s) => s.loading);
const isMedicalStaff = useAuthStore((s) => s.isMedicalStaff);
const navigateAfterLogin = () => {
if (isMedicalStaff()) {
Taro.redirectTo({
url: '/pages/pkg-doctor-core/index',
fail: () => {
Taro.switchTab({
url: '/pages/index/index',
success: () => {
setTimeout(() => {
Taro.navigateTo({ url: '/pages/pkg-doctor-core/index' });
}, 500);
},
});
},
});
} else {
Taro.switchTab({ url: '/pages/index/index' });
}
};
const requireAgreement = () => {
if (!agreed) {
Taro.showToast({ title: '请先阅读并同意用户协议', icon: 'none' });
return false;
}
return true;
};
const handleWechatLogin = async () => {
if (!requireAgreement()) return;
try {
const { code } = await Taro.login();
const result = await login(code);
if (result) {
navigateAfterLogin();
} else {
setNeedBind(true);
Taro.showToast({ title: '请授权手机号完成绑定', icon: 'none' });
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : '登录失败,请重试';
Taro.showToast({ title: msg.substring(0, 20), icon: 'none', duration: 3000 });
}
};
const handleGetPhone = async (e: { detail: { errMsg: string; encryptedData: string; iv: string } }) => {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
Taro.showToast({ title: '需要授权手机号', icon: 'none' });
return;
}
const { encryptedData, iv } = e.detail;
try {
const success = await bindPhone(encryptedData, iv);
if (success) {
navigateAfterLogin();
}
} catch (err: unknown) {
Taro.showModal({
title: '绑定手机号失败',
content: err instanceof Error ? err.message : '绑定失败',
confirmText: '重新登录',
cancelText: '取消',
success: (res) => { if (res.confirm) setNeedBind(false); },
});
}
};
// DevTools 中 getPhoneNumber 不可用,直接传 mock 数据绕过微信 SDK
// 仅在后端 wechat_dev_mode=true 时有效,后端会生成 mock 手机号
const handleDevBindPhone = async () => {
try {
const success = await bindPhone('dev_mock', 'dev_mock');
if (success) {
navigateAfterLogin();
}
} catch (err: unknown) {
Taro.showModal({
title: '绑定失败',
content: err instanceof Error ? err.message : '绑定失败',
confirmText: '重新登录',
cancelText: '取消',
success: (res) => { if (res.confirm) setNeedBind(false); },
});
}
};
const handleDevQuickLogin = async () => {
if (!requireAgreement()) return;
const devUser = process.env.TARO_APP_DEV_USER || '';
const devPass = process.env.TARO_APP_DEV_PASS || '';
if (!devUser || !devPass) return;
try {
const success = await credentialLogin(devUser, devPass);
if (success) {
navigateAfterLogin();
}
} catch (err: unknown) {
Taro.showToast({ title: err instanceof Error ? err.message : '登录失败', icon: 'none' });
}
};
return (
<View className="login-page">
{/* 品牌区 */}
<View className="login-brand">
<View className="login-logo">
<Text className="login-logo-letter">H</Text>
</View>
<Text className="login-title">HMS </Text>
<Text className="login-subtitle"></Text>
</View>
{!needBind ? (
<>
{/* 微信一键登录(主按钮) */}
<View className="login-wechat-btn" onClick={handleWechatLogin}>
<Text className="login-wechat-icon"></Text>
<Text className="login-wechat-text"></Text>
</View>
{/* 协议 */}
<View className="agreement-row">
<View
className={`agreement-check ${agreed ? 'checked' : ''}`}
onClick={() => setAgreed(!agreed)}
>
{agreed && <Text className="agreement-check-mark">&#10003;</Text>}
</View>
<Text className="agreement-text">
<Text className="agreement-link" onClick={() => safeNavigateTo('/pages/legal/user-agreement')}></Text>
<Text className="agreement-link" onClick={() => safeNavigateTo('/pages/legal/privacy-policy')}></Text>
</Text>
</View>
</>
) : (
<View className="login-bind-section">
{/* 真机:微信手机号授权 */}
{!SHOW_DEV_LOGIN && (
<Button
className="login-btn-bind"
openType="getPhoneNumber"
onGetPhoneNumber={handleGetPhone}
loading={loading}
disabled={loading}
>
</Button>
)}
{/* DevTools跳过微信 SDK 直接调后端(后端 wechat_dev_mode 会用 mock 手机号) */}
{SHOW_DEV_LOGIN && (
<Button
className="login-btn-bind"
onClick={handleDevBindPhone}
loading={loading}
disabled={loading}
>
</Button>
)}
{/* 协议 */}
<View className="agreement-row" style={{ marginTop: '16px' }}>
<View
className={`agreement-check ${agreed ? 'checked' : ''}`}
onClick={() => setAgreed(!agreed)}
>
{agreed && <Text className="agreement-check-mark">&#10003;</Text>}
</View>
<Text className="agreement-text">
<Text className="agreement-link" onClick={() => safeNavigateTo('/pages/legal/user-agreement')}></Text>
<Text className="agreement-link" onClick={() => safeNavigateTo('/pages/legal/privacy-policy')}></Text>
</Text>
</View>
</View>
)}
<View style={{ flex: 1 }} />
{/* 开发模式快速登录 — 仅 dev 构建 + DevTools 中显示 */}
{SHOW_DEV_LOGIN && (
<View className="login-dev-btn" onClick={handleDevQuickLogin}>
<Text className="login-dev-btn-text"> </Text>
</View>
)}
</View>
);
}