管理后台系统

权限控制实现

在后台管理系统中,权限控制是保障系统安全的核心环节,需基于用户角色实现路由访问限制与动态路由加载。首先,创建权限管理模块 permission.ts,定义系统支持的角色类型(adminuser)及对应的权限配置:

typescript
复制代码
// 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']

typescript
复制代码
// 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 负责验证用户权限,实现登录状态检查与角色匹配逻辑:

typescript
复制代码
// 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();
});

动态路由需在用户登录后根据角色动态添加,避免未授权用户访问敏感路由:

typescript
复制代码
// 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('/'); // 登录成功跳转首页
};

权限控制关键要点

  1. 路由元信息 meta.roles 需明确声明访问所需角色,避免权限遗漏
  2. 动态路由添加需在登录后执行,确保路由表实时更新
  3. 全局守卫需优先处理登录状态验证,再进行角色权限匹配

数据表格与分页组件

数据表格是后台系统展示结构化数据的核心组件,结合 Element Plus 的 ElTableElPagination 可快速实现数据展示、排序、分页等功能。首先引入所需组件并注册:

typescript
复制代码
// 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 存储当前页数据,currentPagepageSize 控制分页参数:

typescript
复制代码
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 数组定义,支持排序、自定义模板等功能:

typescript
复制代码
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 处理分页逻辑:

plaintext
复制代码
<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>

分页事件处理函数需同步更新表格数据:

typescript
复制代码
// 页码变更
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:

bash
复制代码
npm install vee-validate @vee-validate/rules @vee-validate/i18n
typescript
复制代码
// 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');

在用户注册表单中,使用 useFormdefineField 定义字段与验证规则:

plaintext
复制代码
<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 变量实现品牌风格统一:

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; /* 圆角调整 */
}

在入口文件中导入自定义主题样式:

typescript
复制代码
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import './styles/element-vars.css'; // 自定义主题变量

createApp(App).mount('#app');

国际化需集成 vue-i18n 实现多语言切换,首先安装依赖:

bash
复制代码
npm install vue-i18n@9

配置多语言消息:

typescript
复制代码
// 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

typescript
复制代码
// src/main.ts
import { i18n } from './i18n';

createApp(App)
  .use(i18n)
  .mount('#app');

在模板中使用 $tt 函数获取翻译文本:

plaintext
复制代码
<!-- 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>

为语言切换添加过渡动画,提升用户体验:

css
复制代码
/* 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);
}
plaintext
复制代码
<template>
  <transition name="lang" class="lang-transition">
    <h1>{{ $t('welcome') }}</h1>
  </transition>
</template>

国际化最佳实践

  • 提取公共翻译文本至独立 JSON 文件,避免代码与翻译混杂
  • 使用 vue-i18ncompilationMode: 'strict' 开启严格模式,检测未翻译文本
  • 日期、数字等格式化需结合 @formatjs/intl-numberformat 等国际化 API