本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套基于Vue 2构建的企业官网后台前端源码,开箱即用,无需从零搭建基础架构。包含完整的登录页(Login.vue)、主布局框架(App.vue、MainBox.vue)、路由配置(routerConfig.js)、Vuex状态管理(store)、统一HTTP请求封装(axios.config.js),以及用户管理、新闻管理、产品管理三大核心业务模块,每个模块均提供独立视图与组件结构。项目目录组织清晰,src下按功能划分views(页面)、components(复用组件)、router(路由)、store(状态)、assets(静态资源如logo.png、login.jpg)等;public存放index.html和favicon.ico;配套babel.config.js、jsconfig.、.browserslistrc、package.及yarn.lock等标准构建配置,兼容主流Vue 2开发环境与本地运行调试。支持快速对接后端API,适合用于企业官网类项目的后台快速落地或二次开发,省去路由初始化、权限占位、基础UI布局等重复工作。

1. 这不是“又一个后台模板”,而是一套真正能上线的企业级前端骨架

我用这套 Vue 2 后台源码,三个月内交付了 7 家本地中小企业的官网后台——不是演示站,是客户每天在用、财务在录单、市场在发新闻、客服在查用户的生产环境。它不炫技,没有花哨的动画和未落地的“微前端”概念,但每个文件都在解决一个真实问题:登录页为什么必须独立于主框架?MainBox.vue 里那个看似多余的 <keep-alive> 包裹逻辑,其实是为新闻列表页返回时保留滚动位置;routerConfig.js 里所有路由都加了 meta: { requiresAuth: true },但没写一句权限校验代码——因为真正的权限控制不在前端路由守卫里,而在每个 API 请求头里塞的 token 和后端返回的菜单结构。这正是企业官网后台最常被忽略的真相:前端不是权限的裁判,而是后端策略的忠实执行者和用户体验的精密调度员

关键词里的“Vue2后台”“企业官网源码”“用户管理”“新闻管理”“产品管理”,不是功能罗列,而是业务流切片。用户管理模块不只是增删改查,它要对接 LDAP 或微信扫码登录(你只需替换 axios 请求地址);新闻管理必须支持富文本编辑、封面图裁剪、发布时间定时发布;产品管理得预留 SKU 扩展字段、多图上传、分类树联动。这套源码把所有这些“应该有但没人愿意写的基建”都提前铺好了:axios.config.js 里预置了请求拦截器自动注入 token、响应拦截器统一处理 401 跳转登录、500 弹出错误提示;store/modules/user.js 不只存用户名,还存了当前用户可访问的菜单数组,这个数组直接驱动侧边栏渲染——它来自后端 /api/menu 接口,不是前端硬编码。你拿到手的第一件事,不是改样式,而是打开 src/api/index.js,把 baseURL 换成你后端的真实域名,然后跑起来,看登录页是否能正确跳转、菜单是否动态加载、新闻列表是否能分页拉取。如果这一步卡住了,90% 的问题出在跨域配置或 token 返回格式不一致上,而不是代码本身。它适合谁?适合那些被老板催着“下周上线后台”的前端工程师,适合接手外包项目却只有后端 API 文档的自由职业者,也适合教学生做毕业设计的老师——因为它的目录结构就是一本活的 Vue 2 工程化实践手册,比任何教程都直白。

2. 整体架构设计:为什么选择“去权限化路由”与“API 驱动视图”

2.1 核心思路拆解:拒绝前端权限幻觉,拥抱后端真实授权

很多 Vue 后台模板一上来就搞复杂的路由权限控制:router.beforeEach 里层层判断角色、菜单、按钮权限,甚至用 v-if 控制按钮显隐。这套源码反其道而行之——它的路由配置 routerConfig.js 极其干净:

// src/router/routerConfig.js
export default [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/MainBox.vue'),
    meta: { requiresAuth: true },
    children: [
      { path: '', redirect: '/dashboard' },
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('@/views/Dashboard.vue')
      },
      {
        path: 'user-manage',
        name: 'UserManage',
        component: () => import('@/views/user-manage/UserList.vue')
      },
      {
        path: 'news-manage',
        name: 'NewsManage',
        component: () => import('@/views/news-manage/NewsList.vue')
      },
      {
        path: 'product-manage',
        name: 'ProductManage',
        component: () => import('@/views/product-manage/ProductList.vue')
      }
    ]
  }
]

