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:
iven
2026-05-16 21:29:13 +08:00
parent 1786f0d707
commit 95e219ad5a
124 changed files with 2306 additions and 1142 deletions

View File

@@ -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'>&#10003;</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">&#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>
);
}