📅 发布日期:2025年
🏷️ 标签:Vue3、实战项目、UI设计、表单验证、动画效果
⏱️ 阅读时长:约145分钟
💡 难度:⭐⭐ (入门实战)

📖 前言

大家好!前面我们学习了 Vue3 的基础知识和组合式 API,但可能有些同学会觉得太理论了。所以这一章,我们来做点真正吸引人的东西——一个超级好看的登录注册页面!

为什么选择登录注册页面作为实战项目?

  1. 实用性强 - 几乎每个项目都需要
  2. 视觉效果好 - 容易出效果,有成就感
  3. 知识点全面 - 表单、验证、动画、状态管理都能学到
  4. 可以炫耀 - 做完可以发朋友圈/简历作品集 😎

效果展示:

在这里插入图片描述

本章你将学到:

  • 🎨 玻璃拟态 (Glassmorphism) 设计风格
  • ✨ 登录/注册表单的流畅切换动画
  • 📝 完整的表单验证逻辑(手机号、邮箱、密码强度)
  • 🎭 酷炫的输入框聚焦动效
  • 🌙 暗黑模式切换
  • 🔐 密码强度实时显示
  • ⏱️ 验证码倒计时功能
  • 📱 完美的响应式布局

🎯 一、项目效果展示

在开始写代码之前,让我先给你看看我们要做出什么效果:

1.1 核心功能

功能 描述 难度
登录表单 用户名/邮箱 + 密码登录
注册表单 完整的注册流程 ⭐⭐
表单验证 实时验证,友好的错误提示 ⭐⭐
切换动画 登录/注册平滑切换 ⭐⭐
密码强度 实时显示密码强度 ⭐⭐
验证码倒计时 60秒倒计时
记住密码 本地存储功能
暗黑模式 一键切换主题 ⭐⭐
响应式布局 适配手机/平板/PC ⭐⭐

1.2 设计风格

我们采用现代玻璃拟态风格,主要特点:

  • 🌈 渐变背景 - 柔和的紫蓝色渐变
  • 💎 半透明磨砂玻璃 - backdrop-filter 实现
  • 微妙的阴影和光效 - 立体感十足
  • 🎨 流畅的过渡动画 - 每个交互都有反馈
  • 🌙 深色模式支持 - 护眼又酷炫

🚀 二、从零开始创建项目

2.1 创建 Vue3 项目

如果你还没有 Vue3 项目,让我们从头开始创建一个:

# 1. 使用 Vite 创建 Vue3 + TypeScript 项目
npm create vite@latest auth-demo -- --template vue-ts

# 2. 进入项目目录
cd auth-demo

# 3. 安装依赖
npm install

💡 小知识:

  • vite 是新一代前端构建工具,比 webpack 快很多
  • vue-ts 模板表示使用 Vue3 + TypeScript
  • 项目名 auth-demo 可以改成你喜欢的名字

在这里插入图片描述

2.2 安装必要的依赖

# 安装图标库(用于显示好看的图标)
npm install @iconify/vue

# 安装 Element Plus(UI 组件库,用于消息提示)
npm install element-plus

# 如果使用 pnpm(推荐,更快)
pnpm add @iconify/vue element-plus

💡 为什么需要这些?

  • @iconify/vue - 提供海量图标,比如登录图标、锁图标等
  • element-plus - 只用它的消息提示功能(ElMessage)

2.3 创建项目目录结构

在项目的 src 目录下创建以下文件夹:

# 在项目根目录执行(Windows PowerShell)
New-Item -ItemType Directory -Path src/views/auth -Force
New-Item -ItemType Directory -Path src/composables -Force

# 如果是 Mac/Linux,使用:
# mkdir -p src/views/auth
# mkdir -p src/composables

💡 目录说明:

  • src/views/auth/ - 存放登录注册页面
  • src/composables/ - 存放可复用的逻辑函数(Vue3 的组合式函数)

完成后的目录结构应该是这样:

auth-demo/
├── src/
│   ├── views/
│   │   └── auth/
│   │       └── LoginRegister.vue  # 我们的登录注册页面
│   ├── composables/
│   │   ├── useValidation.ts       # 表单验证工具
│   │   ├── useCountdown.ts        # 倒计时工具
│   │   └── useTheme.ts            # 主题切换工具
│   ├── App.vue
│   └── main.ts
├── package.json
└── vite.config.ts

运行项目:

在这里插入图片描述
在这里插入图片描述

🔧 三、创建工具函数(Composables)

在正式写页面之前,我们先把工具函数准备好。这就像做菜前先把调料准备好一样。

3.1 表单验证工具 - useValidation.ts

创建文件:src/composables/useValidation.ts

/**
 * 表单验证工具
 * 这个文件包含了所有表单验证的逻辑
 * 把验证逻辑单独提取出来,可以在多个页面复用
 */

/**
 * 验证结果的类型定义
 * valid: 是否通过验证
 * message: 错误提示信息
 */
interface ValidationResult {
  valid: boolean
  message: string
}

/**
 * 密码验证结果的类型定义(比普通验证多了一个强度属性)
 */
interface PasswordValidationResult extends ValidationResult {
  strength: 'none' | 'weak' | 'medium' | 'strong'
}

/**
 * 导出验证功能
 * 使用 export function 可以在其他文件中导入使用
 */
export function useValidation() {
  
  /**
   * 验证邮箱格式
   * @param email - 要验证的邮箱字符串
   * @returns 返回验证结果对象
   */
  const validateEmail = (email: string): ValidationResult => {
    // 如果邮箱为空,返回错误
    if (!email) {
      return { valid: false, message: '请输入邮箱' }
    }
  
    // 使用正则表达式验证邮箱格式
    // 这个正则的意思是:xxx@xxx.xxx 这样的格式
    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  
    if (!emailPattern.test(email)) {
      return { valid: false, message: '请输入有效的邮箱地址' }
    }
  
    // 验证通过
    return { valid: true, message: '' }
  }

  /**
   * 验证手机号格式(中国大陆)
   * @param phone - 要验证的手机号字符串
   * @returns 返回验证结果对象
   */
  const validatePhone = (phone: string): ValidationResult => {
    if (!phone) {
      return { valid: false, message: '请输入手机号' }
    }
  
    // 中国大陆手机号格式:1开头,第二位是3-9,后面9位数字
    // 比如:13812345678, 15912345678
    const phonePattern = /^1[3-9]\d{9}$/
  
    if (!phonePattern.test(phone)) {
      return { valid: false, message: '请输入有效的手机号' }
    }
  
    return { valid: true, message: '' }
  }

  /**
   * 验证密码强度
   * @param password - 要验证的密码字符串
   * @returns 返回验证结果对象(包含强度信息)
   * 
   * 💡 密码强度计算规则:
   * - 长度 >= 8: +1分
   * - 长度 >= 12: 再+1分
   * - 包含数字: +1分
   * - 包含小写字母: +1分
   * - 包含大写字母: +1分
   * - 包含特殊字符: +1分
   * 
   * 总分 >= 5: 强
   * 总分 >= 3: 中
   * 总分 < 3: 弱
   */
  const validatePassword = (password: string): PasswordValidationResult => {
    if (!password) {
      return { valid: false, message: '请输入密码', strength: 'none' }
    }
  
    if (password.length < 6) {
      return { valid: false, message: '密码至少需要6个字符', strength: 'weak' }
    }
  
    // 计算密码强度
    let strength: 'weak' | 'medium' | 'strong' = 'weak'
    let score = 0
  
    // 长度加分
    if (password.length >= 8) score++   // 长度8位以上+1分
    if (password.length >= 12) score++  // 长度12位以上再+1分
  
    // 包含数字(0-9任意一个)
    if (/\d/.test(password)) score++
  
    // 包含小写字母(a-z任意一个)
    if (/[a-z]/.test(password)) score++
  
    // 包含大写字母(A-Z任意一个)
    if (/[A-Z]/.test(password)) score++
  
    // 包含特殊字符(常见的符号)
    if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score++
  
    // 根据得分判断强度
    if (score >= 5) strength = 'strong'      // 5分以上:强
    else if (score >= 3) strength = 'medium' // 3-4分:中
    // 否则就是弱
  
    return { valid: true, message: '', strength }
  }

  /**
   * 验证用户名
   * @param username - 要验证的用户名字符串
   * @returns 返回验证结果对象
   * 
   * 💡 用户名规则:
   * - 长度:3-20个字符
   * - 允许:字母、数字、下划线、中文
   */
  const validateUsername = (username: string): ValidationResult => {
    if (!username) {
      return { valid: false, message: '请输入用户名' }
    }
  
    if (username.length < 3) {
      return { valid: false, message: '用户名至少需要3个字符' }
    }
  
    if (username.length > 20) {
      return { valid: false, message: '用户名最多20个字符' }
    }
  
    // 只允许:字母(a-zA-Z)、数字(0-9)、下划线(_)、中文(\u4e00-\u9fa5)
    if (!/^[a-zA-Z0-9_\u4e00-\u9fa5]+$/.test(username)) {
      return { valid: false, message: '用户名只能包含字母、数字、下划线和中文' }
    }
  
    return { valid: true, message: '' }
  }

  /**
   * 验证验证码
   * @param code - 要验证的验证码字符串
   * @returns 返回验证结果对象
   * 
   * 💡 验证码规则:必须是6位数字
   */
  const validateCode = (code: string): ValidationResult => {
    if (!code) {
      return { valid: false, message: '请输入验证码' }
    }
  
    // 必须是6位数字
    if (!/^\d{6}$/.test(code)) {
      return { valid: false, message: '请输入6位数字验证码' }
    }
  
    return { valid: true, message: '' }
  }

  // 把所有验证函数导出,供其他组件使用
  return {
    validateEmail,
    validatePhone,
    validatePassword,
    validateUsername,
    validateCode
  }
}