注意两点:第一,所有业务路由都放在 Homechildren 下,由 MainBox.vue 统一承载;第二,meta.requiresAuth 只做登录态校验,不做权限过滤。为什么?因为企业官网后台的权限本质是数据级的。销售总监能看到所有客户订单,但只能修改自己名下的产品价格;内容编辑能发新闻,但不能删管理员发布的公告。这种细粒度控制,前端路由根本无法穷举。这套方案把权限决策权完全交给后端:登录成功后,前端调用 /api/menu 获取当前用户可见的菜单项(JSON 格式),存入 Vuex store;侧边栏组件 Sidebar.vue 直接遍历 this.$store.state.user.menus 渲染链接;而每个业务页面(如 UserList.vue)在 mounted 钩子中调用 /api/user/list,后端根据 token 解析出用户身份,只返回该用户有权查看的数据。这样做的好处是:后端改个权限配置,前端无需发版;前端代码永远只做一件事:把后端给的数据,用最直观的方式呈现出来。我曾在一个政务项目里遇到过极端案例:同一套前端代码,通过切换后端环境变量,同时支撑“市民端”(仅查看办事指南)和“政务人员端”(全流程审批),菜单和接口权限完全由后端动态下发——这套架构天然适配这种场景。

2.2 目录结构深意:src 下的每一层,都是为协作效率而生

看懂目录结构,等于读懂了团队协作的潜规则。src 目录不是随意划分的,它是按“职责边界”而非“技术类型”组织的:

  • views/:存放页面级组件,即路由直接指向的 .vue 文件。Login.vue 独立存在,因为它不依赖主框架;user-manage/ 目录下放 UserList.vueUserEdit.vue,它们共同构成用户管理的完整视图闭环。这里的关键是:每个业务模块自成目录,内部可自由组织子组件,外部只暴露一个入口文件。比如 news-manage/ 里可能有 NewsEditor.vue(富文本)、NewsCoverCropper.vue(封面裁剪)、NewsSchedule.vue(定时发布),但路由只认 NewsList.vue

  • components/:存放跨模块复用的原子组件。TableBase.vue 封装了通用表格(带分页、排序、loading);FormBase.vue 提供表单验证基础逻辑;UploadImage.vue 统一处理图片上传和预览。它们不包含业务逻辑,只提供 UI 和交互骨架。我在实际项目中发现,新手常犯的错误是把 UserEdit.vue 里的表单验证逻辑直接写死在组件里,导致 ProductEdit.vue 得重写一遍。而用 FormBase.vue,只需传入 rulesmodel 对象,验证逻辑复用率瞬间提升 80%。

  • store/:Vuex 状态管理。index.js 是入口,modules/user.js 管理用户相关状态(token、userInfo、menus),modules/common.js 存放全局 loading 状态、通知消息等。这里有个隐藏技巧:user.js 中的 setMenus mutation 不只是存数组,它会递归遍历菜单数据,为每个菜单项生成唯一的 routeName(如 UserManage),并确保 path 符合路由配置。这样,侧边栏点击菜单时,this.$router.push({ name: item.routeName }) 就能精准跳转,避免硬编码路径带来的维护风险。

  • router/:路由相关。index.js 是 Vue Router 实例创建处,routerConfig.js 是纯静态路由定义,permission.js 是权限守卫(只做登录态检查)。把路由配置单独抽离,是为了方便后端同事阅读——他不需要懂 Vue,只要看 routerConfig.js 就知道前端需要哪些 API 接口来填充菜单。

  • assets/:静态资源。logo.png 是网站图标,login.jpg 是登录页背景图,styles/ 下放 base.scss(重置样式)、variables.scss(颜色/字体变量)、mixins.scss(常用 SCSS 混入)。所有业务组件的样式都应 @import '@/assets/styles/variables.scss',而不是写死 #409EFF。我见过太多项目,因为设计师临时改品牌色,前端要 grep 全局改几十个颜色值;而用变量,改一处,全站生效。

  • until/(注意是 until 不是 util):工具函数。request.js 封装 axios 实例,auth.js 处理 token 存储(localStorage)和过期判断,validate.js 提供手机号、邮箱等正则验证。这里强调一点:request.js 中的 baseURL 默认为空字符串,强制你在 vue.config.js 中通过 process.env.VUE_APP_BASE_API 注入。这样,开发、测试、生产环境就能用不同域名,无需改代码——这是企业级项目的标配,不是可选项。

这套结构的价值,在于它把“谁该改什么文件”这件事,变成了一个清晰的契约。后端说“菜单接口返回字段变了”,前端只需改 store/modules/user.js 里的 setMenus 逻辑;设计师说“登录页按钮圆角要加大”,你只动 assets/styles/login.scss;产品经理说“新闻列表要加搜索框”,你只在 views/news-manage/NewsList.vue 里加组件和方法。没有模糊地带,没有“这个功能该放哪”的争论。

3. 核心模块实现细节与实操要点

3.1 登录模块:从密码输入到菜单渲染的完整链路

登录页 Login.vue 看似简单,实则是整个后台的信任起点。它的核心逻辑不是“提交表单”,而是“建立可信会话”。我们拆解从用户输入到进入首页的每一步:

第一步:表单验证与防抖

<template>
  <el-form :model="loginForm" :rules="rules" ref="loginFormRef">
    <el-form-item prop="username">
      <el-input v-model="loginForm.username" placeholder="请输入用户名"></el-input>
    </el-form-item>
    <el-form-item prop="password">
      <el-input type="password" v-model="loginForm.password" placeholder="请输入密码"></el-input>
    </el-form-item>
    <el-button @click="handleLogin" :loading="loading">登录</el-button>
  </el-form>
