阅读视图

发现新文章,点击刷新页面。

h5移动端项目总结

移动端项目总结

项目名称:金贝移动端(fe-banner-pub-mobile)
项目定位:面向经纪人的商机购买与管理 H5 平台


📋 项目概述

金贝移动端是一个面向房产经纪人的综合性商机管理平台,提供潜客包、准客包、钻展、商机直通车、CPL/CPT、金贝会员等多种商机产品的购买、管理和跟进服务。项目采用前后端分离架构,前端使用 React 技术栈,后端使用 Node.js 中间层,支持多端(APP 内嵌、H5)访问。

核心业务模块

  • 潜客包/准客包管理:商机包的购买、查看、跟进、转委托、退名额等全流程管理
  • 钻展(抢购、报买):广告位购买与管理
  • 商机直通车:CPA 商机购买
  • CPL/CPT:按线索/按时间计费的商机包
  • 金贝会员:会员权益管理
  • 订单管理:订单列表、详情、支付
  • 数据看板:商机数据统计与分析
  • 预算池:预算管理与分配

🛠️ 技术栈

前端技术栈

技术 版本 用途
React ^16.8.6 UI 框架
Redux ^4.0.1 状态管理
React Router ^5.1.0 路由管理
antd-mobile ^2.3.1 / ^5.34.0 UI 组件库(v2 + v5 混用)
dayjs ^1.8.8 日期时间处理
axios ^0.19.2 HTTP 请求库
webpack ^5.75.0 构建工具
less ^4.1.3 CSS 预处理器
react-hot-loader ^4.3.12 热更新
@antv/f2 ^3.8.6 移动端图表库

后端技术栈

  • Node.js (v12)
  • TypeScript
  • Express/Koa (中间层)
  • Redis (缓存)

开发工具

  • Babel:ES6+ 转译
  • ESLint:代码规范
  • PostCSS:CSS 后处理(px2rem、px-to-viewport)
  • vconsole:移动端调试工具

第三方服务集成


✨ 项目亮点

1. 完整的商机处理闭环设计

业务价值:实现了从商机领取到关闭的全流程管理,提升经纪人工作效率。

