You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
12 KiB
12 KiB
03-用户认证页面
目标
实现 APP 端登录和注册页面。
UI 设计参考
参考设计稿:
files/ui/登录页.png
页面布局
| 区域 | 设计要点 |
|---|---|
| 顶部 | 绿色渐变背景 (#10B981 → #2EC4B6) + 医疗插图 |
| Logo | "AI健康助手" 标题(白色 32px)+ slogan |
| 表单卡片 | 白色背景,圆角 16px |
| 输入框 | 圆角 12px,左侧带图标 |
| 主按钮 | 绿色 #10B981,圆角 24px,高度 48px |
样式常量
const colors = {
primary: '#10B981',
primaryDark: '#059669',
background: '#10B981',
cardBackground: '#FFFFFF',
inputBackground: '#F3F4F6',
textPrimary: '#1F2937',
textSecondary: '#6B7280',
textHint: '#9CA3AF',
}
const spacing = {
screenPadding: 20,
cardPadding: 24,
inputMargin: 16,
}
const borderRadius = {
card: 16,
input: 12,
button: 24,
}
前置要求
- 导航配置完成
- 后端认证接口可用
实施步骤
步骤 1:创建认证 API
创建 src/api/auth.ts:
import request from './request'
export interface LoginRequest {
phone: string
password: string
}
export interface RegisterRequest {
phone: string
password: string
nickname?: string
}
export interface AuthResponse {
token: string
user_id: number
nickname: string
survey_completed: boolean
}
export const login = (data: LoginRequest): Promise<AuthResponse> => {
return request.post('/auth/login', data)
}
export const register = (data: RegisterRequest): Promise<AuthResponse> => {
return request.post('/auth/register', data)
}
步骤 2:创建登录页面
更新 src/screens/auth/LoginScreen.tsx:
import React, { useState } from 'react'
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
} from 'react-native'
import { TextInput, Button } from 'react-native-paper'
import { useNavigation } from '@react-navigation/native'
import { useUserStore } from '../../stores/userStore'
import { login } from '../../api/auth'
import type { AuthNavigationProp } from '../../navigation/types'
const LoginScreen = () => {
const navigation = useNavigation<AuthNavigationProp>()
const { setToken, setUser } = useUserStore()
const [phone, setPhone] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const handleLogin = async () => {
if (!phone.trim() || !password.trim()) {
return
}
setLoading(true)
try {
const res = await login({ phone, password })
setToken(res.token)
setUser({
id: res.user_id,
nickname: res.nickname,
phone,
email: '',
avatar: '',
survey_completed: res.survey_completed,
})
} catch (error) {
// 错误已在拦截器处理
} finally {
setLoading(false)
}
}
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
<View style={styles.header}>
<Text style={styles.title}>健康AI助手</Text>
<Text style={styles.subtitle}>您的智能健康管家</Text>
</View>
<View style={styles.form}>
<TextInput
label="手机号"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
style={styles.input}
mode="outlined"
left={<TextInput.Icon icon="phone" />}
/>
<TextInput
label="密码"
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
style={styles.input}
mode="outlined"
left={<TextInput.Icon icon="lock" />}
right={
<TextInput.Icon
icon={showPassword ? 'eye-off' : 'eye'}
onPress={() => setShowPassword(!showPassword)}
/>
}
/>
<Button
mode="contained"
onPress={handleLogin}
loading={loading}
disabled={loading || !phone.trim() || !password.trim()}
style={styles.button}
contentStyle={styles.buttonContent}
>
登录
</Button>
<View style={styles.footer}>
<Text style={styles.footerText}>还没有账号?</Text>
<Button
mode="text"
onPress={() => navigation.navigate('Register')}
compact
>
立即注册
</Button>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#667eea',
},
scrollContent: {
flexGrow: 1,
justifyContent: 'center',
padding: 20,
},
header: {
alignItems: 'center',
marginBottom: 40,
},
title: {
fontSize: 32,
fontWeight: 'bold',
color: '#fff',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: 'rgba(255,255,255,0.8)',
},
form: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 24,
},
input: {
marginBottom: 16,
},
button: {
marginTop: 8,
borderRadius: 8,
},
buttonContent: {
paddingVertical: 8,
},
footer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: 16,
},
footerText: {
color: '#666',
},
})
export default LoginScreen
步骤 3:创建注册页面
创建 src/screens/auth/RegisterScreen.tsx:
import React, { useState } from 'react'
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
Alert,
} from 'react-native'
import { TextInput, Button, Checkbox } from 'react-native-paper'
import { useNavigation } from '@react-navigation/native'
import { useUserStore } from '../../stores/userStore'
import { register } from '../../api/auth'
import type { AuthNavigationProp } from '../../navigation/types'
const RegisterScreen = () => {
const navigation = useNavigation<AuthNavigationProp>()
const { setToken, setUser } = useUserStore()
const [phone, setPhone] = useState('')
const [nickname, setNickname] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [agreement, setAgreement] = useState(false)
const [loading, setLoading] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const handleRegister = async () => {
if (!phone.trim() || !password.trim()) {
Alert.alert('提示', '请输入手机号和密码')
return
}
if (password !== confirmPassword) {
Alert.alert('提示', '两次输入的密码不一致')
return
}
if (!agreement) {
Alert.alert('提示', '请先同意用户协议和隐私政策')
return
}
setLoading(true)
try {
const res = await register({
phone,
password,
nickname: nickname || undefined,
})
setToken(res.token)
setUser({
id: res.user_id,
nickname: res.nickname,
phone,
email: '',
avatar: '',
survey_completed: false,
})
} catch (error) {
// 错误已在拦截器处理
} finally {
setLoading(false)
}
}
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
<View style={styles.header}>
<Text style={styles.title}>创建账号</Text>
</View>
<View style={styles.form}>
<TextInput
label="手机号"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
style={styles.input}
mode="outlined"
left={<TextInput.Icon icon="phone" />}
/>
<TextInput
label="昵称(选填)"
value={nickname}
onChangeText={setNickname}
style={styles.input}
mode="outlined"
left={<TextInput.Icon icon="account" />}
/>
<TextInput
label="密码"
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
style={styles.input}
mode="outlined"
left={<TextInput.Icon icon="lock" />}
right={
<TextInput.Icon
icon={showPassword ? 'eye-off' : 'eye'}
onPress={() => setShowPassword(!showPassword)}
/>
}
/>
<TextInput
label="确认密码"
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry={!showPassword}
style={styles.input}
mode="outlined"
left={<TextInput.Icon icon="lock-check" />}
/>
<View style={styles.checkboxRow}>
<Checkbox
status={agreement ? 'checked' : 'unchecked'}
onPress={() => setAgreement(!agreement)}
/>
<Text style={styles.agreementText}>
我已阅读并同意《用户协议》和《隐私政策》
</Text>
</View>
<Button
mode="contained"
onPress={handleRegister}
loading={loading}
disabled={loading || !phone.trim() || !password.trim() || !agreement}
style={styles.button}
contentStyle={styles.buttonContent}
>
注册
</Button>
<View style={styles.footer}>
<Text style={styles.footerText}>已有账号?</Text>
<Button
mode="text"
onPress={() => navigation.goBack()}
compact
>
立即登录
</Button>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#667eea',
},
scrollContent: {
flexGrow: 1,
justifyContent: 'center',
padding: 20,
},
header: {
alignItems: 'center',
marginBottom: 24,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#fff',
},
form: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 24,
},
input: {
marginBottom: 16,
},
checkboxRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
agreementText: {
flex: 1,
fontSize: 13,
color: '#666',
},
button: {
borderRadius: 8,
},
buttonContent: {
paddingVertical: 8,
},
footer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: 16,
},
footerText: {
color: '#666',
},
})
export default RegisterScreen
需要创建的文件清单
| 文件路径 | 说明 |
|---|---|
src/api/auth.ts |
认证 API |
src/screens/auth/LoginScreen.tsx |
登录页面 |
src/screens/auth/RegisterScreen.tsx |
注册页面 |
验收标准
- 登录页面 UI 正常显示
- 注册页面 UI 正常显示
- 登录功能正常
- 注册功能正常
- 状态切换正确触发导航
预计耗时
25-30 分钟
下一步
完成后进入 04-APP开发/04-健康调查页面.md
备注
由于 APP 端实现与 Web 端逻辑类似,后续文档将提供精简版说明和关键代码,完整实现可参考 Web 端对应模块。
APP 端剩余页面(健康调查、体质测评、AI对话、个人中心)的详细实现将根据实际开发进度逐步补充。关键差异点:
- UI 框架:使用 React Native Paper 代替 Element Plus
- 导航:使用 React Navigation 代替 Vue Router
- 状态管理:使用 Zustand 代替 Pinia
- 样式:使用 StyleSheet 代替 CSS
- 表单:使用 React Hook Form 或原生 useState