☰
基于C# MVC+EasyUI+ECharts的后台管理系统源码实战解析
2026/10/7 18:20:47 网站建设 项目流程

简介:这是一套采用C#与ASP.NET MVC架构、整合EasyUI前端框架和ECharts可视化库的后台管理系统完整源码,适合.NET开发者、前端工程师及需要快速搭建企业级管理后台的学习者参考。源码按BLL、DAL、Common、UI等分层组织,包含完整的MVC控制器、Razor视图与业务逻辑实现,覆盖登录认证、数据表格、图表分析等典型后台功能,可帮助理解主流框架的集成思路。

资源包共1677个文件,压缩后34.36MB,其中css、png、js等构成EasyUI界面样式和交互资源,57个cs与19个cshtml对应后端逻辑和页面结构,还包含64个dll依赖库及sql数据库脚本,整体目录清晰,便于直接对照学习。目前已有117人学习下载,适合作为从零搭建管理后台的参考案例,也可用于二次开发或课程设计。通过研究该源码,读者能掌握MVC分层设计、EasyUI组件用法以及ECharts动态数据展示的完整实践方法。

1. 一套 C# MVC 后台管理系统源码,先回答“拿来干什么”

C# 基于 MVC+EasyUI+ECharts 的后台管理系统,是一个很典型的 BS 端后台骨架:MVC 把路由和 Controller 之间的调用理顺,EasyUI 负责管理后台那一堆表格、弹窗、树形菜单,ECharts 再把统计结果画成折线、柱状和饼图。这类源码包出现在你手上的时候,通常不是让你学习,而是让你尽快改造成自家产品或者内部管理平台。适合谁用?已经能跑通 ASP.NET MVC、但对前端不熟的人最合适——页面交互由 EasyUI 兜着,图表由 ECharts 统一输出。它最大的价值在于一次性把登录、权限、菜单、数据表格和图表这些重复劳动铺好,真正的成本是拆解骨架和踩那些藏在路由、权限、序列化里的坑。这篇就顺着这套源码的落地过程来讲。

2. 拆 MVC 工程:目录结构、数据访问和配置文件,上手第一件事

2.1 先从 Areas 和 Controller 认主入口

拿到 zip 解压后,第一件事不是急着点 F5 跑起来,而是先看项目整体目录。这类基于 ASP.NET MVC 的后台源码,绝大多数会用到 Areas 机制,也就是在解决方案里出现一个Areas文件夹,下面挂Admin、Manage这类后台区域。这个设计不是随便分的,它让后台 Controller 和前台页面在路由上天然隔离,URL 里会多出一段/Admin/...,对应的AreaRegistration.cs里注册了这段路由。

我一般会先搜Login和Session这两个关键词,把程序的起点找出来。最常看到的是顶层AccountController负责登录、退出、验证码,Areas/Admin/HomeController负责后台主页跳转。看这段代码能快速搞清楚两件事:Session 里存了哪些用户信息,以及哪些 Action 是不需要登录就能访问的。免登录的 Action 一般是 Login、Logout、验证码、极少数对外开放的数据接口。这个清单决定了后续加权限过滤器时,哪些接口要放行,哪些必须拦截。

// Areas/Admin/HomeController.cs using System.Web.Mvc; namespace ManageSystem.Areas.Admin.Controllers { public class HomeController : Controller { // GET: /Admin/Home/Dashboard public ActionResult Dashboard() { // 从 Session 里取用户信息 var userId = Session["UserId"]; if (userId == null) { return RedirectToAction("Login", "Account", new { area = "" }); } ViewBag.UserName = Session["UserName"]; return View(); } } }

这段代码逻辑上没什么新奇,但它暴露了这套源码的通用约定:登录状态用 Session 管,后台页面跳转靠RedirectToAction回登录页,View 数据用 ViewBag 塞给页面。理解了这个约定,后面改权限过滤器、加免登录接口时就不会在路由上绕圈子。

