Files
hms/apps/miniprogram/src/pages/login/index.tsx
iven 95e219ad5a refactor(mp): CSS 变量主题 + 登录页改造 — UI 优化 Phase 0-2
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
2026-05-16 21:29:13 +08:00

211 lines
6.8 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 { 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">&#10003;</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>
);
}