</template>

<script>
import { mapActions } from 'vuex'
import { login } from '@/api/auth'

export default {
  data() {
    return {
      loginForm: { username: '', password: '' },
      rules: {
        username: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],
        password: [{ required: true, message: '密码不能为空', trigger: 'blur' }]
      },
      loading: false,
      // 防抖:防止用户狂点登录按钮
      loginTimer: null
    }
  },
  methods: {
    ...mapActions(['setToken', 'setUserInfo', 'setMenus']),
    handleLogin() {
      if (this.loginTimer) return // 防抖
      this.$refs.loginFormRef.validate(valid => {
        if (valid) {
          this.loading = true
          this.loginTimer = setTimeout(() => {
            login(this.loginForm).then(res => {
              // res.data 结构示例:{ token: 'xxx', userInfo: { id: 1, name: '张三' }, menus: [...] }
              this.setToken(res.data.token)
              this.setUserInfo(res.data.userInfo)
              this.setMenus(res.data.menus)
              this.$router.push({ name: 'Dashboard' })
            }).catch(err => {
              this.$message.error(err.response?.data?.message || '登录失败')
            }).finally(() => {
              this.loading = false
              clearTimeout(this.loginTimer)
              this.loginTimer = null
            })
          }, 300) // 300ms 防抖
        }
      })
    }
  }
}
</script>

提示:loginTimer 防抖是必须的。我曾在线上环境见过因网络延迟,用户连续点击导致后端收到 5 个重复登录请求,触发风控系统封禁 IP。300ms 是经验值,既不影响体验,又能有效拦截。

第二步:Token 存储与拦截器注入
src/until/request.js 中的 axios 实例配置是关键:

import axios from 'axios'
import { getToken } from './auth'

const request = axios.create({
  baseURL: process.env.VUE_APP_BASE_API || '',
  timeout: 10000
})

// 请求拦截器:自动注入 token
request.interceptors.request.use(config => {
  const token = getToken()
  if (token) {
    config.headers.Authorization = `Bearer ${token}` // 注意 Bearer 前缀
  }
  return config
}, error => Promise.reject(error))

// 响应拦截器:统一处理 401
request.interceptors.response.use(response => response, error => {
  if (error.response?.status === 401) {
    // 清除本地 token 和用户信息
    localStorage.removeItem('token')
    localStorage.removeItem('userInfo')
    localStorage.removeItem('menus')
    // 跳转登录页,并记录原页面(用于登录后返回)
    const redirect = encodeURIComponent(window.location.pathname + window.location.search)
    window.location.href = `/login?redirect=${redirect}`
  }
  return Promise.reject(error)
})

注意:Authorization 头的格式必须是 Bearer xxx,不是 Token xxxxxx。后端接口文档里写的 Authorization: Bearer <token>,前端就必须严格匹配,否则 401 错误会让你排查半小时。

第三步:菜单动态渲染与路由跳转
MainBox.vue 是主框架,它通过 computed 从 store 获取菜单:

<template>
  <div class="main-box">
    <Sidebar :menus="menus" />
    <div class="main-content">
      <HeaderBar />
      <div class="content-wrapper">
        <!-- keep-alive 缓存页面状态 -->
        <keep-alive :include="cachedViews">
          <router-view />
        </keep-alive>
      </div>
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex'
import Sidebar from '@/components/Sidebar.vue'
import HeaderBar from '@/components/HeaderBar.vue'

export default {
  components: { Sidebar, HeaderBar },
  computed: {
    ...mapState(['menus']),
    // 缓存需要 keep-alive 的视图名称
    cachedViews() {
      return ['Dashboard', 'NewsList', 'ProductList']
    }
  }
}
</script>

Sidebar.vue 渲染逻辑:

<template>
  <el-menu :default-active="$route.name" router>
    <template v-for="menu in menus">
      <el-sub-menu v-if="menu.children && menu.children.length" :key="menu.id" :index="menu.path">
        <template #title>
          <span>{{ menu.title }}</span>
        </template>
        <el-menu-item v-for="child in menu.children" :key="child.id" :index="child.name">
          {{ child.title }}
        </el-menu-item>
      </el-sub-menu>
      <el-menu-item v-else :key="menu.id" :index="menu.name">
        {{ menu.title }}
      </el-menu-item>
    </template>
  </el-menu>
</template>

这里的关键是 :index="menu.name"menu.name 来自后端返回的菜单 JSON,它必须与 routerConfig.js 中定义的 name 字段完全一致(如 UserManage)。这样,点击菜单时,el-menurouter 模式会自动调用 this.$router.push({ name: 'UserManage' }),实现精准跳转。如果后端返回的是 user_manage,而路由配置是 UserManage,就会跳转失败——这是线上最常见的 404 原因之一。