对于想接手这套源码的人,我建议再留意一下 Controller 里是否大量写了业务代码。有的源码把查询、保存、权限判断全堆在 Action 里,Model 文件夹只剩数据库实体类。这种结构不优雅,但改起来并不难——先别大动干戈,等你要加新业务模块时,再按“Controller 只收参数、调服务、返回结果”的节奏抽一层出来。上来就重构是这类源码最容易翻车的开局。

2.2 数据层用 EF 还是裸 SQL,看源码你怎么判断

这类源码的数据访问层通常会选 Entity Framework,少部分用 SqlSugar 或 Dapper。判断它好不好改,不看你喜不喜欢 EF,而看它是否把“查询数据库”这件事收敛到了一个地方。最常见的姿势是 Controller 里直接var db = new DbContext(),然后就开始 LINQ。这种写法对维护不太友好,但对一个后台管理系统来说,只要不是几百个表的大项目,其实能忍。

我更推荐的做法是先找一下有没有Repository或Service目录。有,那恭喜,增删改的逻辑大概率被封装了;没有,也不要急着引入 UnitOfWork 那种重型模式,先写一个泛型仓储基类,把三个最常用的方法补上:分页查询、按条件查询、保存。

// Repositories/RepositoryBase.cs using System; using System.Data.Entity; using System.Linq; using System.Linq.Expressions; namespace ManageSystem.Repositories { public class RepositoryBase<T> where T : class { protected readonly DbContext _context; protected readonly DbSet<T> _dbSet; public RepositoryBase(DbContext context) { _context = context; _dbSet = context.Set<T>(); } // 分页查询:pageIndex 从 1 开始 public IQueryable<T> GetPage(int pageIndex, int pageSize, Expression<Func<T, bool>> where = null) { IQueryable<T> query = _dbSet; if (where != null) { query = query.Where(where); } return query.OrderBy(x => x).Skip((pageIndex - 1) * pageSize) .Take(pageSize); } // 按条件取第一条 public T FirstOrDefault(Expression<Func<T, bool>> where) { return _dbSet.FirstOrDefault(where); } // 新增或修改后统一保存 public void Save(T entity) { if (_context.Entry(entity).State == EntityState.Detached) { _dbSet.Add(entity); } _context.SaveChanges(); } } }

这里的关键是泛型约束where T : class,让仓储类能接任意实体。分页方法里Skip((pageIndex - 1) * pageSize)对应 EasyUI DataGrid 传过来的page和rows,这是后台管理里最常被改的参数,千万别写成Skip(pageIndex * pageSize),否则第一页就会丢数据。

这个仓储基类不改变原系统的 EF 上下文,只是把重复的查询代码收敛。对于那些 Controller 里到处db.Users.Where(...)的源码,你可以边加新模块边把查询替换进来,不用一次改完。注意,EF 的导航属性在这种场景下既是帮手也是坑,后面避坑章节会专门讲序列化的循环引用问题。

2.3 Web.config 四个参数,开跑前先改完

这类源码自带的 Web.config 大多不能直接跑,连接字符串是作者本机的数据库,数据库脚本也可能没自动执行。开跑前先把四个参数看清楚,能省下一个下午的排查时间。

参数位置常见问题建议
connectionStringconnectionStrings 节点服务器名、账号密码对不上先建空库,再手动执行 SQL 脚本
authentication 模式system.web 节点Forms 认证与自定义过滤器冲突保留表单认证,注意 loginUrl 路径
targetFrameworkcompilation 节点本机 4.7.2 与服务器 4.5 不一致统一,避免运行时版本报错
静态资源路径appSettings 里自定义键EasyUI 主题、上传目录写死确认是否存在绝对路径,改成相对路径

看下面这个典型的 Web.config:

<configuration> <connectionStrings> <add name="DefaultConnection" connectionString="Server=.;Database=ManageDB;User Id=sa;Password=123456;" providerName="System.Data.SqlClient" /> </connectionStrings> <appSettings> <!-- 这套源码里常见的主题配置和图表刷新间隔 --> <add key="EasyUITheme" value="gray" /> <add key="ChartRefreshInterval" value="30000" /> </appSettings> <system.web> <authentication mode="Forms"> <forms loginUrl="~/Account/Login" timeout="20" /> </authentication> <compilation targetFramework="4.7.2" /> </system.web> </configuration>

