尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MORVIN模板实战:快速搭建销售数据统计后台

MORVIN模板实战:快速搭建销售数据统计后台 简介MORVIN是一套专为电商行业设计的后台管理UI框架模板面向电商运营者、产品经理与前端开发者可用于搭建销售数据统计、商品与订单管理、客户服务及营销工具等模块的后台界面有效降低从零开发的门槛同时保证视觉一致与交互友好。压缩包共302个文件以JavaScript交互脚本、HTML页面、CSS样式表为主辅以PNG/JPG图片、WOFF字体及说明文档整体体积约8.43MB目录结构清晰便于按模块引用与二次开发。这套模板采用响应式与扁平化设计涵盖销售数据分析、商品管理、订单处理、客户服务、营销工具、数据报表六个功能模块支持图表实时展示与自定义报表生成控件反馈明确并兼顾主流浏览器兼容、性能优化与模块化扩展后续增加或调整功能较为方便。资源发布后已有274人学习下载适合需要快速搭建或美化电商后台界面的团队参考使用。1. 商场销售数据统计管理后台为什么先选模板框架接手一个商城类项目最容易被低估的不是业务逻辑而是管理后台的骨架。销售数据统计页面上要放日期筛选、门店/渠道维度切换、实时订单卡片、趋势折线图、品类占比饼图、TOP10 排行表格如果从零写 CSS 和组件光是把布局对齐、响应式适配、图表 resize 处理好两三天就进去了。MORVIN 这类管理后台 UI 框架模板解决的就是「先有骨架再填业务」这件事你拿到的是一个已经定义好栅格、导航、卡片、表格、图表容器和深色/浅色主题的工程直接往里塞统计接口返回的 JSON 就行。这个模板适合三类人第一类是后端工程师顺手做管理端不想碰繁琐的 CSS 细节第二类是前端新手需要一套能跑通的参考实现照着改比从空白页面开始容易得多第三类是接外包项目的开发者用模板能快速出图给甲方确认风格避免在视觉效果上反复返工。需要提前说明的是MORVIN 本身不是一个被广泛讨论的开源项目更像是一份被压缩打包、在开发者之间流传的后台模板资源所以在动手之前你需要先把它当作「可改造的起点」而不是「开箱即用的最终产品」。下文会从解包开始把你带过「双击 index.html 发现页面空白」的第一道坎然后讲清楚这套模板的静态资源组织方式再深入销售统计页面最核心的筛选联动与数据渲染逻辑最后落到图表组件的封装和打包优化上。整条路径走完你拿到的不仅是一个能用后台还能理解模板里的每一条 CSS 规则和每一段 JavaScript 是为谁服务的。2. 解压 MORVIN 模板后先别急着预览用本地服务跑通最小结构很多人在解压后直接双击index.html结果页面要么一片空白要么样式全部丢失。这不是模板坏了绝大多数后台模板的 CSS、JS、字体和图标文件走的是相对路径或绝对路径浏览器在file://协议下对模块加载有诸多限制特别是使用了 ES Module 或 Ajax 请求本地 JSON 配置的模板双击必然废掉。正确做法是启动一个本地静态文件服务把整个目录作为站点根目录。以 Windows 环境为例如果你装了 VS Code最省事的方式是安装 Live Server 扩展然后在模板根目录右键选择「Open with Live Server」如果你更习惯命令行用 Python 或 Node 都能在五秒内起服务。# 用 Python 3 在 MORVIN 模板根目录启动静态服务 cd MORVIN python -m http.server 8080 # 或者用 npx 快速启动一个静态服务器 npx serve -p 8080启动后访问http://localhost:8080/正常情况下你应该能看到模板的首页。如果打开后仍然白屏按 F12 打开控制台重点关注两类错误一类是404说明模板引用的某个 JS 或 CSS 路径不对常见原因是模板解压后目录层级被改动比如原本assets/应该和index.html同级现在被套了一层文件夹另一类是CORS policy错误说明模板在运行时通过 fetch 请求了本地 JSON 数据非服务环境下被浏览器拦截这也是必须使用本地服务器的原因。python -m http.server 8080这条命令的含义是python -m表示以模块方式运行http.server是 Python 自带的 HTTP 服务器模块8080是指定端口。如果你手头还有其他服务占用 8080可以换成 3000、5000 等随便一个端口只要访问地址同步改成对应端口即可。serve命令同理-p参数指定端口。确认能打开首页后先花十分钟把模板的目录结构过一遍你会看到一个典型后台模板的布局MORVIN/ ├── index.html # 入口通常带登录页/主页/详情页多个 HTML ├── assets/ # 静态资源 │ ├── css/ # 样式文件可能有 style.css, theme.css 等 │ ├── js/ # 公共脚本jquery、bootstrap、echarts 等 │ ├── vendor/ # 第三方库 │ ├── img/ # 图片资源 │ └── fonts/ # 图标字体 ├── pages/ # 子页面 ├── plugins/ # 插件 └── README.html # 或 .txt多半有模板说明不同版本 MORVIN 的目录命名会有差异但本质逃不开这几类。看到pages/目录就是模板把多个管理页面拆分了出来比如销售统计页是pages/sales-statistics.html或类似名字你后续改版的工作重心就落在那个页面里。建议你打开 README看看作者有没有写明依赖了哪些第三方库的版本这决定了你后面加功能时能不能顺利引入新库而不产生冲突。提示如果模板里带了package.json说明它集成了前端构建工具那就别用静态服务器裸跑了老老实实执行npm install再npm run dev。没有package.json的纯静态模板才是本地服务打开即可。3. 拆解 MORVIN 的 UI 框架构成与页面布局3.1 从样式和脚本倒推模板用的是什么技术栈你不需要看文档只要翻一眼assets目录里的文件名就能判断这套模板的底子。如果发现bootstrap.min.css和jquery.min.js同时存在那它多半是 Bootstrap jQuery 的传统后台风格如果看到vue.min.js甚至.vue文件那就是 Vue 系的后台如果出现echarts.min.js图表能力是内置的。MORVIN 这类模板之所以受欢迎就是因为它把 UI 框架选型、页面布局、配色系统都替你做完了你只需要基于它的规则去填充业务。常见的组合是CSS 框架Bootstrap 4/5 负责栅格、按钮、表单、卡片、模态框等基础组件图标Font Awesome 或 Material Icons对应css/font-awesome.min.css这样的文件图表ECharts 或 Chart.js负责销售趋势、占比、排行榜等可视化表格DataTables 或 Bootstrap Table负责订单列表的排序、分页、搜索JavaScriptjQuery 一堆插件偶尔也混入原生 JS这个组合决定了你后续扩展时的习惯写页面先想 Bootstrap 的row和col栅格怎么排初始化图表时先调$.getJSON拿数据表格排序直接调 DataTables 的 API。如果你更习惯 React 或 Vue 那套组件化写法也能在这个模板的基础结构上引入框架但这属于二次改造不在「先用起来」的范围内。3.2 销售统计后台的标准布局骨架打开销售统计页面它的布局基本遵循管理后台的黄金结构顶部导航条、左侧菜单、右侧内容区。内容区又分为统计卡片行、图表行和表格行。你在改造时不需要重写布局只需要理解和替换各区域的内部内容。看一个简化后的 HTML 结构片段!-- 统计卡片区域四个核心指标 -- div classrow div classcol-md-3 col-sm-6 div classcard div classcard-body h5 classcard-title今日销售额/h5 h3 classtext-primary idsales-today¥0.00/h3 p classtext-mutedi classfa fa-arrow-up/i 12.5%/p /div /div /div !-- 其余三个卡片同理订单数、客单价、退款率 -- /div !-- 图表区域折线图与饼图并排 -- div classrow div classcol-lg-8 div classcard div classcard-body h5 classcard-title销售趋势/h5 div idsales-trend-chart styleheight: 350px;/div /div /div /div div classcol-lg-4 div classcard div classcard-body h5 classcard-title品类占比/h5 div idcategory-pie-chart styleheight: 350px;/div /div /div /div /div这里的col-md-3表示中等屏幕以上占 4 列共 12 列栅格四张卡片正好一行col-lg-8和col-lg-4代表大屏下图表区左右分栏宽度约 2:1非常经典。你要是想把饼图改成放右边占三分之一就把col-lg-4改成col-lg-3左边改成col-lg-9栅格系统会自动撑满 12 列。模板里那些text-primary、text-muted、card-title都是框架预置的样式类你不用关心它们背后的颜色值到底是多少除非你准备定制主题。MORVIN 一般内置了浅色和深色两套主题切换方式通常是在body上加一个dark-mode类或者通过右上角一个太阳/月亮图标切换。3.3 三个必调参数栅格占比、卡片高度、内容区边距刚拿到模板你不需要通读所有 CSS但三个位置的参数直接决定页面好不好看栅格占比前面提到过col-md-3这类类名控制的宽度在销售业务里指标卡数量可能是 4 个也可能是 5 个。5 个卡片可以用col-md-2offset组合或者干脆用col自适应但要注意 Bootstrap 5 里col会自动均分每个卡片宽度会窄一些适合显示短数字不适合显示长日期标题。卡片高度ECharts 渲染所在的div必须显式设置高度否则图表容器高度为 0你怎么等数据都画不出图。模板中styleheight: 350px;就是为图表预留的渲染空间。你换成自己的数据后如果图表显示被压缩优先检查这个高度和父级卡片是否设了overflow: hidden。内容区边距有些模板的内容区没有设置padding卡片贴靠在一起非常难看。如果你发现页面间距不舒服找内容区最外层容器的类名常见的是.content-wrapper或.main-content加上padding: 24px;。如果模板用了 CSS 变量定义间距直接改变量的值更一劳永逸比如:root { --content-padding: 24px; --card-border-radius: 12px; }这种自定义属性的写法在现代后台模板中出现频率越来越高。修改前先用浏览器开发者工具选中内容区看 Computed styles 里padding来自哪条规则再决定是直接覆盖还是干脆在自定义样式文件的最后写!important。4. 用模板的筛选栏和表格组件接住销售数据4.1 设计一个按日期、门店、渠道筛选的统计条件栏销售统计的核心交互是先筛选后出数。模板中已经提供了下拉框、日期选择器、按钮等基础元素你只需要把它们组合成一个表单区域放在内容区最上方。常见的实现是form idsales-filter-form classrow align-items-end mb-4 div classcol-md-3 label fordate-range日期范围/label input typetext iddate-range classform-control placeholder起始日期 - 结束日期 /div div classcol-md-2 label forstore-select门店/label select idstore-select classform-control option value全部门店/option option valuestore_001北京旗舰店/option option valuestore_002上海体验店/option /select /div div classcol-md-2 label forchannel-select渠道/label select idchannel-select classform-control option value全部渠道/option option valueonline线上商城/option option valueoffline线下收银/option /select /div div classcol-md-2 button typesubmit classbtn btn-primary查询统计/button button typereset classbtn btn-outline-secondary重置/button /div /form这段代码里form的提交动作不需要真正向后端提交表单而是被 JavaScript 拦截后把筛选条件组织成参数去请求统计接口。date-range输入框一般配合模板自带的日期插件初始化时设置好默认周期为最近 7 天例如用 Bootstrap DateRangePicker 时$(#date-range).daterangepicker({ locale: { format: YYYY-MM-DD }, autoUpdateInput: true, startDate: moment().subtract(6, days), endDate: moment() });这段初始化的含义是日期选择器显示格式为年月日起始时间为今天往前推 6 天共 7 天结束时间为今天并且选中后自动把文本填入输入框。这三个参数startDate、endDate、format就是模板里最常调的日期参数换成你自己的统计口径时把subtract(6, days)改成subtract(29, days)就能默认看最近 30 天。4.2 通过 fetch 请求统计接口并渲染卡片数值模板给你画好了卡片和数据字段的坑剩下的工作是把你的后端响应填进去。假设后端统计接口为/api/sales/overview按条件筛选后返回这样的 JSON{ code: 0, data: { salesToday: 128000.00, orderCount: 326, customerUnitPrice: 392.64, refundRate: 2.1 } }你要做的就是用 jQuery 或原生 fetch 发请求然后把值写进对应 id 的 DOM 中。原生实现更不依赖模板自带的 jQuery 版本// 表单提交后触发统计请求 document.getElementById(sales-filter-form).addEventListener(submit, async function(e) { e.preventDefault(); // 阻止表单默认刷新页面 const dateRange document.getElementById(date-range).value; const storeId document.getElementById(store-select).value; const channel document.getElementById(channel-select).value; const params new URLSearchParams({ dateRange, storeId, channel }); try { const res await fetch(/api/sales/overview?${params.toString()}); const json await res.json(); if (json.code 0) { renderCards(json.data); } else { console.error(接口错误, json.message); } } catch (err) { console.error(请求失败, err); } }); function renderCards(data) { // 格式化金额保留两位小数并加上千分位 document.getElementById(sales-today).textContent ¥ Number(data.salesToday).toLocaleString(zh-CN, { minimumFractionDigits: 2 }); document.getElementById(order-count).textContent data.orderCount; document.getElementById(unit-price).textContent ¥ data.customerUnitPrice.toFixed(2); document.getElementById(refund-rate).textContent data.refundRate %; }URLSearchParams是把筛选参数拼成查询字符串的工具params.toString()生成类似dateRange2025-01-01%20-%202025-01-07storeIdstore_001channeloffline的请求串后端直接解析就能拿到条件。toLocaleString(zh-CN, { minimumFractionDigits: 2 })是数字格式化方法minimumFractionDigits: 2强制保留两位小数这样128000能显示成128,000.00符合财务阅读习惯。注意模板里的卡片文本元素原来可能写着写死的数字你要把你自己的 JavaScript 放到页面底部/body之前执行确保 DOM 已经加载完成。如果模板是用 jQuery 封装的你也可以用$(#sales-today).text(...)的写法效果相同。4.3 把订单明细表切换成 DataTables 模式除了统计指标后台还要一个详单表格。模板里一般已经给了table的骨架但如果你直接往里面塞几十条数据会发现没有分页、没有搜索、列也不会自动排序。这时候需要把模板自带的 DataTables 插件激活$(document).ready(function() { $(#order-table).DataTable({ processing: true, serverSide: true, ajax: { url: /api/sales/orders, data: function(d) { // 在请求中附加额外筛选参数 d.dateRange $(#date-range).val(); d.storeId $(#store-select).val(); d.channel $(#channel-select).val(); } }, columns: [ { data: orderNo }, { data: customer }, { data: amount }, { data: status }, { data: createdAt } ], pageLength: 10, order: [[4, desc]] }); });这里serverSide: true表示分页和搜索都由后端来完成前端每次翻页都会触发一次 AJAX 请求后端需要接收 DataTables 发送的start、length、search[value]、order[column]等参数并返回recordsTotal、recordsFiltered、data三个字段。order: [[4, desc]]表示默认按第五列createdAt索引从 0 算起倒序排列如果你想默认按订单金额从高到低就改成[[2, desc]]。如果你的后端暂时不准备做服务端分页就把serverSide改为false通过data字段一次性传入全部数组DataTables 会前端分页但数据量超过万级时页面会明显变卡。5. 销售趋势图与品类占比图的 ECharts 集成5.1 初始化图表并绑定筛选条件触发更新MORVIN 模板里引入的图表库大概率是 ECharts原因很简单它免费、中文文档全、社区方案多而且后台常见的折线图、柱状图、饼图、漏斗图都能直接画。图表初始化代码通常被封装成一个个函数在页面加载和筛选条件变化时被调用。// 全局管理图表实例避免重复创建 const chartInstances {}; function initSalesTrendChart(domId) { const dom document.getElementById(domId); if (!dom) return; if (chartInstances[domId]) { chartInstances[domId].dispose(); // 销毁旧实例 } const chart echarts.init(dom); chartInstances[domId] chart; const option { tooltip: { trigger: axis }, legend: { data: [销售额, 订单量] }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: [] }, yAxis: [ { type: value, name: 金额(元) }, { type: value, name: 订单量 } ], series: [ { name: 销售额, type: line, data: [], smooth: true }, { name: 订单量, type: bar, yAxisIndex: 1, data: [], barWidth: 20 } ] }; chart.setOption(option); return chart; }这段函数做了两件事一是用echarts.init(dom)创建实例并挂到chartInstances对象里二是定义好图表的配置项。chartInstances[domId].dispose()的作用是销毁之前已经存在的实例这是很多初学者会踩的坑——每次刷新数据都重新init但旧实例和新实例叠加导致页面同时渲染两张图且内存越涨越高。通过实例字典管理同一个容器 id 永远只有一个实例在活动。grid参数里的containLabel: true很重要它保证 y 轴的文字标签不会被挤出去。如果你觉得图表的左边距不够导致坐标轴数值显示不全可以把这个值设为true或者手动调整left为5%。5.2 用后端返回数组填充图表数据筛选条件改变时你不需要重写整个option只需要重新从接口拿数据然后调用chart.setOption({ xAxis: { data: ... }, series: [...] })做局部更新async function updateSalesTrendChart() { const dateRange document.getElementById(date-range).value; const storeId document.getElementById(store-select).value; const params new URLSearchParams({ dateRange, storeId }); const res await fetch(/api/sales/trend?${params.toString()}); const json await res.json(); // 假设返回 { dates: [01-01,01-02,...], amounts: [...], orders: [...] } const chart chartInstances[sales-trend-chart]; if (chart) { chart.setOption({ xAxis: { data: json.data.dates }, series: [ { data: json.data.amounts }, { data: json.data.orders } ] }); } }setOption的合并机制是 ECharts 的一大优点你只传需要改变的部分其他配置比如 tooltip、legend、grid会自动保留。所以代码里并没有重新声明option的全部内容只把xAxis.data和两个series.data传进去图表的平滑曲线、柱状图宽度都不会丢失。饼图的逻辑同理品类占比接口/api/sales/category返回的是[{ name: 数码, value: 120000 }, { name: 服饰, value: 80000 }]这种结构直接映射成 ECharts 需要的data格式const pieData json.data.map(item ({ name: item.name, value: item.value })); chartInstances[category-pie-chart].setOption({ series: [{ type: pie, radius: [40%, 70%], // 环形饼图内外半径 data: pieData, label: { formatter: {b}: {d}% } // 显示名称和百分比 }] });radius: [40%, 70%]表示这是一个内半径 40%、外半径 70% 的环形图比纯实心饼图看起来现代一些。label.formatter用了模板字符串语法{b}代表数据项名称{d}代表百分比最终 label 显示成「数码: 28.5%」这种格式。如果你不想在图上显示百分比而不想占地方把label的show设为false再配合tooltip显示详情即可。5.3 图表自适应容器大小和窗口 Resize 处理后台页面经常被拖拽缩放如果 ECharts 不处理 resize图表会拉伸变形或者边缘被截断。一个必须写进模板的通用函数是window.addEventListener(resize, function() { Object.values(chartInstances).forEach(chart { if (chart chart.resize) { chart.resize(); } }); });这段代码监听浏览器窗口尺寸变化然后遍历所有 chart 实例逐个调用resize()。之所以要遍历而不是只调一个是因为页面可能有销售趋势图、品类占比图、门店对比图等多张图表。有些模板里图表所在的容器宽度是由侧边栏折叠触发生的这类动态变化即使窗口大小不变图表也需要 resize解决办法是在侧边栏切换按钮的点击事件里也调用一遍上述函数。提示如果你的图表在初始化时容器是隐藏的例如放在 Tab 页里初始不显示init时宽度会是 0之后切换 Tab 显示时图表依然空白。此时应在 Tab 切换之后对所有实例调用chart.resize()。6. 模板的二次封装与轻量化打包技巧6.1 把图表初始化封装成统一函数消灭重复代码当你把 MORVIN 模板跑通、数据也能正常渲染后下一步是别急着把更多统计图复制粘贴到页面里先把图表初始化封装成一个通用工厂函数。这样后续每加一张图只需一行配置。function createChart(domId, type, dataConfig) { const dom document.getElementById(domId); if (!dom) return; if (chartInstances[domId]) { chartInstances[domId].dispose(); } const chart echarts.init(dom); chartInstances[domId] chart; const baseOption { tooltip: {}, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: dataConfig.categories || [] }, yAxis: { type: value }, series: [] }; if (type line) { baseOption.series dataConfig.series.map(seriesItem ({ type: line, smooth: true, areaStyle: seriesItem.area ? {} : undefined, data: seriesItem.data || [] })); } else if (type pie) { // 饼图不需要 xAxis/yAxis delete baseOption.xAxis; delete baseOption.yAxis; baseOption.series [{ type: pie, radius: [40%, 70%], data: dataConfig.data || [] }]; } chart.setOption(baseOption); return chart; }这个工厂函数接收三个参数domId是容器元素 idtype是图表类型line或piedataConfig是包含数据和样式的对象。遇到饼图时把不相关的坐标轴配置删掉这样就不会在饼图上显示空的 category 轴。后续你要新增一个「各门店销售对比柱状图」可以在这个函数里加一个else if (type bar)分支复制 line 分支改一下type即可。6.2 移除用不到的第三方插件减小模板体积MORVIN 这类模板为了覆盖尽量多的功能会一次性引入很多插件比如你可能根本用不到的日历控件、地图插件、拖拽排序库。这些没用的 JS 文件虽然不会拖慢页面首屏太多但每一字节的加载量都会影响感知性能。检查方式很简单打开浏览器开发者工具的 Network 面板刷新页面把所有请求列出来看看哪些 JS 文件大小超过 100KB 且没有被调用的迹象在 Sources 里搜索它的全局变量如果搜不到引用就可以删。如果你不想直接删除文件怕改坏页面可以设置构建脚本把你需要的文件合并压缩。但更实际的做法是找到index.html底部的script标签逐个注释掉不需要的插件刷新页面观察功能是否缺失。比如模板默认加载了jquery.multi-select.js但你只用普通select直接删除这一行 script 引用再把对应的 CSS 里没有用到的类名留在原处不影响。用 Gulp 或 npm scripts 做 CSS 压缩也是常见做法但记住如果你只使用模板的静态版本根本没有构建流程就不要为了压缩而强行引入 Node.js 工具链那会让维护复杂度成倍上升。直接在接入后端前把模板里的 TODO 占位代码清理干净效果更直接。6.3 验证模板改造结果的三种方式改完模板后不能只看页面显示正常就结束需要用更严谨的方式确认代码真的可用。第一种验证是 console 无报错测试打开浏览器开发者工具 Console 面板从页面加载到筛选查询、重置、切换日期整个过程不应出现红色错误信息。出现TypeError: Cannot read properties of undefined多半是接口返回的字段名和你写的不一致为提高排查效率统一给接口加一层数据格式校验。第二种验证是接口耗时观测在 Network 面板里点击统计查询查看/api/sales/overview和/api/sales/trend的耗时。如果同一个查询条件连续点击两次第一次应该触发请求第二次最好使用前端缓存把结果直接渲染。实现缓存很简单const cacheMap new Map(); async function fetchWithCache(url, params) { const key url ? params.toString(); if (cacheMap.has(key)) { return cacheMap.get(key); } const res await fetch(key); const json await res.json(); cacheMap.set(key, json); return json; }这个函数先检查cacheMap是否已有相同 key 的响应如果有就直接返回 Promise否则请求后存入缓存。对于销售统计后台用户往往会在同一个筛选条件下反复查看数据这种缓存能显著减少重复请求。第三种验证是深色模式下图表颜色是否清晰。多数模板支持切换暗黑主题但 ECharts 默认配色是亮色系的暗色背景里图表文字可能看不清。解决办法是在切换主题时对每个图表实例调用chart.dispose()后重新初始化并传入echarts.init(dom, dark)这种内置暗色主题。如果你的 MORVIN 版本没有注册暗色主题用echarts.init(dom, null)后手动修改option.textStyle.color和axisLabel.color也行。最后再检查一遍图表 resize 是否符合预期把窗口从宽屏拉到窄屏图表应该平滑跟随而不是出现横向滚动条。本文还有配套的精品资源点击获取
返回列表