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.
 
 
 
 
 
 

10 KiB

03-用户认证页面

目标

实现登录和注册页面,完成用户认证功能。


UI 设计参考

参考设计稿:files/ui/登录页.png

页面布局

区域 设计要点
顶部 绿色渐变背景 (#10B981 → #2EC4B6) + 医疗插图
Logo "AI健康助手" 标题 + "您的专属健康管理伙伴"
表单 白色圆角卡片 (16px),内边距 24px
输入框 灰色背景 #F3F4F6,圆角 24px
主按钮 绿色 #10B981,全宽,圆角 24px
底部 注册/登录切换链接

配色规范

$primary: #10B981;      // 主色调
$primary-dark: #059669; // 深色(hover)
$bg-page: #F5F5F5;      // 页面背景
$bg-input: #F3F4F6;     // 输入框背景
$text-main: #1F2937;    // 主文字
$text-sub: #6B7280;     // 次文字
$text-hint: #9CA3AF;    // 提示文字

前置要求

  • 路由和布局已配置
  • 后端认证接口可用

实施步骤

步骤 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/views/auth/Login.vue

<template>
  <div class="login-page">
    <el-form
      ref="formRef"
      :model="form"
      :rules="rules"
      label-position="top"
      @submit.prevent="handleLogin"
    >
      <el-form-item label="手机号" prop="phone">
        <el-input
          v-model="form.phone"
          placeholder="请输入手机号"
          prefix-icon="Phone"
          size="large"
        />
      </el-form-item>

      <el-form-item label="密码" prop="password">
        <el-input
          v-model="form.password"
          type="password"
          placeholder="请输入密码"
          prefix-icon="Lock"
          size="large"
          show-password
        />
      </el-form-item>

      <el-form-item>
        <el-button
          type="primary"
          size="large"
          :loading="loading"
          style="width: 100%"
          native-type="submit"
        >
          登录
        </el-button>
      </el-form-item>

      <div class="form-footer">
        <span>还没有账号?</span>
        <router-link to="/auth/register">立即注册</router-link>
      </div>
    </el-form>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { useUserStore } from '@/stores/user'
import { login } from '@/api/auth'
import { getUserProfile } from '@/api/user'

const router = useRouter()
const route = useRoute()
const userStore = useUserStore()

const formRef = ref<FormInstance>()
const loading = ref(false)

const form = reactive({
  phone: '',
  password: '',
})

const rules: FormRules = {
  phone: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, message: '密码至少6位', trigger: 'blur' },
  ],
}

const handleLogin = async () => {
  const valid = await formRef.value?.validate()
  if (!valid) return

  loading.value = true
  try {
    const res = await login(form)
    
    // 保存 token 和用户信息
    userStore.setToken(res.token)
    userStore.setUserInfo({
      id: res.user_id,
      nickname: res.nickname,
      survey_completed: res.survey_completed,
    })

    // 获取完整用户信息
    const userInfo = await getUserProfile()
    userStore.setUserInfo(userInfo)

    ElMessage.success('登录成功')

    // 跳转到目标页面或首页
    const redirect = route.query.redirect as string
    if (res.survey_completed) {
      router.push(redirect || '/')
    } else {
      router.push('/survey')
    }
  } catch (error) {
    // 错误已在拦截器中处理
  } finally {
    loading.value = false
  }
}
</script>

<style scoped>
.login-page {
  padding: 20px 0;
}

.form-footer {
  text-align: center;
  font-size: 14px;
  color: #666;
}

.form-footer a {
  color: #409eff;
  text-decoration: none;
  margin-left: 8px;
}

.form-footer a:hover {
  text-decoration: underline;
}
</style>

步骤 3:创建注册页面

创建 src/views/auth/Register.vue

