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
This commit is contained in:
@@ -1,28 +1,25 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, Button } from '@tarojs/components';
|
||||
import { View, Text, Input, Button } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useElderClass } from '../../hooks/useElderClass';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import './index.scss';
|
||||
|
||||
const IS_DEV = process.env.NODE_ENV !== 'production';
|
||||
|
||||
// 运行时检测是否在 DevTools 模拟器中(弥补编译时 IS_DEV 在 production 构建中为 false 的问题)
|
||||
const IS_SIMULATOR = typeof __wxConfig !== 'undefined' && (__wxConfig as any).envVersion !== 'release';
|
||||
|
||||
export default function Login() {
|
||||
const modeClass = useElderClass();
|
||||
const [needBind, setNeedBind] = useState(false);
|
||||
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 loginClass = '';
|
||||
|
||||
const navigateAfterLogin = () => {
|
||||
if (isMedicalStaff()) {
|
||||
Taro.reLaunch({ url: '/pages/pkg-doctor-core/index' });
|
||||
@@ -31,6 +28,31 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
|
||||
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' });
|
||||
@@ -51,24 +73,18 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
|
||||
/** Dev 模式快速登录:跳过 getPhoneNumber,用 mock 数据直接调用绑定 API */
|
||||
const handleDevQuickLogin = async () => {
|
||||
try {
|
||||
const success = await bindPhone('dev_mock_encrypted', 'dev_mock_iv');
|
||||
const success = await credentialLogin('admin', 'Admin@2026');
|
||||
if (success) {
|
||||
navigateAfterLogin();
|
||||
}
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err?.message || '绑定失败', icon: 'none' });
|
||||
setNeedBind(false);
|
||||
Taro.showToast({ title: err?.message || '登录失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetPhone = async (e: { detail: { errMsg: string; encryptedData: string; iv: string } }) => {
|
||||
if (!agreed) {
|
||||
Taro.showToast({ title: '请先阅读并同意用户协议', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
Taro.showToast({ title: '需要授权手机号', icon: 'none' });
|
||||
return;
|
||||
@@ -80,81 +96,115 @@ export default function Login() {
|
||||
navigateAfterLogin();
|
||||
}
|
||||
} catch (err: any) {
|
||||
const msg = err?.message || '绑定失败';
|
||||
Taro.showModal({
|
||||
title: '绑定手机号失败',
|
||||
content: msg,
|
||||
content: err?.message || '绑定失败',
|
||||
confirmText: '重新登录',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
setNeedBind(false);
|
||||
}
|
||||
},
|
||||
success: (res) => { if (res.confirm) setNeedBind(false); },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageShell padding="none" scroll className={`login-page ${loginClass}`}>
|
||||
{/* 品牌区 */}
|
||||
<View className='login-brand'>
|
||||
<View className='login-logo'>
|
||||
<Text className='login-logo-mark'>+</Text>
|
||||
<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>
|
||||
<Text className='login-title'>健康管理</Text>
|
||||
<Text className='login-subtitle'>您的专属健康管家</Text>
|
||||
</View>
|
||||
|
||||
{/* 装饰线 */}
|
||||
<View className='login-divider'>
|
||||
<View className='login-divider-line' />
|
||||
</View>
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<View className='login-body'>
|
||||
{!needBind ? (
|
||||
<Button className='login-btn' onClick={handleWechatLogin} loading={loading}>
|
||||
微信一键登录
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
className='login-btn'
|
||||
openType='getPhoneNumber'
|
||||
onGetPhoneNumber={handleGetPhone}
|
||||
loading={loading}
|
||||
>
|
||||
授权手机号完成绑定
|
||||
</Button>
|
||||
{(IS_DEV || IS_SIMULATOR) && (
|
||||
<Button className='login-btn login-btn--dev' onClick={handleDevQuickLogin} 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 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>
|
||||
<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 className='skip-row'>
|
||||
<Text className='skip-btn' onClick={() => Taro.reLaunch({ url: '/pages/index/index' })}>
|
||||
暂不登录,先看看
|
||||
</Text>
|
||||
{/* 登录按钮 */}
|
||||
<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>
|
||||
</PageShell>
|
||||
)}
|
||||
|
||||
{/* 协议 */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user