要注意的坑有三个。第一,connectionString里的Database=ManageDB必须提前在 SQL Server 里建好,EF 默认并不会自动帮你建库,除非代码里配置了Database.SetInitializer,大部分源码不会配。第二,Forms认证存在时,Ajax 请求在 Session 超时后会收到一个 302 重定向,前端 JavaScript 拿到的是登录页 HTML 而不是 JSON,这块后面避坑章专门处理。第三,compilation targetFramework必须和项目属性里的目标框架一致,否则本地调试时经常出现“未能加载文件或程序集”的玄学错误。

配置文件看完之后,下一步跑起来才能顺利。如果源码包里带了.sql脚本,我习惯先执行脚本再改连接串,而不是先连上再导数据,顺序反了容易出现外键冲突。没有 SQL 脚本的,就先让 EF 跑一次生成库,再用 Navicat 或 SSMS 对比实体表。

3. EasyUI 后端集成:分页表格、弹窗表单和菜单布局

3.1 DataGrid 分页:后端要认 page 和 rows 两个参数

EasyUI 在后台源码里的戏份,主要集中在一块:DataGrid。这个东西类似前端版的表格组件,但它的数据获取方式很固定——初始化时向后端要 JSON,JSON 必须是{ total: 100, rows: [...] }的结构,前端自动按 total 渲染分页条。搞清楚这一条,整个系统里大部分列表页就都能看懂了。

HTML 端的表格可以写在 View 里,用class="easyui-datagrid"声明组件,然后通过><table id="dgUser" class="easyui-datagrid" >public JsonResult PageList(int page = 1, int rows = 20, string keyword = "") { var query = db.Users.AsQueryable(); if (!string.IsNullOrEmpty(keyword)) { query = query.Where(u => u.UserName.Contains(keyword) || u.RealName.Contains(keyword)); } var total = query.Count(); var list = query.OrderBy(u => u.Id) .Skip((page - 1) * rows) .Take(rows) .Select(u => new { u.Id, u.UserName, u.RealName, RoleName = u.Role.RoleName, LastLoginTime = u.LastLoginTime.ToString("yyyy-MM-dd HH:mm:ss") }) .ToList(); return Json(new { total, rows = list }, JsonRequestBehavior.AllowGet); }

这套代码的核心在最后一行:JsonRequestBehavior.AllowGet必须写。EasyUI DataGrid 默认用 GET 请求拉数据,而 ASP.NET MVC 默认禁止 GET 方式返回 JSON,不加这个参数浏览器会直接报 500。很多新手第一次集成 EasyUI 翻车就是翻在这里。

另外,.Skip((page - 1) * rows).Take(rows)直接对应前端传参。也就是说,EasyUI 翻页后自动带page=2&rows=20,如果你的 Action 参数没定义成这两个名字,分页就会失效——要么永远只显示第一页,要么直接把全部数据一次性取出来。

3.2 弹窗表单增删改:form 序列化和 JsonResult 回执

后台系统最常出现的交互是“点新增弹一个窗,填完保存刷列表”。EasyUI 这边用easyui-dialog把表单包起来,提交时用$("#form1").form('submit')或者普通 Ajax 把表单序列化之后往后端丢。这块源码里最容易看出作者水平的地方,是保存成功后到底返回了什么。

一条合格的保存接口,返回的数据应当包含三个信息:是否成功、提示消息、以及可能用到的回跳参数。下面这个是常见做法:

[HttpPost] public JsonResult Save(UserDto dto) { if (!ModelState.IsValid) { return Json(new { success = false, msg = "表单校验未通过" }); } var user = db.Users.Find(dto.Id) ?? new User(); user.UserName = dto.UserName; user.RealName = dto.RealName; user.RoleId = dto.RoleId; if (user.Id == 0) { db.Users.Add(user); } db.SaveChanges(); return Json(new { success = true, msg = "保存成功" }); }

前端配合的脚本大概是:

function saveUser() { $('#ffUser').form('submit', { url: '/User/Save', onSubmit: function () { return $(this).form('validate'); }, success: function (res) { var data = JSON.parse(res); if (data.success) { $('#dlgUser').dialog('close'); $('#dgUser').datagrid('reload'); } else { $.messager.alert('提示', data.msg, 'error'); } } }); }

注意success回调里拿到的res是字符串,要先JSON.parse才能用,这是一个原生的坑。不少源码项目在这里直接判断res == "true",一旦后续改成返回 JSON 对象,旧逻辑就失效了。

另外要注意.form('submit')会自动把 form 里所有带name属性的 input 组合成 POST 数据,因此后端 DTO 的属性名得跟 input 的name保持一致。想省事的话,前端提交前还可以用$('#ffUser').serialize()在控制台打印一遍,确认字段没少没多。

3.3 Layout 与 Tab 页:后台导航如何和权限菜单挂钩

后台管理系统的页面骨架通常是一个左侧菜单、右侧 Tab 的布局。EasyUI 用layout加tabs实现,左侧是accordion菜单,点击菜单项时往右侧 tabs 里追加或选中一个 Tab。

<div id="layoutMain" class="easyui-layout" style="width:100%;height:100%;"> <div>public JsonResult OrderTrend() { var start = DateTime.Now.AddDays(-7); var data = db.Orders .Where(o => o.CreateTime >= start) .GroupBy(o => DbFunctions.TruncateTime(o.CreateTime)) .Select(g => new { date = g.Key.Value.ToString("MM-dd"), count = g.Count() }) .ToList(); return Json(data, JsonRequestBehavior.AllowGet); }

这个接口返回的是一组{ date: "09-12", count: 128 }的对象数组。前端拿到后,用data.map(d => d.date)取 X 轴标签,data.map(d => d.count)取 Y 轴数值,可以直接喂给 ECharts。注意这里用了DbFunctions.TruncateTime去掉时间部分,只保留日期,这是 EF 里按天分组的常用姿势,直接GroupBy(o => o.CreateTime.Date)在某些 EF 版本会报“无法翻译”的错误。

4.2 饼图和柱状图的初始化写法

ECharts 在这套源码里的用法比较固定:页面里放一个div,在脚本里echarts.init,然后setOption。饼图和柱状图几乎是后台报表里出现频率最高的两种,写法上只有 series 配置不同。

先看柱状图的完整示例:

$.getJSON('/Report/OrderTrend', function (data) { var chart = echarts.init(document.getElementById('trendChart')); chart.setOption({ title: { text: '近7日订单量' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.map(d => d.date) }, yAxis: { type: 'value' }, series: [{ name: '订单数', type: 'bar', barWidth: 24, itemStyle: { color: '#409EFF' }, data: data.map(d => d.count) }] }); window.addEventListener('resize', function () { chart.resize(); }); });

这段代码里有几个细节值得留意。第一,chart.resize()要挂在window的 resize 事件上,否则浏览器窗口变大小时图表会溢出容器。第二,barWidth: 24是柱子的固定宽度,数据量少时不设置也能自动算,但数据跨度大时固定宽度更稳定。第三,itemStyle.color可以是固定颜色,也可以是渐变色对象,后者在项目汇报页面上更出效果。

饼图的差异集中在 series 的 type 上,而且数据格式和柱状图不同,需要{ name: '分类', value: 20 }这种结构:

$.getJSON('/Report/RoleDistribution', function (data) { var chart = echarts.init(document.getElementById('pieChart')); chart.setOption({ title: { text: '用户角色分布' }, tooltip: { trigger: 'item' }, series: [{ name: '用户数', type: 'pie', radius: ['40%', '70%'], label: { formatter: '{b}: {d}%' }, data: data }] }); });

饼图这里最容易问的问题有两个。第一,radius: ['40%', '70%']表示环形图,内半径 40%,外半径 70%,如果只想做实心饼图,直接写radius: '70%'。第二,label.formatter: '{b}: {d}%'让每个扇区显示名称和百分比,这是格式占比场景最常用的写法。ECharts 的富文本提示框能力很强,如果你想把悬浮提示做更细致,可以在 tooltip 的 formatter 里拼 HTML,这也是这类源码里能快速出彩的位置。

