Vue 3 纯小白快速入门指南

5 阅读14分钟

Vue 3 纯小白入门指南

一、Vue 3 是什么?

Vue 3 是一个用于构建用户界面的 JavaScript 框架。简单来说,它让你能用更简单的方式创建网页应用。

类比理解:

  • HTML:就像房子的结构(墙、门、窗)
  • CSS:就像房子的装修(颜色、样式)
  • JavaScript:就像房子的功能(开关灯、开门)
  • Vue 3:就像智能家居系统,让所有功能协调工作

二、学习前准备

2.1 需要的基础知识

  1. HTML:了解基本标签(div、p、button、input等)
  2. CSS:了解基本样式(颜色、大小、布局)
  3. JavaScript:了解变量、函数、数组、对象等基础概念

2.2 开发工具准备

  1. 浏览器:Chrome 或 Edge(最新版)
  2. 代码编辑器:推荐 VS Code(免费)
  3. Node.js:用于运行 JavaScript(下载地址:nodejs.org)

三、最快速的上手方式

3.1 在线体验(无需安装)

直接在浏览器中体验 Vue 3:

  1. 打开 Vue SFC Playground
  2. 在左侧写代码,右侧看效果
  3. 这是最快了解 Vue 的方式

3.2 本地快速开始

<!-- 1. 创建一个 HTML 文件,比如 index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>我的第一个 Vue 应用</title>
  <!-- 引入 Vue 3 -->
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <div id="app">
    <!-- Vue 模板 -->
    <h1>{{ message }}</h1>
    <button @click="count++">点击了 {{ count }} 次</button>
  </div>

  <script>
    // 创建 Vue 应用
    const { createApp } = Vue
    
    createApp({
      // 数据
      data() {
        return {
          message: 'Hello Vue 3!',
          count: 0
        }
      }
    }).mount('#app') // 挂载到 id="app" 的元素
  </script>
</body>
</html>

运行步骤

  1. 复制上面代码到 index.html
  2. 用浏览器打开这个文件
  3. 看到效果:显示 "Hello Vue 3!" 和一个按钮

四、Vue 3 核心概念(小白版)

4.1 声明式渲染

<!-- 传统 JavaScript -->
<div id="old-way"></div>
<script>
  const element = document.getElementById('old-way')
  element.textContent = 'Hello World'
</script>

<!-- Vue 3 方式 -->
<div id="app">{{ message }}</div>
<script>
  // 数据变化,界面自动更新
  data() {
    return {
      message: 'Hello Vue 3'
    }
  }
</script>

4.2 响应式数据

// 普通变量
let count = 0
count = 1  // 界面不会自动更新

// Vue 响应式数据
data() {
  return {
    count: 0  // 当 count 改变时,界面自动更新
  }
}

4.3 指令(Directives)

指令是带有 v-前缀的特殊属性:

<!-- v-if:条件显示 -->
<p v-if="isShow">我会根据条件显示/隐藏</p>

<!-- v-for:循环列表 -->
<ul>
  <li v-for="item in items">{{ item.name }}</li>
</ul>

<!-- v-on:事件监听(简写 @) -->
<button @click="handleClick">点击我</button>

<!-- v-bind:属性绑定(简写 :) -->
<img :src="imageUrl" :alt="imageAlt">

五、第一个完整的 Vue 3 应用

5.1 待办事项应用(ToDo List)

