
简介这份CRM客户关系管理系统原形面向软件开发学习者与前端初学者是一份毕业项目型参考覆盖销售、市场营销与服务等核心业务场景重点演示客户管理界面的交互设计。原型中通过JavaScript实现表单校验如邮箱、手机号、密码强度借助jQuery优化DOM操作与下拉、滑动等交互效果并利用Ajax完成无刷新数据更新可实时刷新客户信息、订单状态与销售数据适合理解企业级管理系统的前端实现思路。压缩包为RAR格式约10MB具体目录与文件构成可在下载后查看。目前已有301人学习下载适合正在做CRM系统设计或需要原型参考的开发者。通过研究该原型可了解页面草图、交互流程与前端代码示例如何整合学习直观导航、统一样式与响应式布局在客户管理平台中的落地方法。1. CRM客户关系管理系统原形没有后端也能完整过一遍业务流程很多业务团队在系统立项早期都会遇到这种处境后端还在排期业务演示却催得急。这种时候最救急的不是写完整业务后台而是一套能点、能跳、能填表的页面原形。这份CRM客户关系管理系统原形就是把客户关系管理中最核心的客户、联系人、跟进记录、销售漏斗和统计看板做成了一组可交互的页面原型不依赖任何后端服务浏览器打开就能点完整个流程。适合产品经理拿它验证业务流程、和业务方对齐字段也适合前端开发把它当开发脚手架照着页面结构和数据约定直接开始二开。项目里的每一页都对应真实业务场景不是那种只能看不能点的死图。2. 项目结构与页面模块一份能点完整条业务线的原型导航2.1 目录结构与文件职责拿到这个原型包第一步不是打开某个页面而是先看清文件组织方式。整个原型按页面和资源做了分层页面之间通过链接跳转串联公用样式和脚本各自独立结构非常接近一个正式的纯前端项目crm-prototype/ ├── index.html # 登录页 ├── dashboard.html # 工作台首页登录后默认落地页 ├── customer-list.html # 客户列表页支持搜索与筛选 ├── customer-detail.html # 客户详情页展示客户全貌与跟进时间线 ├── follow-up.html # 独立跟进记录页新增跟进弹窗 ├── sale-pipeline.html # 销售漏斗页看各阶段客户分布 ├── report.html # 统计报表页柱状图与核心指标卡 ├── css/ │ ├── common.css # 全局样式按钮、表单、栅格 │ ├── dashboard.css # 工作台和卡片样式 │ └── table.css # 表格、筛选栏、分页样式 ├── js/ │ ├── data.js # 模拟数据源所有内置客户、跟进、订单数据 │ ├── login.js # 登录校验与登录态写入 │ ├── customer.js # 客户列表渲染、搜索、分页 │ ├── followup.js # 跟进记录时间线渲染与新增逻辑 │ ├── dashboard.js # 统计卡片数据汇总与图表渲染 │ └── common.js # 工具函数存储封装、格式化、状态判断 └── assets/ ├── logo-placeholder.png └── avatar-placeholder.png文件命名全部采用小写英文加连字符这么做是为了避开一些浏览器对中文路径的编码问题后文避坑章节会展开说。页面命名直接对应业务含义login、dashboard、customer 一眼能认出职责对第一次接触这份原型的开发者来说改哪个文件做哪个功能基本不需要额外解释。需要特别注意的是 data.js 和 common.js 这两个文件它们是整个原型能活起来的基础。data.js 管数据common.js 管读写所有页面都引用它们。如果你后面要接真实接口改动的大头也集中在这两个文件上其他页面脚本只需要把数据来源换掉即可。2.2 核心页面清单与字段设计把每个页面当成一个独立的业务场景去理解整个原型就串起来了。最核心的六类页面及其业务字段如下表页面业务定位核心字段与交互登录页身份验证入口账号、密码校验通过后写本地登录态工作台首页销售个人工作台今日待跟进、本月成单数、销售额汇总快捷入口客户列表客户档案管理客户名称、等级、来源、所属顾问、状态搜索 筛选 分页客户详情单客户全貌基本信息、联系人、跟进时间线、关联订单支持新增跟进销售漏斗商机阶段分析各阶段客户数量与转化率以条形图展示统计报表经营数据汇总近六个月新增客户趋势按顾问统计的成单对比字段设计上有一个可以借鉴的点客户等级用了 A/B/C 三级而不是很多系统一上来就搞十几个字段的复杂评分体系。原型阶段最重要的不是字段全而是让业务方看得懂、能围绕字段讨论。A类客户是重点跟进B类是有明确意向C类是潜在培育所有页面都围绕这个分级展开统计和漏斗逻辑。2.3 页面流转从登录到客户详情的完整路径原型的价值在于能走通流程所以页面间的跳转关系值得先理清楚。整个原型有一条主链路和两条分支链路。主链路是登录页 → 工作台首页 → 客户列表 → 客户详情 → 新增跟进记录。这条链路对应一个销售每天最核心的动作登录系统查看今天要做什么然后进入客户列表找到目标客户打开详情看一下历史跟进再补一条新的跟进记录。分支链路之一是从工作台首页的统计卡片直接跳到销售漏斗或报表页对应管理者视角的宏观查看另一条是从客户列表的筛选结果直接进入跟进记录页适合按条件批量处理某类客户。页面之间的参数传递也很简单直接列表页跳详情页时通过 URL 参数带上客户 id例如 customer-detail.html?id1001。详情页脚本读取这个参数后到数据源里匹配对应客户并渲染。这种传参方式在后端还没有就绪时是最贴近真实前后端联调习惯的做法后面换接口时改动最小。3. 核心代码逐段拆解登录、客户列表与跟进记录的实现思路3.1 登录页校验逻辑与状态写入登录页用的是最基础的表单提交方式没有引入重量级框架。HTML 部分保持干净把所有判断逻辑放到 login.js 里统一处理form idloginForm classlogin-box h2客户管理系统/h2 input typetext idusername placeholder账号 autocompleteoff input typepassword idpassword placeholder密码 button typesubmit登 录/button /form// js/login.js const ACCOUNTS [ { username: admin, password: 123456, name: 系统管理员, role: admin }, { username: sales01, password: 123456, name: 顾问A, role: sales }, { username: sales02, password: 123456, name: 顾问B, role: sales } ]; document.getElementById(loginForm).addEventListener(submit, (e) { e.preventDefault(); const username document.getElementById(username).value.trim(); const password document.getElementById(password).value; const matched ACCOUNTS.find( (item) item.username username item.password password ); if (matched) { localStorage.setItem(crm_user, JSON.stringify(matched)); location.href dashboard.html; } else { alert(账号或密码错误请检查后重试); } });这段代码做三件事先阻止表单默认提交再从固定账号表里做精确匹配最后把匹配到的用户信息序列化后写入 localStorage 并跳转。账号表写在脚本顶部这是原型阶段最快的方式后续接后端时直接把这段 find 逻辑替换成接口请求即可。参数说明username 做了 trim 处理避免用户误输空格password 保持原始输入不做任何加工。登录态字段名 crm_user 是全项目统一使用的存储键dashboard.js 和 customer.js 都会读它来判断当前操作人改名时需要全局同步。3.2 客户列表页表格渲染、搜索与筛选客户列表是原型里信息量最大的页面核心逻辑集中在数据的过滤和表格渲染上// js/customer.js function getFilteredCustomers() { const keyword document.getElementById(searchKeyword).value.trim(); const level document.getElementById(levelFilter).value; const status document.getElementById(statusFilter).value; return mockData.customer.filter((item) { const matchKeyword keyword ? item.name.includes(keyword) || item.phone.includes(keyword) : true; const matchLevel level ? item.level level : true; const matchStatus status ? item.status status : true; return matchKeyword matchLevel matchStatus; }); } function renderTable() { const list getFilteredCustomers(); const tbody document.getElementById(customerTableBody); tbody.innerHTML list .map( (item) tr td${item.name}/td tdspan classlevel-tag level-${item.level}${item.level}/span/td td${item.phone}/td td${item.owner}/td td${item.status}/td tda hrefcustomer-detail.html?id${item.id}查看详情/a/td /tr ) .join(); }渲染逻辑是把筛选条件放进一个纯函数里处理每次输入变化时重新执行过滤和渲染。三个筛选条件之间是 AND 关系关键字为空时不做匹配等级和状态选全部时不过滤这段逻辑简洁且不容易翻车。用 innerHTML 拼接字符串的方式在原型阶段完全够用真要长期维护再换成模板引擎或者框架的渲染机制不迟。需要注意的关键点表格里的客户等级用了带前缀的 CSS 类名 level-A、level-B这样样式表里可以分别定义不同背景色视觉上区分重点客户。跳转详情页的链接把客户 id 拼进 URL保证详情页能拿到对应参数。3.3 跟进记录时间线渲染与新增跟进记录是 CRM 系统里使用频率最高的功能原型里用了时间线的方式呈现每条记录按时间倒序排列新增记录通过弹层完成// js/followup.js function renderTimeline(list) { const container document.getElementById(timelineContainer); const sorted [...list].sort( (a, b) new Date(b.followTime) - new Date(a.followTime) ); container.innerHTML sorted .map( (item) div classtimeline-item div classtimeline-time${item.followTime}/div div classtimeline-type${item.type}/div div classtimeline-content${item.content}/div div classtimeline-owner跟进人${item.owner}/div /div ) .join(); } document.getElementById(saveFollowBtn).addEventListener(click, () { const content document.getElementById(followContent).value.trim(); const type document.getElementById(followType).value; if (!content) { alert(请填写跟进内容); return; } const currentUser JSON.parse(localStorage.getItem(crm_user)); mockData.followUp.push({ id: Date.now(), customerId: getUrlParam(id), followTime: formatTime(new Date()), type: type, content: content, owner: currentUser.name }); renderTimeline( mockData.followUp.filter((item) item.customerId getUrlParam(id)) ); });排序用 new Date 差值实现倒序时间格式由 formatTime 统一处理新增记录时拿 Date.now 当 id 这种取巧办法在原型里非常实用既保证唯一又省去自增逻辑。新增前做了空内容拦截用户没填文字时直接 alert 提醒而不写入。跟进人取自登录态不需要用户重复填写。这个模块能看出原型的交互完成度时间、类型、内容、跟进人四个字段完整覆盖了业务要记录的信息。类型字段是下拉选择值有电话沟通、上门拜访、微信跟进、邮件往来四种这些都是数据层里预设的枚举值。3.4 仪表盘统计卡片与图表渲染工作台首页是登录后的第一屏也是各个管理角色最关心的数据总览。它的核心逻辑是从 mockData 里汇总出四组指标并渲染成卡片// js/dashboard.js function loadDashboardStats() { const today formatDate(new Date()); const todayFollowCount mockData.followUp.filter( (item) item.followTime.startsWith(today) ).length; const monthOrders mockData.saleOrder.filter((item) { return item.orderTime.startsWith(today.slice(0, 7)); }); const totalAmount monthOrders.reduce( (sum, item) sum Number(item.amount), 0 ); document.getElementById(statTodayFollow).textContent todayFollowCount; document.getElementById(statMonthOrders).textContent monthOrders.length; document.getElementById(statMonthAmount).textContent totalAmount.toLocaleString(); }统计逻辑全部是数组方法链式处理filter 拿条件数据reduce 做金额汇总。这里的日期匹配用了字符串前缀判断因为 mockData 里的时间格式统一为 YYYY-MM-DDslic(0, 7) 取前七位拿到年月后再匹配就是当月数据。这种方式虽然简单但在格式统一的前提下是最稳的。图表部分没有用 CDN 图表库而是以 CSS 条形图展示销售漏斗各阶段客户数量。这样做的直接好处是离线打开页面不会因为外部资源加载失败而空白原型演示时不怕现场网络问题后文避坑章节会再提到这茬。4. 数据层与模拟逻辑没有后端也能让原型活起来的做法4.1 模拟数据源 mockData 字段设计整个原型能演起来靠的是 js/data.js 里内置的一套模拟数据。这个文件在结构上模仿了真实后端接口的返回格式用数组存放客户、跟进、订单三类核心业务数据// js/data.js const mockData { customer: [ { id: 1001, name: 某制造企业, level: A, source: 展会, owner: 顾问A, status: 跟进中, phone: 138****1234, createTime: 2025-02-14 }, { id: 1002, name: 某跨境电商团队, level: B, source: 转介绍, owner: 顾问B, status: 待联系, phone: 139****5678, createTime: 2025-02-20 } ], followUp: [ { id: 1, customerId: 1001, followTime: 2025-03-01 10:30, type: 电话沟通, content: 客户对产品模块很感兴趣约定下周演示, owner: 顾问A } ], saleOrder: [ { id: 8001, customerId: 1001, orderTime: 2025-03-10, amount: 128000, status: 已签约 } ] };字段设计上刻意模仿了数据库表的命名习惯customer 表的主键是 idfollowUp 表通过 customerId 关联客户saleOrder 也通过 customerId 反查客户名称。这种关联方式和关系型数据库的思路一致后续后端同学看到原型时能直接把这些字段映射成表结构产品对齐成本低很多。owner 字段存的是顾问代称而不是用户 id这是原型阶段为了方便演示做的妥协。真实系统里这里应该存用户 id展示时再关联出姓名。如果二开时要把权限做细这个字段需要优先改造。4.2 基于 localStorage 的存储封装页面间共享状态、新增数据后让其他页面能读到这些都靠 localStorage 和内存数组配合实现。common.js 里做了一个轻量封装统一处理读写的异常情况// js/common.js const storage { get(key, fallback) { try { const value localStorage.getItem(key); return value ? JSON.parse(value) : fallback; } catch (e) { console.warn(读取本地存储失败返回默认值, e); return fallback; } }, set(key, value) { localStorage.setItem(key, JSON.stringify(value)); } };存储内容分成两类一类是登录态这类会话数据用 crm_user 键单独存放另一类是业务数据原型里直接用内存数组 mockData 承载不做持久化。这个设计有个好处每次刷新页面数据都回到初始状态演示时不会因为上次操作留下的脏数据干扰流程。参数说明storage.get 的第二个参数 fallback 是兜底值读取失败或键不存在时返回它避免了页面上到处写三元判断。JSON.parse 包在 try 里是为了防止手动改 localStorage 导致格式损坏时整个脚本崩溃这在原型调试阶段并不少见。4.3 无后端状态约定与页面间传参没有后端时页面之间交换信息只能靠 URL 参数和 localStorage。原型里对这两条通道做了明确约定客户详情页的 id 通过 URL 查询参数传递登录用户信息通过 crm_user 传递列表页筛选条件不跨页共享。getUrlParam 这个工具函数在多个页面里复用负责把 URL 上的参数解析出来function getUrlParam(name) { const query location.search.substring(1); const pairs query.split(); for (const pair of pairs) { const [key, value] pair.split(); if (key name) return decodeURIComponent(value); } return null; }这套约定的价值在于它真实模拟了前后端分离的联调方式。后端接口就绪后URL 参数可以原样保留localStorage 登录态可以替换成 token页面间的数据流方向不需要重新设计。原型到真实系统之间的迁移路径是顺着这套约定自然长出来的而不是推倒重来。5. 运行与避坑排查浏览器打开前后最容易踩的坑5.1 正确的运行方式先起静态服务再打开很多拿到原型的人第一反应是双击 index.html这在部分页面能打开但很快会撞上各种奇怪问题。这个项目包含多个页面通过相对路径互相跳转而且使用了 localStorage 和 ES6 的 const、模板字符串语法直接 file:// 打开会受浏览器安全策略限制。推荐的做法是本地起一个静态文件服务终端进入项目目录后执行# Python 环境 python -m http.server 8080 # Node 环境 npx serve .然后浏览器访问 http://localhost:8080 就能得到完全正常的运行环境。静态服务只是把文件按原样发出去不需要后端逻辑起服务的目的是让浏览器以 http 协议访问页面避免 file 协议下 localStorage 和模块加载的各种限制。判断当前是不是 file 协议很简单看地址栏以 file:// 开头就是。以 http://localhost 开头就是正常的。这一步做好了后面至少七成的问题都不会出现。5.2 踩坑一登录成功却反复跳回登录页现象输入账号密码后明明提示登录成功跳转到 dashboard.html但一闪又被弹回 index.html。原因file:// 协议下部分浏览器对 localStorage 的写入是受限的或者存储空间按目录隔离。上一页写入的 crm_user在下一个页面读取时拿不到common.js 里判断登录态失效就强制回登录页。表现就是死循环式的跳转。解决不用 file 协议直接打开改用 5.1 里的静态服务方式访问。如果已经在用 http 访问还出现这个问题检查浏览器是否开了无痕模式且禁用了站点数据换个普通窗口再试。5.3 踩坑二图表区域空白或样式错乱现象dashboard.html 和 report.html 打开后统计卡片有数据但图表区域是空白或者某些图标显示成小方块。原因如果原型引用了外部 CDN 的图表库或图标库而当前环境不能访问外网脚本加载失败就会产生这个结果。另外assets 目录下缺了占位图片也会让 logo 区域显示异常。解决优先保证所有静态资源本地化。图表改用 CSS 实现或把图表库文件下载到项目 assets 里图标直接用文本符号或内联 SVG确保断网环境下演示不出问题。这个项目在演示场景中的价值恰恰在于离线可用如果被外网资源卡住就本末倒置了。5.4 踩坑三新增的跟进记录刷新后消失现象在客户详情页新增一条跟进记录当时页面显示正常但刷新页面后这条记录没了。原因原型里新增跟进是 push 到内存数组 mockData.followUp刷新页面脚本重新执行数据源重置为初始值。这个行为是刻意的原型阶段要保证演示数据可控但对于不清楚设计的人来说会误以为数据丢失。解决如果你想让新增数据在刷新后保留需要在 storage.set 里把 mockData 整体写入 localStorage并在 data.js 初始化时优先读取本地存储。有一点要提醒这样做会让后续演示数据越来越脏建议在演示环境保留初始重置行为把持久化留到真实系统里再让后端接管。5.5 踩坑四中文文件名导致链接 404现象页面之间能正常跳转但某个图片链接点击后浏览器地址栏出现一串百分号编码页面显示 404。原因文件或目录名含中文不同浏览器对中文 URL 编码处理不一致尤其在老版本浏览器上最容易出错。解决保持这个项目现有的命名约定全英文小写加连字符。如果已经从其他地方复制的资源带了中文名统一改成拼音或英文。这不算大问题但演示现场出现 404 会非常尴尬属于前期顺手就能避开的坑。6. 二次开发改造思路从静态原型进化成可对接后端的系统6.1 第一步把数据层换成接口调用改造的核心不是重写页面而是替换数据来源。先给 common.js 增加一个 request 函数统一封装 fetch// 改造后的 common.js 增加部分 async function request(url, options {}) { const res await fetch(url, { headers: { Content-Type: application/json }, ...options }); if (!res.ok) throw new Error(请求失败${res.status}); return res.json(); }然后逐个改造页面脚本把每个页面顶部的 mockData 引用换成对应接口。比如 customer.js 里的 getFilteredCustomers原来是同步过滤内存数组现在改成 async 函数从 /api/customer 拉数据再过滤。renderTable 在 then 里调用保证数据回来再渲染。这一步做完原型就成了一个真实的前端项目骨架剩下的就是后端把接口按表字段实现出来。6.2 第二步按业务域抽组件告别复制粘贴原型为了演示方便页面间有不少相似结构是直接复制改的表格、筛选栏、弹窗这些区域。二开时建议先把这些公共部分抽成独立模块。以客户列表和跟进列表为例两处都用了表格渲染可以抽出一个 renderTable 方法接收列定义和数据行统一处理空数据状态、loading 状态和错误提示。组件化按业务域来切而不是按页面切。客户域、跟进域、订单域各维护一套数据操作函数页面只负责组装这些函数这样后续加新页面时大部分逻辑直接复用不用再复制一整个脚本然后改类名。这个项目原有代码里已经有这个倾向getUrlParam 和 storage 就是最早的公共模块二开时沿着这条路继续走就行。6.3 第三步补权限和路由形成完整前端工程真实系统比原型多出来的主要是权限控制和路由管理。登录接口替换完成后从后端拿到的响应里加一个 role 字段前端在跳转时根据 role 判断是否允许进入对应页面。销售漏斗和报表这两个页面对普通顾问隐藏只对管理员开放这个关系在原型数据里已经通过 ACCOUNTS 的 role 字段预留了改造时顺势接上即可。我自己做完这类原型改造后养成了一个习惯每次改完数据层都从头到尾走一遍登录、查客户、新增跟进、看报表的完整链路确认没有任何一处还在读 mockData。有一次漏改了一个页面列表数据源混用了 mockData 和接口返回值页面不报错但新增的数据怎么都不显示花了大半个下午排查才定位到是初始化时把两套数据合并导致主键冲突。从那以后我每次替换数据层都强制全局搜索 mockData 关键字确认清零再进下一步。这份 CRM 原形整体设计紧凑页面职责清晰二开时顺着数据流方向改造是最省力的路径希望帮到你。本文还有配套的精品资源点击获取