💡 代码解释:

  1. 为什么要单独写这个文件?

    • 验证逻辑可以在多个页面复用(比如个人信息页也需要邮箱验证)
    • 代码更清晰,登录页面不会太长
    • 方便测试和维护
  2. 正则表达式是什么?

    • 就是一种匹配文本的规则
    • /^1[3-9]\d{9}$/ 表示:以1开头,第二位是3-9,后面9位是数字
    • 不用深究,直接复制使用即可
  3. 为什么返回对象?

    • { valid: true, message: '' } 包含了两个信息
    • valid 告诉我们是否通过验证
    • message 告诉我们错误原因(方便显示给用户)

3.2 倒计时工具 - useCountdown.ts

创建文件:src/composables/useCountdown.ts

/**
 * 倒计时工具
 * 用于验证码按钮的60秒倒计时功能
 */

import { ref, computed } from 'vue'

/**
 * 倒计时 Composable
 * @param initialSeconds - 初始秒数,默认60秒
 * @returns 倒计时相关的状态和方法
 */
export function useCountdown(initialSeconds: number = 60) {
  
  // 当前倒计时的秒数
  // ref 是 Vue3 的响应式数据,值改变时页面会自动更新
  const countdown = ref(0)
  
  // 计算属性:是否正在倒计时
  // 当 countdown > 0 时,表示正在倒计时
  const isCountingDown = computed(() => countdown.value > 0)
  
  // 定时器的ID,用于清除定时器
  let timer: number | null = null
  
  /**
   * 开始倒计时
   * 会每秒减1,直到0为止
   */
  const startCountdown = () => {
    // 如果已经在倒计时了,不要重复开始
    if (countdown.value > 0) return
  
    // 设置初始秒数
    countdown.value = initialSeconds
  
    // 每1000毫秒(1秒)执行一次
    timer = window.setInterval(() => {
      countdown.value--  // 秒数减1
    
      // 如果倒计时结束,停止定时器
      if (countdown.value <= 0) {
        stopCountdown()
      }
    }, 1000)
  }
  
  /**
   * 停止倒计时
   * 清除定时器并重置秒数
   */
  const stopCountdown = () => {
    if (timer) {
      clearInterval(timer)  // 清除定时器
      timer = null
    }
    countdown.value = 0  // 重置秒数
  }
  
  // 导出状态和方法
  return {
    countdown,        // 当前秒数
    isCountingDown,   // 是否正在倒计时
    startCountdown,   // 开始倒计时的方法
    stopCountdown     // 停止倒计时的方法
  }
}

💡 使用示例:

<script setup>
const { countdown, isCountingDown, startCountdown } = useCountdown(60)

// 点击按钮后开始倒计时
function handleClick() {
  startCountdown()
}
</script>

<template>
  <button :disabled="isCountingDown" @click="handleClick">
    {{ isCountingDown ? `${countdown}秒后重试` : '获取验证码' }}
  </button>
</template>

3.3 主题切换工具 - useTheme.ts

创建文件:src/composables/useTheme.ts

/**
 * 主题切换工具
 * 实现暗黑模式和浅色模式的切换
 */

import { ref, watch } from 'vue'

// 主题类型:只能是 'light' 或 'dark'
export type Theme = 'light' | 'dark'

/**
 * 主题管理 Composable
 * @returns 主题相关的状态和方法
 */
export function useTheme() {
  
  /**
   * 当前主题
   * 从 localStorage 读取用户之前的选择
   * 如果是第一次访问,默认使用浅色主题
   * 
   * 💡 localStorage 是浏览器提供的本地存储
   * 可以永久保存数据(除非用户清除缓存)
   */
  const theme = ref<Theme>(
    (localStorage.getItem('theme') as Theme) || 'light'
  )

  /**
   * 切换主题
   * 在浅色和暗黑之间切换
   */
  const toggleTheme = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }

  /**
   * 设置指定主题
   * @param newTheme - 要设置的主题
   */
  const setTheme = (newTheme: Theme) => {
    theme.value = newTheme
  }

  /**
   * 监听主题变化
   * 当主题改变时:
   * 1. 保存到 localStorage(下次访问时记住选择)
   * 2. 更新 HTML 标签的 data-theme 属性(让 CSS 生效)
   * 
   * watch 是 Vue3 的监听器,当数据变化时执行回调函数
   * immediate: true 表示立即执行一次(不等数据变化)
   */
  watch(
    theme,
    (newTheme) => {
      // 保存到本地存储
      localStorage.setItem('theme', newTheme)
    
      // 更新 HTML 的 data-theme 属性
      // CSS 可以通过 [data-theme="dark"] 来应用不同的样式
      document.documentElement.setAttribute('data-theme', newTheme)
    },
    { immediate: true }  // 立即执行一次,确保页面加载时就应用主题
  )

  return {
    theme,         // 当前主题
    toggleTheme,   // 切换主题的方法
    setTheme       // 设置主题的方法
  }
}

💡 原理解释:

  1. 为什么保存到 localStorage?

    • 用户选择暗黑模式后,下次访问也应该是暗黑模式
    • localStorage 可以永久保存数据
  2. data-theme 属性是怎么工作的?

    /* 浅色主题的样式 */
    [data-theme="light"] {
      --bg-color: white;
      --text-color: black;
    }
    
    /* 暗黑主题的样式 */
    [data-theme="dark"] {
      --bg-color: black;
      --text-color: white;
    }
    

🎨 四、创建登录注册页面

现在工具函数都准备好了,让我们开始写页面!我们会分步骤来写,先从最简单的开始。

4.1 第一步:创建基础结构

创建文件:src/views/auth/LoginRegister.vue

<script setup lang="ts">
/**
 * 登录注册页面
 * 这是我们的主页面组件
 */

import { ref } from 'vue'

/**
 * 控制显示登录还是注册
 * true 显示登录表单
 * false 显示注册表单
 */
const isLogin = ref(true)

/**
 * 切换登录/注册模式
 */
const toggleMode = () => {
  isLogin.value = !isLogin.value
}
</script>