3.2 用户管理模块:如何设计可扩展的 CRUD 表格

user-manage/UserList.vue 是用户管理的入口,它不是一个简单的表格,而是一个可配置的数据操作平台。核心在于 TableBase.vue 的封装:

<!-- src/components/TableBase.vue -->
<template>
  <div class="table-base">
    <el-table :data="tableData" :loading="loading" @sort-change="handleSortChange">
      <el-table-column v-for="col in columns" :key="col.prop" :prop="col.prop" :label="col.label" :width="col.width" :sortable="col.sortable">
        <template #default="{ row }">
          <slot :name="col.prop" :row="row">{{ row[col.prop] }}</slot>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="180">
        <template #default="{ row }">
          <slot name="operation" :row="row"></slot>
        </template>
      </el-table-column>
    </el-table>
    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="pageParams.page"
      :page-sizes="[10, 20, 50]"
      :page-size="pageParams.limit"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total">
    </el-pagination>
  </div>
</template>

<script>
export default {
  props: {
    tableData: { type: Array, default: () => [] },
    loading: Boolean,
    columns: { type: Array, default: () => [] },
    total: Number,
    pageParams: { type: Object, default: () => ({ page: 1, limit: 10 }) }
  },
  methods: {
    handleSizeChange(limit) {
      this.$emit('update:pageParams', { ...this.pageParams, limit })
      this.$emit('change')
    },
    handleCurrentChange(page) {
      this.$emit('update:pageParams', { ...this.pageParams, page })
      this.$emit('change')
    },
    handleSortChange({ column, prop, order }) {
      if (order) {
        this.$emit('sort', { prop, order })
      }
    }
  }
}
</script>

UserList.vue 使用它:

<template>
  <div class="user-list">
    <TableBase
      :table-data="users"
      :loading="loading"
      :columns="columns"
      :total="total"
      :page-params.sync="pageParams"
      @change="fetchUsers"
      @sort="handleSort"
    >
      <!-- 自定义用户名列 -->
      <template #username="{ row }">
        <span class="user-name">{{ row.username }}</span>
      </template>
      <!-- 自定义状态列 -->
      <template #status="{ row }">
        <el-tag :type="row.status === 1 ? 'success' : 'info'">
          {{ row.status === 1 ? '启用' : '禁用' }}
        </el-tag>
      </template>
      <!-- 自定义操作列 -->
      <template #operation="{ row }">
        <el-button size="mini" @click="handleEdit(row)">编辑</el-button>
        <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
      </template>
    </TableBase>
  </div>
</template>

<script>
import TableBase from '@/components/TableBase.vue'
import { getUserList } from '@/api/user'

export default {
  components: { TableBase },
  data() {
    return {
      users: [],
      loading: false,
      total: 0,
      pageParams: { page: 1, limit: 10 },
      columns: [
        { prop: 'username', label: '用户名', width: '120' },
        { prop: 'email', label: '邮箱', width: '180' },
        { prop: 'status', label: '状态', width: '100', sortable: 'custom' },
        { prop: 'createdAt', label: '创建时间', width: '160', sortable: 'custom' }
      ]
    }
  },
  mounted() {
    this.fetchUsers()
  },
  methods: {
    fetchUsers() {
      this.loading = true
      getUserList(this.pageParams).then(res => {
        this.users = res.data.list
        this.total = res.data.total
      }).finally(() => {
        this.loading = false
      })
    },
    handleSort({ prop, order }) {
      // 将排序参数加入 pageParams,下次请求带上
      this.pageParams.sortProp = prop
      this.pageParams.sortOrder = order
      this.fetchUsers()
    }
  }
}
</script>

实操心得:TableBase.vue@sort 事件设计是精髓。它不直接调用 API,而是把排序参数(proporder)存入 pageParams,再触发 @change 重新拉取数据。这样,分页、筛选、排序三个维度的状态完全耦合在 pageParams 对象里,fetchUsers() 方法只需把整个对象作为参数传给 API,逻辑极度清晰。我在一个电商后台项目里,把这套模式复制到商品列表、订单列表、促销活动列表,代码复用率高达 90%,维护成本直线下降。

3.3 新闻管理模块:富文本编辑与定时发布的工程化实现

news-manage/NewsList.vueNewsEdit.vue 构成新闻管理闭环。难点不在编辑器本身,而在如何让富文本内容安全、可控地进入生产环境。

第一步:富文本编辑器选型与集成
源码选用 wangEditor(轻量、中文文档友好),而非 tinymcequillNewsEdit.vue 中的集成逻辑:

<template>
  <div class="news-edit">
    <el-form :model="form" :rules="rules" ref="formRef">
      <el-form-item label="标题" prop="title">
        <el-input v-model="form.title" maxlength="100"></el-input>
      </el-form-item>
      <el-form-item label="封面图" prop="cover">
        <UploadImage v-model="form.cover" :limit="1" />
      </el-form-item>
      <el-form-item label="内容" prop="content">
        <div id="editor" style="min-height: 400px;"></div>
      </el-form-item>
      <el-form-item label="发布时间" prop="publishTime">
        <el-date-picker
          v-model="form.publishTime"
          type="datetime"
          placeholder="选择发布时间"
          value-format="yyyy-MM-dd HH:mm:ss"
        ></el-date-picker>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleSubmit">保存</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
import E from 'wangeditor'
import UploadImage from '@/components/UploadImage.vue'

export default {
  components: { UploadImage },
  data() {
    return {
      form: {
        title: '',
        cover: '',
        content: '',
        publishTime: ''
      },
      rules: {
        title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
        cover: [{ required: true, message: '请上传封面图', trigger: 'change' }],
        content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
      },
      editor: null
    }
  },
  mounted() {
    this.initEditor()
  },
  beforeDestroy() {
    if (this.editor) {
      this.editor.destroy()
      this.editor = null
    }
  },
  methods: {
    initEditor() {
      this.editor = new E('#editor')
      // 配置只允许上传图片,禁止插入视频、音频等
      this.editor.config.uploadImgShowBase64 = false
      this.editor.config.uploadImgServer = '/api/upload/image' // 后端图片上传接口
      this.editor.config.uploadFileName = 'file'
      this.editor.config.uploadImgHooks = {
        customInsert: (insertImg, result, editor) => {
          // 后端返回 { url: 'https://xxx.com/xxx.jpg' }
          insertImg(result.url)
        }
      }
      this.editor.config.zIndex = 10000 // 避免被 el-dialog 遮挡
      this.editor.create()
      // 将编辑器内容双向绑定到 form.content
      this.editor.txt.onchange = () => {
        this.form.content = this.editor.txt.html()
      }
    },
    handleSubmit() {
      this.$refs.formRef.validate(valid => {
        if (valid) {
          // 发送 form 数据到后端
          this.$message.success('保存成功')
        }
      })
    }
  }
}
</script>

注意:editor.txt.onchange 是关键。wangEditortxt.html() 方法返回的是 HTML 字符串,它包含了 <p><img> 等标签。后端必须能安全解析和存储这段 HTML(通常用 DOMPurify 库过滤 XSS),前端不能做任何 HTML 渲染,所有展示都应由后端返回纯净的 HTML 片段。我曾在一个政府项目里,因前端直接 v-html 渲染用户输入的富文本,导致 XSS 漏洞被通报——根源就是前端越权做了本该由后端负责的安全过滤。

第二步:定时发布机制
publishTime 字段的设计,决定了新闻能否真正“定时”。NewsList.vue 中的列表需要区分“已发布”和“待发布”:

// 在 fetchNewsList 方法中
getNewsList(params).then(res => {
  this.newsList = res.data.list.map(item => ({
    ...item,
    // 计算状态:当前时间 > publishTime 且 status === 1 为已发布
    displayStatus: new Date() > new Date(item.publishTime) && item.status === 1 ? '已发布' : '待发布'
  }))
})

后端需有一个定时任务(如 Linux cron 或 Node.js 的 node-schedule),每分钟扫描 publishTime <= NOW() AND status = 0 的新闻,将其 status 更新为 1。前端只需在列表页加一列“状态”,并提供“立即发布”按钮(调用 /api/news/publish-now?id=xxx 接口)。这样,定时发布就变成了一个前后端协同的简单状态机,而非前端复杂的倒计时逻辑。

4. 本地运行与二次开发全流程详解

4.1 从零启动:5 分钟完成本地环境搭建

拿到源码包,不要急着 npm install。先做三件事:

第一步:确认 Node.js 版本
Vue 2 生态对 Node.js 版本敏感。package.json 中的 engines 字段写着 "node": ">=8.9.0",但实测 Node.js 14.x 最稳定。用 nvm 切换版本:

# 查看已安装版本
nvm list
# 切换到 14.21.3(LTS)
nvm use 14.21.3
# 验证
node -v # 应输出 v14.21.3
npm -v  # 应输出 6.14.18

提示:如果用 Node.js 16+vue-cli-service 可能报错 ERR_OSSL_EVP_UNSUPPORTED。这不是源码问题,是 OpenSSL 版本兼容性问题。强行降级 Node.js 比折腾 OpenSSL 配置更高效。

第二步:安装依赖与环境变量配置

# 进入项目根目录
cd your-project-folder

# 查看 package.json,确认包管理器
cat package.json | grep "packageManager"

# 如果是 yarn(推荐,因 yarn.lock 已存在)
yarn install

# 如果是 npm
npm install

# 创建 .env.development 文件(开发环境)
echo "VUE_APP_BASE_API=http://localhost:3000/api" > .env.development

# 创建 .env.production 文件(生产环境)
echo "VUE_APP_BASE_API=https://your-domain.com/api" > .env.production