4.3 自适应尺寸与轮询刷新两个容易忽略的点

图表放到后台首页后,两个实际问题马上会找上你。一是图表容器初始化时不可见,二是数据不自动更新。

容器不可见的问题常见于 Tab 页。EasyUI 的 Tab 在切换前,内部 div 可能是隐藏状态,此时调用echarts.init拿到的宽度是 0,等切换到该 Tab 时图表就变得很窄或者不显示。解决套路是在 Tab 激活事件里手动调用一次chart.resize():

$('#tabsMain').tabs({ onSelect: function (title, index) { var charts = window.__chartsList || []; for (var i = 0; i < charts.length; i++) { charts[i].resize(); } } });

把初始化好的 chart 实例收敛到一个数组里,统一在 Tab 切换时 resize,效果最稳。

轮询刷新则取决于业务需求。实时性要求高的,用setInterval每隔一定时间重新拉一遍数据,然后chart.setOption(option)覆盖旧数据。ECharts 的 setOption 默认是合并模式,如果新数据和旧数据字段一致,直接把新的 data 数组替换进去就行。

setInterval(function () { $.getJSON('/Report/OrderTrend', function (data) { trendChart.setOption({ xAxis: { data: data.map(d => d.date) }, series: [{ data: data.map(d => d.count) }] }); }); }, 30000);

这里的 30 秒间隔不是拍脑袋定的。对后台大屏来说,太频繁会徒增数据库压力,太慢又显得不实时。我一般把图表刷新间隔放到 Web.config 的 appSettings 里,比如前面那个ChartRefreshInterval,前端通过一个全局变量读取,后续调优不用翻代码。定时器用完要记得clearInterval,特别是在弹窗关闭或页面切换时,不然接口会一直悄悄请求,白白消耗服务器资源。

5. 避坑:拿到这套源码后最常见的五个拦路虎

5.1 Ajax 请求被登录页拦截,回调拿到的不是 JSON 而是 HTML

现象:EasyUI 表格加载时,页面没显示数据,控制台报SyntaxError: Unexpected token <。

原因:配置了 Forms 认证后,用户 Session 超时或未登录时发起的 Ajax 请求,服务端返回的是 302 重定向,浏览器自动跟上登录页,最后回调里拿到的是登录页的 HTML 字符串。脚本按 JSON 去解析,第一行<就报错。

解决:写一个全局的登录检查过滤器,对 Ajax 请求直接返回 JSON 状态码 401,而不是跳转到登录页。同时在现有的控制器上加这个过滤器。

public class CheckLoginAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { if (filterContext.HttpContext.Session["UserId"] == null) { if (filterContext.HttpContext.Request.IsAjaxRequest()) { filterContext.Result = new JsonResult { Data = new { code = 401, msg = "登录已过期,请刷新页面重新登录" }, JsonRequestBehavior = JsonRequestBehavior.AllowGet }; } else { filterContext.Result = new RedirectResult("/Account/Login"); } } } }

前端再在全局判断 401 状态,弹提示并跳登录页。这个方案改动量小,不用动原有控制器里的任何业务代码。

5.2 EasyUI 与 Bootstrap 样式互相打架,分页条变形

现象:列表页或多或少的样式错乱,最典型的是 datagrid 分页条高度异常、按钮间距变大,输入框圆角被覆盖。

原因:源码的某个页面里同时引了bootstrap.css和easyui.css,两个框架都重置了button、input、pagination等元素的样式,按加载顺序后加载的会覆盖前面的。

解决:后台管理页面不要同时引入两套 UI 框架的完整样式。确认这个页面主要是 EasyUI 组件,就只引 EasyUI 主题;如果公司 UI 规范强制要求 Bootstrap,就把 Bootstrap 放在前面,EasyUI 主题放后面,并给 EasyUI 的根容器加一个作用域类名来隔离。判断是否该保留 Bootstrap,唯一的依据是看这个页面实际用了多少个 Bootstrap 组件。

5.3 EF 导航属性导致 JSON 序列化循环引用报错