<template>
  <!-- 最外层容器:占满整个屏幕 -->
  <div class="auth-container">
  
    <!-- 卡片容器:白色半透明的玻璃卡片 -->
    <div class="auth-card">
    
      <!-- 标题:根据模式显示不同文字 -->
      <h1 class="title">
        {{ isLogin ? '欢迎回来' : '创建账号' }}
      </h1>
    
      <!-- 副标题 -->
      <p class="subtitle">
        {{ isLogin ? '登录以继续您的旅程' : '填写信息开始使用' }}
      </p>
    
      <!-- 表单内容区域(后面会填充) -->
      <div class="form-content">
        <p style="color: white; text-align: center;">
          {{ isLogin ? '登录表单区域' : '注册表单区域' }}
        </p>
      </div>
    
      <!-- 底部切换按钮 -->
      <div class="toggle-mode">
        <span>{{ isLogin ? '还没有账号?' : '已有账号?' }}</span>
        <button @click="toggleMode">
          {{ isLogin ? '立即注册' : '立即登录' }}
        </button>
      </div>
    
    </div>
  </div>
</template>

<style scoped>
/**
 * scoped 表示这些样式只在当前组件生效
 * 不会影响其他组件
 */

/* 
 * 最外层容器
 * 占满整个屏幕,内容居中
 */
.auth-container {
  /* 最小高度100vh(vh = viewport height,视口高度) */
  min-height: 100vh;
  
  /* 使用 flex 布局让内容居中 */
  display: flex;
  justify-content: center;  /* 水平居中 */
  align-items: center;      /* 垂直居中 */
  
  /* 内边距:防止内容贴边 */
  padding: 20px;
  
  /* 
   * 渐变背景:这是页面的颜值担当!
   * linear-gradient 创建一个渐变背景
   * 135deg 表示从左上到右下的方向
   * 三个颜色值创建紫-蓝-粉的渐变
   */
  background: linear-gradient(
    135deg,
    #667eea 0%,    /* 紫色 */
    #764ba2 50%,   /* 深紫色 */
    #f093fb 100%   /* 粉色 */
  );
  
  position: relative;
  overflow: hidden;
}

/*
 * 背景装饰球 - 左上角
 * 使用伪元素创建一个模糊的装饰圆球
 */
.auth-container::before {
  content: '';
  position: absolute;
  width: 300px;
  height: 300px;
  border-radius: 50%;  /* 圆形 */
  
  /* blur(80px) 让圆球边缘非常模糊,产生梦幻效果 */
  filter: blur(80px);
  opacity: 0.5;

  background: #667eea;
  top: -150px;
  left: -150px;
  
  /* 浮动动画 */
  animation: float 8s ease-in-out infinite;
}

/*
 * 背景装饰球 - 右下角
 */
.auth-container::after {
  content: '';
  position: absolute;
  width: 300px;
  height: 300px;
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.5;
  
  background: #f093fb;
  bottom: -150px;
  right: -150px;
  
  /* 延迟4秒开始动画,让两个球的运动不同步 */
  animation: float 8s ease-in-out infinite;
  animation-delay: 4s;
}

/*
 * 浮动动画
 * 让装饰球缓慢移动,增加动态感
 */
@keyframes float {
  0%, 100% {
    transform: translate(0, 0);
  }
  50% {
    transform: translate(30px, 30px);
  }
}

/*
 * 卡片容器
 * 这是页面的核心 - 玻璃拟态效果!
 */
.auth-card {
  /* 
   * 玻璃拟态的关键三要素:
   * 1. 半透明背景
   * 2. backdrop-filter 背景模糊
   * 3. 边框和阴影
   */
  
  /* rgba 的最后一个值是透明度,0.15 表示85%透明 */
  background: rgba(255, 255, 255, 0.15);
  
  /* 
   * backdrop-filter 是玻璃拟态的核心
   * blur(20px) 让背景模糊,产生磨砂玻璃效果
   * 注意:某些老浏览器可能不支持
   */
  backdrop-filter: blur(20px);
  
  /* 白色半透明边框,增加立体感 */
  border: 1px solid rgba(255, 255, 255, 0.3);
  
  /* 阴影让卡片浮起来 */
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  
  /* 内边距 */
  padding: 40px;
  
  /* 圆角,让卡片更柔和 */
  border-radius: 24px;
  
  /* 宽度限制 */
  width: 100%;
  max-width: 450px;
  
  /* 
   * z-index: 1 让卡片显示在装饰球上方
   * 必须配合 position 使用
   */
  position: relative;
  z-index: 1;
  
  /* 过渡动画:所有属性变化时平滑过渡 */
  transition: all 0.3s ease;
}

/*
 * 鼠标悬停效果
 * 让卡片向上浮动一点
 */
.auth-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15);
}

/*
 * 标题样式
 */
.title {
  font-size: 32px;
  font-weight: 700;
  margin-bottom: 8px;
  text-align: center;
  color: #ffffff;
  
  /* 文字阴影,增加立体感 */
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/*
 * 副标题样式
 */
.subtitle {
  color: rgba(255, 255, 255, 0.9);
  text-align: center;
  margin-bottom: 32px;
  font-size: 14px;
}

/*
 * 表单内容区域
 */
.form-content {
  margin-bottom: 24px;
}

/*
 * 切换模式区域
 */
.toggle-mode {
  text-align: center;
  margin-top: 24px;
  color: rgba(255, 255, 255, 0.9);
  font-size: 14px;
}

/*
 * 切换按钮
 */
.toggle-mode button {
  color: #ffffff;
  background: rgba(255, 255, 255, 0.2);
  border: none;
  cursor: pointer;
  margin-left: 8px;
  font-weight: 600;
  padding: 4px 12px;
  border-radius: 6px;
  transition: all 0.3s ease;
}

/*
 * 按钮悬停效果
 */
.toggle-mode button:hover {
  background: rgba(255, 255, 255, 0.3);
  transform: translateY(-2px);
}
</style>

App.vue:

<script setup lang="ts">
import LoginRegister from './views/auth/LoginRegister.vue'
</script>

<template>
  <LoginRegister />
</template>

<style>
/* 重置默认样式 */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 
    'Helvetica Neue', Arial, sans-serif;
}
</style>

💡 到这一步,你应该能看到:

  • 一个漂亮的渐变背景
  • 中间有一个半透明的玻璃卡片
  • 两个在背景缓慢移动的装饰球
  • 点击底部按钮可以切换标题文字

🎯 先保存并运行看看效果!

npm run dev

然后在浏览器打开 http://localhost:5173,你应该能看到一个非常漂亮的卡片了!

在这里插入图片描述

4.2 第二步:添加登录表单

现在让我们在卡片里添加真正的登录表单。继续编辑 LoginRegister.vue

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Icon } from '@iconify/vue'  // 导入图标组件

/**
 * 控制显示登录还是注册
 */
const isLogin = ref(true)

/**
 * 登录表单数据
 * 使用 reactive 创建响应式对象
 * reactive 适合对象类型的数据
 */
const loginForm = reactive({
  username: '',    // 用户名
  password: '',    // 密码
  remember: false  // 是否记住我
})

/**
 * 切换登录/注册模式
 */
const toggleMode = () => {
  isLogin.value = !isLogin.value
}

/**
 * 处理登录
 * 点击"立即登录"按钮时调用
 */
const handleLogin = () => {
  console.log('登录数据:', loginForm)
  // 后面会添加验证和真实登录逻辑
}
</script>

<template>
  <div class="auth-container">
    <div class="auth-card">
      <h1 class="title">
        {{ isLogin ? '欢迎回来' : '创建账号' }}
      </h1>
    
      <p class="subtitle">
        {{ isLogin ? '登录以继续您的旅程' : '填写信息开始使用' }}
      </p>
    
      <!-- 登录表单 -->
      <div v-if="isLogin" class="form-content">
      
        <!-- 用户名输入框 -->
        <div class="form-item">
          <!-- 标签 -->
          <label class="form-label">
            <Icon icon="mdi:account" class="label-icon" />
            用户名或邮箱
          </label>
        
          <!-- 输入框 -->
          <input 
            v-model="loginForm.username"
            type="text"
            placeholder="请输入用户名或邮箱"
            class="form-input"
          />
        </div>
      
        <!-- 密码输入框 -->
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:lock" class="label-icon" />
            密码
          </label>
        
          <input 
            v-model="loginForm.password"
            type="password"
            placeholder="请输入密码"
            class="form-input"
          />
        </div>
      
        <!-- 选项行:记住我 + 忘记密码 -->
        <div class="form-options">
          <!-- 记住我 -->
          <label class="checkbox-label">
            <input 
              v-model="loginForm.remember"
              type="checkbox"
              class="checkbox-input"
            />
            <span>记住我</span>
          </label>
        
          <!-- 忘记密码链接 -->
          <a href="#" class="forgot-link">忘记密码?</a>
        </div>
      
        <!-- 登录按钮 -->
        <button class="submit-btn" @click="handleLogin">
          <Icon icon="mdi:login" class="btn-icon" />
          立即登录
        </button>
      
      </div>
    
      <!-- 注册表单(暂时只显示文字) -->
      <div v-else class="form-content">
        <p style="color: white; text-align: center;">
          注册表单(下一步添加)
        </p>
        </div>
      
      <div class="toggle-mode">
        <span>{{ isLogin ? '还没有账号?' : '已有账号?' }}</span>
        <button @click="toggleMode">
          {{ isLogin ? '立即注册' : '立即登录' }}
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
/* ...前面的样式保持不变,添加以下新样式... */