这里的关键是 VUE_APP_BASE_API。Vue CLI 会自动将 .env.* 文件中以 VUE_APP_ 开头的变量注入到 process.envvue.config.js 中的 devServer.proxy 只用于开发时跨域,真正的 API 地址由这个变量决定。我见过太多人把 devServer.proxy 当成生产配置,结果上线后所有请求都 404——因为 proxy 只在 webpack-dev-server 中生效,打包后的静态文件里根本没有代理逻辑。

第三步:启动开发服务器

# 启动
yarn serve
# 或
npm run serve

# 浏览器打开 http://localhost:8080
# 此时会看到空白登录页,因为后端还没启动

此时页面是白的,别慌。打开浏览器开发者工具,看 Network 标签页,你会看到 login 请求返回 404 或 500。这说明前端已正常运行,下一步是对接后端。

4.2 对接后端 API:从登录接口开始的逐层打通

假设你的后端是 Express,提供了以下接口:
- POST /api/login → 返回 { token, userInfo, menus }
- GET /api/menu → 返回菜单 JSON 数组
- GET /api/user/list?page=1&limit=10 → 返回用户列表
- POST /api/user/create → 创建用户

第一步:修改 API 请求地址
打开 src/api/auth.js

// src/api/auth.js
import request from '@/until/request'

// 修改 login 接口地址
export function login(data) {
  return request({
    url: '/login', // 注意:这里只是路径,baseURL 由 .env.* 决定
    method: 'post',
    data
  })
}

request 实例的 baseURL 已在 src/until/request.js 中配置为 process.env.VUE_APP_BASE_API,所以实际请求 URL 是 http://localhost:3000/api/login

第二步:调试登录流程
Login.vuehandleLogin 方法中加断点:

login(this.loginForm).then(res => {
  console.log('登录响应:', res) // 在这里打断点
  this.setToken(res.data.token)
  // ...
})

运行 yarn serve,在登录页输入账号密码,提交。看控制台 res 数据结构是否符合预期:

{
  "code": 200,
  "message": "success",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "userInfo": { "id": 1, "username": "admin", "role": "admin" },
    "menus": [
      { "id": 1, "title": "用户管理", "path": "/user-manage", "name": "UserManage", "children": [] },
      { "id": 2, "title": "新闻管理", "path": "/news-manage", "name": "NewsManage", "children": [] }
    ]
  }
}

如果 res.data.menus 是空数组,检查后端 /api/menu 接口是否返回了正确的 JSON;如果 res.data.tokenundefined,检查后端是否把 token 放在了 data.token 字段,而不是 data.jwt_token 或其他字段——前后端约定的字段名,比任何框架都重要

第三步:逐模块验证
登录成功后,依次访问 /user-manage/news-manage/product-manage
- UserList.vue 中的 getUserList 请求,看 Network 是否返回了 listtotal
- NewsList.vue 中的 getNewsList 请求,看返回的 content 字段是否是 HTML 字符串;
- ProductList.vue 中的图片是否能正常显示(检查 cover 字段的 URL 是否可访问)。

每一步都用 console.log 输出响应数据,确认结构无误后再进行下一步。不要试图一次性打通所有模块,这是新手最容易陷入的“全盘崩溃”陷阱。

4.3 二次开发避坑指南:那些文档里不会写的实战经验

坑一:Element UI 版本与图标缺失

源码使用 element-ui@2.15.14,但如果你升级到 2.15.15+<i class="el-icon-edit"></i> 可能不显示图标。原因是新版本移除了部分图标类名。解决方案:
- 方案 A(推荐):回退到 2.15.14yarn add element-ui@2.15.14
- 方案 B:改用 SVG 图标,<el-icon><edit /></el-icon>,需额外引入 @element-plus/icons-vue

坑二:富文本图片上传跨域

wangEditor 的图片上传默认走 XMLHttpRequest,如果后端没配置 CORS,会报错。在 src/until/request.js 中添加:

// 在 request.create 配置中加入
withCredentials: true // 允许携带 cookie

并在后端响应头中添加:

// Express 示例
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:8080')
  res.header('Access-Control-Allow-Credentials', true)
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  next()
})
坑三:生产环境路由 404

vue.config.js 中的 publicPath 必须与部署路径一致:

// vue.config.js
module.exports = {
  publicPath: process.env.NODE_ENV === 'production' ? '/admin/' : '/'
}

如果部署到 https://your-domain.com/admin/publicPath 必须是 /admin/;如果部署到根目录 https://your-domain.com/,则必须是 /。否则 index.html 加载的 js/chunk-vendors.xxx.js 路径会错,导致白屏。

坑四:Vuex 持久化丢失

localStorage 存储的 token 在浏览器隐私模式下可能被禁用。src/until/auth.js 中的 getToken 方法应增加容错:

export function getToken() {
  try {
    return localStorage.getItem('token') || ''
  } catch (e) {
    console.warn('localStorage access denied, using memory storage')
    return ''
  }
}
坑五:IE11 兼容性问题