<!DOCTYPE html>
<html>
<head>
  <title>我的待办事项</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <style>
    body { font-family: Arial; max-width: 500px; margin: 50px auto; }
    .todo-item { padding: 10px; border-bottom: 1px solid #eee; }
    .completed { text-decoration: line-through; color: #888; }
    input, button { padding: 8px; margin: 5px; }
  </style>
</head>
<body>
  <div id="app">
    <h1>📝 我的待办事项</h1>
    
    <!-- 添加新事项 -->
    <div>
      <input 
        v-model="newTodo" 
        @keyup.enter="addTodo"
        placeholder="输入待办事项,按回车添加"
      >
      <button @click="addTodo">添加</button>
    </div>
    
    <!-- 待办事项列表 -->
    <div v-if="todos.length === 0" style="color: #888; margin-top: 20px;">
      还没有待办事项,添加一个吧!
    </div>
    
    <div v-else>
      <div v-for="(todo, index) in todos" :key="index" class="todo-item">
        <input type="checkbox" v-model="todo.completed">
        <span :class="{ completed: todo.completed }">
          {{ todo.text }}
        </span>
        <button @click="removeTodo(index)" style="float: right;">删除</button>
      </div>
      
      <!-- 统计 -->
      <div style="margin-top: 20px;">
        总计:{{ todos.length }} 项 | 
        已完成:{{ completedCount }} 项 |
        <button @click="clearCompleted">清除已完成</button>
      </div>
    </div>
  </div>

  <script>
    const { createApp } = Vue
    
    createApp({
      data() {
        return {
          newTodo: '',  // 输入框的内容
          todos: [      // 待办事项列表
            { text: '学习 Vue 3', completed: false },
            { text: '写一个待办应用', completed: true },
            { text: '分享给朋友', completed: false }
          ]
        }
      },
      
      // 计算属性(自动计算的值)
      computed: {
        completedCount() {
          return this.todos.filter(todo => todo.completed).length
        }
      },
      
      // 方法(函数)
      methods: {
        addTodo() {
          if (this.newTodo.trim() === '') return
          
          this.todos.push({
            text: this.newTodo,
            completed: false
          })
          
          this.newTodo = ''  // 清空输入框
        },
        
        removeTodo(index) {
          this.todos.splice(index, 1)
        },
        
        clearCompleted() {
          this.todos = this.todos.filter(todo => !todo.completed)
        }
      }
    }).mount('#app')
  </script>
</body>
</html>

功能说明

  1. 添加待办事项
  2. 标记完成/未完成
  3. 删除事项
  4. 统计数量
  5. 清除已完成事项

六、Vue 3 的两种写法

6.1 选项式 API(适合初学者)

// 像填表格一样,把代码写在对应的位置
export default {
  // 数据
  data() {
    return {
      count: 0
    }
  },
  
  // 方法
  methods: {
    increment() {
      this.count++
    }
  },
  
  // 计算属性
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  
  // 生命周期
  mounted() {
    console.log('组件加载完成')
  }
}

6.2 组合式 API(Vue 3 新特性)

// 像写普通 JavaScript 函数
import { ref, computed, onMounted } from 'vue'

export default {
  setup() {
    // 数据
    const count = ref(0)
    
    // 方法
    function increment() {
      count.value++
    }
    
    // 计算属性
    const doubleCount = computed(() => count.value * 2)
    
    // 生命周期
    onMounted(() => {
      console.log('组件加载完成')
    })
    
    // 返回给模板使用
    return {
      count,
      increment,
      doubleCount
    }
  }
}

Vue 3 组合式 API 核心语法详解

我将用最简单的示例讲解 Vue 3 组合式 API 中的核心语法。每个概念都有代码示例和实际应用场景。

一、基础环境设置

首先,创建一个 Vue 3 项目(使用 Vite):

# 创建项目
npm create vue@latest my-vue-app
# 选择:TypeScript、Pinia、Router、ESLint

# 进入项目
cd my-vue-app
npm install
npm run dev

二、组合式 API 基础结构

<!-- App.vue -->
<template>
  <div>
    <!-- 所有模板语法都在这里 -->
  </div>
</template>

<script setup lang="ts">
// 组合式 API 代码写在这里
// 不需要 export default,直接写逻辑
</script>

<style scoped>
/* 组件样式 */
</style>

三、模板语法基础

3.1 插值表达式 {{ }}

<template>
  <div>
    <!-- 显示文本 -->
    <h1>{{ title }}</h1>
    <p>{{ count }}</p>
    
    <!-- 表达式 -->
    <p>{{ count + 1 }}</p>
    <p>{{ title.toUpperCase() }}</p>
    
    <!-- 三元表达式 -->
    <p>{{ isActive ? '活跃' : '未活跃' }}</p>
    
    <!-- 调用函数 -->
    <p>{{ formatPrice(price) }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

// 响应式数据
const title = ref('Vue 3 教程')
const count = ref(0)
const isActive = ref(true)
const price = ref(99.99)

// 函数
const formatPrice = (value: number) => {
  return ${value.toFixed(2)}`
}
</script>

四、v-ifv-else-ifv-else条件渲染

<template>
  <div>
    <!-- 基本条件 -->
    <p v-if="count > 0">计数大于0</p>
    <p v-else-if="count === 0">计数等于0</p>
    <p v-else>计数小于0</p>
    
    <!-- 多个元素的条件渲染 -->
    <template v-if="isLoggedIn">
      <h2>欢迎回来!</h2>
      <p>您已登录</p>
    </template>
    <template v-else>
      <h2>请登录</h2>
      <button @click="login">登录</button>
    </template>
    
    <!-- 复杂条件示例 -->
    <div class="user-info">
      <div v-if="user.role === 'admin'">
        <h3>管理员面板</h3>
        <button>删除用户</button>
        <button>修改设置</button>
      </div>
      <div v-else-if="user.role === 'editor'">
        <h3>编辑者面板</h3>
        <button>发布文章</button>
        <button>编辑内容</button>
      </div>
      <div v-else>
        <h3>访客面板</h3>
        <p>只能查看内容</p>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const count = ref(5)
const isLoggedIn = ref(false)
const user = ref({
  name: '张三',
  role: 'admin', // 可以改为 'editor' 或 'guest' 测试
  age: 25
})

const login = () => {
  isLoggedIn.value = true
  console.log('用户已登录')
}
</script>

<style scoped>
.user-info {
  margin-top: 20px;
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 5px;
}
</style>

v-if 是条件渲染,切换时销毁/创建元素;v-show 是条件显示,切换时只改 CSS display 属性。v-if 初始渲染开销小但切换成本高,v-show 初始渲染开销大但切换成本低

五、v-for列表渲染

<template>
  <div>
    <!-- 基本数组循环 -->
    <h3>用户列表</h3>
    <ul>
      <li v-for="(user, index) in users" :key="user.id">
        {{ index + 1 }}. {{ user.name }} - {{ user.age }}岁
      </li>
    </ul>
    
    <!-- 循环对象 -->
    <h3>用户信息</h3>
    <ul>
      <li v-for="(value, key, index) in userInfo" :key="key">
        {{ index + 1 }}. {{ key }}: {{ value }}
      </li>
    </ul>
    
    <!-- 循环数字范围 -->
    <h3>数字循环</h3>
    <span v-for="n in 5" :key="n" style="margin: 0 5px;">{{ n }}</span>
    
    <!-- 带条件的循环 -->
    <h3>成年用户</h3>
    <ul>
      <li v-for="user in adultUsers" :key="user.id">
        {{ user.name }} ({{ user.age }}岁)
      </li>
    </ul>
    
    <!-- 循环嵌套 -->
    <h3>分组用户</h3>
    <div v-for="group in groupedUsers" :key="group.groupName">
      <h4>{{ group.groupName }}</h4>
      <ul>
        <li v-for="user in group.members" :key="user.id">
          {{ user.name }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'

// 数组数据
const users = ref([
  { id: 1, name: '张三', age: 25 },
  { id: 2, name: '李四', age: 30 },
  { id: 3, name: '王五', age: 17 },
  { id: 4, name: '赵六', age: 22 }
])

// 对象数据
const userInfo = ref({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com',
  city: '北京'
})

// 计算成年用户
const adultUsers = computed(() => {
  return users.value.filter(user => user.age >= 18)
})

// 分组数据
const groupedUsers = ref([
  {
    groupName: '开发组',
    members: [
      { id: 1, name: '张三' },
      { id: 2, name: '李四' }
    ]
  },
  {
    groupName: '设计组',
    members: [
      { id: 3, name: '王五' },
      { id: 4, name: '赵六' }
    ]
  }
])
</script>

<style scoped>
</style>

六、:class动态类绑定

<template>
  <div>
    <!-- 1. 对象语法(最常用) -->
    <div :class="{ active: isActive, 'text-danger': hasError }">
      对象语法:根据条件添加类
    </div>
    
    <!-- 2. 数组语法 -->
    <div :class="[activeClass, errorClass]">
      数组语法:绑定多个类
    </div>
    
    <!-- 3. 数组和对象混合 -->
    <div :class="[{ active: isActive }, 'base-class', errorClass]">
      混合语法
    </div>
    
    <!-- 4. 计算属性返回类 -->
    <div :class="computedClasses">
      计算属性返回的类
    </div>
    
    <!-- 5. 实际应用:导航菜单 -->
    <div class="nav-menu">
      <button 
        v-for="item in menuItems" 
        :key="item.id"
        :class="{ 'nav-button': true, 'active': activeMenu === item.id }"
        @click="activeMenu = item.id"
      >
        {{ item.name }}
      </button>
    </div>
    
    <!-- 6. 实际应用:消息提示 -->
    <div class="messages">
      <div 
        v-for="msg in messages" 
        :key="msg.id"
        class="message"
        :class="msg.type"
      >
        {{ msg.text }}
      </div>
    </div>
    
    <!-- 7. 实际应用:表单验证样式 -->
    <div class="form-group">
      <input 
        v-model="email"
        type="email"
        placeholder="请输入邮箱"
        :class="{ 
          'form-input': true,
          'valid': isValid,
          'invalid': !isValid && email.length > 0
        }"
      />
      <p v-if="!isValid && email.length > 0" class="error-text">
        请输入有效的邮箱地址
      </p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'

// 1. 对象语法
const isActive = ref(true)
const hasError = ref(false)

// 2. 数组语法
const activeClass = ref('active')
const errorClass = ref('text-danger')

// 3. 计算属性返回类
const computedClasses = computed(() => {
  return {
    'highlight': isActive.value,
    'warning': hasError.value,
    'rounded': true // 总是添加的类
  }
})

// 4. 导航菜单
const activeMenu = ref(1)
const menuItems = ref([
  { id: 1, name: '首页' },
  { id: 2, name: '产品' },
  { id: 3, name: '关于我们' },
  { id: 4, name: '联系' }
])

// 5. 消息提示
const messages = ref([
  { id: 1, text: '操作成功', type: 'success' },
  { id: 2, text: '警告信息', type: 'warning' },
  { id: 3, text: '错误信息', type: 'error' }
])

// 6. 表单验证
const email = ref('')
const isValid = computed(() => {
  const emailRegex = /^[^\s@]+@[^\s@]+.[^\s@]+$/
  return emailRegex.test(email.value)
})
</script>

<style scoped>
/* 基础样式 */
.active {
  background-color: #007bff;
  color: white;
  padding: 10px;
}

.text-danger {
  color: #dc3545;
}

.base-class {
  padding: 10px;
  margin: 10px 0;
}

.highlight {
  background-color: #ffeb3b;
  padding: 10px;
}

.warning {
  border: 2px solid #ff9800;
}

.rounded {
  border-radius: 5px;
}

/* 导航菜单样式 */
.nav-menu {
  display: flex;
  gap: 10px;
  margin: 20px 0;
}

.nav-button {
  padding: 10px 20px;
  border: none;
  background-color: #f0f0f0;
  cursor: pointer;
  border-radius: 4px;
  transition: all 0.3s;
}

.nav-button:hover {
  background-color: #e0e0e0;
}

.nav-button.active {
  background-color: #007bff;
  color: white;
}

/* 消息提示样式 */
.messages {
  margin: 20px 0;
}

.message {
  padding: 10px;
  margin: 5px 0;
  border-radius: 4px;
}

.message.success {
  background-color: #d4edda;
  color: #155724;
  border: 1px solid #c3e6cb;
}

.message.warning {
  background-color: #fff3cd;
  color: #856404;
  border: 1px solid #ffeaa7;
}

.message.error {
  background-color: #f8d7da;
  color: #721c24;
  border: 1px solid #f5c6cb;
}

/* 表单样式 */
.form-group {
  margin: 20px 0;
}

.form-input {
  padding: 10px;
  width: 300px;
  border: 2px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
  transition: border-color 0.3s;
}

.form-input:focus {
  outline: none;
  border-color: #007bff;
}

.form-input.valid {
  border-color: #28a745;
}

.form-input.invalid {
  border-color: #dc3545;
}

.error-text {
  color: #dc3545;
  font-size: 14px;
  margin-top: 5px;
}
</style>

七、计算属性 computed

<template>
  <div>
    <h1>购物车示例</h1>
    
    <!-- 基本计算属性 -->
    <div class="cart-summary">
      <h3>购物车总览</h3>
      <p>商品数量: {{ totalQuantity }}</p>
      <p>商品总价: ¥{{ totalPrice.toFixed(2) }}</p>
      <p>折扣: {{ discount * 100 }}%</p>
      <p>折后价格: ¥{{ discountedPrice.toFixed(2) }}</p>
      <p>运费: ¥{{ shippingFee.toFixed(2) }}</p>
      <p class="total">实付款: ¥{{ finalPrice.toFixed(2) }}</p>
    </div>
    
    <!-- 购物车列表 -->
    <div class="cart-items">
      <h3>购物车商品</h3>
      <div v-for="item in cartItems" :key="item.id" class="cart-item">
        <div class="item-info">
          <span class="item-name">{{ item.name }}</span>
          <span class="item-price">¥{{ item.price.toFixed(2) }}</span>
        </div>
        <div class="item-controls">
          <button @click="decreaseQuantity(item.id)">-</button>
          <span class="quantity">{{ item.quantity }}</span>
          <button @click="increaseQuantity(item.id)">+</button>
          <button @click="removeItem(item.id)" class="remove-btn">删除</button>
        </div>
        <div class="item-total">
          小计: ¥{{ (item.price * item.quantity).toFixed(2) }}
        </div>
      </div>
    </div>
    
    <!-- 优惠券应用 -->
    <div class="coupon-section">
      <h3>优惠券</h3>
      <div v-for="coupon in availableCoupons" :key="coupon.id">
        <input 
          type="radio" 
          :id="'coupon-' + coupon.id"
          :value="coupon.id"
          v-model="selectedCouponId"
        >
        <label :for="'coupon-' + coupon.id">
          {{ coupon.name }} - 减¥{{ coupon.discount }}
        </label>
      </div>
      <p v-if="selectedCoupon">已选择: {{ selectedCoupon.name }}</p>
    </div>
    
    <!-- 商品过滤 -->
    <div class="filter-section">
      <h3>商品筛选</h3>
      <input v-model="searchText" placeholder="搜索商品名称">
      <p>找到 {{ filteredItems.length }} 个商品</p>
      <div v-for="item in filteredItems" :key="item.id" class="product">
        {{ item.name }} - ¥{{ item.price }}
        <button @click="addToCart(item)">加入购物车</button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'

// 购物车数据
const cartItems = ref([
  { id: 1, name: 'iPhone 13', price: 5999, quantity: 1 },
  { id: 2, name: 'MacBook Pro', price: 12999, quantity: 1 },
  { id: 3, name: 'AirPods Pro', price: 1999, quantity: 2 },
  { id: 4, name: 'iPad Air', price: 4799, quantity: 1 }
])

// 1. 基本计算属性:商品总数量
const totalQuantity = computed(() => {
  return cartItems.value.reduce((total, item) => total + item.quantity, 0)
})

// 2. 基本计算属性:商品总价
const totalPrice = computed(() => {
  return cartItems.value.reduce((total, item) => {
    return total + (item.price * item.quantity)
  }, 0)
})

// 3. 折扣计算:根据总价自动计算折扣
const discount = computed(() => {
  if (totalPrice.value > 10000) {
    return 0.1 // 10% 折扣
  } else if (totalPrice.value > 5000) {
    return 0.05 // 5% 折扣
  }
  return 0 // 无折扣
})

// 4. 折后价格
const discountedPrice = computed(() => {
  return totalPrice.value * (1 - discount.value)
})

// 5. 运费计算:满200免运费
const shippingFee = computed(() => {
  if (discountedPrice.value > 200) {
    return 0
  }
  return 15
})

// 6. 最终价格
const finalPrice = computed(() => {
  return discountedPrice.value + shippingFee.value - couponDiscount.value
})

// 7. 优惠券相关
const availableCoupons = ref([
  { id: 1, name: '新人券', discount: 50 },
  { id: 2, name: '满减券', discount: 100 },
  { id: 3, name: '会员券', discount: 200 }
])

const selectedCouponId = ref<number | null>(null)

// 8. 计算选中的优惠券
const selectedCoupon = computed(() => {
  return availableCoupons.value.find(coupon => coupon.id === selectedCouponId.value)
})

// 9. 计算优惠券折扣
const couponDiscount = computed(() => {
  if (!selectedCoupon.value) return 0
  return selectedCoupon.value.discount
})

// 10. 商品搜索
const products = ref([
  { id: 1, name: 'iPhone 13', price: 5999 },
  { id: 2, name: 'MacBook Pro', price: 12999 },
  { id: 3, name: 'AirPods Pro', price: 1999 },
  { id: 4, name: 'iPad Air', price: 4799 },
  { id: 5, name: 'Apple Watch', price: 2999 },
  { id: 6, name: 'iMac', price: 10999 }
])

const searchText = ref('')

// 11. 过滤商品
const filteredItems = computed(() => {
  if (!searchText.value) return products.value
  return products.value.filter(product => 
    product.name.toLowerCase().includes(searchText.value.toLowerCase())
  )
})

// 操作方法
const increaseQuantity = (itemId: number) => {
  const item = cartItems.value.find(item => item.id === itemId)
  if (item) item.quantity++
}

const decreaseQuantity = (itemId: number) => {
  const item = cartItems.value.find(item => item.id === itemId)
  if (item && item.quantity > 1) {
    item.quantity--
  }
}

const removeItem = (itemId: number) => {
  cartItems.value = cartItems.value.filter(item => item.id !== itemId)
}

const addToCart = (product: any) => {
  const existingItem = cartItems.value.find(item => item.id === product.id)
  if (existingItem) {
    existingItem.quantity++
  } else {
    cartItems.value.push({ ...product, quantity: 1 })
  }
}

// 监听购物车变化
watch(cartItems, (newItems) => {
  console.log('购物车更新:', newItems)
  // 这里可以保存到本地存储
  localStorage.setItem('cart', JSON.stringify(newItems))
}, { deep: true })
</script>

<style scoped>
.cart-summary, .cart-items, .coupon-section, .filter-section {
  margin: 20px 0;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.total {
  font-size: 24px;
  color: #e53935;
  font-weight: bold;
}

.cart-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.item-info {
  flex: 1;
}

.item-name {
  font-weight: bold;
  margin-right: 20px;
}

.item-controls button {
  margin: 0 5px;
  padding: 5px 10px;
  cursor: pointer;
}

.quantity {
  margin: 0 10px;
  font-weight: bold;
}

.remove-btn {
  background-color: #ff5252;
  color: white;
  border: none;
  border-radius: 4px;
}

.product {
  padding: 10px;
  margin: 5px 0;
  background-color: #f5f5f5;
  border-radius: 4px;
}

.product button {
  margin-left: 10px;
  background-color: #4caf50;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
}

input[type="text"] {
  padding: 8px;
  width: 300px;
  margin-right: 10px;
}
</style>

工作流程示例:

假设有两个优惠券:

  • 优惠券 A: { id: 1, name: ‘新人券‘, discount: 10 }
  • 优惠券 B: { id: 2, name: ‘满减券‘, discount: 20 }
  1. 渲染时,Vue 会创建两个单选框:

    • 第一个:<input value=“1” ...>(对应优惠券A)
    • 第二个:<input value=“2” ...>(对应优惠券B)
  2. 当用户点击第一个单选框(优惠券A)时,浏览器事件会告诉 Vue:“value”1”的选项被选中了”。

  3. v-model指令监听到这个变化,于是将 selectedCouponId这个变量的值更新为 ”1”

  4. 现在,selectedCouponId === “1”。Vue 会检查所有 v-model绑定的单选框,发现第一个单选框的 :value(即 ”1”) 与 selectedCouponId的值相等,于是自动为这个单选框添加 checked属性,使其显示为选中状态。

总结

  • v-model:管理哪个值被选中了(与 selectedCouponId变量同步)。
  • :value:定义每一个选项具体是哪个值(将 coupon.id绑定到每个选项上)。

八、ref响应式变量

<template>
  <div>
    <h1>ref 响应式变量</h1>
    
    <!-- 1. 基本使用 -->
    <div class="section">
      <h3>1. 基本类型响应式</h3>
      <p>计数器: {{ count }}</p>
      <button @click="increment">+1</button>
      <button @click="decrement">-1</button>
      <button @click="reset">重置</button>
    </div>
    
    <!-- 2. 引用类型 -->
    <div class="section">
      <h3>2. 对象响应式</h3>
      <p>用户名: {{ user.name }}</p>
      <p>年龄: {{ user.age }}</p>
      <input v-model="user.name" placeholder="修改用户名">
      <button @click="user.age++">增加年龄</button>
    </div>
    
    <!-- 3. 数组操作 -->
    <div class="section">
      <h3>3. 数组响应式</h3>
      <ul>
        <li v-for="(item, index) in items" :key="index">
          {{ item }}
          <button @click="removeItem(index)">删除</button>
        </li>
      </ul>
      <input v-model="newItem" placeholder="输入新项目">
      <button @click="addItem">添加</button>
    </div>
    
    <!-- 4. DOM 引用 -->
    <div class="section">
      <h3>4. 模板引用 (ref 获取 DOM)</h3>
      <input ref="inputRef" type="text" placeholder="点击按钮聚焦">
      <button @click="focusInput">聚焦输入框</button>
      <p>输入框值: {{ inputValue }}</p>
    </div>
    
    <!-- 5. 组件引用 -->
    <div class="section">
      <h3>5. 组件引用</h3>
      <ChildComponent ref="childRef" />
      <button @click="callChildMethod">调用子组件方法</button>
    </div>
    
    <!-- 6. 计算属性 vs ref -->
    <div class="section">
      <h3>6. ref 与计算属性的区别</h3>
      <p>原始价格: ¥{{ price }}</p>
      <p>折扣: {{ discount * 100 }}%</p>
      <p>折后价格 (ref): ¥{{ discountedPriceRef }}</p>
      <p>折后价格 (computed): ¥{{ discountedPriceComputed }}</p>
      <button @click="updateDiscount">修改折扣</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 1. 基本类型 ref
const count = ref(0)

const increment = () => {
  count.value++ // 注意:需要访问 .value
}

const decrement = () => {
  count.value--
}

const reset = () => {
  count.value = 0
}

// 2. 对象类型 ref(自动深度响应)
const user = ref({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com'
})

// 3. 数组类型 ref
const items = ref(['苹果', '香蕉', '橙子'])
const newItem = ref('')

const addItem = () => {
  if (newItem.value.trim()) {
    items.value.push(newItem.value)
    newItem.value = ''
  }
}

const removeItem = (index: number) => {
  items.value.splice(index, 1)
}

// 4. 模板引用(DOM 元素)
const inputRef = ref<HTMLInputElement | null>(null)
const inputValue = ref('')

const focusInput = () => {
  if (inputRef.value) {
  // inputRef.value 就是真实的 <input> DOM 元素
    inputRef.value.focus()
    inputRef.value.value = '已聚焦!'
    inputValue.value = inputRef.value.value
  }
}

// 5. 组件引用
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)

const callChildMethod = () => {
  if (childRef.value) {
    childRef.value.sayHello()
  }
}

// 6. ref 与计算属性的区别
const price = ref(100)
const discount = ref(0.1)

// 使用 ref 存储计算结果(需要手动更新)
const discountedPriceRef = ref(price.value * (1 - discount.value))

// 使用 computed 自动计算(推荐)
const discountedPriceComputed = computed(() => {
  return price.value * (1 - discount.value)
})

const updateDiscount = () => {
  discount.value += 0.1
  if (discount.value > 0.5) discount.value = 0
  
  // 手动更新 ref 的值
  discountedPriceRef.value = price.value * (1 - discount.value)
}

// 监听 ref 的变化
watch(count, (newValue, oldValue) => {
  console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})

watch(user, (newUser) => {
  console.log('用户信息更新:', newUser)
}, { deep: true }) // 深度监听对象变化

// 生命周期钩子
onMounted(() => {
  console.log('组件已挂载')
  console.log('inputRef:', inputRef.value)
  console.log('childRef:', childRef.value)
})
</script>

<style scoped>
.section {
  margin: 20px 0;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
}

h3 {
  margin-top: 0;
  color: #333;
}

button {
  margin: 5px;
  padding: 8px 16px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #0056b3;
}

input {
  padding: 8px;
  margin: 5px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  padding: 8px;
  margin: 5px 0;
  background-color: #f5f5f5;
  border-radius: 4px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>
<!-- ChildComponent.vue -->
<template>
  <div class="child">
    <h4>子组件</h4>
    <p>子组件计数: {{ childCount }}</p>
    <button @click="childCount++">子组件 +1</button>
  </div>
</template>

<script setup lang="ts">
import { ref, defineExpose } from 'vue'

const childCount = ref(0)

const sayHello = () => {
  alert(`Hello from Child! Count is ${childCount.value}`)
  childCount.value++
}

// 暴露给父组件使用的方法
defineExpose({
  sayHello,
  childCount
})
</script>

<style scoped>
.child {
  padding: 15px;
  background-color: #f0f0f0;
  border-radius: 5px;
  margin-top: 10px;
}
</style>

ref的核心特性总结

  1. 响应式包装器ref将普通数据包装成响应式对象,使其能够在数据变化时自动更新视图
  2. .value访问:在 <script>中通过 .value访问/修改值,在 <template>中自动解包无需 .value
  3. 类型安全:支持 TypeScript 类型定义,提供更好的开发体验
  4. 广泛适用:支持基本类型、对象、数组、DOM 元素、组件实例等多种数据类型

ref的四大主要用途

  1. 响应式状态管理
  • 基本类型:数字、字符串、布尔值等
  • 引用类型:对象、数组(自动深度响应)
  • 优势:替代 Vue 2 的 data,更灵活的组合式管理
  1. 模板引用 (Template Refs)
<!-- 引用 DOM 元素 -->
<input ref="inputRef">

<!-- 引用子组件 -->
<ChildComponent ref="childRef" />
  • 应用场景:直接操作 DOM、调用子组件方法、集成第三方库

reactive() ​ - 用于创建响应式的对象

const user = reactive({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com'
})
const updateUserName = () => {
  if (newName.value.trim()) {
    user.name = newName.value
    newName.value = ''
  }
}

toRef() ​ - 用于从响应式对象中提取单个属性并保持其响应性

<script setup>
import { reactive, toRef, ref } from 'vue'

const state = reactive({
  name: '张三',
  age: 25
})

// 方式1: 使用 toRef(保持连接)
const nameRef1 = toRef(state, 'name')

// 方式2: 使用 ref(失去连接)
const nameRef2 = ref(state.name)

const updateData = () => {
  // 修改原对象
  state.name = '李四'
  
  console.log('nameRef1:', nameRef1.value) // '李四' ✓ 同步更新
  console.log('nameRef2:', nameRef2.value) // '张三' ✗ 不同步
  
  // 修改 ref
  nameRef1.value = '王五'
  console.log('state.name:', state.name) // '王五' ✓ 同步
  
  nameRef2.value = '赵六'
  console.log('state.name:', state.name) // '王五' ✗ 不同步
}
</script>

九、v-model双向数据绑定

<template>
  <div>
    <h1>v-model 双向绑定</h1>
    
    <!-- 1. 基础输入框 -->
    <div class="section">
      <h3>1. 文本输入框</h3>
      <input v-model="message" placeholder="输入文本">
      <p>输入的内容: {{ message }}</p>
      <p>字符数: {{ message.length }}</p>
    </div>
    
    <!-- 2. 多行文本框 -->
    <div class="section">
      <h3>2. 多行文本框</h3>
      <textarea v-model="bio" placeholder="个人简介"></textarea>
      <p>预览:</p>
      <pre>{{ bio }}</pre>
      <p>行数: {{ bio.split('\n').length }}</p>
    </div>
    
    <!-- 3. 复选框 -->
    <div class="section">
      <h3>3. 复选框</h3>
      <div>
        <label>
          <input type="checkbox" v-model="checked">
          我同意条款
        </label>
        <p>状态: {{ checked ? '已同意' : '未同意' }}</p>
      </div>
      
      <h4>多个复选框</h4>
      <div v-for="option in options" :key="option.id">
        <label>
          <input type="checkbox" :value="option.value" v-model="selectedOptions">
          {{ option.label }}
        </label>
      </div>
      <p>选择: {{ selectedOptions.join(', ') }}</p>
    </div>
    
    <!-- 4. 单选框 -->
    <div class="section">
      <h3>4. 单选框</h3>
      <div v-for="gender in genders" :key="gender.value">
        <label>
          <input type="radio" :value="gender.value" v-model="selectedGender">
          {{ gender.label }}
        </label>
      </div>
      <p>选择: {{ selectedGender }}</p>
    </div>
    
    <!-- 5. 选择框 -->
    <div class="section">
      <h3>5. 下拉选择框</h3>
      <select v-model="selectedCity">
        <option disabled value="">请选择城市</option>
        <option v-for="city in cities" :key="city.value" :value="city.value">
          {{ city.label }}
        </option>
      </select>
      <p>选择: {{ selectedCity }}</p>
      
      <h4>多选下拉</h4>
      <select v-model="selectedCities" multiple style="height: 100px;">
        <option v-for="city in cities" :key="city.value" :value="city.value">
          {{ city.label }}
        </option>
      </select>
      <p>选择: {{ selectedCities.join(', ') }}</p>
    </div>
    
    <!-- 6. 表单绑定对象 -->
    <div class="section">
      <h3>6. 表单绑定到对象</h3>
      <form @submit.prevent="handleSubmit">
        <div class="form-group">
          <label>用户名:</label>
          <input v-model="formData.username" placeholder="请输入用户名">
        </div>
        
        <div class="form-group">
          <label>邮箱:</label>
          <input v-model="formData.email" type="email" placeholder="请输入邮箱">
        </div>
        
        <div class="form-group">
          <label>年龄:</label>
          <input v-model.number="formData.age" type="number" min="0" max="150">
        </div>
        
        <div class="form-group">
          <label>性别:</label>
          <select v-model="formData.gender">
            <option value="male"></option>
            <option value="female"></option>
            <option value="other">其他</option>
          </select>
        </div>
        
        <div class="form-group">
          <label>爱好:</label>
          <div v-for="hobby in hobbies" :key="hobby">
            <label>
              <input type="checkbox" :value="hobby" v-model="formData.hobbies">
              {{ hobby }}
            </label>
          </div>
        </div>
        
        <div class="form-group">
          <label>个人简介:</label>
          <textarea v-model="formData.bio" placeholder="个人简介"></textarea>
        </div>
        
        <button type="submit">提交</button>
        <button type="button" @click="resetForm">重置</button>
      </form>
      
      <h4>表单数据预览:</h4>
      <pre>{{ formData }}</pre>
    </div>
    
    <!-- 7. 自定义组件 v-model -->
    <div class="section">
      <h3>7. 自定义组件的 v-model</h3>
      <p>父组件值: {{ customValue }}</p>
      <CustomInput v-model="customValue" />
      <CustomInput v-model.number="customNumber" />
    </div>
    
    <!-- 8. v-model 修饰符 -->
    <div class="section">
      <h3>8. v-model 修饰符</h3>
      
      <h4>.lazy - 输入时不会立即更新,失去焦点时更新</h4>
      <input v-model.lazy="lazyMessage" placeholder="输入内容">
      <p>值: {{ lazyMessage }}</p>
      
      <h4>.number - 自动转换为数字</h4>
      <input v-model.number="numberValue" type="number" placeholder="输入数字">
      <p>值: {{ numberValue }},类型: {{ typeof numberValue }}</p>
      
      <h4>.trim - 自动去除首尾空格</h4>
      <input v-model.trim="trimmedValue" placeholder="输入带空格的内容">
      <p>值: "{{ trimmedValue }}"</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import CustomInput from './CustomInput.vue'

// 1. 文本输入
const message = ref('')

// 2. 多行文本
const bio = ref('')

// 3. 复选框
const checked = ref(false)

// 多个复选框
const options = ref([
  { id: 1, label: '选项 A', value: 'A' },
  { id: 2, label: '选项 B', value: 'B' },
  { id: 3, label: '选项 C', value: 'C' }
])
const selectedOptions = ref<string[]>([])

// 4. 单选框
const genders = ref([
  { label: '男', value: 'male' },
  { label: '女', value: 'female' },
  { label: '其他', value: 'other' }
])
const selectedGender = ref('male')

// 5. 选择框
const cities = ref([
  { label: '北京', value: 'beijing' },
  { label: '上海', value: 'shanghai' },
  { label: '广州', value: 'guangzhou' },
  { label: '深圳', value: 'shenzhen' }
])
const selectedCity = ref('')
const selectedCities = ref<string[]>([])

// 6. 表单对象
const formData = reactive({
  username: '',
  email: '',
  age: 25,
  gender: 'male',
  hobbies: [] as string[],
  bio: ''
})

const hobbies = ref(['篮球', '足球', '游泳', '阅读', '音乐'])

const handleSubmit = () => {
  console.log('表单提交:', formData)
  alert('表单提交成功!查看控制台输出')
}

const resetForm = () => {
  Object.assign(formData, {
    username: '',
    email: '',
    age: 25,
    gender: 'male',
    hobbies: [] as string[],
    bio: ''
  })
}

// 7. 自定义组件 v-model
const customValue = ref('')
const customNumber = ref(0)

// 8. v-model 修饰符
const lazyMessage = ref('')
const numberValue = ref(0)
const trimmedValue = ref('')
</script>

<style scoped>
.section {
  margin: 20px 0;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
}

h3 {
  margin-top: 0;
  color: #333;
}

h4 {
  margin: 15px 0 10px 0;
  color: #666;
}

input[type="text"],
input[type="email"],
input[type="number"],
textarea,
select {
  width: 300px;
  padding: 8px;
  margin: 5px 0;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

textarea {
  height: 100px;
  resize: vertical;
}

.form-group {
  margin: 10px 0;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

button {
  margin: 10px 5px 0 0;
  padding: 8px 16px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #0056b3;
}

pre {
  background-color: #f5f5f5;
  padding: 10px;
  border-radius: 4px;
  overflow-x: auto;
}

label {
  display: inline-flex;
  align-items: center;
  margin-right: 15px;
  cursor: pointer;
}

input[type="checkbox"],
input[type="radio"] {
  margin-right: 5px;
}
</style>
<!-- CustomInput.vue - 自定义组件 -->
<template>
  <div class="custom-input">
    <input 
      :type="type" 
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      :placeholder="placeholder"
    />
    <span class="char-count">{{ charCount }} 字符</span>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'

// 定义 props
defineProps({
  modelValue: {
    type: [String, Number],
    default: ''
  },
  type: {
    type: String,
    default: 'text'
  },
  placeholder: {
    type: String,
    default: '请输入'
  }
})

// 定义 emits
defineEmits(['update:modelValue'])

// 计算属性:字符数
const charCount = computed(() => {
  return String(props.modelValue).length
})
</script>

<style scoped>
.custom-input {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

.custom-input input {
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

.char-count {
  font-size: 12px;
  color: #666;
}
</style>

十、综合示例:用户管理系统

<template>
  <div class="user-management">
    <h1>用户管理系统</h1>
    
    <!-- 搜索和过滤 -->
    <div class="controls">
      <div class="search-box">
        <input 
          v-model.trim="searchQuery"
          placeholder="搜索用户..."
          @input="debouncedSearch"
        >
        <span v-if="searchQuery" class="clear-btn" @click="clearSearch"></span>
      </div>
      
      <div class="filters">
        <select v-model="roleFilter">
          <option value="">所有角色</option>
          <option v-for="role in roles" :key="role" :value="role">{{ role }}</option>
        </select>
        
        <label class="toggle">
          <input type="checkbox" v-model="showActiveOnly">
          仅显示活跃用户
        </label>
      </div>
    </div>
    
    <!-- 用户列表 -->
    <div class="user-list">
      <div v-if="filteredUsers.length === 0" class="empty-state">
        没有找到用户
      </div>
      
      <div 
        v-for="user in filteredUsers" 
        :key="user.id"
        class="user-card"
        :class="{ 
          'active': user.isActive,
          'inactive': !user.isActive,
          'selected': selectedUserId === user.id
        }"
        @click="selectUser(user.id)"
      >
        <div class="user-avatar">
          {{ user.name.charAt(0) }}
        </div>
        
        <div class="user-info">
          <h3>{{ user.name }}</h3>
          <p class="user-email">{{ user.email }}</p>
          <div class="user-meta">
            <span class="role-badge" :class="user.role">{{ user.role }}</span>
            <span class="age">{{ user.age }}岁</span>
            <span class="status" :class="user.isActive ? 'active' : 'inactive'">
              {{ user.isActive ? '活跃' : '非活跃' }}
            </span>
          </div>
        </div>
        
        <div class="user-actions">
          <button 
            @click.stop="toggleActive(user.id)"
            :class="user.isActive ? 'btn-deactivate' : 'btn-activate'"
          >
            {{ user.isActive ? '停用' : '激活' }}
          </button>
          <button 
            @click.stop="editUser(user.id)"
            class="btn-edit"
          >
            编辑
          </button>
          <button 
            @click.stop="deleteUser(user.id)"
            class="btn-delete"
          >
            删除
          </button>
        </div>
      </div>
    </div>
    
    <!-- 统计信息 -->
    <div class="stats">
      <div class="stat-card">
        <h4>总用户数</h4>
        <p class="stat-value">{{ totalUsers }}</p>
      </div>
      <div class="stat-card">
        <h4>活跃用户</h4>
        <p class="stat-value">{{ activeUsersCount }}</p>
      </div>
      <div class="stat-card">
        <h4>平均年龄</h4>
        <p class="stat-value">{{ averageAge }}</p>
      </div>
      <div class="stat-card">
        <h4>角色分布</h4>
        <div class="role-distribution">
          <span v-for="role in roleDistribution" :key="role.name" class="role-item">
            {{ role.name }}: {{ role.count }}
          </span>
        </div>
      </div>
    </div>
    
    <!-- 用户表单 -->
    <div class="user-form">
      <h3>{{ isEditing ? '编辑用户' : '添加新用户' }}</h3>
      <form @submit.prevent="submitForm">
        <div class="form-row">
          <div class="form-group">
            <label>姓名 *</label>
            <input 
              v-model="form.name" 
              required 
              placeholder="请输入姓名"
              :class="{ 'error': !form.name && submitted }"
            >
            <p v-if="!form.name && submitted" class="error-message">姓名不能为空</p>
          </div>
          
          <div class="form-group">
            <label>邮箱 *</label>
            <input 
              v-model="form.email" 
              type="email" 
              required
              placeholder="请输入邮箱"
              :class="{ 'error': !isEmailValid && submitted }"
            >
            <p v-if="!isEmailValid && submitted" class="error-message">请输入有效的邮箱</p>
          </div>
        </div>
        
        <div class="form-row">
          <div class="form-group">
            <label>年龄</label>
            <input 
              v-model.number="form.age" 
              type="number" 
              min="1" 
              max="150"
              placeholder="年龄"
            >
          </div>
          
          <div class="form-group">
            <label>角色</label>
            <select v-model="form.role">
              <option v-for="role in roles" :key="role" :value="role">{{ role }}</option>
            </select>
          </div>
        </div>
        
        <div class="form-group">
          <label>
            <input type="checkbox" v-model="form.isActive">
            活跃状态
          </label>
        </div>
        
        <div class="form-actions">
          <button type="submit" class="btn-submit">
            {{ isEditing ? '更新用户' : '添加用户' }}
          </button>
          <button type="button" @click="resetForm" class="btn-reset">
            重置
          </button>
          <button v-if="isEditing" type="button" @click="cancelEdit" class="btn-cancel">
            取消编辑
          </button>
        </div>
      </form>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'

// 用户接口
interface User {
  id: number
  name: string
  email: string
  age: number
  role: string
  isActive: boolean
}

// 初始用户数据
const initialUsers: User[] = [
  { id: 1, name: '张三', email: 'zhangsan@example.com', age: 25, role: 'admin', isActive: true },
  { id: 2, name: '李四', email: 'lisi@example.com', age: 30, role: 'editor', isActive: true },
  { id: 3, name: '王五', email: 'wangwu@example.com', age: 22, role: 'user', isActive: false },
  { id: 4, name: '赵六', email: 'zhaoliu@example.com', age: 28, role: 'editor', isActive: true },
  { id: 5, name: '钱七', email: 'qianqi@example.com', age: 35, role: 'user', isActive: true }
]

// 响应式数据
const users = ref<User[]>(initialUsers)
const searchQuery = ref('')
const roleFilter = ref('')
const showActiveOnly = ref(false)
const selectedUserId = ref<number | null>(null)

// 表单数据
const form = reactive({
  id: 0,
  name: '',
  email: '',
  age: 25,
  role: 'user',
  isActive: true
})

const isEditing = ref(false)
const submitted = ref(false)

// 角色列表
const roles = ['admin', 'editor', 'user']

// 计算属性
const filteredUsers = computed(() => {
  let filtered = users.value
  
  // 搜索过滤
  if (searchQuery.value) {
    const query = searchQuery.value.toLowerCase()
    filtered = filtered.filter(user => 
      user.name.toLowerCase().includes(query) ||
      user.email.toLowerCase().includes(query)
    )
  }
  
  // 角色过滤
  if (roleFilter.value) {
    filtered = filtered.filter(user => user.role === roleFilter.value)
  }
  
  // 活跃状态过滤
  if (showActiveOnly.value) {
    filtered = filtered.filter(user => user.isActive)
  }
  
  return filtered
})

const totalUsers = computed(() => users.value.length)
const activeUsersCount = computed(() => users.value.filter(user => user.isActive).length)
const averageAge = computed(() => {
  if (users.value.length === 0) return 0
  const total = users.value.reduce((sum, user) => sum + user.age, 0)
  return Math.round(total / users.value.length)
})

const roleDistribution = computed(() => {
  const distribution: Record<string, number> = {}
  
  users.value.forEach(user => {
    distribution[user.role] = (distribution[user.role] || 0) + 1
  })
  
  return Object.entries(distribution).map(([name, count]) => ({ name, count }))
})

const isEmailValid = computed(() => {
  const emailRegex = /^[^\s@]+@[^\s@]+.[^\s@]+$/
  return emailRegex.test(form.email)
})

// 方法
const selectUser = (userId: number) => {
  selectedUserId.value = userId
}

const toggleActive = (userId: number) => {
  const user = users.value.find(u => u.id === userId)
  if (user) {
    user.isActive = !user.isActive
  }
}

const editUser = (userId: number) => {
  const user = users.value.find(u => u.id === userId)
  if (user) {
    Object.assign(form, { ...user })
    isEditing.value = true
    selectedUserId.value = userId
  }
}

const deleteUser = (userId: number) => {
  if (confirm('确定要删除这个用户吗?')) {
    users.value = users.value.filter(user => user.id !== userId)
    if (selectedUserId.value === userId) {
      selectedUserId.value = null
    }
  }
}

const submitForm = () => {
  submitted.value = true
  
  if (!form.name || !isEmailValid.value) {
    return
  }
  
  if (isEditing.value) {
    // 更新用户
    const index = users.value.findIndex(user => user.id === form.id)
    if (index !== -1) {
      users.value[index] = { ...form }
    }
  } else {
    // 添加新用户
    const newUser: User = {
      id: users.value.length > 0 ? Math.max(...users.value.map(u => u.id)) + 1 : 1,
      ...form
    }
    users.value.push(newUser)
  }
  
  resetForm()
}

const resetForm = () => {
  Object.assign(form, {
    id: 0,
    name: '',
    email: '',
    age: 25,
    role: 'user',
    isActive: true
  })
  isEditing.value = false
  submitted.value = false
  selectedUserId.value = null
}

const cancelEdit = () => {
  resetForm()
}

const clearSearch = () => {
  searchQuery.value = ''
}

const debouncedSearch = (() => {
  let timeoutId: number
  
  return () => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      // 搜索逻辑已由计算属性处理
    }, 300)
  }
})()

// 模拟 API 加载
onMounted(() => {
  console.log('用户管理系统已加载')
  // 这里可以添加从 API 加载数据的逻辑
})
</script>

<style scoped>
.user-management {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

h1 {
  text-align: center;
  color: #333;
  margin-bottom: 30px;
}

.controls {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 30px;
  gap: 20px;
  flex-wrap: wrap;
}

.search-box {
  position: relative;
  flex: 1;
  max-width: 400px;
}

.search-box input {
  width: 100%;
  padding: 10px 40px 10px 15px;
  border: 2px solid #ddd;
  border-radius: 25px;
  font-size: 16px;
  transition: border-color 0.3s;
}

.search-box input:focus {
  outline: none;
  border-color: #007bff;
}

.clear-btn {
  position: absolute;
  right: 15px;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
  color: #999;
  font-size: 18px;
}

.clear-btn:hover {
  color: #333;
}

.filters {
  display: flex;
  gap: 20px;
  align-items: center;
}

.filters select {
  padding: 10px 15px;
  border: 2px solid #ddd;
  border-radius: 6px;
  font-size: 14px;
  background-color: white;
}

.toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  user-select: none;
}

.user-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}

.empty-state {
  grid-column: 1 / -1;
  text-align: center;
  padding: 40px;
  color: #999;
  font-size: 18px;
}

.user-card {
  background: white;
  border: 2px solid #eee;
  border-radius: 10px;
  padding: 20px;
  cursor: pointer;
  transition: all 0.3s;
  display: flex;
  gap: 15px;
  align-items: center;
}

.user-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
  border-color: #007bff;
}

.user-card.selected {
  border-color: #007bff;
  background-color: #f0f8ff;
}

.user-card.active {
  border-left: 4px solid #4caf50;
}

.user-card.inactive {
  border-left: 4px solid #ff5252;
  opacity: 0.8;
}

.user-avatar {
  width: 50px;
  height: 50px;
  background-color: #007bff;
  color: white;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  font-weight: bold;
  flex-shrink: 0;
}

.user-info {
  flex: 1;
  min-width: 0;
}

.user-info h3 {
  margin: 0 0 5px 0;
  color: #333;
  font-size: 18px;
}

.user-email {
  margin: 0 0 10px 0;
  color: #666;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.user-meta {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.role-badge {
  padding: 3px 10px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: bold;
  text-transform: uppercase;
}

.role-badge.admin {
  background-color: #ff9800;
  color: white;
}

.role-badge.editor {
  background-color: #2196f3;
  color: white;
}

.role-badge.user {
  background-color: #4caf50;
  color: white;
}

.age, .status {
  font-size: 12px;
  color: #666;
  padding: 3px 8px;
  background-color: #f5f5f5;
  border-radius: 12px;
}

.status.active {
  background-color: #e8f5e9;
  color: #4caf50;
}

.status.inactive {
  background-color: #ffebee;
  color: #ff5252;
}

.user-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.user-actions button {
  padding: 6px 12px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  transition: opacity 0.3s;
}

.user-actions button:hover {
  opacity: 0.9;
}

.btn-activate {
  background-color: #4caf50;
  color: white;
}

.btn-deactivate {
  background-color: #ff9800;
  color: white;
}

.btn-edit {
  background-color: #2196f3;
  color: white;
}

.btn-delete {
  background-color: #ff5252;
  color: white;
}

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}

.stat-card {
  background: white;
  border: 1px solid #eee;
  border-radius: 10px;
  padding: 20px;
  text-align: center;
}

.stat-card h4 {
  margin: 0 0 10px 0;
  color: #666;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 1px;
}

.stat-value {
  margin: 0;
  font-size: 32px;
  font-weight: bold;
  color: #333;
}

.role-distribution {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.role-item {
  font-size: 14px;
  color: #666;
}

.user-form {
  background: white;
  border: 1px solid #eee;
  border-radius: 10px;
  padding: 30px;
  margin-top: 30px;
}

.user-form h3 {
  margin: 0 0 20px 0;
  color: #333;
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin-bottom: 20px;
}

@media (max-width: 768px) {
  .form-row {
    grid-template-columns: 1fr;
  }
}

.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 8px;
  font-weight: 500;
  color: #333;
}

.form-group input,
.form-group select {
  width: 100%;
  padding: 10px;
  border: 2px solid #ddd;
  border-radius: 6px;
  font-size: 14px;
  transition: border-color 0.3s;
}

.form-group input:focus,
.form-group select:focus {
  outline: none;
  border-color: #007bff;
}

.form-group input.error {
  border-color: #ff5252;
}

.error-message {
  margin: 5px 0 0 0;
  color: #ff5252;
  font-size: 12px;
}

.form-actions {
  display: flex;
  gap: 10px;
  margin-top: 20px;
}

.form-actions button {
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: opacity 0.3s;
}

.form-actions button:hover {
  opacity: 0.9;
}

.btn-submit {
  background-color: #007bff;
  color: white;
}

.btn-reset {
  background-color: #6c757d;
  color: white;
}

.btn-cancel {
  background-color: #dc3545;
  color: white;
}
</style>