技术实现

  • 统一的交互入口:通过 bottomAreaClick 方法统一处理所有商机操作
  • 多场景适配:根据商机来源(opptySource)、类型(新房/二手/租赁)、状态(opptyStatus)动态展示操作按钮
  • 操作类型包括:
    • 加私/转委托:根据 conversionType 动态展示,支持预校验和跳转
    • 联系:区分电话(contactType === 1)和 IM(contactType === 2
    • 记录跟进:弹窗录入,支持局部数据更新
    • 申请退名额:复杂的时间规则和风控校验
    • 查看工单:跳转司南工单系统

代码示例

bottomAreaClick = (e, item, desc, callBack) => {
  const { opptyId, opptyProcessVo, custId } = item
  const { contactType, conversionType } = opptyProcessVo
  e.stopPropagation()
  switch (desc) {
    case '加私':
      this.openXinfang(conversionPageScheme, opptyId)
      break
    case '联系':
      contactType == 2 ? this.imCustomer(custId, opptyId) : this.callTel(opptyId)
      break
    // ... 其他操作
  }
}

2. 复杂状态标签体系的可视化呈现

业务价值:通过标签系统清晰展示商机状态,降低业务理解成本。

技术实现

  • 动态标签生成:根据 conventionLevelisGivenisWeihupan 等状态动态拼装标签数组
  • 交互式标签:点击特定标签(赠送、已联系、未联系、已委托)显示业务提示
  • 样式差异化:不同标签使用不同颜色和样式(如 赠送 红色、维护盘商机 金色背景)

代码示例

// 标签动态生成
if (isGiven && Array.isArray(tags)) {
  tags.unshift('赠送')
}
if (+conventionLevel === -1) {
  tags.push('待委托')
}
if (+conventionLevel === 0) {
  tags.push('已联系')
}
if (+conventionLevel === 1) {
  tags.push('已委托')
}

// 标签点击交互
isGivenClick = (evt, item, shangjiItem) => {
  if (item === '已联系') {
    if (shangjiItem.opptySource === '400') {
      Toast.info('您已拨打过客户电话', 2)
    } else {
      Toast.info('您已回复过客户消息', 2)
    }
  }
}

3. 多端跳转与埋点闭环

业务价值:打通 APP、H5、工单系统等多个平台,实现数据追踪和运营分析。

技术实现

  • 统一埋点封装sendDig(clickId, opptyId) 方法统一上报点击事件
  • 多端跳转封装:通过 Utils 工具类封装拨打电话、IM 联系、跳转客户端等功能
  • 环境区分:根据 window.location.host 区分测试和正式环境,使用不同的跳转链接
  • 埋点数据:包含 opptyIdagent_ucidclick_idc_uicode 等关键字段

代码示例

sendDig(clickId, opptyId) {
  if (window.$ULOG) {
    window.$ULOG.send(this.evtId, {
      event: 'mModuleClick',
      action: {
        opptyId,
        c_uicode: 'qiankebao_qiankebaoliebiao',
        click_id: clickId,
        agent_ucid: window._GLOBAL_DATA.userInfo.id,
      },
    })
  }
}

4. 列表性能与体验优化

业务价值:提升移动端加载速度和用户体验,降低服务器压力。

技术实现

  • 无限滚动:使用 antd-mobile-v5InfiniteScroll 组件实现分页加载
  • 滚动位置保持:记录跟进后恢复滚动位置,避免页面跳回顶部
  • 局部数据更新:通过回调函数 addTempRecordData 更新列表中的单条数据,避免全量刷新
  • 按需插入数据:根据权限和数据返回情况动态插入数据概览卡片

代码示例

// 无限滚动
<InfiniteScroll
  loadMore={() => {
    this.getPackages()
  }}
  hasMore={this.state.packages.pageNum < this.state.packages.totalPage}
  threshold={120}
/>

// 滚动位置保持
record = (data) => {
  this.setState({ textareaModal: false })
  KeFetch(api.saveRecord, { method: 'post', data }).then(() => {
    document.documentElement.scrollTo(0, this.ScroolTop)
    this.genjiCallBack && this.genjiCallBack(data)
  })
}

5. 统一请求封装与错误处理

业务价值:统一 API 调用规范,提升代码可维护性和错误处理能力。

技术实现

  • KeFetch 封装:基于 Ketch 库封装统一请求方法
  • 多环境适配:支持 Node 层(SUCCESS_CODE_NODE: 100000)和 H5 层(SUCCESS_CODE_H5: 1)不同的成功码
  • 统一错误提示:自动处理错误码并展示 Toast 提示
  • 超时控制:默认 20s 超时

🎯 项目难点

难点一:多状态、多来源商机的分支逻辑复杂

问题描述

  • 不同商机来源(OPPTY_POOLSSCCUSTOMER_CLUE400 等)有不同的处理逻辑
  • 不同商机类型(新房/二手/租赁)需要不同的跳转路径
  • 不同场景(OPPTYZHUN_KE_BAOB_PLUSCPS)影响功能展示

解决方案

  1. 统一入口函数:通过 bottomAreaClick 方法统一处理所有操作,内部根据 desc 参数分支处理
  2. 预校验封装:将复杂的校验逻辑抽离成独立方法(如 delegatePrecheckrefundPrecheck
  3. 状态机模式:使用 switch-case 清晰表达不同操作的处理流程
  4. 配置化:通过 getOpptyType() 方法统一获取场景类型,避免重复判断

代码示例

getOpptyType = () => {
  const { type } = Utils.getUrlParams()
  if (type === ZHUN_KE_BAO) return ZHUN_KE_BAO
  else if (type === B_PLUS) return B_PLUS
  else if (type === CPS) return CPS
  else return OPPTY
}

// 转委托预校验
inputCustomer = async (d, conversionPageScheme) => {
  if (d.opptySource == 'OPPTY_POOL' || d.opptySource == 'SSC') {
    KeFetch(api.delegatePrecheck, { data: { opptyId: d.opptyId } })
      .then((data) => {
        if (data.resultCode == 1) {
          // 继续转委托流程
        } else {
          Modal.alert('提示', data.tip || '')
        }
      })
  }
}

收获:通过统一入口和预校验封装,将复杂的业务逻辑集中管理,提升了代码的可维护性和可扩展性。


难点二:时间规则与风控、退款规则耦合

问题描述

  • 申请退名额需要满足多个条件:
    • 商机下发时间需满 25 小时
    • 风控拦截:14 天内提交不通过工单超过 3 次
    • 后端预校验:refundPrecheck 接口返回是否允许退款
  • 时间计算和展示需要精确到秒

解决方案

  1. 时间库统一:使用 dayjs 统一处理所有时间计算和格式化
  2. 状态分离:将不同场景拆解成独立的 Modal 状态(antiCheatModalrefundModal
  3. 链式调用:使用 dayjs(opptyTime).add(25, 'hour') 等链式 API 提升可读性
  4. 用户提示:在弹窗中明确展示截止时间,提升用户体验

代码示例

case '申请退名额':
  const { opptyStatus, opptyTime } = item
  const deadline = dayjs(opptyTime).add(25, 'hour')
  
  if (opptyStatus === 4) {
    // 风控拦截
    this.setState({ antiCheatModal: true })
  } else if (!dayjs().isAfter(deadline)) {
    // 时间未到
    this.setState({
      refundModal: true,
      deadline: deadline.format('YYYY-MM-DD HH:mm:ss'),
    })
  } else {
    // 后端预校验
    KeFetch(api.refundPrecheck, { data: { opptyId } }).then((res) => {
      const { refund, remark } = res
      if (remark) {
        Modal.alert('提示', remark, [/* ... */])
      } else if (!refund) {
        this.onNext(item) // 跳转司南
      }
    })
  }
  break

收获:通过时间库统一和状态分离,将复杂的业务规则清晰地表达出来,便于后续根据运营策略调整。


难点三:列表内局部数据更新与用户滚动位置保持

问题描述

  • 用户在列表中点击「记录跟进」后,需要更新对应商机的 opptyBrief 数据
  • 更新后页面不能跳回顶部,需要保持用户当前的滚动位置
  • 需要支持新增和更新两种场景

解决方案

  1. 滚动位置缓存:在打开弹窗前记录当前滚动位置 this.ScroolTop
  2. 回调函数传递:通过 bottomAreaClickcallBack 参数传递更新函数
  3. 局部状态更新:在子组件中通过 useState 维护列表状态,通过 addTempRecordData 方法更新单条数据
  4. 恢复滚动位置:接口成功后调用 document.documentElement.scrollTo(0, this.ScroolTop)

代码示例

// 父组件:记录滚动位置
case '记录跟进':
  this.genjiCallBack = callBack
  this.ScroolTop = document.body.scrollTop || document.documentElement.scrollTop
  this.genji(opptyId, opptyRemarkVo || {})
  break

// 父组件:恢复滚动位置
record = (data) => {
  this.setState({ textareaModal: false })
  KeFetch(api.saveRecord, { method: 'post', data }).then(() => {
    document.documentElement.scrollTo(0, this.ScroolTop)
    this.genjiCallBack && this.genjiCallBack(data)
  })
}

// 子组件:局部数据更新
const addTempRecordData = recordData => {
  const newData = opptyListState.map(item => {
    if (item.opptyId === recordData.opptyId) {
      if (Array.isArray(item.opptyBrief)) {
        const result = item.opptyBrief.filter(item2 => item2.name === '跟进反馈')
        if (result.length <= 0) {
          item.opptyBrief.push({
            name: '跟进反馈',
            desc: `${recordData.opptyFlagName},${recordData.remark}`,
          })
        } else {
          item.opptyBrief.map(item2 => {
            if (item2.name === '跟进反馈') {
              item2.desc = `${recordData.opptyFlagName},${recordData.remark}`
            }
            return item2
          })
        }
      }
    }
    return item
  })
  setOpptyListState(newData)
}

收获:通过滚动位置缓存和回调函数机制,实现了局部数据更新和用户体验的平衡,避免了全量刷新带来的性能问题。


难点四:老项目技术栈混用带来的兼容问题

问题描述

  • 项目中同时使用 antd-mobile v2 和 v5 两个版本
  • 类组件和函数组件混用(QianKe 是类组件,waitDeal 是函数组件)
  • 需要兼容不同版本的 API 和组件特性

解决方案

  1. 渐进式升级:新功能使用新版本(如 InfiniteScroll 使用 v5),老功能保持原样
  2. 统一接口设计:通过 props 和回调函数统一组件间的通信接口
  3. 工具函数封装:将通用逻辑抽离成工具函数,避免重复代码
  4. 文档记录:在代码注释中标注版本差异和注意事项

代码示例

// 使用 v5 的 InfiniteScroll
import { InfiniteScroll } from 'antd-mobile-v5'

// 使用 v2 的 Modal、Toast
import { Modal, Toast } from 'antd-mobile'

// 统一回调接口
<QkbOrder
  data={item}
  onClick={this.itemClick}
  onSpeedClick={this.executeSpeed}
  bottomAreaClick={this.bottomAreaClick}
  purposeValiageClick={this.purposeValiageClick}
/>

收获:通过渐进式升级和统一接口设计,在保证项目稳定性的同时,逐步引入新技术,为后续整体重构打下基础。


📁 项目结构

fe-banner-pub-mobile/
├── client/                    # 前端代码
│   ├── src/
│   │   ├── components/        # 公共组件
│   │   │   └── QkbOrder/      # 潜客包订单组件
│   │   │       └── component/
│   │   │           └── waitDeal.js  # 待处理商机列表
│   │   ├── containers/        # 页面容器
│   │   │   └── ShangJi/       # 商机模块
│   │   │       ├── Package/   # 我的商品
│   │   │       │   └── Qianke/  # 潜客包页面
│   │   │       ├── Home/      # 首页
│   │   │       ├── Data/      # 数据看板
│   │   │       └── Order/     # 订单管理
│   │   ├── config/            # 配置文件
│   │   │   ├── apiConfig.js   # API 配置
│   │   │   └── digConfig.js   # 埋点配置
│   │   ├── utils/             # 工具函数
│   │   │   ├── keFetch.js     # 请求封装
│   │   │   └── storage.js     # 本地存储
│   │   ├── router/            # 路由配置
│   │   ├── store/             # Redux store
│   │   └── App.js             # 根组件
│   ├── webpack/               # Webpack 配置
│   └── package.json
├── server/                     # Node.js 中间层
│   ├── src/
│   │   ├── apis/              # API 接口
│   │   ├── actions/           # 业务逻辑
│   │   └── configs/           # 配置文件
│   └── package.json
└── README.md

🔧 核心功能实现

1. 潜客包列表加载

getPackages(Kdata) {
  const pageNum = (this.state.packages.pageNum || 0) + 1
  const scene = this.getOpptyType()
  
  KeFetch(api.getPackages, {
    data: {
      cityCode: workCity(),
      pageSize: 10,
      pageNum,
      scene,
    },
  }).then((data) => {
    // 根据权限动态插入数据概览
    if (data.statisticsShow && Kdata && scene === OPPTY) {
      const hasDataOverviewPermission = (window._GLOBAL_DATA.userInfo.perms || [])
        .includes('BRAND_M_qianke_dataOverview')
      if (!this.hasInsert && hasDataOverviewPermission) {
        data.list.unshift({ ...Kdata, type: 'qkDataOverview' })
        this.hasInsert = true
      }
    }
    
    data.list = [...(this.state.packages.list || []), ...data.list]
    this.setState({
      packages: data,
      loading: false,
    })
  })
}

2. 虚拟号码拨打

callTel = (opptyId) => {
  this.sendDig(10016, opptyId)
  KeFetch(api.getShangjiVirtualPhone, { data: { opptyId } })
    .then((data) => {
      if (data.virtualPhone) {
        Modal.alert(
          `客户电话${data.virtualPhone}`,
          '此号码为虚拟号码,非客户真实号码',
          [
            { text: '取消', onPress: () => {} },
            { text: '立即拨打', onPress: () => Utils.callTelphone(data.virtualPhone) },
          ]
        )
      }
    })
}

3. 转委托流程

inputCustomer = async (d, conversionPageScheme) => {
  // 预校验
  if (d.opptySource == 'OPPTY_POOL' || d.opptySource == 'SSC') {
    const precheckData = await KeFetch(api.delegatePrecheck, {
      data: { opptyId: d.opptyId },
    })
    if (precheckData.resultCode !== 1) {
      Modal.alert('提示', precheckData.tip || '')
      return
    }
  }
  
  // 执行转委托
  const res = await KeFetch(api.delegateQiankeOpportunity, {
    data: { opptyId: d.opptyId },
  })
  
  if (res.result) {
    Modal.alert('提示', res.resultDesc || '', [
      {
        text: '去编辑委托',
        onPress: () => {
          Utils.inputCustomer(() => {
            Modal.alert('请先将客户端升级至最新版本', '', [
              { text: '取消' },
              { text: '立即升级', onPress: () => Utils.openAbout() },
            ])
          }, res.conversionPageScheme)
        },
      },
    ])
  }
}

📊 性能优化

  1. 代码分割:使用 Webpack 的 code splitting 按路由分割代码
  2. 图片优化:使用 url-loaderfile-loader 处理图片资源
  3. CSS 优化:使用 mini-css-extract-plugin 提取 CSS,使用 px2rem 适配移动端
  4. 无限滚动:使用 InfiniteScroll 实现分页加载,避免一次性加载大量数据
  5. 局部更新:通过回调函数更新局部数据,避免全量刷新

🐛 常见问题与解决方案

1. 鸿蒙系统 Picker 组件滑动穿透

问题:antd-mobile v2 的 Picker 组件在鸿蒙系统滑动选择时会有滑动穿透 bug。

解决方案:使用 components/WithTouchMoveControlHoc 高阶组件包裹 Picker 组件。

2. Node 版本兼容

问题:client 端使用 Node 14,server 端使用 Node 12。

解决方案:使用 nvm 进行 Node 版本切换,或在启动脚本中自动切换。

3. VPN 冲突

问题:Node 层加了 Redis 之后,挂了外网 VPN 项目启动不起来。

解决方案:关闭 VPN 后再启动项目。


🚀 部署流程

  1. 开发环境
    npm install
    npm start  # 自动切换 Node 版本并启动前后端
    

📝 总结

技术收获

  1. 复杂业务逻辑处理:通过统一入口函数和预校验封装,将复杂的业务规则清晰地表达出来
  2. 用户体验优化:通过滚动位置保持、局部数据更新等技术手段,提升用户操作体验
  3. 多端适配:通过 JSBridge 和工具函数封装,实现 APP 和 H5 的统一适配
  4. 性能优化:通过无限滚动、代码分割等技术,提升页面加载速度和运行性能

业务理解

  1. 商机管理全流程:深入理解了从商机产生到关闭的完整业务流程
  2. 风控规则:理解了时间规则、风控拦截等业务规则的设计思路
  3. 数据埋点:理解了埋点数据对运营分析和产品优化的重要性

项目价值

  1. 提升效率:通过完整的商机处理闭环,大幅提升经纪人的工作效率
  2. 规范操作:通过统一的交互入口和校验逻辑,规范了业务操作流程
  3. 数据支撑:通过完善的埋点体系,为后续的运营分析和策略优化提供了数据支撑

📚 相关文档


最后更新:2024年

❌