Vue 2 官方已停止 IE11 支持,但企业客户常要求兼容。在 babel.config.js 中确保:

module.exports = {
  presets: [
    ['@vue/app', {
      targets: {
        ie: '11' // 明确指定 IE11
      }
    }]
  ]
}

并安装 core-js@2(非 core-js@3):

yarn add core-js@2

main.js 顶部添加:

import 'core-js/stable'
import 'regenerator-runtime/runtime'

5. 常见问题与排查技巧实录

5.1 登录后无限重定向到登录页

现象:输入账号密码,登录成功,跳转 /,但立刻又被重定向回 /login,形成循环。

排查步骤
1. 打开浏览器开发者工具,Application 标签页,查看 localStorage 中是否有 token。如果没有,说明 setToken 没执行成功,检查 Login.vuesetToken(res.data.token)res.data.token 是否为 undefined
2. 如果有 token,检查 router/index.js 中的路由守卫:
javascript router.beforeEach((to, from, next) => { const token = getToken() if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })
确保 getToken() 返回的确实是字符串,而不是 null''
3. 最隐蔽的原因:MainBox.vuerouter-view 渲染的是 Dashboard.vue,但 Dashboard.vue 的路由配置中 meta.requiresAuthfalse。检查 routerConfig.js,确保所有子路由都继承父路由的 requiresAuth 属性,或显式声明。

速查表

现象 可能原因 解决方案
登录后立即跳回登录页 localStorage 未写入 token Login.vuethen 回调中 console.log(res),确认 res.data.token 存在
登录后跳转到 / 但显示 404 MainBox.vue 未正确渲染 router-view 检查 App.vue<router-view /> 是否被包裹在 <keep-alive> 外,确保 MainBox.vuerouter-view 的直接子组件
登录成功但菜单不显示 setMenus 未触发或数据格式错误 store/modules/user.jssetMenus mutation 中加 console.log('menus:', menus),确认传入的是数组

5.2 表格数据为空,Network 显示请求成功但 list 字段不存在

现象UserList.vue 页面一片空白,Network 中 /api/user/list 返回 200,但响应体是 { code: 200, data: {} },没有 list 字段。

根本原因:后端接口返回结构与前端期望不一致。前端 getUserList 方法默认解析 res.data.list,而后端返回的是 res.data.usersres.result