/*
 * 表单内容动画
 * 当切换登录/注册时有淡入效果
 */
.form-content {
  animation: fadeIn 0.5s ease;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/*
 * 表单项(每个输入框的外层容器)
 */
.form-item {
  margin-bottom: 20px;
}

/*
 * 表单标签
 */
.form-label {
  display: flex;
  align-items: center;
  gap: 6px;
  color: rgba(255, 255, 255, 0.95);
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 8px;
}

/*
 * 标签图标
 */
.label-icon {
  font-size: 18px;
}

/*
 * 输入框样式
 * 这是表单的颜值担当!
 */
.form-input {
  /* 宽度占满父容器 */
  width: 100%;
  
  /* 内边距 */
  padding: 12px 16px;
  
  /* 半透明白色背景 */
  background: rgba(255, 255, 255, 0.2);
  
  /* 半透明白色边框 */
  border: 1px solid rgba(255, 255, 255, 0.3);
  
  /* 圆角 */
  border-radius: 12px;
  
  /* 文字颜色 */
  color: #ffffff;
  
  /* 字体大小 */
  font-size: 14px;
  
  /* 过渡动画 */
  transition: all 0.3s ease;
  
  /* 确保宽度计算包含 padding 和 border */
  box-sizing: border-box;
}

/*
 * 输入框占位符(placeholder)样式
 */
.form-input::placeholder {
  color: rgba(255, 255, 255, 0.5);
}

/*
 * 输入框聚焦效果
 * 这是最酷炫的部分!
 */
.form-input:focus {
  /* 去掉浏览器默认的蓝色边框 */
  outline: none;
  
  /* 背景变亮一点 */
  background: rgba(255, 255, 255, 0.25);
  
  /* 边框变亮 */
  border-color: rgba(255, 255, 255, 0.6);
  
  /* 外发光效果 */
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
  
  /* 轻微上浮 */
  transform: translateY(-2px);
}

/*
 * 表单选项行
 * 记住我和忘记密码在同一行
 */
.form-options {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
  font-size: 14px;
}

/*
 * 复选框标签
 */
.checkbox-label {
  display: flex;
  align-items: center;
  gap: 6px;
  color: rgba(255, 255, 255, 0.9);
  cursor: pointer;
}

/*
 * 复选框
 */
.checkbox-input {
  width: 16px;
  height: 16px;
  cursor: pointer;
}

/*
 * 忘记密码链接
 */
.forgot-link {
  color: rgba(255, 255, 255, 0.9);
  text-decoration: none;
  transition: color 0.3s ease;
}

.forgot-link:hover {
  color: #ffffff;
  text-decoration: underline;
}

/*
 * 提交按钮
 * 超大、超显眼的按钮!
 */
.submit-btn {
  /* 宽度占满 */
  width: 100%;
  
  /* 内边距 */
  padding: 14px;
  
  /* 渐变背景 */
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  
  /* 无边框 */
  border: none;
  
  /* 圆角 */
  border-radius: 12px;
  
  /* 白色文字 */
  color: #ffffff;
  
  /* 字体 */
  font-size: 16px;
  font-weight: 600;
  
  /* 鼠标样式 */
  cursor: pointer;
  
  /* 过渡动画 */
  transition: all 0.3s ease;
  
  /* 使用 flex 让图标和文字居中 */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  
  /* 阴影 */
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}

/*
 * 按钮悬停效果
 */
.submit-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}

/*
 * 按钮按下效果
 */
.submit-btn:active {
  transform: translateY(0);
}

/*
 * 按钮图标
 */
.btn-icon {
  font-size: 20px;
}

/* ...前面容器、卡片等样式保持不变... */
</style>

💡 保存后刷新浏览器,你应该能看到:

  • 一个完整的登录表单
  • 输入框有漂亮的聚焦效果
  • 按钮有悬停和按下动画

在这里插入图片描述

4.3 第三步:添加注册表单

现在让我们添加注册表单。注册表单比登录表单复杂一些,有更多输入项。

继续编辑 LoginRegister.vue

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Icon } from '@iconify/vue'

const isLogin = ref(true)

// 登录表单数据
const loginForm = reactive({
  username: '',
  password: '',
  remember: false
})

/**
 * 注册表单数据
 * 包含更多字段
 */
const registerForm = reactive({
  username: '',         // 用户名
  email: '',            // 邮箱
  phone: '',            // 手机号
  password: '',         // 密码
  confirmPassword: '',  // 确认密码
  code: ''              // 验证码
})

const toggleMode = () => {
  isLogin.value = !isLogin.value
}

const handleLogin = () => {
  console.log('登录数据:', loginForm)
}

/**
 * 处理注册
 * 点击"立即注册"按钮时调用
 */
const handleRegister = () => {
  console.log('注册数据:', registerForm)
  // 后面会添加验证和真实注册逻辑
}

/**
 * 获取验证码
 * 点击"获取验证码"按钮时调用
 */
const handleGetCode = () => {
  console.log('发送验证码到:', registerForm.phone)
  // 后面会添加倒计时和真实发送逻辑
}
</script>

<template>
  <div class="auth-container">
    <div class="auth-card">
      <h1 class="title">
        {{ isLogin ? '欢迎回来' : '创建账号' }}
      </h1>
    
      <p class="subtitle">
        {{ isLogin ? '登录以继续您的旅程' : '填写信息开始使用' }}
      </p>
    
      <!-- 登录表单(保持不变) -->
      <div v-if="isLogin" class="form-content">
        <!-- ...登录表单代码不变... -->
      </div>
    
      <!-- 注册表单 -->
      <div v-else class="form-content">
      
        <!-- 用户名 -->
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:account" class="label-icon" />
            用户名
          </label>
          <input 
            v-model="registerForm.username"
            type="text"
            placeholder="请输入用户名(3-20个字符)"
            class="form-input"
          />
        </div>
      
        <!-- 邮箱 -->
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:email" class="label-icon" />
            邮箱
          </label>
          <input 
            v-model="registerForm.email"
            type="email"
            placeholder="请输入邮箱"
            class="form-input"
          />
        </div>
      
        <!-- 手机号 + 获取验证码按钮 -->
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:phone" class="label-icon" />
            手机号
          </label>
        
          <!-- 
            输入框和按钮在同一行
            使用 input-with-button 容器
          -->
          <div class="input-with-button">
            <input 
              v-model="registerForm.phone"
              type="tel"
              placeholder="请输入手机号"
              class="form-input"
            />
          
            <!-- 获取验证码按钮 -->
            <button class="code-btn" @click="handleGetCode">
              获取验证码
            </button>
          </div>
        </div>
      
        <!-- 验证码 -->
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:message-text" class="label-icon" />
            验证码
          </label>
          <input 
            v-model="registerForm.code"
            type="text"
            placeholder="请输入6位验证码"
            class="form-input"
            maxlength="6"
          />
        </div>
      
        <!-- 密码 -->
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:lock" class="label-icon" />
            密码
          </label>
          <input 
            v-model="registerForm.password"
            type="password"
            placeholder="请输入密码(至少6个字符)"
            class="form-input"
          />
        </div>
      
        <!-- 确认密码 -->
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:lock-check" class="label-icon" />
            确认密码
          </label>
          <input 
            v-model="registerForm.confirmPassword"
            type="password"
            placeholder="请再次输入密码"
            class="form-input"
          />
        </div>
      
        <!-- 注册按钮 -->
        <button class="submit-btn" @click="handleRegister">
          <Icon icon="mdi:account-plus" class="btn-icon" />
          立即注册
        </button>
      
      </div>
    
      <div class="toggle-mode">
        <span>{{ isLogin ? '还没有账号?' : '已有账号?' }}</span>
        <button @click="toggleMode">
          {{ isLogin ? '立即注册' : '立即登录' }}
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
/* ...前面的样式保持不变,添加以下新样式... */

