一、环境准备与项目搭建
本文假设你已经有一个可用的 Vue3 + TypeScript 项目。如果还没有,可以通过 Vite 脚手架快速创建:
bash
npm create vite@latest my-project -- --template vue-ts
cd my-project
npm install
接下来安装必要的依赖。本文以 Element Plus 作为 UI 组件库,Axios 作为 HTTP 请求库:
bash
npm install element-plus axios
在 main.ts 中全局注册 Element Plus:
typescript
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
二、定义 TypeScript 接口
在 src/types 目录下新建 user.ts(以用户管理为例),定义数据实体的类型:
typescript
export interface User {
id: number
name: string
email: string
role: string
status: number
createTime: string
}
// 分页请求参数
export interface UserQueryParams {
page: number
pageSize: number
keyword?: string
}
// 分页响应结构
export interface PageResult<T> {
list: T[]
total: number
page: number
pageSize: number
}
这一步很重要:TypeScript 接口不仅让代码有智能提示,还能在后续的增删改操作中对请求体和响应体做类型约束,减少低级错误。
三、封装 Axios 请求层
在 src/api 目录下创建 request.ts,封装统一的请求实例:
typescript
import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
})
// 响应拦截器:统一处理错误
request.interceptors.response.use(
(response) => {
const { code, data, message } = response.data
if (code === 200) {
return data
}
ElMessage.error(message || '请求失败')
return Promise.reject(new Error(message))
},
(error) => {
ElMessage.error(error.message || '网络异常')
return Promise.reject(error)
}
)
export default request
再创建 src/api/user.ts,封装与用户表相关的所有接口:
typescript
import request from './request'
import type { User, UserQueryParams, PageResult } from '@/types/user'
// 分页查询用户列表
export function getUserList(params: UserQueryParams) {
return request.get<PageResult<User>>('/users', { params })
}
// 新增用户
export function addUser(data: Omit<User, 'id' | 'createTime'>) {
return request.post<User>('/users', data)
}
// 编辑用户
export function updateUser(data: Partial<User> & { id: number }) {
return request.put<User>(`/users/${data.id}`, data)
}
// 删除用户
export function deleteUser(id: number) {
return request.delete<void>(`/users/${id}`)
}
这里用到了 TypeScript 的 Omit 和 Partial 工具类型:新增时不需要传 id 和 createTime,编辑时所有字段可选但 id 必传。这种类型约束能有效防止字段遗漏或传错。
四、在组件中获取数据并渲染表格
在 src/views 下创建 UserList.vue,先实现基础的数据获取和表格渲染:
vue
<template>
<div class="user-container">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-model="queryParams.keyword"
placeholder="请输入姓名或邮箱搜索"
clearable
style="width: 240px"
@keyup.enter="fetchData"
/>
<el-button type="primary" @click="fetchData">搜索</el-button>
<el-button type="success" @click="openAddDialog">新增用户</el-button>
</div>
<!-- 数据表格 -->
<el-table
:data="tableData"
border
stripe
v-loading="loading"
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column prop="email" label="邮箱" min-width="180" />
<el-table-column prop="role" label="角色" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="openEditDialog(row)">
编辑
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页器 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getUserList, deleteUser } from '@/api/user'
import type { User, UserQueryParams } from '@/types/user'
const loading = ref(false)
const tableData = ref<User[]>([])
const total = ref(0)
const queryParams = reactive<UserQueryParams>({
page: 1,
pageSize: 10,
keyword: '',
})
async function fetchData() {
loading.value = true
try {
const res = await getUserList({ ...queryParams })
tableData.value = res.list
total.value = res.total
} catch (error) {
console.error('获取数据失败', error)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchData()
})
</script>
至此,数据已经可以从后端拉取并以表格形式渲染出来。接下来进入核心部分------增删改的具体实现。
五、新增与编辑(表单弹窗)
新增和编辑通常共用同一个表单弹窗。我们通过一个 dialogVisible 控制弹窗显隐,通过 isEdit 区分新增还是编辑模式,通过 formData 绑定表单数据。
在 <script setup> 中补充以下逻辑:
typescript
import { addUser, updateUser } from '@/api/user'
const dialogVisible = ref(false)
const isEdit = ref(false)
const formRef = ref()
const formLoading = ref(false)
const formData = reactive<Partial<User>>({
name: '',
email: '',
role: '',
status: 1,
})
const formRules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' },
],
role: [{ required: true, message: '请选择角色', trigger: 'change' }],
}
// 打开新增弹窗
function openAddDialog() {
isEdit.value = false
// 重置表单
Object.assign(formData, {
name: '',
email: '',
role: '',
status: 1,
})
dialogVisible.value = true
// 清除上一次的校验状态
nextTick(() => formRef.value?.resetFields())
}
// 打开编辑弹窗(回填当前行数据)
function openEditDialog(row: User) {
isEdit.value = true
Object.assign(formData, {
id: row.id,
name: row.name,
email: row.email,
role: row.role,
status: row.status,
})
dialogVisible.value = true
nextTick(() => formRef.value?.clearValidate())
}
// 提交表单
async function handleSubmit() {
const valid = await formRef.value.validate().catch(() => false)
if (!valid) return
formLoading.value = true
try {
if (isEdit.value) {
await updateUser(formData as User)
ElMessage.success('编辑成功')
} else {
await addUser(formData as Omit<User, 'id' | 'createTime'>)
ElMessage.success('新增成功')
}
dialogVisible.value = false
fetchData() // 刷新表格
} catch (error) {
console.error('提交失败', error)
} finally {
formLoading.value = false
}
}
</script>
在 <template> 中添加弹窗组件:
vue
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑用户' : '新增用户'"
width="520px"
:close-on-click-modal="false"
@closed="formRef?.resetFields()"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="formData.role" placeholder="请选择角色" style="width: 100%">
<el-option label="管理员" value="admin" />
<el-option label="普通用户" value="user" />
<el-option label="访客" value="guest" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="formLoading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
关键点:新增 时清空所有字段并重置校验;编辑 时通过 Object.assign 将当前行的数据回填到表单,同时记录 id 用于调用更新接口。
六、删除操作
删除操作需要弹出确认框,避免用户误操作。确认后才调用后端接口:
typescript
async function handleDelete(row: User) {
try {
await ElMessageBox.confirm(
`确定要删除用户「${row.name}」吗?删除后不可恢复。`,
'删除确认',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning',
}
)
await deleteUser(row.id)
ElMessage.success('删除成功')
// 如果当前页删除后只剩 1 条,回退到上一页
if (tableData.value.length === 1 && queryParams.page > 1) {
queryParams.page--
}
fetchData()
} catch (error) {
// 用户取消删除或接口报错都会进入这里
if (error !== 'cancel' && error !== 'close') {
console.error('删除失败', error)
}
}
}
</script>
这里做了一个贴心的分页处理:当删除当前页最后一条数据后,自动回退到上一页,避免表格显示为空。
七、乐观更新 vs 重新请求
上述所有增删改操作在成功后都调用了 fetchData() 重新拉取列表。这种方式最简单可靠,但会有短暂的网络等待。如果你的场景对响应速度要求很高,也可以采用乐观更新:
- 新增 :接口成功后直接
tableData.value.unshift(newRecord),不重新请求。 - 编辑 :接口成功后找到对应索引,直接替换
tableData.value[index]。 - 删除 :接口成功后
tableData.value.splice(index, 1)。
但乐观更新需要额外处理失败回滚的问题,建议在稳定的小型项目中尝试。绝大多数场景下,重新请求一次列表是最稳妥的做法。
八、完整文件结构
最后整理一下本文涉及的所有文件,方便对照检查:
bash
src/
├── api/
│ ├── request.ts # Axios 实例 + 拦截器
│ └── user.ts # 用户相关接口
├── types/
│ └── user.ts # User 类型定义
├── views/
│ └── UserList.vue # 表格 + 增删改完整组件
└── main.ts # 入口文件(注册 Element Plus)
九、总结
回顾整个流程,Vue3 + TypeScript 实现后端数据表格渲染及增删改的核心步骤可以浓缩为以下几点:
- 定义类型接口 :使用 TypeScript 的
interface和工具类型(Omit、Partial、Pick)为数据实体和 API 响应建模。 - 封装请求层:通过 Axios 实例统一处理 baseURL、超时和错误拦截,再按模块拆分接口文件。
- 组件内获取数据 :在
onMounted或搜索触发时请求列表,将响应赋值给tableData响应式数组。 - 新增/编辑共用弹窗 :通过
isEdit标志区分模式,编辑时回填数据,提交时调用不同接口。 - 删除二次确认 :使用
ElMessageBox.confirm防止误删,并处理分页边界情况。 - 刷新策略 :增删改成功后统一调用
fetchData()刷新表格,保证数据与后端一致。
按照以上步骤实现后,一个完整的「请求 → 渲染 → 增删改」流程就跑通了。你可以将本文的示例代码直接拷贝到项目中,根据实际业务字段调整表格列和表单字段即可快速落地。