Vue 2企业官网后台源码:含登录、用户、新闻、产品四大管理模块
简介:一套基于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')
}
]
}
]
注意两点:第一,所有业务路由都放在 Home 的 children 下,由 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.vue、UserEdit.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,只需传入rules和model对象,验证逻辑复用率瞬间提升 80%。 -
store/:Vuex 状态管理。index.js是入口,modules/user.js管理用户相关状态(token、userInfo、menus),modules/common.js存放全局 loading 状态、通知消息等。这里有个隐藏技巧:user.js中的setMenusmutation 不只是存数组,它会递归遍历菜单数据,为每个菜单项生成唯一的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 xxx或xxx。后端接口文档里写的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-menu 的 router 模式会自动调用 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,而是把排序参数(prop和order)存入pageParams,再触发@change重新拉取数据。这样,分页、筛选、排序三个维度的状态完全耦合在pageParams对象里,fetchUsers()方法只需把整个对象作为参数传给 API,逻辑极度清晰。我在一个电商后台项目里,把这套模式复制到商品列表、订单列表、促销活动列表,代码复用率高达 90%,维护成本直线下降。
3.3 新闻管理模块:富文本编辑与定时发布的工程化实现
news-manage/NewsList.vue 和 NewsEdit.vue 构成新闻管理闭环。难点不在编辑器本身,而在如何让富文本内容安全、可控地进入生产环境。
第一步:富文本编辑器选型与集成
源码选用 wangEditor(轻量、中文文档友好),而非 tinymce 或 quill。NewsEdit.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是关键。wangEditor的txt.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.env。vue.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.vue 的 handleLogin 方法中加断点:
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.token 是 undefined,检查后端是否把 token 放在了 data.token 字段,而不是 data.jwt_token 或其他字段——前后端约定的字段名,比任何框架都重要。
第三步:逐模块验证
登录成功后,依次访问 /user-manage、/news-manage、/product-manage:
- UserList.vue 中的 getUserList 请求,看 Network 是否返回了 list 和 total;
- 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.14,yarn 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.vue 中 setToken(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.vue 的 router-view 渲染的是 Dashboard.vue,但 Dashboard.vue 的路由配置中 meta.requiresAuth 是 false。检查 routerConfig.js,确保所有子路由都继承父路由的 requiresAuth 属性,或显式声明。
速查表:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 登录后立即跳回登录页 | localStorage 未写入 token |
在 Login.vue 的 then 回调中 console.log(res),确认 res.data.token 存在 |
登录后跳转到 / 但显示 404 |
MainBox.vue 未正确渲染 router-view |
检查 App.vue 中 <router-view /> 是否被包裹在 <keep-alive> 外,确保 MainBox.vue 是 router-view 的直接子组件 |
| 登录成功但菜单不显示 | setMenus 未触发或数据格式错误 |
在 store/modules/user.js 的 setMenus 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.users 或 res.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,但后端可能期望 image 或 uploadFile。
解决方法:
1. 查看后端接口文档,确认上传字段名。
2. 在 NewsEdit.vue 的 initEditor 方法中修改: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-menu 的 router 模式无法识别。
终极调试法:
在 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.js 和 src/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() 函数,将 data、methods 逻辑逐步迁移到 ref 和 onMounted 中。
- 第二步:使用 @vue/composition-api 插件(Vue 2 兼容版),无需改动现有 Options API。
- 第三步:当 80% 组件完成迁移,再升级 Vue 3 核心库。
我主导的一个政务项目,就是用此策略,3 个月完成 200+ 个组件的平滑迁移,零 downtime。
最后分享一个小技巧:每次 git commit 前,运行 yarn lint。源码中已配置 eslint-plugin-vue,它会检查 v-for 是否有 key、v-model 是否绑定到 data 属性、this.$router.push 是否用了 name 而非 path。这些看似琐碎的规则,恰恰是大型项目不出低级错误的保障。代码可以重构,但规范一旦松懈,技术债就会像雪球一样越滚越大。
简介:一套基于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布局等重复工作。
更多推荐




所有评论(0)