Files
hms/apps/miniprogram/src/pages/login/index.tsx
iven e555496528 feat(mp): design-handoff 产出的页面样式和组件优化
- 首页/商城/医生端/积分/家庭档案等页面 SCSS + TSX 更新
- TabFilter 组件样式优化
- points service 接口调整
- app.config 路由注册更新
2026-05-18 02:12:41 +08:00

227 lines
7.3 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, Input, Button } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { safeNavigateTo } from '@/utils/navigate';
import { useAuthStore } from '../../stores/auth';
import './index.scss';
const IS_DEV = process.env.NODE_ENV !== 'production';
const IS_SIMULATOR = typeof __wxConfig !== 'undefined' && (__wxConfig as any).envVersion !== 'release';
export default function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
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()) {
// 使用 redirectTo 替代 reLaunch 避免分包加载超时
// redirectTo 只替换当前页面,不销毁整个页栈,分包预加载不会被中断
Taro.redirectTo({
url: '/pages/pkg-doctor-core/index',
fail: () => {
// fallback: 先跳首页再 redirectTo
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 handleCredentialLogin = async () => {
if (!username.trim()) {
Taro.showToast({ title: '请输入账号', icon: 'none' });
return;
}
if (!password.trim()) {
Taro.showToast({ title: '请输入密码', icon: 'none' });
return;
}
if (!agreed) {
Taro.showToast({ title: '请先阅读并同意用户协议', icon: 'none' });
return;
}
try {
const success = await credentialLogin(username.trim(), password);
if (success) {
navigateAfterLogin();
} else {
Taro.showToast({ title: '账号或密码错误', icon: 'none' });
}
} catch {
Taro.showToast({ title: '登录失败,请重试', icon: 'none' });
}
};
const handleWechatLogin = async () => {
if (!agreed) {
Taro.showToast({ title: '请先阅读并同意用户协议', icon: 'none' });
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: any) {
const msg = err?.message || '登录失败,请重试';
Taro.showToast({ title: msg.substring(0, 20), icon: 'none', duration: 3000 });
}
};
const handleDevQuickLogin = async () => {
try {
const success = await credentialLogin('admin', 'Admin@2026');
if (success) {
navigateAfterLogin();
}
} catch (err: any) {
Taro.showToast({ title: err?.message || '登录失败', icon: 'none' });
}
};
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: any) {
Taro.showModal({
title: '绑定手机号失败',
content: err?.message || '绑定失败',
confirmText: '重新登录',
cancelText: '取消',
success: (res) => { if (res.confirm) setNeedBind(false); },
});
}
};
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-field">
<Input
className="login-input"
type="text"
placeholder="请输入账号"
placeholderClass="login-placeholder"
value={username}
onInput={(e) => setUsername(e.detail.value)}
/>
</View>
{/* 密码输入 */}
<View className="login-field">
<Input
className="login-input"
type="text"
password={!showPassword}
placeholder="请输入密码"
placeholderClass="login-placeholder"
value={password}
onInput={(e) => setPassword(e.detail.value)}
/>
<Text
className="login-eye"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? '隐藏' : '显示'}
</Text>
</View>
{/* 登录按钮 */}
<View className="login-submit" onClick={handleCredentialLogin}>
<Text className="login-submit-text">{loading ? '登录中...' : '登录'}</Text>
</View>
{/* 分隔线 */}
<View className="login-divider">
<View className="login-divider-line" />
<Text className="login-divider-text"></Text>
<View className="login-divider-line" />
</View>
{/* 微信一键登录 */}
<View className="login-wechat-btn" onClick={handleWechatLogin}>
<Text className="login-wechat-icon"></Text>
<Text className="login-wechat-text"></Text>
</View>
</>
) : (
<View className="login-bind-section">
<Button
className="login-btn-bind"
openType="getPhoneNumber"
onGetPhoneNumber={handleGetPhone}
loading={loading}
>
</Button>
</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 style={{ flex: 1 }} />
{/* 开发模式 */}
{(IS_DEV || IS_SIMULATOR) && (
<View className="login-dev-btn" onClick={handleDevQuickLogin}>
<Text className="login-dev-btn-text"> </Text>
</View>
)}
</View>
);
}