/*
 * 带按钮的输入框容器
 * 让输入框和按钮在同一行
 */
.input-with-button {
  display: flex;
  gap: 8px;  /* 输入框和按钮之间的间距 */
}

/*
 * 输入框占据剩余空间
 */
.input-with-button .form-input {
  flex: 1;
}

/*
 * 验证码按钮
 */
.code-btn {
  /* 内边距 */
  padding: 12px 20px;
  
  /* 半透明白色背景 */
  background: rgba(255, 255, 255, 0.25);
  
  /* 边框 */
  border: 1px solid rgba(255, 255, 255, 0.3);
  
  /* 圆角 */
  border-radius: 12px;
  
  /* 文字颜色 */
  color: #ffffff;
  
  /* 字体 */
  font-size: 13px;
  font-weight: 500;
  
  /* 鼠标样式 */
  cursor: pointer;
  
  /* 过渡动画 */
  transition: all 0.3s ease;
  
  /* 不换行 */
  white-space: nowrap;
}

/*
 * 验证码按钮悬停效果
 */
.code-btn:hover {
  background: rgba(255, 255, 255, 0.35);
  transform: translateY(-2px);
}

/* ...其他样式保持不变... */
</style>

💡 现在你可以:
在这里插入图片描述

  • 点击底部按钮切换登录/注册
  • 看到流畅的切换动画
  • 注册表单有更多输入项

🎯 五、添加表单验证功能

现在表单能显示了,但还没有验证功能。让我们把前面写好的验证工具用起来!

5.1 在页面中使用验证功能

继续编辑 LoginRegister.vue,添加验证逻辑:

<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { Icon } from '@iconify/vue'
import { ElMessage } from 'element-plus'  // Element Plus 的消息提示

// 导入我们前面写好的工具函数
import { useValidation } from '../../composables/useValidation'
import { useCountdown } from '../../composables/useCountdown'

const isLogin = ref(true)

// 使用验证工具
const {
  validateEmail,
  validatePhone,
  validatePassword,
  validateUsername,
  validateCode
} = useValidation()

// 使用倒计时工具(60秒)
const { countdown, isCountingDown, startCountdown } = useCountdown(60)

// 登录表单数据
const loginForm = reactive({
  username: '',
  password: '',
  remember: false
})

/**
 * 登录表单错误信息
 * 用于显示验证错误提示
 */
const loginErrors = reactive({
  username: '',
  password: ''
})

// 注册表单数据
const registerForm = reactive({
  username: '',
  email: '',
  phone: '',
  password: '',
  confirmPassword: '',
  code: ''
})

/**
 * 注册表单错误信息
 */
const registerErrors = reactive({
  username: '',
  email: '',
  phone: '',
  password: '',
  confirmPassword: '',
  code: ''
})

/**
 * 计算密码强度
 * 使用 computed 计算属性,密码改变时自动更新
 */
const passwordStrength = computed(() => {
  if (!registerForm.password) return 'none'
  return validatePassword(registerForm.password).strength
})

/**
 * 密码强度对应的文字
 */
const passwordStrengthText = computed(() => {
  const map = {
    none: '',
    weak: '弱',
    medium: '中',
    strong: '强'
  }
  return map[passwordStrength.value]
})

/**
 * 密码强度对应的颜色
 */
const passwordStrengthColor = computed(() => {
  const map = {
    none: '',
    weak: '#f56c6c',    // 红色
    medium: '#e6a23c',  // 橙色
    strong: '#67c23a'   // 绿色
  }
  return map[passwordStrength.value]
})

const toggleMode = () => {
  isLogin.value = !isLogin.value
  
  // 切换模式时清空所有错误信息
  Object.keys(loginErrors).forEach(key => loginErrors[key] = '')
  Object.keys(registerErrors).forEach(key => registerErrors[key] = '')
}

/**
 * 验证登录表单
 * @returns 返回是否通过验证
 */
const validateLoginForm = (): boolean => {
  let isValid = true
  
  // 验证用户名
  const usernameResult = validateUsername(loginForm.username)
  if (!usernameResult.valid) {
    loginErrors.username = usernameResult.message
    isValid = false
  } else {
    loginErrors.username = ''
  }
  
  // 验证密码
  if (!loginForm.password) {
    loginErrors.password = '请输入密码'
    isValid = false
  } else if (loginForm.password.length < 6) {
    loginErrors.password = '密码至少需要6个字符'
    isValid = false
  } else {
    loginErrors.password = ''
  }
  
  return isValid
}

/**
 * 处理登录
 */
const handleLogin = () => {
  // 先验证表单
  if (!validateLoginForm()) {
    ElMessage.warning('请检查输入信息')
    return
  }
  
  console.log('登录数据:', loginForm)
  ElMessage.success('登录成功!')
  
  // 这里添加真实的登录 API 调用
  // 比如:await loginApi(loginForm)
}

/**
 * 验证注册表单
 * @returns 返回是否通过验证
 */
const validateRegisterForm = (): boolean => {
  let isValid = true
  
  // 验证用户名
  const usernameResult = validateUsername(registerForm.username)
  registerErrors.username = usernameResult.message
  if (!usernameResult.valid) isValid = false
  
  // 验证邮箱
  const emailResult = validateEmail(registerForm.email)
  registerErrors.email = emailResult.message
  if (!emailResult.valid) isValid = false
  
  // 验证手机号
  const phoneResult = validatePhone(registerForm.phone)
  registerErrors.phone = phoneResult.message
  if (!phoneResult.valid) isValid = false
  
  // 验证验证码
  const codeResult = validateCode(registerForm.code)
  registerErrors.code = codeResult.message
  if (!codeResult.valid) isValid = false
  
  // 验证密码
  const passwordResult = validatePassword(registerForm.password)
  registerErrors.password = passwordResult.message
  if (!passwordResult.valid) isValid = false
  
  // 验证确认密码
  if (!registerForm.confirmPassword) {
    registerErrors.confirmPassword = '请确认密码'
    isValid = false
  } else if (registerForm.password !== registerForm.confirmPassword) {
    registerErrors.confirmPassword = '两次输入的密码不一致'
    isValid = false
  } else {
    registerErrors.confirmPassword = ''
  }
  
  return isValid
}

/**
 * 处理注册
 */
const handleRegister = () => {
  // 先验证表单
  if (!validateRegisterForm()) {
    ElMessage.warning('请检查输入信息')
    return
  }
  
  console.log('注册数据:', registerForm)
  ElMessage.success('注册成功!')
  
  // 这里添加真实的注册 API 调用
}

/**
 * 获取验证码
 */
const handleGetCode = () => {
  // 先验证手机号
  const phoneResult = validatePhone(registerForm.phone)
  if (!phoneResult.valid) {
    registerErrors.phone = phoneResult.message
    ElMessage.warning(phoneResult.message)
    return
  }
  
  // 清空手机号错误
  registerErrors.phone = ''
  
  // 开始倒计时
  startCountdown()
  
  ElMessage.success('验证码已发送!')
  
  // 这里添加真实的发送验证码 API 调用
  // 比如:await sendCodeApi(registerForm.phone)
}

/**
 * 输入框失去焦点时验证
 * 这样用户填完一个输入框,就能立即看到错误提示
 */
const handleBlurUsername = (isLoginMode: boolean) => {
  if (isLoginMode) {
    const result = validateUsername(loginForm.username)
    loginErrors.username = result.message
  } else {
    const result = validateUsername(registerForm.username)
    registerErrors.username = result.message
  }
}

const handleBlurEmail = () => {
  const result = validateEmail(registerForm.email)
  registerErrors.email = result.message
}

const handleBlurPhone = () => {
  const result = validatePhone(registerForm.phone)
  registerErrors.phone = result.message
}
</script>