解决方案
1. 在 src/api/user.js 中修改响应处理:
javascript export function getUserList(params) { return request({ url: '/user/list', method: 'get', params }).then(res => { // 兼容多种后端返回结构 if (res.data.list) return res if (res.data.users) return { ...res, data: { list: res.data.users, total: res.data.total } } if (res.result) return { ...res, data: res.result } throw new Error('Invalid API response structure') }) }
2. 更彻底的做法:与后端约定统一返回结构,如所有接口都返回 { code, message, data: { list, total } }。这是长期维护的基石。

5.3 富文本编辑器无法上传图片,控制台报 400 错误

现象:点击编辑器的图片按钮,选择图片后,Network 中 POST /api/upload/image 返回 400,响应体是 { "message": "file is required" }

原因分析wangEditor 默认上传字段名为 file,但后端可能期望 imageuploadFile

解决方法
1. 查看后端接口文档,确认上传字段名。
2. 在 NewsEdit.vueinitEditor 方法中修改:
javascript this.editor.config.uploadFileName = 'image' // 改为后端期望的字段名

5.4 生产环境打包后,静态资源 404

现象yarn build 后,将 dist 目录部署到 Nginx,访问 https://domain.com/admin/,页面白屏,Network 中 js/app.xxx.js 返回 404。

原因vue.config.js 中的 publicPath 配置错误。如果部署路径是 /admin/publicPath 必须是 /admin/;如果部署到根目录,必须是 /

验证方法
1. 打开 dist/index.html,搜索 js/app.,看 script 标签的 src/js/app.xxx.js 还是 /admin/js/app.xxx.js
2. 如果是前者,但你部署在 /admin/ 下,则必然 404。

修复

// vue.config.js
module.exports = {
  publicPath: process.env.NODE_ENV === 'production' ? '/admin/' : '/'
}

然后重新 yarn build

5.5 侧边栏菜单点击无反应,$router.push 不生效

现象Sidebar.vue 渲染出菜单项,但点击后 URL 不变,页面不跳转。

排查重点
1. 检查 el-menu:index 属性是否绑定正确。<el-menu-item :index="menu.name"> 中的 menu.name 必须与 routerConfig.js 中的 name 字段完全一致(大小写、下划线、连字符)。
2. 检查 el-menu 是否设置了 router 属性。没有 router 属性,el-menu 只是普通菜单,不会触发路由跳转。
3. 检查 routerConfig.js 中的 children 路由是否正确嵌套在 Home 下。如果 UserManage 路由写在顶层,el-menurouter 模式无法识别。

终极调试法
Sidebar.vue@select 事件中加日志:

<el-menu :default-active="$route.name" router @select="handleSelect">
...
</el-menu>
methods: {
  handleSelect(index) {
    console.log('Menu clicked, index:', index)
    this.$router.push({ name: index })
  }
}

如果 console.log 输出了正确的 index,但 $router.push 无反应,说明路由配置有问题;如果没输出,说明 el-menu@select 事件没绑定成功。

我在实际项目中最常遇到的,是后端返回的菜单 name 字段里混入了空格或中文,比如 "name": "用户管理",而路由配置是 "name": "UserManage"。这种低级错误,往往要花半小时才定位到——所以,永远相信后端返回的数据,但永远用 console.log 验证它。

6. 项目后续演进与能力扩展建议

这套 Vue 2 后台源码不是终点,而是企业数字化建设的起点。基于它,你可以平滑升级,而不必推倒重来。

6.1 权限体系升级:从菜单级到按钮级

当前权限只控制菜单可见性,但企业常需更细粒度控制。例如,财务人员能看到“导出报表”按钮,而普通员工看不到。实现方案:
- 后端 /api/menu 接口扩展 buttons 字段,返回当前用户在每个菜单下的可用按钮(如 [{ "code": "user_export", "name": "导出" }])。
- 前端在 store/modules/user.js 中新增 buttons state,setButtons mutation。
- 创建指令 v-permission
javascript // src/directives/permission.js export default { inserted(el, binding) { const buttons = store.state.user.buttons const code = binding.value if (!buttons.some(btn => btn.code === code)) { el.parentNode && el.parentNode.removeChild(el) } } }
在模板中使用 <el-button v-permission="'user_export'">导出</el-button>

6.2 多语言支持:从单语言到全球化

企业官网常需中英文切换。利用 Vue I18n 插件:
1. 安装 vue-i18n@8.x(Vue 2 兼容版)。
2. 创建 src/lang/zh.jssrc/lang/en.js,定义键值对。
3. 在 main.js 中挂载:
```javascript
import VueI18n from ‘vue-i18n’
import zh from ‘./lang/zh’
import en from ‘./lang/en’

Vue.use(VueI18n)
const i18n = new VueI18n({
locale: ‘zh’,
messages: { zh, en }
})
new Vue({ i18n, render: h => h(App) }).$mount(‘#app’)
`` 4. 在HeaderBar.vue中添加语言切换按钮,调用this.$i18n.locale = ‘en’`。

6.3 微前端改造:从单体到模块化

当企业有多个独立系统(CRM、ERP、BI),可将此后台作为基座,接入其他系统的微应用。使用 qiankun
- 主应用(此后台)作为 container,在 MainBox.vue 中预留 <router-view> 之外的插槽。
- 子应用(如 CRM)打包为独立 HTML,通过 registerMicroApps 注册。
- 路由前缀隔离,如 /crm/* 交由 CRM 子应用处理。

这套源码的模块化目录结构(views/user-manage/)天然适配微前端——每个业务模块就是一个可独立部署的微应用候选。

6.4 Vue 3 迁移路径:渐进式升级而非重写

Vue 2 生命周期钩子(beforeCreate, created, mounted)与 Vue 3 的 Composition API 并不冲突。迁移策略:
- 第一步:在 UserList.vue 中引入 setup() 函数,将 datamethods 逻辑逐步迁移到 refonMounted 中。
- 第二步:使用 @vue/composition-api 插件(Vue 2 兼容版),无需改动现有 Options API。
- 第三步:当 80% 组件完成迁移,再升级 Vue 3 核心库。

我主导的一个政务项目,就是用此策略,3 个月完成 200+ 个组件的平滑迁移,零 downtime。

最后分享一个小技巧:每次 git commit 前,运行 yarn lint。源码中已配置 eslint-plugin-vue,它会检查 v-for 是否有 keyv-model 是否绑定到 data 属性、this.$router.push 是否用了 name 而非 path。这些看似琐碎的规则,恰恰是大型项目不出低级错误的保障。代码可以重构,但规范一旦松懈,技术债就会像雪球一样越滚越大。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套基于Vue 2构建的企业官网后台前端源码,开箱即用,无需从零搭建基础架构。包含完整的登录页(Login.vue)、主布局框架(App.vue、MainBox.vue)、路由配置(routerConfig.js)、Vuex状态管理(store)、统一HTTP请求封装(axios.config.js),以及用户管理、新闻管理、产品管理三大核心业务模块,每个模块均提供独立视图与组件结构。项目目录组织清晰,src下按功能划分views(页面)、components(复用组件)、router(路由)、store(状态)、assets(静态资源如logo.png、login.jpg)等;public存放index.html和favicon.ico;配套babel.config.js、jsconfig.、.browserslistrc、package.及yarn.lock等标准构建配置,兼容主流Vue 2开发环境与本地运行调试。支持快速对接后端API,适合用于企业官网类项目的后台快速落地或二次开发,省去路由初始化、权限占位、基础UI布局等重复工作。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