简介: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 路由,数据请求用 axios,Mock 拦截用 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 写入 localStorage,AuthRoute 完全不用动。
路由表本身也很直观,业务页面全部收敛到布局路由下:
<Routes> <Route path="/login" element={<Login />} /> <Route path="/" element={<AuthRoute><BasicLayout /></AuthRoute>}> <Route index element={<Navigate to="/dashboard" />} /> <Route path="dashboard" element={<Dashboard />} /> <Route path="customers" element={<CustomersList />} /> <Route path="customers/:id" element={<CustomerDetail />} /> <Route path="opportunities" element={<Opportunities />} /> <Route path="activities" element={<Activities />} /> <Route path="reports" element={<Reports />} /> </Route> <Route path="*" element={<Navigate to="/dashboard" />} /> </Routes>嵌套路由让布局与页面解耦:外层的BasicLayout只关心侧边栏和内容区容器,内层页面各自维护自己的数据请求。Route path="customers/: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,刷新永远不会丢失。
现象二:接口请求发出去了,但控制台显示 404,Mock 没有拦截到。原因: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 原型,都会先把数据源从内存数组挪到独立文件,再跑一遍主链路,确认所有字段在页面间没有丢失,最后才敢动组件代码。希望帮到你。
本文还有配套的精品资源,点击获取