Vue3 实战:带 Token 认证的多功能表格实现方案

在后台管理系统中,我们经常需要实现带权限控制的多功能表格 —— 表格数据通过 Token 认证获取,且包含文本展示、状态切换、下拉筛选、操作按钮等复杂交互。 本文将模拟一个 “用户角色管理” 场景,通过三种不同方案实现这一需求,并详解核心逻辑。

场景模拟:用户角色管理表格

需求说明:

  1. 页面加载时,通过 Token 认证请求用户列表接口(/api/users
  2. 表格需展示以下列:
    • 用户名(文本)
    • 年龄(数字)
    • 状态(单选框:isActivetrue 则勾选)
    • 角色(下拉框:根据 role 字段显示对应角色名)
    • 操作(根据 permission 字段显示按钮:如编辑、删除、重置密码)
  3. 点击操作按钮时,携带 Token 调用对应接口(如删除用户 /api/users/{id}

通用准备工作

封装认证请求工具

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import axios from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import router from '@/router';

const request = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 5000
});

// 请求拦截器:添加 Token
request.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('adminToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 响应拦截器:处理 Token 失效
request.interceptors.response.use(
  (res) => res.data,
  (error) => {
    if (error.response?.status === 401) {
      // Token 失效,跳转登录页
      localStorage.removeItem('adminToken');
      ElMessageBox.alert('登录已过期,请重新登录', '提示', {
        confirmButtonText: '确定',
        callback: () => {
          router.push('/login');
        }
      });
    } else {
      ElMessage.error(error.response?.data?.msg || '操作失败');
    }
    return Promise.reject(error);
  }
);

export default request;

定义 API 接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import request from '@/utils/request';

// 获取用户列表
export const getUsers = () => request.get('/users');

// 更新用户状态
export const updateUserStatus = (id, isActive) => 
  request.patch(`/users/${id}`, { isActive });

// 更改用户角色
export const updateUserRole = (id, role) => 
  request.patch(`/users/${id}`, { role });

// 删除用户
export const deleteUser = (id) => request.delete(`/users/${id}`);

// 重置密码
export const resetPassword = (id) => 
  request.post(`/users/${id}/reset-password`);

角色映射配置(通用)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export const ROLE_MAP = {
  admin: '管理员',
  editor: '编辑',
  viewer: '查看者'
};

export const ROLE_OPTIONS = [
  { label: '管理员', value: 'admin' },
  { label: '编辑', value: 'editor' },
  { label: '查看者', value: 'viewer' }
];

方案一:原生 Composition API 实现

适合需要完全控制状态逻辑的场景,灵活性最高。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<template>
  <div class="user-table-container">
    <el-table 
      :data="tableData" 
      border 
      style="width: 100%"
      v-loading="loading"
    >
      <el-table-column prop="username" label="用户名" width="180" />
      <el-table-column prop="age" label="年龄" width="120" />
      
      <!-- 状态列单选框 -->
      <el-table-column label="状态" width="140">
        <template #default="scope">
          <el-switch 
            v-model="scope.row.isActive" 
            active-text="启用" 
            inactive-text="禁用"
            @change="handleStatusChange(scope.row)"
          />
        </template>
      </el-table-column>
      
      <!-- 角色列下拉框 -->
      <el-table-column label="角色" width="160">
        <template #default="scope">
          <el-select 
            v-model="scope.row.role" 
            size="small"
            @change="handleRoleChange(scope.row)"
          >
            <el-option 
              v-for="opt in ROLE_OPTIONS" 
              :key="opt.value"
              :label="opt.label" 
              :value="opt.value"
            />
          </el-select>
        </template>
      </el-table-column>
      
      <!-- 操作列动态按钮 -->
      <el-table-column label="操作" width="300">
        <template #default="scope">
          <el-button 
            v-if="scope.row.permission.includes('edit')"
            size="small" 
            type="primary" 
            @click="handleEdit(scope.row)"
          >
            编辑
          </el-button>
          <el-button 
            v-if="scope.row.permission.includes('delete')"
            size="small" 
            type="danger" 
            @click="handleDelete(scope.row.id)"
          >
            删除
          </el-button>
          <el-button 
            v-if="scope.row.permission.includes('reset')"
            size="small" 
            @click="handleReset(scope.row.id)"
          >
            重置密码
          </el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { 
  getUsers, 
  updateUserStatus, 
  updateUserRole, 
  deleteUser, 
  resetPassword 
} from '@/api/user';
import { ROLE_MAP, ROLE_OPTIONS } from '@/constants/role';
import { ElMessage, ElMessageBox } from 'element-plus';

// 状态管理
const tableData = ref([]);
const loading = ref(true);

// 初始化加载数据
onMounted(async () => {
  try {
    const res = await getUsers();
    tableData.value = res.list; // 假设接口返回 { list: [...] }
  } catch (err) {
    console.error('加载用户失败:', err);
  } finally {
    loading.value = false;
  }
});

// 处理状态切换
const handleStatusChange = async (row) => {
  try {
    await updateUserStatus(row.id, row.isActive);
    ElMessage.success(`已${row.isActive ? '启用' : '禁用'}用户`);
  } catch (err) {
    // 接口失败时回滚状态
    row.isActive = !row.isActive;
  }
};

// 处理角色变更
const handleRoleChange = async (row) => {
  try {
    await updateUserRole(row.id, row.role);
    ElMessage.success(`已将用户角色改为${ROLE_MAP[row.role]}`);
  } catch (err) {
    // 回滚到原始角色(需缓存原始值)
    row.role = row.originalRole;
  }
};

// 编辑用户(此处简化为弹窗提示)
const handleEdit = (row) => {
  ElMessageBox.alert(`编辑用户: ${row.username}`, '提示');
};

// 删除用户
const handleDelete = async (id) => {
  try {
    await ElMessageBox.confirm(
      '确定要删除该用户吗?',
      '警告',
      { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
    );
    await deleteUser(id);
    ElMessage.success('删除成功');
    // 从表格中移除该用户
    tableData.value = tableData.value.filter(item => item.id !== id);
  } catch (err) {
    // 取消删除时不做处理
    if (err !== 'cancel') console.error('删除失败:', err);
  }
};

// 重置密码
const handleReset = async (id) => {
  try {
    const res = await resetPassword(id);
    ElMessage.success(`密码已重置为: ${res.tempPassword}`);
  } catch (err) {
    console.error('重置失败:', err);
  }
};
</script>

方案二:使用 Pinia 管理状态

适合需要在多个组件间共享表格数据的场景,如侧边栏筛选与表格联动。

创建 Pinia Store

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { defineStore } from 'pinia';
import { getUsers, updateUserStatus, updateUserRole, deleteUser, resetPassword } from '@/api/user';

export const useUserStore = defineStore('user', {
  state: () => ({
    list: [],
    loading: false,
    error: null
  }),
  
  actions: {
    // 加载用户列表
    async fetchUsers() {
      this.loading = true;
      try {
        const res = await getUsers();
        this.list = res.list;
        this.error = null;
      } catch (err) {
        this.error = err;
      } finally {
        this.loading = false;
      }
    },
    
    // 更新状态
    async updateStatus(id, isActive) {
      await updateUserStatus(id, isActive);
      const user = this.list.find(u => u.id === id);
      if (user) user.isActive = isActive;
    },
    
    // 更新角色
    async updateRole(id, role) {
      await updateUserRole(id, role);
      const user = this.list.find(u => u.id === id);
      if (user) user.role = role;
    },
    
    // 删除用户
    async removeUser(id) {
      await deleteUser(id);
      this.list = this.list.filter(u => u.id !== id);
    }
  }
});

表格组件(使用 Pinia)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<template>
  <!-- 表格结构与方案一基本一致仅状态来源不同 -->
  <el-table 
    :data="userStore.list" 
    border 
    style="width: 100%"
    v-loading="userStore.loading"
  >
    <!-- 列定义与方案一相同 -->
    <el-table-column prop="username" label="用户名" />
    <!-- 状态列 -->
    <el-table-column label="状态">
      <template #default="scope">
        <el-switch 
          v-model="scope.row.isActive" 
          @change="handleStatusChange(scope.row)"
        />
      </template>
    </el-table-column>
    <!-- 其他列... -->
  </el-table>
</template>

<script setup>
import { onMounted } from 'vue';
import { useUserStore } from '@/stores/userStore';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ROLE_MAP, ROLE_OPTIONS } from '@/constants/role';

const userStore = useUserStore();

// 初始化数据
onMounted(() => {
  userStore.fetchUsers();
});

// 状态变更(调用 store 方法)
const handleStatusChange = async (row) => {
  try {
    await userStore.updateStatus(row.id, row.isActive);
    ElMessage.success('状态更新成功');
  } catch (err) {
    row.isActive = !row.isActive; // 回滚
  }
};

// 其他方法(角色变更、删除等)均通过 store 实现,省略...
</script>

方案三:使用 vue-request 简化请求逻辑

适合追求代码简洁性,减少重复状态管理的场景。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<template>
  <el-table 
    :data="data?.list || []" 
    border 
    style="width: 100%"
    v-loading="loading"
  >
    <!-- 表格列定义与方案一完全相同 -->
    <el-table-column prop="username" label="用户名" />
    <!-- 状态列 -->
    <el-table-column label="状态">
      <template #default="scope">
        <el-switch 
          v-model="scope.row.isActive" 
          @change="handleStatusChange(scope.row)"
        />
      </template>
    </el-table-column>
    <!-- 其他列... -->
  </el-table>
</template>

<script setup>
import { useRequest } from 'vue-request';
import { 
  getUsers, 
  updateUserStatus, 
  updateUserRole, 
  deleteUser, 
  resetPassword 
} from '@/api/user';
import { ROLE_MAP, ROLE_OPTIONS } from '@/constants/role';
import { ElMessage, ElMessageBox } from 'element-plus';

// 使用 vue-request 获取用户列表
const { data, loading, run: refreshUsers } = useRequest(getUsers, {
  refreshDeps: [], // 依赖变化时重新请求
  onError: (err) => console.error('加载失败:', err)
});

// 状态变更
const handleStatusChange = async (row) => {
  try {
    await updateUserStatus(row.id, row.isActive);
    ElMessage.success('状态更新成功');
  } catch (err) {
    row.isActive = !row.isActive;
  }
};

// 删除用户(删除后刷新列表)
const handleDelete = async (id) => {
  try {
    await ElMessageBox.confirm('确定删除?', '警告');
    await deleteUser(id);
    ElMessage.success('删除成功');
    refreshUsers(); // 重新请求列表
  } catch (err) {}
};

// 其他方法与方案一类似...
</script>

关键功能实现解析

动态操作按钮渲染

根据 permission 数组动态显示按钮:

1
2
3
<!-- 仅当 permission 包含对应权限时显示按钮 -->
<el-button v-if="scope.row.permission.includes('edit')">编辑</el-button>
<el-button v-if="scope.row.permission.includes('delete')">删除</el-button>

接口返回的 permission 示例:

1
2
3
4
5
{
   "id": 1,
   "username": "admin",
   "permission": ["edit", "delete", "reset"] // 拥有全部权限
}

状态回滚机制

接口调用失败时恢复原始状态,避免界面与后端不一致:

1
2
3
4
5
6
7
8
const handleStatusChange = async (row) => {
  const originalStatus = row.isActive; // 缓存原始值
  try {
    await updateUserStatus(row.id, row.isActive);
  } catch (err) {
    row.isActive = originalStatus; // 回滚
  }
};

Token 认证与权限控制

  • 通过请求拦截器自动添加 Token 到请求头
  • 响应拦截器处理 Token 失效(401 状态码),自动跳转登录页
  • 前端通过 permission 字段控制按钮显示,后端需同时做权限校验(防止越权操作)

方案对比与选择建议

方案 优先 缺点 适用场景
原生 Composition API 无额外依赖,完全掌控状态 重复代码多,状态分散 简单页面,无状态共享需求
Pinia 状态管理 状态集中管理,多组件共享 需额外定义 store,略复杂 复杂页面,多组件联动
vue-request 简化请求逻辑,自动管理状态 增加依赖,灵活性稍弱 以数据请求为主的页面

实际开发中,中小型项目推荐使用 方案一 或 方案三,大型项目(尤其是需要多组件共享数据时)推荐 方案二。

如果本文对您有所帮助,欢迎打赏支持作者!

Licensed under CC BY-NC-SA 4.0
最后更新于 2025-10-26 12:14
使用 Hugo 构建
主题 StackJimmy 设计