若依的 App 框架(uni-app 版)给了一套移动端脚手架——登录、主页、个人中心都有了。但从脚手架到真正能打的产品,中间有好几个关键步骤。这篇文章用真实代码讲清楚每一步怎么做、为什么这么做。
一、若依 App 框架给了什么
若依 uni-app 版(RuoYi-App)是基于 uni-app + uView UI 的移动端框架,开箱提供:
| 功能 | 说明 |
|---|---|
| 登录/注册 | 手机号 + 验证码方式 |
| Token 管理 | 自动注入 + 过期处理 |
| TabBar 主页 | 首页、工作台、消息、我的 |
| 个人信息 | 头像、昵称、修改密码 |
| 请求封装 | 统一的 request.js |
| uView 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 的 logo,pages.json里配置启动图。
三、定制第二步:首页工作台
CRM 用户打开 App 的第一眼应该是工作台,不是欢迎页。
<!-- pages/index/index.vue --> <template> <view class="workbench"> <!-- 用户信息卡片 --> <view class="user-card"> <image :src="userInfo.avatar" class="avatar" /> <view class="user-info"> <text class="name">{{ userInfo.nickName }}</text> <text class="dept">{{ userInfo.dept?.deptName }}</text> </view> <view class="today-stats"> <text class="number">{{ todayVisits }}</text> <text class="label">今日拜访</text> </view> </view> <!-- 快捷入口 --> <view class="shortcuts"> <view class="shortcut-item" v-for="item in shortcuts" :key="item.key" @click="goPage(item.path)"> <u-icon :name="item.icon" size="28" :color="item.color" /> <text>{{ item.label }}</text> </view> </view> <!-- 待办提醒 --> <view class="todos"> <view class="section-title">待办事项</view> <view class="todo-item" v-for="item in todos" :key="item.id"> <u-tag :text="item.type" :type="item.tagType" size="mini" /> <text>{{ item.title }}</text> </view> </view> </view> </template>四、定制第三步:客户列表——移动端的核心页面
CRM 移动端最重要的页面是客户列表,销售要在拜访路上快速查找和筛选客户。
<!-- pages/customer/list.vue --> <template> <view class="customer-list"> <!-- 搜索栏(固定在顶部) --> <view class="search-bar"> <u-search v-model="keyword" placeholder="搜索客户" @search="handleSearch" /> <u-icon name="filter" @click="showFilter = true" /> </view> <!-- 客户卡片列表 --> <scroll-view scroll-y class="list" @scrolltolower="loadMore"> <view class="customer-card" v-for="item in list" :key="item.id" @click="goDetail(item.id)"> <view class="card-header"> <text class="name">{{ item.name }}</text> <u-tag :text="item.level" :type="levelMap[item.level]" size="mini" /> </view> <view class="card-body"> <text class="contact">{{ item.contact }} {{ item.phone }}</text> <text class="address">📍 {{ item.address }}</text> </view> <view class="card-footer"> <text>上次跟进:{{ item.lastFollowTime }}</text> <u-icon name="arrow-right" /> </view> </view> <!-- 加载更多 --> <u-loadmore :status="loadStatus" /> </scroll-view> <!-- 筛选面板 --> <u-popup v-model="showFilter" mode="right"> <view class="filter-panel"> <text class="title">筛选条件</text> <u-form> <u-form-item label="客户等级"> <dict-select v-model="filter.level" dict-type="customer_level" /> </u-form-item> <u-form-item label="客户状态"> <dict-select v-model="filter.status" dict-type="customer_status" /> </u-form-item> </u-form> <u-button type="primary" @click="applyFilter">应用</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 class="customer-detail"> <!-- 基本信息 --> <view class="info-card"> <text class="name">{{ customer.name }}</text> <text class="level"> <u-tag :text="customer.levelName" /> </text> </view> <!-- 地图位置 --> <view class="map-card"> <map :latitude="customer.latitude" :longitude="customer.longitude" :markers="markers" scale="15" /> </view> <!-- 跟进记录 --> <view class="follow-list"> <view class="section-title">跟进记录</view> <timeline :list="followRecords" /> </view> <!-- 底部操作栏 --> <view class="bottom-bar"> <u-button type="primary" icon="phone" @click="callPhone">拨号</u-button> <u-button type="success" icon="edit-pen" @click="addFollow">写跟进</u-button> <u-button icon="map" @click="openMap">导航</u-button> </view> </view> </template>六、定制第五步:数据看板——移动端的经营驾驶舱
CRM 不能只是"录入工具",管理者需要随时掌握团队数据。移动端看板是老板用得最多的功能。
<!-- pages/dashboard/index.vue --> <template> <view class="dashboard"> <!-- 核心指标卡片 --> <view class="kpi-row"> <view class="kpi-card" v-for="item in kpiList" :key="item.label" @click="goDetail(item.type)"> <text class="kpi-value">{{ item.value }}</text> <text class="kpi-label">{{ item.label }}</text> <view class="kpi-trend" :class="item.trend > 0 ? 'up' : 'down'"> <u-icon :name="item.trend > 0 ? 'arrow-up' : 'arrow-down'" size="12" /> <text>{{ Math.abs(item.trend) }}%</text> </view> </view> </view> <!-- 销售漏斗(使用 uCharts 渲染) --> <view class="chart-card"> <text class="section-title">销售漏斗</text> <qiun-data-charts type="funnel" :chartData="funnelData" :opts="{ color: ['#1A73E8','#4A90D9','#7AB8F5','#A8D4FF'] }" canvasId="funnel" /> </view> <!-- 团队排行 --> <view class="rank-card"> <text class="section-title">今日拜访排行</text> <view class="rank-item" v-for="(item, idx) in rankList" :key="item.userId"> <text class="rank-num" :class="{ top3: idx < 3 }">{{ idx + 1 }}</text> <u-avatar :src="item.avatar" size="32" /> <text class="rank-name">{{ item.nickName }}</text> <text class="rank-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 lang="scss" 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 class="scan-page"> <camera device-position="back" flash="off" @error="onCameraError"> </camera> <!-- 扫描框 --> <view class="scan-box"> <view class="scan-line"></view> </view> <view class="scan-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 lang="scss" 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; } } </style>uni-app 内置的uni.scanCode已经支持二维码、条形码、Data Matrix 等多种格式。如果再接入 OCR API,拍张名片就能自动创建客户——对印刷行业的销售来说,这是最能打动人的功能。
八、定制清单总览
| 步骤 | 内容 | 预计工时 |
|---|---|---|
| 1 | 换品牌色 + Logo + 启动页 | 2h |
| 2 | TabBar 换成业务功能(客户、工单) | 1h |
| 3 | 首页工作台(统计卡片 + 待办) | 4h |
| 4 | 客户列表(搜索 + 筛选 + 卡片) | 4h |
| 5 | 客户详情(信息 + 地图 + 跟进记录) | 4h |
| 6 | 移动端数据看板(KPI + 漏斗 + 排行) | 5h |
| 7 | 消息推送(App + 小程序 + 公众号) | 3h |
| 8 | 扫码能力(名片识别 + 合同二维码) | 3h |
| 9 | 权限控制(不同角色看不同菜单) | 2h |
| 10 | 打包发布 | 1h |
| 合计 | ~29h(约 4 天) |
九、总结
若依 App 框架的价值是让你从30%的进度开始,而不是从 0%。但剩下的 70% 才是产品真正产生价值的部分——而且这 70% 才是用户真正感知到的"软件好不好用"。
脚手架帮你省掉了基础设施的时间,省下来的时间应该花在业务体验上——把数据看板做好、把扫码做流畅、把推送做精准,这些才是打动用户的点。
如果你也在独立开发产品,或者对制造业数字化感兴趣,欢迎关注这个公众号。我会持续分享从代码到产品的全过程——包括成功的经验,也包括踩过的坑。
一个人的产品之路,不孤单。👇
原创作者 MqCode(全栈开发者,印刷包装行业 MES+CRM 系统独立开发),欢迎自由转发。