healthapp
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

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对话、个人中心)的详细实现将根据实际开发进度逐步补充。关键差异点:

  1. UI 框架:使用 React Native Paper 代替 Element Plus
  2. 导航:使用 React Navigation 代替 Vue Router
  3. 状态管理:使用 Zustand 代替 Pinia
  4. 样式:使用 StyleSheet 代替 CSS
  5. 表单:使用 React Hook Form 或原生 useState