在后台管理系统中,权限控制是保障系统安全的核心环节,需基于用户角色实现路由访问限制与动态路由加载。首先,创建权限管理模块 permission.ts,定义系统支持的角色类型(admin 与 user)及对应的权限配置:
// src/store/permission.ts
export const Roles = ['admin', 'user'] as const;
export type Role = typeof Roles[number];
// 路由元信息类型扩展
declare module 'vue-router' {
interface RouteMeta {
roles?: Role[]; // 路由所需角色
requiresAuth?: boolean; // 是否需要登录
}
}
路由配置时,通过 meta 字段声明路由的访问权限,例如管理员专属路由需添加 roles: ['admin']:
// src/router/routes.ts
import { RouteRecordRaw } from 'vue-router';
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/login',
component: () => import('@/views/login.vue'),
meta: { requiresAuth: false }
},
{
path: '/403',
component: () => import('@/views/403.vue'),
meta: { requiresAuth: false }
}
];
// 动态路由(需权限控制)
export const adminRoutes: RouteRecordRaw[] = [
{
path: '/admin',
component: () => import('@/views/admin/index.vue'),
meta: { roles: ['admin'], requiresAuth: true }
}
];
export const userRoutes: RouteRecordRaw[] = [
{
path: '/profile',
component: () => import('@/views/profile.vue'),
meta: { roles: ['user', 'admin'], requiresAuth: true }
}
];
全局路由守卫 beforeEach 负责验证用户权限,实现登录状态检查与角色匹配逻辑:
// src/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';
import { constantRoutes, adminRoutes, userRoutes } from './routes';
import { useUserStore } from '@/store/user';
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes
});
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
const isLoggedIn = userStore.token;
// 未登录访问需授权路由,跳转至登录页
if (to.meta.requiresAuth && !isLoggedIn) {
return next('/login');
}
// 已登录用户访问登录页,重定向至首页
if (to.path === '/login' && isLoggedIn) {
return next('/');
}
// 权限检查:验证用户角色是否包含路由所需角色
if (to.meta.roles && !to.meta.roles.includes(userStore.role)) {
return next('/403');
}
next();
});
动态路由需在用户登录后根据角色动态添加,避免未授权用户访问敏感路由:
// src/views/login.vue
import { useRouter } from 'vue-router';
import { useUserStore } from '@/store/user';
import { adminRoutes, userRoutes } from '@/router/routes';
const router = useRouter();
const userStore = useUserStore();
const handleLogin = async () => {
const { role } = await loginApi(formData); // 登录接口返回用户角色
userStore.setRole(role);
// 根据角色添加动态路由
if (role === 'admin') {
adminRoutes.forEach(route => router.addRoute(route));
} else {
userRoutes.forEach(route => router.addRoute(route));
}
router.push('/'); // 登录成功跳转首页
};
权限控制关键要点:
meta.roles 需明确声明访问所需角色,避免权限遗漏 数据表格是后台系统展示结构化数据的核心组件,结合 Element Plus 的 ElTable 与 ElPagination 可快速实现数据展示、排序、分页等功能。首先引入所需组件并注册:
// src/views/product/list.vue
import { ElTable, ElTableColumn, ElPagination, ElButton } from 'element-plus';
import 'element-plus/es/components/table/style/css';
import 'element-plus/es/components/pagination/style/css';
表格数据与分页状态通过响应式变量管理,tableData 存储当前页数据,currentPage 与 pageSize 控制分页参数:
import { ref, onMounted } from 'vue';
// 表格数据
const tableData = ref([]);
// 分页参数
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0); // 总条数
// 加载表格数据
const fetchTableData = async (page = 1, size = 10) => {
const res = await productApi.getList({ page, size });
tableData.value = res.items;
total.value = res.total;
};
onMounted(() => {
fetchTableData(currentPage.value, pageSize.value);
});
表格列配置通过 columns 数组定义,支持排序、自定义模板等功能:
const columns = [
{
prop: 'id',
label: 'ID',
width: 80,
align: 'center'
},
{
prop: 'name',
label: '商品名称',
sortable: true, // 支持排序
minWidth: 180
},
{
prop: 'price',
label: '价格',
sortable: true,
formatter: (row) => `¥${row.price.toFixed(2)}`, // 格式化价格
align: 'right'
},
{
prop: 'status',
label: '状态',
align: 'center',
// 自定义状态标签
render: (row) => (
<ElTag type={row.status === 'active' ? 'success' : 'danger'}>
{row.status === 'active' ? '启用' : '禁用'}
</ElTag>
)
},
{
label: '操作',
align: 'center',
fixed: 'right',
width: 180,
// 行操作按钮
render: (row) => (
<div class="operation-buttons">
<ElButton
size="small"
type="primary"
onClick={() => handleEdit(row.id)}
>
编辑
</ElButton>
<ElButton
size="small"
type="danger"
onClick={() => handleDelete(row.id)}
>
删除
</ElButton>
</div>
)
}
];
模板中通过 ElTable 绑定数据与列配置,ElPagination 处理分页逻辑:
<template>
<div class="table-container">
<ElTable
:data="tableData"
border
stripe
style="width: 100%"
>
<ElTableColumn
v-for="col in columns"
:key="col.prop || col.label"
:prop="col.prop"
:label="col.label"
:width="col.width"
:align="col.align"
:sortable="col.sortable"
>
<!-- 自定义列渲染 -->
<template #default="scope" v-if="col.render">
{col.render(scope.row)}
</template>
</ElTableColumn>
</ElTable>
<!-- 分页组件 -->
<div class="pagination">
<ElPagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</template>
分页事件处理函数需同步更新表格数据:
// 页码变更
const handleCurrentChange = (page) => {
currentPage.value = page;
fetchTableData(page, pageSize.value);
};
// 每页条数变更
const handleSizeChange = (size) => {
pageSize.value = size;
currentPage.value = 1; // 重置页码为1
fetchTableData(1, size);
};
// 编辑与删除操作
const handleEdit = (id) => {
router.push(`/product/edit/${id}`);
};
const handleDelete = async (id) => {
await productApi.delete(id);
fetchTableData(currentPage.value, pageSize.value); // 重新加载数据
};
表格性能优化建议:
virtual-scroll 虚拟滚动,减少 DOM 节点 stripe 斑马纹与 border 边框,降低渲染开销 debounce(fetchTableData, 300))表单验证是保障数据合法性的关键,VeeValidate 提供了灵活的表单验证方案,支持同步规则、异步验证与错误信息自定义。首先安装依赖并配置 VeeValidate:
npm install vee-validate @vee-validate/rules @vee-validate/i18n
// src/plugins/vee-validate.ts
import { configure, defineRule } from 'vee-validate';
import { required, min, max, regex } from '@vee-validate/rules';
import { localize, setLocale } from '@vee-validate/i18n';
import zhCN from '@vee-validate/i18n/dist/locale/zh_CN.json';
// 注册基础规则
defineRule('required', required);
defineRule('min', min);
defineRule('max', max);
defineRule('regex', regex);
// 配置本地化
configure({
generateMessage: localize({ zh_CN: zhCN }),
validateOnInput: true // 输入时触发验证
});
setLocale('zh_CN');
在用户注册表单中,使用 useForm 与 defineField 定义字段与验证规则:
<template>
<form class="register-form" @submit.prevent="onSubmit">
<div class="form-group">
<label>用户名</label>
<input
v-model="username"
type="text"
class="form-control"
:class="{ 'is-invalid': errors.username }"
>
<span class="error-message">{{ errors.username }}</span>
</div>
<div class="form-group">
<label>密码</label>
<input
v-model="password"
type="password"
class="form-control"
:class="{ 'is-invalid': errors.password }"
>
<span class="error-message">{{ errors.password }}</span>
</div>
<div class="form-group">
<label>确认密码</label>
<input
v-model="confirmPassword"
type="password"
class="form-control"
:class="{ 'is-invalid': errors.confirmPassword }"
>
<span class="error-message">{{ errors.confirmPassword }}</span>
</div>
<ElButton type="primary" class="submit-btn" :loading="isSubmitting">
注册
</ElButton>
</form>
</template>
<script setup lang="ts">
import { useForm, defineField } from 'vee-validate';
import { ElButton } from 'element-plus';
// 定义表单字段与验证规则
const { handleSubmit, errors, isSubmitting } = useForm({
initialValues: {
username: '',
password: '',
confirmPassword: ''
},
validationSchema: {
username: defineField('username', [
{ required: true, message: '用户名不能为空' },
{ min: 3, message: '用户名长度不能少于3个字符' },
// 异步验证:检查用户名是否已存在
async (value) => {
if (!value) return true; // 跳过空值验证(由required规则处理)
const res = await fetch(`/api/check-username?name=${value}`);
return res.ok ? true : '用户名已存在';
}
]),
password: defineField('password', [
{ required: true, message: '密码不能为空' },
{
regex: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
message: '密码需包含大小写字母和数字,且长度不少于8位'
}
]),
confirmPassword: defineField('confirmPassword', [
{ required: true, message: '请确认密码' },
// 验证与密码一致
(value, { form }) => {
if (value !== form.password) {
return '两次密码输入不一致';
}
return true;
}
])
}
});
// 表单提交处理
const onSubmit = handleSubmit(async (values) => {
await registerApi(values); // 提交注册数据
ElMessage.success('注册成功');
});
</script>
复杂验证场景处理:
loading 状态 regex 规则结合正向预查 (?=.*[条件]) useFieldArray 管理字段数组与验证Element Plus 主题定制与国际化可提升系统的可定制性与多语言支持。主题定制通过覆盖 CSS 变量实现品牌风格统一:
/* src/styles/element-vars.css */
/* 导入 Element Plus 基础样式 */
@import "element-plus/dist/theme-chalk/index.css";
/* 覆盖主题变量 */
:root {
--el-color-primary: #6200ee; /* 主色调改为紫色 */
--el-color-primary-light-3: #7b1fff;
--el-color-primary-light-5: #943dff;
--el-color-primary-light-7: #ab66ff;
--el-color-primary-light-8: #c48fff;
--el-color-primary-light-9: #ddb8ff;
--el-border-radius-base: 6px; /* 圆角调整 */
}
在入口文件中导入自定义主题样式:
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import './styles/element-vars.css'; // 自定义主题变量
createApp(App).mount('#app');
国际化需集成 vue-i18n 实现多语言切换,首先安装依赖:
npm install vue-i18n@9
配置多语言消息:
// src/i18n/index.ts
import { createI18n } from 'vue-i18n';
// 语言消息
const messages = {
en: {
welcome: 'Welcome to Admin System',
login: {
username: 'Username',
password: 'Password',
submit: 'Login'
},
table: {
name: 'Product Name',
price: 'Price',
status: 'Status',
operation: 'Operation'
}
},
zh: {
welcome: '欢迎使用管理系统',
login: {
username: '用户名',
password: '密码',
submit: '登录'
},
table: {
name: '商品名称',
price: '价格',
status: '状态',
operation: '操作'
}
}
};
export const i18n = createI18n({
legacy: false, // 启用 Composition API
locale: 'zh', // 默认语言
fallbackLocale: 'en', // 回退语言
messages
});
在 main.ts 中注册 i18n:
// src/main.ts
import { i18n } from './i18n';
createApp(App)
.use(i18n)
.mount('#app');
在模板中使用 $t 或 t 函数获取翻译文本:
<!-- src/App.vue -->
<template>
<div class="app-header">
<h1>{{ $t('welcome') }}</h1>
<ElButton @click="toggleLocale">
{{ $t(`lang.${i18n.locale}`) }}
</ElButton>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ElButton } from 'element-plus';
const { locale } = useI18n();
// 切换语言
const toggleLocale = () => {
locale.value = locale.value === 'zh' ? 'en' : 'zh';
};
</script>
为语言切换添加过渡动画,提升用户体验:
/* src/styles/transition.css */
.lang-transition {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.lang-enter-from,
.lang-leave-to {
opacity: 0;
transform: translateY(10px);
}
<template>
<transition name="lang" class="lang-transition">
<h1>{{ $t('welcome') }}</h1>
</transition>
</template>
国际化最佳实践:
vue-i18n 的 compilationMode: 'strict' 开启严格模式,检测未翻译文本 @formatjs/intl-numberformat 等国际化 API