从零完成一个列表页:增删改查实战

从零完成一个列表页:增删改查实战

这是 Vue 入门系列的最后一篇。我们做一个完整的用户列表页,包含增删改查、分页、搜索、表单验证。

需求分析

  • 用户列表展示(表格)
  • 搜索用户名
  • 新建/编辑用户(弹窗表单)
  • 删除用户(确认)
  • 分页

组件结构

src/views/
├── UserList.vue      # 主页面
└── UserModal.vue     # 新建/编辑弹窗
src/
├── api/
│   └── user.ts       # API 封装
└── types/
    └── user.ts       # 类型定义

类型定义

// src/types/user.ts
export interface User {
  id?: number
  name: string
  email: string
  role: 'admin' | 'user' | 'guest'
}

export interface Pagination {
  page: number
  size: number
  total: number
}

API 封装

// src/api/user.ts
import request from '../utils/request'

export const userApi = {
  list(params: { page: number; size: number; name?: string }) {
    return request.get<{ list: User[]; total: number }>('/users', { params })
  },
  create(data: User) {
    return request.post<User>('/users', data)
  },
  update(id: number, data: User) {
    return request.put<User>(`/users/${id}`, data)
  },
  delete(id: number) {
    return request.delete(`/users/${id}`)
  }
}

弹窗组件

<!-- UserModal.vue -->
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { User } from '../types/user'

const props = defineProps<{
  visible: boolean
  user?: User | null
}>()

const emit = defineEmits<{
  (e: 'update:visible', val: boolean): void
  (e: 'submit', data: User): void
}>()

const form = ref<User>({
  name: '',
  email: '',
  role: 'user'
})

const rules = {
  name: [{ required: true, message: '请输入姓名' }],
  email: [
    { required: true, message: '请输入邮箱' },
    { type: 'email', message: '邮箱格式不正确' }
  ]
}

watch(() => props.user, (newUser) => {
  form.value = newUser ? { ...newUser } : { name: '', email: '', role: 'user' }
}, { immediate: true })

function handleSubmit() {
  emit('submit', { ...form.value })
  emit('update:visible', false)
}

function handleClose() {
  emit('update:visible', false)
}
</script>

<template>
  <Teleport to="body">
    <div v-if="visible" class="modal-mask" @click.self="handleClose">
      <div class="modal-content">
        <h3>{{ user ? '编辑用户' : '新建用户' }}</h3>
        <form @submit.prevent="handleSubmit">
          <div class="form-item">
            <label>姓名</label>
            <input v-model="form.name" />
          </div>
          <div class="form-item">
            <label>邮箱</label>
            <input v-model="form.email" type="email" />
          </div>
          <div class="form-item">
            <label>角色</label>
            <select v-model="form.role">
              <option value="admin">管理员</option>
              <option value="user">普通用户</option>
              <option value="guest">访客</option>
            </select>
          </div>
          <div class="actions">
            <button type="button" @click="handleClose">取消</button>
            <button type="submit">确定</button>
          </div>
        </form>
      </div>
    </div>
  </Teleport>
</template>

<style scoped>
.modal-mask {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}
.modal-content {
  background: white;
  padding: 24px;
  border-radius: 8px;
  min-width: 400px;
}
.form-item {
  margin-bottom: 16px;
}
.form-item label {
  display: block;
  margin-bottom: 4px;
}
.form-item input,
.form-item select {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}
.actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 24px;
}
</style>

主页面

<!-- UserList.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { userApi } from '../api/user'
import type { User } from '../types/user'
import UserModal from './UserModal.vue'

const users = ref<User[]>([])
const pagination = ref({ page: 1, size: 10, total: 0 })
const searchName = ref('')
const showModal = ref(false)
const editingUser = ref<User | null>(null)
const loading = ref(false)

async function fetchUsers() {
  loading.value = true
  try {
    const res = await userApi.list({
      page: pagination.value.page,
      size: pagination.value.size,
      name: searchName.value || undefined
    })
    users.value = res.list
    pagination.value.total = res.total
  } finally {
    loading.value = false
  }
}

function handleSearch() {
  pagination.value.page = 1
  fetchUsers()
}

function handlePageChange(page: number) {
  pagination.value.page = page
  fetchUsers()
}

function handleAdd() {
  editingUser.value = null
  showModal.value = true
}

function handleEdit(user: User) {
  editingUser.value = user
  showModal.value = true
}

async function handleSubmit(data: User) {
  if (data.id) {
    await userApi.update(data.id, data)
  } else {
    await userApi.create(data)
  }
  await fetchUsers()
}

async function handleDelete(user: User) {
  if (!user.id) return
  if (confirm(`确定删除用户 ${user.name} 吗?`)) {
    await userApi.delete(user.id)
    await fetchUsers()
  }
}

onMounted(fetchUsers)
</script>

<template>
  <div class="user-list">
    <header>
      <h2>用户管理</h2>
      <div class="search">
        <input v-model="searchName" placeholder="搜索用户名" @keyup.enter="handleSearch" />
        <button @click="handleSearch">搜索</button>
        <button @click="handleAdd">新建</button>
      </div>
    </header>

    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>姓名</th>
          <th>邮箱</th>
          <th>角色</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr v-if="loading">
          <td colspan="5">加载中...</td>
        </tr>
        <tr v-else v-for="user in users" :key="user.id">
          <td>{{ user.id }}</td>
          <td>{{ user.name }}</td>
          <td>{{ user.email }}</td>
          <td>{{ user.role }}</td>
          <td>
            <button @click="handleEdit(user)">编辑</button>
            <button @click="handleDelete(user)">删除</button>
          </td>
        </tr>
      </tbody>
    </table>

    <div class="pagination">
      <span>共 {{ pagination.total }} 条</span>
      <button
        v-for="page in Math.ceil(pagination.total / pagination.size)"
        :key="page"
        @click="handlePageChange(page)"
      >
        {{ page }}
      </button>
    </div>

    <UserModal
      v-model:visible="showModal"
      :user="editingUser"
      @submit="handleSubmit"
    />
  </div>
</template>

<style scoped>
.user-list {
  padding: 24px;
}
header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 24px;
}
.search {
  display: flex;
  gap: 8px;
}
.search input {
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}
table {
  width: 100%;
  border-collapse: collapse;
}
th, td {
  border: 1px solid #eee;
  padding: 12px;
  text-align: left;
}
th {
  background: #f5f5f5;
}
.pagination {
  margin-top: 16px;
  display: flex;
  gap: 8px;
  align-items: center;
}
</style>

总结

这个系列我们学了:

  1. 环境搭建 - Node.js、Vite、项目结构
  2. 基础语法 - 响应式、模板、事件、组件、生命周期
  3. 路由与状态 - Vue Router、Pinia
  4. 进阶技巧 - 插槽、指令、异步组件、TypeScript
  5. 项目实战 - 接口调用、CRUD 列表页

学完这些,你已经具备了独立完成 Vue 3 项目的能力。接下来多写代码、多踩坑,进步会更快。

有问题欢迎留言。

最后更新 4/30/2026, 8:57:45 AM