CRM客户关系管理系统原型:React+Ant Design+Mock数据实战
发布时间:2026/10/18 18:51:03 锦皓数字建站

简介CRM客户关系管理系统原型面向Web前端开发者、产品设计师及毕业设计学习者集中展示销售、市场与服务模块的交互界面设计思路适合作为企业级管理系统前端的参考样例。原型重点运用JavaScript实现表单验证覆盖邮箱格式、手机号合法性、密码强度等常见校验场景借助jQuery优化DOM操作与事件处理实现下拉菜单、页面切换等流畅交互并通过Ajax技术完成无刷新数据更新如客户信息实时同步、订单状态查询等能够帮助读者快速理解前端技术在CRM场景中的整合方式。资源为RAR压缩包大小约10MB解压后可查看页面草图、交互流程、代码示例及关键注释涵盖客户列表、订单查询、表单提交等多个功能模块的交互细节便于对照学习页面布局与前端实现逻辑。已有301人学习下载适合需要参考完整CRM界面原型或着手构建类似管理系统的初中级开发者。1. CRM客户关系管理系统原形用页面原型先把销售链路讲清楚如果你所在的团队正在做一个 CRM客户关系管理项目最怕的不是写代码而是产品、销售、开发三方对「客户列表长什么样、跟进记录记在哪、商机怎么流动」各说各话。这套 CRM客户关系管理系统原形就是拿来解决这个问题的它不是一份 PPT而是一套可以直接点开、逐页操作的高保真前端页面把客户管理、跟进记录、销售机会看板、数据报表这些核心业务全部做成可交互 DEMO。适合三类人——产品经理拿它当评审底稿售前拿它给客户演示业务流程前端新手拿它练手拆页面结构与组件写法。它不依赖后端服务数据全在本地 Mock意味着你拿到手之后打开浏览器就能把一套完整的销售管理流程从头点到尾。2. 技术选型与工程结构React Ant Design Mock 数据的三层设计2.1 为什么用页面级 Mock 而不是搭后端原型阶段最忌讳一上来就联调接口。后端表结构没定、字段命名还在改前端如果把页面写死在本地变量里后面每改一次字段就要动一遍组件如果去搭一个完整后端又会拖慢原型评审的节奏。这个项目的做法是用 Mock 数据拦截所有 HTTP 请求让页面层完全按照「真实请求 真实返回」的方式工作等后端接口就绪后只需要改掉请求地址。这种取舍很实际——页面代码和数据结构从一开始就是最终的形态而不是临时写死的死数据。我一般会把 Mock 层单独放在一个目录里并且在入口文件中通过一个配置开关控制是否启用。这样以后接真实接口时后端先给一份字段文档前端把 Mock 模板清掉、把开关关掉就行了页面组件一行都不用改。这套原型采用的是主流的中后台技术栈React 18 Vite 构建 Ant Design 组件库 React Router 路由数据请求用 axiosMock 拦截用 mockjs。组件状态没有引入 Redux 这类重型方案原型阶段用组件内 state 加统一 API 层足够撑住所有页面。2.2 目录结构逐层拆解整个工程的结构很清晰拿到手之后先花两分钟把目录过一遍后面改页面的时候就不会迷路├── package.json ├── vite.config.js ├── index.html └── src ├── main.jsx # 入口挂载路由和全局配置 ├── App.jsx # 根组件包含 ConfigProvider 和路由表 ├── mock │ ├── index.js # Mock 开关统一注册所有接口 │ ├── customers.js # 客户列表与详情数据 │ ├── opportunities.js # 销售机会数据 │ └── activities.js # 跟进记录数据 ├── api │ ├── request.js # axios 实例封装 │ ├── customer.js # 客户相关接口 │ └── opportunity.js # 商机相关接口 ├── layouts │ └── BasicLayout.jsx # 侧边栏 顶栏 内容区布局 ├── router │ └── index.jsx # 路由配置与登录守卫 └── pages ├── Login ├── Dashboard ├── Customers │ ├── List │ └── Detail ├── Opportunities ├── Activities └── Reports关键点在于mock和api这两个目录的对应关系api里定义的每一个方法在mock里都有相同 URL 的拦截定义。比如api/customer.js里写着getCustomerList(params)mock/customers.js里就对应注册了Mock.mock(/api/customers, get, ...)。这样做的收益是切换到真实后端时你只需要保证后端接口路径和api目录里的方法保持一致页面层完全不受影响。2.3 路由与登录壳子从登录页到业务页的跳转逻辑这个原型虽然不需要真实鉴权但路由设计是按正式系统来做的。/login是登录页登录表单写死了账号口令后台用 localStorage 存一个模拟 token其余页面都挂在BasicLayout布局下面侧边栏菜单和顶部用户信息在这层统一渲染。路由守卫用了一个很朴素的方式封装一个AuthRoute组件判断 localStorage 里有没有 token没有就跳回登录页。// src/router/index.jsx import { Navigate, useLocation } from react-router-dom; export default function AuthRoute({ children }) { const token localStorage.getItem(crm_token); const location useLocation(); if (!token) { return Navigate to/login replace state{{ from: location }} /; } return children; }这个组件的逻辑很直白从 localStorage 读取crm_token存在就渲染子页面不存在就重定向到登录页并且把来源路径记录在 state 里登录成功之后再跳回原来的页面。很多正式项目的路由守卫也是同样的思路只是会把 token 换成真实接口的校验逻辑。对于原型来说这套代码已经够用了。提示如果后续接真实登录接口只需要在登录页面里把「写死校验」换成「调用后端接口校验」然后让接口返回 token 写入 localStorageAuthRoute 完全不用动。路由表本身也很直观业务页面全部收敛到布局路由下Routes Route path/login element{Login /} / Route path/ element{AuthRouteBasicLayout //AuthRoute} Route index element{Navigate to/dashboard /} / Route pathdashboard element{Dashboard /} / Route pathcustomers element{CustomersList /} / Route pathcustomers/:id element{CustomerDetail /} / Route pathopportunities element{Opportunities /} / Route pathactivities element{Activities /} / Route pathreports element{Reports /} / /Route Route path* element{Navigate to/dashboard /} / /Routes嵌套路由让布局与页面解耦外层的BasicLayout只关心侧边栏和内容区容器内层页面各自维护自己的数据请求。Route pathcustomers/:id是客户详情的动态路由详情页从 URL 参数里读出客户 ID再请求对应的客户资料和跟进记录。这套路由结构哪怕之后要加「销售订单」「售后工单」这类新模块也只需要在布局下新增一个 Route 和对应页面目录。3. 核心模块拆解一个客户从列表到跟进再到商机的完整链路3.1 客户列表页表格、筛选、分页和 Mock 接口怎么对齐客户列表是整个 CRM 的门面。这个页面由三部分组成顶部筛选区域关键字搜索、客户状态下拉、来源下拉、中间的操作栏新增客户按钮、批量操作、主体表格客户名称、联系人、电话、等级、状态、负责人、下次跟进时间、操作列。页面加载时调用getCustomerList接口把筛选条件和分页参数一起传给后端。Mock 接口那边用了一个小技巧来模拟真实分页——数据源是一个固定的数组接口根据page和pageSize做切片返回// src/mock/customers.js let customers []; for (let i 0; i 36; i) { customers.push({ id: i 1, name: cname(), company: city()company(), phone: /^1[3-9]\d{9}$/, level: pick([A, B, C]), status: pick([active, inactive, lost]), owner: cname(), nextFollowAt: date(2024-MM-dd), }); } Mock.mock(/api/customers, get, (options) { const url new URL(options.url, window.location.origin); const page Number(url.searchParams.get(page) || 1); const pageSize Number(url.searchParams.get(pageSize) || 10); const keyword url.searchParams.get(keyword) || ; const status url.searchParams.get(status) || ; let list customers.filter((item) { const matchKeyword item.name.includes(keyword) || item.company.includes(keyword); const matchStatus status ? item.status status : true; return matchKeyword matchStatus; }); return { code: 0, data: { list: list.slice((page - 1) * pageSize, page * pageSize), total: list.length, }, }; });这段 Mock 的核心是拦截函数而不是模板对象options.url可以拿到完整的请求地址借用URL对象解析出查询参数然后对数组做过滤和切片。这里每个字段都有讲究——name用了mockjs的中文名占位符phone用正则占位符生成手机号level从 A/B/C 三个客户等级中随机选一个。注意customers数组定义在Mock.mock外层这一点非常重要如果定义在拦截函数内部每次请求都会重新生成一批新数据而定义在顶层可以实现「会话级持久化」也就是这次页面里新增的客户在本次运行期间刷新列表仍然存在。axios 封装这边同样保持简洁统一在响应拦截器里解包后端返回的结构// src/api/request.js import axios from axios; const request axios.create({ baseURL: /api, timeout: 5000, }); request.interceptors.response.use( (response) { const res response.data; if (res.code ! 0) { return Promise.reject(new Error(res.message || 请求失败)); } return res.data; }, (error) { return Promise.reject(error); } ); export default request;拦截器把{ code, data, message }的包裹结构拆掉最终返回给页面的是纯净的业务数据。这样的设计让页面组件调用接口时不用每次手动判断错误码错误提示统一在拦截器里处理。如果后端接口的返回结构不是{ code, data }只需要改这一处拦截器就可以全局兼容。baseURL设置为/api后页面里请求/customers最终发出去的是/api/customers和 Mock 注册的地址正好对上。3.2 客户详情页与跟进记录时间线 新增记录的交互闭环客户详情页是这个原型里信息密度最高的页面。上半部分是客户基础信息和关键统计卡片客户等级、所属行业、当前状态、累计成交金额下半部分左栏是跟进记录时间线右栏是联系人列表和操作面板。跟进记录按时间倒序排列每条记录包含跟进方式电话、拜访、微信、跟进内容、下次跟进时间、记录人。新增跟进记录通过一个 Modal 弹窗完成提交后调用addFollowUp接口。// src/api/customer.js import request from ./request; export function getCustomerDetail(id) { return request.get(/customers/${id}); } export function getFollowUps(id) { return request.get(/customers/${id}/followups); } export function addFollowUp(id, data) { return request.post(/customers/${id}/followups, data); }跟进记录是 CRM 里最容易做乱的功能这个原型用「客户详情页内嵌时间线」的方式把关系理清了客户是主体跟进是挂在客户下的时间流。getFollowUps按客户 ID 拉取该客户的全部跟进记录addFollowUp提交新记录后页面不刷新整个详情页而是把接口返回的新记录unshift到现有的时间线数组顶部。注意这里我没有用「重新拉全量列表」的方式因为每次刷新整个客户详情会让页面弹一下而且用户体验差局部追加数据明显更顺滑。提交新增跟进时表单里会带上一个nextFollowAt字段这个字段同时会回写到客户表里的nextFollowAt也就是列表页表格中的「下次跟进时间」列。这样客户列表、详情、跟进记录三处数据就形成了一个闭环在详情页安排了一次跟进回到客户列表就能看到这条客户的下一步动作时间被更新了。原型阶段用这种「内存级同步」已经足够接真实后端后这个逻辑对应的是两张表的事务更新。3.3 销售机会看板用阶段列把签单漏斗撑起来销售机会模块是整个原型里最直观展示「销售流程」的地方。它把商机按阶段分成四列初步接洽、需求确认、方案报价、赢单每一列是一个卡片容器商机卡片按金额从上往下排。看板顶部是各阶段金额汇总末尾是「输单」统计方便售前一眼看出哪一阶段卡住了最多的金额。// src/pages/Opportunities/index.jsx const stages [ { key: contact, name: 初步接洽, color: blue }, { key: needs, name: 需求确认, color: cyan }, { key: quotation, name: 方案报价, color: orange }, { key: won, name: 赢单, color: green }, ]; const grouped stages.map((stage) ({ ...stage, list: opportunities.filter((item) item.stage stage.key), }));看板页的数据源是getOpportunityList接口返回的全部商机前端按stage字段分组渲染四列。每一个商机卡片上展示客户名称、商机名称、预计金额、当前阶段、赢单概率卡片底部的「推进阶段」按钮调用updateOpportunityStage(id)接口把商机往后推一个阶段。当商机推进到「赢单」时赢单金额会计入客户详情页的「累计成交金额」统计卡片。这个模块设计成看板而不是表格是有意为之——销售管理者的核心诉求是「看哪里卡住了」表格给的是明细看板给的才是节奏。原型阶段用前端过滤分组是最快的实现方式数据量大了之后再改成后端按阶段统计接口也不迟。4. 本地启动与常见问题排查从 install 到浏览器里点完一遍4.1 启动步骤环境要求与三条命令拿到源码包之后本地跑起来的门槛很低。环境方面只需要 Node.js 16 或以上版本包管理器用 npm 或 pnpm 都行。整个启动过程就是三条命令先安装依赖再启动开发服务器最后浏览器打开 Vite 打印出来的地址。登录页写死的账号口令统一在src/mock/index.js顶部注释里标注页面上的提示文字也会写明当前使用的是模拟账号。npm install npm run dev # Vite 默认起在 http://localhost:3000npm install需要一点耐心Ant Design 和配套组件体积不小。如果安装过程中报依赖冲突常见原因是 Node 版本太旧建议先用node -v检查版本再重新安装。启动成功后浏览器打开页面左侧菜单栏会列出仪表盘、客户管理、销售机会、跟进记录、报表五个模块登录后默认落在仪表盘页。仪表盘页放了几个统计卡片和一组 Mock 出来的趋势图表这些数据同样来自 Mock 接口不是为了好看硬编码在页面里的。4.2 配置说明端口、代理和 Mock 开关开发端口和代理配置集中在vite.config.js里。原型阶段因为所有请求都被 mockjs 拦截不需要配置代理但后端接口就绪后你需要在server.proxy里把/api转发到真实服务地址。这个切换过程我已经验证过后端先启动在本地 8080 端口然后 Vite 配置里加一段转发规则同时把 Mock 开关关掉页面不需要任何改动。// vite.config.js export default defineConfig({ plugins: [react()], resolve: { alias: { : /src }, }, server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, }, }, }, });Mock 开关在src/mock/index.js里通过一个常量控制USE_MOCK true时入口文件会注册 mockjs 拦截所有请求走本地数据改成false并重启开发服务器后axios 请求会直接打到vite.config.js里配置的代理地址。这里要提醒一句修改 Mock 开关后必须重启 dev server 才会生效因为 mockjs 的注册动作发生在模块加载阶段单纯刷新页面不会重新加载入口模块。4.3 排查记录原型阶段最容易翻车的 4 个问题现象一登录之后刷新页面直接白屏控制台报「Cannot GET /customers」。原因路由用了 BrowserRouter地址是真实的 URL 路径开发服务器如果没有把所有路由 fallback 到 index.html刷新就找不到页面。解决这个原型在vite.config.js里已经配置了appType: spa的默认处理如果你自己新建项目遇到这个问题最简单的做法是改用 HashRouter地址变成/#/customers刷新永远不会丢失。现象二接口请求发出去了但控制台显示 404Mock 没有拦截到。原因axios 的baseURL是/api页面里请求/customers最终发到/api/customers但 Mock 里注册的地址写成了/customers少了前缀。解决所有 Mock 接口注册时必须带上/api前缀这是这个项目里最容易踩的坑因为api目录下的函数名和 Mock 注册地址是分开维护的改了一处忘了另一处就会翻车。现象三新增了一条客户记录刷新页面之后又没了。原因mockjs 的模板数据在页面刷新后重新生成如果customers数组定义在拦截函数内部每次请求都会重新初始化。解决把数据源数组提升到模块顶层像 3.1 节代码里那样在Mock.mock外层定义let customers []这样数据能活到整个 dev server 生命周期结束。现象四antd 组件里日期显示成英文格式分页器文案也是英文。原因Ant Design 默认使用英文语言包中文文案需要手动配置。解决在src/main.jsx入口文件里用ConfigProvider包裹根组件locale传入zhCN同时引入 dayjs 的中文语言包。这也是所有 antd 项目起步必做的一步我每次新建项目都先加这一层再写业务代码。5. 二次开发路线图把原型逐步换成真实接口的四个落点5.1 数据层把 mockjs 数组抽成 JSON 直接喂给 json-server第一件值得做的事是把 Mock 数据源从内存数组改成 JSON 文件这样后端表结构未定之前前端就能跑「真实接口」。常见做法是直接用 json-server把src/mock/customers.js里的数据数组导出成db.json然后npx json-server db.json --port 4000再把 vite 代理指向 4000。这样做的好处是数据能持久化新增、修改、删除都会落到 JSON 文件里调试体验比内存版好一个档次。5.2 接口层在 axios 封装里补上 token 和统一错误提示原型里的request.js只做了响应解包接真实后端后需要加两样东西请求拦截器里从localStorage取 token 塞进Authorization请求头响应拦截器里对 401 做统一跳转登录页处理。这一步改动只发生在src/api/request.js一个文件里所有业务接口自动生效这也是当初把 API 层单独抽出来的红利。5.3 验证方式按「新增客户 → 跟进 → 推进商机 → 赢单」走一遍冒烟流程改造完成后我习惯用一条业务主链路做回归验证新增一个客户进详情页添加一条跟进记录再把客户关联的商机从「初步接洽」一路推到「赢单」最后回客户列表确认「下次跟进时间」和「累计成交金额」已经更新。这条链路覆盖了列表、详情、跟进、看板四个核心页面只要它能走通数据层换成真实接口基本没有大问题。从那以后我每次接手这种带 Mock 的 CRM 原型都会先把数据源从内存数组挪到独立文件再跑一遍主链路确认所有字段在页面间没有丢失最后才敢动组件代码。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。