从零完成一个列表页:增删改查实战
从零完成一个列表页:增删改查实战
这是 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>
总结
这个系列我们学了:
- 环境搭建 - Node.js、Vite、项目结构
- 基础语法 - 响应式、模板、事件、组件、生命周期
- 路由与状态 - Vue Router、Pinia
- 进阶技巧 - 插槽、指令、异步组件、TypeScript
- 项目实战 - 接口调用、CRUD 列表页
学完这些,你已经具备了独立完成 Vue 3 项目的能力。接下来多写代码、多踩坑,进步会更快。
有问题欢迎留言。