现象:调用列表接口时,报Newtonsoft.Json.JsonSerializationException: Self referencing loop detected,或者浏览器直接 500,但 SQL 语句能查出数据。

原因:实体类里定义了导航属性,比如User里有Role,Role里又有IQueryable<User>。查询时把导航属性带出来,序列化器顺着关系来回遍历,形成死循环。

解决:最直接、副作用最小的办法是查询后投影成匿名对象,不用实体直接序列化。回看 3.1 里的PageList正是这么写的,只取需要的字段。另一个办法是把 DbContext 的Configuration.LazyLoadingEnabled = false,让导航属性默认不加载,但这会影响全局,有些页面会因此拿到 null 引用。还有一种方案是设置ReferenceLoopHandling.Ignore,这通常作为最后手段,因为它会让数据出现重复嵌套,前端拿到的结构也不干净。

5.4 部署到 IIS 子目录后页面白屏或图片样式全丢

现象:本机调试一切正常,发布到服务器的虚拟目录/admin/下,登录页能打开,但登录进去后页面布局完全错乱,图片和脚本 404。

原因:View 里写死了绝对路径,比如<link href="/Content/easyui.css">或<script src="/Scripts/jquery.min.js">。部署到子目录后,浏览器把/Content/...解析成域名根路径,实际文件在/admin/Content/...,全部找不到。

解决:所有路径改用@Url.Content("~/Content/easyui.css")或者@Url.Action(...)。这个改造比较枯燥,但必须做。建议在拿到源码后先全局搜索href="/和src="/,把这些写死的路径全部替换成~写法。替换完再刷新页面,重点看登录页、列表页、弹窗页三个地方布局是否正常。

5.5 ECharts 旧版按需引入,地图和组件莫名缺失

现象:页面引了echarts.min.js,柱状图和折线图正常,但一渲染中国地图就空白,或者报Component series.map is not exists。

原因:源码包里带的 ECharts 是某个旧版本或按需定制版本,地图数据没有被打进包里。老版本 ECharts 需要单独引china.js,新版要求注册 geoJSON,用法完全不同。

解决:先看源码里引用的 ECharts 版本,在控制台执行echarts.version即可看到。确认版本后按对应的方式处理。常用图表如柱状、折线、饼图基本不用额外引,但使用地图、雷达图、树图这些特殊组件前,先去官网查一下版本对应的引入方式,不要套用 GitHub 上最新示例。这个坑改起来不复杂,但排查时间容易拖长,所以我把这个版本检查列为图表排障的第一步。

6. 验证这套源码的第一步:把第一个报表模块加进去

拿到源码,确认能跑通、排掉几个明显问题之后,真正的价值是把它改造成自己的系统。我的建议是不要一上来就改登录页或者换菜单,而是先加一个最小的报表模块,完整走一遍“新增菜单—建 View—写图表接口—前端渲染”的链路。这一条走通,整个系统的开发模式也就摸清了。

先建一张菜单记录。如果源码用数据库菜单,就在Sys_Menu表里插一行,父级挂在“报表中心”,URL 填写@Url.Action("DeviceReport", "Report")对应的路径。如果菜单写死在视图里,直接加一行accordion或 tab 项即可。这一步不涉及代码编译,很快能看到菜单是否出来。

接着写图表接口。沿用第 4 章的数据格式,在 ReportController 里加一个 Action,返回近一周的上报数量;然后建对应的 View,里面放一个div容器和 echarts 初始化脚本。最后打开菜单,看到页面出现图表,再用开发者工具的 Network 面板确认接口只调了一次,数据格式是对的。这样就验证完毕。

我目前接到这类源码的习惯是:先用十几分钟,把“登录 → 首页 → 菜单 → 表格 → 弹窗 → 图表”整条链路完整跑一遍,每遇到一个问题就记录一次。这套源码能不能用,不是看它有多少个文件,而是看用最短路径走通一条业务链路要修多少个地方——路径写死、统一认证失效、页面残留脚本,都是需要修的;而像 EF 循环引用这种,只会在特定查询下爆出来。这种检查方式帮我把踩坑成本压得很低。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询