若依 App 框架深度定制:从脚手架到 CRM 产品
若依的 App 框架uni-app 版给了一套移动端脚手架——登录、主页、个人中心都有了。但从脚手架到真正能打的产品中间有好几个关键步骤。这篇文章用真实代码讲清楚每一步怎么做、为什么这么做。一、若依 App 框架给了什么若依 uni-app 版RuoYi-App是基于 uni-app uView UI 的移动端框架开箱提供功能说明登录/注册手机号 验证码方式Token 管理自动注入 过期处理TabBar 主页首页、工作台、消息、我的个人信息头像、昵称、修改密码请求封装统一的 request.jsuView UI一套移动端组件库这套脚手架节约了大约2 周的开发时间——不用从零搭登录、权限和基础布局。二、定制第一步换皮——从若依风格到 MqCode 风格2.1 替换主题色// uni.scss // 把若依默认的蓝色换成我们的品牌色 $u-primary: #1A73E8; // 主题色 $u-warning: #FF9500; $u-success: #34C759; $u-danger: #FF3B30; $u-info: #8E8E93; // 全局背景 $u-bg-color: #F2F2F7;2.2 替换 TabBar// pages.json { tabBar: { color: #8E8E93, selectedColor: #1A73E8, list: [ { pagePath: pages/index/index, text: 工作台, iconPath: static/tab/workbench.png, selectedIconPath: static/tab/workbench-active.png }, { pagePath: pages/customer/list, text: 客户, iconPath: static/tab/customer.png, selectedIconPath: static/tab/customer-active.png }, { pagePath: pages/message/list, text: 消息, iconPath: static/tab/message.png, selectedIconPath: static/tab/message-active.png }, { pagePath: pages/mine/index, text: 我的, iconPath: static/tab/mine.png, selectedIconPath: static/tab/mine-active.png } ] } }2.3 替换启动页和引导页static/logo.png换成 MqCode 的 logopages.json里配置启动图。三、定制第二步首页工作台CRM 用户打开 App 的第一眼应该是工作台不是欢迎页。!-- pages/index/index.vue -- template view classworkbench !-- 用户信息卡片 -- view classuser-card image :srcuserInfo.avatar classavatar / view classuser-info text classname{{ userInfo.nickName }}/text text classdept{{ userInfo.dept?.deptName }}/text /view view classtoday-stats text classnumber{{ todayVisits }}/text text classlabel今日拜访/text /view /view !-- 快捷入口 -- view classshortcuts view classshortcut-item v-foritem in shortcuts :keyitem.key clickgoPage(item.path) u-icon :nameitem.icon size28 :coloritem.color / text{{ item.label }}/text /view /view !-- 待办提醒 -- view classtodos view classsection-title待办事项/view view classtodo-item v-foritem in todos :keyitem.id u-tag :textitem.type :typeitem.tagType sizemini / text{{ item.title }}/text /view /view /view /template四、定制第三步客户列表——移动端的核心页面CRM 移动端最重要的页面是客户列表销售要在拜访路上快速查找和筛选客户。!-- pages/customer/list.vue -- template view classcustomer-list !-- 搜索栏固定在顶部 -- view classsearch-bar u-search v-modelkeyword placeholder搜索客户 searchhandleSearch / u-icon namefilter clickshowFilter true / /view !-- 客户卡片列表 -- scroll-view scroll-y classlist scrolltolowerloadMore view classcustomer-card v-foritem in list :keyitem.id clickgoDetail(item.id) view classcard-header text classname{{ item.name }}/text u-tag :textitem.level :typelevelMap[item.level] sizemini / /view view classcard-body text classcontact{{ item.contact }} {{ item.phone }}/text text classaddress {{ item.address }}/text /view view classcard-footer text上次跟进{{ item.lastFollowTime }}/text u-icon namearrow-right / /view /view !-- 加载更多 -- u-loadmore :statusloadStatus / /scroll-view !-- 筛选面板 -- u-popup v-modelshowFilter moderight view classfilter-panel text classtitle筛选条件/text u-form u-form-item label客户等级 dict-select v-modelfilter.level dict-typecustomer_level / /u-form-item u-form-item label客户状态 dict-select v-modelfilter.status dict-typecustomer_status / /u-form-item /u-form u-button typeprimary clickapplyFilter应用/u-button /view /u-popup /view /template script setup import { ref, onMounted } from vue import { listCustomer } from /api/crm/customer import { useDictStore } from /stores/dict const keyword ref() const list ref([]) const page ref(1) const loadStatus ref(more) const showFilter ref(false) const filter ref({}) const dictStore useDictStore() async function fetchData(reset false) { if (reset) { page.value 1; list.value [] } loadStatus.value loading const { rows, total } await listCustomer({ pageNum: page.value, pageSize: 10, name: keyword.value, ...filter.value }) list.value reset ? rows : [...list.value, ...rows] loadStatus.value list.value.length total ? noMore : more } function loadMore() { if (loadStatus.value noMore) return page.value fetchData() } function handleSearch() { fetchData(true) } function applyFilter() { showFilter.value false fetchData(true) } onMounted(() fetchData()) /script五、定制第四步客户详情——信息 地图!-- pages/customer/detail.vue -- template view classcustomer-detail !-- 基本信息 -- view classinfo-card text classname{{ customer.name }}/text text classlevel u-tag :textcustomer.levelName / /text /view !-- 地图位置 -- view classmap-card map :latitudecustomer.latitude :longitudecustomer.longitude :markersmarkers scale15 / /view !-- 跟进记录 -- view classfollow-list view classsection-title跟进记录/view timeline :listfollowRecords / /view !-- 底部操作栏 -- view classbottom-bar u-button typeprimary iconphone clickcallPhone拨号/u-button u-button typesuccess iconedit-pen clickaddFollow写跟进/u-button u-button iconmap clickopenMap导航/u-button /view /view /template六、定制第五步数据看板——移动端的经营驾驶舱CRM 不能只是录入工具管理者需要随时掌握团队数据。移动端看板是老板用得最多的功能。!-- pages/dashboard/index.vue -- template view classdashboard !-- 核心指标卡片 -- view classkpi-row view classkpi-card v-foritem in kpiList :keyitem.label clickgoDetail(item.type) text classkpi-value{{ item.value }}/text text classkpi-label{{ item.label }}/text view classkpi-trend :classitem.trend 0 ? up : down u-icon :nameitem.trend 0 ? arrow-up : arrow-down size12 / text{{ Math.abs(item.trend) }}%/text /view /view /view !-- 销售漏斗使用 uCharts 渲染 -- view classchart-card text classsection-title销售漏斗/text qiun-data-charts typefunnel :chartDatafunnelData :opts{ color: [#1A73E8,#4A90D9,#7AB8F5,#A8D4FF] } canvasIdfunnel / /view !-- 团队排行 -- view classrank-card text classsection-title今日拜访排行/text view classrank-item v-for(item, idx) in rankList :keyitem.userId text classrank-num :class{ top3: idx 3 }{{ idx 1 }}/text u-avatar :srcitem.avatar size32 / text classrank-name{{ item.nickName }}/text text classrank-value{{ item.visitCount }}次/text /view /view /view /template script setup import { ref, onMounted } from vue import { getDashboardSummary, getFunnelData, getVisitRank } from /api/crm/dashboard const kpiList ref([ { label: 今日新增客户, value: 0, trend: 0, type: new }, { label: 今日拜访量, value: 0, trend: 0, type: visit }, { label: 本月成交额, value: 0万, trend: 0, type: amount }, { label: 待跟进客户, value: 0, trend: 0, type: follow } ]) const funnelData ref({}) const rankList ref([]) onMounted(async () { const [summary, funnel, rank] await Promise.all([ getDashboardSummary(), getFunnelData(), getVisitRank() ]) kpiList.value summary funnelData.value funnel rankList.value rank }) /script style langscss scoped .kpi-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 16px; .kpi-card { background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.04); .kpi-value { font-size: 28px; font-weight: 700; color: #1A1A1A; } .kpi-trend { font-size: 12px; .up { color: #34C759; } .down { color: #FF3B30; } } } } /style为什么移动端看板比 PC 端更重要老板和管理者大部分时间在手机上看数据PC 端看板的人远少于移动端。把关键指标做成 4~6 张卡片一眼看完比什么花哨图表都实用。七、定制第六步扫码能力——打通线上线下印刷包装行业的客户经常有纸质合同、送货单扫码能大幅提升录入效率。!-- pages/scan/scan.vue -- template view classscan-page camera device-positionback flashoff erroronCameraError /camera !-- 扫描框 -- view classscan-box view classscan-line/view /view view classscan-tips将二维码/条形码放入框内即可自动扫描/view /view /template script setup import { onScanCode } from /utils/scan // uni-app 的扫码能力 function startScan() { uni.scanCode({ scanType: [qrCode, barCode, datamatrix], success(res) { const code res.result // 根据前缀判断类型 if (code.startsWith(ORDER-)) { // 订单二维码 → 跳转订单详情 const orderId code.replace(ORDER-, ) uni.navigateTo({ url: /pages/order/detail?id${orderId} }) } else if (code.startsWith(CUST-)) { // 客户二维码 → 跳转客户详情 const custId code.replace(CUST-, ) uni.navigateTo({ url: /pages/customer/detail?id${custId} }) } else { // 通用条形码 → 搜索 uni.navigateTo({ url: /pages/search/index?code${code} }) } } }) } // 也可以调用相机拍照后 OCR 识别名片 async function scanBusinessCard(imagePath) { const result await uploadAndOCR(imagePath) // result { name: 张三, phone: 138..., company: XX印刷厂 } // 自动预填新建客户表单 uni.navigateTo({ url: /pages/customer/add?prefill${encodeURIComponent(JSON.stringify(result))} }) } /script style langscss scoped .scan-page { position: relative; height: 100vh; background: #000; camera { width: 100%; height: 100%; } .scan-box { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 260px; height: 260px; border: 2px solid rgba(26, 115, 232, 0.8); border-radius: 12px; } .scan-line { width: 100%; height: 2px; background: #1A73E8; animation: scanAnim 2s ease-in-out infinite; } keyframes scanAnim { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(256px); } } .scan-tips { position: absolute; bottom: 80px; width: 100%; text-align: center; color: rgba(255, 255, 255, 0.7); font-size: 14px; } } /styleuni-app 内置的uni.scanCode已经支持二维码、条形码、Data Matrix 等多种格式。如果再接入 OCR API拍张名片就能自动创建客户——对印刷行业的销售来说这是最能打动人的功能。八、定制清单总览步骤内容预计工时1换品牌色 Logo 启动页2h2TabBar 换成业务功能客户、工单1h3首页工作台统计卡片 待办4h4客户列表搜索 筛选 卡片4h5客户详情信息 地图 跟进记录4h6移动端数据看板KPI 漏斗 排行5h7消息推送App 小程序 公众号3h8扫码能力名片识别 合同二维码3h9权限控制不同角色看不同菜单2h10打包发布1h合计~29h约 4 天九、总结若依 App 框架的价值是让你从30%的进度开始而不是从 0%。但剩下的 70% 才是产品真正产生价值的部分——而且这 70% 才是用户真正感知到的软件好不好用。脚手架帮你省掉了基础设施的时间省下来的时间应该花在业务体验上——把数据看板做好、把扫码做流畅、把推送做精准这些才是打动用户的点。如果你也在独立开发产品或者对制造业数字化感兴趣欢迎关注这个公众号。我会持续分享从代码到产品的全过程——包括成功的经验也包括踩过的坑。一个人的产品之路不孤单。原创作者 MqCode全栈开发者印刷包装行业 MESCRM 系统独立开发欢迎自由转发。