<template>
  <div class="register-page">
    <el-form
      ref="formRef"
      :model="form"
      :rules="rules"
      label-position="top"
      @submit.prevent="handleRegister"
    >
      <el-form-item label="手机号" prop="phone">
        <el-input
          v-model="form.phone"
          placeholder="请输入手机号"
          prefix-icon="Phone"
          size="large"
        />
      </el-form-item>

      <el-form-item label="昵称" prop="nickname">
        <el-input
          v-model="form.nickname"
          placeholder="请输入昵称(选填)"
          prefix-icon="User"
          size="large"
        />
      </el-form-item>

      <el-form-item label="密码" prop="password">
        <el-input
          v-model="form.password"
          type="password"
          placeholder="请输入密码(至少6位)"
          prefix-icon="Lock"
          size="large"
          show-password
        />
      </el-form-item>

      <el-form-item label="确认密码" prop="confirmPassword">
        <el-input
          v-model="form.confirmPassword"
          type="password"
          placeholder="请再次输入密码"
          prefix-icon="Lock"
          size="large"
          show-password
        />
      </el-form-item>

      <el-form-item>
        <el-checkbox v-model="form.agreement">
          我已阅读并同意
          <a href="javascript:;" @click.stop="showAgreement">《用户协议》</a>
          <a href="javascript:;" @click.stop="showPrivacy">《隐私政策》</a>
        </el-checkbox>
      </el-form-item>

      <el-form-item>
        <el-button
          type="primary"
          size="large"
          :loading="loading"
          :disabled="!form.agreement"
          style="width: 100%"
          native-type="submit"
        >
          注册
        </el-button>
      </el-form-item>

      <div class="form-footer">
        <span>已有账号?</span>
        <router-link to="/auth/login">立即登录</router-link>
      </div>
    </el-form>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { useUserStore } from '@/stores/user'
import { register } from '@/api/auth'

const router = useRouter()
const userStore = useUserStore()

const formRef = ref<FormInstance>()
const loading = ref(false)

const form = reactive({
  phone: '',
  nickname: '',
  password: '',
  confirmPassword: '',
  agreement: false,
})

const validateConfirmPassword = (rule: any, value: string, callback: any) => {
  if (value !== form.password) {
    callback(new Error('两次输入的密码不一致'))
  } else {
    callback()
  }
}

const rules: FormRules = {
  phone: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, message: '密码至少6位', trigger: 'blur' },
  ],
  confirmPassword: [
    { required: true, message: '请再次输入密码', trigger: 'blur' },
    { validator: validateConfirmPassword, trigger: 'blur' },
  ],
}

const handleRegister = async () => {
  const valid = await formRef.value?.validate()
  if (!valid) return

  if (!form.agreement) {
    ElMessage.warning('请先同意用户协议和隐私政策')
    return
  }

  loading.value = true
  try {
    const res = await register({
      phone: form.phone,
      password: form.password,
      nickname: form.nickname || undefined,
    })

    // 保存 token
    userStore.setToken(res.token)
    userStore.setUserInfo({
      id: res.user_id,
      nickname: res.nickname,
      survey_completed: false,
    })

    ElMessage.success('注册成功,请完成健康调查')
    router.push('/survey')
  } catch (error) {
    // 错误已在拦截器中处理
  } finally {
    loading.value = false
  }
}

const showAgreement = () => {
  ElMessageBox.alert(
    '本应用仅提供健康咨询建议,不构成医疗诊断。如有不适,请及时就医。',
    '用户协议',
    { confirmButtonText: '我知道了' }
  )
}

const showPrivacy = () => {
  ElMessageBox.alert(
    '我们重视您的隐私,您的健康信息将被加密存储,不会泄露给第三方。',
    '隐私政策',
    { confirmButtonText: '我知道了' }
  )
}
</script>

<style scoped>
.register-page {
  padding: 20px 0;
}

.el-checkbox {
  height: auto;
  line-height: 1.5;
}

.el-checkbox a {
  color: #409eff;
}

.form-footer {
  text-align: center;
  font-size: 14px;
  color: #666;
}

.form-footer a {
  color: #409eff;
  text-decoration: none;
  margin-left: 8px;
}
</style>

需要创建的文件清单

文件路径 说明
src/api/auth.ts 认证 API
src/views/auth/Login.vue 登录页面
src/views/auth/Register.vue 注册页面

功能说明

登录页面

  • 手机号+密码登录
  • 表单验证
  • 登录成功后:
    • 已完成调查 → 跳转首页
    • 未完成调查 → 跳转健康调查页

注册页面

  • 手机号+密码注册
  • 可选昵称
  • 密码确认
  • 用户协议确认
  • 注册成功后跳转健康调查页

验收标准

  • 登录表单验证正常
  • 注册表单验证正常
  • 登录成功保存 Token
  • 注册成功自动登录
  • 路由跳转逻辑正确

预计耗时

20-25 分钟


下一步

完成后进入 03-Web前端开发/04-健康调查页面.md