☰
无后端CRM客户关系管理原型:可交互业务流程设计与实现
2026/10/18 8:46:02 网站建设 项目流程

简介:这份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?id=1001。详情页脚本读取这个参数后到数据源里匹配对应客户并渲染。这种传参方式在后端还没有就绪时,是最贴近真实前后端联调习惯的做法,后面换接口时改动最小。

3. 核心代码逐段拆解:登录、客户列表与跟进记录的实现思路

3.1 登录页:校验逻辑与状态写入

登录页用的是最基础的表单提交方式,没有引入重量级框架。HTML 部分保持干净,把所有判断逻辑放到 login.js 里统一处理:

<form id="loginForm" class="login-box"> <h2>客户管理系统</h2> <input type="text" id="username" placeholder="账号" autocomplete="off"> <input type="password" id="password" placeholder="密码"> <button type="submit">登 录</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> <td><span class="level-tag level-${item.level}">${item.level}</span></td> <td>${item.phone}</td> <td>${item.owner}</td> <td>${item.status}</td> <td><a href="customer-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 class="timeline-item"> <div class="timeline-time">${item.followTime}</div> <div class="timeline-type">${item.type}</div> <div class="timeline-content">${item.content}</div> <div class="timeline-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-DD,slic(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 表的主键是 id,followUp 表通过 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 原形整体设计紧凑,页面职责清晰,二开时顺着数据流方向改造是最省力的路径,希望帮到你。

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

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

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

立即咨询