<template>
  <div class="auth-container">
    <div class="auth-card">
      <h1 class="title">
        {{ isLogin ? '欢迎回来' : '创建账号' }}
      </h1>
    
      <p class="subtitle">
        {{ isLogin ? '登录以继续您的旅程' : '填写信息开始使用' }}
      </p>
    
      <!-- 登录表单 -->
      <div v-if="isLogin" class="form-content">
      
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:account" class="label-icon" />
            用户名或邮箱
          </label>
          <input 
            v-model="loginForm.username"
            type="text"
            placeholder="请输入用户名或邮箱"
            class="form-input"
            :class="{ 'input-error': loginErrors.username }"
            @blur="handleBlurUsername(true)"
          />
          <!-- 错误提示 -->
          <div v-if="loginErrors.username" class="error-message">
            {{ loginErrors.username }}
          </div>
        </div>
      
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:lock" class="label-icon" />
            密码
          </label>
          <input 
            v-model="loginForm.password"
            type="password"
            placeholder="请输入密码"
            class="form-input"
            :class="{ 'input-error': loginErrors.password }"
          />
          <div v-if="loginErrors.password" class="error-message">
            {{ loginErrors.password }}
          </div>
        </div>
      
        <div class="form-options">
          <label class="checkbox-label">
            <input 
              v-model="loginForm.remember"
              type="checkbox"
              class="checkbox-input"
            />
            <span>记住我</span>
          </label>
          <a href="#" class="forgot-link">忘记密码?</a>
        </div>
      
        <button class="submit-btn" @click="handleLogin">
          <Icon icon="mdi:login" class="btn-icon" />
          立即登录
        </button>
      
      </div>
    
      <!-- 注册表单 -->
      <div v-else class="form-content">
      
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:account" class="label-icon" />
            用户名
          </label>
          <input 
            v-model="registerForm.username"
            type="text"
            placeholder="请输入用户名(3-20个字符)"
            class="form-input"
            :class="{ 'input-error': registerErrors.username }"
            @blur="handleBlurUsername(false)"
          />
          <div v-if="registerErrors.username" class="error-message">
            {{ registerErrors.username }}
          </div>
        </div>
      
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:email" class="label-icon" />
            邮箱
          </label>
          <input 
            v-model="registerForm.email"
            type="email"
            placeholder="请输入邮箱"
            class="form-input"
            :class="{ 'input-error': registerErrors.email }"
            @blur="handleBlurEmail"
          />
          <div v-if="registerErrors.email" class="error-message">
            {{ registerErrors.email }}
          </div>
        </div>
      
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:phone" class="label-icon" />
            手机号
          </label>
          <div class="input-with-button">
            <div class="input-wrapper">
              <input 
                v-model="registerForm.phone"
                type="tel"
                placeholder="请输入手机号"
                class="form-input"
                :class="{ 'input-error': registerErrors.phone }"
                @blur="handleBlurPhone"
              />
            </div>
          
            <!-- 
              :disabled 绑定是否禁用
              倒计时期间按钮禁用
            -->
            <button 
              class="code-btn"
              :disabled="isCountingDown"
              @click="handleGetCode"
            >
              {{ isCountingDown ? `${countdown}秒后重试` : '获取验证码' }}
            </button>
          </div>
          <div v-if="registerErrors.phone" class="error-message">
            {{ registerErrors.phone }}
          </div>
        </div>
      
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:message-text" class="label-icon" />
            验证码
          </label>
          <input 
            v-model="registerForm.code"
            type="text"
            placeholder="请输入6位验证码"
            class="form-input"
            :class="{ 'input-error': registerErrors.code }"
            maxlength="6"
          />
          <div v-if="registerErrors.code" class="error-message">
            {{ registerErrors.code }}
          </div>
        </div>
      
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:lock" class="label-icon" />
            密码
          
            <!-- 密码强度显示 -->
            <span 
              v-if="passwordStrength !== 'none'" 
              class="password-strength"
              :style="{ color: passwordStrengthColor }"
            >
              强度:{{ passwordStrengthText }}
            </span>
          </label>
        
          <input 
            v-model="registerForm.password"
            type="password"
            placeholder="请输入密码(至少6个字符)"
            class="form-input"
            :class="{ 'input-error': registerErrors.password }"
          />
        
          <!-- 密码强度条 -->
          <div v-if="registerForm.password" class="strength-bar">
            <div 
              class="strength-fill"
              :class="`strength-${passwordStrength}`"
            ></div>
          </div>
        
          <div v-if="registerErrors.password" class="error-message">
            {{ registerErrors.password }}
          </div>
        </div>
      
        <div class="form-item">
          <label class="form-label">
            <Icon icon="mdi:lock-check" class="label-icon" />
            确认密码
          </label>
          <input 
            v-model="registerForm.confirmPassword"
            type="password"
            placeholder="请再次输入密码"
            class="form-input"
            :class="{ 'input-error': registerErrors.confirmPassword }"
          />
          <div v-if="registerErrors.confirmPassword" class="error-message">
            {{ registerErrors.confirmPassword }}
          </div>
        </div>
      
        <button class="submit-btn" @click="handleRegister">
          <Icon icon="mdi:account-plus" class="btn-icon" />
          立即注册
        </button>
      
      </div>
    
      <div class="toggle-mode">
        <span>{{ isLogin ? '还没有账号?' : '已有账号?' }}</span>
        <button @click="toggleMode">
          {{ isLogin ? '立即注册' : '立即登录' }}
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
/* ...前面的样式保持不变,添加以下新样式... */

/*
 * 输入框错误状态
 */
.input-error {
  border-color: #f56c6c !important;
  background: rgba(245, 108, 108, 0.1) !important;
}

/*
 * 错误提示信息
 */
.error-message {
  color: #f56c6c;
  font-size: 12px;
  margin-top: 6px;
  display: flex;
  align-items: center;
  gap: 4px;
  
  /* 抖动动画,增加注意力 */
  animation: shake 0.3s ease;
}

/*
 * 抖动动画
 */
@keyframes shake {
  0%, 100% {
    transform: translateX(0);
  }
  25% {
    transform: translateX(-5px);
  }
  75% {
    transform: translateX(5px);
  }
}

/*
 * 密码强度文字
 */
.password-strength {
  margin-left: auto;
  font-size: 12px;
  font-weight: 600;
}

/*
 * 密码强度条容器
 */
.strength-bar {
  width: 100%;
  height: 4px;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 2px;
  margin-top: 8px;
  overflow: hidden;
}

/*
 * 密码强度条填充
 */
.strength-fill {
  height: 100%;
  transition: all 0.3s ease;
  border-radius: 2px;
}

/*
 * 弱密码:红色,宽度33%
 */
.strength-weak {
  width: 33%;
  background: #f56c6c;
}

/*
 * 中等密码:橙色,宽度66%
 */
.strength-medium {
  width: 66%;
  background: #e6a23c;
}

/*
 * 强密码:绿色,宽度100%
 */
.strength-strong {
  width: 100%;
  background: #67c23a;
}

/*
 * 验证码按钮禁用状态
 */
.code-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.code-btn:disabled:hover {
  transform: none;
  background: rgba(255, 255, 255, 0.25);
}

/*
 * 输入框包装器
 */
.input-wrapper {
  flex: 1;
}
</style>

在这里插入图片描述

💡 到这一步,你的页面已经有完整的验证功能了!

  • ✅ 输入框失去焦点时自动验证
  • ✅ 错误信息有红色高亮和抖动动画
  • ✅ 密码强度实时显示(弱/中/强)
  • ✅ 密码强度条动态变化
  • ✅ 验证码按钮有60秒倒计时

🌙 六、添加暗黑模式

现在让我们添加一个酷炫的暗黑模式切换功能!

6.1 在页面中添加主题切换

继续编辑 LoginRegister.vue

<script setup lang="ts">
// ...前面的导入保持不变...
import { useTheme } from '../../composables/useTheme'  // 添加这行

// ...前面的代码保持不变...

// 使用主题工具
const { theme, toggleTheme } = useTheme()

// ...其他代码保持不变...
</script>

