Phase 0: 建立 design token 体系 - tokens.scss 新增 --tk-pri/--tk-pri-l/--tk-pri-d/--tk-shadow-btn/--tk-shadow-tab - .doctor-mode 覆盖为靛蓝色系,.elder-mode 非线性放大字号 - variables.scss 新增医生端色彩 + 阴影变量 Phase 1: 组件库 + 页面全局替换 - 75 个页面 SCSS $pri → var(--tk-pri) 全量替换 - 11 个新 UI 组件(PrimaryButton/TabFilter/FormInput/ProgressRing 等) - 8 个现有组件 SCSS 更新 - 18 个医生端页面 useElderClass → useDoctorClass - PageHeader 匹配原型 NavBar 规格 Phase 2: 登录页重写 - Logo: 方形+ → 圆形渐变 H - 登录方式: 纯微信 → 账号密码 + 微信一键登录 - 新增 credentialLogin API + store action - 字号/间距严格匹配原型 mp-01-login.html
211 lines
6.8 KiB
TypeScript
211 lines
6.8 KiB
TypeScript
import { useState } from 'react';
|
||
import { View, Text, Input, Button } from '@tarojs/components';
|
||
import Taro from '@tarojs/taro';
|
||
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()) {
|
||
Taro.reLaunch({ url: '/pages/pkg-doctor-core/index' });
|
||
} 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">✓</Text>}
|
||
</View>
|
||
<Text className="agreement-text">
|
||
登录即同意
|
||
<Text className="agreement-link" onClick={() => Taro.navigateTo({ url: '/pages/legal/user-agreement' })}>《用户协议》</Text>
|
||
和
|
||
<Text className="agreement-link" onClick={() => Taro.navigateTo({ url: '/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>
|
||
);
|
||
}
|