<template>
  <div class="auth-container">
  
    <!-- 主题切换按钮(右上角) -->
    <button class="theme-toggle" @click="toggleTheme">
      <!-- 根据当前主题显示不同图标 -->
      <Icon 
        :icon="theme === 'light' ? 'mdi:weather-night' : 'mdi:weather-sunny'" 
        class="theme-icon"
      />
    </button>
  
    <div class="auth-card">
      <!-- ...表单内容保持不变... -->
    </div>
  </div>
</template>

<style scoped>


/*
 * 主题切换按钮
 * 固定在右上角
 */
.theme-toggle {
  /* 固定定位 */
  position: fixed;
  top: 20px;
  right: 20px;
  
  /* 圆形按钮 */
  width: 50px;
  height: 50px;
  border-radius: 50%;
  
  /* 半透明玻璃效果 */
  background: var(--card-bg);
  backdrop-filter: blur(20px);
  border: 1px solid var(--input-border);
  
  /* 文字颜色 */
  color: var(--text-primary);
  
  /* 鼠标样式 */
  cursor: pointer;
  
  /* 过渡动画 */
  transition: all 0.3s ease;
  
  /* 层级:显示在最上面 */
  z-index: 100;
  
  /* 使用 flex 让图标居中 */
  display: flex;
  align-items: center;
  justify-content: center;
  
  /* 阴影 */
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}

/*
 * 按钮悬停效果
 * 上浮并旋转
 */
.theme-toggle:hover {
  transform: translateY(-3px) rotate(15deg);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}

/*
 * 主题图标
 */
.theme-icon {
  font-size: 24px;
  transition: transform 0.3s ease;
}

/*
 * 悬停时图标旋转
 */
.theme-toggle:hover .theme-icon {
  transform: rotate(180deg);
}

/*
 * 更新容器背景使用 CSS 变量
 */
.auth-container {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 20px;
  
  /* 使用 CSS 变量 */
  background: linear-gradient(
    135deg, 
    var(--bg-gradient-1) 0%, 
    var(--bg-gradient-2) 50%, 
    var(--bg-gradient-3) 100%
  );
  
  position: relative;
  overflow: hidden;
  
  /* 添加过渡动画 */
  transition: background 0.5s ease;
}

/*
 * 更新卡片背景
 */
.auth-card {
  background: var(--card-bg);
  backdrop-filter: blur(20px);
  border: 1px solid var(--input-border);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  
  padding: 40px;
  border-radius: 24px;
  width: 100%;
  max-width: 450px;
  position: relative;
  z-index: 1;
  transition: all 0.3s ease;
}

/*
 * 更新文字颜色
 */
.title {
  font-size: 32px;
  font-weight: 700;
  margin-bottom: 8px;
  text-align: center;
  color: var(--text-primary);
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.subtitle {
  color: var(--text-secondary);
  text-align: center;
  margin-bottom: 32px;
  font-size: 14px;
}

.form-label {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--text-secondary);
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 8px;
}

/*
 * 更新输入框样式使用 CSS 变量
 */
.form-input {
  width: 100%;
  padding: 12px 16px;
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  border-radius: 12px;
  color: var(--text-primary);
  font-size: 14px;
  transition: all 0.3s ease;
  box-sizing: border-box;
}

.form-input::placeholder {
  color: var(--text-secondary);
  opacity: 0.6;
}

.form-input:focus {
  outline: none;
  background: var(--input-focus-bg);
  border-color: rgba(255, 255, 255, 0.6);
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
  transform: translateY(-2px);
}

.toggle-mode {
  text-align: center;
  margin-top: 24px;
  color: var(--text-secondary);
  font-size: 14px;
}

.toggle-mode button {
  color: var(--text-primary);
  background: var(--input-bg);
  border: none;
  cursor: pointer;
  margin-left: 8px;
  font-weight: 600;
  padding: 4px 12px;
  border-radius: 6px;
  transition: all 0.3s ease;
}

.checkbox-label {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--text-secondary);
  cursor: pointer;
}

.forgot-link {
  color: var(--text-secondary);
  text-decoration: none;
  transition: color 0.3s ease;
}

.forgot-link:hover {
  color: var(--text-primary);
  text-decoration: underline;
}

.code-btn {
  padding: 12px 20px;
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  border-radius: 12px;
  color: var(--text-primary);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.3s ease;
  white-space: nowrap;
}

.code-btn:hover {
  background: var(--input-focus-bg);
  transform: translateY(-2px);
}

/* ...其他样式保持不变(提交按钮、错误提示、密码强度等)... */
</style>

App.vue

<script setup lang="ts">
import LoginRegister from './views/auth/LoginRegister.vue'
</script>

<template>
  <LoginRegister />
</template>

<style>
/* 重置默认样式 */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 
    'Helvetica Neue', Arial, sans-serif;
}

/*
 * ===== 主题 CSS 变量定义 =====
 * 这些变量必须定义在全局样式中,不能在 scoped 样式中
 */

/* 浅色主题变量 */
:root[data-theme="light"] {
  --bg-gradient-1: #667eea;
  --bg-gradient-2: #764ba2;
  --bg-gradient-3: #f093fb;
  --card-bg: rgba(255, 255, 255, 0.15);
  --text-primary: #ffffff;
  --text-secondary: rgba(255, 255, 255, 0.9);
  --input-bg: rgba(255, 255, 255, 0.2);
  --input-border: rgba(255, 255, 255, 0.3);
  --input-focus-bg: rgba(255, 255, 255, 0.25);
  --button-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

/* 暗黑主题变量 */
:root[data-theme="dark"] {
  --bg-gradient-1: #1a1a2e;
  --bg-gradient-2: #16213e;
  --bg-gradient-3: #0f3460;
  --card-bg: rgba(30, 30, 46, 0.8);
  --text-primary: #e0e0e0;
  --text-secondary: rgba(224, 224, 224, 0.8);
  --input-bg: rgba(255, 255, 255, 0.05);
  --input-border: rgba(255, 255, 255, 0.1);
  --input-focus-bg: rgba(255, 255, 255, 0.1);
  --button-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
</style>

在这里插入图片描述

💡 现在你的页面支持暗黑模式了!

  • ✅ 点击右上角按钮切换主题
  • ✅ 主题选择会保存到 localStorage
  • ✅ 下次访问时自动应用上次的主题
  • ✅ 所有颜色平滑过渡

📱 七、响应式布局优化

让我们确保页面在手机上也好看。在 <style scoped> 的末尾添加:

/*
 * ======== 响应式布局 ========
 */

/* 平板和手机(屏幕宽度 <= 768px) */
@media (max-width: 768px) {
  .auth-container {
    padding: 16px;
  }
  
  .auth-card {
    padding: 24px 20px;
    border-radius: 20px;
  }
  
  .title {
    font-size: 24px;
  }
  
  .subtitle {
    font-size: 13px;
  }
  
  .form-input {
    padding: 10px 14px;
    font-size: 14px;
  }
  
  /* 验证码按钮在小屏幕上独占一行 */
  .input-with-button {
    flex-direction: column;
  }
  
  .code-btn {
    width: 100%;
    padding: 10px;
  }
  
  .submit-btn {
    padding: 12px;
    font-size: 15px;
  }
  
  .theme-toggle {
    width: 44px;
    height: 44px;
    top: 16px;
    right: 16px;
  }
  
  .theme-icon {
    font-size: 20px;
  }
}

/* 超小屏幕(屏幕宽度 <= 375px) */
@media (max-width: 375px) {
  .auth-card {
    padding: 20px 16px;
  }
  
  .title {
    font-size: 22px;
  }
  
  .form-item {
    margin-bottom: 16px;
  }
}

🎨 八、配置 Element Plus

为了让消息提示(ElMessage)正常工作,我们需要在 main.ts 中配置 Element Plus。

编辑 src/main.ts

import { createApp } from 'vue'
import App from './App.vue'

// 导入 Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)

// 使用 Element Plus
app.use(ElementPlus)

app.mount('#app')

🚀 九、配置路由(可选)

如果你想通过路由访问登录页面,可以安装并配置 Vue Router。

9.1 安装 Vue Router

npm install vue-router@4
# 或
pnpm add vue-router@4

9.2 创建路由配置

创建文件:src/router/index.ts

import { createRouter, createWebHistory } from 'vue-router'
import LoginRegister from '@/views/auth/LoginRegister.vue'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      redirect: '/auth'
    },
    {
      path: '/auth',
      name: 'Auth',
      component: LoginRegister
    }
  ]
})

export default router

9.3 在 main.ts 中使用路由

编辑 src/main.ts

import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router'  // 添加这行

const app = createApp(App)

app.use(ElementPlus)
app.use(router)  // 添加这行

app.mount('#app')

9.4 更新 App.vue

编辑 src/App.vue

<script setup lang="ts">
</script>

<template>
  <router-view />
</template>

<style>
/* 重置默认样式 */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 
    'Helvetica Neue', Arial, sans-serif;
}
</style>

✅ 十、运行项目

现在一切都准备好了!让我们运行项目看看效果。

npm run dev

打开浏览器访问 http://localhost:5173,你应该能看到:

✅ 一个超级漂亮的渐变背景
✅ 半透明的玻璃拟态卡片
✅ 完整的登录和注册表单
✅ 实时的表单验证
✅ 密码强度显示
✅ 验证码倒计时
✅ 暗黑模式切换
✅ 响应式布局


💡 十一、知识点总结

通过这个项目,你应该学会了:

11.1 Vue3 核心知识

  1. Composition API

    • ref 创建响应式基本类型数据
    • reactive 创建响应式对象
    • computed 计算属性(自动根据依赖更新)
    • watch 监听器(数据变化时执行回调)
  2. 组件化思想

    • 自定义 Composables(可复用的逻辑)
    • 逻辑和视图分离
    • 关注点分离原则
  3. 表单处理

    • v-model 双向绑定
    • @blur 失去焦点事件
    • 表单验证逻辑
    • 错误提示显示

11.2 CSS 技巧

  1. 现代 CSS

    • CSS 变量(--variable-name
    • backdrop-filter 毛玻璃效果
    • linear-gradient 渐变背景
    • transitionanimation 动画
  2. 响应式设计

    • @media 媒体查询
    • Flexbox 弹性布局
    • 移动端适配
  3. 玻璃拟态设计

    • 半透明背景
    • 背景模糊
    • 边框和阴影

11.3 TypeScript

  1. 类型定义

    • interface 定义接口
    • 函数参数类型
    • 返回值类型
  2. 类型推导

    • TypeScript 自动推导类型
    • 减少类型标注

11.4 用户体验

  1. 视觉反馈

    • 输入框聚焦效果
    • 错误提示动画
    • 按钮悬停效果
    • 加载状态显示
  2. 交互优化

    • 实时验证
    • 友好的错误提示
    • 流畅的动画
    • 倒计时防止重复点击

🚀 十二、下一步优化建议

如果你想继续提升这个页面,可以尝试:

12.1 功能增强

  1. 第三方登录

    <div class="social-login">
      <button class="social-btn wechat">
        <Icon icon="mdi:wechat" /> 微信登录
      </button>
      <button class="social-btn qq">
        <Icon icon="mdi:qqchat" /> QQ登录
      </button>
    </div>
    
  2. 密码可见性切换

    <div class="password-input">
      <input :type="passwordVisible ? 'text' : 'password'" />
      <button @click="passwordVisible = !passwordVisible">
        <Icon :icon="passwordVisible ? 'mdi:eye' : 'mdi:eye-off'" />
      </button>
    </div>
    
  3. 键盘快捷键

    // 按 Enter 键提交表单
    const handleKeydown = (e: KeyboardEvent) => {
      if (e.key === 'Enter') {
        isLogin.value ? handleLogin() : handleRegister()
      }
    }
    

12.2 技术进阶

  1. 连接真实 API

    // src/api/auth.ts
    import request from '@/utils/request'
    
    export const login = (data: LoginParams) => {
      return request.post('/api/auth/login', data)
    }
    
  2. 使用 Pinia 管理用户状态

    // src/stores/user.ts
    import { defineStore } from 'pinia'
    
    export const useUserStore = defineStore('user', {
      state: () => ({
        token: '',
        userInfo: null
      }),
      actions: {
        async login(data: LoginParams) {
          const res = await loginApi(data)
          this.token = res.token
        }
      }
    })
    
  3. 添加加载状态

    const loading = ref(false)
    
    const handleLogin = async () => {
      loading.value = true
      try {
        await loginApi(loginForm)
      } finally {
        loading.value = false
      }
    }
    

📚 十三、常见问题解答

Q1: backdrop-filter 在某些浏览器不生效?

A: backdrop-filter 在旧版浏览器可能不支持。解决方案:

.auth-card {
  /* 降级方案:不透明背景 */
  background: rgba(255, 255, 255, 0.9);
  
  /* 现代浏览器:玻璃拟态效果 */
  @supports (backdrop-filter: blur(20px)) {
    background: rgba(255, 255, 255, 0.15);
    backdrop-filter: blur(20px);
  }
}

Q2: 图标不显示怎么办?

A: 确保正确安装和导入了 @iconify/vue

npm install @iconify/vue
<script setup>
import { Icon } from '@iconify/vue'
</script>

Q3: ElMessage 提示"找不到"?

A: 确保在 main.ts 中导入了 Element Plus 的 CSS:

import 'element-plus/dist/index.css'

Q4: 密码应该如何加密?

A: 建议使用加密库:

npm install crypto-js
npm install -D @types/crypto-js
import CryptoJS from 'crypto-js'

const encryptPassword = (password: string) => {
  return CryptoJS.SHA256(password).toString()
}

注意: 即使前端加密,后端仍需要再次加密存储(使用 bcrypt)。

Q5: 如何实现"记住我"功能?

A: 使用 localStorage:

// 登录时
if (loginForm.remember) {
  localStorage.setItem('savedUsername', loginForm.username)
}

// 页面加载时
import { onMounted } from 'vue'

onMounted(() => {
  const savedUsername = localStorage.getItem('savedUsername')
  if (savedUsername) {
    loginForm.username = savedUsername
    loginForm.remember = true
  }
})

🎓 结语

恭喜你!你已经完成了一个功能完整、视觉效果出色的登录注册页面。

这个项目不仅可以用在你的实际项目中,还可以作为:

  • 💼 求职作品集 - 展示你的前端能力
  • 📚 学习案例 - 巩固 Vue3 知识
  • 🎨 设计参考 - 作为其他页面的设计基础

接下来的学习建议:

  1. 继续学习 Pinia 状态管理(下一章内容)
  2. 学习如何与后端 API 对接
  3. 尝试添加更多高级功能(第三方登录、邮箱验证等)
  4. 优化性能和用户体验

📦 完整文件清单

目录结构

auth-demo/
├── src/
│   ├── views/
│   │   └── auth/
│   │       └── LoginRegister.vue      # 登录注册页面
│   ├── composables/
│   │   ├── useValidation.ts           # 表单验证工具
│   │   ├── useCountdown.ts            # 倒计时工具
│   │   └── useTheme.ts                # 主题切换工具
│   ├── router/
│   │   └── index.ts                   # 路由配置
│   ├── App.vue
│   └── main.ts
├── package.json
└── vite.config.ts

核心功能清单

登录表单

  • 用户名/邮箱输入
  • 密码输入
  • 记住我选项
  • 表单验证

注册表单

  • 用户名验证(3-20字符)
  • 邮箱验证
  • 手机号验证(中国大陆)
  • 验证码倒计时(60秒)
  • 密码强度显示(弱/中/强)
  • 确认密码验证

交互效果

  • 表单切换动画
  • 输入框聚焦动效
  • 错误提示抖动动画
  • 按钮悬停效果

高级功能

  • 暗黑模式切换
  • 响应式布局
  • 本地存储
  • 实时表单验证

📖 相关阅读


如果这篇教程对你有帮助,别忘了点赞收藏!
有问题欢迎在评论区留言,我会及时回复。

下一章我们将学习 Pinia 状态管理,把这个登录页面的用户状态管理得更专业!

💡 彩蛋: 你可以尝试把这个页面改成你喜欢的配色方案,或者添加更多动画效果。发挥你的创意!


作者:代码小库
本文为原创文章,转载请注明出处
最后更新:2025年

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